Skip to content

Commit 3242095

Browse files
committed
added support to execute automated test using playwright
1 parent e793948 commit 3242095

7 files changed

Lines changed: 483 additions & 2 deletions

File tree

.github/workflows/build-lint-test.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,51 @@ jobs:
159159
run: npx puppeteer browsers install chrome
160160
- name: A11y tests
161161
run: npm run serve:docs & npm run test:a11y
162+
test_playwright:
163+
runs-on: ubuntu-latest
164+
env:
165+
GH_PR_NUM: ${{ github.event.number }}
166+
needs: build
167+
steps:
168+
- uses: actions/checkout@v4
169+
- run: |
170+
if [[ ! -z "${GH_PR_NUM}" ]]; then
171+
echo "Checking out PR"
172+
git fetch origin pull/$GH_PR_NUM/head:tmp
173+
git checkout tmp
174+
fi
175+
- uses: actions/setup-node@v4
176+
with:
177+
node-version: '20'
178+
- uses: actions/cache@v4
179+
id: npm-cache
180+
name: Cache npm deps
181+
with:
182+
path: |
183+
node_modules
184+
**/node_modules
185+
key: ${{ runner.os }}-npm-14-${{ secrets.CACHE_VERSION }}-${{ hashFiles('package-lock.json') }}
186+
- run: npm install --frozen-lockfile
187+
if: steps.npm-cache.outputs.cache-hit != 'true'
188+
- uses: actions/cache@v4
189+
id: dist
190+
name: Cache dist
191+
with:
192+
path: |
193+
packages/*/dist
194+
key: ${{ runner.os }}-dist-14-${{ secrets.CACHE_VERSION }}-${{ hashFiles('package-lock.json', 'package.json', 'packages/*/*', '!packages/*/dist', '!packages/*/node_modules') }}
195+
- name: Build dist
196+
run: npm run build
197+
if: steps.dist.outputs.cache-hit != 'true'
198+
- name: Build docs
199+
run: npm run build:docs
200+
- name: Install Playwright browsers
201+
run: npx playwright install --with-deps chromium
202+
- name: Run Playwright tests
203+
run: npx playwright test
204+
- uses: actions/upload-artifact@v4
205+
if: ${{ !cancelled() }}
206+
with:
207+
name: playwright-report
208+
path: playwright-report/
209+
retention-days: 14

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,9 @@ lerna-debug.log
3535
# For vim
3636
*.swp
3737

38-
public
38+
public
39+
40+
# Playwright
41+
playwright-report/
42+
test-results/
43+
blob-report/

e2e/basic-chatbot.spec.ts

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
import { test, expect, type Page } from '@playwright/test';
2+
3+
const DEMO_URL = '/extensions/chatbot/overview/demo/basic-chatbot';
4+
5+
test.beforeEach(async ({ page }) => {
6+
await page.goto(DEMO_URL);
7+
await expect(page.getByRole('region', { name: 'Chatbot' })).toBeVisible();
8+
});
9+
10+
test.describe('Basic Chatbot - Page Load', () => {
11+
test('chatbot is visible on page load', async ({ page }) => {
12+
const chatbot = page.getByRole('region', { name: 'Chatbot' });
13+
await expect(chatbot).toBeVisible();
14+
});
15+
16+
test('displays welcome heading', async ({ page }) => {
17+
await expect(
18+
page.getByRole('heading', { name: /Hi, ChatBot User/i })
19+
).toBeVisible();
20+
});
21+
22+
test('displays footnote disclaimer', async ({ page }) => {
23+
await expect(
24+
page.getByText('Always review AI-generated content prior to use.')
25+
).toBeVisible();
26+
});
27+
28+
test('toggle button shows expanded state', async ({ page }) => {
29+
const toggle = page.getByRole('button', { name: 'Chatbot toggle' });
30+
await expect(toggle).toBeVisible();
31+
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
32+
});
33+
});
34+
35+
test.describe('Basic Chatbot - Welcome Prompts', () => {
36+
test('displays "Set up account" prompt card', async ({ page }) => {
37+
await expect(page.getByRole('button', { name: 'Set up account' })).toBeVisible();
38+
await expect(
39+
page.getByText('Choose the necessary settings and preferences for your account.')
40+
).toBeVisible();
41+
});
42+
43+
test('displays "Troubleshoot issue" prompt card', async ({ page }) => {
44+
await expect(page.getByRole('button', { name: 'Troubleshoot issue' })).toBeVisible();
45+
await expect(
46+
page.getByText('Find documentation and instructions to resolve your issue.')
47+
).toBeVisible();
48+
});
49+
});
50+
51+
test.describe('Basic Chatbot - Pre-loaded Messages', () => {
52+
test('displays initial user message', async ({ page }) => {
53+
const userMessage = page.getByRole('region', { name: /Message from user/i });
54+
await expect(userMessage).toBeVisible();
55+
await expect(userMessage).toContainText(
56+
'Hello, can you give me an example of what you can do?'
57+
);
58+
});
59+
60+
test('displays initial bot response with rich content', async ({ page }) => {
61+
const botMessage = page.getByRole('region', { name: /Message from bot/i });
62+
await expect(botMessage).toBeVisible();
63+
await expect(botMessage).toContainText('strong importance');
64+
});
65+
66+
test('bot response includes code blocks', async ({ page }) => {
67+
const botMessage = page.getByRole('region', { name: /Message from bot/i });
68+
await expect(botMessage.getByText('yaml', { exact: true })).toBeVisible();
69+
await expect(botMessage.getByText('js', { exact: true })).toBeVisible();
70+
});
71+
72+
test('bot response has copy code buttons', async ({ page }) => {
73+
const copyButtons = page.getByRole('button', { name: 'Copy code' });
74+
await expect(copyButtons).toHaveCount(2);
75+
});
76+
});
77+
78+
test.describe('Basic Chatbot - Response Actions', () => {
79+
test('bot message displays all action buttons', async ({ page }) => {
80+
await expect(page.getByRole('button', { name: 'Good response' })).toBeVisible();
81+
await expect(page.getByRole('button', { name: 'Bad response' })).toBeVisible();
82+
await expect(page.getByRole('button', { name: 'Copy', exact: true })).toBeVisible();
83+
await expect(page.getByRole('button', { name: 'Download' })).toBeVisible();
84+
await expect(page.getByRole('button', { name: 'Listen' })).toBeVisible();
85+
});
86+
87+
test('can click "Good response" action', async ({ page }) => {
88+
const goodBtn = page.getByRole('button', { name: 'Good response' });
89+
await goodBtn.click();
90+
// Button should remain visible after click (toggled state)
91+
await expect(goodBtn).toBeVisible();
92+
});
93+
94+
test('can click "Bad response" action', async ({ page }) => {
95+
const badBtn = page.getByRole('button', { name: 'Bad response' });
96+
await badBtn.click();
97+
await expect(badBtn).toBeVisible();
98+
});
99+
});
100+
101+
test.describe('Basic Chatbot - Send Message', () => {
102+
test('message bar is visible with placeholder', async ({ page }) => {
103+
const textbox = page.getByRole('textbox', { name: 'Send a message...' });
104+
await expect(textbox).toBeVisible();
105+
await expect(textbox).toHaveAttribute('placeholder', 'Send a message...');
106+
});
107+
108+
test('send button appears when text is entered', async ({ page }) => {
109+
const textbox = page.getByRole('textbox', { name: 'Send a message...' });
110+
await textbox.fill('Test message');
111+
await expect(page.getByRole('button', { name: 'Send' })).toBeVisible();
112+
});
113+
114+
test('send button disappears when text is cleared', async ({ page }) => {
115+
const textbox = page.getByRole('textbox', { name: 'Send a message...' });
116+
await textbox.fill('Test message');
117+
await expect(page.getByRole('button', { name: 'Send' })).toBeVisible();
118+
await textbox.fill('');
119+
await expect(page.getByRole('button', { name: 'Send' })).not.toBeVisible();
120+
});
121+
122+
test('can send a message by clicking Send button', async ({ page }) => {
123+
const textbox = page.getByRole('textbox', { name: 'Send a message...' });
124+
await textbox.fill('What is PatternFly?');
125+
await page.getByRole('button', { name: 'Send' }).click();
126+
127+
// New user message should appear
128+
const messages = page.getByRole('region', { name: /Message from user/i });
129+
await expect(messages).toHaveCount(2);
130+
await expect(messages.last()).toContainText('What is PatternFly?');
131+
132+
// Textbox should be cleared after sending
133+
await expect(textbox).toHaveValue('');
134+
});
135+
136+
test('can send a message by pressing Enter', async ({ page }) => {
137+
const textbox = page.getByRole('textbox', { name: 'Send a message...' });
138+
await textbox.fill('Hello from keyboard');
139+
await textbox.press('Enter');
140+
141+
const messages = page.getByRole('region', { name: /Message from user/i });
142+
await expect(messages).toHaveCount(2);
143+
await expect(messages.last()).toContainText('Hello from keyboard');
144+
});
145+
146+
test('Shift+Enter adds a newline instead of sending', async ({ page }) => {
147+
const textbox = page.getByRole('textbox', { name: 'Send a message...' });
148+
await textbox.fill('Line one');
149+
await textbox.press('Shift+Enter');
150+
await textbox.pressSequentially('Line two');
151+
152+
// Message should not have been sent (still only 1 user message region)
153+
const messages = page.getByRole('region', { name: /Message from user/i });
154+
await expect(messages).toHaveCount(1);
155+
});
156+
157+
test('bot responds after sending a message (simulated API delay)', async ({ page }) => {
158+
const textbox = page.getByRole('textbox', { name: 'Send a message...' });
159+
await textbox.fill('Tell me something');
160+
await page.getByRole('button', { name: 'Send' }).click();
161+
162+
// Wait for bot response to finish loading (~5s simulated delay)
163+
await expect(
164+
page.getByRole('button', { name: 'Good response' }).last()
165+
).toBeVisible({ timeout: 10_000 });
166+
167+
// Should now have 2 bot messages (initial + new response)
168+
const botMessages = page.getByRole('region', { name: /Message from bot/i });
169+
await expect(botMessages).toHaveCount(2);
170+
});
171+
172+
test('attach button is visible in message bar', async ({ page }) => {
173+
await expect(page.getByRole('button', { name: 'Attach' })).toBeVisible();
174+
});
175+
});
176+
177+
test.describe('Basic Chatbot - Toggle Visibility', () => {
178+
test('can hide chatbot by clicking toggle', async ({ page }) => {
179+
const toggle = page.getByRole('button', { name: 'Chatbot toggle' });
180+
await toggle.click();
181+
182+
await expect(toggle).toHaveAttribute('aria-expanded', 'false');
183+
await expect(page.getByRole('region', { name: 'Chatbot' })).not.toBeVisible();
184+
});
185+
186+
test('can show chatbot again after hiding', async ({ page }) => {
187+
const toggle = page.getByRole('button', { name: 'Chatbot toggle' });
188+
189+
await toggle.click();
190+
await expect(page.getByRole('region', { name: 'Chatbot' })).not.toBeVisible();
191+
192+
await toggle.click();
193+
await expect(page.getByRole('region', { name: 'Chatbot' })).toBeVisible();
194+
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
195+
});
196+
});
197+
198+
test.describe('Basic Chatbot - Header Controls', () => {
199+
test('model selector shows "Granite 7B" by default', async ({ page }) => {
200+
const modelSelector = page.getByRole('button', { name: 'Select model' });
201+
await expect(modelSelector).toBeVisible();
202+
await expect(modelSelector).toContainText('Granite 7B');
203+
});
204+
205+
test('can open model selector dropdown and see options', async ({ page }) => {
206+
await page.getByRole('button', { name: 'Select model' }).click();
207+
await expect(page.getByText('Llama 3.0')).toBeVisible();
208+
await expect(page.getByText('Mistral 3B')).toBeVisible();
209+
});
210+
211+
test('can switch to a different model', async ({ page }) => {
212+
await page.getByRole('button', { name: 'Select model' }).click();
213+
await page.getByText('Llama 3.0').click();
214+
215+
const modelSelector = page.getByRole('button', { name: /Select model/i });
216+
await expect(modelSelector).toContainText('Llama 3.0');
217+
});
218+
219+
test('options dropdown opens with display mode choices', async ({ page }) => {
220+
await page.getByRole('button', { name: 'Chatbot options' }).click();
221+
222+
await expect(page.getByText('Overlay')).toBeVisible();
223+
await expect(page.getByText('Dock to window')).toBeVisible();
224+
await expect(page.getByText('Fullscreen')).toBeVisible();
225+
});
226+
227+
test('can switch display mode to fullscreen', async ({ page }) => {
228+
await page.getByRole('button', { name: 'Chatbot options' }).click();
229+
await page.getByText('Fullscreen').click();
230+
231+
const chatbot = page.locator('.pf-chatbot--fullscreen');
232+
await expect(chatbot).toBeVisible();
233+
});
234+
235+
test('can switch display mode to docked', async ({ page }) => {
236+
await page.getByRole('button', { name: 'Chatbot options' }).click();
237+
await page.getByText('Dock to window').click();
238+
239+
const chatbot = page.locator('.pf-chatbot--docked');
240+
await expect(chatbot).toBeVisible();
241+
});
242+
});
243+
244+
test.describe('Basic Chatbot - Chat History Drawer', () => {
245+
test('can open chat history drawer', async ({ page }) => {
246+
await page.getByRole('button', { name: 'Chat history drawer' }).click();
247+
const drawer = page.getByRole('dialog');
248+
await expect(drawer).toBeVisible();
249+
await expect(drawer.getByRole('heading', { name: 'Chat history' })).toBeVisible();
250+
await expect(
251+
drawer.getByRole('textbox', { name: 'Search previous conversations' })
252+
).toBeVisible();
253+
});
254+
255+
test('"New chat" button is visible in history drawer', async ({ page }) => {
256+
await page.getByRole('button', { name: 'Chat history drawer' }).click();
257+
await expect(page.getByRole('dialog').getByRole('button', { name: 'New chat' })).toBeVisible();
258+
});
259+
260+
test('displays conversation history grouped by date', async ({ page }) => {
261+
await page.getByRole('button', { name: 'Chat history drawer' }).click();
262+
const drawer = page.getByRole('dialog');
263+
await expect(drawer.getByRole('heading', { name: 'Today' })).toBeVisible();
264+
await expect(drawer.getByRole('heading', { name: 'This month' })).toBeVisible();
265+
});
266+
267+
test('can search in chat history', async ({ page }) => {
268+
await page.getByRole('button', { name: 'Chat history drawer' }).click();
269+
const drawer = page.getByRole('dialog');
270+
const searchBox = drawer.getByRole('textbox', {
271+
name: 'Search previous conversations',
272+
});
273+
await searchBox.fill('nonexistent query');
274+
await expect(drawer.getByText('No results found')).toBeVisible();
275+
});
276+
277+
test('can close drawer with close button', async ({ page }) => {
278+
await page.getByRole('button', { name: 'Chat history drawer' }).click();
279+
const drawer = page.getByRole('dialog');
280+
await expect(drawer).toBeVisible();
281+
282+
await drawer.getByRole('button', { name: 'Close drawer panel' }).click();
283+
await expect(drawer).not.toBeVisible();
284+
});
285+
286+
test('"New chat" clears messages and closes drawer', async ({ page }) => {
287+
await page.getByRole('button', { name: 'Chat history drawer' }).click();
288+
await page.getByRole('dialog').getByRole('button', { name: 'New chat' }).click();
289+
290+
// Drawer should close
291+
await expect(page.getByRole('dialog')).not.toBeVisible();
292+
293+
// Messages should be cleared — only welcome prompt visible
294+
await expect(
295+
page.getByRole('region', { name: /Message from user/i })
296+
).toHaveCount(0);
297+
});
298+
});
299+
300+
test.describe('Basic Chatbot - Accessibility', () => {
301+
test('skip-to-chatbot link is present', async ({ page }) => {
302+
await expect(page.getByRole('link', { name: 'Skip to chatbot' })).toBeAttached();
303+
});
304+
305+
test('message log region has correct aria label', async ({ page }) => {
306+
await expect(
307+
page.getByRole('region', { name: 'Scrollable message log' })
308+
).toBeVisible();
309+
});
310+
311+
test('chatbot container is a landmark region', async ({ page }) => {
312+
const chatbot = page.getByRole('region', { name: 'Chatbot' });
313+
await expect(chatbot).toBeVisible();
314+
});
315+
316+
test('messages have descriptive aria labels with timestamps', async ({ page }) => {
317+
const userMessage = page.getByRole('region', { name: /Message from user - /i });
318+
await expect(userMessage).toBeVisible();
319+
320+
const botMessage = page.getByRole('region', { name: /Message from bot - /i });
321+
await expect(botMessage).toBeVisible();
322+
});
323+
});

0 commit comments

Comments
 (0)