test: add std gfx test (#11442)
### Changed - Move some intergraion tests to std as they are more like basic tests - Add some basic gfx-related tests
@@ -11,6 +11,7 @@ import {
|
|||||||
HeadingBlockSchemaExtension,
|
HeadingBlockSchemaExtension,
|
||||||
NoteBlockSchemaExtension,
|
NoteBlockSchemaExtension,
|
||||||
RootBlockSchemaExtension,
|
RootBlockSchemaExtension,
|
||||||
|
SurfaceBlockSchemaExtension,
|
||||||
} from './test-schema.js';
|
} from './test-schema.js';
|
||||||
import { testSpecs } from './test-spec.js';
|
import { testSpecs } from './test-spec.js';
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ const extensions = [
|
|||||||
RootBlockSchemaExtension,
|
RootBlockSchemaExtension,
|
||||||
NoteBlockSchemaExtension,
|
NoteBlockSchemaExtension,
|
||||||
HeadingBlockSchemaExtension,
|
HeadingBlockSchemaExtension,
|
||||||
|
SurfaceBlockSchemaExtension,
|
||||||
];
|
];
|
||||||
|
|
||||||
function createTestOptions() {
|
function createTestOptions() {
|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,358 @@
|
|||||||
|
import {
|
||||||
|
createAutoIncrementIdGenerator,
|
||||||
|
TestWorkspace,
|
||||||
|
} from '@blocksuite/store/test';
|
||||||
|
import { describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { effects } from '../../effects.js';
|
||||||
|
import type { TestShapeElement } from '../test-gfx-element.js';
|
||||||
|
import {
|
||||||
|
RootBlockSchemaExtension,
|
||||||
|
type SurfaceBlockModel,
|
||||||
|
SurfaceBlockSchemaExtension,
|
||||||
|
} from '../test-schema.js';
|
||||||
|
|
||||||
|
effects();
|
||||||
|
|
||||||
|
const extensions = [RootBlockSchemaExtension, SurfaceBlockSchemaExtension];
|
||||||
|
|
||||||
|
function createTestOptions() {
|
||||||
|
const idGenerator = createAutoIncrementIdGenerator();
|
||||||
|
return { id: 'test-collection', idGenerator };
|
||||||
|
}
|
||||||
|
|
||||||
|
const commonSetup = () => {
|
||||||
|
const collection = new TestWorkspace(createTestOptions());
|
||||||
|
|
||||||
|
collection.meta.initialize();
|
||||||
|
const doc = collection.createDoc('home');
|
||||||
|
const store = doc.getStore({ extensions });
|
||||||
|
doc.load();
|
||||||
|
|
||||||
|
const rootId = store.addBlock('test:page');
|
||||||
|
const surfaceId = store.addBlock('test:surface', {}, rootId);
|
||||||
|
|
||||||
|
const surfaceBlock = store.getBlock(surfaceId)!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
surfaceId,
|
||||||
|
surfaceModel: surfaceBlock.model as SurfaceBlockModel,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('surface basic', () => {
|
||||||
|
test('addElement should work correctly', () => {
|
||||||
|
const { surfaceModel: model } = commonSetup();
|
||||||
|
const id = model.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(model.elementModels[0].id).toBe(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removeElement should work correctly', () => {
|
||||||
|
const { surfaceModel: model } = commonSetup();
|
||||||
|
const id = model.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
});
|
||||||
|
|
||||||
|
model.deleteElement(id);
|
||||||
|
|
||||||
|
expect(model.elementModels.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updateElement should work correctly', () => {
|
||||||
|
const { surfaceModel: model } = commonSetup();
|
||||||
|
const id = model.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
});
|
||||||
|
|
||||||
|
model.updateElement(id, { xywh: '[10,10,200,200]' });
|
||||||
|
|
||||||
|
expect(model.elementModels[0].xywh).toBe('[10,10,200,200]');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getElementById should return element', () => {
|
||||||
|
const { surfaceModel: model } = commonSetup();
|
||||||
|
|
||||||
|
const id = model.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(model.getElementById(id)).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getElementById should return null if not found', () => {
|
||||||
|
const { surfaceModel: model } = commonSetup();
|
||||||
|
|
||||||
|
expect(model.getElementById('not-found')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('created observer should be called', () => {
|
||||||
|
const { surfaceModel } = commonSetup();
|
||||||
|
|
||||||
|
let expectPayload;
|
||||||
|
const elementAddedCallback = vi.fn(payload => (expectPayload = payload));
|
||||||
|
|
||||||
|
surfaceModel.elementAdded.subscribe(elementAddedCallback);
|
||||||
|
|
||||||
|
const shapeId = surfaceModel.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
rotate: 0,
|
||||||
|
xywh: '[0, 0, 10, 10]',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(elementAddedCallback).toHaveBeenCalled();
|
||||||
|
expect(expectPayload).toMatchObject({
|
||||||
|
id: shapeId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update and props observer should be called', () => {
|
||||||
|
const { surfaceModel } = commonSetup();
|
||||||
|
|
||||||
|
const shapeId = surfaceModel.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
rotate: 0,
|
||||||
|
xywh: '[0, 0, 10, 10]',
|
||||||
|
});
|
||||||
|
const shapeModel = surfaceModel.getElementById(shapeId)!;
|
||||||
|
|
||||||
|
let expectPayload;
|
||||||
|
const elementUpdatedCallback = vi.fn(payload => (expectPayload = payload));
|
||||||
|
let propsUpdatedPayload;
|
||||||
|
const propsUpdatedCallback = vi.fn(payload => {
|
||||||
|
propsUpdatedPayload = payload;
|
||||||
|
});
|
||||||
|
|
||||||
|
surfaceModel.elementUpdated.subscribe(elementUpdatedCallback);
|
||||||
|
shapeModel.propsUpdated.subscribe(propsUpdatedCallback);
|
||||||
|
|
||||||
|
surfaceModel.updateElement(shapeId, {
|
||||||
|
rotate: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(elementUpdatedCallback).toHaveBeenCalled();
|
||||||
|
expect(propsUpdatedCallback).toHaveBeenCalled();
|
||||||
|
expect(expectPayload).toMatchObject({
|
||||||
|
id: shapeId,
|
||||||
|
props: {
|
||||||
|
rotate: 10,
|
||||||
|
},
|
||||||
|
oldValues: {
|
||||||
|
rotate: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(propsUpdatedPayload).toMatchObject({
|
||||||
|
key: 'rotate',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete observer should be called', () => {
|
||||||
|
const { surfaceModel } = commonSetup();
|
||||||
|
|
||||||
|
const shapeId = surfaceModel.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
rotate: 0,
|
||||||
|
xywh: '[0, 0, 10, 10]',
|
||||||
|
});
|
||||||
|
|
||||||
|
let expectPayload;
|
||||||
|
const deletedCallback = vi.fn(payload => (expectPayload = payload));
|
||||||
|
|
||||||
|
surfaceModel.elementRemoved.subscribe(deletedCallback);
|
||||||
|
surfaceModel.deleteElement(shapeId);
|
||||||
|
|
||||||
|
expect(deletedCallback).toHaveBeenCalled();
|
||||||
|
expect(expectPayload).toMatchObject({
|
||||||
|
id: shapeId,
|
||||||
|
type: 'testShape',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('element model', () => {
|
||||||
|
test('default value should work correctly', () => {
|
||||||
|
const { surfaceModel: model } = commonSetup();
|
||||||
|
const id = model.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
});
|
||||||
|
|
||||||
|
const element = model.getElementById(id)! as TestShapeElement;
|
||||||
|
|
||||||
|
expect(element.rotate).toBe(0);
|
||||||
|
expect(element.xywh).toBe('[0,0,10,10]');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('defined prop should not be overwritten by default value', () => {
|
||||||
|
const { surfaceModel: model } = commonSetup();
|
||||||
|
const id = model.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
rotate: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
const element = model.getElementById(id)! as TestShapeElement;
|
||||||
|
|
||||||
|
expect(element.rotate).toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('assign value to model property should update ymap directly', () => {
|
||||||
|
const { surfaceModel: model } = commonSetup();
|
||||||
|
const id = model.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
});
|
||||||
|
|
||||||
|
const element = model.getElementById(id)! as TestShapeElement;
|
||||||
|
|
||||||
|
expect(element.yMap.get('rotate')).toBe(0);
|
||||||
|
element.rotate = 30;
|
||||||
|
expect(element.yMap.get('rotate')).toBe(30);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stash/pop', () => {
|
||||||
|
const { surfaceModel: model } = commonSetup();
|
||||||
|
test('stash and pop should work correctly', () => {
|
||||||
|
const id = model.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
});
|
||||||
|
const elementModel = model.getElementById(id)! as TestShapeElement;
|
||||||
|
|
||||||
|
expect(elementModel.rotate).toBe(0);
|
||||||
|
|
||||||
|
elementModel.stash('rotate');
|
||||||
|
elementModel.rotate = 10;
|
||||||
|
expect(elementModel.rotate).toBe(10);
|
||||||
|
expect(elementModel.yMap.get('rotate')).toBe(0);
|
||||||
|
|
||||||
|
elementModel.pop('rotate');
|
||||||
|
expect(elementModel.rotate).toBe(10);
|
||||||
|
expect(elementModel.yMap.get('rotate')).toBe(10);
|
||||||
|
|
||||||
|
elementModel.rotate = 6;
|
||||||
|
expect(elementModel.rotate).toBe(6);
|
||||||
|
expect(elementModel.yMap.get('rotate')).toBe(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('assign stashed property should emit event', () => {
|
||||||
|
const id = model.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
rotate: 4,
|
||||||
|
});
|
||||||
|
const elementModel = model.getElementById(id)! as TestShapeElement;
|
||||||
|
|
||||||
|
elementModel.stash('rotate');
|
||||||
|
|
||||||
|
const onchange = vi.fn();
|
||||||
|
const subscription = model.elementUpdated.subscribe(({ id }) => {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
onchange(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
elementModel.rotate = 10;
|
||||||
|
expect(onchange).toHaveBeenCalledWith(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stashed property should also trigger derive decorator', () => {
|
||||||
|
const id = model.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
rotate: 20,
|
||||||
|
});
|
||||||
|
const elementModel = model.getElementById(id)! as TestShapeElement;
|
||||||
|
|
||||||
|
elementModel.stash('shapeType');
|
||||||
|
elementModel.shapeType = 'triangle';
|
||||||
|
|
||||||
|
// rotation should be 0 'cause of derive decorator
|
||||||
|
expect(elementModel.rotate).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-field property should not allow stash/pop, and should fail silently ', () => {
|
||||||
|
const id = model.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
});
|
||||||
|
const elementModel = model.getElementById(id)! as TestShapeElement;
|
||||||
|
|
||||||
|
// opacity is a local property, so it should not be stashed
|
||||||
|
elementModel.stash('opacity');
|
||||||
|
expect(elementModel['_stashed'].has('opacity')).toBe(false);
|
||||||
|
|
||||||
|
// pop the `opacity` should not affect yMap
|
||||||
|
elementModel.opacity = 0.5;
|
||||||
|
elementModel.pop('opacity');
|
||||||
|
expect(elementModel.yMap.has('opacity')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('derive decorator', () => {
|
||||||
|
test('derived decorator should work correctly', () => {
|
||||||
|
const { surfaceModel: model } = commonSetup();
|
||||||
|
const id = model.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
rotate: 20,
|
||||||
|
});
|
||||||
|
const elementModel = model.getElementById(id)! as TestShapeElement;
|
||||||
|
|
||||||
|
elementModel.shapeType = 'triangle';
|
||||||
|
|
||||||
|
expect(elementModel.rotate).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('local decorator', () => {
|
||||||
|
test('local decorator should work correctly', () => {
|
||||||
|
const { surfaceModel: model } = commonSetup();
|
||||||
|
const id = model.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
});
|
||||||
|
const elementModel = model.getElementById(id)! as TestShapeElement;
|
||||||
|
|
||||||
|
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 { surfaceModel: model } = commonSetup();
|
||||||
|
const id = model.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
});
|
||||||
|
const elementModel = model.getElementById(id)! as TestShapeElement;
|
||||||
|
|
||||||
|
const onchange = vi.fn();
|
||||||
|
const subscription = model.elementUpdated.subscribe(({ id }) => {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
onchange(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onPropChange = vi.fn();
|
||||||
|
elementModel.propsUpdated.subscribe(({ key }) => {
|
||||||
|
onPropChange(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
elementModel.display = false;
|
||||||
|
|
||||||
|
expect(elementModel.display).toBe(false);
|
||||||
|
expect(onchange).toHaveBeenCalledWith(id);
|
||||||
|
expect(onPropChange).toHaveBeenCalledWith('display');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('convert decorator', () => {
|
||||||
|
test('convert decorator', () => {
|
||||||
|
const { surfaceModel: model } = commonSetup();
|
||||||
|
const id = model.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
});
|
||||||
|
const elementModel = model.getElementById(id)! as TestShapeElement;
|
||||||
|
|
||||||
|
// @ts-expect-error test needed
|
||||||
|
elementModel.shapeType = 'otherImpossibleType';
|
||||||
|
|
||||||
|
expect(elementModel.shapeType).toBe('rect');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import {
|
||||||
|
createAutoIncrementIdGenerator,
|
||||||
|
TestWorkspace,
|
||||||
|
} from '@blocksuite/store/test';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import { effects } from '../../effects.js';
|
||||||
|
import { GfxControllerIdentifier } from '../../gfx/identifiers.js';
|
||||||
|
import { TestEditorContainer } from '../test-editor.js';
|
||||||
|
import { TestLocalElement } from '../test-gfx-element.js';
|
||||||
|
import {
|
||||||
|
RootBlockSchemaExtension,
|
||||||
|
type SurfaceBlockModel,
|
||||||
|
SurfaceBlockSchemaExtension,
|
||||||
|
TestGfxBlockSchemaExtension,
|
||||||
|
} from '../test-schema.js';
|
||||||
|
import { testSpecs } from '../test-spec.js';
|
||||||
|
|
||||||
|
effects();
|
||||||
|
|
||||||
|
const extensions = [
|
||||||
|
RootBlockSchemaExtension,
|
||||||
|
SurfaceBlockSchemaExtension,
|
||||||
|
TestGfxBlockSchemaExtension,
|
||||||
|
];
|
||||||
|
|
||||||
|
function createTestOptions() {
|
||||||
|
const idGenerator = createAutoIncrementIdGenerator();
|
||||||
|
return { id: 'test-collection', idGenerator };
|
||||||
|
}
|
||||||
|
|
||||||
|
const commonSetup = async () => {
|
||||||
|
const collection = new TestWorkspace(createTestOptions());
|
||||||
|
|
||||||
|
collection.meta.initialize();
|
||||||
|
const doc = collection.createDoc('home');
|
||||||
|
const store = doc.getStore({ extensions });
|
||||||
|
doc.load();
|
||||||
|
|
||||||
|
const rootId = store.addBlock('test:page');
|
||||||
|
const surfaceId = store.addBlock('test:surface', {}, rootId);
|
||||||
|
|
||||||
|
const surfaceBlock = store.getBlock(surfaceId)!;
|
||||||
|
|
||||||
|
const editorContainer = new TestEditorContainer();
|
||||||
|
editorContainer.doc = store;
|
||||||
|
editorContainer.specs = testSpecs;
|
||||||
|
document.body.append(editorContainer);
|
||||||
|
|
||||||
|
await editorContainer.updateComplete;
|
||||||
|
|
||||||
|
const gfx = editorContainer.std.get(GfxControllerIdentifier);
|
||||||
|
|
||||||
|
return {
|
||||||
|
gfx,
|
||||||
|
surfaceId,
|
||||||
|
rootId,
|
||||||
|
surfaceModel: surfaceBlock.model as SurfaceBlockModel,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('gfx element view basic', () => {
|
||||||
|
test('view should be created', async () => {
|
||||||
|
const { gfx, surfaceModel } = await commonSetup();
|
||||||
|
|
||||||
|
const id = surfaceModel.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
});
|
||||||
|
const shapeView = gfx.view.get(id);
|
||||||
|
|
||||||
|
expect(shapeView).not.toBeNull();
|
||||||
|
expect(shapeView!.model.id).toBe(id);
|
||||||
|
expect(shapeView!.isConnected).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('view should be removed', async () => {
|
||||||
|
const { gfx, surfaceModel } = await commonSetup();
|
||||||
|
|
||||||
|
const id = surfaceModel.addElement({
|
||||||
|
type: 'testShape',
|
||||||
|
});
|
||||||
|
const shapeView = gfx.view.get(id);
|
||||||
|
|
||||||
|
expect(shapeView).not.toBeNull();
|
||||||
|
expect(shapeView!.model.id).toBe(id);
|
||||||
|
|
||||||
|
surfaceModel.deleteElement(id);
|
||||||
|
expect(gfx.view.get(id)).toBeNull();
|
||||||
|
expect(shapeView!.isConnected).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('query gfx block view should work', async () => {
|
||||||
|
const { gfx, surfaceId, rootId } = await commonSetup();
|
||||||
|
|
||||||
|
const waitGfxViewConnected = (id: string) => {
|
||||||
|
const { promise, resolve } = Promise.withResolvers<void>();
|
||||||
|
const subscription = gfx.std.view.viewUpdated.subscribe(payload => {
|
||||||
|
if (
|
||||||
|
payload.id === id &&
|
||||||
|
payload.type === 'block' &&
|
||||||
|
payload.method === 'add'
|
||||||
|
) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
const id = gfx.std.store.addBlock('test:gfx-block', undefined, surfaceId);
|
||||||
|
await waitGfxViewConnected(id);
|
||||||
|
const gfxBlockView = gfx.view.get(id);
|
||||||
|
expect(gfxBlockView).not.toBeNull();
|
||||||
|
|
||||||
|
const rootView = gfx.view.get(rootId);
|
||||||
|
// root is not a gfx block, so it should be null
|
||||||
|
expect(rootView).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('local element view should be created', async () => {
|
||||||
|
const { gfx, surfaceModel } = await commonSetup();
|
||||||
|
const localElement = new TestLocalElement(surfaceModel);
|
||||||
|
localElement.id = 'test-local-element';
|
||||||
|
|
||||||
|
surfaceModel.addLocalElement(localElement);
|
||||||
|
|
||||||
|
const localView = gfx.view.get(localElement);
|
||||||
|
expect(localView).not.toBeNull();
|
||||||
|
expect(localView!.isConnected).toBe(true);
|
||||||
|
|
||||||
|
surfaceModel.deleteLocalElement(localElement);
|
||||||
|
expect(localView!.isConnected).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { customElement } from 'lit/decorators.js';
|
import { customElement } from 'lit/decorators.js';
|
||||||
|
|
||||||
import { BlockComponent } from '../view/index.js';
|
import { BlockComponent, GfxBlockComponent } from '../view/index.js';
|
||||||
import type {
|
import type {
|
||||||
HeadingBlockModel,
|
HeadingBlockModel,
|
||||||
NoteBlockModel,
|
NoteBlockModel,
|
||||||
RootBlockModel,
|
RootBlockModel,
|
||||||
|
SurfaceBlockModel,
|
||||||
|
TestGfxBlockModel,
|
||||||
} from './test-schema.js';
|
} from './test-schema.js';
|
||||||
|
|
||||||
@customElement('test-root-block')
|
@customElement('test-root-block')
|
||||||
@@ -39,3 +41,21 @@ export class HeadingH2BlockComponent extends BlockComponent<HeadingBlockModel> {
|
|||||||
return html` <div class="test-heading-block h2">${this.model.text}</div> `;
|
return html` <div class="test-heading-block h2">${this.model.text}</div> `;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@customElement('test-surface-block')
|
||||||
|
export class SurfaceBlockComponent extends BlockComponent<SurfaceBlockModel> {
|
||||||
|
override renderBlock() {
|
||||||
|
return html`
|
||||||
|
<div class="test-surface-block">${this.renderChildren(this.model)}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('test-gfx-block')
|
||||||
|
export class TestGfxBlockComponent extends GfxBlockComponent<TestGfxBlockModel> {
|
||||||
|
override renderGfxBlock() {
|
||||||
|
return html`
|
||||||
|
<div class="test-gfx-block">${this.renderChildren(this.model)}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { SerializedXYWH } from '@blocksuite/global/gfx';
|
||||||
|
|
||||||
|
import {
|
||||||
|
convert,
|
||||||
|
derive,
|
||||||
|
field,
|
||||||
|
GfxLocalElementModel,
|
||||||
|
GfxPrimitiveElementModel,
|
||||||
|
} from '../gfx/index.js';
|
||||||
|
|
||||||
|
export class TestShapeElement extends GfxPrimitiveElementModel {
|
||||||
|
get type() {
|
||||||
|
return 'testShape';
|
||||||
|
}
|
||||||
|
|
||||||
|
@field()
|
||||||
|
accessor rotate: number = 0;
|
||||||
|
|
||||||
|
@field()
|
||||||
|
accessor xywh: SerializedXYWH = '[0,0,10,10]';
|
||||||
|
|
||||||
|
@convert(val => {
|
||||||
|
if (['rect', 'triangle'].includes(val)) {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'rect';
|
||||||
|
})
|
||||||
|
@derive(val => {
|
||||||
|
if (val === 'triangle') {
|
||||||
|
return {
|
||||||
|
rotate: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
})
|
||||||
|
@field()
|
||||||
|
accessor shapeType: 'rect' | 'triangle' = 'rect';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TestLocalElement extends GfxLocalElementModel {
|
||||||
|
override type: string = 'testLocal';
|
||||||
|
}
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
|
import type { SerializedXYWH } from '@blocksuite/global/gfx';
|
||||||
import {
|
import {
|
||||||
BlockModel,
|
BlockModel,
|
||||||
BlockSchemaExtension,
|
BlockSchemaExtension,
|
||||||
defineBlockSchema,
|
defineBlockSchema,
|
||||||
} from '@blocksuite/store';
|
} from '@blocksuite/store';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
|
import { SurfaceBlockModel as BaseSurfaceModel } from '../gfx/index.js';
|
||||||
|
import {
|
||||||
|
GfxCompatibleBlockModel,
|
||||||
|
type GfxCompatibleProps,
|
||||||
|
} from '../gfx/model/gfx-block-model.js';
|
||||||
|
import { TestShapeElement } from './test-gfx-element.js';
|
||||||
|
|
||||||
export const RootBlockSchema = defineBlockSchema({
|
export const RootBlockSchema = defineBlockSchema({
|
||||||
flavour: 'test:page',
|
flavour: 'test:page',
|
||||||
@@ -15,7 +24,7 @@ export const RootBlockSchema = defineBlockSchema({
|
|||||||
metadata: {
|
metadata: {
|
||||||
version: 2,
|
version: 2,
|
||||||
role: 'root',
|
role: 'root',
|
||||||
children: ['test:note'],
|
children: ['test:note', 'test:surface'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,3 +70,58 @@ export const HeadingBlockSchemaExtension =
|
|||||||
export class HeadingBlockModel extends BlockModel<
|
export class HeadingBlockModel extends BlockModel<
|
||||||
ReturnType<(typeof HeadingBlockSchema)['model']['props']>
|
ReturnType<(typeof HeadingBlockSchema)['model']['props']>
|
||||||
> {}
|
> {}
|
||||||
|
|
||||||
|
export const SurfaceBlockSchema = defineBlockSchema({
|
||||||
|
flavour: 'test:surface',
|
||||||
|
props: internal => ({
|
||||||
|
elements: internal.Boxed<Y.Map<Y.Map<unknown>>>(new Y.Map()),
|
||||||
|
}),
|
||||||
|
metadata: {
|
||||||
|
version: 1,
|
||||||
|
role: 'hub',
|
||||||
|
parent: ['test:page'],
|
||||||
|
},
|
||||||
|
toModel: () => new SurfaceBlockModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SurfaceBlockSchemaExtension =
|
||||||
|
BlockSchemaExtension(SurfaceBlockSchema);
|
||||||
|
|
||||||
|
export class SurfaceBlockModel extends BaseSurfaceModel {
|
||||||
|
override _init() {
|
||||||
|
this._extendElement({
|
||||||
|
testShape: TestShapeElement,
|
||||||
|
});
|
||||||
|
super._init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type GfxTestBlockProps = {
|
||||||
|
xywh: SerializedXYWH;
|
||||||
|
rotate: number;
|
||||||
|
index: string;
|
||||||
|
} & GfxCompatibleProps;
|
||||||
|
|
||||||
|
export const TestGfxBlockSchema = defineBlockSchema({
|
||||||
|
flavour: 'test:gfx-block',
|
||||||
|
props: () =>
|
||||||
|
({
|
||||||
|
xywh: '[0,0,10,10]' as SerializedXYWH,
|
||||||
|
rotate: 0,
|
||||||
|
index: 'a0',
|
||||||
|
lockedBySelf: false,
|
||||||
|
}) as GfxTestBlockProps,
|
||||||
|
metadata: {
|
||||||
|
version: 1,
|
||||||
|
role: 'content',
|
||||||
|
parent: ['test:surface'],
|
||||||
|
},
|
||||||
|
toModel: () => new TestGfxBlockModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TestGfxBlockSchemaExtension =
|
||||||
|
BlockSchemaExtension(TestGfxBlockSchema);
|
||||||
|
|
||||||
|
export class TestGfxBlockModel extends GfxCompatibleBlockModel<GfxTestBlockProps>(
|
||||||
|
BlockModel
|
||||||
|
) {}
|
||||||
|
|||||||
@@ -20,4 +20,8 @@ export const testSpecs: ExtensionType[] = [
|
|||||||
|
|
||||||
return literal`test-h2-block`;
|
return literal`test-h2-block`;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
BlockViewExtension('test:surface', literal`test-surface-block`),
|
||||||
|
|
||||||
|
BlockViewExtension('test:gfx-block', literal`test-gfx-block`),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ import type { SurfaceBlockModel } from '@blocksuite/affine/blocks/surface';
|
|||||||
import type {
|
import type {
|
||||||
BrushElementModel,
|
BrushElementModel,
|
||||||
GroupElementModel,
|
GroupElementModel,
|
||||||
ShapeElementModel,
|
|
||||||
} from '@blocksuite/affine/model';
|
} from '@blocksuite/affine/model';
|
||||||
import { DefaultTheme } from '@blocksuite/affine/model';
|
import { beforeEach, describe, expect, test } from 'vitest';
|
||||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
||||||
|
|
||||||
import { wait } from '../utils/common.js';
|
import { wait } from '../utils/common.js';
|
||||||
import { setupEditor } from '../utils/setup.js';
|
import { setupEditor } from '../utils/setup.js';
|
||||||
@@ -23,88 +21,19 @@ beforeEach(async () => {
|
|||||||
return cleanup;
|
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', () => {
|
describe('group', () => {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
test('should get group', () => {
|
test('should get group', () => {
|
||||||
const id = model.addElement({
|
const id = model.addElement({
|
||||||
type: 'shape',
|
type: 'shape',
|
||||||
@@ -323,185 +252,6 @@ describe('connector', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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();
|
|
||||||
const subscription = model.elementUpdated.subscribe(({ id }) => {
|
|
||||||
subscription.unsubscribe();
|
|
||||||
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();
|
|
||||||
|
|
||||||
const subscription = model.elementUpdated.subscribe(({ id }) => {
|
|
||||||
subscription.unsubscribe();
|
|
||||||
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', () => {
|
describe('brush', () => {
|
||||||
test('same lineWidth should have same xywh', () => {
|
test('same lineWidth should have same xywh', () => {
|
||||||
const id = model.addElement({
|
const id = model.addElement({
|
||||||
|
|||||||