Skip to content

Commit

Permalink
[CHORE] Test suite for editor normalizers (#4096)
Browse files Browse the repository at this point in the history
  • Loading branch information
marc-hughes authored Aug 9, 2023
1 parent 8721acf commit bad5144
Show file tree
Hide file tree
Showing 18 changed files with 539 additions and 183 deletions.
20 changes: 11 additions & 9 deletions assets/src/components/editing/editor/normalizers/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,25 @@ export const normalize = (
// so deletion removes the inner block and causes validation errors
if (node.type === 'p' && parent.type === 'code') {
Transforms.removeNodes(editor, { at: parentPath });
console.warn('Normalizing content: Special case code, removing node', node.type);
console.warn(`Normalizing content: Special case code, removing node ${node.type}`);
return true;
}

Transforms.unwrapNodes(editor, { at: path });
console.warn('Normalizing content: Invalid child type for parent', node.type, parent.type);
console.warn(
`Normalizing content: Invalid child type for parent ${parent.type} > ${node.type}`,
);
return true;
}
}
}

// Check the top-level constraints
if (Editor.isBlock(editor, node) && !schema[node.type].isTopLevel) {
if (Editor.isEditor(parent)) {
Transforms.unwrapNodes(editor, { at: path });
console.warn('Normalizing content: Unwrapping top level block node', node.type);
return true;
}
// Check the top-level constraints
if (Element.isElement(node) && Editor.isBlock(editor, node) && !schema[node.type].isTopLevel) {
if (Editor.isEditor(parent)) {
Transforms.unwrapNodes(editor, { at: path });
console.warn('Normalizing content: Unwrapping top level block node ' + node.type);
return true;
}
}
return false;
Expand Down
31 changes: 0 additions & 31 deletions assets/src/components/editing/editor/normalizers/code.ts

This file was deleted.

5 changes: 2 additions & 3 deletions assets/src/components/editing/editor/normalizers/lists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,14 @@ export const normalize = (

const [parent] = Editor.parent(editor, path);
if (Element.isElement(parent)) {
const config = schema[parent.type];
const parentConfig = schema[parent.type];
if (['ol', 'ul'].includes(parent.type)) {
if (Text.isText(node)) {
Transforms.wrapNodes(editor, Model.li(), { at: path });
console.warn('Normalizing content: Wrapping text in list with list item');
return true;
}
if (Element.isElement(node) && !config.validChildren[node.type]) {
//Transforms.setNodes(editor, { type: 'li' }, { at: path });
if (Element.isElement(node) && !parentConfig.validChildren[node.type]) {
Transforms.wrapNodes(editor, Model.li(), { at: path });
console.warn('Normalizing content: Wrapping node in list to list item type');
return true;
Expand Down
14 changes: 8 additions & 6 deletions assets/src/components/editing/editor/normalizers/normalizer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Editor, Element, Node, NodeEntry, Text, Transforms } from 'slate';
import { normalize as blockNormalize } from 'components/editing/editor/normalizers/block';
import { normalize as codeNormalize } from 'components/editing/editor/normalizers/code';
import { normalize as forceRootNode } from 'components/editing/editor/normalizers/forceRootNode';
import { normalize as listNormalize } from 'components/editing/editor/normalizers/lists';
import { normalize as rootNormalize } from 'components/editing/editor/normalizers/root';
Expand All @@ -16,13 +15,12 @@ export interface NormalizerContext {

const restrictedElements = new Set(['input_ref']);

interface NormalizerOptions {
export interface NormalizerOptions {
insertParagraphStartEnd: boolean;
removeRestricted: boolean;
wrapParagraphs: boolean;
spacesNormalize: boolean;
blockNormalize: boolean;
codeNormalize: boolean;
listNormalize: boolean;
tableNormalize: boolean;
conjugationNormalize: boolean;
Expand Down Expand Up @@ -92,12 +90,16 @@ export function installNormalizer(
}

if (options.spacesNormalize && spacesNormalize(editor, node, path)) return;

// 8/9/2023 - When code elements were set to isVoid=true, these normalizers stopped doing anything
// if (options.codeNormalize && codeNormalize(editor, node, path)) return;

if (options.listNormalize && listNormalize(editor, node, path)) return; // Must come before block normalizer
if (options.blockNormalize && blockNormalize(editor, node, path)) return;
if (options.codeNormalize && codeNormalize(editor, node, path)) return;
if (options.listNormalize && listNormalize(editor, node, path)) return;

if (options.tableNormalize && tableNormalize(editor, node, path)) return;
} catch (e) {
// tslint:disable-next-line
// istanbul ignore next
console.error('Normalization Error:', e);
}
normalizeNode(entry);
Expand Down
6 changes: 4 additions & 2 deletions assets/src/components/editing/editor/normalizers/spaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import * as Immutable from 'immutable';
import { Editor, Element, Path, Transforms } from 'slate';
import { Model } from 'data/content/model/elements/factories';
import { ModelElement } from 'data/content/model/elements/types';
import { ModelTypes } from 'data/content/model/schema';
import { FormattedText } from 'data/content/model/text';

const spacesRequiredBetween = Immutable.Set<string>([
'image',
export const spacesRequiredBetween = Immutable.Set<ModelTypes>([
'img',
'youtube',
'audio',
'blockquote',
Expand All @@ -32,6 +33,7 @@ export const normalize = (
if (path[path.length - 1] + 1 < parent.children.length) {
const nextItem = Editor.node(editor, Path.next(path));

// istanbul ignore else
if (nextItem !== undefined) {
const [nextNode] = nextItem;
if (Element.isElement(node) && Element.isElement(nextNode)) {
Expand Down
2 changes: 1 addition & 1 deletion assets/src/components/editing/editor/normalizers/tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const getColumnSpan = (cells: TableCell[]): number =>
export const getEffectiveColumns = (row: TableRow, table: Table): number => {
const rowIndex = table.children.indexOf(row);
if (rowIndex === -1) {
console.error("Tried to getEffectiveColumns for a row that doesn't belong to the table");
console.warn("Tried to getEffectiveColumns for a row that doesn't belong to the table");
return 0;
}

Expand Down
31 changes: 24 additions & 7 deletions assets/src/data/content/model/elements/factories.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Text } from 'slate';
import {
AllModelElements,
Audio,
Blockquote,
Callout,
CalloutInline,
Citation,
CodeLine,
CodeV1,
CodeV2,
CommandButton,
Conjugation,
Expand Down Expand Up @@ -78,11 +79,18 @@ export const Model = {

tr: (children: TableCell[]) => create<TableRow>({ type: 'tr', children }),

table: (children: TableRow[]) => create<Table>({ type: 'table', children }),
table: (children: TableRow[] = []) => create<Table>({ type: 'table', children }),

li: (text = '') => create<ListItem>({ type: 'li', children: [Model.p(text)] }),
li: (text: string | ListItem['children'] = '') => {
if (typeof text === 'string') {
return create<ListItem>({ type: 'li', children: [Model.p(text)] });
}

ol: () => create<OrderedList>({ type: 'ol', children: [Model.li()] }),
return create<ListItem>({ type: 'li', children: text });
},

ol: (children?: OrderedList['children']) =>
create<OrderedList>({ type: 'ol', children: children || [Model.li()] }),

ul: (children?: ListChildren | undefined) =>
create<UnorderedList>({ type: 'ul', children: children || [Model.li()] }),
Expand Down Expand Up @@ -126,8 +134,8 @@ export const Model = {

dialogSpeaker: (name: string) => ({ name, image: '', id: guid() }),

dialogLine: (speaker: string) =>
create<DialogLine>({ type: 'dialog_line', speaker, children: [Model.p()] }),
dialogLine: (speaker: string, text = '') =>
create<DialogLine>({ type: 'dialog_line', speaker, children: [Model.p(text)] }),

dialog: (title = '') =>
create<Dialog>({
Expand Down Expand Up @@ -165,7 +173,7 @@ export const Model = {

audio: (src?: string) => create<Audio>({ type: 'audio', src }),

p: (children?: (InputRef | Text)[] | string) => {
p: (children?: Paragraph['children'] | string) => {
if (!children) return create<Paragraph>({ type: 'p' });
if (Array.isArray(children)) return create<Paragraph>({ type: 'p', children });
return create<Paragraph>({ type: 'p', children: [{ text: children }] });
Expand All @@ -176,13 +184,22 @@ export const Model = {
type: 'blockquote',
}),

codeLine: (text: string) => create<CodeLine>({ type: 'code_line', children: [{ text }] }),

code: (code = '') =>
create<CodeV2>({
type: 'code',
code,
language: 'Text',
}),

codeV1: (code = '') =>
create<CodeV1>({
type: 'code',
language: 'Text',
children: [Model.codeLine(code)],
}),

ecl: (code = '') =>
create<ECLRepl>({
type: 'ecl',
Expand Down
2 changes: 1 addition & 1 deletion assets/src/data/content/model/elements/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ type SubElements = DefinitionMeaning | Pronunciation | DefinitionTranslation | D
export type TableCell = TableHeader | TableData | TableConjugation;

type HeadingChildren = Text[];
export interface Paragraph extends SlateElement<(InputRef | Text | ImageBlock)[]> {
export interface Paragraph extends SlateElement<(InputRef | Text | ImageBlock | Inline)[]> {
type: 'p';
}

Expand Down
2 changes: 1 addition & 1 deletion assets/src/data/content/model/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export const schema: Schema = {
isVoid: true,
isBlock: true,
isTopLevel: true,
validChildren: {},
validChildren: toObj(['code_line']),
},
code_line: {
isVoid: false,
Expand Down
95 changes: 95 additions & 0 deletions assets/test/editor/normalize-test-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Descendant, Editor, Element, createEditor } from 'slate';
import { withHistory } from 'slate-history';
import { withReact } from 'slate-react';
import {
NormalizerOptions,
installNormalizer,
} from 'components/editing/editor/normalizers/normalizer';
import { withInlines } from 'components/editing/editor/overrides/inlines';
import { withTables } from 'components/editing/editor/overrides/tables';
import { withVoids } from 'components/editing/editor/overrides/voids';

export const expectAnyEmptyParagraph = {
type: 'p',
id: expect.any(String),
children: [{ text: '' }],
} as Descendant;

export const expectConsoleMessage = (
message: string | string[],
calls: jest.SpyInstance['mock']['calls'],
) => {
expect(calls).toContainEqual([message]);
};

/**
* Utility function to help test our normalizer suite. This will mock out console.warn and console.error
* so you can test if the normalizer is generating the expected debug output as well.
*
* @param content Slate content to normalize
* @returns The editor, and the console.warn and console.error calls that were made during normalization
*/
export const runNormalizer = (
content: Descendant[],
{
showLogs = false,
normalizerOptions = {},
}: {
showLogs?: boolean;
normalizerOptions?: Partial<NormalizerOptions>;
} = {},
): {
editor: Editor;
consoleErrorCalls: jest.SpyInstance['mock']['calls'];
consoleWarnCalls: jest.SpyInstance['mock']['calls'];
} => {
const warn = jest.spyOn(console, 'warn').mockImplementation((...args) => {
if (showLogs) {
console.log('console.warn', ...args);
}
});
const error = jest.spyOn(console, 'error').mockImplementation((...args) => {
if (showLogs) {
console.log('console.error', ...args);
}
});

jest.useFakeTimers();
const editor = withReact(withHistory(withTables(withInlines(withVoids(createEditor())))));
editor.children = [...content];
installNormalizer(editor, {}, normalizerOptions);
Editor.normalize(editor, { force: true });

jest.runAllTimers();
jest.runAllImmediates();

const consoleWarnCalls = warn.mock.calls;
const consoleErrorCalls = error.mock.calls;
jest.resetAllMocks();
jest.useRealTimers();

return {
editor,
consoleWarnCalls,
consoleErrorCalls,
};
};

/* Replaces {id: "xxxx"} with {id: expect.any(String)} */
export const expectAnyId = (content: Descendant | Descendant[]): Descendant | Descendant[] => {
if (!Array.isArray(content)) {
const [first] = expectAnyId([content]) as Descendant[];
return first;
}

return content.map((node) => {
if (Element.isElement(node) && 'id' in node) {
return {
...node,
id: expect.any(String),
children: expectAnyId(node.children),
};
}
return node;
}) as Descendant[];
};
Loading

0 comments on commit bad5144

Please sign in to comment.