From cb4ff98bfc64d4e2d7b4e7fbec01c33e43f3b7fc Mon Sep 17 00:00:00 2001 From: Jagger <634750802@qq.com> Date: Fri, 13 Sep 2024 10:28:54 +0800 Subject: [PATCH] e2e: add widget chat tests (#279) --- e2e/test-html/widget-controlled.html | 11 ++++ e2e/test-html/widget.html | 2 +- e2e/tests/chat.spec.ts | 41 ++---------- e2e/tests/widget.spec.ts | 66 +++++++++++++++++-- e2e/utils/chat.ts | 36 ++++++++++ .../api/[[...fallback_placeholder]]/route.ts | 2 +- 6 files changed, 115 insertions(+), 43 deletions(-) create mode 100644 e2e/test-html/widget-controlled.html create mode 100644 e2e/utils/chat.ts diff --git a/e2e/test-html/widget-controlled.html b/e2e/test-html/widget-controlled.html new file mode 100644 index 00000000..1c90f28a --- /dev/null +++ b/e2e/test-html/widget-controlled.html @@ -0,0 +1,11 @@ + + + + + + Document + + + + + \ No newline at end of file diff --git a/e2e/test-html/widget.html b/e2e/test-html/widget.html index f266889d..6b3b65b3 100644 --- a/e2e/test-html/widget.html +++ b/e2e/test-html/widget.html @@ -6,6 +6,6 @@ Document - + \ No newline at end of file diff --git a/e2e/tests/chat.spec.ts b/e2e/tests/chat.spec.ts index 76bbf2e8..f20312cc 100644 --- a/e2e/tests/chat.spec.ts +++ b/e2e/tests/chat.spec.ts @@ -1,6 +1,5 @@ -import { expect, type Page, type Request, test } from '@playwright/test'; - -const QUESTION = 'What is the content of sample.pdf?'; +import { expect, test } from '@playwright/test'; +import { getChatRequestPromise, QUESTION, testNewChat } from '../utils/chat'; test.describe('Chat', () => { test('From Home Page', async ({ page, baseURL }) => { @@ -12,7 +11,7 @@ test.describe('Chat', () => { await page.getByPlaceholder('Input your question here...').fill(QUESTION); // https://playwright.dev/docs/events#waiting-for-event - const chatRequestPromise = page.waitForRequest(request => request.url() === `${baseURL}/api/v1/chats` && request.method() === 'POST'); + const chatRequestPromise = getChatRequestPromise(page, baseURL); const trigger = page.locator('button', { has: page.locator('svg.lucide-arrow-right') }); await trigger.click(); @@ -20,7 +19,7 @@ test.describe('Chat', () => { return await chatRequestPromise; }); - await testNewChat(page, chatRequest); + await testNewChat(page, chatRequest, true); }); test('From Keyboard Shortcut', async ({ page, baseURL }) => { @@ -33,39 +32,11 @@ test.describe('Chat', () => { await page.keyboard.insertText(QUESTION); // https://playwright.dev/docs/events#waiting-for-event - const chatRequestPromise = page.waitForRequest(request => request.url() === `${baseURL}/api/v1/chats` && request.method() === 'POST'); + const chatRequestPromise = getChatRequestPromise(page, baseURL); await page.keyboard.press('ControlOrMeta+Enter'); return await chatRequestPromise; }); - await testNewChat(page, chatRequest); + await testNewChat(page, chatRequest, true); }); }); - -async function testNewChat (page: Page, chatRequest: Request) { - await test.step('Wait page changes', async () => { - await page.waitForURL(/\/c\/.+/); - - expect(await page.title()).toContain(QUESTION); - await page.getByRole('heading', { name: QUESTION }).waitFor({ state: 'visible' }); - }); - - const streamText = await test.step('Wait for chat stop', async () => { - const chatResponse = await chatRequest.response(); - expect(chatResponse.ok()).toBe(true); - - // Feedback button indicates chat ends. - await page.locator('button', { has: page.locator('svg.lucide-message-square-plus') }).waitFor({ state: 'visible' }); - - return await chatResponse.text(); - }); - - await test.step('Check response text', async () => { - const lastLine = streamText.split('\n').filter(t => !!t.trim()).slice(-1)[0]; - expect(lastLine).toMatch(/^2:/); - const message = JSON.parse(lastLine.slice(2))[0].assistant_message; - - expect(message.finished_at).toBeTruthy(); - expect(message.content.trim().length).toBeGreaterThan(0); - }); -} diff --git a/e2e/tests/widget.spec.ts b/e2e/tests/widget.spec.ts index 4337c8db..6b08c96d 100644 --- a/e2e/tests/widget.spec.ts +++ b/e2e/tests/widget.spec.ts @@ -1,13 +1,67 @@ -import { expect, test } from '@playwright/test'; +import { expect, type Locator, type Page, test } from '@playwright/test'; +import { getChatRequestPromise, QUESTION, testNewChat } from '../utils/chat'; test('JS Widget', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: 'Ask AI' }).waitFor({ state: 'visible' }); - expect(await page.evaluate('window.tidbai')).toMatchObject({ open: false }); + expect(await page.evaluate('tidbai')).toMatchObject({ open: false }); }); -test('JS Widget from other page', async ({ page }) => { - await page.goto('http://localhost:4001/widget.html'); - await page.getByRole('button', { name: 'Ask AI' }).waitFor({ state: 'visible' }); - expect(await page.evaluate('window.tidbai')).toMatchObject({ open: false }); +test('Embedded JS Widget with trigger button', async ({ page }) => { + const trigger = await test.step('Wait trigger visible and tidbai object ready', async () => { + await page.goto('http://localhost:4001/widget.html'); + const trigger = page.getByRole('button', { name: 'Ask AI' }); + await trigger.waitFor({ state: 'visible' }); + expect(await page.evaluate('tidbai')).toMatchObject({ open: false }); + return trigger; + }); + + const dialog = await test.step('Click and show dialog', async () => { + await trigger.click(); + + const dialog = page.getByRole('dialog', { name: 'Ask AI' }); + await dialog.waitFor({ state: 'visible' }); + + return dialog; + }); + + await testWidgetChat(page, dialog); }); + +// Used by docs.pingcap.com +test('Embedded JS Widget controlled by js', async ({ page }) => { + await test.step('Wait trigger visible and tidbai object ready', async () => { + await page.goto('http://localhost:4001/widget-controlled.html'); + const trigger = page.getByRole('button', { name: 'Ask AI' }); + await trigger.waitFor({ state: 'detached' }); + expect(await page.evaluate('window.tidbai')).toMatchObject({ open: false }); + }); + + const dialog = await test.step('JS api call and show dialog', async () => { + await page.evaluate('tidbai.open = true'); + + const dialog = page.getByRole('dialog', { name: 'Ask AI' }); + await dialog.waitFor({ state: 'visible' }); + + return dialog; + }); + + await testWidgetChat(page, dialog); +}); + +async function testWidgetChat (page: Page, dialog: Locator) { + await test.step('Fill in question', async () => { + const input = dialog.getByPlaceholder('Input your question here...'); + await input.focus(); + await input.fill(QUESTION); + }); + + const chatRequestPromise = await test.step('Trigger ask by press ControlOrMeta+Enter', async () => { + const chatRequestPromise = getChatRequestPromise(page, 'http://127.0.0.1:3000'); + await page.keyboard.press('ControlOrMeta+Enter'); + + return chatRequestPromise; + }); + + await testNewChat(page, chatRequestPromise, false); +} \ No newline at end of file diff --git a/e2e/utils/chat.ts b/e2e/utils/chat.ts new file mode 100644 index 00000000..291df694 --- /dev/null +++ b/e2e/utils/chat.ts @@ -0,0 +1,36 @@ +import { expect, type Page, type Request, test } from '@playwright/test'; + +export const QUESTION = 'What is the content of sample.pdf?'; + +export function getChatRequestPromise (page: Page, baseURL: string) { + return page.waitForRequest(request => request.url() === `${baseURL}/api/v1/chats` && request.method() === 'POST'); +} + +export async function testNewChat (page: Page, chatRequest: Request, validatePageUrlAndTitle: boolean) { + await test.step('Wait page changes', async () => { + if (validatePageUrlAndTitle) { + await page.waitForURL(/\/c\/.+/); + expect(await page.title()).toContain(QUESTION); + } + await page.getByRole('heading', { name: QUESTION }).waitFor({ state: 'visible' }); + }); + + const streamText = await test.step('Wait for chat stop', async () => { + const chatResponse = await chatRequest.response(); + expect(chatResponse.ok()).toBe(true); + + // Feedback button indicates chat ends. + await page.locator('button', { has: page.locator('svg.lucide-message-square-plus') }).waitFor({ state: 'visible' }); + + return await chatResponse.text(); + }); + + await test.step('Check response text', async () => { + const lastLine = streamText.split('\n').filter(t => !!t.trim()).slice(-1)[0]; + expect(lastLine).toMatch(/^2:/); + const message = JSON.parse(lastLine.slice(2))[0].assistant_message; + + expect(message.finished_at).toBeTruthy(); + expect(message.content.trim().length).toBeGreaterThan(0); + }); +} diff --git a/frontend/app/src/app/api/[[...fallback_placeholder]]/route.ts b/frontend/app/src/app/api/[[...fallback_placeholder]]/route.ts index 9384b1ed..de5ca82a 100644 --- a/frontend/app/src/app/api/[[...fallback_placeholder]]/route.ts +++ b/frontend/app/src/app/api/[[...fallback_placeholder]]/route.ts @@ -50,7 +50,7 @@ function originalUrl (request: NextRequest) { } } -export { handler as GET, handler as POST, handler as DELETE, handler as HEAD, handler as PUT, handler as PATCH }; +export { handler as GET, handler as POST, handler as DELETE, handler as HEAD, handler as PUT, handler as PATCH, handler as OPTIONS }; export const runtime = 'edge';