|
| 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