refactor(editor): rename presets to integration test (#10340)
3
blocksuite/integration-test/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `@blocksuite/integration-test`
|
||||
|
||||
Integration test for BlockSuite.
|
||||
56
blocksuite/integration-test/package.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@blocksuite/integration-test",
|
||||
"description": "Integration test for BlockSuite",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc",
|
||||
"test:unit": "vitest --browser.headless --run",
|
||||
"test:debug": "PWDEBUG=1 npx vitest"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"keywords": [],
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blocksuite/affine-block-note": "workspace:*",
|
||||
"@blocksuite/affine-block-surface": "workspace:*",
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/affine-shared": "workspace:*",
|
||||
"@blocksuite/block-std": "workspace:*",
|
||||
"@blocksuite/blocks": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/icons": "^2.2.2",
|
||||
"@blocksuite/inline": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@lit/context": "^1.1.3",
|
||||
"@lottiefiles/dotlottie-wc": "^0.4.0",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.11",
|
||||
"@vanilla-extract/css": "^1.17.0",
|
||||
"lit": "^3.2.0",
|
||||
"yjs": "^13.6.21",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./effects": "./src/effects.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
"themes",
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@vanilla-extract/vite-plugin": "^5.0.0",
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-istanbul": "^7.0.0",
|
||||
"vite-plugin-wasm": "^3.4.1",
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"version": "0.19.0"
|
||||
}
|
||||
21
blocksuite/integration-test/renderer.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Kalam&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Debug Entry</title>
|
||||
</head>
|
||||
<body>
|
||||
<script
|
||||
type="module"
|
||||
src="./src/__tests__/utils/renderer-entry.ts"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
153
blocksuite/integration-test/src/__tests__/edgeless/basic.spec.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { LocalShapeElementModel } from '@blocksuite/affine-model';
|
||||
import { Text } from '@blocksuite/store';
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { addNote, getSurface } from '../utils/edgeless.js';
|
||||
import { setupEditor } from '../utils/setup.js';
|
||||
|
||||
beforeEach(async () => {
|
||||
const cleanup = await setupEditor('edgeless');
|
||||
|
||||
return cleanup;
|
||||
});
|
||||
|
||||
test('basic assert', () => {
|
||||
expect(window.doc).toBeDefined();
|
||||
expect(window.editor).toBeDefined();
|
||||
expect(window.editor.mode).toBe('edgeless');
|
||||
|
||||
expect(getSurface(window.doc, window.editor)).toBeDefined();
|
||||
});
|
||||
|
||||
describe('doc / note empty checker', () => {
|
||||
test('a paragraph is empty if it dose not contain text and child blocks', () => {
|
||||
const noteId = addNote(doc);
|
||||
const paragraphId = doc.addBlock('affine:paragraph', {}, noteId);
|
||||
const paragraph = doc.getBlock(paragraphId)?.model;
|
||||
expect(paragraph?.isEmpty()).toBe(true);
|
||||
});
|
||||
|
||||
test('a paragraph is not empty if it contains text', () => {
|
||||
const noteId = addNote(doc);
|
||||
const paragraphId = doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new Text('hello'),
|
||||
},
|
||||
noteId
|
||||
);
|
||||
const paragraph = doc.getBlock(paragraphId)?.model;
|
||||
expect(paragraph?.isEmpty()).toBe(false);
|
||||
});
|
||||
|
||||
test('a paragraph is not empty if it contains children blocks', () => {
|
||||
const noteId = addNote(doc);
|
||||
const paragraphId = doc.addBlock('affine:paragraph', {}, noteId);
|
||||
const paragraph = doc.getBlock(paragraphId)?.model;
|
||||
|
||||
// sub paragraph
|
||||
doc.addBlock('affine:paragraph', {}, paragraphId);
|
||||
expect(paragraph?.isEmpty()).toBe(false);
|
||||
});
|
||||
|
||||
test('a note is empty if it dose not contain any blocks', () => {
|
||||
const noteId = addNote(doc);
|
||||
const note = doc.getBlock(noteId)!.model;
|
||||
note.children.forEach(child => {
|
||||
doc.deleteBlock(child);
|
||||
});
|
||||
expect(note.children.length).toBe(0);
|
||||
expect(note.isEmpty()).toBe(true);
|
||||
});
|
||||
|
||||
test('a note is empty if it only contains a empty paragraph', () => {
|
||||
// `addNote` will create a empty paragraph
|
||||
const noteId = addNote(doc);
|
||||
const note = doc.getBlock(noteId)!.model;
|
||||
expect(note.isEmpty()).toBe(true);
|
||||
});
|
||||
|
||||
test('a note is not empty if it contains multi blocks', () => {
|
||||
const noteId = addNote(doc);
|
||||
const note = doc.getBlock(noteId)!.model;
|
||||
doc.addBlock('affine:paragraph', {}, noteId);
|
||||
expect(note.isEmpty()).toBe(false);
|
||||
});
|
||||
|
||||
test('a surface is empty if it dose not contains any element or blocks', () => {
|
||||
const surface = getSurface(doc, editor).model;
|
||||
expect(surface.isEmpty()).toBe(true);
|
||||
|
||||
const shapeId = surface.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
expect(surface.isEmpty()).toBe(false);
|
||||
surface.deleteElement(shapeId);
|
||||
expect(surface.isEmpty()).toBe(true);
|
||||
|
||||
const frameId = doc.addBlock('affine:frame', {}, surface.id);
|
||||
const frame = doc.getBlock(frameId)!.model;
|
||||
expect(surface.isEmpty()).toBe(false);
|
||||
doc.deleteBlock(frame);
|
||||
expect(surface.isEmpty()).toBe(true);
|
||||
});
|
||||
|
||||
test('a surface is empty if it only contains local elements', () => {
|
||||
const surface = getSurface(doc, editor).model;
|
||||
const localShape = new LocalShapeElementModel(surface);
|
||||
surface.addLocalElement(localShape);
|
||||
expect(surface.isEmpty()).toBe(true);
|
||||
});
|
||||
|
||||
test('a just initialized doc is empty', () => {
|
||||
expect(doc.isEmpty).toBe(true);
|
||||
expect(editor.rootModel.isEmpty()).toBe(true);
|
||||
});
|
||||
|
||||
test('a doc is empty if it only contains a note', () => {
|
||||
addNote(doc);
|
||||
expect(doc.isEmpty).toBe(true);
|
||||
|
||||
addNote(doc);
|
||||
expect(
|
||||
doc.isEmpty,
|
||||
'a doc is not empty if it contains multi-notes'
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('a note is empty if its children array is empty', () => {
|
||||
const noteId = addNote(doc);
|
||||
const note = doc.getBlock(noteId)?.model;
|
||||
note?.children.forEach(child => doc.deleteBlock(child));
|
||||
expect(note?.children.length === 0).toBe(true);
|
||||
expect(note?.isEmpty()).toBe(true);
|
||||
});
|
||||
|
||||
test('a doc is empty if its only contains an empty note and an empty surface', () => {
|
||||
const noteId = addNote(doc);
|
||||
const note = doc.getBlock(noteId)!.model;
|
||||
expect(doc.isEmpty).toBe(true);
|
||||
|
||||
const newNoteId = addNote(doc);
|
||||
const newNote = doc.getBlock(newNoteId)!.model;
|
||||
expect(doc.isEmpty).toBe(false);
|
||||
doc.deleteBlock(newNote);
|
||||
expect(doc.isEmpty).toBe(true);
|
||||
|
||||
const newParagraphId = doc.addBlock('affine:paragraph', {}, note);
|
||||
const newParagraph = doc.getBlock(newParagraphId)!.model;
|
||||
expect(doc.isEmpty).toBe(false);
|
||||
doc.deleteBlock(newParagraph);
|
||||
expect(doc.isEmpty).toBe(true);
|
||||
|
||||
const surface = getSurface(doc, editor).model;
|
||||
expect(doc.isEmpty).toBe(true);
|
||||
|
||||
const shapeId = surface.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
expect(doc.isEmpty).toBe(false);
|
||||
surface.deleteElement(shapeId);
|
||||
expect(doc.isEmpty).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import '@toeverything/theme/style.css';
|
||||
|
||||
import {
|
||||
ColorScheme,
|
||||
type EdgelessRootBlockComponent,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/blocks';
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { getDocRootBlock } from '../utils/edgeless.js';
|
||||
import { setupEditor } from '../utils/setup.js';
|
||||
|
||||
describe('theme service', () => {
|
||||
let edgeless!: EdgelessRootBlockComponent;
|
||||
|
||||
beforeEach(async () => {
|
||||
const cleanup = await setupEditor('edgeless');
|
||||
|
||||
edgeless = getDocRootBlock(doc, editor, 'edgeless');
|
||||
|
||||
edgeless.gfx.tool.setTool('default');
|
||||
|
||||
const themeService = edgeless.gfx.std.get(ThemeProvider);
|
||||
themeService.theme$.value = ColorScheme.Light;
|
||||
|
||||
return cleanup;
|
||||
});
|
||||
|
||||
test('switches theme', () => {
|
||||
const themeService = edgeless.gfx.std.get(ThemeProvider);
|
||||
expect(themeService.theme).toBe(ColorScheme.Light);
|
||||
|
||||
themeService.theme$.value = ColorScheme.Dark;
|
||||
expect(themeService.theme).toBe(ColorScheme.Dark);
|
||||
|
||||
themeService.theme$.value = ColorScheme.Light;
|
||||
expect(themeService.theme).toBe(ColorScheme.Light);
|
||||
});
|
||||
|
||||
test('generates a color property', () => {
|
||||
const themeService = edgeless.gfx.std.get(ThemeProvider);
|
||||
expect(themeService.theme).toBe(ColorScheme.Light);
|
||||
|
||||
expect(themeService.generateColorProperty('--affine-hover-color')).toBe(
|
||||
'var(--affine-hover-color)'
|
||||
);
|
||||
|
||||
expect(themeService.generateColorProperty('--affine-transparent')).toBe(
|
||||
'transparent'
|
||||
);
|
||||
|
||||
expect(themeService.generateColorProperty('transparent')).toBe(
|
||||
'transparent'
|
||||
);
|
||||
|
||||
expect(
|
||||
themeService.generateColorProperty({ dark: 'white', light: 'black' })
|
||||
).toBe('black');
|
||||
|
||||
themeService.theme$.value = ColorScheme.Dark;
|
||||
expect(themeService.theme).toBe(ColorScheme.Dark);
|
||||
|
||||
expect(
|
||||
themeService.generateColorProperty({ dark: 'white', light: 'black' })
|
||||
).toBe('white');
|
||||
|
||||
expect(themeService.generateColorProperty({ normal: 'grey' })).toBe('grey');
|
||||
|
||||
expect(themeService.generateColorProperty('', 'blue')).toBe('blue');
|
||||
});
|
||||
|
||||
test('gets a color value', () => {
|
||||
const themeService = edgeless.gfx.std.get(ThemeProvider);
|
||||
expect(themeService.theme).toBe(ColorScheme.Light);
|
||||
|
||||
expect(themeService.getColorValue('--affine-transparent')).toBe(
|
||||
'--affine-transparent'
|
||||
);
|
||||
expect(
|
||||
themeService.getColorValue('--affine-transparent', 'transparent', true)
|
||||
).toBe('transparent');
|
||||
expect(
|
||||
themeService.getColorValue('--affine-hover-color', 'transparent', true)
|
||||
).toBe('rgba(0, 0, 0, 0.04)');
|
||||
expect(
|
||||
themeService.getColorValue('--affine-tooltip', undefined, true)
|
||||
).toBe('rgba(0, 0, 0, 1)');
|
||||
|
||||
expect(
|
||||
themeService.getColorValue(
|
||||
{ dark: 'white', light: 'black' },
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
).toBe('black');
|
||||
expect(
|
||||
themeService.getColorValue({ dark: 'white', light: '' }, undefined, true)
|
||||
).toBe('transparent');
|
||||
expect(
|
||||
themeService.getColorValue({ normal: 'grey' }, undefined, true)
|
||||
).toBe('grey');
|
||||
|
||||
themeService.theme$.value = ColorScheme.Dark;
|
||||
expect(themeService.theme).toBe(ColorScheme.Dark);
|
||||
|
||||
expect(
|
||||
themeService.getColorValue('--affine-hover-color', 'transparent', true)
|
||||
).toEqual('rgba(255, 255, 255, 0.1)');
|
||||
expect(
|
||||
themeService.getColorValue('--affine-tooltip', undefined, true)
|
||||
).toEqual('rgba(234, 234, 234, 1)'); // #eaeaea
|
||||
});
|
||||
});
|
||||
128
blocksuite/integration-test/src/__tests__/edgeless/frame.spec.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type {
|
||||
AffineFrameTitleWidget,
|
||||
EdgelessRootBlockComponent,
|
||||
FrameBlockComponent,
|
||||
FrameBlockModel,
|
||||
} from '@blocksuite/blocks';
|
||||
import { assertType } from '@blocksuite/global/utils';
|
||||
import { Text } from '@blocksuite/store';
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { wait } from '../utils/common.js';
|
||||
import { getDocRootBlock } from '../utils/edgeless.js';
|
||||
import { setupEditor } from '../utils/setup.js';
|
||||
|
||||
describe('frame', () => {
|
||||
let service!: EdgelessRootBlockComponent['service'];
|
||||
|
||||
beforeEach(async () => {
|
||||
const cleanup = await setupEditor('edgeless');
|
||||
service = getDocRootBlock(window.doc, window.editor, 'edgeless').service;
|
||||
|
||||
return cleanup;
|
||||
});
|
||||
|
||||
test('frame should have title', async () => {
|
||||
const frame = service.doc.addBlock(
|
||||
'affine:frame',
|
||||
{
|
||||
xywh: '[0,0,300,300]',
|
||||
title: new Text('Frame 1'),
|
||||
},
|
||||
service.surface.id
|
||||
);
|
||||
await wait();
|
||||
|
||||
const frameTitleWidget = service.std.view.getWidget(
|
||||
'affine-frame-title-widget',
|
||||
doc.root!.id
|
||||
) as AffineFrameTitleWidget | null;
|
||||
|
||||
const frameTitle = frameTitleWidget?.getFrameTitle(frame);
|
||||
const rect = frameTitle?.getBoundingClientRect();
|
||||
|
||||
expect(frameTitle).toBeTruthy();
|
||||
expect(rect).toBeTruthy();
|
||||
expect(rect!.width).toBeGreaterThan(0);
|
||||
expect(rect!.height).toBeGreaterThan(0);
|
||||
|
||||
const [titleX, titleY] = service.viewport.toModelCoord(rect!.x, rect!.y);
|
||||
expect(titleX).toBeCloseTo(0);
|
||||
expect(titleY).toBeLessThan(0);
|
||||
|
||||
const nestedFrame = service.doc.addBlock(
|
||||
'affine:frame',
|
||||
{
|
||||
xywh: '[20,20,200,200]',
|
||||
title: new Text('Frame 2'),
|
||||
},
|
||||
service.surface.id
|
||||
);
|
||||
await wait();
|
||||
|
||||
const nestedTitle = frameTitleWidget?.getFrameTitle(nestedFrame);
|
||||
expect(nestedTitle).toBeTruthy();
|
||||
if (!nestedTitle) return;
|
||||
|
||||
const nestedTitleRect = nestedTitle.getBoundingClientRect()!;
|
||||
const [nestedTitleX, nestedTitleY] = service.viewport.toModelCoord(
|
||||
nestedTitleRect.x,
|
||||
nestedTitleRect.y
|
||||
);
|
||||
|
||||
expect(nestedTitleX).toBeGreaterThan(20);
|
||||
expect(nestedTitleY).toBeGreaterThan(20);
|
||||
});
|
||||
|
||||
test('frame should have externalXYWH after moving viewport to contains frame', async () => {
|
||||
const frameId = service.doc.addBlock(
|
||||
'affine:frame',
|
||||
{
|
||||
xywh: '[1800,1800,200,200]',
|
||||
title: new Text('Frame 1'),
|
||||
},
|
||||
service.surface.id
|
||||
);
|
||||
await wait();
|
||||
|
||||
const frame = service.doc.getBlock(frameId);
|
||||
expect(frame).toBeTruthy();
|
||||
|
||||
assertType<FrameBlockComponent>(frame);
|
||||
|
||||
service.viewport.setCenter(900, 900);
|
||||
expect(frame?.model.externalXYWH).toBeDefined();
|
||||
});
|
||||
|
||||
test('descendant of frame should not contain itself', async () => {
|
||||
const frameIds = [1, 2, 3].map(i => {
|
||||
return service.doc.addBlock(
|
||||
'affine:frame',
|
||||
{
|
||||
xywh: '[0,0,300,300]',
|
||||
title: new Text(`Frame ${i}`),
|
||||
},
|
||||
service.surface.id
|
||||
);
|
||||
});
|
||||
|
||||
await wait();
|
||||
|
||||
const frames = frameIds.map(
|
||||
id => service.doc.getBlock(id)?.model as FrameBlockModel
|
||||
);
|
||||
|
||||
frames.forEach(frame => {
|
||||
expect(frame.descendantElements).toHaveLength(0);
|
||||
});
|
||||
|
||||
frames[0].addChild(frames[1]);
|
||||
frames[1].addChild(frames[2]);
|
||||
frames[2].addChild(frames[0]);
|
||||
|
||||
await wait();
|
||||
expect(frames[0].descendantElements).toHaveLength(2);
|
||||
expect(frames[1].descendantElements).toHaveLength(1);
|
||||
expect(frames[2].descendantElements).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
377
blocksuite/integration-test/src/__tests__/edgeless/group.spec.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
import type { MindmapElementModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
type EdgelessRootBlockComponent,
|
||||
type GroupElementModel,
|
||||
LayoutType,
|
||||
NoteDisplayMode,
|
||||
} from '@blocksuite/blocks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { wait } from '../utils/common.js';
|
||||
import { addNote, getDocRootBlock } from '../utils/edgeless.js';
|
||||
import { setupEditor } from '../utils/setup.js';
|
||||
|
||||
describe('group', () => {
|
||||
let service!: EdgelessRootBlockComponent['service'];
|
||||
|
||||
beforeEach(async () => {
|
||||
const cleanup = await setupEditor('edgeless');
|
||||
service = getDocRootBlock(window.doc, window.editor, 'edgeless').service;
|
||||
|
||||
return cleanup;
|
||||
});
|
||||
|
||||
test('group with no children will be removed automatically', () => {
|
||||
const map = new Y.Map<boolean>();
|
||||
const ids = Array.from({ length: 2 })
|
||||
.map(() => {
|
||||
const id = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!;
|
||||
map.set(id, true);
|
||||
|
||||
return id;
|
||||
})
|
||||
.concat(
|
||||
Array.from({ length: 2 }).map(() => {
|
||||
const id = addNote(doc);
|
||||
map.set(id, true);
|
||||
return id;
|
||||
})
|
||||
);
|
||||
service.crud.addElement('group', { children: map });
|
||||
doc.captureSync();
|
||||
expect(service.elements.length).toBe(3);
|
||||
|
||||
service.removeElement(ids[0]);
|
||||
service.removeElement(ids[1]);
|
||||
doc.captureSync();
|
||||
expect(service.elements.length).toBe(1);
|
||||
|
||||
service.removeElement(ids[2]);
|
||||
service.removeElement(ids[3]);
|
||||
doc.captureSync();
|
||||
expect(service.elements.length).toBe(0);
|
||||
|
||||
doc.undo();
|
||||
expect(service.elements.length).toBe(1);
|
||||
doc.redo();
|
||||
expect(service.elements.length).toBe(0);
|
||||
});
|
||||
|
||||
test('remove group should remove its children at the same time', () => {
|
||||
const map = new Y.Map<boolean>();
|
||||
const doc = service.doc;
|
||||
const noteId = addNote(doc);
|
||||
const shapeId = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
});
|
||||
assertExists(shapeId);
|
||||
|
||||
map.set(noteId, true);
|
||||
map.set(shapeId, true);
|
||||
const groupId = service.crud.addElement('group', { children: map });
|
||||
assertExists(groupId);
|
||||
expect(service.elements.length).toBe(2);
|
||||
expect(doc.getBlock(noteId)).toBeDefined();
|
||||
doc.captureSync();
|
||||
|
||||
service.removeElement(groupId);
|
||||
expect(service.elements.length).toBe(0);
|
||||
expect(doc.getBlock(noteId)).toBeUndefined();
|
||||
|
||||
doc.undo();
|
||||
expect(doc.getBlock(noteId)).toBeDefined();
|
||||
expect(service.elements.length).toBe(2);
|
||||
});
|
||||
|
||||
test("group's xywh should update automatically when children change", async () => {
|
||||
const shape1 = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
xywh: '[0,0,100,100]',
|
||||
});
|
||||
assertExists(shape1);
|
||||
const shape2 = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
xywh: '[100,100,100,100]',
|
||||
});
|
||||
assertExists(shape2);
|
||||
const note1 = addNote(doc, {
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
xywh: '[200,200,800,100]',
|
||||
edgeless: {
|
||||
style: {
|
||||
borderRadius: 8,
|
||||
borderSize: 4,
|
||||
borderStyle: 'solid',
|
||||
shadowType: '--affine-note-shadow-box',
|
||||
},
|
||||
collapse: true,
|
||||
collapsedHeight: 100,
|
||||
},
|
||||
});
|
||||
const children = new Y.Map<boolean>();
|
||||
|
||||
children.set(shape1, true);
|
||||
children.set(shape2, true);
|
||||
children.set(note1, true);
|
||||
|
||||
const groupId = service.crud.addElement('group', { children });
|
||||
assertExists(groupId);
|
||||
|
||||
const group = service.crud.getElementById(groupId) as GroupElementModel;
|
||||
const assertInitial = () => {
|
||||
expect(group.x).toBe(0);
|
||||
expect(group.y).toBe(0);
|
||||
expect(group.w).toBe(1000);
|
||||
expect(group.h).toBe(300);
|
||||
};
|
||||
|
||||
doc.captureSync();
|
||||
await wait();
|
||||
assertInitial();
|
||||
|
||||
service.removeElement(note1);
|
||||
await wait();
|
||||
expect(group.x).toBe(0);
|
||||
expect(group.y).toBe(0);
|
||||
expect(group.w).toBe(200);
|
||||
expect(group.h).toBe(200);
|
||||
doc.captureSync();
|
||||
|
||||
doc.undo();
|
||||
await wait();
|
||||
assertInitial();
|
||||
|
||||
service.crud.updateElement(note1, {
|
||||
xywh: '[300,300,800,100]',
|
||||
});
|
||||
await wait();
|
||||
expect(group.x).toBe(0);
|
||||
expect(group.y).toBe(0);
|
||||
expect(group.w).toBe(1100);
|
||||
expect(group.h).toBe(400);
|
||||
doc.captureSync();
|
||||
|
||||
doc.undo();
|
||||
await wait();
|
||||
assertInitial();
|
||||
|
||||
service.removeElement(shape1);
|
||||
await wait();
|
||||
expect(group.x).toBe(100);
|
||||
expect(group.y).toBe(100);
|
||||
expect(group.w).toBe(900);
|
||||
expect(group.h).toBe(200);
|
||||
doc.captureSync();
|
||||
|
||||
doc.undo();
|
||||
await wait();
|
||||
assertInitial();
|
||||
|
||||
service.crud.updateElement(shape1, {
|
||||
xywh: '[100,100,100,100]',
|
||||
});
|
||||
await wait();
|
||||
expect(group.x).toBe(100);
|
||||
expect(group.y).toBe(100);
|
||||
expect(group.w).toBe(900);
|
||||
expect(group.h).toBe(200);
|
||||
doc.captureSync();
|
||||
|
||||
doc.undo();
|
||||
await wait();
|
||||
assertInitial();
|
||||
});
|
||||
|
||||
test('empty group should have all zero xywh', () => {
|
||||
const map = new Y.Map<boolean>();
|
||||
const groupId = service.crud.addElement('group', { children: map });
|
||||
assertExists(groupId);
|
||||
const group = service.crud.getElementById(groupId) as GroupElementModel;
|
||||
|
||||
expect(group.x).toBe(0);
|
||||
expect(group.y).toBe(0);
|
||||
expect(group.w).toBe(0);
|
||||
expect(group.h).toBe(0);
|
||||
});
|
||||
|
||||
test('descendant of group should not contain itself', () => {
|
||||
const groupIds = [1, 2, 3].map(_ => {
|
||||
return service.crud.addElement('group', {
|
||||
children: new Y.Map<boolean>(),
|
||||
}) as string;
|
||||
});
|
||||
const groups = groupIds.map(
|
||||
id => service.crud.getElementById(id) as GroupElementModel
|
||||
);
|
||||
|
||||
groups.forEach(group => {
|
||||
expect(group.descendantElements).toHaveLength(0);
|
||||
});
|
||||
|
||||
groups[0].addChild(groups[1]);
|
||||
groups[1].addChild(groups[2]);
|
||||
groups[2].addChild(groups[0]);
|
||||
|
||||
expect(groups[0].descendantElements).toHaveLength(2);
|
||||
expect(groups[1].descendantElements).toHaveLength(1);
|
||||
expect(groups[2].descendantElements).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mindmap', () => {
|
||||
let service!: EdgelessRootBlockComponent['service'];
|
||||
|
||||
beforeEach(async () => {
|
||||
const cleanup = await setupEditor('edgeless');
|
||||
service = getDocRootBlock(window.doc, window.editor, 'edgeless').service;
|
||||
|
||||
return cleanup;
|
||||
});
|
||||
|
||||
test('delete the root node should remove all children', async () => {
|
||||
const tree = {
|
||||
text: 'root',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf1',
|
||||
},
|
||||
{
|
||||
text: 'leaf2',
|
||||
},
|
||||
{
|
||||
text: 'leaf3',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf4',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const mindmapId = service.crud.addElement('mindmap', { children: tree });
|
||||
assertExists(mindmapId);
|
||||
const mindmap = () =>
|
||||
service.crud.getElementById(mindmapId) as MindmapElementModel;
|
||||
|
||||
expect(service.surface.elementModels.length).toBe(6);
|
||||
doc.captureSync();
|
||||
|
||||
service.removeElement(mindmap().tree.element);
|
||||
await wait();
|
||||
expect(service.surface.elementModels.length).toBe(0);
|
||||
doc.captureSync();
|
||||
await wait();
|
||||
|
||||
doc.undo();
|
||||
expect(service.surface.elementModels.length).toBe(6);
|
||||
await wait();
|
||||
|
||||
service.removeElement(mindmap().tree.children[2].element);
|
||||
await wait();
|
||||
expect(service.surface.elementModels.length).toBe(4);
|
||||
await wait();
|
||||
|
||||
doc.undo();
|
||||
await wait();
|
||||
expect(service.surface.elementModels.length).toBe(6);
|
||||
});
|
||||
|
||||
test('mindmap should layout automatically when creating', async () => {
|
||||
const tree = {
|
||||
text: 'root',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf1',
|
||||
},
|
||||
{
|
||||
text: 'leaf2',
|
||||
},
|
||||
{
|
||||
text: 'leaf3',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf4',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const mindmapId = service.crud.addElement('mindmap', {
|
||||
type: LayoutType.RIGHT,
|
||||
children: tree,
|
||||
});
|
||||
assertExists(mindmapId);
|
||||
const mindmap = () =>
|
||||
service.crud.getElementById(mindmapId) as MindmapElementModel;
|
||||
|
||||
doc.captureSync();
|
||||
await wait();
|
||||
|
||||
const root = mindmap().tree.element;
|
||||
const children = mindmap().tree.children.map(child => child.element);
|
||||
const leaf4 = mindmap().tree.children[2].children[0].element;
|
||||
|
||||
expect(children[0].x).greaterThan(root.x + root.w);
|
||||
expect(children[1].x).greaterThan(root.x + root.w);
|
||||
expect(children[2].x).greaterThan(root.x + root.w);
|
||||
|
||||
expect(children[1].y).greaterThan(children[0].y + children[0].h);
|
||||
expect(children[2].y).greaterThan(children[1].y + children[1].h);
|
||||
|
||||
expect(leaf4.x).greaterThan(children[2].x + children[2].w);
|
||||
});
|
||||
|
||||
test('deliberately creating a circular reference should be resolved correctly', async () => {
|
||||
const tree = {
|
||||
text: 'root',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf1',
|
||||
},
|
||||
{
|
||||
text: 'leaf2',
|
||||
},
|
||||
{
|
||||
text: 'leaf3',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf4',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const mindmapId = service.crud.addElement('mindmap', {
|
||||
type: LayoutType.RIGHT,
|
||||
children: tree,
|
||||
});
|
||||
assertExists(mindmapId);
|
||||
const mindmap = () =>
|
||||
service.crud.getElementById(mindmapId) as MindmapElementModel;
|
||||
|
||||
doc.captureSync();
|
||||
await wait();
|
||||
|
||||
// create a circular reference
|
||||
doc.transact(() => {
|
||||
const root = mindmap().tree;
|
||||
const leaf3 = root.children[2];
|
||||
const leaf4 = root.children[2].children[0];
|
||||
|
||||
mindmap().children.set(leaf3.id, {
|
||||
index: leaf3.detail.index,
|
||||
parent: leaf4.id,
|
||||
});
|
||||
});
|
||||
doc.captureSync();
|
||||
|
||||
await wait();
|
||||
|
||||
// the circular referenced node should be removed
|
||||
expect(mindmap().nodeMap.size).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,276 @@
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import {
|
||||
type BrushElementModel,
|
||||
type ConnectorElementModel,
|
||||
DEFAULT_NOTE_SHADOW,
|
||||
DefaultTheme,
|
||||
type EdgelessRootBlockComponent,
|
||||
type EdgelessTextBlockModel,
|
||||
EditPropsStore,
|
||||
FontFamily,
|
||||
type FrameBlockModel,
|
||||
getSurfaceBlock,
|
||||
LayoutType,
|
||||
type MindmapElementModel,
|
||||
MindmapStyle,
|
||||
type NoteBlockModel,
|
||||
NoteShadow,
|
||||
type ShapeElementModel,
|
||||
ShapeType,
|
||||
type TextElementModel,
|
||||
} from '@blocksuite/blocks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { getDocRootBlock } from '../utils/edgeless.js';
|
||||
import { setupEditor } from '../utils/setup.js';
|
||||
|
||||
describe('apply last props', () => {
|
||||
let edgelessRoot!: EdgelessRootBlockComponent;
|
||||
let service!: EdgelessRootBlockComponent['service'];
|
||||
let std!: BlockStdScope;
|
||||
|
||||
beforeEach(async () => {
|
||||
sessionStorage.removeItem('blocksuite:prop:record');
|
||||
const cleanup = await setupEditor('edgeless');
|
||||
edgelessRoot = getDocRootBlock(window.doc, window.editor, 'edgeless');
|
||||
service = edgelessRoot.service;
|
||||
std = edgelessRoot.std;
|
||||
return cleanup;
|
||||
});
|
||||
|
||||
test('shapes', () => {
|
||||
// rect shape
|
||||
const rectId = service.crud.addElement('shape', {
|
||||
shapeType: ShapeType.Rect,
|
||||
});
|
||||
assertExists(rectId);
|
||||
const rectShape = service.crud.getElementById(rectId) as ShapeElementModel;
|
||||
expect(rectShape.fillColor).toBe(DefaultTheme.shapeFillColor);
|
||||
service.crud.updateElement(rectId, {
|
||||
fillColor: DefaultTheme.FillColorMap.Orange,
|
||||
});
|
||||
expect(
|
||||
std.get(EditPropsStore).lastProps$.value[`shape:${ShapeType.Rect}`]
|
||||
.fillColor
|
||||
).toBe(DefaultTheme.FillColorMap.Orange);
|
||||
|
||||
// diamond shape
|
||||
const diamondId = service.crud.addElement('shape', {
|
||||
shapeType: ShapeType.Diamond,
|
||||
});
|
||||
assertExists(diamondId);
|
||||
const diamondShape = service.crud.getElementById(
|
||||
diamondId
|
||||
) as ShapeElementModel;
|
||||
expect(diamondShape.fillColor).toBe(DefaultTheme.FillColorMap.Yellow);
|
||||
service.crud.updateElement(diamondId, {
|
||||
fillColor: DefaultTheme.FillColorMap.Blue,
|
||||
});
|
||||
expect(
|
||||
std.get(EditPropsStore).lastProps$.value[`shape:${ShapeType.Diamond}`]
|
||||
.fillColor
|
||||
).toBe(DefaultTheme.FillColorMap.Blue);
|
||||
|
||||
// rounded rect shape
|
||||
const roundedRectId = service.crud.addElement('shape', {
|
||||
shapeType: ShapeType.Rect,
|
||||
radius: 0.1,
|
||||
});
|
||||
assertExists(roundedRectId);
|
||||
const roundedRectShape = service.crud.getElementById(
|
||||
roundedRectId
|
||||
) as ShapeElementModel;
|
||||
expect(roundedRectShape.fillColor).toBe(DefaultTheme.FillColorMap.Yellow);
|
||||
service.crud.updateElement(roundedRectId, {
|
||||
fillColor: DefaultTheme.FillColorMap.Green,
|
||||
});
|
||||
expect(
|
||||
std.get(EditPropsStore).lastProps$.value['shape:roundedRect'].fillColor
|
||||
).toBe(DefaultTheme.FillColorMap.Green);
|
||||
|
||||
// apply last props
|
||||
const rectId2 = service.crud.addElement('shape', {
|
||||
shapeType: ShapeType.Rect,
|
||||
});
|
||||
assertExists(rectId2);
|
||||
const rectShape2 = service.crud.getElementById(
|
||||
rectId2
|
||||
) as ShapeElementModel;
|
||||
expect(rectShape2.fillColor).toBe(DefaultTheme.FillColorMap.Orange);
|
||||
|
||||
const diamondId2 = service.crud.addElement('shape', {
|
||||
shapeType: ShapeType.Diamond,
|
||||
});
|
||||
assertExists(diamondId2);
|
||||
const diamondShape2 = service.crud.getElementById(
|
||||
diamondId2
|
||||
) as ShapeElementModel;
|
||||
expect(diamondShape2.fillColor).toBe(DefaultTheme.FillColorMap.Blue);
|
||||
|
||||
const roundedRectId2 = service.crud.addElement('shape', {
|
||||
shapeType: ShapeType.Rect,
|
||||
radius: 0.1,
|
||||
});
|
||||
assertExists(roundedRectId2);
|
||||
const roundedRectShape2 = service.crud.getElementById(
|
||||
roundedRectId2
|
||||
) as ShapeElementModel;
|
||||
expect(roundedRectShape2.fillColor).toBe(DefaultTheme.FillColorMap.Green);
|
||||
});
|
||||
|
||||
test('connector', () => {
|
||||
const id = service.crud.addElement('connector', { mode: 0 });
|
||||
assertExists(id);
|
||||
const connector = service.crud.getElementById(id) as ConnectorElementModel;
|
||||
expect(connector.stroke).toBe(DefaultTheme.connectorColor);
|
||||
expect(connector.strokeWidth).toBe(2);
|
||||
expect(connector.strokeStyle).toBe('solid');
|
||||
expect(connector.frontEndpointStyle).toBe('None');
|
||||
expect(connector.rearEndpointStyle).toBe('Arrow');
|
||||
service.crud.updateElement(id, { strokeWidth: 10 });
|
||||
|
||||
const id2 = service.crud.addElement('connector', { mode: 1 });
|
||||
assertExists(id2);
|
||||
const connector2 = service.crud.getElementById(
|
||||
id2
|
||||
) as ConnectorElementModel;
|
||||
expect(connector2.strokeWidth).toBe(10);
|
||||
service.crud.updateElement(id2, {
|
||||
labelStyle: {
|
||||
color: DefaultTheme.black,
|
||||
fontFamily: FontFamily.Kalam,
|
||||
},
|
||||
});
|
||||
|
||||
const id3 = service.crud.addElement('connector', { mode: 1 });
|
||||
assertExists(id3);
|
||||
const connector3 = service.crud.getElementById(
|
||||
id3
|
||||
) as ConnectorElementModel;
|
||||
expect(connector3.strokeWidth).toBe(10);
|
||||
expect(connector3.labelStyle.color).toEqual(DefaultTheme.black);
|
||||
expect(connector3.labelStyle.fontFamily).toBe(FontFamily.Kalam);
|
||||
});
|
||||
|
||||
test('brush', () => {
|
||||
const id = service.crud.addElement('brush', {});
|
||||
assertExists(id);
|
||||
const brush = service.crud.getElementById(id) as BrushElementModel;
|
||||
expect(brush.color).toEqual(DefaultTheme.black);
|
||||
expect(brush.lineWidth).toBe(4);
|
||||
service.crud.updateElement(id, { lineWidth: 10 });
|
||||
const secondBrush = service.crud.getElementById(
|
||||
service.crud.addElement('brush', {}) as string
|
||||
) as BrushElementModel;
|
||||
expect(secondBrush.lineWidth).toBe(10);
|
||||
});
|
||||
|
||||
test('text', () => {
|
||||
const id = service.crud.addElement('text', {});
|
||||
assertExists(id);
|
||||
const text = service.crud.getElementById(id) as TextElementModel;
|
||||
expect(text.fontSize).toBe(24);
|
||||
service.crud.updateElement(id, { fontSize: 36 });
|
||||
const secondText = service.crud.getElementById(
|
||||
service.crud.addElement('text', {}) as string
|
||||
) as TextElementModel;
|
||||
expect(secondText.fontSize).toBe(36);
|
||||
});
|
||||
|
||||
test('mindmap', () => {
|
||||
const id = service.crud.addElement('mindmap', {});
|
||||
assertExists(id);
|
||||
const mindmap = service.crud.getElementById(id) as MindmapElementModel;
|
||||
expect(mindmap.layoutType).toBe(LayoutType.RIGHT);
|
||||
expect(mindmap.style).toBe(MindmapStyle.ONE);
|
||||
service.crud.updateElement(id, {
|
||||
layoutType: LayoutType.BALANCE,
|
||||
style: MindmapStyle.THREE,
|
||||
});
|
||||
|
||||
const id2 = service.crud.addElement('mindmap', {});
|
||||
assertExists(id2);
|
||||
const mindmap2 = service.crud.getElementById(id2) as MindmapElementModel;
|
||||
expect(mindmap2.layoutType).toBe(LayoutType.BALANCE);
|
||||
expect(mindmap2.style).toBe(MindmapStyle.THREE);
|
||||
});
|
||||
|
||||
test('edgeless-text', () => {
|
||||
const surface = getSurfaceBlock(doc);
|
||||
const id = service.crud.addBlock('affine:edgeless-text', {}, surface!.id);
|
||||
assertExists(id);
|
||||
const text = service.crud.getElementById(id) as EdgelessTextBlockModel;
|
||||
expect(text.color).toBe(DefaultTheme.textColor);
|
||||
expect(text.fontFamily).toBe(FontFamily.Inter);
|
||||
service.crud.updateElement(id, {
|
||||
color: DefaultTheme.StrokeColorMap.Green,
|
||||
fontFamily: FontFamily.OrelegaOne,
|
||||
});
|
||||
|
||||
const id2 = service.crud.addBlock('affine:edgeless-text', {}, surface!.id);
|
||||
assertExists(id2);
|
||||
const text2 = service.crud.getElementById(id2) as EdgelessTextBlockModel;
|
||||
expect(text2.color).toBe(DefaultTheme.StrokeColorMap.Green);
|
||||
expect(text2.fontFamily).toBe(FontFamily.OrelegaOne);
|
||||
});
|
||||
|
||||
test('note', () => {
|
||||
const id = service.crud.addBlock('affine:note', {}, doc.root!.id);
|
||||
assertExists(id);
|
||||
const note = service.crud.getElementById(id) as NoteBlockModel;
|
||||
expect(note.background).toEqual(DefaultTheme.noteBackgrounColor);
|
||||
expect(note.edgeless.style.shadowType).toBe(DEFAULT_NOTE_SHADOW);
|
||||
service.crud.updateElement(id, {
|
||||
background: DefaultTheme.NoteBackgroundColorMap.Purple,
|
||||
edgeless: {
|
||||
style: {
|
||||
shadowType: NoteShadow.Film,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const id2 = service.crud.addBlock('affine:note', {}, doc.root!.id);
|
||||
assertExists(id2);
|
||||
const note2 = service.crud.getElementById(id2) as NoteBlockModel;
|
||||
expect(note2.background).toEqual(
|
||||
DefaultTheme.NoteBackgroundColorMap.Purple
|
||||
);
|
||||
expect(note2.edgeless.style.shadowType).toBe(NoteShadow.Film);
|
||||
});
|
||||
|
||||
test('frame', () => {
|
||||
const surface = getSurfaceBlock(doc);
|
||||
const id = service.crud.addBlock('affine:frame', {}, surface!.id);
|
||||
assertExists(id);
|
||||
const note = service.crud.getElementById(id) as FrameBlockModel;
|
||||
expect(note.background).toBe('transparent');
|
||||
service.crud.updateElement(id, {
|
||||
background: DefaultTheme.StrokeColorMap.Purple,
|
||||
});
|
||||
|
||||
const id2 = service.crud.addBlock('affine:frame', {}, surface!.id);
|
||||
assertExists(id2);
|
||||
const frame2 = service.crud.getElementById(id2) as FrameBlockModel;
|
||||
expect(frame2.background).toBe(DefaultTheme.StrokeColorMap.Purple);
|
||||
service.crud.updateElement(id2, {
|
||||
background: { normal: '#def4e740' },
|
||||
});
|
||||
|
||||
const id3 = service.crud.addBlock('affine:frame', {}, surface!.id);
|
||||
assertExists(id3);
|
||||
const frame3 = service.crud.getElementById(id3) as FrameBlockModel;
|
||||
expect(frame3.background).toEqual({ normal: '#def4e740' });
|
||||
service.crud.updateElement(id3, {
|
||||
background: { light: '#a381aa23', dark: '#6e907452' },
|
||||
});
|
||||
|
||||
const id4 = service.crud.addBlock('affine:frame', {}, surface!.id);
|
||||
assertExists(id4);
|
||||
const frame4 = service.crud.getElementById(id4) as FrameBlockModel;
|
||||
expect(frame4.background).toEqual({
|
||||
light: '#a381aa23',
|
||||
dark: '#6e907452',
|
||||
});
|
||||
});
|
||||
});
|
||||
885
blocksuite/integration-test/src/__tests__/edgeless/layer.spec.ts
Normal file
@@ -0,0 +1,885 @@
|
||||
import type { BlockComponent } from '@blocksuite/block-std';
|
||||
import { generateKeyBetween } from '@blocksuite/block-std/gfx';
|
||||
import type {
|
||||
EdgelessRootBlockComponent,
|
||||
GroupElementModel,
|
||||
NoteBlockModel,
|
||||
SurfaceElementModel,
|
||||
} from '@blocksuite/blocks';
|
||||
import type { BlockModel, Store } from '@blocksuite/store';
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { wait } from '../utils/common.js';
|
||||
import {
|
||||
addNote as _addNote,
|
||||
getDocRootBlock,
|
||||
getSurface,
|
||||
} from '../utils/edgeless.js';
|
||||
import { setupEditor } from '../utils/setup.js';
|
||||
|
||||
let service!: EdgelessRootBlockComponent['service'];
|
||||
|
||||
const addNote = (doc: Store, props: Record<string, unknown> = {}) => {
|
||||
return _addNote(doc, {
|
||||
index: service.layer.generateIndex(),
|
||||
...props,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const cleanup = await setupEditor('edgeless');
|
||||
service = getDocRootBlock(window.doc, window.editor, 'edgeless').service;
|
||||
|
||||
return async () => {
|
||||
await wait(100);
|
||||
cleanup();
|
||||
};
|
||||
});
|
||||
|
||||
test('layer manager initial state', () => {
|
||||
expect(service.layer).toBeDefined();
|
||||
expect(service.layer.layers.length).toBe(0);
|
||||
expect(service.layer.canvasLayers.length).toBe(1);
|
||||
});
|
||||
|
||||
describe('add new edgeless blocks or canvas elements should update layer automatically', () => {
|
||||
test('add note, note, shape sequentially', async () => {
|
||||
addNote(doc);
|
||||
addNote(doc);
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
});
|
||||
|
||||
await wait();
|
||||
|
||||
expect(service.layer.layers.length).toBe(2);
|
||||
});
|
||||
|
||||
test('add note, shape, note sequentially', async () => {
|
||||
addNote(doc);
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
});
|
||||
addNote(doc);
|
||||
await wait();
|
||||
|
||||
expect(service.layer.layers.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
test('delete element should update layer automatically', () => {
|
||||
const id = addNote(doc);
|
||||
const canvasElId = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
});
|
||||
|
||||
service.removeElement(id);
|
||||
|
||||
expect(service.layer.layers.length).toBe(1);
|
||||
|
||||
service.removeElement(canvasElId!);
|
||||
|
||||
expect(service.layer.layers.length).toBe(0);
|
||||
});
|
||||
|
||||
test('change element should update layer automatically', async () => {
|
||||
const id = addNote(doc);
|
||||
const canvasElId = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
});
|
||||
|
||||
await wait();
|
||||
|
||||
service.crud.updateElement(id, {
|
||||
index: service.layer.getReorderedIndex(
|
||||
service.crud.getElementById(id)!,
|
||||
'forward'
|
||||
),
|
||||
});
|
||||
expect(service.layer.layers[service.layer.layers.length - 1].type).toBe(
|
||||
'block'
|
||||
);
|
||||
|
||||
service.crud.updateElement(canvasElId!, {
|
||||
index: service.layer.getReorderedIndex(
|
||||
service.crud.getElementById(canvasElId!)!,
|
||||
'forward'
|
||||
),
|
||||
});
|
||||
expect(service.layer.layers[service.layer.layers.length - 1].type).toBe(
|
||||
'canvas'
|
||||
);
|
||||
});
|
||||
|
||||
test('new added canvas elements should be placed in the topmost canvas layer', async () => {
|
||||
addNote(doc);
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
});
|
||||
|
||||
await wait();
|
||||
|
||||
expect(service.layer.layers.length).toBe(2);
|
||||
expect(service.layer.layers[1].type).toBe('canvas');
|
||||
});
|
||||
|
||||
test("there should be at lease one layer in canvasLayers property even there's no canvas element", () => {
|
||||
addNote(doc);
|
||||
|
||||
expect(service.layer.canvasLayers.length).toBe(1);
|
||||
});
|
||||
|
||||
test('if the topmost layer is canvas layer, the length of canvasLayers array should equal to the counts of canvas layers', () => {
|
||||
addNote(doc);
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
});
|
||||
addNote(doc);
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
});
|
||||
|
||||
expect(service.layer.layers.length).toBe(4);
|
||||
expect(service.layer.canvasLayers.length).toBe(
|
||||
service.layer.layers.filter(layer => layer.type === 'canvas').length
|
||||
);
|
||||
});
|
||||
|
||||
test('a new layer should be created in canvasLayers prop when the topmost layer is not canvas layer', () => {
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
});
|
||||
addNote(doc);
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
});
|
||||
addNote(doc);
|
||||
|
||||
expect(service.layer.canvasLayers.length).toBe(3);
|
||||
});
|
||||
|
||||
test('layer zindex should update correctly when elements changed', async () => {
|
||||
addNote(doc);
|
||||
const noteId = addNote(doc);
|
||||
const note = service.crud.getElementById(noteId);
|
||||
addNote(doc);
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
});
|
||||
const topShapeId = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
});
|
||||
const topShape = service.crud.getElementById(topShapeId!)!;
|
||||
|
||||
await wait();
|
||||
|
||||
const assertInitialState = () => {
|
||||
expect(service.layer.layers[0].type).toBe('block');
|
||||
expect(service.layer.layers[0].zIndex).toBe(1);
|
||||
|
||||
expect(service.layer.layers[1].type).toBe('canvas');
|
||||
expect(service.layer.layers[1].zIndex).toBe(4);
|
||||
};
|
||||
assertInitialState();
|
||||
|
||||
service.doc.captureSync();
|
||||
|
||||
service.crud.updateElement(noteId, {
|
||||
index: service.layer.getReorderedIndex(note!, 'front'),
|
||||
});
|
||||
await wait();
|
||||
service.doc.captureSync();
|
||||
|
||||
const assert2StepState = () => {
|
||||
expect(service.layer.layers[1].type).toBe('canvas');
|
||||
expect(service.layer.layers[1].zIndex).toBe(3);
|
||||
|
||||
expect(service.layer.layers[2].type).toBe('block');
|
||||
expect(service.layer.layers[2].zIndex).toBe(4);
|
||||
};
|
||||
assert2StepState();
|
||||
|
||||
service.crud.updateElement(topShapeId!, {
|
||||
index: service.layer.getReorderedIndex(topShape!, 'front'),
|
||||
});
|
||||
await wait();
|
||||
service.doc.captureSync();
|
||||
|
||||
expect(service.layer.layers[3].type).toBe('canvas');
|
||||
expect(service.layer.layers[3].zIndex).toBe(5);
|
||||
|
||||
service.doc.undo();
|
||||
await wait();
|
||||
assert2StepState();
|
||||
|
||||
service.doc.undo();
|
||||
await wait();
|
||||
assertInitialState();
|
||||
});
|
||||
|
||||
test('blocks should rerender when their z-index changed', async () => {
|
||||
const blocks = [addNote(doc), addNote(doc), addNote(doc), addNote(doc)];
|
||||
const assertBlocksContent = () => {
|
||||
const blocks = Array.from(
|
||||
document.querySelectorAll(
|
||||
'affine-edgeless-root gfx-viewport > [data-block-id]'
|
||||
)
|
||||
);
|
||||
|
||||
expect(blocks.length).toBe(5);
|
||||
|
||||
blocks.forEach(element => {
|
||||
expect(element.children.length).toBeGreaterThan(0);
|
||||
});
|
||||
};
|
||||
|
||||
await wait();
|
||||
assertBlocksContent();
|
||||
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
index: generateKeyBetween(
|
||||
service.crud.getElementById(blocks[1])!.index,
|
||||
service.crud.getElementById(blocks[2])!.index
|
||||
),
|
||||
});
|
||||
|
||||
await wait();
|
||||
assertBlocksContent();
|
||||
});
|
||||
|
||||
describe('layer reorder functionality', () => {
|
||||
let ids: string[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
ids = [
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!,
|
||||
addNote(doc),
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!,
|
||||
addNote(doc),
|
||||
];
|
||||
});
|
||||
|
||||
test('forward', async () => {
|
||||
service.crud.updateElement(ids[0], {
|
||||
index: service.layer.getReorderedIndex(
|
||||
service.crud.getElementById(ids[0])!,
|
||||
'forward'
|
||||
),
|
||||
});
|
||||
|
||||
expect(
|
||||
service.layer.layers.findIndex(layer =>
|
||||
layer.set.has(service.crud.getElementById(ids[0]) as any)
|
||||
)
|
||||
).toBe(1);
|
||||
expect(
|
||||
service.layer.layers.findIndex(layer =>
|
||||
layer.set.has(service.crud.getElementById(ids[1]) as any)
|
||||
)
|
||||
).toBe(0);
|
||||
|
||||
await wait();
|
||||
|
||||
service.crud.updateElement(ids[1], {
|
||||
index: service.layer.getReorderedIndex(
|
||||
service.crud.getElementById(ids[1])!,
|
||||
'forward'
|
||||
),
|
||||
});
|
||||
|
||||
expect(
|
||||
service.layer.layers.findIndex(layer =>
|
||||
layer.set.has(service.crud.getElementById(ids[0]) as any)
|
||||
)
|
||||
).toBe(0);
|
||||
expect(
|
||||
service.layer.layers.findIndex(layer =>
|
||||
layer.set.has(service.crud.getElementById(ids[1]) as any)
|
||||
)
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('front', async () => {
|
||||
service.crud.updateElement(ids[0], {
|
||||
index: service.layer.getReorderedIndex(
|
||||
service.crud.getElementById(ids[0])!,
|
||||
'front'
|
||||
),
|
||||
});
|
||||
|
||||
await wait();
|
||||
|
||||
expect(
|
||||
service.layer.layers.findIndex(layer =>
|
||||
layer.set.has(service.crud.getElementById(ids[0]) as any)
|
||||
)
|
||||
).toBe(3);
|
||||
|
||||
service.crud.updateElement(ids[1], {
|
||||
index: service.layer.getReorderedIndex(
|
||||
service.crud.getElementById(ids[1])!,
|
||||
'front'
|
||||
),
|
||||
});
|
||||
|
||||
expect(
|
||||
service.layer.layers.findIndex(layer =>
|
||||
layer.set.has(service.crud.getElementById(ids[1]) as any)
|
||||
)
|
||||
).toBe(3);
|
||||
});
|
||||
|
||||
test('backward', async () => {
|
||||
service.crud.updateElement(ids[3], {
|
||||
index: service.layer.getReorderedIndex(
|
||||
service.crud.getElementById(ids[3])!,
|
||||
'backward'
|
||||
),
|
||||
});
|
||||
|
||||
expect(
|
||||
service.layer.layers.findIndex(layer =>
|
||||
layer.set.has(service.crud.getElementById(ids[3]) as any)
|
||||
)
|
||||
).toBe(1);
|
||||
expect(
|
||||
service.layer.layers.findIndex(layer =>
|
||||
layer.set.has(service.crud.getElementById(ids[2]) as any)
|
||||
)
|
||||
).toBe(2);
|
||||
|
||||
await wait();
|
||||
|
||||
service.crud.updateElement(ids[2], {
|
||||
index: service.layer.getReorderedIndex(
|
||||
service.crud.getElementById(ids[2])!,
|
||||
'backward'
|
||||
),
|
||||
});
|
||||
|
||||
expect(
|
||||
service.layer.layers.findIndex(layer =>
|
||||
layer.set.has(service.crud.getElementById(ids[3]) as any)
|
||||
)
|
||||
).toBe(3);
|
||||
expect(
|
||||
service.layer.layers.findIndex(layer =>
|
||||
layer.set.has(service.crud.getElementById(ids[2]) as any)
|
||||
)
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
test('back', async () => {
|
||||
service.crud.updateElement(ids[3], {
|
||||
index: service.layer.getReorderedIndex(
|
||||
service.crud.getElementById(ids[3])!,
|
||||
'back'
|
||||
),
|
||||
});
|
||||
|
||||
expect(
|
||||
service.layer.layers.findIndex(layer =>
|
||||
layer.set.has(service.crud.getElementById(ids[3]) as any)
|
||||
)
|
||||
).toBe(0);
|
||||
|
||||
await wait();
|
||||
|
||||
service.crud.updateElement(ids[2], {
|
||||
index: service.layer.getReorderedIndex(
|
||||
service.crud.getElementById(ids[2])!,
|
||||
'back'
|
||||
),
|
||||
});
|
||||
|
||||
expect(
|
||||
service.layer.layers.findIndex(layer =>
|
||||
layer.set.has(service.crud.getElementById(ids[2]) as any)
|
||||
)
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('group related functionality', () => {
|
||||
const createGroup = (
|
||||
service: EdgelessRootBlockComponent['service'],
|
||||
childIds: string[]
|
||||
) => {
|
||||
const children = new Y.Map<boolean>();
|
||||
childIds.forEach(id => children.set(id, true));
|
||||
|
||||
return service.crud.addElement('group', {
|
||||
children,
|
||||
});
|
||||
};
|
||||
|
||||
test("new added group should effect it children's layer", async () => {
|
||||
const edgeless = getDocRootBlock(doc, editor, 'edgeless');
|
||||
const elements = [
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!,
|
||||
addNote(doc),
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!,
|
||||
addNote(doc),
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!,
|
||||
];
|
||||
|
||||
await wait(0);
|
||||
expect(
|
||||
edgeless.querySelectorAll<HTMLCanvasElement>('.indexable-canvas').length
|
||||
).toBe(2);
|
||||
|
||||
Array.from(
|
||||
edgeless.querySelectorAll<HTMLCanvasElement>('.indexable-canvas')
|
||||
).forEach(canvas => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
expect(rect.width).toBeGreaterThan(0);
|
||||
expect(rect.height).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
createGroup(
|
||||
service,
|
||||
elements.filter((_, idx) => idx !== 1 && idx !== 3)
|
||||
);
|
||||
|
||||
expect(service.layer.layers.length).toBe(2);
|
||||
|
||||
expect(service.layer.layers[0].type).toBe('block');
|
||||
expect(service.layer.layers[0].set.size).toBe(2);
|
||||
|
||||
expect(service.layer.layers[1].type).toBe('canvas');
|
||||
expect(service.layer.layers[1].set.size).toBe(4);
|
||||
|
||||
expect(
|
||||
edgeless.querySelectorAll<HTMLCanvasElement>('.indexable-canvas').length
|
||||
).toBe(0);
|
||||
|
||||
const topCanvas = edgeless.querySelector(
|
||||
'affine-surface canvas'
|
||||
) as HTMLCanvasElement;
|
||||
|
||||
expect(
|
||||
Number(
|
||||
(
|
||||
edgeless.querySelector(
|
||||
`[data-block-id="${elements[1]}"]`
|
||||
) as HTMLElement
|
||||
).style.zIndex
|
||||
)
|
||||
).toBeLessThan(Number(topCanvas.style.zIndex));
|
||||
expect(
|
||||
Number(
|
||||
(
|
||||
edgeless.querySelector(
|
||||
`[data-block-id="${elements[3]}"]`
|
||||
) as HTMLElement
|
||||
).style.zIndex
|
||||
)
|
||||
).toBeLessThan(Number(topCanvas.style.zIndex));
|
||||
});
|
||||
|
||||
test("change group index should update its children's layer", () => {
|
||||
const elements = [
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!,
|
||||
addNote(doc),
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!,
|
||||
addNote(doc),
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!,
|
||||
];
|
||||
|
||||
const groupId = createGroup(
|
||||
service,
|
||||
elements.filter((_, idx) => idx !== 1 && idx !== 3)
|
||||
)!;
|
||||
const group = service.crud.getElementById(groupId)!;
|
||||
|
||||
expect(service.layer.layers.length).toBe(2);
|
||||
|
||||
group.index = service.layer.getReorderedIndex(group, 'back');
|
||||
expect(service.layer.layers[0].type).toBe('canvas');
|
||||
expect(service.layer.layers[0].set.size).toBe(4);
|
||||
expect(service.layer.layers[0].elements[0]).toBe(group);
|
||||
|
||||
group.index = service.layer.getReorderedIndex(group, 'front');
|
||||
expect(service.layer.layers[1].type).toBe('canvas');
|
||||
expect(service.layer.layers[1].set.size).toBe(4);
|
||||
expect(service.layer.layers[1].elements[0]).toBe(group);
|
||||
});
|
||||
|
||||
test('should keep relative index order of elements after group, ungroup, undo, redo', () => {
|
||||
const edgeless = getDocRootBlock(doc, editor, 'edgeless');
|
||||
const elementIds = [
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!,
|
||||
addNote(doc),
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!,
|
||||
addNote(doc),
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!,
|
||||
];
|
||||
service.doc.captureSync();
|
||||
const elements = elementIds.map(id => service.crud.getElementById(id)!);
|
||||
|
||||
const isKeptRelativeOrder = () => {
|
||||
return elements.every((element, idx) => {
|
||||
if (idx === 0) return true;
|
||||
return elements[idx - 1].index < element.index;
|
||||
});
|
||||
};
|
||||
|
||||
expect(isKeptRelativeOrder()).toBeTruthy();
|
||||
|
||||
const groupId = createGroup(edgeless.service, elementIds)!;
|
||||
expect(isKeptRelativeOrder()).toBeTruthy();
|
||||
|
||||
service.ungroup(service.crud.getElementById(groupId) as GroupElementModel);
|
||||
expect(isKeptRelativeOrder()).toBeTruthy();
|
||||
|
||||
service.doc.undo();
|
||||
expect(isKeptRelativeOrder()).toBeTruthy();
|
||||
|
||||
service.doc.redo();
|
||||
expect(isKeptRelativeOrder()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('compare function', () => {
|
||||
const SORT_ORDER = {
|
||||
AFTER: 1,
|
||||
BEFORE: -1,
|
||||
SAME: 0,
|
||||
};
|
||||
const createGroup = (
|
||||
service: EdgelessRootBlockComponent['service'],
|
||||
childIds: string[]
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
) => {
|
||||
const children = new Y.Map<boolean>();
|
||||
childIds.forEach(id => children.set(id, true));
|
||||
|
||||
return service.crud.addElement('group', {
|
||||
children,
|
||||
});
|
||||
};
|
||||
|
||||
test('compare same element', () => {
|
||||
const shapeId = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!;
|
||||
const shapeEl = service.crud.getElementById(shapeId)!;
|
||||
expect(service.layer.compare(shapeEl, shapeEl)).toBe(SORT_ORDER.SAME);
|
||||
|
||||
const groupId = createGroup(service, [shapeId])!;
|
||||
const groupEl = service.crud.getElementById(groupId)!;
|
||||
expect(service.layer.compare(groupEl, groupEl)).toBe(SORT_ORDER.SAME);
|
||||
|
||||
const noteId = addNote(doc);
|
||||
const note = service.crud.getElementById(noteId)! as NoteBlockModel;
|
||||
expect(service.layer.compare(note, note)).toBe(SORT_ORDER.SAME);
|
||||
});
|
||||
|
||||
test('compare a group and its child', () => {
|
||||
const shapeId = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!;
|
||||
const shapeEl = service.crud.getElementById(shapeId)!;
|
||||
const noteId = addNote(doc);
|
||||
const note = service.crud.getElementById(noteId)! as NoteBlockModel;
|
||||
const groupId = createGroup(service, [shapeId, noteId])!;
|
||||
const groupEl = service.crud.getElementById(groupId)!;
|
||||
|
||||
expect(service.layer.compare(groupEl, shapeEl)).toBe(SORT_ORDER.BEFORE);
|
||||
expect(service.layer.compare(shapeEl, groupEl)).toBe(SORT_ORDER.AFTER);
|
||||
expect(service.layer.compare(groupEl, note)).toBe(SORT_ORDER.BEFORE);
|
||||
expect(service.layer.compare(note, groupEl)).toBe(SORT_ORDER.AFTER);
|
||||
});
|
||||
|
||||
test('compare two different elements', () => {
|
||||
const shape1Id = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!;
|
||||
const shape1 = service.crud.getElementById(shape1Id)!;
|
||||
const shape2Id = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!;
|
||||
const shape2 = service.crud.getElementById(shape2Id)!;
|
||||
const note1Id = addNote(doc);
|
||||
|
||||
const note1 = service.crud.getElementById(note1Id)! as NoteBlockModel;
|
||||
const note2Id = addNote(doc);
|
||||
const note2 = service.crud.getElementById(note2Id)! as NoteBlockModel;
|
||||
|
||||
expect(service.layer.compare(shape1, shape2)).toBe(SORT_ORDER.BEFORE);
|
||||
expect(service.layer.compare(shape2, shape1)).toBe(SORT_ORDER.AFTER);
|
||||
|
||||
expect(service.layer.compare(note1, note2)).toBe(SORT_ORDER.BEFORE);
|
||||
expect(service.layer.compare(note2, note1)).toBe(SORT_ORDER.AFTER);
|
||||
|
||||
expect(service.layer.compare(shape1, note1)).toBe(SORT_ORDER.BEFORE);
|
||||
expect(service.layer.compare(note1, shape1)).toBe(SORT_ORDER.AFTER);
|
||||
|
||||
expect(service.layer.compare(shape2, note2)).toBe(SORT_ORDER.BEFORE);
|
||||
expect(service.layer.compare(note2, shape2)).toBe(SORT_ORDER.AFTER);
|
||||
});
|
||||
|
||||
test('compare nested elements', () => {
|
||||
const shape1Id = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!;
|
||||
const shape2Id = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!;
|
||||
const note1Id = addNote(doc);
|
||||
const note2Id = addNote(doc);
|
||||
const group1Id = createGroup(service, [
|
||||
shape1Id,
|
||||
shape2Id,
|
||||
note1Id,
|
||||
note2Id,
|
||||
])!;
|
||||
const group2Id = createGroup(service, [group1Id])!;
|
||||
|
||||
const shape1 = service.crud.getElementById(shape1Id)!;
|
||||
const shape2 = service.crud.getElementById(shape2Id)!;
|
||||
const note1 = service.crud.getElementById(note1Id)! as NoteBlockModel;
|
||||
const note2 = service.crud.getElementById(note2Id)! as NoteBlockModel;
|
||||
const group1 = service.crud.getElementById(group1Id)!;
|
||||
const group2 = service.crud.getElementById(group2Id)!;
|
||||
|
||||
// assert nested group to group
|
||||
expect(service.layer.compare(group2, group1)).toBe(SORT_ORDER.BEFORE);
|
||||
expect(service.layer.compare(group1, group2)).toBe(SORT_ORDER.AFTER);
|
||||
|
||||
// assert element in the same group
|
||||
expect(service.layer.compare(shape1, shape2)).toBe(SORT_ORDER.BEFORE);
|
||||
expect(service.layer.compare(shape2, shape1)).toBe(SORT_ORDER.AFTER);
|
||||
expect(service.layer.compare(note1, note2)).toBe(SORT_ORDER.BEFORE);
|
||||
expect(service.layer.compare(note2, note1)).toBe(SORT_ORDER.AFTER);
|
||||
|
||||
// assert group and its nested element
|
||||
expect(service.layer.compare(group2, shape1)).toBe(SORT_ORDER.BEFORE);
|
||||
expect(service.layer.compare(shape1, group2)).toBe(SORT_ORDER.AFTER);
|
||||
expect(service.layer.compare(group1, shape2)).toBe(SORT_ORDER.BEFORE);
|
||||
expect(service.layer.compare(shape2, group1)).toBe(SORT_ORDER.AFTER);
|
||||
expect(service.layer.compare(group2, note1)).toBe(SORT_ORDER.BEFORE);
|
||||
expect(service.layer.compare(note1, group2)).toBe(SORT_ORDER.AFTER);
|
||||
expect(service.layer.compare(group1, note2)).toBe(SORT_ORDER.BEFORE);
|
||||
expect(service.layer.compare(note2, group1)).toBe(SORT_ORDER.AFTER);
|
||||
});
|
||||
|
||||
test('compare two nested elements', () => {
|
||||
const groupAShapeId = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!;
|
||||
const groupANoteId = addNote(doc);
|
||||
const groupAId = createGroup(service, [
|
||||
createGroup(service, [groupAShapeId, groupANoteId])!,
|
||||
])!;
|
||||
const groupAShape = service.crud.getElementById(groupAShapeId)!;
|
||||
const groupANote = service.crud.getElementById(groupANoteId)!;
|
||||
const groupA = service.crud.getElementById(groupAId)!;
|
||||
|
||||
const groupBShapeId = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!;
|
||||
const groupBNoteId = addNote(doc);
|
||||
const groupBId = createGroup(service, [
|
||||
createGroup(service, [groupBShapeId, groupBNoteId])!,
|
||||
])!;
|
||||
const groupBShape = service.crud.getElementById(groupBShapeId)!;
|
||||
const groupBNote = service.crud.getElementById(groupBNoteId)!;
|
||||
const groupB = service.crud.getElementById(groupBId)!;
|
||||
|
||||
expect(service.layer.compare(groupAShape, groupBShape)).toBe(
|
||||
SORT_ORDER.BEFORE
|
||||
);
|
||||
expect(service.layer.compare(groupBShape, groupAShape)).toBe(
|
||||
SORT_ORDER.AFTER
|
||||
);
|
||||
expect(service.layer.compare(groupANote, groupBNote)).toBe(
|
||||
SORT_ORDER.BEFORE
|
||||
);
|
||||
expect(service.layer.compare(groupBNote, groupANote)).toBe(
|
||||
SORT_ORDER.AFTER
|
||||
);
|
||||
expect(service.layer.compare(groupB, groupA)).toBe(SORT_ORDER.AFTER);
|
||||
|
||||
groupB.index = service.layer.getReorderedIndex(groupB, 'back');
|
||||
expect(service.layer.compare(groupB, groupA)).toBe(SORT_ORDER.BEFORE);
|
||||
expect(service.layer.compare(groupAShape, groupBShape)).toBe(
|
||||
SORT_ORDER.AFTER
|
||||
);
|
||||
expect(service.layer.compare(groupBShape, groupAShape)).toBe(
|
||||
SORT_ORDER.BEFORE
|
||||
);
|
||||
expect(service.layer.compare(groupANote, groupBNote)).toBe(
|
||||
SORT_ORDER.AFTER
|
||||
);
|
||||
expect(service.layer.compare(groupBNote, groupANote)).toBe(
|
||||
SORT_ORDER.BEFORE
|
||||
);
|
||||
|
||||
groupA.index = service.layer.getReorderedIndex(groupA, 'back');
|
||||
expect(service.layer.compare(groupA, groupB)).toBe(SORT_ORDER.BEFORE);
|
||||
expect(service.layer.compare(groupAShape, groupBShape)).toBe(
|
||||
SORT_ORDER.BEFORE
|
||||
);
|
||||
expect(service.layer.compare(groupBShape, groupAShape)).toBe(
|
||||
SORT_ORDER.AFTER
|
||||
);
|
||||
expect(service.layer.compare(groupANote, groupBNote)).toBe(
|
||||
SORT_ORDER.BEFORE
|
||||
);
|
||||
expect(service.layer.compare(groupBNote, groupANote)).toBe(
|
||||
SORT_ORDER.AFTER
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('indexed canvas should be inserted into edgeless portal when switch to edgeless mode', async () => {
|
||||
let surface = getSurface(doc, editor);
|
||||
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!;
|
||||
|
||||
addNote(doc);
|
||||
|
||||
await wait();
|
||||
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!;
|
||||
|
||||
editor.mode = 'page';
|
||||
await wait();
|
||||
editor.mode = 'edgeless';
|
||||
await wait();
|
||||
|
||||
surface = getSurface(doc, editor);
|
||||
const edgeless = getDocRootBlock(doc, editor, 'edgeless');
|
||||
expect(edgeless.querySelectorAll('.indexable-canvas').length).toBe(1);
|
||||
|
||||
const indexedCanvas = edgeless.querySelectorAll(
|
||||
'.indexable-canvas'
|
||||
)[0] as HTMLCanvasElement;
|
||||
|
||||
expect(indexedCanvas.width).toBe(surface.renderer.canvas.width);
|
||||
expect(indexedCanvas.height).toBe(surface.renderer.canvas.height);
|
||||
expect(indexedCanvas.width).not.toBe(0);
|
||||
expect(indexedCanvas.height).not.toBe(0);
|
||||
});
|
||||
|
||||
test('the actual rendering z-index should satisfy the logic order of their indexes', async () => {
|
||||
editor.mode = 'page';
|
||||
|
||||
await wait();
|
||||
|
||||
const indexes = [
|
||||
'ao',
|
||||
'b0D',
|
||||
'ar',
|
||||
'as',
|
||||
'at',
|
||||
'au',
|
||||
'av',
|
||||
'b0Y',
|
||||
'b0V',
|
||||
'b0H',
|
||||
'b0M',
|
||||
'b0T',
|
||||
'b0f',
|
||||
'b0fV',
|
||||
'b0g',
|
||||
'b0i',
|
||||
'b0fl',
|
||||
];
|
||||
|
||||
indexes.forEach(index => {
|
||||
addNote(doc, {
|
||||
index,
|
||||
});
|
||||
});
|
||||
|
||||
await wait();
|
||||
|
||||
editor.mode = 'edgeless';
|
||||
await wait(500);
|
||||
|
||||
const edgeless = getDocRootBlock(doc, editor, 'edgeless');
|
||||
const blocks = Array.from(
|
||||
edgeless.querySelectorAll('gfx-viewport > [data-block-id]')
|
||||
) as BlockComponent[];
|
||||
|
||||
expect(blocks.length).toBe(indexes.length + 1);
|
||||
|
||||
blocks
|
||||
.filter(block => block.flavour !== 'affine:surface')
|
||||
.forEach((block, index) => {
|
||||
if (index === blocks.length - 1) return;
|
||||
|
||||
const model = block.model as BlockModel<{ index: string }>;
|
||||
const nextModel = blocks[index + 1].model as BlockModel<{
|
||||
index: string;
|
||||
}>;
|
||||
|
||||
const zIndex = Number(block.style.zIndex);
|
||||
const nextZIndex = Number(blocks[index + 1].style.zIndex);
|
||||
|
||||
expect(model.index <= nextModel.index).equals(zIndex <= nextZIndex);
|
||||
});
|
||||
});
|
||||
|
||||
describe('index generator', () => {
|
||||
let preinsertedShape: SurfaceElementModel;
|
||||
let preinsertedNote: NoteBlockModel;
|
||||
|
||||
beforeEach(() => {
|
||||
const shapeId = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
})!;
|
||||
const noteId = addNote(doc);
|
||||
|
||||
preinsertedShape = service.crud.getElementById(
|
||||
shapeId
|
||||
)! as SurfaceElementModel;
|
||||
preinsertedNote = service.crud.getElementById(noteId)! as NoteBlockModel;
|
||||
});
|
||||
|
||||
test('generator should remember the index it generated', () => {
|
||||
const generator = service.layer.createIndexGenerator();
|
||||
|
||||
const shape1 = generator();
|
||||
const block1 = generator();
|
||||
const shape2 = generator();
|
||||
const block2 = generator();
|
||||
|
||||
expect(block2 > shape2).toBeTruthy();
|
||||
expect(shape2 > block1).toBeTruthy();
|
||||
expect(block1 > shape1).toBeTruthy();
|
||||
expect(shape1 > preinsertedNote.index).toBeTruthy();
|
||||
expect(shape1 > preinsertedShape.index).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,468 @@
|
||||
import type { MindmapElementModel } from '@blocksuite/affine-model';
|
||||
import type { GfxController } from '@blocksuite/block-std/gfx';
|
||||
import { LayoutType, type MindMapView } from '@blocksuite/blocks';
|
||||
import { Bound } from '@blocksuite/global/utils';
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { click, pointermove, wait } from '../utils/common.js';
|
||||
import { getDocRootBlock } from '../utils/edgeless.js';
|
||||
import { setupEditor } from '../utils/setup.js';
|
||||
|
||||
describe('mindmap', () => {
|
||||
let gfx: GfxController;
|
||||
const moveAndClick = ({ x, y, w, h }: Bound) => {
|
||||
const { left, top } = gfx.viewport;
|
||||
x += left;
|
||||
y += top;
|
||||
|
||||
// trigger enter event
|
||||
pointermove(editor.host!, { x: x + w / 2, y: y + h / 2 });
|
||||
// trigger move event
|
||||
pointermove(editor.host!, { x: x + w / 2 + 1, y: y + h / 2 + 1 });
|
||||
click(editor.host!, { x: x + w / 2, y: y + h / 2 });
|
||||
};
|
||||
|
||||
const move = ({ x, y, w, h }: Bound) => {
|
||||
x += gfx.viewport.left;
|
||||
y += gfx.viewport.top;
|
||||
|
||||
pointermove(editor.host!, { x: x + w / 2, y: y + h / 2 });
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const cleanup = await setupEditor('edgeless');
|
||||
gfx = getDocRootBlock(window.doc, window.editor, 'edgeless').gfx;
|
||||
|
||||
return cleanup;
|
||||
});
|
||||
|
||||
test('delete the root node should remove all children', async () => {
|
||||
const tree = {
|
||||
text: 'root',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf1',
|
||||
},
|
||||
{
|
||||
text: 'leaf2',
|
||||
},
|
||||
{
|
||||
text: 'leaf3',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf4',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const mindmapId = gfx.surface!.addElement({
|
||||
type: 'mindmap',
|
||||
children: tree,
|
||||
});
|
||||
const mindmap = () => gfx.getElementById(mindmapId) as MindmapElementModel;
|
||||
|
||||
expect(gfx.surface!.elementModels.length).toBe(6);
|
||||
doc.captureSync();
|
||||
|
||||
gfx.deleteElement(mindmap().tree.element);
|
||||
await wait();
|
||||
expect(gfx.surface!.elementModels.length).toBe(0);
|
||||
doc.captureSync();
|
||||
await wait();
|
||||
|
||||
doc.undo();
|
||||
expect(gfx.surface!.elementModels.length).toBe(6);
|
||||
await wait();
|
||||
|
||||
gfx.deleteElement(mindmap().tree.children[2].element);
|
||||
await wait();
|
||||
expect(gfx.surface!.elementModels.length).toBe(4);
|
||||
await wait();
|
||||
|
||||
doc.undo();
|
||||
await wait();
|
||||
expect(gfx.surface!.elementModels.length).toBe(6);
|
||||
});
|
||||
|
||||
test('mindmap should layout automatically when creating', async () => {
|
||||
const tree = {
|
||||
text: 'root',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf1',
|
||||
},
|
||||
{
|
||||
text: 'leaf2',
|
||||
},
|
||||
{
|
||||
text: 'leaf3',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf4',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const mindmapId = gfx.surface!.addElement({
|
||||
type: 'mindmap',
|
||||
layoutType: LayoutType.RIGHT,
|
||||
children: tree,
|
||||
});
|
||||
const mindmap = () => gfx.getElementById(mindmapId) as MindmapElementModel;
|
||||
|
||||
doc.captureSync();
|
||||
await wait();
|
||||
|
||||
const root = mindmap().tree.element;
|
||||
const children = mindmap().tree.children.map(child => child.element);
|
||||
const leaf4 = mindmap().tree.children[2].children[0].element;
|
||||
|
||||
expect(children[0].x).greaterThan(root.x + root.w);
|
||||
expect(children[1].x).greaterThan(root.x + root.w);
|
||||
expect(children[2].x).greaterThan(root.x + root.w);
|
||||
|
||||
expect(children[1].y).greaterThan(children[0].y + children[0].h);
|
||||
expect(children[2].y).greaterThan(children[1].y + children[1].h);
|
||||
|
||||
expect(leaf4.x).greaterThan(children[2].x + children[2].w);
|
||||
});
|
||||
|
||||
test('deliberately creating a circular reference should be resolved correctly', async () => {
|
||||
const tree = {
|
||||
text: 'root',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf1',
|
||||
},
|
||||
{
|
||||
text: 'leaf2',
|
||||
},
|
||||
{
|
||||
text: 'leaf3',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf4',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const mindmapId = gfx.surface!.addElement({
|
||||
type: 'mindmap',
|
||||
layoutType: LayoutType.RIGHT,
|
||||
children: tree,
|
||||
});
|
||||
const mindmap = () => gfx.getElementById(mindmapId) as MindmapElementModel;
|
||||
|
||||
doc.captureSync();
|
||||
await wait();
|
||||
|
||||
// create a circular reference
|
||||
doc.transact(() => {
|
||||
const root = mindmap().tree;
|
||||
const leaf3 = root.children[2];
|
||||
const leaf4 = root.children[2].children[0];
|
||||
|
||||
mindmap().children.set(leaf3.id, {
|
||||
index: leaf3.detail.index,
|
||||
parent: leaf4.id,
|
||||
});
|
||||
});
|
||||
doc.captureSync();
|
||||
|
||||
await wait();
|
||||
|
||||
// the circular referenced node should be removed
|
||||
expect(mindmap().nodeMap.size).toBe(3);
|
||||
});
|
||||
|
||||
test('mindmap collapse and expand should work correctly', async () => {
|
||||
const tree = {
|
||||
text: 'root',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf1',
|
||||
},
|
||||
{
|
||||
text: 'leaf2',
|
||||
},
|
||||
{
|
||||
text: 'leaf3',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf4',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// click to active the editor
|
||||
click(editor.host!, { x: 50, y: 50 });
|
||||
|
||||
const mindmapId = gfx.surface!.addElement({
|
||||
type: 'mindmap',
|
||||
layoutType: LayoutType.RIGHT,
|
||||
children: tree,
|
||||
});
|
||||
const mindmap = () => gfx.getElementById(mindmapId) as MindmapElementModel;
|
||||
const mindmapView = () => gfx.view.get(mindmapId) as MindMapView;
|
||||
|
||||
doc.captureSync();
|
||||
await wait(100);
|
||||
|
||||
// collapse the root node
|
||||
{
|
||||
const rootButton = mindmapView().getCollapseButton(mindmap().tree)!;
|
||||
|
||||
moveAndClick(gfx.viewport.toViewBound(rootButton.elementBound));
|
||||
await wait(500);
|
||||
|
||||
expect(rootButton.hidden).toBe(false);
|
||||
expect(rootButton.opacity).toBe(1);
|
||||
expect(mindmap().tree.detail.collapsed).toBe(true);
|
||||
expect(mindmap().getNodeByPath([0, 0])!.element.hidden).toBe(true);
|
||||
expect(mindmap().getNodeByPath([0, 1])!.element.hidden).toBe(true);
|
||||
expect(mindmap().getNodeByPath([0, 2])!.element.hidden).toBe(true);
|
||||
expect(mindmap().getNodeByPath([0, 2, 0])!.element.hidden).toBe(true);
|
||||
|
||||
doc.captureSync();
|
||||
await wait();
|
||||
|
||||
doc.undo();
|
||||
await wait();
|
||||
|
||||
expect(mindmap().tree.detail.collapsed).toBe(undefined);
|
||||
expect(mindmap().getNodeByPath([0, 0])!.element.hidden).toBe(false);
|
||||
expect(mindmap().getNodeByPath([0, 1])!.element.hidden).toBe(false);
|
||||
expect(mindmap().getNodeByPath([0, 2])!.element.hidden).toBe(false);
|
||||
expect(mindmap().getNodeByPath([0, 2, 0])!.element.hidden).toBe(false);
|
||||
}
|
||||
|
||||
// collapse a child node
|
||||
{
|
||||
const node = mindmap().getNodeByPath([0, 2])!;
|
||||
const childButton = mindmapView().getCollapseButton(node)!;
|
||||
|
||||
moveAndClick(gfx.viewport.toViewBound(childButton.elementBound));
|
||||
await wait(500);
|
||||
|
||||
expect(childButton.hidden).toBe(false);
|
||||
expect(childButton.opacity).toBe(1);
|
||||
|
||||
expect(mindmap().getNodeByPath([0, 2])!.element.hidden).toBe(false);
|
||||
expect(mindmap().getNodeByPath([0, 2])!.detail.collapsed).toBe(true);
|
||||
expect(mindmap().getNodeByPath([0, 2, 0])!.element.hidden).toBe(true);
|
||||
|
||||
doc.captureSync();
|
||||
await wait();
|
||||
|
||||
doc.undo();
|
||||
await wait();
|
||||
|
||||
expect(mindmap().getNodeByPath([0, 2])!.detail.collapsed).toBe(undefined);
|
||||
expect(mindmap().getNodeByPath([0, 2, 0])!.element.hidden).toBe(false);
|
||||
}
|
||||
|
||||
// collapse root node and collapse a child node
|
||||
{
|
||||
const childButton = mindmapView().getCollapseButton(
|
||||
mindmap().getNodeByPath([0, 2])!
|
||||
)!;
|
||||
// collapse child node
|
||||
moveAndClick(gfx.viewport.toViewBound(childButton.elementBound));
|
||||
await wait(500);
|
||||
|
||||
doc.captureSync();
|
||||
await wait();
|
||||
|
||||
const rootButton = mindmapView().getCollapseButton(mindmap().tree)!;
|
||||
// collapse root node
|
||||
moveAndClick(gfx.viewport.toViewBound(rootButton.elementBound));
|
||||
await wait(500);
|
||||
|
||||
// child button should be hidden
|
||||
expect(childButton.hidden).toBe(true);
|
||||
expect(childButton.opacity).toBe(0);
|
||||
|
||||
// expand root node
|
||||
doc.undo();
|
||||
await wait();
|
||||
|
||||
// child button should be visible
|
||||
expect(childButton.hidden).toBe(false);
|
||||
expect(childButton.opacity).toBe(1);
|
||||
|
||||
// child nodes should still be collapsed
|
||||
expect(mindmap().getNodeByPath([0, 2])!.detail.collapsed).toBe(true);
|
||||
expect(mindmap().getNodeByPath([0, 2, 0])!.element.hidden).toBe(true);
|
||||
|
||||
// expand child node
|
||||
doc.undo();
|
||||
await wait();
|
||||
|
||||
// child button should be visible
|
||||
expect(mindmap().getNodeByPath([0, 2])!.detail.collapsed).toBe(undefined);
|
||||
expect(mindmap().getNodeByPath([0, 2, 0])!.element.hidden).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("selected node's collapse button should be visible", async () => {
|
||||
const tree = {
|
||||
text: 'root',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf1',
|
||||
},
|
||||
{
|
||||
text: 'leaf2',
|
||||
},
|
||||
{
|
||||
text: 'leaf3',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf4',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// click to active the editor
|
||||
click(editor.host!, { x: 50, y: 50 });
|
||||
|
||||
const mindmapId = gfx.surface!.addElement({
|
||||
type: 'mindmap',
|
||||
layoutType: LayoutType.RIGHT,
|
||||
children: tree,
|
||||
});
|
||||
const mindmap = () => gfx.getElementById(mindmapId) as MindmapElementModel;
|
||||
const mindmapView = () => gfx.view.get(mindmapId) as MindMapView;
|
||||
await wait();
|
||||
|
||||
gfx.selection.set({ elements: [mindmap().tree.id] });
|
||||
const rootButton = mindmapView().getCollapseButton(mindmap().tree)!;
|
||||
expect(rootButton.hidden).toBe(false);
|
||||
expect(rootButton.opacity).toBe(1);
|
||||
|
||||
gfx.selection.set({ elements: [mindmap().getNodeByPath([0, 2])!.id] });
|
||||
const childButton = mindmapView().getCollapseButton(
|
||||
mindmap().getNodeByPath([0, 2])!
|
||||
)!;
|
||||
expect(childButton.hidden).toBe(false);
|
||||
expect(childButton.opacity).toBe(1);
|
||||
});
|
||||
|
||||
test('move near to the collapsed button should show the button', async () => {
|
||||
const tree = {
|
||||
text: 'root',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf1',
|
||||
},
|
||||
{
|
||||
text: 'leaf2',
|
||||
},
|
||||
{
|
||||
text: 'leaf3',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf4',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// click to active the editor
|
||||
click(editor.host!, { x: 50, y: 50 });
|
||||
|
||||
const mindmapId = gfx.surface!.addElement({
|
||||
type: 'mindmap',
|
||||
layoutType: LayoutType.RIGHT,
|
||||
children: tree,
|
||||
});
|
||||
const mindmap = () => gfx.getElementById(mindmapId) as MindmapElementModel;
|
||||
const mindmapView = () => gfx.view.get(mindmapId) as MindMapView;
|
||||
await wait();
|
||||
|
||||
const rootButton = mindmapView().getCollapseButton(mindmap().tree)!;
|
||||
move(gfx.viewport.toViewBound(rootButton.elementBound).moveDelta(10, 10));
|
||||
await wait();
|
||||
expect(rootButton.opacity).toBe(1);
|
||||
|
||||
const childButton = mindmapView().getCollapseButton(
|
||||
mindmap().getNodeByPath([0, 2])!
|
||||
)!;
|
||||
move(gfx.viewport.toViewBound(childButton.elementBound).moveDelta(10, 10));
|
||||
await wait();
|
||||
expect(childButton.opacity).toBe(1);
|
||||
|
||||
move(new Bound(0, 0, 0, 0));
|
||||
await wait();
|
||||
|
||||
expect(childButton.opacity).toBe(0);
|
||||
expect(rootButton.opacity).toBe(0);
|
||||
});
|
||||
|
||||
test("collapsed node's button should be always visible except its ancestor is collapsed", async () => {
|
||||
const tree = {
|
||||
text: 'root',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf1',
|
||||
},
|
||||
{
|
||||
text: 'leaf2',
|
||||
},
|
||||
{
|
||||
text: 'leaf3',
|
||||
children: [
|
||||
{
|
||||
text: 'leaf4',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// click to active the editor
|
||||
click(editor.host!, { x: 50, y: 50 });
|
||||
|
||||
const mindmapId = gfx.surface!.addElement({
|
||||
type: 'mindmap',
|
||||
layoutType: LayoutType.RIGHT,
|
||||
children: tree,
|
||||
});
|
||||
const mindmap = () => gfx.getElementById(mindmapId) as MindmapElementModel;
|
||||
const mindmapView = () => gfx.view.get(mindmapId) as MindMapView;
|
||||
|
||||
doc.captureSync();
|
||||
await wait();
|
||||
|
||||
const childButton = mindmapView().getCollapseButton(
|
||||
mindmap().getNodeByPath([0, 2])!
|
||||
)!;
|
||||
// collapse the child node
|
||||
moveAndClick(gfx.viewport.toViewBound(childButton.elementBound));
|
||||
// move out of the button, the button should still be visible
|
||||
move(new Bound(0, 0, 0, 0));
|
||||
await wait();
|
||||
expect(childButton.hidden).toBe(false);
|
||||
expect(childButton.opacity).toBe(1);
|
||||
|
||||
const rootButton = mindmapView().getCollapseButton(mindmap().tree)!;
|
||||
// collapse the root node
|
||||
moveAndClick(gfx.viewport.toViewBound(rootButton.elementBound));
|
||||
// move out of the button, the root button should be visible
|
||||
move(new Bound(0, 0, 0, 0));
|
||||
await wait();
|
||||
expect(rootButton.hidden).toBe(false);
|
||||
expect(rootButton.opacity).toBe(1);
|
||||
// the collapsed child button should be hidden
|
||||
expect(childButton.hidden).toBe(true);
|
||||
expect(childButton.opacity).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,519 @@
|
||||
import type {
|
||||
BrushElementModel,
|
||||
GroupElementModel,
|
||||
ShapeElementModel,
|
||||
SurfaceBlockModel,
|
||||
} from '@blocksuite/blocks';
|
||||
import { DefaultTheme } from '@blocksuite/blocks';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { wait } from '../utils/common.js';
|
||||
import { setupEditor } from '../utils/setup.js';
|
||||
|
||||
let model: SurfaceBlockModel;
|
||||
|
||||
beforeEach(async () => {
|
||||
const cleanup = await setupEditor('edgeless');
|
||||
const models = doc.getBlockByFlavour('affine:surface') as SurfaceBlockModel[];
|
||||
|
||||
model = models[0];
|
||||
|
||||
return cleanup;
|
||||
});
|
||||
|
||||
describe('elements management', () => {
|
||||
test('addElement should work correctly', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
|
||||
expect(model.elementModels[0].id).toBe(id);
|
||||
});
|
||||
|
||||
test('removeElement should work correctly', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
|
||||
model.deleteElement(id);
|
||||
|
||||
expect(model.elementModels.length).toBe(0);
|
||||
});
|
||||
|
||||
test('updateElement should work correctly', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
|
||||
model.updateElement(id, { xywh: '[10,10,200,200]' });
|
||||
|
||||
expect(model.elementModels[0].xywh).toBe('[10,10,200,200]');
|
||||
});
|
||||
|
||||
test('getElementById should return element', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
|
||||
expect(model.getElementById(id)).not.toBeNull();
|
||||
});
|
||||
|
||||
test('getElementById should return null if not found', () => {
|
||||
expect(model.getElementById('not-found')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('element model', () => {
|
||||
test('default value should work correctly', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
|
||||
const element = model.getElementById(id)! as ShapeElementModel;
|
||||
|
||||
expect(element.index).toBe('a0');
|
||||
expect(element.strokeColor).toBe(DefaultTheme.shapeStrokeColor);
|
||||
expect(element.strokeWidth).toBe(4);
|
||||
});
|
||||
|
||||
test('defined prop should not be overwritten by default value', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
strokeColor: '--affine-palette-line-black',
|
||||
});
|
||||
|
||||
const element = model.getElementById(id)! as ShapeElementModel;
|
||||
|
||||
expect(element.strokeColor).toBe('--affine-palette-line-black');
|
||||
});
|
||||
|
||||
test('assign value to model property should update ymap directly', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
|
||||
const element = model.getElementById(id)! as ShapeElementModel;
|
||||
|
||||
expect(element.yMap.get('strokeColor')).toBe(DefaultTheme.shapeStrokeColor);
|
||||
|
||||
element.strokeColor = '--affine-palette-line-black';
|
||||
expect(element.yMap.get('strokeColor')).toBe('--affine-palette-line-black');
|
||||
expect(element.strokeColor).toBe('--affine-palette-line-black');
|
||||
});
|
||||
});
|
||||
|
||||
describe('group', () => {
|
||||
test('should get group', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
const id2 = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
|
||||
const groupId = model.addElement({
|
||||
type: 'group',
|
||||
children: {
|
||||
[id]: true,
|
||||
[id2]: true,
|
||||
},
|
||||
});
|
||||
const group = model.getElementById(groupId);
|
||||
const shape = model.getElementById(id)!;
|
||||
const shape2 = model.getElementById(id2)!;
|
||||
|
||||
expect(group).not.toBe(null);
|
||||
expect(model.getGroup(id)).toBe(group);
|
||||
expect(model.getGroup(id2)).toBe(group);
|
||||
expect(shape.group).toBe(group);
|
||||
expect(shape2.group).toBe(group);
|
||||
});
|
||||
|
||||
test('should return null if children property is updated', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
const id2 = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
const id3 = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
|
||||
const groupId = model.addElement({
|
||||
type: 'group',
|
||||
children: {
|
||||
[id]: true,
|
||||
[id2]: true,
|
||||
[id3]: true,
|
||||
},
|
||||
});
|
||||
const group = model.getElementById(groupId) as GroupElementModel;
|
||||
|
||||
model.doc.transact(() => {
|
||||
group.children.delete(id);
|
||||
group.children.delete(id2);
|
||||
});
|
||||
|
||||
expect(model.getElementById(groupId)).toBe(group);
|
||||
expect(model.getGroup(id)).toBeNull();
|
||||
expect(model.getGroup(id2)).toBeNull();
|
||||
});
|
||||
|
||||
test('should return null if group are deleted', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
const id2 = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
|
||||
const groupId = model.addElement({
|
||||
type: 'group',
|
||||
children: {
|
||||
[id]: true,
|
||||
[id2]: true,
|
||||
},
|
||||
});
|
||||
|
||||
model.deleteElement(groupId);
|
||||
expect(model.getGroup(id)).toBeNull();
|
||||
expect(model.getGroup(id2)).toBeNull();
|
||||
});
|
||||
|
||||
test('children can be updated with a plain object', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
const id2 = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
|
||||
const groupId = model.addElement({
|
||||
type: 'group',
|
||||
children: {
|
||||
[id]: true,
|
||||
[id2]: true,
|
||||
},
|
||||
});
|
||||
const group = model.getElementById(groupId) as GroupElementModel;
|
||||
|
||||
model.updateElement(groupId, {
|
||||
children: {
|
||||
[id]: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(group.childIds).toEqual([id]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('connector', () => {
|
||||
test('should get connector', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
const id2 = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
const connectorId = model.addElement({
|
||||
type: 'connector',
|
||||
source: {
|
||||
id,
|
||||
},
|
||||
target: {
|
||||
id: id2,
|
||||
},
|
||||
});
|
||||
const connector = model.getElementById(connectorId)!;
|
||||
|
||||
expect(model.getConnectors(id).map(el => el.id)).toEqual([connector.id]);
|
||||
expect(model.getConnectors(id2).map(el => el.id)).toEqual([connector.id]);
|
||||
});
|
||||
|
||||
test('multiple connectors are supported', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
const id2 = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
const connectorId = model.addElement({
|
||||
type: 'connector',
|
||||
source: {
|
||||
id,
|
||||
},
|
||||
target: {
|
||||
id: id2,
|
||||
},
|
||||
});
|
||||
const connectorId2 = model.addElement({
|
||||
type: 'connector',
|
||||
source: {
|
||||
id,
|
||||
},
|
||||
target: {
|
||||
id: id2,
|
||||
},
|
||||
});
|
||||
|
||||
const connector = model.getElementById(connectorId)!;
|
||||
const connector2 = model.getElementById(connectorId2)!;
|
||||
const connectors = [connector.id, connector2.id];
|
||||
|
||||
expect(model.getConnectors(id).map(c => c.id)).toEqual(connectors);
|
||||
expect(model.getConnectors(id2).map(c => c.id)).toEqual(connectors);
|
||||
});
|
||||
|
||||
test('should return null if connector are updated', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
const id2 = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
const connectorId = model.addElement({
|
||||
type: 'connector',
|
||||
source: {
|
||||
id,
|
||||
},
|
||||
target: {
|
||||
id: id2,
|
||||
},
|
||||
});
|
||||
|
||||
model.updateElement(connectorId, {
|
||||
source: {
|
||||
position: [0, 0],
|
||||
},
|
||||
target: {
|
||||
position: [0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
expect(model.getConnectors(id)).toEqual([]);
|
||||
expect(model.getConnectors(id2)).toEqual([]);
|
||||
});
|
||||
|
||||
test('should return null if connector are deleted', async () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
const id2 = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
const connectorId = model.addElement({
|
||||
type: 'connector',
|
||||
source: {
|
||||
id,
|
||||
},
|
||||
target: {
|
||||
id: id2,
|
||||
},
|
||||
});
|
||||
|
||||
model.deleteElement(connectorId);
|
||||
|
||||
await wait();
|
||||
|
||||
expect(model.getConnectors(id)).toEqual([]);
|
||||
expect(model.getConnectors(id2)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stash/pop', () => {
|
||||
test('stash and pop should work correctly', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
strokeWidth: 4,
|
||||
});
|
||||
const elementModel = model.getElementById(id)! as ShapeElementModel;
|
||||
|
||||
expect(elementModel.strokeWidth).toBe(4);
|
||||
|
||||
elementModel.stash('strokeWidth');
|
||||
elementModel.strokeWidth = 10;
|
||||
expect(elementModel.strokeWidth).toBe(10);
|
||||
expect(elementModel.yMap.get('strokeWidth')).toBe(4);
|
||||
|
||||
elementModel.pop('strokeWidth');
|
||||
expect(elementModel.strokeWidth).toBe(10);
|
||||
expect(elementModel.yMap.get('strokeWidth')).toBe(10);
|
||||
|
||||
elementModel.strokeWidth = 6;
|
||||
expect(elementModel.strokeWidth).toBe(6);
|
||||
expect(elementModel.yMap.get('strokeWidth')).toBe(6);
|
||||
});
|
||||
|
||||
test('assign stashed property should emit event', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
strokeWidth: 4,
|
||||
});
|
||||
const elementModel = model.getElementById(id)! as ShapeElementModel;
|
||||
|
||||
elementModel.stash('strokeWidth');
|
||||
|
||||
const onchange = vi.fn();
|
||||
model.elementUpdated.once(({ id }) => onchange(id));
|
||||
|
||||
elementModel.strokeWidth = 10;
|
||||
expect(onchange).toHaveBeenCalledWith(id);
|
||||
});
|
||||
|
||||
test('stashed property should also trigger derive decorator', () => {
|
||||
const id = model.addElement({
|
||||
type: 'brush',
|
||||
points: [
|
||||
[0, 0],
|
||||
[100, 100],
|
||||
[120, 150],
|
||||
],
|
||||
});
|
||||
const elementModel = model.getElementById(id)! as BrushElementModel;
|
||||
|
||||
elementModel.stash('points');
|
||||
elementModel.points = [
|
||||
[0, 0],
|
||||
[50, 50],
|
||||
[135, 145],
|
||||
[150, 170],
|
||||
[200, 180],
|
||||
];
|
||||
const points = elementModel.points;
|
||||
|
||||
expect(elementModel.w).toBe(200 + elementModel.lineWidth);
|
||||
expect(elementModel.h).toBe(180 + elementModel.lineWidth);
|
||||
|
||||
expect(elementModel.yMap.get('points')).not.toEqual(points);
|
||||
expect(elementModel.w).toBe(200 + elementModel.lineWidth);
|
||||
expect(elementModel.h).toBe(180 + elementModel.lineWidth);
|
||||
});
|
||||
|
||||
test('non-field property should not allow stash/pop, and should failed silently ', () => {
|
||||
const id = model.addElement({
|
||||
type: 'group',
|
||||
});
|
||||
const elementModel = model.getElementById(id)! as GroupElementModel;
|
||||
|
||||
elementModel.stash('xywh');
|
||||
elementModel.xywh = '[10,10,200,200]';
|
||||
|
||||
expect(elementModel['_stashed'].has('xywh')).toBe(false);
|
||||
|
||||
elementModel.pop('xywh');
|
||||
|
||||
expect(elementModel['_stashed'].has('xywh')).toBe(false);
|
||||
expect(elementModel.yMap.has('xywh')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('derive decorator', () => {
|
||||
test('derived decorator should work correctly', () => {
|
||||
const id = model.addElement({
|
||||
type: 'brush',
|
||||
points: [
|
||||
[0, 0],
|
||||
[100, 100],
|
||||
[120, 150],
|
||||
],
|
||||
});
|
||||
const elementModel = model.getElementById(id)! as BrushElementModel;
|
||||
|
||||
expect(elementModel.w).toBe(120 + elementModel.lineWidth);
|
||||
expect(elementModel.h).toBe(150 + elementModel.lineWidth);
|
||||
});
|
||||
});
|
||||
|
||||
describe('local decorator', () => {
|
||||
test('local decorator should work correctly', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
const elementModel = model.getElementById(id)! as BrushElementModel;
|
||||
|
||||
expect(elementModel.display).toBe(true);
|
||||
|
||||
elementModel.display = false;
|
||||
expect(elementModel.display).toBe(false);
|
||||
|
||||
elementModel.opacity = 0.5;
|
||||
expect(elementModel.opacity).toBe(0.5);
|
||||
});
|
||||
|
||||
test('assign local property should emit event', () => {
|
||||
const id = model.addElement({
|
||||
type: 'shape',
|
||||
});
|
||||
const elementModel = model.getElementById(id)! as BrushElementModel;
|
||||
|
||||
const onchange = vi.fn();
|
||||
|
||||
model.elementUpdated.once(({ id }) => onchange(id));
|
||||
elementModel.display = false;
|
||||
|
||||
expect(elementModel.display).toBe(false);
|
||||
expect(onchange).toHaveBeenCalledWith(id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convert decorator', () => {
|
||||
test('convert decorator', () => {
|
||||
const id = model.addElement({
|
||||
type: 'brush',
|
||||
points: [
|
||||
[50, 25],
|
||||
[200, 200],
|
||||
[300, 300],
|
||||
],
|
||||
});
|
||||
const elementModel = model.getElementById(id)! as BrushElementModel;
|
||||
const halfLineWidth = elementModel.lineWidth / 2;
|
||||
const xOffset = 50 - halfLineWidth;
|
||||
const yOffset = 25 - halfLineWidth;
|
||||
|
||||
expect(elementModel.points).toEqual([
|
||||
[50 - xOffset, 25 - yOffset],
|
||||
[200 - xOffset, 200 - yOffset],
|
||||
[300 - xOffset, 300 - yOffset],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('basic property', () => {
|
||||
test('empty group should have all zero xywh', () => {
|
||||
const id = model.addElement({
|
||||
type: 'group',
|
||||
});
|
||||
const group = model.getElementById(id)! as GroupElementModel;
|
||||
|
||||
expect(group.x).toBe(0);
|
||||
expect(group.y).toBe(0);
|
||||
expect(group.w).toBe(0);
|
||||
expect(group.h).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('brush', () => {
|
||||
test('same lineWidth should have same xywh', () => {
|
||||
const id = model.addElement({
|
||||
type: 'brush',
|
||||
lineWidth: 2,
|
||||
points: [
|
||||
[0, 0],
|
||||
[100, 100],
|
||||
[120, 150],
|
||||
],
|
||||
});
|
||||
const brush = model.getElementById(id) as BrushElementModel;
|
||||
const oldBrushXYWH = brush.xywh;
|
||||
|
||||
brush.lineWidth = 4;
|
||||
|
||||
expect(brush.xywh).not.toBe(oldBrushXYWH);
|
||||
|
||||
brush.lineWidth = 2;
|
||||
|
||||
expect(brush.xywh).toBe(oldBrushXYWH);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,307 @@
|
||||
import {
|
||||
type EdgelessRootBlockComponent,
|
||||
EdgelessRootService,
|
||||
type FrameBlockComponent,
|
||||
type SurfaceRefBlockComponent,
|
||||
} from '@blocksuite/blocks';
|
||||
import type { DocSnapshot } from '@blocksuite/store';
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { wait } from '../utils/common.js';
|
||||
import { addNote, getDocRootBlock } from '../utils/edgeless.js';
|
||||
import { importFromSnapshot } from '../utils/misc.js';
|
||||
import { setupEditor } from '../utils/setup.js';
|
||||
|
||||
describe('basic', () => {
|
||||
let service: EdgelessRootBlockComponent['service'];
|
||||
let edgelessRoot: EdgelessRootBlockComponent;
|
||||
let noteAId = '';
|
||||
let noteBId = '';
|
||||
let shapeAId = '';
|
||||
let shapeBId = '';
|
||||
let frameId = '';
|
||||
|
||||
beforeEach(async () => {
|
||||
const cleanup = await setupEditor('edgeless');
|
||||
edgelessRoot = getDocRootBlock(doc, editor, 'edgeless');
|
||||
service = edgelessRoot.service;
|
||||
|
||||
noteAId = addNote(doc, {
|
||||
index: service.generateIndex(),
|
||||
});
|
||||
shapeAId = service.crud.addElement('shape', {
|
||||
type: 'rect',
|
||||
xywh: '[0, 0, 100, 100]',
|
||||
index: service.generateIndex(),
|
||||
})!;
|
||||
noteBId = addNote(doc, {
|
||||
index: service.generateIndex(),
|
||||
});
|
||||
shapeBId = service.crud.addElement('shape', {
|
||||
type: 'rect',
|
||||
xywh: '[100, 0, 100, 100]',
|
||||
index: service.generateIndex(),
|
||||
})!;
|
||||
frameId = service.crud.addBlock(
|
||||
'affine:frame',
|
||||
{
|
||||
xywh: '[0, 0, 800, 200]',
|
||||
index: service.generateIndex(),
|
||||
},
|
||||
service.surface.id
|
||||
);
|
||||
|
||||
return cleanup;
|
||||
});
|
||||
|
||||
test('surface-ref should be rendered in page mode', async () => {
|
||||
const surfaceRefId = doc.addBlock(
|
||||
'affine:surface-ref',
|
||||
{
|
||||
reference: frameId,
|
||||
},
|
||||
noteAId
|
||||
);
|
||||
|
||||
editor.mode = 'page';
|
||||
await wait();
|
||||
|
||||
expect(
|
||||
document.querySelector(
|
||||
`affine-surface-ref[data-block-id="${surfaceRefId}"]`
|
||||
)
|
||||
).instanceOf(Element);
|
||||
});
|
||||
|
||||
test('surface-ref should be rendered as empty surface-ref-block-edgeless component page mode', async () => {
|
||||
const surfaceRefId = doc.addBlock(
|
||||
'affine:surface-ref',
|
||||
{
|
||||
reference: frameId,
|
||||
},
|
||||
noteAId
|
||||
);
|
||||
|
||||
await wait();
|
||||
|
||||
const refBlock = document.querySelector(
|
||||
`affine-edgeless-surface-ref[data-block-id="${surfaceRefId}"]`
|
||||
)! as HTMLElement;
|
||||
|
||||
expect(refBlock).instanceOf(Element);
|
||||
expect(refBlock.innerText).toBe('');
|
||||
});
|
||||
|
||||
test('content in frame should be rendered in the correct order', async () => {
|
||||
const surfaceRefId = doc.addBlock(
|
||||
'affine:surface-ref',
|
||||
{
|
||||
reference: frameId,
|
||||
},
|
||||
noteAId
|
||||
);
|
||||
|
||||
editor.mode = 'page';
|
||||
await wait();
|
||||
|
||||
const surfaceRef = document.querySelector(
|
||||
`affine-surface-ref[data-block-id="${surfaceRefId}"]`
|
||||
) as HTMLElement;
|
||||
const refBlocks = Array.from(
|
||||
surfaceRef.querySelectorAll('affine-edgeless-note')
|
||||
) as HTMLElement[];
|
||||
const stackingCanvas = Array.from(
|
||||
surfaceRef.querySelectorAll('.indexable-canvas')!
|
||||
) as HTMLCanvasElement[];
|
||||
|
||||
expect(refBlocks.length).toBe(2);
|
||||
expect(stackingCanvas.length).toBe(2);
|
||||
expect(stackingCanvas[0].style.zIndex > refBlocks[0].style.zIndex).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('content in group should be rendered in the correct order', async () => {
|
||||
const groupId = service.crud.addElement('group', {
|
||||
children: {
|
||||
[shapeAId]: true,
|
||||
[shapeBId]: true,
|
||||
[noteAId]: true,
|
||||
[noteBId]: true,
|
||||
},
|
||||
});
|
||||
const surfaceRefId = doc.addBlock(
|
||||
'affine:surface-ref',
|
||||
{
|
||||
reference: groupId,
|
||||
},
|
||||
noteAId
|
||||
);
|
||||
|
||||
editor.mode = 'page';
|
||||
await wait();
|
||||
|
||||
const surfaceRef = document.querySelector(
|
||||
`affine-surface-ref[data-block-id="${surfaceRefId}"]`
|
||||
) as HTMLElement;
|
||||
const refBlocks = Array.from(
|
||||
surfaceRef.querySelectorAll('affine-edgeless-note')
|
||||
) as HTMLElement[];
|
||||
const stackingCanvas = Array.from(
|
||||
surfaceRef.querySelectorAll('.indexable-canvas')
|
||||
) as HTMLCanvasElement[];
|
||||
|
||||
expect(refBlocks.length).toBe(2);
|
||||
expect(stackingCanvas.length).toBe(2);
|
||||
expect(stackingCanvas[1].style.zIndex > refBlocks[0].style.zIndex).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('frame should be rendered in surface-ref viewport', async () => {
|
||||
const surfaceRefId = doc.addBlock(
|
||||
'affine:surface-ref',
|
||||
{
|
||||
reference: frameId,
|
||||
},
|
||||
noteAId
|
||||
);
|
||||
|
||||
editor.mode = 'page';
|
||||
await wait();
|
||||
|
||||
const surfaceRef = document.querySelector(
|
||||
`affine-surface-ref[data-block-id="${surfaceRefId}"]`
|
||||
) as SurfaceRefBlockComponent;
|
||||
|
||||
const edgeless = surfaceRef.previewEditor!.std.get(EdgelessRootService);
|
||||
|
||||
const frame = surfaceRef.querySelector(
|
||||
'affine-frame'
|
||||
) as FrameBlockComponent;
|
||||
|
||||
expect(
|
||||
edgeless.viewport.isInViewport(frame.model.elementBound)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('group should be rendered in surface-ref viewport', async () => {
|
||||
const groupId = service.crud.addElement('group', {
|
||||
children: {
|
||||
[shapeAId]: true,
|
||||
[shapeBId]: true,
|
||||
[noteAId]: true,
|
||||
[noteBId]: true,
|
||||
},
|
||||
})!;
|
||||
const surfaceRefId = doc.addBlock(
|
||||
'affine:surface-ref',
|
||||
{
|
||||
reference: groupId,
|
||||
},
|
||||
noteAId
|
||||
);
|
||||
|
||||
editor.mode = 'page';
|
||||
await wait();
|
||||
|
||||
const surfaceRef = document.querySelector(
|
||||
`affine-surface-ref[data-block-id="${surfaceRefId}"]`
|
||||
) as SurfaceRefBlockComponent;
|
||||
|
||||
const edgeless = surfaceRef.previewEditor!.std.get(EdgelessRootService);
|
||||
|
||||
const group = edgeless.crud.getElementById(groupId)!;
|
||||
|
||||
expect(edgeless.viewport.isInViewport(group.elementBound)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('viewport of surface-ref should be updated when the reference xywh updated', async () => {
|
||||
const surfaceRefId = doc.addBlock(
|
||||
'affine:surface-ref',
|
||||
{
|
||||
reference: frameId,
|
||||
},
|
||||
noteAId
|
||||
);
|
||||
|
||||
editor.mode = 'page';
|
||||
await wait();
|
||||
|
||||
const surfaceRef = document.querySelector(
|
||||
`affine-surface-ref[data-block-id="${surfaceRefId}"]`
|
||||
) as SurfaceRefBlockComponent;
|
||||
|
||||
const edgeless = surfaceRef.previewEditor!.std.get(EdgelessRootService);
|
||||
|
||||
const frame = surfaceRef.querySelector(
|
||||
'affine-frame'
|
||||
) as FrameBlockComponent;
|
||||
|
||||
const oldViewport = edgeless.viewport.viewportBounds;
|
||||
|
||||
frame.model.xywh = '[100, 100, 800, 200]';
|
||||
await wait();
|
||||
|
||||
expect(edgeless.viewport.viewportBounds).not.toEqual(oldViewport);
|
||||
});
|
||||
|
||||
test('view in edgeless mode button', async () => {
|
||||
const groupId = service.crud.addElement('group', {
|
||||
children: {
|
||||
[shapeAId]: true,
|
||||
[shapeBId]: true,
|
||||
[noteAId]: true,
|
||||
[noteBId]: true,
|
||||
},
|
||||
});
|
||||
const surfaceRefId = doc.addBlock(
|
||||
'affine:surface-ref',
|
||||
{
|
||||
reference: groupId,
|
||||
},
|
||||
noteAId
|
||||
);
|
||||
|
||||
editor.mode = 'page';
|
||||
await wait();
|
||||
|
||||
const surfaceRef = document.querySelector(
|
||||
`affine-surface-ref[data-block-id="${surfaceRefId}"]`
|
||||
) as HTMLElement;
|
||||
|
||||
expect(surfaceRef).instanceOf(Element);
|
||||
(surfaceRef as SurfaceRefBlockComponent).viewInEdgeless();
|
||||
await wait();
|
||||
});
|
||||
});
|
||||
|
||||
import snapshot from '../snapshots/edgeless/surface-ref.spec.ts/surface-ref.json' assert { type: 'json' };
|
||||
|
||||
describe('clipboard', () => {
|
||||
test('import surface-ref snapshot should render content correctly', async () => {
|
||||
await setupEditor('page');
|
||||
|
||||
const pageRoot = getDocRootBlock(doc, editor, 'page');
|
||||
const pageRootService = pageRoot.service;
|
||||
|
||||
const newDoc = await importFromSnapshot(
|
||||
pageRootService.collection,
|
||||
snapshot as DocSnapshot
|
||||
);
|
||||
expect(newDoc).toBeTruthy();
|
||||
|
||||
editor.doc = newDoc!;
|
||||
await wait();
|
||||
|
||||
const surfaceRefs = newDoc!.getBlocksByFlavour('affine:surface-ref');
|
||||
expect(surfaceRefs).toHaveLength(2);
|
||||
|
||||
const surfaceRefBlocks = surfaceRefs.map(({ id }) =>
|
||||
editor.std.view.getBlock(id)
|
||||
) as SurfaceRefBlockComponent[];
|
||||
|
||||
expect(surfaceRefBlocks[0].querySelector('.ref-placeholder')).toBeFalsy();
|
||||
expect(surfaceRefBlocks[1].querySelector('.ref-placeholder')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
EdgelessTemplatePanel,
|
||||
type Template,
|
||||
type TemplateManager,
|
||||
} from '@blocksuite/blocks';
|
||||
import { beforeEach, expect, test } from 'vitest';
|
||||
|
||||
import { setupEditor } from '../utils/setup.js';
|
||||
|
||||
beforeEach(async () => {
|
||||
const cleanup = await setupEditor('edgeless');
|
||||
|
||||
return cleanup;
|
||||
});
|
||||
|
||||
test('extension api', async () => {
|
||||
const mockTemplate = {
|
||||
name: 'Test',
|
||||
type: 'template',
|
||||
} as Template;
|
||||
const customTemplate = {
|
||||
list: () => {
|
||||
return [mockTemplate];
|
||||
},
|
||||
categories: () => {
|
||||
return ['custom'];
|
||||
},
|
||||
search: (_, __) => {
|
||||
return [mockTemplate];
|
||||
},
|
||||
} satisfies TemplateManager;
|
||||
|
||||
EdgelessTemplatePanel.templates.extend(customTemplate);
|
||||
|
||||
const categories = await EdgelessTemplatePanel.templates.categories();
|
||||
expect(categories).toContain('custom');
|
||||
|
||||
const templates = await EdgelessTemplatePanel.templates.list('any');
|
||||
expect(templates).toContain(mockTemplate);
|
||||
|
||||
const searchTemplates = await EdgelessTemplatePanel.templates.search('any');
|
||||
expect(searchTemplates).toContain(mockTemplate);
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import type {
|
||||
EdgelessRootBlockComponent,
|
||||
SurfaceBlockComponent,
|
||||
} from '@blocksuite/blocks';
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { click, drag, wait } from '../utils/common.js';
|
||||
import { addNote, getDocRootBlock, getSurface } from '../utils/edgeless.js';
|
||||
import { setupEditor } from '../utils/setup.js';
|
||||
|
||||
describe('default tool', () => {
|
||||
let surface!: SurfaceBlockComponent;
|
||||
let edgeless!: EdgelessRootBlockComponent;
|
||||
let service!: EdgelessRootBlockComponent['service'];
|
||||
|
||||
beforeEach(async () => {
|
||||
const cleanup = await setupEditor('edgeless');
|
||||
|
||||
edgeless = getDocRootBlock(doc, editor, 'edgeless');
|
||||
surface = getSurface(window.doc, window.editor);
|
||||
service = edgeless.service;
|
||||
|
||||
edgeless.gfx.tool.setTool('default');
|
||||
|
||||
return cleanup;
|
||||
});
|
||||
|
||||
test('element click selection', async () => {
|
||||
const id = service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
xywh: '[0,0,100,100]',
|
||||
fillColor: 'red',
|
||||
});
|
||||
|
||||
await wait();
|
||||
|
||||
service.viewport.setViewport(1, [
|
||||
service.viewport.width / 2,
|
||||
service.viewport.height / 2,
|
||||
]);
|
||||
|
||||
click(edgeless.host, { x: 0, y: 50 });
|
||||
|
||||
expect(edgeless.service.selection.surfaceSelections[0].elements).toEqual([
|
||||
id,
|
||||
]);
|
||||
});
|
||||
|
||||
test('element drag moving', async () => {
|
||||
const id = edgeless.service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
xywh: '[0,0,100,100]',
|
||||
fillColor: 'red',
|
||||
});
|
||||
await wait();
|
||||
|
||||
edgeless.service.viewport.setViewport(1, [
|
||||
edgeless.service.viewport.width / 2,
|
||||
edgeless.service.viewport.height / 2,
|
||||
]);
|
||||
await wait();
|
||||
|
||||
click(edgeless.host, { x: 0, y: 50 });
|
||||
drag(edgeless.host, { x: 0, y: 50 }, { x: 0, y: 150 });
|
||||
await wait();
|
||||
|
||||
const element = service.crud.getElementById(id!)!;
|
||||
expect(element.xywh).toEqual(`[0,100,100,100]`);
|
||||
});
|
||||
|
||||
test('block drag moving', async () => {
|
||||
const noteId = addNote(doc);
|
||||
|
||||
await wait();
|
||||
|
||||
edgeless.service.viewport.setViewport(1, [
|
||||
surface.renderer.viewport.width / 2,
|
||||
surface.renderer.viewport.height / 2,
|
||||
]);
|
||||
await wait();
|
||||
|
||||
click(edgeless.host, { x: 50, y: 50 });
|
||||
expect(edgeless.service.selection.surfaceSelections[0].elements).toEqual([
|
||||
noteId,
|
||||
]);
|
||||
drag(edgeless.host, { x: 50, y: 50 }, { x: 150, y: 150 });
|
||||
await wait();
|
||||
|
||||
const element = service.crud.getElementById(noteId)!;
|
||||
const [x, y] = JSON.parse(element.xywh);
|
||||
|
||||
expect(x).toEqual(100);
|
||||
expect(y).toEqual(100);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ViewportTurboRendererExtension } from '@blocksuite/affine-shared/viewport-renderer';
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { wait } from '../utils/common.js';
|
||||
import { addSampleNotes } from '../utils/doc-generator.js';
|
||||
import { setupEditor } from '../utils/setup.js';
|
||||
|
||||
describe('viewport turbo renderer', () => {
|
||||
beforeEach(async () => {
|
||||
const cleanup = await setupEditor('edgeless', [
|
||||
ViewportTurboRendererExtension,
|
||||
]);
|
||||
return cleanup;
|
||||
});
|
||||
|
||||
test('should render 6 notes in viewport', async () => {
|
||||
addSampleNotes(doc, 6);
|
||||
await wait();
|
||||
|
||||
const notes = document.querySelectorAll('affine-edgeless-note');
|
||||
expect(notes.length).toBe(6);
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
111
blocksuite/integration-test/src/__tests__/main/snapshot.spec.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { BlockServiceIdentifier } from '@blocksuite/block-std';
|
||||
import type { PageRootService, SurfaceBlockModel } from '@blocksuite/blocks';
|
||||
import type { PointLocation } from '@blocksuite/global/utils';
|
||||
import { beforeEach, expect, test } from 'vitest';
|
||||
|
||||
import { wait } from '../utils/common.js';
|
||||
import { setupEditor } from '../utils/setup.js';
|
||||
|
||||
const excludes = new Set([
|
||||
'shape-textBound',
|
||||
'externalXYWH',
|
||||
'connector-text',
|
||||
'connector-labelXYWH',
|
||||
]);
|
||||
|
||||
const fieldChecker: Record<string, (value: any) => boolean> = {
|
||||
'connector-path': (value: PointLocation[]) => {
|
||||
return value.length > 0;
|
||||
},
|
||||
xywh: (value: string) => {
|
||||
return value.match(xywhPattern) !== null;
|
||||
},
|
||||
};
|
||||
|
||||
const skipFields = new Set(['_lastXYWH']);
|
||||
|
||||
const snapshotTest = async (snapshotUrl: string, elementsCount: number) => {
|
||||
const pageService = window.editor.host!.std.getOptional(
|
||||
BlockServiceIdentifier('affine:page')
|
||||
) as PageRootService;
|
||||
if (!pageService) {
|
||||
throw new Error('page service not found');
|
||||
}
|
||||
const transformer = pageService.transformers.zip;
|
||||
|
||||
const snapshotFile = await fetch(snapshotUrl)
|
||||
.then(res => res.blob())
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
throw e;
|
||||
});
|
||||
const [newDoc] = await transformer.importDocs(
|
||||
window.editor.doc.workspace,
|
||||
snapshotFile
|
||||
);
|
||||
|
||||
if (!newDoc) {
|
||||
throw new Error('Failed to import snapshot');
|
||||
}
|
||||
|
||||
editor.doc = newDoc;
|
||||
await wait();
|
||||
|
||||
const surface = newDoc.getBlockByFlavour(
|
||||
'affine:surface'
|
||||
)[0] as SurfaceBlockModel;
|
||||
const surfaceElements = [...surface['_elementModels']].map(
|
||||
([_, { model }]) => model
|
||||
);
|
||||
|
||||
expect(surfaceElements.length).toBe(elementsCount);
|
||||
|
||||
surfaceElements.forEach(element => {
|
||||
const type = element.type;
|
||||
|
||||
for (const field in element) {
|
||||
const value = element[field as keyof typeof element];
|
||||
const typeField = `${type}-${field}`;
|
||||
|
||||
if (excludes.has(`${type}-${field}`) || excludes.has(field)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (skipFields.has(field)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fieldChecker[typeField] || fieldChecker[field]) {
|
||||
const checker = fieldChecker[typeField] || fieldChecker[field];
|
||||
expect(checker(value)).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
expect(
|
||||
value,
|
||||
`type: ${element.type} field: "${field}"`
|
||||
).not.toBeUndefined();
|
||||
expect(value, `type: ${element.type} field: "${field}"`).not.toBeNull();
|
||||
expect(value, `type: ${element.type} field: "${field}"`).not.toBeNaN();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const cleanup = await setupEditor('page');
|
||||
|
||||
return cleanup;
|
||||
});
|
||||
|
||||
const xywhPattern = /\[(\s*-?\d+(\.\d+)?\s*,){3}(\s*-?\d+(\.\d+)?\s*)\]/;
|
||||
|
||||
test('snapshot 1 importing', async () => {
|
||||
await snapshotTest('https://test.affineassets.com/test-snapshot-1.zip', 25);
|
||||
});
|
||||
|
||||
test('snapshot 2 importing', async () => {
|
||||
await snapshotTest(
|
||||
'https://test.affineassets.com/test-snapshot-2%20(onboarding).zip',
|
||||
174
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
{
|
||||
"type": "page",
|
||||
"meta": {
|
||||
"id": "doc:home",
|
||||
"title": "",
|
||||
"createDate": 1725961648308,
|
||||
"tags": []
|
||||
},
|
||||
"blocks": {
|
||||
"type": "block",
|
||||
"id": "xguBwzpkGD",
|
||||
"flavour": "affine:page",
|
||||
"version": 2,
|
||||
"props": {
|
||||
"title": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": []
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "m9-hJeL979",
|
||||
"flavour": "affine:surface",
|
||||
"version": 5,
|
||||
"props": {
|
||||
"elements": {
|
||||
"UrT0kJjtKN": {
|
||||
"index": "a2",
|
||||
"seed": 1855830969,
|
||||
"color": "--affine-palette-line-black",
|
||||
"fillColor": "--affine-palette-shape-yellow",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontSize": 20,
|
||||
"fontStyle": "normal",
|
||||
"fontWeight": "400",
|
||||
"maxWidth": false,
|
||||
"padding": [10, 20],
|
||||
"radius": 0,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": null,
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "triangle",
|
||||
"strokeColor": "--affine-palette-line-yellow",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"textAlign": "center",
|
||||
"textResizing": 1,
|
||||
"xywh": "[220.12515258789062,240.90625,99.82000732421875,80.5]",
|
||||
"type": "shape",
|
||||
"id": "UrT0kJjtKN"
|
||||
},
|
||||
"0AlG3AohCi": {
|
||||
"index": "a3",
|
||||
"seed": 1305411005,
|
||||
"color": "--affine-palette-line-black",
|
||||
"fillColor": "--affine-palette-shape-yellow",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontSize": 20,
|
||||
"fontStyle": "normal",
|
||||
"fontWeight": "400",
|
||||
"maxWidth": false,
|
||||
"padding": [10, 20],
|
||||
"radius": 0,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": null,
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "ellipse",
|
||||
"strokeColor": "--affine-palette-line-yellow",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"textAlign": "center",
|
||||
"textResizing": 1,
|
||||
"xywh": "[485.93280029296875,231.14300537109375,99.84002685546875,99.84002685546875]",
|
||||
"type": "shape",
|
||||
"id": "0AlG3AohCi"
|
||||
},
|
||||
"8fUiHCCqfG": {
|
||||
"index": "a4",
|
||||
"seed": 513823325,
|
||||
"color": "--affine-palette-line-black",
|
||||
"fillColor": "--affine-palette-shape-yellow",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
"fontSize": 20,
|
||||
"fontStyle": "normal",
|
||||
"fontWeight": "400",
|
||||
"maxWidth": false,
|
||||
"padding": [10, 20],
|
||||
"radius": 0.1,
|
||||
"rotate": 0,
|
||||
"roughness": 1.4,
|
||||
"shadow": null,
|
||||
"shapeStyle": "General",
|
||||
"shapeType": "rect",
|
||||
"strokeColor": "--affine-palette-line-yellow",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"textAlign": "center",
|
||||
"textResizing": 1,
|
||||
"xywh": "[620.0800170898438,234.9613037109375,99.8399658203125,99.8399658203125]",
|
||||
"type": "shape",
|
||||
"id": "8fUiHCCqfG"
|
||||
},
|
||||
"KfzvfxYw5C": {
|
||||
"index": "a5",
|
||||
"seed": 1817567358,
|
||||
"children": {
|
||||
"affine:surface:ymap": true,
|
||||
"json": {
|
||||
"8fUiHCCqfG": true,
|
||||
"0AlG3AohCi": true
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"affine:surface:text": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Group 1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "group",
|
||||
"id": "KfzvfxYw5C"
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "yBzeQ6raOv",
|
||||
"flavour": "affine:frame",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"title": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Frame 1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"background": "--affine-palette-transparent",
|
||||
"xywh": "[140.703125,199.1015625,258.6640625,164.109375]",
|
||||
"index": "a1",
|
||||
"childElementIds": {
|
||||
"UrT0kJjtKN": true
|
||||
}
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"id": "eWPFqL6VST",
|
||||
"flavour": "affine:note",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"xywh": "[0,0,498,92]",
|
||||
"background": "--affine-note-background-white",
|
||||
"index": "a0",
|
||||
"hidden": false,
|
||||
"displayMode": "both",
|
||||
"edgeless": {
|
||||
"style": {
|
||||
"borderRadius": 8,
|
||||
"borderSize": 4,
|
||||
"borderStyle": "none",
|
||||
"shadowType": "--affine-note-shadow-box"
|
||||
}
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "block",
|
||||
"id": "09MoQfa5ex",
|
||||
"flavour": "affine:paragraph",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"type": "text",
|
||||
"text": {
|
||||
"$blocksuite:internal:text$": true,
|
||||
"delta": []
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"id": "NhoAqBBMZg",
|
||||
"flavour": "affine:surface-ref",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"reference": "yBzeQ6raOv",
|
||||
"caption": "",
|
||||
"refFlavour": "affine:frame"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"id": "jJDE1rYIYJ",
|
||||
"flavour": "affine:surface-ref",
|
||||
"version": 1,
|
||||
"props": {
|
||||
"reference": "KfzvfxYw5C",
|
||||
"caption": "",
|
||||
"refFlavour": "group"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
202
blocksuite/integration-test/src/__tests__/utils/common.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import type { Point } from '@blocksuite/global/utils';
|
||||
|
||||
export function wait(time: number = 0) {
|
||||
return new Promise(resolve => {
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(resolve, time);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* simulate click event
|
||||
* @param target
|
||||
* @param position position relative to the target
|
||||
*/
|
||||
export function click(target: HTMLElement, position: { x: number; y: number }) {
|
||||
const element = target.getBoundingClientRect();
|
||||
const clientX = element.x + position.x;
|
||||
const clientY = element.y + position.y;
|
||||
|
||||
target.dispatchEvent(
|
||||
new PointerEvent('pointerdown', {
|
||||
clientX,
|
||||
clientY,
|
||||
bubbles: true,
|
||||
pointerId: 1,
|
||||
isPrimary: true,
|
||||
})
|
||||
);
|
||||
target.dispatchEvent(
|
||||
new PointerEvent('pointerup', {
|
||||
clientX,
|
||||
clientY,
|
||||
bubbles: true,
|
||||
pointerId: 1,
|
||||
isPrimary: true,
|
||||
})
|
||||
);
|
||||
target.dispatchEvent(
|
||||
new MouseEvent('click', {
|
||||
clientX,
|
||||
clientY,
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
type PointerOptions = {
|
||||
isPrimary?: boolean;
|
||||
pointerId?: number;
|
||||
pointerType?: string;
|
||||
};
|
||||
|
||||
const defaultPointerOptions: PointerOptions = {
|
||||
isPrimary: true,
|
||||
pointerId: 1,
|
||||
pointerType: 'mouse',
|
||||
};
|
||||
|
||||
/**
|
||||
* simulate pointerdown event
|
||||
* @param target
|
||||
* @param position position relative to the target
|
||||
*/
|
||||
export function pointerdown(
|
||||
target: HTMLElement,
|
||||
position: { x: number; y: number },
|
||||
options: PointerOptions = defaultPointerOptions
|
||||
) {
|
||||
const element = target.getBoundingClientRect();
|
||||
const clientX = element.x + position.x;
|
||||
const clientY = element.y + position.y;
|
||||
|
||||
target.dispatchEvent(
|
||||
new PointerEvent('pointerdown', {
|
||||
clientX,
|
||||
clientY,
|
||||
bubbles: true,
|
||||
...options,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* simulate pointerup event
|
||||
* @param target
|
||||
* @param position position relative to the target
|
||||
*/
|
||||
export function pointerup(
|
||||
target: HTMLElement,
|
||||
position: { x: number; y: number },
|
||||
options: PointerOptions = defaultPointerOptions
|
||||
) {
|
||||
const element = target.getBoundingClientRect();
|
||||
const clientX = element.x + position.x;
|
||||
const clientY = element.y + position.y;
|
||||
|
||||
target.dispatchEvent(
|
||||
new PointerEvent('pointerup', {
|
||||
clientX,
|
||||
clientY,
|
||||
bubbles: true,
|
||||
...options,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* simulate pointermove event
|
||||
* @param target
|
||||
* @param position position relative to the target
|
||||
*/
|
||||
export function pointermove(
|
||||
target: HTMLElement,
|
||||
position: { x: number; y: number },
|
||||
options: PointerOptions = defaultPointerOptions
|
||||
) {
|
||||
const element = target.getBoundingClientRect();
|
||||
const clientX = element.x + position.x;
|
||||
const clientY = element.y + position.y;
|
||||
|
||||
target.dispatchEvent(
|
||||
new PointerEvent('pointermove', {
|
||||
clientX,
|
||||
clientY,
|
||||
bubbles: true,
|
||||
...options,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function drag(
|
||||
target: HTMLElement,
|
||||
start: { x: number; y: number },
|
||||
end: { x: number; y: number },
|
||||
step: number = 5
|
||||
) {
|
||||
pointerdown(target, start);
|
||||
pointermove(target, start);
|
||||
|
||||
if (step !== 0) {
|
||||
const xStep = (end.x - start.x) / step;
|
||||
const yStep = (end.y - start.y) / step;
|
||||
|
||||
for (const [i] of Array.from({ length: step }).entries()) {
|
||||
pointermove(target, {
|
||||
x: start.x + xStep * (i + 1),
|
||||
y: start.y + yStep * (i + 1),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pointermove(target, end);
|
||||
pointerup(target, end);
|
||||
}
|
||||
|
||||
export function multiTouchDown(target: Element, points: Point[]) {
|
||||
points.forEach((point, index) => {
|
||||
pointerdown(target as HTMLElement, point, {
|
||||
isPrimary: index === 0,
|
||||
pointerId: index,
|
||||
pointerType: 'touch',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function multiTouchMove(
|
||||
target: Element,
|
||||
from: Point[],
|
||||
to: Point[],
|
||||
step = 5
|
||||
) {
|
||||
if (from.length !== to.length) {
|
||||
throw new Error('from and to should have the same length');
|
||||
}
|
||||
|
||||
if (step !== 0) {
|
||||
for (const [i] of Array.from({ length: step }).entries()) {
|
||||
const stepPoints = from.map((point, index) => ({
|
||||
x: point.x + ((to[index].x - point.x) / step) * (i + 1),
|
||||
y: point.y + ((to[index].y - point.y) / step) * (i + 1),
|
||||
}));
|
||||
from.forEach((_, index) => {
|
||||
pointermove(target as HTMLElement, stepPoints[index], {
|
||||
isPrimary: index === 0,
|
||||
pointerId: index,
|
||||
pointerType: 'touch',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function multiTouchUp(target: Element, points: Point[]) {
|
||||
points.forEach((point, index) => {
|
||||
pointerup(target as HTMLElement, point, {
|
||||
isPrimary: index === 0,
|
||||
pointerId: index,
|
||||
pointerType: 'touch',
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { type Store, Text } from '@blocksuite/store';
|
||||
|
||||
function addParagraph(doc: Store, noteId: string, content: string) {
|
||||
const props = { text: new Text(content) };
|
||||
doc.addBlock('affine:paragraph', props, noteId);
|
||||
}
|
||||
|
||||
function addSampleNote(doc: Store, noteId: string, i: number) {
|
||||
addParagraph(doc, noteId, `Note ${i + 1}`);
|
||||
addParagraph(doc, noteId, 'Hello World!');
|
||||
addParagraph(
|
||||
doc,
|
||||
noteId,
|
||||
'Hello World! Lorem ipsum dolor sit amet. Consectetur adipiscing elit. Sed do eiusmod tempor incididunt.'
|
||||
);
|
||||
addParagraph(
|
||||
doc,
|
||||
noteId,
|
||||
'你好这是测试,这是一个为了换行而写的中文段落。这个段落会自动换行。'
|
||||
);
|
||||
}
|
||||
|
||||
export function addSampleNotes(doc: Store, n: number) {
|
||||
const cols = Math.ceil(Math.sqrt(n));
|
||||
const NOTE_WIDTH = 500;
|
||||
const NOTE_HEIGHT = 250;
|
||||
const SPACING = 50;
|
||||
|
||||
const rootId = doc.getBlocksByFlavour('affine:page')[0]?.id;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const row = Math.floor(i / cols);
|
||||
const col = i % cols;
|
||||
const x = col * (NOTE_WIDTH + SPACING);
|
||||
const y = row * (NOTE_HEIGHT + SPACING);
|
||||
|
||||
const xywh = `[${x},${y},${NOTE_WIDTH},${NOTE_HEIGHT}]`;
|
||||
const noteId = doc.addBlock('affine:note', { xywh }, rootId);
|
||||
addSampleNote(doc, noteId, i);
|
||||
}
|
||||
}
|
||||
51
blocksuite/integration-test/src/__tests__/utils/edgeless.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type {
|
||||
EdgelessRootBlockComponent,
|
||||
PageRootBlockComponent,
|
||||
SurfaceBlockComponent,
|
||||
} from '@blocksuite/blocks';
|
||||
import type { Store } from '@blocksuite/store';
|
||||
|
||||
import type { TestAffineEditorContainer } from '../../index.js';
|
||||
|
||||
export function getSurface(doc: Store, editor: TestAffineEditorContainer) {
|
||||
const surfaceModel = doc.getBlockByFlavour('affine:surface');
|
||||
|
||||
return editor.host!.view.getBlock(
|
||||
surfaceModel[0]!.id
|
||||
) as SurfaceBlockComponent;
|
||||
}
|
||||
|
||||
export function getDocRootBlock(
|
||||
doc: Store,
|
||||
editor: TestAffineEditorContainer,
|
||||
mode: 'page'
|
||||
): PageRootBlockComponent;
|
||||
export function getDocRootBlock(
|
||||
doc: Store,
|
||||
editor: TestAffineEditorContainer,
|
||||
mode: 'edgeless'
|
||||
): EdgelessRootBlockComponent;
|
||||
export function getDocRootBlock(
|
||||
doc: Store,
|
||||
editor: TestAffineEditorContainer,
|
||||
_?: 'edgeless' | 'page'
|
||||
) {
|
||||
return editor.host!.view.getBlock(doc.root!.id) as
|
||||
| EdgelessRootBlockComponent
|
||||
| PageRootBlockComponent;
|
||||
}
|
||||
|
||||
export function addNote(doc: Store, props: Record<string, any> = {}) {
|
||||
const noteId = doc.addBlock(
|
||||
'affine:note',
|
||||
{
|
||||
xywh: '[0, 0, 800, 100]',
|
||||
...props,
|
||||
},
|
||||
doc.root
|
||||
);
|
||||
|
||||
doc.addBlock('affine:paragraph', {}, noteId);
|
||||
|
||||
return noteId;
|
||||
}
|
||||
24
blocksuite/integration-test/src/__tests__/utils/misc.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { replaceIdMiddleware } from '@blocksuite/blocks';
|
||||
import {
|
||||
type DocSnapshot,
|
||||
Transformer,
|
||||
type Workspace,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
export async function importFromSnapshot(
|
||||
collection: Workspace,
|
||||
snapshot: DocSnapshot
|
||||
) {
|
||||
const job = new Transformer({
|
||||
schema: collection.schema,
|
||||
blobCRUD: collection.blobSync,
|
||||
docCRUD: {
|
||||
create: (id: string) => collection.createDoc({ id }),
|
||||
get: (id: string) => collection.getDoc(id),
|
||||
delete: (id: string) => collection.removeDoc(id),
|
||||
},
|
||||
middlewares: [replaceIdMiddleware(collection.idGenerator)],
|
||||
});
|
||||
|
||||
return job.snapshotToDoc(snapshot);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ViewportTurboRendererExtension } from '@blocksuite/affine-shared/viewport-renderer';
|
||||
|
||||
import { addSampleNotes } from './doc-generator.js';
|
||||
import { setupEditor } from './setup.js';
|
||||
|
||||
async function init() {
|
||||
setupEditor('edgeless', [ViewportTurboRendererExtension]);
|
||||
addSampleNotes(doc, 100);
|
||||
doc.load();
|
||||
}
|
||||
|
||||
init();
|
||||
140
blocksuite/integration-test/src/__tests__/utils/setup.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import '@toeverything/theme/style.css';
|
||||
import '@toeverything/theme/fonts.css';
|
||||
|
||||
import { effects as blocksEffects } from '@blocksuite/blocks/effects';
|
||||
import type { ExtensionType, Store, Transformer } from '@blocksuite/store';
|
||||
|
||||
import { effects } from '../../effects.js';
|
||||
|
||||
blocksEffects();
|
||||
effects();
|
||||
|
||||
import {
|
||||
CommunityCanvasTextFonts,
|
||||
type DocMode,
|
||||
EdgelessEditorBlockSpecs,
|
||||
FontConfigExtension,
|
||||
PageEditorBlockSpecs,
|
||||
StoreExtensions,
|
||||
} from '@blocksuite/blocks';
|
||||
import { AffineSchemas } from '@blocksuite/blocks/schemas';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { Schema, Text } from '@blocksuite/store';
|
||||
import {
|
||||
createAutoIncrementIdGenerator,
|
||||
TestWorkspace,
|
||||
} from '@blocksuite/store/test';
|
||||
|
||||
import { TestAffineEditorContainer } from '../../index.js';
|
||||
|
||||
function createCollectionOptions() {
|
||||
const schema = new Schema();
|
||||
const room = Math.random().toString(16).slice(2, 8);
|
||||
|
||||
schema.register(AffineSchemas);
|
||||
|
||||
const idGenerator = createAutoIncrementIdGenerator();
|
||||
|
||||
return {
|
||||
id: room,
|
||||
schema,
|
||||
idGenerator,
|
||||
defaultFlags: {
|
||||
enable_synced_doc_block: true,
|
||||
enable_pie_menu: true,
|
||||
readonly: {
|
||||
'doc:home': false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function initCollection(collection: TestWorkspace) {
|
||||
const doc = collection.createDoc({ id: 'doc:home' });
|
||||
|
||||
doc.load(() => {
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new Text(),
|
||||
});
|
||||
doc.addBlock('affine:surface', {}, rootId);
|
||||
});
|
||||
doc.resetHistory();
|
||||
}
|
||||
|
||||
async function createEditor(
|
||||
collection: TestWorkspace,
|
||||
mode: DocMode = 'page',
|
||||
extensions: ExtensionType[] = []
|
||||
) {
|
||||
const app = document.createElement('div');
|
||||
const blockCollection = collection.docs.values().next().value;
|
||||
assertExists(blockCollection, 'Need to create a doc first');
|
||||
const doc = blockCollection.getStore();
|
||||
const editor = new TestAffineEditorContainer();
|
||||
editor.doc = doc;
|
||||
editor.mode = mode;
|
||||
editor.pageSpecs = [
|
||||
...PageEditorBlockSpecs,
|
||||
FontConfigExtension(CommunityCanvasTextFonts),
|
||||
...extensions,
|
||||
];
|
||||
editor.edgelessSpecs = [
|
||||
...EdgelessEditorBlockSpecs,
|
||||
FontConfigExtension(CommunityCanvasTextFonts),
|
||||
...extensions,
|
||||
];
|
||||
app.append(editor);
|
||||
|
||||
window.editor = editor;
|
||||
window.doc = doc;
|
||||
|
||||
app.style.width = '100%';
|
||||
app.style.height = '1280px';
|
||||
app.style.overflowY = 'auto';
|
||||
|
||||
document.body.append(app);
|
||||
await editor.updateComplete;
|
||||
return app;
|
||||
}
|
||||
|
||||
export async function setupEditor(
|
||||
mode: DocMode = 'page',
|
||||
extensions: ExtensionType[] = []
|
||||
) {
|
||||
const collection = new TestWorkspace(createCollectionOptions());
|
||||
collection.storeExtensions = StoreExtensions;
|
||||
collection.meta.initialize();
|
||||
|
||||
window.collection = collection;
|
||||
|
||||
initCollection(collection);
|
||||
const appElement = await createEditor(collection, mode, extensions);
|
||||
|
||||
return () => {
|
||||
appElement.remove();
|
||||
cleanup();
|
||||
};
|
||||
}
|
||||
|
||||
export function cleanup() {
|
||||
window.editor.remove();
|
||||
|
||||
delete (window as any).collection;
|
||||
|
||||
delete (window as any).editor;
|
||||
|
||||
delete (window as any).doc;
|
||||
}
|
||||
|
||||
declare global {
|
||||
const editor: TestAffineEditorContainer;
|
||||
const doc: Store;
|
||||
const collection: TestWorkspace;
|
||||
const job: Transformer;
|
||||
interface Window {
|
||||
editor: TestAffineEditorContainer;
|
||||
doc: Store;
|
||||
job: Transformer;
|
||||
collection: TestWorkspace;
|
||||
}
|
||||
}
|
||||
220
blocksuite/integration-test/src/editors/editor-container.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { BlockStdScope, ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { type DocMode, ThemeProvider } from '@blocksuite/blocks';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
|
||||
import {
|
||||
type BlockModel,
|
||||
type ExtensionType,
|
||||
type Store,
|
||||
} from '@blocksuite/store';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { css, html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
|
||||
export class TestAffineEditorContainer extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
.affine-page-viewport {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
container-name: viewport;
|
||||
container-type: inline-size;
|
||||
font-family: var(--affine-font-family);
|
||||
}
|
||||
.affine-page-viewport * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.affine-page-viewport {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.playground-page-editor-container {
|
||||
flex-grow: 1;
|
||||
font-family: var(--affine-font-family);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.playground-page-editor-container * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.playground-page-editor-container {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.edgeless-editor-container {
|
||||
font-family: var(--affine-font-family);
|
||||
background: var(--affine-background-primary-color);
|
||||
display: block;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.edgeless-editor-container * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.edgeless-editor-container {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.affine-edgeless-viewport {
|
||||
display: block;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: clip;
|
||||
container-name: viewport;
|
||||
container-type: inline-size;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _doc = signal<Store>();
|
||||
|
||||
private readonly _edgelessSpecs = signal<ExtensionType[]>([]);
|
||||
|
||||
private readonly _mode = signal<DocMode>('page');
|
||||
|
||||
private readonly _pageSpecs = signal<ExtensionType[]>([]);
|
||||
|
||||
private readonly _specs = computed(() =>
|
||||
this._mode.value === 'page'
|
||||
? this._pageSpecs.value
|
||||
: this._edgelessSpecs.value
|
||||
);
|
||||
|
||||
private readonly _std = computed(() => {
|
||||
return new BlockStdScope({
|
||||
store: this.doc,
|
||||
extensions: this._specs.value,
|
||||
});
|
||||
});
|
||||
|
||||
private readonly _editorTemplate = computed(() => {
|
||||
return this._std.value.render();
|
||||
});
|
||||
|
||||
get doc() {
|
||||
return this._doc.value as Store;
|
||||
}
|
||||
|
||||
set doc(doc: Store) {
|
||||
this._doc.value = doc;
|
||||
}
|
||||
|
||||
set edgelessSpecs(specs: ExtensionType[]) {
|
||||
this._edgelessSpecs.value = specs;
|
||||
}
|
||||
|
||||
get edgelessSpecs() {
|
||||
return this._edgelessSpecs.value;
|
||||
}
|
||||
|
||||
get host() {
|
||||
try {
|
||||
return this.std.host;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this._mode.value;
|
||||
}
|
||||
|
||||
set mode(mode: DocMode) {
|
||||
this._mode.value = mode;
|
||||
}
|
||||
|
||||
set pageSpecs(specs: ExtensionType[]) {
|
||||
this._pageSpecs.value = specs;
|
||||
}
|
||||
|
||||
get pageSpecs() {
|
||||
return this._pageSpecs.value;
|
||||
}
|
||||
|
||||
get rootModel() {
|
||||
return this.doc.root as BlockModel;
|
||||
}
|
||||
|
||||
get std() {
|
||||
return this._std.value;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this._disposables.add(
|
||||
this.doc.slots.rootAdded.on(() => this.requestUpdate())
|
||||
);
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
if (this.mode === 'page') {
|
||||
setTimeout(() => {
|
||||
if (this.autofocus) {
|
||||
const richText = this.querySelector('rich-text');
|
||||
const inlineEditor = richText?.inlineEditor;
|
||||
inlineEditor?.focusEnd();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
const mode = this._mode.value;
|
||||
const themeService = this.std.get(ThemeProvider);
|
||||
const appTheme = themeService.app$.value;
|
||||
const edgelessTheme = themeService.edgeless$.value;
|
||||
|
||||
return html`${keyed(
|
||||
this.rootModel.id + mode,
|
||||
html`
|
||||
<div
|
||||
data-theme=${mode === 'page' ? appTheme : edgelessTheme}
|
||||
class=${mode === 'page'
|
||||
? 'affine-page-viewport'
|
||||
: 'affine-edgeless-viewport'}
|
||||
>
|
||||
${when(
|
||||
mode === 'page',
|
||||
() => html` <doc-title .doc=${this.doc}></doc-title> `
|
||||
)}
|
||||
<div
|
||||
class=${mode === 'page'
|
||||
? 'page-editor playground-page-editor-container'
|
||||
: 'edgeless-editor-container'}
|
||||
>
|
||||
${this._editorTemplate.value}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}`;
|
||||
}
|
||||
|
||||
switchEditor(mode: DocMode) {
|
||||
this._mode.value = mode;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
override accessor autofocus = false;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-editor-container': TestAffineEditorContainer;
|
||||
}
|
||||
}
|
||||
1
blocksuite/integration-test/src/editors/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './editor-container.js';
|
||||
7
blocksuite/integration-test/src/effects.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import '@blocksuite/blocks/effects';
|
||||
|
||||
import { TestAffineEditorContainer } from './editors/index.js';
|
||||
|
||||
export function effects() {
|
||||
customElements.define('affine-editor-container', TestAffineEditorContainer);
|
||||
}
|
||||
1
blocksuite/integration-test/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './editors';
|
||||
21
blocksuite/integration-test/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["./src", "./src/**/*.json"],
|
||||
"references": [
|
||||
{ "path": "../affine/block-note" },
|
||||
{ "path": "../affine/block-surface" },
|
||||
{ "path": "../affine/components" },
|
||||
{ "path": "../affine/model" },
|
||||
{ "path": "../affine/shared" },
|
||||
{ "path": "../framework/block-std" },
|
||||
{ "path": "../blocks" },
|
||||
{ "path": "../framework/global" },
|
||||
{ "path": "../framework/inline" },
|
||||
{ "path": "../framework/store" }
|
||||
]
|
||||
}
|
||||
40
blocksuite/integration-test/vite.config.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { cpus } from 'node:os';
|
||||
|
||||
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
|
||||
import { defineConfig } from 'vite';
|
||||
import wasm from 'vite-plugin-wasm';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
plugins: [wasm(), vanillaExtractPlugin()],
|
||||
esbuild: {
|
||||
target: 'es2022',
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
allowedHosts: true,
|
||||
},
|
||||
build: {
|
||||
target: 'es2022',
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
cache: false,
|
||||
maxParallelFileOps: Math.max(1, cpus().length - 1),
|
||||
onwarn(warning, defaultHandler) {
|
||||
if (
|
||||
warning.code &&
|
||||
['EVAL', 'SOURCEMAP_ERROR'].includes(warning.code)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
defaultHandler(warning);
|
||||
},
|
||||
treeshake: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
66
blocksuite/integration-test/vitest.config.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig(_configEnv =>
|
||||
defineConfig({
|
||||
esbuild: { target: 'es2018' },
|
||||
optimizeDeps: {
|
||||
force: true,
|
||||
esbuildOptions: {
|
||||
// Vitest hardcodes the esbuild target to es2020,
|
||||
// override it to es2022 for top level await.
|
||||
target: 'es2022',
|
||||
},
|
||||
},
|
||||
plugins: [vanillaExtractPlugin()],
|
||||
test: {
|
||||
include: ['src/__tests__/**/*.spec.ts'],
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: process.env.CI === 'true',
|
||||
name: 'chromium',
|
||||
provider: 'playwright',
|
||||
isolate: false,
|
||||
providerOptions: {},
|
||||
viewport: {
|
||||
width: 1024,
|
||||
height: 768,
|
||||
},
|
||||
},
|
||||
coverage: {
|
||||
provider: 'istanbul', // or 'c8'
|
||||
reporter: ['lcov'],
|
||||
reportsDirectory: '../../.coverage/presets',
|
||||
},
|
||||
deps: {
|
||||
interopDefault: true,
|
||||
},
|
||||
testTransformMode: {
|
||||
web: ['src/__tests__/**/*.spec.ts'],
|
||||
},
|
||||
alias: {
|
||||
'@blocksuite/blocks': path.resolve(
|
||||
fileURLToPath(new URL('../blocks/src', import.meta.url))
|
||||
),
|
||||
'@blocksuite/blocks/*': path.resolve(
|
||||
fileURLToPath(new URL('../blocks/src/*', import.meta.url))
|
||||
),
|
||||
'@blocksuite/global/*': path.resolve(
|
||||
fileURLToPath(new URL('../framework/global/src/*', import.meta.url))
|
||||
),
|
||||
'@blocksuite/store': path.resolve(
|
||||
fileURLToPath(new URL('../framework/store/src', import.meta.url))
|
||||
),
|
||||
'@blocksuite/inline': path.resolve(
|
||||
fileURLToPath(new URL('../framework/inline/src', import.meta.url))
|
||||
),
|
||||
'@blocksuite/inline/*': path.resolve(
|
||||
fileURLToPath(new URL('../framework/inline/src/*', import.meta.url))
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||