Sometimes, missing `await` in the test code can cause timing issues, leading to test failures. This PR enables the `no-floating-promises` rule for the test code to ensure that such errors do not occur.
474 lines
12 KiB
TypeScript
474 lines
12 KiB
TypeScript
import { copyByKeyboard } from '@affine-test/kit/utils/keyboard';
|
|
import { expect } from '@playwright/test';
|
|
|
|
import { test } from '../base/base-test';
|
|
|
|
test.describe('AIBasic/Chat', () => {
|
|
test.beforeEach(async ({ utils, loggedInPage: page }) => {
|
|
await utils.testUtils.setupTestEnvironment(page);
|
|
await utils.chatPanel.openChatPanel(page);
|
|
});
|
|
|
|
test('should display empty state when no messages', async ({
|
|
loggedInPage: page,
|
|
}) => {
|
|
// Verify empty state UI
|
|
await expect(page.getByTestId('chat-panel-empty-state')).toBeVisible();
|
|
await expect(page.getByTestId('ai-onboarding')).toBeVisible();
|
|
});
|
|
|
|
test(`should send message and receive AI response:
|
|
- send message
|
|
- AI is loading
|
|
- AI generating
|
|
- AI success
|
|
`, async ({ loggedInPage: page, utils }) => {
|
|
// Type and send a message
|
|
await utils.chatPanel.makeChat(page, 'Introduce AFFiNE to me');
|
|
|
|
// AI is loading
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'Introduce AFFiNE to me',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'loading',
|
|
},
|
|
]);
|
|
|
|
await expect(page.getByTestId('ai-loading')).toBeVisible();
|
|
|
|
// AI Generating
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'Introduce AFFiNE to me',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'transmitting',
|
|
},
|
|
]);
|
|
|
|
await expect(page.getByTestId('ai-loading')).not.toBeVisible();
|
|
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'Introduce AFFiNE to me',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'success',
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('should support stop generating', async ({
|
|
loggedInPage: page,
|
|
utils,
|
|
}) => {
|
|
await utils.chatPanel.makeChat(page, 'Introduce AFFiNE to me');
|
|
|
|
// AI Generating
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'Introduce AFFiNE to me',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'transmitting',
|
|
},
|
|
]);
|
|
|
|
await page.getByTestId('chat-panel-stop').click();
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'Introduce AFFiNE to me',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'success',
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('should render ai actions inline if the answer is the last one in the list, otherwise, nest them under the "More" menu', async ({
|
|
loggedInPage: page,
|
|
utils,
|
|
}) => {
|
|
await utils.chatPanel.makeChat(page, 'Hello, how can you help me?');
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'Hello, how can you help me?',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'success',
|
|
},
|
|
]);
|
|
|
|
await expect(page.getByTestId('chat-action-list')).toBeVisible();
|
|
await utils.chatPanel.makeChat(page, 'Nice to meet you');
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'Hello, how can you help me?',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'idle',
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: 'Nice to meet you',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'success',
|
|
},
|
|
]);
|
|
|
|
const firstAnswer = await page
|
|
.getByTestId('chat-message-assistant')
|
|
.first();
|
|
const more = firstAnswer.getByTestId('action-more-button');
|
|
await more.click();
|
|
await expect(firstAnswer.getByTestId('chat-actions')).toBeVisible();
|
|
});
|
|
|
|
test('should show scroll indicator when there are many messages', async ({
|
|
loggedInPage: page,
|
|
utils,
|
|
}) => {
|
|
// Set window height to 100px to ensure scroll indicator appears
|
|
await page.setViewportSize({ width: 1280, height: 400 });
|
|
|
|
// Type and send a message
|
|
await utils.chatPanel.makeChat(
|
|
page,
|
|
'Hello, write a poem about the moon with 50 words.'
|
|
);
|
|
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'Hello, write a poem about the moon with 50 words.',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'success',
|
|
},
|
|
]);
|
|
|
|
// Wait for the answer to be completely rendered
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Scroll up to trigger scroll indicator
|
|
const chatMessagesContainer = page.getByTestId(
|
|
'chat-panel-messages-container'
|
|
);
|
|
await chatMessagesContainer.evaluate(el => {
|
|
el.scrollTop = 0;
|
|
});
|
|
|
|
const scrollDownIndicator = page.getByTestId(
|
|
'chat-panel-scroll-down-indicator'
|
|
);
|
|
|
|
// Verify scroll indicator appears
|
|
await expect(scrollDownIndicator).toBeVisible();
|
|
|
|
// Click scroll indicator to scroll to bottom
|
|
await scrollDownIndicator.click();
|
|
|
|
// Verify scroll indicator disappears
|
|
await expect(scrollDownIndicator).not.toBeVisible();
|
|
});
|
|
|
|
test('should show error when request failed', async ({
|
|
loggedInPage: page,
|
|
utils,
|
|
}) => {
|
|
// Simulate network error by disconnecting
|
|
await page.route('**/graphql', route => route.abort('failed'));
|
|
|
|
// Send a message that will fail
|
|
await utils.chatPanel.makeChat(page, 'Hello');
|
|
|
|
await expect(page.getByTestId('ai-error')).toBeVisible();
|
|
await expect(page.getByTestId('action-retry-button')).toBeVisible();
|
|
});
|
|
|
|
test('should support retrying failed messages', async ({
|
|
loggedInPage: page,
|
|
utils,
|
|
}) => {
|
|
// Simulate network error by disconnecting
|
|
await page.route('**/graphql', route => route.abort('failed'));
|
|
|
|
// Send a message that will fail
|
|
await utils.chatPanel.makeChat(page, 'Hello');
|
|
|
|
// Verify error state
|
|
await expect(page.getByTestId('ai-error')).toBeVisible();
|
|
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'Hello',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'error',
|
|
},
|
|
]);
|
|
|
|
// Reconnect network
|
|
await page.route('**/graphql', route => route.continue());
|
|
|
|
await page.getByTestId('action-retry-button').click();
|
|
|
|
// Verify message is resent and AI responds
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'Hello',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'success',
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('should support retrying question', async ({
|
|
loggedInPage: page,
|
|
utils,
|
|
}) => {
|
|
await utils.chatPanel.makeChat(
|
|
page,
|
|
'Introduce Large Language Model in under 500 words'
|
|
);
|
|
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'Introduce Large Language Model in under 500 words',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'success',
|
|
},
|
|
]);
|
|
|
|
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
|
|
await actions.retry();
|
|
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'Introduce Large Language Model in under 500 words',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'transmitting',
|
|
},
|
|
]);
|
|
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'Introduce Large Language Model in under 500 words',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'success',
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('should support sending message with button', async ({
|
|
loggedInPage: page,
|
|
utils,
|
|
}) => {
|
|
await utils.chatPanel.openChatPanel(page);
|
|
await utils.chatPanel.typeChat(page, 'Hello');
|
|
await page.getByTestId('chat-panel-send').click();
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'Hello',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'loading',
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('should support clearing chat', async ({
|
|
loggedInPage: page,
|
|
utils,
|
|
}) => {
|
|
await utils.chatPanel.openChatPanel(page);
|
|
await utils.chatPanel.makeChat(page, 'Hello');
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'Hello',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'success',
|
|
},
|
|
]);
|
|
await utils.chatPanel.clearChat(page);
|
|
await utils.chatPanel.waitForHistory(page, []);
|
|
});
|
|
test('should support copying answer', async ({
|
|
loggedInPage: page,
|
|
utils,
|
|
}) => {
|
|
await utils.chatPanel.openChatPanel(page);
|
|
await utils.chatPanel.makeChat(page, 'Hello');
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'Hello',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'success',
|
|
},
|
|
]);
|
|
|
|
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
|
|
await actions.copy();
|
|
await page.getByText('Copied to clipboard').isVisible();
|
|
await expect(async () => {
|
|
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
|
|
const clipboardText = await page.evaluate(() =>
|
|
navigator.clipboard.readText()
|
|
);
|
|
expect(clipboardText).toBe(content);
|
|
}).toPass({ timeout: 5000 });
|
|
});
|
|
|
|
test('should support copying selected answer content', async ({
|
|
loggedInPage: page,
|
|
utils,
|
|
}) => {
|
|
await utils.chatPanel.openChatPanel(page);
|
|
await utils.chatPanel.makeChat(
|
|
page,
|
|
'Help me write a two-line love poem, return two paragraphs for me.'
|
|
);
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content:
|
|
'Help me write a two-line love poem, return two paragraphs for me.',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'success',
|
|
},
|
|
]);
|
|
|
|
await expect(async () => {
|
|
const { content, message } =
|
|
await utils.chatPanel.getLatestAssistantMessage(page);
|
|
// Select multiple rich text
|
|
const firstParagraph = await message
|
|
.locator('affine-paragraph rich-text v-text')
|
|
.first()
|
|
.boundingBox();
|
|
const lastParagraph = await message
|
|
.locator('affine-paragraph rich-text v-text')
|
|
.last()
|
|
.boundingBox();
|
|
|
|
if (firstParagraph && lastParagraph) {
|
|
await page.mouse.move(firstParagraph.x, firstParagraph.y);
|
|
await page.mouse.down();
|
|
await page.mouse.move(
|
|
lastParagraph.x + lastParagraph.width,
|
|
lastParagraph.y + lastParagraph.height
|
|
);
|
|
await page.mouse.up();
|
|
}
|
|
|
|
await copyByKeyboard(page);
|
|
const clipboardText = await page.evaluate(() =>
|
|
navigator.clipboard.readText()
|
|
);
|
|
expect(clipboardText).toBe(content);
|
|
}).toPass({ timeout: 5000 });
|
|
});
|
|
|
|
test('chat with ask ai input in page mode', async ({
|
|
loggedInPage: page,
|
|
utils,
|
|
}) => {
|
|
await utils.chatPanel.closeChatPanel(page);
|
|
await utils.editor.askAIWithText(
|
|
page,
|
|
'AFFiNE is an open source all in one workspace.'
|
|
);
|
|
await page.keyboard.type('Translate to chinese');
|
|
|
|
const sendButton = await page.getByTestId('ai-panel-input-send');
|
|
await expect(sendButton).toHaveAttribute('data-active', 'true');
|
|
await sendButton.click();
|
|
|
|
await expect(page.getByTestId('sidebar-tab-content-chat')).toBeVisible();
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content:
|
|
'AFFiNE is an open source all in one workspace.\nTranslate to chinese',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'success',
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('chat with ask ai input in edgeless mode', async ({
|
|
loggedInPage: page,
|
|
utils,
|
|
}) => {
|
|
await utils.chatPanel.closeChatPanel(page);
|
|
await utils.editor.askAIWithEdgeless(page, async () => {
|
|
await utils.editor.createShape(page, 'HelloWorld');
|
|
});
|
|
await page.waitForTimeout(1000);
|
|
await page.keyboard.type('What color is it?');
|
|
|
|
await page.waitForTimeout(1000);
|
|
const sendButton = await page.getByTestId('ai-panel-input-send');
|
|
await expect(sendButton).toHaveAttribute('data-active', 'true');
|
|
await sendButton.click();
|
|
|
|
await expect(page.getByTestId('sidebar-tab-content-chat')).toBeVisible();
|
|
await expect(page.locator('chat-content-images')).toBeVisible();
|
|
await utils.chatPanel.waitForHistory(page, [
|
|
{
|
|
role: 'user',
|
|
content: 'What color is it?',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
status: 'success',
|
|
},
|
|
]);
|
|
});
|
|
});
|