From 4d966d41aa83e3442be6c569b0f1fcf6848950fa Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Thu, 5 Oct 2023 21:36:28 +0100 Subject: [PATCH 01/11] implementation --- package.json | 2 +- src/entities/BlockNode/index.ts | 69 ++++++++++++++++--- .../BlockNode/types/BlockChildType.ts | 4 ++ .../types/BlockNodeConstructorParameters.ts | 11 ++- src/entities/BlockNode/types/BlockNodeData.ts | 8 ++- .../BlockNode/types/BlockNodeSerialized.ts | 13 +++- src/entities/BlockNode/types/DataKey.ts | 2 +- src/entities/BlockNode/types/index.ts | 5 +- src/entities/BlockTune/index.ts | 5 +- .../types/BlockTuneConstructorParameters.ts | 3 +- .../BlockTune/types/BlockTuneSerialized.ts | 13 +--- src/entities/EditorDocument/index.ts | 32 +++++++-- .../EditorDocumentConstructorParameters.ts | 4 +- .../types/EditorDocumentSerialized.ts | 7 ++ src/entities/EditorDocument/types/index.ts | 1 + src/entities/ValueNode/index.ts | 13 +++- .../ValueNode/types/ValueSerialized.ts | 3 + src/entities/ValueNode/types/index.ts | 1 + .../inline-fragments/InlineNode/index.ts | 9 ++- .../ParentInlineNode/index.ts | 4 +- .../inline-fragments/TextInlineNode/index.ts | 4 +- .../inline-fragments/TextNode/index.ts | 10 ++- .../specs/InlineTree.integration.spec.ts | 4 +- src/tools/ToolsRegistry.ts | 3 +- src/tools/types/BlockDataType.ts | 7 -- src/tools/types/BlockToolConstructable.ts | 9 +-- src/tools/types/BlockToolDataScheme.ts | 9 --- src/tools/types/index.ts | 2 - yarn.lock | 18 ++--- 29 files changed, 182 insertions(+), 93 deletions(-) create mode 100644 src/entities/BlockNode/types/BlockChildType.ts create mode 100644 src/entities/EditorDocument/types/EditorDocumentSerialized.ts create mode 100644 src/entities/ValueNode/types/ValueSerialized.ts delete mode 100644 src/tools/types/BlockDataType.ts delete mode 100644 src/tools/types/BlockToolDataScheme.ts diff --git a/package.json b/package.json index 50f0ba3f..fd77e0e1 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,6 @@ "stryker-cli": "^1.0.2", "ts-jest": "^29.1.0", "ts-node": "^10.9.1", - "typescript": "^5.0.4" + "typescript": "^5.2.2" } } diff --git a/src/entities/BlockNode/index.ts b/src/entities/BlockNode/index.ts index f9e262b8..06133b49 100644 --- a/src/entities/BlockNode/index.ts +++ b/src/entities/BlockNode/index.ts @@ -1,15 +1,21 @@ import { EditorDocument } from '../EditorDocument'; -import { BlockTune, BlockTuneName } from '../BlockTune'; +import { BlockTune, BlockTuneName, createBlockTuneName } from '../BlockTune'; import { BlockNodeConstructorParameters, BlockToolName, createBlockToolName, DataKey, - createDataKey, BlockNodeData, - BlockNodeSerialized + createDataKey, + BlockNodeData, + BlockNodeSerialized, + BlockNodeDataSerialized, + BlockNodeDataSerializedValue, + BlockChildType, + ChildNode, + BlockNodeDataValue } from './types'; import { ValueNode } from '../ValueNode'; -import { InlineToolData, InlineToolName, TextNode } from '../inline-fragments'; +import { InlineToolData, InlineToolName, TextNode, TextNodeSerialized } from '../inline-fragments'; /** * BlockNode class represents a node in a tree-like structure used to store and manipulate Blocks in an editor document. @@ -25,7 +31,7 @@ export class BlockNode { /** * Field representing the content of the Block */ - #data: BlockNodeData; + #data: BlockNodeData = {}; /** * Field representing the parent EditorDocument of the BlockNode @@ -47,10 +53,22 @@ export class BlockNode { * @param [args.tunes] - The BlockTunes associated with the BlockNode. */ constructor({ name, data = {}, parent, tunes = {} }: BlockNodeConstructorParameters) { - this.#name = name; - this.#data = data; + this.#name = createBlockToolName(name); this.#parent = parent ?? null; - this.#tunes = tunes; + this.#tunes = Object.fromEntries( + Object.entries(tunes) + .map( + ([tuneName, tuneData]) => ([ + createBlockTuneName(tuneName), + new BlockTune({ + name: createBlockTuneName(tuneName), + data: tuneData, + }), + ]) + ) + ); + + this.#initialize(data); } /** @@ -174,6 +192,41 @@ export class BlockNode { node.unformat(tool, start, end); } + /** + * + * @param data + */ + #initialize(data: BlockNodeDataSerialized): void { + const map = (value: BlockNodeDataSerializedValue): BlockNodeData | BlockNodeDataValue => { + if (Array.isArray(value)) { + return value.map(map) as BlockNodeData[] | ChildNode[]; + } + + if (typeof value === 'object' && value !== null) { + if ('$t' in value) { + switch (value.$t) { + case BlockChildType.Value: + return new ValueNode({ value }); + case BlockChildType.Text: + return new TextNode(value as TextNodeSerialized); + } + } + + return Object.fromEntries( + Object.entries(value) + .map(([key, v]) => ([key, map(v)])) + ); + } + + return new ValueNode({ value }); + }; + + this.#data = Object.fromEntries( + Object.entries(data) + .map(([key, value]) => ([key, map(value)])) + ); + } + /** * Validates data key and node type * diff --git a/src/entities/BlockNode/types/BlockChildType.ts b/src/entities/BlockNode/types/BlockChildType.ts new file mode 100644 index 00000000..a61b6416 --- /dev/null +++ b/src/entities/BlockNode/types/BlockChildType.ts @@ -0,0 +1,4 @@ +export enum BlockChildType { + Value = 'v', + Text = 't', +} diff --git a/src/entities/BlockNode/types/BlockNodeConstructorParameters.ts b/src/entities/BlockNode/types/BlockNodeConstructorParameters.ts index 5a8d9e62..2358d903 100644 --- a/src/entities/BlockNode/types/BlockNodeConstructorParameters.ts +++ b/src/entities/BlockNode/types/BlockNodeConstructorParameters.ts @@ -1,18 +1,17 @@ import { EditorDocument } from '../../EditorDocument'; -import { BlockTune, BlockTuneName } from '../../BlockTune'; -import { BlockToolName } from './BlockToolName'; -import { BlockNodeData } from './BlockNodeData'; +import { BlockTuneSerialized } from '../../BlockTune'; +import { BlockNodeDataSerialized } from './BlockNodeSerialized'; export interface BlockNodeConstructorParameters { /** * The name of the tool created a Block */ - name: BlockToolName; + name: string; /** * The content of the Block */ - data?: BlockNodeData; + data?: BlockNodeDataSerialized; /** * The parent EditorDocument of the BlockNode @@ -22,5 +21,5 @@ export interface BlockNodeConstructorParameters { /** * The BlockTunes associated with the BlockNode */ - tunes?: Record; + tunes?: Record; } diff --git a/src/entities/BlockNode/types/BlockNodeData.ts b/src/entities/BlockNode/types/BlockNodeData.ts index e4d30ec5..18c1d2e5 100644 --- a/src/entities/BlockNode/types/BlockNodeData.ts +++ b/src/entities/BlockNode/types/BlockNodeData.ts @@ -2,8 +2,14 @@ import { DataKey } from './DataKey'; import { ValueNode } from '../../ValueNode'; import { TextNode } from '../../inline-fragments'; +export type ChildNode = ValueNode | TextNode; + /** * Represents a record object containing the data of a block node. * Each root node is associated with a specific data key. */ -export type BlockNodeData = Record; +export interface BlockNodeData { + [key: DataKey]: BlockNodeDataValue; +} + +export type BlockNodeDataValue = ChildNode | ChildNode[] | BlockNodeData | BlockNodeData[]; diff --git a/src/entities/BlockNode/types/BlockNodeSerialized.ts b/src/entities/BlockNode/types/BlockNodeSerialized.ts index 6c110b2e..1c420030 100644 --- a/src/entities/BlockNode/types/BlockNodeSerialized.ts +++ b/src/entities/BlockNode/types/BlockNodeSerialized.ts @@ -1,4 +1,15 @@ import { BlockTuneSerialized } from '../../BlockTune'; +import { ValueSerialized } from '../../ValueNode/types'; +import { TextNodeSerialized } from '../../inline-fragments'; + +export type BlockChildNodeSerialized = ValueSerialized | TextNodeSerialized; + +export type BlockNodeDataSerializedValue = BlockChildNodeSerialized | BlockChildNodeSerialized[] | BlockNodeDataSerialized | BlockNodeDataSerialized[]; + +export interface BlockNodeDataSerialized { + [key: string]: BlockNodeDataSerializedValue; +} + /** * Serialized version of the BlockNode @@ -12,7 +23,7 @@ export interface BlockNodeSerialized { /** * The content of the Block */ - data: Record; // @todo replace unknown type with serialized root node and value node + data: BlockNodeDataSerialized; /** * Serialized BlockTunes associated with the BlockNode diff --git a/src/entities/BlockNode/types/DataKey.ts b/src/entities/BlockNode/types/DataKey.ts index e63925ab..7c232300 100644 --- a/src/entities/BlockNode/types/DataKey.ts +++ b/src/entities/BlockNode/types/DataKey.ts @@ -3,7 +3,7 @@ import { create, Nominal } from '../../../utils/Nominal'; /** * Base type of the data key field */ -type DataKeyBase = string; +type DataKeyBase = string | number; /** * Nominal type for the data key field diff --git a/src/entities/BlockNode/types/index.ts b/src/entities/BlockNode/types/index.ts index 058d09bf..2efb8618 100644 --- a/src/entities/BlockNode/types/index.ts +++ b/src/entities/BlockNode/types/index.ts @@ -1,5 +1,6 @@ export { BlockNodeConstructorParameters } from './BlockNodeConstructorParameters'; export { BlockToolName, createBlockToolName } from './BlockToolName'; export { DataKey, createDataKey } from './DataKey'; -export { BlockNodeData } from './BlockNodeData'; -export { BlockNodeSerialized } from './BlockNodeSerialized'; +export { BlockNodeData, ChildNode, BlockNodeDataValue } from './BlockNodeData'; +export { BlockNodeSerialized, BlockChildNodeSerialized, BlockNodeDataSerialized, BlockNodeDataSerializedValue } from './BlockNodeSerialized'; +export { BlockChildType } from './BlockChildType'; diff --git a/src/entities/BlockTune/index.ts b/src/entities/BlockTune/index.ts index ad9b1d97..138132c7 100644 --- a/src/entities/BlockTune/index.ts +++ b/src/entities/BlockTune/index.ts @@ -41,10 +41,7 @@ export class BlockTune { * Returns serialized version of the BlockTune. */ public get serialized(): BlockTuneSerialized { - return { - name: this.#name, - data: this.#data, - }; + return this.#data; } } diff --git a/src/entities/BlockTune/types/BlockTuneConstructorParameters.ts b/src/entities/BlockTune/types/BlockTuneConstructorParameters.ts index b292da5a..8a6d3ccf 100644 --- a/src/entities/BlockTune/types/BlockTuneConstructorParameters.ts +++ b/src/entities/BlockTune/types/BlockTuneConstructorParameters.ts @@ -1,4 +1,5 @@ import { BlockTuneName } from './BlockTuneName'; +import { BlockTuneSerialized } from './BlockTuneSerialized'; export interface BlockTuneConstructorParameters { /** @@ -9,5 +10,5 @@ export interface BlockTuneConstructorParameters { /** * Any additional data associated with the tune */ - data?: Record; + data?: BlockTuneSerialized; } diff --git a/src/entities/BlockTune/types/BlockTuneSerialized.ts b/src/entities/BlockTune/types/BlockTuneSerialized.ts index 4aa08aa0..8892f4f1 100644 --- a/src/entities/BlockTune/types/BlockTuneSerialized.ts +++ b/src/entities/BlockTune/types/BlockTuneSerialized.ts @@ -1,15 +1,4 @@ /** * BlockTuneSerialized represents a serialized version of a BlockTune. */ -export interface BlockTuneSerialized { - /** - * The name of the tune. - * Serialized as a string. - */ - name: string; - - /** - * Any additional data associated with the tune. - */ - data: Record; -} +export type BlockTuneSerialized = Record; diff --git a/src/entities/EditorDocument/index.ts b/src/entities/EditorDocument/index.ts index c27cb9a6..3d2aef30 100644 --- a/src/entities/EditorDocument/index.ts +++ b/src/entities/EditorDocument/index.ts @@ -1,9 +1,10 @@ import { BlockNode, DataKey } from '../BlockNode'; -import type { BlockNodeData, EditorDocumentConstructorParameters, Properties } from './types'; +import type { EditorDocumentSerialized, EditorDocumentConstructorParameters, Properties } from './types'; import { BlockTuneName } from '../BlockTune'; import { InlineToolData, InlineToolName } from '../inline-fragments'; import { IoCContainer, TOOLS_REGISTRY } from '../../IoC'; import { ToolsRegistry } from '../../tools'; +import { BlockNodeSerialized } from '../BlockNode/types'; /** * EditorDocument class represents the top-level container for a tree-like structure of BlockNodes in an editor document. @@ -13,7 +14,7 @@ export class EditorDocument { /** * Private field representing the child BlockNodes of the EditorDocument */ - #children: BlockNode[]; + #children: BlockNode[] = []; /** * Private field representing the properties of the document @@ -28,13 +29,14 @@ export class EditorDocument { * @param [args.properties] - The properties of the document. * @param [args.toolsRegistry] - ToolsRegistry instance for the current document. Defaults to a new ToolsRegistry instance. */ - constructor({ children = [], properties = {}, toolsRegistry = new ToolsRegistry() }: EditorDocumentConstructorParameters = {}) { - this.#children = children; + constructor({ blocks = [], properties = {}, toolsRegistry = new ToolsRegistry() }: EditorDocumentConstructorParameters = {}) { this.#properties = properties; const container = IoCContainer.of(this); container.set(TOOLS_REGISTRY, toolsRegistry); + + this.#initialize(blocks); } /** @@ -52,7 +54,7 @@ export class EditorDocument { * @param index - The index at which to add the BlockNode * @throws Error if the index is out of bounds */ - public addBlock(blockNodeData: BlockNodeData, index?: number): void { + public addBlock(blockNodeData: BlockNodeSerialized, index?: number): void { const blockNode = new BlockNode({ ...blockNodeData, parent: this, @@ -209,6 +211,26 @@ export class EditorDocument { this.#children[blockIndex].unformat(key, tool, start, end); } + /** + * + */ + public get serialized(): EditorDocumentSerialized { + return { + blocks: this.#children.map((block) => block.serialized), + properties: this.#properties, + }; + } + + /** + * + * @param blocks + */ + #initialize(blocks: BlockNodeSerialized[]): void { + blocks.forEach((block) => { + this.addBlock(block); + }); + } + /** * Checks if the index is out of bounds. * diff --git a/src/entities/EditorDocument/types/EditorDocumentConstructorParameters.ts b/src/entities/EditorDocument/types/EditorDocumentConstructorParameters.ts index 5804fd3c..2ce635ed 100644 --- a/src/entities/EditorDocument/types/EditorDocumentConstructorParameters.ts +++ b/src/entities/EditorDocument/types/EditorDocumentConstructorParameters.ts @@ -1,12 +1,12 @@ -import type { BlockNode } from '../../BlockNode'; import type { Properties } from './Properties'; import { ToolsRegistry } from '../../../tools/ToolsRegistry'; +import { BlockNodeSerialized } from '../../BlockNode/types'; export interface EditorDocumentConstructorParameters { /** * The child BlockNodes of the EditorDocument */ - children?: BlockNode[]; + blocks?: BlockNodeSerialized[]; /** * The properties of the document diff --git a/src/entities/EditorDocument/types/EditorDocumentSerialized.ts b/src/entities/EditorDocument/types/EditorDocumentSerialized.ts new file mode 100644 index 00000000..ff05612d --- /dev/null +++ b/src/entities/EditorDocument/types/EditorDocumentSerialized.ts @@ -0,0 +1,7 @@ +import { BlockNodeSerialized } from '../../BlockNode/types'; +import { Properties } from './Properties'; + +export interface EditorDocumentSerialized { + blocks: BlockNodeSerialized[]; + properties: Properties; +} diff --git a/src/entities/EditorDocument/types/index.ts b/src/entities/EditorDocument/types/index.ts index f5425ddc..923193ee 100644 --- a/src/entities/EditorDocument/types/index.ts +++ b/src/entities/EditorDocument/types/index.ts @@ -1,3 +1,4 @@ export type { EditorDocumentConstructorParameters } from './EditorDocumentConstructorParameters'; export type { Properties } from './Properties'; export type { BlockNodeData } from './BlockNodeData'; +export type { EditorDocumentSerialized } from './EditorDocumentSerialized'; diff --git a/src/entities/ValueNode/index.ts b/src/entities/ValueNode/index.ts index 00ed18a2..243f28bb 100644 --- a/src/entities/ValueNode/index.ts +++ b/src/entities/ValueNode/index.ts @@ -1,4 +1,5 @@ -import type { ValueNodeConstructorParameters } from './types'; +import type { ValueNodeConstructorParameters, ValueSerialized } from './types'; +import { BlockChildType } from '../BlockNode/types'; /** * ValueNode class represents a node in a tree-like structure, used to store and manipulate data associated with a BlockNode. @@ -33,8 +34,14 @@ export class ValueNode { /** * Returns serialized data associated with this value node. */ - public get serialized(): ValueType { - return this.#value; + public get serialized(): ValueSerialized { + let value = this.#value; + + if (typeof value === 'object' && this.#value !== null) { + value = Object.assign({ $t: BlockChildType.Value }, value); + } + + return value as ValueSerialized; } } diff --git a/src/entities/ValueNode/types/ValueSerialized.ts b/src/entities/ValueNode/types/ValueSerialized.ts new file mode 100644 index 00000000..ab557327 --- /dev/null +++ b/src/entities/ValueNode/types/ValueSerialized.ts @@ -0,0 +1,3 @@ +import { BlockChildType } from '../../BlockNode/types'; + +export type ValueSerialized = V extends Record ? V & { $t: BlockChildType.Value } : V; diff --git a/src/entities/ValueNode/types/index.ts b/src/entities/ValueNode/types/index.ts index fb3eb3d1..728236d2 100644 --- a/src/entities/ValueNode/types/index.ts +++ b/src/entities/ValueNode/types/index.ts @@ -1 +1,2 @@ export { ValueNodeConstructorParameters } from './ValueNodeConstructorParameters'; +export { ValueSerialized } from './ValueSerialized'; diff --git a/src/entities/inline-fragments/InlineNode/index.ts b/src/entities/inline-fragments/InlineNode/index.ts index 6ed5cb6a..53da4583 100644 --- a/src/entities/inline-fragments/InlineNode/index.ts +++ b/src/entities/inline-fragments/InlineNode/index.ts @@ -1,4 +1,5 @@ import { InlineToolData, InlineToolName } from '../FormattingInlineNode'; +import { BlockChildType } from '../../BlockNode/types'; /** * Interface describing abstract InlineNode — common properties and methods for all inline nodes @@ -12,7 +13,7 @@ export interface InlineNode { /** * Serialized value of the node */ - readonly serialized: TextNodeSerialized; + readonly serialized: ChildTextNodeSerialized; /** * Returns text value in passed range @@ -122,7 +123,7 @@ export interface InlineFragment { /** * Serialized Inline Node value */ -export interface TextNodeSerialized { +export interface ChildTextNodeSerialized { /** * Text value of the node and its subtree */ @@ -133,3 +134,7 @@ export interface TextNodeSerialized { */ fragments: InlineFragment[]; } + +export interface TextNodeSerialized extends ChildTextNodeSerialized { + $t: BlockChildType.Text; +} diff --git a/src/entities/inline-fragments/ParentInlineNode/index.ts b/src/entities/inline-fragments/ParentInlineNode/index.ts index 484cfaa1..1d0d1758 100644 --- a/src/entities/inline-fragments/ParentInlineNode/index.ts +++ b/src/entities/inline-fragments/ParentInlineNode/index.ts @@ -1,4 +1,4 @@ -import { InlineFragment, InlineNode, TextNodeSerialized } from '../InlineNode'; +import { InlineFragment, InlineNode, ChildTextNodeSerialized } from '../InlineNode'; import { ParentNode, ParentNodeConstructorOptions } from '../mixins/ParentNode'; import { ChildNode } from '../mixins/ChildNode'; import type { InlineToolData, InlineToolName } from '../FormattingInlineNode'; @@ -30,7 +30,7 @@ export class ParentInlineNode implements InlineNode { /** * Returns serialized value of the node: text and formatting fragments */ - public get serialized(): TextNodeSerialized { + public get serialized(): ChildTextNodeSerialized { return { text: this.getText(), fragments: this.getFragments(), diff --git a/src/entities/inline-fragments/TextInlineNode/index.ts b/src/entities/inline-fragments/TextInlineNode/index.ts index 4252db46..986e3d9b 100644 --- a/src/entities/inline-fragments/TextInlineNode/index.ts +++ b/src/entities/inline-fragments/TextInlineNode/index.ts @@ -1,6 +1,6 @@ import { FormattingInlineNode, InlineToolName, InlineToolData } from '../index'; import { TextInlineNodeConstructorParameters } from './types'; -import { InlineNode, TextNodeSerialized } from '../InlineNode'; +import { InlineNode, ChildTextNodeSerialized } from '../InlineNode'; import { ChildNode } from '../mixins/ChildNode'; export * from './types'; @@ -37,7 +37,7 @@ export class TextInlineNode implements InlineNode { /** * Returns serialized value of the node */ - public get serialized(): TextNodeSerialized { + public get serialized(): ChildTextNodeSerialized { return { text: this.getText(), // No fragments for text node diff --git a/src/entities/inline-fragments/TextNode/index.ts b/src/entities/inline-fragments/TextNode/index.ts index 2f6dbb4d..0508863f 100644 --- a/src/entities/inline-fragments/TextNode/index.ts +++ b/src/entities/inline-fragments/TextNode/index.ts @@ -1,4 +1,5 @@ -import { InlineFragment, ParentInlineNode, ParentInlineNodeConstructorOptions } from '../index'; +import { InlineFragment, ParentInlineNode, ParentInlineNodeConstructorOptions, TextNodeSerialized } from '../index'; +import { BlockChildType } from '../../BlockNode/types'; interface TextNodeConstructorOptions extends ParentInlineNodeConstructorOptions { value?: string; @@ -23,6 +24,13 @@ export class TextNode extends ParentInlineNode { this.#initialize(value, fragments); } + /** + * Returns serialized TextNode + */ + public get serialized(): TextNodeSerialized { + return Object.assign({ $t: BlockChildType.Text as const }, super.serialized); + } + /** * Private method to initialize the TextNode with passed initial data * diff --git a/src/entities/inline-fragments/specs/InlineTree.integration.spec.ts b/src/entities/inline-fragments/specs/InlineTree.integration.spec.ts index 6e1ac2ab..4439821c 100644 --- a/src/entities/inline-fragments/specs/InlineTree.integration.spec.ts +++ b/src/entities/inline-fragments/specs/InlineTree.integration.spec.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-magic-numbers */ -import { TextInlineNode, TextNode, createInlineToolData, createInlineToolName, TextNodeSerialized } from '../index'; +import { TextInlineNode, TextNode, createInlineToolData, createInlineToolName, ChildTextNodeSerialized } from '../index'; describe('Inline fragments tree integration', () => { describe('text insertion', () => { @@ -421,7 +421,7 @@ describe('Inline fragments tree integration', () => { ], }, ], - } as TextNodeSerialized; + } as ChildTextNodeSerialized; const tree = new TextNode({ value: data.text, diff --git a/src/tools/ToolsRegistry.ts b/src/tools/ToolsRegistry.ts index 075ef7e9..3e1913c5 100644 --- a/src/tools/ToolsRegistry.ts +++ b/src/tools/ToolsRegistry.ts @@ -1,7 +1,6 @@ /* eslint-disable jsdoc/require-jsdoc */ import { BlockToolName, InlineToolName } from '../entities'; -import { BlockToolConstructable } from './types/BlockToolConstructable'; -import { InlineToolConstructable } from './types/InlineToolConstructable'; +import { BlockToolConstructable, InlineToolConstructable } from './types'; /** * ToolsRegistry map stores Editor.js Tools by their names diff --git a/src/tools/types/BlockDataType.ts b/src/tools/types/BlockDataType.ts deleted file mode 100644 index 6ceaa74d..00000000 --- a/src/tools/types/BlockDataType.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Possible types of BlockData values - */ -export enum BlockDataType { - Value = '$value', - Text = '$text', -} diff --git a/src/tools/types/BlockToolConstructable.ts b/src/tools/types/BlockToolConstructable.ts index 887b081e..e789e324 100644 --- a/src/tools/types/BlockToolConstructable.ts +++ b/src/tools/types/BlockToolConstructable.ts @@ -1,11 +1,4 @@ -import { BlockToolDataScheme } from './BlockToolDataScheme'; - /** * Interface describes BlockTool static properties */ -export interface BlockToolConstructable { - /** - * BlockTool data scheme to describe the structure of the data - */ - scheme: BlockToolDataScheme; -} +export interface BlockToolConstructable {} diff --git a/src/tools/types/BlockToolDataScheme.ts b/src/tools/types/BlockToolDataScheme.ts deleted file mode 100644 index 3c66c54e..00000000 --- a/src/tools/types/BlockToolDataScheme.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BlockDataType } from './BlockDataType'; -import { DataKey } from '../../entities'; - -/** - * Block Tool data scheme to describe the structure of the data - */ -export interface BlockToolDataScheme { - [key: DataKey]: BlockDataType | BlockToolDataScheme | BlockDataType[] | BlockToolDataScheme[]; -} diff --git a/src/tools/types/index.ts b/src/tools/types/index.ts index 3f41f36d..f94c3617 100644 --- a/src/tools/types/index.ts +++ b/src/tools/types/index.ts @@ -1,4 +1,2 @@ export * from './BlockToolConstructable'; export * from './InlineToolConstructable'; -export * from './BlockDataType'; -export * from './BlockToolDataScheme'; diff --git a/yarn.lock b/yarn.lock index 94872cd5..0f7d29b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -898,7 +898,7 @@ __metadata: stryker-cli: ^1.0.2 ts-jest: ^29.1.0 ts-node: ^10.9.1 - typescript: ^5.0.4 + typescript: ^5.2.2 languageName: unknown linkType: soft @@ -6658,23 +6658,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.0.4": - version: 5.0.4 - resolution: "typescript@npm:5.0.4" +"typescript@npm:^5.2.2": + version: 5.2.2 + resolution: "typescript@npm:5.2.2" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 82b94da3f4604a8946da585f7d6c3025fff8410779e5bde2855ab130d05e4fd08938b9e593b6ebed165bda6ad9292b230984f10952cf82f0a0ca07bbeaa08172 + checksum: 7912821dac4d962d315c36800fe387cdc0a6298dba7ec171b350b4a6e988b51d7b8f051317786db1094bd7431d526b648aba7da8236607febb26cf5b871d2d3c languageName: node linkType: hard -"typescript@patch:typescript@^5.0.4#~builtin": - version: 5.0.4 - resolution: "typescript@patch:typescript@npm%3A5.0.4#~builtin::version=5.0.4&hash=85af82" +"typescript@patch:typescript@^5.2.2#~builtin": + version: 5.2.2 + resolution: "typescript@patch:typescript@npm%3A5.2.2#~builtin::version=5.2.2&hash=85af82" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: bb309d320c59a26565fb3793dba550576ab861018ff3fd1b7fccabbe46ae4a35546bc45f342c0a0b6f265c801ccdf64ffd68f548f117ceb7f0eac4b805cd52a9 + checksum: 07106822b4305de3f22835cbba949a2b35451cad50888759b6818421290ff95d522b38ef7919e70fb381c5fe9c1c643d7dea22c8b31652a717ddbd57b7f4d554 languageName: node linkType: hard From 0afab60b2e7332cab886135995d3fc4f6e068249 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Sat, 7 Oct 2023 22:17:21 +0100 Subject: [PATCH 02/11] Fix existing tests --- src/entities/BlockNode/BlockNode.spec.ts | 116 +++--- src/entities/BlockNode/index.ts | 19 +- .../BlockNode/types/BlockNodeSerialized.ts | 2 +- src/entities/BlockTune/BlockTune.spec.ts | 29 +- src/entities/BlockTune/__mocks__/index.ts | 4 + .../EditorDocument/EditorDocument.spec.ts | 338 ++++++++++++------ src/entities/EditorDocument/index.ts | 2 +- .../specs/InlineTree.integration.spec.ts | 13 +- 8 files changed, 333 insertions(+), 190 deletions(-) diff --git a/src/entities/BlockNode/BlockNode.spec.ts b/src/entities/BlockNode/BlockNode.spec.ts index f5b8a8e2..2743ad82 100644 --- a/src/entities/BlockNode/BlockNode.spec.ts +++ b/src/entities/BlockNode/BlockNode.spec.ts @@ -4,9 +4,9 @@ import { BlockTune, BlockTuneName } from '../BlockTune'; import { ValueNode } from '../ValueNode'; import type { EditorDocument } from '../EditorDocument'; -import type { BlockTuneConstructorParameters } from '../BlockTune/types'; import type { ValueNodeConstructorParameters } from '../ValueNode'; import { InlineToolData, InlineToolName, TextNode } from '../inline-fragments'; +import { BlockChildType } from './types'; jest.mock('../BlockTune'); @@ -59,14 +59,10 @@ describe('BlockNode', () => { const blockTunes = blockTunesNames.reduce((acc, name) => ({ ...acc, - [name]: new BlockTune({} as BlockTuneConstructorParameters), + [name]: {}, }), {}); - const spyArray = Object - .values(blockTunes) - .map((blockTune) => { - return jest.spyOn(blockTune as BlockTune, 'serialized', 'get'); - }); + const spy = jest.spyOn(BlockTune.prototype, 'serialized', 'get'); const blockNode = new BlockNode({ name: createBlockToolName('paragraph'), @@ -77,25 +73,19 @@ describe('BlockNode', () => { blockNode.serialized; - spyArray.forEach((spy) => { - expect(spy).toHaveBeenCalled(); - }); + expect(spy).toHaveBeenCalledTimes(blockTunesNames.length); }); it('should call .serialized getter of all child ValueNodes associated with the BlockNode', () => { - const countOfValueNodes = 2; + const numberOfValueNodes = 2; - const valueNodes = [ ...Array(countOfValueNodes).keys() ] + const valueNodes = [ ...Array(numberOfValueNodes).keys() ] .reduce((acc, index) => ({ ...acc, - [createDataKey(`data-key-${index}c${index}d`)]: new ValueNode({} as ValueNodeConstructorParameters), + [createDataKey(`data-key-${index}c${index}d`)]: index, }), {}); - const spyArray = Object - .values(valueNodes) - .map((valueNode) => { - return jest.spyOn(valueNode as ValueNode, 'serialized', 'get'); - }); + const spy = jest.spyOn(ValueNode.prototype, 'serialized', 'get'); const blockNode = new BlockNode({ name: createBlockToolName('paragraph'), @@ -107,25 +97,23 @@ describe('BlockNode', () => { blockNode.serialized; - spyArray.forEach((spy) => { - expect(spy).toHaveBeenCalled(); - }); + expect(spy).toHaveBeenCalledTimes(numberOfValueNodes); }); - it('should call .serialized getter of all child RootInlineNodes associated with the BlockNode', () => { - const countOfTextNodes = 3; - + it('should call .serialized getter of all child TextNodes associated with the BlockNode', () => { + const numberOfTextNodes = 3; - const textNodes = [ ...Array(countOfTextNodes).keys() ] + const textNodes = [ ...Array(numberOfTextNodes).keys() ] .reduce((acc, index) => ({ ...acc, - [createDataKey(`data-key-${index}c${index}d`)]: new TextNode(), + [createDataKey(`data-key-${index}c${index}d`)]: { + $t: BlockChildType.Text, + value: '', + fragments: [], + }, }), {}); - const spyArray = Object.values(textNodes) - .map((textNode) => { - return jest.spyOn(textNode as TextNode, 'serialized', 'get'); - }); + const spy = jest.spyOn(TextNode.prototype, 'serialized', 'get'); const blockNode = new BlockNode({ name: createBlockToolName('paragraph'), @@ -137,9 +125,7 @@ describe('BlockNode', () => { blockNode.serialized; - spyArray.forEach((spy) => { - expect(spy).toHaveBeenCalled(); - }); + expect(spy).toHaveBeenCalledTimes(numberOfTextNodes); }); }); @@ -151,14 +137,12 @@ describe('BlockNode', () => { it('should call .update() method of the BlockTune', () => { const blockTuneName = 'align' as BlockTuneName; - const blockTune = new BlockTune({} as BlockTuneConstructorParameters); - const blockNode = new BlockNode({ name: createBlockToolName('paragraph'), data: {}, parent: {} as EditorDocument, tunes: { - [blockTuneName]: blockTune, + [blockTuneName]: {}, }, }); @@ -168,7 +152,7 @@ describe('BlockNode', () => { [dataKey]: dataValue, }; - const spy = jest.spyOn(blockTune, 'update'); + const spy = jest.spyOn(BlockTune.prototype, 'update'); blockNode.updateTuneData(blockTuneName, data); @@ -185,17 +169,15 @@ describe('BlockNode', () => { const dataKey = createDataKey('data-key-1a2b'); const value = 'Some value'; - const valueNode = new ValueNode({} as ValueNodeConstructorParameters); - const blockNode = new BlockNode({ name: createBlockToolName('paragraph'), data: { - [dataKey]: valueNode, + [dataKey]: 'value', }, parent: {} as EditorDocument, }); - const spy = jest.spyOn(valueNode, 'update'); + const spy = jest.spyOn(ValueNode.prototype, 'update'); blockNode.updateValue(dataKey, value); @@ -238,21 +220,22 @@ describe('BlockNode', () => { describe('.insertText()', () => { let node: BlockNode; const dataKey = createDataKey('text'); - let textNode: TextNode; const text = 'Some text'; beforeEach(() => { - textNode = new TextNode(); - node = new BlockNode({ name: createBlockToolName('header'), data: { - [dataKey]: textNode, + [dataKey]: { + $t: BlockChildType.Text, + value: '', + fragments: [], + }, }, }); }); it('should call .insertText() method of the TextNode', () => { - const spy = jest.spyOn(textNode, 'insertText'); + const spy = jest.spyOn(TextNode.prototype, 'insertText'); node.insertText(dataKey, text); @@ -260,7 +243,7 @@ describe('BlockNode', () => { }); it('should pass start index to the .insertText() method of the TextNode', () => { - const spy = jest.spyOn(textNode, 'insertText'); + const spy = jest.spyOn(TextNode.prototype, 'insertText'); const start = 5; node.insertText(dataKey, text, start); @@ -289,20 +272,21 @@ describe('BlockNode', () => { describe('.removeText()', () => { let node: BlockNode; const dataKey = createDataKey('text'); - let textNode: TextNode; beforeEach(() => { - textNode = new TextNode(); - node = new BlockNode({ name: createBlockToolName('header'), data: { - [dataKey]: textNode, + [dataKey]: { + $t: BlockChildType.Text, + value: '', + fragments: [], + }, }, }); }); it('should call .removeText() method of the TextNode', () => { - const spy = jest.spyOn(textNode, 'removeText'); + const spy = jest.spyOn(TextNode.prototype, 'removeText'); node.removeText(dataKey); @@ -310,7 +294,7 @@ describe('BlockNode', () => { }); it('should pass start index to the .removeText() method of the TextNode', () => { - const spy = jest.spyOn(textNode, 'removeText'); + const spy = jest.spyOn(TextNode.prototype, 'removeText'); const start = 5; node.removeText(dataKey, start); @@ -319,7 +303,7 @@ describe('BlockNode', () => { }); it('should pass end index to the .removeText() method of the TextNode', () => { - const spy = jest.spyOn(textNode, 'removeText'); + const spy = jest.spyOn(TextNode.prototype, 'removeText'); const start = 5; const end = 10; @@ -352,20 +336,21 @@ describe('BlockNode', () => { const tool = 'bold' as InlineToolName; const start = 5; const end = 10; - let textNode: TextNode; beforeEach(() => { - textNode = new TextNode(); - node = new BlockNode({ name: createBlockToolName('header'), data: { - [dataKey]: textNode, + [dataKey]: { + $t: BlockChildType.Text, + value: '', + fragments: [], + }, }, }); }); it('should call .format() method of the TextNode', () => { - const spy = jest.spyOn(textNode, 'format'); + const spy = jest.spyOn(TextNode.prototype, 'format'); node.format(dataKey, tool, start, end); @@ -373,7 +358,7 @@ describe('BlockNode', () => { }); it('should pass data to the .format() method of the TextNode', () => { - const spy = jest.spyOn(textNode, 'format'); + const spy = jest.spyOn(TextNode.prototype, 'format'); const data = {} as InlineToolData; node.format(dataKey, tool, start, end, data); @@ -405,20 +390,21 @@ describe('BlockNode', () => { const tool = 'bold' as InlineToolName; const start = 5; const end = 10; - let textNode: TextNode; beforeEach(() => { - textNode = new TextNode(); - node = new BlockNode({ name: createBlockToolName('header'), data: { - [dataKey]: textNode, + [dataKey]: { + $t: BlockChildType.Text, + value: '', + fragments: [], + }, }, }); }); it('should call .unformat() method of the TextNode', () => { - const spy = jest.spyOn(textNode, 'unformat'); + const spy = jest.spyOn(TextNode.prototype, 'unformat'); node.unformat(dataKey, tool, start, end); diff --git a/src/entities/BlockNode/index.ts b/src/entities/BlockNode/index.ts index 06133b49..5b8d176b 100644 --- a/src/entities/BlockNode/index.ts +++ b/src/entities/BlockNode/index.ts @@ -12,7 +12,7 @@ import { BlockNodeDataSerializedValue, BlockChildType, ChildNode, - BlockNodeDataValue + BlockNodeDataValue, } from './types'; import { ValueNode } from '../ValueNode'; import { InlineToolData, InlineToolName, TextNode, TextNodeSerialized } from '../inline-fragments'; @@ -82,10 +82,25 @@ export class BlockNode { * Returns serialized object representing the BlockNode */ public get serialized(): BlockNodeSerialized { + const map = (data: BlockNodeDataValue): BlockNodeDataSerializedValue => { + if (Array.isArray(data)) { + return data.map(map) as BlockNodeDataSerialized[]; + } + + if (data instanceof ValueNode || data instanceof TextNode) { + return data.serialized; + } + + return Object.fromEntries( + Object.entries(data) + .map(([key, value]) => ([key, map(value)])) + ); + }; + const serializedData = Object.fromEntries( Object .entries(this.#data) - .map(([dataKey, value]) => ([dataKey, value.serialized])) + .map(([dataKey, value]) => ([dataKey, map(value)])) ); const serializedTunes = Object.fromEntries( diff --git a/src/entities/BlockNode/types/BlockNodeSerialized.ts b/src/entities/BlockNode/types/BlockNodeSerialized.ts index 1c420030..ce7c2c5d 100644 --- a/src/entities/BlockNode/types/BlockNodeSerialized.ts +++ b/src/entities/BlockNode/types/BlockNodeSerialized.ts @@ -28,5 +28,5 @@ export interface BlockNodeSerialized { /** * Serialized BlockTunes associated with the BlockNode */ - tunes: Record; + tunes?: Record; } diff --git a/src/entities/BlockTune/BlockTune.spec.ts b/src/entities/BlockTune/BlockTune.spec.ts index 90f8c941..a29aedd3 100644 --- a/src/entities/BlockTune/BlockTune.spec.ts +++ b/src/entities/BlockTune/BlockTune.spec.ts @@ -7,7 +7,8 @@ describe('BlockTune', () => { it('should have empty object as default data value', () => { const blockTune = new BlockTune({ name: tuneName }); - expect(blockTune.serialized.data).toEqual({}); + expect(blockTune.serialized) + .toEqual({}); }); }); @@ -23,9 +24,10 @@ describe('BlockTune', () => { blockTune.update('align', 'left'); // Assert - expect(blockTune.serialized.data).toEqual({ - align: 'left', - }); + expect(blockTune.serialized) + .toEqual({ + align: 'left', + }); }); it('should update field in data object by key', () => { @@ -41,9 +43,10 @@ describe('BlockTune', () => { blockTune.update('align', 'right'); // Assert - expect(blockTune.serialized.data).toEqual({ - align: 'right', - }); + expect(blockTune.serialized) + .toEqual({ + align: 'right', + }); }); }); @@ -61,14 +64,12 @@ describe('BlockTune', () => { const tuneSerialized = tune.serialized; // Assert - expect(tuneSerialized).toEqual( - { - name: tuneName, - data: { + expect(tuneSerialized) + .toEqual( + { background: 'transparent', - }, - } - ); + } + ); }); }); }); diff --git a/src/entities/BlockTune/__mocks__/index.ts b/src/entities/BlockTune/__mocks__/index.ts index c95b9a48..4fc2fcaf 100644 --- a/src/entities/BlockTune/__mocks__/index.ts +++ b/src/entities/BlockTune/__mocks__/index.ts @@ -1,3 +1,5 @@ +import { createBlockTuneName } from '../types'; + /** * Mock for BlockTune class */ @@ -16,3 +18,5 @@ export class BlockTune { return; } } + +export { createBlockTuneName }; diff --git a/src/entities/EditorDocument/EditorDocument.spec.ts b/src/entities/EditorDocument/EditorDocument.spec.ts index d9c1ccd5..996f2570 100644 --- a/src/entities/EditorDocument/EditorDocument.spec.ts +++ b/src/entities/EditorDocument/EditorDocument.spec.ts @@ -1,6 +1,5 @@ import { EditorDocument } from './index'; import { BlockNode, BlockToolName, DataKey } from '../BlockNode'; -import { BlockNodeConstructorParameters } from '../BlockNode/types'; import type { BlockTuneName } from '../BlockTune'; import { InlineToolData, InlineToolName } from '../inline-fragments'; @@ -11,14 +10,16 @@ jest.mock('../BlockNode'); */ function createEditorDocumentWithSomeBlocks(): EditorDocument { const countOfBlocks = 3; - const blocks = new Array(countOfBlocks).fill(undefined) - .map(() => new BlockNode({ - name: 'header' as BlockToolName, - })); + const document = new EditorDocument({}); - return new EditorDocument({ - children: blocks, - }); + new Array(countOfBlocks).fill(undefined) + .forEach(() => { + document.addBlock({ + name: 'header' as BlockToolName, + }); + }); + + return document; } describe('EditorDocument', () => { @@ -31,7 +32,6 @@ describe('EditorDocument', () => { // Arrange const blocksCount = 3; const document = new EditorDocument({ - children: [], properties: { readOnly: false, }, @@ -49,7 +49,8 @@ describe('EditorDocument', () => { const actual = document.length; // Assert - expect(actual).toBe(blocksCount); + expect(actual) + .toBe(blocksCount); }); }); @@ -66,8 +67,11 @@ describe('EditorDocument', () => { const lastBlock = document.getBlock(document.length - 1); const blockBeforeAdded = document.getBlock(document.length - 2); - expect(lastBlockBeforeTest).not.toBe(lastBlock); - expect(blockBeforeAdded).toBe(lastBlockBeforeTest); + expect(lastBlockBeforeTest) + .not + .toBe(lastBlock); + expect(blockBeforeAdded) + .toBe(lastBlockBeforeTest); }); it('should add the block to the beginning of the document if index is 0', () => { @@ -82,8 +86,11 @@ describe('EditorDocument', () => { const firstBlock = document.getBlock(0); const blockAfterAdded = document.getBlock(1); - expect(firstBlockBeforeTest).not.toBe(firstBlock); - expect(blockAfterAdded).toBe(firstBlockBeforeTest); + expect(firstBlockBeforeTest) + .not + .toBe(firstBlock); + expect(blockAfterAdded) + .toBe(firstBlockBeforeTest); }); it('should add the block to the specified index in the middle of the document', () => { @@ -99,8 +106,11 @@ describe('EditorDocument', () => { const addedBlock = document.getBlock(index); const blockAfterAdded = document.getBlock(index + 1); - expect(blockBeforeTest).not.toBe(addedBlock); - expect(blockBeforeTest).toBe(blockAfterAdded); + expect(blockBeforeTest) + .not + .toBe(addedBlock); + expect(blockBeforeTest) + .toBe(blockAfterAdded); }); it('should add the block to the end of the document if the index after the last element is passed', () => { @@ -115,8 +125,11 @@ describe('EditorDocument', () => { const lastBlock = document.getBlock(document.length - 1); const blockBeforeAdded = document.getBlock(document.length - 2); - expect(lastBlockBeforeTest).not.toBe(lastBlock); - expect(blockBeforeAdded).toBe(lastBlockBeforeTest); + expect(lastBlockBeforeTest) + .not + .toBe(lastBlock); + expect(blockBeforeAdded) + .toBe(lastBlockBeforeTest); }); it('should throw an error if index is less then 0', () => { @@ -130,7 +143,8 @@ describe('EditorDocument', () => { const action = (): void => document.addBlock(blockData, -1); // Assert - expect(action).toThrowError('Index out of bounds'); + expect(action) + .toThrowError('Index out of bounds'); }); it('should throw an error if index is greater then document length', () => { @@ -144,7 +158,8 @@ describe('EditorDocument', () => { const action = (): void => document.addBlock(blockData, document.length + 1); // Assert - expect(action).toThrowError('Index out of bounds'); + expect(action) + .toThrowError('Index out of bounds'); }); }); @@ -158,7 +173,9 @@ describe('EditorDocument', () => { document.removeBlock(0); // Assert - expect(document.getBlock(0)).not.toBe(block); + expect(document.getBlock(0)) + .not + .toBe(block); }); it('should remove the block from the specified index in the middle of the document', () => { @@ -170,7 +187,9 @@ describe('EditorDocument', () => { document.removeBlock(1); // Assert - expect(document.getBlock(1)).not.toBe(block); + expect(document.getBlock(1)) + .not + .toBe(block); }); it('should remove the block from the end of the document if the last index is passed', () => { @@ -182,7 +201,8 @@ describe('EditorDocument', () => { document.removeBlock(document.length - 1); // Assert - expect(document.length).toBe(documentLengthBeforeRemove - 1); + expect(document.length) + .toBe(documentLengthBeforeRemove - 1); }); it('should throw an error if index is less then 0', () => { @@ -193,7 +213,8 @@ describe('EditorDocument', () => { const action = (): void => document.removeBlock(-1); // Assert - expect(action).toThrowError('Index out of bounds'); + expect(action) + .toThrowError('Index out of bounds'); }); it('should throw an error if index is greater then document length', () => { @@ -204,32 +225,36 @@ describe('EditorDocument', () => { const action = (): void => document.removeBlock(document.length); // Assert - expect(action).toThrowError('Index out of bounds'); + expect(action) + .toThrowError('Index out of bounds'); }); }); describe('.getBlock()', () => { - it('should return the block from the specific index', () => { + /** + * @todo move this to integration tests + */ + it.skip('should return the block from the specific index', () => { const countOfBlocks = 5; - const blocks = []; + const blocksData = []; + const document = new EditorDocument(); for (let i = 0; i < countOfBlocks; i++) { - const block = new BlockNode({ + const blockData = { name: (`header-${i}`) as BlockToolName, - }); + }; - blocks.push(block); - } + document.addBlock(blockData); - const document = new EditorDocument({ - children: blocks, - }); + blocksData.push(blockData); + } const index = 1; const block = document.getBlock(index); - expect(block).toBe(blocks[index]); + expect(block.serialized) + .toBe(blocksData[index]); }); it('should throw an error if index is less then 0', () => { @@ -240,7 +265,8 @@ describe('EditorDocument', () => { const action = (): BlockNode => document.getBlock(-1); // Assert - expect(action).toThrowError('Index out of bounds'); + expect(action) + .toThrowError('Index out of bounds'); }); it('should throw an error if index is greater then document length', () => { @@ -251,14 +277,15 @@ describe('EditorDocument', () => { const action = (): BlockNode => document.getBlock(document.length); // Assert - expect(action).toThrowError('Index out of bounds'); + expect(action) + .toThrowError('Index out of bounds'); }); }); describe('.properties', () => { it('should return the properties of the document', () => { const properties = { - 'readOnly' : true, + 'readOnly': true, }; const document = new EditorDocument({ @@ -267,7 +294,8 @@ describe('EditorDocument', () => { }, }); - expect(document.properties).toEqual(properties); + expect(document.properties) + .toEqual(properties); }); }); @@ -283,7 +311,8 @@ describe('EditorDocument', () => { const actualValue = document.getProperty(propertyName); - expect(actualValue).toBe(expectedValue); + expect(actualValue) + .toBe(expectedValue); }); it('should return undefined if the property does not exist', () => { @@ -294,7 +323,8 @@ describe('EditorDocument', () => { const actualValue = document.getProperty(propertyName); - expect(actualValue).toBeUndefined(); + expect(actualValue) + .toBeUndefined(); }); }); @@ -310,7 +340,8 @@ describe('EditorDocument', () => { document.setProperty(propertyName, expectedValue); - expect(document.properties[propertyName]).toBe(expectedValue); + expect(document.properties[propertyName]) + .toBe(expectedValue); }); it('should add the property if it does not exist', () => { @@ -322,7 +353,8 @@ describe('EditorDocument', () => { document.setProperty(propertyName, expectedValue); - expect(document.properties[propertyName]).toBe(expectedValue); + expect(document.properties[propertyName]) + .toBe(expectedValue); }); }); @@ -332,48 +364,75 @@ describe('EditorDocument', () => { }); it('should call .updateValue() method of the BlockNode at the specific index', () => { - const blockNodes = [ - new BlockNode({} as BlockNodeConstructorParameters), - new BlockNode({} as BlockNodeConstructorParameters), - new BlockNode({} as BlockNodeConstructorParameters), + const blocksData = [ + { + name: 'header' as BlockToolName, + data: {}, + }, + { + name: 'header' as BlockToolName, + data: {}, + }, + { + name: 'header' as BlockToolName, + data: {}, + }, ]; + const document = new EditorDocument({ + blocks: blocksData, + }); + + blocksData.forEach((_, i) => { + const blockNode = document.getBlock(i); - blockNodes.forEach((blockNode) => { jest .spyOn(blockNode, 'updateValue') // eslint-disable-next-line @typescript-eslint/no-empty-function -- mock of the method - .mockImplementation(() => {}); + .mockImplementation(() => { + }); }); - const document = new EditorDocument({ - children: blockNodes, - }); const blockIndexToUpdate = 1; const dataKey = 'data-key-1a2b' as DataKey; const value = 'Some value'; document.updateValue(blockIndexToUpdate, dataKey, value); - expect(document.getBlock(blockIndexToUpdate).updateValue).toHaveBeenCalledWith(dataKey, value); + expect(document.getBlock(blockIndexToUpdate).updateValue) + .toHaveBeenCalledWith(dataKey, value); }); it('should not call .updateValue() method of other BlockNodes', () => { - const blockNodes = [ - new BlockNode({} as BlockNodeConstructorParameters), - new BlockNode({} as BlockNodeConstructorParameters), - new BlockNode({} as BlockNodeConstructorParameters), + const blocksData = [ + { + name: 'header' as BlockToolName, + data: {}, + }, + { + name: 'header' as BlockToolName, + data: {}, + }, + { + name: 'header' as BlockToolName, + data: {}, + }, ]; + const document = new EditorDocument({ + blocks: blocksData, + }); + + const blockNodes = blocksData.map((_, i) => { + const blockNode = document.getBlock(i); - blockNodes.forEach((blockNode) => { jest .spyOn(blockNode, 'updateValue') // eslint-disable-next-line @typescript-eslint/no-empty-function -- mock of the method - .mockImplementation(() => {}); - }); + .mockImplementation(() => { + }); - const document = new EditorDocument({ - children: blockNodes, + return blockNode; }); + const blockIndexToUpdate = 1; const dataKey = 'data-key-1a2b' as DataKey; const value = 'Some value'; @@ -385,7 +444,9 @@ describe('EditorDocument', () => { return; } - expect(blockNode.updateValue).not.toHaveBeenCalled(); + expect(blockNode.updateValue) + .not + .toHaveBeenCalled(); }); }); @@ -397,7 +458,8 @@ describe('EditorDocument', () => { const action = (): void => document.updateValue(blockIndexOutOfBound, dataKey, expectedValue); - expect(action).toThrowError('Index out of bounds'); + expect(action) + .toThrowError('Index out of bounds'); }); }); @@ -407,22 +469,34 @@ describe('EditorDocument', () => { }); it('should call .updateTuneData() method of the BlockNode at the specific index', () => { - const blockNodes = [ - new BlockNode({} as BlockNodeConstructorParameters), - new BlockNode({} as BlockNodeConstructorParameters), - new BlockNode({} as BlockNodeConstructorParameters), + const blocksData = [ + { + name: 'header' as BlockToolName, + data: {}, + }, + { + name: 'header' as BlockToolName, + data: {}, + }, + { + name: 'header' as BlockToolName, + data: {}, + }, ]; + const document = new EditorDocument({ + blocks: blocksData, + }); + + blocksData.forEach((_, i) => { + const blockNode = document.getBlock(i); - blockNodes.forEach((blockNode) => { jest .spyOn(blockNode, 'updateTuneData') // eslint-disable-next-line @typescript-eslint/no-empty-function -- mock of the method - .mockImplementation(() => {}); + .mockImplementation(() => { + }); }); - const document = new EditorDocument({ - children: blockNodes, - }); const blockIndexToUpdate = 1; const tuneName = 'blockFormatting' as BlockTuneName; const updateData = { @@ -431,26 +505,41 @@ describe('EditorDocument', () => { document.updateTuneData(blockIndexToUpdate, tuneName, updateData); - expect(document.getBlock(blockIndexToUpdate).updateTuneData).toHaveBeenCalledWith(tuneName, updateData); + expect(document.getBlock(blockIndexToUpdate).updateTuneData) + .toHaveBeenCalledWith(tuneName, updateData); }); it('should not call .updateTuneData() method of other BlockNodes', () => { - const blockNodes = [ - new BlockNode({} as BlockNodeConstructorParameters), - new BlockNode({} as BlockNodeConstructorParameters), - new BlockNode({} as BlockNodeConstructorParameters), + const blocksData = [ + { + name: 'header' as BlockToolName, + data: {}, + }, + { + name: 'header' as BlockToolName, + data: {}, + }, + { + name: 'header' as BlockToolName, + data: {}, + }, ]; + const document = new EditorDocument({ + blocks: blocksData, + }); + + const blockNodes = blocksData.map((_, i) => { + const blockNode = document.getBlock(i); - blockNodes.forEach((blockNode) => { jest .spyOn(blockNode, 'updateTuneData') // eslint-disable-next-line @typescript-eslint/no-empty-function -- mock of the method - .mockImplementation(() => {}); - }); + .mockImplementation(() => { + }); - const document = new EditorDocument({ - children: blockNodes, + return blockNode; }); + const blockIndexToUpdate = 1; const tuneName = 'blockFormatting' as BlockTuneName; const updateData = { @@ -464,7 +553,9 @@ describe('EditorDocument', () => { return; } - expect(blockNode.updateTuneData).not.toHaveBeenCalled(); + expect(blockNode.updateTuneData) + .not + .toHaveBeenCalled(); }); }); @@ -478,7 +569,8 @@ describe('EditorDocument', () => { const action = (): void => document.updateTuneData(blockIndexOutOfBound, tuneName, updateData); - expect(action).toThrowError('Index out of bounds'); + expect(action) + .toThrowError('Index out of bounds'); }); }); @@ -490,10 +582,16 @@ describe('EditorDocument', () => { let block: BlockNode; beforeEach(() => { - block = new BlockNode({} as BlockNodeConstructorParameters); + const blockData = { + name: 'header' as BlockToolName, + data: {} + }; + document = new EditorDocument({ - children: [ block ], + blocks: [blockData], }); + + block = document.getBlock(0); }); it('should call .insertText() method of the BlockNode', () => { @@ -501,7 +599,8 @@ describe('EditorDocument', () => { document.insertText(blockIndex, dataKey, text); - expect(spy).toHaveBeenCalledWith(dataKey, text, undefined); + expect(spy) + .toHaveBeenCalledWith(dataKey, text, undefined); }); it('should pass start index to the .insertText() method of the BlockNode', () => { @@ -510,11 +609,13 @@ describe('EditorDocument', () => { document.insertText(blockIndex, dataKey, text, start); - expect(spy).toHaveBeenCalledWith(dataKey, text, start); + expect(spy) + .toHaveBeenCalledWith(dataKey, text, start); }); it('should throw an error if index is out of bounds', () => { - expect(() => document.insertText(document.length + 1, dataKey, text)).toThrowError('Index out of bounds'); + expect(() => document.insertText(document.length + 1, dataKey, text)) + .toThrowError('Index out of bounds'); }); }); @@ -525,10 +626,16 @@ describe('EditorDocument', () => { let block: BlockNode; beforeEach(() => { - block = new BlockNode({} as BlockNodeConstructorParameters); + const blockData = { + name: 'header' as BlockToolName, + data: {} + }; + document = new EditorDocument({ - children: [ block ], + blocks: [blockData], }); + + block = document.getBlock(0); }); it('should call .removeText() method of the BlockNode', () => { @@ -536,7 +643,8 @@ describe('EditorDocument', () => { document.removeText(blockIndex, dataKey); - expect(spy).toHaveBeenCalledWith(dataKey, undefined, undefined); + expect(spy) + .toHaveBeenCalledWith(dataKey, undefined, undefined); }); it('should pass start index to the .removeText() method of the BlockNode', () => { @@ -545,7 +653,8 @@ describe('EditorDocument', () => { document.removeText(blockIndex, dataKey, start); - expect(spy).toHaveBeenCalledWith(dataKey, start, undefined); + expect(spy) + .toHaveBeenCalledWith(dataKey, start, undefined); }); it('should pass end index to the .removeText() method of the BlockNode', () => { @@ -555,11 +664,13 @@ describe('EditorDocument', () => { document.removeText(blockIndex, dataKey, start, end); - expect(spy).toHaveBeenCalledWith(dataKey, start, end); + expect(spy) + .toHaveBeenCalledWith(dataKey, start, end); }); it('should throw an error if index is out of bounds', () => { - expect(() => document.removeText(document.length + 1, dataKey)).toThrowError('Index out of bounds'); + expect(() => document.removeText(document.length + 1, dataKey)) + .toThrowError('Index out of bounds'); }); }); @@ -573,10 +684,16 @@ describe('EditorDocument', () => { let block: BlockNode; beforeEach(() => { - block = new BlockNode({} as BlockNodeConstructorParameters); + const blockData = { + name: 'header' as BlockToolName, + data: {} + }; + document = new EditorDocument({ - children: [ block ], + blocks: [blockData], }); + + block = document.getBlock(0); }); it('should call .format() method of the BlockNode', () => { @@ -584,7 +701,8 @@ describe('EditorDocument', () => { document.format(blockIndex, dataKey, tool, start, end); - expect(spy).toHaveBeenCalledWith(dataKey, tool, start, end, undefined); + expect(spy) + .toHaveBeenCalledWith(dataKey, tool, start, end, undefined); }); it('should pass data to the .format() method of the BlockNode', () => { @@ -593,11 +711,13 @@ describe('EditorDocument', () => { document.format(blockIndex, dataKey, tool, start, end, data); - expect(spy).toHaveBeenCalledWith(dataKey, tool, start, end, data); + expect(spy) + .toHaveBeenCalledWith(dataKey, tool, start, end, data); }); it('should throw an error if index is out of bounds', () => { - expect(() => document.format(document.length + 1, dataKey, tool, start, end)).toThrowError('Index out of bounds'); + expect(() => document.format(document.length + 1, dataKey, tool, start, end)) + .toThrowError('Index out of bounds'); }); }); @@ -611,10 +731,16 @@ describe('EditorDocument', () => { let block: BlockNode; beforeEach(() => { - block = new BlockNode({} as BlockNodeConstructorParameters); + const blockData = { + name: 'header' as BlockToolName, + data: {} + }; + document = new EditorDocument({ - children: [ block ], + blocks: [blockData], }); + + block = document.getBlock(0); }); it('should call .unformat() method of the BlockNode', () => { @@ -622,11 +748,13 @@ describe('EditorDocument', () => { document.unformat(blockIndex, dataKey, tool, start, end); - expect(spy).toHaveBeenCalledWith(dataKey, tool, start, end); + expect(spy) + .toHaveBeenCalledWith(dataKey, tool, start, end); }); it('should throw an error if index is out of bounds', () => { - expect(() => document.unformat(document.length + 1, dataKey, tool, start, end)).toThrowError('Index out of bounds'); + expect(() => document.unformat(document.length + 1, dataKey, tool, start, end)) + .toThrowError('Index out of bounds'); }); }); }); diff --git a/src/entities/EditorDocument/index.ts b/src/entities/EditorDocument/index.ts index 3d2aef30..5f6b5b46 100644 --- a/src/entities/EditorDocument/index.ts +++ b/src/entities/EditorDocument/index.ts @@ -54,7 +54,7 @@ export class EditorDocument { * @param index - The index at which to add the BlockNode * @throws Error if the index is out of bounds */ - public addBlock(blockNodeData: BlockNodeSerialized, index?: number): void { + public addBlock(blockNodeData: Pick & Partial>, index?: number): void { const blockNode = new BlockNode({ ...blockNodeData, parent: this, diff --git a/src/entities/inline-fragments/specs/InlineTree.integration.spec.ts b/src/entities/inline-fragments/specs/InlineTree.integration.spec.ts index 4439821c..0f062287 100644 --- a/src/entities/inline-fragments/specs/InlineTree.integration.spec.ts +++ b/src/entities/inline-fragments/specs/InlineTree.integration.spec.ts @@ -1,5 +1,12 @@ /* eslint-disable @typescript-eslint/no-magic-numbers */ -import { TextInlineNode, TextNode, createInlineToolData, createInlineToolName, ChildTextNodeSerialized } from '../index'; +import { + TextInlineNode, + TextNode, + createInlineToolData, + createInlineToolName, + TextNodeSerialized +} from '../index'; +import { BlockChildType } from '../../BlockNode/types'; describe('Inline fragments tree integration', () => { describe('text insertion', () => { @@ -366,6 +373,7 @@ describe('Inline fragments tree integration', () => { tree.unformat(boldTool, ...(pluggable.map((value) => value - removedChars) as [number, number])); expect(tree.serialized).toStrictEqual({ + $t: BlockChildType.Text, text: firstFragment + secondFragment.replace(' data', '') + thirdFragment, fragments: [ { @@ -390,6 +398,7 @@ describe('Inline fragments tree integration', () => { it('should initialize tree with initial text and fragments', () => { const data = { + $t: BlockChildType.Text, text: 'Editor.js is a block-styled editor. It returns clean output in JSON. Designed to be extendable and pluggable with a simple API.', fragments: [ { @@ -421,7 +430,7 @@ describe('Inline fragments tree integration', () => { ], }, ], - } as ChildTextNodeSerialized; + } as TextNodeSerialized; const tree = new TextNode({ value: data.text, From a2a0508f3e7bff0488a4484fe3186c843bbf8d43 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Sat, 7 Oct 2023 23:52:58 +0100 Subject: [PATCH 03/11] Cover new lines --- src/entities/BlockNode/BlockNode.spec.ts | 238 +++++++++++++++++- src/entities/BlockNode/__mocks__/index.ts | 7 + .../EditorDocument/EditorDocument.spec.ts | 48 ++-- src/entities/ValueNode/ValueNode.spec.ts | 12 + 4 files changed, 285 insertions(+), 20 deletions(-) diff --git a/src/entities/BlockNode/BlockNode.spec.ts b/src/entities/BlockNode/BlockNode.spec.ts index 2743ad82..0975aaca 100644 --- a/src/entities/BlockNode/BlockNode.spec.ts +++ b/src/entities/BlockNode/BlockNode.spec.ts @@ -47,7 +47,8 @@ describe('BlockNode', () => { const serialized = blockNode.serialized; - expect(serialized.name).toEqual(blockNodeName); + expect(serialized.name) + .toEqual(blockNodeName); }); it('should call .serialized getter of all tunes associated with the BlockNode', () => { @@ -73,13 +74,15 @@ describe('BlockNode', () => { blockNode.serialized; - expect(spy).toHaveBeenCalledTimes(blockTunesNames.length); + expect(spy) + .toHaveBeenCalledTimes(blockTunesNames.length); }); it('should call .serialized getter of all child ValueNodes associated with the BlockNode', () => { const numberOfValueNodes = 2; - const valueNodes = [ ...Array(numberOfValueNodes).keys() ] + const valueNodes = [ ...Array(numberOfValueNodes) + .keys() ] .reduce((acc, index) => ({ ...acc, [createDataKey(`data-key-${index}c${index}d`)]: index, @@ -97,13 +100,15 @@ describe('BlockNode', () => { blockNode.serialized; - expect(spy).toHaveBeenCalledTimes(numberOfValueNodes); + expect(spy) + .toHaveBeenCalledTimes(numberOfValueNodes); }); it('should call .serialized getter of all child TextNodes associated with the BlockNode', () => { const numberOfTextNodes = 3; - const textNodes = [ ...Array(numberOfTextNodes).keys() ] + const textNodes = [ ...Array(numberOfTextNodes) + .keys() ] .reduce((acc, index) => ({ ...acc, [createDataKey(`data-key-${index}c${index}d`)]: { @@ -125,7 +130,228 @@ describe('BlockNode', () => { blockNode.serialized; - expect(spy).toHaveBeenCalledTimes(numberOfTextNodes); + expect(spy) + .toHaveBeenCalledTimes(numberOfTextNodes); + }); + + + it('should call .serialized getter of ValueNodes in an array', () => { + const spy = jest.spyOn(ValueNode.prototype, 'serialized', 'get'); + const blockNode = new BlockNode({ + name: createBlockToolName('paragraph'), + data: { + items: [ 'value' ], + }, + parent: {} as EditorDocument, + }); + + blockNode.serialized; + + expect(spy) + .toHaveBeenCalledTimes(1); + }); + + it('should call .serialized getter of TextNode in an array', () => { + const spy = jest.spyOn(TextNode.prototype, 'serialized', 'get'); + const blockNode = new BlockNode({ + name: createBlockToolName('paragraph'), + data: { + items: [ + { + $t: BlockChildType.Text, + value: '', + fragments: [], + }, + ], + }, + parent: {} as EditorDocument, + }); + + blockNode.serialized; + + expect(spy) + .toHaveBeenCalledTimes(1); + }); + + it('should call .serialized getter of ValueNodes in an nested object', () => { + const spy = jest.spyOn(ValueNode.prototype, 'serialized', 'get'); + const blockNode = new BlockNode({ + name: createBlockToolName('paragraph'), + data: { + object: { + nestedObject: { value: 'value' }, + }, + }, + parent: {} as EditorDocument, + }); + + blockNode.serialized; + + expect(spy) + .toHaveBeenCalledTimes(1); + }); + + it('should call .serialized getter of TextNode in a nested object', () => { + const spy = jest.spyOn(TextNode.prototype, 'serialized', 'get'); + const blockNode = new BlockNode({ + name: createBlockToolName('paragraph'), + data: { + object: { + nestedObject: { + text: { + $t: BlockChildType.Text, + value: '', + fragments: [], + }, + }, + }, + }, + parent: {} as EditorDocument, + }); + + blockNode.serialized; + + expect(spy) + .toHaveBeenCalledTimes(1); + }); + + it('should call .serialized getter of ValueNodes in an array inside an object', () => { + const spy = jest.spyOn(ValueNode.prototype, 'serialized', 'get'); + const blockNode = new BlockNode({ + name: createBlockToolName('paragraph'), + data: { + object: { + array: [ 'value' ], + }, + }, + parent: {} as EditorDocument, + }); + + blockNode.serialized; + + expect(spy) + .toHaveBeenCalledTimes(1); + }); + + it('should call .serialized getter of TextNode in an array inside an object', () => { + const spy = jest.spyOn(TextNode.prototype, 'serialized', 'get'); + const blockNode = new BlockNode({ + name: createBlockToolName('paragraph'), + data: { + object: { + array: [ { + text: { + $t: BlockChildType.Text, + value: '', + fragments: [], + }, + } ], + }, + }, + parent: {} as EditorDocument, + }); + + blockNode.serialized; + + expect(spy) + .toHaveBeenCalledTimes(1); + }); + + it('should call .serialized getter of ValueNodes in an object inside an array', () => { + const spy = jest.spyOn(ValueNode.prototype, 'serialized', 'get'); + const blockNode = new BlockNode({ + name: createBlockToolName('paragraph'), + data: { + array: [ { value: 'value' } ], + }, + parent: {} as EditorDocument, + }); + + blockNode.serialized; + + expect(spy) + .toHaveBeenCalledTimes(1); + }); + + it('should call .serialized getter of TextNode in an object inside an array', () => { + const spy = jest.spyOn(TextNode.prototype, 'serialized', 'get'); + const blockNode = new BlockNode({ + name: createBlockToolName('paragraph'), + data: { + array: [ { + object: { + text: { + $t: BlockChildType.Text, + value: '', + fragments: [], + }, + }, + } ], + }, + parent: {} as EditorDocument, + }); + + blockNode.serialized; + + expect(spy) + .toHaveBeenCalledTimes(1); + }); + + it('should call .serialized getter of ValueNodes in a nested array', () => { + const spy = jest.spyOn(ValueNode.prototype, 'serialized', 'get'); + const blockNode = new BlockNode({ + name: createBlockToolName('paragraph'), + data: { + array: [ [ 'value' ] ], + }, + parent: {} as EditorDocument, + }); + + blockNode.serialized; + + expect(spy) + .toHaveBeenCalledTimes(1); + }); + + it('should call .serialized getter of TextNode in a nested array', () => { + const spy = jest.spyOn(TextNode.prototype, 'serialized', 'get'); + const blockNode = new BlockNode({ + name: createBlockToolName('paragraph'), + data: { + array: [ [ + { + $t: BlockChildType.Text, + value: '', + fragments: [], + }, + ] ], + }, + parent: {} as EditorDocument, + }); + + blockNode.serialized; + + expect(spy) + .toHaveBeenCalledTimes(1); + }); + + it('should call .serialized getter of object marked as value node', () => { + const spy = jest.spyOn(ValueNode.prototype, 'serialized', 'get'); + const blockNode = new BlockNode({ + name: createBlockToolName('paragraph'), + data: { + value: { + $t: BlockChildType.Value, + property: '', + }, + }, + parent: {} as EditorDocument, + }); + + blockNode.serialized; + + expect(spy) + .toHaveBeenCalledTimes(1); }); }); diff --git a/src/entities/BlockNode/__mocks__/index.ts b/src/entities/BlockNode/__mocks__/index.ts index 79daa181..e2cc89c0 100644 --- a/src/entities/BlockNode/__mocks__/index.ts +++ b/src/entities/BlockNode/__mocks__/index.ts @@ -43,4 +43,11 @@ export class BlockNode { public unformat(): void { return; } + + /** + * Mock getter + */ + public get serialized(): void { + return; + } } diff --git a/src/entities/EditorDocument/EditorDocument.spec.ts b/src/entities/EditorDocument/EditorDocument.spec.ts index 996f2570..65450a0d 100644 --- a/src/entities/EditorDocument/EditorDocument.spec.ts +++ b/src/entities/EditorDocument/EditorDocument.spec.ts @@ -231,10 +231,7 @@ describe('EditorDocument', () => { }); describe('.getBlock()', () => { - /** - * @todo move this to integration tests - */ - it.skip('should return the block from the specific index', () => { + it('should return the block from the specific index', () => { const countOfBlocks = 5; const blocksData = []; const document = new EditorDocument(); @@ -253,8 +250,8 @@ describe('EditorDocument', () => { const block = document.getBlock(index); - expect(block.serialized) - .toBe(blocksData[index]); + expect(block) + .toBeInstanceOf(BlockNode); }); it('should throw an error if index is less then 0', () => { @@ -584,11 +581,11 @@ describe('EditorDocument', () => { beforeEach(() => { const blockData = { name: 'header' as BlockToolName, - data: {} + data: {}, }; document = new EditorDocument({ - blocks: [blockData], + blocks: [ blockData ], }); block = document.getBlock(0); @@ -628,11 +625,11 @@ describe('EditorDocument', () => { beforeEach(() => { const blockData = { name: 'header' as BlockToolName, - data: {} + data: {}, }; document = new EditorDocument({ - blocks: [blockData], + blocks: [ blockData ], }); block = document.getBlock(0); @@ -686,11 +683,11 @@ describe('EditorDocument', () => { beforeEach(() => { const blockData = { name: 'header' as BlockToolName, - data: {} + data: {}, }; document = new EditorDocument({ - blocks: [blockData], + blocks: [ blockData ], }); block = document.getBlock(0); @@ -733,11 +730,11 @@ describe('EditorDocument', () => { beforeEach(() => { const blockData = { name: 'header' as BlockToolName, - data: {} + data: {}, }; document = new EditorDocument({ - blocks: [blockData], + blocks: [ blockData ], }); block = document.getBlock(0); @@ -757,4 +754,27 @@ describe('EditorDocument', () => { .toThrowError('Index out of bounds'); }); }); + + describe('.serialized', () => { + it('should call .serialized property of the BlockNodes', () => { + const spy = jest.spyOn(BlockNode.prototype, 'serialized', 'get'); + const document = createEditorDocumentWithSomeBlocks(); + + document.serialized; + + expect(spy).toBeCalledTimes(document.length); + }); + + + it('should return document properties', () => { + const properties = { + readOnly: true, + }; + const document = new EditorDocument({ + properties, + }); + + expect(document.serialized).toHaveProperty('properties', properties); + }); + }); }); diff --git a/src/entities/ValueNode/ValueNode.spec.ts b/src/entities/ValueNode/ValueNode.spec.ts index 647a86d5..8f2f14ae 100644 --- a/src/entities/ValueNode/ValueNode.spec.ts +++ b/src/entities/ValueNode/ValueNode.spec.ts @@ -1,4 +1,5 @@ import { ValueNode } from './index'; +import { BlockChildType } from '../BlockNode/types'; describe('ValueNode', () => { describe('.update()', () => { @@ -31,5 +32,16 @@ describe('ValueNode', () => { // Assert expect(serializedLongitude).toStrictEqual(longitude); }); + + it('should mark serialized value as value node if object returned', () => { + const value = { align: 'left' }; + const longitudeValueNode = new ValueNode({ + value, + }); + + const serializedValue = longitudeValueNode.serialized; + + expect(serializedValue).toHaveProperty('$t', BlockChildType.Value); + }); }); }); From 0225ec105ae3aacb43aae2bac5e43621107410fc Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Sat, 7 Oct 2023 23:56:19 +0100 Subject: [PATCH 04/11] Apply linter & add comments --- src/entities/BlockNode/index.ts | 5 +++-- src/entities/BlockNode/types/BlockNodeSerialized.ts | 9 +++++++++ src/entities/EditorDocument/index.ts | 3 ++- src/entities/ValueNode/types/ValueSerialized.ts | 5 ++++- src/entities/inline-fragments/InlineNode/index.ts | 3 +++ 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/entities/BlockNode/index.ts b/src/entities/BlockNode/index.ts index 5b8d176b..abf49d45 100644 --- a/src/entities/BlockNode/index.ts +++ b/src/entities/BlockNode/index.ts @@ -12,7 +12,7 @@ import { BlockNodeDataSerializedValue, BlockChildType, ChildNode, - BlockNodeDataValue, + BlockNodeDataValue } from './types'; import { ValueNode } from '../ValueNode'; import { InlineToolData, InlineToolName, TextNode, TextNodeSerialized } from '../inline-fragments'; @@ -208,8 +208,9 @@ export class BlockNode { } /** + * Initializes BlockNode with passed block data * - * @param data + * @param data - block data */ #initialize(data: BlockNodeDataSerialized): void { const map = (value: BlockNodeDataSerializedValue): BlockNodeData | BlockNodeDataValue => { diff --git a/src/entities/BlockNode/types/BlockNodeSerialized.ts b/src/entities/BlockNode/types/BlockNodeSerialized.ts index ce7c2c5d..ddb88abb 100644 --- a/src/entities/BlockNode/types/BlockNodeSerialized.ts +++ b/src/entities/BlockNode/types/BlockNodeSerialized.ts @@ -2,10 +2,19 @@ import { BlockTuneSerialized } from '../../BlockTune'; import { ValueSerialized } from '../../ValueNode/types'; import { TextNodeSerialized } from '../../inline-fragments'; +/** + * Union type of serialized BlockNode child nodes + */ export type BlockChildNodeSerialized = ValueSerialized | TextNodeSerialized; +/** + * Reccurrent type representing serialized BlockNode data + */ export type BlockNodeDataSerializedValue = BlockChildNodeSerialized | BlockChildNodeSerialized[] | BlockNodeDataSerialized | BlockNodeDataSerialized[]; +/** + * Root type representing serialized BlockNode data + */ export interface BlockNodeDataSerialized { [key: string]: BlockNodeDataSerializedValue; } diff --git a/src/entities/EditorDocument/index.ts b/src/entities/EditorDocument/index.ts index 5f6b5b46..3d259c25 100644 --- a/src/entities/EditorDocument/index.ts +++ b/src/entities/EditorDocument/index.ts @@ -222,8 +222,9 @@ export class EditorDocument { } /** + * Initializes EditorDocument with passed blocks * - * @param blocks + * @param blocks - document serialized blocks */ #initialize(blocks: BlockNodeSerialized[]): void { blocks.forEach((block) => { diff --git a/src/entities/ValueNode/types/ValueSerialized.ts b/src/entities/ValueNode/types/ValueSerialized.ts index ab557327..d66ba217 100644 --- a/src/entities/ValueNode/types/ValueSerialized.ts +++ b/src/entities/ValueNode/types/ValueSerialized.ts @@ -1,3 +1,6 @@ import { BlockChildType } from '../../BlockNode/types'; -export type ValueSerialized = V extends Record ? V & { $t: BlockChildType.Value } : V; +/** + * Type representing serialized ValueNode + */ +export type ValueSerialized = V extends Record ? V & { $t: BlockChildType.Value } : V; diff --git a/src/entities/inline-fragments/InlineNode/index.ts b/src/entities/inline-fragments/InlineNode/index.ts index 53da4583..04b58520 100644 --- a/src/entities/inline-fragments/InlineNode/index.ts +++ b/src/entities/inline-fragments/InlineNode/index.ts @@ -135,6 +135,9 @@ export interface ChildTextNodeSerialized { fragments: InlineFragment[]; } +/** + * Root type representing serialized TextNode data + */ export interface TextNodeSerialized extends ChildTextNodeSerialized { $t: BlockChildType.Text; } From f347a68fb68988c620fb20c2483fb080e55959a1 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Wed, 25 Oct 2023 01:18:13 +0200 Subject: [PATCH 05/11] Update ValueNode and TextNode methods calls from BlockNode & fix review comments --- .../BlockNode/BlockNode.integration.spec.ts | 103 +++++ src/entities/BlockNode/BlockNode.spec.ts | 357 ++++++++++++++++-- src/entities/BlockNode/consts.ts | 1 + src/entities/BlockNode/index.ts | 96 ++--- src/entities/BlockNode/types/DataKey.ts | 3 + src/entities/EditorDocument/index.ts | 4 + .../types/EditorDocumentSerialized.ts | 16 +- src/entities/ValueNode/ValueNode.spec.ts | 5 +- src/entities/ValueNode/index.ts | 3 +- .../ValueNode/types/ValueSerialized.ts | 5 +- .../inline-fragments/InlineNode/index.ts | 15 +- .../ParentInlineNode/index.ts | 6 +- .../inline-fragments/TextInlineNode/index.ts | 6 +- .../inline-fragments/TextNode/index.ts | 3 +- .../specs/InlineTree.integration.spec.ts | 11 +- .../specs/ParentInlineNode.spec.ts | 2 +- .../specs/TextInlineNode.spec.ts | 2 +- src/utils/keypath.spec.ts | 244 ++++++++++++ src/utils/keypath.ts | 58 +++ src/utils/mapObject.spec.ts | 40 ++ src/utils/mapObject.ts | 9 + 21 files changed, 878 insertions(+), 111 deletions(-) create mode 100644 src/entities/BlockNode/BlockNode.integration.spec.ts create mode 100644 src/entities/BlockNode/consts.ts create mode 100644 src/utils/keypath.spec.ts create mode 100644 src/utils/keypath.ts create mode 100644 src/utils/mapObject.spec.ts create mode 100644 src/utils/mapObject.ts diff --git a/src/entities/BlockNode/BlockNode.integration.spec.ts b/src/entities/BlockNode/BlockNode.integration.spec.ts new file mode 100644 index 00000000..a22b25d0 --- /dev/null +++ b/src/entities/BlockNode/BlockNode.integration.spec.ts @@ -0,0 +1,103 @@ +import { BlockNode, createDataKey } from './index'; +import { BlockChildType } from './types'; +import { NODE_TYPE_HIDDEN_PROP } from './consts'; + +describe('BlockNode integration tests', () => { + it('should create ValueNode by primitive value', () => { + const value = 'value'; + const newValue = 'updated value'; + const node = new BlockNode({ + name: 'blockNode', + data: { + value, + }, + }); + + node.updateValue(createDataKey('value'), newValue); + + expect(node.serialized.data) + .toEqual({ + value: newValue, + }); + }); + + it('should create ValueNode by object marked as value', () => { + const value = { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Value, + value: 'value', + }; + const newValue = { + value: 'updated value', + }; + const node = new BlockNode({ + name: 'blockNode', + data: { + value, + }, + }); + + node.updateValue(createDataKey('value'), newValue); + + expect(node.serialized.data) + .toEqual({ + value: { + ...newValue, + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Value, + }, + }); + }); + + it('should create TextNode by passed text node data', () => { + const text = { + value: 'Editor.js is a block-styled editor', + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + }; + const addedText = ' for rich media web content'; + const node = new BlockNode({ + name: 'blockNode', + data: { + text, + }, + }); + + node.insertText(createDataKey('text'), addedText); + + expect(node.serialized.data).toEqual({ + text: { + value: `${text.value}${addedText}`, + fragments: [], + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + }, + }); + }); + + it('should create relevant nodes from the array', () => { + const value = 'value'; + const updatedValue = 'updated value'; + const text = { + value: 'Editor.js is a block-styled editor', + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + }; + const addedText = ' for rich media web content'; + const node = new BlockNode({ + name: 'blockNode', + data: { + array: [value, text], + }, + }); + + node.updateValue(createDataKey('array.0'), updatedValue); + node.insertText(createDataKey('array.1'), addedText); + + expect(node.serialized.data).toEqual({ + array: [ + updatedValue, + { + value: `${text.value}${addedText}`, + fragments: [], + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + }, + ], + }); + }); +}); diff --git a/src/entities/BlockNode/BlockNode.spec.ts b/src/entities/BlockNode/BlockNode.spec.ts index 0975aaca..7a603dff 100644 --- a/src/entities/BlockNode/BlockNode.spec.ts +++ b/src/entities/BlockNode/BlockNode.spec.ts @@ -7,6 +7,7 @@ import type { EditorDocument } from '../EditorDocument'; import type { ValueNodeConstructorParameters } from '../ValueNode'; import { InlineToolData, InlineToolName, TextNode } from '../inline-fragments'; import { BlockChildType } from './types'; +import { NODE_TYPE_HIDDEN_PROP } from './consts'; jest.mock('../BlockTune'); @@ -23,11 +24,13 @@ describe('BlockNode', () => { }); it('should have empty object as data by default', () => { - expect(node.serialized.data).toEqual({}); + expect(node.serialized.data) + .toEqual({}); }); it('should set null as parent by default', () => { - expect(node.parent).toBeNull(); + expect(node.parent) + .toBeNull(); }); }); @@ -112,7 +115,7 @@ describe('BlockNode', () => { .reduce((acc, index) => ({ ...acc, [createDataKey(`data-key-${index}c${index}d`)]: { - $t: BlockChildType.Text, + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, value: '', fragments: [], }, @@ -158,7 +161,7 @@ describe('BlockNode', () => { data: { items: [ { - $t: BlockChildType.Text, + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, value: '', fragments: [], }, @@ -173,7 +176,7 @@ describe('BlockNode', () => { .toHaveBeenCalledTimes(1); }); - it('should call .serialized getter of ValueNodes in an nested object', () => { + it('should call .serialized getter of ValueNodes in a nested object', () => { const spy = jest.spyOn(ValueNode.prototype, 'serialized', 'get'); const blockNode = new BlockNode({ name: createBlockToolName('paragraph'), @@ -199,7 +202,7 @@ describe('BlockNode', () => { object: { nestedObject: { text: { - $t: BlockChildType.Text, + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, value: '', fragments: [], }, @@ -241,7 +244,7 @@ describe('BlockNode', () => { object: { array: [ { text: { - $t: BlockChildType.Text, + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, value: '', fragments: [], }, @@ -281,7 +284,7 @@ describe('BlockNode', () => { array: [ { object: { text: { - $t: BlockChildType.Text, + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, value: '', fragments: [], }, @@ -320,7 +323,7 @@ describe('BlockNode', () => { data: { array: [ [ { - $t: BlockChildType.Text, + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, value: '', fragments: [], }, @@ -341,7 +344,7 @@ describe('BlockNode', () => { name: createBlockToolName('paragraph'), data: { value: { - $t: BlockChildType.Value, + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Value, property: '', }, }, @@ -382,7 +385,8 @@ describe('BlockNode', () => { blockNode.updateTuneData(blockTuneName, data); - expect(spy).toHaveBeenCalledWith(dataKey, dataValue); + expect(spy) + .toHaveBeenCalledWith(dataKey, dataValue); }); }); @@ -407,7 +411,70 @@ describe('BlockNode', () => { blockNode.updateValue(dataKey, value); - expect(spy).toHaveBeenCalledWith(value); + expect(spy) + .toHaveBeenCalledWith(value); + }); + + it('should call .update() method of ValueNode when node is inside an object', () => { + const dataKey = createDataKey('data-key-1a2b'); + const value = 'Some value'; + + const blockNode = new BlockNode({ + name: createBlockToolName('paragraph'), + data: { + object: { + [dataKey]: 'value', + }, + }, + parent: {} as EditorDocument, + }); + + const spy = jest.spyOn(ValueNode.prototype, 'update'); + + blockNode.updateValue(createDataKey(`object.${dataKey}`), value); + + expect(spy) + .toHaveBeenCalledWith(value); + }); + + it('should call .update() method of ValueNode when node is in an array', () => { + const value = 'Some value'; + + const blockNode = new BlockNode({ + name: createBlockToolName('paragraph'), + data: { + array: [ 'value' ], + }, + parent: {} as EditorDocument, + }); + + const spy = jest.spyOn(ValueNode.prototype, 'update'); + + blockNode.updateValue(createDataKey(`array.0`), value); + + expect(spy) + .toHaveBeenCalledWith(value); + }); + + it('should call .update() method of ValueNode when node is in an array in an object', () => { + const value = 'Some value'; + + const blockNode = new BlockNode({ + name: createBlockToolName('paragraph'), + data: { + object: { + array: [ 'value' ], + }, + }, + parent: {} as EditorDocument, + }); + + const spy = jest.spyOn(ValueNode.prototype, 'update'); + + blockNode.updateValue(createDataKey(`object.array.0`), value); + + expect(spy) + .toHaveBeenCalledWith(value); }); it('should throw an error if the ValueNode with the passed dataKey does not exist', () => { @@ -422,7 +489,8 @@ describe('BlockNode', () => { expect(() => { blockNode.updateValue(dataKey, value); - }).toThrowError(`BlockNode: data with key ${dataKey} does not exist`); + }) + .toThrowError(`BlockNode: data with key ${dataKey} does not exist`); }); it('should throw an error if the ValueNode with the passed dataKey is not a ValueNode', () => { @@ -439,7 +507,8 @@ describe('BlockNode', () => { expect(() => { blockNode.updateValue(dataKey, value); - }).toThrowError(`BlockNode: data with key ${dataKey} is not a ValueNode`); + }) + .toThrowError(`BlockNode: data with key ${dataKey} is not a ValueNode`); }); }); @@ -449,13 +518,35 @@ describe('BlockNode', () => { const text = 'Some text'; beforeEach(() => { - node = new BlockNode({ name: createBlockToolName('header'), + node = new BlockNode({ + name: createBlockToolName('header'), data: { [dataKey]: { - $t: BlockChildType.Text, + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, value: '', fragments: [], }, + object: { + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + array: [ + { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + ], + }, + array: [ + { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + ], }, }); }); @@ -465,7 +556,35 @@ describe('BlockNode', () => { node.insertText(dataKey, text); - expect(spy).toHaveBeenCalledWith(text, undefined); + expect(spy) + .toHaveBeenCalledWith(text, undefined); + }); + + it('should call .insertText() method of the TextNode in an object', () => { + const spy = jest.spyOn(TextNode.prototype, 'insertText'); + + node.insertText(createDataKey(`object.${dataKey}`), text); + + expect(spy) + .toHaveBeenCalledWith(text, undefined); + }); + + it('should call .insertText() method of the TextNode in an array in an object', () => { + const spy = jest.spyOn(TextNode.prototype, 'insertText'); + + node.insertText(createDataKey(`object.array.0`), text); + + expect(spy) + .toHaveBeenCalledWith(text, undefined); + }); + + it('should call .insertText() method of the TextNode in an array', () => { + const spy = jest.spyOn(TextNode.prototype, 'insertText'); + + node.insertText(createDataKey(`array.0`), text); + + expect(spy) + .toHaveBeenCalledWith(text, undefined); }); it('should pass start index to the .insertText() method of the TextNode', () => { @@ -474,13 +593,15 @@ describe('BlockNode', () => { node.insertText(dataKey, text, start); - expect(spy).toHaveBeenCalledWith(text, start); + expect(spy) + .toHaveBeenCalledWith(text, start); }); it('should throw an error if node does not exist', () => { const key = createDataKey('non-existing-key'); - expect(() => node.insertText(key, text)).toThrow(); + expect(() => node.insertText(key, text)) + .toThrow(); }); it('should throw an error if node is not a TextNode', () => { @@ -491,7 +612,8 @@ describe('BlockNode', () => { }, }); - expect(() => node.insertText(dataKey, text)).toThrow(); + expect(() => node.insertText(dataKey, text)) + .toThrow(); }); }); @@ -500,13 +622,35 @@ describe('BlockNode', () => { const dataKey = createDataKey('text'); beforeEach(() => { - node = new BlockNode({ name: createBlockToolName('header'), + node = new BlockNode({ + name: createBlockToolName('header'), data: { [dataKey]: { - $t: BlockChildType.Text, + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, value: '', fragments: [], }, + object: { + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + array: [ + { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + ], + }, + array: [ + { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + ], }, }); }); @@ -516,7 +660,35 @@ describe('BlockNode', () => { node.removeText(dataKey); - expect(spy).toHaveBeenCalledWith(undefined, undefined); + expect(spy) + .toHaveBeenCalledWith(undefined, undefined); + }); + + it('should call .removeText() method of the TextNode in an object', () => { + const spy = jest.spyOn(TextNode.prototype, 'removeText'); + + node.removeText(createDataKey(`object.${dataKey}`)); + + expect(spy) + .toHaveBeenCalledWith(undefined, undefined); + }); + + it('should call .removeText() method of the TextNode in an array in an object', () => { + const spy = jest.spyOn(TextNode.prototype, 'removeText'); + + node.removeText(createDataKey(`object.array.0`)); + + expect(spy) + .toHaveBeenCalledWith(undefined, undefined); + }); + + it('should call .removeText() method of the TextNode in an array', () => { + const spy = jest.spyOn(TextNode.prototype, 'removeText'); + + node.removeText(createDataKey(`array.0`)); + + expect(spy) + .toHaveBeenCalledWith(undefined, undefined); }); it('should pass start index to the .removeText() method of the TextNode', () => { @@ -525,7 +697,8 @@ describe('BlockNode', () => { node.removeText(dataKey, start); - expect(spy).toHaveBeenCalledWith(start, undefined); + expect(spy) + .toHaveBeenCalledWith(start, undefined); }); it('should pass end index to the .removeText() method of the TextNode', () => { @@ -535,13 +708,15 @@ describe('BlockNode', () => { node.removeText(dataKey, start, end); - expect(spy).toHaveBeenCalledWith(start, end); + expect(spy) + .toHaveBeenCalledWith(start, end); }); it('should throw an error if node does not exist', () => { const key = createDataKey('non-existing-key'); - expect(() => node.removeText(key)).toThrow(); + expect(() => node.removeText(key)) + .toThrow(); }); it('should throw an error if node is not a TextNode', () => { @@ -552,7 +727,8 @@ describe('BlockNode', () => { }, }); - expect(() => node.removeText(dataKey)).toThrow(); + expect(() => node.removeText(dataKey)) + .toThrow(); }); }); @@ -564,13 +740,35 @@ describe('BlockNode', () => { const end = 10; beforeEach(() => { - node = new BlockNode({ name: createBlockToolName('header'), + node = new BlockNode({ + name: createBlockToolName('header'), data: { [dataKey]: { - $t: BlockChildType.Text, + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, value: '', fragments: [], }, + object: { + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + array: [ + { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + ], + }, + array: [ + { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + ], }, }); }); @@ -580,7 +778,35 @@ describe('BlockNode', () => { node.format(dataKey, tool, start, end); - expect(spy).toHaveBeenCalledWith(tool, start, end, undefined); + expect(spy) + .toHaveBeenCalledWith(tool, start, end, undefined); + }); + + it('should call .format() method of the TextNode in an object', () => { + const spy = jest.spyOn(TextNode.prototype, 'format'); + + node.format(createDataKey(`object.${dataKey}`), tool, start, end); + + expect(spy) + .toHaveBeenCalledWith(tool, start, end, undefined); + }); + + it('should call .format() method of the TextNode in an array in an object', () => { + const spy = jest.spyOn(TextNode.prototype, 'format'); + + node.format(createDataKey(`object.array.0`), tool, start, end); + + expect(spy) + .toHaveBeenCalledWith(tool, start, end, undefined); + }); + + it('should call .format() method of the TextNode in an array', () => { + const spy = jest.spyOn(TextNode.prototype, 'format'); + + node.format(createDataKey(`array.0`), tool, start, end); + + expect(spy) + .toHaveBeenCalledWith(tool, start, end, undefined); }); it('should pass data to the .format() method of the TextNode', () => { @@ -589,13 +815,15 @@ describe('BlockNode', () => { node.format(dataKey, tool, start, end, data); - expect(spy).toHaveBeenCalledWith(tool, start, end, data); + expect(spy) + .toHaveBeenCalledWith(tool, start, end, data); }); it('should throw an error if node does not exist', () => { const key = createDataKey('non-existing-key'); - expect(() => node.format(key, tool, start, end)).toThrow(); + expect(() => node.format(key, tool, start, end)) + .toThrow(); }); it('should throw an error if node is not a TextNode', () => { @@ -606,7 +834,8 @@ describe('BlockNode', () => { }, }); - expect(() => node.format(dataKey, tool, start, end)).toThrow(); + expect(() => node.format(dataKey, tool, start, end)) + .toThrow(); }); }); @@ -618,13 +847,35 @@ describe('BlockNode', () => { const end = 10; beforeEach(() => { - node = new BlockNode({ name: createBlockToolName('header'), + node = new BlockNode({ + name: createBlockToolName('header'), data: { [dataKey]: { - $t: BlockChildType.Text, + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, value: '', fragments: [], }, + object: { + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + array: [ + { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + ], + }, + array: [ + { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + ], }, }); }); @@ -634,13 +885,42 @@ describe('BlockNode', () => { node.unformat(dataKey, tool, start, end); - expect(spy).toHaveBeenCalledWith(tool, start, end); + expect(spy) + .toHaveBeenCalledWith(tool, start, end); + }); + + it('should call .unformat() method of the TextNode in an object', () => { + const spy = jest.spyOn(TextNode.prototype, 'unformat'); + + node.unformat(createDataKey(`object.${dataKey}`), tool, start, end); + + expect(spy) + .toHaveBeenCalledWith(tool, start, end); + }); + + it('should call .unformat() method of the TextNode in an array in an object', () => { + const spy = jest.spyOn(TextNode.prototype, 'unformat'); + + node.unformat(createDataKey(`object.array.0`), tool, start, end); + + expect(spy) + .toHaveBeenCalledWith(tool, start, end); + }); + + it('should call .unformat() method of the TextNode in an array', () => { + const spy = jest.spyOn(TextNode.prototype, 'unformat'); + + node.unformat(createDataKey('array.0'), tool, start, end); + + expect(spy) + .toHaveBeenCalledWith(tool, start, end); }); it('should throw an error if node does not exist', () => { const key = createDataKey('non-existing-key'); - expect(() => node.unformat(key, tool, start, end)).toThrow(); + expect(() => node.unformat(key, tool, start, end)) + .toThrow(); }); it('should throw an error if node is not a TextNode', () => { @@ -651,7 +931,8 @@ describe('BlockNode', () => { }, }); - expect(() => node.unformat(dataKey, tool, start, end)).toThrow(); + expect(() => node.unformat(dataKey, tool, start, end)) + .toThrow(); }); }); }); diff --git a/src/entities/BlockNode/consts.ts b/src/entities/BlockNode/consts.ts new file mode 100644 index 00000000..fd1a6973 --- /dev/null +++ b/src/entities/BlockNode/consts.ts @@ -0,0 +1 @@ +export const NODE_TYPE_HIDDEN_PROP = '$t' as const; diff --git a/src/entities/BlockNode/index.ts b/src/entities/BlockNode/index.ts index abf49d45..29044027 100644 --- a/src/entities/BlockNode/index.ts +++ b/src/entities/BlockNode/index.ts @@ -1,5 +1,5 @@ import { EditorDocument } from '../EditorDocument'; -import { BlockTune, BlockTuneName, createBlockTuneName } from '../BlockTune'; +import { BlockTune, BlockTuneName, BlockTuneSerialized, createBlockTuneName } from '../BlockTune'; import { BlockNodeConstructorParameters, BlockToolName, @@ -16,6 +16,9 @@ import { } from './types'; import { ValueNode } from '../ValueNode'; import { InlineToolData, InlineToolName, TextNode, TextNodeSerialized } from '../inline-fragments'; +import { get, has } from '../../utils/keypath'; +import { NODE_TYPE_HIDDEN_PROP } from './consts'; +import { mapObject } from '../../utils/mapObject'; /** * BlockNode class represents a node in a tree-like structure used to store and manipulate Blocks in an editor document. @@ -52,20 +55,20 @@ export class BlockNode { * @param [args.parent] - The parent EditorDocument of the BlockNode. * @param [args.tunes] - The BlockTunes associated with the BlockNode. */ - constructor({ name, data = {}, parent, tunes = {} }: BlockNodeConstructorParameters) { + constructor({ + name, + data = {}, + parent, + tunes = {}, + }: BlockNodeConstructorParameters) { this.#name = createBlockToolName(name); this.#parent = parent ?? null; - this.#tunes = Object.fromEntries( - Object.entries(tunes) - .map( - ([tuneName, tuneData]) => ([ - createBlockTuneName(tuneName), - new BlockTune({ - name: createBlockTuneName(tuneName), - data: tuneData, - }), - ]) - ) + this.#tunes = mapObject( + tunes, + (tuneData: BlockTuneSerialized, tuneName: string) => new BlockTune({ + name: createBlockTuneName(tuneName), + data: tuneData, + }) ); this.#initialize(data); @@ -91,24 +94,14 @@ export class BlockNode { return data.serialized; } - return Object.fromEntries( - Object.entries(data) - .map(([key, value]) => ([key, map(value)])) - ); + return mapObject(data, map); }; - const serializedData = Object.fromEntries( - Object - .entries(this.#data) - .map(([dataKey, value]) => ([dataKey, map(value)])) - ); + const serializedData = mapObject(this.#data, map); - const serializedTunes = Object.fromEntries( - Object - .entries(this.#tunes) - .map( - ([name, tune]) => ([name, tune.serialized]) - ) + const serializedTunes = mapObject( + this.#tunes, + (tune) => tune.serialized ); return { @@ -125,9 +118,10 @@ export class BlockNode { * @param data - The data to update the BlockTune with */ public updateTuneData(tuneName: BlockTuneName, data: Record): void { - Object.entries(data).forEach(([key, value]) => { - this.#tunes[tuneName].update(key, value); - }); + Object.entries(data) + .forEach(([key, value]) => { + this.#tunes[tuneName].update(key, value); + }); } /** @@ -139,7 +133,7 @@ export class BlockNode { public updateValue(dataKey: DataKey, value: T): void { this.#validateKey(dataKey, ValueNode); - const node = this.#data[dataKey] as ValueNode; + const node = get(this.#data, dataKey as string) as ValueNode; node.update(value); } @@ -154,7 +148,7 @@ export class BlockNode { public insertText(dataKey: DataKey, text: string, start?: number): void { this.#validateKey(dataKey, TextNode); - const node = this.#data[dataKey] as TextNode; + const node = get(this.#data, dataKey as string) as TextNode; node.insertText(text, start); } @@ -169,7 +163,7 @@ export class BlockNode { public removeText(dataKey: DataKey, start?: number, end?: number): string { this.#validateKey(dataKey, TextNode); - const node = this.#data[dataKey] as TextNode; + const node = get(this.#data, dataKey as string) as TextNode; return node.removeText(start, end); } @@ -186,7 +180,7 @@ export class BlockNode { public format(dataKey: DataKey, tool: InlineToolName, start: number, end: number, data?: InlineToolData): void { this.#validateKey(dataKey, TextNode); - const node = this.#data[dataKey] as TextNode; + const node = get(this.#data, dataKey as string) as TextNode; node.format(tool, start, end, data); } @@ -202,7 +196,7 @@ export class BlockNode { public unformat(key: DataKey, tool: InlineToolName, start: number, end: number): void { this.#validateKey(key, TextNode); - const node = this.#data[key] as TextNode; + const node = get(this.#data, key as string) as TextNode; node.unformat(tool, start, end); } @@ -213,14 +207,26 @@ export class BlockNode { * @param data - block data */ #initialize(data: BlockNodeDataSerialized): void { + /** + * Recursively maps serialized data to BlockNodeData + * + * 1. If value is an object with NODE_TYPE_HIDDEN_PROP, then it's a serialized node. + * a. If NODE_TYPE_HIDDEN_PROP is BlockChildType.Value, then it's a serialized ValueNode + * b. If NODE_TYPE_HIDDEN_PROP is BlockChildType.Text, then it's a serialized TextNode + * 2. If value is an array, then it's an array of serialized nodes, so map it recursively + * 3. If value is an object without NODE_TYPE_HIDDEN_PROP, then it's a JSON object, so map it recursively + * 4. Otherwise, it's a primitive value, so create a ValueNode with it + * + * @param value - serialized value + */ const map = (value: BlockNodeDataSerializedValue): BlockNodeData | BlockNodeDataValue => { if (Array.isArray(value)) { return value.map(map) as BlockNodeData[] | ChildNode[]; } if (typeof value === 'object' && value !== null) { - if ('$t' in value) { - switch (value.$t) { + if (NODE_TYPE_HIDDEN_PROP in value) { + switch (value[NODE_TYPE_HIDDEN_PROP]) { case BlockChildType.Value: return new ValueNode({ value }); case BlockChildType.Text: @@ -228,19 +234,13 @@ export class BlockNode { } } - return Object.fromEntries( - Object.entries(value) - .map(([key, v]) => ([key, map(v)])) - ); + return mapObject(value as BlockNodeDataSerialized, map); } return new ValueNode({ value }); }; - this.#data = Object.fromEntries( - Object.entries(data) - .map(([key, value]) => ([key, map(value)])) - ); + this.#data = mapObject(data, map); } /** @@ -251,11 +251,11 @@ export class BlockNode { * @private */ #validateKey(key: DataKey, Node?: typeof ValueNode | typeof TextNode): void { - if (this.#data[key] === undefined) { + if (!has(this.#data, key as string)) { throw new Error(`BlockNode: data with key ${key} does not exist`); } - if (Node && !(this.#data[key] instanceof Node)) { + if (Node && !(get(this.#data, key as string) instanceof Node)) { throw new Error(`BlockNode: data with key ${key} is not a ${Node.name}`); } } diff --git a/src/entities/BlockNode/types/DataKey.ts b/src/entities/BlockNode/types/DataKey.ts index 7c232300..669be6c7 100644 --- a/src/entities/BlockNode/types/DataKey.ts +++ b/src/entities/BlockNode/types/DataKey.ts @@ -2,6 +2,9 @@ import { create, Nominal } from '../../../utils/Nominal'; /** * Base type of the data key field + * + * DataKeyBase is a string for object properties or a number for array indexes + * DataKey could be a compound key path, e.g. 'dataKey.nestedArrayKey.0' */ type DataKeyBase = string | number; diff --git a/src/entities/EditorDocument/index.ts b/src/entities/EditorDocument/index.ts index 3d259c25..b1a56f92 100644 --- a/src/entities/EditorDocument/index.ts +++ b/src/entities/EditorDocument/index.ts @@ -212,7 +212,11 @@ export class EditorDocument { } /** + * Returns serialized data associated with the document * + * Data contains: + * - blocks - array of serialized blocks + * - properties - JSON object with document properties (eg read-only) */ public get serialized(): EditorDocumentSerialized { return { diff --git a/src/entities/EditorDocument/types/EditorDocumentSerialized.ts b/src/entities/EditorDocument/types/EditorDocumentSerialized.ts index ff05612d..606f9b07 100644 --- a/src/entities/EditorDocument/types/EditorDocumentSerialized.ts +++ b/src/entities/EditorDocument/types/EditorDocumentSerialized.ts @@ -1,7 +1,19 @@ import { BlockNodeSerialized } from '../../BlockNode/types'; import { Properties } from './Properties'; +/** + * Type representing serialized EditorDocument + * + * Serialized EditorDocument is a JSON object containing blocks and document properties + */ export interface EditorDocumentSerialized { - blocks: BlockNodeSerialized[]; - properties: Properties; + /** + * Array of serialized BlockNodes + */ + blocks: BlockNodeSerialized[]; + + /** + * JSON object containing document properties (eg read-only) + */ + properties: Properties; } diff --git a/src/entities/ValueNode/ValueNode.spec.ts b/src/entities/ValueNode/ValueNode.spec.ts index 8f2f14ae..8d31c94e 100644 --- a/src/entities/ValueNode/ValueNode.spec.ts +++ b/src/entities/ValueNode/ValueNode.spec.ts @@ -1,5 +1,6 @@ import { ValueNode } from './index'; import { BlockChildType } from '../BlockNode/types'; +import { NODE_TYPE_HIDDEN_PROP } from '../BlockNode/consts'; describe('ValueNode', () => { describe('.update()', () => { @@ -33,7 +34,7 @@ describe('ValueNode', () => { expect(serializedLongitude).toStrictEqual(longitude); }); - it('should mark serialized value as value node if object returned', () => { + it('should mark serialized value as value node by using custom hidden property $t if object returned', () => { const value = { align: 'left' }; const longitudeValueNode = new ValueNode({ value, @@ -41,7 +42,7 @@ describe('ValueNode', () => { const serializedValue = longitudeValueNode.serialized; - expect(serializedValue).toHaveProperty('$t', BlockChildType.Value); + expect(serializedValue).toHaveProperty(NODE_TYPE_HIDDEN_PROP, BlockChildType.Value); }); }); }); diff --git a/src/entities/ValueNode/index.ts b/src/entities/ValueNode/index.ts index 243f28bb..610892d0 100644 --- a/src/entities/ValueNode/index.ts +++ b/src/entities/ValueNode/index.ts @@ -1,5 +1,6 @@ import type { ValueNodeConstructorParameters, ValueSerialized } from './types'; import { BlockChildType } from '../BlockNode/types'; +import { NODE_TYPE_HIDDEN_PROP } from '../BlockNode/consts'; /** * ValueNode class represents a node in a tree-like structure, used to store and manipulate data associated with a BlockNode. @@ -38,7 +39,7 @@ export class ValueNode { let value = this.#value; if (typeof value === 'object' && this.#value !== null) { - value = Object.assign({ $t: BlockChildType.Value }, value); + value = Object.assign({ [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Value }, value); } return value as ValueSerialized; diff --git a/src/entities/ValueNode/types/ValueSerialized.ts b/src/entities/ValueNode/types/ValueSerialized.ts index d66ba217..afd5ded0 100644 --- a/src/entities/ValueNode/types/ValueSerialized.ts +++ b/src/entities/ValueNode/types/ValueSerialized.ts @@ -1,6 +1,9 @@ import { BlockChildType } from '../../BlockNode/types'; +import { NODE_TYPE_HIDDEN_PROP } from '../../BlockNode/consts'; /** * Type representing serialized ValueNode + * + * Serialized ValueNode is a JSON primitive value or a JSON object with hidden property $t */ -export type ValueSerialized = V extends Record ? V & { $t: BlockChildType.Value } : V; +export type ValueSerialized = V extends Record ? V & { [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Value } : V; diff --git a/src/entities/inline-fragments/InlineNode/index.ts b/src/entities/inline-fragments/InlineNode/index.ts index 04b58520..1b63f357 100644 --- a/src/entities/inline-fragments/InlineNode/index.ts +++ b/src/entities/inline-fragments/InlineNode/index.ts @@ -1,5 +1,6 @@ import { InlineToolData, InlineToolName } from '../FormattingInlineNode'; import { BlockChildType } from '../../BlockNode/types'; +import { NODE_TYPE_HIDDEN_PROP } from '../../BlockNode/consts'; /** * Interface describing abstract InlineNode — common properties and methods for all inline nodes @@ -13,7 +14,7 @@ export interface InlineNode { /** * Serialized value of the node */ - readonly serialized: ChildTextNodeSerialized; + readonly serialized: InlineTreeNodeSerialized; /** * Returns text value in passed range @@ -122,12 +123,14 @@ export interface InlineFragment { /** * Serialized Inline Node value + * + * Interface used for tree nodes except TextNode which is a tree root */ -export interface ChildTextNodeSerialized { +export interface InlineTreeNodeSerialized { /** * Text value of the node and its subtree */ - text: string; + value: string; /** * Fragments which node and its subtree contains @@ -137,7 +140,9 @@ export interface ChildTextNodeSerialized { /** * Root type representing serialized TextNode data + * + * Interface used for a tree root */ -export interface TextNodeSerialized extends ChildTextNodeSerialized { - $t: BlockChildType.Text; +export interface TextNodeSerialized extends InlineTreeNodeSerialized { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text; } diff --git a/src/entities/inline-fragments/ParentInlineNode/index.ts b/src/entities/inline-fragments/ParentInlineNode/index.ts index 1d0d1758..a21e29bd 100644 --- a/src/entities/inline-fragments/ParentInlineNode/index.ts +++ b/src/entities/inline-fragments/ParentInlineNode/index.ts @@ -1,4 +1,4 @@ -import { InlineFragment, InlineNode, ChildTextNodeSerialized } from '../InlineNode'; +import { InlineFragment, InlineNode, InlineTreeNodeSerialized } from '../InlineNode'; import { ParentNode, ParentNodeConstructorOptions } from '../mixins/ParentNode'; import { ChildNode } from '../mixins/ChildNode'; import type { InlineToolData, InlineToolName } from '../FormattingInlineNode'; @@ -30,9 +30,9 @@ export class ParentInlineNode implements InlineNode { /** * Returns serialized value of the node: text and formatting fragments */ - public get serialized(): ChildTextNodeSerialized { + public get serialized(): InlineTreeNodeSerialized { return { - text: this.getText(), + value: this.getText(), fragments: this.getFragments(), }; } diff --git a/src/entities/inline-fragments/TextInlineNode/index.ts b/src/entities/inline-fragments/TextInlineNode/index.ts index 986e3d9b..307614f9 100644 --- a/src/entities/inline-fragments/TextInlineNode/index.ts +++ b/src/entities/inline-fragments/TextInlineNode/index.ts @@ -1,6 +1,6 @@ import { FormattingInlineNode, InlineToolName, InlineToolData } from '../index'; import { TextInlineNodeConstructorParameters } from './types'; -import { InlineNode, ChildTextNodeSerialized } from '../InlineNode'; +import { InlineNode, InlineTreeNodeSerialized } from '../InlineNode'; import { ChildNode } from '../mixins/ChildNode'; export * from './types'; @@ -37,9 +37,9 @@ export class TextInlineNode implements InlineNode { /** * Returns serialized value of the node */ - public get serialized(): ChildTextNodeSerialized { + public get serialized(): InlineTreeNodeSerialized { return { - text: this.getText(), + value: this.getText(), // No fragments for text node fragments: [], }; diff --git a/src/entities/inline-fragments/TextNode/index.ts b/src/entities/inline-fragments/TextNode/index.ts index 0508863f..085bf0ff 100644 --- a/src/entities/inline-fragments/TextNode/index.ts +++ b/src/entities/inline-fragments/TextNode/index.ts @@ -1,5 +1,6 @@ import { InlineFragment, ParentInlineNode, ParentInlineNodeConstructorOptions, TextNodeSerialized } from '../index'; import { BlockChildType } from '../../BlockNode/types'; +import { NODE_TYPE_HIDDEN_PROP } from '../../BlockNode/consts'; interface TextNodeConstructorOptions extends ParentInlineNodeConstructorOptions { value?: string; @@ -28,7 +29,7 @@ export class TextNode extends ParentInlineNode { * Returns serialized TextNode */ public get serialized(): TextNodeSerialized { - return Object.assign({ $t: BlockChildType.Text as const }, super.serialized); + return Object.assign({ [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text as const }, super.serialized); } /** diff --git a/src/entities/inline-fragments/specs/InlineTree.integration.spec.ts b/src/entities/inline-fragments/specs/InlineTree.integration.spec.ts index 0f062287..8cb820fe 100644 --- a/src/entities/inline-fragments/specs/InlineTree.integration.spec.ts +++ b/src/entities/inline-fragments/specs/InlineTree.integration.spec.ts @@ -7,6 +7,7 @@ import { TextNodeSerialized } from '../index'; import { BlockChildType } from '../../BlockNode/types'; +import { NODE_TYPE_HIDDEN_PROP } from '../../BlockNode/consts'; describe('Inline fragments tree integration', () => { describe('text insertion', () => { @@ -373,8 +374,8 @@ describe('Inline fragments tree integration', () => { tree.unformat(boldTool, ...(pluggable.map((value) => value - removedChars) as [number, number])); expect(tree.serialized).toStrictEqual({ - $t: BlockChildType.Text, - text: firstFragment + secondFragment.replace(' data', '') + thirdFragment, + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: firstFragment + secondFragment.replace(' data', '') + thirdFragment, fragments: [ { tool: boldTool, @@ -398,8 +399,8 @@ describe('Inline fragments tree integration', () => { it('should initialize tree with initial text and fragments', () => { const data = { - $t: BlockChildType.Text, - text: 'Editor.js is a block-styled editor. It returns clean output in JSON. Designed to be extendable and pluggable with a simple API.', + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: 'Editor.js is a block-styled editor. It returns clean output in JSON. Designed to be extendable and pluggable with a simple API.', fragments: [ { tool: 'bold', @@ -433,7 +434,7 @@ describe('Inline fragments tree integration', () => { } as TextNodeSerialized; const tree = new TextNode({ - value: data.text, + value: data.value, fragments: data.fragments, }); diff --git a/src/entities/inline-fragments/specs/ParentInlineNode.spec.ts b/src/entities/inline-fragments/specs/ParentInlineNode.spec.ts index 22159f5f..690a53bc 100644 --- a/src/entities/inline-fragments/specs/ParentInlineNode.spec.ts +++ b/src/entities/inline-fragments/specs/ParentInlineNode.spec.ts @@ -59,7 +59,7 @@ describe('ParentInlineNode', () => { it('should have correct data format', () => { const result = node.serialized; - expect(result).toHaveProperty('text'); + expect(result).toHaveProperty('value'); expect(result).toHaveProperty('fragments'); }); }); diff --git a/src/entities/inline-fragments/specs/TextInlineNode.spec.ts b/src/entities/inline-fragments/specs/TextInlineNode.spec.ts index 8b2d1eb7..2fc31bdc 100644 --- a/src/entities/inline-fragments/specs/TextInlineNode.spec.ts +++ b/src/entities/inline-fragments/specs/TextInlineNode.spec.ts @@ -264,7 +264,7 @@ describe('TextInlineNode', () => { const result = node.serialized; expect(result).toEqual({ - text: initialText, + value: initialText, fragments: [], }); }); diff --git a/src/utils/keypath.spec.ts b/src/utils/keypath.spec.ts new file mode 100644 index 00000000..2f1b2983 --- /dev/null +++ b/src/utils/keypath.spec.ts @@ -0,0 +1,244 @@ +import { get, has, set } from './keypath'; + +describe('keypath util', () => { + const value = 'value'; + + describe('set()', () => { + it('should do nothing if no key passed', () => { + const object = {}; + + set(object, [], value); + + expect(object).toEqual(object); + }); + + it('should created nested objects by string key parts', () => { + const object: Record = {}; + + set(object, 'a.b.c', value); + + expect(object.a.b.c).toEqual(value); + }); + + it('should created nested objects by string key parts when keys passed as an array', () => { + const object: Record = {}; + + set(object, ['a', 'b', 'c'], value); + + expect(object.a.b.c).toEqual(value); + }); + + it('should update existing value', () => { + const updatedValue = 'updated value'; + const object = { + value, + }; + + set(object, 'value', updatedValue); + + expect(object.value).toEqual(updatedValue); + }); + + it('should update existing nested value', () => { + const updatedValue = 'updated value'; + const object = { + a: { value }, + }; + + set(object, 'a.value', updatedValue); + + expect(object.a.value).toEqual(updatedValue); + }); + + it('should not replace a nested object', () => { + const updatedValue = 'updated value'; + const object = { + a: { + value, + assert: 'assert', + }, + }; + + set(object, 'a.value', updatedValue); + + expect(object.a).toEqual({ assert: 'assert', + value: updatedValue }); + }); + + it('should create array for numeric key parts', () => { + const object: Record = {}; + + set(object, 'a.0', value); + + expect(object.a).toBeInstanceOf(Array); + }); + + it('should insert value into array for numeric key parts', () => { + const object: Record = {}; + + set(object, 'a.0', value); + + expect(object.a[0]).toEqual(value); + }); + + it('should insert value into an array by the correct index for numeric key parts when index is ', () => { + const object: Record = {}; + + set(object, 'a.1', value); + + expect(object.a).toEqual([undefined, value]); + }); + + it('should create an object inside an array', () => { + const object: Record = {}; + + set(object, 'a.0.b', value); + + expect(object.a[0].b).toEqual(value); + }); + }); + + describe('get()', () => { + it('should return original object if no key is passed', () => { + const object = {}; + + const result = get(object, []); + + expect(result).toEqual(object); + }); + + it('should return value from nested objects', () => { + const object = { + a: { + b: { + c: value, + }, + }, + }; + + const result = get(object, 'a.b.c'); + + expect(result).toEqual(value); + }); + + it('should return a nested object if keypath is not full', () => { + const object = { + a: { + b: { + c: value, + }, + }, + }; + + const result = get(object, 'a.b'); + + expect(result).toEqual({ c: value }); + }); + + it('should return value from nested objects when keys are passed as array', () => { + const object = { + a: { + b: { + c: value, + }, + }, + }; + + const result = get(object, ['a', 'b', 'c']); + + expect(result).toEqual(value); + }); + + it('should return value from an array', () => { + const object = { + a: [ value ], + }; + + const result = get(object, 'a.0'); + + expect(result).toEqual(value); + }); + + it('should return value from an object inside an array', () => { + const object = { + a: [ { b: value } ], + }; + + const result = get(object, 'a.0.b'); + + expect(result).toEqual(value); + }); + + it('should return undefined if there is no value by given key', () => { + const object = {}; + + const result = get(object, 'a.b.c'); + + expect(result).toBeUndefined(); + }); + }); + + describe('has()', () => { + it('should return true if value exists by keypath', () => { + const object = { + a: { + b: { + c: value, + }, + }, + }; + + const result = has(object, 'a.b.c'); + + expect(result).toEqual(true); + }); + + it('should return false if value doesnt exist by keypath', () => { + const object = {}; + + const result = has(object, 'a.b.c'); + + expect(result).toEqual(false); + }); + + it('should return true if value exists but equals false', () => { + const object = { + a: false, + }; + + const result = has(object, 'a'); + + expect(result).toEqual(true); + }); + + it('should return true if value exists but equals 0', () => { + const object = { + a: 0, + }; + + const result = has(object, 'a'); + + expect(result).toEqual(true); + }); + + it('should return true if value exists but equals null', () => { + const object = { + a: null, + }; + + const result = has(object, 'a'); + + expect(result).toEqual(true); + }); + + it('should return true if value exists but equals empty string', () => { + const object = { + a: '', + }; + + const result = has(object, 'a'); + + expect(result).toEqual(true); + }); + }); +}); diff --git a/src/utils/keypath.ts b/src/utils/keypath.ts new file mode 100644 index 00000000..a82fd006 --- /dev/null +++ b/src/utils/keypath.ts @@ -0,0 +1,58 @@ +/** + * Get value from object by keypath + * + * @param data - object to get value from + * @param keys - keypath to a value + */ +export function get(data: Record, keys: string | string[]): T | undefined { + const parsedKeys = Array.isArray(keys) ? keys : keys.split('.'); + const key = parsedKeys.shift(); + + if (!key) { + return data as T; + } + + if (data[key] === undefined) { + return undefined; + } + + return get(data[key] as Record, parsedKeys); +} + +/** + * Set value to object by keypath + * + * @param data - object to set value to + * @param keys - keypath to a value + * @param value - value to set + */ +export function set(data: Record, keys: string | string[], value: T): void { + const parsedKeys = Array.isArray(keys) ? keys : keys.split('.'); + const key = parsedKeys.shift(); + + if (!key) { + return; + } + + if (parsedKeys.length === 0) { + data[key] = value; + + return; + } + + if (data[key] === undefined) { + data[key] = !isNaN(Number(parsedKeys[0])) ? [] : {}; + } + + set(data[key] as Record, parsedKeys, value); +} + +/** + * Check if object has value by keypath + * + * @param data - object to check + * @param keys - keypath to a value + */ +export function has(data: Record, keys: string | string[]): boolean { + return get(data, keys) !== undefined; +} diff --git a/src/utils/mapObject.spec.ts b/src/utils/mapObject.spec.ts new file mode 100644 index 00000000..203042ca --- /dev/null +++ b/src/utils/mapObject.spec.ts @@ -0,0 +1,40 @@ +import { mapObject } from './mapObject'; + +describe('mapObject()', () => { + it('should map through passed object', () => { + const object = { + a: 1, + b: 2, + c: 3, + d: 4, + }; + + const result = mapObject(object, (value) => value * 2); + + expect(result).toEqual({ + a: 2, + b: 4, + c: 6, + d: 8, + }); + }); + + it('should pass key to map function', () => { + const key = 'a'; + const object = { [key]: 'value' }; + + /** + * Map function + * + * @param value - entry value + * @param k - entry key + */ + const map = (value: string, k: string): string => { + expect(k).toEqual(key); + + return value; + }; + + mapObject(object, map); + }); +}); diff --git a/src/utils/mapObject.ts b/src/utils/mapObject.ts new file mode 100644 index 00000000..f28fb2a4 --- /dev/null +++ b/src/utils/mapObject.ts @@ -0,0 +1,9 @@ +/** + * Helper to map through object properties + * + * @param obj - object to map through + * @param fn - map function + */ +export function mapObject, M extends Record>(obj: T, fn: (entry: T[keyof T], key: string) => M[keyof T]): M { + return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, fn(value, key)])) as M; +} From c8b7e8495eda6c1dd0814a948c25f6fb5d3a3166 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Wed, 25 Oct 2023 01:20:38 +0200 Subject: [PATCH 06/11] Add comment to hidden prop const --- src/entities/BlockNode/consts.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/entities/BlockNode/consts.ts b/src/entities/BlockNode/consts.ts index fd1a6973..d412169f 100644 --- a/src/entities/BlockNode/consts.ts +++ b/src/entities/BlockNode/consts.ts @@ -1 +1,4 @@ +/** + * Name of hidden property to mark value and text nodes in serialized data + */ export const NODE_TYPE_HIDDEN_PROP = '$t' as const; From de31234ec77894132d43aef832602bfe437e3be0 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Wed, 25 Oct 2023 01:33:03 +0200 Subject: [PATCH 07/11] Update test cases names --- src/entities/BlockNode/BlockNode.integration.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/entities/BlockNode/BlockNode.integration.spec.ts b/src/entities/BlockNode/BlockNode.integration.spec.ts index a22b25d0..4e239d8a 100644 --- a/src/entities/BlockNode/BlockNode.integration.spec.ts +++ b/src/entities/BlockNode/BlockNode.integration.spec.ts @@ -21,7 +21,7 @@ describe('BlockNode integration tests', () => { }); }); - it('should create ValueNode by object marked as value', () => { + it('should create ValueNode by object marked as value and update its value', () => { const value = { [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Value, value: 'value', @@ -47,7 +47,7 @@ describe('BlockNode integration tests', () => { }); }); - it('should create TextNode by passed text node data', () => { + it('should create TextNode by passed text node data and insert new text into it', () => { const text = { value: 'Editor.js is a block-styled editor', [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, @@ -71,7 +71,7 @@ describe('BlockNode integration tests', () => { }); }); - it('should create relevant nodes from the array', () => { + it('should create relevant nodes from the array and update their values', () => { const value = 'value'; const updatedValue = 'updated value'; const text = { From 606e52d763cd062e65f94d514538a4b74c4acb99 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Wed, 25 Oct 2023 01:38:30 +0200 Subject: [PATCH 08/11] Apply linter --- src/utils/keypath.spec.ts | 1 + src/utils/keypath.ts | 4 ++-- src/utils/mapObject.ts | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utils/keypath.spec.ts b/src/utils/keypath.spec.ts index 2f1b2983..9490ee61 100644 --- a/src/utils/keypath.spec.ts +++ b/src/utils/keypath.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { get, has, set } from './keypath'; describe('keypath util', () => { diff --git a/src/utils/keypath.ts b/src/utils/keypath.ts index a82fd006..b4ddb727 100644 --- a/src/utils/keypath.ts +++ b/src/utils/keypath.ts @@ -4,7 +4,7 @@ * @param data - object to get value from * @param keys - keypath to a value */ -export function get(data: Record, keys: string | string[]): T | undefined { +export function get(data: Record, keys: string | string[]): T | undefined { const parsedKeys = Array.isArray(keys) ? keys : keys.split('.'); const key = parsedKeys.shift(); @@ -53,6 +53,6 @@ export function set(data: Record, keys: string | s * @param data - object to check * @param keys - keypath to a value */ -export function has(data: Record, keys: string | string[]): boolean { +export function has(data: Record, keys: string | string[]): boolean { return get(data, keys) !== undefined; } diff --git a/src/utils/mapObject.ts b/src/utils/mapObject.ts index f28fb2a4..cd0f5c7f 100644 --- a/src/utils/mapObject.ts +++ b/src/utils/mapObject.ts @@ -4,6 +4,7 @@ * @param obj - object to map through * @param fn - map function */ -export function mapObject, M extends Record>(obj: T, fn: (entry: T[keyof T], key: string) => M[keyof T]): M { +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- any used in generic +export function mapObject, M extends Record>(obj: T, fn: (entry: T[keyof T], key: string) => M[keyof T]): M { return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, fn(value, key)])) as M; } From 3a95807fa9039b4a67b3e6e5bbf33967d2e9e618 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Wed, 25 Oct 2023 01:43:38 +0200 Subject: [PATCH 09/11] Fix build --- src/utils/keypath.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/utils/keypath.ts b/src/utils/keypath.ts index b4ddb727..bee27e80 100644 --- a/src/utils/keypath.ts +++ b/src/utils/keypath.ts @@ -4,7 +4,8 @@ * @param data - object to get value from * @param keys - keypath to a value */ -export function get(data: Record, keys: string | string[]): T | undefined { +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- unknown can't be used as data parameter is used for recursion +export function get(data: Record, keys: string | string[]): T | undefined { const parsedKeys = Array.isArray(keys) ? keys : keys.split('.'); const key = parsedKeys.shift(); @@ -53,6 +54,7 @@ export function set(data: Record, keys: string | s * @param data - object to check * @param keys - keypath to a value */ -export function has(data: Record, keys: string | string[]): boolean { +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- unknown can't be used as data parameter is used for recursion +export function has(data: Record, keys: string | string[]): boolean { return get(data, keys) !== undefined; } From dbf4972256e23583c50ecc3b76b5a5db026683f2 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Thu, 2 Nov 2023 20:42:10 +0000 Subject: [PATCH 10/11] Update src/entities/BlockNode/BlockNode.spec.ts Co-authored-by: Peter Savchenko --- src/entities/BlockNode/BlockNode.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/entities/BlockNode/BlockNode.spec.ts b/src/entities/BlockNode/BlockNode.spec.ts index 7a603dff..1e87b196 100644 --- a/src/entities/BlockNode/BlockNode.spec.ts +++ b/src/entities/BlockNode/BlockNode.spec.ts @@ -415,7 +415,7 @@ describe('BlockNode', () => { .toHaveBeenCalledWith(value); }); - it('should call .update() method of ValueNode when node is inside an object', () => { + it('should call .update() method of ValueNode with passed keypath when node is inside an object', () => { const dataKey = createDataKey('data-key-1a2b'); const value = 'Some value'; From 91b40eec6d6b9cecbe7a8bbba43b9c615aba79a2 Mon Sep 17 00:00:00 2001 From: George Berezhnoy Date: Thu, 2 Nov 2023 20:52:08 +0000 Subject: [PATCH 11/11] Use single TextNode for each test case --- src/entities/BlockNode/BlockNode.spec.ts | 351 +++++++++++++---------- src/utils/keypath.spec.ts | 2 +- 2 files changed, 207 insertions(+), 146 deletions(-) diff --git a/src/entities/BlockNode/BlockNode.spec.ts b/src/entities/BlockNode/BlockNode.spec.ts index 1e87b196..328aa97a 100644 --- a/src/entities/BlockNode/BlockNode.spec.ts +++ b/src/entities/BlockNode/BlockNode.spec.ts @@ -6,7 +6,7 @@ import { ValueNode } from '../ValueNode'; import type { EditorDocument } from '../EditorDocument'; import type { ValueNodeConstructorParameters } from '../ValueNode'; import { InlineToolData, InlineToolName, TextNode } from '../inline-fragments'; -import { BlockChildType } from './types'; +import { BlockChildType, BlockNodeDataSerialized } from './types'; import { NODE_TYPE_HIDDEN_PROP } from './consts'; jest.mock('../BlockTune'); @@ -15,6 +15,13 @@ jest.mock('../inline-fragments/TextNode'); jest.mock('../ValueNode'); +const createBlockNodeWithData = (data: BlockNodeDataSerialized): BlockNode => { + return new BlockNode({ + name: createBlockToolName('header'), + data, + }); +}; + describe('BlockNode', () => { describe('constructor', () => { let node: BlockNode; @@ -513,46 +520,18 @@ describe('BlockNode', () => { }); describe('.insertText()', () => { - let node: BlockNode; const dataKey = createDataKey('text'); const text = 'Some text'; - beforeEach(() => { - node = new BlockNode({ - name: createBlockToolName('header'), - data: { - [dataKey]: { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, - value: '', - fragments: [], - }, - object: { - [dataKey]: { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, - value: '', - fragments: [], - }, - array: [ - { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, - value: '', - fragments: [], - }, - ], - }, - array: [ - { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, - value: '', - fragments: [], - }, - ], - }, - }); - }); - it('should call .insertText() method of the TextNode', () => { const spy = jest.spyOn(TextNode.prototype, 'insertText'); + const node = createBlockNodeWithData({ + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + }); node.insertText(dataKey, text); @@ -562,6 +541,15 @@ describe('BlockNode', () => { it('should call .insertText() method of the TextNode in an object', () => { const spy = jest.spyOn(TextNode.prototype, 'insertText'); + const node = createBlockNodeWithData({ + object: { + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + }, + }); node.insertText(createDataKey(`object.${dataKey}`), text); @@ -571,6 +559,17 @@ describe('BlockNode', () => { it('should call .insertText() method of the TextNode in an array in an object', () => { const spy = jest.spyOn(TextNode.prototype, 'insertText'); + const node = createBlockNodeWithData({ + object: { + array: [ + { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + ], + }, + }); node.insertText(createDataKey(`object.array.0`), text); @@ -580,6 +579,15 @@ describe('BlockNode', () => { it('should call .insertText() method of the TextNode in an array', () => { const spy = jest.spyOn(TextNode.prototype, 'insertText'); + const node = createBlockNodeWithData({ + array: [ + { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + ], + }); node.insertText(createDataKey(`array.0`), text); @@ -590,6 +598,13 @@ describe('BlockNode', () => { it('should pass start index to the .insertText() method of the TextNode', () => { const spy = jest.spyOn(TextNode.prototype, 'insertText'); const start = 5; + const node = createBlockNodeWithData({ + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + }); node.insertText(dataKey, text, start); @@ -599,13 +614,14 @@ describe('BlockNode', () => { it('should throw an error if node does not exist', () => { const key = createDataKey('non-existing-key'); + const node = createBlockNodeWithData({}); expect(() => node.insertText(key, text)) .toThrow(); }); it('should throw an error if node is not a TextNode', () => { - node = new BlockNode({ + const node = new BlockNode({ name: createBlockToolName('header'), data: { [dataKey]: new ValueNode({} as ValueNodeConstructorParameters), @@ -618,45 +634,17 @@ describe('BlockNode', () => { }); describe('.removeText()', () => { - let node: BlockNode; const dataKey = createDataKey('text'); - beforeEach(() => { - node = new BlockNode({ - name: createBlockToolName('header'), - data: { - [dataKey]: { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, - value: '', - fragments: [], - }, - object: { - [dataKey]: { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, - value: '', - fragments: [], - }, - array: [ - { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, - value: '', - fragments: [], - }, - ], - }, - array: [ - { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, - value: '', - fragments: [], - }, - ], - }, - }); - }); - it('should call .removeText() method of the TextNode', () => { const spy = jest.spyOn(TextNode.prototype, 'removeText'); + const node = createBlockNodeWithData({ + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + }); node.removeText(dataKey); @@ -666,6 +654,15 @@ describe('BlockNode', () => { it('should call .removeText() method of the TextNode in an object', () => { const spy = jest.spyOn(TextNode.prototype, 'removeText'); + const node = createBlockNodeWithData({ + object: { + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + }, + }); node.removeText(createDataKey(`object.${dataKey}`)); @@ -675,6 +672,17 @@ describe('BlockNode', () => { it('should call .removeText() method of the TextNode in an array in an object', () => { const spy = jest.spyOn(TextNode.prototype, 'removeText'); + const node = createBlockNodeWithData({ + object: { + array: [ + { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + ], + }, + }); node.removeText(createDataKey(`object.array.0`)); @@ -684,6 +692,15 @@ describe('BlockNode', () => { it('should call .removeText() method of the TextNode in an array', () => { const spy = jest.spyOn(TextNode.prototype, 'removeText'); + const node = createBlockNodeWithData({ + array: [ + { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + ], + }); node.removeText(createDataKey(`array.0`)); @@ -694,6 +711,13 @@ describe('BlockNode', () => { it('should pass start index to the .removeText() method of the TextNode', () => { const spy = jest.spyOn(TextNode.prototype, 'removeText'); const start = 5; + const node = createBlockNodeWithData({ + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + }); node.removeText(dataKey, start); @@ -705,6 +729,13 @@ describe('BlockNode', () => { const spy = jest.spyOn(TextNode.prototype, 'removeText'); const start = 5; const end = 10; + const node = createBlockNodeWithData({ + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + }); node.removeText(dataKey, start, end); @@ -714,13 +745,20 @@ describe('BlockNode', () => { it('should throw an error if node does not exist', () => { const key = createDataKey('non-existing-key'); + const node = createBlockNodeWithData({ + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + }); expect(() => node.removeText(key)) .toThrow(); }); it('should throw an error if node is not a TextNode', () => { - node = new BlockNode({ + const node = new BlockNode({ name: createBlockToolName('header'), data: { [dataKey]: new ValueNode({} as ValueNodeConstructorParameters), @@ -733,48 +771,20 @@ describe('BlockNode', () => { }); describe('.format()', () => { - let node: BlockNode; const dataKey = createDataKey('text'); const tool = 'bold' as InlineToolName; const start = 5; const end = 10; - beforeEach(() => { - node = new BlockNode({ - name: createBlockToolName('header'), - data: { - [dataKey]: { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, - value: '', - fragments: [], - }, - object: { - [dataKey]: { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, - value: '', - fragments: [], - }, - array: [ - { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, - value: '', - fragments: [], - }, - ], - }, - array: [ - { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, - value: '', - fragments: [], - }, - ], - }, - }); - }); - it('should call .format() method of the TextNode', () => { const spy = jest.spyOn(TextNode.prototype, 'format'); + const node = createBlockNodeWithData({ + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + }); node.format(dataKey, tool, start, end); @@ -784,6 +794,15 @@ describe('BlockNode', () => { it('should call .format() method of the TextNode in an object', () => { const spy = jest.spyOn(TextNode.prototype, 'format'); + const node = createBlockNodeWithData({ + object: { + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + }, + }); node.format(createDataKey(`object.${dataKey}`), tool, start, end); @@ -793,6 +812,17 @@ describe('BlockNode', () => { it('should call .format() method of the TextNode in an array in an object', () => { const spy = jest.spyOn(TextNode.prototype, 'format'); + const node = createBlockNodeWithData({ + object: { + array: [ + { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + ], + }, + }); node.format(createDataKey(`object.array.0`), tool, start, end); @@ -802,6 +832,15 @@ describe('BlockNode', () => { it('should call .format() method of the TextNode in an array', () => { const spy = jest.spyOn(TextNode.prototype, 'format'); + const node = createBlockNodeWithData({ + array: [ + { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + ], + }); node.format(createDataKey(`array.0`), tool, start, end); @@ -812,6 +851,13 @@ describe('BlockNode', () => { it('should pass data to the .format() method of the TextNode', () => { const spy = jest.spyOn(TextNode.prototype, 'format'); const data = {} as InlineToolData; + const node = createBlockNodeWithData({ + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + }); node.format(dataKey, tool, start, end, data); @@ -821,13 +867,20 @@ describe('BlockNode', () => { it('should throw an error if node does not exist', () => { const key = createDataKey('non-existing-key'); + const node = createBlockNodeWithData({ + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + }); expect(() => node.format(key, tool, start, end)) .toThrow(); }); it('should throw an error if node is not a TextNode', () => { - node = new BlockNode({ + const node = new BlockNode({ name: createBlockToolName('header'), data: { [dataKey]: new ValueNode({} as ValueNodeConstructorParameters), @@ -840,48 +893,20 @@ describe('BlockNode', () => { }); describe('.unformat()', () => { - let node: BlockNode; const dataKey = createDataKey('text'); const tool = 'bold' as InlineToolName; const start = 5; const end = 10; - beforeEach(() => { - node = new BlockNode({ - name: createBlockToolName('header'), - data: { - [dataKey]: { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, - value: '', - fragments: [], - }, - object: { - [dataKey]: { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, - value: '', - fragments: [], - }, - array: [ - { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, - value: '', - fragments: [], - }, - ], - }, - array: [ - { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, - value: '', - fragments: [], - }, - ], - }, - }); - }); - it('should call .unformat() method of the TextNode', () => { const spy = jest.spyOn(TextNode.prototype, 'unformat'); + const node = createBlockNodeWithData({ + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + }); node.unformat(dataKey, tool, start, end); @@ -891,6 +916,15 @@ describe('BlockNode', () => { it('should call .unformat() method of the TextNode in an object', () => { const spy = jest.spyOn(TextNode.prototype, 'unformat'); + const node = createBlockNodeWithData({ + object: { + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + }, + }); node.unformat(createDataKey(`object.${dataKey}`), tool, start, end); @@ -900,6 +934,17 @@ describe('BlockNode', () => { it('should call .unformat() method of the TextNode in an array in an object', () => { const spy = jest.spyOn(TextNode.prototype, 'unformat'); + const node = createBlockNodeWithData({ + object: { + array: [ + { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + ], + }, + }); node.unformat(createDataKey(`object.array.0`), tool, start, end); @@ -909,6 +954,15 @@ describe('BlockNode', () => { it('should call .unformat() method of the TextNode in an array', () => { const spy = jest.spyOn(TextNode.prototype, 'unformat'); + const node = createBlockNodeWithData({ + array: [ + { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + ], + }); node.unformat(createDataKey('array.0'), tool, start, end); @@ -918,13 +972,20 @@ describe('BlockNode', () => { it('should throw an error if node does not exist', () => { const key = createDataKey('non-existing-key'); + const node = createBlockNodeWithData({ + [dataKey]: { + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text, + value: '', + fragments: [], + }, + }); expect(() => node.unformat(key, tool, start, end)) .toThrow(); }); it('should throw an error if node is not a TextNode', () => { - node = new BlockNode({ + const node = new BlockNode({ name: createBlockToolName('header'), data: { [dataKey]: new ValueNode({} as ValueNodeConstructorParameters), diff --git a/src/utils/keypath.spec.ts b/src/utils/keypath.spec.ts index 9490ee61..da2dde27 100644 --- a/src/utils/keypath.spec.ts +++ b/src/utils/keypath.spec.ts @@ -82,7 +82,7 @@ describe('keypath util', () => { expect(object.a[0]).toEqual(value); }); - it('should insert value into an array by the correct index for numeric key parts when index is ', () => { + it('should insert value into an array by the correct index for numeric key parts when index is greater than array length', () => { const object: Record = {}; set(object, 'a.1', value);