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
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:
20
tests/affine-cloud-copilot/README.md
Normal file
20
tests/affine-cloud-copilot/README.md
Normal 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
|
||||
@@ -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']));
|
||||
});
|
||||
});
|
||||
87
tests/affine-cloud-copilot/e2e/ai-action/change-tone.spec.ts
Normal file
87
tests/affine-cloud-copilot/e2e/ai-action/change-tone.spec.ts
Normal 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/);
|
||||
});
|
||||
});
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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/);
|
||||
});
|
||||
});
|
||||
146
tests/affine-cloud-copilot/e2e/ai-action/find-actions.spec.ts
Normal file
146
tests/affine-cloud-copilot/e2e/ai-action/find-actions.spec.ts
Normal 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/);
|
||||
});
|
||||
});
|
||||
84
tests/affine-cloud-copilot/e2e/ai-action/fix-grammar.spec.ts
Normal file
84
tests/affine-cloud-copilot/e2e/ai-action/fix-grammar.spec.ts
Normal 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/);
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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']));
|
||||
});
|
||||
});
|
||||
86
tests/affine-cloud-copilot/e2e/ai-action/summarize.spec.ts
Normal file
86
tests/affine-cloud-copilot/e2e/ai-action/summarize.spec.ts
Normal 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/);
|
||||
});
|
||||
});
|
||||
74
tests/affine-cloud-copilot/e2e/ai-action/translate.spec.ts
Normal file
74
tests/affine-cloud-copilot/e2e/ai-action/translate.spec.ts
Normal 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/);
|
||||
});
|
||||
});
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
40
tests/affine-cloud-copilot/e2e/base/base-test.ts
Normal file
40
tests/affine-cloud-copilot/e2e/base/base-test.ts
Normal 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;
|
||||
35
tests/affine-cloud-copilot/e2e/basic/authority.spec.ts
Normal file
35
tests/affine-cloud-copilot/e2e/basic/authority.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
536
tests/affine-cloud-copilot/e2e/basic/chat.spec.ts
Normal file
536
tests/affine-cloud-copilot/e2e/basic/chat.spec.ts
Normal 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, []);
|
||||
});
|
||||
});
|
||||
71
tests/affine-cloud-copilot/e2e/basic/guidance.spec.ts
Normal file
71
tests/affine-cloud-copilot/e2e/basic/guidance.spec.ts
Normal 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 });
|
||||
});
|
||||
});
|
||||
78
tests/affine-cloud-copilot/e2e/basic/onboarding.spec.ts
Normal file
78
tests/affine-cloud-copilot/e2e/basic/onboarding.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
117
tests/affine-cloud-copilot/e2e/chat-with/attachments.spec.ts
Normal file
117
tests/affine-cloud-copilot/e2e/chat-with/attachments.spec.ts
Normal 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 });
|
||||
});
|
||||
});
|
||||
124
tests/affine-cloud-copilot/e2e/chat-with/collections.spec.ts
Normal file
124
tests/affine-cloud-copilot/e2e/chat-with/collections.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
89
tests/affine-cloud-copilot/e2e/chat-with/doc.spec.ts
Normal file
89
tests/affine-cloud-copilot/e2e/chat-with/doc.spec.ts
Normal 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 });
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
/苹果/
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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]*苹果/
|
||||
);
|
||||
});
|
||||
});
|
||||
38
tests/affine-cloud-copilot/e2e/chat-with/image-block.spec.ts
Normal file
38
tests/affine-cloud-copilot/e2e/chat-with/image-block.spec.ts
Normal file
File diff suppressed because one or more lines are too long
106
tests/affine-cloud-copilot/e2e/chat-with/image.spec.ts
Normal file
106
tests/affine-cloud-copilot/e2e/chat-with/image.spec.ts
Normal file
File diff suppressed because one or more lines are too long
28
tests/affine-cloud-copilot/e2e/chat-with/network.spec.ts
Normal file
28
tests/affine-cloud-copilot/e2e/chat-with/network.spec.ts
Normal 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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
30
tests/affine-cloud-copilot/e2e/chat-with/reasoning.spec.ts
Normal file
30
tests/affine-cloud-copilot/e2e/chat-with/reasoning.spec.ts
Normal 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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
93
tests/affine-cloud-copilot/e2e/chat-with/tags.spec.ts
Normal file
93
tests/affine-cloud-copilot/e2e/chat-with/tags.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
181
tests/affine-cloud-copilot/e2e/chat-with/text.spec.ts
Normal file
181
tests/affine-cloud-copilot/e2e/chat-with/text.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
198
tests/affine-cloud-copilot/e2e/insertion/insert.spec.ts
Normal file
198
tests/affine-cloud-copilot/e2e/insertion/insert.spec.ts
Normal 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 });
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
75
tests/affine-cloud-copilot/e2e/insertion/save-as-doc.spec.ts
Normal file
75
tests/affine-cloud-copilot/e2e/insertion/save-as-doc.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
496
tests/affine-cloud-copilot/e2e/settings/embedding.spec.ts
Normal file
496
tests/affine-cloud-copilot/e2e/settings/embedding.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
3
tests/affine-cloud-copilot/e2e/types/global.d.ts
vendored
Normal file
3
tests/affine-cloud-copilot/e2e/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
interface Window {
|
||||
showOpenFilePicker?: () => Promise<FileSystemFileHandle[]>;
|
||||
}
|
||||
393
tests/affine-cloud-copilot/e2e/utils/chat-panel-utils.ts
Normal file
393
tests/affine-cloud-copilot/e2e/utils/chat-panel-utils.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
771
tests/affine-cloud-copilot/e2e/utils/editor-utils.ts
Normal file
771
tests/affine-cloud-copilot/e2e/utils/editor-utils.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
280
tests/affine-cloud-copilot/e2e/utils/settings-panel-utils.ts
Normal file
280
tests/affine-cloud-copilot/e2e/utils/settings-panel-utils.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
70
tests/affine-cloud-copilot/e2e/utils/test-utils.ts
Normal file
70
tests/affine-cloud-copilot/e2e/utils/test-utils.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
38
tests/affine-cloud-copilot/global-setup.ts
Normal file
38
tests/affine-cloud-copilot/global-setup.ts
Normal 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();
|
||||
}
|
||||
13
tests/affine-cloud-copilot/package.json
Normal file
13
tests/affine-cloud-copilot/package.json
Normal 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"
|
||||
}
|
||||
78
tests/affine-cloud-copilot/playwright.config.ts
Normal file
78
tests/affine-cloud-copilot/playwright.config.ts
Normal 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;
|
||||
28
tests/affine-cloud-copilot/runtime-config.json
Normal file
28
tests/affine-cloud-copilot/runtime-config.json
Normal 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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
10
tests/affine-cloud-copilot/tsconfig.json
Normal file
10
tests/affine-cloud-copilot/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.web.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./e2e",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["e2e"],
|
||||
"references": [{ "path": "../kit" }]
|
||||
}
|
||||
207
tests/affine-cloud/e2e/collaboration.spec.ts
Normal file
207
tests/affine-cloud/e2e/collaboration.spec.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { Path, skipOnboarding, test } from '@affine-test/kit/playwright';
|
||||
import {
|
||||
addUserToWorkspace,
|
||||
createRandomUser,
|
||||
enableCloudWorkspace,
|
||||
loginUser,
|
||||
} from '@affine-test/kit/utils/cloud';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
getBlockSuiteEditorTitle,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { clickUserInfoCard } from '@affine-test/kit/utils/setting';
|
||||
import { clickSideBarSettingButton } from '@affine-test/kit/utils/sidebar';
|
||||
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
let user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
user = await createRandomUser();
|
||||
await loginUser(page, user);
|
||||
});
|
||||
|
||||
// SKIP until BS-671 fix
|
||||
test.skip('can collaborate with other user and name should display when editing', async ({
|
||||
page,
|
||||
browser,
|
||||
}) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspace(page);
|
||||
await clickNewPageButton(page);
|
||||
const currentUrl = page.url();
|
||||
// format: http://localhost:8080/workspace/${workspaceId}/xxx
|
||||
const workspaceId = currentUrl.split('/')[4];
|
||||
const userB = await createRandomUser();
|
||||
const context = await browser.newContext();
|
||||
await skipOnboarding(context);
|
||||
const page2 = await context.newPage();
|
||||
await loginUser(page2, userB);
|
||||
await addUserToWorkspace(workspaceId, userB.id, 1 /* READ */);
|
||||
await page2.reload();
|
||||
await waitForEditorLoad(page2);
|
||||
await page2.goto(currentUrl);
|
||||
{
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.pressSequentially('TEST TITLE', {
|
||||
delay: 50,
|
||||
});
|
||||
}
|
||||
await page2.waitForTimeout(200);
|
||||
{
|
||||
const title = getBlockSuiteEditorTitle(page2);
|
||||
await expect(title).toHaveText('TEST TITLE');
|
||||
const typingPromise = (async () => {
|
||||
await page.keyboard.press('Enter', { delay: 50 });
|
||||
await page.keyboard.type('TEST CONTENT', { delay: 50 });
|
||||
})();
|
||||
// username should be visible when editing
|
||||
await expect(page2.getByText(user.name)).toBeVisible();
|
||||
await typingPromise;
|
||||
}
|
||||
|
||||
// change username
|
||||
await clickSideBarSettingButton(page);
|
||||
await clickUserInfoCard(page);
|
||||
const input = page.getByTestId('user-name-input');
|
||||
await input.clear();
|
||||
await input.pressSequentially('TEST USER', {
|
||||
delay: 50,
|
||||
});
|
||||
await page.getByTestId('save-user-name').click({
|
||||
delay: 50,
|
||||
});
|
||||
await page.keyboard.press('Escape', {
|
||||
delay: 50,
|
||||
});
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.focus();
|
||||
await page.keyboard.press('ArrowDown', { delay: 50 });
|
||||
{
|
||||
await expect(page2.getByText('TEST USER')).toBeVisible({
|
||||
timeout: 2000,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('can sync collections between different browser', async ({
|
||||
page,
|
||||
browser,
|
||||
}) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspace(page);
|
||||
await page.getByTestId('navigation-panel-bar-add-collection-button').click();
|
||||
const title = page.getByTestId('prompt-modal-input');
|
||||
await title.isVisible();
|
||||
await title.fill('test collection');
|
||||
await page.getByTestId('prompt-modal-confirm').click();
|
||||
|
||||
{
|
||||
const context = await browser.newContext();
|
||||
await skipOnboarding(context);
|
||||
const page2 = await context.newPage();
|
||||
await loginUser(page2, user);
|
||||
await page2.goto(page.url());
|
||||
const collections = page2.getByTestId('navigation-panel-collections');
|
||||
await collections.getByTestId('category-divider-collapse-button').click();
|
||||
await expect(collections.getByText('test collection')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('can sync svg between different browsers', async ({ page, browser }) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspace(page);
|
||||
await clickNewPageButton(page);
|
||||
await waitForEditorLoad(page);
|
||||
|
||||
// upload local svg
|
||||
|
||||
const slashMenu = page.locator(`.slash-menu`);
|
||||
const image = page.locator('affine-image');
|
||||
|
||||
await page.evaluate(async () => {
|
||||
// https://github.com/toeverything/blocksuite/blob/master/packages/blocks/src/_common/utils/filesys.ts#L20
|
||||
(window as any).showOpenFilePicker = undefined;
|
||||
});
|
||||
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.pressSequentially('TEST TITLE', {
|
||||
delay: 50,
|
||||
});
|
||||
await page.keyboard.press('Enter', { delay: 50 });
|
||||
await page.waitForTimeout(100);
|
||||
await page.keyboard.type('/', { delay: 50 });
|
||||
await expect(slashMenu).toBeVisible();
|
||||
await page.keyboard.type('image', { delay: 100 });
|
||||
await expect(slashMenu).toBeVisible();
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await page.keyboard.press('Enter', { delay: 50 });
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(Path.dir(import.meta.url).join('logo.svg').value);
|
||||
await expect(image).toBeVisible();
|
||||
|
||||
// the user should see the svg
|
||||
// get the image src under "affine-image img"
|
||||
const src1 = await page.locator('affine-image img').getAttribute('src');
|
||||
expect(src1).not.toBeNull();
|
||||
|
||||
// fetch the actual src1 resource in the browser
|
||||
const svg1 = await page.evaluate(
|
||||
src =>
|
||||
fetch(src!)
|
||||
.then(res => res.blob())
|
||||
.then(blob => blob.text()),
|
||||
src1
|
||||
);
|
||||
|
||||
{
|
||||
const context = await browser.newContext();
|
||||
await skipOnboarding(context);
|
||||
const page2 = await context.newPage();
|
||||
await loginUser(page2, user);
|
||||
await page2.goto(page.url());
|
||||
|
||||
// second user should see the svg
|
||||
// get the image src under "affine-image img"
|
||||
const src2 = await page2.locator('affine-image img').getAttribute('src');
|
||||
expect(src2).not.toBeNull();
|
||||
|
||||
// fetch the actual src2 resource in the browser
|
||||
const svg2 = await page2.evaluate(
|
||||
src =>
|
||||
fetch(src!)
|
||||
.then(res => res.blob())
|
||||
.then(blob => blob.text()),
|
||||
src2
|
||||
);
|
||||
|
||||
expect(svg2).toEqual(svg1);
|
||||
}
|
||||
});
|
||||
121
tests/affine-cloud/e2e/comments.spec.ts
Normal file
121
tests/affine-cloud/e2e/comments.spec.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { test } from '@affine-test/kit/playwright';
|
||||
import {
|
||||
createRandomUser,
|
||||
enableCloudWorkspace,
|
||||
loginUser,
|
||||
} from '@affine-test/kit/utils/cloud';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
getBlockSuiteEditorTitle,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test.describe('comments', () => {
|
||||
let user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
user = await createRandomUser();
|
||||
await loginUser(page, user);
|
||||
});
|
||||
|
||||
test('can create and display a comment on selected text', async ({
|
||||
page,
|
||||
}) => {
|
||||
// Reload page and wait for editor - following the pattern from working collaboration test
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
|
||||
// Create a new workspace
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test-comment-workspace',
|
||||
},
|
||||
page
|
||||
);
|
||||
|
||||
// Enable cloud workspace for comments feature
|
||||
await enableCloudWorkspace(page);
|
||||
|
||||
// Create a new doc
|
||||
await clickNewPageButton(page);
|
||||
await waitForEditorLoad(page);
|
||||
|
||||
// Add title and content
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.click();
|
||||
await title.fill('Test Comment Document');
|
||||
|
||||
// Add some text content
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.type(
|
||||
'This is a test paragraph with some text that we will comment on.',
|
||||
{ delay: 50 }
|
||||
);
|
||||
|
||||
for (let i = 0; i < 11; i++) {
|
||||
await page.keyboard.press('Shift+ArrowLeft');
|
||||
}
|
||||
|
||||
// Wait for the toolbar to appear after text selection
|
||||
const toolbar = page.locator('editor-toolbar');
|
||||
await expect(toolbar).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Click the comment button in the toolbar
|
||||
// The comment button has tooltip "Comment" based on blockCommentToolbarButton
|
||||
const commentButton = page.locator(
|
||||
'editor-icon-button[aria-label="Comment"]'
|
||||
);
|
||||
await expect(commentButton).toBeVisible();
|
||||
await commentButton.click();
|
||||
|
||||
// Verify comment sidebar is opened by checking comment editor is visible
|
||||
await page.waitForTimeout(300); // Wait for sidebar animation
|
||||
|
||||
// Find the comment editor
|
||||
const commentEditor = page.locator(
|
||||
'.comment-editor-viewport .page-editor-container'
|
||||
);
|
||||
await expect(commentEditor).toBeVisible();
|
||||
|
||||
// Enter comment text
|
||||
await commentEditor.click();
|
||||
await commentEditor.focus();
|
||||
await page.keyboard.type('This is my first comment on this text', {
|
||||
delay: 50,
|
||||
});
|
||||
|
||||
// Commit the comment (click the commit button or press Cmd/Ctrl+Enter)
|
||||
// The commit button is in the footer with an arrow icon
|
||||
const commitButton = page.locator('.comment-editor-viewport button').last();
|
||||
await expect(commitButton).toBeVisible();
|
||||
await commitButton.click();
|
||||
|
||||
// Wait for comment to be saved
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify the comment appears in the sidebar
|
||||
await expect(page.locator('text=Comments')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('text=This is my first comment on this text')
|
||||
).toBeVisible();
|
||||
|
||||
// Verify the preview text appears in the sidebar
|
||||
// The preview should show the selected text that was commented on
|
||||
// Target specifically the sidebar tab content to avoid conflicts with editor content
|
||||
const sidebarTab = page.getByTestId('sidebar-tab-content-comment');
|
||||
await expect(sidebarTab.locator('text=comment on.')).toBeVisible();
|
||||
|
||||
// This text should appear in the sidebar as the preview of what was commented on
|
||||
|
||||
// Verify the comment is successfully created by checking the sidebar content
|
||||
const commentSidebar = page.locator('text=Comments').locator('..');
|
||||
await expect(commentSidebar).toBeVisible();
|
||||
});
|
||||
});
|
||||
110
tests/affine-cloud/e2e/login.spec.ts
Normal file
110
tests/affine-cloud/e2e/login.spec.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { test } from '@affine-test/kit/playwright';
|
||||
import {
|
||||
createRandomUser,
|
||||
enableCloudWorkspace,
|
||||
loginUser,
|
||||
} from '@affine-test/kit/utils/cloud';
|
||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
|
||||
import { clickUserInfoCard } from '@affine-test/kit/utils/setting';
|
||||
import {
|
||||
clickSideBarAllPageButton,
|
||||
clickSideBarSettingButton,
|
||||
clickSideBarUseAvatar,
|
||||
} from '@affine-test/kit/utils/sidebar';
|
||||
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test('can open login modal in workspace list', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await page.getByTestId('sidebar-user-avatar').click({
|
||||
delay: 200,
|
||||
});
|
||||
await expect(page.getByTestId('auth-modal')).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe('login first', () => {
|
||||
let user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
user = await createRandomUser();
|
||||
await loginUser(page, user);
|
||||
});
|
||||
|
||||
test('exit successfully and re-login', async ({ page }) => {
|
||||
await page.reload();
|
||||
await clickSideBarAllPageButton(page);
|
||||
await page.waitForTimeout(200);
|
||||
const url = page.url();
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspace(page);
|
||||
await clickSideBarSettingButton(page);
|
||||
await clickUserInfoCard(page);
|
||||
await page.getByTestId('sign-out-button').click();
|
||||
await page.getByTestId('confirm-sign-out-button').click();
|
||||
await page.waitForTimeout(5000);
|
||||
expect(page.url()).toBe(url);
|
||||
});
|
||||
|
||||
test('can sign out', async ({ page }) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await clickSideBarAllPageButton(page);
|
||||
const currentUrl = page.url();
|
||||
await clickSideBarUseAvatar(page);
|
||||
await page.getByTestId('workspace-modal-sign-out-option').click();
|
||||
await page.getByTestId('confirm-sign-out-button').click();
|
||||
await page.reload();
|
||||
await clickSideBarUseAvatar(page);
|
||||
const authModal = page.getByTestId('auth-modal');
|
||||
await expect(authModal).toBeVisible();
|
||||
expect(page.url()).toBe(currentUrl);
|
||||
});
|
||||
|
||||
test('can see and change email and password in setting panel', async ({
|
||||
page,
|
||||
}) => {
|
||||
const newName = 'test name';
|
||||
{
|
||||
await clickSideBarSettingButton(page);
|
||||
const locator = page.getByTestId('user-info-card');
|
||||
await expect(locator.getByText(user.email)).toBeVisible();
|
||||
await expect(locator.getByText(user.name)).toBeVisible();
|
||||
await locator.click({
|
||||
delay: 50,
|
||||
});
|
||||
const nameInput = page.getByPlaceholder('Input account name');
|
||||
await nameInput.clear();
|
||||
await nameInput.pressSequentially(newName, {
|
||||
delay: 50,
|
||||
});
|
||||
await page.getByTestId('save-user-name').click({
|
||||
delay: 50,
|
||||
});
|
||||
}
|
||||
await page.reload();
|
||||
{
|
||||
await clickSideBarSettingButton(page);
|
||||
const locator = page.getByTestId('user-info-card');
|
||||
await expect(locator.getByText(user.email)).toBeVisible();
|
||||
await expect(locator.getByText(newName)).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
1
tests/affine-cloud/e2e/logo.svg
Normal file
1
tests/affine-cloud/e2e/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><style>.cls-1{fill:#000000;}</style></defs><g id="_20230202-pull_request"><path class="cls-1" d="m512,133.82L342.5,0,0,67.88v310.26l169.56,133.86,342.44-67.88V133.82Zm-203.39-84.81v89.67l51.84-10.27v-56.25l74.87,59.09-249.93,49.53-108.71-85.8,231.93-45.96ZM51.84,133.32l99.71,78.7v93.92l51.84-10.27v-76.15l256.78-50.89v210.06l-99.71-78.7v-93.92l-51.84,10.27v76.16l-256.78,50.89v-210.06Zm151.55,329.67v-89.67l-51.84,10.27v56.25l-74.87-59.09,249.93-49.53,108.71,85.8-231.93,45.96Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 608 B |
109
tests/affine-cloud/e2e/migration.spec.skip.ts
Normal file
109
tests/affine-cloud/e2e/migration.spec.skip.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
import { Path, test } from '@affine-test/kit/playwright';
|
||||
import {
|
||||
createRandomUser,
|
||||
deleteUser,
|
||||
enableCloudWorkspace,
|
||||
getLoginCookie,
|
||||
loginUser,
|
||||
runPrisma,
|
||||
} from '@affine-test/kit/utils/cloud';
|
||||
import { coreUrl } 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 { expect } from '@playwright/test';
|
||||
|
||||
let user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
test.beforeEach(async () => {
|
||||
user = await createRandomUser();
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
await loginUser(page, user, {
|
||||
beforeLogin: async () => {
|
||||
expect(await getLoginCookie(context)).toBeUndefined();
|
||||
},
|
||||
afterLogin: async () => {
|
||||
expect(await getLoginCookie(context)).toBeTruthy();
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
expect(await getLoginCookie(context)).toBeTruthy();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
// if you want to keep the user in the database for debugging,
|
||||
// comment this line
|
||||
await deleteUser(user.email);
|
||||
});
|
||||
|
||||
// TODO(@eyhn) mock migration from server data after page level upgrade implemented.
|
||||
test.skip('migration', async ({ page, browser }) => {
|
||||
let workspaceId: string;
|
||||
{
|
||||
// create the old cloud workspace in another browser
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await loginUser(page, user);
|
||||
await page.reload();
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspace(page);
|
||||
await clickNewPageButton(page);
|
||||
await waitForEditorLoad(page);
|
||||
// http://localhost:8080/workspace/2bc0b6c8-f68d-4dd3-98a8-be746754f9e1/xxx
|
||||
workspaceId = page.url().split('/')[4];
|
||||
await runPrisma(async client => {
|
||||
const sqls = (
|
||||
await readFile(
|
||||
Path.dir(import.meta.url).join(
|
||||
'fixtures',
|
||||
'0.9.0-canary.9-snapshots.sql'
|
||||
).value,
|
||||
'utf-8'
|
||||
)
|
||||
)
|
||||
.replaceAll('2bc0b6c8-f68d-4dd3-98a8-be746754f9e1', workspaceId)
|
||||
.split('\n');
|
||||
await client.snapshot.deleteMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
for (const sql of sqls) {
|
||||
await client.$executeRawUnsafe(sql);
|
||||
}
|
||||
});
|
||||
await page.close();
|
||||
}
|
||||
await page.reload();
|
||||
await page.waitForTimeout(1000);
|
||||
await page.goto(`${coreUrl}/workspace/${workspaceId}/all`);
|
||||
await page.getByTestId('upgrade-workspace-button').click();
|
||||
await expect(page.getByText('Refresh Current Page')).toBeVisible({
|
||||
timeout: 60000,
|
||||
});
|
||||
await page.goto(
|
||||
// page 'F1SX6cgNxy' has edgeless mode
|
||||
`${coreUrl}/workspace/${workspaceId}/F1SX6cgNxy`
|
||||
);
|
||||
await page.waitForTimeout(5000);
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
});
|
||||
59
tests/affine-cloud/e2e/open-in-app.spec.ts
Normal file
59
tests/affine-cloud/e2e/open-in-app.spec.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { test } from '@affine-test/kit/playwright';
|
||||
import {
|
||||
createRandomUser,
|
||||
deleteUser,
|
||||
enableCloudWorkspace,
|
||||
loginUser,
|
||||
} from '@affine-test/kit/utils/cloud';
|
||||
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
let user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
user = await createRandomUser();
|
||||
await loginUser(page, user);
|
||||
await enableCloudWorkspace(page);
|
||||
await waitForEditorLoad(page);
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
// if you want to keep the user in the database for debugging,
|
||||
// comment this line
|
||||
await deleteUser(user.email);
|
||||
});
|
||||
|
||||
test('open in app card should be shown for cloud workspace', async ({
|
||||
page,
|
||||
}) => {
|
||||
await expect(page.getByTestId('open-in-app-card')).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByTestId('open-in-app-card')
|
||||
.getByRole('checkbox', {
|
||||
name: 'Remember choice',
|
||||
})
|
||||
.locator('input')
|
||||
.click();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Dismiss',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(page.getByTestId('open-in-app-card')).not.toBeInViewport();
|
||||
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByTestId('open-in-app-card')).not.toBeInViewport();
|
||||
|
||||
// seems there is no way to bypass the open popup blocker via playwright
|
||||
});
|
||||
185
tests/affine-cloud/e2e/page-history.spec.ts
Normal file
185
tests/affine-cloud/e2e/page-history.spec.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { test } from '@affine-test/kit/playwright';
|
||||
import {
|
||||
createRandomUser,
|
||||
deleteUser,
|
||||
enableCloudWorkspace,
|
||||
loginUser,
|
||||
runPrisma,
|
||||
} from '@affine-test/kit/utils/cloud';
|
||||
import {
|
||||
getBlockSuiteEditorTitle,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
let user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
user = await createRandomUser();
|
||||
await loginUser(page, user);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
// if you want to keep the user in the database for debugging,
|
||||
// comment this line
|
||||
await deleteUser(user.email);
|
||||
});
|
||||
|
||||
test('newly created page shows empty history', async ({ page }) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspace(page);
|
||||
|
||||
// click the history button
|
||||
await page.getByTestId('header-dropDownButton').click();
|
||||
await page.getByTestId('editor-option-menu-history').click();
|
||||
|
||||
const modal = page.getByTestId('page-history-modal');
|
||||
|
||||
// expect history modal shown up
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
await expect(page.getByTestId('empty-history-prompt')).toBeVisible();
|
||||
});
|
||||
|
||||
const pushCurrentPageUpdates = async (page: Page) => {
|
||||
const [workspaceId, guid, updates, state] = await page.evaluate(() => {
|
||||
// @ts-expect-error ignore the type error
|
||||
const Y = window.Y;
|
||||
// @ts-expect-error ignore the type error
|
||||
const spaceDoc = window.currentEditor.page.spaceDoc;
|
||||
// @ts-expect-error ignore the type error
|
||||
const workspaceId: string = window.currentWorkspace.id;
|
||||
const updates: Uint8Array = Y.encodeStateAsUpdate(spaceDoc);
|
||||
const state: Uint8Array = Y.encodeStateVector(spaceDoc);
|
||||
|
||||
return [workspaceId, spaceDoc.guid, [...updates], [...state]] as const;
|
||||
});
|
||||
|
||||
const toBuffer = (arr: readonly number[]) => Buffer.from(arr);
|
||||
|
||||
await runPrisma(async client => {
|
||||
await client.snapshotHistory.create({
|
||||
data: {
|
||||
workspaceId: workspaceId,
|
||||
id: guid,
|
||||
blob: toBuffer(updates),
|
||||
state: toBuffer(state),
|
||||
timestamp: new Date(Date.now() - 10000),
|
||||
expiredAt: new Date(Date.now() + 100000),
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
test('can restore page to a history version', async ({ page }) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspace(page);
|
||||
|
||||
await pushCurrentPageUpdates(page);
|
||||
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
|
||||
await title.focus();
|
||||
|
||||
await title.pressSequentially('TEST TITLE', {
|
||||
delay: 50,
|
||||
});
|
||||
|
||||
// write something and push to history
|
||||
await pushCurrentPageUpdates(page);
|
||||
|
||||
await title.selectText();
|
||||
|
||||
await title.press('Backspace');
|
||||
|
||||
await title.pressSequentially('New Title', {
|
||||
delay: 50,
|
||||
});
|
||||
|
||||
// click the history button
|
||||
await page.getByTestId('header-dropDownButton').click();
|
||||
await page.getByTestId('editor-option-menu-history').click();
|
||||
|
||||
const modal = page.getByTestId('page-history-modal');
|
||||
|
||||
// expect history modal shown up
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// expect history list to have 2 items
|
||||
await expect(modal.getByTestId('version-history-item')).toHaveCount(2);
|
||||
|
||||
// check the first item in the preview should have title 'TEST TITLE'
|
||||
await expect(modal.locator('[data-block-is-title]')).toHaveText('TEST TITLE');
|
||||
|
||||
// click restore
|
||||
await modal
|
||||
.getByRole('button', {
|
||||
name: 'Restore current version',
|
||||
})
|
||||
.click();
|
||||
|
||||
const confirm = page.getByTestId('confirm-restore-history-modal');
|
||||
|
||||
// expect confirm dialog to show up
|
||||
await expect(confirm).toBeVisible();
|
||||
|
||||
// click restore
|
||||
await confirm
|
||||
.getByRole('button', {
|
||||
name: 'Restore',
|
||||
})
|
||||
.click();
|
||||
|
||||
// title should be restored to 'TEST TITLE'
|
||||
await expect(title).toContainText('TEST TITLE');
|
||||
});
|
||||
|
||||
test('empty history doc should not show starter bar', async ({ page }) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace({ name: 'test' }, page);
|
||||
await enableCloudWorkspace(page);
|
||||
|
||||
await pushCurrentPageUpdates(page);
|
||||
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.focus();
|
||||
await title.pressSequentially('TEST TITLE', {
|
||||
delay: 50,
|
||||
});
|
||||
// write something and push to history
|
||||
await pushCurrentPageUpdates(page);
|
||||
|
||||
// click the history button
|
||||
await page.getByTestId('header-dropDownButton').click();
|
||||
await page.getByTestId('editor-option-menu-history').click();
|
||||
|
||||
const modal = page.getByTestId('page-history-modal');
|
||||
|
||||
// expect history modal shown up
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
const aiBadge = modal.getByTestId('start-with-ai-badge');
|
||||
await expect(aiBadge).not.toBeVisible();
|
||||
});
|
||||
257
tests/affine-cloud/e2e/share-page-1.spec.ts
Normal file
257
tests/affine-cloud/e2e/share-page-1.spec.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import { skipOnboarding, test } from '@affine-test/kit/playwright';
|
||||
import {
|
||||
createRandomUser,
|
||||
enableCloudWorkspaceFromShareButton,
|
||||
enableShare,
|
||||
loginUser,
|
||||
} from '@affine-test/kit/utils/cloud';
|
||||
import { clickEdgelessModeButton } from '@affine-test/kit/utils/editor';
|
||||
import { importImage } from '@affine-test/kit/utils/image';
|
||||
import {
|
||||
getBlockSuiteEditorTitle,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
let user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
user = await createRandomUser();
|
||||
await loginUser(page, user);
|
||||
});
|
||||
|
||||
test('can enable share page', async ({ page, browser }) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspaceFromShareButton(page);
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.pressSequentially('TEST TITLE', {
|
||||
delay: 50,
|
||||
});
|
||||
await page.keyboard.press('Enter', { delay: 50 });
|
||||
await page.keyboard.type('TEST CONTENT', { delay: 50 });
|
||||
|
||||
// enable share page and copy page link
|
||||
await enableShare(page);
|
||||
await page.getByTestId('share-menu-copy-link-button').click();
|
||||
await page.getByTestId('share-link-menu-copy-page').click();
|
||||
|
||||
// check share page is accessible
|
||||
{
|
||||
const context = await browser.newContext();
|
||||
await skipOnboarding(context);
|
||||
const url: string = await page.evaluate(() =>
|
||||
navigator.clipboard.readText()
|
||||
);
|
||||
const page2 = await context.newPage();
|
||||
await page2.goto(url);
|
||||
await waitForEditorLoad(page2);
|
||||
const title = getBlockSuiteEditorTitle(page2);
|
||||
await expect(title).toContainText('TEST TITLE');
|
||||
await expect(page2.locator('affine-paragraph').first()).toContainText(
|
||||
'TEST CONTENT'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('share page should have toc', async ({ page, browser }) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspaceFromShareButton(page);
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.pressSequentially('TEST TITLE', {
|
||||
delay: 50,
|
||||
});
|
||||
await page.keyboard.press('Enter', { delay: 50 });
|
||||
|
||||
await page.keyboard.type('# Heading 1');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.type('# Heading 2');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// enable share page and copy page link
|
||||
await enableShare(page);
|
||||
await page.getByTestId('share-menu-copy-link-button').click();
|
||||
await page.getByTestId('share-link-menu-copy-page').click();
|
||||
|
||||
// check share page is accessible
|
||||
{
|
||||
const context = await browser.newContext();
|
||||
await skipOnboarding(context);
|
||||
const url: string = await page.evaluate(() =>
|
||||
navigator.clipboard.readText()
|
||||
);
|
||||
const page2 = await context.newPage();
|
||||
await page2.goto(url);
|
||||
await waitForEditorLoad(page2);
|
||||
|
||||
const tocIndicators = page2.locator(
|
||||
'affine-outline-viewer .outline-viewer-indicator'
|
||||
);
|
||||
await expect(tocIndicators).toHaveCount(3);
|
||||
await expect(tocIndicators.nth(0)).toBeVisible();
|
||||
await expect(tocIndicators.nth(1)).toBeVisible();
|
||||
await expect(tocIndicators.nth(2)).toBeVisible();
|
||||
|
||||
const viewer = page2.locator('affine-outline-viewer');
|
||||
await tocIndicators.first().hover({ force: true });
|
||||
await expect(viewer).toBeVisible();
|
||||
|
||||
const toggleButton = viewer.locator(
|
||||
'[data-testid="toggle-outline-panel-button"]'
|
||||
);
|
||||
await expect(toggleButton).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('append paragraph should be disabled in shared mode', async ({
|
||||
page,
|
||||
browser,
|
||||
}) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspaceFromShareButton(page);
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.pressSequentially('TEST TITLE', {
|
||||
delay: 50,
|
||||
});
|
||||
|
||||
// enable share page and copy page link
|
||||
await enableShare(page);
|
||||
await page.getByTestId('share-menu-copy-link-button').click();
|
||||
await page.getByTestId('share-link-menu-copy-page').click();
|
||||
|
||||
{
|
||||
const context = await browser.newContext();
|
||||
await skipOnboarding(context);
|
||||
const url: string = await page.evaluate(() =>
|
||||
navigator.clipboard.readText()
|
||||
);
|
||||
const page2 = await context.newPage();
|
||||
await page2.goto(url);
|
||||
await waitForEditorLoad(page2);
|
||||
|
||||
const paragraph = page2.locator('affine-paragraph');
|
||||
const numParagraphs = await paragraph.count();
|
||||
|
||||
let error = null;
|
||||
try {
|
||||
await page2.locator('[data-testid=page-editor-blank]').click();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).toBeNull();
|
||||
|
||||
await expect(paragraph).toHaveCount(numParagraphs);
|
||||
}
|
||||
});
|
||||
|
||||
test('share page with default edgeless', async ({ page, browser }) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspaceFromShareButton(page);
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.pressSequentially('TEST TITLE', {
|
||||
delay: 50,
|
||||
});
|
||||
await page.keyboard.press('Enter', { delay: 50 });
|
||||
await page.keyboard.type('TEST CONTENT', { delay: 50 });
|
||||
await clickEdgelessModeButton(page);
|
||||
await expect(page.locator('affine-edgeless-root')).toBeVisible({
|
||||
timeout: 1000,
|
||||
});
|
||||
|
||||
// enable share page and copy page link
|
||||
await enableShare(page);
|
||||
await page.getByTestId('share-menu-copy-link-button').click();
|
||||
await page.getByTestId('share-link-menu-copy-edgeless').click();
|
||||
|
||||
// check share page is accessible
|
||||
{
|
||||
const context = await browser.newContext();
|
||||
await skipOnboarding(context);
|
||||
const url = new URL(
|
||||
await page.evaluate(() => navigator.clipboard.readText())
|
||||
);
|
||||
url.searchParams.delete('mode');
|
||||
const page2 = await context.newPage();
|
||||
await page2.goto(url.toString());
|
||||
await waitForEditorLoad(page2);
|
||||
await expect(page2.locator('affine-edgeless-root')).toBeVisible({
|
||||
timeout: 1000,
|
||||
});
|
||||
await expect(page2.locator('affine-paragraph').first()).toContainText(
|
||||
'TEST CONTENT'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('image preview should be shown', async ({ page, browser }) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspaceFromShareButton(page);
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.click();
|
||||
await page.keyboard.press('Enter');
|
||||
await importImage(page, 'large-image.png');
|
||||
|
||||
// enable share page and copy page link
|
||||
await enableShare(page);
|
||||
await page.getByTestId('share-menu-copy-link-button').click();
|
||||
await page.getByTestId('share-link-menu-copy-page').click();
|
||||
|
||||
// check share page is accessible
|
||||
{
|
||||
const context = await browser.newContext();
|
||||
await skipOnboarding(context);
|
||||
const url: string = await page.evaluate(() =>
|
||||
navigator.clipboard.readText()
|
||||
);
|
||||
const page2 = await context.newPage();
|
||||
await page2.goto(url);
|
||||
await waitForEditorLoad(page2);
|
||||
|
||||
await page.locator('affine-page-image').first().dblclick();
|
||||
const locator = page.getByTestId('image-preview-modal');
|
||||
await expect(locator).toBeVisible();
|
||||
await page.getByTestId('image-preview-close-button').first().click();
|
||||
await page.waitForTimeout(500);
|
||||
await expect(locator).not.toBeVisible();
|
||||
}
|
||||
});
|
||||
284
tests/affine-cloud/e2e/share-page-2.spec.ts
Normal file
284
tests/affine-cloud/e2e/share-page-2.spec.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { skipOnboarding, test } from '@affine-test/kit/playwright';
|
||||
import { importAttachment } from '@affine-test/kit/utils/attachment';
|
||||
import {
|
||||
createRandomUser,
|
||||
enableCloudWorkspaceFromShareButton,
|
||||
enableShare,
|
||||
loginUser,
|
||||
} from '@affine-test/kit/utils/cloud';
|
||||
import { getParagraphIds, locateToolbar } from '@affine-test/kit/utils/editor';
|
||||
import { copyByKeyboard } from '@affine-test/kit/utils/keyboard';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
getBlockSuiteEditorTitle,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { setSelection } from '@affine-test/kit/utils/selection';
|
||||
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
let user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
user = await createRandomUser();
|
||||
await loginUser(page, user);
|
||||
});
|
||||
|
||||
test('The reference links in the shared page should be accessible normally and can go back and forward', async ({
|
||||
page,
|
||||
browser,
|
||||
}) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspaceFromShareButton(page);
|
||||
|
||||
// create linked page and share
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.pressSequentially('Test linked doc', {
|
||||
delay: 50,
|
||||
});
|
||||
await page.keyboard.press('Enter', { delay: 50 });
|
||||
await page.keyboard.type('Test linked content', { delay: 50 });
|
||||
await enableShare(page);
|
||||
|
||||
// create a new page and link to the shared page
|
||||
await clickNewPageButton(page, 'Test Page');
|
||||
await waitForEditorLoad(page);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.type('@', { delay: 50 });
|
||||
const linkedPagePopover = page.locator('.linked-doc-popover');
|
||||
await expect(linkedPagePopover).toBeVisible();
|
||||
await page.keyboard.type('Test linked doc', { delay: 50 });
|
||||
await page.locator('icon-button:has-text("Test linked doc")').first().click();
|
||||
|
||||
// enable share page and copy page link
|
||||
await enableShare(page);
|
||||
await page.getByTestId('share-menu-copy-link-button').click();
|
||||
await page.getByTestId('share-link-menu-copy-page').click();
|
||||
|
||||
// check share page is accessible
|
||||
{
|
||||
const context = await browser.newContext();
|
||||
await skipOnboarding(context);
|
||||
const url: string = await page.evaluate(() =>
|
||||
navigator.clipboard.readText()
|
||||
);
|
||||
const page2 = await context.newPage();
|
||||
await page2.goto(url);
|
||||
await waitForEditorLoad(page2);
|
||||
const title = getBlockSuiteEditorTitle(page2);
|
||||
await expect(title).toContainText('Test Page');
|
||||
|
||||
// check linked page
|
||||
const link = page2.locator('.affine-reference');
|
||||
await expect(link).toBeVisible();
|
||||
await expect(link).toContainText('Test linked doc');
|
||||
await link.click();
|
||||
await waitForEditorLoad(page2);
|
||||
await expect(
|
||||
page2.locator('.doc-title-container:has-text("Test linked doc")')
|
||||
).toBeVisible();
|
||||
await expect(page2.locator('affine-paragraph').first()).toContainText(
|
||||
'Test linked content'
|
||||
);
|
||||
|
||||
// go back and forward
|
||||
await page2.goBack();
|
||||
await waitForEditorLoad(page2);
|
||||
await expect(title).toContainText('Test Page');
|
||||
await expect(link).toBeVisible();
|
||||
await expect(link).toContainText('Test linked doc');
|
||||
|
||||
await page2.goForward();
|
||||
await waitForEditorLoad(page2);
|
||||
await expect(
|
||||
page2.locator('.doc-title-container:has-text("Test linked doc")')
|
||||
).toBeVisible();
|
||||
await expect(page2.locator('affine-paragraph').first()).toContainText(
|
||||
'Test linked content'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('Should show no permission page when the share page is not found', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('http://localhost:8080/workspace/abc/123');
|
||||
|
||||
await expect(
|
||||
page.getByText(
|
||||
'Sorry, you do not have access or this content does not exist...'
|
||||
)
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Inline latex modal should be not shown in shared mode when clicking', async ({
|
||||
page,
|
||||
browser,
|
||||
}) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspaceFromShareButton(page);
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.pressSequentially('TEST TITLE', {
|
||||
delay: 50,
|
||||
});
|
||||
await page.keyboard.press('Enter', { delay: 50 });
|
||||
|
||||
await page.keyboard.type('$$E=mc^2$$');
|
||||
await page.keyboard.press('Space');
|
||||
|
||||
// there should be a inline latex node
|
||||
const latexLocator = page.locator('affine-latex-node');
|
||||
await expect(latexLocator).toBeVisible();
|
||||
|
||||
// click the latex node
|
||||
// the latex editor should be shown when the doc can be editing
|
||||
await latexLocator.click();
|
||||
const modalLocator = page.locator('.latex-editor-container');
|
||||
await expect(modalLocator).toBeVisible();
|
||||
|
||||
// enable share page and copy page link
|
||||
await enableShare(page);
|
||||
await page.getByTestId('share-menu-copy-link-button').click();
|
||||
await page.getByTestId('share-link-menu-copy-page').click();
|
||||
|
||||
// check share page is accessible
|
||||
{
|
||||
const context = await browser.newContext();
|
||||
await skipOnboarding(context);
|
||||
const url: string = await page.evaluate(() =>
|
||||
navigator.clipboard.readText()
|
||||
);
|
||||
const page2 = await context.newPage();
|
||||
await page2.goto(url);
|
||||
await waitForEditorLoad(page2);
|
||||
|
||||
// click the latex node
|
||||
const latexLocator = page2.locator('affine-latex-node');
|
||||
await latexLocator.click();
|
||||
|
||||
// the latex editor should not be shown when the doc is readonly
|
||||
const modalLocator = page2.locator('.latex-editor-container');
|
||||
await expect(modalLocator).not.toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('share page should support copying content', async ({ page, browser }) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspaceFromShareButton(page);
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.pressSequentially('TEST TITLE', {
|
||||
delay: 50,
|
||||
});
|
||||
await page.keyboard.press('Enter', { delay: 50 });
|
||||
await page.keyboard.type('Hello World');
|
||||
|
||||
// enable share page and copy page link
|
||||
await enableShare(page);
|
||||
await page.getByTestId('share-menu-copy-link-button').click();
|
||||
await page.getByTestId('share-link-menu-copy-page').click();
|
||||
|
||||
// check share page is accessible and content can be copied
|
||||
{
|
||||
const context = await browser.newContext();
|
||||
await skipOnboarding(context);
|
||||
const url: string = await page.evaluate(() =>
|
||||
navigator.clipboard.readText()
|
||||
);
|
||||
const page2 = await context.newPage();
|
||||
await page2.goto(url);
|
||||
await waitForEditorLoad(page2);
|
||||
|
||||
const { blockIds: paragraphIds } = await getParagraphIds(page2);
|
||||
await setSelection(page2, paragraphIds[0], 0, paragraphIds[0], 11);
|
||||
await copyByKeyboard(page2);
|
||||
|
||||
// Verify copied content
|
||||
const copiedText = await page2.evaluate(() =>
|
||||
navigator.clipboard.readText()
|
||||
);
|
||||
expect(copiedText).toContain('Hello World');
|
||||
}
|
||||
});
|
||||
|
||||
test('should enable opening peek view with pdf viewer in readonly and sharing modes', async ({
|
||||
page,
|
||||
browser,
|
||||
}) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspaceFromShareButton(page);
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.click();
|
||||
await page.keyboard.press('Enter');
|
||||
await importAttachment(page, 'lorem-ipsum.pdf');
|
||||
|
||||
const toolbar = locateToolbar(page);
|
||||
const switchViewButton = toolbar.getByLabel('Switch view');
|
||||
const embedViewButton = toolbar.getByLabel('Embed view');
|
||||
|
||||
const attachment = page.locator('affine-attachment');
|
||||
await attachment.click();
|
||||
|
||||
await switchViewButton.click();
|
||||
await embedViewButton.click();
|
||||
|
||||
await expect(attachment.locator('lit-react-portal')).toBeVisible();
|
||||
|
||||
// enable share page and copy page link
|
||||
await enableShare(page);
|
||||
await page.getByTestId('share-menu-copy-link-button').click();
|
||||
await page.getByTestId('share-link-menu-copy-page').click();
|
||||
|
||||
// check share page is accessible
|
||||
{
|
||||
const context = await browser.newContext();
|
||||
await skipOnboarding(context);
|
||||
const url: string = await page.evaluate(() =>
|
||||
navigator.clipboard.readText()
|
||||
);
|
||||
const page2 = await context.newPage();
|
||||
await page2.goto(url);
|
||||
await waitForEditorLoad(page2);
|
||||
|
||||
const attachment = page2.locator('affine-attachment');
|
||||
|
||||
await expect(attachment.locator('lit-react-portal')).toBeVisible();
|
||||
|
||||
await attachment.dblclick();
|
||||
|
||||
const pdfViewer = page2.getByTestId('pdf-viewer');
|
||||
await expect(pdfViewer).not.toBeVisible();
|
||||
}
|
||||
});
|
||||
81
tests/affine-cloud/e2e/storage.spec.ts
Normal file
81
tests/affine-cloud/e2e/storage.spec.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { test } from '@affine-test/kit/playwright';
|
||||
import {
|
||||
createRandomUser,
|
||||
deleteUser,
|
||||
enableCloudWorkspace,
|
||||
loginUser,
|
||||
} from '@affine-test/kit/utils/cloud';
|
||||
import { getPageByTitle } from '@affine-test/kit/utils/page-logic';
|
||||
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
let user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
user = await createRandomUser();
|
||||
await loginUser(page, user);
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
// if you want to keep the user in the database for debugging,
|
||||
// comment this line
|
||||
await deleteUser(user.email);
|
||||
});
|
||||
|
||||
test('should delete unused blobs after permanently deleting a doc', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enableCloudWorkspace(page);
|
||||
|
||||
await clickSideBarAllPageButton(page);
|
||||
|
||||
// delete the welcome page ('Getting Started')
|
||||
await getPageByTitle(page, 'Getting Started')
|
||||
.getByTestId('doc-list-operation-button')
|
||||
.click();
|
||||
const deleteBtn = page.getByTestId('doc-list-operation-trash');
|
||||
await deleteBtn.click();
|
||||
await expect(page.getByText('Delete doc?')).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
await page.getByTestId('trash-page').click();
|
||||
await getPageByTitle(page, 'Getting Started')
|
||||
.getByTestId('delete-page-button')
|
||||
.click();
|
||||
const confirmDelete = page.getByRole('dialog', {
|
||||
name: 'Delete permanently?',
|
||||
});
|
||||
await expect(confirmDelete).toBeVisible();
|
||||
await confirmDelete
|
||||
.getByRole('button', { name: 'Delete', exact: true })
|
||||
.click();
|
||||
await expect(getPageByTitle(page, 'Getting Started')).toBeHidden();
|
||||
|
||||
await page.getByTestId('slider-bar-workspace-setting-button').click();
|
||||
await expect(page.getByTestId('setting-modal')).toBeVisible();
|
||||
await page.getByTestId('workspace-setting:storage').click();
|
||||
await expect(page.getByText(/Unused blobs \([1-9]\d*\)/)).toBeVisible();
|
||||
|
||||
// get the unused blobs count
|
||||
const count = await page.getByText(/Unused blobs \(\d+\)/).textContent();
|
||||
const unusedBlobsCount = parseInt(count?.match(/\d+/)?.[0] ?? '0');
|
||||
|
||||
expect(unusedBlobsCount).toBeGreaterThan(0);
|
||||
await expect(page.getByTestId('blob-preview-card')).toHaveCount(
|
||||
Math.min(9, unusedBlobsCount)
|
||||
);
|
||||
await page.getByTestId('blob-preview-card').nth(0).click();
|
||||
await expect(page.getByText('1 Selected')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
await expect(page.getByText('Delete blob files')).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText(`Unused blobs (${unusedBlobsCount - 1})`)
|
||||
).toBeVisible();
|
||||
});
|
||||
35
tests/affine-cloud/e2e/template.spec.ts
Normal file
35
tests/affine-cloud/e2e/template.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { test } from '@affine-test/kit/playwright';
|
||||
import { createRandomUser, loginUser } from '@affine-test/kit/utils/cloud';
|
||||
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const user = await createRandomUser();
|
||||
await loginUser(page, user);
|
||||
});
|
||||
|
||||
test('import from template should work', async ({ page }) => {
|
||||
await page.goto('https://affine.pro/templates', { waitUntil: 'load' });
|
||||
|
||||
await page.click('.template-list > a:first-child');
|
||||
const importButton = page.getByRole('button', {
|
||||
name: 'Use this template',
|
||||
});
|
||||
|
||||
const href = await importButton.evaluate((el: HTMLElement) => {
|
||||
const a = el.closest('a');
|
||||
if (!a) {
|
||||
throw new Error('Import link not found');
|
||||
}
|
||||
return a.href;
|
||||
});
|
||||
|
||||
const url = new URL(href);
|
||||
|
||||
await page.goto(url.pathname + url.search);
|
||||
|
||||
const btn = page.getByTestId('import-template-to-workspace-btn');
|
||||
|
||||
await btn.isVisible();
|
||||
await btn.click();
|
||||
await waitForEditorLoad(page);
|
||||
});
|
||||
57
tests/affine-cloud/e2e/workspace.spec.ts
Normal file
57
tests/affine-cloud/e2e/workspace.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { test } from '@affine-test/kit/playwright';
|
||||
import {
|
||||
createRandomUser,
|
||||
enableCloudWorkspace,
|
||||
loginUser,
|
||||
} from '@affine-test/kit/utils/cloud';
|
||||
import { clickPageModeButton } from '@affine-test/kit/utils/editor';
|
||||
import {
|
||||
getBlockSuiteEditorTitle,
|
||||
waitForEditorLoad,
|
||||
waitForEmptyEditor,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
let user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
user = await createRandomUser();
|
||||
await loginUser(page, user);
|
||||
});
|
||||
|
||||
test('should transform local favorites data', async ({ page }) => {
|
||||
await page.reload();
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await page
|
||||
.getByTestId('navigation-panel-bar-add-favorite-button')
|
||||
.first()
|
||||
.click();
|
||||
await clickPageModeButton(page);
|
||||
await waitForEmptyEditor(page);
|
||||
|
||||
await getBlockSuiteEditorTitle(page).fill('this is a new fav page');
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('navigation-panel-favorites')
|
||||
.locator('[draggable] >> text=this is a new fav page')
|
||||
).toBeVisible();
|
||||
|
||||
await enableCloudWorkspace(page);
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('navigation-panel-favorites')
|
||||
.locator('[draggable] >> text=this is a new fav page')
|
||||
).toBeVisible();
|
||||
});
|
||||
13
tests/affine-cloud/package.json
Normal file
13
tests/affine-cloud/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@affine-test/affine-cloud",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"e2e": "yarn playwright test --reporter=list"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@playwright/test": "=1.58.2"
|
||||
},
|
||||
"version": "0.27.5"
|
||||
}
|
||||
77
tests/affine-cloud/playwright.config.ts
Normal file
77
tests/affine-cloud/playwright.config.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
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: !process.env.CI,
|
||||
timeout: 120_000,
|
||||
outputDir: testResultDir,
|
||||
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: process.env.CI && !process.env.COPILOT ? 1 : 4,
|
||||
retries: process.env.COPILOT || !process.env.CI ? 1 : 3,
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
webServer: [
|
||||
{
|
||||
// TODO(@forehalo):
|
||||
// in ci, all the target will be built,
|
||||
// we could download the builds from archives
|
||||
// and then run the web with simple http serve, it's will be faster
|
||||
command: 'yarn run -T affine dev -p @affine/web',
|
||||
timeout: 120 * 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: 120 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
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;
|
||||
9
tests/affine-cloud/runtime-config.json
Normal file
9
tests/affine-cloud/runtime-config.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"indexer": {
|
||||
"enabled": true,
|
||||
"provider": {
|
||||
"type": "embedded",
|
||||
"endpoint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
10
tests/affine-cloud/tsconfig.json
Normal file
10
tests/affine-cloud/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.web.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./e2e",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["e2e"],
|
||||
"references": [{ "path": "../kit" }]
|
||||
}
|
||||
34
tests/affine-desktop-cloud/e2e/basic.spec.ts
Normal file
34
tests/affine-desktop-cloud/e2e/basic.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { test } from '@affine-test/kit/electron';
|
||||
import {
|
||||
createRandomUser,
|
||||
enableCloudWorkspace,
|
||||
loginUser,
|
||||
} from '@affine-test/kit/utils/cloud';
|
||||
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
|
||||
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||
|
||||
process.env.DEV_SERVER_URL = 'http://localhost:8080';
|
||||
|
||||
let user: {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
test.beforeEach(async () => {
|
||||
user = await createRandomUser();
|
||||
});
|
||||
|
||||
test('new page', async ({ page }) => {
|
||||
await loginUser(page, user, {
|
||||
isElectron: true,
|
||||
});
|
||||
await waitForEditorLoad(page);
|
||||
await createLocalWorkspace(
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
page
|
||||
);
|
||||
await enableCloudWorkspace(page);
|
||||
});
|
||||
13
tests/affine-desktop-cloud/package.json
Normal file
13
tests/affine-desktop-cloud/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@affine-test/affine-desktop-cloud",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"e2e": "yarn playwright test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@playwright/test": "=1.58.2"
|
||||
},
|
||||
"version": "0.27.5"
|
||||
}
|
||||
70
tests/affine-desktop-cloud/playwright.config.ts
Normal file
70
tests/affine-desktop-cloud/playwright.config.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { testResultDir } from '@affine-test/kit/playwright';
|
||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
// import { devices } from '@playwright/test';
|
||||
|
||||
const runtimeConfigPath = fileURLToPath(
|
||||
new URL('./runtime-config.json', import.meta.url)
|
||||
);
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
timeout: process.env.CI ? 300_000 : 60_000,
|
||||
outputDir: testResultDir,
|
||||
use: {
|
||||
viewport: { width: 1440, height: 800 },
|
||||
},
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
webServer: [
|
||||
// Intentionally not building the web, reminds you to run it by yourself.
|
||||
{
|
||||
command: 'yarn run -T affine dev -p @affine/electron-renderer',
|
||||
timeout: 120 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: 'pipe',
|
||||
env: {
|
||||
COVERAGE: process.env.COVERAGE || 'false',
|
||||
DISTRIBUTION: 'desktop',
|
||||
},
|
||||
url: 'http://localhost:8080',
|
||||
},
|
||||
{
|
||||
command: 'yarn run -T affine dev -p @affine/server',
|
||||
timeout: 120 * 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_SENDER: 'noreply@toeverything.info',
|
||||
},
|
||||
url: 'http://localhost:3010/graphql',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (process.env.CI) {
|
||||
config.retries = 3;
|
||||
config.workers = '50%';
|
||||
}
|
||||
|
||||
export default config;
|
||||
9
tests/affine-desktop-cloud/runtime-config.json
Normal file
9
tests/affine-desktop-cloud/runtime-config.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"indexer": {
|
||||
"enabled": true,
|
||||
"provider": {
|
||||
"type": "embedded",
|
||||
"endpoint": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
10
tests/affine-desktop-cloud/tsconfig.json
Normal file
10
tests/affine-desktop-cloud/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.web.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./e2e",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["e2e"],
|
||||
"references": [{ "path": "../kit" }]
|
||||
}
|
||||
174
tests/affine-desktop/e2e/basic.spec.ts
Normal file
174
tests/affine-desktop/e2e/basic.spec.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { test } from '@affine-test/kit/electron';
|
||||
import {
|
||||
ensureInEdgelessMode,
|
||||
ensureInPageMode,
|
||||
} from '@affine-test/kit/utils/editor';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
getBlockSuiteEditorTitle,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { clickSideBarSettingButton } from '@affine-test/kit/utils/sidebar';
|
||||
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
const historyShortcut = async (page: Page, command: 'goBack' | 'goForward') => {
|
||||
await page.keyboard.press(
|
||||
command === 'goBack' ? 'ControlOrMeta+[' : 'ControlOrMeta+]'
|
||||
);
|
||||
};
|
||||
|
||||
const setNewDocDefaultMode = async (
|
||||
page: Page,
|
||||
mode: 'page' | 'edgeless' | 'ask'
|
||||
) => {
|
||||
const modeTriggerByValue = {
|
||||
page: 'page-mode-trigger',
|
||||
edgeless: 'edgeless-mode-trigger',
|
||||
ask: 'ask-every-time-trigger',
|
||||
} as const;
|
||||
|
||||
await clickSideBarSettingButton(page);
|
||||
await page.getByTestId('editor-panel-trigger').click();
|
||||
await page.getByTestId('new-doc-default-mode-trigger').click();
|
||||
await page.getByTestId(modeTriggerByValue[mode]).click();
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
};
|
||||
|
||||
test('new page', async ({ page, workspace }) => {
|
||||
await clickNewPageButton(page);
|
||||
const flavour = (await workspace.current()).meta.flavour;
|
||||
expect(flavour).toBe('local');
|
||||
});
|
||||
|
||||
test('application menu respects default new doc mode', async ({
|
||||
electronApp,
|
||||
page,
|
||||
}) => {
|
||||
await waitForEditorLoad(page);
|
||||
await ensureInPageMode(page);
|
||||
|
||||
await setNewDocDefaultMode(page, 'edgeless');
|
||||
await electronApp.evaluate(({ BrowserWindow, Menu }) => {
|
||||
const menuItem =
|
||||
Menu.getApplicationMenu()?.getMenuItemById('affine:new-page');
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
|
||||
if (!menuItem) {
|
||||
throw new Error('Missing application menu item: affine:new-page');
|
||||
}
|
||||
if (!focusedWindow) {
|
||||
throw new Error('Missing focused window for application menu dispatch');
|
||||
}
|
||||
|
||||
menuItem.click(undefined, focusedWindow, focusedWindow.webContents);
|
||||
});
|
||||
|
||||
await ensureInEdgelessMode(page);
|
||||
});
|
||||
|
||||
test('app sidebar router forward/back', async ({ page }) => {
|
||||
// create pages
|
||||
await page.waitForTimeout(500);
|
||||
await clickNewPageButton(page);
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.focus();
|
||||
await title.pressSequentially('test1', {
|
||||
delay: 100,
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
await clickNewPageButton(page);
|
||||
|
||||
await title.focus();
|
||||
await title.pressSequentially('test2', {
|
||||
delay: 100,
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
await clickNewPageButton(page);
|
||||
await title.focus();
|
||||
await title.pressSequentially('test3', {
|
||||
delay: 100,
|
||||
});
|
||||
await expect(getBlockSuiteEditorTitle(page)).toHaveText('test3');
|
||||
|
||||
await page.click('[data-testid="app-navigation-button-back"]');
|
||||
await page.click('[data-testid="app-navigation-button-back"]');
|
||||
await expect(getBlockSuiteEditorTitle(page)).toHaveText('test1');
|
||||
await page.click('[data-testid="app-navigation-button-forward"]');
|
||||
await page.click('[data-testid="app-navigation-button-forward"]');
|
||||
await expect(getBlockSuiteEditorTitle(page)).toHaveText('test3');
|
||||
await historyShortcut(page, 'goBack');
|
||||
await historyShortcut(page, 'goBack');
|
||||
await expect(getBlockSuiteEditorTitle(page)).toHaveText('test1');
|
||||
await historyShortcut(page, 'goForward');
|
||||
await historyShortcut(page, 'goForward');
|
||||
await expect(getBlockSuiteEditorTitle(page)).toHaveText('test3');
|
||||
});
|
||||
|
||||
test('clientBorder value should disable by default on window', async ({
|
||||
page,
|
||||
}) => {
|
||||
await clickSideBarSettingButton(page);
|
||||
await page.waitForTimeout(1000);
|
||||
const settingItem = page.locator(
|
||||
'[data-testid="client-border-style-trigger"]'
|
||||
);
|
||||
expect(await settingItem.locator('input').inputValue()).toEqual(
|
||||
process.platform === 'win32' ? 'off' : 'on'
|
||||
);
|
||||
});
|
||||
|
||||
test('app theme', async ({ page, electronApp }) => {
|
||||
const root = page.locator('html');
|
||||
{
|
||||
const themeMode = await root.evaluate(element => element.dataset.theme);
|
||||
expect(themeMode).toBe('light');
|
||||
|
||||
const theme = await electronApp.evaluate(({ nativeTheme }) => {
|
||||
return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
||||
});
|
||||
|
||||
expect(theme).toBe('light');
|
||||
}
|
||||
|
||||
{
|
||||
await page.getByTestId('settings-modal-trigger').click();
|
||||
await page.getByTestId('appearance-panel-trigger').click();
|
||||
await page.waitForTimeout(50);
|
||||
await page.getByTestId('dark-theme-trigger').click();
|
||||
const themeMode = await root.evaluate(element => element.dataset.theme);
|
||||
expect(themeMode).toBe('dark');
|
||||
const theme = await electronApp.evaluate(({ nativeTheme }) => {
|
||||
return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
||||
});
|
||||
expect(theme).toBe('dark');
|
||||
}
|
||||
});
|
||||
|
||||
test('windows only check', async ({ page }) => {
|
||||
const windowOnlyUI = page.locator('[data-platform-target=win32]');
|
||||
if (process.platform === 'win32') {
|
||||
await expect(windowOnlyUI.first()).toBeVisible();
|
||||
} else {
|
||||
await expect(windowOnlyUI.first()).not.toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('delete workspace', async ({ page }) => {
|
||||
await clickNewPageButton(page);
|
||||
|
||||
await createLocalWorkspace({ name: 'Delete Me' }, page);
|
||||
await page.waitForTimeout(1000);
|
||||
await clickSideBarSettingButton(page);
|
||||
await page.getByTestId('workspace-setting:preference').click();
|
||||
await expect(page.getByTestId('workspace-name-input')).toHaveValue(
|
||||
'Delete Me'
|
||||
);
|
||||
await page.getByTestId('delete-workspace-button').click();
|
||||
await page.getByTestId('delete-workspace-input').fill('Delete Me');
|
||||
await page.getByTestId('delete-workspace-confirm-button').click();
|
||||
await expect(page.getByTestId('workspace-name')).toContainText(
|
||||
'Demo Workspace'
|
||||
);
|
||||
});
|
||||
53
tests/affine-desktop/e2e/image.spec.ts
Normal file
53
tests/affine-desktop/e2e/image.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { test } from '@affine-test/kit/electron';
|
||||
import { importImage } from '@affine-test/kit/utils/image';
|
||||
import { pasteByKeyboard } from '@affine-test/kit/utils/keyboard';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
getBlockSuiteEditorTitle,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test('should be able to insert SVG images', async ({ page }) => {
|
||||
await page.waitForTimeout(500);
|
||||
await clickNewPageButton(page);
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await importImage(page, 'affine.svg');
|
||||
|
||||
const svg = page.locator('affine-image').first();
|
||||
await expect(svg).toBeVisible();
|
||||
});
|
||||
|
||||
test('should paste it as PNG after copying SVG', async ({ page }) => {
|
||||
await page.waitForTimeout(500);
|
||||
await clickNewPageButton(page);
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await importImage(page, 'affine.svg');
|
||||
|
||||
const svg = page.locator('affine-image').first();
|
||||
await expect(svg).toBeVisible();
|
||||
|
||||
await svg.hover();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
||||
await expect(toolbar).toBeVisible();
|
||||
|
||||
const moreMenu = toolbar.getByLabel('More menu');
|
||||
await moreMenu.click();
|
||||
|
||||
await moreMenu.getByRole('button', { name: /^Copy$/ }).click();
|
||||
|
||||
await svg.click();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
await pasteByKeyboard(page);
|
||||
|
||||
const png = page.locator('affine-image').nth(1);
|
||||
await expect(png).toBeVisible();
|
||||
});
|
||||
135
tests/affine-desktop/e2e/split-view.spec.ts
Normal file
135
tests/affine-desktop/e2e/split-view.spec.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { test } from '@affine-test/kit/electron';
|
||||
import {
|
||||
expectActiveTab,
|
||||
expectTabTitle,
|
||||
} from '@affine-test/kit/utils/app-tabs';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
createLinkedPage,
|
||||
dragTo,
|
||||
getPageByTitle,
|
||||
waitForAllPagesLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
const SPLIT_VIEW_READY_TIMEOUT = 30_000;
|
||||
|
||||
test('open split view', async ({ page }) => {
|
||||
await clickNewPageButton(page);
|
||||
await page.waitForTimeout(500);
|
||||
await page.keyboard.press('Enter');
|
||||
await createLinkedPage(page, 'hi from another page');
|
||||
await page
|
||||
.locator('.affine-reference-title:has-text("hi from another page")')
|
||||
.click({
|
||||
modifiers: ['ControlOrMeta', 'Alt'],
|
||||
});
|
||||
await expect(page.locator('.doc-title-container')).toHaveCount(2, {
|
||||
timeout: SPLIT_VIEW_READY_TIMEOUT,
|
||||
});
|
||||
|
||||
// check tab title
|
||||
await expect(page.getByTestId('split-view-label')).toHaveCount(2, {
|
||||
timeout: SPLIT_VIEW_READY_TIMEOUT,
|
||||
});
|
||||
await expectTabTitle(
|
||||
page,
|
||||
0,
|
||||
['Untitled', 'hi from another page'],
|
||||
SPLIT_VIEW_READY_TIMEOUT
|
||||
);
|
||||
|
||||
// the first split view should be active
|
||||
await expectActiveTab(page, 0, 0);
|
||||
|
||||
// by clicking the first split view label, the first split view should be active
|
||||
await page.getByTestId('split-view-label').nth(0).click();
|
||||
await expectActiveTab(page, 0, 0);
|
||||
await expect(page.getByTestId('split-view-indicator').nth(0)).toHaveAttribute(
|
||||
'data-active',
|
||||
'true'
|
||||
);
|
||||
|
||||
const firstDragHandle = page
|
||||
.getByTestId('split-view-panel')
|
||||
.first()
|
||||
.getByTestId('split-view-indicator');
|
||||
|
||||
await dragTo(
|
||||
page,
|
||||
firstDragHandle,
|
||||
page.getByTestId('split-view-panel').last(),
|
||||
'center',
|
||||
true
|
||||
);
|
||||
|
||||
await expectTabTitle(
|
||||
page,
|
||||
0,
|
||||
['hi from another page', 'Untitled'],
|
||||
SPLIT_VIEW_READY_TIMEOUT
|
||||
);
|
||||
});
|
||||
|
||||
test('open split view in all docs (operations button)', async ({ page }) => {
|
||||
const testTitle = 'test-page';
|
||||
await clickNewPageButton(page, testTitle);
|
||||
await clickSideBarAllPageButton(page);
|
||||
await waitForAllPagesLoad(page);
|
||||
await getPageByTitle(page, testTitle)
|
||||
.getByTestId('doc-list-operation-button')
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: 'Open in split view' }).click();
|
||||
await expect(page.getByTestId('split-view-panel')).toHaveCount(2, {
|
||||
timeout: SPLIT_VIEW_READY_TIMEOUT,
|
||||
});
|
||||
const targetPage = page.getByTestId('split-view-panel').last();
|
||||
await expect(targetPage).toHaveAttribute('data-is-active', 'true');
|
||||
await expect(targetPage.locator('.doc-title-container')).toBeVisible({
|
||||
timeout: SPLIT_VIEW_READY_TIMEOUT,
|
||||
});
|
||||
await expect(targetPage.locator('.doc-title-container')).toContainText(
|
||||
testTitle,
|
||||
{ timeout: SPLIT_VIEW_READY_TIMEOUT }
|
||||
);
|
||||
});
|
||||
|
||||
test('open split view in all docs (drag to resize handle)', async ({
|
||||
page,
|
||||
}) => {
|
||||
const testTitle = 'test-page';
|
||||
await clickNewPageButton(page, testTitle);
|
||||
await clickSideBarAllPageButton(page);
|
||||
await waitForAllPagesLoad(page);
|
||||
|
||||
const pageItem = getPageByTitle(page, testTitle);
|
||||
|
||||
const leftResizeHandle = page.getByTestId('resize-handle').first();
|
||||
|
||||
await dragTo(page, pageItem, leftResizeHandle, 'center');
|
||||
await expectTabTitle(
|
||||
page,
|
||||
0,
|
||||
['test-page', 'All docs'],
|
||||
SPLIT_VIEW_READY_TIMEOUT
|
||||
);
|
||||
});
|
||||
|
||||
test('creating split view by dragging sidebar journals', async ({ page }) => {
|
||||
const journalButton = page.getByTestId('slider-bar-journals-button');
|
||||
const leftResizeHandle = page.getByTestId('resize-handle').first();
|
||||
|
||||
await dragTo(page, journalButton, leftResizeHandle, 'center');
|
||||
await expect(page.getByTestId('split-view-panel')).toHaveCount(2, {
|
||||
timeout: SPLIT_VIEW_READY_TIMEOUT,
|
||||
});
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('split-view-panel')
|
||||
.filter({
|
||||
has: page.locator('[data-is-first="true"]'),
|
||||
})
|
||||
.getByTestId('date-today-label')
|
||||
).toBeVisible({ timeout: SPLIT_VIEW_READY_TIMEOUT });
|
||||
});
|
||||
153
tests/affine-desktop/e2e/tabs.spec.ts
Normal file
153
tests/affine-desktop/e2e/tabs.spec.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { test } from '@affine-test/kit/electron';
|
||||
import {
|
||||
closeTab,
|
||||
expectActiveTab,
|
||||
expectTabCount,
|
||||
expectTabTitle,
|
||||
} from '@affine-test/kit/utils/app-tabs';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
createLinkedPage,
|
||||
dragTo,
|
||||
getPageByTitle,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test('create new tab', async ({ views }) => {
|
||||
let page = await views.getActive();
|
||||
|
||||
await page.getByTestId('add-tab-view-button').click();
|
||||
await expectTabCount(page, 2);
|
||||
// new tab title should be All docs
|
||||
await expectTabTitle(page, 1, 'All docs');
|
||||
await expectActiveTab(page, 1);
|
||||
});
|
||||
|
||||
test('can switch & close tab by clicking', async ({ page }) => {
|
||||
await page.getByTestId('add-tab-view-button').click();
|
||||
|
||||
await expectActiveTab(page, 1);
|
||||
|
||||
// switch to the previous tab by clicking on it
|
||||
await page.getByTestId('workbench-tab').nth(0).click();
|
||||
await expectActiveTab(page, 0);
|
||||
|
||||
// switch to the next tab by clicking on it
|
||||
await page.getByTestId('workbench-tab').nth(1).click();
|
||||
await expectActiveTab(page, 1);
|
||||
|
||||
// close the current tab
|
||||
await closeTab(page, 1);
|
||||
|
||||
// the first tab should be active
|
||||
await expectActiveTab(page, 0);
|
||||
await expectTabCount(page, 1);
|
||||
});
|
||||
|
||||
test('Collapse Sidebar', async ({ page }) => {
|
||||
await page
|
||||
.locator('[data-testid=app-sidebar-arrow-button-collapse][data-show=true]')
|
||||
.click();
|
||||
const sliderBarArea = page.getByTestId('app-sidebar');
|
||||
await expect(sliderBarArea).not.toBeInViewport();
|
||||
});
|
||||
|
||||
test('Expand Sidebar', async ({ page }) => {
|
||||
await page
|
||||
.locator('[data-testid=app-sidebar-arrow-button-collapse][data-show=true]')
|
||||
.click();
|
||||
const sliderBarArea = page.getByTestId('app-sidebar');
|
||||
await expect(sliderBarArea).not.toBeInViewport();
|
||||
|
||||
await page
|
||||
.locator('[data-testid=app-sidebar-arrow-button-expand][data-show=true]')
|
||||
.click();
|
||||
await expect(sliderBarArea).toBeInViewport();
|
||||
});
|
||||
|
||||
test('tab title will change when navigating', async ({ page }) => {
|
||||
await expectTabTitle(page, 0, 'Getting Started');
|
||||
|
||||
// create new page
|
||||
await clickNewPageButton(page);
|
||||
await expectTabTitle(page, 0, 'Untitled');
|
||||
|
||||
// go to all page
|
||||
await page.getByTestId('all-pages').click();
|
||||
await expectTabTitle(page, 0, 'All docs');
|
||||
|
||||
// go to today's journal
|
||||
await page.getByTestId('slider-bar-journals-button').click();
|
||||
await page.getByTestId('confirm-create-journal-button').click();
|
||||
await expect(page.locator('.doc-title-container')).toContainText('Today');
|
||||
const dateString = await page
|
||||
.locator('.doc-title-container > span:first-of-type')
|
||||
.textContent();
|
||||
|
||||
if (dateString) {
|
||||
await expectTabTitle(page, 0, dateString);
|
||||
}
|
||||
});
|
||||
|
||||
test('open new tab via cmd+click page link', async ({ page }) => {
|
||||
await clickNewPageButton(page);
|
||||
await page.waitForTimeout(500);
|
||||
await page.keyboard.press('Enter');
|
||||
await createLinkedPage(page, 'hi from another page');
|
||||
await page
|
||||
.locator('.affine-reference-title:has-text("hi from another page")')
|
||||
.click({
|
||||
modifiers: ['ControlOrMeta'],
|
||||
});
|
||||
await expectTabCount(page, 2);
|
||||
await expectTabTitle(page, 0, 'Untitled');
|
||||
await expectTabTitle(page, 1, 'hi from another page');
|
||||
await expectActiveTab(page, 0);
|
||||
});
|
||||
|
||||
test('drag a page from "All pages" list to tabs header', async ({ page }) => {
|
||||
const title = 'this is a new page to drag';
|
||||
await clickNewPageButton(page, title);
|
||||
await clickSideBarAllPageButton(page);
|
||||
|
||||
await dragTo(
|
||||
page,
|
||||
getPageByTitle(page, title),
|
||||
page.getByTestId('add-tab-view-button')
|
||||
);
|
||||
|
||||
await expectTabCount(page, 2);
|
||||
await expectTabTitle(page, 1, title);
|
||||
await expectActiveTab(page, 1);
|
||||
});
|
||||
|
||||
test('reorder tabs', async ({ page }) => {
|
||||
await clickNewPageButton(page);
|
||||
await page.waitForTimeout(500);
|
||||
await page.keyboard.press('Enter');
|
||||
const titles = ['aaa', 'bbb'];
|
||||
await createLinkedPage(page, titles[0]);
|
||||
await createLinkedPage(page, titles[1]);
|
||||
await page.locator(`.affine-reference-title:has-text("${titles[0]}")`).click({
|
||||
modifiers: ['ControlOrMeta'],
|
||||
});
|
||||
await page.locator(`.affine-reference-title:has-text("${titles[1]}")`).click({
|
||||
modifiers: ['ControlOrMeta'],
|
||||
});
|
||||
|
||||
await expectTabTitle(page, 0, 'Untitled');
|
||||
await expectTabTitle(page, 1, titles[0]);
|
||||
await expectTabTitle(page, 2, titles[1]);
|
||||
|
||||
await dragTo(
|
||||
page,
|
||||
page.getByTestId('workbench-tab').nth(0),
|
||||
page.getByTestId('workbench-tab').nth(1),
|
||||
'right'
|
||||
);
|
||||
|
||||
await expectTabTitle(page, 0, titles[0]);
|
||||
await expectTabTitle(page, 1, 'Untitled');
|
||||
await expectTabTitle(page, 2, titles[1]);
|
||||
});
|
||||
188
tests/affine-desktop/e2e/workspace.spec.ts
Normal file
188
tests/affine-desktop/e2e/workspace.spec.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import type { apis } from '@affine/electron-api';
|
||||
import { test } from '@affine-test/kit/electron';
|
||||
import {
|
||||
getBlockSuiteEditorTitle,
|
||||
getPageByTitle,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
clickSideBarCurrentWorkspaceBanner,
|
||||
} from '@affine-test/kit/utils/sidebar';
|
||||
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||
import { expect } from '@playwright/test';
|
||||
import fs from 'fs-extra';
|
||||
import type { ElectronApplication } from 'playwright';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__apis: typeof apis;
|
||||
}
|
||||
}
|
||||
|
||||
async function mockNextSaveDialog(
|
||||
electronApp: ElectronApplication,
|
||||
filePath: string
|
||||
) {
|
||||
await electronApp.evaluate(({ dialog }, mockedFilePath) => {
|
||||
const original = dialog.showSaveDialog.bind(dialog);
|
||||
dialog.showSaveDialog = async () => {
|
||||
dialog.showSaveDialog = original;
|
||||
return { canceled: false, filePath: mockedFilePath };
|
||||
};
|
||||
}, filePath);
|
||||
}
|
||||
|
||||
async function mockNextOpenDialog(
|
||||
electronApp: ElectronApplication,
|
||||
filePath: string
|
||||
) {
|
||||
await electronApp.evaluate(({ dialog }, mockedFilePath) => {
|
||||
const original = dialog.showOpenDialog.bind(dialog);
|
||||
dialog.showOpenDialog = async () => {
|
||||
dialog.showOpenDialog = original;
|
||||
return { canceled: false, filePaths: [mockedFilePath] };
|
||||
};
|
||||
}, filePath);
|
||||
}
|
||||
|
||||
test('check workspace has a DB file', async ({ appInfo, workspace }) => {
|
||||
const w = await workspace.current();
|
||||
const dbPath = path.join(
|
||||
appInfo.sessionData,
|
||||
'workspaces',
|
||||
'local',
|
||||
w.meta.id,
|
||||
'storage.db'
|
||||
);
|
||||
// check if db file exists
|
||||
expect(await fs.exists(dbPath)).toBe(true);
|
||||
});
|
||||
|
||||
test('export then add', async ({ electronApp, page, appInfo, workspace }) => {
|
||||
await clickNewPageButton(page);
|
||||
const w = await workspace.current();
|
||||
|
||||
await getBlockSuiteEditorTitle(page).fill('test1');
|
||||
|
||||
await page.getByTestId('slider-bar-workspace-setting-button').click();
|
||||
await expect(page.getByTestId('setting-modal')).toBeVisible();
|
||||
|
||||
const originalId = w.meta.id;
|
||||
|
||||
const newWorkspaceName = 'new-test-name';
|
||||
|
||||
// goto workspace setting
|
||||
await page.getByTestId('workspace-setting:preference').click();
|
||||
const input = page.getByTestId('workspace-name-input');
|
||||
await expect(input).toBeVisible();
|
||||
|
||||
// change workspace name
|
||||
await input.fill(newWorkspaceName);
|
||||
await page.getByTestId('save-workspace-name').click();
|
||||
await page.waitForSelector('text="Update workspace name success"');
|
||||
|
||||
const tmpPath = path.join(appInfo.sessionData, w.meta.id + '-tmp.db');
|
||||
|
||||
// export db file to tmp folder
|
||||
await mockNextSaveDialog(electronApp, tmpPath);
|
||||
|
||||
await page.getByTestId('workspace-setting:storage').click();
|
||||
await page.getByTestId('export-affine-backup').click();
|
||||
await page.waitForSelector('text="Export success"');
|
||||
expect(await fs.exists(tmpPath)).toBe(true);
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
|
||||
// add workspace
|
||||
// we are reusing the same db file so that we don't need to maintain one
|
||||
// in the codebase
|
||||
await clickSideBarCurrentWorkspaceBanner(page);
|
||||
|
||||
await mockNextOpenDialog(electronApp, tmpPath);
|
||||
|
||||
// load the db file
|
||||
await page.getByTestId('add-workspace').click();
|
||||
|
||||
// should show "Added Successfully" dialog
|
||||
// await page.waitForSelector('text="Added Successfully"');
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const newWorkspace = await workspace.current();
|
||||
return newWorkspace.meta.id !== originalId;
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
// check its name is correct
|
||||
await expect(page.getByTestId('workspace-name')).toHaveText(newWorkspaceName);
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// find button which has the title "test1"
|
||||
await getPageByTitle(page, 'test1').click();
|
||||
await waitForEditorLoad(page);
|
||||
const title = page.locator('[data-block-is-title] >> text="test1"');
|
||||
await expect(title).toBeVisible();
|
||||
});
|
||||
|
||||
test('delete workspace and then restore it from backup', async ({ page }) => {
|
||||
//#region 1. create a new workspace
|
||||
const newWorkspaceName = 'new-test-name';
|
||||
await createLocalWorkspace({ name: newWorkspaceName }, page);
|
||||
//#endregion
|
||||
|
||||
//#region 2. create a page in the new workspace (will verify later if it is successfully recovered)
|
||||
await clickNewPageButton(page);
|
||||
|
||||
await getBlockSuiteEditorTitle(page).fill('test1');
|
||||
//#endregion
|
||||
|
||||
//#region 3. delete the workspace
|
||||
await page.getByTestId('slider-bar-workspace-setting-button').click();
|
||||
await expect(page.getByTestId('setting-modal')).toBeVisible();
|
||||
|
||||
await page.getByTestId('workspace-setting:preference').click();
|
||||
await page.getByTestId('delete-workspace-button').click();
|
||||
await page.getByTestId('delete-workspace-input').fill(newWorkspaceName);
|
||||
|
||||
await page.getByTestId('delete-workspace-confirm-button').click();
|
||||
|
||||
// we are back to the original workspace
|
||||
await expect(page.getByTestId('workspace-name')).toContainText(
|
||||
'Demo Workspace'
|
||||
);
|
||||
//#endregion
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
//#region 4. restore the workspace from backup
|
||||
await page.getByTestId('slider-bar-workspace-setting-button').click();
|
||||
await expect(page.getByTestId('setting-modal')).toBeVisible();
|
||||
|
||||
await page.getByTestId('backup-panel-trigger').click();
|
||||
await expect(page.getByTestId('backup-workspace-item')).toHaveCount(1);
|
||||
await page.getByTestId('backup-workspace-item').click();
|
||||
await page.getByRole('menuitem', { name: 'Enable local workspace' }).click();
|
||||
const toast = page.locator(
|
||||
'[data-sonner-toast]:has-text("Workspace enabled successfully")'
|
||||
);
|
||||
await expect(toast).toBeVisible();
|
||||
await toast.getByRole('button', { name: 'Open' }).click();
|
||||
//#endregion
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// verify the workspace name & page title
|
||||
await expect(page.getByTestId('workspace-name')).toContainText(
|
||||
newWorkspaceName
|
||||
);
|
||||
// find button which has the title "test1"
|
||||
const test1PageButton = await page.waitForSelector(`text="test1"`);
|
||||
await test1PageButton.click();
|
||||
|
||||
const title = page.locator('[data-block-is-title] >> text="test1"');
|
||||
await expect(title).toBeVisible();
|
||||
});
|
||||
17
tests/affine-desktop/package.json
Normal file
17
tests/affine-desktop/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@affine-test/affine-desktop",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"e2e": "yarn playwright test --reporter=list"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@affine/electron-api": "workspace:*",
|
||||
"@playwright/test": "=1.58.2",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"fs-extra": "^11.2.0",
|
||||
"playwright": "=1.58.2"
|
||||
},
|
||||
"version": "0.27.5"
|
||||
}
|
||||
54
tests/affine-desktop/playwright.config.ts
Normal file
54
tests/affine-desktop/playwright.config.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import assert from 'node:assert';
|
||||
|
||||
import { testResultDir } from '@affine-test/kit/playwright';
|
||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
// import { devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
timeout: process.env.CI ? 50_000 : 30_000,
|
||||
expect: {
|
||||
timeout: process.env.CI ? 15_000 : 5_000,
|
||||
},
|
||||
outputDir: testResultDir,
|
||||
use: {
|
||||
viewport: { width: 1440, height: 800 },
|
||||
trace: 'retain-on-failure',
|
||||
},
|
||||
};
|
||||
|
||||
if (process.env.CI) {
|
||||
config.retries = 5;
|
||||
config.workers = 1;
|
||||
}
|
||||
|
||||
if (process.env.DEV_SERVER_URL) {
|
||||
const devServerUrl = new URL(process.env.DEV_SERVER_URL);
|
||||
assert(
|
||||
devServerUrl.origin === 'http://localhost:8080',
|
||||
'DEV_SERVER_URL must be http://localhost:8080'
|
||||
);
|
||||
config.webServer = [
|
||||
{
|
||||
command: 'yarn run -T affine bundle -p @affine/electron-renderer --dev',
|
||||
timeout: 120 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
env: {
|
||||
COVERAGE: process.env.COVERAGE || 'false',
|
||||
},
|
||||
url: 'http://localhost:8080',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default config;
|
||||
14
tests/affine-desktop/tsconfig.json
Normal file
14
tests/affine-desktop/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.web.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "./e2e",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["e2e"],
|
||||
"references": [
|
||||
{ "path": "../kit" },
|
||||
{ "path": "../../packages/frontend/electron-api" }
|
||||
]
|
||||
}
|
||||
17
tests/affine-local/e2e/ai-land.spec.ts
Normal file
17
tests/affine-local/e2e/ai-land.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { test } from '@affine-test/kit/playwright';
|
||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test('Click ai-land icon', async ({ page }) => {
|
||||
test.skip(process.env.CI !== undefined, 'Skip test in CI');
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await clickNewPageButton(page);
|
||||
await page.locator('[data-testid=ai-island]').click();
|
||||
|
||||
await expect(page.getByTestId('chat-panel-input-container')).toBeVisible();
|
||||
});
|
||||
225
tests/affine-local/e2e/all-page.spec.ts
Normal file
225
tests/affine-local/e2e/all-page.spec.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
/* oxlint-disable unicorn/prefer-dom-node-dataset */
|
||||
import { test } from '@affine-test/kit/playwright';
|
||||
import { getPagesCount } from '@affine-test/kit/utils/filter';
|
||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
getAllPage,
|
||||
getBlockSuiteEditorTitle,
|
||||
waitForAllPagesLoad,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test('all page', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await clickSideBarAllPageButton(page);
|
||||
});
|
||||
|
||||
test('all page can create new page', async ({ page }) => {
|
||||
const { clickNewPageButton } = getAllPage(page);
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await clickSideBarAllPageButton(page);
|
||||
await clickNewPageButton();
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.fill('this is a new page');
|
||||
await clickSideBarAllPageButton(page);
|
||||
const cell = page.getByRole('cell', { name: 'this is a new page' });
|
||||
expect(cell).not.toBeUndefined();
|
||||
});
|
||||
|
||||
test('all page can create new edgeless page', async ({ page }) => {
|
||||
const { clickNewEdgelessDropdown } = getAllPage(page);
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await clickSideBarAllPageButton(page);
|
||||
await clickNewEdgelessDropdown();
|
||||
await expect(page.locator('affine-edgeless-root')).toBeVisible();
|
||||
});
|
||||
|
||||
test('enable selection and use ESC to disable selection', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await clickSideBarAllPageButton(page);
|
||||
await waitForAllPagesLoad(page);
|
||||
|
||||
// there should be no checkbox in the page list by default
|
||||
expect(
|
||||
await page
|
||||
.locator('[data-testid="doc-list-item-select"][data-select-mode="true"]')
|
||||
.count()
|
||||
).toBe(0);
|
||||
|
||||
// by shift + clicking [data-testid="doc-list-item"], checkboxes should appear
|
||||
await page
|
||||
.locator('[data-testid="doc-list-item"]')
|
||||
.first()
|
||||
.click({
|
||||
modifiers: ['Shift'],
|
||||
});
|
||||
|
||||
// wait for 500ms
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// there should be checkboxes in the page list now
|
||||
expect(
|
||||
await page
|
||||
.locator('[data-testid="doc-list-item-select"][data-select-mode="true"]')
|
||||
.count()
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// esc again, checkboxes should disappear
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// wait for 500ms
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
expect(
|
||||
await page
|
||||
.locator('[data-testid="doc-list-item-select"][data-select-mode="true"]')
|
||||
.count()
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test('select two pages and delete', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await clickNewPageButton(page);
|
||||
await clickSideBarAllPageButton(page);
|
||||
await waitForAllPagesLoad(page);
|
||||
|
||||
const pageCount = await getPagesCount(page);
|
||||
|
||||
// by shift + clicking [data-testid="doc-list-item"], checkboxes should appear, and first doc be selected
|
||||
await page
|
||||
.locator('[data-testid="doc-list-item"]')
|
||||
.first()
|
||||
.click({
|
||||
modifiers: ['Shift'],
|
||||
});
|
||||
|
||||
// select the first two pages
|
||||
await page.locator('[data-testid="doc-list-item"]').nth(1).click();
|
||||
|
||||
// the floating popover should appear
|
||||
await expect(page.locator('[data-testid="floating-toolbar"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="floating-toolbar"]')).toHaveText(
|
||||
'2 doc(s) selected'
|
||||
);
|
||||
|
||||
// click delete button
|
||||
await page.locator('[data-testid="list-toolbar-delete"]').click();
|
||||
|
||||
// the confirm dialog should appear
|
||||
await expect(page.getByText('Delete 2 docs?')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
// check the page count again
|
||||
await page.waitForTimeout(300);
|
||||
await expect(
|
||||
page.locator('[data-testid="floating-toolbar"]')
|
||||
).not.toBeVisible();
|
||||
|
||||
expect(await getPagesCount(page)).toBe(pageCount - 2);
|
||||
});
|
||||
|
||||
test('select three pages with shiftKey and delete', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await clickNewPageButton(page);
|
||||
await clickNewPageButton(page);
|
||||
await clickNewPageButton(page);
|
||||
await clickSideBarAllPageButton(page);
|
||||
await waitForAllPagesLoad(page);
|
||||
|
||||
const pageCount = await getPagesCount(page);
|
||||
await page.keyboard.down('Shift');
|
||||
await page.locator('[data-testid="doc-list-item"]').nth(0).click();
|
||||
|
||||
await page.locator('[data-testid="doc-list-item"]').nth(2).click();
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
// the floating popover should appear
|
||||
await expect(page.locator('[data-testid="floating-toolbar"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="floating-toolbar"]')).toHaveText(
|
||||
'3 doc(s) selected'
|
||||
);
|
||||
|
||||
// click delete button
|
||||
await page.locator('[data-testid="list-toolbar-delete"]').click();
|
||||
|
||||
// the confirm dialog should appear
|
||||
await expect(page.getByText('Delete 3 docs?')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
// check the page count again
|
||||
await page.waitForTimeout(300);
|
||||
await expect(
|
||||
page.locator('[data-testid="floating-toolbar"]')
|
||||
).not.toBeVisible();
|
||||
|
||||
expect(await getPagesCount(page)).toBe(pageCount - 3);
|
||||
});
|
||||
|
||||
test('select all and delete closes the floating toolbar', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await clickNewPageButton(page);
|
||||
await clickNewPageButton(page);
|
||||
await clickSideBarAllPageButton(page);
|
||||
await waitForAllPagesLoad(page);
|
||||
|
||||
const pageCount = await getPagesCount(page);
|
||||
|
||||
const selectAllButton = page
|
||||
.getByRole('button', { name: 'Select all' })
|
||||
.last();
|
||||
await selectAllButton.hover();
|
||||
await selectAllButton.click();
|
||||
|
||||
await expect(page.locator('[data-testid="floating-toolbar"]')).toBeVisible();
|
||||
await page.locator('[data-testid="list-toolbar-delete"]').click();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
await page.waitForTimeout(300);
|
||||
await expect(
|
||||
page.locator('[data-testid="floating-toolbar"]')
|
||||
).not.toBeVisible();
|
||||
expect(await getPagesCount(page)).toBeLessThan(pageCount);
|
||||
});
|
||||
|
||||
test('create a tag and delete it', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await clickNewPageButton(page);
|
||||
await clickSideBarAllPageButton(page);
|
||||
await waitForAllPagesLoad(page);
|
||||
await page.getByTestId('workspace-tags-button').click();
|
||||
|
||||
// create a tag
|
||||
await page.getByTestId('all-tags-new-button').click();
|
||||
await expect(page.getByTestId('edit-tag-modal')).toBeVisible();
|
||||
await page.getByTestId('edit-tag-input').fill('test-tag');
|
||||
await page.getByTestId('save-tag').click();
|
||||
|
||||
// check the tag is created
|
||||
await clickSideBarAllPageButton(page);
|
||||
await waitForAllPagesLoad(page);
|
||||
await page.getByTestId('workspace-tags-button').click();
|
||||
const cell = page.getByTestId('tag-list-item').getByText('test-tag');
|
||||
await expect(cell).toBeVisible();
|
||||
|
||||
// delete the tag
|
||||
await page.getByTestId('tag-item-operation-button').click();
|
||||
await page.getByTestId('delete-tag').click();
|
||||
await page.getByTestId('confirm-modal-confirm').getByText('Delete').click();
|
||||
await page.waitForURL(url => url.pathname.endsWith('tag'));
|
||||
|
||||
const newCell = page.getByTestId('tag-list-item').getByText('test-tag');
|
||||
await expect(newCell).not.toBeVisible();
|
||||
});
|
||||
462
tests/affine-local/e2e/attachment-preview.spec.ts
Normal file
462
tests/affine-local/e2e/attachment-preview.spec.ts
Normal file
@@ -0,0 +1,462 @@
|
||||
import { test } from '@affine-test/kit/playwright';
|
||||
import { importAttachment } from '@affine-test/kit/utils/attachment';
|
||||
import { locateToolbar } from '@affine-test/kit/utils/editor';
|
||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
getBlockSuiteEditorTitle,
|
||||
waitForEditorLoad,
|
||||
waitForEmptyEditor,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import {
|
||||
confirmExperimentalPrompt,
|
||||
openEditorSetting,
|
||||
openExperimentalFeaturesPanel,
|
||||
} from '@affine-test/kit/utils/setting';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
async function clickPeekViewControl(page: Page, n = 0) {
|
||||
await page.getByTestId('peek-view-control').nth(n).click();
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async function enablePDFEmbedView(page: Page) {
|
||||
// Opens settings panel
|
||||
await openEditorSetting(page);
|
||||
await openExperimentalFeaturesPanel(page);
|
||||
await confirmExperimentalPrompt(page);
|
||||
|
||||
const settingModal = page.locator('[data-testid=setting-modal-content]');
|
||||
const item = settingModal.locator('div').getByText('PDF embed preview');
|
||||
await item.waitFor({ state: 'attached' });
|
||||
await expect(item).toBeVisible();
|
||||
const button = item.locator('label');
|
||||
const isChecked = await button.locator('input').isChecked();
|
||||
if (!isChecked) {
|
||||
await button.click();
|
||||
}
|
||||
|
||||
// Closes settings panel
|
||||
await page.keyboard.press('Escape');
|
||||
}
|
||||
|
||||
test('attachment preview should be shown', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await clickNewPageButton(page);
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.click();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await importAttachment(page, 'lorem-ipsum.pdf');
|
||||
|
||||
await page.locator('affine-attachment').first().dblclick();
|
||||
|
||||
const attachmentViewer = page.getByTestId('pdf-viewer');
|
||||
await expect(attachmentViewer).toBeVisible();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const pageCursor = attachmentViewer.locator('.page-cursor');
|
||||
expect(await pageCursor.textContent()).toBe('1');
|
||||
const pageCount = attachmentViewer.locator('.page-count');
|
||||
expect(await pageCount.textContent()).toBe('3');
|
||||
|
||||
const thumbnails = attachmentViewer.locator('.thumbnails');
|
||||
await thumbnails.locator('button').click();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
expect(
|
||||
await thumbnails
|
||||
.getByTestId('virtuoso-item-list')
|
||||
.locator('[data-item-index]')
|
||||
.count()
|
||||
).toBe(3);
|
||||
|
||||
await clickPeekViewControl(page);
|
||||
await expect(attachmentViewer).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('attachment preview can be expanded', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await clickNewPageButton(page);
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.click();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await importAttachment(page, 'lorem-ipsum.pdf');
|
||||
|
||||
await page.locator('affine-attachment').first().dblclick();
|
||||
|
||||
const attachmentViewer = page.getByTestId('pdf-viewer');
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await expect(attachmentViewer).toBeVisible();
|
||||
|
||||
await clickPeekViewControl(page, 1);
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const pageCursor = attachmentViewer.locator('.page-cursor');
|
||||
expect(await pageCursor.textContent()).toBe('1');
|
||||
const pageCount = attachmentViewer.locator('.page-count');
|
||||
expect(await pageCount.textContent()).toBe('3');
|
||||
|
||||
const thumbnails = attachmentViewer.locator('.thumbnails');
|
||||
await thumbnails.locator('button').click();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
expect(
|
||||
await thumbnails
|
||||
.getByTestId('virtuoso-item-list')
|
||||
.locator('[data-item-index]')
|
||||
.count()
|
||||
).toBe(3);
|
||||
});
|
||||
|
||||
test('should preview PDF in embed view', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await clickNewPageButton(page);
|
||||
await waitForEmptyEditor(page);
|
||||
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.click();
|
||||
await page.keyboard.type('PDF preview');
|
||||
|
||||
await enablePDFEmbedView(page);
|
||||
|
||||
await clickNewPageButton(page);
|
||||
await waitForEmptyEditor(page);
|
||||
|
||||
await title.click();
|
||||
await page.keyboard.type('PDF page');
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await importAttachment(page, 'lorem-ipsum.pdf');
|
||||
|
||||
const attachment = page.locator('affine-attachment');
|
||||
await attachment.click();
|
||||
|
||||
const toolbar = locateToolbar(page);
|
||||
await expect(toolbar).toBeVisible();
|
||||
|
||||
// Switches to embed view
|
||||
await toolbar.getByLabel('Switch view').click();
|
||||
await toolbar.getByLabel('Embed view').click();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const portal = attachment.locator('lit-react-portal');
|
||||
await expect(portal).toBeVisible();
|
||||
|
||||
await attachment.click();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const pageCursor = portal.locator('.page-cursor');
|
||||
expect(await pageCursor.textContent()).toBe('1');
|
||||
const pageCount = portal.locator('.page-count');
|
||||
expect(await pageCount.textContent()).toBe('3');
|
||||
|
||||
const prevButton = portal.getByRole('button', { name: 'Prev' });
|
||||
const nextButton = portal.getByRole('button', { name: 'Next' });
|
||||
|
||||
await nextButton.click();
|
||||
expect(await pageCursor.textContent()).toBe('2');
|
||||
|
||||
await nextButton.click();
|
||||
expect(await pageCursor.textContent()).toBe('3');
|
||||
|
||||
await prevButton.click();
|
||||
expect(await pageCursor.textContent()).toBe('2');
|
||||
|
||||
// Title alias
|
||||
{
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await page.keyboard.press('@');
|
||||
|
||||
const doc0 = page.locator('.linked-doc-popover').getByText('PDF preview');
|
||||
await doc0.click();
|
||||
|
||||
await page.keyboard.press('@');
|
||||
|
||||
const doc1 = page.locator('.linked-doc-popover').getByText('PDF page');
|
||||
await doc1.click();
|
||||
|
||||
const inlineLink = page.locator('affine-reference').nth(0);
|
||||
const inlineTitle = inlineLink.locator('.affine-reference-title');
|
||||
|
||||
await expect(inlineTitle).toHaveText('PDF preview');
|
||||
|
||||
const bouding = await inlineLink.boundingBox();
|
||||
expect(bouding).not.toBeNull();
|
||||
|
||||
await page.mouse.move(bouding!.x - 50, bouding!.y + bouding!.height / 2);
|
||||
await page.waitForTimeout(500);
|
||||
await page.mouse.click(bouding!.x - 50, bouding!.y + bouding!.height / 2);
|
||||
|
||||
await inlineLink.hover();
|
||||
|
||||
// Edits title
|
||||
await toolbar.getByRole('button', { name: 'Edit' }).click();
|
||||
|
||||
// Title alias
|
||||
await page.keyboard.type('PDF embed preview');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await expect(inlineTitle).toHaveText('PDF embed preview');
|
||||
}
|
||||
|
||||
// PDF embed view should not be re-rendered
|
||||
expect(await pageCursor.textContent()).toBe('2');
|
||||
expect(await pageCount.textContent()).toBe('3');
|
||||
|
||||
// Chagnes origin title
|
||||
{
|
||||
const inlineLink = page.locator('affine-reference').nth(1);
|
||||
const inlineTitle = inlineLink.locator('.affine-reference-title');
|
||||
await expect(inlineTitle).toHaveText('PDF page');
|
||||
|
||||
await title.click();
|
||||
await page.keyboard.type(' preview');
|
||||
|
||||
await expect(inlineTitle).toHaveText('PDF page preview');
|
||||
}
|
||||
|
||||
// PDF embed view should not be re-rendered
|
||||
expect(await pageCursor.textContent()).toBe('2');
|
||||
expect(await pageCount.textContent()).toBe('3');
|
||||
});
|
||||
|
||||
test('should sync name in pdf embed view', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await enablePDFEmbedView(page);
|
||||
await clickNewPageButton(page);
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.click();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await importAttachment(page, 'lorem-ipsum.pdf');
|
||||
|
||||
const attachment = page.locator('affine-attachment');
|
||||
await attachment.click();
|
||||
|
||||
const toolbar = locateToolbar(page);
|
||||
await expect(toolbar).toBeVisible();
|
||||
|
||||
const attachmentTitle = attachment.locator(
|
||||
'.affine-attachment-content-title-text'
|
||||
);
|
||||
await expect(attachmentTitle).toHaveText('lorem-ipsum.pdf');
|
||||
|
||||
// Renames
|
||||
await toolbar.getByRole('button', { name: 'Rename' }).click();
|
||||
const input = page
|
||||
.locator('.affine-attachment-rename-input-wrapper')
|
||||
.locator('input');
|
||||
await input.fill('What is Lorem Ipsum');
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(attachmentTitle).toHaveText('What is Lorem Ipsum.pdf');
|
||||
|
||||
await attachment.click();
|
||||
|
||||
// Switches to embed view
|
||||
await toolbar.getByLabel('Switch view').click();
|
||||
await toolbar.getByLabel('Embed view').click();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const portal = attachment.locator('lit-react-portal');
|
||||
const portalName = portal.locator('.pdf-name');
|
||||
await expect(portal).toBeVisible();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
await expect(portalName).toHaveText('What is Lorem Ipsum.pdf');
|
||||
|
||||
await attachment.click();
|
||||
|
||||
// Renames
|
||||
await toolbar.getByRole('button', { name: 'Rename' }).click();
|
||||
await input.fill('lorem-ipsum');
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(portalName).toHaveText('lorem-ipsum.pdf');
|
||||
});
|
||||
|
||||
test('should enable pointer event in pdf viewer', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await clickNewPageButton(page);
|
||||
await waitForEmptyEditor(page);
|
||||
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.click();
|
||||
await page.keyboard.type('PDF preview');
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await importAttachment(page, 'lorem-ipsum.pdf');
|
||||
|
||||
const attachment = page.locator('affine-attachment');
|
||||
await attachment.click();
|
||||
|
||||
const attachmentSelection = attachment.locator(
|
||||
'.affine-attachment-container.focused'
|
||||
);
|
||||
|
||||
const toolbar = locateToolbar(page);
|
||||
|
||||
// Switches to embed view
|
||||
await toolbar.getByLabel('Switch view').click();
|
||||
await toolbar.getByLabel('Embed view').click();
|
||||
|
||||
await attachment
|
||||
.locator('.affine-attachment-embed-container')
|
||||
.waitFor({ state: 'visible' });
|
||||
|
||||
const rect = await attachment.boundingBox();
|
||||
expect(rect).not.toBeNull();
|
||||
|
||||
const { x, y, width, height } = rect!;
|
||||
const startPoint = [x - 10, y - 10];
|
||||
const endPoint = [x + width / 2, y + height / 2];
|
||||
|
||||
await page.mouse.move(startPoint[0], startPoint[1]);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(endPoint[0], endPoint[1]);
|
||||
await page.mouse.move(
|
||||
endPoint[0] + width / 2 + 10,
|
||||
endPoint[1] + height / 2 + 10
|
||||
);
|
||||
|
||||
await expect(attachmentSelection).toBeVisible();
|
||||
|
||||
await page.mouse.move(startPoint[0], startPoint[1]);
|
||||
|
||||
await expect(attachmentSelection).toBeHidden();
|
||||
|
||||
await page.mouse.move(startPoint[0] - 50, startPoint[1] - 50);
|
||||
|
||||
await page.mouse.up();
|
||||
|
||||
await expect(attachmentSelection).toBeHidden();
|
||||
});
|
||||
|
||||
test('should re-render pdf viewer', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await clickNewPageButton(page);
|
||||
await waitForEmptyEditor(page);
|
||||
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.click();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await importAttachment(page, 'lorem-ipsum.pdf');
|
||||
|
||||
const attachment = page.locator('affine-attachment');
|
||||
await attachment.click();
|
||||
|
||||
const toolbar = locateToolbar(page);
|
||||
|
||||
// Switches to embed view
|
||||
await toolbar.getByLabel('Switch view').click();
|
||||
await toolbar.getByLabel('Embed view').click();
|
||||
|
||||
const portal = attachment.locator('lit-react-portal');
|
||||
await expect(portal).toBeVisible();
|
||||
|
||||
const portalId = await portal.getAttribute('portalid');
|
||||
expect(portalId).not.toBeNull();
|
||||
|
||||
const moreMenu = toolbar.getByLabel('More menu');
|
||||
await moreMenu.click();
|
||||
|
||||
await toolbar.getByRole('button', { name: 'Reload' }).click();
|
||||
|
||||
const newPortalId = await portal.getAttribute('portalid');
|
||||
expect(newPortalId).not.toBeNull();
|
||||
|
||||
expect(portalId).not.toEqual(newPortalId);
|
||||
});
|
||||
|
||||
test('should display status when an error is thrown in peek view', async ({
|
||||
context,
|
||||
page,
|
||||
}) => {
|
||||
await openHomePage(page);
|
||||
await clickNewPageButton(page);
|
||||
await waitForEmptyEditor(page);
|
||||
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.click();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await importAttachment(page, 'lorem-ipsum.pdf');
|
||||
|
||||
const attachment = page.locator('affine-attachment');
|
||||
await attachment.click();
|
||||
|
||||
const toolbar = locateToolbar(page);
|
||||
|
||||
// Switches to embed view
|
||||
await toolbar.getByLabel('Switch view').click();
|
||||
await toolbar.getByLabel('Embed view').click();
|
||||
|
||||
await context.setOffline(true);
|
||||
|
||||
await attachment.dblclick();
|
||||
|
||||
// Peek view
|
||||
const pdfViewer = page.getByTestId('pdf-viewer');
|
||||
await expect(pdfViewer).toBeHidden();
|
||||
|
||||
const statusWrapper = page.getByTestId('pdf-viewer-status-wrapper');
|
||||
await expect(statusWrapper).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display 404 when attachment is not found', async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await clickNewPageButton(page);
|
||||
await waitForEmptyEditor(page);
|
||||
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.click();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await importAttachment(page, 'lorem-ipsum.pdf');
|
||||
|
||||
const attachment = page.locator('affine-attachment');
|
||||
await attachment.click();
|
||||
|
||||
const toolbar = locateToolbar(page);
|
||||
|
||||
// Switches to embed view
|
||||
await toolbar.getByLabel('Switch view').click();
|
||||
await toolbar.getByLabel('Embed view').click();
|
||||
|
||||
await attachment.dblclick();
|
||||
|
||||
// Peek view
|
||||
const pdfViewer = page.getByTestId('pdf-viewer');
|
||||
await expect(pdfViewer).toBeVisible();
|
||||
|
||||
const statusWrapper = page.getByTestId('pdf-viewer-status-wrapper');
|
||||
await expect(statusWrapper).toBeHidden();
|
||||
|
||||
await clickPeekViewControl(page, 1);
|
||||
|
||||
await page.goto(page.url() + 'test');
|
||||
|
||||
await expect(pdfViewer).toBeHidden();
|
||||
|
||||
await expect(page.getByTestId('not-found')).toBeVisible();
|
||||
});
|
||||
28
tests/affine-local/e2e/blocksuite/at-menu.spec.ts
Normal file
28
tests/affine-local/e2e/blocksuite/at-menu.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { test } from '@affine-test/kit/playwright';
|
||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
type,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await clickNewPageButton(page);
|
||||
await waitForEditorLoad(page);
|
||||
});
|
||||
|
||||
test('at menu should not be close when press arrow key', async ({ page }) => {
|
||||
await page.keyboard.press('Enter');
|
||||
await type(page, '@');
|
||||
|
||||
const atMenu = page.locator('.linked-doc-popover');
|
||||
await expect(atMenu).toBeVisible();
|
||||
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await expect(atMenu).toBeVisible();
|
||||
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await expect(atMenu).toBeVisible();
|
||||
});
|
||||
118
tests/affine-local/e2e/blocksuite/attachment/toolbar.spec.ts
Normal file
118
tests/affine-local/e2e/blocksuite/attachment/toolbar.spec.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { test } from '@affine-test/kit/playwright';
|
||||
import {
|
||||
importAttachment,
|
||||
importFile,
|
||||
} from '@affine-test/kit/utils/attachment';
|
||||
import {
|
||||
clickEdgelessModeButton,
|
||||
clickView,
|
||||
locateToolbar,
|
||||
toViewCoord,
|
||||
} from '@affine-test/kit/utils/editor';
|
||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
getBlockSuiteEditorTitle,
|
||||
waitForEmptyEditor,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await clickNewPageButton(page);
|
||||
await waitForEmptyEditor(page);
|
||||
});
|
||||
|
||||
test.describe('Replaces attachment', () => {
|
||||
test('should replace attachment in page', async ({ page }) => {
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.click();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await importAttachment(page, 'lorem-ipsum.pdf');
|
||||
|
||||
const attachment = page.locator('affine-attachment').first();
|
||||
await attachment.click();
|
||||
|
||||
const name = attachment.locator('.affine-attachment-content-title-text');
|
||||
|
||||
await expect(name).toHaveText('lorem-ipsum.pdf');
|
||||
|
||||
const toolbar = locateToolbar(page);
|
||||
const replaceButton = toolbar.getByLabel('Replace attachment');
|
||||
|
||||
await importFile(page, 'v1-color-palettes-snapshot.zip', async () => {
|
||||
await replaceButton.click({ delay: 50 });
|
||||
});
|
||||
|
||||
await expect(attachment).toBeVisible();
|
||||
|
||||
await expect(name).toHaveText('v1-color-palettes-snapshot.zip');
|
||||
});
|
||||
|
||||
test('should replace attachment in edgeless', async ({ page }) => {
|
||||
await clickEdgelessModeButton(page);
|
||||
|
||||
const button = page.locator('edgeless-mindmap-tool-button');
|
||||
await button.click();
|
||||
|
||||
const menu = page.locator('edgeless-mindmap-menu');
|
||||
const mediaItem = menu.locator('.media-item');
|
||||
await mediaItem.click();
|
||||
|
||||
await importFile(page, 'lorem-ipsum.pdf', async () => {
|
||||
await toViewCoord(page, [100, 250]);
|
||||
await clickView(page, [100, 250]);
|
||||
});
|
||||
|
||||
const attachment = page.locator('affine-edgeless-attachment').first();
|
||||
await attachment.click();
|
||||
|
||||
const name = attachment.locator('.affine-attachment-content-title-text');
|
||||
|
||||
await expect(name).toHaveText('lorem-ipsum.pdf');
|
||||
|
||||
const toolbar = locateToolbar(page);
|
||||
const replaceButton = toolbar.getByLabel('Replace attachment');
|
||||
|
||||
await importFile(page, 'v1-color-palettes-snapshot.zip', async () => {
|
||||
await replaceButton.click({ delay: 50 });
|
||||
});
|
||||
|
||||
await expect(attachment).toBeVisible();
|
||||
|
||||
await expect(name).toHaveText('v1-color-palettes-snapshot.zip');
|
||||
});
|
||||
|
||||
test('should fall back to card view when file type does not support embed view', async ({
|
||||
page,
|
||||
}) => {
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.click();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await importAttachment(page, 'lorem-ipsum.pdf');
|
||||
|
||||
const attachment = page.locator('affine-attachment').first();
|
||||
await attachment.click();
|
||||
|
||||
const toolbar = locateToolbar(page);
|
||||
|
||||
// Switches to embed view
|
||||
await toolbar.getByLabel('Switch view').click();
|
||||
await toolbar.getByLabel('Embed view').click();
|
||||
|
||||
const portal = attachment.locator('lit-react-portal');
|
||||
await expect(portal).toBeVisible();
|
||||
|
||||
const replaceButton = toolbar.getByLabel('Replace attachment');
|
||||
await importFile(page, 'v1-color-palettes-snapshot.zip', async () => {
|
||||
await replaceButton.click({ delay: 50 });
|
||||
});
|
||||
|
||||
await expect(portal).toBeHidden();
|
||||
|
||||
const name = attachment.locator('.affine-attachment-content-title-text');
|
||||
await expect(name).toHaveText('v1-color-palettes-snapshot.zip');
|
||||
});
|
||||
});
|
||||
76
tests/affine-local/e2e/blocksuite/callout/callout.spec.ts
Normal file
76
tests/affine-local/e2e/blocksuite/callout/callout.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
pressArrowDown,
|
||||
pressArrowUp,
|
||||
pressBackspace,
|
||||
pressEnter,
|
||||
} from '@affine-test/kit/utils/keyboard';
|
||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
type,
|
||||
waitForEmptyEditor,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await clickNewPageButton(page);
|
||||
await waitForEmptyEditor(page);
|
||||
await page.locator('affine-paragraph v-line div').click();
|
||||
});
|
||||
|
||||
test('add callout block using slash menu and change emoji', async ({
|
||||
page,
|
||||
}) => {
|
||||
await type(page, '/callout\naaaa\nbbbb');
|
||||
const callout = page.locator('affine-callout');
|
||||
const emoji = page.locator('affine-callout').getByTestId('callout-emoji');
|
||||
await expect(callout).toBeVisible();
|
||||
await expect(emoji).toContainText('💡');
|
||||
|
||||
const paragraph = page.locator('affine-callout affine-paragraph');
|
||||
await expect(paragraph).toHaveCount(2);
|
||||
|
||||
const vLine = page.locator('affine-callout v-line');
|
||||
await expect(vLine).toHaveCount(2);
|
||||
expect(await vLine.nth(0).innerText()).toBe('aaaa');
|
||||
expect(await vLine.nth(1).innerText()).toBe('bbbb');
|
||||
});
|
||||
|
||||
test('press backspace after callout block', async ({ page }) => {
|
||||
await pressEnter(page);
|
||||
await pressArrowUp(page);
|
||||
await type(page, '/callout\n');
|
||||
await pressArrowDown(page);
|
||||
|
||||
const paragraph = page.locator('affine-paragraph');
|
||||
const callout = page.locator('affine-callout');
|
||||
expect(await paragraph.count()).toBe(3);
|
||||
expect(await callout.count()).toBe(1);
|
||||
|
||||
await pressBackspace(page);
|
||||
expect(await paragraph.count()).toBe(3);
|
||||
expect(await callout.count()).toBe(1);
|
||||
|
||||
await pressBackspace(page);
|
||||
await expect(paragraph).toHaveCount(2);
|
||||
await expect(callout).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('press backspace in callout block', async ({ page }) => {
|
||||
const paragraph = page.locator('affine-paragraph');
|
||||
const callout = page.locator('affine-callout');
|
||||
|
||||
await type(page, '/callout\n');
|
||||
|
||||
expect(await paragraph.count()).toBe(2);
|
||||
expect(await callout.count()).toBe(1);
|
||||
|
||||
await pressBackspace(page);
|
||||
await expect(paragraph).toHaveCount(1);
|
||||
await expect(callout).toHaveCount(1);
|
||||
|
||||
await pressBackspace(page);
|
||||
await expect(paragraph).toHaveCount(1);
|
||||
await expect(callout).toHaveCount(0);
|
||||
});
|
||||
581
tests/affine-local/e2e/blocksuite/clipboard/clipboard.spec.ts
Normal file
581
tests/affine-local/e2e/blocksuite/clipboard/clipboard.spec.ts
Normal file
@@ -0,0 +1,581 @@
|
||||
import { test } from '@affine-test/kit/playwright';
|
||||
import { importFile } from '@affine-test/kit/utils/attachment';
|
||||
import { pasteContent } from '@affine-test/kit/utils/clipboard';
|
||||
import {
|
||||
clickEdgelessModeButton,
|
||||
clickPageModeButton,
|
||||
clickView,
|
||||
getCodeBlockIds,
|
||||
getParagraphIds,
|
||||
locateEditorContainer,
|
||||
toViewCoord,
|
||||
} from '@affine-test/kit/utils/editor';
|
||||
import {
|
||||
copyByKeyboard,
|
||||
cutByKeyboard,
|
||||
pasteByKeyboard,
|
||||
pressEnter,
|
||||
} from '@affine-test/kit/utils/keyboard';
|
||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||
import {
|
||||
addCodeBlock,
|
||||
clickNewPageButton,
|
||||
type,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { setSelection } from '@affine-test/kit/utils/selection';
|
||||
import type { CodeBlockComponent } from '@blocksuite/affine-block-code';
|
||||
import type { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
|
||||
import type { PageRootBlockComponent } from '@blocksuite/affine-block-root';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
const paragraphLocator = 'affine-note affine-paragraph';
|
||||
const codeBlockLocator = 'affine-note affine-code';
|
||||
|
||||
// Helper function to create paragraph blocks with text
|
||||
async function createParagraphBlocks(page: Page, texts: string[]) {
|
||||
for (const text of texts) {
|
||||
await pressEnter(page);
|
||||
await type(page, text);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to verify block text content
|
||||
async function verifyBlockContent<T extends BlockComponent>(
|
||||
page: Page,
|
||||
selector: string,
|
||||
index: number,
|
||||
expectedText: string
|
||||
) {
|
||||
expect(
|
||||
await page
|
||||
.locator(selector)
|
||||
.nth(index)
|
||||
.evaluate((block: T, expected: string) => {
|
||||
const model = block.model;
|
||||
// Check if model has text property
|
||||
if (!('text' in model)) return false;
|
||||
const text = model.text;
|
||||
// Check if text exists and has toString method
|
||||
return text && text.toString() === expected;
|
||||
}, expectedText)
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
// Helper functions using the generic verifyBlockContent
|
||||
async function verifyParagraphContent(
|
||||
page: Page,
|
||||
index: number,
|
||||
expectedText: string
|
||||
) {
|
||||
await verifyBlockContent<ParagraphBlockComponent>(
|
||||
page,
|
||||
paragraphLocator,
|
||||
index,
|
||||
expectedText
|
||||
);
|
||||
}
|
||||
|
||||
async function verifyCodeBlockContent(
|
||||
page: Page,
|
||||
index: number,
|
||||
expectedText: string
|
||||
) {
|
||||
await verifyBlockContent<CodeBlockComponent>(
|
||||
page,
|
||||
codeBlockLocator,
|
||||
index,
|
||||
expectedText
|
||||
);
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await clickNewPageButton(page, 'Clipboard Test');
|
||||
await waitForEditorLoad(page);
|
||||
});
|
||||
|
||||
test.describe('paste in multiple blocks text selection', () => {
|
||||
test('paste plain text', async ({ page }) => {
|
||||
const texts = ['hello world', 'hello world', 'hello world'];
|
||||
await createParagraphBlocks(page, texts);
|
||||
|
||||
const { blockIds: paragraphIds } = await getParagraphIds(page);
|
||||
|
||||
/**
|
||||
* select text cross the 3 blocks:
|
||||
* hello |world
|
||||
* hello world
|
||||
* hello| world
|
||||
*/
|
||||
await setSelection(page, paragraphIds[0], 6, paragraphIds[2], 5);
|
||||
|
||||
await pasteContent(page, { 'text/plain': 'test' });
|
||||
|
||||
/**
|
||||
* after paste:
|
||||
* hello test world
|
||||
*/
|
||||
await verifyParagraphContent(page, 0, 'hello test world');
|
||||
});
|
||||
|
||||
test('paste snapshot', async ({ page }) => {
|
||||
// Create initial test blocks
|
||||
await createParagraphBlocks(page, ['test', 'test']);
|
||||
|
||||
// Create target blocks
|
||||
await createParagraphBlocks(page, [
|
||||
'hello world',
|
||||
'hello world',
|
||||
'hello world',
|
||||
]);
|
||||
|
||||
/**
|
||||
* before paste:
|
||||
* test
|
||||
* test
|
||||
* hello world
|
||||
* hello world
|
||||
* hello world
|
||||
*/
|
||||
const { blockIds: paragraphIds } = await getParagraphIds(page);
|
||||
|
||||
/**
|
||||
* select the first 2 blocks:
|
||||
* |test
|
||||
* test|
|
||||
* hello world
|
||||
* hello world
|
||||
* hello world
|
||||
*/
|
||||
await setSelection(page, paragraphIds[0], 0, paragraphIds[1], 4);
|
||||
// copy the first 2 blocks
|
||||
await copyByKeyboard(page);
|
||||
|
||||
/**
|
||||
* select the last 3 blocks:
|
||||
* test
|
||||
* test
|
||||
* hello |world
|
||||
* hello world
|
||||
* hello| world
|
||||
*/
|
||||
await setSelection(page, paragraphIds[2], 6, paragraphIds[4], 5);
|
||||
|
||||
// paste the first 2 blocks
|
||||
await pasteByKeyboard(page);
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
/**
|
||||
* after paste:
|
||||
* test
|
||||
* test
|
||||
* hello test
|
||||
* test world
|
||||
*/
|
||||
await verifyParagraphContent(page, 2, 'hello test');
|
||||
await verifyParagraphContent(page, 3, 'test world');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('surface-ref block', () => {
|
||||
async function setupSurfaceRefBlock(page: Page) {
|
||||
await clickEdgelessModeButton(page);
|
||||
const container = locateEditorContainer(page);
|
||||
await container.click();
|
||||
|
||||
// add a shape
|
||||
await page.keyboard.press('s');
|
||||
await container.click({ position: { x: 100, y: 300 } });
|
||||
await page.waitForTimeout(50);
|
||||
|
||||
// add a frame
|
||||
await page.keyboard.press('f');
|
||||
await page.waitForTimeout(50);
|
||||
|
||||
// click on the frame title to trigger the change frame button toolbar
|
||||
const frameTitle = page.locator('affine-frame-title');
|
||||
await frameTitle.click();
|
||||
await page.waitForTimeout(50);
|
||||
|
||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
||||
const insertIntoPageButton = toolbar.getByLabel('Insert into Page');
|
||||
await insertIntoPageButton.click();
|
||||
|
||||
await clickPageModeButton(page);
|
||||
await waitForEditorLoad(page);
|
||||
await container.click();
|
||||
|
||||
return { container };
|
||||
}
|
||||
|
||||
test('paste surface-ref block to another doc as embed-linked-doc block', async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupSurfaceRefBlock(page);
|
||||
|
||||
// copy surface-ref block
|
||||
const surfaceRefBlock = page.locator('affine-surface-ref');
|
||||
await surfaceRefBlock.click();
|
||||
await page.waitForSelector('affine-surface-ref .focused');
|
||||
await copyByKeyboard(page);
|
||||
|
||||
// paste to another doc
|
||||
await clickNewPageButton(page, 'page2');
|
||||
await pressEnter(page);
|
||||
|
||||
// paste the surface-ref block
|
||||
await pasteByKeyboard(page);
|
||||
await page.waitForTimeout(50);
|
||||
|
||||
const embedLinkedDocBlock = page.locator('affine-embed-linked-doc-block');
|
||||
await expect(embedLinkedDocBlock).toBeVisible();
|
||||
const embedLinkedDocBlockTitle = embedLinkedDocBlock.locator(
|
||||
'.affine-embed-linked-doc-content-title-text'
|
||||
);
|
||||
await expect(embedLinkedDocBlockTitle).toHaveText('Clipboard Test');
|
||||
});
|
||||
|
||||
test('cut and paste surface-ref block to same doc should remain surface-ref block', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { container } = await setupSurfaceRefBlock(page);
|
||||
|
||||
// cut surface-ref block
|
||||
const surfaceRefBlock = page.locator('affine-surface-ref');
|
||||
await surfaceRefBlock.click();
|
||||
await page.waitForSelector('affine-surface-ref .focused');
|
||||
await cutByKeyboard(page);
|
||||
|
||||
// focus on the editor
|
||||
await container.click();
|
||||
|
||||
// paste the surface-ref block
|
||||
await pasteByKeyboard(page);
|
||||
await page.waitForTimeout(50);
|
||||
await expect(surfaceRefBlock).toHaveCount(1);
|
||||
await expect(surfaceRefBlock).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('paste to code block', () => {
|
||||
test('should replace the selected text when pasting plain text', async ({
|
||||
page,
|
||||
}) => {
|
||||
// press enter to focus on the first block of the editor
|
||||
await pressEnter(page);
|
||||
await addCodeBlock(page);
|
||||
await type(page, 'hello world hello');
|
||||
const { blockIds: codeBlockIds } = await getCodeBlockIds(page);
|
||||
await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11);
|
||||
await pasteContent(page, { 'text/plain': 'test' });
|
||||
await verifyCodeBlockContent(page, 0, 'hello test hello');
|
||||
});
|
||||
|
||||
test('should replace the selected text when pasting single snapshot', async ({
|
||||
page,
|
||||
}) => {
|
||||
// Create initial test blocks
|
||||
// add a paragraph block
|
||||
await createParagraphBlocks(page, ['test']);
|
||||
// add a code block
|
||||
await pressEnter(page);
|
||||
await addCodeBlock(page);
|
||||
await type(page, 'hello world hello');
|
||||
|
||||
// select the paragraph content
|
||||
const { blockIds: paragraphIds } = await getParagraphIds(page);
|
||||
await setSelection(page, paragraphIds[0], 0, paragraphIds[0], 4);
|
||||
// copy the paragraph content
|
||||
await copyByKeyboard(page);
|
||||
|
||||
// select 'world' in the code block
|
||||
const { blockIds: codeBlockIds } = await getCodeBlockIds(page);
|
||||
await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11);
|
||||
|
||||
// paste to the code block
|
||||
await pasteByKeyboard(page);
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
await verifyCodeBlockContent(page, 0, 'hello test hello');
|
||||
});
|
||||
|
||||
test('should replace the selected text when pasting multiple snapshots', async ({
|
||||
page,
|
||||
}) => {
|
||||
// Create initial test blocks
|
||||
// add three paragraph blocks
|
||||
await createParagraphBlocks(page, ['test', 'test', 'test']);
|
||||
|
||||
// add a code block
|
||||
await pressEnter(page);
|
||||
await addCodeBlock(page);
|
||||
await type(page, 'hello world hello');
|
||||
|
||||
// select all paragraph content
|
||||
const { blockIds: paragraphIds } = await getParagraphIds(page);
|
||||
await setSelection(page, paragraphIds[0], 0, paragraphIds[2], 4);
|
||||
// copy the paragraph content
|
||||
await copyByKeyboard(page);
|
||||
|
||||
// select 'world' in the code block
|
||||
const { blockIds: codeBlockIds } = await getCodeBlockIds(page);
|
||||
await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11);
|
||||
|
||||
// paste to the code block
|
||||
await pasteByKeyboard(page);
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
await verifyCodeBlockContent(page, 0, 'hello test\ntest\ntest hello');
|
||||
});
|
||||
|
||||
test('should preserve indentation when pasting code with spaces into code block', async ({
|
||||
page,
|
||||
}) => {
|
||||
await pressEnter(page);
|
||||
await addCodeBlock(page);
|
||||
|
||||
// Sample code with proper indentation for text/plain
|
||||
const plainTextCode = [
|
||||
'const fibonacci = (n: number): number => {',
|
||||
' if (n <= 1) return n;',
|
||||
' return fibonacci(n - 1) + fibonacci(n - 2);',
|
||||
'}',
|
||||
'function generateSequence(length: number) {',
|
||||
' const sequence = [];',
|
||||
' for (let i = 0; i < length; i++) {',
|
||||
' sequence.push(fibonacci(i));',
|
||||
' }',
|
||||
' return sequence;',
|
||||
'}',
|
||||
].join('\n');
|
||||
|
||||
const htmlCode =
|
||||
'<div><span>const</span><span> </span><span>fibonacci</span><span> </span><span>=</span><span> (n</span><span>:</span><span> </span><span>number</span><span>)</span><span>:</span><span> </span><span>number</span><span> </span><span>=></span><span> {</span></div><div><span> </span><span>if</span><span> (n </span><span><=</span><span> </span><span>1</span><span>) </span><span>return</span><span> n;</span></div><div><span> </span><span>return</span><span> </span><span>fibonacci</span><span>(n </span><span>-</span><span> </span><span>1</span><span>) </span><span>+</span><span> </span><span>fibonacci</span><span>(n </span><span>-</span><span> </span><span>2</span><span>);</span></div><div><span>}</span></div><div><span>function</span><span> </span><span>generateSequence</span><span>(length</span><span>:</span><span> </span><span>number</span><span>) {</span></div><div><span> </span><span>const</span><span> </span><span>sequence</span><span> </span><span>=</span><span> [];</span></div><div><span> </span><span>for</span><span> (</span><span>let</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span><</span><span> length; i</span><span>++</span><span>) {</span></div><div><span> sequence.</span><span>push</span><span>(</span><span>fibonacci</span><span>(i));</span></div><div><span> }</span></div><div><span> </span><span>return</span><span> sequence;</span></div><div><span>}</span></div></div>';
|
||||
|
||||
await pasteContent(page, {
|
||||
'text/plain': plainTextCode,
|
||||
'text/html': htmlCode,
|
||||
});
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Verify the pasted code maintains indentation
|
||||
await verifyCodeBlockContent(page, 0, plainTextCode);
|
||||
});
|
||||
|
||||
test('html tag should be treated as plain text when pasting', async ({
|
||||
page,
|
||||
}) => {
|
||||
await pressEnter(page);
|
||||
await addCodeBlock(page);
|
||||
|
||||
const textWithHtmlTags =
|
||||
'<div><span>const</span><span> </span><span>fibonacci</span><span> </span><span>=</span><span> (n</span><span>:</span><span> </span><span>number</span><span>)</span><span>:</span><span> </span><span>number</span><span> </span><span>=></span><span> {</span></div><div><span> </span><span>if</span><span> (n </span><span><=</span><span> </span><span>1</span><span>) </span><span>return</span><span> n;</span></div><div><span> </span><span>return</span><span> </span><span>fibonacci</span><span>(n </span><span>-</span><span> </span><span>1</span><span>) </span><span>+</span><span> </span><span>fibonacci</span><span>(n </span><span>-</span><span> </span><span>2</span><span>);</span></div><div><span>}</span></div><div><span>function</span><span> </span><span>generateSequence</span><span>(length</span><span>:</span><span> </span><span>number</span><span>) {</span></div><div><span> </span><span>const</span><span> </span><span>sequence</span><span> </span><span>=</span><span> [];</span></div><div><span> </span><span>for</span><span> (</span><span>let</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span><</span><span> length; i</span><span>++</span><span>) {</span></div><div><span> sequence.</span><span>push</span><span>(</span><span>fibonacci</span><span>(i));</span></div><div><span> }</span></div><div><span> </span><span>return</span><span> sequence;</span></div><div><span>}</span></div></div>';
|
||||
|
||||
await pasteContent(page, { 'text/plain': textWithHtmlTags });
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Verify the pasted code maintains indentation
|
||||
await verifyCodeBlockContent(page, 0, textWithHtmlTags);
|
||||
});
|
||||
|
||||
test('should not wrap line in brackets when pasting code', async ({
|
||||
page,
|
||||
}) => {
|
||||
await pressEnter(page);
|
||||
await addCodeBlock(page);
|
||||
const plainTextCode = [
|
||||
' model: anthropic("claude-sonnet-4-5-20250929"),',
|
||||
' prompt: How many people will live in the world in 2040?',
|
||||
' providerOptions: {',
|
||||
' anthropic: {',
|
||||
' thinking: { type: enabled, budgetTokens: 12000 },',
|
||||
' } satisfies AnthropicProviderOptions,',
|
||||
' },',
|
||||
].join('\n');
|
||||
|
||||
await pasteContent(page, { 'text/plain': plainTextCode });
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Verify the pasted code maintains indentation
|
||||
await verifyCodeBlockContent(page, 0, plainTextCode);
|
||||
});
|
||||
|
||||
test('should paste markdown text as plain text', async ({ page }) => {
|
||||
await pressEnter(page);
|
||||
await addCodeBlock(page);
|
||||
|
||||
const markdownText = [
|
||||
'# Heading 1',
|
||||
'',
|
||||
'## Heading 2 with **bold** and *italic*',
|
||||
'',
|
||||
'### Lists:',
|
||||
'- Item 1',
|
||||
' - Nested item with `inline code`',
|
||||
' - Another nested item',
|
||||
'- Item 2 with [link](https://example.com)',
|
||||
'',
|
||||
'```typescript',
|
||||
'const code = "block";',
|
||||
'console.log(code);',
|
||||
'```',
|
||||
'',
|
||||
'> This is a blockquote with **bold** text',
|
||||
'> Multiple lines in blockquote',
|
||||
'',
|
||||
'| Table | Header |',
|
||||
'|-------|--------|',
|
||||
'| Cell 1 | Cell 2 |',
|
||||
'$This is a inline latex$',
|
||||
].join('\n');
|
||||
|
||||
await pasteContent(page, { 'text/plain': markdownText });
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Verify the pasted code maintains indentation
|
||||
await verifyCodeBlockContent(page, 0, markdownText);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('paste in readonly mode', () => {
|
||||
test('should not paste content when document is in readonly mode', async ({
|
||||
page,
|
||||
}) => {
|
||||
await createParagraphBlocks(page, ['This is a test paragraph']);
|
||||
const { blockIds } = await getParagraphIds(page);
|
||||
const initialParagraphCount = blockIds.length;
|
||||
|
||||
await page.evaluate(() => {
|
||||
const pageRoot = document.querySelector(
|
||||
'affine-page-root'
|
||||
) as PageRootBlockComponent;
|
||||
pageRoot.store.readonly = true;
|
||||
});
|
||||
|
||||
await setSelection(page, blockIds[0], 0, blockIds[0], 4);
|
||||
await pasteContent(page, {
|
||||
'text/plain': ' - Added text that should not appear',
|
||||
});
|
||||
|
||||
await verifyParagraphContent(page, 0, 'This is a test paragraph');
|
||||
|
||||
await pressEnter(page);
|
||||
await pasteContent(page, { 'text/plain': 'This should not be pasted' });
|
||||
|
||||
const { blockIds: afterParagraphIds } = await getParagraphIds(page);
|
||||
expect(afterParagraphIds.length).toBe(initialParagraphCount);
|
||||
|
||||
await setSelection(page, blockIds[0], 0, blockIds[0], 4);
|
||||
await pasteByKeyboard(page);
|
||||
|
||||
await verifyParagraphContent(page, 0, 'This is a test paragraph');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('cross document clipboard regression', () => {
|
||||
test('copy and paste paragraph content between docs', async ({ page }) => {
|
||||
const container = locateEditorContainer(page);
|
||||
await container.click();
|
||||
|
||||
const sourceText = "Cross-doc paste can't fail again";
|
||||
await type(page, sourceText);
|
||||
|
||||
const { blockIds } = await getParagraphIds(page);
|
||||
await setSelection(page, blockIds[0], 0, blockIds[0], sourceText.length);
|
||||
|
||||
await copyByKeyboard(page);
|
||||
|
||||
await clickNewPageButton(page, 'Clipboard Destination');
|
||||
await waitForEditorLoad(page);
|
||||
|
||||
const destination = locateEditorContainer(page);
|
||||
await destination.click();
|
||||
|
||||
await pasteByKeyboard(page);
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
const pastedTexts = await page.locator(paragraphLocator).allTextContents();
|
||||
expect(pastedTexts.some(text => text.includes(sourceText))).toBe(true);
|
||||
});
|
||||
|
||||
test('copied content remains available to external clipboard consumers', async ({
|
||||
page,
|
||||
}) => {
|
||||
const container = locateEditorContainer(page);
|
||||
await container.click();
|
||||
|
||||
const textForExternal = 'External clipboard visibility check';
|
||||
await type(page, textForExternal);
|
||||
|
||||
const { blockIds } = await getParagraphIds(page);
|
||||
await setSelection(
|
||||
page,
|
||||
blockIds[0],
|
||||
0,
|
||||
blockIds[0],
|
||||
textForExternal.length
|
||||
);
|
||||
|
||||
await copyByKeyboard(page);
|
||||
|
||||
const plainText = await page.evaluate(() => navigator.clipboard.readText());
|
||||
|
||||
expect(plainText).toBe(textForExternal);
|
||||
});
|
||||
|
||||
test('copy and paste within a single document still duplicates content', async ({
|
||||
page,
|
||||
}) => {
|
||||
const container = locateEditorContainer(page);
|
||||
await container.click();
|
||||
|
||||
const intraDocText = 'Same doc paste regression guard';
|
||||
await type(page, intraDocText);
|
||||
|
||||
const { blockIds } = await getParagraphIds(page);
|
||||
await setSelection(page, blockIds[0], 0, blockIds[0], intraDocText.length);
|
||||
|
||||
await copyByKeyboard(page);
|
||||
|
||||
await pressEnter(page);
|
||||
await pasteByKeyboard(page);
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
await verifyParagraphContent(page, 1, intraDocText);
|
||||
});
|
||||
});
|
||||
|
||||
test('should copy single image from edgeless and paste to page', async ({
|
||||
page,
|
||||
}) => {
|
||||
await clickEdgelessModeButton(page);
|
||||
|
||||
const button = page.locator('edgeless-mindmap-tool-button');
|
||||
await button.click();
|
||||
|
||||
const menu = page.locator('edgeless-mindmap-menu');
|
||||
const mediaItem = menu.locator('.media-item');
|
||||
await mediaItem.click();
|
||||
|
||||
await importFile(page, 'large-image.png', async () => {
|
||||
await toViewCoord(page, [100, 250]);
|
||||
await clickView(page, [100, 250]);
|
||||
});
|
||||
|
||||
const image = page.locator('affine-edgeless-image').first();
|
||||
await image.click();
|
||||
|
||||
await copyByKeyboard(page);
|
||||
|
||||
await clickPageModeButton(page);
|
||||
await waitForEditorLoad(page);
|
||||
|
||||
const container = locateEditorContainer(page);
|
||||
await container.click();
|
||||
|
||||
await pasteByKeyboard(page);
|
||||
|
||||
await expect(page.locator('affine-page-image')).toBeVisible();
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user