Files
AFFiNE-Mirror/tests/affine-cloud-copilot/e2e/basic/chat.spec.ts
yoyoyohamapi 3f762cc87b feat(core): embedding status tooltip (#12382)
### TL;DR

feat: display embedding tip for ai chat

![截屏2025-05-20 10.35.11.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/MyktQ6Qwc7H6TiRCFoYN/c3a4fe47-1995-4ec0-bde0-ddbe19ce95a2.png)

> CLOSE BS-3051

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced an embedding status tooltip in the AI chat interface, providing real-time feedback on embedding progress for your workspace.
  - Added support for embedding status tracking within the AI provider and client services.
- **Style**
  - Updated the AI chat footer layout for improved clarity and usability.
- **Tests**
  - Added an end-to-end test to ensure the embedding status tooltip displays correctly.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-20 11:08:33 +00:00

488 lines
13 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 display embedding status tooltip', async ({
loggedInPage: page,
}) => {
const check = await page.getByTestId(
'ai-chat-embedding-status-tooltip-check'
);
await expect(check).toBeVisible();
await check.hover();
const tooltip = await page.getByTestId('ai-chat-embedding-status-tooltip');
await expect(tooltip).toBeVisible();
await expect(tooltip).toHaveText(/Results will improve after embedding/i);
});
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',
},
]);
});
});