mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-11 15:16:28 +08:00
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,414 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { Command } from '../command/index.js';
|
||||
import { CommandManager } from '../command/index.js';
|
||||
import type { BlockStdScope } from '../scope/std-scope.js';
|
||||
|
||||
type Command1 = Command<
|
||||
{
|
||||
command1Option?: string;
|
||||
},
|
||||
{
|
||||
commandData1: string;
|
||||
}
|
||||
>;
|
||||
|
||||
type Command2 = Command<{ commandData1: string }, { commandData2: string }>;
|
||||
|
||||
describe('CommandManager', () => {
|
||||
let std: BlockStdScope;
|
||||
let commandManager: CommandManager;
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-expect-error FIXME: ts error
|
||||
std = {};
|
||||
commandManager = new CommandManager(std);
|
||||
});
|
||||
|
||||
test('can add and execute a command', () => {
|
||||
const command1: Command1 = vi.fn((_ctx, next) => next());
|
||||
const command2: Command2 = vi.fn((_ctx, _next) => {});
|
||||
|
||||
const [success1] = commandManager.chain().pipe(command1, {}).run();
|
||||
const [success2] = commandManager
|
||||
.chain()
|
||||
.pipe(command1, {})
|
||||
.pipe(command2)
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalled();
|
||||
expect(command2).toHaveBeenCalled();
|
||||
expect(success1).toBeTruthy();
|
||||
expect(success2).toBeFalsy();
|
||||
});
|
||||
|
||||
test('can chain multiple commands', () => {
|
||||
const command1: Command = vi.fn((_ctx, next) => next());
|
||||
const command2: Command = vi.fn((_ctx, next) => next());
|
||||
const command3: Command = vi.fn((_ctx, next) => next());
|
||||
|
||||
const [success] = commandManager
|
||||
.chain()
|
||||
.pipe(command1)
|
||||
.pipe(command2)
|
||||
.pipe(command3)
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalled();
|
||||
expect(command2).toHaveBeenCalled();
|
||||
expect(command3).toHaveBeenCalled();
|
||||
expect(success).toBeTruthy();
|
||||
});
|
||||
|
||||
test('skip commands if there is a command failed before them (`next` not executed)', () => {
|
||||
const command1: Command = vi.fn((_ctx, next) => next());
|
||||
const command2: Command = vi.fn((_ctx, _next) => {});
|
||||
const command3: Command = vi.fn((_ctx, next) => next());
|
||||
|
||||
const [success] = commandManager
|
||||
.chain()
|
||||
.pipe(command1)
|
||||
.pipe(command2)
|
||||
.pipe(command3)
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalled();
|
||||
expect(command2).toHaveBeenCalled();
|
||||
expect(command3).not.toHaveBeenCalled();
|
||||
expect(success).toBeFalsy();
|
||||
});
|
||||
|
||||
test('can handle command failure', () => {
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const command1: Command = vi.fn((_ctx, next) => next());
|
||||
const command2: Command = vi.fn((_ctx, _next) => {
|
||||
throw new Error('command2 failed');
|
||||
});
|
||||
const command3: Command = vi.fn((_ctx, next) => next());
|
||||
|
||||
const [success] = commandManager
|
||||
.chain()
|
||||
.pipe(command1)
|
||||
.pipe(command2)
|
||||
.pipe(command3)
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalled();
|
||||
expect(command2).toHaveBeenCalled();
|
||||
expect(command3).not.toHaveBeenCalled();
|
||||
expect(success).toBeFalsy();
|
||||
expect(errorSpy).toHaveBeenCalledWith(new Error('command2 failed'));
|
||||
});
|
||||
|
||||
test('can pass data to command when calling a command', () => {
|
||||
const command1: Command1 = vi.fn((_ctx, next) => next());
|
||||
|
||||
const [success] = commandManager
|
||||
.chain()
|
||||
.pipe(command1, { command1Option: 'test' })
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ command1Option: 'test' }),
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(success).toBeTruthy();
|
||||
});
|
||||
|
||||
test('can add data to the command chain with `with` method', () => {
|
||||
const command1: Command<{ commandData1: string }> = vi.fn((_ctx, next) =>
|
||||
next()
|
||||
);
|
||||
|
||||
const [success, ctx] = commandManager
|
||||
.chain()
|
||||
.with({ commandData1: 'test' })
|
||||
.pipe(command1)
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ commandData1: 'test' }),
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(success).toBeTruthy();
|
||||
expect(ctx.commandData1).toBe('test');
|
||||
});
|
||||
|
||||
test('passes and updates context across commands', () => {
|
||||
const command1: Command<{}, { commandData1: string }> = vi.fn(
|
||||
(_ctx, next) => next({ commandData1: '123' })
|
||||
);
|
||||
const command2: Command<{ commandData1: string }> = vi.fn((ctx, next) => {
|
||||
expect(ctx.commandData1).toBe('123');
|
||||
next({ commandData1: '456' });
|
||||
});
|
||||
|
||||
const [success, ctx] = commandManager
|
||||
.chain()
|
||||
.pipe(command1)
|
||||
.pipe(command2)
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalled();
|
||||
expect(command2).toHaveBeenCalled();
|
||||
expect(success).toBeTruthy();
|
||||
expect(ctx.commandData1).toBe('456');
|
||||
});
|
||||
|
||||
test('should not continue with the rest of the chain if all commands in `try` fail', () => {
|
||||
const command1: Command = vi.fn((_ctx, _next) => {});
|
||||
const command2: Command = vi.fn((_ctx, _next) => {});
|
||||
const command3: Command = vi.fn((_ctx, next) => next());
|
||||
|
||||
const [success] = commandManager
|
||||
.chain()
|
||||
.try(chain => [chain.pipe(command1), chain.pipe(command2)])
|
||||
.pipe(command3)
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalled();
|
||||
expect(command2).toHaveBeenCalled();
|
||||
expect(command3).not.toHaveBeenCalled();
|
||||
expect(success).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should not re-execute previous commands in the chain before `try`', () => {
|
||||
const command1: Command<{}, { commandData1: string }> = vi.fn(
|
||||
(_ctx, next) => next({ commandData1: '123' })
|
||||
);
|
||||
const command2: Command = vi.fn((_ctx, _next) => {});
|
||||
const command3: Command = vi.fn((_ctx, next) => next());
|
||||
|
||||
const [success, ctx] = commandManager
|
||||
.chain()
|
||||
.pipe(command1)
|
||||
.try(chain => [chain.pipe(command2), chain.pipe(command3)])
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalledTimes(1);
|
||||
expect(command2).toHaveBeenCalled();
|
||||
expect(command3).toHaveBeenCalled();
|
||||
expect(success).toBeTruthy();
|
||||
expect(ctx.commandData1).toBe('123');
|
||||
});
|
||||
|
||||
test('should continue with the rest of the chain if one command in `try` succeeds', () => {
|
||||
const command1: Command<
|
||||
{},
|
||||
{ commandData1?: string; commandData2?: string }
|
||||
> = vi.fn((_ctx, _next) => {});
|
||||
const command2: Command<
|
||||
{},
|
||||
{ commandData1?: string; commandData2?: string }
|
||||
> = vi.fn((_ctx, next) => next({ commandData2: '123' }));
|
||||
const command3: Command<{}, { commandData3: string }> = vi.fn(
|
||||
(_ctx, next) => next({ commandData3: '456' })
|
||||
);
|
||||
|
||||
const [success, ctx] = commandManager
|
||||
.chain()
|
||||
.try(chain => [chain.pipe(command1), chain.pipe(command2)])
|
||||
.pipe(command3)
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalled();
|
||||
expect(command2).toHaveBeenCalled();
|
||||
expect(command3).toHaveBeenCalled();
|
||||
expect(success).toBeTruthy();
|
||||
expect(ctx.commandData1).toBeUndefined();
|
||||
expect(ctx.commandData2).toBe('123');
|
||||
expect(ctx.commandData3).toBe('456');
|
||||
});
|
||||
|
||||
test('should not execute any further commands in `try` after one succeeds', () => {
|
||||
const command1: Command<
|
||||
{},
|
||||
{ commandData1?: string; commandData2?: string }
|
||||
> = vi.fn((_ctx, next) => next({ commandData1: '123' }));
|
||||
const command2: Command<
|
||||
{},
|
||||
{ commandData1?: string; commandData2?: string }
|
||||
> = vi.fn((_ctx, next) => next({ commandData2: '456' }));
|
||||
|
||||
const [success, ctx] = commandManager
|
||||
.chain()
|
||||
.try(chain => [chain.pipe(command1), chain.pipe(command2)])
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalled();
|
||||
expect(command2).not.toHaveBeenCalled();
|
||||
expect(success).toBeTruthy();
|
||||
expect(ctx.commandData1).toBe('123');
|
||||
expect(ctx.commandData2).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should pass context correctly in `try` when a command succeeds', () => {
|
||||
const command1: Command = vi.fn((_ctx, next) =>
|
||||
next({ commandData1: 'fromCommand1', commandData2: 'fromCommand1' })
|
||||
);
|
||||
const command2: Command = vi.fn((ctx, next) => {
|
||||
expect(ctx.commandData1).toBe('fromCommand1');
|
||||
expect(ctx.commandData2).toBe('fromCommand1');
|
||||
// override commandData2
|
||||
next({ commandData2: 'fromCommand2' });
|
||||
});
|
||||
const command3: Command = vi.fn((ctx, next) => {
|
||||
expect(ctx.commandData1).toBe('fromCommand1');
|
||||
expect(ctx.commandData2).toBe('fromCommand2');
|
||||
next();
|
||||
});
|
||||
|
||||
const [success] = commandManager
|
||||
.chain()
|
||||
.pipe(command1)
|
||||
.try(chain => [chain.pipe(command2)])
|
||||
.pipe(command3)
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalled();
|
||||
expect(command2).toHaveBeenCalled();
|
||||
expect(command3).toHaveBeenCalled();
|
||||
expect(success).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should continue with the rest of the chain if at least one command in `tryAll` succeeds', () => {
|
||||
const command1: Command = vi.fn((_ctx, _next) => {});
|
||||
const command2: Command = vi.fn((_ctx, next) => next());
|
||||
const command3: Command = vi.fn((_ctx, next) => next());
|
||||
|
||||
const [success] = commandManager
|
||||
.chain()
|
||||
.tryAll(chain => [chain.pipe(command1), chain.pipe(command2)])
|
||||
.pipe(command3)
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalled();
|
||||
expect(command2).toHaveBeenCalled();
|
||||
expect(command3).toHaveBeenCalled();
|
||||
expect(success).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should execute all commands in `tryAll` even if one has already succeeded', () => {
|
||||
const command1: Command<
|
||||
{},
|
||||
{ commandData1?: string; commandData2?: string; commandData3?: string }
|
||||
> = vi.fn((_ctx, next) => next({ commandData1: '123' }));
|
||||
const command2: Command<
|
||||
{},
|
||||
{ commandData1?: string; commandData2?: string; commandData3?: string }
|
||||
> = vi.fn((_ctx, next) => next({ commandData2: '456' }));
|
||||
const command3: Command<
|
||||
{},
|
||||
{ commandData1?: string; commandData2?: string; commandData3?: string }
|
||||
> = vi.fn((_ctx, next) => next({ commandData3: '789' }));
|
||||
|
||||
const [success, ctx] = commandManager
|
||||
.chain()
|
||||
.tryAll(chain => [
|
||||
chain.pipe(command1),
|
||||
chain.pipe(command2),
|
||||
chain.pipe(command3),
|
||||
])
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalled();
|
||||
expect(command2).toHaveBeenCalled();
|
||||
expect(command3).toHaveBeenCalled();
|
||||
expect(ctx.commandData1).toBe('123');
|
||||
expect(ctx.commandData2).toBe('456');
|
||||
expect(ctx.commandData3).toBe('789');
|
||||
expect(success).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should not continue with the rest of the chain if all commands in `tryAll` fail', () => {
|
||||
const command1: Command = vi.fn((_ctx, _next) => {});
|
||||
const command2: Command = vi.fn((_ctx, _next) => {});
|
||||
const command3: Command<{}, { commandData3: string }> = vi.fn(
|
||||
(_ctx, next) => next({ commandData3: '123' })
|
||||
);
|
||||
|
||||
const [success, ctx] = commandManager
|
||||
.chain()
|
||||
.tryAll(chain => [chain.pipe(command1), chain.pipe(command2)])
|
||||
.pipe(command3)
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalled();
|
||||
expect(command2).toHaveBeenCalled();
|
||||
expect(command3).not.toHaveBeenCalled();
|
||||
expect(ctx.commandData3).toBeUndefined();
|
||||
expect(success).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should pass context correctly in `tryAll` when at least one command succeeds', () => {
|
||||
const command1: Command<{}, { commandData1: string }> = vi.fn(
|
||||
(_ctx, next) => next({ commandData1: 'fromCommand1' })
|
||||
);
|
||||
const command2: Command<
|
||||
{ commandData1: string; commandData2?: string },
|
||||
{ commandData2: string; commandData3: string }
|
||||
> = vi.fn((ctx, next) => {
|
||||
expect(ctx.commandData1).toBe('fromCommand1');
|
||||
// override commandData1
|
||||
next({ commandData1: 'fromCommand2', commandData2: 'fromCommand2' });
|
||||
});
|
||||
const command3: Command<
|
||||
{ commandData1: string; commandData2?: string },
|
||||
{ commandData2: string; commandData3: string }
|
||||
> = vi.fn((ctx, next) => {
|
||||
expect(ctx.commandData1).toBe('fromCommand2');
|
||||
expect(ctx.commandData2).toBe('fromCommand2');
|
||||
next({
|
||||
// override commandData2
|
||||
commandData2: 'fromCommand3',
|
||||
commandData3: 'fromCommand3',
|
||||
});
|
||||
});
|
||||
const command4: Command<
|
||||
{ commandData1: string; commandData2: string; commandData3: string },
|
||||
{}
|
||||
> = vi.fn((ctx, next) => {
|
||||
expect(ctx.commandData1).toBe('fromCommand2');
|
||||
expect(ctx.commandData2).toBe('fromCommand3');
|
||||
expect(ctx.commandData3).toBe('fromCommand3');
|
||||
next();
|
||||
});
|
||||
|
||||
const [success, ctx] = commandManager
|
||||
.chain()
|
||||
.pipe(command1)
|
||||
.tryAll(chain => [chain.pipe(command2), chain.pipe(command3)])
|
||||
.pipe(command4)
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalled();
|
||||
expect(command2).toHaveBeenCalled();
|
||||
expect(command3).toHaveBeenCalled();
|
||||
expect(command4).toHaveBeenCalled();
|
||||
expect(success).toBeTruthy();
|
||||
expect(ctx.commandData1).toBe('fromCommand2');
|
||||
expect(ctx.commandData2).toBe('fromCommand3');
|
||||
expect(ctx.commandData3).toBe('fromCommand3');
|
||||
});
|
||||
|
||||
test('should not re-execute commands before `tryAll` after executing `tryAll`', () => {
|
||||
const command1: Command = vi.fn((_ctx, next) => next());
|
||||
const command2: Command = vi.fn((_ctx, next) => next());
|
||||
const command3: Command = vi.fn((_ctx, _next) => {});
|
||||
const command4: Command = vi.fn((_ctx, next) => next());
|
||||
|
||||
const [success] = commandManager
|
||||
.chain()
|
||||
.pipe(command1)
|
||||
.tryAll(chain => [chain.pipe(command2), chain.pipe(command3)])
|
||||
.pipe(command4)
|
||||
.run();
|
||||
|
||||
expect(command1).toHaveBeenCalledTimes(1);
|
||||
expect(command2).toHaveBeenCalled();
|
||||
expect(command3).toHaveBeenCalled();
|
||||
expect(command4).toHaveBeenCalled();
|
||||
expect(success).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
createAutoIncrementIdGenerator,
|
||||
TestWorkspace,
|
||||
} from '@blocksuite/store/test';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { effects } from '../effects.js';
|
||||
import { TestEditorContainer } from './test-editor.js';
|
||||
import {
|
||||
type HeadingBlockModel,
|
||||
HeadingBlockSchemaExtension,
|
||||
NoteBlockSchemaExtension,
|
||||
RootBlockSchemaExtension,
|
||||
} from './test-schema.js';
|
||||
import { testSpecs } from './test-spec.js';
|
||||
|
||||
effects();
|
||||
|
||||
const extensions = [
|
||||
RootBlockSchemaExtension,
|
||||
NoteBlockSchemaExtension,
|
||||
HeadingBlockSchemaExtension,
|
||||
];
|
||||
|
||||
function createTestOptions() {
|
||||
const idGenerator = createAutoIncrementIdGenerator();
|
||||
return { id: 'test-collection', idGenerator };
|
||||
}
|
||||
|
||||
function wait(time: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, time));
|
||||
}
|
||||
|
||||
describe('editor host', () => {
|
||||
test('editor host should rerender model when view changes', 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 noteId = store.addBlock('test:note', {}, rootId);
|
||||
const headingId = store.addBlock('test:heading', { type: 'h1' }, noteId);
|
||||
const headingBlock = store.getBlock(headingId)!;
|
||||
|
||||
const editorContainer = new TestEditorContainer();
|
||||
editorContainer.doc = store;
|
||||
editorContainer.specs = testSpecs;
|
||||
|
||||
document.body.append(editorContainer);
|
||||
|
||||
await wait(50);
|
||||
let headingElm = editorContainer.std.view.getBlock(headingId);
|
||||
|
||||
expect(headingElm!.tagName).toBe('TEST-H1-BLOCK');
|
||||
|
||||
(headingBlock.model as HeadingBlockModel).props.type = 'h2';
|
||||
await wait(50);
|
||||
headingElm = editorContainer.std.view.getBlock(headingId);
|
||||
|
||||
expect(headingElm!.tagName).toBe('TEST-H2-BLOCK');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import rehypeParse from 'rehype-parse';
|
||||
import { unified } from 'unified';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { onlyContainImgElement } from '../clipboard/utils.js';
|
||||
|
||||
describe('only contains img elements', () => {
|
||||
test('normal with head', () => {
|
||||
const htmlAst = unified().use(rehypeParse).parse(`<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<!--StartFragment--><img src="https://files.slack.com/deadbeef.png" alt="image.png"/><!--EndFragment-->
|
||||
</body>
|
||||
</html>`);
|
||||
const isImgOnly =
|
||||
htmlAst.children.map(onlyContainImgElement).reduce((a, b) => {
|
||||
if (a === 'no' || b === 'no') {
|
||||
return 'no';
|
||||
}
|
||||
if (a === 'maybe' && b === 'maybe') {
|
||||
return 'maybe';
|
||||
}
|
||||
return 'yes';
|
||||
}, 'maybe') === 'yes';
|
||||
expect(isImgOnly).toBe(true);
|
||||
});
|
||||
|
||||
test('normal without head', () => {
|
||||
const htmlAst = unified().use(rehypeParse).parse(`<html>
|
||||
<body>
|
||||
<!--StartFragment--><img src="https://files.slack.com/deadbeef.png" alt="image.png"/><!--EndFragment-->
|
||||
</body>
|
||||
</html>`);
|
||||
const isImgOnly =
|
||||
htmlAst.children.map(onlyContainImgElement).reduce((a, b) => {
|
||||
if (a === 'no' || b === 'no') {
|
||||
return 'no';
|
||||
}
|
||||
if (a === 'maybe' && b === 'maybe') {
|
||||
return 'maybe';
|
||||
}
|
||||
return 'yes';
|
||||
}, 'maybe') === 'yes';
|
||||
expect(isImgOnly).toBe(true);
|
||||
});
|
||||
|
||||
test('contain spans', () => {
|
||||
const htmlAst = unified().use(rehypeParse).parse(`<html>
|
||||
<body>
|
||||
<!--StartFragment--><img src="https://files.slack.com/deadbeef.png" alt="image.png"/><span></span><!--EndFragment-->
|
||||
</body>
|
||||
</html>`);
|
||||
const isImgOnly =
|
||||
htmlAst.children.map(onlyContainImgElement).reduce((a, b) => {
|
||||
if (a === 'no' || b === 'no') {
|
||||
return 'no';
|
||||
}
|
||||
if (a === 'maybe' && b === 'maybe') {
|
||||
return 'maybe';
|
||||
}
|
||||
return 'yes';
|
||||
}, 'maybe') === 'yes';
|
||||
expect(isImgOnly).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
deltaInsertsToChunks,
|
||||
transformDelta,
|
||||
} from '../../inline/utils/delta-convert.js';
|
||||
|
||||
test('transformDelta', () => {
|
||||
expect(
|
||||
transformDelta({
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
transformDelta({
|
||||
insert: '\n\naaa\n\nbbb\n\n',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
})
|
||||
).toEqual([
|
||||
'\n',
|
||||
'\n',
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
'\n',
|
||||
'\n',
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
'\n',
|
||||
'\n',
|
||||
]);
|
||||
});
|
||||
|
||||
test('deltaInsertsToChunks', () => {
|
||||
expect(
|
||||
deltaInsertsToChunks([
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
])
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(
|
||||
deltaInsertsToChunks([
|
||||
{
|
||||
insert: '\n\naaa\nbbb\n\n',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
])
|
||||
).toEqual([
|
||||
[],
|
||||
[],
|
||||
[
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[],
|
||||
[],
|
||||
]);
|
||||
|
||||
expect(
|
||||
deltaInsertsToChunks([
|
||||
{
|
||||
insert: '\n\naaa\n',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
insert: '\nbbb\n\n',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
])
|
||||
).toEqual([
|
||||
[],
|
||||
[],
|
||||
[
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[],
|
||||
[
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[],
|
||||
[],
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,526 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { InlineEditor } from '../../inline/index.js';
|
||||
|
||||
test('getDeltaByRangeIndex', () => {
|
||||
const yDoc = new Y.Doc();
|
||||
const yText = yDoc.getText('text');
|
||||
yText.applyDelta([
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
const inlineEditor = new InlineEditor(yText);
|
||||
|
||||
expect(inlineEditor.getDeltaByRangeIndex(0)).toEqual({
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(inlineEditor.getDeltaByRangeIndex(1)).toEqual({
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(inlineEditor.getDeltaByRangeIndex(3)).toEqual({
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(inlineEditor.getDeltaByRangeIndex(4)).toEqual({
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('getDeltasByInlineRange', () => {
|
||||
const yDoc = new Y.Doc();
|
||||
const yText = yDoc.getText('text');
|
||||
yText.applyDelta([
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
insert: 'ccc',
|
||||
attributes: {
|
||||
underline: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
const inlineEditor = new InlineEditor(yText);
|
||||
|
||||
expect(
|
||||
inlineEditor.getDeltasByInlineRange({
|
||||
index: 0,
|
||||
length: 0,
|
||||
})
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 0,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(
|
||||
inlineEditor.getDeltasByInlineRange({
|
||||
index: 0,
|
||||
length: 1,
|
||||
})
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 0,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(
|
||||
inlineEditor.getDeltasByInlineRange({
|
||||
index: 0,
|
||||
length: 3,
|
||||
})
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 0,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(
|
||||
inlineEditor.getDeltasByInlineRange({
|
||||
index: 0,
|
||||
length: 4,
|
||||
})
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 0,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(
|
||||
inlineEditor.getDeltasByInlineRange({
|
||||
index: 3,
|
||||
length: 1,
|
||||
})
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 0,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(
|
||||
inlineEditor.getDeltasByInlineRange({
|
||||
index: 3,
|
||||
length: 3,
|
||||
})
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 0,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(
|
||||
inlineEditor.getDeltasByInlineRange({
|
||||
index: 3,
|
||||
length: 4,
|
||||
})
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 0,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
insert: 'ccc',
|
||||
attributes: {
|
||||
underline: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 6,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(
|
||||
inlineEditor.getDeltasByInlineRange({
|
||||
index: 4,
|
||||
length: 0,
|
||||
})
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(
|
||||
inlineEditor.getDeltasByInlineRange({
|
||||
index: 4,
|
||||
length: 1,
|
||||
})
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(
|
||||
inlineEditor.getDeltasByInlineRange({
|
||||
index: 4,
|
||||
length: 2,
|
||||
})
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(
|
||||
inlineEditor.getDeltasByInlineRange({
|
||||
index: 4,
|
||||
length: 4,
|
||||
})
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
insert: 'ccc',
|
||||
attributes: {
|
||||
underline: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 6,
|
||||
length: 3,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test('cursor with format', () => {
|
||||
const yDoc = new Y.Doc();
|
||||
const yText = yDoc.getText('text');
|
||||
const inlineEditor = new InlineEditor(yText);
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: 0,
|
||||
length: 0,
|
||||
},
|
||||
'aaa',
|
||||
{
|
||||
bold: true,
|
||||
}
|
||||
);
|
||||
|
||||
inlineEditor.setMarks({
|
||||
italic: true,
|
||||
});
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: 3,
|
||||
length: 0,
|
||||
},
|
||||
'bbb'
|
||||
);
|
||||
|
||||
expect(inlineEditor.yText.toDelta()).toEqual([
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
bold: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('getFormat', () => {
|
||||
const yDoc = new Y.Doc();
|
||||
const yText = yDoc.getText('text');
|
||||
const inlineEditor = new InlineEditor(yText);
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: 0,
|
||||
length: 0,
|
||||
},
|
||||
'aaa',
|
||||
{
|
||||
bold: true,
|
||||
}
|
||||
);
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: 3,
|
||||
length: 0,
|
||||
},
|
||||
'bbb',
|
||||
{
|
||||
italic: true,
|
||||
}
|
||||
);
|
||||
|
||||
expect(inlineEditor.getFormat({ index: 0, length: 0 })).toEqual({});
|
||||
|
||||
expect(inlineEditor.getFormat({ index: 0, length: 1 })).toEqual({
|
||||
bold: true,
|
||||
});
|
||||
|
||||
expect(inlineEditor.getFormat({ index: 0, length: 3 })).toEqual({
|
||||
bold: true,
|
||||
});
|
||||
|
||||
expect(inlineEditor.getFormat({ index: 3, length: 0 })).toEqual({
|
||||
bold: true,
|
||||
});
|
||||
|
||||
expect(inlineEditor.getFormat({ index: 3, length: 1 })).toEqual({
|
||||
italic: true,
|
||||
});
|
||||
|
||||
expect(inlineEditor.getFormat({ index: 3, length: 3 })).toEqual({
|
||||
italic: true,
|
||||
});
|
||||
|
||||
expect(inlineEditor.getFormat({ index: 6, length: 0 })).toEqual({
|
||||
italic: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('incorrect format value `false`', () => {
|
||||
const yDoc = new Y.Doc();
|
||||
const yText = yDoc.getText('text');
|
||||
const inlineEditor = new InlineEditor(yText);
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: 0,
|
||||
length: 0,
|
||||
},
|
||||
'aaa',
|
||||
{
|
||||
// @ts-expect-error insert incorrect value
|
||||
bold: false,
|
||||
italic: true,
|
||||
}
|
||||
);
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: 3,
|
||||
length: 0,
|
||||
},
|
||||
'bbb',
|
||||
{
|
||||
underline: true,
|
||||
}
|
||||
);
|
||||
|
||||
expect(inlineEditor.yText.toDelta()).toEqual([
|
||||
{
|
||||
insert: 'aaa',
|
||||
attributes: {
|
||||
italic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
insert: 'bbb',
|
||||
attributes: {
|
||||
underline: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('yText should not contain \r', () => {
|
||||
const yDoc = new Y.Doc();
|
||||
const yText = yDoc.getText('text');
|
||||
yText.insert(0, 'aaa\r');
|
||||
|
||||
expect(yText.toString()).toEqual('aaa\r');
|
||||
expect(() => {
|
||||
new InlineEditor(yText);
|
||||
}).toThrow(
|
||||
'yText must not contain "\\r" because it will break the range synchronization'
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,347 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
intersectInlineRange,
|
||||
isInlineRangeAfter,
|
||||
isInlineRangeBefore,
|
||||
isInlineRangeContain,
|
||||
isInlineRangeEdge,
|
||||
isInlineRangeEdgeAfter,
|
||||
isInlineRangeEdgeBefore,
|
||||
isInlineRangeEqual,
|
||||
isInlineRangeIntersect,
|
||||
isPoint,
|
||||
mergeInlineRange,
|
||||
} from '../../inline/utils/inline-range.js';
|
||||
|
||||
test('isInlineRangeContain', () => {
|
||||
expect(
|
||||
isInlineRangeContain({ index: 0, length: 0 }, { index: 0, length: 0 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeContain({ index: 0, length: 0 }, { index: 0, length: 2 })
|
||||
).toEqual(false);
|
||||
|
||||
expect(
|
||||
isInlineRangeContain({ index: 0, length: 2 }, { index: 0, length: 0 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeContain({ index: 0, length: 2 }, { index: 0, length: 1 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeContain({ index: 0, length: 2 }, { index: 0, length: 2 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeContain({ index: 1, length: 3 }, { index: 0, length: 0 })
|
||||
).toEqual(false);
|
||||
|
||||
expect(
|
||||
isInlineRangeContain({ index: 1, length: 3 }, { index: 0, length: 1 })
|
||||
).toEqual(false);
|
||||
|
||||
expect(
|
||||
isInlineRangeContain({ index: 1, length: 3 }, { index: 0, length: 2 })
|
||||
).toEqual(false);
|
||||
|
||||
expect(
|
||||
isInlineRangeContain({ index: 1, length: 4 }, { index: 2, length: 0 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeContain({ index: 1, length: 4 }, { index: 2, length: 3 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeContain({ index: 1, length: 4 }, { index: 2, length: 4 })
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
test('isInlineRangeEqual', () => {
|
||||
expect(
|
||||
isInlineRangeEqual({ index: 0, length: 0 }, { index: 0, length: 0 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeEqual({ index: 0, length: 2 }, { index: 0, length: 1 })
|
||||
).toEqual(false);
|
||||
|
||||
expect(
|
||||
isInlineRangeEqual({ index: 1, length: 3 }, { index: 1, length: 3 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeEqual({ index: 0, length: 0 }, { index: 1, length: 0 })
|
||||
).toEqual(false);
|
||||
|
||||
expect(
|
||||
isInlineRangeEqual({ index: 2, length: 0 }, { index: 2, length: 0 })
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('isInlineRangeIntersect', () => {
|
||||
expect(
|
||||
isInlineRangeIntersect({ index: 0, length: 2 }, { index: 0, length: 0 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeIntersect({ index: 0, length: 2 }, { index: 2, length: 0 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeIntersect({ index: 0, length: 0 }, { index: 1, length: 0 })
|
||||
).toEqual(false);
|
||||
|
||||
expect(
|
||||
isInlineRangeIntersect({ index: 1, length: 0 }, { index: 1, length: 0 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeIntersect({ index: 1, length: 0 }, { index: 0, length: 1 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeIntersect({ index: 1, length: 0 }, { index: 0, length: 0 })
|
||||
).toEqual(false);
|
||||
|
||||
expect(
|
||||
isInlineRangeIntersect({ index: 1, length: 0 }, { index: 2, length: 0 })
|
||||
).toEqual(false);
|
||||
|
||||
expect(
|
||||
isInlineRangeIntersect({ index: 1, length: 0 }, { index: 0, length: 2 })
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('isInlineRangeBefore', () => {
|
||||
expect(
|
||||
isInlineRangeBefore({ index: 0, length: 1 }, { index: 2, length: 0 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeBefore({ index: 2, length: 0 }, { index: 0, length: 1 })
|
||||
).toEqual(false);
|
||||
|
||||
expect(
|
||||
isInlineRangeBefore({ index: 0, length: 0 }, { index: 1, length: 0 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeBefore({ index: 1, length: 0 }, { index: 0, length: 0 })
|
||||
).toEqual(false);
|
||||
|
||||
expect(
|
||||
isInlineRangeBefore({ index: 0, length: 0 }, { index: 0, length: 0 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeBefore({ index: 0, length: 0 }, { index: 0, length: 1 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeBefore({ index: 0, length: 1 }, { index: 0, length: 0 })
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
test('isInlineRangeAfter', () => {
|
||||
expect(
|
||||
isInlineRangeAfter({ index: 2, length: 0 }, { index: 0, length: 1 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeAfter({ index: 0, length: 1 }, { index: 2, length: 0 })
|
||||
).toEqual(false);
|
||||
|
||||
expect(
|
||||
isInlineRangeAfter({ index: 1, length: 0 }, { index: 0, length: 0 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeAfter({ index: 0, length: 0 }, { index: 1, length: 0 })
|
||||
).toEqual(false);
|
||||
|
||||
expect(
|
||||
isInlineRangeAfter({ index: 0, length: 0 }, { index: 0, length: 0 })
|
||||
).toEqual(true);
|
||||
|
||||
expect(
|
||||
isInlineRangeAfter({ index: 0, length: 0 }, { index: 0, length: 1 })
|
||||
).toEqual(false);
|
||||
|
||||
expect(
|
||||
isInlineRangeAfter({ index: 0, length: 1 }, { index: 0, length: 0 })
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('isInlineRangeEdge', () => {
|
||||
expect(isInlineRangeEdge(1, { index: 1, length: 0 })).toEqual(true);
|
||||
|
||||
expect(isInlineRangeEdge(1, { index: 0, length: 1 })).toEqual(true);
|
||||
|
||||
expect(isInlineRangeEdge(0, { index: 0, length: 0 })).toEqual(true);
|
||||
|
||||
expect(isInlineRangeEdge(1, { index: 0, length: 0 })).toEqual(false);
|
||||
|
||||
expect(isInlineRangeEdge(0, { index: 1, length: 0 })).toEqual(false);
|
||||
|
||||
expect(isInlineRangeEdge(0, { index: 0, length: 1 })).toEqual(true);
|
||||
});
|
||||
|
||||
test('isInlineRangeEdgeBefore', () => {
|
||||
expect(isInlineRangeEdgeBefore(1, { index: 1, length: 0 })).toEqual(true);
|
||||
|
||||
expect(isInlineRangeEdgeBefore(1, { index: 0, length: 1 })).toEqual(false);
|
||||
|
||||
expect(isInlineRangeEdgeBefore(0, { index: 0, length: 0 })).toEqual(true);
|
||||
|
||||
expect(isInlineRangeEdgeBefore(1, { index: 0, length: 0 })).toEqual(false);
|
||||
|
||||
expect(isInlineRangeEdgeBefore(0, { index: 1, length: 0 })).toEqual(false);
|
||||
|
||||
expect(isInlineRangeEdgeBefore(0, { index: 0, length: 1 })).toEqual(true);
|
||||
});
|
||||
|
||||
test('isInlineRangeEdgeAfter', () => {
|
||||
expect(isInlineRangeEdgeAfter(1, { index: 0, length: 1 })).toEqual(true);
|
||||
|
||||
expect(isInlineRangeEdgeAfter(1, { index: 1, length: 0 })).toEqual(true);
|
||||
|
||||
expect(isInlineRangeEdgeAfter(0, { index: 0, length: 0 })).toEqual(true);
|
||||
|
||||
expect(isInlineRangeEdgeAfter(0, { index: 1, length: 0 })).toEqual(false);
|
||||
|
||||
expect(isInlineRangeEdgeAfter(1, { index: 0, length: 0 })).toEqual(false);
|
||||
|
||||
expect(isInlineRangeEdgeAfter(0, { index: 0, length: 1 })).toEqual(false);
|
||||
|
||||
expect(isInlineRangeEdgeAfter(0, { index: 0, length: 0 })).toEqual(true);
|
||||
});
|
||||
|
||||
test('isPoint', () => {
|
||||
expect(isPoint({ index: 1, length: 0 })).toEqual(true);
|
||||
|
||||
expect(isPoint({ index: 0, length: 2 })).toEqual(false);
|
||||
|
||||
expect(isPoint({ index: 0, length: 0 })).toEqual(true);
|
||||
|
||||
expect(isPoint({ index: 2, length: 0 })).toEqual(true);
|
||||
|
||||
expect(isPoint({ index: 2, length: 2 })).toEqual(false);
|
||||
});
|
||||
|
||||
test('mergeInlineRange', () => {
|
||||
expect(
|
||||
mergeInlineRange({ index: 0, length: 0 }, { index: 1, length: 0 })
|
||||
).toEqual({
|
||||
index: 0,
|
||||
length: 1,
|
||||
});
|
||||
|
||||
expect(
|
||||
mergeInlineRange({ index: 0, length: 0 }, { index: 0, length: 0 })
|
||||
).toEqual({
|
||||
index: 0,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
expect(
|
||||
mergeInlineRange({ index: 1, length: 0 }, { index: 2, length: 0 })
|
||||
).toEqual({
|
||||
index: 1,
|
||||
length: 1,
|
||||
});
|
||||
|
||||
expect(
|
||||
mergeInlineRange({ index: 2, length: 0 }, { index: 1, length: 0 })
|
||||
).toEqual({
|
||||
index: 1,
|
||||
length: 1,
|
||||
});
|
||||
|
||||
expect(
|
||||
mergeInlineRange({ index: 1, length: 3 }, { index: 2, length: 2 })
|
||||
).toEqual({
|
||||
index: 1,
|
||||
length: 3,
|
||||
});
|
||||
|
||||
expect(
|
||||
mergeInlineRange({ index: 2, length: 2 }, { index: 1, length: 1 })
|
||||
).toEqual({
|
||||
index: 1,
|
||||
length: 3,
|
||||
});
|
||||
|
||||
expect(
|
||||
mergeInlineRange({ index: 3, length: 2 }, { index: 2, length: 1 })
|
||||
).toEqual({
|
||||
index: 2,
|
||||
length: 3,
|
||||
});
|
||||
|
||||
expect(
|
||||
mergeInlineRange({ index: 0, length: 4 }, { index: 1, length: 1 })
|
||||
).toEqual({
|
||||
index: 0,
|
||||
length: 4,
|
||||
});
|
||||
|
||||
expect(
|
||||
mergeInlineRange({ index: 1, length: 1 }, { index: 0, length: 4 })
|
||||
).toEqual({
|
||||
index: 0,
|
||||
length: 4,
|
||||
});
|
||||
|
||||
expect(
|
||||
mergeInlineRange({ index: 0, length: 2 }, { index: 1, length: 3 })
|
||||
).toEqual({
|
||||
index: 0,
|
||||
length: 4,
|
||||
});
|
||||
});
|
||||
|
||||
test('intersectInlineRange', () => {
|
||||
expect(
|
||||
intersectInlineRange({ index: 0, length: 0 }, { index: 1, length: 0 })
|
||||
).toEqual(null);
|
||||
|
||||
expect(
|
||||
intersectInlineRange({ index: 0, length: 2 }, { index: 1, length: 1 })
|
||||
).toEqual({ index: 1, length: 1 });
|
||||
|
||||
expect(
|
||||
intersectInlineRange({ index: 0, length: 2 }, { index: 2, length: 0 })
|
||||
).toEqual({ index: 2, length: 0 });
|
||||
|
||||
expect(
|
||||
intersectInlineRange({ index: 1, length: 0 }, { index: 1, length: 0 })
|
||||
).toEqual({ index: 1, length: 0 });
|
||||
|
||||
expect(
|
||||
intersectInlineRange({ index: 1, length: 3 }, { index: 2, length: 2 })
|
||||
).toEqual({ index: 2, length: 2 });
|
||||
|
||||
expect(
|
||||
intersectInlineRange({ index: 1, length: 2 }, { index: 0, length: 3 })
|
||||
).toEqual({ index: 1, length: 2 });
|
||||
|
||||
expect(
|
||||
intersectInlineRange({ index: 1, length: 1 }, { index: 2, length: 2 })
|
||||
).toEqual({ index: 2, length: 0 });
|
||||
|
||||
expect(
|
||||
intersectInlineRange({ index: 2, length: 2 }, { index: 1, length: 3 })
|
||||
).toEqual({ index: 2, length: 2 });
|
||||
|
||||
expect(
|
||||
intersectInlineRange({ index: 2, length: 1 }, { index: 1, length: 1 })
|
||||
).toEqual({ index: 2, length: 0 });
|
||||
|
||||
expect(
|
||||
intersectInlineRange({ index: 0, length: 4 }, { index: 1, length: 2 })
|
||||
).toEqual({ index: 1, length: 2 });
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
import type { DeltaInsert } from '@blocksuite/store';
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
import type { InlineEditor, InlineRange } from '../../inline/index.js';
|
||||
|
||||
const defaultPlaygroundURL = new URL(
|
||||
`http://localhost:${process.env.CI ? 4173 : 5173}/`
|
||||
);
|
||||
|
||||
export async function type(page: Page, content: string) {
|
||||
await page.keyboard.type(content, { delay: 50 });
|
||||
}
|
||||
|
||||
export async function press(page: Page, content: string) {
|
||||
await page.keyboard.press(content, { delay: 50 });
|
||||
await page.waitForTimeout(50);
|
||||
}
|
||||
|
||||
export async function enterInlineEditorPlayground(page: Page) {
|
||||
const url = new URL('examples/inline/index.html', defaultPlaygroundURL);
|
||||
await page.goto(url.toString());
|
||||
}
|
||||
|
||||
export async function focusInlineRichText(
|
||||
page: Page,
|
||||
index = 0
|
||||
): Promise<void> {
|
||||
await page.evaluate(index => {
|
||||
const richTexts = document
|
||||
.querySelector('test-page')
|
||||
?.querySelectorAll('test-rich-text');
|
||||
|
||||
if (!richTexts) {
|
||||
throw new Error('Cannot find test-rich-text');
|
||||
}
|
||||
|
||||
(richTexts[index] as any).inlineEditor.focusEnd();
|
||||
}, index);
|
||||
}
|
||||
|
||||
export async function getDeltaFromInlineRichText(
|
||||
page: Page,
|
||||
index = 0
|
||||
): Promise<DeltaInsert> {
|
||||
await page.waitForTimeout(100);
|
||||
return page.evaluate(index => {
|
||||
const richTexts = document
|
||||
.querySelector('test-page')
|
||||
?.querySelectorAll('test-rich-text');
|
||||
|
||||
if (!richTexts) {
|
||||
throw new Error('Cannot find test-rich-text');
|
||||
}
|
||||
|
||||
const editor = (richTexts[index] as any).inlineEditor as InlineEditor;
|
||||
return editor.yText.toDelta();
|
||||
}, index);
|
||||
}
|
||||
|
||||
export async function getInlineRangeFromInlineRichText(
|
||||
page: Page,
|
||||
index = 0
|
||||
): Promise<InlineRange | null> {
|
||||
await page.waitForTimeout(100);
|
||||
return page.evaluate(index => {
|
||||
const richTexts = document
|
||||
.querySelector('test-page')
|
||||
?.querySelectorAll('test-rich-text');
|
||||
|
||||
if (!richTexts) {
|
||||
throw new Error('Cannot find test-rich-text');
|
||||
}
|
||||
|
||||
const editor = (richTexts[index] as any).inlineEditor as InlineEditor;
|
||||
return editor.getInlineRange();
|
||||
}, index);
|
||||
}
|
||||
|
||||
export async function setInlineRichTextRange(
|
||||
page: Page,
|
||||
inlineRange: InlineRange,
|
||||
index = 0
|
||||
): Promise<void> {
|
||||
await page.evaluate(
|
||||
([inlineRange, index]) => {
|
||||
const richTexts = document
|
||||
.querySelector('test-page')
|
||||
?.querySelectorAll('test-rich-text');
|
||||
|
||||
if (!richTexts) {
|
||||
throw new Error('Cannot find test-rich-text');
|
||||
}
|
||||
|
||||
const editor = (richTexts[index as number] as any)
|
||||
.inlineEditor as InlineEditor;
|
||||
editor.setInlineRange(inlineRange as InlineRange);
|
||||
},
|
||||
[inlineRange, index]
|
||||
);
|
||||
}
|
||||
|
||||
export async function getInlineRichTextLine(
|
||||
page: Page,
|
||||
index: number,
|
||||
i = 0
|
||||
): Promise<readonly [string, number]> {
|
||||
return page.evaluate(
|
||||
([index, i]) => {
|
||||
const richTexts = document.querySelectorAll('test-rich-text');
|
||||
|
||||
if (!richTexts) {
|
||||
throw new Error('Cannot find test-rich-text');
|
||||
}
|
||||
|
||||
const editor = (richTexts[i] as any).inlineEditor as InlineEditor;
|
||||
const result = editor.getLine(index);
|
||||
if (!result) {
|
||||
throw new Error('Cannot find line');
|
||||
}
|
||||
const { line, rangeIndexRelatedToLine } = result;
|
||||
return [line.vTextContent, rangeIndexRelatedToLine] as const;
|
||||
},
|
||||
[index, i]
|
||||
);
|
||||
}
|
||||
|
||||
export async function getInlineRangeIndexRect(
|
||||
page: Page,
|
||||
[richTextIndex, inlineIndex]: [number, number],
|
||||
coordOffSet: { x: number; y: number } = { x: 0, y: 0 }
|
||||
) {
|
||||
const rect = await page.evaluate(
|
||||
({ richTextIndex, inlineIndex: vIndex, coordOffSet }) => {
|
||||
const richText = document.querySelectorAll('test-rich-text')[
|
||||
richTextIndex
|
||||
] as any;
|
||||
const domRange = richText.inlineEditor.toDomRange({
|
||||
index: vIndex,
|
||||
length: 0,
|
||||
});
|
||||
const pointBound = domRange.getBoundingClientRect();
|
||||
return {
|
||||
x: pointBound.left + coordOffSet.x,
|
||||
y: pointBound.top + pointBound.height / 2 + coordOffSet.y,
|
||||
};
|
||||
},
|
||||
{
|
||||
richTextIndex,
|
||||
inlineIndex,
|
||||
coordOffSet,
|
||||
}
|
||||
);
|
||||
return rect;
|
||||
}
|
||||
|
||||
export async function assertSelection(
|
||||
page: Page,
|
||||
richTextIndex: number,
|
||||
rangeIndex: number,
|
||||
rangeLength = 0
|
||||
) {
|
||||
const actual = await page.evaluate(
|
||||
([richTextIndex]) => {
|
||||
const richText =
|
||||
document?.querySelectorAll('test-rich-text')[richTextIndex];
|
||||
// @ts-expect-error getInlineRange
|
||||
const inlineEditor = richText.inlineEditor;
|
||||
return inlineEditor?.getInlineRange();
|
||||
},
|
||||
[richTextIndex]
|
||||
);
|
||||
expect(actual).toEqual({ index: rangeIndex, length: rangeLength });
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
import { BlockComponent } from '../view/index.js';
|
||||
import type {
|
||||
HeadingBlockModel,
|
||||
NoteBlockModel,
|
||||
RootBlockModel,
|
||||
} from './test-schema.js';
|
||||
|
||||
@customElement('test-root-block')
|
||||
export class RootBlockComponent extends BlockComponent<RootBlockModel> {
|
||||
override renderBlock() {
|
||||
return html`
|
||||
<div class="test-root-block">${this.renderChildren(this.model)}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('test-note-block')
|
||||
export class NoteBlockComponent extends BlockComponent<NoteBlockModel> {
|
||||
override renderBlock() {
|
||||
return html`
|
||||
<div class="test-note-block">${this.renderChildren(this.model)}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('test-h1-block')
|
||||
export class HeadingH1BlockComponent extends BlockComponent<HeadingBlockModel> {
|
||||
override renderBlock() {
|
||||
return html` <div class="test-heading-block h1">${this.model.text}</div> `;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('test-h2-block')
|
||||
export class HeadingH2BlockComponent extends BlockComponent<HeadingBlockModel> {
|
||||
override renderBlock() {
|
||||
return html` <div class="test-heading-block h2">${this.model.text}</div> `;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import type { ExtensionType, Store } from '@blocksuite/store';
|
||||
import { html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
|
||||
import { BlockStdScope } from '../scope/index.js';
|
||||
import { ShadowlessElement } from '../view/index.js';
|
||||
|
||||
@customElement('test-editor-container')
|
||||
export class TestEditorContainer extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
private _std!: BlockStdScope;
|
||||
|
||||
get std() {
|
||||
return this._std;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._std = new BlockStdScope({
|
||||
store: this.doc,
|
||||
extensions: this.specs,
|
||||
});
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
return html` <div class="test-editor-container">
|
||||
${this._std.render()}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor doc!: Store;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor specs: ExtensionType[] = [];
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
BlockModel,
|
||||
BlockSchemaExtension,
|
||||
defineBlockSchema,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
export const RootBlockSchema = defineBlockSchema({
|
||||
flavour: 'test:page',
|
||||
props: internal => ({
|
||||
title: internal.Text(),
|
||||
count: 0,
|
||||
style: {} as Record<string, unknown>,
|
||||
items: [] as unknown[],
|
||||
}),
|
||||
metadata: {
|
||||
version: 2,
|
||||
role: 'root',
|
||||
children: ['test:note'],
|
||||
},
|
||||
});
|
||||
|
||||
export const RootBlockSchemaExtension = BlockSchemaExtension(RootBlockSchema);
|
||||
|
||||
export class RootBlockModel extends BlockModel<
|
||||
ReturnType<(typeof RootBlockSchema)['model']['props']>
|
||||
> {}
|
||||
|
||||
export const NoteBlockSchema = defineBlockSchema({
|
||||
flavour: 'test:note',
|
||||
props: () => ({}),
|
||||
metadata: {
|
||||
version: 1,
|
||||
role: 'hub',
|
||||
parent: ['test:page'],
|
||||
children: ['test:heading'],
|
||||
},
|
||||
});
|
||||
|
||||
export const NoteBlockSchemaExtension = BlockSchemaExtension(NoteBlockSchema);
|
||||
|
||||
export class NoteBlockModel extends BlockModel<
|
||||
ReturnType<(typeof NoteBlockSchema)['model']['props']>
|
||||
> {}
|
||||
|
||||
export const HeadingBlockSchema = defineBlockSchema({
|
||||
flavour: 'test:heading',
|
||||
props: internal => ({
|
||||
type: 'h1',
|
||||
text: internal.Text(),
|
||||
}),
|
||||
metadata: {
|
||||
version: 1,
|
||||
role: 'content',
|
||||
parent: ['test:note'],
|
||||
},
|
||||
});
|
||||
|
||||
export const HeadingBlockSchemaExtension =
|
||||
BlockSchemaExtension(HeadingBlockSchema);
|
||||
|
||||
export class HeadingBlockModel extends BlockModel<
|
||||
ReturnType<(typeof HeadingBlockSchema)['model']['props']>
|
||||
> {}
|
||||
@@ -0,0 +1,23 @@
|
||||
import './test-block.js';
|
||||
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { BlockViewExtension } from '../extension/index.js';
|
||||
import type { HeadingBlockModel } from './test-schema.js';
|
||||
|
||||
export const testSpecs: ExtensionType[] = [
|
||||
BlockViewExtension('test:page', literal`test-root-block`),
|
||||
|
||||
BlockViewExtension('test:note', literal`test-note-block`),
|
||||
|
||||
BlockViewExtension('test:heading', model => {
|
||||
const h = (model as HeadingBlockModel).props.type$.value;
|
||||
|
||||
if (h === 'h1') {
|
||||
return literal`test-h1-block`;
|
||||
}
|
||||
|
||||
return literal`test-h2-block`;
|
||||
}),
|
||||
];
|
||||
Reference in New Issue
Block a user