mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-21 03:56:23 +08:00
refactor(editor): rename presets to integration test (#10340)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user