Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"packages/*"
],
"scripts": {
"build": "yarn workspaces foreach -At run build",
"test": "yarn workspaces foreach -A run test",
"lint": "yarn workspaces foreach -A run lint",
"lint:fix": "yarn workspaces foreach -A run lint --fix"
}
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/components/BlockRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,9 @@ export class BlockRenderer {
tool: data.name,
index: index.blockIndex,
}));

/**
* @todo clear block tool adapter memory
*/
}
}
250 changes: 243 additions & 7 deletions packages/model/src/entities/BlockNode/BlockNode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { NODE_TYPE_HIDDEN_PROP } from './consts.js';
import { TextAddedEvent, TuneModifiedEvent, ValueModifiedEvent } from '../../EventBus/events/index.js';
import { EventType } from '../../EventBus/types/EventType.js';
import { createBlockTuneName } from '../BlockTune/index.js';
import { get } from '../../utils/keypath.js';
import { AlreadyExistingKeyError } from './errors/AlreadyExistingKeyError.js';

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- needed to spy on conditional-typed getter with @jest/globals strict types
const ValueNodeProto = ValueNode.prototype as unknown as {
Expand Down Expand Up @@ -508,29 +510,151 @@ describe('BlockNode', () => {
}));
});

it('should not change the node if key already exists', () => {
it('should throw an error if key already exists', () => {
const key = createDataKey('url');
const value = 'https://editorjs.io';
const blockNode = createBlockNodeWithData({ [key]: value });

const currentNode = blockNode.data[key];

blockNode.createDataNode(key, 'another value');
expect(() => {
blockNode.createDataNode(key, 'another value');
}).toThrowError(AlreadyExistingKeyError);
});

it('should create value node at a nested path within an object', () => {
const blockNode = createBlockNodeWithData({});
const key = createDataKey('meta.url');
const value = 'https://editorjs.io';

expect(blockNode.data[key]).toStrictEqual(currentNode);
blockNode.createDataNode(key, value);

expect(get(blockNode.data, 'meta.url')).toBeInstanceOf(ValueNode);
});

it('should not emit DataNodeAddedEvent if key already exists', () => {
const key = createDataKey('url');
it('should create text node at a nested path within an object', () => {
const blockNode = createBlockNodeWithData({});
const key = createDataKey('meta.title');
const value = { [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text,
value: 'hello',
fragments: [] };

blockNode.createDataNode(key, value);

expect(get(blockNode.data, 'meta.title')).toBeInstanceOf(TextNode);
});

it('should create value node at an array index path', () => {
const blockNode = createBlockNodeWithData({});
const key = createDataKey('items.0');
const value = 'first item';

blockNode.createDataNode(key, value);

expect(get(blockNode.data, 'items.0')).toBeInstanceOf(ValueNode);
});

it('should create value node in a nested object inside an array', () => {
Comment thread
ilyamore88 marked this conversation as resolved.
const blockNode = createBlockNodeWithData({});
const key = createDataKey('items.0.content');
const value = 'content text';

blockNode.createDataNode(key, value);

expect(get(blockNode.data, 'items.0.content')).toBeInstanceOf(ValueNode);
});

it('should create text node in a nested object inside an array', () => {
const blockNode = createBlockNodeWithData({});
const key = createDataKey('items.0.content');
const value = {
value: 'text',
fragments: [],
$t: 't',
};

blockNode.createDataNode(key, value);

expect(get(blockNode.data, 'items.0.content')).toBeInstanceOf(TextNode);
});

it('should throw an error if a nested key already exists', () => {
const blockNode = createBlockNodeWithData({ meta: { url: 'editorjs.io' } });
const key = createDataKey('meta.url');
const existingNode = get(blockNode.data, 'meta.url');

expect(() => blockNode.createDataNode(key, 'another value'))
.toThrowError(AlreadyExistingKeyError);

expect(get(blockNode.data, 'meta.url')).toStrictEqual(existingNode);
});

it('should emit DataNodeAddedEvent with nested dataKey', async () => {
const blockNode = createBlockNodeWithData({});
const key = createDataKey('meta.url');
const value = 'https://editorjs.io';
const blockNode = createBlockNodeWithData({ [key]: value });
const listener = jest.fn();

blockNode.addEventListener(EventType.Changed, listener);

blockNode.createDataNode(key, value);

expect(listener).not.toHaveBeenCalled();
await Promise.resolve();

expect(listener).toBeCalledWith(expect.objectContaining({
detail: expect.objectContaining({
action: EventAction.Added,
index: expect.objectContaining({ dataKey: key }),
}),
}));
});

it('should splice a new node into an existing array at the given index', () => {
const blockNode = createBlockNodeWithData({
items: [
{ [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text,
value: 'first',
fragments: [] },
{ [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text,
value: 'third',
fragments: [] },
],
});

blockNode.createDataNode(createDataKey('items.1'), {
[NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text,
value: 'second',
fragments: [],
});

const items = (blockNode.data as Record<string, unknown[]>)['items'];
const expectedLength = 3;

expect(items).toHaveLength(expectedLength);
expect(items[1]).toBeInstanceOf(TextNode);
});

it('should shift existing nodes right when splicing into an array', () => {
const blockNode = createBlockNodeWithData({
items: [
{ [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text,
value: 'second',
fragments: [] },
],
});

const originalNode = (blockNode.data as Record<string, unknown[]>)['items'][0];

blockNode.createDataNode(createDataKey('items.0'), {
[NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text,
value: 'first',
fragments: [],
});

const items = (blockNode.data as Record<string, unknown[]>)['items'];

expect(items).toHaveLength(2);
expect(items[1]).toStrictEqual(originalNode);
});
});

Expand All @@ -544,6 +668,20 @@ describe('BlockNode', () => {
expect(result).toBeUndefined();
});

it('should return undefined if the nested key does not exist', () => {
const blockNode = createBlockNodeWithData({});
const result = blockNode.getDataNode(createDataKey('meta.nonexistent'));

expect(result).toBeUndefined();
});

it('should return undefined if the array index does not exist', () => {
const blockNode = createBlockNodeWithData({});
const result = blockNode.getDataNode(createDataKey('meta.0'));

expect(result).toBeUndefined();
});

it('should return serialized ValueNode for a value key', () => {
const key = createDataKey('url');
const value = 'https://editorjs.io';
Expand Down Expand Up @@ -630,6 +768,62 @@ describe('BlockNode', () => {

expect(listener).not.toHaveBeenCalled();
});

it('should remove data at a nested object path', () => {
const blockNode = createBlockNodeWithData({ meta: { url: 'editorjs.io' } });

blockNode.removeDataNode(createDataKey('meta.url'));

expect(get(blockNode.data, 'meta.url')).toBeUndefined();
});

it('should not remove sibling properties when removing a nested key', () => {
const blockNode = createBlockNodeWithData({ meta: { url: 'editorjs.io',
title: 'Editor.js' } });

blockNode.removeDataNode(createDataKey('meta.url'));

expect(get(blockNode.data, 'meta.title')).toBeDefined();
});

it('should remove a node at an array index path', () => {
const blockNode = createBlockNodeWithData({ items: ['first', 'second'] });

blockNode.removeDataNode(createDataKey('items.0'));

// After splice, 'second' shifts to index 0
expect((blockNode.data as Record<string, unknown[]>)['items']).toHaveLength(1);
});

it('should emit DataNodeRemovedEvent with a nested dataKey', () => {
const blockNode = createBlockNodeWithData({ meta: { url: 'editorjs.io' } });
const key = createDataKey('meta.url');
const listener = jest.fn();

jest.spyOn(ValueNodeProto, 'serialized', 'get').mockReturnValueOnce('editorjs.io');

blockNode.addEventListener(EventType.Changed, listener);

blockNode.removeDataNode(key);

expect(listener).toBeCalledWith(expect.objectContaining({
detail: expect.objectContaining({
action: EventAction.Removed,
index: expect.objectContaining({ dataKey: key }),
}),
}));
});

it('should not emit DataNodeRemovedEvent if nested key doesnt exist', () => {
const blockNode = createBlockNodeWithData({ meta: {} });
const listener = jest.fn();

blockNode.addEventListener(EventType.Changed, listener);

blockNode.removeDataNode(createDataKey('meta.nonexistent'));

expect(listener).not.toHaveBeenCalled();
});
});

describe('.updateTuneData()', () => {
Expand Down Expand Up @@ -766,6 +960,30 @@ describe('BlockNode', () => {
expect(blockNode.data[dataKey]).toBeInstanceOf(ValueNode);
});

it('should create new ValueNode at a nested path if the node does not exist', () => {
const blockNode = new BlockNode({
name: createBlockToolName('paragraph'),
data: {},
parent: {} as EditorDocument,
});

blockNode.updateValue(createDataKey('meta.url'), 'https://editorjs.io');

expect(get(blockNode.data, 'meta.url')).toBeInstanceOf(ValueNode);
});

it('should create new ValueNode inside an array if the node does not exist', () => {
const blockNode = new BlockNode({
name: createBlockToolName('paragraph'),
data: {},
parent: {} as EditorDocument,
});

blockNode.updateValue(createDataKey('items.0'), 'first item');

expect(get(blockNode.data, 'items.0')).toBeInstanceOf(ValueNode);
});

it('should throw an error if the ValueNode with the passed dataKey is not a ValueNode', () => {
const dataKey = createDataKey('data-key-1a2b');
const value = 'Some value';
Expand Down Expand Up @@ -913,6 +1131,22 @@ describe('BlockNode', () => {
expect(node.data[key]).toBeInstanceOf(TextNode);
});

it('should create new TextNode at a nested path if the node does not exist', () => {
const node = createBlockNodeWithData({});

node.insertText(createDataKey('meta.title'), text);

expect(get(node.data, 'meta.title')).toBeInstanceOf(TextNode);
});

it('should create new TextNode inside an array if the node does not exist', () => {
const node = createBlockNodeWithData({});

node.insertText(createDataKey('items.0'), text);

expect(get(node.data, 'items.0')).toBeInstanceOf(TextNode);
});

it('should throw an error if node is not a TextNode', () => {
const node = new BlockNode({
name: createBlockToolName('header'),
Expand Down Expand Up @@ -1503,6 +1737,8 @@ describe('BlockNode', () => {
tuneKey: key,
tuneName: tuneName,
}));
expect(event)
Comment thread
ilyamore88 marked this conversation as resolved.
.toHaveProperty('detail.userId', 'user');
});

it('should not emit Changed event if ValueNode dispatched event that is not a BaseDocumentEvent', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { DataKey } from '../types/index.js';

/**
* Error is thrown on attempt to create data with already existing key
*/
Comment on lines +3 to +5
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc

export class AlreadyExistingKeyError extends Error {
/**
* AlreadyExistingKeyError constructor
* @param key - data key existing node
*/
constructor(key: DataKey) {
super(`BlockNode: data with key "${key}" already exists`);
}
}
Loading
Loading