snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 1s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Mobile (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 1s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Mobile (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
This commit is contained in:
966
blocksuite/integration-test/src/__tests__/edgeless/layer.spec.ts
Normal file
966
blocksuite/integration-test/src/__tests__/edgeless/layer.spec.ts
Normal file
@@ -0,0 +1,966 @@
|
||||
import type { EdgelessRootBlockComponent } from '@blocksuite/affine/blocks/root';
|
||||
import type {
|
||||
CanvasRenderer,
|
||||
SurfaceElementModel,
|
||||
} from '@blocksuite/affine/blocks/surface';
|
||||
import { ungroupCommand } from '@blocksuite/affine/gfx/group';
|
||||
import type {
|
||||
GroupElementModel,
|
||||
MindmapElementModel,
|
||||
NoteBlockModel,
|
||||
} from '@blocksuite/affine/model';
|
||||
import { generateKeyBetween } from '@blocksuite/affine/std/gfx';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
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);
|
||||
await 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(5);
|
||||
};
|
||||
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();
|
||||
});
|
||||
|
||||
test('block host z-index should update after reordering', async () => {
|
||||
const backId = addNote(doc);
|
||||
const frontId = addNote(doc);
|
||||
|
||||
await wait();
|
||||
|
||||
const getBlockHost = (id: string) =>
|
||||
document.querySelector<HTMLElement>(
|
||||
`affine-edgeless-root gfx-viewport > [data-block-id="${id}"]`
|
||||
);
|
||||
|
||||
const backHost = getBlockHost(backId);
|
||||
const frontHost = getBlockHost(frontId);
|
||||
|
||||
expect(backHost).not.toBeNull();
|
||||
expect(frontHost).not.toBeNull();
|
||||
expect(Number(backHost!.style.zIndex)).toBeLessThan(
|
||||
Number(frontHost!.style.zIndex)
|
||||
);
|
||||
|
||||
service.crud.updateElement(backId, {
|
||||
index: service.layer.getReorderedIndex(
|
||||
service.crud.getElementById(backId)!,
|
||||
'front'
|
||||
),
|
||||
});
|
||||
|
||||
await wait();
|
||||
|
||||
expect(Number(backHost!.style.zIndex)).toBeGreaterThan(
|
||||
Number(frontHost!.style.zIndex)
|
||||
);
|
||||
});
|
||||
|
||||
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',
|
||||
xywh: '[0,0,100,100]',
|
||||
})!,
|
||||
addNote(doc),
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
xywh: '[120,0,100,100]',
|
||||
})!,
|
||||
addNote(doc),
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
xywh: '[240,0,100,100]',
|
||||
})!,
|
||||
];
|
||||
|
||||
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("change mindmap index should update its nodes' layer", async () => {
|
||||
const noteId = addNote(doc);
|
||||
const mindmapId = service.crud.addElement('mindmap', {
|
||||
children: {
|
||||
text: 'root',
|
||||
children: [{ text: 'child' }],
|
||||
},
|
||||
})!;
|
||||
|
||||
await wait();
|
||||
|
||||
const note = service.crud.getElementById(noteId)!;
|
||||
const mindmap = service.crud.getElementById(
|
||||
mindmapId
|
||||
)! as MindmapElementModel;
|
||||
const root = mindmap.tree.element;
|
||||
|
||||
expect(service.layer.getZIndex(root)).toBeGreaterThan(
|
||||
service.layer.getZIndex(note)
|
||||
);
|
||||
|
||||
mindmap.index = service.layer.getReorderedIndex(mindmap, 'back');
|
||||
await wait();
|
||||
|
||||
expect(service.layer.getZIndex(root)).toBeLessThan(
|
||||
service.layer.getZIndex(note)
|
||||
);
|
||||
});
|
||||
|
||||
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.std.command.exec(ungroupCommand, {
|
||||
group: 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[]
|
||||
// oxlint-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',
|
||||
xywh: '[0,0,100,100]',
|
||||
})!;
|
||||
|
||||
addNote(doc);
|
||||
|
||||
await wait();
|
||||
|
||||
service.crud.addElement('shape', {
|
||||
shapeType: 'rect',
|
||||
xywh: '[120,0,100,100]',
|
||||
})!;
|
||||
|
||||
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).toBeLessThanOrEqual(
|
||||
(surface.renderer as CanvasRenderer).canvas.width
|
||||
);
|
||||
expect(indexedCanvas.height).toBeLessThanOrEqual(
|
||||
(surface.renderer as CanvasRenderer).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.props.index <= nextModel.props.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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user