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

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

View File

@@ -0,0 +1,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();
});

View 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');
});
});

View 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);
});

View 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();
});

View File

@@ -0,0 +1,115 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import { type, waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
import {
createNewPage,
gotoContentFromTitle,
initCodeBlockByOneStep,
} from './utils';
test.describe('Code Block Autocomplete Operations', () => {
test('angle brackets are not supported', async ({ page }) => {
// open the home page and insert the code block
await initCodeBlockByOneStep(page);
await page.keyboard.type('<');
const codeUnit = page.locator('affine-code-unit');
await expect(codeUnit).toHaveText('<');
});
});
test.describe('Code Block Preview', () => {
test('enable html preview', async ({ page }) => {
const code = page.locator('affine-code');
await openHomePage(page);
await createNewPage(page);
await waitForEditorLoad(page);
await gotoContentFromTitle(page);
await type(page, '```html aaa');
await code.hover({
position: {
x: 155,
y: 65,
},
});
await page.getByText('Preview').click();
await expect(
page
.locator('iframe[title="HTML Preview"]')
.contentFrame()
.getByText('aaa')
).toBeVisible();
});
test('enable mermaid preview', async ({ page }) => {
const code = page.locator('affine-code');
const mermaidSvg = page.locator('mermaid-preview .mermaid-preview-svg svg');
await openHomePage(page);
await createNewPage(page);
await waitForEditorLoad(page);
await gotoContentFromTitle(page);
await type(page, '```mermaid graph TD;A-->B');
await code.hover({
position: {
x: 155,
y: 65,
},
});
await page.getByText('Preview').click();
await expect(mermaidSvg).toBeVisible();
});
test('enable typst preview', async ({ page }) => {
const code = page.locator('affine-code');
const typstPreview = page.locator('typst-preview');
await openHomePage(page);
await createNewPage(page);
await waitForEditorLoad(page);
await gotoContentFromTitle(page);
await type(page, '```typst = Title');
await code.hover({
position: {
x: 155,
y: 65,
},
});
await page.getByText('Preview').click();
await expect(typstPreview).toBeVisible();
});
test('change lang without preview', async ({ page }) => {
const code = page.locator('affine-code');
const preview = page.locator('affine-code .affine-code-block-preview');
await openHomePage(page);
await createNewPage(page);
await waitForEditorLoad(page);
await gotoContentFromTitle(page);
await type(page, '```html aaa');
await code.hover({
position: {
x: 155,
y: 65,
},
});
await page.getByText('Preview').click();
await expect(preview).toBeVisible();
// change to lang without preview support
await page.getByTestId('lang-button').click();
await page.getByRole('button', { name: 'ABAP' }).click();
await expect(preview).toBeHidden();
// change back to html
await page.getByTestId('lang-button').click();
await page.getByRole('button', { name: 'HTML', exact: true }).click();
await expect(preview).toBeVisible();
});
});

View File

@@ -0,0 +1,192 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
addCodeBlock,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import {
closeSettingModal,
openEditorSetting,
} from '@affine-test/kit/utils/setting';
import { expect, type Page } from '@playwright/test';
import { initCodeBlockByOneStep, openCodeBlockMoreMenu } from './utils';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Opens Editor Settings and returns a locator for the
* "Show line numbers in code blocks" toggle switch.
*/
async function openLineNumbersSetting(page: Page) {
await openEditorSetting(page);
return page.getByTestId('code-block-line-numbers-trigger');
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test.describe('Global code block line numbers setting', () => {
test('line numbers are visible by default', async ({ page }) => {
await initCodeBlockByOneStep(page);
const lineNumber = page.locator('affine-code .line-number');
await expect(lineNumber).toBeVisible();
});
test('global toggle hides line numbers on all existing code blocks', async ({
page,
}) => {
await initCodeBlockByOneStep(page);
const lineNumber = page.locator('affine-code .line-number');
await expect(lineNumber).toBeVisible();
// Turn the global setting OFF
const toggle = await openLineNumbersSetting(page);
await expect(toggle).toBeVisible();
await toggle.click();
await closeSettingModal(page);
// Line numbers must now be hidden
await expect(lineNumber).toBeHidden();
});
test('turning global toggle back on restores line numbers', async ({
page,
}) => {
await initCodeBlockByOneStep(page);
const lineNumber = page.locator('affine-code .line-number');
// OFF
const toggle = await openLineNumbersSetting(page);
await toggle.click();
await closeSettingModal(page);
await expect(lineNumber).toBeHidden();
// ON again
const toggle2 = await openLineNumbersSetting(page);
await toggle2.click();
await closeSettingModal(page);
await expect(lineNumber).toBeVisible();
});
test('per-block toggle shows line numbers when global is off', async ({
page,
}) => {
await initCodeBlockByOneStep(page);
const lineNumber = page.locator('affine-code .line-number');
// Turn global setting OFF
const toggle = await openLineNumbersSetting(page);
await toggle.click();
await closeSettingModal(page);
await expect(lineNumber).toBeHidden();
// Per-block: explicitly enable for this block
const { lineNumberButton } = await openCodeBlockMoreMenu(page);
await lineNumberButton.click();
await expect(lineNumber).toBeVisible();
});
test('per-block toggle can hide line numbers when global is on', async ({
page,
}) => {
await initCodeBlockByOneStep(page);
const lineNumber = page.locator('affine-code .line-number');
// Global is ON by default
await expect(lineNumber).toBeVisible();
// Per-block: hide for this block
const { cancelLineNumberButton } = await openCodeBlockMoreMenu(page);
await cancelLineNumberButton.click();
// Click away to dismiss menu
await page.mouse.click(300, 300);
await expect(lineNumber).toBeHidden();
// Turn global setting OFF and then ON - per-block override persists
const toggle = await openLineNumbersSetting(page);
await toggle.click(); // OFF
await toggle.click(); // ON
await closeSettingModal(page);
// Per-block explicitly set to false - still hidden
await expect(lineNumber).toBeHidden();
});
test('global setting is reflected on newly created code blocks', async ({
page,
}) => {
// Start on the home page and disable line numbers globally first
await openHomePage(page);
await waitForEditorLoad(page);
const toggle = await openLineNumbersSetting(page);
await toggle.click(); // OFF
await closeSettingModal(page);
// Now create a fresh code block
await initCodeBlockByOneStep(page);
const lineNumber = page.locator('affine-code .line-number');
await expect(lineNumber).toBeHidden();
});
test('global toggle affects all code blocks on the page', async ({
page,
}) => {
await initCodeBlockByOneStep(page);
// Exit the code block (Mod+Enter creates a paragraph below and moves focus there)
const isMac = process.platform === 'darwin';
await page.keyboard.press(isMac ? 'Meta+Enter' : 'Control+Enter');
// Add a second code block in the new paragraph
await addCodeBlock(page);
// Both blocks should show line numbers by default
const lineNumbers = page.locator('affine-code .line-number');
await expect(lineNumbers).toHaveCount(2);
await expect(lineNumbers.first()).toBeVisible();
await expect(lineNumbers.last()).toBeVisible();
// Turn global OFF - both must hide
const toggle = await openLineNumbersSetting(page);
await toggle.click();
await closeSettingModal(page);
await expect(lineNumbers.first()).toBeHidden();
await expect(lineNumbers.last()).toBeHidden();
// Turn global ON - both must show again
const toggle2 = await openLineNumbersSetting(page);
await toggle2.click();
await closeSettingModal(page);
await expect(lineNumbers.first()).toBeVisible();
await expect(lineNumbers.last()).toBeVisible();
});
test('global setting persists across page reload', async ({ page }) => {
await initCodeBlockByOneStep(page);
const lineNumber = page.locator('affine-code .line-number');
await expect(lineNumber).toBeVisible();
// Turn global OFF
const toggle = await openLineNumbersSetting(page);
await toggle.click();
await closeSettingModal(page);
await expect(lineNumber).toBeHidden();
// Reload the page - setting must survive
await page.reload();
await waitForEditorLoad(page);
await expect(lineNumber).toBeHidden();
});
});

View File

@@ -0,0 +1,46 @@
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
addCodeBlock,
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import type { Page } from '@playwright/test';
export const gotoContentFromTitle = async (page: Page) => {
await page.keyboard.press('Enter');
};
export const createNewPage = async (page: Page) => {
await clickNewPageButton(page);
};
export const initCodeBlockByOneStep = async (page: Page) => {
await openHomePage(page);
await createNewPage(page);
await waitForEditorLoad(page);
await gotoContentFromTitle(page);
await addCodeBlock(page);
};
/**
* Opens the "More" menu on the first code block and returns locators for
* the line-number toggle buttons inside it.
* Uses .first() to stay deterministic when multiple code blocks are present.
*/
export const openCodeBlockMoreMenu = async (page: Page) => {
const codeBlock = page.locator('affine-code').first();
await codeBlock.hover();
const moreButton = page
.locator('affine-code-toolbar')
.getByRole('button', { name: 'More' });
await moreButton.click();
const menu = page.locator('.more-popup-menu');
const lineNumberButton = menu.getByRole('button', { name: 'Line number' });
const cancelLineNumberButton = menu.getByRole('button', {
name: 'Cancel line number',
});
return { menu, lineNumberButton, cancelLineNumberButton };
};

View File

@@ -0,0 +1,111 @@
import { test } from '@affine-test/kit/playwright';
import { importFile } from '@affine-test/kit/utils/attachment';
import {
clickEdgelessModeButton,
dragView,
locateToolbar,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
getBlockSuiteEditorTitle,
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('should keep color on custom color button', async ({ page }) => {
await page.getByTestId('header-dropDownButton').click();
await page.getByTestId('editor-option-menu-import').click();
const importDialog = page.getByTestId('import-dialog');
await expect(importDialog).toBeVisible();
const importSnapshot = importDialog.getByTestId(
'editor-option-menu-import-snapshot'
);
const completeButton = importDialog.getByRole('button', { name: 'Complete' });
await importFile(page, 'v1-color-palettes-snapshot.zip', async () => {
await importSnapshot.click();
});
await completeButton.click();
await waitForEditorLoad(page);
const title = getBlockSuiteEditorTitle(page);
await expect(title).toContainText('Getting Started');
await clickEdgelessModeButton(page);
const toolbar = locateToolbar(page);
const colorPicker = toolbar.locator('edgeless-color-picker-button');
// frame
{
const frameTitle = page.locator('affine-frame-title');
await frameTitle.click();
await expect(toolbar).toBeVisible();
const backgroundButton = toolbar.getByLabel('Background');
const colorButton = backgroundButton
.locator('edgeless-color-button')
.first();
const color = await colorButton.locator('svg').getAttribute('fill');
expect(color).not.toBeNull();
await backgroundButton.click();
const customColorButton = colorPicker.locator(
'edgeless-color-custom-button'
);
const keptColor = await customColorButton.evaluate(e =>
e.style.getPropertyValue('--c')
);
expect(keptColor.startsWith('#')).toBe(true);
expect(keptColor.length).toBe(7);
expect(color).toBe(keptColor);
}
// shape
{
await dragView(page, [5425 - 10, 2658 - 10], [5425 + 20, 2658 + 20]);
await expect(toolbar).toBeVisible();
const colorButton = toolbar.getByLabel(/^Color$/);
await colorButton.click();
const fillCustomColorButton = colorPicker
.locator('edgeless-color-custom-button')
.first();
const strokeCustomColorButton = colorPicker
.locator('edgeless-color-custom-button')
.last();
const fillKeptColor = await fillCustomColorButton.evaluate(e =>
e.style.getPropertyValue('--c')
);
const strokeKeptColor = await strokeCustomColorButton.evaluate(e =>
e.style.getPropertyValue('--c')
);
expect(fillKeptColor.length).toBe(7);
expect(fillKeptColor.startsWith('#')).toBe(true);
expect(strokeKeptColor.length).toBe(7);
expect(strokeKeptColor.startsWith('#')).toBe(true);
}
});

View File

@@ -0,0 +1,69 @@
import { test } from '@affine-test/kit/playwright';
import {
addColumn,
addRows,
initDatabaseByOneStep,
pasteString,
selectCell,
verifyCellContents,
} from './utils';
test.describe('Database Clipboard Operations', () => {
test('paste tab-separated data from Excel into database', async ({
page,
}) => {
// Open the home page and wait for the editor to load
await initDatabaseByOneStep(page);
await addColumn(page, 'multi-select');
// Create a database block with two rows
await addRows(page, 2);
// Select the first cell and paste data
await selectCell(page, 0, false);
const mockExcelData = 'Cell 1A\tCell 1B\nCell 2A\tCell 2B';
await pasteString(page, mockExcelData);
// Verify cell contents
await verifyCellContents(page, [
'Cell 1A',
'Cell 1B',
'Cell 2A',
'Cell 2B',
]);
});
test('handle empty cells when pasting tab-separated data', async ({
page,
}) => {
// Open the home page and wait for the editor to load
await initDatabaseByOneStep(page);
await addColumn(page, 'multi-select');
// Create a database block with two rows
await addRows(page, 2);
// Select the first cell and paste data with empty cells
await selectCell(page, 0, false);
const mockExcelData = 'Cell 1A\t\nCell 2A\tCell 2B';
await pasteString(page, mockExcelData);
// Verify cell contents including empty cells
await verifyCellContents(page, ['Cell 1A', '', 'Cell 2A', 'Cell 2B']);
});
test('handle pasting data larger than selected area', async ({ page }) => {
// Open the home page and wait for the editor to load
await initDatabaseByOneStep(page);
await addColumn(page, 'multi-select');
// Create a database block with one row
await addRows(page, 1);
// Select the first cell and paste data larger than table
await selectCell(page, 0, false);
const mockExcelData = 'Cell 1A\tCell 1B\nCell 2A\tCell 2B';
await pasteString(page, mockExcelData);
// Verify only the cells that exist are filled
await verifyCellContents(page, ['Cell 1A', 'Cell 1B']);
});
});

View File

@@ -0,0 +1,99 @@
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';
import {
addColumn,
createDatabaseBlock,
createNewPage,
gotoContentFromTitle,
selectCell,
} from './utils';
test.describe('Database Rich Text Column', () => {
test('paste document link into rich text cell', async ({ page }) => {
// Step 1: Open home page
await openHomePage(page);
await waitForEditorLoad(page);
// Step 2: Create a new page
await createNewPage(page);
await gotoContentFromTitle(page);
// Step 3: Create a database in the page
await createDatabaseBlock(page);
// Step 4: Add a text column
await addColumn(page, 'Text');
// Step 5: Create a new page to get its link
await clickNewPageButton(page, 'Test Page');
const pageUrl = page.url();
// Step 6: Go back to database page
await page.goBack();
await waitForEditorLoad(page);
// Step 7: Select and edit the rich text cell
const richTextCell = await selectCell(page, 2);
// Step 8: Paste the document link
await page.evaluate(url => {
const clipboardData = new DataTransfer();
clipboardData.setData('text/plain', url);
const pasteEvent = new ClipboardEvent('paste', {
clipboardData,
bubbles: true,
cancelable: true,
});
document.activeElement?.dispatchEvent(pasteEvent);
}, pageUrl);
// Step 9: Verify the result
const referenceTitle = richTextCell.locator('.affine-reference-title');
await expect(referenceTitle).toBeVisible();
await expect(referenceTitle).toContainText('Test Page');
});
test('add document link via @ in rich text cell', async ({ page }) => {
// Step 1: Open home page
await openHomePage(page);
await waitForEditorLoad(page);
// Step 2: Create a new page
await createNewPage(page);
await gotoContentFromTitle(page);
// Step 3: Create a database in the page
await createDatabaseBlock(page);
// Step 4: Add a text column
await addColumn(page, 'Text');
// Step 5: Create a new page as reference target
await clickNewPageButton(page, 'Reference Target');
await page.goBack();
await waitForEditorLoad(page);
// Step 6: Select and edit the rich text cell
const richTextCell = await selectCell(page, 2);
await richTextCell.click();
await page.keyboard.type('@');
// Step 7: Wait for reference picker and select the page
const linkedDocPopover = page.locator('.linked-doc-popover');
await expect(linkedDocPopover).toBeVisible();
const targetPage = linkedDocPopover.getByText('Reference Target');
await targetPage.click();
// Step 8: Verify the result
const referenceTitle = richTextCell.locator('.affine-reference-title');
await expect(referenceTitle).toBeVisible();
await expect(referenceTitle).toContainText('Reference Target');
});
});

View File

@@ -0,0 +1,122 @@
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
addDatabase,
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
export async function createNewPage(page: Page) {
await clickNewPageButton(page);
}
export const gotoContentFromTitle = async (page: Page) => {
await page.keyboard.press('Enter');
};
export async function createDatabaseBlock(page: Page) {
await addDatabase(page);
}
export async function addRows(page: Page, rowCount: number) {
for (let i = 0; i < rowCount; i++) {
await addDatabaseRow(page);
}
}
export async function addDatabaseRow(page: Page) {
const addButton = page.locator('.data-view-table-group-add-row');
await addButton.click();
}
export async function pasteString(page: Page, data: string) {
await page.evaluate(data => {
const clipboardData = new DataTransfer();
clipboardData.setData('text/plain', data);
const pasteEvent = new ClipboardEvent('paste', {
clipboardData,
bubbles: true,
cancelable: true,
});
const activeElement = document.activeElement;
if (activeElement) {
pasteEvent.preventDefault();
activeElement.dispatchEvent(pasteEvent);
}
}, data);
}
export async function selectCell(page: Page, nth: number, editing = true) {
const firstCell = page.locator('dv-table-view-cell-container').nth(nth);
// First click for focus
await firstCell.click({ delay: 100 });
// Second click for edit mode
if (editing) {
await firstCell.click({ delay: 100 });
}
return firstCell;
}
export async function verifyCellContents(
page: Page,
expectedContents: string[]
) {
const cells = page.locator('dv-table-view-cell-container');
for (let i = 0; i < expectedContents.length; i++) {
const cell = cells.nth(i);
await expect(cell.locator('uni-lit > *:first-child')).toHaveText(
expectedContents[i]
);
}
}
export async function selectColumnType(
page: Page,
columnType: string,
nth: number = 1
) {
const typeMenu = page.locator('affine-menu').getByText('Type');
await page.waitForTimeout(100);
await typeMenu.hover();
await page.mouse.move(0, 0);
await page.waitForTimeout(100);
for (const char of columnType.split('')) {
await page.keyboard.type(char);
await page.waitForTimeout(10);
}
await page.waitForTimeout(100);
for (let i = 0; i < nth; i++) {
await page.keyboard.press('ArrowDown');
}
await page.waitForTimeout(100);
await page.keyboard.press('Enter');
await page.waitForTimeout(100);
}
export async function addColumn(page: Page, type: string, nth: number = 1) {
await clickAddColumnButton(page);
await selectColumnType(page, type, nth);
}
export async function clickAddColumnButton(page: Page) {
const addColumnButton = page.locator('.header-add-column-button');
await addColumnButton.click();
}
export async function changeColumnType(
page: Page,
columnIndex: number,
columnType: string
) {
const header = page.locator('affine-database-header-column').nth(columnIndex);
await header.click();
await selectColumnType(page, columnType);
}
export const initDatabaseByOneStep = async (page: Page) => {
await openHomePage(page);
await createNewPage(page);
await waitForEditorLoad(page);
await gotoContentFromTitle(page);
await createDatabaseBlock(page);
};

View File

@@ -0,0 +1,100 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickView,
dragView,
locateEditorContainer,
locateToolbar,
setEdgelessTool,
toViewCoord,
} from '@affine-test/kit/utils/editor';
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.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
test('should hide toolbar when creating connector', async ({ page }) => {
await setEdgelessTool(page, 'shape');
await dragView(page, [100, 300], [200, 400]);
await clickView(page, [150, 350]);
const toolbar = locateToolbar(page);
await expect(toolbar).toBeVisible();
const autoComplete = page.locator('edgeless-auto-complete');
const rightArrowButton = autoComplete
.locator('.edgeless-auto-complete-arrow')
.nth(0);
const bounds = await rightArrowButton.boundingBox();
expect(bounds).not.toBeNull();
const { x, y, width: w, height: h } = bounds!;
const startPos = [x + w / 2, y + h / 2];
const endPos = [startPos[0] + 100, startPos[1] + 100];
await page.mouse.move(startPos[0], startPos[1]);
await page.mouse.down();
await page.mouse.move(endPos[0], endPos[1]);
await expect(toolbar).toBeHidden();
await page.mouse.up();
const autoCompletePanel = page
.locator('edgeless-auto-complete-panel')
.locator('.auto-complete-panel-container');
await expect(toolbar).toBeHidden();
await expect(autoCompletePanel).toBeVisible();
});
test('should toggle toolbar when connecting target', async ({ page }) => {
await setEdgelessTool(page, 'shape');
// source
await dragView(page, [100, 300], [200, 400]);
// target
await setEdgelessTool(page, 'shape');
await dragView(page, [300, 300], [400, 400]);
await clickView(page, [150, 350]);
const toolbar = locateToolbar(page);
await expect(toolbar).toBeVisible();
const autoComplete = page.locator('edgeless-auto-complete');
const rightArrowButton = autoComplete
.locator('.edgeless-auto-complete-arrow')
.nth(0);
const bounds = await rightArrowButton.boundingBox();
expect(bounds).not.toBeNull();
const { x, y, width: w, height: h } = bounds!;
const startPos = [x + w / 2, y + h / 2];
const endPos = await toViewCoord(page, [300, 350]);
await page.mouse.move(startPos[0], startPos[1]);
await page.mouse.down();
await page.mouse.move(endPos[0], endPos[1]);
await expect(toolbar).toBeHidden();
await page.mouse.up();
await expect(toolbar).toBeVisible();
});

View File

@@ -0,0 +1,197 @@
import { test } from '@affine-test/kit/playwright';
import {
type CanvasRendererPerfSnapshot,
deleteEdgelessElements,
getCanvasRendererPerfSnapshot,
resetCanvasRendererPerfMetrics,
seedEdgelessPerfScene,
} from '@affine-test/kit/utils/edgeless-perf';
import {
clickEdgelessModeButton,
dragView,
fitViewportToContent,
getEdgelessSelectedIds,
getSelectedXYWH,
locateEditorContainer,
setEdgelessTool,
setViewportZoom,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
const PERF_ENV = 'AFFINE_RUN_PERF_E2E';
const perfEnabled = process.env[PERF_ENV] === '1';
const modKey = process.platform === 'darwin' ? 'Meta' : 'Control';
type PerfScenarioResult = {
name: string;
snapshot: CanvasRendererPerfSnapshot;
};
test.describe.serial('canvas renderer perf probes', () => {
test.skip(!perfEnabled, `Set ${PERF_ENV}=1 to run manual perf probes`);
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
await locateEditorContainer(page).click();
});
test('collect metrics for common edgeless canvas scenarios', async ({
page,
}, testInfo) => {
test.slow();
const results: PerfScenarioResult[] = [];
let addedShapeIds: string[] = [];
const selectWholePerfScene = async () => {
await setEdgelessTool(page, 'default');
await dragView(page, [80, 140], [2300, 1500]);
await expect
.poll(async () => (await getEdgelessSelectedIds(page)).length)
.toBeGreaterThan(0);
};
const recordScenario = async (
name: string,
action: () => Promise<void>
) => {
await resetCanvasRendererPerfMetrics(page);
await action();
await page.waitForTimeout(400);
const snapshot = await getCanvasRendererPerfSnapshot(page);
results.push({ name, snapshot });
console.log(
`[canvas-perf] ${name}: ${JSON.stringify(snapshot.metrics, null, 2)}`
);
};
const initial = await seedEdgelessPerfScene(page, {
shapeCount: 120,
rowLength: 12,
startX: 120,
startY: 180,
width: 160,
height: 120,
});
addedShapeIds = initial.shapeIds;
await fitViewportToContent(page);
await page.waitForTimeout(500);
await recordScenario('add-shapes', async () => {
const seeded = await seedEdgelessPerfScene(page, {
shapeCount: 40,
rowLength: 10,
startX: 160,
startY: 1720,
width: 160,
height: 120,
});
addedShapeIds = addedShapeIds.concat(seeded.shapeIds);
await fitViewportToContent(page);
});
await recordScenario('delete-shapes', async () => {
await deleteEdgelessElements(page, addedShapeIds.slice(-20));
});
await recordScenario('box-select', async () => {
await selectWholePerfScene();
});
await recordScenario('group-selection', async () => {
await selectWholePerfScene();
await page.keyboard.press(`${modKey}+g`);
});
await recordScenario('ungroup-selection', async () => {
await page.keyboard.press(`${modKey}+Shift+g`);
});
await recordScenario('large-drag-selection', async () => {
await selectWholePerfScene();
const [x, y, w, h] = await getSelectedXYWH(page);
const center: [number, number] = [x + w / 2, y + h / 2];
await dragView(page, center, [center[0] + 1200, center[1] + 900]);
});
await recordScenario('large-pan', async () => {
await setEdgelessTool(page, 'pan');
await dragView(page, [1200, 900], [200, 180]);
});
await recordScenario('large-zoom', async () => {
await setViewportZoom(page, 0.25);
await page.waitForTimeout(200);
await setViewportZoom(page, 2.2);
await page.waitForTimeout(200);
await fitViewportToContent(page);
});
const finalSnapshot = await getCanvasRendererPerfSnapshot(page);
expect(finalSnapshot.rendererType).toBe('CanvasRenderer');
expect(results.length).toBeGreaterThanOrEqual(7);
await testInfo.attach('canvas-renderer-perf-scenarios.json', {
body: JSON.stringify(results, null, 2),
contentType: 'application/json',
});
});
test('collect metrics for interleaved block and canvas layers', async ({
page,
}, testInfo) => {
test.slow();
await seedEdgelessPerfScene(page, {
interleaved: true,
noteCount: 21,
shapeCount: 20,
rowLength: 1,
startX: 120,
startY: 180,
width: 180,
height: 120,
});
await fitViewportToContent(page);
await page.waitForTimeout(500);
const snapshot = await getCanvasRendererPerfSnapshot(page);
const metrics = snapshot.metrics as {
canvasMemoryMegabytes?: number;
lastRenderMetrics?: {
renderByBoundCallCount?: number;
};
stackingCanvasCount?: number;
visibleStackingCanvasCount?: number;
} | null;
console.log(
`[canvas-perf] interleaved-layers: ${JSON.stringify(snapshot, null, 2)}`
);
expect(snapshot.rendererType).toBe('CanvasRenderer');
expect(metrics).not.toBeNull();
expect(metrics?.stackingCanvasCount ?? 0).toBeGreaterThan(0);
expect(
metrics?.lastRenderMetrics?.renderByBoundCallCount ?? 0
).toBeGreaterThan(1);
expect(metrics?.visibleStackingCanvasCount ?? 0).toBeGreaterThan(0);
expect(metrics?.canvasMemoryMegabytes ?? 0).toBeLessThan(5);
await testInfo.attach('canvas-renderer-layering.json', {
body: JSON.stringify(snapshot, null, 2),
contentType: 'application/json',
});
});
});

View File

@@ -0,0 +1,65 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickView,
dblclickView,
dragView,
locateEditorContainer,
locateToolbar,
setEdgelessTool,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
switchEdgelessTheme,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
test('should correct text color on toolbar', async ({ page }) => {
await setEdgelessTool(page, 'connector');
await dragView(page, [100, 300], [200, 400]);
await dblclickView(page, [150, 350]);
await expect(
page.locator('edgeless-connector-label-editor rich-text')
).toBeVisible();
await page.keyboard.type('label');
await page.keyboard.press('Escape');
const toolbar = locateToolbar(page);
const textColorContainer = toolbar.locator(
'edgeless-color-picker-button.text-color'
);
const textColorBtn = textColorContainer.getByLabel('Text color');
const blackBtn = textColorContainer
.locator('edgeless-color-button[active]')
.getByLabel('Black');
await expect(textColorContainer).toBeVisible();
await textColorBtn.click();
await expect(blackBtn).toHaveCount(1);
const svgFillColor = await blackBtn.locator('svg').getAttribute('fill');
expect(svgFillColor).toBe('#000000');
await switchEdgelessTheme(page, 'dark');
await clickView(page, [150, 350]);
await textColorBtn.click();
await expect(blackBtn).toHaveCount(1);
const svgFillColor2 = await blackBtn.locator('svg').getAttribute('fill');
expect(svgFillColor2).toBe('#ffffff');
});

View File

@@ -0,0 +1,60 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
dblclickNoteBody,
locateEditorContainer,
locateToolbar,
} from '@affine-test/kit/utils/editor';
import { pressEnter } from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { isContainedInBoundingBox } from '@affine-test/kit/utils/utils';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
test('should close embed editing modal when editor switching to page mode by short cut', async ({
page,
}) => {
await page.keyboard.press('@');
await page.getByTestId('cmdk-label').getByText('Getting Started').click();
const toolbar = locateToolbar(page);
await toolbar.getByLabel('Switch view').click();
await toolbar.getByLabel('Card view').click();
await toolbar.getByLabel('Edit').click();
const editingModal = page.locator('embed-card-edit-modal');
await expect(editingModal).toBeVisible();
await page.keyboard.press('Alt+s');
await waitForEditorLoad(page);
await expect(editingModal).toBeHidden();
});
test('embed card should not overflow the edgeless note', async ({ page }) => {
const note = page.locator('affine-edgeless-note');
await dblclickNoteBody(page);
await type(page, '/github');
await pressEnter(page);
await page
.locator('.embed-card-modal-input')
.fill('https://github.com/toeverything/AFFiNE/pull/10442');
await pressEnter(page);
const embedCard = page.locator('affine-embed-github-block');
await embedCard
.locator('.affine-embed-github-block:not(.loading)')
.waitFor({ state: 'visible' });
expect(await isContainedInBoundingBox(note, embedCard, true)).toBe(true);
});

View File

@@ -0,0 +1,64 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickView,
createEdgelessNoteBlock,
dragView,
locateEditorContainer,
locateToolbar,
toViewCoord,
} from '@affine-test/kit/utils/editor';
import {
pressBackspace,
pressEscape,
selectAllByKeyboard,
} from '@affine-test/kit/utils/keyboard';
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.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
await selectAllByKeyboard(page);
await pressBackspace(page);
});
test('should update zindex of element when moving it into frame', async ({
page,
}) => {
const toolbar = locateToolbar(page);
// create a top frame
await page.keyboard.press('f');
await dragView(page, [0, 100], [300, 400]);
await toolbar.getByLabel('Background').click();
await toolbar.getByLabel('LightRed').click();
await pressEscape(page);
// create a note
await createEdgelessNoteBlock(page, [500, 500]);
await clickView(page, [0, 100]);
await clickView(page, [500, 500]);
await toolbar.getByLabel('More menu').click();
await toolbar.getByLabel('Send to Back').click();
await pressEscape(page);
await dragView(page, [500, 500], [100, 200]);
const point = await toViewCoord(page, [100, 200]);
const isNoteAboveFrame = await page.evaluate(point => {
return !!document
.elementFromPoint(point[0], point[1])
?.closest('affine-edgeless-note');
}, point);
expect(isNoteAboveFrame).toBe(true);
});

View File

@@ -0,0 +1,85 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickView,
dragView,
locateEditorContainer,
locateToolbar,
setEdgelessTool,
} from '@affine-test/kit/utils/editor';
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.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
test('should add highlighter', async ({ page }) => {
await setEdgelessTool(page, 'highlighter');
await dragView(page, [100, 300], [200, 400]);
await setEdgelessTool(page, 'default');
await clickView(page, [150, 350]);
const toolbar = locateToolbar(page);
await page.waitForTimeout(250);
await expect(toolbar).toBeVisible();
const lineWidthButton = toolbar
.locator('affine-slider')
.locator('.point-button[data-selected]')
.last();
const defaultLineWidth = await lineWidthButton.getAttribute('aria-label');
expect(defaultLineWidth).toBe('22');
});
test('should exit drawing tools menu when Escape is pressed', async ({
page,
}) => {
await setEdgelessTool(page, 'highlighter');
const drawingToolsMenu = page.locator('edgeless-pen-menu');
await expect(drawingToolsMenu).toBeVisible();
await page.keyboard.press('Escape');
await expect(drawingToolsMenu).toBeHidden();
});
test('should enter highlighter tool when `Shift + P` is pressed', async ({
page,
}) => {
const drawingToolButton = page.locator('.edgeless-pen-button');
const drawingToolsMenu = page.locator('edgeless-pen-menu');
await expect(drawingToolButton).toHaveAttribute('data-drawing-tool', 'brush');
await expect(drawingToolsMenu).toBeHidden();
await page.keyboard.press('Shift+P');
await expect(drawingToolButton).toHaveAttribute(
'data-drawing-tool',
'highlighter'
);
await expect(drawingToolsMenu).toBeVisible();
await page.keyboard.press('Escape');
await expect(drawingToolsMenu).toBeHidden();
await expect(drawingToolButton).toHaveAttribute(
'data-drawing-tool',
'highlighter'
);
});

View File

@@ -0,0 +1,535 @@
import { test } from '@affine-test/kit/playwright';
import {
assertTitle,
clickEdgelessModeButton,
clickView,
createEdgelessNoteBlock,
getEdgelessSelectedIds,
getPageMode,
getSelectedXYWH,
isDocTitleFocused,
locateEditorContainer,
locateModeSwitchButton,
locateToolbar,
moveToView,
resizeElementByHandle,
toViewCoord,
} from '@affine-test/kit/utils/editor';
import {
pasteByKeyboard,
pressBackspace,
pressEnter,
pressEscape,
selectAllByKeyboard,
undoByKeyboard,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { clickLocatorByRatio } from '@affine-test/kit/utils/utils';
import type { EdgelessRootBlockComponent } from '@blocksuite/affine/blocks/root';
import type { IVec } from '@blocksuite/affine/global/gfx';
import type { NoteBlockModel } from '@blocksuite/affine/model';
import { expect, type Page } from '@playwright/test';
const title = 'Edgeless Note Header Test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page, title);
await page.keyboard.press('Enter');
await page.keyboard.type('Hello');
await page.keyboard.press('Enter');
await page.keyboard.type('World');
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
// the first note block is called page block
test.describe('edgeless page block', () => {
const locateHeaderToolbar = (page: Page) =>
page.getByTestId('edgeless-page-block-header');
test('only first note block has header toolbar and its element toolbar', async ({
page,
}) => {
const toolbar = locateHeaderToolbar(page);
await expect(toolbar).toHaveCount(1);
await expect(toolbar).toBeVisible();
await createEdgelessNoteBlock(page, [100, 100]);
await expect(toolbar).toHaveCount(1);
await expect(toolbar).toBeVisible();
});
test('should shrink note block when clicking on the toggle button', async ({
page,
}) => {
const toolbar = locateHeaderToolbar(page);
const toolBox = await toolbar.boundingBox();
const noteBox = await page.locator('affine-edgeless-note').boundingBox();
if (!noteBox || !toolBox) throw new Error('Bounding box not found');
expect(noteBox.height).toBeGreaterThan(toolBox.height);
const toggleButton = toolbar.getByTestId('edgeless-note-toggle-button');
await toggleButton.click();
const newNoteBox = await page.locator('affine-edgeless-note').boundingBox();
if (!newNoteBox) throw new Error('Bounding box not found');
expect(newNoteBox.height).toBe(toolBox.height);
await toggleButton.click();
const newNoteBox2 = await page
.locator('affine-edgeless-note')
.boundingBox();
if (!newNoteBox2) throw new Error('Bounding box not found');
expect(newNoteBox2).toEqual(noteBox);
});
test('page title in toolbar should be displayed when page block is collapsed and hidden when page block is not collapsed', async ({
page,
}) => {
const toolbar = locateHeaderToolbar(page);
const toolbarTitle = toolbar.getByTestId('edgeless-note-title');
await expect(toolbarTitle).toHaveText('');
const toggleButton = toolbar.getByTestId('edgeless-note-toggle-button');
await toggleButton.click();
await expect(toolbarTitle).toHaveText(title);
await toggleButton.click();
await expect(toolbarTitle).toHaveText('');
});
test('should switch to page mode when expand button is clicked', async ({
page,
}) => {
const toolbar = locateHeaderToolbar(page);
const viewInPageButton = toolbar.getByTestId(
'edgeless-note-view-in-page-button'
);
await viewInPageButton.click();
expect(await getPageMode(page)).toBe('page');
});
test('should open doc properties dialog when info button is clicked', async ({
page,
}) => {
const toolbar = locateHeaderToolbar(page);
const infoButton = toolbar.getByTestId('edgeless-note-info-button');
await infoButton.click();
const infoModal = page.getByTestId('info-modal');
await expect(infoModal).toBeVisible();
});
test('should copy note edgeless link to clipboard when link button is clicked', async ({
page,
}) => {
const toolbar = locateHeaderToolbar(page);
await selectAllByKeyboard(page);
const noteId = (await getEdgelessSelectedIds(page))[0];
const linkButton = toolbar.getByTestId('edgeless-note-link-button');
await linkButton.click();
const url = page.url();
const link = await page.evaluate(() => navigator.clipboard.readText());
expect(link).toBe(`${url}&blockIds=${noteId}`);
});
test('info button should hidden in peek view', async ({ page }) => {
const url = page.url();
await page.evaluate(url => navigator.clipboard.writeText(url), url);
await clickNewPageButton(page);
await page.keyboard.press('Enter');
await pasteByKeyboard(page);
const reference = page.locator('affine-reference');
await reference.click({ modifiers: ['Shift'] });
const toolbar = locateHeaderToolbar(page);
const infoButton = toolbar.getByTestId('edgeless-note-info-button');
await expect(toolbar).toBeVisible();
await expect(infoButton).toBeHidden();
});
test('caret on focusing', async ({ page }) => {
const note = page.locator('affine-edgeless-note');
await note.click(); // focus note
// click on title's rear
const docTitle = note.locator('edgeless-page-block-title');
await expect(docTitle).toBeVisible();
await clickLocatorByRatio(page, docTitle, { xRatio: 0.9, yRatio: 0.8 });
const hasCaretInTitle = await page.evaluate(
hasCaretIn,
'edgeless-page-block-title'
);
expect(hasCaretInTitle).toBe(true);
await clickLocatorByRatio(page, note, { xRatio: 1.1, yRatio: 0.1 }); // cancel note focus
await note.click(); // focus note again
// click on firstParagraph's rear
const firstParagraph = note.locator('affine-paragraph:first-child');
await expect(firstParagraph).toBeVisible();
await clickLocatorByRatio(page, firstParagraph, {
xRatio: 0.9,
yRatio: 0.5,
});
const hasCaretInParagraph = await page.evaluate(
hasCaretIn,
'affine-paragraph'
);
expect(hasCaretInParagraph).toBe(true);
function hasCaretIn(elemSelector: string) {
const sel = document.getSelection();
if (!sel || sel.rangeCount === 0) return false;
const startContainer = sel.getRangeAt(0).startContainer;
const selContainer =
startContainer.nodeType === Node.TEXT_NODE
? startContainer.parentElement
: startContainer;
if (!selContainer) return false;
const closestDstElem = (selContainer as HTMLElement)?.closest(
elemSelector
);
if (!closestDstElem) return false;
return true;
}
});
test('page title should be editable', async ({ page }) => {
const note = page.locator('affine-edgeless-note');
const docTitle = note.locator('edgeless-page-block-title');
await expect(docTitle).toBeVisible();
await assertTitle(page, title);
await note.dblclick();
await docTitle.click();
// clear the title
await selectAllByKeyboard(page);
await pressBackspace(page);
await assertTitle(page, '');
// type new title
await type(page, 'New Title');
await assertTitle(page, 'New Title');
// cursor could move between doc title and note content
await page.keyboard.press('ArrowDown');
await type(page, 'xx');
const paragraphs = note.locator('affine-paragraph v-line');
const numParagraphs = await paragraphs.count();
await expect(paragraphs.first()).toHaveText('xxHello');
await page.keyboard.press('ArrowUp');
await type(page, 'yy');
await assertTitle(page, 'yyNew Title');
await pressEnter(page);
await assertTitle(page, 'yy');
await expect(paragraphs).toHaveCount(numParagraphs + 1);
await expect(paragraphs.nth(0)).toHaveText('New Title');
await expect(paragraphs.nth(1)).toHaveText('xxHello');
});
});
test.describe('edgeless note element toolbar', () => {
test('the toolbar of page block should not contains auto-height button and display in page button', async ({
page,
}) => {
await selectAllByKeyboard(page);
const toolbar = locateToolbar(page);
const autoHeight = toolbar.getByTestId('auto-height');
const displayInPage = toolbar.getByTestId('display-in-page');
await expect(toolbar).toBeVisible();
await expect(autoHeight).toHaveCount(0);
await expect(displayInPage).toHaveCount(0);
});
test('the toolbar of note block should contains auto-height button and display in page button', async ({
page,
}) => {
await createEdgelessNoteBlock(page, [100, 100]);
await page.waitForSelector('.affine-paragraph-placeholder.visible');
await clickView(page, [0, 0]);
await clickView(page, [100, 100]);
const toolbar = locateToolbar(page);
const autoHeight = toolbar.getByTestId('auto-height');
const displayInPage = toolbar.getByTestId('display-in-page');
await expect(toolbar).toBeVisible();
await expect(autoHeight).toBeVisible();
await expect(displayInPage).toBeVisible();
});
test('display in page button', async ({ page }) => {
const editorContainer = locateEditorContainer(page);
const notes = editorContainer.locator('affine-note');
await createEdgelessNoteBlock(page, [100, 100]);
await page.waitForSelector('.affine-paragraph-placeholder.visible');
await page.keyboard.type('Note 2');
await clickView(page, [0, 300]);
await clickView(page, [100, 100]);
const toolbar = locateToolbar(page);
const displayInPage = toolbar.getByTestId('display-in-page');
await displayInPage.click();
await locateModeSwitchButton(page, 'page').click();
await expect(notes).toHaveCount(2);
await clickEdgelessModeButton(page);
await clickView(page, [100, 100]);
await displayInPage.click();
await locateModeSwitchButton(page, 'page').click();
await waitForEditorLoad(page);
await expect(notes).toHaveCount(1);
const undoButton = page.getByTestId('notification-card-undo');
const viewTocButton = page.getByTestId('view-in-toc');
await clickEdgelessModeButton(page);
await waitForEditorLoad(page);
await clickView(page, [100, 100]);
await displayInPage.click();
await expect(undoButton).toBeVisible();
await expect(viewTocButton).toBeVisible();
await undoButton.click();
await expect(undoButton).toBeHidden();
await locateModeSwitchButton(page, 'page').click();
await waitForEditorLoad(page);
await expect(notes).toHaveCount(1);
await clickEdgelessModeButton(page);
await waitForEditorLoad(page);
await clickView(page, [100, 100]);
await displayInPage.click();
await undoByKeyboard(page);
await page.waitForTimeout(500);
await expect(
undoButton,
'the toast should be hidden immediately when undo by keyboard'
).toBeHidden();
await displayInPage.click();
await viewTocButton.click();
const toc = page.locator('affine-outline-panel');
await toc.waitFor({ state: 'visible' });
const highlightNoteCards = toc.locator(
'affine-outline-note-card > [data-status="selected"]'
);
await expect(highlightNoteCards).toHaveCount(1);
});
test('note edgeless styles', async ({ page }) => {
const getNoteEdgelessProps = async (page: Page, noteId: string) => {
const container = locateEditorContainer(page);
return await container.evaluate((container: HTMLElement, noteId) => {
const root = container.querySelector(
'affine-edgeless-root'
) as EdgelessRootBlockComponent;
const note = root.gfx.getElementById(noteId) as NoteBlockModel;
return note.props.edgeless;
}, noteId);
};
const toolbar = locateToolbar(page);
await selectAllByKeyboard(page);
const noteId = (await getEdgelessSelectedIds(page))[0];
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
style: {
borderRadius: 8,
borderSize: 4,
borderStyle: 'none',
shadowType: '--affine-note-shadow-box',
},
});
await toolbar.getByRole('button', { name: 'Note Style' }).click();
const noteStylePanel = page.locator('edgeless-note-style-panel');
await noteStylePanel.getByTestId('affine-note-shadow-film').click();
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
style: {
borderRadius: 8,
borderSize: 4,
borderStyle: 'none',
shadowType: '--affine-note-shadow-film',
},
});
const borderStylePanel = noteStylePanel.getByTestId(
'affine-note-border-style-panel'
);
await borderStylePanel.locator('.mode-solid').click();
await borderStylePanel.locator('affine-slider').getByLabel('8').click();
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
style: {
borderRadius: 8,
borderSize: 8,
borderStyle: 'solid',
shadowType: '--affine-note-shadow-film',
},
});
const cornerPanel = noteStylePanel.getByTestId(
'affine-note-corner-radius-panel'
);
await cornerPanel.locator('affine-slider').getByLabel('24').click();
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
style: {
borderRadius: 24,
borderSize: 8,
borderStyle: 'solid',
shadowType: '--affine-note-shadow-film',
},
});
await pressEscape(page);
const headerToolbar = page.getByTestId('edgeless-page-block-header');
const toggleButton = headerToolbar.getByTestId(
'edgeless-note-toggle-button'
);
await toggleButton.click();
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
collapse: true,
collapsedHeight: 48,
style: {
borderRadius: 24,
borderSize: 8,
borderStyle: 'solid',
shadowType: '--affine-note-shadow-film',
},
});
});
});
test.describe('note block rendering', () => {
test('collapsed content rendering', async ({ page }) => {
await createEdgelessNoteBlock(page, [50, 50]);
await type(page, 'paragraph 1');
for (let i = 0; i < 5; i++) {
await pressEnter(page);
}
await type(page, 'paragraph 2');
await pressEscape(page, 3);
await clickView(page, [50, 50]);
await resizeElementByHandle(page, [0, -50], 'bottom-right');
const xywh = await getSelectedXYWH(page);
const center: IVec = [xywh[0] + xywh[2] / 2, xywh[1] + xywh[3] / 2];
const note = page
.locator('affine-edgeless-note')
.getByTestId('edgeless-note-clip-container')
.nth(1);
await expect(note, 'should hide collapsed content').toHaveCSS(
'overflow-y',
'clip'
);
await moveToView(page, center);
await expect(note, 'should show collapsed content when hover').toHaveCSS(
'overflow-y',
'visible'
);
const [x1, y1] = await toViewCoord(page, center);
const [x2, y2] = await toViewCoord(page, [center[0], center[1] + 25]);
const [x3, y3] = await toViewCoord(page, [center[0], center[1] + 50]);
await page.mouse.move(x1, y1);
await page.mouse.down();
await page.mouse.move(x2, y2, { steps: 10 });
await expect(
note,
'should hide collapsed content during dragging'
).toHaveCSS('overflow-y', 'clip');
await page.mouse.move(x3, y3, { steps: 10 });
await page.mouse.up();
await page.mouse.move(x3, y3);
await expect(
note,
'should show collapsed content when dragging is finished'
).toHaveCSS('overflow-y', 'visible');
});
test('cursor should not jump to page block title from note block', async ({
page,
}) => {
await createEdgelessNoteBlock(page, [50, 50]);
await pressBackspace(page);
expect(await isDocTitleFocused(page)).toBeFalsy();
});
});
test('should convert note block to linked doc when clicking turn into linked doc button', async ({
page,
}) => {
await createEdgelessNoteBlock(page, [100, 100]);
await type(page, '# Two Questions');
await pressEnter(page);
await type(page, 'Is this the real life?');
await pressEnter(page);
await type(page, 'Is this just fantasy?');
await pressEscape(page, 3);
const note = page.locator('affine-edgeless-note', {
hasText: 'Is this just fantasy?',
});
await note.click();
const toolbar = locateToolbar(page);
const moreBtn = toolbar.getByRole('button', { name: 'More' });
await moreBtn.click();
const turnIntoLinkedDocBtn = toolbar.getByRole('button', {
name: 'Turn into linked doc',
});
await turnIntoLinkedDocBtn.click();
const inputModal = page.getByRole('dialog').locator('input');
await expect(inputModal).toHaveValue('Two Questions');
await type(page, 'Bohemian Rhapsody');
const confirmBtn = page.getByTestId('confirm-modal-confirm');
await confirmBtn.click();
const syncedDoc = page.locator('affine-embed-edgeless-synced-doc-block');
await expect(syncedDoc).toBeVisible();
const noteInSyncedDoc = syncedDoc.locator('affine-note');
await expect(noteInSyncedDoc).toBeVisible();
const paragraphs = noteInSyncedDoc.locator('affine-paragraph');
await expect(paragraphs.nth(0)).toContainText('Two Questions');
await expect(paragraphs.nth(1)).toContainText('Is this the real life?');
await expect(paragraphs.nth(2)).toContainText('Is this just fantasy?');
});

View File

@@ -0,0 +1,110 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickView,
dblclickView,
dragView,
locateEditorContainer,
locateToolbar,
setEdgelessTool,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
switchEdgelessTheme,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
test('should add text to shape, default to pure black', async ({ page }) => {
await setEdgelessTool(page, 'shape');
await dragView(page, [100, 300], [200, 400]);
await dblclickView(page, [150, 350]);
await expect(
page.locator('edgeless-shape-text-editor rich-text')
).toBeVisible();
await page.keyboard.type('text');
await page.keyboard.press('Escape');
const toolbar = locateToolbar(page);
const textColorContainer = toolbar.locator(
'edgeless-color-picker-button.text-color'
);
const textColorBtn = textColorContainer.getByLabel('Text color');
const blackBtn = textColorContainer
.locator('edgeless-color-button[active]')
.getByLabel('Black');
await expect(textColorContainer).toBeVisible();
await textColorBtn.click();
await expect(blackBtn).toHaveCount(1);
const svgFillColor = await blackBtn.locator('svg').getAttribute('fill');
expect(svgFillColor).toBe('#000000');
await switchEdgelessTheme(page, 'dark');
await clickView(page, [150, 350]);
await textColorBtn.click();
await expect(blackBtn).toHaveCount(1);
const svgFillColor2 = await blackBtn.locator('svg').getAttribute('fill');
expect(svgFillColor2).toBe('#000000');
});
test('should add text to shape with pure white', async ({ page }) => {
await setEdgelessTool(page, 'shape');
await dragView(page, [100, 300], [200, 400]);
await dblclickView(page, [150, 350]);
await expect(
page.locator('edgeless-shape-text-editor rich-text')
).toBeVisible();
await page.keyboard.type('text');
await page.keyboard.press('Escape');
const toolbar = locateToolbar(page);
const textColorContainer = toolbar.locator(
'edgeless-color-picker-button.text-color'
);
const textColorBtn = textColorContainer.getByLabel('Text color');
let currentColor = await textColorBtn
.locator('svg rect')
.getAttribute('fill');
expect(currentColor).toBe('#000000');
await textColorBtn.click();
const blackBtn = textColorContainer
.locator('edgeless-color-button[active]')
.getByLabel('Black');
await expect(blackBtn).toHaveCount(1);
const whiteBtn = textColorContainer
.locator('edgeless-color-button')
.getByLabel('White');
await whiteBtn.click();
currentColor = await textColorBtn.locator('svg rect').getAttribute('fill');
expect(currentColor).toBe('#ffffff');
await switchEdgelessTheme(page, 'dark');
await clickView(page, [150, 350]);
currentColor = await textColorBtn.locator('svg rect').getAttribute('fill');
expect(currentColor).toBe('#ffffff');
});

View File

@@ -0,0 +1,102 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
locateEditorContainer,
locateToolbar,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect, type Locator } from '@playwright/test';
function getEdgelessTextColor(text: Locator) {
return text
.locator('.affine-block-children-container')
.first()
.evaluate(e => e.style.getPropertyValue('--edgeless-text-color'));
}
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
test('should update color of edgeless text when switching theme', async ({
page,
}) => {
const container = locateEditorContainer(page);
await container.dblclick();
await page.waitForSelector('affine-edgeless-text');
await type(page, 'text color');
await page.keyboard.press('Escape');
await page.keyboard.press('Escape');
const text = page.locator('affine-edgeless-text');
await text.click();
const toolbar = locateToolbar(page);
await expect(toolbar).toBeVisible();
const colorPicker = toolbar.locator('edgeless-color-picker-button');
const colorButton = toolbar.getByLabel('Text color');
await colorButton.click();
const pickedColorButton = colorPicker.locator(
'edgeless-color-button[active]'
);
let pickedColor = await pickedColorButton.locator('svg').getAttribute('fill');
let textColor = await getEdgelessTextColor(text);
await expect(pickedColorButton.getByLabel('Black')).toHaveCount(1);
expect(pickedColor).toBe(textColor);
const blackColorButton = colorPicker
.locator('edgeless-color-button')
.filter({
has: page.locator('.color-unit'),
})
.filter({
has: page.getByLabel('Black'),
});
await blackColorButton.click();
pickedColor = await blackColorButton.locator('svg').getAttribute('fill');
textColor = await getEdgelessTextColor(text);
expect(pickedColor).toBe(textColor);
expect(pickedColor).toBe('#000000');
await page.getByTestId('header-info-button').click();
await page
.locator('[data-info-id="edgelessTheme"]')
.locator('[data-property-value="true"]')
.locator('button[value="dark"]')
.click();
await page.keyboard.press('Escape');
await expect(page.getByTestId('property-collapsible-section')).toBeHidden();
await colorButton.click();
pickedColor = await blackColorButton.locator('svg').getAttribute('fill');
textColor = await getEdgelessTextColor(text);
expect(pickedColor).toBe(textColor);
expect(pickedColor).toBe('#ffffff');
});

View File

@@ -0,0 +1,99 @@
import { waitNextFrame } from '@affine-test/kit/bs/misc';
import { test } from '@affine-test/kit/playwright';
import { locateEditorContainer } from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
addDatabase,
clickNewPageButton,
getBlockSuiteEditorTitle,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test('database is useable', async ({ page }) => {
test.slow();
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
const title = getBlockSuiteEditorTitle(page);
await title.pressSequentially('test title');
await page.keyboard.press('Enter');
expect(await title.innerText()).toBe('test title');
await addDatabase(page);
const database = page.locator('affine-database');
await expect(database).toBeVisible();
await page.reload();
await waitForEditorLoad(page);
await clickNewPageButton(page);
const title2 = getBlockSuiteEditorTitle(page);
await title2.pressSequentially('test title2');
await page.waitForTimeout(500);
expect(await title2.innerText()).toBe('test title2');
await page.keyboard.press('Enter');
await addDatabase(page);
const database2 = page.locator('affine-database');
await expect(database2).toBeVisible();
});
test('link page is useable', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
const title = getBlockSuiteEditorTitle(page);
await title.pressSequentially('page1');
await page.keyboard.press('Enter');
expect(await title.innerText()).toBe('page1');
await clickNewPageButton(page);
await waitForEditorLoad(page);
const title2 = getBlockSuiteEditorTitle(page);
await title2.pressSequentially('page2');
await page.keyboard.press('Enter');
expect(await title2.innerText()).toBe('page2');
await page.keyboard.press('@', { delay: 50 });
await page.keyboard.press('p');
await page.keyboard.press('a');
await page.keyboard.press('g');
await page.keyboard.press('e');
await page.keyboard.press('1');
await waitNextFrame(page);
await page.locator('icon-button:has-text("page1")').first().click();
const link = page.locator('.affine-reference');
await expect(link).toBeVisible();
await page.click('.affine-reference');
await page.waitForTimeout(500);
await expect(
page.locator('.doc-title-container:has-text("page1")')
).toBeVisible();
});
test('append paragraph when click editor gap', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
const title = getBlockSuiteEditorTitle(page);
await title.pressSequentially('test title');
await page.keyboard.press('ArrowDown');
await page.keyboard.insertText('test content');
const paragraph = page.locator('affine-paragraph');
const numParagraphs = await paragraph.count();
await page.locator('[data-testid=page-editor-blank]').click();
expect(await paragraph.count()).toBe(numParagraphs + 1);
await page.locator('[data-testid=page-editor-blank]').click();
expect(
await paragraph.count(),
'click the gap again, should not append another paragraph'
).toBe(numParagraphs + 1);
const editorContainer = locateEditorContainer(page);
expect(
await editorContainer.evaluate(el => el.contains(document.activeElement)),
'editor should should keep being focused'
).toBe(true);
});

View File

@@ -0,0 +1,263 @@
import {
clickEdgelessModeButton,
locateEditorContainer,
locateToolbar,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
import { expect, type Page, test } from '@playwright/test';
const TEST_SPOTIFY_URL =
'https://open.spotify.com/episode/7makk4oTQel546B0PZlDM5';
const TEST_AFFINE_URL = 'https://app.affine.pro/';
const EMBED_IFRAME_BLOCK = 'affine-embed-iframe-block';
const EMBED_EDGELESS_IFRAME_BLOCK = 'affine-embed-edgeless-iframe-block';
const IDLE_CARD = 'embed-iframe-idle-card';
const LINK_INPUT_POPUP = 'embed-iframe-link-input-popup';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
await page.locator('affine-paragraph v-line div').click();
});
test.describe('embed iframe block', () => {
async function addEmbedIframeBlock(page: Page) {
await type(page, '/embed');
const embedItem = page.getByTestId('Embed');
await embedItem.click();
const embedIframeBlock = page.locator(EMBED_IFRAME_BLOCK);
await expect(embedIframeBlock).toBeVisible();
const embedIframeIdleCard = page.locator(IDLE_CARD);
await expect(embedIframeIdleCard).toBeVisible();
const embedIframeLinkInputPopup = page.locator(LINK_INPUT_POPUP);
await expect(embedIframeLinkInputPopup).toBeVisible();
const input = embedIframeLinkInputPopup.locator('input');
await expect(input).toBeFocused();
return {
embedIframeBlock,
embedIframeIdleCard,
embedIframeLinkInputPopup,
input,
};
}
async function openToolbarAndSwitchView(page: Page) {
// expect toolbar to be visible
const toolbar = locateToolbar(page);
await expect(toolbar).toBeVisible();
// switch to inline view
await page.getByRole('button', { name: 'Switch view' }).click();
}
test('add embed iframe block using slash menu', async ({ page }) => {
const {
embedIframeBlock,
embedIframeIdleCard,
embedIframeLinkInputPopup,
input,
} = await addEmbedIframeBlock(page);
await input.fill(TEST_SPOTIFY_URL);
await input.press('Enter');
await page.waitForTimeout(100);
await expect(embedIframeLinkInputPopup).not.toBeVisible();
await expect(embedIframeIdleCard).not.toBeVisible();
// expect the embed iframe block count is 1
await expect(embedIframeBlock).toHaveCount(1);
});
test('add bookmark when the url cannot be embedded', async ({ page }) => {
const {
embedIframeBlock,
embedIframeIdleCard,
embedIframeLinkInputPopup,
input,
} = await addEmbedIframeBlock(page);
await input.fill(TEST_AFFINE_URL);
await input.press('Enter');
await expect(embedIframeLinkInputPopup).not.toBeVisible();
await expect(embedIframeIdleCard).not.toBeVisible();
// expect the embed iframe block count is 0
await expect(embedIframeBlock).toHaveCount(0);
// expect the bookmark block count is 1
const bookmarkBlock = page.locator('affine-bookmark');
await expect(bookmarkBlock).toHaveCount(1);
});
test('add embed iframe url by clicking the idle card', async ({ page }) => {
const {
embedIframeBlock,
embedIframeIdleCard,
embedIframeLinkInputPopup,
input,
} = await addEmbedIframeBlock(page);
// click somewhere else to close the link input popup
// click position below the popup
const popupBoundingBox = await embedIframeLinkInputPopup.boundingBox();
expect(popupBoundingBox).not.toBeNull();
if (!popupBoundingBox) return;
await page.mouse.click(popupBoundingBox.x - 50, popupBoundingBox.y + 100);
await page.waitForTimeout(100);
await expect(embedIframeLinkInputPopup).not.toBeVisible();
// click the idle card
await embedIframeIdleCard.click();
await expect(embedIframeLinkInputPopup).toBeVisible();
// fill the url
await input.fill(TEST_SPOTIFY_URL);
await input.press('Enter');
await expect(embedIframeLinkInputPopup).not.toBeVisible();
await expect(embedIframeIdleCard).not.toBeVisible();
await expect(embedIframeBlock).toHaveCount(1);
});
test.describe('conversions', () => {
async function setupEmbedIframe(page: Page) {
const { embedIframeBlock, input } = await addEmbedIframeBlock(page);
// fill the url
await input.fill(TEST_SPOTIFY_URL);
await input.press('Enter');
// wait until iframe is loaded
await expect(embedIframeBlock).toBeVisible();
// click to select embed iframe block
await embedIframeBlock.click();
return { embedIframeBlock };
}
test('embed iframe block to link text', async ({ page }) => {
const { embedIframeBlock } = await setupEmbedIframe(page);
await openToolbarAndSwitchView(page);
await page.getByRole('button', { name: 'Inline view' }).click();
const affineLink = page.locator('affine-link');
await expect(affineLink).toBeVisible();
// hover affine link
await affineLink.hover();
await page.waitForTimeout(100);
// convert back to embed iframe block
await openToolbarAndSwitchView(page);
await page.getByRole('button', { name: 'Embed view' }).click();
// expect the embed iframe block count is 1
await expect(embedIframeBlock).toHaveCount(1);
// expect the link text content is the url
await expect(affineLink).toHaveCount(0);
});
test('embed iframe block to bookmark card', async ({ page }) => {
const { embedIframeBlock } = await setupEmbedIframe(page);
await openToolbarAndSwitchView(page);
await page.getByRole('button', { name: 'Card view' }).click();
// expect the embed iframe block count is 0
await expect(embedIframeBlock).toHaveCount(0);
// expect the bookmark block count is 1
const bookmarkBlock = page.locator('affine-bookmark');
await expect(bookmarkBlock).toHaveCount(1);
// click the bookmark block
await bookmarkBlock.click();
// convert back to embed iframe block
await openToolbarAndSwitchView(page);
await page.getByRole('button', { name: 'Embed view' }).click();
// expect the embed iframe block count is 1
await expect(embedIframeBlock).toHaveCount(1);
await expect(bookmarkBlock).toHaveCount(0);
});
});
test.describe('edgeless mode', () => {
// add embed iframe block to edgeless mode by cmdk menu
async function addEmbedEdgelessIframeBlock(page: Page) {
// switch to edgeless mode
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
// press @ to trigger the menu
await page.keyboard.press('@');
// type spotify url
await page.keyboard.type(TEST_SPOTIFY_URL);
// click item which data-testid='cmdk-label'
await page
.locator(
`[data-testid="cmdk-label"][data-value="external-link:${TEST_SPOTIFY_URL}"]`
)
.click();
// expect the embed iframe block count is 1
const embedEdgelessIframeBlock = page.locator(
EMBED_EDGELESS_IFRAME_BLOCK
);
await expect(embedEdgelessIframeBlock).toHaveCount(1);
}
test('insert embed iframe block to edgeless directly', async ({ page }) => {
await addEmbedEdgelessIframeBlock(page);
});
test('convert between embed view and card view', async ({ page }) => {
await addEmbedEdgelessIframeBlock(page);
await openToolbarAndSwitchView(page);
await page.getByRole('button', { name: 'Card view' }).click();
// expect the embed iframe block count is 0
const embedEdgelessIframeBlock = page.locator(
EMBED_EDGELESS_IFRAME_BLOCK
);
await expect(embedEdgelessIframeBlock).toHaveCount(0);
// expect the bookmark block count is 1
const bookmarkEdgelessBlock = page.locator('affine-edgeless-bookmark');
await expect(bookmarkEdgelessBlock).toHaveCount(1);
// click the bookmark block
await bookmarkEdgelessBlock.click();
// convert back to embed view
await openToolbarAndSwitchView(page);
await page.getByRole('button', { name: 'Embed view' }).click();
// expect the embed iframe block count is 1
await expect(embedEdgelessIframeBlock).toHaveCount(1);
});
});
});

View File

@@ -0,0 +1,170 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickView,
createEdgelessNoteBlock,
dblclickNoteBody,
fitViewportToContent,
focusDocTitle,
getSelectedXYWH,
locateEditorContainer,
resizeElementByHandle,
scaleElementByHandle,
} from '@affine-test/kit/utils/editor';
import { pressEnter } from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
createLinkedPage,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
const title = 'Synced Block Test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page, title);
await pressEnter(page);
await page.keyboard.type('test content');
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
test('should not show hidden note in embed view page mode', async ({
page,
}) => {
await dblclickNoteBody(page);
await page.keyboard.type('visible content');
await createEdgelessNoteBlock(page, [100, 100]);
await page.keyboard.press('Enter');
await page.keyboard.type('hidden content');
await page.keyboard.press('Enter');
// create a new page and navigate
await createLinkedPage(page, 'Test Page');
const inlineLink = page.locator('affine-reference');
await inlineLink.dblclick();
// reference the previous page
await focusDocTitle(page);
await page.keyboard.press('Enter');
await page.keyboard.type('@' + title);
const docPopover = page.locator('.linked-doc-popover');
await docPopover.getByText(/^Synced Block Test$/).click();
// switch to embed view
await inlineLink.hover();
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await toolbar.getByLabel('Switch view').click();
await toolbar.getByLabel('Embed view').click();
// check the content
const embedLink = page.locator('affine-embed-synced-doc-block');
await expect(embedLink.getByText(/visible content/)).toBeVisible();
await expect(embedLink.getByText(/hidden content/)).toBeHidden();
});
test.describe('edgeless', () => {
test.beforeEach(async ({ page }) => {
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
await clickView(page, [0, 0]);
await page.keyboard.type('@' + title);
await page
.getByTestId('cmdk-quick-search')
.getByText(/^Synced Block Test$/)
.click();
await fitViewportToContent(page);
});
test.describe('header of edgeless embed synced doc', () => {
test('should fold button works', async ({ page }) => {
const embedBlock = page.locator('affine-embed-edgeless-synced-doc-block');
const foldButton = embedBlock.getByTestId(
'edgeless-embed-synced-doc-fold-button'
);
const content = embedBlock.locator('editor-host');
await expect(foldButton).toHaveAttribute('data-folded', 'false');
await expect(content).toBeInViewport();
await foldButton.click();
await expect(content).not.toBeInViewport();
await expect(foldButton).toHaveAttribute('data-folded', 'true');
});
test('should show title in header', async ({ page }) => {
const embedBlock = page.locator('affine-embed-edgeless-synced-doc-block');
const headerTitle = embedBlock.getByTestId(
'edgeless-embed-synced-doc-title'
);
await expect(headerTitle).toHaveText(title);
});
});
test.describe('size adjustment of embed synced doc', () => {
test.beforeEach(async ({ page }) => {
await scaleElementByHandle(page, [10, 10], 'bottom-right');
});
test('should fold embed synced doc when adjust height to smallest', async ({
page,
}) => {
const [, , , h] = await getSelectedXYWH(page);
await resizeElementByHandle(page, [0, -(h - 10)], 'bottom-right');
const embedBlock = page.locator('affine-embed-edgeless-synced-doc-block');
const foldButton = embedBlock.getByTestId(
'edgeless-embed-synced-doc-fold-button'
);
const content = embedBlock.locator('editor-host');
await expect(foldButton).toHaveAttribute('data-folded', 'true');
await expect(content).not.toBeInViewport();
await foldButton.click();
await expect(foldButton).toHaveAttribute('data-folded', 'false');
await expect(content).toBeInViewport();
await embedBlock.click();
const [, , , h2] = await getSelectedXYWH(page);
expect(
h2,
'should recover height when unfold embed synced doc which was resized to smallest height directly'
).toEqual(h);
});
test('should be able to adjust height the folded embed synced doc', async ({
page,
}) => {
const embedBlock = page.locator('affine-embed-edgeless-synced-doc-block');
const content = embedBlock.locator('editor-host');
const foldButton = embedBlock.getByTestId(
'edgeless-embed-synced-doc-fold-button'
);
await foldButton.click();
await resizeElementByHandle(page, [50, 0], 'bottom-right');
await expect(content).not.toBeInViewport();
await expect(foldButton).toHaveAttribute('data-folded', 'true');
await resizeElementByHandle(page, [-50, 0], 'bottom-right');
await expect(content).not.toBeInViewport();
await expect(foldButton).toHaveAttribute('data-folded', 'true');
await resizeElementByHandle(page, [0, 50], 'bottom-right');
await expect(
content,
'should unfold the embed synced doc when adjust height to greater'
).toBeInViewport();
await expect(foldButton).toHaveAttribute('data-folded', 'false');
await expect(content).toBeInViewport();
});
});
});

View File

@@ -0,0 +1,119 @@
import { toolbarButtons } from '@affine-test/kit/bs/linked-toolbar';
import { test } from '@affine-test/kit/playwright';
import {
pressArrowUp,
pressEnter,
pressTab,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
createLinkedPage,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page, 'List Test');
await waitForEditorLoad(page);
});
test.describe('split list', () => {
test('split at the start of the list', async ({ page }) => {
await pressEnter(page);
await type(page, '1. aaa\nbbb\nccc\nddd');
await pressArrowUp(page, 2);
await pressTab(page);
await pressArrowUp(page, 2);
const listLocator = page.locator('affine-list');
/**
* 1. |aaa
* a. bbb
* 2. ccc
* 3. ddd
*/
await expect(listLocator.nth(0).locator('rich-text')).toHaveText([
'aaa',
'bbb',
]);
await expect(
listLocator.nth(0).locator('.affine-list-block__numbered')
).toHaveText(['1.', 'a.']);
await expect(listLocator.nth(2).locator('rich-text')).toHaveText('ccc');
await expect(
listLocator.nth(2).locator('.affine-list-block__numbered')
).toHaveText('2.');
await expect(listLocator.nth(3).locator('rich-text')).toHaveText('ddd');
await expect(
listLocator.nth(3).locator('.affine-list-block__numbered')
).toHaveText('3.');
await pressEnter(page);
/**
* 1.
* 2. aaa
* a. bbb
* 3. ccc
* 4. ddd
*/
await expect(
listLocator.nth(0).locator('.affine-list-block__numbered')
).toHaveText('1.');
await expect(listLocator.nth(1).locator('rich-text')).toHaveText([
'aaa',
'bbb',
]);
await expect(
listLocator.nth(1).locator('.affine-list-block__numbered')
).toHaveText(['2.', 'a.']);
await expect(listLocator.nth(3).locator('rich-text')).toHaveText('ccc');
await expect(
listLocator.nth(3).locator('.affine-list-block__numbered')
).toHaveText('3.');
await expect(listLocator.nth(4).locator('rich-text')).toHaveText('ddd');
await expect(
listLocator.nth(4).locator('.affine-list-block__numbered')
).toHaveText('4.');
});
test('tab in list should not propagate out of editor', async ({ page }) => {
await pressEnter(page);
await type(page, '1. aaa');
await page.keyboard.press('Enter');
await page.keyboard.press('Tab');
await createLinkedPage(page, 'Test Page');
const inlineLink = page.locator('affine-reference');
const { switchViewBtn, cardViewBtn } = toolbarButtons(page);
const list = page.locator('affine-list');
const card = page.locator('affine-embed-linked-doc-block');
const pressWithCount = async (key: string, count: number) => {
for (let i = 0; i < count; i++) {
await new Promise(resolve => setTimeout(resolve, 5));
await page.keyboard.press(key);
}
};
await inlineLink.hover();
await switchViewBtn.click();
await cardViewBtn.click();
await expect(list.filter({ has: card })).toHaveCount(1);
await page.keyboard.press('Shift+Tab');
await expect(list.filter({ hasNot: card })).toHaveCount(1);
await pressWithCount('Tab', 5);
await expect(list.filter({ has: card })).toHaveCount(1);
await pressWithCount('Shift+Tab', 5);
await expect(list.filter({ hasNot: card })).toHaveCount(1);
});
});

View File

@@ -0,0 +1,565 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickPageModeButton,
clickView,
createEdgelessNoteBlock,
focusDocTitle,
getEdgelessSelectedIds,
getViewportCenter,
locateToolbar,
setViewportCenter,
} from '@affine-test/kit/utils/editor';
import {
pressBackspace,
pressEnter,
pressEscape,
selectAllByKeyboard,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import {
closeSettingModal,
confirmExperimentalPrompt,
openExperimentalFeaturesPanel,
openSettingModal,
} from '@affine-test/kit/utils/setting';
import { openRightSideBar } from '@affine-test/kit/utils/sidebar';
import { expect, type Locator, type Page } from '@playwright/test';
import {
createHeadings,
createTitle,
getVerticalCenterFromLocator,
} from './utils';
async function openTocPanel(page: Page) {
const toc = page.locator('affine-outline-panel');
if (await toc.isVisible()) return toc;
await openRightSideBar(page, 'outline');
await toc.waitFor({ state: 'visible' });
return toc;
}
function getTocHeading(panel: Locator, level: number) {
return panel.getByTestId(`outline-block-preview-h${level}`).locator('span');
}
// locate cards in outline panel
// ! Please note that when any card mode changed, the locator will be mutated
function locateCards(toc: Locator, mode?: 'both' | 'page' | 'edgeless') {
const cards = toc.locator('affine-outline-note-card');
return mode
? cards.locator(`>div[data-visibility="${mode}"]`)
: cards.locator('>div');
}
function locateSortingButton(panel: Locator) {
return panel.getByTestId('toggle-notes-sorting-button');
}
async function changeNoteDisplayMode(
card: Locator,
mode: 'both' | 'doc' | 'edgeless'
) {
await card.hover();
await card.getByTestId('display-mode-button').click();
await card.locator(`note-display-mode-panel .item.${mode}`).click();
}
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
});
test.describe('TOC display', () => {
test('should display title and headings when there are non-empty headings in editor', async ({
page,
}) => {
await createTitle(page);
await createHeadings(page);
const toc = await openTocPanel(page);
await expect(toc.getByTestId('outline-block-preview-title')).toBeVisible();
for (let i = 1; i <= 6; i++) {
await expect(getTocHeading(toc, i)).toBeVisible();
await expect(getTocHeading(toc, i)).toContainText(`Heading ${i}`);
}
});
test('should display placeholder when no headings', async ({ page }) => {
const toc = await openTocPanel(page);
const noHeadingPlaceholder = toc.getByTestId('empty-panel-placeholder');
await createTitle(page);
await pressEnter(page);
await type(page, 'hello world');
await expect(noHeadingPlaceholder).toBeVisible();
});
test('should not display headings when there are only empty headings', async ({
page,
}) => {
await createTitle(page);
// create empty headings
for (let i = 1; i <= 6; i++) {
await type(page, `${'#'.repeat(i)} `);
await pressEnter(page);
}
const toc = await openTocPanel(page);
await expect(toc.getByTestId('outline-block-preview-title')).toBeHidden();
for (let i = 1; i <= 6; i++) {
await expect(getTocHeading(toc, i)).toBeHidden();
}
});
test('should update panel when modify or clear title or headings', async ({
page,
}) => {
const title = await createTitle(page);
const headings = await createHeadings(page);
const toc = await openTocPanel(page);
await title.scrollIntoViewIfNeeded();
await title.click();
await type(page, 'xxx');
await expect(toc.getByTestId('outline-block-preview-title')).toContainText([
'Titlexxx',
]);
await selectAllByKeyboard(page);
await pressBackspace(page);
await expect(toc.getByTestId('outline-block-preview-title')).toBeHidden();
for (let i = 1; i <= 6; i++) {
await headings[i - 1].click();
await type(page, 'xxx');
await expect(getTocHeading(toc, i)).toContainText(`Heading ${i}xxx`);
await selectAllByKeyboard(page);
await pressBackspace(page);
await expect(getTocHeading(toc, i)).toBeHidden();
}
});
test('should update panel when switch doc', async ({ page }) => {
const toc = await openTocPanel(page);
await focusDocTitle(page);
await page.keyboard.press('ArrowDown');
await type(page, '# Heading 1');
await clickNewPageButton(page);
await expect(getTocHeading(toc, 1)).toBeHidden();
await page.goBack();
await expect(getTocHeading(toc, 1)).toBeVisible();
});
test('should add padding to sub-headings', async ({ page }) => {
await createHeadings(page);
const toc = await openTocPanel(page);
let prev = getTocHeading(toc, 1);
for (let i = 2; i <= 6; i++) {
const curr = getTocHeading(toc, i);
const prevRect = await prev.boundingBox();
const currRect = await curr.boundingBox();
expect(prevRect).not.toBeNull();
expect(currRect).not.toBeNull();
expect(prevRect!.x).toBeLessThan(currRect!.x);
prev = curr;
}
});
test('visibility sorting should be enabled in edgeless mode and disabled in page mode by default, and can be changed', async ({
page,
}) => {
await pressEnter(page);
await type(page, '# Heading 1');
const toc = await openTocPanel(page);
const sortingButton = locateSortingButton(toc);
await expect(sortingButton).not.toHaveClass(/active/);
await expect(toc.locator('[data-sortable="false"]')).toHaveCount(1);
await clickEdgelessModeButton(page);
await expect(sortingButton).toHaveClass(/active/);
await expect(toc.locator('[data-sortable="true"]')).toHaveCount(1);
await sortingButton.click();
await expect(sortingButton).not.toHaveClass(/active/);
await expect(toc.locator('[data-sortable="false"]')).toHaveCount(1);
});
test('should notify user when there are some page only notes and sorting is disabled', async ({
page,
}) => {
await clickEdgelessModeButton(page);
const toc = await openTocPanel(page);
const viewportCenter = await getViewportCenter(page);
await createEdgelessNoteBlock(page, [viewportCenter.x, viewportCenter.y]);
const card = locateCards(toc, 'edgeless');
await changeNoteDisplayMode(card, 'doc');
const notification = toc.getByTestId('affine-outline-notice');
await expect(notification).toBeHidden();
const sortingButton = locateSortingButton(toc);
await sortingButton.click();
await expect(notification).toBeVisible();
await notification.getByTestId('outline-notice-sort-button').click();
await expect(notification).toBeHidden();
await expect(sortingButton).toHaveClass(/active/);
await sortingButton.click();
await expect(notification).toBeVisible();
await notification.getByTestId('outline-notice-close-button').click();
await expect(notification).toBeHidden();
});
});
test.describe('TOC and editor scroll', () => {
test('should highlight heading when scroll to area before viewport center', async ({
page,
}) => {
const title = await createTitle(page);
for (let i = 0; i < 3; i++) {
await pressEnter(page);
}
const headings = await createHeadings(page, 10);
await title.scrollIntoViewIfNeeded();
const toc = await openTocPanel(page);
const viewportCenter = await getVerticalCenterFromLocator(
page.locator('body')
);
const activeHeadingContainer = toc.locator(
'affine-outline-panel-body .active'
);
await title.click();
await expect(activeHeadingContainer).toContainText('Title');
for (let i = 0; i < headings.length; i++) {
const lastHeadingCenter = await getVerticalCenterFromLocator(headings[i]);
await page.mouse.wheel(0, lastHeadingCenter - viewportCenter + 20);
await page.waitForTimeout(10);
await expect(activeHeadingContainer).toContainText(`Heading ${i + 1}`);
}
});
test('should scroll to heading and highlight heading when click item in outline panel', async ({
page,
}) => {
const headings = await createHeadings(page, 10);
const toc = await openTocPanel(page);
const activeHeadingContainer = toc.locator(
'affine-outline-panel-body .active'
);
const headingsInPanel = Array.from({ length: 6 }, (_, i) =>
getTocHeading(toc, i + 1)
);
await headingsInPanel[2].click();
await expect(headings[2]).toBeVisible();
await expect(activeHeadingContainer).toContainText('Heading 3');
});
test('should scroll to title when click title in outline panel', async ({
page,
}) => {
const title = await createTitle(page);
await pressEnter(page);
await createHeadings(page, 10);
const toc = await openTocPanel(page);
const titleInPanel = toc.getByTestId('outline-block-preview-title');
await expect(title).not.toBeInViewport();
await titleInPanel.click();
await expect(title).toBeVisible();
});
});
test.describe('TOC and edgeless selection', () => {
test('should select note blocks when selecting cards in TOC', async ({
page,
}) => {
const toc = await openTocPanel(page);
await clickEdgelessModeButton(page);
const cards = locateCards(toc);
await createEdgelessNoteBlock(page, [100, 100]);
await changeNoteDisplayMode(cards.last(), 'doc');
await createEdgelessNoteBlock(page, [200, 200]);
await cards.nth(0).click();
expect(await getEdgelessSelectedIds(page)).toHaveLength(1);
await cards.nth(0).click();
expect(await getEdgelessSelectedIds(page)).toHaveLength(0);
await cards.nth(1).click();
expect(
await getEdgelessSelectedIds(page),
'should not select doc only note'
).toHaveLength(0);
await cards.nth(2).click();
expect(await getEdgelessSelectedIds(page)).toHaveLength(1);
await cards.nth(2).click();
expect(await getEdgelessSelectedIds(page)).toHaveLength(0);
await cards.nth(0).click({ modifiers: ['Shift'] });
await cards.nth(1).click({ modifiers: ['Shift'] });
await cards.nth(2).click({ modifiers: ['Shift'] });
expect(await getEdgelessSelectedIds(page)).toHaveLength(2);
});
test('should select note cards when select note blocks in canvas', async ({
page,
}) => {
await clickEdgelessModeButton(page);
await setViewportCenter(page, [0, 0]);
await selectAllByKeyboard(page);
await pressBackspace(page);
await createEdgelessNoteBlock(page, [100, 100]);
await createEdgelessNoteBlock(page, [200, 200]);
await clickView(page, [0, 0]);
const toc = await openTocPanel(page);
const cards = locateCards(toc);
await clickView(page, [100, 100]);
await expect(cards.nth(0)).toHaveAttribute('data-status', 'selected');
await clickView(page, [200, 200]);
await expect(cards.nth(1)).toHaveAttribute('data-status', 'selected');
await selectAllByKeyboard(page);
await expect(cards.nth(0)).toHaveAttribute('data-status', 'selected');
await expect(cards.nth(1)).toHaveAttribute('data-status', 'selected');
await pressEscape(page);
await expect(cards.nth(0)).toHaveAttribute('data-status', 'normal');
await expect(cards.nth(1)).toHaveAttribute('data-status', 'normal');
});
});
test.describe('drag and drop note in outline panel', () => {
const dragCard = async (
from: Locator,
to: Locator,
position: 'before' | 'after' = 'before'
) => {
const fromRect = await from.boundingBox();
const fromCenter = {
x: fromRect!.width / 2,
y: fromRect!.height / 2,
};
const toRect = await to.boundingBox();
const toCenter = {
x: toRect!.width / 2,
y: toRect!.height / 2,
};
await from.dragTo(to, {
sourcePosition: fromCenter,
targetPosition:
position === 'before'
? { x: toCenter.x, y: toCenter.y - 10 }
: { x: toCenter.x, y: toCenter.y + 10 },
});
};
// create 2 both cards, 2 page cards and 2 edgeless cards
test.beforeEach(async ({ page }) => {
const toc = await openTocPanel(page);
const edgelessCards = locateCards(toc, 'edgeless');
// 2 both cards
{
await focusDocTitle(page);
await page.keyboard.press('ArrowDown');
await type(page, '0');
await clickEdgelessModeButton(page);
await createEdgelessNoteBlock(page, [100, 100]);
await type(page, '1');
await changeNoteDisplayMode(edgelessCards.first(), 'both');
}
// 2 page cards
{
await createEdgelessNoteBlock(page, [150, 150]);
await type(page, '2');
await changeNoteDisplayMode(edgelessCards.first(), 'doc');
await createEdgelessNoteBlock(page, [200, 200]);
await type(page, '3');
await changeNoteDisplayMode(edgelessCards.first(), 'doc');
}
// 2 edgeless cards
{
await createEdgelessNoteBlock(page, [250, 250]);
await type(page, '4');
await createEdgelessNoteBlock(page, [300, 300]);
await type(page, '5');
}
});
test('should reorder notes when drag and drop a note in outline panel', async ({
page,
}) => {
const toc = await openTocPanel(page);
const cards = locateCards(toc);
await dragCard(cards.nth(3), cards.nth(1));
await clickPageModeButton(page);
const paragraphs = page
.locator('affine-paragraph')
.locator('[data-v-text="true"]');
await expect(paragraphs).toHaveCount(4);
await expect(paragraphs.nth(0)).toContainText('0');
await expect(paragraphs.nth(1)).toContainText('3');
await expect(paragraphs.nth(2)).toContainText('1');
await expect(paragraphs.nth(3)).toContainText('2');
// Note card should be able to drag and drop in page mode
await locateSortingButton(toc).click();
await dragCard(cards.nth(3), cards.nth(1));
await expect(paragraphs.nth(0)).toContainText('0');
await expect(paragraphs.nth(1)).toContainText('2');
await expect(paragraphs.nth(2)).toContainText('3');
await expect(paragraphs.nth(3)).toContainText('1');
});
test('multiple selected note cards can be dragged and dropped at same time', async ({
page,
}) => {
const toc = await openTocPanel(page);
const cards = locateCards(toc);
await cards.nth(2).click({ modifiers: ['Shift'] });
await cards.nth(3).click({ modifiers: ['Shift'] });
await dragCard(cards.nth(2), cards.nth(0));
await clickPageModeButton(page);
const paragraphs = page
.locator('affine-paragraph')
.locator('[data-v-text="true"]');
await expect(paragraphs).toHaveCount(4);
await expect(paragraphs.nth(0)).toContainText('2');
await expect(paragraphs.nth(1)).toContainText('3');
await expect(paragraphs.nth(2)).toContainText('0');
await expect(paragraphs.nth(3)).toContainText('1');
});
});
test.describe('advanced visibility control', () => {
test.beforeEach(async ({ page }) => {
await openSettingModal(page);
await openExperimentalFeaturesPanel(page);
await confirmExperimentalPrompt(page);
await page.getByTestId('enable_advanced_block_visibility').click();
await closeSettingModal(page);
await page.reload();
});
test('should update notes when change note display mode from note toolbar', async ({
page,
}) => {
await clickEdgelessModeButton(page);
await setViewportCenter(page, [0, 0]);
await createEdgelessNoteBlock(page, [100, 100]);
await type(page, 'hello');
await clickView(page, [200, 200]);
const toc = await openTocPanel(page);
const bothCard = locateCards(toc, 'both');
const edgelessCard = locateCards(toc, 'edgeless');
await expect(bothCard).toHaveCount(1);
await expect(edgelessCard).toHaveCount(1);
await clickView(page, [100, 100]);
const toolbar = locateToolbar(page);
await toolbar.getByRole('button', { name: 'Mode' }).click();
await toolbar.locator('note-display-mode-panel .item.both').click();
await expect(bothCard).toHaveCount(2);
await expect(edgelessCard).toHaveCount(0);
});
test('should update notes after slicing note', async ({ page }) => {
await clickEdgelessModeButton(page);
await createEdgelessNoteBlock(page, [200, 100]);
await type(page, 'hello');
await pressEnter(page);
await type(page, 'world');
await pressEscape(page, 3);
await createEdgelessNoteBlock(page, [200, 400]);
const toc = await openTocPanel(page);
const bothCard = locateCards(toc, 'both');
const edgelessCards = locateCards(toc, 'edgeless');
await expect(bothCard).toHaveCount(1);
await expect(edgelessCards).toHaveCount(2);
while ((await edgelessCards.count()) > 0) {
await edgelessCards.first().hover();
await edgelessCards.first().getByTestId('display-mode-button').click();
await edgelessCards
.first()
.locator('note-display-mode-panel .item.both')
.click();
}
await expect(bothCard).toHaveCount(3);
await clickView(page, [200, 100]);
const toolbar = locateToolbar(page);
await toolbar.getByRole('button', { name: 'Slicer' }).click();
await expect(page.locator('.note-slicer-button')).toBeVisible();
await page.locator('.note-slicer-button').click();
await expect(bothCard).toHaveCount(4);
await expect(
bothCard.nth(1),
'the sliced note card should keep the order in its parent'
).toContainText('hello');
await expect(
bothCard.nth(2),
'the sliced note card should keep the order in its parent'
).toContainText('world');
});
});

View File

@@ -0,0 +1,279 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickPageModeButton,
createEdgelessNoteBlock,
} from '@affine-test/kit/utils/editor';
import {
pressBackspace,
pressEnter,
selectAllByKeyboard,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
createLinkedPage,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect, type Locator, type Page } from '@playwright/test';
import {
createHeadings,
createTitle,
getVerticalCenterFromLocator,
} from './utils';
function getIndicators(container: Page | Locator) {
return container.locator('affine-outline-viewer .outline-viewer-indicator');
}
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
});
test('should display indicators when non-empty headings exists', async ({
page,
}) => {
const indicators = getIndicators(page);
await createHeadings(page);
await expect(indicators).toHaveCount(6);
for (let i = 0; i < 6; i++) {
await expect(indicators.nth(i)).toBeVisible();
}
});
test('should be hidden when only empty headings exists', async ({ page }) => {
const indicators = getIndicators(page);
await expect(indicators).toHaveCount(0);
for (let i = 1; i <= 6; i++) {
// empty heading
await type(page, `${'#'.repeat(i)} `);
await pressEnter(page);
}
await expect(indicators).toHaveCount(0);
});
test('should update indicator when clear title or headings', async ({
page,
}) => {
const indicators = getIndicators(page);
const title = await createTitle(page);
const headings = await createHeadings(page);
await expect(indicators).toHaveCount(7);
await title.scrollIntoViewIfNeeded();
await title.click();
await selectAllByKeyboard(page);
await pressBackspace(page);
await expect(indicators).toHaveCount(6);
for (let i = 1; i <= 6; i++) {
await headings[i - 1].click();
await selectAllByKeyboard(page);
await pressBackspace(page);
await expect(indicators).toHaveCount(6 - i);
}
});
test('should display simple outline panel when hovering over indicators', async ({
page,
}) => {
const indicators = getIndicators(page);
await createTitle(page);
await createHeadings(page);
await indicators.first().hover({ force: true });
const items = page.locator('.outline-viewer-item');
await expect(items).toHaveCount(8);
await expect(items.nth(0)).toContainText(['Table of Contents']);
await expect(items.nth(1)).toContainText(['Title']);
for (let i = 2; i <= 7; i++) {
await expect(items.nth(i)).toContainText([`Heading ${i - 1}`]);
}
});
test('should highlight indicator when scrolling', async ({ page }) => {
const indicators = getIndicators(page);
const title = await createTitle(page);
for (let i = 1; i <= 3; i++) {
await pressEnter(page);
}
const headings = await createHeadings(page, 10);
await title.scrollIntoViewIfNeeded();
const viewportCenter = await getVerticalCenterFromLocator(
page.locator('body')
);
for (let i = 0; i < headings.length; i++) {
const lastHeadingCenter = await getVerticalCenterFromLocator(headings[i]);
await expect(indicators.nth(i)).toHaveClass(/active/);
await page.mouse.wheel(0, lastHeadingCenter - viewportCenter + 20);
await page.waitForTimeout(10);
}
});
test('should highlight indicator when click item in outline panel', async ({
page,
}) => {
const viewer = page.locator('affine-outline-viewer');
const indicators = getIndicators(page);
const headings = await createHeadings(page, 10);
await indicators.first().hover({ force: true });
const headingsInPanel = Array.from({ length: 6 }, (_, i) =>
viewer.getByTestId(`outline-block-preview-h${i + 1}`)
);
await headingsInPanel[2].click();
await expect(headings[2]).toBeVisible();
await expect(indicators.nth(2)).toHaveClass(/active/);
});
test('outline viewer should hide in edgeless mode', async ({ page }) => {
await createTitle(page);
await pressEnter(page);
await type(page, '# ');
await type(page, 'Heading 1');
const indicators = getIndicators(page);
await expect(indicators).toHaveCount(2);
await clickEdgelessModeButton(page);
await expect(indicators).toHaveCount(0);
await clickPageModeButton(page);
await expect(indicators).toHaveCount(2);
});
test('should hide edgeless-only note headings', async ({ page }) => {
await createTitle(page);
await pressEnter(page);
await type(page, '# Heading 1');
await pressEnter(page);
await type(page, '## Heading 2');
await clickEdgelessModeButton(page);
await createEdgelessNoteBlock(page, [100, 100]);
await type(page, '# Edgeless');
await clickPageModeButton(page);
await waitForEditorLoad(page);
const indicators = getIndicators(page);
await expect(indicators).toHaveCount(3);
await indicators.first().hover({ force: true });
const viewer = page.locator('affine-outline-viewer');
await expect(viewer).toBeVisible();
const h1InPanel = viewer
.getByTestId('outline-block-preview-h1')
.locator('span');
await h1InPanel.waitFor({ state: 'visible' });
await expect(h1InPanel).toContainText(['Heading 1']);
});
test('outline viewer should update after change heading in edgeless mode', async ({
page,
}) => {
await createTitle(page);
await pressEnter(page);
await type(page, '# ');
await type(page, 'Heading 1');
await clickEdgelessModeButton(page);
const note = page.locator('affine-edgeless-note');
await note.dblclick();
await type(page, '# New Heading');
await clickPageModeButton(page);
const indicators = getIndicators(page);
await expect(indicators).toHaveCount(3);
});
test('outline viewer should be useable in doc peek preview', async ({
page,
}) => {
await pressEnter(page);
await createLinkedPage(page, 'Test Page');
await page.locator('affine-reference').hover();
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
await toolbar.getByLabel(/^Open doc with$/).click();
await toolbar
.getByLabel('Open doc menu')
.getByLabel('Open in center peek')
.click();
const peekView = page.getByTestId('peek-view-modal');
await expect(peekView).toBeVisible();
const title = peekView.locator('doc-title .inline-editor');
await title.click();
await pressEnter(page);
await type(page, '# Heading 1');
for (let i = 0; i < 10; i++) {
await pressEnter(page);
}
await type(page, '## Heading 2');
const outlineViewer = peekView.locator('affine-outline-viewer');
const outlineViewerBound = await outlineViewer.boundingBox();
expect(outlineViewerBound).not.toBeNull();
const indicators = getIndicators(peekView);
await expect(indicators).toHaveCount(3);
await expect(indicators.nth(0)).toBeVisible();
await expect(indicators.nth(1)).toBeVisible();
await expect(indicators.nth(2)).toBeVisible();
await indicators.first().hover({ force: true });
const viewer = peekView.locator('affine-outline-viewer');
await expect(viewer).toBeVisible();
// position of outline viewer should be fixed
{
const headingButtons = peekView.locator(
'affine-outline-viewer .outline-viewer-item:not(.outline-viewer-header)'
);
await expect(headingButtons).toHaveCount(3);
await expect(headingButtons.nth(0)).toBeVisible();
await expect(headingButtons.nth(1)).toBeVisible();
await expect(headingButtons.nth(2)).toBeVisible();
await headingButtons.last().click();
await page.mouse.move(0, 0);
await headingButtons.last().waitFor({ state: 'hidden' });
const currentOutlineViewerBound = await outlineViewer.boundingBox();
expect(currentOutlineViewerBound).not.toBeNull();
expect(outlineViewerBound).toEqual(currentOutlineViewerBound);
}
// outline viewer should be hidden when clicking the outline panel toggle button
{
await indicators.first().hover({ force: true });
const toggleButton = peekView.locator(
'.outline-viewer-header edgeless-tool-icon-button'
);
await toggleButton.click();
await page.waitForTimeout(500);
await expect(peekView).toBeHidden();
await expect(viewer).toBeHidden();
await expect(page.locator('affine-outline-panel')).toBeVisible();
}
});

View File

@@ -0,0 +1,34 @@
import { locateEditorContainer } from '@affine-test/kit/utils/editor';
import { pressEnter } from '@affine-test/kit/utils/keyboard';
import {
getBlockSuiteEditorTitle,
type,
} from '@affine-test/kit/utils/page-logic';
import type { Locator, Page } from '@playwright/test';
export async function createTitle(page: Page) {
const title = getBlockSuiteEditorTitle(page);
await title.scrollIntoViewIfNeeded();
await title.click();
await type(page, 'Title');
await pressEnter(page);
return title;
}
export async function createHeadings(page: Page, gap = 0) {
const editorContainer = locateEditorContainer(page);
const headings: Locator[] = [];
await pressEnter(page, gap + 1);
for (let i = 1; i <= 6; i++) {
await type(page, `${'#'.repeat(i)} Heading ${i}`);
headings.push(editorContainer.locator(`.h${i}`));
await pressEnter(page, gap + 1);
}
return headings;
}
export async function getVerticalCenterFromLocator(locator: Locator) {
const rect = await locator.boundingBox();
return rect!.y + rect!.height / 2;
}

View File

@@ -0,0 +1,460 @@
import { test } from '@affine-test/kit/playwright';
import { locateToolbar } from '@affine-test/kit/utils/editor';
import {
pressEnter,
pressShiftTab,
pressTab,
selectAllByKeyboard,
undoByKeyboard,
} from '@affine-test/kit/utils/keyboard';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import type { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
import { expect, type Locator } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page, 'Paragraph Test');
await waitForEditorLoad(page);
});
async function expectParagraphState(
paragraphs: Locator,
index: number,
expectedType: string,
expectedText: string
) {
await expect
.poll(async () => {
return await paragraphs
.nth(index)
.evaluate(
(
block: ParagraphBlockComponent,
expected: { type: string; text: string }
) =>
block.model.props.type === expected.type &&
block.model.props.text.toString() === expected.text,
{
type: expectedType,
text: expectedText,
}
);
})
.toBeTruthy();
}
async function expectParagraphVisibility(
paragraphs: Locator,
index: number,
visible: boolean
) {
await expect
.poll(async () => {
return await paragraphs.nth(index).isVisible();
})
.toBe(visible);
}
test('heading icon should be updated after change heading level', async ({
page,
}) => {
await page.keyboard.press('Enter');
await page.keyboard.type('Hello');
// Hello|
// empty paragraph
const paragraph = page.locator('affine-note affine-paragraph').nth(0);
await selectAllByKeyboard(page);
const toolbar = locateToolbar(page);
await toolbar.getByLabel('Conversions').click();
await toolbar.getByLabel('Heading 1').click();
await paragraph.hover();
await expect(page.getByTestId('heading-icon-1')).toBeVisible();
await selectAllByKeyboard(page);
await toolbar.getByLabel('Conversions').click();
await toolbar.getByLabel('Heading 2').click();
await paragraph.hover();
await expect(page.getByTestId('heading-icon-1')).toBeHidden();
await expect(page.getByTestId('heading-icon-2')).toBeVisible();
});
test('basic heading collapsed', async ({ page }) => {
await pressEnter(page);
/**
* # aaa
* bbb
* ## ccc
* ddd
* ### eee
* fff
* ## ggg
* hhh
* # iii
* jjj
*/
await type(
page,
'# aaa\nbbb\n## ccc\nddd\n### eee\nfff\n## ggg\nhhh\n# iii\njjj'
);
const a = page.locator('affine-note affine-paragraph').nth(0);
const b = page.locator('affine-note affine-paragraph').nth(1);
const c = page.locator('affine-note affine-paragraph').nth(2);
const d = page.locator('affine-note affine-paragraph').nth(3);
const e = page.locator('affine-note affine-paragraph').nth(4);
const f = page.locator('affine-note affine-paragraph').nth(5);
const g = page.locator('affine-note affine-paragraph').nth(6);
const h = page.locator('affine-note affine-paragraph').nth(7);
const i = page.locator('affine-note affine-paragraph').nth(8);
const j = page.locator('affine-note affine-paragraph').nth(9);
const assertInitState = async () => {
expect(await a.isVisible()).toBeTruthy();
expect(await b.isVisible()).toBeTruthy();
expect(await c.isVisible()).toBeTruthy();
expect(await d.isVisible()).toBeTruthy();
expect(await e.isVisible()).toBeTruthy();
expect(await f.isVisible()).toBeTruthy();
expect(await g.isVisible()).toBeTruthy();
expect(await h.isVisible()).toBeTruthy();
expect(await i.isVisible()).toBeTruthy();
expect(await j.isVisible()).toBeTruthy();
};
await assertInitState();
// fold h1 aaa
await a.hover();
await page.waitForTimeout(100);
await a.locator('blocksuite-toggle-button .toggle-icon').click();
expect(await a.isVisible()).toBeTruthy();
expect(await b.isVisible()).toBeFalsy();
expect(await c.isVisible()).toBeFalsy();
expect(await d.isVisible()).toBeFalsy();
expect(await e.isVisible()).toBeFalsy();
expect(await f.isVisible()).toBeFalsy();
expect(await g.isVisible()).toBeFalsy();
expect(await h.isVisible()).toBeFalsy();
expect(await i.isVisible()).toBeTruthy();
expect(await j.isVisible()).toBeTruthy();
await undoByKeyboard(page);
await assertInitState();
// fold h2 ccc
await c.hover();
await page.waitForTimeout(100);
await c.locator('blocksuite-toggle-button .toggle-icon').click();
expect(await a.isVisible()).toBeTruthy();
expect(await b.isVisible()).toBeTruthy();
expect(await c.isVisible()).toBeTruthy();
expect(await d.isVisible()).toBeFalsy();
expect(await e.isVisible()).toBeFalsy();
expect(await f.isVisible()).toBeFalsy();
expect(await g.isVisible()).toBeTruthy();
expect(await h.isVisible()).toBeTruthy();
expect(await i.isVisible()).toBeTruthy();
expect(await j.isVisible()).toBeTruthy();
await undoByKeyboard(page);
await assertInitState();
// fold h3 eee
await e.hover();
await page.waitForTimeout(100);
await e.locator('blocksuite-toggle-button .toggle-icon').click();
expect(await a.isVisible()).toBeTruthy();
expect(await b.isVisible()).toBeTruthy();
expect(await c.isVisible()).toBeTruthy();
expect(await d.isVisible()).toBeTruthy();
expect(await e.isVisible()).toBeTruthy();
expect(await f.isVisible()).toBeFalsy();
expect(await g.isVisible()).toBeTruthy();
expect(await h.isVisible()).toBeTruthy();
expect(await i.isVisible()).toBeTruthy();
expect(await j.isVisible()).toBeTruthy();
await undoByKeyboard(page);
await assertInitState();
// fold h2 ggg
await g.hover();
await page.waitForTimeout(100);
await g.locator('blocksuite-toggle-button .toggle-icon').click();
expect(await a.isVisible()).toBeTruthy();
expect(await b.isVisible()).toBeTruthy();
expect(await c.isVisible()).toBeTruthy();
expect(await d.isVisible()).toBeTruthy();
expect(await e.isVisible()).toBeTruthy();
expect(await f.isVisible()).toBeTruthy();
expect(await g.isVisible()).toBeTruthy();
expect(await j.isVisible()).toBeTruthy();
});
test('add new heading when press enter at the end of collapsed heading', async ({
page,
}) => {
await pressEnter(page);
await type(page, '# aaa');
await page.keyboard.press('ArrowDown');
await type(page, 'bbb');
await page.keyboard.press('ArrowUp');
await page.keyboard.press('ArrowRight');
const paragraph = page.locator('affine-note affine-paragraph');
expect(
await paragraph
.nth(1)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.text.toString() === 'bbb'
)
).toBeTruthy();
await pressEnter(page);
expect(
await paragraph
.nth(1)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.type === 'text' &&
block.model.props.text.toString() === ''
)
).toBeTruthy();
expect(
await paragraph
.nth(2)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.text.toString() === 'bbb'
)
).toBeTruthy();
await undoByKeyboard(page);
expect(
await paragraph
.nth(1)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.text.toString() === 'bbb'
)
).toBeTruthy();
expect(await paragraph.nth(1).isVisible()).toBeTruthy();
await paragraph
.nth(0)
.locator('blocksuite-toggle-button .toggle-icon')
.click();
expect(await paragraph.nth(1).isVisible()).toBeFalsy();
await paragraph.nth(0).click();
expect(await paragraph.count()).toBe(2);
await pressEnter(page);
expect(await paragraph.count()).toBe(3);
expect(
await paragraph
.nth(2)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.type === 'h1' &&
block.model.props.text.toString() === ''
)
).toBeTruthy();
});
test('unfold collapsed heading when some block indented to be its child', async ({
page,
}) => {
await pressEnter(page);
await type(page, '# aaa\nbbb\n# ccc');
const paragraph = page.locator('affine-note affine-paragraph');
await paragraph
.nth(0)
.locator('blocksuite-toggle-button .toggle-icon')
.click();
expect(await paragraph.nth(1).isVisible()).toBeFalsy();
await paragraph.nth(2).click();
await pressTab(page);
expect(await paragraph.nth(1).isVisible()).toBeTruthy();
});
test('unfold collapsed heading when its siblings changed to text type from heading type', async ({
page,
}) => {
await pressEnter(page);
await type(page, '# aaa\nbbb\n# ccc');
const paragraph = page.locator('affine-note affine-paragraph');
await paragraph
.nth(0)
.locator('blocksuite-toggle-button .toggle-icon')
.click();
expect(await paragraph.nth(1).isVisible()).toBeFalsy();
await paragraph.nth(2).click();
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('ArrowLeft');
expect(
await paragraph
.nth(2)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.type === 'h1' &&
block.model.props.text.toString() === 'ccc'
)
).toBeTruthy();
await page.keyboard.press('Backspace');
expect(
await paragraph
.nth(2)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.type === 'text' &&
block.model.props.text.toString() === 'ccc'
)
).toBeTruthy();
expect(await paragraph.nth(1).isVisible()).toBeTruthy();
});
test('also move children when dedent collapsed heading', async ({ page }) => {
await pressEnter(page);
await type(page, 'aaa');
await page.keyboard.press('ArrowDown');
await pressTab(page);
await type(page, '# bbb\nccc');
const paragraph = page.locator('affine-note affine-paragraph');
const subParagraph = paragraph.nth(0).locator('affine-paragraph');
await expect.poll(() => subParagraph.count()).toBe(2);
await expectParagraphState(subParagraph, 0, 'h1', 'bbb');
await expectParagraphState(subParagraph, 1, 'text', 'ccc');
expect(await subParagraph.nth(1).isVisible()).toBeTruthy();
await subParagraph
.nth(0)
.locator('blocksuite-toggle-button .toggle-icon')
.click();
expect(await subParagraph.nth(1).isVisible()).toBeFalsy();
await subParagraph.nth(0).click();
await pressShiftTab(page);
expect(await subParagraph.count()).toBe(0);
await expectParagraphState(paragraph, 1, 'h1', 'bbb');
await expectParagraphState(paragraph, 2, 'text', 'ccc');
await expectParagraphVisibility(paragraph, 2, false);
await paragraph
.nth(1)
.locator('blocksuite-toggle-button .toggle-icon')
.click({
position: {
x: 5,
y: 5,
},
});
await expectParagraphVisibility(paragraph, 2, true);
});
test('also move collapsed siblings when indent collapsed heading', async ({
page,
}) => {
await pressEnter(page);
await type(page, 'aaa');
await page.keyboard.press('ArrowDown');
await type(page, '# bbb\nccc');
await page.keyboard.press('ArrowUp');
const paragraph = page.locator('affine-note affine-paragraph');
expect(await paragraph.nth(0).locator('affine-paragraph').count()).toBe(0);
await pressTab(page);
expect(await paragraph.nth(0).locator('affine-paragraph').count()).toBe(1);
expect(
await paragraph
.nth(0)
.locator('affine-paragraph')
.nth(0)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.type === 'h1' &&
block.model.props.text.toString() === 'bbb'
)
).toBeTruthy();
await undoByKeyboard(page);
expect(await paragraph.nth(2).isVisible()).toBeTruthy();
await paragraph
.nth(1)
.locator('blocksuite-toggle-button .toggle-icon')
.click();
expect(await paragraph.nth(2).isVisible()).toBeFalsy();
await paragraph.nth(1).click();
await pressTab(page);
expect(await paragraph.nth(2).isVisible()).toBeFalsy();
expect(await paragraph.nth(0).locator('affine-paragraph').count()).toBe(2);
expect(
await paragraph
.nth(0)
.locator('affine-paragraph')
.nth(0)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.type === 'h1' &&
block.model.props.text.toString() === 'bbb'
)
).toBeTruthy();
expect(
await paragraph
.nth(0)
.locator('affine-paragraph')
.nth(1)
.evaluate(
(block: ParagraphBlockComponent) =>
block.model.props.type === 'text' &&
block.model.props.text.toString() === 'ccc'
)
).toBeTruthy();
});
test('unfold collapsed heading when its other blocks indented to be its sibling', async ({
page,
}) => {
await pressEnter(page);
await type(page, 'aaa');
await page.keyboard.press('ArrowDown');
await type(page, '# bbb\nddd');
await page.keyboard.press('ArrowUp');
await pressTab(page);
const paragraph = page.locator('affine-note affine-paragraph');
const subParagraph = paragraph.nth(0).locator('affine-paragraph');
await expect.poll(() => subParagraph.count()).toBe(1);
await expectParagraphState(subParagraph, 0, 'h1', 'bbb');
await subParagraph.nth(0).click();
await page.keyboard.press('End');
await pressEnter(page);
await type(page, 'ccc');
/**
* aaa
* # bbb
* ccc
* ddd
*/
await expectParagraphVisibility(paragraph, 2, true);
await expectParagraphState(paragraph, 2, 'text', 'ccc');
await paragraph.locator('blocksuite-toggle-button .toggle-icon').click();
await expectParagraphVisibility(paragraph, 2, false);
await paragraph.nth(3).click(); // ddd
await expectParagraphVisibility(paragraph, 2, false);
await expect
.poll(() => paragraph.nth(0).locator('affine-paragraph').count())
.toBe(2);
await pressTab(page);
await expect
.poll(() => paragraph.nth(0).locator('affine-paragraph').count())
.toBe(3);
await expectParagraphVisibility(paragraph, 2, true);
});

View File

@@ -0,0 +1,126 @@
import {
clickDeleteButtonInTableMenu,
createTable,
getCellText,
inputToCell,
} from '@affine-test/kit/bs/table';
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('should table collab work', async ({ page: pageA, context }) => {
await openHomePage(pageA);
await clickNewPageButton(pageA);
await waitForEditorLoad(pageA);
await pageA.keyboard.press('Enter');
/**
* | Cell1 | Cell2 |
* | Cell3 | Cell4 |
*/
await createTable(pageA);
const cellsInA = pageA.locator('affine-table-cell');
const cellCountInA = await cellsInA.count();
expect(cellCountInA).toBe(4);
const currentUrl = pageA.url();
const pageB = await context.newPage();
await pageB.goto(currentUrl);
await waitForEditorLoad(pageB);
await pageB.keyboard.press('Enter');
const tableInB = pageB.locator('affine-table');
await expect(tableInB).toBeVisible();
const cellsInB = tableInB.locator('affine-table-cell');
expect(await cellsInB.count()).toBe(cellCountInA);
expect(await getCellText(pageB, 0)).toBe('Cell1');
expect(await getCellText(pageB, 3)).toBe('Cell4');
await cellsInB.last().hover();
const addColumnButton = tableInB.locator('data-testid=add-column-button');
expect(await addColumnButton.isVisible()).toBe(true);
await addColumnButton.click();
expect(await cellsInA.count()).toBe(6);
expect(await cellsInB.count()).toBe(6);
// new created cells should be synced to both page
/**
* | Cell1 | Cell2 | Cell5 |
* | Cell3 | Cell4 | Cell6 |
*/
await inputToCell(pageA, 2, 'Cell5');
await inputToCell(pageA, 5, 'Cell6');
expect(await getCellText(pageB, 2)).toBe('Cell5');
expect(await getCellText(pageB, 5)).toBe('Cell6');
await cellsInB.last().hover();
const addRowButton = tableInB.locator('data-testid=add-row-button');
expect(await addRowButton.isVisible()).toBe(true);
await addRowButton.click();
expect(await cellsInA.count()).toBe(9);
expect(await cellsInB.count()).toBe(9);
// new created cells should be synced to both page
/**
* | Cell1 | Cell2 | Cell5 |
* | Cell3 | Cell4 | Cell6 |
* | Cell7 | Cell8 | Cell9 |
*/
await inputToCell(pageA, 6, 'Cell7');
await inputToCell(pageA, 7, 'Cell8');
await inputToCell(pageA, 8, 'Cell9');
expect(await getCellText(pageB, 6)).toBe('Cell7');
expect(await getCellText(pageB, 7)).toBe('Cell8');
expect(await getCellText(pageB, 8)).toBe('Cell9');
// delete a column
await cellsInA.nth(1).hover();
const dragColumnHandle = cellsInA
.nth(1)
.locator('data-testid=drag-column-handle');
expect(await dragColumnHandle.isVisible()).toBe(true);
await dragColumnHandle.click();
await clickDeleteButtonInTableMenu(pageA);
/**
* | Cell1 | Cell5 |
* | Cell3 | Cell6 |
* | Cell7 | Cell9 |
*/
expect(await cellsInA.count()).toBe(6);
expect(await cellsInB.count()).toBe(6);
expect(await getCellText(pageB, 0)).toBe('Cell1');
expect(await getCellText(pageB, 1)).toBe('Cell5');
expect(await getCellText(pageB, 2)).toBe('Cell3');
expect(await getCellText(pageB, 3)).toBe('Cell6');
expect(await getCellText(pageB, 4)).toBe('Cell7');
expect(await getCellText(pageB, 5)).toBe('Cell9');
// delete a row
await cellsInB.nth(0).hover();
const dragRowHandle = cellsInB.nth(0).locator('data-testid=drag-row-handle');
expect(await dragRowHandle.isVisible()).toBe(true);
await dragRowHandle.click();
await clickDeleteButtonInTableMenu(pageB);
/**
* | Cell3 | Cell6 |
* | Cell7 | Cell9 |
*/
expect(await cellsInA.count()).toBe(4);
expect(await cellsInB.count()).toBe(4);
expect(await getCellText(pageB, 0)).toBe('Cell3');
expect(await getCellText(pageB, 1)).toBe('Cell6');
expect(await getCellText(pageB, 2)).toBe('Cell7');
expect(await getCellText(pageB, 3)).toBe('Cell9');
});

View File

@@ -0,0 +1,200 @@
import { waitNextFrame } from '@affine-test/kit/bs/misc';
import { createTable, getCellText } from '@affine-test/kit/bs/table';
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';
/**
* This test suite validates the edge case functionality of table insertion operations.
* It tests insertion at boundaries of the table, such as:
* - Insert Left at the first column
* - Insert Above at the first row
* - Insert Right at the last column
* - Insert Below at the last row
*/
test.describe('Table edge case insertion operations', () => {
test('should correctly insert at edges', async ({ page }) => {
// Setup: Create a new page with a table
await openHomePage(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
await page.keyboard.press('Enter');
// Create a simple table with default 2x2 cells
await createTable(page);
const table = page.locator('affine-table');
await expect(table).toBeVisible();
// Wait for table rendering to complete
await waitNextFrame(page);
// Initialize all cell contents
const cells = table.locator('affine-table-cell');
expect(await cells.count()).toBe(4);
// Clear and set cell contents
for (let i = 0; i < 4; i++) {
const cell = cells.nth(i);
await cell.click({ clickCount: 3 }); // Select all content in the cell
await page.keyboard.press('Backspace'); // Clear content
await page.keyboard.type(`Cell${i + 1}`); // Input new content
await waitNextFrame(page);
}
// Verify initial cell contents
expect(await getCellText(page, 0)).toBe('Cell1');
expect(await getCellText(page, 1)).toBe('Cell2');
// Edge case 1: Insert to the left of the first column
// Hover over the first column to make the column options button visible
await cells.nth(0).hover();
await waitNextFrame(page);
// Click on the column options button to open the column options menu
const columnOptionButton = table
.locator('[data-testid="drag-column-handle"]')
.first();
await expect(columnOptionButton).toBeVisible();
await columnOptionButton.click();
// Click on the "Insert Left" option in the menu
const menu = page.locator('affine-menu');
await expect(menu).toBeVisible();
await menu.getByText('Insert Left').click();
await waitNextFrame(page);
// Verify: Now we should have 6 cells (3x2 table)
const cellsAfterLeftEdgeInsert = table.locator('affine-table-cell');
expect(await cellsAfterLeftEdgeInsert.count()).toBe(6);
// Input text to verify insertion position
const leftEdgeCell = cellsAfterLeftEdgeInsert.nth(0);
await leftEdgeCell.click({ clickCount: 3 }); // Select all content
await page.keyboard.press('Backspace'); // Clear content
await page.keyboard.type('Left Edge');
await waitNextFrame(page);
// Verify first row values: Left Edge, Cell1, Cell2
expect(await getCellText(page, 0)).toBe('Left Edge');
// Fix: Re-get and verify second column content
const secondCell = cellsAfterLeftEdgeInsert.nth(1);
await secondCell.click({ clickCount: 3 }); // Select all content
await page.keyboard.press('Backspace'); // Clear content
await page.keyboard.type('Cell1'); // Re-input
await waitNextFrame(page);
expect(await getCellText(page, 1)).toBe('Cell1');
// Fix: Re-get and verify third column content
const thirdCell = cellsAfterLeftEdgeInsert.nth(2);
await thirdCell.click({ clickCount: 3 }); // Select all content
await page.keyboard.press('Backspace'); // Clear content
await page.keyboard.type('Cell2'); // Re-input
await waitNextFrame(page);
expect(await getCellText(page, 2)).toBe('Cell2');
// Edge case 2: Insert above the first row
// Hover over the first row to make the row options button visible
await cellsAfterLeftEdgeInsert.nth(0).hover();
await waitNextFrame(page);
// Click on the row options button to open the row options menu
const rowOptionButton = table
.locator('[data-testid="drag-row-handle"]')
.first();
await expect(rowOptionButton).toBeVisible();
await rowOptionButton.click();
// Click on the "Insert Above" option in the menu
await expect(menu).toBeVisible();
await menu.getByText('Insert Above').click();
await waitNextFrame(page);
// Verify: Now we should have 9 cells (3x3 table)
const cellsAfterAboveEdgeInsert = table.locator('affine-table-cell');
expect(await cellsAfterAboveEdgeInsert.count()).toBe(9);
// Input text to verify insertion position
const aboveEdgeCell = cellsAfterAboveEdgeInsert.nth(0);
await aboveEdgeCell.click({ clickCount: 3 }); // Select all content
await page.keyboard.press('Backspace'); // Clear content
await page.keyboard.type('Above Edge');
await waitNextFrame(page);
// Verify first row first column value
expect(await getCellText(page, 0)).toBe('Above Edge');
// Verify second row first column value (previously the first row first column)
expect(await getCellText(page, 3)).toBe('Left Edge');
// Edge case 3: Insert to the right of the last column
// Hover over the last column to make the column options button visible
const lastColumnCell = cellsAfterAboveEdgeInsert.nth(2);
await lastColumnCell.hover();
await waitNextFrame(page);
// Click on the last column's options button
const lastColumnOptionButton = table
.locator('[data-testid="drag-column-handle"]')
.nth(2);
await expect(lastColumnOptionButton).toBeVisible();
await lastColumnOptionButton.click();
// Click on the "Insert Right" option in the menu
await expect(menu).toBeVisible();
await menu.getByText('Insert Right').click();
await waitNextFrame(page);
// Verify: Now we should have 12 cells (4x3 table)
const cellsAfterRightEdgeInsert = table.locator('affine-table-cell');
expect(await cellsAfterRightEdgeInsert.count()).toBe(12);
// Input text to verify insertion position
const rightEdgeCell = cellsAfterRightEdgeInsert.nth(3);
await rightEdgeCell.click({ clickCount: 3 }); // Select all content
await page.keyboard.press('Backspace'); // Clear content
await page.keyboard.type('Right Edge');
await waitNextFrame(page);
// Verify first row last column value
expect(await getCellText(page, 3)).toBe('Right Edge');
// Edge case 4: Insert below the last row
// Find the first cell of the last row
const lastRowCell = cellsAfterRightEdgeInsert.nth(8); // Row 3, Column 1
await lastRowCell.hover();
await waitNextFrame(page);
// Click on the last row's options button
const lastRowOptionButton = table
.locator('[data-testid="drag-row-handle"]')
.nth(2);
await expect(lastRowOptionButton).toBeVisible();
await lastRowOptionButton.click();
// Click on the "Insert Below" option in the menu
await expect(menu).toBeVisible();
await menu.getByText('Insert Below').click();
await waitNextFrame(page);
// Verify: Now we should have 16 cells (4x4 table)
const cellsAfterBelowEdgeInsert = table.locator('affine-table-cell');
expect(await cellsAfterBelowEdgeInsert.count()).toBe(16);
// Input text to verify insertion position
const belowEdgeCell = cellsAfterBelowEdgeInsert.nth(12); // Row 4, Column 1
await belowEdgeCell.click({ clickCount: 3 }); // Select all content
await page.keyboard.press('Backspace'); // Clear content
await page.keyboard.type('Below Edge');
await waitNextFrame(page);
// Verify last row first column value
expect(await getCellText(page, 12)).toBe('Below Edge');
});
});

View File

@@ -0,0 +1,198 @@
import { waitNextFrame } from '@affine-test/kit/bs/misc';
import { createTable, getCellText } from '@affine-test/kit/bs/table';
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';
/**
* This test suite validates the correct functionality of table insertion operations.
* It tests Insert Left, Insert Right, Insert Above, and Insert Below operations
* to verify they correctly insert rows and columns in the expected positions.
*/
test.describe('Table insertion operations', () => {
test('should correctly insert columns', async ({ page }) => {
// Setup: Create a new page with a table
await openHomePage(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
await page.keyboard.press('Enter');
// Create a simple table with default 2x2 cells
await createTable(page);
const table = page.locator('affine-table');
await expect(table).toBeVisible();
// Verify initial table structure (2x2)
const initialCells = table.locator('affine-table-cell');
expect(await initialCells.count()).toBe(4);
// Hover over the first cell to make the column options button visible
await initialCells.nth(0).hover();
await waitNextFrame(page);
// Click on the column options button (three dots icon) to open the column options menu
const columnOptionButton = table
.locator('[data-testid="drag-column-handle"]')
.first();
await expect(columnOptionButton).toBeVisible();
await columnOptionButton.click();
// Click on the "Insert Right" option in the menu
const menu = page.locator('affine-menu');
await expect(menu).toBeVisible();
await menu.getByText('Insert Right').click();
await waitNextFrame(page);
// Verify: Now we should have 6 cells (3x2 table)
const cellsAfterRightInsert = table.locator('affine-table-cell');
expect(await cellsAfterRightInsert.count()).toBe(6);
// Input text in the newly inserted column to verify its position
// After inserting right of column 0, our new cell should be at position 1
await getCellText(page, 0); // Wait for cell to be ready
const newColumnCell = cellsAfterRightInsert.nth(1);
await newColumnCell.hover();
await waitNextFrame(page);
await newColumnCell.click();
await page.keyboard.type('New Right');
// Verify values in row 0: Cell1, New Right, Cell2
expect(await getCellText(page, 0)).toBe('Cell1');
expect(await getCellText(page, 1)).toBe('New Right');
expect(await getCellText(page, 2)).toBe('Cell2');
// Hover over the first column again to make the column options button visible
await cellsAfterRightInsert.nth(0).hover();
await waitNextFrame(page);
// Click on the column options button to open the column options menu
await expect(columnOptionButton).toBeVisible();
await columnOptionButton.click();
// Click on the "Insert Left" option in the menu
await expect(menu).toBeVisible();
await menu.getByText('Insert Left').click();
await waitNextFrame(page);
// Verify: Now we should have 8 cells (4x2 table)
const cellsAfterLeftInsert = table.locator('affine-table-cell');
expect(await cellsAfterLeftInsert.count()).toBe(8);
// Input text in the newly inserted column
await getCellText(page, 0); // Wait for cell to be ready
const leftColumnCell = cellsAfterLeftInsert.nth(0);
await leftColumnCell.hover();
await waitNextFrame(page);
await leftColumnCell.click();
await page.keyboard.type('New Left');
// Verify values in row 0: New Left, Cell1, New Right, Cell2
expect(await getCellText(page, 0)).toBe('New Left');
expect(await getCellText(page, 1)).toBe('Cell1');
expect(await getCellText(page, 2)).toBe('New Right');
expect(await getCellText(page, 3)).toBe('Cell2');
});
test('should correctly insert rows', async ({ page }) => {
// Setup: Create a new page with a table
await openHomePage(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
await page.keyboard.press('Enter');
// Create a simple table with default 2x2 cells
await createTable(page);
const table = page.locator('affine-table');
await expect(table).toBeVisible();
// Verify initial table structure (2x2)
const initialCells = table.locator('affine-table-cell');
expect(await initialCells.count()).toBe(4);
// Hover over the first cell to make the row options button visible
await initialCells.nth(0).hover();
await waitNextFrame(page);
// Click on the row options button (three dots icon) to open the row options menu
const rowOptionButton = table
.locator('[data-testid="drag-row-handle"]')
.first();
await expect(rowOptionButton).toBeVisible();
await rowOptionButton.click();
// Click on the "Insert Below" option in the menu
const menu = page.locator('affine-menu');
await expect(menu).toBeVisible();
await menu.getByText('Insert Below').click();
await waitNextFrame(page);
// Verify: Now we should have 6 cells (2x3 table)
const cellsAfterBelowInsert = table.locator('affine-table-cell');
expect(await cellsAfterBelowInsert.count()).toBe(6);
// Input text in the newly inserted row
await getCellText(page, 2); // Wait for cell to be ready
const newRowCell1 = cellsAfterBelowInsert.nth(2);
await newRowCell1.hover();
await waitNextFrame(page);
await newRowCell1.click();
await page.keyboard.type('New Below 1');
await getCellText(page, 3); // Wait for cell to be ready
const newRowCell2 = cellsAfterBelowInsert.nth(3);
await newRowCell2.hover();
await waitNextFrame(page);
await newRowCell2.click();
await page.keyboard.type('New Below 2');
// Verify values: First row: Cell1, Cell2; Second row: New Below 1, New Below 2; Third row: Cell3, Cell4
expect(await getCellText(page, 0)).toBe('Cell1');
expect(await getCellText(page, 1)).toBe('Cell2');
expect(await getCellText(page, 2)).toBe('New Below 1');
expect(await getCellText(page, 3)).toBe('New Below 2');
expect(await getCellText(page, 4)).toBe('Cell3');
expect(await getCellText(page, 5)).toBe('Cell4');
// Hover over the first row again to make the row options button visible
await cellsAfterBelowInsert.nth(0).hover();
await waitNextFrame(page);
// Click on the row options button to open the row options menu
await expect(rowOptionButton).toBeVisible();
await rowOptionButton.click();
// Click on the "Insert Above" option in the menu
await expect(menu).toBeVisible();
await menu.getByText('Insert Above').click();
await waitNextFrame(page);
// Verify: Now we should have 8 cells (2x4 table)
const cellsAfterAboveInsert = table.locator('affine-table-cell');
expect(await cellsAfterAboveInsert.count()).toBe(8);
// Input text in the newly inserted row
await getCellText(page, 0); // Wait for cell to be ready
const aboveRowCell1 = cellsAfterAboveInsert.nth(0);
await aboveRowCell1.hover();
await waitNextFrame(page);
await aboveRowCell1.click();
await page.keyboard.type('New Above 1');
await getCellText(page, 1); // Wait for cell to be ready
const aboveRowCell2 = cellsAfterAboveInsert.nth(1);
await aboveRowCell2.hover();
await waitNextFrame(page);
await aboveRowCell2.click();
await page.keyboard.type('New Above 2');
// Verify the first row contains our new values
expect(await getCellText(page, 0)).toBe('New Above 1');
expect(await getCellText(page, 1)).toBe('New Above 2');
expect(await getCellText(page, 2)).toBe('Cell1');
expect(await getCellText(page, 3)).toBe('Cell2');
});
});

View File

@@ -0,0 +1,458 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
dragView,
locateToolbar,
setEdgelessTool,
} from '@affine-test/kit/utils/editor';
import { importImage } from '@affine-test/kit/utils/image';
import {
selectAllByKeyboard,
writeTextToClipboard,
} 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 } from '@playwright/test';
function hexToRGB(hex: string) {
hex = hex.replace(/^#/, '');
const len = hex.length;
let arr: string[] = [];
if (len === 3 || len === 4) {
arr = hex.split('').map(s => s.repeat(2));
} else if (len === 6 || len === 8) {
arr = Array.from<number>({ length: len / 2 })
.fill(0)
.map((n, i) => n + i * 2)
.map(n => hex.substring(n, n + 2));
}
const values = arr
.map(s => parseInt(s, 16))
.map((n, i) => (i === 3 ? (n % 255) / 255 : n));
return `rgb${values.length === 4 ? 'a' : ''}(${values.join(', ')})`;
}
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await clickNewPageButton(page);
await waitForEmptyEditor(page);
});
test('should toggle toolbar when dragging page area', async ({ page }) => {
await page.keyboard.press('Enter');
await page.keyboard.type('Roman');
await selectAllByKeyboard(page);
const toolbar = locateToolbar(page);
await expect(toolbar).toBeVisible();
await expect(toolbar).toBeInViewport();
const paragraph = page.locator('affine-note affine-paragraph').nth(0);
const bounds = await paragraph.boundingBox();
expect(bounds).toBeTruthy();
const { x, y, width } = bounds!;
await page.mouse.move(x + width + 10, y - 10, { steps: 2 });
await page.mouse.down();
await page.mouse.move(x + width - 10, y + 10, { steps: 2 });
await expect(toolbar).toBeHidden();
await page.mouse.up();
await expect(toolbar).toBeVisible();
});
test.describe('Formatting', () => {
test('should change text color', async ({ page }) => {
await page.keyboard.press('Enter');
await page.keyboard.type('hello world');
await page.keyboard.press('Shift+ArrowLeft');
await page.keyboard.press('Shift+ArrowLeft');
await page.keyboard.press('Shift+ArrowLeft');
const toolbar = locateToolbar(page);
const highlightButton = toolbar.locator('affine-highlight-duotone-icon');
await highlightButton.click();
const fgGreenButton = toolbar.locator('[data-testid="foreground-green"]');
await fgGreenButton.click();
const fgColor = await fgGreenButton
.locator('affine-text-duotone-icon')
.evaluate(e => window.getComputedStyle(e).getPropertyValue('--color'));
const paragraph = page.locator('affine-paragraph');
const textSpan = paragraph
.locator('affine-text:has-text("rld")')
.locator('span')
.first();
await expect(textSpan).toBeVisible();
await expect(textSpan).toHaveCSS('color', hexToRGB(fgColor));
});
test('should change text background color', async ({ page }) => {
await page.keyboard.press('Enter');
await page.keyboard.type('hello world');
await page.keyboard.press('Shift+ArrowLeft');
await page.keyboard.press('Shift+ArrowLeft');
await page.keyboard.press('Shift+ArrowLeft');
const toolbar = locateToolbar(page);
const highlightButton = toolbar.locator('affine-highlight-duotone-icon');
await highlightButton.click();
const fgGreenButton = toolbar.locator('[data-testid="foreground-green"]');
await fgGreenButton.click();
await page.waitForTimeout(200);
const fgColor = await fgGreenButton
.locator('affine-text-duotone-icon')
.evaluate(e => window.getComputedStyle(e).getPropertyValue('--color'));
const paragraph = page.locator('affine-paragraph');
const textSpan1 = paragraph
.locator('affine-text:has-text("rld")')
.locator('span')
.first();
await expect(textSpan1).toHaveCSS('color', hexToRGB(fgColor));
await page.keyboard.press('ArrowRight');
for (let i = 0; i < 5; i++) {
await page.keyboard.press('Shift+ArrowLeft');
}
await highlightButton.click();
const yellow = 'var(--affine-text-highlight-yellow)';
const bgYellowButton = toolbar.locator('[data-testid="background-yellow"]');
await bgYellowButton.click();
const textSpan2 = paragraph
.locator('affine-text:has-text("wo")')
.locator('span')
.first();
await expect(textSpan2).toBeVisible();
const bgColor1 = await textSpan1.evaluate(e => e.style.backgroundColor);
const bgColor2 = await textSpan2.evaluate(e => e.style.backgroundColor);
expect(yellow).toBe(bgColor1);
expect(yellow).toBe(bgColor2);
const bgColor = await bgYellowButton
.locator('affine-text-duotone-icon')
.evaluate(e =>
window.getComputedStyle(e).getPropertyValue('--background')
);
await expect(textSpan1).toHaveCSS('background-color', hexToRGB(bgColor));
await expect(textSpan2).toHaveCSS('background-color', hexToRGB(bgColor));
});
});
test('should not show toolbar when releasing spacebar and elements have been deleted', async ({
page,
}) => {
await clickEdgelessModeButton(page);
await setEdgelessTool(page, 'shape');
await dragView(page, [100, 300], [200, 400]);
const toolbar = locateToolbar(page);
await expect(toolbar).toBeVisible();
await page.keyboard.down('Space');
await expect(toolbar).toBeHidden();
await page.keyboard.up('Space');
await expect(toolbar).toBeVisible();
await page.keyboard.press('Delete');
await expect(toolbar).toBeHidden();
await page.keyboard.down('Space');
await expect(toolbar).toBeHidden();
await page.keyboard.up('Space');
await expect(toolbar).toBeHidden();
});
test('should not show inner toolbar of surface-ref in note under edgeless', async ({
page,
}) => {
await page.keyboard.press('Enter');
await page.keyboard.type('/frame');
await page.keyboard.press('Enter');
const toolbar = locateToolbar(page);
const surfaceRef = page.locator('affine-surface-ref');
await surfaceRef.hover();
await expect(toolbar).toBeVisible();
await clickEdgelessModeButton(page);
const note = page.locator('affine-edgeless-note');
await note.click();
await note.click();
const edgelessSurfaceRef = note.locator('affine-edgeless-surface-ref');
await edgelessSurfaceRef.hover();
await expect(toolbar).toBeHidden();
const dragHandler = page.locator('.affine-drag-handle-grabber');
await dragHandler.hover();
await dragHandler.click();
await expect(
edgelessSurfaceRef.locator('.affine-edgeless-surface-ref-container')
).toHaveClass(/focused$/);
await expect(toolbar).toBeHidden();
});
test('should show toolbar when inline link is preceded by image or surface-ref', async ({
page,
}) => {
await page.keyboard.press('Enter');
await importImage(page, 'affine-preview.png');
const image = page.locator('affine-image');
await image.click();
await page.keyboard.press('Enter');
const url = new URL(page.url());
await writeTextToClipboard(page, url.toString());
const toolbar = locateToolbar(page);
const inlineLink = page.locator('affine-reference');
await inlineLink.hover();
await expect(toolbar).toBeVisible();
await toolbar.hover();
await expect(toolbar).toBeVisible();
await image.hover();
await expect(toolbar).toBeVisible();
await expect(toolbar).toHaveAttribute('data-placement', 'inner');
await inlineLink.hover();
await expect(toolbar).toBeVisible();
await expect(toolbar).not.toHaveAttribute('data-placement', 'inner');
});
test('should focus on input of popover on toolbar', async ({ page }) => {
await clickEdgelessModeButton(page);
const toolbar = locateToolbar(page);
await expect(toolbar).toBeHidden();
const note = page.locator('affine-edgeless-note').first();
await note.click();
await expect(toolbar).toBeVisible();
const scaleMenu = toolbar.locator('.scale-menu');
const scaleInput = scaleMenu.locator('input');
const scaleButton = scaleMenu.getByLabel('Scale');
await expect(scaleInput).toBeHidden();
await scaleButton.click();
await scaleInput.click();
await expect(scaleInput).toBeFocused();
await scaleInput.fill('150');
await expect(scaleInput).toBeFocused();
const scaleValue = await scaleInput.inputValue();
expect(scaleValue).toBe('150');
const stylePanelButton = toolbar.locator('edgeless-note-style-panel');
const cornersInput = stylePanelButton.locator(
'.edgeless-note-corner-radius-panel input'
);
await stylePanelButton.click();
await cornersInput.click();
await expect(cornersInput).toBeFocused();
await cornersInput.fill('36');
await expect(cornersInput).toBeFocused();
const cornersValue = await cornersInput.inputValue();
expect(cornersValue).toBe('36');
});
test('Dropdown menus should be closed automatically when toolbar is displayed', async ({
page,
}) => {
await page.keyboard.press('Enter');
await page.keyboard.type('/frame');
await page.keyboard.press('Enter');
const toolbar = locateToolbar(page);
const surfaceRef = page.locator('affine-surface-ref');
await surfaceRef.hover();
await expect(toolbar).toBeVisible();
const moreMenuContainer = toolbar.getByLabel('More menu');
const moreMenuButton = moreMenuContainer.getByLabel('More');
const moreMenu = moreMenuContainer.getByRole('menu');
await expect(moreMenu).toBeHidden();
await moreMenuButton.click();
await expect(moreMenu).toBeVisible();
await page.keyboard.press('Escape');
await expect(toolbar).toBeHidden();
await surfaceRef.hover();
await expect(toolbar).toBeVisible();
await expect(moreMenu).toBeHidden();
});
test('should clear selection when switching doc mode', async ({ page }) => {
await page.keyboard.press('Enter');
await page.keyboard.type('hello world');
await page.keyboard.press('Shift+ArrowLeft');
await page.keyboard.press('Shift+ArrowLeft');
await page.keyboard.press('Shift+ArrowLeft');
const toolbar = locateToolbar(page);
await expect(toolbar).toBeVisible();
await clickEdgelessModeButton(page);
await expect(toolbar).toBeHidden();
});
test.describe('Toolbar More Actions', () => {
test('should copy selected text as markdown', async ({ page }) => {
await page.keyboard.press('Enter');
await page.keyboard.type('toolbar-copy-as-markdown');
await selectAllByKeyboard(page);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
await toolbar.getByLabel('More menu').click();
await toolbar.getByLabel('Copy as Markdown').click();
const clipboardText = await (
await page.evaluateHandle(() => navigator.clipboard.readText())
).jsonValue();
expect(clipboardText).toContain('toolbar-copy-as-markdown');
});
test('should preserve heading syntax when copying as markdown from toolbar', async ({
page,
}) => {
await page.keyboard.press('Enter');
await type(page, '# toolbar-heading');
await selectAllByKeyboard(page);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
await toolbar.getByLabel('More menu').click();
await toolbar.getByLabel('Copy as Markdown').click();
const clipboardText = await (
await page.evaluateHandle(() => navigator.clipboard.readText())
).jsonValue();
expect(clipboardText).toContain('# toolbar-heading');
});
test('should preserve bulleted list syntax when copying as markdown from toolbar', async ({
page,
}) => {
await page.keyboard.press('Enter');
await type(page, '- toolbar-list-item');
await selectAllByKeyboard(page);
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
await expect(toolbar).toBeVisible();
await toolbar.getByLabel('More menu').click();
await toolbar.getByLabel('Copy as Markdown').click();
const clipboardText = await (
await page.evaluateHandle(() => navigator.clipboard.readText())
).jsonValue();
expect(clipboardText).toMatch(/^\* toolbar-list-item/m);
});
test('should duplicate block', async ({ page }) => {
await page.keyboard.press('Enter');
await importImage(page, 'large-image.png');
const images = page.locator('affine-page-image');
const firstImage = images.first();
const firstImageUrl = await firstImage.locator('img').getAttribute('src');
await firstImage.hover();
const toolbar = locateToolbar(page);
const moreMenu = toolbar.getByLabel('More menu');
await moreMenu.click();
const duplicateButton = toolbar.getByTestId('duplicate');
await duplicateButton.click();
await expect(images).toHaveCount(2);
const secondImage = images.nth(1);
const secondImageUrl = await secondImage.locator('img').getAttribute('src');
expect(firstImageUrl).not.toBeNull();
expect(firstImageUrl!.startsWith('blob:')).toBe(true);
expect(secondImageUrl).not.toBeNull();
expect(secondImageUrl!.startsWith('blob:')).toBe(true);
});
});