snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-16 04:01:20 +05:00
commit 702cc698a7
9990 changed files with 1073618 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,53 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/BrainstormIdeasWithMindMap', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate a mind map for the selected content', async ({
loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: page,
utils,
}) => {
const { brainstormMindMap } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(page, 'Panda');
}
);
const { answer, responses } = await brainstormMindMap();
await expect(answer.locator('mini-mindmap-preview')).toBeVisible();
expect(responses).toEqual(new Set(['insert-below']));
});
});

View File

@@ -0,0 +1,87 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/ChangeTone', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support changing the tone of the selected content', async ({
loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: 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.skip('should show chat history in chat panel', async ({
loggedInPage: page,
utils,
}) => {
const { changeTone } = await utils.editor.askAIWithText(
page,
'AFFiNE is a great note-taking app'
);
const { answer } = await changeTone('informal');
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Change tone/);
await expect(actionName).toHaveText(/Change tone/);
});
});

View File

@@ -0,0 +1,55 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/CheckCodeError', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support check code error', async ({
loggedInPage: page,
utils,
}) => {
const { checkCodeError } = await utils.editor.askAIWithCode(
page,
'console.log("Hello,World!"',
'javascript'
);
const { answer, responses } = await checkCodeError();
const answerText = await answer.innerText();
expect(answerText).toMatch(/syntax|parenthesis|unexpected|missing/i);
expect(answerText).not.toMatch(/No syntax errors were found/i);
await expect(responses).toEqual(
new Set(['insert-below', 'replace-selection'])
);
});
test.skip('should show chat history in chat panel', async ({
loggedInPage: page,
utils,
}) => {
const { checkCodeError } = await utils.editor.askAIWithCode(
page,
'consloe.log("Hello,World!");',
'javascript'
);
const { answer } = await checkCodeError();
const insert = answer.getByTestId('answer-insert-below');
await insert.click();
await utils.chatPanel.waitForHistory(page, [{ role: 'action' }]);
const {
message,
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(
message.getByTestId('original-text').locator('affine-code')
).toBeVisible();
await expect(panelAnswer).toHaveText(/console/);
await expect(prompt).toHaveText(/Check the code error of the follow code/);
await expect(actionName).toHaveText(/Check code error/);
});
});

View File

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

View File

@@ -0,0 +1,92 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/ContinueWriting', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support continue writing the selected content', async ({
loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: 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.skip('should show chat history in chat panel', async ({
loggedInPage: page,
utils,
}) => {
const { continueWriting } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await continueWriting();
const insert = answer.getByTestId('answer-insert-below');
await insert.click();
await utils.chatPanel.waitForHistory(
page,
[
{
role: 'action',
},
],
10000
);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/,*/);
await expect(prompt).toHaveText(/Continue the following text/);
await expect(actionName).toHaveText(/Continue writing/);
});
});

View File

@@ -0,0 +1,38 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('expand mindmap node', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should expand the mindmap node', async ({
loggedInPage: 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: 60000 });
});
});

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -0,0 +1,146 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/FindActions', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should find actions for selected content', async ({
loggedInPage: 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/i,
/Enter Travel Details/i,
/Compare and Select Flights/i,
];
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 ({
loggedInPage: 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/i,
/Enter Travel Details/i,
/Compare and Select Flights/i,
];
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 ({
loggedInPage: 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/i,
/Enter Travel Details/i,
/Compare and Select Flights/i,
];
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.skip('should show chat history in chat panel', async ({
loggedInPage: 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/i,
/Enter Travel Details/i,
/Compare and Select Flights/i,
];
await Promise.all(
todos.map(async (todo, index) => {
await expect(
todo.locator('.affine-list-block__todo-prefix')
).toBeVisible();
await expect(todo).toHaveText(expectedTexts[index]);
})
);
await expect(prompt).toHaveText(/Find action items of the follow text/);
await expect(actionName).toHaveText(/Find action items from it/);
});
});

View File

@@ -0,0 +1,84 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/FixGrammar', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support fixing grammatical errors in the selected content', async ({
loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: 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.skip('should show chat history in chat panel', async ({
loggedInPage: page,
utils,
}) => {
const { fixGrammar } = await utils.editor.askAIWithText(
page,
'I is a student'
);
const { answer } = await fixGrammar();
await expect(answer).toHaveText(/I am a student/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/I am a student/);
await expect(prompt).toHaveText(
/Improve the grammar of the following text/
);
await expect(actionName).toHaveText(/Improve grammar for it/);
});
});

View File

@@ -0,0 +1,78 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/FixSpelling', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support fixing spelling errors in the selected content', async ({
loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: 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.skip('should show chat history in chat panel', async ({
loggedInPage: page,
utils,
}) => {
const { fixSpelling } = await utils.editor.askAIWithText(page, 'Appel');
const { answer } = await fixSpelling();
await expect(answer).toHaveText(/Apple/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/Apple/);
await expect(prompt).toHaveText(
/Correct the spelling of the following text/
);
await expect(actionName).toHaveText(/Fix spelling for it/);
});
});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,90 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/GenerateAnImageWithText', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate an image for the selected content', async ({
loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: 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 generate an image for the selected shape in edgeless', async ({
loggedInPage: page,
utils,
}) => {
const { generateImage } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createShape(page, 'HelloWorld');
}
);
const { answer, responses } = await generateImage();
await expect(answer.getByTestId('ai-answer-image')).toBeVisible();
expect(responses).toEqual(new Set(['insert-below']));
});
test.skip('should show chat history in chat panel', async ({
loggedInPage: 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 page.waitForSelector('.affine-image-container');
await page.reload();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const { answer: panelAnswer, actionName } =
await utils.chatPanel.getLatestAIActionMessage(page);
await expect(
panelAnswer.getByTestId('generated-image').locator('img')
).toBeVisible();
await expect(actionName).toHaveText(/image/);
});
});

View File

@@ -0,0 +1,108 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/GenerateHeadings', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate headings for selected content', async ({
loggedInPage: 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('should generate headings for selected text block in edgeless', async ({
loggedInPage: 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-below']));
});
test('should generate headings for selected note block in edgeless', async ({
loggedInPage: 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-below']));
});
test.skip('should show chat history in chat panel', async ({
loggedInPage: page,
utils,
}) => {
const { generateHeadings } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await generateHeadings();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await Promise.race([
panelAnswer.locator('h1').isVisible(),
panelAnswer.locator('h2').isVisible(),
panelAnswer.locator('h3').isVisible(),
]);
await expect(prompt).toHaveText(/Create headings of the follow text/);
await expect(actionName).toHaveText(/Create headings/);
});
});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,86 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/GenerateOutline', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate outline for selected content', async ({
loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: 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.skip('should show chat history in chat panel', async ({
loggedInPage: page,
utils,
}) => {
const { generateOutline } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await generateOutline();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Write an outline about this/);
await expect(actionName).toHaveText(/Write outline/);
});
});

View File

@@ -0,0 +1,61 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/GeneratePresentation', () => {
test.describe.configure({ timeout: 240000 });
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate a presentation for the selected content', async ({
loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: page,
utils,
}) => {
const { generatePresentation } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(
page,
'AFFiNE is a workspace with fully merged docs'
);
}
);
const { answer, responses } = await generatePresentation();
await expect(answer.locator('ai-slides-renderer')).toBeVisible();
expect(responses).toEqual(new Set(['insert-below']));
});
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,80 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/ImproveWriting', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support improving the writing of the selected content', async ({
loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: 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.skip('should show chat history in chat panel', async ({
loggedInPage: page,
utils,
}) => {
const { improveWriting } = await utils.editor.askAIWithText(
page,
'AFFiNE is so smart'
);
const { answer } = await improveWriting();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Improve the follow text/);
await expect(actionName).toHaveText(/Improve writing for it/);
});
});

View File

@@ -0,0 +1,86 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/MakeItLonger', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support making the selected content longer', async ({
loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: 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.skip('should show chat history in chat panel', async ({
loggedInPage: page,
utils,
}) => {
const { makeItLonger } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await makeItLonger();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Expand the following text/);
await expect(actionName).toHaveText(/Make it longer/);
});
});

View File

@@ -0,0 +1,91 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/MakeItReal', () => {
test.describe.configure({ timeout: 180000 });
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support making the selected content to real', async ({
loggedInPage: page,
utils,
}) => {
const { makeItReal } = await utils.editor.askAIWithText(page, 'Hello');
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 ({
loggedInPage: page,
utils,
}) => {
const { makeItReal } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(page, 'Hello');
}
);
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 ({
loggedInPage: page,
utils,
}) => {
const { makeItReal } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(page, 'Hello');
}
);
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 element to real in edgeless', async ({
loggedInPage: page,
utils,
}) => {
const { makeItReal } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createShape(page, 'HelloWorld');
}
);
const { answer, responses } = await makeItReal();
await expect(answer.locator('iframe')).toBeVisible({ timeout: 30000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test.skip('should show chat history in chat panel', async ({
loggedInPage: page,
utils,
}) => {
const { makeItReal } = await utils.editor.askAIWithText(page, 'Hello');
const { answer } = await makeItReal();
const insert = answer.getByTestId('answer-insert-below');
await insert.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer.locator('affine-code')).toBeVisible();
await expect(prompt).toHaveText(/Write a web page of follow text/);
await expect(actionName).toHaveText(/Make it real with text/);
});
});

View File

@@ -0,0 +1,86 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/MakeItShorter', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support making the selected content shorter', async ({
loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: 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.skip('should show chat history in chat panel', async ({
loggedInPage: page,
utils,
}) => {
const { makeItShorter } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await makeItShorter();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Shorten the follow text/);
await expect(actionName).toHaveText(/Make it shorter/);
});
});

View File

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

View File

@@ -0,0 +1,86 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/Summarize', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support summarizing the selected content', async ({
loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: 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.skip('should show chat history in chat panel', async ({
loggedInPage: page,
utils,
}) => {
const { summarize } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await summarize();
await expect(answer).toHaveText(/AFFiNE/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Summary the follow text/);
await expect(actionName).toHaveText(/Summary/);
});
});

View File

@@ -0,0 +1,74 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/Translate', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support translating the selected content', async ({
loggedInPage: page,
utils,
}) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer, responses } = await translate('Simplified Chinese');
await expect(answer).toHaveText(/苹果/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below', 'replace-selection']));
});
test('should support translating the selected text block in edgeless', async ({
loggedInPage: page,
utils,
}) => {
const { translate } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessText(page, 'Apple');
}
);
const { answer, responses } = await translate('Simplified Chinese');
await expect(answer).toHaveText(/苹果/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test('should support translating the selected note block in edgeless', async ({
loggedInPage: page,
utils,
}) => {
const { translate } = await utils.editor.askAIWithEdgeless(
page,
async () => {
await utils.editor.createEdgelessNote(page, 'Apple');
}
);
const { answer, responses } = await translate('Simplified Chinese');
await expect(answer).toHaveText(/苹果/, { timeout: 10000 });
expect(responses).toEqual(new Set(['insert-below']));
});
test.skip('should show chat history in chat panel', async ({
loggedInPage: page,
utils,
}) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('Simplified Chinese');
await expect(answer).toHaveText(/苹果/, { 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(/苹果/);
await expect(prompt).toHaveText(/Translate/);
await expect(actionName).toHaveText(/Translate/);
});
});

View File

@@ -0,0 +1,86 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/WriteAnArticleAboutThis', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate an article for the selected content', async ({
loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: 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.skip('should show chat history in chat panel', async ({
loggedInPage: page,
utils,
}) => {
const { writeArticle } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await writeArticle();
await expect(answer).toHaveText(/AFFiNE/);
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Write an article about this/);
await expect(actionName).toHaveText(/Write an article about this/);
});
});

View File

@@ -0,0 +1,86 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/WriteAnBlogPostAboutThis', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate an blog post for the selected content', async ({
loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: 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.skip('should show chat history in chat panel', async ({
loggedInPage: page,
utils,
}) => {
const { writeBlogPost } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await writeBlogPost();
await expect(answer).toHaveText(/AFFiNE/);
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Write a blog post about this/);
await expect(actionName).toHaveText(/Write a blog post about this/);
});
});

View File

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

View File

@@ -0,0 +1,86 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIAction/WriteAnTweetAboutThis', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should generate an tweet for the selected content', async ({
loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: 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.skip('should show chat history in chat panel', async ({
loggedInPage: page,
utils,
}) => {
const { writeTwitterPost } = await utils.editor.askAIWithText(
page,
'AFFiNE is a workspace with fully merged docs'
);
const { answer } = await writeTwitterPost();
await expect(answer).toHaveText(/AFFiNE/);
const replace = answer.getByTestId('answer-replace');
await replace.click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'action',
},
]);
const {
answer: panelAnswer,
prompt,
actionName,
} = await utils.chatPanel.getLatestAIActionMessage(page);
await expect(panelAnswer).toHaveText(/AFFiNE/);
await expect(prompt).toHaveText(/Write a twitter about this/);
await expect(actionName).toHaveText(/Write a twitter about this/);
});
});

View File

@@ -0,0 +1,40 @@
// oxlint-disable no-empty-pattern
import { test as base } from '@affine-test/kit/playwright';
import type { Page } from '@playwright/test';
import { ChatPanelUtils } from '../utils/chat-panel-utils';
import { EditorUtils } from '../utils/editor-utils';
import { SettingsPanelUtils } from '../utils/settings-panel-utils';
import { TestUtils } from '../utils/test-utils';
interface TestUtilsFixtures {
utils: {
testUtils: TestUtils;
chatPanel: typeof ChatPanelUtils;
editor: typeof EditorUtils;
settings: typeof SettingsPanelUtils;
};
loggedInPage: Page;
}
export const test = base.extend<TestUtilsFixtures>({
utils: async ({}, use) => {
const testUtils = TestUtils.getInstance();
await use({
testUtils,
chatPanel: ChatPanelUtils,
editor: EditorUtils,
settings: SettingsPanelUtils,
});
},
loggedInPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'storageState.json',
});
const page = await context.newPage();
await use(page);
await context.close();
},
});
export type TestFixtures = typeof test;

View File

@@ -0,0 +1,35 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIBasic/Authority', () => {
test.beforeEach(async ({ page, utils }) => {
// Sign out
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should show messages placeholder when no login', async ({ page }) => {
await expect(
page.getByTestId('chat-panel-messages-placeholder')
).toBeVisible();
});
test('should show error & login button when no login', async ({
page,
utils,
}) => {
await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.');
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. Answer in 50 words.');
const loginButton = page.getByTestId('ai-error-action-button');
await loginButton.click();
await expect(page.getByTestId('auth-modal')).toBeVisible();
});
});

View File

@@ -0,0 +1,536 @@
import { copyByKeyboard } from '@affine-test/kit/utils/keyboard';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIBasic/Chat', () => {
test.beforeEach(async ({ utils, loggedInPage: page }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should display empty state when no messages', async ({
loggedInPage: page,
}) => {
// Verify empty state UI
await expect(page.getByTestId('chat-panel-empty-state')).toBeVisible();
await expect(page.getByTestId('ai-onboarding')).toBeVisible();
});
// test('should open embedding settings when clicking check status button', async ({
// loggedInPage: page,
// utils,
// }) => {
// await utils.editor.createDoc(page, 'Doc 1', 'doc1');
// await utils.editor.createDoc(page, 'Doc 2', 'doc2');
// await utils.editor.createDoc(page, 'Doc 3', 'doc3');
// await utils.editor.createDoc(page, 'Doc 4', 'doc4');
// await utils.editor.createDoc(page, 'Doc 5', 'doc5');
// const check = await page.getByTestId(
// 'ai-chat-embedding-status-tooltip-check'
// );
// await expect(check).toBeVisible({ timeout: 50 * 1000 });
// await check.click();
// await expect(page.getByTestId('workspace-setting:embedding')).toBeVisible();
// });
test(`should send message and receive AI response:
- send message
- AI is loading
- AI generating
- AI success
`, async ({ loggedInPage: page, utils }) => {
// Type and send a message
await utils.chatPanel.makeChat(
page,
'Introduce AFFiNE to me. Answer in 500 words.'
);
if (!(await page.getByTestId('ai-loading').isVisible())) {
// AI is loading
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Introduce AFFiNE to me. Answer in 500 words.',
},
{
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. Answer in 500 words.',
},
{
role: 'assistant',
status: 'transmitting',
},
]);
await expect(page.getByTestId('ai-loading')).not.toBeVisible();
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Introduce AFFiNE to me. Answer in 500 words.',
},
{
role: 'assistant',
status: 'success',
},
]);
});
test('should support stop generating', async ({
loggedInPage: page,
utils,
}) => {
await utils.chatPanel.makeChat(
page,
'Introduce AFFiNE to me. Answer in 5000 words.'
);
// AI Generating
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Introduce AFFiNE to me. Answer in 5000 words.',
},
{
role: 'assistant',
status: 'transmitting',
},
]);
await page.getByTestId('chat-panel-stop').click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Introduce AFFiNE to me. Answer in 5000 words.',
},
{
role: 'assistant',
status: 'success',
},
]);
});
test('should render ai actions inline if the answer is the last one in the list, otherwise, nest them under the "More" menu', async ({
loggedInPage: page,
utils,
}) => {
await utils.chatPanel.makeChat(
page,
'Hello, how can you help me? Answer in 50 words.'
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello, how can you help me? Answer in 50 words.',
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(page.getByTestId('chat-action-list')).toBeVisible();
await utils.chatPanel.makeChat(
page,
'Nice to meet you. Answer in 50 words.'
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello, how can you help me? Answer in 50 words.',
},
{
role: 'assistant',
status: 'idle',
},
{
role: 'user',
content: 'Nice to meet you. Answer in 50 words.',
},
{
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 error when request failed', async ({
loggedInPage: page,
utils,
}) => {
// Simulate network error by disconnecting
await page.route('**/graphql', route => route.abort('failed'));
// Send a message that will fail
await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.');
await expect(page.getByTestId('ai-error')).toBeVisible();
await expect(page.getByTestId('action-retry-button')).toBeVisible();
});
test('should support retrying failed messages', async ({
loggedInPage: page,
utils,
}) => {
// Simulate network error by disconnecting
await page.route('**/graphql', route => route.abort('failed'));
// Send a message that will fail
await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.');
// Verify error state
await expect(page.getByTestId('ai-error')).toBeVisible();
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello. Answer in 50 words.',
},
{
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. Answer in 50 words.',
},
{
role: 'assistant',
status: 'success',
},
]);
});
test('should support retrying question', async ({
loggedInPage: page,
utils,
}) => {
await utils.chatPanel.makeChat(
page,
'Introduce Large Language Model. Answer in 50 words.'
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Introduce Large Language Model. Answer in 50 words.',
},
{
role: 'assistant',
status: 'success',
},
]);
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
await actions.retry();
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Introduce Large Language Model. Answer in 50 words.',
},
{
role: 'assistant',
status: 'success',
},
]);
});
test('should support sending message with button', async ({
loggedInPage: page,
utils,
}) => {
await utils.chatPanel.openChatPanel(page);
await utils.chatPanel.typeChat(page, 'Hello');
await page.getByTestId('chat-panel-send').click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello',
},
{
role: 'assistant',
status: 'loading',
},
]);
});
test('should support copying answer', async ({
loggedInPage: page,
utils,
}) => {
await utils.chatPanel.openChatPanel(page);
await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello. Answer in 50 words.',
},
{
role: 'assistant',
status: 'success',
},
]);
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
await actions.copy();
await page.getByText('Copied to clipboard').isVisible();
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
const clipboardText = await page.evaluate(() =>
navigator.clipboard.readText()
);
expect(clipboardText).toBe(content);
}).toPass({ timeout: 5000 });
});
test('should support copying selected answer content', async ({
loggedInPage: page,
utils,
}) => {
await utils.chatPanel.openChatPanel(page);
await utils.chatPanel.makeChat(
page,
'Help me write a two-line love poem. Answer in 50 words.'
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Help me write a two-line love poem. Answer in 50 words.',
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { content, message } =
await utils.chatPanel.getLatestAssistantMessage(page);
// Select multiple rich text
const firstParagraph = await message
.locator('affine-paragraph rich-text v-text')
.first()
.boundingBox();
const lastParagraph = await message
.locator('affine-paragraph rich-text v-text')
.last()
.boundingBox();
if (firstParagraph && lastParagraph) {
await page.mouse.move(firstParagraph.x, firstParagraph.y);
await page.mouse.down();
await page.mouse.move(
lastParagraph.x + lastParagraph.width,
lastParagraph.y + lastParagraph.height
);
await page.mouse.up();
}
await copyByKeyboard(page);
const clipboardText = await page.evaluate(() =>
navigator.clipboard.readText()
);
expect(clipboardText).toBe(content);
}).toPass({ timeout: 5000 });
});
test('chat with ask ai input in page mode', async ({
loggedInPage: page,
utils,
}) => {
await utils.chatPanel.closeChatPanel(page);
await utils.editor.askAIWithText(
page,
'AFFiNE is an open source all in one workspace.'
);
await page.keyboard.type('Translate to chinese.');
const sendButton = await page.getByTestId('ai-panel-input-send');
await expect(sendButton).toHaveAttribute('data-active', 'true');
await sendButton.click();
await expect(page.getByTestId('sidebar-tab-content-chat')).toBeVisible();
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Translate to chinese.',
},
{
role: 'assistant',
status: 'success',
},
]);
});
test('chat with ask ai input in edgeless mode', async ({
loggedInPage: page,
utils,
}) => {
await utils.chatPanel.closeChatPanel(page);
await utils.editor.askAIWithEdgeless(page, async () => {
await utils.editor.createShape(page, 'HelloWorld');
});
await page.waitForTimeout(1000);
await page.keyboard.type('What color is it? Answer in 50 words.');
await page.waitForTimeout(1000);
const sendButton = await page.getByTestId('ai-panel-input-send');
await expect(sendButton).toHaveAttribute('data-active', 'true');
await sendButton.click();
await expect(page.getByTestId('sidebar-tab-content-chat')).toBeVisible();
await expect(page.locator('chat-content-images')).toBeVisible();
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'What color is it? Answer in 50 words.',
},
{
role: 'assistant',
status: 'success',
},
]);
});
test('should support chat with ask ai input in edgeless mode when nothing selected', async ({
loggedInPage: page,
utils,
}) => {
await utils.chatPanel.closeChatPanel(page);
await utils.editor.switchToEdgelessMode(page);
await utils.editor.removeAll(page);
await page.mouse.move(300, 300);
await page.mouse.down({ button: 'right' });
await page.mouse.move(350, 350);
await page.mouse.up({ button: 'right' });
await utils.chatPanel.openChatPanel(page);
await utils.chatPanel.makeChat(page, 'Who are you? Answer in 50 words.');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Who are you? Answer in 50 words.',
},
{
role: 'assistant',
status: 'success',
},
]);
});
test('should support create a new chat after ask ai', async ({
loggedInPage: page,
utils,
}) => {
await utils.chatPanel.closeChatPanel(page);
await utils.editor.askAIWithText(
page,
'AFFiNE is an open source all in one workspace.'
);
await page.keyboard.type('Translate to chinese.');
const sendButton = await page.getByTestId('ai-panel-input-send');
await expect(sendButton).toHaveAttribute('data-active', 'true');
await sendButton.click();
await expect(page.getByTestId('sidebar-tab-content-chat')).toBeVisible();
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Translate to chinese.',
},
{
role: 'assistant',
status: 'success',
},
]);
await page.getByTestId('ai-panel-new-chat').click();
await page.waitForTimeout(1000);
await utils.chatPanel.expectToHaveHistory(page, []);
});
test('should support pin chat', async ({ loggedInPage: page, utils }) => {
await utils.chatPanel.openChatPanel(page);
await utils.chatPanel.makeChat(
page,
'Hello, how can you help me? Answer in 50 words.'
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello, how can you help me? Answer in 50 words.',
},
{
role: 'assistant',
status: 'success',
},
]);
// pinned
await expect(page.getByTestId('ai-panel-pin-chat')).toHaveAttribute(
'data-pinned',
'false'
);
await page.getByTestId('ai-panel-pin-chat').click();
await expect(page.getByTestId('ai-panel-pin-chat')).toHaveAttribute(
'data-pinned',
'true'
);
// create new doc
await utils.editor.createDoc(page, 'Doc 1', 'doc1');
await utils.chatPanel.expectToHaveHistory(page, [
{
role: 'user',
content: 'Hello, how can you help me? Answer in 50 words.',
},
{
role: 'assistant',
status: 'idle',
},
]);
await page.getByTestId('ai-panel-pin-chat').click();
// unpinned
await expect(page.getByTestId('ai-panel-pin-chat')).toHaveAttribute(
'data-pinned',
'false'
);
await utils.editor.createDoc(page, 'Doc 2', 'doc2');
await utils.chatPanel.expectToHaveHistory(page, []);
});
});

View File

@@ -0,0 +1,71 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIBasic/Guidance', () => {
test.beforeEach(async ({ page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
});
test('should show AI panel when space is pressed on empty paragraph', async ({
page,
utils,
}) => {
await utils.editor.focusToEditor(page);
await page.keyboard.press('Space');
await expect(page.locator('affine-ai-panel-widget')).toBeVisible();
});
test('should not show AI panel when space is pressed on non-empty paragraph', async ({
page,
utils,
}) => {
await utils.editor.focusToEditor(page);
await page.keyboard.insertText('HelloWorld');
await page.keyboard.press('Space');
await expect(page.locator('affine-ai-panel-widget')).not.toBeVisible();
});
test('should not show AI panel when space is pressed on non-paragraph block', async ({
page,
utils,
}) => {
await utils.editor.focusToEditor(page);
await page.keyboard.insertText('```js');
await page.keyboard.press('Enter');
await expect(page.locator('affine-ai-panel-widget')).not.toBeVisible();
});
test('should hide AI panel and insert space back to editor when space is pressed on empty input', async ({
page,
utils,
}) => {
await utils.editor.focusToEditor(page);
await page.keyboard.press('Space');
await expect(page.locator('affine-ai-panel-widget')).toBeVisible();
await page.keyboard.press('Space');
await expect(page.locator('affine-ai-panel-widget')).not.toBeVisible();
await expect(async () => {
const content = await utils.editor.getEditorContent(page, false);
expect(content).toBe(' ');
}).toPass({ timeout: 5000 });
});
test('should support text with space in ai panel input', async ({
page,
utils,
}) => {
await utils.editor.focusToEditor(page);
await page.keyboard.press('Space');
await expect(page.locator('affine-ai-panel-widget')).toBeVisible();
await page.keyboard.insertText('Hello');
await page.keyboard.press('Space');
await page.keyboard.insertText('World');
await expect(async () => {
const input = await page.locator('ai-panel-input textarea');
expect(await input.inputValue()).toBe('Hello World');
}).toPass({ timeout: 5000 });
});
});

View File

@@ -0,0 +1,78 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe.configure({ mode: 'parallel' });
test.describe('AIBasic/Onboarding', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should show AI onboarding', async ({ loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: 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 ({
loggedInPage: 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 ({ loggedInPage: 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 ({ loggedInPage: page, utils }) => {
await page.getByTestId('freely-communicate-with-ai').click();
await utils.editor.isEdgelessMode(page);
const docTitle = await utils.editor.getDocTitle(page);
await expect(docTitle).toContain('Freely communicate');
});
});

View File

@@ -0,0 +1,117 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe.configure({ mode: 'serial' });
test.describe('AIChatWith/Attachments', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('support chat with attachment', async ({
loggedInPage: page,
utils,
}) => {
const textContent = 'AttachmentEEee is a cute cat';
const buffer = Buffer.from(textContent);
await utils.chatPanel.chatWithAttachments(
page,
[
{
name: 'test.txt',
mimeType: 'text/plain',
buffer: buffer,
},
],
'What is AttachmentEEee? Answer with a citation.'
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'What is AttachmentEEee? Answer with a citation.',
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { content, message } =
await utils.chatPanel.getLatestAssistantMessage(page);
expect(content).toMatch(/EEee/);
const references = await message
.locator('affine-footnote-node')
.evaluateAll(nodes =>
nodes.map(
node =>
(
node as HTMLElement & {
footnote?: { reference?: Record<string, unknown> };
}
).footnote?.reference
)
);
expect(references).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'attachment',
artifactId: expect.any(String),
}),
])
);
}).toPass({ timeout: 10000 });
});
test('support chat with multiple attachments', async ({
loggedInPage: page,
utils,
}) => {
const textContent1 = 'This attachment describes a cute cat.';
const textContent2 = 'This attachment describes a cute dog.';
const buffer1 = Buffer.from(textContent1);
const buffer2 = Buffer.from(textContent2);
const firstName = 'cat-document.txt';
const secondName = 'dog-document.txt';
await utils.chatPanel.chatWithAttachments(
page,
[
{
name: firstName,
mimeType: 'text/plain',
buffer: buffer1,
},
{
name: secondName,
mimeType: 'text/plain',
buffer: buffer2,
},
],
`Which animal is described in ${firstName} and which animal is described in ${secondName}? Answer with both attachment names.`
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: `Which animal is described in ${firstName} and which animal is described in ${secondName}? Answer with both attachment names.`,
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
expect(content).toContain(firstName);
expect(content).toContain(secondName);
expect(content).toMatch(/cat/i);
expect(content).toMatch(/dog/i);
}).toPass({ timeout: 20000 });
});
});

View File

@@ -0,0 +1,124 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe.configure({ mode: 'serial' });
test.describe('AIChatWith/Collections', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
await utils.editor.clearAllCollections(page);
await utils.testUtils.createNewPage(page);
});
test.afterEach(async ({ loggedInPage: page, utils }) => {
// clear all collections
await utils.editor.clearAllCollections(page);
});
test('should support chat with collection', async ({
loggedInPage: page,
utils,
}) => {
const randomStr = Math.random().toString(36).substring(2, 6);
// Create two collections
await utils.editor.createCollectionAndDoc(
page,
'Collection 1',
`Collection${randomStr} is a cute dog`
);
await utils.chatPanel.chatWithCollections(page, ['Collection 1']);
await utils.settings.openSettingsPanel(page);
await utils.settings.disableWorkspaceEmbedding(page);
await utils.settings.closeSettingsPanel(page);
await utils.chatPanel.makeChat(
page,
`What is Collection${randomStr}? Use English and answer with a citation.`
);
await expect(
page.getByText(
'One or more selected sources are unavailable. Check the sources or workspace AI indexing settings, then retry.'
)
).toBeVisible();
expect(
(await utils.chatPanel.collectHistory(page)).filter(
message => message.role === 'user'
)
).toHaveLength(1);
await utils.settings.openSettingsPanel(page);
await utils.settings.enableWorkspaceEmbedding(page);
await utils.settings.closeSettingsPanel(page);
await page.getByTestId('ai-error-action-button').click();
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: `What is Collection${randomStr}? Use English and answer with a citation.`,
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { content, message } =
await utils.chatPanel.getLatestAssistantMessage(page);
expect(content).toMatch(new RegExp(`Collection${randomStr}.*dog`));
expect(
await message.locator('affine-footnote-node').count()
).toBeGreaterThanOrEqual(1);
}).toPass();
});
test('should support chat with multiple collections', async ({
loggedInPage: page,
utils,
}) => {
const randomStr1 = Math.random().toString(36).substring(2, 6);
const randomStr2 = Math.random().toString(36).substring(2, 6);
// Create two collections
await utils.editor.createCollectionAndDoc(
page,
'Collection 2',
`Collection${randomStr1} is a cute cat`
);
await utils.editor.createCollectionAndDoc(
page,
'Collection 3',
`Collection${randomStr2} is a cute dog`
);
await utils.chatPanel.chatWithCollections(page, [
'Collection 2',
'Collection 3',
]);
await utils.chatPanel.makeChat(
page,
`What is Collection${randomStr1}? What is Collection${randomStr2}? Use English and cite both sources.`
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: `What is Collection${randomStr1}? What is Collection${randomStr2}? Use English and cite both sources.`,
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { content, message } =
await utils.chatPanel.getLatestAssistantMessage(page);
expect(content).toMatch(new RegExp(`Collection${randomStr1}.*cat`));
expect(content).toMatch(new RegExp(`Collection${randomStr2}.*dog`));
expect(
await message.locator('affine-footnote-node').count()
).toBeGreaterThanOrEqual(2);
}).toPass();
});
});

View File

@@ -0,0 +1,89 @@
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 ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('support chat with specified doc', async ({
loggedInPage: page,
utils,
}) => {
// Initialize the doc
await focusDocTitle(page);
await page.keyboard.insertText('Test Doc');
await page.keyboard.press('Enter');
await page.keyboard.insertText('DocEEee is a cute cat');
await utils.editor.waitForCurrentDocSynced(page);
await utils.chatPanel.chatWithDoc(page, 'Test Doc');
await utils.chatPanel.makeChat(page, 'What is DocEEee?');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'What is DocEEee?',
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
expect(content).toMatch(/DocEEee/);
}).toPass({ timeout: 10000 });
});
// FIXME: This test is flaky, need to fix it.
test.skip('support chat with specified docs', async ({
loggedInPage: page,
utils,
}) => {
// Initialize the doc 1
await focusDocTitle(page);
await page.keyboard.insertText('Test Doc1');
await page.keyboard.press('Enter');
await page.keyboard.insertText('DocEEee is a cute cat');
await utils.editor.waitForCurrentDocSynced(page);
// 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('DocFFff is a cute dog');
await utils.editor.waitForCurrentDocSynced(page);
await utils.chatPanel.chatWithDoc(page, 'Test Doc1');
await utils.chatPanel.chatWithDoc(page, 'Test Doc2');
await utils.chatPanel.makeChat(page, 'What is DocEEee? What is DocFFff?');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'What is DocEEee? What is DocFFff?',
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
expect(content).toMatch(/DocEEee/);
expect(content).toMatch(/DocFFff/);
}).toPass({ timeout: 10000 });
});
});

View File

@@ -0,0 +1,181 @@
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';
type MindmapSnapshot = {
childCount: number;
count: number;
id: string | null;
};
test.describe('AIChatWith/EdgelessMindMap', () => {
test.describe.configure({ timeout: 180000 });
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should preview the regenerated mindmap before replacing it', async ({
loggedInPage: page,
utils,
}) => {
let id: string;
let originalChildCount: number;
const { regenerateMindMap } = await utils.editor.askAIWithEdgeless(
page,
async () => {
id = await utils.editor.createMindmap(page);
originalChildCount = await page.evaluate(mindmapId => {
const edgelessBlock = document.querySelector(
'affine-edgeless-root'
) as EdgelessRootBlockComponent;
const mindmap = edgelessBlock.gfx.getElementById(mindmapId) as {
tree: { children?: unknown[] };
} | null;
return mindmap?.tree.children?.length ?? 0;
}, id);
},
async () => {
const { id: rootId } = await utils.editor.getMindMapNode(
page,
id!,
[0]
);
await utils.editor.selectElementInEdgeless(page, [rootId]);
}
);
const { answer, responses } = await regenerateMindMap();
expect(responses).toEqual(new Set(['replace-selection']));
await expect
.poll(
async () => {
return answer
.locator('mini-mindmap-preview')
.evaluate(async preview => {
const walk = (root: ParentNode): Element[] => {
const results: Element[] = [];
for (const element of root.querySelectorAll('*')) {
results.push(element);
if (element.shadowRoot) {
results.push(...walk(element.shadowRoot));
}
}
return results;
};
await customElements.whenDefined('mini-mindmap-preview');
const previewElement =
preview instanceof HTMLElement
? (preview as HTMLElement & {
updateComplete?: Promise<unknown>;
})
: null;
await previewElement?.updateComplete;
await new Promise(resolve =>
requestAnimationFrame(() => resolve(null))
);
const shadowRoot = previewElement?.shadowRoot ?? null;
const descendants = walk(shadowRoot ?? preview);
const surface = descendants.find(
element =>
element instanceof HTMLElement &&
element.classList.contains('affine-mini-mindmap-surface')
) as HTMLElement | undefined;
const surfaceRect = surface?.getBoundingClientRect();
return {
hasShadowRoot: !!shadowRoot,
hasRootBlock: descendants.some(
element =>
element.tagName.toLowerCase() === 'mini-mindmap-root-block'
),
hasSurfaceBlock: descendants.some(
element =>
element.tagName.toLowerCase() ===
'mini-mindmap-surface-block'
),
surfaceReady:
!!surface &&
(surfaceRect?.width ?? 0) > 0 &&
(surfaceRect?.height ?? 0) > 0,
};
});
},
{ timeout: 15_000 }
)
.toEqual({
hasShadowRoot: true,
hasRootBlock: true,
hasSurfaceBlock: true,
surfaceReady: true,
});
const replace = answer.getByTestId('answer-replace');
await expect(replace).toBeVisible();
await replace.click({ force: true });
await expect
.poll(
async () => {
return page.evaluate<MindmapSnapshot>(() => {
const edgelessBlock = document.querySelector(
'affine-edgeless-root'
) as EdgelessRootBlockComponent;
const mindmaps = edgelessBlock?.gfx.gfxElements.filter(
(el: GfxModel) => 'type' in el && el.type === 'mindmap'
) as unknown as Array<{
id: string;
tree: {
children?: unknown[];
element: { text?: { toString(): string } };
};
}>;
const mindmap = mindmaps?.[0];
return {
count: mindmaps?.length ?? 0,
id: mindmap?.id ?? null,
childCount: mindmap?.tree.children?.length ?? 0,
};
});
},
{ timeout: 15_000 }
)
.toMatchObject({
count: 1,
});
const replacedMindmap = await page.evaluate<MindmapSnapshot>(() => {
const edgelessBlock = document.querySelector(
'affine-edgeless-root'
) as EdgelessRootBlockComponent;
const mindmaps = edgelessBlock?.gfx.gfxElements.filter(
(el: GfxModel) => 'type' in el && el.type === 'mindmap'
) as unknown as Array<{
id: string;
tree: {
children?: unknown[];
element: { text?: { toString(): string } };
};
}>;
const mindmap = mindmaps?.[0];
return {
count: mindmaps?.length ?? 0,
id: mindmap?.id ?? null,
childCount: mindmap?.tree.children?.length ?? 0,
};
});
expect(replacedMindmap.childCount).toBeGreaterThan(originalChildCount!);
expect(replacedMindmap.childCount).toBeGreaterThan(0);
});
});

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,28 @@
import { test } from '../base/base-test';
test.describe('AIChatWith/Network', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test.skip('should support chat with network if network search enabled', async ({
loggedInPage: page,
utils,
}) => {
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',
},
]);
});
});

View File

@@ -0,0 +1,30 @@
import { test } from '../base/base-test';
test.describe('AIChatWith/Reasoning', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test.skip('should support chat with reasoning', async ({
loggedInPage: page,
utils,
}) => {
await utils.chatPanel.enableReasoning(page);
await utils.chatPanel.makeChat(
page,
'How do you measure exactly 4 liters of water using a jug that only holds 3 and 5 liters?'
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content:
'How do you measure exactly 4 liters of water using a jug that only holds 3 and 5 liters?',
},
{
role: 'assistant',
status: 'success',
},
]);
});
});

View File

@@ -0,0 +1,93 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe.configure({ mode: 'serial' });
test.describe('AIChatWith/tags', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
await utils.editor.clearAllTags(page);
await utils.testUtils.createNewPage(page);
});
test.afterEach(async ({ loggedInPage: page, utils }) => {
await utils.editor.clearAllTags(page);
});
test.skip('should support chat with tag', async ({
loggedInPage: page,
utils,
}) => {
const randomStr = Math.random().toString(36).substring(2, 6);
await utils.editor.createTagAndDoc(
page,
'Tag 1',
`Tag${randomStr} is a cute cat`
);
await utils.chatPanel.chatWithTags(page, ['Tag 1']);
await utils.chatPanel.makeChat(
page,
`What is Tag${randomStr}(Use English)`
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: `What is Tag${randomStr}(Use English)`,
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { content, message } =
await utils.chatPanel.getLatestAssistantMessage(page);
expect(content).toMatch(new RegExp(`Tag${randomStr}.*cat`));
await expect(message.locator('affine-footnote-node')).toHaveCount(1);
}).toPass();
});
// FIXME: This test is flaky, need to fix it.
test.skip('should support chat with multiple tags', async ({
loggedInPage: page,
utils,
}) => {
const randomStr1 = Math.random().toString(36).substring(2, 6);
const randomStr2 = Math.random().toString(36).substring(2, 6);
await utils.editor.createTagAndDoc(
page,
'Tag 2',
`Tag${randomStr1} is a cute cat`
);
await utils.editor.createTagAndDoc(
page,
'Tag 3',
`Tag${randomStr2} is a cute dog`
);
await utils.chatPanel.chatWithTags(page, ['Tag 2', 'Tag 3']);
await utils.chatPanel.makeChat(
page,
`What is Tag${randomStr1}? What is Tag${randomStr2}?(Use English)`
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: `What is Tag${randomStr1}? What is Tag${randomStr2}?(Use English)`,
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { content, message } =
await utils.chatPanel.getLatestAssistantMessage(page);
expect(content).toMatch(new RegExp(`Tag${randomStr1}.*cat`));
expect(content).toMatch(new RegExp(`Tag${randomStr2}.*dog`));
await expect(message.locator('affine-footnote-node')).toHaveCount(2);
}).toPass();
});
});

View File

@@ -0,0 +1,181 @@
import { IS_MAC } from '@blocksuite/global/env';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIChatWith/Text', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should support stop generating', async ({
loggedInPage: 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 stop generating when click outside', async ({
loggedInPage: page,
utils,
}) => {
await utils.editor.askAIWithText(page, 'Panda');
await page.getByTestId('action-generate-image').click();
await expect(page.getByTestId('ai-generating')).toBeVisible();
await page.mouse.click(0, 0);
await expect(
page.getByText('AI is generating content. Do you want to stop generating')
).toBeVisible();
await page.getByTestId('confirm-modal-confirm').click();
await expect(page.getByTestId('ai-generating')).not.toBeVisible();
});
test('should support copy answer', async ({ loggedInPage: page, utils }) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('Simplified Chinese');
await expect(answer).toHaveText(/苹果/, { 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('苹果');
});
test('should support insert below', async ({ loggedInPage: page, utils }) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('Simplified Chinese');
await expect(answer).toHaveText(/苹果/, { timeout: 10000 });
const insertBelow = answer.getByTestId('answer-insert-below');
await insertBelow.click();
const content = await utils.editor.getEditorContent(page);
expect(content).toBe('Apple\n苹果');
});
test('should support insert above', async ({ loggedInPage: 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 ({
loggedInPage: page,
utils,
}) => {
await utils.editor.focusToEditor(page);
await page.keyboard.insertText('I Loev Apple');
// Select the word "Loev"
const SHORT_KEY = IS_MAC ? 'Alt' : 'Control';
await page.keyboard.press(`${SHORT_KEY}+ArrowLeft`);
await page.keyboard.press('ArrowLeft');
await page.keyboard.press(`Shift+${SHORT_KEY}+ArrowLeft`);
const { fixSpelling } = await utils.editor.showAIMenu(page);
const { answer } = await fixSpelling();
await expect(answer).toHaveText(/Love/, { timeout: 10000 });
const replace = answer.getByTestId('answer-replace');
await replace.click();
const content = await utils.editor.getEditorContent(page);
expect(content).toBe('I Love Apple');
});
test('should support continue in chat', async ({
loggedInPage: page,
utils,
}) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('Simplified Chinese');
await expect(answer).toHaveText(/苹果/, { 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 ({ loggedInPage: page, utils }) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('Simplified Chinese');
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 ({
loggedInPage: 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 ({
loggedInPage: 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 ({ loggedInPage: page, utils }) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('Simplified Chinese');
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 ({
loggedInPage: page,
utils,
}) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('Simplified Chinese');
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');
});
test('should focus on textarea', async ({ loggedInPage: page, utils }) => {
await utils.editor.askAIWithText(page, 'Apple');
const textarea = await utils.editor.whatAreYourThoughts(page, 'Coffee');
await expect(textarea).toBeFocused();
const value = await textarea.inputValue();
expect(value).toBe('Coffee');
});
});

View File

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

View File

@@ -0,0 +1,198 @@
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 ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should insert content below selected block in page mode', async ({
loggedInPage: 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. Answer in 50 words.');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello. Answer in 50 words.',
},
{
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 ({
loggedInPage: 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. Answer in 50 words.');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello. Answer in 50 words.',
},
{
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 ({
loggedInPage: 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. Answer in 50 words.');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello. Answer in 50 words.',
},
{
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 ({
loggedInPage: 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. Answer in 50 words.');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello. Answer in 50 words.',
},
{
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 ({
loggedInPage: page,
utils,
}) => {
await utils.editor.switchToEdgelessMode(page);
// Delete default note
await (await page.waitForSelector('affine-edgeless-note')).click();
await page.keyboard.press('Delete');
await utils.chatPanel.openChatPanel(page);
await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello. Answer in 50 words.',
},
{
role: 'assistant',
status: 'success',
},
]);
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
await actions.insert();
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
const noteContent = await utils.editor.getNoteContent(page);
expect(noteContent).toBe(content);
}).toPass({ timeout: 5000 });
});
});

View File

@@ -0,0 +1,69 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIInsertion/SaveAsBlock', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should save content as a chat block in page mode', async ({
loggedInPage: page,
utils,
}) => {
await utils.chatPanel.openChatPanel(page);
await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello. Answer in 50 words.',
},
{
role: 'assistant',
status: 'success',
},
]);
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
await actions.saveAsBlock();
await page.getByText('Successfully saved chat to a block').waitFor();
await utils.editor.isEdgelessMode(page);
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 ({
loggedInPage: page,
utils,
}) => {
await utils.editor.switchToEdgelessMode(page);
await utils.chatPanel.openChatPanel(page);
await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello. Answer in 50 words.',
},
{
role: 'assistant',
status: 'success',
},
]);
const { actions } = await utils.chatPanel.getLatestAssistantMessage(page);
await actions.saveAsBlock();
await page.getByText('Successfully saved chat to a block');
// Verify the ai block is created
await page.waitForSelector('affine-edgeless-ai-chat');
const aiBlock = await page.locator('affine-edgeless-ai-chat');
await expect(aiBlock).toBeVisible();
});
});

View File

@@ -0,0 +1,75 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIInsertion/SaveAsDoc', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test('should save content as a doc in page mode', async ({
loggedInPage: page,
utils,
}) => {
await utils.chatPanel.openChatPanel(page);
await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello. Answer in 50 words.',
},
{
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').waitFor({ state: 'visible' });
// 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 ({
loggedInPage: page,
utils,
}) => {
await utils.editor.switchToEdgelessMode(page);
await utils.chatPanel.openChatPanel(page);
await utils.chatPanel.makeChat(page, 'Hello. Answer in 50 words.');
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'Hello. Answer in 50 words.',
},
{
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').waitFor({ state: 'visible' });
// Switch to page mode
await utils.editor.isPageMode(page);
// Verify the ai block is created
const editorContent = await utils.editor.getEditorContent(page);
expect(editorContent).toBe(content);
});
});

View File

@@ -0,0 +1,496 @@
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe.configure({ mode: 'serial' });
test.describe('AISettings/Embedding', () => {
test.beforeEach(async ({ loggedInPage: page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
await utils.chatPanel.openChatPanel(page);
});
test.afterEach(async ({ loggedInPage: page, utils }) => {
await utils.settings.openSettingsPanel(page);
await utils.settings.clearAllIgnoredDocs(page);
await utils.settings.removeAllAttachments(page);
await utils.settings.closeSettingsPanel(page);
});
test('should show workspace embedding enabled status', async ({
loggedInPage: page,
utils,
}) => {
await utils.settings.openSettingsPanel(page);
await utils.settings.waitForWorkspaceEmbeddingSwitchToBe(page, true);
});
test('should support disable workspace embedding', async ({
loggedInPage: page,
utils,
}) => {
await utils.settings.openSettingsPanel(page);
await utils.settings.enableWorkspaceEmbedding(page);
await utils.settings.disableWorkspaceEmbedding(page);
await utils.settings.waitForWorkspaceEmbeddingSwitchToBe(page, false);
});
test('should support enable workspace embedding', async ({
loggedInPage: page,
utils,
}) => {
await utils.settings.openSettingsPanel(page);
await utils.settings.disableWorkspaceEmbedding(page);
await utils.settings.enableWorkspaceEmbedding(page);
await utils.settings.waitForWorkspaceEmbeddingSwitchToBe(page, true);
});
test('should show enable cloud panel if workspace is local', async ({
loggedInPage: page,
utils,
}) => {
await utils.settings.closeSettingsPanel(page);
await createLocalWorkspace({ name: 'test' }, page);
await utils.settings.openSettingsPanel(page);
await expect(
page.getByTestId('publish-enable-affine-cloud-button')
).toBeVisible();
});
test('should disable embedding settings if the user is not workspace owner', async ({
loggedInPage: page,
utils,
}) => {
// mock the features to be empty(without CopilotEmbedding)
await page.route('**/graphql', async (route, request) => {
const postData = request.postData();
if (postData && postData.includes('serverConfig')) {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
data: {
serverConfig: {
version: '1.0.0',
baseUrl: 'http://localhost:8080',
name: 'AFFiNE',
features: [],
type: 'cloud',
initialized: true,
credentialsRequirement: null,
},
},
}),
});
} else {
await route.continue();
}
});
await page.reload();
await utils.settings.openSettingsPanel(page);
const wrapper = await page.getByTestId(
'workspace-embedding-setting-wrapper'
);
await expect(wrapper).toHaveAttribute('aria-disabled', 'true');
});
test('should show error message if enable workspace embedding failed', async ({
loggedInPage: page,
utils,
}) => {
await utils.settings.openSettingsPanel(page);
await utils.settings.enableWorkspaceEmbedding(page);
await utils.settings.disableWorkspaceEmbedding(page);
await utils.settings.waitForWorkspaceEmbeddingSwitchToBe(page, false);
await page.context().setOffline(true);
await utils.settings.enableWorkspaceEmbedding(page, false);
await expect(
page.getByText(/Failed to update workspace doc embedding enabled/i)
).toBeVisible();
await page.context().setOffline(false);
});
test('should show embedding progress', async ({
loggedInPage: page,
utils,
}) => {
await utils.settings.openSettingsPanel(page);
await utils.settings.enableWorkspaceEmbedding(page);
await page.getByTestId('embedding-progress-wrapper');
const progress = await page.getByTestId('embedding-progress');
const title = await page.getByTestId('embedding-progress-title');
await expect(title).not.toHaveAttribute('data-progress', 'loading');
const count = await page.getByTestId('embedding-progress-count');
await expect(count).toHaveText(/\d+\/\d+/);
await expect(progress).toBeVisible();
});
test('should allow manual attachment upload for embedding', async ({
loggedInPage: page,
utils,
}) => {
await createLocalWorkspace({ name: 'test' }, page, false, 'affine-cloud');
await utils.settings.openSettingsPanel(page);
await utils.settings.enableWorkspaceEmbedding(page);
const randomStr1 = Math.random().toString(36).substring(2, 6);
const randomStr2 = Math.random().toString(36).substring(2, 6);
const textContent1 = `Workspace${randomStr1} is a cute cat`;
const textContent2 = `Workspace${randomStr2} is a cute dog`;
const buffer1 = Buffer.from(textContent1);
const buffer2 = Buffer.from(textContent2);
const attachments = [
{
name: 'document1.txt',
mimeType: 'text/plain',
buffer: buffer1,
},
{
name: 'document2.txt',
mimeType: 'text/plain',
buffer: buffer2,
},
];
await utils.settings.uploadWorkspaceEmbedding(page, attachments);
await utils.settings.waitForFileEmbeddingReadiness(page, 2);
await utils.settings.closeSettingsPanel(page);
const query = `Use semantic search across workspace and attached files, then tell me whether Workspace${randomStr1} is a cat or dog and whether Workspace${randomStr2} is a cat or dog. Answer with citations.`;
await utils.chatPanel.makeChat(page, query);
await utils.chatPanel.waitForHistory(page, [
{ role: 'user', content: query },
{ role: 'assistant', status: 'success' },
]);
await expect(async () => {
const { message } = await utils.chatPanel.getLatestAssistantMessage(page);
const fullText = await message.innerText();
expect(fullText).toMatch(new RegExp(`Workspace${randomStr1}.*cat`));
expect(fullText).toMatch(new RegExp(`Workspace${randomStr2}.*dog`));
expect(
await message.locator('affine-footnote-node').count()
).toBeGreaterThanOrEqual(1);
}).toPass({ timeout: 20000 });
});
test('should display failed info if upload attachment failed', async ({
loggedInPage: page,
utils,
}) => {
await createLocalWorkspace({ name: 'test' }, page, false, 'affine-cloud');
await utils.settings.openSettingsPanel(page);
await utils.settings.enableWorkspaceEmbedding(page);
const attachments = [
{
name: 'document1.txt',
mimeType: 'text/plain',
buffer: Buffer.from('HelloWorld'),
},
];
await page.context().setOffline(true);
await utils.settings.uploadWorkspaceEmbedding(page, attachments);
const attachmentList = page.getByTestId(
'workspace-embedding-setting-attachment-list'
);
const errorItem = await attachmentList.getByTestId(
'workspace-embedding-setting-attachment-error-item'
);
await errorItem.hover();
await expect(page.getByText(/Network error/i)).toBeVisible();
await page.context().setOffline(false);
});
test('should isolate selected attachments from workspace artifact search', async ({
loggedInPage: page,
utils,
}) => {
await createLocalWorkspace({ name: 'test' }, page, false, 'affine-cloud');
await utils.settings.openSettingsPanel(page);
await utils.settings.enableWorkspaceEmbedding(page);
const person = 'test123';
const hobby1 = Buffer.from(`${person} love climbing`);
const hobby2 = Buffer.from(`${person} love skating`);
const attachments = [
{
name: 'hobby.txt',
mimeType: 'text/plain',
buffer: hobby1,
},
];
await utils.settings.uploadWorkspaceEmbedding(page, attachments);
await utils.settings.waitForFileEmbeddingReadiness(page, 1);
await utils.settings.closeSettingsPanel(page);
const query = `Use semantic search across workspace and attached files, then list all hobbies of ${person}.`;
await utils.chatPanel.chatWithAttachments(
page,
[
{
name: 'hobby2.txt',
mimeType: 'text/plain',
buffer: hobby2,
},
],
query
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: query,
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { message } = await utils.chatPanel.getLatestAssistantMessage(page);
const fullText = await message.innerText();
expect(fullText).toMatch(/skating/i);
expect(fullText).not.toMatch(/climbing/i);
expect(
await message.locator('affine-footnote-node').count()
).toBeGreaterThanOrEqual(1);
}).toPass({ timeout: 20000 });
});
test('should support attachments pagination', async ({
loggedInPage: page,
utils,
}) => {
await createLocalWorkspace({ name: 'test' }, page, false, 'affine-cloud');
await utils.settings.openSettingsPanel(page);
await utils.settings.enableWorkspaceEmbedding(page);
const attachments = Array.from({ length: 11 }, (_, i) => ({
name: `document${i + 1}.txt`,
mimeType: 'text/plain',
buffer: Buffer.from(`attachment content ${i + 1}`),
}));
await utils.settings.uploadWorkspaceEmbedding(page, attachments);
const attachmentList = await page.getByTestId(
'workspace-embedding-setting-attachment-list'
);
await expect(
attachmentList.getByTestId('workspace-embedding-setting-attachment-item')
).toHaveCount(10);
const pagination = attachmentList.getByRole('navigation');
const currentPage = pagination.locator('li.active');
await expect(currentPage).toHaveText('1');
const page2 = pagination.locator('li').nth(2);
await page2.click();
await expect(
attachmentList.getByTestId('workspace-embedding-setting-attachment-item')
).toHaveCount(1);
await expect(
attachmentList
.getByTestId('workspace-embedding-setting-attachment-item')
.first()
).toHaveText('document1.txt');
});
test('should support remove attachment with confirm', async ({
loggedInPage: page,
utils,
}) => {
await createLocalWorkspace({ name: 'test' }, page, false, 'affine-cloud');
await utils.settings.openSettingsPanel(page);
await utils.settings.enableWorkspaceEmbedding(page);
const randomStr1 = Math.random().toString(36).substring(2, 6);
const textContent = `Workspace${randomStr1} is a cute cat`;
const attachments = [
{
name: 'document1.txt',
mimeType: 'text/plain',
buffer: Buffer.from(textContent),
},
];
await utils.settings.uploadWorkspaceEmbedding(page, attachments);
const attachmentList = page.getByTestId(
'workspace-embedding-setting-attachment-list'
);
await expect(
attachmentList.getByTestId('workspace-embedding-setting-attachment-item')
).toHaveCount(1);
await utils.settings.removeAttachment(page, 'document1.txt');
});
test('should show error message if remove attachment failed', async ({
loggedInPage: page,
utils,
}) => {
await createLocalWorkspace({ name: 'test' }, page, false, 'affine-cloud');
await utils.settings.openSettingsPanel(page);
await utils.settings.enableWorkspaceEmbedding(page);
const randomStr1 = Math.random().toString(36).substring(2, 6);
const textContent = `Workspace${randomStr1} is a cute cat`;
const attachments = [
{
name: 'document1.txt',
mimeType: 'text/plain',
buffer: Buffer.from(textContent),
},
];
await utils.settings.uploadWorkspaceEmbedding(page, attachments);
const attachmentList = page.getByTestId(
'workspace-embedding-setting-attachment-list'
);
await expect(
attachmentList.getByTestId('workspace-embedding-setting-attachment-item')
).toHaveCount(1);
await page.context().setOffline(true);
await utils.settings.clickRemoveAttachment(page, 'document1.txt');
await expect(
page.getByText(/Failed to remove attachment from embedding/i)
).toBeVisible();
await page.context().setOffline(false);
});
test('should support remove error attachment directly', async ({
loggedInPage: page,
utils,
}) => {
await createLocalWorkspace({ name: 'test' }, page, false, 'affine-cloud');
await utils.settings.openSettingsPanel(page);
await utils.settings.enableWorkspaceEmbedding(page);
const randomStr1 = Math.random().toString(36).substring(2, 6);
const textContent = `Workspace${randomStr1} is a cute cat`;
const attachments = [
{
name: 'document1.txt',
mimeType: 'text/plain',
buffer: Buffer.from(textContent),
},
];
await page.context().setOffline(true);
await utils.settings.uploadWorkspaceEmbedding(page, attachments);
await utils.settings.removeAttachment(page, 'document1.txt', false);
await page.context().setOffline(false);
});
// FIXME: wait for indexer
test.skip('should support ignore docs for embedding', async ({
loggedInPage: page,
utils,
}) => {
await utils.settings.openSettingsPanel(page);
await utils.settings.enableWorkspaceEmbedding(page);
await utils.settings.closeSettingsPanel(page);
await utils.editor.createDoc(
page,
'WBIgnoreDoc1',
'WBIgnoreEEE is a cute cat'
);
await utils.editor.createDoc(
page,
'WBIgnoreDoc2',
'WBIgnoreFFF is a cute dog'
);
await utils.settings.openSettingsPanel(page);
await utils.settings.waitForEmbeddingComplete(page);
await utils.settings.closeSettingsPanel(page);
await utils.chatPanel.makeChat(
page,
'What is WBIgnoreEEE? What is WBIgnoreFFF?If you dont know, just say "I dont know"'
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content:
'What is WBIgnoreEEE? What is WBIgnoreFFF?If you dont know, just say "I dont know"',
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { content, message } =
await utils.chatPanel.getLatestAssistantMessage(page);
expect(content).toMatch(/WBIgnoreEEE.*cat/);
expect(content).toMatch(/WBIgnoreFFF.*dog/);
expect(await message.locator('affine-footnote-node').count()).toBe(2);
}).toPass({ timeout: 20000 });
// Ignore docs
await utils.settings.openSettingsPanel(page);
await utils.settings.ignoreDocForEmbedding(page, 'WBIgnoreDoc1');
await utils.settings.ignoreDocForEmbedding(page, 'WBIgnoreDoc2');
await utils.settings.closeSettingsPanel(page);
// Ignored docs should not be used for embedding
await utils.chatPanel.makeChat(
page,
'What is WBIgnoreEEE? What is WBIgnoreFFF?If you dont know, just say "I dont know"'
);
await utils.chatPanel.waitForHistory(page, [
{
role: 'user',
content: 'What is WBIgnoreEEE? What is WBIgnoreFFF?',
},
{
role: 'assistant',
status: 'success',
},
]);
await expect(async () => {
const { content } = await utils.chatPanel.getLatestAssistantMessage(page);
expect(content).toMatch(/I dont know/i);
}).toPass({ timeout: 20000 });
});
test('should show error message if update ignored docs failed', async ({
loggedInPage: page,
utils,
}) => {
await utils.settings.openSettingsPanel(page);
await utils.settings.enableWorkspaceEmbedding(page);
await utils.settings.closeSettingsPanel(page);
await utils.editor.createDoc(page, 'Test Doc', 'HelloWorld');
// Ignore docs
await utils.settings.openSettingsPanel(page);
await page.context().setOffline(true);
await utils.settings.ignoreDocForEmbedding(page, 'Test Doc', false);
await page
.getByText(/Failed to update ignored docs/i)
.waitFor({ state: 'visible', timeout: 20000 });
await page.context().setOffline(false);
});
});

View File

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

View File

@@ -0,0 +1,393 @@
// oxlint-disable 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.waitForTimeout(500); // wait the sidebar stable
}
await page.getByTestId('sidebar-tab-chat').click();
await expect(page.getByTestId('sidebar-tab-content-chat')).toBeVisible();
}
public static async closeChatPanel(page: Page) {
await page.getByTestId('right-sidebar-close').click({
delay: 200,
});
await expect(page.getByTestId('sidebar-tab-content-chat')).toBeHidden();
}
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.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) {
const selectors =
':is(chat-message-user,chat-message-assistant,chat-message-action,[data-testid="chat-message-user"],[data-testid="chat-message-assistant"],[data-testid="chat-message-action"])';
const messages = page.locator(selectors);
const count = await messages.count();
if (!count) return [] as ChatMessage[];
const history: ChatMessage[] = [];
for (let i = 0; i < count; i++) {
const message = messages.nth(i);
const testId = await message.getAttribute('data-testid');
const tag = await message.evaluate(el => el.tagName.toLowerCase());
const isAssistant =
testId === 'chat-message-assistant' || tag === 'chat-message-assistant';
const isAction =
testId === 'chat-message-action' || tag === 'chat-message-action';
const isUser =
testId === 'chat-message-user' || tag === 'chat-message-user';
if (!isAssistant && !isAction && !isUser) continue;
const titleNode = message.locator('.user-info').first();
const title =
(await titleNode.count()) > 0 ? await titleNode.innerText() : '';
if (isUser) {
const pureText = message.getByTestId('chat-content-pure-text').first();
const content =
(await pureText.count()) > 0
? await pureText.innerText()
: ((await message.innerText()) ?? '');
history.push({ role: 'user', content });
continue;
}
const richText = message.locator('chat-content-rich-text editor-host');
const richContent =
(await richText.count()) > 0
? (await richText.allInnerTexts()).join(' ')
: '';
const content = richContent || ((await message.innerText()) ?? '').trim();
if (isAssistant) {
const inferredStatus = (await message
.getByTestId('ai-loading')
.isVisible()
.catch(() => false))
? 'transmitting'
: content
? 'success'
: 'idle';
history.push({
role: 'assistant',
status: ((await message.getAttribute('data-status')) ??
inferredStatus) as ChatStatus,
title,
content,
});
continue;
}
history.push({ role: 'action', title, content });
}
return history;
}
private static expectHistory(
history: ChatMessage[],
expected: (
| Partial<ChatUserMessage>
| Partial<ChatAssistantMessage>
| Partial<ChatActionMessage>
)[]
) {
expect(history).toHaveLength(expected.length);
const assistantStage = {
loading: 1,
transmitting: 1,
success: 2,
} as const;
history.forEach((message, index) => {
const expectedMessage = expected[index];
if (
message.role === 'assistant' &&
expectedMessage?.role === 'assistant' &&
expectedMessage.status
) {
const expectedStatus = expectedMessage.status;
if (
expectedStatus in assistantStage &&
message.status in assistantStage
) {
expect(
assistantStage[message.status as keyof typeof assistantStage]
).toBeGreaterThanOrEqual(
assistantStage[expectedStatus as keyof typeof assistantStage]
);
const { status: _status, ...expectedRest } = expectedMessage;
expect(message).toMatchObject(expectedRest);
return;
}
}
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')
.allInnerTexts()
).join(' '),
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 = page.getByTestId('chat-panel-with-button');
await withButton.hover();
await withButton.click({ delay: 200 });
const withMenu = page.getByTestId('ai-add-popover');
await withMenu.waitFor({ state: 'visible' });
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 = page.getByTestId('chat-panel-with-button');
await withButton.hover();
await withButton.click({ delay: 200 });
const withMenu = page.getByTestId('ai-add-popover');
await withMenu.waitFor({ state: 'visible' });
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.dataset.state));
expect(states.every(state => state === 'finished')).toBe(true);
}).toPass({ timeout: 20000 });
}
await expect(async () => {
const states = await page
.getByTestId('chat-panel-chip')
.evaluateAll(elements => elements.map(el => el.dataset.state));
expect(states).toHaveLength(attachments.length);
expect(states.every(state => state === 'finished')).toBe(true);
}).toPass({ timeout: 20000 });
await this.makeChat(page, text);
}
public static async uploadImages(
page: Page,
images: { name: string; mimeType: string; buffer: Buffer }[]
) {
await page.evaluate(() => {
delete window.showOpenFilePicker;
});
const fileChooserPromise = page.waitForEvent('filechooser');
const withButton = page.getByTestId('chat-panel-with-button');
await withButton.hover();
await withButton.click({ delay: 200 });
const withMenu = page.getByTestId('ai-add-popover');
await withMenu.waitFor({ state: 'visible' });
await withMenu.getByTestId('ai-chat-with-images').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(images);
}
public static async chatWithImages(
page: Page,
images: { name: string; mimeType: string; buffer: Buffer }[],
text: string
) {
await this.uploadImages(page, images);
await page.waitForSelector('ai-chat-input .image-container');
await this.makeChat(page, text);
}
public static async chatWithTags(page: Page, tags: string[]) {
for (const tag of tags) {
const withButton = page.getByTestId('chat-panel-with-button');
await withButton.hover();
await withButton.click({ delay: 200 });
const withMenu = page.getByTestId('ai-add-popover');
await withMenu.waitFor({ state: 'visible' });
await withMenu.getByTestId('ai-chat-with-tags').click();
await withMenu.getByText(tag).click();
await page.getByTestId('chat-panel-chips').getByText(tag);
await this.waitForEmbeddingProgress(page);
await withMenu.waitFor({
state: 'hidden',
});
}
}
public static async chatWithCollections(page: Page, collections: string[]) {
for (const collection of collections) {
const withButton = page.getByTestId('chat-panel-with-button');
await withButton.hover();
await withButton.click({ delay: 200 });
const withMenu = page.getByTestId('ai-add-popover');
await withMenu.waitFor({ state: 'visible' });
await withMenu.getByTestId('ai-chat-with-collections').click();
await withMenu.getByText(collection).click();
await page.getByTestId('chat-panel-chips').getByText(collection);
await this.waitForEmbeddingProgress(page);
await withMenu.waitFor({
state: 'hidden',
});
}
}
public static async waitForEmbeddingProgress(page: Page) {
try {
await page.getByTestId('chat-panel-embedding-progress').waitFor({
state: 'visible',
});
await page.getByTestId('chat-panel-embedding-progress').waitFor({
state: 'hidden',
});
} catch {
// do nothing
}
}
public static async openChatInputPreference(page: Page) {
const trigger = page.getByTestId('chat-input-preference-trigger');
await trigger.click();
await page.getByTestId('chat-input-preference').waitFor({
state: 'visible',
});
}
public static async enableReasoning(page: Page) {
await this.openChatInputPreference(page);
const reasoning = page.getByTestId('chat-reasoning');
if ((await reasoning.getAttribute('data-active')) === 'false') {
await reasoning.click();
}
}
public static async disableReasoning(page: Page) {
await this.openChatInputPreference(page);
const reasoning = page.getByTestId('chat-reasoning');
if ((await reasoning.getAttribute('data-active')) === 'true') {
await reasoning.click();
}
}
}

View File

@@ -0,0 +1,771 @@
import {
createEdgelessNoteBlock,
setEdgelessTool,
} from '@affine-test/kit/utils/editor';
import {
pressEscape,
selectAllByKeyboard,
} from '@affine-test/kit/utils/keyboard';
import {
clickNewPageButton,
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 waitForCurrentDocSynced(page: Page) {
await page.evaluate(async () => {
const [, , workspaceId, docId] = location.pathname.split('/');
const workspace = (
window as typeof window & {
currentWorkspace?: {
engine: {
doc: {
waitForSynced(docId: string, abort: AbortSignal): Promise<void>;
};
};
};
}
).currentWorkspace;
if (!workspaceId || !docId || !workspace) {
throw new Error('Current document is unavailable');
}
const abort = AbortSignal.timeout(60_000);
await Promise.all([
workspace.engine.doc.waitForSynced(docId, abort),
workspace.engine.doc.waitForSynced('db$docProperties', abort),
]);
const response = await fetch(
`/api/workspaces/${workspaceId}/docs/${docId}`
);
if (!response.ok) {
throw new Error('Current document could not be persisted');
}
});
}
public static async focusToEditor(page: Page) {
const title = getBlockSuiteEditorTitle(page);
await title.focus();
await page.keyboard.press('Enter');
}
public static async getEditorContent(page: Page, trim = true) {
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 => {
const invisibleFiltered = c.replace(/[\u200B-\u200D\uFEFF]/g, '');
if (trim) {
return invisibleFiltered.trim();
}
return invisibleFiltered;
})
.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-\u200D\uFEFF]/g, '')
.trim();
}
public static async switchToEdgelessMode(page: Page) {
const editor = await page.waitForSelector('page-editor');
await page.getByTestId('switch-edgeless-mode-button').click();
await 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, timeout = 2 * 60000) {
const answer = page.getByTestId('ai-penel-answer').last();
await answer.waitFor({ state: 'visible', timeout });
return answer;
}
private static createAction(
page: Page,
action: () => Promise<void>,
options?: { responseTimeoutMs?: number }
) {
return async () => {
const responseTimeoutMs = options?.responseTimeoutMs ?? 60000;
await action();
await this.waitForAiAnswer(page, responseTimeoutMs);
await page.getByTestId('ai-generating').waitFor({
state: 'hidden',
timeout: responseTimeoutMs,
});
const responses = new Set<string>();
const answer = await this.waitForAiAnswer(page);
const responsesMenu = answer.getByTestId('answer-responses');
await responsesMenu.waitFor({
state: 'visible',
timeout: responseTimeoutMs,
});
await responsesMenu.scrollIntoViewIfNeeded({
timeout: responseTimeoutMs,
});
await responsesMenu
.getByTestId('answer-insert-below-loading')
.waitFor({ state: 'hidden', timeout: responseTimeoutMs });
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 createShape(page: Page, text: string) {
// Create shape
await page.keyboard.press('s');
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 shapes = edgelessBlock.gfx.gfxElements.filter(
(el: GfxModel) => 'type' in el && el.type === 'shape'
);
return shapes[shapes.length - 1].id;
});
// Insert text inside shape
await page.mouse.dblclick(450, 450);
await page.keyboard.insertText(text);
// Prevent the shape from being dragged
await page.mouse.click(500, 500);
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 clearAllCollections(page: Page) {
while (true) {
const collection = await page
.getByTestId('navigation-panel-collections')
.locator('[data-testid^="navigation-panel-collection-"]')
.first();
if (!(await collection.isVisible())) {
break;
}
const collectionContent = await collection.locator('div').first();
await collectionContent.hover();
const more = await collectionContent.getByTestId(
'navigation-panel-tree-node-operation-button'
);
await more.click();
await page.getByTestId('collection-delete-button').click();
}
await page.waitForTimeout(100);
}
public static async clearAllTags(page: Page) {
while (true) {
const tag = await page
.getByTestId('navigation-panel-tags')
.locator('[data-testid^="navigation-panel-tag-"]')
.first();
if (!(await tag.isVisible())) {
break;
}
const tagContent = await tag.locator('div').first();
await tagContent.hover();
const more = await tagContent.getByTestId(
'navigation-panel-tree-node-operation-button'
);
await more.click();
await page.getByTestId('tag-delete-button').click();
}
await page.waitForTimeout(100);
}
public static async createDoc(page: Page, title: string, docContent: string) {
await clickNewPageButton(page);
await page.keyboard.insertText(title);
await this.focusToEditor(page);
const texts = docContent.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 createCollectionAndDoc(
page: Page,
collectionName: string,
docContent: string
) {
// Create collection
await page
.getByTestId('navigation-panel-bar-add-collection-button')
.click();
const input = await page.getByTestId('prompt-modal-input');
await input.focus();
await input.pressSequentially(collectionName);
await page.getByTestId('prompt-modal-confirm').click();
const collections = await page.getByTestId('collapsible-section-content');
const collection = await collections
.getByText(collectionName)
.locator('..');
// Create doc
await collection.hover();
await collection.getByTestId('collection-add-doc-button').click();
await page.getByTestId('confirm-modal-confirm').click();
await this.focusToEditor(page);
const texts = docContent.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 this.waitForCurrentDocSynced(page);
}
public static async createTagAndDoc(
page: Page,
tagName: string,
docContent: string
) {
// Create tag
const tags = await page.getByTestId('navigation-panel-tags');
await tags.hover();
await tags.getByTestId('navigation-panel-bar-add-tag-button').click();
const input = await page.getByTestId('rename-modal-input');
await input.focus();
await input.pressSequentially(tagName);
await input.press('Enter');
const tag = await tags.getByText(tagName).locator('..');
// Create doc
await tag.hover();
await tag.getByTestId('tag-add-doc-button').click();
await this.focusToEditor(page);
const texts = docContent.split('\n');
for (const [index, line] of texts.entries()) {
await page.keyboard.insertText(line);
if (index !== texts.length - 1) {
await page.keyboard.press('Enter');
}
}
// sleep 1 sec to wait the doc sync
await page.waitForTimeout(1000);
}
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 removeAll(page: Page) {
await selectAllByKeyboard(page);
await page.keyboard.press('Delete');
}
public static async askAIWithEdgeless(
page: Page,
createBlock: () => Promise<void>,
afterSelected?: () => Promise<void>
) {
await this.switchToEdgelessMode(page);
await this.removeAll(page);
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: () => 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(),
{ responseTimeoutMs: 120000 }
),
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,
async () => {
await page.getByTestId('action-generate-image').click();
const input = page.locator(
'affine-ai-panel-widget .ai-panel-container textarea'
);
await input.waitFor({ state: 'visible' });
await input.fill('generate an image');
await page.getByTestId('ai-panel-input-send').waitFor({
state: 'visible',
});
await page.getByTestId('ai-panel-input-send').click();
},
{ responseTimeoutMs: 4 * 60000 }
),
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();
},
{ responseTimeoutMs: 4 * 60000 }
)(),
imageFilter: (style: string) =>
this.createAction(
page,
async () => {
await page.getByTestId('action-ai-image-filter').hover();
await page.getByTestId(`action-image-filter-${style}`).click();
},
{ responseTimeoutMs: 4 * 60000 }
)(),
};
}
public static async showAIMenu(page: Page) {
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: () => 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(),
{ responseTimeoutMs: 120000 }
),
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;
}
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');
return await this.showAIMenu(page);
}
public static async whatAreYourThoughts(page: Page, text: string) {
const textarea = page.locator(
'affine-ai-panel-widget .ai-panel-container textarea'
);
await textarea.fill(text);
return textarea;
}
}

View File

@@ -0,0 +1,280 @@
import { expect, type Page } from '@playwright/test';
const WORKSPACE_EMBEDDING_SWITCH_TEST_ID = 'workspace-embedding-setting-switch';
export class SettingsPanelUtils {
public static async openSettingsPanel(page: Page) {
if (await page.getByTestId('workspace-setting:embedding').isHidden()) {
await page
.getByTestId('slider-bar-workspace-setting-button')
.waitFor({ state: 'visible' });
await page.getByTestId('slider-bar-workspace-setting-button').click();
await page.getByTestId('workspace-setting:embedding').click();
await page.getByTestId('workspace-embedding-setting-header').waitFor({
state: 'visible',
});
}
}
public static async closeSettingsPanel(page: Page) {
if (
await page.getByTestId('workspace-embedding-setting-wrapper').isVisible()
) {
await page.keyboard.press('Escape');
await page.getByTestId('workspace-embedding-setting-wrapper').waitFor({
state: 'hidden',
});
}
}
public static async isWorkspaceEmbeddingEnabled(page: Page) {
const input = await page
.getByTestId(WORKSPACE_EMBEDDING_SWITCH_TEST_ID)
.locator('input');
return (await input.getAttribute('value')) === 'on';
}
public static async waitForWorkspaceEmbeddingSwitchToBe(
page: Page,
enabled: boolean
) {
const input = await page
.getByTestId(WORKSPACE_EMBEDDING_SWITCH_TEST_ID)
.locator('input');
await expect(input).toHaveAttribute('value', enabled ? 'on' : 'off');
}
public static async toggleWorkspaceEmbedding(page: Page) {
const input = await page.getByTestId(WORKSPACE_EMBEDDING_SWITCH_TEST_ID);
await input.click();
}
public static async enableWorkspaceEmbedding(
page: Page,
waitForEnabled = true
) {
const enabled = await this.isWorkspaceEmbeddingEnabled(page);
if (!enabled) {
await this.toggleWorkspaceEmbedding(page);
}
if (waitForEnabled) {
await this.waitForWorkspaceEmbeddingSwitchToBe(page, true);
}
}
public static async disableWorkspaceEmbedding(
page: Page,
waitForDisabled = true
) {
const enabled = await this.isWorkspaceEmbeddingEnabled(page);
if (enabled) {
await this.toggleWorkspaceEmbedding(page);
}
if (waitForDisabled) {
await this.waitForWorkspaceEmbeddingSwitchToBe(page, false);
}
}
public static async uploadWorkspaceEmbedding(
page: Page,
attachments: { name: string; mimeType: string; buffer: Buffer }[]
) {
await page.evaluate(() => {
delete window.showOpenFilePicker;
});
for (const attachment of attachments) {
const fileChooserPromise = page.waitForEvent('filechooser');
await page
.getByTestId('workspace-embedding-setting-upload-button')
.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(attachment);
await page
.getByTestId('workspace-embedding-setting-attachment-uploading-item')
.waitFor({ state: 'hidden', timeout: 30_000 });
}
}
public static async removeAllAttachments(page: Page) {
const itemId = 'workspace-embedding-setting-attachment-item';
let count = await page.getByTestId(itemId).count();
while (count > 0) {
const attachmentItem = await page.getByTestId(itemId).first();
const hasErrorItem = await attachmentItem
.getByTestId('workspace-embedding-setting-attachment-error-item')
.isVisible();
await attachmentItem
.getByTestId('workspace-embedding-setting-attachment-delete-button')
.click();
if (!hasErrorItem) {
await page.getByTestId('confirm-modal-confirm').click();
}
await page.waitForTimeout(1000);
count = await page.getByTestId(itemId).count();
}
}
public static async clickRemoveAttachment(
page: Page,
attachment: string,
shouldConfirm = true
) {
const attachmentItem = await page
.getByTestId('workspace-embedding-setting-attachment-item')
.filter({ hasText: attachment });
await attachmentItem
.getByTestId('workspace-embedding-setting-attachment-delete-button')
.click();
if (shouldConfirm) {
await page.getByTestId('confirm-modal-confirm').click();
}
}
public static async removeAttachment(
page: Page,
attachment: string,
shouldConfirm = true
) {
await this.clickRemoveAttachment(page, attachment, shouldConfirm);
await page
.getByTestId('workspace-embedding-setting-attachment-item')
.filter({ hasText: attachment })
.waitFor({
state: 'hidden',
});
}
public static async ignoreDocForEmbedding(
page: Page,
doc: string,
shouldWaitForRefresh = true
) {
// Open Dos Searcher
const ignoreDocsButton = await page.getByTestId(
'workspace-embedding-setting-ignore-docs-button'
);
await ignoreDocsButton.click();
// Search and select the doc
const searcher = await page.getByTestId('doc-selector-layout');
const searchInput = await page.getByTestId('doc-selector-search-input');
await searchInput.waitFor({ state: 'visible' });
await searchInput.focus();
await page.keyboard.insertText(doc);
const pageListItem = searcher
.getByTestId('doc-list-item')
.filter({
has: page
.getByTestId('doc-list-item-title')
.getByText(doc, { exact: true }),
})
.first();
await expect(pageListItem).toBeVisible();
await expect(pageListItem.getByTestId('doc-list-item-title')).toHaveText(
doc
);
await pageListItem.click();
await searcher.getByTestId('doc-selector-confirm-button').click();
if (shouldWaitForRefresh) {
const ignoredDocs = await page.getByTestId(
'workspace-embedding-setting-ignore-docs-list'
);
await expect(
ignoredDocs
.getByTestId('workspace-embedding-setting-ignore-docs-list-item')
.filter({ hasText: doc })
).toBeVisible();
}
}
public static async clearAllIgnoredDocs(page: Page) {
const ignoredDocs = await page.getByTestId('ignore-doc-title').all();
for (const ignoredDoc of ignoredDocs) {
const doc = await ignoredDoc.innerText();
// Open Dos Searcher
const ignoreDocsButton = await page.getByTestId(
'workspace-embedding-setting-ignore-docs-button'
);
await ignoreDocsButton.click();
// Search and select the doc
const searcher = await page.getByTestId('doc-selector-layout');
const searchInput = await page.getByTestId('doc-selector-search-input');
await searchInput.focus();
await page.keyboard.insertText(doc);
const pageListItem = searcher.getByTestId('doc-list-item');
await expect(pageListItem).toHaveCount(1);
await pageListItem.getByTestId('affine-checkbox').uncheck();
await searcher.getByTestId('doc-selector-confirm-button').click();
}
}
private static async waitForEmbeddingStatus(
page: Page,
timeout: number,
status = 'synced'
) {
await expect(async () => {
await this.openSettingsPanel(page);
const title = page.getByTestId('embedding-progress-title');
// oxlint-disable-next-line prefer-dom-node-dataset
const progressAttr = await title.getAttribute('data-progress');
expect(progressAttr).not.toBe('loading');
expect(progressAttr).toBe(status);
}).toPass({ timeout });
}
public static async waitForEmbeddingComplete(page: Page, timeout = 30000) {
await this.waitForEmbeddingStatus(page, timeout);
// check embedding progress count
await expect(async () => {
const count = page.getByTestId('embedding-progress-count');
const countText = await count.textContent();
if (countText) {
const [embedded, total] = countText.split('/').map(Number);
expect(embedded).toBe(total);
expect(embedded).toBeGreaterThan(0);
}
}).toPass({ timeout });
}
public static async waitForFileEmbeddingReadiness(
page: Page,
expectedFileCount: number,
timeout = 30000
) {
await expect(async () => {
const attachmentList = page.getByTestId(
'workspace-embedding-setting-attachment-list'
);
const attachmentItems = attachmentList.getByTestId(
'workspace-embedding-setting-attachment-item'
);
await expect(attachmentItems).toHaveCount(expectedFileCount);
await expect(
attachmentList.getByTestId(
'workspace-embedding-setting-attachment-uploading-item'
)
).toHaveCount(0);
}).toPass({ timeout });
await expect(async () => {
await expect(
page.getByTestId('workspace-embedding-setting-attachment-ready-item')
).toHaveCount(expectedFileCount);
}).toPass({ timeout });
}
}

View File

@@ -0,0 +1,70 @@
import { skipOnboarding } from '@affine-test/kit/playwright';
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 createNewPage(page: Page) {
await clickNewPageButton(page);
await waitForEditorLoad(page);
}
public async setupTestEnvironment(page: Page) {
await skipOnboarding(page.context());
await openHomePage(page);
await this.createNewPage(page);
}
public async createTestWorkspace(page: Page, name: string = 'test') {
await createLocalWorkspace({ name }, page);
}
}

View File

@@ -0,0 +1,38 @@
import fs from 'node:fs';
import { skipOnboarding } from '@affine-test/kit/playwright';
import {
createRandomAIUser,
enableCloudWorkspace,
loginUserDirectly,
} from '@affine-test/kit/utils/cloud';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import type { Page } from '@playwright/test';
import { chromium } from '@playwright/test';
function getUser() {
return createRandomAIUser();
}
async function setupTestEnvironment(page: Page) {
await openHomePage(page);
await enableCloudWorkspace(page);
}
export default async function globalSetup() {
const browser = await chromium.launch();
const context = await browser.newContext();
await skipOnboarding(context);
const page = await context.newPage();
await page.goto('http://localhost:8080/', { timeout: 240 * 1000 });
const user = await getUser();
await page.getByTestId('sidebar-user-avatar').click({
delay: 200,
timeout: 20 * 1000,
});
await loginUserDirectly(page, user);
await setupTestEnvironment(page);
const state = await page.context().storageState();
fs.writeFileSync('storageState.json', JSON.stringify(state));
await browser.close();
}

View File

@@ -0,0 +1,13 @@
{
"name": "@affine-test/affine-cloud-copilot",
"private": true,
"type": "module",
"scripts": {
"e2e": "yarn playwright test"
},
"devDependencies": {
"@affine-test/kit": "workspace:*",
"@playwright/test": "=1.58.2"
},
"version": "0.27.5"
}

View File

@@ -0,0 +1,78 @@
import { fileURLToPath } from 'node:url';
import { testResultDir } from '@affine-test/kit/playwright';
import type {
PlaywrightTestConfig,
PlaywrightWorkerOptions,
} from '@playwright/test';
const runtimeConfigPath = fileURLToPath(
new URL('./runtime-config.json', import.meta.url)
);
const config: PlaywrightTestConfig = {
testDir: './e2e',
fullyParallel: true,
timeout: 120_000,
outputDir: testResultDir,
globalSetup: './global-setup.ts',
use: {
baseURL: 'http://localhost:8080/',
browserName:
(process.env.BROWSER as PlaywrightWorkerOptions['browserName']) ??
'chromium',
permissions: ['clipboard-read', 'clipboard-write'],
viewport: { width: 1440, height: 800 },
actionTimeout: 10 * 1000,
locale: 'en-US',
trace: 'retain-on-failure',
video: 'retain-on-failure',
},
forbidOnly: !!process.env.CI,
workers: 4,
retries: 3,
reporter: process.env.CI ? 'github' : 'list',
webServer: [
{
command: 'yarn run -T affine dev -p @affine/web',
stdout: 'pipe',
stderr: 'pipe',
timeout: 240 * 1000,
reuseExistingServer: !process.env.CI,
env: {
COVERAGE: process.env.COVERAGE || 'false',
},
url: 'http://localhost:8080',
},
{
command: 'yarn run -T affine dev -p @affine/server',
timeout: 240 * 1000,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stderr: 'pipe',
env: {
DATABASE_URL:
process.env.DATABASE_URL ??
'postgresql://affine:affine@localhost:5432/affine',
NODE_ENV: 'test',
AFFINE_ENV: process.env.AFFINE_ENV ?? 'dev',
AFFINE_BACKEND_RUNTIME_CONFIG_PATH: runtimeConfigPath,
DEBUG: 'affine:*',
FORCE_COLOR: 'true',
DEBUG_COLORS: 'true',
MAILER_HOST: '0.0.0.0',
MAILER_PORT: '1025',
MAILER_SENDER: 'noreply@toeverything.info',
MAILER_USER: 'noreply@toeverything.info',
MAILER_PASSWORD: 'affine',
},
url: 'http://localhost:3010/graphql',
},
],
};
if (process.env.CI) {
config.retries = 3;
}
export default config;

View File

@@ -0,0 +1,28 @@
{
"storages": {
"blob.storage": {
"provider": "assetpack",
"bucket": "blobs",
"config": {
"path": "/tmp/affine-cloud-copilot-storage"
}
}
},
"copilot": {
"enabled": true,
"storage": {
"provider": "assetpack",
"bucket": "copilot",
"config": {
"path": "/tmp/affine-cloud-copilot-storage"
}
}
},
"indexer": {
"enabled": true,
"provider": {
"type": "embedded",
"endpoint": ""
}
}
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.web.json",
"compilerOptions": {
"rootDir": "./e2e",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
},
"include": ["e2e"],
"references": [{ "path": "../kit" }]
}