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 committed 2025-03-29 03:41:09 +00:00
1 parent a709ed2ef1
commit 317d3e7ea6
94 files changed
+4898 -1225

No files matched your search

@@ -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']));
});
});
@@ -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/);
});
});
@@ -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/);
});
});
@@ -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 });
});
});
@@ -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/);
});
});
@@ -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 });
});
});
@@ -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/);
});
});
@@ -0,0 +1,52 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
const base64 =
'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 Line truncated
const image = {
name: 'image.jpeg',
mimeType: 'image/jpeg',
buffer: Buffer.from(base64, 'base64'),
};
test.describe('AIAction/ExplainImage', () => {
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 image', async ({
page,
utils,
}) => {
const { explainImage } = await utils.editor.askAIWithImage(page, image);
const { answer, responses } = await explainImage();
await expect(answer).toHaveText(/kitten/i, { timeout: 20000 });
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { explainImage } = await utils.editor.askAIWithImage(page, image);
const { answer } = await explainImage();
const insertBelow = answer.getByTestId('answer-insert-below');
await insertBelow.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
message,
answer: panelAnswer,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(message.getByTestId('original-images')).toBeVisible();
await expect(panelAnswer).toHaveText(/kitten/i);
await expect(actionName).toHaveText(/Explain this image/);
});
});
@@ -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/);
});
});
@@ -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/);
});
});
@@ -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/);
});
});
@@ -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/);
});
});
@@ -0,0 +1,55 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
const base64 =
'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 Line truncated
const image = {
name: 'image.jpeg',
mimeType: 'image/jpeg',
buffer: Buffer.from(base64, 'base64'),
};
test.describe('AIAction/GenerateAnImageWithImage', () => {
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 image', async ({
page,
utils,
}) => {
const { generateImage } = await utils.editor.askAIWithImage(page, image);
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.askAIWithImage(page, image);
const { answer } = await generateImage();
await expect(answer.getByTestId('ai-answer-image')).toBeVisible();
const insert = answer.getByTestId('answer-insert-below');
await insert.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
message,
answer: panelAnswer,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(message.getByTestId('original-image')).toBeVisible();
await expect(
panelAnswer.getByTestId('generated-image').locator('img')
).toBeVisible();
await expect(actionName).toHaveText(/image/);
});
});
@@ -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/);
});
});
@@ -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/);
});
});
@@ -0,0 +1,52 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
const base64 =
'/9j/4AAQSkZJRgABAQEAAAAAAAD/4QAuRXhpZgAATU0AKgAAAAgAAkAAAAMAAAABAAAAAEABAAEAAAABAAAAAAAAAAD/2wBDAAoHBwkHBgoJCAkLCwoMDxkQDw4ODx4WFxIZJCAmJSMgIyIoLTkwKCo2KyIjMkQyNjs9QEBAJjBGS0U+Sjk/QD3/2wBDAQsLCw8NDx0QEB09KSMpPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT3/wAARCAD6APoDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD0yWZ/MPPb0phmf1/SmSk+YaKuwWEMr56/pSGZ/X9KaTSU2y0SCZ/X9KPNf1/QUztTwOKNkIkEr+v6UvnP/e/So6KQ0iTzn/vfpR5z/wB79KjpM0ATfaJPX9KabmT+9+gqEnPSkJxQIebh/X9KYZnz979BUZkUck1XkuVHTmgC4Zn/AL36CozcsP4/0FUTKx74p6IepouMsG5kI4P6U0Syk/e/QUgTkZqUIKQhySORy36U8SP6/pTEAz7VJsxzTAcJXx1/Sjzn24z+lMOBSGgCTznx1/SmGV/736CkplA2Mkmk/vfoKheV8fe/QU+Soz0pAiIytn736CozIwOSePwpx703HrQxksaSy4wDgnAbHFASfONjdcY2+lNWaRE2KxCZzinC4lGAGPHsKQF6zMoTdyVx1wKs+fJ/e/QVVtpGKbd3GOmKm5q47CFkJ8w02nSD94abQhWAk0g5pcZoxiiwBUlMFOzQx3FopMim5pAOJxUZkA5JpskgHeqFzcYBOaTdgsTyajHGcck1Sm1J3OFGB61myXBkc+lKDnHcVk5tvQvlSLcdyxcLIxyelWQKzZAZE2gkMOVNS6dfLMNj/LKvBU1SfcTRpRpzmpgCGHFMjKkDHOamBGcE1VyRwBp4Timoe/pUgOTQIAOfemksDjNWAAKZImeRVXAi5ptSADpSkYoEJjimmnU00AQPUR9Klf7pqPFIoicGoz1qZxmoiKQxo+9TqbTqALdscCp81UhI6VNketNMC3L/AK00ynT8SN9Kjyaq4rDqO1NzTu1CYWDNGaKQkAUNoQZ5qOSTHFRyS4qpJNnNK4x0svPX8Kz76QiAmrPJqlqJxHg1nPYqO6Mz7SYzzVmKRiQwBNV4/KLgOpb2J4q9HJFGMLFj3zkVhFM2k12JEIk6ZDDsaztWtpQwvLY4lU/MB3FXxeRbwpABPQ9KsAqcjA56itbGL0MG28SyxusTKTJ161pS6vOE3EY3DjFc9qds1vrEYRSVZgCce9dJNbKEjY7cAYP0oVwujOHjJrIlZELjOARRZ/EWKS8MckRCKM5BrjPEFwEuniT7ynt6msyztpDIIlyZZm2ii7JZ7Xo/iWz1VPkkG49s9K2I5klzsYHHvXjEVtPphkVZSjjCnHHHc1a0Txjc2F/GsjF4GO0Z7DPWi7Qj2Apk5FRE1gX/AIljs7VZ1+YEAkA9jUemeMLLUY+G2uTjaetaJodjos03qc0yOZJAArA5HrT/AKVVxETjJIqKpz0qIpzxQx9BjdqikAxmpiOaY5UDLHFICAikBycelQTX0UQ65Y9AKW2O9N38R5NJjLKnBqTzKipc0rgakv3zUdSTH94ajqxoKeDTKUuAOTQJjiQBVaWWmzXGBwaoySk9D1pNiHyS5amZqME55p4INF9AFBwaz9WkUR7s1cJA5NY+psJQVB5rOb0KhuVLWS2mO2SVQxPGa0o7N0I2TBoz/DXMx6ZLJIShwQc11eiRgRhWB3dwfWs4LozWbtsyaOyeQbWUMvoRVuOxPlnJxx19KuwhUGD07U+Q4HbArS1jBtsypNOQYZwGZSeSKzdbuhbJheSO34Vs38xjhJAwCRzXDaxfG4vDbo3TIZuvsaTBGBbW7X2pTXEmDGoLHPc1raHpzSahJeMpwDhAR0rWh0pbbRwQgLHJbA59hWvp1itnp4cjLbRj60JAcnrUbRyTfMdzKFGOnqa5oJmSJsEFT8uOnHet3xLcE6gY8nCoe/8AEf8A61Q+H7aO+1KOJhkKAW9xnJoe4jXhtp7+1KgZUYVQR696dD4alsozNuzLztHTGa6TQRHc3t2kaqFRgCQP0rd+wiRzlep447VSQ72Oc0xLm2GXZiBgc10NveCQAMcN6Ur6cACuB1qq0SxuQMlgevqaaugvc0iQR1pnGMk1Vh80gBVPPU9hVmONQcM2+Q9B2FVcQqQM/PQHuaebO2JBkJY/WnyGUjAwPwquUZRlpAT6UrjFfR7SU7lUKfUVShszZySxM27DZBI7Vajm2TBV3e+OafckGQN3KjNC1CzKxTAzSVKehpuKYzQl/wBYajp8v+sNRvIEHWmJsHcAelVJZuuKZNcj1qm7knOaGGoskhc02gGjNZgLSEgc+lGaimOI2PfFO4EU1yHcop6dayJnV7ofOV5pY5JEJ7kmq88MpcOq5x1FZN3ZolY2Yo1yGyM9mFX7dwHHAB7HFZWnFtg3DgHDKRWkf3TAj5l7VSJe5oiRg4JHyt19jQZicg/QVX8wGMMrE56fWp4drAccnlaogbfR77YqThtpIrz+w0ySXWyGUkZLHI/hr0e5AIyDn5c1jk21ncyS/KGIAX8//r0NK4XLlnpp8iNWHHU0/U41t7DauenQda1LaRJEGCOFzmsTxHcGKFmH3QAMeuaYjzTX5QbyVmGTgEe3bmpfCsqQ/abpyAVTGf6Vm6/KDeTKpPzNjk5xTrUNb6Ufm/1rZ2jv6VHmUej/AA8LTx3crdGc4x+p/Ou6jhA5x1rjPh3btHpILDBZufoK7kEYwKpbEsq3MbBDtHzVUjtY4xmbl85NaxGQMioJIVcknNXYVyhJIv3VGQOuOlLGFPRwrexp8qeUCCOD7VXMSy5JA59R/WoZaQtwlzGhZSzL7YNUXuJDnduBHqKvDzIRgM209c/MKoXLsX2jBB9DkVnNtbGsUhltJLJdDMeUzyRV+4/1nTgVHY2xjy7ZGKdJknPrV07tXZM7X0I+9L+FKB6U/HvWhBLcyBHPNZk1115pt9efvGGf84rOMpL5P4UNgiZ5Gc5oD+9RA5pam4yXPuKM89RUJOBQHzSuBMT6VVmdidoNSvJgZNQR/vJumM0mA9LHcAx6nriphZAcrg46g1fhHyY4NK6YHcUrIVzP8kg/KF+nrTowwJRlbB5API/OrQjJcED8xTzGwIYArk846U7BcZCUI8sLg9gf8asSQmOPcudobdkVC8bRjcFBUckD+da0cYltgwIIK8+4qkJmVLc5jAHXaRiub1q1kwJwzZU4OO+eldJJb/6TtUYJOcD9affaaskZXBYdgPXtQ0F7CaKJZIck4ULjjvjj+lYfi+5eIMpOQASw9DjA/LJNdjplqLawjTBBC4OfWvNfiTePHerEm796u0YHX/J/pQ9ETfU4Zo2vtQLRsW3Hqa1RHG9tDAqsZNw4A5bn/GoREttJHFajdLtAc4zyef8AP4V1fhvw5KZIbmYH5mLAkccdKnco7vwxZGw02OI8sFAJA7+ldDGMYqhZx+Wir6DFXwcirSESZB6Uw4Apc/hTH6igLFW6jMoI5zVARNEeTJj1HNaUqeYpG4qfUdqxbm2uQ+Y53YH1OCKllw10ZY84gYVw2exqAoskgIXD1AZLj7siq/8AtEYNWbONjN83X0qG76GtkkWyfLg29z1quRVibk4x04qEjFbJaGPUYBjk06kpcUrsZhX0UsU5Eikeh7Hiquea7G6tklLKyhlPYisO90R0y1sdw/unqKGiEzNBOKdn3phDRnaylW7g0mTSKQ4mjPem5pCalgR3MoA5as6TVlt5gwY8HvU2oyFYSwGfrXJXTiaQ8MpHcHipb1KW1z0uxvlmiVlKtkdqtmbfxtrzPSbq5t3CgttY43Aniu2sZp5IR5gYnHUjGaadybGzHKp+UnBHrUySKVKsACfQ5rn769ltoSQ23A9M155qPinV7l5Ugu2hhVjucYXHsMVSYmrK56+XUckjI9RkH61o2JURlUb5TyF9K+f7PxlrWnPtttVnZM5KyAOP1rvvDHj5rx44NSjWC6YbkcDCyjvT2EtdDubeNTezE8FflFaghUg8dSKyYZVMxkHO7BHuMVsRPvH4UwaYECOM+mK8u8URNc6rJc3DARQgrHkdW6kfoBn3r1G5P7sjHJFcZeaGbyQLMx5fdkjqDyaTBGH4S8LtJeC5ulVlGSfdu5/pXcFIkmEUfBQBcAcVd07T0sbUKqgYGTj1qtbwia6kkUtgsSOOBTSsO5oRAYH0qcEAc1WlmS2j3MyqB3JqC2vJbqTcEZY+x9adxGh19qCPxojHbv6VKEFAinIGxxn6CojGD/Ft9zWgYlPNVbm2V0Khc1LKTMiYxxTA7wxPYVehj81wfugD Line truncated
const image = {
name: 'image.jpeg',
mimeType: 'image/jpeg',
buffer: Buffer.from(base64, 'base64'),
};
test.describe('AIAction/GenerateImageCaption', () => {
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 image caption', async ({ page, utils }) => {
const { generateCaption } = await utils.editor.askAIWithImage(page, image);
const { answer, responses } = await generateCaption();
await expect(answer).toHaveText(/cat|kitten/i);
await expect(responses).toEqual(
new Set(['insert-below', 'use-as-caption', 'replace-selection'])
);
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { generateCaption } = await utils.editor.askAIWithImage(page, image);
const { answer } = await generateCaption();
await expect(answer).toHaveText(/cat|kitten/i);
const insert = answer.getByTestId('answer-insert-below');
await insert.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
message,
answer: panelAnswer,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(message.getByTestId('original-images')).toBeVisible();
await expect(panelAnswer).toHaveText(/cat|kitten/i);
await expect(actionName).toHaveText(/Generate a caption/);
});
});
@@ -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/);
});
});
@@ -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']));
});
});
@@ -0,0 +1,29 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
const base64 =
'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 Line truncated
const image = {
name: 'image.jpeg',
mimeType: 'image/jpeg',
buffer: Buffer.from(base64, 'base64'),
};
test.describe('AIAction/ImageFilter', () => {
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 image filter', async ({ page, utils }) => {
const { imageFilter } = await utils.editor.askAIWithImage(page, image);
const { answer, responses } = await imageFilter('pixel-style');
await expect(answer.getByTestId('ai-answer-image')).toBeVisible();
await expect(responses).toEqual(new Set(['insert-below']));
});
});
@@ -0,0 +1,52 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
const base64 =
'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 Line truncated
const image = {
name: 'image.jpeg',
mimeType: 'image/jpeg',
buffer: Buffer.from(base64, 'base64'),
};
test.describe('AIAction/ImageProcessing', () => {
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 image processing', async ({ page, utils }) => {
const { imageProcessing } = await utils.editor.askAIWithImage(page, image);
const { answer, responses } = await imageProcessing('remove-background');
await expect(answer.getByTestId('ai-answer-image')).toBeVisible();
await expect(responses).toEqual(new Set(['insert-below']));
});
test('should show chat history in chat panel', async ({ page, utils }) => {
const { imageProcessing } = await utils.editor.askAIWithImage(page, image);
const { answer } = await imageProcessing('remove-background');
await expect(answer.getByTestId('ai-answer-image')).toBeVisible();
const insert = answer.getByTestId('answer-insert-below');
await insert.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
message,
answer: panelAnswer,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(message.getByTestId('original-image')).toBeVisible();
await expect(
panelAnswer.getByTestId('generated-image').locator('img')
).toBeVisible();
await expect(actionName).toHaveText(/Remove background/);
});
});
@@ -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/);
});
});
@@ -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/);
});
});
@@ -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/);
});
});
@@ -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/);
});
});
@@ -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']));
});
});
@@ -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/);
});
});
@@ -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/);
});
});
@@ -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/);
});
});
@@ -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/);
});
});
@@ -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/);
});
});
@@ -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/);
});
});
@@ -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;
@@ -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();
});
});
@@ -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 });
});
});
@@ -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');
});
});
@@ -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 });
});
});
@@ -0,0 +1,3 @@
import { test } from '../base/base-test';
test.describe('AIChatWith/Collections', () => {});
@@ -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 });
});
});
@@ -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!);
});
});
@@ -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/
);
});
});
@@ -0,0 +1,3 @@
import { test } from '../base/base-test';
test.describe('AIChatWith/EdgelessShape', () => {});
@@ -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/
);
});
});
@@ -0,0 +1,35 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
const base64 =
'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 Line truncated
const image = {
name: 'image.jpeg',
mimeType: 'image/jpeg',
buffer: Buffer.from(base64, 'base64'),
};
test.describe('AIChatWith/Image', () => {
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 use the generated caption', async ({ page, utils }) => {
const { generateCaption } = await utils.editor.askAIWithImage(page, image);
const { answer } = await generateCaption();
await expect(answer).toHaveText(/cat|kitten/i);
const caption = await answer.innerText();
const useAsCaption = answer.getByTestId('answer-use-as-caption');
await useAsCaption.click();
const imageBlock = await page.locator('affine-image');
const captionBlock = await imageBlock.locator('block-caption-editor');
await expect(captionBlock).toBeVisible();
await expect(captionBlock.locator('textarea')).toHaveValue(caption);
});
});
@@ -0,0 +1,77 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { test } from '../base/base-test';
test.describe('AIChatWith/Image', () => {
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 image', async ({ page, utils }) => {
const image =
'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 Line truncated
const buffer = Buffer.from(image, 'base64');
await utils.chatPanel.chatWithImages(
page,
[
{
name: 'image.jpeg',
mimeType: 'image/jpeg',
buffer: buffer,
},
],
'What is the small animal in the picture?'
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'What is the small animal in the picture?',
},
{
role: 'assistant',
status: 'success',
},
]);
});
test('support chat with mutiple images', async ({ page, utils }) => {
const image =
'/9j/4AAQSkZJRgABAQEAAAAAAAD/4QAuRXhpZgAATU0AKgAAAAgAAkAAAAMAAAABAAAAAEABAAEAAAABAAAAAAAAAAD/2wBDAAoHBwkHBgoJCAkLCwoMDxkQDw4ODx4WFxIZJCAmJSMgIyIoLTkwKCo2KyIjMkQyNjs9QEBAJjBGS0U+Sjk/QD3/2wBDAQsLCw8NDx0QEB09KSMpPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT3/wAARCAD6APoDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD0yWZ/MPPb0phmf1/SmSk+YaKuwWEMr56/pSGZ/X9KaTSU2y0SCZ/X9KPNf1/QUztTwOKNkIkEr+v6UvnP/e/So6KQ0iTzn/vfpR5z/wB79KjpM0ATfaJPX9KabmT+9+gqEnPSkJxQIebh/X9KYZnz979BUZkUck1XkuVHTmgC4Zn/AL36CozcsP4/0FUTKx74p6IepouMsG5kI4P6U0Syk/e/QUgTkZqUIKQhySORy36U8SP6/pTEAz7VJsxzTAcJXx1/Sjzn24z+lMOBSGgCTznx1/SmGV/736CkplA2Mkmk/vfoKheV8fe/QU+Soz0pAiIytn736CozIwOSePwpx703HrQxksaSy4wDgnAbHFASfONjdcY2+lNWaRE2KxCZzinC4lGAGPHsKQF6zMoTdyVx1wKs+fJ/e/QVVtpGKbd3GOmKm5q47CFkJ8w02nSD94abQhWAk0g5pcZoxiiwBUlMFOzQx3FopMim5pAOJxUZkA5JpskgHeqFzcYBOaTdgsTyajHGcck1Sm1J3OFGB61myXBkc+lKDnHcVk5tvQvlSLcdyxcLIxyelWQKzZAZE2gkMOVNS6dfLMNj/LKvBU1SfcTRpRpzmpgCGHFMjKkDHOamBGcE1VyRwBp4Timoe/pUgOTQIAOfemksDjNWAAKZImeRVXAi5ptSADpSkYoEJjimmnU00AQPUR9Klf7pqPFIoicGoz1qZxmoiKQxo+9TqbTqALdscCp81UhI6VNketNMC3L/AK00ynT8SN9Kjyaq4rDqO1NzTu1CYWDNGaKQkAUNoQZ5qOSTHFRyS4qpJNnNK4x0svPX8Kz76QiAmrPJqlqJxHg1nPYqO6Mz7SYzzVmKRiQwBNV4/KLgOpb2J4q9HJFGMLFj3zkVhFM2k12JEIk6ZDDsaztWtpQwvLY4lU/MB3FXxeRbwpABPQ9KsAqcjA56itbGL0MG28SyxusTKTJ161pS6vOE3EY3DjFc9qds1vrEYRSVZgCce9dJNbKEjY7cAYP0oVwujOHjJrIlZELjOARRZ/EWKS8MckRCKM5BrjPEFwEuniT7ynt6msyztpDIIlyZZm2ii7JZ7Xo/iWz1VPkkG49s9K2I5klzsYHHvXjEVtPphkVZSjjCnHHHc1a0Txjc2F/GsjF4GO0Z7DPWi7Qj2Apk5FRE1gX/AIljs7VZ1+YEAkA9jUemeMLLUY+G2uTjaetaJodjos03qc0yOZJAArA5HrT/AKVVxETjJIqKpz0qIpzxQx9BjdqikAxmpiOaY5UDLHFICAikBycelQTX0UQ65Y9AKW2O9N38R5NJjLKnBqTzKipc0rgakv3zUdSTH94ajqxoKeDTKUuAOTQJjiQBVaWWmzXGBwaoySk9D1pNiHyS5amZqME55p4INF9AFBwaz9WkUR7s1cJA5NY+psJQVB5rOb0KhuVLWS2mO2SVQxPGa0o7N0I2TBoz/DXMx6ZLJIShwQc11eiRgRhWB3dwfWs4LozWbtsyaOyeQbWUMvoRVuOxPlnJxx19KuwhUGD07U+Q4HbArS1jBtsypNOQYZwGZSeSKzdbuhbJheSO34Vs38xjhJAwCRzXDaxfG4vDbo3TIZuvsaTBGBbW7X2pTXEmDGoLHPc1raHpzSahJeMpwDhAR0rWh0pbbRwQgLHJbA59hWvp1itnp4cjLbRj60JAcnrUbRyTfMdzKFGOnqa5oJmSJsEFT8uOnHet3xLcE6gY8nCoe/8AEf8A61Q+H7aO+1KOJhkKAW9xnJoe4jXhtp7+1KgZUYVQR696dD4alsozNuzLztHTGa6TQRHc3t2kaqFRgCQP0rd+wiRzlep447VSQ72Oc0xLm2GXZiBgc10NveCQAMcN6Ur6cACuB1qq0SxuQMlgevqaaugvc0iQR1pnGMk1Vh80gBVPPU9hVmONQcM2+Q9B2FVcQqQM/PQHuaebO2JBkJY/WnyGUjAwPwquUZRlpAT6UrjFfR7SU7lUKfUVShszZySxM27DZBI7Vajm2TBV3e+OafckGQN3KjNC1CzKxTAzSVKehpuKYzQl/wBYajp8v+sNRvIEHWmJsHcAelVJZuuKZNcj1qm7knOaGGoskhc02gGjNZgLSEgc+lGaimOI2PfFO4EU1yHcop6dayJnV7ofOV5pY5JEJ7kmq88MpcOq5x1FZN3ZolY2Yo1yGyM9mFX7dwHHAB7HFZWnFtg3DgHDKRWkf3TAj5l7VSJe5oiRg4JHyt19jQZicg/QVX8wGMMrE56fWp4drAccnlaogbfR77YqThtpIrz+w0ySXWyGUkZLHI/hr0e5AIyDn5c1jk21ncyS/KGIAX8//r0NK4XLlnpp8iNWHHU0/U41t7DauenQda1LaRJEGCOFzmsTxHcGKFmH3QAMeuaYjzTX5QbyVmGTgEe3bmpfCsqQ/abpyAVTGf6Vm6/KDeTKpPzNjk5xTrUNb6Ufm/1rZ2jv6VHmUej/AA8LTx3crdGc4x+p/Ou6jhA5x1rjPh3btHpILDBZufoK7kEYwKpbEsq3MbBDtHzVUjtY4xmbl85NaxGQMioJIVcknNXYVyhJIv3VGQOuOlLGFPRwrexp8qeUCCOD7VXMSy5JA59R/WoZaQtwlzGhZSzL7YNUXuJDnduBHqKvDzIRgM209c/MKoXLsX2jBB9DkVnNtbGsUhltJLJdDMeUzyRV+4/1nTgVHY2xjy7ZGKdJknPrV07tXZM7X0I+9L+FKB6U/HvWhBLcyBHPNZk1115pt9efvGGf84rOMpL5P4UNgiZ5Gc5oD+9RA5pam4yXPuKM89RUJOBQHzSuBMT6VVmdidoNSvJgZNQR/vJumM0mA9LHcAx6nriphZAcrg46g1fhHyY4NK6YHcUrIVzP8kg/KF+nrTowwJRlbB5API/OrQjJcED8xTzGwIYArk846U7BcZCUI8sLg9gf8asSQmOPcudobdkVC8bRjcFBUckD+da0cYltgwIIK8+4qkJmVLc5jAHXaRiub1q1kwJwzZU4OO+eldJJb/6TtUYJOcD9affaaskZXBYdgPXtQ0F7CaKJZIck4ULjjvjj+lYfi+5eIMpOQASw9DjA/LJNdjplqLawjTBBC4OfWvNfiTePHerEm796u0YHX/J/pQ9ETfU4Zo2vtQLRsW3Hqa1RHG9tDAqsZNw4A5bn/GoREttJHFajdLtAc4zyef8AP4V1fhvw5KZIbmYH5mLAkccdKnco7vwxZGw02OI8sFAJA7+ldDGMYqhZx+Wir6DFXwcirSESZB6Uw4Apc/hTH6igLFW6jMoI5zVARNEeTJj1HNaUqeYpG4qfUdqxbm2uQ+Y53YH1OCKllw10ZY84gYVw2exqAoskgIXD1AZLj7siq/8AtEYNWbONjN83X0qG76GtkkWyfLg29z1quRVibk4x04qEjFbJaGPUYBjk06kpcUrsZhX0UsU5Eikeh7Hiquea7G6tklLKyhlPYisO90R0y1sdw/unqKGiEzNBOKdn3phDRnaylW7g0mTSKQ4mjPem5pCalgR3MoA5as6TVlt5gwY8HvU2oyFYSwGfrXJXTiaQ8MpHcHipb1KW1z0uxvlmiVlKtkdqtmbfxtrzPSbq5t3CgttY43Aniu2sZp5IR5gYnHUjGaadybGzHKp+UnBHrUySKVKsACfQ5rn769ltoSQ23A9M155qPinV7l5Ugu2hhVjucYXHsMVSYmrK56+XUckjI9RkH61o2JURlUb5TyF9K+f7PxlrWnPtttVnZM5KyAOP1rvvDHj5rx44NSjWC6YbkcDCyjvT2EtdDubeNTezE8FflFaghUg8dSKyYZVMxkHO7BHuMVsRPvH4UwaYECOM+mK8u8URNc6rJc3DARQgrHkdW6kfoBn3r1G5P7sjHJFcZeaGbyQLMx5fdkjqDyaTBGH4S8LtJeC5ulVlGSfdu5/pXcFIkmEUfBQBcAcVd07T0sbUKqgYGTj1qtbwia6kkUtgsSOOBTSsO5oRAYH0qcEAc1WlmS2j3MyqB3JqC2vJbqTcEZY+x9adxGh19qCPxojHbv6VKEFAinIGxxn6CojGD/Ft9zWgYlPNVbm2V0Khc1LKTMiYxxTA7wxPYVehj81w Line truncated
const buffer = Buffer.from(image, 'base64');
await utils.chatPanel.chatWithImages(
page,
[
{
name: 'image1.jpeg',
mimeType: 'image/jpeg',
buffer: buffer,
},
{
name: 'image2.jpeg',
mimeType: 'image/jpeg',
buffer: buffer,
},
],
'What is the small animal in the picture?'
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'What is the small animal in the picture?',
},
{
role: 'assistant',
status: 'success',
},
]);
});
});
@@ -0,0 +1,104 @@
import { loginUser } from '@affine-test/kit/utils/cloud';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIChatWith/Network', () => {
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 chat with network if network search enabled', async ({
page,
utils,
}) => {
await utils.chatPanel.enableNetworkSearch(page);
await utils.chatPanel.makeChat(
page,
'What is the weather like in Shanghai today?'
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'What is the weather like in Shanghai today?',
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { message } = await utils.chatPanel.getLatestAssistantMessage(page);
expect(
await message.locator('affine-footnote-node').count()
).toBeGreaterThan(0);
}).toPass({ timeout: 10000 });
});
test('should disable chat with image if network search enabled', async ({
page,
utils,
}) => {
await utils.chatPanel.enableNetworkSearch(page);
const isImageUploadEnabled =
await utils.chatPanel.isImageUploadEnabled(page);
expect(isImageUploadEnabled).toBe(false);
});
test('should prevent network search if disabled', async ({ page, utils }) => {
await utils.chatPanel.disableNetworkSearch(page);
await utils.chatPanel.makeChat(
page,
'What is the weather like in Shanghai today?'
);
await utils.chatPanel.waitForHistory(
page,
[
{
role: 'user',
content: 'What is the weather like in Shanghai today?',
},
{
role: 'assistant',
status: 'success',
},
],
20000
);
const { message } = await utils.chatPanel.getLatestAssistantMessage(page);
expect(await message.locator('affine-footnote-node').count()).toBe(0);
});
test('should disable network search when chating with image', async ({
page,
utils,
}) => {
const image =
'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 Line truncated
const buffer = Buffer.from(image, 'base64');
page.evaluate(() => {
delete window.showOpenFilePicker;
}, buffer);
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({
name: 'image1.jpeg',
mimeType: 'image/jpeg',
buffer: buffer,
});
await page.waitForSelector('chat-panel-input img');
const isNetworkSearchEnabled =
await utils.chatPanel.isNetworkSearchEnabled(page);
await expect(isNetworkSearchEnabled).toBe(false);
});
});
@@ -0,0 +1,3 @@
import { test } from '../base/base-test';
test.describe('AIChatWith/tags', () => {});
@@ -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
@@ -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 });
});
});
@@ -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 });
});
});
@@ -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();
});
});
@@ -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);
});
});
+3
View File
@@ -0,0 +1,3 @@
interface Window {
showOpenFilePicker?: () => Promise<FileSystemFileHandle[]>;
}
@@ -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';
}
}
@@ -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;
}
}
@@ -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);
}
}