snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
This commit is contained in:
28
tests/affine-local/e2e/blocksuite/at-menu.spec.ts
Normal file
28
tests/affine-local/e2e/blocksuite/at-menu.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { test } from '@affine-test/kit/playwright';
|
||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
type,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await clickNewPageButton(page);
|
||||
await waitForEditorLoad(page);
|
||||
});
|
||||
|
||||
test('at menu should not be close when press arrow key', async ({ page }) => {
|
||||
await page.keyboard.press('Enter');
|
||||
await type(page, '@');
|
||||
|
||||
const atMenu = page.locator('.linked-doc-popover');
|
||||
await expect(atMenu).toBeVisible();
|
||||
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await expect(atMenu).toBeVisible();
|
||||
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await expect(atMenu).toBeVisible();
|
||||
});
|
||||
118
tests/affine-local/e2e/blocksuite/attachment/toolbar.spec.ts
Normal file
118
tests/affine-local/e2e/blocksuite/attachment/toolbar.spec.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { test } from '@affine-test/kit/playwright';
|
||||
import {
|
||||
importAttachment,
|
||||
importFile,
|
||||
} from '@affine-test/kit/utils/attachment';
|
||||
import {
|
||||
clickEdgelessModeButton,
|
||||
clickView,
|
||||
locateToolbar,
|
||||
toViewCoord,
|
||||
} from '@affine-test/kit/utils/editor';
|
||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
getBlockSuiteEditorTitle,
|
||||
waitForEmptyEditor,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await clickNewPageButton(page);
|
||||
await waitForEmptyEditor(page);
|
||||
});
|
||||
|
||||
test.describe('Replaces attachment', () => {
|
||||
test('should replace attachment in page', async ({ page }) => {
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.click();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await importAttachment(page, 'lorem-ipsum.pdf');
|
||||
|
||||
const attachment = page.locator('affine-attachment').first();
|
||||
await attachment.click();
|
||||
|
||||
const name = attachment.locator('.affine-attachment-content-title-text');
|
||||
|
||||
await expect(name).toHaveText('lorem-ipsum.pdf');
|
||||
|
||||
const toolbar = locateToolbar(page);
|
||||
const replaceButton = toolbar.getByLabel('Replace attachment');
|
||||
|
||||
await importFile(page, 'v1-color-palettes-snapshot.zip', async () => {
|
||||
await replaceButton.click({ delay: 50 });
|
||||
});
|
||||
|
||||
await expect(attachment).toBeVisible();
|
||||
|
||||
await expect(name).toHaveText('v1-color-palettes-snapshot.zip');
|
||||
});
|
||||
|
||||
test('should replace attachment in edgeless', async ({ page }) => {
|
||||
await clickEdgelessModeButton(page);
|
||||
|
||||
const button = page.locator('edgeless-mindmap-tool-button');
|
||||
await button.click();
|
||||
|
||||
const menu = page.locator('edgeless-mindmap-menu');
|
||||
const mediaItem = menu.locator('.media-item');
|
||||
await mediaItem.click();
|
||||
|
||||
await importFile(page, 'lorem-ipsum.pdf', async () => {
|
||||
await toViewCoord(page, [100, 250]);
|
||||
await clickView(page, [100, 250]);
|
||||
});
|
||||
|
||||
const attachment = page.locator('affine-edgeless-attachment').first();
|
||||
await attachment.click();
|
||||
|
||||
const name = attachment.locator('.affine-attachment-content-title-text');
|
||||
|
||||
await expect(name).toHaveText('lorem-ipsum.pdf');
|
||||
|
||||
const toolbar = locateToolbar(page);
|
||||
const replaceButton = toolbar.getByLabel('Replace attachment');
|
||||
|
||||
await importFile(page, 'v1-color-palettes-snapshot.zip', async () => {
|
||||
await replaceButton.click({ delay: 50 });
|
||||
});
|
||||
|
||||
await expect(attachment).toBeVisible();
|
||||
|
||||
await expect(name).toHaveText('v1-color-palettes-snapshot.zip');
|
||||
});
|
||||
|
||||
test('should fall back to card view when file type does not support embed view', async ({
|
||||
page,
|
||||
}) => {
|
||||
const title = getBlockSuiteEditorTitle(page);
|
||||
await title.click();
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await importAttachment(page, 'lorem-ipsum.pdf');
|
||||
|
||||
const attachment = page.locator('affine-attachment').first();
|
||||
await attachment.click();
|
||||
|
||||
const toolbar = locateToolbar(page);
|
||||
|
||||
// Switches to embed view
|
||||
await toolbar.getByLabel('Switch view').click();
|
||||
await toolbar.getByLabel('Embed view').click();
|
||||
|
||||
const portal = attachment.locator('lit-react-portal');
|
||||
await expect(portal).toBeVisible();
|
||||
|
||||
const replaceButton = toolbar.getByLabel('Replace attachment');
|
||||
await importFile(page, 'v1-color-palettes-snapshot.zip', async () => {
|
||||
await replaceButton.click({ delay: 50 });
|
||||
});
|
||||
|
||||
await expect(portal).toBeHidden();
|
||||
|
||||
const name = attachment.locator('.affine-attachment-content-title-text');
|
||||
await expect(name).toHaveText('v1-color-palettes-snapshot.zip');
|
||||
});
|
||||
});
|
||||
76
tests/affine-local/e2e/blocksuite/callout/callout.spec.ts
Normal file
76
tests/affine-local/e2e/blocksuite/callout/callout.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
pressArrowDown,
|
||||
pressArrowUp,
|
||||
pressBackspace,
|
||||
pressEnter,
|
||||
} from '@affine-test/kit/utils/keyboard';
|
||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
type,
|
||||
waitForEmptyEditor,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await clickNewPageButton(page);
|
||||
await waitForEmptyEditor(page);
|
||||
await page.locator('affine-paragraph v-line div').click();
|
||||
});
|
||||
|
||||
test('add callout block using slash menu and change emoji', async ({
|
||||
page,
|
||||
}) => {
|
||||
await type(page, '/callout\naaaa\nbbbb');
|
||||
const callout = page.locator('affine-callout');
|
||||
const emoji = page.locator('affine-callout').getByTestId('callout-emoji');
|
||||
await expect(callout).toBeVisible();
|
||||
await expect(emoji).toContainText('💡');
|
||||
|
||||
const paragraph = page.locator('affine-callout affine-paragraph');
|
||||
await expect(paragraph).toHaveCount(2);
|
||||
|
||||
const vLine = page.locator('affine-callout v-line');
|
||||
await expect(vLine).toHaveCount(2);
|
||||
expect(await vLine.nth(0).innerText()).toBe('aaaa');
|
||||
expect(await vLine.nth(1).innerText()).toBe('bbbb');
|
||||
});
|
||||
|
||||
test('press backspace after callout block', async ({ page }) => {
|
||||
await pressEnter(page);
|
||||
await pressArrowUp(page);
|
||||
await type(page, '/callout\n');
|
||||
await pressArrowDown(page);
|
||||
|
||||
const paragraph = page.locator('affine-paragraph');
|
||||
const callout = page.locator('affine-callout');
|
||||
expect(await paragraph.count()).toBe(3);
|
||||
expect(await callout.count()).toBe(1);
|
||||
|
||||
await pressBackspace(page);
|
||||
expect(await paragraph.count()).toBe(3);
|
||||
expect(await callout.count()).toBe(1);
|
||||
|
||||
await pressBackspace(page);
|
||||
await expect(paragraph).toHaveCount(2);
|
||||
await expect(callout).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('press backspace in callout block', async ({ page }) => {
|
||||
const paragraph = page.locator('affine-paragraph');
|
||||
const callout = page.locator('affine-callout');
|
||||
|
||||
await type(page, '/callout\n');
|
||||
|
||||
expect(await paragraph.count()).toBe(2);
|
||||
expect(await callout.count()).toBe(1);
|
||||
|
||||
await pressBackspace(page);
|
||||
await expect(paragraph).toHaveCount(1);
|
||||
await expect(callout).toHaveCount(1);
|
||||
|
||||
await pressBackspace(page);
|
||||
await expect(paragraph).toHaveCount(1);
|
||||
await expect(callout).toHaveCount(0);
|
||||
});
|
||||
581
tests/affine-local/e2e/blocksuite/clipboard/clipboard.spec.ts
Normal file
581
tests/affine-local/e2e/blocksuite/clipboard/clipboard.spec.ts
Normal file
@@ -0,0 +1,581 @@
|
||||
import { test } from '@affine-test/kit/playwright';
|
||||
import { importFile } from '@affine-test/kit/utils/attachment';
|
||||
import { pasteContent } from '@affine-test/kit/utils/clipboard';
|
||||
import {
|
||||
clickEdgelessModeButton,
|
||||
clickPageModeButton,
|
||||
clickView,
|
||||
getCodeBlockIds,
|
||||
getParagraphIds,
|
||||
locateEditorContainer,
|
||||
toViewCoord,
|
||||
} from '@affine-test/kit/utils/editor';
|
||||
import {
|
||||
copyByKeyboard,
|
||||
cutByKeyboard,
|
||||
pasteByKeyboard,
|
||||
pressEnter,
|
||||
} from '@affine-test/kit/utils/keyboard';
|
||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||
import {
|
||||
addCodeBlock,
|
||||
clickNewPageButton,
|
||||
type,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { setSelection } from '@affine-test/kit/utils/selection';
|
||||
import type { CodeBlockComponent } from '@blocksuite/affine-block-code';
|
||||
import type { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
|
||||
import type { PageRootBlockComponent } from '@blocksuite/affine-block-root';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
const paragraphLocator = 'affine-note affine-paragraph';
|
||||
const codeBlockLocator = 'affine-note affine-code';
|
||||
|
||||
// Helper function to create paragraph blocks with text
|
||||
async function createParagraphBlocks(page: Page, texts: string[]) {
|
||||
for (const text of texts) {
|
||||
await pressEnter(page);
|
||||
await type(page, text);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to verify block text content
|
||||
async function verifyBlockContent<T extends BlockComponent>(
|
||||
page: Page,
|
||||
selector: string,
|
||||
index: number,
|
||||
expectedText: string
|
||||
) {
|
||||
expect(
|
||||
await page
|
||||
.locator(selector)
|
||||
.nth(index)
|
||||
.evaluate((block: T, expected: string) => {
|
||||
const model = block.model;
|
||||
// Check if model has text property
|
||||
if (!('text' in model)) return false;
|
||||
const text = model.text;
|
||||
// Check if text exists and has toString method
|
||||
return text && text.toString() === expected;
|
||||
}, expectedText)
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
// Helper functions using the generic verifyBlockContent
|
||||
async function verifyParagraphContent(
|
||||
page: Page,
|
||||
index: number,
|
||||
expectedText: string
|
||||
) {
|
||||
await verifyBlockContent<ParagraphBlockComponent>(
|
||||
page,
|
||||
paragraphLocator,
|
||||
index,
|
||||
expectedText
|
||||
);
|
||||
}
|
||||
|
||||
async function verifyCodeBlockContent(
|
||||
page: Page,
|
||||
index: number,
|
||||
expectedText: string
|
||||
) {
|
||||
await verifyBlockContent<CodeBlockComponent>(
|
||||
page,
|
||||
codeBlockLocator,
|
||||
index,
|
||||
expectedText
|
||||
);
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await openHomePage(page);
|
||||
await clickNewPageButton(page, 'Clipboard Test');
|
||||
await waitForEditorLoad(page);
|
||||
});
|
||||
|
||||
test.describe('paste in multiple blocks text selection', () => {
|
||||
test('paste plain text', async ({ page }) => {
|
||||
const texts = ['hello world', 'hello world', 'hello world'];
|
||||
await createParagraphBlocks(page, texts);
|
||||
|
||||
const { blockIds: paragraphIds } = await getParagraphIds(page);
|
||||
|
||||
/**
|
||||
* select text cross the 3 blocks:
|
||||
* hello |world
|
||||
* hello world
|
||||
* hello| world
|
||||
*/
|
||||
await setSelection(page, paragraphIds[0], 6, paragraphIds[2], 5);
|
||||
|
||||
await pasteContent(page, { 'text/plain': 'test' });
|
||||
|
||||
/**
|
||||
* after paste:
|
||||
* hello test world
|
||||
*/
|
||||
await verifyParagraphContent(page, 0, 'hello test world');
|
||||
});
|
||||
|
||||
test('paste snapshot', async ({ page }) => {
|
||||
// Create initial test blocks
|
||||
await createParagraphBlocks(page, ['test', 'test']);
|
||||
|
||||
// Create target blocks
|
||||
await createParagraphBlocks(page, [
|
||||
'hello world',
|
||||
'hello world',
|
||||
'hello world',
|
||||
]);
|
||||
|
||||
/**
|
||||
* before paste:
|
||||
* test
|
||||
* test
|
||||
* hello world
|
||||
* hello world
|
||||
* hello world
|
||||
*/
|
||||
const { blockIds: paragraphIds } = await getParagraphIds(page);
|
||||
|
||||
/**
|
||||
* select the first 2 blocks:
|
||||
* |test
|
||||
* test|
|
||||
* hello world
|
||||
* hello world
|
||||
* hello world
|
||||
*/
|
||||
await setSelection(page, paragraphIds[0], 0, paragraphIds[1], 4);
|
||||
// copy the first 2 blocks
|
||||
await copyByKeyboard(page);
|
||||
|
||||
/**
|
||||
* select the last 3 blocks:
|
||||
* test
|
||||
* test
|
||||
* hello |world
|
||||
* hello world
|
||||
* hello| world
|
||||
*/
|
||||
await setSelection(page, paragraphIds[2], 6, paragraphIds[4], 5);
|
||||
|
||||
// paste the first 2 blocks
|
||||
await pasteByKeyboard(page);
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
/**
|
||||
* after paste:
|
||||
* test
|
||||
* test
|
||||
* hello test
|
||||
* test world
|
||||
*/
|
||||
await verifyParagraphContent(page, 2, 'hello test');
|
||||
await verifyParagraphContent(page, 3, 'test world');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('surface-ref block', () => {
|
||||
async function setupSurfaceRefBlock(page: Page) {
|
||||
await clickEdgelessModeButton(page);
|
||||
const container = locateEditorContainer(page);
|
||||
await container.click();
|
||||
|
||||
// add a shape
|
||||
await page.keyboard.press('s');
|
||||
await container.click({ position: { x: 100, y: 300 } });
|
||||
await page.waitForTimeout(50);
|
||||
|
||||
// add a frame
|
||||
await page.keyboard.press('f');
|
||||
await page.waitForTimeout(50);
|
||||
|
||||
// click on the frame title to trigger the change frame button toolbar
|
||||
const frameTitle = page.locator('affine-frame-title');
|
||||
await frameTitle.click();
|
||||
await page.waitForTimeout(50);
|
||||
|
||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
||||
const insertIntoPageButton = toolbar.getByLabel('Insert into Page');
|
||||
await insertIntoPageButton.click();
|
||||
|
||||
await clickPageModeButton(page);
|
||||
await waitForEditorLoad(page);
|
||||
await container.click();
|
||||
|
||||
return { container };
|
||||
}
|
||||
|
||||
test('paste surface-ref block to another doc as embed-linked-doc block', async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupSurfaceRefBlock(page);
|
||||
|
||||
// copy surface-ref block
|
||||
const surfaceRefBlock = page.locator('affine-surface-ref');
|
||||
await surfaceRefBlock.click();
|
||||
await page.waitForSelector('affine-surface-ref .focused');
|
||||
await copyByKeyboard(page);
|
||||
|
||||
// paste to another doc
|
||||
await clickNewPageButton(page, 'page2');
|
||||
await pressEnter(page);
|
||||
|
||||
// paste the surface-ref block
|
||||
await pasteByKeyboard(page);
|
||||
await page.waitForTimeout(50);
|
||||
|
||||
const embedLinkedDocBlock = page.locator('affine-embed-linked-doc-block');
|
||||
await expect(embedLinkedDocBlock).toBeVisible();
|
||||
const embedLinkedDocBlockTitle = embedLinkedDocBlock.locator(
|
||||
'.affine-embed-linked-doc-content-title-text'
|
||||
);
|
||||
await expect(embedLinkedDocBlockTitle).toHaveText('Clipboard Test');
|
||||
});
|
||||
|
||||
test('cut and paste surface-ref block to same doc should remain surface-ref block', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { container } = await setupSurfaceRefBlock(page);
|
||||
|
||||
// cut surface-ref block
|
||||
const surfaceRefBlock = page.locator('affine-surface-ref');
|
||||
await surfaceRefBlock.click();
|
||||
await page.waitForSelector('affine-surface-ref .focused');
|
||||
await cutByKeyboard(page);
|
||||
|
||||
// focus on the editor
|
||||
await container.click();
|
||||
|
||||
// paste the surface-ref block
|
||||
await pasteByKeyboard(page);
|
||||
await page.waitForTimeout(50);
|
||||
await expect(surfaceRefBlock).toHaveCount(1);
|
||||
await expect(surfaceRefBlock).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('paste to code block', () => {
|
||||
test('should replace the selected text when pasting plain text', async ({
|
||||
page,
|
||||
}) => {
|
||||
// press enter to focus on the first block of the editor
|
||||
await pressEnter(page);
|
||||
await addCodeBlock(page);
|
||||
await type(page, 'hello world hello');
|
||||
const { blockIds: codeBlockIds } = await getCodeBlockIds(page);
|
||||
await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11);
|
||||
await pasteContent(page, { 'text/plain': 'test' });
|
||||
await verifyCodeBlockContent(page, 0, 'hello test hello');
|
||||
});
|
||||
|
||||
test('should replace the selected text when pasting single snapshot', async ({
|
||||
page,
|
||||
}) => {
|
||||
// Create initial test blocks
|
||||
// add a paragraph block
|
||||
await createParagraphBlocks(page, ['test']);
|
||||
// add a code block
|
||||
await pressEnter(page);
|
||||
await addCodeBlock(page);
|
||||
await type(page, 'hello world hello');
|
||||
|
||||
// select the paragraph content
|
||||
const { blockIds: paragraphIds } = await getParagraphIds(page);
|
||||
await setSelection(page, paragraphIds[0], 0, paragraphIds[0], 4);
|
||||
// copy the paragraph content
|
||||
await copyByKeyboard(page);
|
||||
|
||||
// select 'world' in the code block
|
||||
const { blockIds: codeBlockIds } = await getCodeBlockIds(page);
|
||||
await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11);
|
||||
|
||||
// paste to the code block
|
||||
await pasteByKeyboard(page);
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
await verifyCodeBlockContent(page, 0, 'hello test hello');
|
||||
});
|
||||
|
||||
test('should replace the selected text when pasting multiple snapshots', async ({
|
||||
page,
|
||||
}) => {
|
||||
// Create initial test blocks
|
||||
// add three paragraph blocks
|
||||
await createParagraphBlocks(page, ['test', 'test', 'test']);
|
||||
|
||||
// add a code block
|
||||
await pressEnter(page);
|
||||
await addCodeBlock(page);
|
||||
await type(page, 'hello world hello');
|
||||
|
||||
// select all paragraph content
|
||||
const { blockIds: paragraphIds } = await getParagraphIds(page);
|
||||
await setSelection(page, paragraphIds[0], 0, paragraphIds[2], 4);
|
||||
// copy the paragraph content
|
||||
await copyByKeyboard(page);
|
||||
|
||||
// select 'world' in the code block
|
||||
const { blockIds: codeBlockIds } = await getCodeBlockIds(page);
|
||||
await setSelection(page, codeBlockIds[0], 6, codeBlockIds[0], 11);
|
||||
|
||||
// paste to the code block
|
||||
await pasteByKeyboard(page);
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
await verifyCodeBlockContent(page, 0, 'hello test\ntest\ntest hello');
|
||||
});
|
||||
|
||||
test('should preserve indentation when pasting code with spaces into code block', async ({
|
||||
page,
|
||||
}) => {
|
||||
await pressEnter(page);
|
||||
await addCodeBlock(page);
|
||||
|
||||
// Sample code with proper indentation for text/plain
|
||||
const plainTextCode = [
|
||||
'const fibonacci = (n: number): number => {',
|
||||
' if (n <= 1) return n;',
|
||||
' return fibonacci(n - 1) + fibonacci(n - 2);',
|
||||
'}',
|
||||
'function generateSequence(length: number) {',
|
||||
' const sequence = [];',
|
||||
' for (let i = 0; i < length; i++) {',
|
||||
' sequence.push(fibonacci(i));',
|
||||
' }',
|
||||
' return sequence;',
|
||||
'}',
|
||||
].join('\n');
|
||||
|
||||
const htmlCode =
|
||||
'<div><span>const</span><span> </span><span>fibonacci</span><span> </span><span>=</span><span> (n</span><span>:</span><span> </span><span>number</span><span>)</span><span>:</span><span> </span><span>number</span><span> </span><span>=></span><span> {</span></div><div><span> </span><span>if</span><span> (n </span><span><=</span><span> </span><span>1</span><span>) </span><span>return</span><span> n;</span></div><div><span> </span><span>return</span><span> </span><span>fibonacci</span><span>(n </span><span>-</span><span> </span><span>1</span><span>) </span><span>+</span><span> </span><span>fibonacci</span><span>(n </span><span>-</span><span> </span><span>2</span><span>);</span></div><div><span>}</span></div><div><span>function</span><span> </span><span>generateSequence</span><span>(length</span><span>:</span><span> </span><span>number</span><span>) {</span></div><div><span> </span><span>const</span><span> </span><span>sequence</span><span> </span><span>=</span><span> [];</span></div><div><span> </span><span>for</span><span> (</span><span>let</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span><</span><span> length; i</span><span>++</span><span>) {</span></div><div><span> sequence.</span><span>push</span><span>(</span><span>fibonacci</span><span>(i));</span></div><div><span> }</span></div><div><span> </span><span>return</span><span> sequence;</span></div><div><span>}</span></div></div>';
|
||||
|
||||
await pasteContent(page, {
|
||||
'text/plain': plainTextCode,
|
||||
'text/html': htmlCode,
|
||||
});
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Verify the pasted code maintains indentation
|
||||
await verifyCodeBlockContent(page, 0, plainTextCode);
|
||||
});
|
||||
|
||||
test('html tag should be treated as plain text when pasting', async ({
|
||||
page,
|
||||
}) => {
|
||||
await pressEnter(page);
|
||||
await addCodeBlock(page);
|
||||
|
||||
const textWithHtmlTags =
|
||||
'<div><span>const</span><span> </span><span>fibonacci</span><span> </span><span>=</span><span> (n</span><span>:</span><span> </span><span>number</span><span>)</span><span>:</span><span> </span><span>number</span><span> </span><span>=></span><span> {</span></div><div><span> </span><span>if</span><span> (n </span><span><=</span><span> </span><span>1</span><span>) </span><span>return</span><span> n;</span></div><div><span> </span><span>return</span><span> </span><span>fibonacci</span><span>(n </span><span>-</span><span> </span><span>1</span><span>) </span><span>+</span><span> </span><span>fibonacci</span><span>(n </span><span>-</span><span> </span><span>2</span><span>);</span></div><div><span>}</span></div><div><span>function</span><span> </span><span>generateSequence</span><span>(length</span><span>:</span><span> </span><span>number</span><span>) {</span></div><div><span> </span><span>const</span><span> </span><span>sequence</span><span> </span><span>=</span><span> [];</span></div><div><span> </span><span>for</span><span> (</span><span>let</span><span> i </span><span>=</span><span> </span><span>0</span><span>; i </span><span><</span><span> length; i</span><span>++</span><span>) {</span></div><div><span> sequence.</span><span>push</span><span>(</span><span>fibonacci</span><span>(i));</span></div><div><span> }</span></div><div><span> </span><span>return</span><span> sequence;</span></div><div><span>}</span></div></div>';
|
||||
|
||||
await pasteContent(page, { 'text/plain': textWithHtmlTags });
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Verify the pasted code maintains indentation
|
||||
await verifyCodeBlockContent(page, 0, textWithHtmlTags);
|
||||
});
|
||||
|
||||
test('should not wrap line in brackets when pasting code', async ({
|
||||
page,
|
||||
}) => {
|
||||
await pressEnter(page);
|
||||
await addCodeBlock(page);
|
||||
const plainTextCode = [
|
||||
' model: anthropic("claude-sonnet-4-5-20250929"),',
|
||||
' prompt: How many people will live in the world in 2040?',
|
||||
' providerOptions: {',
|
||||
' anthropic: {',
|
||||
' thinking: { type: enabled, budgetTokens: 12000 },',
|
||||
' } satisfies AnthropicProviderOptions,',
|
||||
' },',
|
||||
].join('\n');
|
||||
|
||||
await pasteContent(page, { 'text/plain': plainTextCode });
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Verify the pasted code maintains indentation
|
||||
await verifyCodeBlockContent(page, 0, plainTextCode);
|
||||
});
|
||||
|
||||
test('should paste markdown text as plain text', async ({ page }) => {
|
||||
await pressEnter(page);
|
||||
await addCodeBlock(page);
|
||||
|
||||
const markdownText = [
|
||||
'# Heading 1',
|
||||
'',
|
||||
'## Heading 2 with **bold** and *italic*',
|
||||
'',
|
||||
'### Lists:',
|
||||
'- Item 1',
|
||||
' - Nested item with `inline code`',
|
||||
' - Another nested item',
|
||||
'- Item 2 with [link](https://example.com)',
|
||||
'',
|
||||
'```typescript',
|
||||
'const code = "block";',
|
||||
'console.log(code);',
|
||||
'```',
|
||||
'',
|
||||
'> This is a blockquote with **bold** text',
|
||||
'> Multiple lines in blockquote',
|
||||
'',
|
||||
'| Table | Header |',
|
||||
'|-------|--------|',
|
||||
'| Cell 1 | Cell 2 |',
|
||||
'$This is a inline latex$',
|
||||
].join('\n');
|
||||
|
||||
await pasteContent(page, { 'text/plain': markdownText });
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Verify the pasted code maintains indentation
|
||||
await verifyCodeBlockContent(page, 0, markdownText);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('paste in readonly mode', () => {
|
||||
test('should not paste content when document is in readonly mode', async ({
|
||||
page,
|
||||
}) => {
|
||||
await createParagraphBlocks(page, ['This is a test paragraph']);
|
||||
const { blockIds } = await getParagraphIds(page);
|
||||
const initialParagraphCount = blockIds.length;
|
||||
|
||||
await page.evaluate(() => {
|
||||
const pageRoot = document.querySelector(
|
||||
'affine-page-root'
|
||||
) as PageRootBlockComponent;
|
||||
pageRoot.store.readonly = true;
|
||||
});
|
||||
|
||||
await setSelection(page, blockIds[0], 0, blockIds[0], 4);
|
||||
await pasteContent(page, {
|
||||
'text/plain': ' - Added text that should not appear',
|
||||
});
|
||||
|
||||
await verifyParagraphContent(page, 0, 'This is a test paragraph');
|
||||
|
||||
await pressEnter(page);
|
||||
await pasteContent(page, { 'text/plain': 'This should not be pasted' });
|
||||
|
||||
const { blockIds: afterParagraphIds } = await getParagraphIds(page);
|
||||
expect(afterParagraphIds.length).toBe(initialParagraphCount);
|
||||
|
||||
await setSelection(page, blockIds[0], 0, blockIds[0], 4);
|
||||
await pasteByKeyboard(page);
|
||||
|
||||
await verifyParagraphContent(page, 0, 'This is a test paragraph');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('cross document clipboard regression', () => {
|
||||
test('copy and paste paragraph content between docs', async ({ page }) => {
|
||||
const container = locateEditorContainer(page);
|
||||
await container.click();
|
||||
|
||||
const sourceText = "Cross-doc paste can't fail again";
|
||||
await type(page, sourceText);
|
||||
|
||||
const { blockIds } = await getParagraphIds(page);
|
||||
await setSelection(page, blockIds[0], 0, blockIds[0], sourceText.length);
|
||||
|
||||
await copyByKeyboard(page);
|
||||
|
||||
await clickNewPageButton(page, 'Clipboard Destination');
|
||||
await waitForEditorLoad(page);
|
||||
|
||||
const destination = locateEditorContainer(page);
|
||||
await destination.click();
|
||||
|
||||
await pasteByKeyboard(page);
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
const pastedTexts = await page.locator(paragraphLocator).allTextContents();
|
||||
expect(pastedTexts.some(text => text.includes(sourceText))).toBe(true);
|
||||
});
|
||||
|
||||
test('copied content remains available to external clipboard consumers', async ({
|
||||
page,
|
||||
}) => {
|
||||
const container = locateEditorContainer(page);
|
||||
await container.click();
|
||||
|
||||
const textForExternal = 'External clipboard visibility check';
|
||||
await type(page, textForExternal);
|
||||
|
||||
const { blockIds } = await getParagraphIds(page);
|
||||
await setSelection(
|
||||
page,
|
||||
blockIds[0],
|
||||
0,
|
||||
blockIds[0],
|
||||
textForExternal.length
|
||||
);
|
||||
|
||||
await copyByKeyboard(page);
|
||||
|
||||
const plainText = await page.evaluate(() => navigator.clipboard.readText());
|
||||
|
||||
expect(plainText).toBe(textForExternal);
|
||||
});
|
||||
|
||||
test('copy and paste within a single document still duplicates content', async ({
|
||||
page,
|
||||
}) => {
|
||||
const container = locateEditorContainer(page);
|
||||
await container.click();
|
||||
|
||||
const intraDocText = 'Same doc paste regression guard';
|
||||
await type(page, intraDocText);
|
||||
|
||||
const { blockIds } = await getParagraphIds(page);
|
||||
await setSelection(page, blockIds[0], 0, blockIds[0], intraDocText.length);
|
||||
|
||||
await copyByKeyboard(page);
|
||||
|
||||
await pressEnter(page);
|
||||
await pasteByKeyboard(page);
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
await verifyParagraphContent(page, 1, intraDocText);
|
||||
});
|
||||
});
|
||||
|
||||
test('should copy single image from edgeless and paste to page', async ({
|
||||
page,
|
||||
}) => {
|
||||
await clickEdgelessModeButton(page);
|
||||
|
||||
const button = page.locator('edgeless-mindmap-tool-button');
|
||||
await button.click();
|
||||
|
||||
const menu = page.locator('edgeless-mindmap-menu');
|
||||
const mediaItem = menu.locator('.media-item');
|
||||
await mediaItem.click();
|
||||
|
||||
await importFile(page, 'large-image.png', async () => {
|
||||
await toViewCoord(page, [100, 250]);
|
||||
await clickView(page, [100, 250]);
|
||||
});
|
||||
|
||||
const image = page.locator('affine-edgeless-image').first();
|
||||
await image.click();
|
||||
|
||||
await copyByKeyboard(page);
|
||||
|
||||
await clickPageModeButton(page);
|
||||
await waitForEditorLoad(page);
|
||||
|
||||
const container = locateEditorContainer(page);
|
||||
await container.click();
|
||||
|
||||
await pasteByKeyboard(page);
|
||||
|
||||
await expect(page.locator('affine-page-image')).toBeVisible();
|
||||
});
|
||||
115
tests/affine-local/e2e/blocksuite/code/crud.spec.ts
Normal file
115
tests/affine-local/e2e/blocksuite/code/crud.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
192
tests/affine-local/e2e/blocksuite/code/line-numbers.spec.ts
Normal file
192
tests/affine-local/e2e/blocksuite/code/line-numbers.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
46
tests/affine-local/e2e/blocksuite/code/utils.ts
Normal file
46
tests/affine-local/e2e/blocksuite/code/utils.ts
Normal 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 };
|
||||
};
|
||||
111
tests/affine-local/e2e/blocksuite/color-picker.spec.ts
Normal file
111
tests/affine-local/e2e/blocksuite/color-picker.spec.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
69
tests/affine-local/e2e/blocksuite/database/clipboard.spec.ts
Normal file
69
tests/affine-local/e2e/blocksuite/database/clipboard.spec.ts
Normal 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']);
|
||||
});
|
||||
});
|
||||
99
tests/affine-local/e2e/blocksuite/database/rich-text.spec.ts
Normal file
99
tests/affine-local/e2e/blocksuite/database/rich-text.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
122
tests/affine-local/e2e/blocksuite/database/utils.ts
Normal file
122
tests/affine-local/e2e/blocksuite/database/utils.ts
Normal 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);
|
||||
};
|
||||
100
tests/affine-local/e2e/blocksuite/edgeless/auto-complete.spec.ts
Normal file
100
tests/affine-local/e2e/blocksuite/edgeless/auto-complete.spec.ts
Normal 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();
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
65
tests/affine-local/e2e/blocksuite/edgeless/connector.spec.ts
Normal file
65
tests/affine-local/e2e/blocksuite/edgeless/connector.spec.ts
Normal 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');
|
||||
});
|
||||
60
tests/affine-local/e2e/blocksuite/edgeless/embed.spec.ts
Normal file
60
tests/affine-local/e2e/blocksuite/edgeless/embed.spec.ts
Normal 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);
|
||||
});
|
||||
64
tests/affine-local/e2e/blocksuite/edgeless/frame.spec.ts
Normal file
64
tests/affine-local/e2e/blocksuite/edgeless/frame.spec.ts
Normal 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);
|
||||
});
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
535
tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts
Normal file
535
tests/affine-local/e2e/blocksuite/edgeless/note.spec.ts
Normal 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?');
|
||||
});
|
||||
110
tests/affine-local/e2e/blocksuite/edgeless/shape.spec.ts
Normal file
110
tests/affine-local/e2e/blocksuite/edgeless/shape.spec.ts
Normal 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');
|
||||
});
|
||||
102
tests/affine-local/e2e/blocksuite/edgeless/text.spec.ts
Normal file
102
tests/affine-local/e2e/blocksuite/edgeless/text.spec.ts
Normal 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');
|
||||
});
|
||||
99
tests/affine-local/e2e/blocksuite/editor.spec.ts
Normal file
99
tests/affine-local/e2e/blocksuite/editor.spec.ts
Normal 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);
|
||||
});
|
||||
263
tests/affine-local/e2e/blocksuite/embed/embed-iframe.spec.ts
Normal file
263
tests/affine-local/e2e/blocksuite/embed/embed-iframe.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
170
tests/affine-local/e2e/blocksuite/embed/synced.spec.ts
Normal file
170
tests/affine-local/e2e/blocksuite/embed/synced.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
119
tests/affine-local/e2e/blocksuite/list.spec.ts
Normal file
119
tests/affine-local/e2e/blocksuite/list.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
565
tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts
Normal file
565
tests/affine-local/e2e/blocksuite/outline/outline-panel.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
279
tests/affine-local/e2e/blocksuite/outline/outline-viewer.spec.ts
Normal file
279
tests/affine-local/e2e/blocksuite/outline/outline-viewer.spec.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
34
tests/affine-local/e2e/blocksuite/outline/utils.ts
Normal file
34
tests/affine-local/e2e/blocksuite/outline/utils.ts
Normal 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;
|
||||
}
|
||||
460
tests/affine-local/e2e/blocksuite/paragraph.spec.ts
Normal file
460
tests/affine-local/e2e/blocksuite/paragraph.spec.ts
Normal 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);
|
||||
});
|
||||
126
tests/affine-local/e2e/blocksuite/table/collab.spec.ts
Normal file
126
tests/affine-local/e2e/blocksuite/table/collab.spec.ts
Normal 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');
|
||||
});
|
||||
200
tests/affine-local/e2e/blocksuite/table/edge-insertion.spec.ts
Normal file
200
tests/affine-local/e2e/blocksuite/table/edge-insertion.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
198
tests/affine-local/e2e/blocksuite/table/insertion.spec.ts
Normal file
198
tests/affine-local/e2e/blocksuite/table/insertion.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
458
tests/affine-local/e2e/blocksuite/toolbar.spec.ts
Normal file
458
tests/affine-local/e2e/blocksuite/toolbar.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user