test(core): split and enhance copilot e2e tests (#11007)

### TL;DR

Split and enhance copilot e2e tests.

### What Changed

#### Tests Structure

The e2e tests are organized into the following categories:

1. **Basic Tests (`/basic`)**: Tests for verifying core AI capabilities including feature onboarding, authorization workflows, and basic chat interactions.
2. **Chat Interaction Tests (`/chat-with`)**: Tests for verifying the AI's interaction with various ​object types, such as attachments, images, text content, Edgeless elements, etc.
3. **AI Action Tests (`/ai-action`)**: Tests for verifying the AI's actions, such as text translation, gramma correction, etc.
4. **Insertion Tests (`/insertion`)**: Tests for verifying answer insertion functionality.

#### Tests Writing

Writing a copilot test cases is easier and clear

e.g.
```ts
test('support chat with specified doc', async ({ page, utils }) => {
  // Initialize the doc
  await focusDocTitle(page);
  await page.keyboard.insertText('Test Doc');
  await page.keyboard.press('Enter');
  await page.keyboard.insertText('EEee is a cute cat');

  await utils.chatPanel.chatWithDoc(page, 'Test Doc');

  await utils.chatPanel.makeChat(page, 'What is EEee?');
  await utils.chatPanel.waitForHistory(page, [
    {
      role: 'user',
      content: 'What is EEee?',
    },
    {
      role: 'assistant',
      status: 'success',
    },
  ]);

  const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
  expect(content).toMatch(/EEee/);
});
```

#### Summary

||Cases|
|------|----|
|Before|19||
|After|151||

> Close BS-2769
This commit is contained in:
yoyoyohamapi
2025-03-29 03:41:09 +00:00
parent a709ed2ef1
commit 317d3e7ea6
94 changed files with 4898 additions and 1225 deletions

View File

@@ -0,0 +1,20 @@
# AFFiNE Cloud Copilot E2E Tests
This directory contains end-to-end tests for the AFFiNE Cloud Copilot feature. The tests are organized in a structured way to ensure comprehensive coverage of different functionalities.
## Test Structure
The e2e tests are organized into the following categories:
1. **Basic Tests (`/basic`)**: Tests for verifying core AI capabilities including feature onboarding, authorization workflows, and basic chat interactions.
2. **Chat Interaction Tests (`/chat-with`)**: Tests for verifying the AI's interaction with various object types, such as attachments, images, text content, Edgeless elements, etc.
3. **AI Action Tests (`/ai-action`)**: Tests for verifying the AI's actions, such as text translation, gramma correction, etc.
4. **Insertion Tests (`/insertion`)**: Tests for verifying answer insertion functionality.
## Test Utilities
The `/utils` directory contains shared utilities for testing:
- **ChatPanelUtils**: Helper functions for chat panel interactions
- **EditorUtils**: Helper functions for editor operations
- **TestUtils**: General test utilities and setup functions

View File

@@ -0,0 +1,56 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/BrainstormIdeasWithMindMap', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate a mind map for the selected content', async ({
page,
utils,
}) => {
const { brainstormMindMap } = await utils.editor.askAIWithText(
page,
'Panda'
);
const { answer, responses } = await brainstormMindMap();
await expect(answer.locator('mini-mindmap-preview')).toBeVisible();
expect(responses).toEqual(new Set(['insert-below']));
});
test('should generate a mind map for the selected text block in edgeless', async ({
page,
utils,
}) => {
const { brainstormMindMap } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(page, 'Panda');
}
);
const { answer, responses } = await brainstormMindMap();
await expect(answer.locator('mini-mindmap-preview')).toBeVisible();
expect(responses).toEqual(new Set(['insert-below']));
});
test('should generate a mind map for the selected note block in edgeless', async ({
page,
utils,
}) => {
const { brainstormMindMap } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(page, 'Panda');
}
);
const { answer, responses } = await brainstormMindMap();
await expect(answer.locator('mini-mindmap-preview')).toBeVisible();
expect(responses).toEqual(new Set(['insert-below']));
});
});

View File

@@ -0,0 +1,87 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/ChangeTone', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support changing the tone of the selected content', async ({
page,
utils,
}) => {
const { changeTone } = await utils.editor.askAIWithText(
page,
'AFFiNE is a great note-taking app'
);
const { answer, responses } = await changeTone('informal');
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should support changing the tone of the selected text block in edgeless', async ({
page,
utils,
}) => {
const { changeTone } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(
page,
'AFFiNE is a great note-taking app'
);
}
);
const { answer, responses } = await changeTone('informal');
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should support changing the tone of the selected note block in edgeless', async ({
page,
utils,
}) => {
const { changeTone } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(
page,
'AFFiNE is a great note-taking app'
);
}
);
const { answer, responses } = await changeTone('informal');
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { changeTone } = await utils.editor.askAIWithText(
page,
'AFFiNE is a great note-taking app'
);
const { answer } = await changeTone('informal');
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Change tone/);
await expect(actionName).toHaveText(/Change tone/);
});
});

View File

@@ -0,0 +1,50 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/CheckCodeError', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support check code error', async ({ page, utils }) => {
const { checkCodeError } = await utils.editor.askAIWithCode(
page,
'consloe.log("Hello,World!");',
'javascript'
);
const { answer, responses } = await checkCodeError();
await expect(answer).toHaveText(/console/);
await expect(responses).toEqual(
new Set(['insert-below', 'replace-selection'])
);
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { checkCodeError } = await utils.editor.askAIWithCode(
page,
'consloe.log("Hello,World!");',
'javascript'
);
const { answer } = await checkCodeError();
const insert = answer.getByTestId('answer-insert-below');
await insert.click();
await utils.chatPanel.waitForHistory(page, [{ role: 'action' }]);
const {
message,
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(
message.getByTestId('original-text').locator('affine-code')
).toBeVisible();
await expect(panelAnswer).toHaveText(/console/);
await expect(prompt).toHaveText(/Check the code error of the follow code/);
await expect(actionName).toHaveText(/Check code error/);
});
});

View File

@@ -0,0 +1,20 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/ContinueWithAI', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
});
test('should support continue in chat panel', async ({ page, utils }) => {
const { continueWithAi } = await utils.editor.askAIWithText(page, 'Apple');
await continueWithAi();
const chatPanelInput = await page.getByTestId('chat-panel-input-container');
const quote = await chatPanelInput.getByTestId('chat-selection-quote');
await expect(quote).toHaveText(/Apple/, { timeout: 10000 });
});
});

View File

@@ -0,0 +1,92 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/ContinueWriting', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support continue writing the selected content', async ({
page,
utils,
}) => {
await page.setViewportSize({ width: 1280, height: 2000 });
const { continueWriting } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer, responses } = await continueWriting();
await expect(answer).toHaveText(/,*/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should support continue writing the selected text block in edgeless', async ({
page,
utils,
}) => {
const { continueWriting } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await continueWriting();
await expect(answer).toHaveText(/,*/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should support continue writing the selected note block in edgeless', async ({
page,
utils,
}) => {
const { continueWriting } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await continueWriting();
await expect(answer).toHaveText(/,*/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { continueWriting } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await continueWriting();
const insert = answer.getByTestId('answer-insert-below');
await insert.click();
await utils.chatPanel.waitForHistory(
page,
[
{
role: 'action',
},
],
10000
);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/,*/);
await expect(prompt).toHaveText(/Continue the following text/);
await expect(actionName).toHaveText(/Continue writing/);
});
});

View File

@@ -0,0 +1,38 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('expand mindmap node', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should expand the mindmap node', async ({ page, utils }) => {
let id: string;
const { expandMindMapNode } = await utils.editor.askAIWithEdgeless(
page,
async () => {
id = await utils.editor.createMindmap(page);
},
async () => {
// Select the first child in the mindmap
const { id: childId } = await utils.editor.getMindMapNode(
page,
id!,
[0, 0]
);
await utils.editor.selectElementInEdgeless(page, [childId]);
}
);
await expandMindMapNode();
// Child node should be expanded
await expect(async () => {
const newChild = await utils.editor.getMindMapNode(page, id!, [0, 0, 0]);
expect(newChild).toBeDefined();
}).toPass({ timeout: 20000 });
});
});

View File

@@ -0,0 +1,47 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/ExplainCode', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support explain code', async ({ page, utils }) => {
const { explainCode } = await utils.editor.askAIWithCode(
page,
'console.log("Hello, World!");',
'javascript'
);
const { answer } = await explainCode();
await expect(answer).toHaveText(/console.log/);
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { explainCode } = await utils.editor.askAIWithCode(
page,
'console.log("Hello, World!");',
'javascript'
);
const { answer } = await explainCode();
const insert = answer.getByTestId('answer-insert-below');
await insert.click();
await utils.chatPanel.waitForHistory(page, [{ role: 'action' }]);
const {
message,
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(
message.getByTestId('original-text').locator('affine-code')
).toBeVisible();
await expect(panelAnswer).toHaveText(/console.log/);
await expect(prompt).toHaveText(/Analyze and explain the follow code/);
await expect(actionName).toHaveText(/Explain this code/);
});
});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,78 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/ExplainSelection', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support explaining the selected content', async ({
page,
utils,
}) => {
const { explainSelection } = await utils.editor.askAIWithText(
page,
'LLM(AI)'
);
const { answer, responses } = await explainSelection();
await expect(answer).toHaveText(/Large Language Model/, { timeout: 20000 });
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should support explaining the selected text block in edgeless', async ({
page,
utils,
}) => {
const { explainSelection } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(page, 'LLM(AI)');
}
);
const { answer, responses } = await explainSelection();
await expect(answer).toHaveText(/Large Language Model/, { timeout: 20000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should support explaining the selected note block in edgeless', async ({
page,
utils,
}) => {
const { explainSelection } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(page, 'LLM(AI)');
}
);
const { answer, responses } = await explainSelection();
await expect(answer).toHaveText(/Large Language Model/, { timeout: 20000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { explainSelection } = await utils.editor.askAIWithText(page, 'LLM');
const { answer } = await explainSelection();
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/Large Language Model/);
await expect(prompt).toHaveText(/Analyze and explain the follow text/);
await expect(actionName).toHaveText(/Explain this/);
});
});

View File

@@ -0,0 +1,143 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/FindActions', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should find actions for selected content', async ({ page, utils }) => {
const { findActions } = await utils.editor.askAIWithText(
page,
`Choose a Booking Platform
Enter Travel Details
Compare and Select Flights`
);
const { answer, responses } = await findActions();
const todos = await answer.locator('affine-list').all();
const expectedTexts = [
'Choose a Booking Platform',
'Enter Travel Details',
'Compare and Select Flights',
];
await Promise.all(
todos.map(async (todo, index) => {
await expect(
todo.locator('.affine-list-block__todo-prefix')
).toBeVisible();
await expect(todo).toHaveText(expectedTexts[index]);
})
);
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should find actions for selected text block in edgeless', async ({
page,
utils,
}) => {
const { findActions } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(
page,
'Choose a Booking Platform'
);
}
);
const { answer, responses } = await findActions();
const todos = await answer.locator('affine-list').all();
const expectedTexts = [
'Choose a Booking Platform',
'Enter Travel Details',
'Compare and Select Flights',
];
await Promise.all(
todos.map(async (todo, index) => {
await expect(
todo.locator('.affine-list-block__todo-prefix')
).toBeVisible();
await expect(todo).toHaveText(expectedTexts[index]);
})
);
expect(responses).toEqual(new Set(['insert-below']));
});
test('should find actions for selected note block in edgeless', async ({
page,
utils,
}) => {
const { findActions } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(
page,
'Choose a Booking Platform'
);
}
);
const { answer, responses } = await findActions();
const todos = await answer.locator('affine-list').all();
const expectedTexts = [
'Choose a Booking Platform',
'Enter Travel Details',
'Compare and Select Flights',
];
await Promise.all(
todos.map(async (todo, index) => {
await expect(
todo.locator('.affine-list-block__todo-prefix')
).toBeVisible();
await expect(todo).toHaveText(expectedTexts[index]);
})
);
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { findActions } = await utils.editor.askAIWithText(
page,
`Choose a Booking Platform
Enter Travel Details
Compare and Select Flights`
);
const { answer } = await findActions();
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
const todos = await panelAnswer.locator('affine-list').all();
const expectedTexts = [
'Choose a Booking Platform',
'Enter Travel Details',
'Compare and Select Flights',
];
await Promise.all(
todos.map(async (todo, index) => {
await expect(
todo.locator('.affine-list-block__todo-prefix')
).toBeVisible();
await expect(todo).toHaveText(expectedTexts[index]);
})
);
await expect(prompt).toHaveText(/Find action items of the follow text/);
await expect(actionName).toHaveText(/Find action items from it/);
});
});

View File

@@ -0,0 +1,84 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/FixGrammar', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support fixing grammatical errors in the selected content', async ({
page,
utils,
}) => {
const { fixGrammar } = await utils.editor.askAIWithText(
page,
'I is a student'
);
const { answer, responses } = await fixGrammar();
await expect(answer).toHaveText(/I am a student/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should support fixing grammatical errors in the selected text block in edgeless', async ({
page,
utils,
}) => {
const { fixGrammar } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(page, 'I is a student');
}
);
const { answer, responses } = await fixGrammar();
await expect(answer).toHaveText(/I am a student/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should support fixing grammatical errors in the selected note block in edgeless', async ({
page,
utils,
}) => {
const { fixGrammar } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(page, 'I is a student');
}
);
const { answer, responses } = await fixGrammar();
await expect(answer).toHaveText(/I am a student/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { fixGrammar } = await utils.editor.askAIWithText(
page,
'I is a student'
);
const { answer } = await fixGrammar();
await expect(answer).toHaveText(/I am a student/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/I am a student/);
await expect(prompt).toHaveText(
/Improve the grammar of the following text/
);
await expect(actionName).toHaveText(/Improve grammar for it/);
});
});

View File

@@ -0,0 +1,78 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/FixSpelling', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support fixing spelling errors in the selected content', async ({
page,
utils,
}) => {
const { fixSpelling } = await utils.editor.askAIWithText(page, 'Appel');
const { answer, responses } = await fixSpelling();
await expect(answer).toHaveText(/Apple/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should support fixing spelling errors in the selected text block in edgeless', async ({
page,
utils,
}) => {
const { fixSpelling } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(page, 'Appel');
}
);
const { answer, responses } = await fixSpelling();
await expect(answer).toHaveText(/Apple/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should support fixing spelling errors in the selected note block in edgeless', async ({
page,
utils,
}) => {
const { fixSpelling } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(page, 'Appel');
}
);
const { answer, responses } = await fixSpelling();
await expect(answer).toHaveText(/Apple/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { fixSpelling } = await utils.editor.askAIWithText(page, 'Appel');
const { answer } = await fixSpelling();
await expect(answer).toHaveText(/Apple/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/Apple/);
await expect(prompt).toHaveText(
/Correct the spelling of the following text/
);
await expect(actionName).toHaveText(/Fix spelling for it/);
});
});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,71 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/GenerateAnImageWithText', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate an image for the selected content', async ({
page,
utils,
}) => {
const { generateImage } = await utils.editor.askAIWithText(page, 'Panda');
const { answer, responses } = await generateImage();
await expect(answer.getByTestId('ai-answer-image')).toBeVisible();
expect(responses).toEqual(new Set(['insert-below']));
});
test('should generate an image for the selected text block in edgeless', async ({
page,
utils,
}) => {
const { generateImage } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(page, 'Panda');
}
);
const { answer, responses } = await generateImage();
await expect(answer.getByTestId('ai-answer-image')).toBeVisible();
expect(responses).toEqual(new Set(['insert-below']));
});
test('should generate an image for the selected note block in edgeless', async ({
page,
utils,
}) => {
const { generateImage } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(page, 'Panda');
}
);
const { answer, responses } = await generateImage();
await expect(answer.getByTestId('ai-answer-image')).toBeVisible();
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { generateImage } = await utils.editor.askAIWithText(page, 'Panda');
const { answer } = await generateImage();
const insert = answer.getByTestId('answer-insert-below');
await insert.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const { answer: panelAnswer, actionName } =
await utils.chatPanel.getLatestAIActionMessage(page);
await expect(
panelAnswer.getByTestId('generated-image').locator('img')
).toBeVisible();
await expect(actionName).toHaveText(/image/);
});
});

View File

@@ -0,0 +1,108 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/GenerateHeadings', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate headings for selected content', async ({
page,
utils,
}) => {
const { generateHeadings } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer, responses } = await generateHeadings();
await Promise.race([
answer.locator('h1').isVisible(),
answer.locator('h2').isVisible(),
answer.locator('h3').isVisible(),
]);
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-above', 'replace-selection']));
});
test.fixme(
'should generate headings for selected text block in edgeless',
async ({ page, utils }) => {
const { generateHeadings } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await generateHeadings();
await Promise.race([
answer.locator('h1').isVisible(),
answer.locator('h2').isVisible(),
answer.locator('h3').isVisible(),
]);
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-above']));
}
);
test.fixme(
'should generate headings for selected note block in edgeless',
async ({ page, utils }) => {
const { generateHeadings } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await generateHeadings();
await Promise.race([
answer.locator('h1').isVisible(),
answer.locator('h2').isVisible(),
answer.locator('h3').isVisible(),
]);
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-above']));
}
);
test('should show chat history in chat panel', async ({ page, utils }) => {
const { generateHeadings } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await generateHeadings();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await Promise.race([
panelAnswer.locator('h1').isVisible(),
panelAnswer.locator('h2').isVisible(),
panelAnswer.locator('h3').isVisible(),
]);
await expect(prompt).toHaveText(/Create headings of the follow text/);
await expect(actionName).toHaveText(/Create headings/);
});
});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,86 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/GenerateOutline', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate outline for selected content', async ({
page,
utils,
}) => {
const { generateOutline } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer, responses } = await generateOutline();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should generate outline for selected text block in edgeless', async ({
page,
utils,
}) => {
const { generateOutline } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await generateOutline();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should generate outline for selected note block in edgeless', async ({
page,
utils,
}) => {
const { generateOutline } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await generateOutline();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { generateOutline } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await generateOutline();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Write an outline about this/);
await expect(actionName).toHaveText(/Write outline/);
});
});

View File

@@ -0,0 +1,62 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/GeneratePresentation', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate a presentation for the selected content', async ({
page,
utils,
}) => {
const { generatePresentation } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer, responses } = await generatePresentation();
await expect(answer.locator('ai-slides-renderer')).toBeVisible();
expect(responses).toEqual(new Set(['insert-below']));
});
test('should generate a presentation for the selected text block in edgeless', async ({
page,
utils,
}) => {
const { generatePresentation } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await generatePresentation();
await expect(answer.locator('ai-slides-renderer')).toBeVisible();
expect(responses).toEqual(new Set(['insert-below']));
});
test('should generate a presentation for the selected note block in edgeless', async ({
page,
utils,
}) => {
const { generatePresentation } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await generatePresentation();
await expect(answer.locator('ai-slides-renderer')).toBeVisible();
expect(responses).toEqual(new Set(['insert-below']));
});
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,80 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/ImproveWriting', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support improving the writing of the selected content', async ({
page,
utils,
}) => {
const { improveWriting } = await utils.editor.askAIWithText(
page,
'AFFiNE is so smart'
);
const { answer, responses } = await improveWriting();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should support improving the writing of the selected text block in edgeless', async ({
page,
utils,
}) => {
const { improveWriting } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(page, 'AFFiNE is so smart');
}
);
const { answer, responses } = await improveWriting();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should support improving the writing of the selected note block in edgeless', async ({
page,
utils,
}) => {
const { improveWriting } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(page, 'AFFiNE is so smart');
}
);
const { answer, responses } = await improveWriting();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { improveWriting } = await utils.editor.askAIWithText(
page,
'AFFiNE is so smart'
);
const { answer } = await improveWriting();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Improve the follow text/);
await expect(actionName).toHaveText(/Improve writing for it/);
});
});

View File

@@ -0,0 +1,86 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/MakeItLonger', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support making the selected content longer', async ({
page,
utils,
}) => {
const { makeItLonger } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer, responses } = await makeItLonger();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should support making the selected text block longer in edgeless', async ({
page,
utils,
}) => {
const { makeItLonger } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await makeItLonger();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should support making the selected note block longer in edgeless', async ({
page,
utils,
}) => {
const { makeItLonger } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await makeItLonger();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { makeItLonger } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await makeItLonger();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Expand the following text/);
await expect(actionName).toHaveText(/Make it longer/);
});
});

View File

@@ -0,0 +1,85 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/MakeItReal', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support making the selected content to real', async ({
page,
utils,
}) => {
const { makeItReal } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer, responses } = await makeItReal();
await expect(answer.locator('iframe')).toBeVisible({ timeout: 30000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should support making the selected text block to real in edgeless', async ({
page,
utils,
}) => {
const { makeItReal } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await makeItReal();
await expect(answer.locator('iframe')).toBeVisible({ timeout: 30000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should support making the selected note block to real in edgeless', async ({
page,
utils,
}) => {
const { makeItReal } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await makeItReal();
await expect(answer.locator('iframe')).toBeVisible({ timeout: 30000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { makeItReal } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await makeItReal();
const insert = answer.getByTestId('answer-insert-below');
await insert.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer.locator('affine-code')).toBeVisible();
await expect(prompt).toHaveText(/Write a web page of follow text/);
await expect(actionName).toHaveText(/Make it real with text/);
});
});

View File

@@ -0,0 +1,86 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/MakeItShorter', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support making the selected content shorter', async ({
page,
utils,
}) => {
const { makeItShorter } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer, responses } = await makeItShorter();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should support making the selected text block shorter in edgeless', async ({
page,
utils,
}) => {
const { makeItShorter } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await makeItShorter();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should support making the selected note block shorter in edgeless', async ({
page,
utils,
}) => {
const { makeItShorter } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await makeItShorter();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { makeItShorter } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await makeItShorter();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Shorten the follow text/);
await expect(actionName).toHaveText(/Make it shorter/);
});
});

View File

@@ -0,0 +1,36 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/RegenerateMindMap', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support regenerate the mind map for mindmap root', async ({
page,
utils,
}) => {
let id: string;
const { regenerateMindMap } = await utils.editor.askAIWithEdgeless(
page,
async () => {
id = await utils.editor.createMindmap(page);
},
async () => {
const { id: rootId } = await utils.editor.getMindMapNode(page, id!, [
0,
]);
await utils.editor.selectElementInEdgeless(page, [rootId]);
}
);
const { answer, responses } = await regenerateMindMap();
await expect(answer.locator('mini-mindmap-preview')).toBeVisible();
expect(responses).toEqual(new Set(['replace-selection']));
});
});

View File

@@ -0,0 +1,86 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/Summarize', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support summarizing the selected content', async ({
page,
utils,
}) => {
const { summarize } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer, responses } = await summarize();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should support summarizing the selected text block in edgeless', async ({
page,
utils,
}) => {
const { summarize } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await summarize();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should support summarizing the selected note block in edgeless', async ({
page,
utils,
}) => {
const { summarize } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await summarize();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { summarize } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await summarize();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Summary the follow text/);
await expect(actionName).toHaveText(/Summary/);
});
});

View File

@@ -0,0 +1,74 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/Translate', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support translating the selected content', async ({
page,
utils,
}) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer, responses } = await translate('German');
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should support translating the selected text block in edgeless', async ({
page,
utils,
}) => {
const { translate } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(page, 'Apple');
}
);
const { answer, responses } = await translate('German');
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should support translating the selected note block in edgeless', async ({
page,
utils,
}) => {
const { translate } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(page, 'Apple');
}
);
const { answer, responses } = await translate('German');
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('support show chat history in chat panel', async ({ page, utils }) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('German');
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/Apfel/);
await expect(prompt).toHaveText(/Translate/);
await expect(actionName).toHaveText(/Translate/);
});
});

View File

@@ -0,0 +1,86 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/WriteAnArticleAboutThis', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate an article for the selected content', async ({
page,
utils,
}) => {
const { writeArticle } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer, responses } = await writeArticle();
await expect(answer).toHaveText(/AFFiNE/);
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should support writing an article for the selected text block in edgeless', async ({
page,
utils,
}) => {
const { writeArticle } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await writeArticle();
await expect(answer).toHaveText(/AFFiNE/);
expect(responses).toEqual(new Set(['insert-below']));
});
test('should support writing an article for the selected note block in edgeless', async ({
page,
utils,
}) => {
const { writeArticle } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await writeArticle();
await expect(answer).toHaveText(/AFFiNE/);
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { writeArticle } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await writeArticle();
await expect(answer).toHaveText(/AFFiNE/);
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Write an article about this/);
await expect(actionName).toHaveText(/Write an article about this/);
});
});

View File

@@ -0,0 +1,86 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/WriteAnBlogPostAboutThis', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate an blog post for the selected content', async ({
page,
utils,
}) => {
const { writeBlogPost } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer, responses } = await writeBlogPost();
await expect(answer).toHaveText(/AFFiNE/);
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should support writing an blog post for the selected text block in edgeless', async ({
page,
utils,
}) => {
const { writeBlogPost } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await writeBlogPost();
await expect(answer).toHaveText(/AFFiNE/);
expect(responses).toEqual(new Set(['insert-below']));
});
test('should support writing an blog post for the selected note block in edgeless', async ({
page,
utils,
}) => {
const { writeBlogPost } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await writeBlogPost();
await expect(answer).toHaveText(/AFFiNE/);
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { writeBlogPost } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await writeBlogPost();
await expect(answer).toHaveText(/AFFiNE/);
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Write a blog post about this/);
await expect(actionName).toHaveText(/Write a blog post about this/);
});
});

View File

@@ -0,0 +1,86 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/WriteAnPoemAboutThis', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate an poem for the selected content', async ({
page,
utils,
}) => {
const { writePoem } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer, responses } = await writePoem();
await expect(answer).toHaveText(/AFFiNE/);
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should generate an poem for the selected text block in edgeless', async ({
page,
utils,
}) => {
const { writePoem } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await writePoem();
await expect(answer).toHaveText(/AFFiNE/);
expect(responses).toEqual(new Set(['insert-below']));
});
test('should generate an poem for the selected note block in edgeless', async ({
page,
utils,
}) => {
const { writePoem } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await writePoem();
await expect(answer).toHaveText(/AFFiNE/);
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { writePoem } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await writePoem();
await expect(answer).toHaveText(/AFFiNE/);
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Write a poem about this/);
await expect(actionName).toHaveText(/Write a poem about this/);
});
});

View File

@@ -0,0 +1,86 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/WriteAnTweetAboutThis', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate an tweet for the selected content', async ({
page,
utils,
}) => {
const { writeTwitterPost } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer, responses } = await writeTwitterPost();
await expect(answer).toHaveText(/AFFiNE/);
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should generate an tweet for the selected text block in edgeless', async ({
page,
utils,
}) => {
const { writeTwitterPost } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await writeTwitterPost();
await expect(answer).toHaveText(/AFFiNE/);
expect(responses).toEqual(new Set(['insert-below']));
});
test('should generate an tweet for the selected note block in edgeless', async ({
page,
utils,
}) => {
const { writeTwitterPost } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await writeTwitterPost();
await expect(answer).toHaveText(/AFFiNE/);
expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { writeTwitterPost } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await writeTwitterPost();
await expect(answer).toHaveText(/AFFiNE/);
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Write a twitter about this/);
await expect(actionName).toHaveText(/Write a twitter about this/);
});
});

View File

@@ -0,0 +1,27 @@
// eslint-disable no-empty-pattern
import { test as base } from '@affine-test/kit/playwright';
import { ChatPanelUtils } from '../utils/chat-panel-utils';
import { EditorUtils } from '../utils/editor-utils';
import { TestUtils } from '../utils/test-utils';
interface TestUtilsFixtures {
utils: {
testUtils: TestUtils;
chatPanel: typeof ChatPanelUtils;
editor: typeof EditorUtils;
};
}
export const test = base.extend<TestUtilsFixtures>({
utils: async ({}, use) => {
const testUtils = TestUtils.getInstance();
await use({
testUtils,
chatPanel: ChatPanelUtils,
editor: EditorUtils,
});
},
});
export type TestFixtures = typeof test;

View File

@@ -0,0 +1,28 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIBasic/Authority', () => {
test.beforeEach(async ({ page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should show error & login button when no login', async ({
page,
utils,
}) => {
await utils.chatPanel.makeChat(page, 'Hello');
await expect(page.getByTestId('ai-error')).toBeVisible();
await expect(page.getByTestId('ai-error-action-button')).toBeVisible();
});
test('should support login in error state', async ({ page, utils }) => {
await utils.chatPanel.makeChat(page, 'Hello');
const loginButton = page.getByTestId('ai-error-action-button');
await loginButton.click();
await expect(page.getByTestId('auth-modal')).toBeVisible();
});
});

View File

@@ -0,0 +1,346 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIBasic/Chat', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should display empty state when no messages', async ({ 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 ({ 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 ({ 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 ({
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',
},
]);
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 ({
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 ({ 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 ({ 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 ({ 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 page.pause();
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 ({
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 ({ 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 ({ 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 });
});
});

View File

@@ -0,0 +1,72 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe.configure({ mode: 'parallel' });
test.describe('AIBasic/Onboarding', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should show AI onboarding', async ({ page }) => {
await expect(page.getByTestId('ai-onboarding')).toBeVisible();
// Show options
await expect(
page.getByTestId('read-foreign-language-article-with-ai')
).toBeVisible();
await expect(
page.getByTestId('tidy-an-article-with-ai-mindmap-action')
).toBeVisible();
await expect(
page.getByTestId('add-illustrations-to-the-article')
).toBeVisible();
await expect(page.getByTestId('complete-writing-with-ai')).toBeVisible();
await expect(page.getByTestId('freely-communicate-with-ai')).toBeVisible();
});
test('read a foreign language article with AI', async ({ page, utils }) => {
await page.getByTestId('read-foreign-language-article-with-ai').click();
await utils.editor.isEdgelessMode(page);
const docTitle = await utils.editor.getDocTitle(page);
await expect(docTitle).toContain('Read a foreign language');
});
test('tidy an article with AI MindMap Action', async ({ page, utils }) => {
await page.getByTestId('tidy-an-article-with-ai-mindmap-action').click();
await utils.editor.isEdgelessMode(page);
const docTitle = await utils.editor.getDocTitle(page);
await expect(docTitle).toContain('Tidy');
});
test('add illustrations to the article', async ({ page, utils }) => {
await page.getByTestId('add-illustrations-to-the-article').click();
await utils.editor.isEdgelessMode(page);
const docTitle = await utils.editor.getDocTitle(page);
await expect(docTitle).toContain('Add illustrations');
});
test('complete writing with AI', async ({ page, utils }) => {
await page.getByTestId('complete-writing-with-ai').click();
await utils.editor.isEdgelessMode(page);
const docTitle = await utils.editor.getDocTitle(page);
await expect(docTitle).toContain('Complete writing');
});
test('freely communicate with AI', async ({ page, utils }) => {
await page.getByTestId('freely-communicate-with-ai').click();
await utils.editor.isEdgelessMode(page);
const docTitle = await utils.editor.getDocTitle(page);
await expect(docTitle).toContain('Freely communicate');
});
});

View File

@@ -0,0 +1,89 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIChatWith/Attachments', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('support chat with attachment', async ({ page, utils }) => {
const textContent = 'EEee is a cute cat';
const buffer = Buffer.from(textContent);
await utils.chatPanel.chatWithAttachments(
page,
[
{
name: 'test.txt',
mimeType: 'text/plain',
buffer: buffer,
},
],
'What is EEee?'
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'What is EEee?',
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
expect(content).toMatch(/EEee/);
}).toPass({ timeout: 10000 });
});
test('support chat with multiple attachments', async ({ page, utils }) => {
const textContent1 = 'EEee is a cute cat';
const textContent2 = 'FFff is a cute dog';
const buffer1 = Buffer.from(textContent1);
const buffer2 = Buffer.from(textContent2);
await utils.chatPanel.chatWithAttachments(
page,
[
{
name: 'document1.txt',
mimeType: 'text/plain',
buffer: buffer1,
},
{
name: 'document2.txt',
mimeType: 'text/plain',
buffer: buffer2,
},
],
'What is EEee? What is FFff?'
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'What is EEee? What is FFff?',
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { content, message } =
await utils.chatPanel.getLatestAssistantMessage(page);
expect(content).toMatch(/EEee/);
expect(content).toMatch(/FFff/);
expect(await message.locator('affine-footnote-node').count()).toBe(2);
}).toPass({ timeout: 20000 });
});
});

View File

@@ -0,0 +1,3 @@
import { test } from '../base/base-test';
test.describe('AIChatWith/Collections', () => {});

View File

@@ -0,0 +1,82 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { focusDocTitle } from '@affine-test/kit/utils/editor';
import {
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIChatWith/Doc', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('support chat with specified doc', async ({ page, utils }) => {
// Initialize the doc
await focusDocTitle(page);
await page.keyboard.insertText('Test Doc');
await page.keyboard.press('Enter');
await page.keyboard.insertText('EEee is a cute cat');
await utils.chatPanel.chatWithDoc(page, 'Test Doc');
await utils.chatPanel.makeChat(page, 'What is EEee?');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'What is EEee?',
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
expect(content).toMatch(/EEee/);
}).toPass({ timeout: 10000 });
});
test('support chat with specified docs', async ({ page, utils }) => {
// Initialize the doc 1
await focusDocTitle(page);
await page.keyboard.insertText('Test Doc1');
await page.keyboard.press('Enter');
await page.keyboard.insertText('EEee is a cute cat');
// Initialize the doc 2
await clickNewPageButton(page);
await waitForEditorLoad(page);
await focusDocTitle(page);
await page.keyboard.insertText('Test Doc2');
await page.keyboard.press('Enter');
await page.keyboard.insertText('FFff is a cute dog');
await utils.chatPanel.chatWithDoc(page, 'Test Doc1');
await utils.chatPanel.chatWithDoc(page, 'Test Doc2');
await utils.chatPanel.makeChat(page, 'What is EEee? What is FFff?');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'What is EEee? What is FFff?',
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
expect(content).toMatch(/EEee/);
expect(content).toMatch(/FFff/);
}).toPass({ timeout: 10000 });
});
});

View File

@@ -0,0 +1,52 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import type { EdgelessRootBlockComponent } from '@blocksuite/affine/blocks/root';
import type { GfxModel } from '@blocksuite/std/gfx';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIChatWith/EdgelessMindMap', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support replace mindmap with the regenerated one', async ({
page,
utils,
}) => {
let id: string;
const { regenerateMindMap } = await utils.editor.askAIWithEdgeless(
page,
async () => {
id = await utils.editor.createMindmap(page);
},
async () => {
const { id: rootId } = await utils.editor.getMindMapNode(page, id!, [
0,
]);
await utils.editor.selectElementInEdgeless(page, [rootId]);
}
);
const { answer } = await regenerateMindMap();
await expect(answer.locator('mini-mindmap-preview')).toBeVisible();
const replace = answer.getByTestId('answer-replace');
await replace.click();
// Expect original mindmap to be replaced
const mindmaps = await page.evaluate(() => {
const edgelessBlock = document.querySelector(
'affine-edgeless-root'
) as EdgelessRootBlockComponent;
const mindmaps = edgelessBlock?.gfx.gfxElements
.filter((el: GfxModel) => 'type' in el && el.type === 'mindmap')
.map((el: GfxModel) => el.id);
return mindmaps;
});
expect(mindmaps).toHaveLength(1);
expect(mindmaps?.[0]).not.toBe(id!);
});
});

View File

@@ -0,0 +1,32 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIChatWith/EdgelessNoteBlock', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support insert a new note block below the current', async ({
page,
utils,
}) => {
const { translate } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(page, 'Apple');
}
);
const { answer } = await translate('German');
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
const insertBelow = answer.getByTestId('answer-insert-below');
await insertBelow.click();
await expect(page.locator('affine-edgeless-note').nth(1)).toHaveText(
/Apfel/
);
});
});

View File

@@ -0,0 +1,3 @@
import { test } from '../base/base-test';
test.describe('AIChatWith/EdgelessShape', () => {});

View File

@@ -0,0 +1,32 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIChatWith/EdgelessTextBlock', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support insert answer below the current text', async ({
page,
utils,
}) => {
const { translate } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(page, 'Apple');
}
);
const { answer } = await translate('German');
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
const insertBelow = answer.getByTestId('answer-insert-below');
await insertBelow.click();
await expect(page.locator('affine-edgeless-text')).toHaveText(
/Apple[\s\S]*Apfel/
);
});
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
import { test } from '../base/base-test';
test.describe('AIChatWith/tags', () => {});

View File

@@ -0,0 +1,130 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIChatWith/Text', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support stop generating', async ({ page, utils }) => {
await utils.editor.askAIWithText(page, 'Appel');
await page.getByTestId('action-fix-grammar').click();
await expect(page.getByTestId('ai-generating')).toBeVisible();
const stop = await page.getByTestId('ai-stop');
await stop.click();
await expect(page.getByTestId('ai-generating')).not.toBeVisible();
});
test('should support copy answer', async ({ page, utils }) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('German');
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
const copy = answer.getByTestId('answer-copy-button');
await copy.click();
await expect(answer.getByTestId('answer-copied')).toBeVisible();
const clipboardText = await page.evaluate(() =>
navigator.clipboard.readText()
);
expect(clipboardText).toBe('Apfel');
});
test('should support insert below', async ({ page, utils }) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('German');
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
const insertBelow = answer.getByTestId('answer-insert-below');
await insertBelow.click();
const content = await utils.editor.getEditorContent(page);
expect(content).toBe('Apple\nApfel');
});
test('should support insert above', async ({ page, utils }) => {
const { generateHeadings } = await utils.editor.askAIWithText(
page,
'AFFiNE'
);
const { answer } = await generateHeadings();
await answer.locator('h1').isVisible();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
const insertAbove = answer.getByTestId('answer-insert-above');
await insertAbove.click();
const content = await utils.editor.getEditorContent(page);
expect(content).toBe('AFFiNE\nAFFiNE');
});
test('should support replace selection', async ({ page, utils }) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('German');
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
const content = await utils.editor.getEditorContent(page);
expect(content).toBe('Apfel');
});
test('should support continue in chat', async ({ page, utils }) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('German');
await expect(answer).toHaveText(/Apfel/, { timeout: 10000 });
const continueInChat = answer.getByTestId('answer-continue-in-chat');
await continueInChat.click();
const chatPanelInput = await page.getByTestId('chat-panel-input-container');
const quote = await chatPanelInput.getByTestId('chat-selection-quote');
await expect(quote).toHaveText(/Apple/, { timeout: 10000 });
});
test('should support regenerate', async ({ page, utils }) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('German');
const regenerate = answer.getByTestId('answer-regenerate');
await regenerate.click();
const content = await utils.editor.getEditorContent(page);
expect(content).toBe('Apple');
});
test('should show error when request failed', async ({ page, utils }) => {
await page.route('**/graphql', route => route.abort('failed'));
await utils.editor.askAIWithText(page, 'Appel');
await page.getByTestId('action-fix-spelling').click();
await expect(page.getByTestId('ai-error')).toBeVisible();
});
test('should support retry when error', async ({ page, utils }) => {
await page.route('**/graphql', route => route.abort('failed'));
await utils.editor.askAIWithText(page, 'Appel');
await page.getByTestId('action-fix-spelling').click();
const aiPanelContainer = await page.getByTestId('ai-panel-container');
await page.route('**/graphql', route => route.continue());
await aiPanelContainer.getByTestId('error-retry').click();
const answer = await utils.editor.waitForAiAnswer(page);
await expect(answer).toHaveText(/Apple/, { timeout: 10000 });
});
test('should support discard', async ({ page, utils }) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('German');
const discard = answer.getByTestId('answer-discard');
await discard.click();
await expect(answer).not.toBeVisible();
const content = await utils.editor.getEditorContent(page);
expect(content).toBe('Apple');
});
test('should support discard when click outside', async ({ page, utils }) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('German');
await page.mouse.click(0, 0);
await expect(page.getByText('Discard the AI result')).toBeVisible();
await page.getByTestId('confirm-modal-confirm').click();
await expect(answer).not.toBeVisible();
const content = await utils.editor.getEditorContent(page);
expect(content).toBe('Apple');
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,73 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIInsertion/AddToEdgelessAsNote', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should only show option in edgeless mode', async ({ page, utils }) => {
await utils.editor.focusToEditor(page);
await utils.chatPanel.makeChat(page, 'Hello');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello',
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(
page.getByTestId('action-add-to-edgeless-as-note')
).not.toBeVisible();
await utils.editor.switchToEdgelessMode(page);
await expect(
page.getByTestId('action-add-to-edgeless-as-note')
).toBeVisible();
});
test('should add to edgeless as note in edgeless mode', async ({
page,
utils,
}) => {
await utils.editor.switchToEdgelessMode(page);
// Delete default note
await (await page.waitForSelector('affine-edgeless-note')).click();
page.keyboard.press('Delete');
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.addAsNote();
await page.getByText('New note created');
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
const noteContent = await utils.editor.getNoteContent(page);
expect(noteContent).toBe(content);
}).toPass({ timeout: 5000 });
});
});

View File

@@ -0,0 +1,201 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { focusDocTitle } from '@affine-test/kit/utils/editor';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIInsertion/Insert', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should insert content below selected block in page mode', async ({
page,
utils,
}) => {
// Create tow blocks
// - Hello Block
// - World Block
await utils.editor.focusToEditor(page);
await page.keyboard.insertText('Hello Block');
await page.keyboard.press('Enter');
await page.keyboard.insertText('World Block');
await utils.chatPanel.openChatPanel(page);
await utils.chatPanel.makeChat(page, 'Hello');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello',
},
{
role: 'assistant',
status: 'success',
},
]);
// Focus to Hello
// - Hello<cursor />
await page.getByText('Hello Block').click();
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
await actions.insert();
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
const editorContent = await utils.editor.getEditorContent(page);
expect(editorContent).toBe(`Hello Block\n${content}\nWorld Block`);
}).toPass({ timeout: 5000 });
});
test('should insert content below selected block in edgeless mode', async ({
page,
utils,
}) => {
await utils.editor.switchToEdgelessMode(page);
await utils.editor.focusToEditor(page);
await page.keyboard.insertText('Hello Block');
await page.keyboard.press('Enter');
await page.keyboard.insertText('World Block');
await utils.chatPanel.openChatPanel(page);
await utils.chatPanel.makeChat(page, 'Hello');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello',
},
{
role: 'assistant',
status: 'success',
},
]);
// Focus to Hello
// - Hello<cursor />
await page.locator('affine-edgeless-note').dblclick();
await page.getByText('Hello Block').click();
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
await actions.insert();
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
const noteContent = await utils.editor.getNoteContent(page);
expect(noteContent).toBe(`Hello Block\n${content}\nWorld Block`);
}).toPass({ timeout: 5000 });
});
test('should insert content at the end of the page when no block is selected', async ({
page,
utils,
}) => {
// Create tow blocks
// - Hello Block
// - World Block
await utils.editor.focusToEditor(page);
await page.keyboard.insertText('Hello Block');
await page.keyboard.press('Enter');
await page.keyboard.insertText('World Block');
await utils.chatPanel.openChatPanel(page);
await utils.chatPanel.makeChat(page, 'Hello');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello',
},
{
role: 'assistant',
status: 'success',
},
]);
// Focus Doc Title
// - Hello<cursor />
await focusDocTitle(page);
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
await actions.insert();
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
const editorContent = await utils.editor.getEditorContent(page);
expect(editorContent).toBe(`Hello Block\nWorld Block\n${content}`);
}).toPass({ timeout: 5000 });
});
test('should insert content at the end of the note when no block is selected in edgeless mode', async ({
page,
utils,
}) => {
await utils.editor.switchToEdgelessMode(page);
await utils.editor.focusToEditor(page);
await page.keyboard.insertText('Hello Block');
await page.keyboard.press('Enter');
await page.keyboard.insertText('World Block');
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.insert();
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
const noteContent = await utils.editor.getNoteContent(page);
expect(noteContent).toBe(`Hello Block\nWorld Block\n${content}`);
}).toPass({ timeout: 5000 });
});
test('should create a new note when no block or note is selected in edgeless mode', async ({
page,
utils,
}) => {
await utils.editor.switchToEdgelessMode(page);
// Delete default note
await (await page.waitForSelector('affine-edgeless-note')).click();
page.keyboard.press('Delete');
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.insert();
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
const noteContent = await utils.editor.getNoteContent(page);
expect(noteContent).toBe(content);
}).toPass({ timeout: 5000 });
});
});

View File

@@ -0,0 +1,73 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIInsertion/SaveAsBlock', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should save content as a chat block in page mode', async ({
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.saveAsBlock();
// Switch to edgeless mode
await utils.editor.isEdgelessMode(page);
// Verify the ai block is created
await page.waitForSelector('affine-edgeless-ai-chat');
const aiBlock = await page.locator('affine-edgeless-ai-chat');
await expect(aiBlock).toBeVisible();
});
test('should save content as a chat block in edgeless mode', async ({
page,
utils,
}) => {
await utils.editor.switchToEdgelessMode(page);
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.saveAsBlock();
await page.getByText('Successfully saved chat to a block');
// Verify the ai block is created
await page.waitForSelector('affine-edgeless-ai-chat');
const aiBlock = await page.locator('affine-edgeless-ai-chat');
await expect(aiBlock).toBeVisible();
});
});

View File

@@ -0,0 +1,75 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIInsertion/SaveAsDoc', () => {
test.beforeEach(async ({ page, utils }) => {
const user = await utils.testUtils.getUser();
await loginUser(page, user);
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should save content as a doc in page mode', async ({ 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',
},
]);
// Wait for the assistant answer to be completely rendered
await page.waitForTimeout(1000);
const { actions, content } =
await utils.chatPanel.getLatestAssistantMessage(page);
await actions.saveAsDoc();
await page.getByText('New doc created');
// Verify the ai block is created
const editorContent = await utils.editor.getEditorContent(page);
expect(editorContent).toBe(content);
});
test('should save content as a doc in edgeless mode', async ({
page,
utils,
}) => {
await utils.editor.switchToEdgelessMode(page);
await utils.chatPanel.openChatPanel(page);
await utils.chatPanel.makeChat(page, 'Hello');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello',
},
{
role: 'assistant',
status: 'success',
},
]);
// Wait for the assistant answer to be completely rendered
await page.waitForTimeout(1000);
const { actions, content } =
await utils.chatPanel.getLatestAssistantMessage(page);
await actions.saveAsDoc();
await page.getByText('New doc created');
// Switch to page mode
await utils.editor.isPageMode(page);
// Verify the ai block is created
const editorContent = await utils.editor.getEditorContent(page);
expect(editorContent).toBe(content);
});
});

View File

@@ -0,0 +1,3 @@
interface Window {
showOpenFilePicker?: () => Promise<FileSystemFileHandle[]>;
}

View File

@@ -0,0 +1,276 @@
// eslint-disable eslint-plugin-unicorn(prefer-dom-node-dataset
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
type ChatStatus = 'loading' | 'success' | 'error' | 'idle' | 'transmitting';
type ChatUserMessage = {
role: 'user';
content: string;
};
type ChatAssistantMessage = {
role: 'assistant';
status: ChatStatus;
title: string;
content: string;
};
type ChatActionMessage = {
role: 'action';
title: string;
content: string;
};
type ChatMessage = ChatUserMessage | ChatAssistantMessage | ChatActionMessage;
export class ChatPanelUtils {
public static async openChatPanel(page: Page) {
if (await page.getByTestId('sidebar-tab-chat').isHidden()) {
await page.getByTestId('right-sidebar-toggle').click({
delay: 200,
});
}
await page.getByTestId('sidebar-tab-chat').click();
await expect(page.getByTestId('sidebar-tab-content-chat')).toBeVisible();
}
public static async typeChat(page: Page, content: string) {
await page.getByTestId('chat-panel-input').focus();
await page.keyboard.type(content);
}
public static async typeChatSequentially(page: Page, content: string) {
const input = await page.locator('chat-panel-input textarea').nth(0);
await input.pressSequentially(content, {
delay: 50,
});
}
public static async makeChat(page: Page, content: string) {
await this.openChatPanel(page);
await this.typeChat(page, content);
await page.keyboard.press('Enter');
}
public static async clearChat(page: Page) {
await page.getByTestId('chat-panel-clear').click();
await page.getByTestId('confirm-modal-confirm').click();
await page.waitForTimeout(500);
}
public static async collectHistory(page: Page) {
return await page.evaluate(() => {
const chatPanel = document.querySelector<HTMLElement>(
'[data-testid="chat-panel-messages"]'
);
if (!chatPanel) {
return [] as ChatMessage[];
}
const messages = chatPanel.querySelectorAll<HTMLElement>(
'chat-message-user,chat-message-assistant,chat-message-action'
);
return Array.from(messages).map(m => {
const isAssistant = m.dataset.testid === 'chat-message-assistant';
const isChatAction = m.dataset.testid === 'chat-message-action';
const isUser = !isAssistant && !isChatAction;
if (isUser) {
return {
role: 'user' as const,
content:
m.querySelector<HTMLElement>(
'[data-testid="chat-content-pure-text"]'
)?.innerText || '',
};
}
if (isAssistant) {
return {
role: 'assistant' as const,
status: m.dataset.status as ChatStatus,
title: m.querySelector<HTMLElement>('.user-info')?.innerText || '',
content:
m.querySelector<HTMLElement>('chat-content-rich-text editor-host')
?.innerText || '',
};
}
// Must be chat action at this point
return {
role: 'action' as const,
title: m.querySelector<HTMLElement>('.user-info')?.innerText || '',
content:
m.querySelector<HTMLElement>('chat-content-rich-text editor-host')
?.innerText || '',
};
});
});
}
private static expectHistory(
history: ChatMessage[],
expected: (
| Partial<ChatUserMessage>
| Partial<ChatAssistantMessage>
| Partial<ChatActionMessage>
)[]
) {
expect(history).toHaveLength(expected.length);
history.forEach((message, index) => {
const expectedMessage = expected[index];
expect(message).toMatchObject(expectedMessage);
});
}
public static async expectToHaveHistory(
page: Page,
expected: (
| Partial<ChatUserMessage>
| Partial<ChatAssistantMessage>
| Partial<ChatActionMessage>
)[]
) {
const history = await this.collectHistory(page);
this.expectHistory(history, expected);
}
public static async waitForHistory(
page: Page,
expected: (
| Partial<ChatUserMessage>
| Partial<ChatAssistantMessage>
| Partial<ChatActionMessage>
)[],
timeout = 2 * 60000
) {
await expect(async () => {
const history = await this.collectHistory(page);
this.expectHistory(history, expected);
}).toPass({ timeout });
}
public static async getLatestAssistantMessage(page: Page) {
const message = page.getByTestId('chat-message-assistant').last();
const actions = await message.getByTestId('chat-actions');
const actionList = await message.getByTestId('chat-action-list');
return {
message,
content: await message
.locator('chat-content-rich-text editor-host')
.innerText(),
actions: {
copy: async () => actions.getByTestId('action-copy-button').click(),
retry: async () => actions.getByTestId('action-retry-button').click(),
insert: async () => actionList.getByTestId('action-insert').click(),
saveAsBlock: async () =>
actionList.getByTestId('action-save-as-block').click(),
saveAsDoc: async () =>
actionList.getByTestId('action-save-as-doc').click(),
addAsNote: async () =>
actionList.getByTestId('action-add-to-edgeless-as-note').click(),
},
};
}
public static async getLatestAIActionMessage(page: Page) {
const message = page.getByTestId('chat-message-action').last();
const actionName = await message.getByTestId('action-name');
await actionName.click();
const answer = await message.getByTestId('answer-prompt');
const prompt = await message.getByTestId('chat-message-action-prompt');
return {
message,
answer,
prompt,
actionName,
};
}
public static async chatWithDoc(page: Page, docName: string) {
const withButton = await page.getByTestId('chat-panel-with-button');
await withButton.click();
const withMenu = await page.getByTestId('ai-add-popover');
await withMenu.getByText(docName).click();
await page.getByTestId('chat-panel-chips').getByText(docName);
}
public static async chatWithAttachments(
page: Page,
attachments: { name: string; mimeType: string; buffer: Buffer }[],
text: string
) {
await page.evaluate(() => {
delete window.showOpenFilePicker;
});
for (const attachment of attachments) {
const fileChooserPromise = page.waitForEvent('filechooser');
const withButton = await page.getByTestId('chat-panel-with-button');
await withButton.click();
const withMenu = await page.getByTestId('ai-add-popover');
await withMenu.getByTestId('ai-chat-with-files').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(attachment);
}
await expect(async () => {
const states = await page
.getByTestId('chat-panel-chip')
.evaluateAll(elements =>
elements.map(el => el.getAttribute('data-state'))
);
await expect(states).toHaveLength(attachments.length);
await expect(states.every(state => state === 'finished')).toBe(true);
}).toPass({ timeout: 20000 });
await page.pause();
await this.makeChat(page, text);
}
public static async chatWithImages(
page: Page,
images: { name: string; mimeType: string; buffer: Buffer }[],
text: string
) {
await page.evaluate(() => {
delete window.showOpenFilePicker;
});
const fileChooserPromise = page.waitForEvent('filechooser');
// Open file upload dialog
await page.getByTestId('chat-panel-input-image-upload').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(images);
await page.waitForSelector('chat-panel-input img');
await this.makeChat(page, text);
}
public static async enableNetworkSearch(page: Page) {
const networkSearch = await page.getByTestId('chat-network-search');
if ((await networkSearch.getAttribute('data-active')) === 'false') {
await networkSearch.click();
}
}
public static async disableNetworkSearch(page: Page) {
const networkSearch = await page.getByTestId('chat-network-search');
if ((await networkSearch.getAttribute('data-active')) === 'true') {
await networkSearch.click();
}
}
public static async isNetworkSearchEnabled(page: Page) {
const networkSearch = await page.getByTestId('chat-network-search');
return (await networkSearch.getAttribute('aria-disabled')) === 'false';
}
public static async isImageUploadEnabled(page: Page) {
const imageUpload = await page.getByTestId('chat-panel-input-image-upload');
const disabled = await imageUpload.getAttribute('data-disabled');
return disabled === 'false';
}
}

View File

@@ -0,0 +1,528 @@
import {
createEdgelessNoteBlock,
setEdgelessTool,
} from '@affine-test/kit/utils/editor';
import {
pressEscape,
selectAllByKeyboard,
} from '@affine-test/kit/utils/keyboard';
import { getBlockSuiteEditorTitle } from '@affine-test/kit/utils/page-logic';
import type { EdgelessRootBlockComponent } from '@blocksuite/affine/blocks/root';
import type {
MindmapElementModel,
ShapeElementModel,
} from '@blocksuite/affine-model';
import type { GfxModel } from '@blocksuite/std/gfx';
import { type Page } from '@playwright/test';
export class EditorUtils {
public static async focusToEditor(page: Page) {
const title = getBlockSuiteEditorTitle(page);
await title.focus();
await page.keyboard.press('Enter');
}
public static async getEditorContent(page: Page) {
let content = '';
let retry = 3;
while (!content && retry > 0) {
const lines = await page.$$('page-editor .inline-editor');
const contents = await Promise.all(lines.map(el => el.innerText()));
content = contents
.map(c => c.replace(/\u200B/g, '').trim())
.filter(c => !!c)
.join('\n');
if (!content) {
await page.waitForTimeout(500);
retry -= 1;
}
}
return content;
}
public static async getNoteContent(page: Page) {
const edgelessNode = await page.waitForSelector(
'affine-edgeless-note .edgeless-note-page-content'
);
return (await edgelessNode.innerText()).replace(/\u200B/g, '').trim();
}
public static async switchToEdgelessMode(page: Page) {
const editor = await page.waitForSelector('page-editor');
await page.getByTestId('switch-edgeless-mode-button').click();
editor.waitForElementState('hidden');
await page.waitForSelector('edgeless-editor');
}
public static async switchToPageMode(page: Page) {
await page.getByTestId('switch-page-mode-button').click();
await page.waitForSelector('page-editor');
}
public static async isPageMode(page: Page) {
return await page.waitForSelector('page-editor');
}
public static async isEdgelessMode(page: Page) {
return await page.waitForSelector('edgeless-editor');
}
public static async getDocTitle(page: Page) {
return page.getByTestId('title-edit-button').innerText();
}
public static async waitForAiAnswer(page: Page) {
const answer = await page.getByTestId('ai-penel-answer');
await answer.waitFor({
state: 'visible',
timeout: 2 * 60000,
});
return answer;
}
private static createAction(page: Page, action: () => Promise<void>) {
return async () => {
await action();
const responses = new Set<string>();
const answer = await this.waitForAiAnswer(page);
const responsesMenu = answer.getByTestId('answer-responses');
await responsesMenu.isVisible();
await responsesMenu.scrollIntoViewIfNeeded({ timeout: 60000 });
if (await responsesMenu.getByTestId('answer-insert-below').isVisible()) {
responses.add('insert-below');
}
if (await responsesMenu.getByTestId('answer-insert-above').isVisible()) {
responses.add('insert-above');
}
if (await responsesMenu.getByTestId('answer-replace').isVisible()) {
responses.add('replace-selection');
}
if (
await responsesMenu.getByTestId('answer-use-as-caption').isVisible()
) {
responses.add('use-as-caption');
}
if (
await responsesMenu.getByTestId('answer-create-new-note').isVisible()
) {
responses.add('create-new-note');
}
return {
answer: await this.waitForAiAnswer(page),
responses,
};
};
}
public static async createEdgelessText(page: Page, text: string) {
await setEdgelessTool(page, 'text');
await page.mouse.click(400, 400);
await page.locator('affine-edgeless-text').waitFor({ state: 'visible' });
await page.waitForTimeout(100);
const texts = text.split('\n');
for (const [index, line] of texts.entries()) {
await page.keyboard.insertText(line);
if (index !== texts.length - 1) {
await page.keyboard.press('Enter');
}
}
}
public static async createEdgelessNote(page: Page, text: string) {
await createEdgelessNoteBlock(page, [500, 300]);
const texts = text.split('\n');
for (const [index, line] of texts.entries()) {
await page.keyboard.insertText(line);
if (index !== texts.length - 1) {
await page.keyboard.press('Enter');
}
}
}
public static async createMindmap(page: Page) {
await page.keyboard.press('m');
await page.mouse.click(400, 400);
const id = await page.evaluate(() => {
const edgelessBlock = document.querySelector(
'affine-edgeless-root'
) as EdgelessRootBlockComponent;
if (!edgelessBlock) {
throw new Error('edgeless block not found');
}
const mindmaps = edgelessBlock.gfx.gfxElements.filter(
(el: GfxModel) => 'type' in el && el.type === 'mindmap'
);
return mindmaps[mindmaps.length - 1].id;
});
return id;
}
public static async getMindMapNode(
page: Page,
mindmapId: string,
path: number[]
) {
return page.evaluate(
({ mindmapId, path }) => {
const edgelessBlock = document.querySelector(
'affine-edgeless-root'
) as EdgelessRootBlockComponent;
if (!edgelessBlock) {
throw new Error('edgeless block not found');
}
const mindmap = edgelessBlock.gfx.getElementById(
mindmapId
) as MindmapElementModel;
if (!mindmap) {
throw new Error(`Mindmap not found: ${mindmapId}`);
}
const node = mindmap.getNodeByPath(path);
if (!node) {
throw new Error(`Mindmap node not found at: ${path}`);
}
const rect = edgelessBlock.gfx.viewport.toViewBound(
node.element.elementBound
);
return {
path: mindmap.getPath(node),
id: node.id,
text: (node.element as ShapeElementModel).text?.toString() ?? '',
rect: {
x: rect.x,
y: rect.y,
w: rect.w,
h: rect.h,
},
};
},
{
mindmapId,
path,
}
);
}
public static async selectElementInEdgeless(page: Page, elements: string[]) {
await page.evaluate(
({ elements }) => {
const edgelessBlock = document.querySelector(
'affine-edgeless-root'
) as EdgelessRootBlockComponent;
if (!edgelessBlock) {
throw new Error('edgeless block not found');
}
edgelessBlock.gfx.selection.set({
elements,
});
},
{ elements }
);
}
public static async askAIWithEdgeless(
page: Page,
createBlock: () => Promise<void>,
afterSelected?: () => Promise<void>
) {
await this.switchToEdgelessMode(page);
await selectAllByKeyboard(page);
await page.keyboard.press('Delete');
await createBlock();
await pressEscape(page, 5);
await selectAllByKeyboard(page);
await afterSelected?.();
await page.getByTestId('ask-ai-button').click();
return {
aiImageFilter: this.createAction(page, () =>
page.getByTestId('action-ai-image-filter').click()
),
brainstorm: this.createAction(page, () =>
page.getByTestId('action-brainstorm').click()
),
brainstormMindMap: this.createAction(page, () =>
page.getByTestId('action-brainstorm-mindmap').click()
),
changeTone: (
tone: 'professional' | 'informal' | 'friendly' | 'critical' | 'humorous'
) =>
this.createAction(page, async () => {
await page.getByTestId('action-change-tone').hover();
await page.getByTestId(`action-change-tone-${tone}`).click();
})(),
checkCodeError: this.createAction(page, () =>
page.getByTestId('action-check-code-error').click()
),
continueWithAi: async () => {
page.getByTestId('action-continue-with-ai').click();
},
continueWriting: this.createAction(page, () =>
page.getByTestId('action-continue-writing').click()
),
createHeadings: this.createAction(page, () =>
page.getByTestId('action-create-headings').click()
),
explainSelection: this.createAction(page, () =>
page.getByTestId('action-explain-selection').click()
),
findActions: this.createAction(page, () =>
page.getByTestId('action-find-actions').click()
),
fixGrammar: this.createAction(page, () =>
page.getByTestId('action-fix-grammar').click()
),
fixSpelling: this.createAction(page, () =>
page.getByTestId('action-fix-spelling').click()
),
generateCaption: this.createAction(page, () =>
page.getByTestId('action-generate-caption').click()
),
generateHeadings: this.createAction(page, () =>
page.getByTestId('action-generate-headings').click()
),
generateImage: this.createAction(page, () =>
page.getByTestId('action-generate-image').click()
),
generateOutline: this.createAction(page, () =>
page.getByTestId('action-generate-outline').click()
),
generatePresentation: this.createAction(page, () =>
page.getByTestId('action-generate-presentation').click()
),
imageProcessing: this.createAction(page, () =>
page.getByTestId('action-image-processing').click()
),
improveGrammar: this.createAction(page, () =>
page.getByTestId('action-improve-grammar').click()
),
improveWriting: this.createAction(page, () =>
page.getByTestId('action-improve-writing').click()
),
makeItLonger: this.createAction(page, () =>
page.getByTestId('action-make-it-longer').click()
),
makeItReal: this.createAction(page, () =>
page.getByTestId('action-make-it-real').click()
),
makeItShorter: this.createAction(page, () =>
page.getByTestId('action-make-it-shorter').click()
),
summarize: this.createAction(page, () =>
page.getByTestId('action-summarize').click()
),
translate: (language: string) =>
this.createAction(page, async () => {
await page.getByTestId('action-translate').hover();
await page.getByTestId(`action-translate-${language}`).click();
})(),
writeArticle: this.createAction(page, () =>
page.getByTestId('action-write-article').click()
),
writeBlogPost: this.createAction(page, () =>
page.getByTestId('action-write-blog-post').click()
),
writePoem: this.createAction(page, () =>
page.getByTestId('action-write-poem').click()
),
writeTwitterPost: this.createAction(page, () =>
page.getByTestId('action-write-twitter-post').click()
),
regenerateMindMap: this.createAction(page, () =>
page.getByTestId('action-regenerate-mindmap').click()
),
expandMindMapNode: async () =>
page.getByTestId('action-expand-mindmap-node').click(),
} as const;
}
public static async askAIWithCode(
page: Page,
code: string,
language: string
) {
await this.focusToEditor(page);
await page.keyboard.insertText(`\`\`\`${language}`);
await page.keyboard.press('Enter');
await page.keyboard.insertText(code);
await page.locator('affine-code').blur();
await page.locator('affine-code').hover();
await page.getByTestId('ask-ai-button').click();
return {
explainCode: this.createAction(page, () =>
page.getByTestId('action-explain-code').click()
),
checkCodeError: this.createAction(page, () =>
page.getByTestId('action-check-code-error').click()
),
};
}
public static async askAIWithImage(
page: Page,
image: { name: string; mimeType: string; buffer: Buffer }
) {
await page.evaluate(() => {
delete window.showOpenFilePicker;
});
const fileChooserPromise = page.waitForEvent('filechooser');
await this.focusToEditor(page);
await page.keyboard.press('/');
await page.keyboard.insertText('image');
await page.locator('affine-slash-menu').getByTestId('Image').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(image);
await page.locator('affine-page-image').click();
await page.getByTestId('ask-ai-button').click();
return {
explainImage: this.createAction(page, () =>
page.getByTestId('action-explain-image').click()
),
generateImage: this.createAction(page, () =>
page.getByTestId('action-generate-image').click()
),
generateCaption: this.createAction(page, () =>
page.getByTestId('action-generate-caption').click()
),
imageProcessing: (type: string) =>
this.createAction(page, async () => {
await page.getByTestId('action-image-processing').hover();
await page.getByTestId(`action-image-processing-${type}`).click();
})(),
imageFilter: (style: string) =>
this.createAction(page, async () => {
await page.getByTestId('action-ai-image-filter').hover();
await page.getByTestId(`action-image-filter-${style}`).click();
})(),
};
}
public static async askAIWithText(page: Page, text: string) {
await this.focusToEditor(page);
const texts = text.split('\n');
for (const [index, line] of texts.entries()) {
await page.keyboard.insertText(line);
if (index !== texts.length - 1) {
await page.keyboard.press('Enter');
}
}
await page.keyboard.press('ControlOrMeta+A');
await page.keyboard.press('ControlOrMeta+A');
await page.keyboard.press('ControlOrMeta+A');
const askAI = await page.locator('page-editor editor-toolbar ask-ai-icon');
await askAI.waitFor({
state: 'attached',
timeout: 5000,
});
await askAI.click();
return {
aiImageFilter: this.createAction(page, () =>
page.getByTestId('action-ai-image-filter').click()
),
brainstorm: this.createAction(page, () =>
page.getByTestId('action-brainstorm').click()
),
brainstormMindMap: this.createAction(page, () =>
page.getByTestId('action-brainstorm-mindmap').click()
),
changeTone: (
tone: 'professional' | 'informal' | 'friendly' | 'critical' | 'humorous'
) =>
this.createAction(page, async () => {
await page.getByTestId('action-change-tone').hover();
await page.getByTestId(`action-change-tone-${tone}`).click();
})(),
checkCodeError: this.createAction(page, () =>
page.getByTestId('action-check-code-error').click()
),
continueWithAi: async () => {
page.getByTestId('action-continue-with-ai').click();
},
continueWriting: this.createAction(page, () =>
page.getByTestId('action-continue-writing').click()
),
createHeadings: this.createAction(page, () =>
page.getByTestId('action-create-headings').click()
),
explainSelection: this.createAction(page, () =>
page.getByTestId('action-explain-selection').click()
),
findActions: this.createAction(page, () =>
page.getByTestId('action-find-actions').click()
),
fixGrammar: this.createAction(page, () =>
page.getByTestId('action-fix-grammar').click()
),
fixSpelling: this.createAction(page, () =>
page.getByTestId('action-fix-spelling').click()
),
generateCaption: this.createAction(page, () =>
page.getByTestId('action-generate-caption').click()
),
generateHeadings: this.createAction(page, () =>
page.getByTestId('action-generate-headings').click()
),
generateImage: this.createAction(page, () =>
page.getByTestId('action-generate-image').click()
),
generateOutline: this.createAction(page, () =>
page.getByTestId('action-generate-outline').click()
),
generatePresentation: this.createAction(page, () =>
page.getByTestId('action-generate-presentation').click()
),
imageProcessing: this.createAction(page, () =>
page.getByTestId('action-image-processing').click()
),
improveGrammar: this.createAction(page, () =>
page.getByTestId('action-improve-grammar').click()
),
improveWriting: this.createAction(page, () =>
page.getByTestId('action-improve-writing').click()
),
makeItLonger: this.createAction(page, () =>
page.getByTestId('action-make-it-longer').click()
),
makeItReal: this.createAction(page, () =>
page.getByTestId('action-make-it-real').click()
),
makeItShorter: this.createAction(page, () =>
page.getByTestId('action-make-it-shorter').click()
),
summarize: this.createAction(page, () =>
page.getByTestId('action-summarize').click()
),
translate: (language: string) =>
this.createAction(page, async () => {
await page.getByTestId('action-translate').hover();
await page.getByTestId(`action-translate-${language}`).click();
})(),
writeArticle: this.createAction(page, () =>
page.getByTestId('action-write-article').click()
),
writeBlogPost: this.createAction(page, () =>
page.getByTestId('action-write-blog-post').click()
),
writePoem: this.createAction(page, () =>
page.getByTestId('action-write-poem').click()
),
writeTwitterPost: this.createAction(page, () =>
page.getByTestId('action-write-twitter-post').click()
),
} as const;
}
}

View File

@@ -0,0 +1,64 @@
import { createRandomAIUser } from '@affine-test/kit/utils/cloud';
import { openHomePage, setCoreUrl } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
import type { Store } from '@blocksuite/affine/store';
import type { Page } from '@playwright/test';
declare global {
interface Window {
doc: Store;
}
}
export class TestUtils {
private static instance: TestUtils;
private isProduction: boolean;
private constructor() {
this.isProduction = process.env.NODE_ENV === 'production';
if (
process.env.PLAYWRIGHT_USER_AGENT &&
process.env.PLAYWRIGHT_EMAIL &&
!process.env.PLAYWRIGHT_PASSWORD
) {
setCoreUrl(process.env.PLAYWRIGHT_CORE_URL || 'http://localhost:8080');
this.isProduction = true;
}
}
public static getInstance(): TestUtils {
if (!TestUtils.instance) {
TestUtils.instance = new TestUtils();
}
return TestUtils.instance;
}
public getUser() {
if (
!this.isProduction ||
!process.env.PLAYWRIGHT_EMAIL ||
!process.env.PLAYWRIGHT_PASSWORD
) {
return createRandomAIUser();
}
return {
email: process.env.PLAYWRIGHT_EMAIL,
password: process.env.PLAYWRIGHT_PASSWORD,
};
}
public async setupTestEnvironment(page: Page) {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
}
public async createTestWorkspace(page: Page, name: string = 'test') {
await createLocalWorkspace({ name }, page);
}
}