mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-10 21:48:48 +08:00
fe5f0f62ec
This PR performs a significant architectural refactoring by extracting rich text functionality into a dedicated package. Here are the key changes: 1. **New Package Creation** - Created a new package `@blocksuite/affine-rich-text` to house rich text related functionality - Moved rich text components, utilities, and types from `@blocksuite/affine-components` to this new package 2. **Dependency Updates** - Updated multiple block packages to include the new `@blocksuite/affine-rich-text` as a direct dependency: - block-callout - block-code - block-database - block-edgeless-text - block-embed - block-list - block-note - block-paragraph 3. **Import Path Updates** - Refactored all imports that previously referenced rich text functionality from `@blocksuite/affine-components/rich-text` to now use `@blocksuite/affine-rich-text` - Updated imports for components like: - DefaultInlineManagerExtension - RichText types and interfaces - Text manipulation utilities (focusTextModel, textKeymap, etc.) - Reference node components and providers 4. **Build Configuration Updates** - Added references to the new rich text package in the `tsconfig.json` files of all affected packages - Maintained workspace dependencies using the `workspace:*` version specifier The primary motivation appears to be: 1. Better separation of concerns by isolating rich text functionality 2. Improved maintainability through more modular package structure 3. Clearer dependencies between packages 4. Potential for better tree-shaking and bundle optimization This is primarily an architectural improvement that should make the codebase more maintainable and better organized.
599 lines
17 KiB
TypeScript
599 lines
17 KiB
TypeScript
import type { EdgelessTextBlockComponent } from '@blocksuite/blocks';
|
|
import { Bound } from '@blocksuite/global/gfx';
|
|
import { expect, type Page } from '@playwright/test';
|
|
|
|
import {
|
|
autoFit,
|
|
captureHistory,
|
|
cutByKeyboard,
|
|
dragBetweenIndices,
|
|
enterPlaygroundRoom,
|
|
getEdgelessSelectedRect,
|
|
getPageSnapshot,
|
|
initEmptyEdgelessState,
|
|
pasteByKeyboard,
|
|
pressArrowDown,
|
|
pressArrowLeft,
|
|
pressArrowRight,
|
|
pressArrowUp,
|
|
pressBackspace,
|
|
pressEnter,
|
|
pressEscape,
|
|
selectAllByKeyboard,
|
|
setEdgelessTool,
|
|
switchEditorMode,
|
|
toViewCoord,
|
|
type,
|
|
undoByKeyboard,
|
|
waitNextFrame,
|
|
} from '../utils/actions/index.js';
|
|
import {
|
|
assertBlockChildrenIds,
|
|
assertBlockFlavour,
|
|
assertBlockTextContent,
|
|
assertRichTextInlineDeltas,
|
|
assertRichTextInlineRange,
|
|
} from '../utils/asserts.js';
|
|
import { test } from '../utils/playwright.js';
|
|
import { getFormatBar } from '../utils/query.js';
|
|
|
|
async function assertEdgelessTextModelRect(
|
|
page: Page,
|
|
id: string,
|
|
bound: Bound
|
|
) {
|
|
const realXYWH = await page.evaluate(id => {
|
|
const block = window.host.view.getBlock(id) as EdgelessTextBlockComponent;
|
|
return block?.model.xywh;
|
|
}, id);
|
|
const realBound = Bound.deserialize(realXYWH);
|
|
expect(realBound.x).toBeCloseTo(bound.x, 0);
|
|
expect(realBound.y).toBeCloseTo(bound.y, 0);
|
|
expect(realBound.w).toBeCloseTo(bound.w, 0);
|
|
expect(realBound.h).toBeCloseTo(bound.h, 0);
|
|
}
|
|
|
|
test.describe('edgeless text block', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await enterPlaygroundRoom(page);
|
|
await initEmptyEdgelessState(page);
|
|
await switchEditorMode(page);
|
|
});
|
|
|
|
test('add text block in default mode', async ({ page }) => {
|
|
await setEdgelessTool(page, 'default');
|
|
await page.mouse.dblclick(130, 140, {
|
|
delay: 100,
|
|
});
|
|
await waitNextFrame(page);
|
|
|
|
// https://github.com/toeverything/blocksuite/pull/8574
|
|
await pressBackspace(page);
|
|
|
|
await type(page, 'aaa');
|
|
await pressEnter(page);
|
|
await type(page, 'bbb');
|
|
await pressEnter(page);
|
|
await type(page, 'ccc');
|
|
|
|
await assertBlockFlavour(page, 4, 'affine:edgeless-text');
|
|
await assertBlockFlavour(page, 5, 'affine:paragraph');
|
|
await assertBlockFlavour(page, 6, 'affine:paragraph');
|
|
await assertBlockFlavour(page, 7, 'affine:paragraph');
|
|
await assertBlockChildrenIds(page, '4', ['5', '6', '7']);
|
|
await assertBlockTextContent(page, 5, 'aaa');
|
|
await assertBlockTextContent(page, 6, 'bbb');
|
|
await assertBlockTextContent(page, 7, 'ccc');
|
|
|
|
await dragBetweenIndices(page, [1, 1], [3, 2]);
|
|
await captureHistory(page);
|
|
await pressBackspace(page);
|
|
await assertBlockChildrenIds(page, '4', ['5']);
|
|
await assertBlockTextContent(page, 5, 'ac');
|
|
|
|
await undoByKeyboard(page);
|
|
await assertBlockChildrenIds(page, '4', ['5', '6', '7']);
|
|
await assertBlockTextContent(page, 5, 'aaa');
|
|
await assertBlockTextContent(page, 6, 'bbb');
|
|
await assertBlockTextContent(page, 7, 'ccc');
|
|
|
|
const { boldBtn } = getFormatBar(page);
|
|
await boldBtn.click();
|
|
await assertRichTextInlineDeltas(
|
|
page,
|
|
[
|
|
{
|
|
insert: 'a',
|
|
},
|
|
{
|
|
insert: 'aa',
|
|
attributes: {
|
|
bold: true,
|
|
},
|
|
},
|
|
],
|
|
1
|
|
);
|
|
await assertRichTextInlineDeltas(
|
|
page,
|
|
[
|
|
{
|
|
insert: 'bbb',
|
|
attributes: {
|
|
bold: true,
|
|
},
|
|
},
|
|
],
|
|
2
|
|
);
|
|
await assertRichTextInlineDeltas(
|
|
page,
|
|
[
|
|
{
|
|
insert: 'cc',
|
|
attributes: {
|
|
bold: true,
|
|
},
|
|
},
|
|
{
|
|
insert: 'c',
|
|
},
|
|
],
|
|
3
|
|
);
|
|
|
|
await pressArrowRight(page);
|
|
await assertRichTextInlineRange(page, 3, 2);
|
|
await pressArrowUp(page);
|
|
await assertRichTextInlineRange(page, 2, 2);
|
|
});
|
|
|
|
test('edgeless text width auto-adjusting', async ({ page }) => {
|
|
await setEdgelessTool(page, 'default');
|
|
const point = await toViewCoord(page, [0, 0]);
|
|
await page.mouse.dblclick(point[0], point[1], {
|
|
delay: 100,
|
|
});
|
|
await waitNextFrame(page);
|
|
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 220, 26));
|
|
|
|
await type(page, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
|
|
await waitNextFrame(page, 1000);
|
|
// just width changed
|
|
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 323, 26));
|
|
|
|
await type(page, '\nbbb');
|
|
// width not changed, height changed
|
|
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 323, 50));
|
|
await type(page, '\nccccccccccccccccccccccccccccccccccccccccccccccccc');
|
|
await waitNextFrame(page, 1000);
|
|
|
|
// width and height changed
|
|
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 395, 74));
|
|
|
|
// blur, max width set to true
|
|
await page.mouse.click(point[0] - 50, point[1], {
|
|
delay: 100,
|
|
});
|
|
await page.mouse.dblclick(point[0], point[1], {
|
|
delay: 100,
|
|
});
|
|
// to end of line
|
|
await pressArrowDown(page, 3);
|
|
await type(page, 'dddddddddddddddddddd');
|
|
await waitNextFrame(page, 1000);
|
|
// width not changed, height changed
|
|
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 395, 98));
|
|
});
|
|
|
|
test('edgeless text width fixed when drag moving', async ({ page }) => {
|
|
// https://github.com/toeverything/blocksuite/pull/7486
|
|
|
|
await setEdgelessTool(page, 'default');
|
|
await page.mouse.dblclick(130, 140, {
|
|
delay: 100,
|
|
});
|
|
await waitNextFrame(page);
|
|
await type(page, 'aaaaaa bbbb ');
|
|
await pressEscape(page);
|
|
await waitNextFrame(page);
|
|
await page.mouse.click(130, 140);
|
|
await page.mouse.down();
|
|
await page.mouse.move(800, 800, {
|
|
steps: 15,
|
|
});
|
|
|
|
const rect = await page.evaluate(() => {
|
|
const container = document.querySelector(
|
|
'.edgeless-text-block-container'
|
|
)!;
|
|
return container.getBoundingClientRect();
|
|
});
|
|
const modelXYWH = await page.evaluate(() => {
|
|
const block = window.host.view.getBlock(
|
|
'4'
|
|
) as EdgelessTextBlockComponent;
|
|
return block.model.xywh;
|
|
});
|
|
const bound = Bound.deserialize(modelXYWH);
|
|
expect(rect.width).toBeCloseTo(bound.w);
|
|
expect(rect.height).toBeCloseTo(bound.h);
|
|
});
|
|
|
|
test('When creating edgeless text, if the input is empty, it will be automatically deleted', async ({
|
|
page,
|
|
}) => {
|
|
await setEdgelessTool(page, 'default');
|
|
await page.mouse.dblclick(130, 140, {
|
|
delay: 100,
|
|
});
|
|
await waitNextFrame(page);
|
|
let block = page.locator('affine-edgeless-text[data-block-id="4"]');
|
|
expect(await block.isVisible()).toBe(true);
|
|
await page.mouse.click(0, 0);
|
|
expect(await block.isVisible()).toBe(false);
|
|
|
|
block = page.locator('affine-edgeless-text[data-block-id="6"]');
|
|
expect(await block.isVisible()).not.toBe(true);
|
|
await page.mouse.dblclick(130, 140, {
|
|
delay: 100,
|
|
});
|
|
expect(await block.isVisible()).toBe(true);
|
|
await type(page, '\na');
|
|
expect(await block.isVisible()).toBe(true);
|
|
await page.mouse.click(0, 0);
|
|
expect(await block.isVisible()).not.toBe(false);
|
|
});
|
|
|
|
test('edgeless text should maintain selection when deleting across multiple lines', async ({
|
|
page,
|
|
}) => {
|
|
// https://github.com/toeverything/blocksuite/pull/7443
|
|
|
|
await setEdgelessTool(page, 'default');
|
|
await page.mouse.dblclick(130, 140, {
|
|
delay: 100,
|
|
});
|
|
await waitNextFrame(page);
|
|
await type(page, 'aaaa\nbbbb');
|
|
await assertBlockTextContent(page, 5, 'aaaa');
|
|
await assertBlockTextContent(page, 6, 'bbbb');
|
|
|
|
await pressArrowLeft(page);
|
|
await page.keyboard.down('Shift');
|
|
await pressArrowLeft(page, 3);
|
|
await pressArrowUp(page);
|
|
await pressArrowRight(page);
|
|
await page.keyboard.up('Shift');
|
|
await pressBackspace(page);
|
|
await assertBlockTextContent(page, 5, 'ab');
|
|
await type(page, 'sss\n');
|
|
await assertBlockTextContent(page, 5, 'asss');
|
|
await assertBlockTextContent(page, 7, 'b');
|
|
});
|
|
|
|
test('edgeless text should not blur after pressing backspace', async ({
|
|
page,
|
|
}) => {
|
|
// https://github.com/toeverything/blocksuite/pull/7555
|
|
|
|
await setEdgelessTool(page, 'default');
|
|
await page.mouse.dblclick(130, 140, {
|
|
delay: 100,
|
|
});
|
|
await waitNextFrame(page);
|
|
await type(page, 'a');
|
|
await assertBlockTextContent(page, 5, 'a');
|
|
await pressBackspace(page);
|
|
await type(page, 'b');
|
|
await assertBlockTextContent(page, 5, 'b');
|
|
});
|
|
|
|
// FIXME(@flrande): This test fails randomly on CI
|
|
test.fixme('edgeless text max width', async ({ page }) => {
|
|
await setEdgelessTool(page, 'default');
|
|
const point = await toViewCoord(page, [0, 0]);
|
|
await page.mouse.dblclick(point[0], point[1], {
|
|
delay: 100,
|
|
});
|
|
await waitNextFrame(page);
|
|
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 50, 56));
|
|
|
|
await type(page, 'aaaaaa');
|
|
await waitNextFrame(page);
|
|
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 71, 56));
|
|
await type(page, 'bbb');
|
|
await waitNextFrame(page, 200);
|
|
// height not changed
|
|
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 98, 56));
|
|
|
|
// blur
|
|
await page.mouse.click(0, 0);
|
|
// select text element
|
|
await page.mouse.click(point[0] + 10, point[1] + 10);
|
|
|
|
let selectedRect = await getEdgelessSelectedRect(page);
|
|
|
|
// move cursor to the right edge and drag it to resize the width of text
|
|
|
|
// from left to right
|
|
await page.mouse.move(
|
|
selectedRect.x + selectedRect.width,
|
|
selectedRect.y + selectedRect.height / 2
|
|
);
|
|
await page.mouse.down();
|
|
await page.mouse.move(
|
|
selectedRect.x + selectedRect.width + 30,
|
|
selectedRect.y + selectedRect.height / 2,
|
|
{
|
|
steps: 10,
|
|
}
|
|
);
|
|
await page.mouse.up();
|
|
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 128, 56));
|
|
selectedRect = await getEdgelessSelectedRect(page);
|
|
let textRect = await page
|
|
.locator('affine-edgeless-text[data-block-id="4"]')
|
|
.boundingBox();
|
|
expect(selectedRect).not.toBeNull();
|
|
expect(selectedRect.width).toBeCloseTo(textRect!.width);
|
|
expect(selectedRect.height).toBeCloseTo(textRect!.height);
|
|
expect(selectedRect.x).toBeCloseTo(textRect!.x);
|
|
expect(selectedRect.y).toBeCloseTo(textRect!.y);
|
|
|
|
// from right to left
|
|
await page.mouse.move(
|
|
selectedRect.x + selectedRect.width,
|
|
selectedRect.y + selectedRect.height / 2
|
|
);
|
|
await page.mouse.down();
|
|
await page.mouse.move(
|
|
selectedRect.x + selectedRect.width - 45,
|
|
selectedRect.y + selectedRect.height / 2,
|
|
{
|
|
steps: 10,
|
|
}
|
|
);
|
|
await page.mouse.up();
|
|
// height changed
|
|
await assertEdgelessTextModelRect(page, '4', new Bound(-25, -25, 83, 80));
|
|
selectedRect = await getEdgelessSelectedRect(page);
|
|
textRect = await page
|
|
.locator('affine-edgeless-text[data-block-id="4"]')
|
|
.boundingBox();
|
|
expect(selectedRect).not.toBeNull();
|
|
expect(selectedRect.width).toBeCloseTo(textRect!.width);
|
|
expect(selectedRect.height).toBeCloseTo(textRect!.height);
|
|
expect(selectedRect.x).toBeCloseTo(textRect!.x);
|
|
expect(selectedRect.y).toBeCloseTo(textRect!.y);
|
|
});
|
|
|
|
test('min width limit for embed block', async ({ page }, testInfo) => {
|
|
await setEdgelessTool(page, 'default');
|
|
const point = await toViewCoord(page, [0, 0]);
|
|
await page.mouse.dblclick(point[0], point[1], {
|
|
delay: 100,
|
|
});
|
|
await waitNextFrame(page, 2000);
|
|
|
|
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
|
|
`${testInfo.title}_init.json`
|
|
);
|
|
|
|
await type(page, '@');
|
|
await pressEnter(page);
|
|
await waitNextFrame(page, 200);
|
|
|
|
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
|
|
`${testInfo.title}_add_linked_doc.json`
|
|
);
|
|
|
|
await page.locator('affine-reference').hover();
|
|
await page.getByLabel('Switch view').click();
|
|
await page.getByTestId('link-to-card').click();
|
|
await autoFit(page);
|
|
|
|
await waitNextFrame(page);
|
|
|
|
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
|
|
`${testInfo.title}_link_to_card.json`
|
|
);
|
|
|
|
// blur
|
|
await page.mouse.click(0, 0);
|
|
// select text element
|
|
await page.mouse.click(point[0] + 10, point[1] + 10);
|
|
await waitNextFrame(page, 200);
|
|
const selectedRect0 = await getEdgelessSelectedRect(page);
|
|
|
|
// from right to left
|
|
await page.mouse.move(
|
|
selectedRect0.x + selectedRect0.width,
|
|
selectedRect0.y + selectedRect0.height / 2
|
|
);
|
|
await page.mouse.down();
|
|
await page.mouse.move(
|
|
selectedRect0.x,
|
|
selectedRect0.y + selectedRect0.height / 2,
|
|
{
|
|
steps: 10,
|
|
}
|
|
);
|
|
await page.mouse.up();
|
|
|
|
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
|
|
`${testInfo.title}_link_to_card_min_width.json`
|
|
);
|
|
|
|
const selectedRect1 = await getEdgelessSelectedRect(page);
|
|
// from left to right
|
|
await page.mouse.move(
|
|
selectedRect1.x + selectedRect1.width,
|
|
selectedRect1.y + selectedRect1.height / 2
|
|
);
|
|
await page.mouse.down();
|
|
await page.mouse.move(
|
|
selectedRect0.x + selectedRect0.width + 45,
|
|
selectedRect1.y + selectedRect1.height / 2,
|
|
{
|
|
steps: 10,
|
|
}
|
|
);
|
|
await page.mouse.up();
|
|
|
|
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
|
|
`${testInfo.title}_drag.json`
|
|
);
|
|
});
|
|
|
|
test('cut edgeless text', async ({ page }) => {
|
|
await setEdgelessTool(page, 'default');
|
|
await page.mouse.dblclick(130, 140, {
|
|
delay: 100,
|
|
});
|
|
await waitNextFrame(page);
|
|
await type(page, 'aaaa\nbbbb\ncccc');
|
|
|
|
const edgelessText = page.locator('affine-edgeless-text');
|
|
const paragraph = page.locator('affine-edgeless-text affine-paragraph');
|
|
|
|
expect(await edgelessText.count()).toBe(1);
|
|
expect(await paragraph.count()).toBe(3);
|
|
|
|
await page.mouse.click(50, 50, {
|
|
delay: 100,
|
|
});
|
|
await waitNextFrame(page);
|
|
await page.mouse.click(130, 140, {
|
|
delay: 100,
|
|
});
|
|
await cutByKeyboard(page);
|
|
expect(await edgelessText.count()).toBe(0);
|
|
expect(await paragraph.count()).toBe(0);
|
|
|
|
await pasteByKeyboard(page);
|
|
expect(await edgelessText.count()).toBe(1);
|
|
expect(await paragraph.count()).toBe(3);
|
|
});
|
|
|
|
test('latex in edgeless text', async ({ page }) => {
|
|
await setEdgelessTool(page, 'default');
|
|
await page.mouse.dblclick(130, 140, {
|
|
delay: 100,
|
|
});
|
|
await waitNextFrame(page);
|
|
await type(page, '$$bbb$$ ');
|
|
await assertRichTextInlineDeltas(
|
|
page,
|
|
[
|
|
{
|
|
insert: ' ',
|
|
attributes: {
|
|
latex: 'bbb',
|
|
},
|
|
},
|
|
],
|
|
1
|
|
);
|
|
|
|
await page.locator('affine-latex-node').click();
|
|
await waitNextFrame(page);
|
|
await type(page, 'ccc');
|
|
const menu = page.locator('latex-editor-menu');
|
|
const confirm = menu.locator('.latex-editor-confirm');
|
|
await confirm.click();
|
|
await assertRichTextInlineDeltas(
|
|
page,
|
|
[
|
|
{
|
|
insert: ' ',
|
|
attributes: {
|
|
latex: 'bbbccc',
|
|
},
|
|
},
|
|
],
|
|
1
|
|
);
|
|
|
|
await page.locator('affine-latex-node').click();
|
|
await page.locator('.latex-editor-hint').click();
|
|
await type(page, 'sss');
|
|
await assertRichTextInlineDeltas(
|
|
page,
|
|
[
|
|
{
|
|
insert: ' ',
|
|
attributes: {
|
|
latex: 'bbbccc',
|
|
},
|
|
},
|
|
],
|
|
1
|
|
);
|
|
await page.locator('latex-editor-unit').click();
|
|
await selectAllByKeyboard(page);
|
|
await type(page, 'sss');
|
|
await confirm.click();
|
|
await assertRichTextInlineDeltas(
|
|
page,
|
|
[
|
|
{
|
|
insert: ' ',
|
|
attributes: {
|
|
latex: 'sss',
|
|
},
|
|
},
|
|
],
|
|
1
|
|
);
|
|
});
|
|
});
|
|
|
|
test('press backspace at the start of first line when edgeless text exist', async ({
|
|
page,
|
|
}, testInfo) => {
|
|
await enterPlaygroundRoom(page);
|
|
await page.evaluate(() => {
|
|
const { doc } = window;
|
|
const rootId = doc.addBlock('affine:page', {
|
|
title: new window.$blocksuite.store.Text(),
|
|
});
|
|
doc.addBlock('affine:surface', {}, rootId);
|
|
doc.addBlock('affine:note', {}, rootId);
|
|
|
|
// do not add paragraph block
|
|
|
|
doc.resetHistory();
|
|
});
|
|
await switchEditorMode(page);
|
|
|
|
await setEdgelessTool(page, 'default');
|
|
const point = await toViewCoord(page, [0, 0]);
|
|
await page.mouse.dblclick(point[0], point[1], {
|
|
delay: 100,
|
|
});
|
|
await waitNextFrame(page, 2000);
|
|
await type(page, 'aaa');
|
|
|
|
await waitNextFrame(page);
|
|
await switchEditorMode(page);
|
|
|
|
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
|
|
`${testInfo.title}_note_empty.json`
|
|
);
|
|
|
|
await page.locator('.affine-page-root-block-container').click();
|
|
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
|
|
`${testInfo.title}_note_not_empty.json`
|
|
);
|
|
|
|
await type(page, 'bbb');
|
|
await pressArrowLeft(page, 3);
|
|
await pressBackspace(page);
|
|
await waitNextFrame(page);
|
|
|
|
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
|
|
`${testInfo.title}_finial.json`
|
|
);
|
|
});
|