test: fix tests

This commit is contained in:
b12f 2024-10-09 16:47:31 +02:00
parent 9ff091bd39
commit a51aad42ee
Signed by: b12f
GPG key ID: 729956E1124F8F26
8 changed files with 198 additions and 226 deletions

View file

@ -6,13 +6,13 @@ describe('@schlechtenburg/core', () => {
const a = 'a';
const b = 'b';
it('Should activate', () => {
it('Should activate', async () => {
const {
activeBlockId,
isActive,
activate,
deactivate,
} = withSetup(() => useActivation(a));
} = await withSetup(() => useActivation(a));
activate(a);
expect(activeBlockId.value).toBe(a);
@ -22,9 +22,9 @@ describe('@schlechtenburg/core', () => {
expect(activeBlockId.value).toBe(b);
expect(isActive.value).toBeFalsy();
deactivate();
deactivate(activeBlockId.value);
expect(isActive.value).toBeFalsy();
expect(activeBlockId.value).toBe(undefined);
expect(activeBlockId.value).toBe(null);
activate();
expect(activeBlockId.value).toBe(a);

View file

@ -1,7 +1,4 @@
/**
* Internal dependencies
*/
import { useFormatTypes } from './use-format-types';
import { FormatTypeStore } from './use-format-types';
import { createElement } from './create-element';
import { mergePair } from './concat';
import { OBJECT_REPLACEMENT_CHARACTER, ZWNBSP } from './special-characters';
@ -15,8 +12,14 @@ function createEmptyValue(): RichTextValue {
};
}
function toFormat( { tagName, attributes }: { tagName: string, attributes: Record<string,any> } ): RichTextFormat {
const { getFormatTypeForClassName, getFormatTypeForBareElement } = useFormatTypes();
function toFormat(
{ tagName, attributes }: { tagName: string, attributes: Record<string,any> },
store: FormatTypeStore,
): RichTextFormat {
if (!store) {
console.dir((new Error()).stack);
}
const { getFormatTypeForClassName, getFormatTypeForBareElement } = store;
let formatType: RichTextFormatType|undefined;
if ( attributes && attributes.class ) {
@ -81,14 +84,14 @@ function toFormat( { tagName, attributes }: { tagName: string, attributes: Recor
};
}
export const fromPlainText = (text: string) => create({ text });
export const fromHTMLString = (html: string) => create({ html });
export const fromHTMLElement = (htmlElement: Element, options: { preserveWhiteSpace?: boolean } = {}) => {
export const fromPlainText = (text: string, store: FormatTypeStore) => create({ text }, store);
export const fromHTMLString = (html: string, store: FormatTypeStore) => create({ html }, store);
export const fromHTMLElement = (htmlElement: Element, options: { preserveWhiteSpace?: boolean } = {}, store: FormatTypeStore) => {
const { preserveWhiteSpace = false } = options;
const element = preserveWhiteSpace
? htmlElement
: collapseWhiteSpace( htmlElement );
const richTextValue = create({ element });
const richTextValue = create({ element }, store);
Object.defineProperty( richTextValue, 'originalHTML', {
value: htmlElement.innerHTML,
} );
@ -120,27 +123,23 @@ export const fromHTMLElement = (htmlElement: Element, options: { preserveWhiteSp
* holds information about the formatting at the relevant text indices. Finally
* `start` and `end` state which text indices are selected. They are only
* provided if a `Range` was given.
*
* @param {Object} [$1] Optional named arguments.
* @param {Element} [$1.element] Element to create value from.
* @param {string} [$1.text] Text to create value from.
* @param {string} [$1.html] HTML to create value from.
* @param {Range} [$1.range] Range to create value from.
* @return {RichTextValue} A rich text value.
*/
export function create({
element,
text,
html,
range,
isEditableTree = false,
}: {
element?: Element|Node,
text?: string,
html?: string,
range?: SimpleRange,
isEditableTree?: boolean,
} = {} ): RichTextValue {
export function create(
{
element,
text,
html,
range,
isEditableTree = false,
}: {
element?: Element|Node,
text?: string,
html?: string,
range?: SimpleRange,
isEditableTree?: boolean,
} = {},
store: FormatTypeStore,
): RichTextValue {
if ( typeof text === 'string' && text.length > 0 ) {
return {
formats: Array( text.length ),
@ -163,17 +162,12 @@ export function create({
element,
range,
isEditableTree,
} );
}, store );
}
/**
* Helper to accumulate the value's selection start and end from the current
* node and range.
*
* @param {Object} accumulator Object to accumulate into.
* @param {Node} node Node to create value with.
* @param {Range} range Range to create value with.
* @param {Object} value Value that is being accumulated.
*/
function accumulateSelection(
accumulator: RichTextValue,
@ -238,12 +232,6 @@ function accumulateSelection(
/**
* Adjusts the start and end offsets from a range based on a text filter.
*
* @param {Node} node Node of which the text should be filtered.
* @param {Range} range The range to filter.
* @param {Function} filter Function to use to filter the text.
*
* @return {Object|undefined} Object containing range properties.
*/
function filterRange(node: Node, range?: SimpleRange, filter?: Function): SimpleRange|undefined {
if ( ! range ) {
@ -280,11 +268,6 @@ function filterRange(node: Node, range?: SimpleRange, filter?: Function): Simple
*
* @see
* https://developer.mozilla.org/en-US/docs/Web/CSS/white-space-collapse#collapsing_of_white_space
*
* @param {HTMLElement} element
* @param {boolean} isRoot
*
* @return {HTMLElement} New element with collapsed whitespace.
*/
function collapseWhiteSpace(element: HTMLElement, isRoot: boolean = true): HTMLElement {
const clone = element.cloneNode( true ) as HTMLElement;
@ -346,25 +329,18 @@ export function removeReservedCharacters( string: string ): string {
/**
* Creates a Rich Text value from a DOM element and range.
*
* @param {Object} $1 Named arguments.
* @param {Element} [$1.element] Element to create value from.
* @param {Range} [$1.range] Range to create value from.
* @param {boolean} [$1.isEditableTree]
*
* @return {RichTextValue} A rich text value.
*/
function createFromElement(
{
element,
range,
isEditableTree,
}:
{
}: {
element?:Element|Node,
range?:SimpleRange,
isEditableTree?: boolean,
}
},
store: FormatTypeStore,
): RichTextValue {
const accumulator = createEmptyValue();
@ -435,14 +411,14 @@ function createFromElement(
if ( tagName === 'br' ) {
accumulateSelection( accumulator, node, newRange, createEmptyValue() );
mergePair( accumulator, create( { text: '\n' } ) );
mergePair( accumulator, create( { text: '\n' }, store ) );
continue;
}
const format = toFormat( {
tagName,
attributes: getAttributes( { element: node as HTMLElement } ),
} );
}, store );
// When a format type is declared as not editable, replace it with an
// object replacement character and preserve the inner HTML.
@ -472,7 +448,7 @@ function createFromElement(
element: node as HTMLElement,
range: newRange,
isEditableTree,
} );
}, store );
accumulateSelection( accumulator, node, newRange, value );
@ -524,12 +500,6 @@ function createFromElement(
/**
* Gets the attributes of an element in object shape.
*
* @param {Object} $1 Named arguments.
* @param {Element} $1.element Element to get attributes from.
*
* @return {Object} Attribute object or `undefined` if the element has no
* attributes.
*/
function getAttributes({ element }: { element: Element }): Record<string, any>{
let accumulator: Record<string, any> = {};

View file

@ -1,17 +1,27 @@
import { describe, expect, it, beforeAll } from 'vitest'
import { mount } from '@vue/test-utils';
import { defineComponent, Component } from 'vue'
import { useFormatTypes, FormatTypeStore } from '../use-format-types';
import { create, removeReservedCharacters } from '../create';
import { OBJECT_REPLACEMENT_CHARACTER, ZWNBSP } from '../special-characters';
import { createElement } from '../create-element';
import { getSparseArrayLength, spec, specWithRegistration } from './helpers';
describe( 'create', () => {
const em = { type: 'em' };
const strong = { type: 'strong' };
const em = { type: 'em', attributes: {} };
const strong = { type: 'strong', attributes: {} };
beforeAll( () => {
// Initialize the rich-text store.
// require( '../store' );
} );
let TestComponent: Component;
let store: FormatTypeStore;
beforeAll(async () => {
TestComponent = defineComponent({
setup () {
return useFormatTypes();
}
});
store = mount(TestComponent).vm;
});
spec.forEach( ( { description, html, createRange, record } ) => {
if ( html === undefined ) {
@ -25,7 +35,7 @@ describe( 'create', () => {
const createdRecord = create( {
element,
range,
} );
}, store );
const formatsLength = getSparseArrayLength( record.formats );
const createdFormatsLength = getSparseArrayLength(
createdRecord.formats
@ -47,13 +57,13 @@ describe( 'create', () => {
// eslint-disable-next-line jest/valid-title
it( description, () => {
if ( formatName ) {
registerFormatType( formatName, formatType );
store.registerFormatTypes( formatName, formatType );
}
const result = create( { html } );
const result = create({ html }, store);
if ( formatName ) {
unregisterFormatType( formatName );
store.unregisterFormatTypes( formatName );
}
expect( result ).toEqual( expectedValue );
@ -62,7 +72,7 @@ describe( 'create', () => {
);
it( 'should reference formats', () => {
const value = create( { html: '<em>te<strong>st</strong></em>' } );
const value = create( { html: '<em>te<strong>st</strong></em>' }, store );
expect( value ).toEqual( {
formats: [ [ em ], [ em ], [ em, strong ], [ em, strong ] ],
@ -81,7 +91,7 @@ describe( 'create', () => {
} );
it( 'should use different reference for equal format', () => {
const value = create( { html: '<a href="#">a</a><a href="#">a</a>' } );
const value = create( { html: '<a href="#">a</a><a href="#">a</a>' }, store );
// Format objects.
expect( value.formats[ 0 ][ 0 ] ).not.toBe( value.formats[ 1 ][ 0 ] );
@ -91,7 +101,7 @@ describe( 'create', () => {
} );
it( 'should use different reference for different format', () => {
const value = create( { html: '<a href="#">a</a><a href="#a">a</a>' } );
const value = create( { html: '<a href="#">a</a><a href="#a">a</a>' }, store );
// Format objects.
expect( value.formats[ 0 ][ 0 ] ).not.toBe( value.formats[ 1 ][ 0 ] );

View file

@ -1,15 +1,27 @@
import { describe, expect, it, beforeAll } from 'vitest'
import { mount } from '@vue/test-utils';
import { defineComponent, ComponentInstance, Component } from 'vue'
import { useFormatTypes } from '../use-format-types';
import { toDom, applyValue } from '../to-dom';
import { createElement } from '../create-element';
import { spec } from './helpers';
describe( 'recordToDom', () => {
let TestComponent: Component;
let wrapper: ComponentInstance<typeof TestComponent>;
beforeAll(async () => {
TestComponent = defineComponent({
setup () {
return useFormatTypes();
}
});
wrapper = mount(TestComponent);
});
spec.forEach( ( { description, record, startPath, endPath } ) => {
// eslint-disable-next-line jest/valid-title
it( description, () => {
const { body, selection } = toDom( {
value: record,
} );
const { body, selection } = toDom({ value: record }, wrapper.componentVM);
expect( body ).toMatchSnapshot();
expect( selection ).toEqual( { startPath, endPath } );
} );

View file

@ -2,10 +2,10 @@ import { describe, expect, it, beforeAll } from 'vitest'
import { mount } from '@vue/test-utils';
import { defineComponent } from 'vue'
import { useFormatTypes } from '../use-format-types';
import { useFormatTypes, FormatTypeStore } from '../use-format-types';
import { create } from '../create';
import { toHTMLString } from '../to-html-string';
import { withSetup, specWithRegistration } from './helpers';
import { specWithRegistration } from './helpers';
function createNode( HTML ) {
const doc = document.implementation.createHTMLDocument( '' );
@ -14,7 +14,8 @@ function createNode( HTML ) {
}
describe( 'toHTMLString', () => {
let TestComponent, wrapper;
let TestComponent;
let store: FormatTypeStore;
beforeAll(async () => {
TestComponent = defineComponent({
setup () {
@ -22,7 +23,7 @@ describe( 'toHTMLString', () => {
}
});
wrapper = mount(TestComponent);
store = mount(TestComponent).vm;
});
specWithRegistration.forEach(
@ -42,13 +43,13 @@ describe( 'toHTMLString', () => {
it.skip( description, () => {
console.log(description, formatName);
if ( formatName ) {
wrapper.vm.addFormatTypes( { name: formatName, ...formatType } );
store.registerFormatTypes( { name: formatName, ...formatType } );
}
const result = toHTMLString( { value } );
if ( formatName ) {
wrapper.vm.removeFormatTypes( formatName );
store.unregisterFormatTypes( formatName );
}
expect( result ).toEqual( html );

View file

@ -1,23 +1,12 @@
/**
* Internal dependencies
*/
import { toTree } from './to-tree';
import { FormatTypeStore } from './use-format-types';
import { createElement } from './create-element';
import { isRangeEqual } from './is-range-equal';
import { RichTextValue } from './types';
/** @typedef {import('./types').RichTextValue} RichTextValue */
/**
* Creates a path as an array of indices from the given root node to the given
* node.
*
* @param {Node} node Node to find the path of.
* @param {HTMLElement} rootNode Root node to find the path from.
* @param {Array} path Initial path to build on.
*
* @return {Array} The path from the root node to the node.
*/
function createPathToNode( node: Node, rootNode: HTMLElement, path: number[] ) {
let workingNode: Node|null = node;
@ -39,11 +28,6 @@ function createPathToNode( node: Node, rootNode: HTMLElement, path: number[] ) {
/**
* Gets a node given a path (array of indices) from the given node.
*
* @param {HTMLElement} node Root node to find the wanted node in.
* @param {Array} path Path (indices) to the wanted node.
*
* @return {Object} Object with the found node and the remaining offset (if any).
*/
function getNodeByPath( node: HTMLElement, path: number[] ) {
let workingNode: Node = node;
@ -105,19 +89,22 @@ function remove( node ) {
return node.parentNode.removeChild( node );
}
export function toDom({
value,
prepareEditableTree,
isEditableTree = true,
placeholder,
doc = document,
}: {
value: RichTextValue,
prepareEditableTree: Function,
isEditableTree?: boolean,
placeholder?: string,
doc?: Document,
}) {
export function toDom(
{
value,
prepareEditableTree,
isEditableTree = true,
placeholder,
doc = document,
}: {
value: RichTextValue,
prepareEditableTree: Function,
isEditableTree?: boolean,
placeholder?: string,
doc?: Document,
},
store: FormatTypeStore,
) {
let startPath: number[] = [];
let endPath: number[] = [];
@ -135,8 +122,6 @@ export function toDom({
* Note: The current implementation will return a shared reference, reset on
* each call to `createEmpty`. Therefore, you should not hold a reference to
* the value to operate upon asynchronously, as it may have unexpected results.
*
* @return {Object} RichText tree.
*/
const createEmpty = () => createElement( doc, '' );
@ -162,7 +147,8 @@ export function toDom({
},
isEditableTree,
placeholder,
} );
},
store );
return {
body: tree,
@ -173,32 +159,28 @@ export function toDom({
/**
* Create an `Element` tree from a Rich Text value and applies the difference to
* the `Element` tree contained by `current`.
*
* @param {Object} $1 Named arguments.
* @param {RichTextValue} $1.value Value to apply.
* @param {HTMLElement} $1.current The live root node to apply the element tree to.
* @param {Function} [$1.prepareEditableTree] Function to filter editorable formats.
* @param {boolean} [$1.__unstableDomOnly] Only apply elements, no selection.
* @param {string} [$1.placeholder] Placeholder text.
*/
export function apply( {
value,
current,
prepareEditableTree,
placeholder,
}: {
value: RichTextValue,
current: HTMLElement,
prepareEditableTree: Function,
placeholder: string,
}) {
export function apply(
{
value,
current,
prepareEditableTree,
placeholder,
}: {
value: RichTextValue,
current: HTMLElement,
prepareEditableTree: Function,
placeholder: string,
},
store: FormatTypeStore,
) {
// Construct a new element tree in memory.
const { body, selection } = toDom( {
const { body, selection } = toDom({
value,
prepareEditableTree,
placeholder,
doc: current.ownerDocument,
} );
}, store );
applyValue( body, current );

View file

@ -1,7 +1,7 @@
import { useFormatTypes } from './use-format-types';
import { getActiveFormats } from './get-active-formats';
import { OBJECT_REPLACEMENT_CHARACTER, ZWNBSP } from './special-characters';
import { RichTextValue } from './types';
import { FormatTypeStore } from './use-format-types';
function restoreOnAttributes( attributes: Record<string, any>, isEditableTree: boolean ): Record<string, any> {
if ( isEditableTree ) {
@ -25,40 +25,28 @@ function restoreOnAttributes( attributes: Record<string, any>, isEditableTree: b
/**
* Converts a format object to information that can be used to create an element
* from (type, attributes and object).
*
* @param {Object} $1 Named parameters.
* @param {string} $1.type The format type.
* @param {string} $1.tagName The tag name.
* @param {Object} $1.attributes The format attributes.
* @param {Object} $1.unregisteredAttributes The unregistered format
* attributes.
* @param {boolean} $1.object Whether or not it is an object
* format.
* @param {boolean} $1.boundaryClass Whether or not to apply a boundary
* class.
* @param {boolean} $1.isEditableTree
*
* @return {Object} Information to be used for element creation.
*/
function fromFormat( {
type,
tagName,
attributes,
unregisteredAttributes,
object,
boundaryClass,
isEditableTree,
}: {
type: string,
tagName: string,
attributes: Record<string, any>,
unregisteredAttributes: Record<string, any>
object: boolean,
boundaryClass: boolean,
isEditableTree: boolean,
}) {
const { findFormatTypeByName } = useFormatTypes();
const formatType = findFormatTypeByName(type);
function fromFormat(
{
type,
tagName,
attributes,
unregisteredAttributes,
object,
boundaryClass,
isEditableTree,
}: {
type: string,
tagName?: string,
attributes: Record<string, any>,
unregisteredAttributes: Record<string, any>
object: boolean,
boundaryClass: boolean,
isEditableTree: boolean,
},
store: FormatTypeStore,
) {
const formatType = store.getFormatTypeByName(type);
let elementAttributes: Record<string, any> = {};
@ -118,10 +106,6 @@ function fromFormat( {
/**
* Checks if both arrays of formats up until a certain index are equal.
*
* @param {Array} a Array of formats to compare.
* @param {Array} b Array of formats to compare.
* @param {number} index Index to check until.
*/
function isEqualUntil<T>( a: T[], b: T[], index: number ): boolean {
do {
@ -133,37 +117,40 @@ function isEqualUntil<T>( a: T[], b: T[], index: number ): boolean {
return true;
}
export function toTree( {
value,
preserveWhiteSpace,
createEmpty,
append,
getLastChild,
getParent,
isText,
getText,
remove,
appendText,
onStartIndex,
onEndIndex,
isEditableTree,
placeholder,
}: {
value: RichTextValue,
preserveWhiteSpace?: boolean,
createEmpty: Function,
append: Function,
getLastChild: Function,
getParent: Function,
isText: Function,
getText: Function,
remove: Function,
appendText: Function,
onStartIndex?: Function,
onEndIndex?: Function,
isEditableTree?: boolean,
placeholder?: string,
}) {
export function toTree(
{
value,
preserveWhiteSpace,
createEmpty,
append,
getLastChild,
getParent,
isText,
getText,
remove,
appendText,
onStartIndex,
onEndIndex,
isEditableTree,
placeholder,
}: {
value: RichTextValue,
preserveWhiteSpace?: boolean,
createEmpty: Function,
append: Function,
getLastChild: Function,
getParent: Function,
isText: Function,
getText: Function,
remove: Function,
appendText: Function,
onStartIndex?: Function,
onEndIndex?: Function,
isEditableTree?: boolean,
placeholder?: string,
},
store: FormatTypeStore,
) {
const { formats, replacements, text, start, end } = value;
const formatsLength = formats.length + 1;
const tree = createEmpty();
@ -220,7 +207,7 @@ export function toTree( {
unregisteredAttributes,
boundaryClass,
isEditableTree,
} )
}, store )
);
if ( isText( pointer ) && getText( pointer ).length === 0 ) {
@ -248,7 +235,7 @@ export function toTree( {
continue;
}
const { type, attributes, innerHTML } = replacement;
const formatType = getFormatType( type );
const formatType = store.getFormatTypeByName( type );
if ( ! isEditableTree && type === 'script' ) {
pointer = append(
@ -256,7 +243,7 @@ export function toTree( {
fromFormat( {
type: 'script',
isEditableTree,
} )
}, store )
);
append( pointer, {
html: decodeURIComponent(
@ -271,7 +258,7 @@ export function toTree( {
...replacement,
isEditableTree,
boundaryClass: start === i && end === i + 1,
} )
}, store )
);
if ( innerHTML ) {
@ -286,7 +273,7 @@ export function toTree( {
...replacement,
object: true,
isEditableTree,
} )
}, store )
);
}
// Ensure pointer is text node.

View file

@ -6,17 +6,27 @@ import {
import { RichTextFormatType } from './types';
export const SymFormatTypes = Symbol('Schlechtenburg rich text formats');
export function useFormatTypes() {
export interface FormatTypeStore {
formatTypes: Ref<RichTextFormatType[]>;
registerFormatTypes: (types: RichTextFormatType|RichTextFormatType[]) => void;
unregisterFormatTypes: (types: string|string[]) => void;
findFormatType: (fn: (f:RichTextFormatType) => boolean) => RichTextFormatType|undefined;
getFormatTypeByName: (name: string) => RichTextFormatType|undefined;
getFormatTypeForClassName: (name: string) => RichTextFormatType|undefined;
getFormatTypeForBareElement: (name: string) => RichTextFormatType|undefined;
}
export function useFormatTypes(): FormatTypeStore {
const formatTypes: Ref<RichTextFormatType[]> = inject(SymFormatTypes, ref([]));
const addFormatTypes = (typesToAdd: RichTextFormatType|RichTextFormatType[]) => {
const registerFormatTypes = (typesToAdd: RichTextFormatType|RichTextFormatType[]) => {
formatTypes.value = [
...formatTypes.value,
...(Array.isArray(typesToAdd) ? typesToAdd : [typesToAdd]),
];
};
const removeFormatTypes = (typesToRemove: string|string[]) => {
const unregisterFormatTypes = (typesToRemove: string|string[]) => {
const isArray = Array.isArray(typesToRemove);
formatTypes.value = formatTypes.value.filter(({ name }) => {
@ -29,16 +39,16 @@ export function useFormatTypes() {
};
const findFormatType = (fn: (f:RichTextFormatType) => boolean) => formatTypes.value.find(type => fn(type));
const findFormatTypeByName = (name: string) => formatTypes.value.find(type => type.name === name);
const getFormatTypeByName = (name: string) => formatTypes.value.find(type => type.name === name);
const getFormatTypeForClassName = (name: string) => formatTypes.value.find(type => type.className === name);
const getFormatTypeForBareElement = (name: string) => formatTypes.value.find(type => type.tagName === name);
return {
formatTypes,
addFormatTypes,
removeFormatTypes,
findFormatTypeByName,
registerFormatTypes,
unregisterFormatTypes,
findFormatType,
getFormatTypeByName,
getFormatTypeForClassName,
getFormatTypeForBareElement,
};