Improved typing and migrated to Vitest (#132)

This commit is contained in:
宋铄运 (Alan Song) 2023-07-29 19:24:10 -07:00 committed by GitHub
parent 26053f7d2e
commit 4fe064304e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 3954 additions and 7685 deletions

View File

@ -8,16 +8,16 @@ on:
jobs:
test:
runs-on: ubuntu-latest
# https://playwright.dev/docs/ci#via-containers
container:
image: mcr.microsoft.com/playwright:v1.36.0-jammy
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm ci
- run: npm run lint
- run: npm run test:types
- run: npm run test:ci
- run: npm test
timeout-minutes: 10
env:
SAUCE_USERNAME: quill
SAUCE_ACCESS_KEY: ${{ secrets.SAUCELABS_ACCESS_KEY }}

View File

@ -3,7 +3,7 @@
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "dist/typings/parchment.d.ts",
"mainEntryPointFilePath": "dist/typings/src/parchment.d.ts",
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "<projectFolder>/dist/parchment.d.ts"

View File

@ -1,36 +0,0 @@
module.exports = (config) => {
config.set({
plugins: ['karma-jasmine', 'karma-vite', 'karma-chrome-launcher', 'karma-sauce-launcher'],
frameworks: ['jasmine', 'vite'],
files: [
{
pattern: 'test/unit/*.ts',
type: 'module',
watched: false,
served: false,
},
],
exclude: [],
reporters: ['progress'],
browsers: ['Chrome'],
customLaunchers: {
'saucelabs-chrome': {
base: 'SauceLabs',
browserName: 'Chrome',
platform: 'OS X 10.15',
version: '75',
},
},
sauceLabs: {
testName: 'Parchment Unit Tests',
build: process.env.GITHUB_RUN_ID
? `${process.env.GITHUB_REPOSITORY} run #${process.env.GITHUB_RUN_ID}`
: null,
},
port: process.env.GITHUB_ACTION ? 9876 : 10876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
singleRun: true,
});
};

10259
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,25 +13,21 @@
"src"
],
"devDependencies": {
"@microsoft/api-extractor": "^7.34.6",
"@microsoft/api-extractor": "^7.36.3",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "^5.14.0",
"@typescript-eslint/parser": "^5.14.0",
"del-cli": "^4.0.1",
"eslint": "^8.10.0",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
"@vitest/browser": "^0.33.0",
"del-cli": "^5.0.0",
"eslint": "^8.46.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"jasmine-core": "^4.6.0",
"karma": "^6.4.2",
"karma-babel-preprocessor": "^8.0.2",
"karma-chrome-launcher": "^3.2.0",
"karma-jasmine": "^5.1.0",
"karma-sauce-launcher": "^4.3.6",
"karma-vite": "^1.0.4",
"playwright": "^1.36.2",
"prettier": "^2.5.1",
"tsd": "^0.28.1",
"typescript": "^4.9.5",
"vite": "^4.2.1"
"typescript": "^5.1.6",
"vite": "^4.4.7",
"vitest": "^0.33.0"
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
@ -66,13 +62,11 @@
},
"scripts": {
"build": "vite build",
"build:types": "tsc --emitDeclarationOnly && api-extractor run && rm -rf dist/typings",
"build:types": "tsc --emitDeclarationOnly && api-extractor run && del-cli dist/typings",
"lint": "eslint 'src/**/*.ts'",
"prepare": "npm run build && npm run build:types",
"test": "karma start",
"test:types": "npm run build && npm run build:types && tsd",
"test:server": "karma start --no-single-run",
"test:ci": "karma start --browsers saucelabs-chrome --reporters dots,saucelabs"
"test": "vitest",
"test:types": "npm run build && npm run build:types && tsd"
},
"bugs": {
"url": "https://github.com/quilljs/parchment/issues"

View File

@ -10,18 +10,14 @@ export default class Attributor {
return Array.from(node.attributes).map((item: Attr) => item.name);
}
public attrName: string;
public keyName: string;
public scope: Scope;
public whitelist: string[] | undefined;
constructor(
attrName: string,
keyName: string,
public readonly attrName: string,
public readonly keyName: string,
options: AttributorOptions = {},
) {
this.attrName = attrName;
this.keyName = keyName;
const attributeBit = Scope.TYPE & Scope.ATTRIBUTE;
this.scope =
options.scope != null

View File

@ -1,4 +1,4 @@
import { Formattable } from '../blot/abstract/blot';
import type { Formattable } from '../blot/abstract/blot';
import Registry from '../registry';
import Scope from '../scope';
import Attributor from './attributor';

View File

@ -1,6 +1,6 @@
import Attributor from '../../attributor/attributor';
import LinkedList from '../../collection/linked-list';
import LinkedNode from '../../collection/linked-node';
import type LinkedList from '../../collection/linked-list';
import type LinkedNode from '../../collection/linked-node';
import type { RegistryDefinition } from '../../registry';
import Scope from '../../scope';
export interface BlotConstructor {
@ -38,7 +38,7 @@ export interface Blot extends LinkedNode {
replaceWith(name: string, value: any): Blot;
replaceWith(replacement: Blot): Blot;
split(index: number, force?: boolean): Blot | null;
wrap(name: string, value: any): Parent;
wrap(name: string, value?: any): Parent;
wrap(wrapper: Parent): Parent;
deleteAt(index: number, length: number): void;
@ -72,10 +72,7 @@ export interface Parent extends Blot {
export interface Root extends Parent {
create(input: Node | string | Scope, value?: any): Blot;
find(node: Node | null, bubble?: boolean): Blot | null;
query(
query: string | Node | Scope,
scope?: Scope,
): Attributor | BlotConstructor | null;
query(query: string | Node | Scope, scope?: Scope): RegistryDefinition | null;
}
export interface Formattable extends Blot {

View File

@ -1,5 +1,5 @@
import Scope from '../../scope';
import { Leaf } from './blot';
import type { Leaf } from './blot';
import ShadowBlot from './shadow';
class LeafBlot extends ShadowBlot implements Leaf {

View File

@ -1,32 +1,30 @@
import LinkedList from '../../collection/linked-list';
import ParchmentError from '../../error';
import Scope from '../../scope';
import { Blot, BlotConstructor, Parent, Root } from './blot';
import type { Blot, BlotConstructor, Parent, Root } from './blot';
import ShadowBlot from './shadow';
function makeAttachedBlot(node: Node, scroll: Root): Blot {
let blot = scroll.find(node);
if (blot == null) {
try {
blot = scroll.create(node);
} catch (e) {
blot = scroll.create(Scope.INLINE) as Blot;
Array.from(node.childNodes).forEach((child: Node) => {
// @ts-expect-error
blot.domNode.appendChild(child);
});
if (node.parentNode) {
node.parentNode.replaceChild(blot.domNode, node);
}
blot.attach();
const found = scroll.find(node);
if (found) return found;
try {
return scroll.create(node);
} catch (e) {
const blot = scroll.create(Scope.INLINE);
Array.from(node.childNodes).forEach((child: Node) => {
blot.domNode.appendChild(child);
});
if (node.parentNode) {
node.parentNode.replaceChild(blot.domNode, node);
}
blot.attach();
return blot;
}
return blot as Blot;
}
class ParentBlot extends ShadowBlot implements Parent {
public static allowedChildren: BlotConstructor[] | null;
public static defaultChild: BlotConstructor | null;
public static defaultChild?: BlotConstructor;
public static uiClass = '';
public children!: LinkedList<Blot>;
@ -241,7 +239,7 @@ class ParentBlot extends ShadowBlot implements Parent {
});
}
public optimize(context: { [key: string]: any }): void {
public optimize(context?: { [key: string]: any }): void {
super.optimize(context);
this.enforceAllowedChildren();
if (this.uiNode != null && this.uiNode !== this.domNode.firstChild) {

View File

@ -1,7 +1,7 @@
import ParchmentError from '../../error';
import Registry from '../../registry';
import Scope from '../../scope';
import { Blot, BlotConstructor, Formattable, Parent, Root } from './blot';
import type { Blot, BlotConstructor, Formattable, Parent, Root } from './blot';
class ShadowBlot implements Blot {
public static blotName = 'abstract';
@ -10,21 +10,24 @@ class ShadowBlot implements Blot {
public static scope: Scope;
public static tagName: string | string[];
public static create(value: any): Node {
public static create(rawValue?: unknown): Node {
if (this.tagName == null) {
throw new ParchmentError('Blot definition missing tagName');
}
let node;
let node: HTMLElement;
let value: string | number | undefined;
if (Array.isArray(this.tagName)) {
if (typeof value === 'string') {
value = value.toUpperCase();
if (typeof rawValue === 'string') {
value = rawValue.toUpperCase();
if (parseInt(value, 10).toString() === value) {
value = parseInt(value, 10);
}
} else if (typeof rawValue === 'number') {
value = rawValue;
}
if (typeof value === 'number') {
node = document.createElement(this.tagName[value - 1]);
} else if (this.tagName.indexOf(value) > -1) {
} else if (value && this.tagName.indexOf(value) > -1) {
node = document.createElement(value);
} else {
node = document.createElement(this.tagName[0]);
@ -120,7 +123,7 @@ class ShadowBlot implements Blot {
return this.parent.children.offset(this) + this.parent.offset(root);
}
public optimize(_context: { [key: string]: any }): void {
public optimize(_context?: { [key: string]: any }): void {
if (
this.statics.requiredContainer &&
!(this.parent instanceof this.statics.requiredContainer)

View File

@ -1,7 +1,7 @@
import Attributor from '../attributor/attributor';
import AttributorStore from '../attributor/store';
import Scope from '../scope';
import { Blot, BlotConstructor, Formattable, Root } from './abstract/blot';
import type { Blot, BlotConstructor, Formattable, Root } from './abstract/blot';
import LeafBlot from './abstract/leaf';
import ParentBlot from './abstract/parent';
import InlineBlot from './inline';
@ -16,6 +16,10 @@ class BlockBlot extends ParentBlot implements Formattable {
LeafBlot,
];
static create(value?: unknown) {
return super.create(value) as HTMLElement;
}
public static formats(domNode: HTMLElement, scroll: Root): any {
const match = scroll.query(BlockBlot.blotName);
if (

View File

@ -1,4 +1,4 @@
import { Formattable, Root } from './abstract/blot';
import type { Formattable, Root } from './abstract/blot';
import LeafBlot from './abstract/leaf';
class EmbedBlot extends LeafBlot implements Formattable {

View File

@ -1,7 +1,7 @@
import Attributor from '../attributor/attributor';
import AttributorStore from '../attributor/store';
import Scope from '../scope';
import {
import type {
Blot,
BlotConstructor,
Formattable,
@ -12,12 +12,14 @@ import LeafBlot from './abstract/leaf';
import ParentBlot from './abstract/parent';
// Shallow object comparison
function isEqual(obj1: object, obj2: object): boolean {
function isEqual(
obj1: Record<string, unknown>,
obj2: Record<string, unknown>,
): boolean {
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
return false;
}
for (const prop in obj1) {
// @ts-expect-error
if (obj1[prop] !== obj2[prop]) {
return false;
}
@ -31,6 +33,10 @@ class InlineBlot extends ParentBlot implements Formattable {
public static scope = Scope.INLINE_BLOT;
public static tagName: string | string[] = 'SPAN';
static create(value?: unknown) {
return super.create(value) as HTMLElement;
}
public static formats(domNode: HTMLElement, scroll: Root): any {
const match = scroll.query(InlineBlot.blotName);
if (

View File

@ -1,7 +1,6 @@
import Attributor from '../attributor/attributor';
import Registry from '../registry';
import Registry, { type RegistryDefinition } from '../registry';
import Scope from '../scope';
import { Blot, BlotConstructor, Root } from './abstract/blot';
import type { Blot, BlotConstructor, Root } from './abstract/blot';
import ContainerBlot from './abstract/container';
import ParentBlot from './abstract/parent';
import BlockBlot from './block';
@ -23,13 +22,11 @@ class ScrollBlot extends ParentBlot implements Root {
public static scope = Scope.BLOCK_BLOT;
public static tagName = 'DIV';
public registry: Registry;
public observer: MutationObserver;
constructor(registry: Registry, node: HTMLDivElement) {
// @ts-expect-error
constructor(public registry: Registry, node: HTMLDivElement) {
// @ts-expect-error scroll is the root with no parent
super(null, node);
this.registry = registry;
this.scroll = this;
this.build();
this.observer = new MutationObserver((mutations: MutationRecord[]) => {
@ -57,11 +54,11 @@ class ScrollBlot extends ParentBlot implements Root {
public query(
query: string | Node | Scope,
scope: Scope = Scope.ANY,
): Attributor | BlotConstructor | null {
): RegistryDefinition | null {
return this.registry.query(query, scope);
}
public register(...definitions: any[]): any {
public register(...definitions: RegistryDefinition[]) {
return this.registry.register(...definitions);
}
@ -103,7 +100,7 @@ class ScrollBlot extends ParentBlot implements Root {
super.insertAt(index, value, def);
}
public optimize(context: { [key: string]: any }): void;
public optimize(context?: { [key: string]: any }): void;
public optimize(
mutations: MutationRecord[],
context: { [key: string]: any },

View File

@ -1,9 +1,9 @@
import Scope from '../scope';
import { Blot, Leaf, Root } from './abstract/blot';
import type { Blot, Leaf, Root } from './abstract/blot';
import LeafBlot from './abstract/leaf';
class TextBlot extends LeafBlot implements Leaf {
public static blotName = 'text';
public static readonly blotName = 'text';
public static scope = Scope.INLINE_BLOT;
public static create(value: string): Text {

View File

@ -1,4 +1,4 @@
import LinkedNode from './linked-node';
import type LinkedNode from './linked-node';
class LinkedList<T extends LinkedNode> {
public head: T | null;

View File

@ -5,4 +5,4 @@ interface LinkedNode {
length(): number;
}
export default LinkedNode;
export type { LinkedNode as default };

View File

@ -33,7 +33,7 @@ export {
Scope,
};
export type { RegistryInterface } from './registry';
export type { RegistryInterface, RegistryDefinition } from './registry';
export type { default as ShadowBlot } from './blot/abstract/shadow';
export type { default as LinkedList } from './collection/linked-list';
export type { default as LinkedNode } from './collection/linked-node';

View File

@ -1,21 +1,24 @@
import Attributor from './attributor/attributor';
import { Blot, BlotConstructor, Root } from './blot/abstract/blot';
import {
type Blot,
type BlotConstructor,
type Root,
} from './blot/abstract/blot';
import ParchmentError from './error';
import Scope from './scope';
export type RegistryDefinition = Attributor | BlotConstructor;
export interface RegistryInterface {
create(sroll: Root, input: Node | string | Scope, value?: any): Blot;
query(
query: string | Node | Scope,
scope: Scope,
): Attributor | BlotConstructor | null;
query(query: string | Node | Scope, scope: Scope): RegistryDefinition | null;
register(...definitions: any[]): any;
}
export default class Registry implements RegistryInterface {
public static blots = new WeakMap<Node, Blot>();
public static find(node: Node | null, bubble = false): Blot | null {
public static find(node?: Node | null, bubble = false): Blot | null {
if (node == null) {
return null;
}
@ -41,7 +44,7 @@ export default class Registry implements RegistryInterface {
private attributes: { [key: string]: Attributor } = {};
private classes: { [key: string]: BlotConstructor } = {};
private tags: { [key: string]: BlotConstructor } = {};
private types: { [key: string]: Attributor | BlotConstructor } = {};
private types: { [key: string]: RegistryDefinition } = {};
public create(scroll: Root, input: Node | string | Scope, value?: any): Blot {
const match = this.query(input);
@ -67,7 +70,7 @@ export default class Registry implements RegistryInterface {
public query(
query: string | Node | Scope,
scope: Scope = Scope.ANY,
): Attributor | BlotConstructor | null {
): RegistryDefinition | null {
let match;
if (typeof query === 'string') {
match = this.types[query] || this.attributes[query];
@ -94,53 +97,59 @@ export default class Registry implements RegistryInterface {
if (match == null) {
return null;
}
// @ts-expect-error
if (scope & Scope.LEVEL & match.scope && scope & Scope.TYPE & match.scope) {
if (
'scope' in match &&
scope & Scope.LEVEL & match.scope &&
scope & Scope.TYPE & match.scope
) {
return match;
}
return null;
}
public register(...definitions: any[]): any {
if (definitions.length > 1) {
return definitions.map((d) => {
return this.register(d);
});
}
const definition = definitions[0];
if (
typeof definition.blotName !== 'string' &&
typeof definition.attrName !== 'string'
) {
throw new ParchmentError('Invalid definition');
} else if (definition.blotName === 'abstract') {
throw new ParchmentError('Cannot register abstract class');
}
this.types[definition.blotName || definition.attrName] = definition;
if (typeof definition.keyName === 'string') {
this.attributes[definition.keyName] = definition;
} else {
if (definition.className != null) {
this.classes[definition.className] = definition;
public register(...definitions: RegistryDefinition[]): RegistryDefinition[] {
return definitions.map((definition) => {
const isBlot = 'blotName' in definition;
const isAttr = 'attrName' in definition;
if (!isBlot && !isAttr) {
throw new ParchmentError('Invalid definition');
} else if (isBlot && definition.blotName === 'abstract') {
throw new ParchmentError('Cannot register abstract class');
}
if (definition.tagName != null) {
if (Array.isArray(definition.tagName)) {
definition.tagName = definition.tagName.map((tagName: string) => {
return tagName.toUpperCase();
});
} else {
definition.tagName = definition.tagName.toUpperCase();
const key = isBlot
? definition.blotName
: isAttr
? definition.attrName
: (undefined as never); // already handled by above checks
this.types[key] = definition;
if (isAttr) {
if (typeof definition.keyName === 'string') {
this.attributes[definition.keyName] = definition;
}
const tagNames = Array.isArray(definition.tagName)
? definition.tagName
: [definition.tagName];
tagNames.forEach((tag: string) => {
if (this.tags[tag] == null || definition.className == null) {
this.tags[tag] = definition;
} else if (isBlot) {
if (definition.className) {
this.classes[definition.className] = definition;
}
if (definition.tagName) {
if (Array.isArray(definition.tagName)) {
definition.tagName = definition.tagName.map((tagName: string) => {
return tagName.toUpperCase();
});
} else {
definition.tagName = definition.tagName.toUpperCase();
}
});
const tagNames = Array.isArray(definition.tagName)
? definition.tagName
: [definition.tagName];
tagNames.forEach((tag: string) => {
if (this.tags[tag] == null || definition.className == null) {
this.tags[tag] = definition;
}
});
}
}
}
return definition;
return definition;
});
}
}

View File

@ -1,5 +1,5 @@
import { expectType } from 'tsd';
import { Blot, EmbedBlot, Registry, ScrollBlot, ParentBlot } from '..';
import { type Blot, EmbedBlot, Registry, ScrollBlot, ParentBlot } from '..';
const registry = new Registry();
const root = document.createElement('div');

View File

@ -1,5 +1,9 @@
import BlockBlot from '../../src/blot/block';
export class HeaderBlot extends BlockBlot {}
HeaderBlot.blotName = 'header';
HeaderBlot.tagName = ['h1', 'h2'];
export class HeaderBlot extends BlockBlot {
static readonly blotName = 'header';
static tagName = ['h1', 'h2'];
static create(value?: number | string) {
return super.create(value) as HTMLHeadingElement;
}
}

View File

@ -1,5 +1,6 @@
import EmbedBlot from '../../src/blot/embed';
export class BreakBlot extends EmbedBlot {}
BreakBlot.blotName = 'break';
BreakBlot.tagName = 'br';
export class BreakBlot extends EmbedBlot {
static readonly blotName = 'break';
static tagName = 'br';
}

View File

@ -2,26 +2,29 @@ import EmbedBlot from '../../src/blot/embed';
import Scope from '../../src/scope';
export class ImageBlot extends EmbedBlot {
static create(value) {
let node = super.create(value);
declare domNode: HTMLImageElement;
static readonly blotName = 'image';
static tagName = 'IMG';
static create(value: string) {
let node = super.create(value) as HTMLElement;
if (typeof value === 'string') {
node.setAttribute('src', value);
}
return node;
}
static value(domNode) {
static value(domNode: HTMLImageElement) {
return domNode.getAttribute('src');
}
static formats(domNode) {
static formats(domNode: HTMLImageElement) {
if (domNode.hasAttribute('alt')) {
return { alt: domNode.getAttribute('alt') };
}
return undefined;
}
format(name, value) {
format(name: string, value: string) {
if (name === 'alt') {
this.domNode.setAttribute(name, value);
} else {
@ -29,35 +32,37 @@ export class ImageBlot extends EmbedBlot {
}
}
}
ImageBlot.blotName = 'image';
ImageBlot.tagName = 'IMG';
export class VideoBlot extends EmbedBlot {
static create(value) {
let node = super.create(value);
declare domNode: HTMLVideoElement;
static scope = Scope.BLOCK_BLOT;
static readonly blotName = 'video';
static tagName = 'VIDEO';
static create(value: string) {
let node = super.create(value) as HTMLVideoElement;
if (typeof value === 'string') {
node.setAttribute('src', value);
}
return node;
}
static formats(domNode) {
let formats = {};
if (domNode.hasAttribute('height'))
formats['height'] = domNode.getAttribute('height');
if (domNode.hasAttribute('width'))
formats['width'] = domNode.getAttribute('width');
static formats(domNode: HTMLVideoElement) {
let formats: Partial<{ height: string; width: string }> = {};
const height = domNode.getAttribute('height');
const width = domNode.getAttribute('width');
height && (formats.height = height);
width && (formats.width = width);
return formats;
}
static value(domNode) {
static value(domNode: HTMLVideoElement) {
return domNode.getAttribute('src');
}
format(name, value) {
format(name: string, value: unknown) {
if (name === 'height' || name === 'width') {
if (value) {
this.domNode.setAttribute(name, value);
this.domNode.setAttribute(name, value.toString());
} else {
this.domNode.removeAttribute(name);
}
@ -66,6 +71,3 @@ export class VideoBlot extends EmbedBlot {
}
}
}
VideoBlot.blotName = 'video';
VideoBlot.scope = Scope.BLOCK_BLOT;
VideoBlot.tagName = 'VIDEO';

View File

@ -1,17 +1,21 @@
import InlineBlot from '../../src/blot/inline';
export class AuthorBlot extends InlineBlot {}
AuthorBlot.blotName = 'author';
AuthorBlot.className = 'author-blot';
export class AuthorBlot extends InlineBlot {
static readonly blotName = 'author';
static className = 'author-blot';
}
export class BoldBlot extends InlineBlot {}
BoldBlot.blotName = 'bold';
BoldBlot.tagName = 'STRONG';
export class BoldBlot extends InlineBlot {
static readonly blotName = 'bold';
static tagName = 'strong';
}
export class ItalicBlot extends InlineBlot {}
ItalicBlot.blotName = 'italic';
ItalicBlot.tagName = 'em';
export class ItalicBlot extends InlineBlot {
static readonly blotName = 'italic';
static tagName = 'em';
}
export class ScriptBlot extends InlineBlot {}
ScriptBlot.blotName = 'script';
ScriptBlot.tagName = ['sup', 'sub'];
export class ScriptBlot extends InlineBlot {
static readonly blotName = 'script';
static tagName = ['sup', 'sub'];
}

View File

@ -1,13 +1,16 @@
import ContainerBlot from '../../src/blot/abstract/container';
import BlockBlot from '../../src/blot/block';
export class ListItem extends BlockBlot {}
ListItem.blotName = 'list';
ListItem.tagName = 'LI';
export class ListItem extends BlockBlot {
static readonly blotName = 'list';
static tagName = 'LI';
}
export class ListContainer extends ContainerBlot {}
ListContainer.blotName = 'list-container';
ListContainer.tagName = 'OL';
export class ListContainer extends ContainerBlot {
static readonly blotName = 'list-container';
static tagName = 'OL';
static allowedChildren = [ListItem];
}
ListContainer.allowedChildren = [ListItem];
// Can only define outside of ListItem class due to used-before-declaration error
ListItem.requiredContainer = ListContainer;

View File

@ -1,9 +1,13 @@
import Registry from '../src/registry';
import { beforeEach } from 'vitest';
import ScrollBlot from '../src/blot/scroll';
import BlockBlot from '../src/blot/block';
import InlineBlot from '../src/blot/inline';
import TextBlot from '../src/blot/text';
import {
Registry,
ScrollBlot,
BlockBlot,
InlineBlot,
TextBlot,
type BlotConstructor,
} from '../src/parchment';
import {
AuthorBlot,
BoldBlot,
@ -19,7 +23,7 @@ import { BreakBlot } from './registry/break';
const getTestRegistry = () => {
const reg = new Registry();
reg.register(ScrollBlot);
reg.register(ScrollBlot as unknown as BlotConstructor);
reg.register(BlockBlot);
reg.register(InlineBlot);
reg.register(TextBlot);
@ -41,7 +45,7 @@ type TestContext = {
};
export const setupContextBeforeEach = () => {
const ctx: TestContext = {} as TestContext;
const ctx = {} as TestContext;
beforeEach(() => {
const container = document.createElement('div');
const registry = getTestRegistry();

View File

@ -1,23 +1,34 @@
import { describe, it, expect } from 'vitest';
import type {
Attributor,
BlockBlot,
Formattable,
InlineBlot,
} from '../../src/parchment';
import type { HeaderBlot } from '../registry/block';
import type { BoldBlot } from '../registry/inline';
import { setupContextBeforeEach } from '../setup';
describe('Attributor', function () {
const ctx = setupContextBeforeEach();
it('build', function () {
let blot = ctx.scroll.create('inline');
let blot = ctx.scroll.create('inline') as InlineBlot;
blot.domNode.style.color = 'red';
blot.domNode.style.fontSize = '24px';
blot.domNode.id = 'blot-test';
blot.domNode.classList.add('indent-2');
blot.attributes.build();
expect(Object.keys(blot.attributes.attributes).sort()).toEqual(
// Use bracket notation to access private fields as escape hatch
// https://github.com/microsoft/TypeScript/issues/19335
blot['attributes'].build();
expect(Object.keys(blot['attributes']['attributes']).sort()).toEqual(
['color', 'size', 'id', 'indent'].sort(),
);
});
it('add to inline', function () {
let container = ctx.scroll.create('block');
let boldBlot = ctx.scroll.create('bold');
let container = ctx.scroll.create('block') as BlockBlot;
let boldBlot = ctx.scroll.create('bold') as BoldBlot;
container.appendChild(boldBlot);
boldBlot.format('id', 'test-add');
expect(boldBlot.domNode.id).toEqual('test-add');
@ -35,15 +46,15 @@ describe('Attributor', function () {
});
it('add to text', function () {
let container = ctx.scroll.create('block');
let container = ctx.scroll.create('block') as BlockBlot;
let textBlot = ctx.scroll.create('text', 'Test');
container.appendChild(textBlot);
textBlot.formatAt(0, 4, 'color', 'red');
expect(textBlot.domNode.parentNode.style.color).toEqual('red');
expect(textBlot.domNode.parentElement?.style.color).toEqual('red');
});
it('add existing style', function () {
let boldBlot = ctx.scroll.create('bold');
let boldBlot = ctx.scroll.create('bold') as BoldBlot;
boldBlot.format('color', 'red');
expect(boldBlot.domNode.style.color).toEqual('red');
let original = boldBlot.domNode.outerHTML;
@ -54,7 +65,7 @@ describe('Attributor', function () {
});
it('replace existing class', function () {
let blockBlot = ctx.scroll.create('block');
let blockBlot = ctx.scroll.create('block') as BlockBlot;
blockBlot.format('indent', 2);
expect(blockBlot.domNode.classList.contains('indent-2')).toBe(true);
blockBlot.format('indent', 3);
@ -63,19 +74,19 @@ describe('Attributor', function () {
});
it('add whitelist style', function () {
let blockBlot = ctx.scroll.create('block');
let blockBlot = ctx.scroll.create('block') as BlockBlot;
blockBlot.format('align', 'right');
expect(blockBlot.domNode.style.textAlign).toBe('right');
});
it('add non-whitelisted style', function () {
let blockBlot = ctx.scroll.create('block');
let blockBlot = ctx.scroll.create('block') as BlockBlot;
blockBlot.format('align', 'justify');
expect(blockBlot.domNode.style.textAlign).toBeFalsy();
});
it('unwrap', function () {
let container = ctx.scroll.create('block');
let container = ctx.scroll.create('block') as BlockBlot;
let node = document.createElement('strong');
node.style.color = 'red';
node.innerHTML = '<em>01</em>23';
@ -88,7 +99,7 @@ describe('Attributor', function () {
});
it('remove', function () {
let container = ctx.scroll.create('block');
let container = ctx.scroll.create('block') as BlockBlot;
let node = document.createElement('strong');
node.innerHTML = 'Bold';
node.style.color = 'red';
@ -99,7 +110,7 @@ describe('Attributor', function () {
container.appendChild(boldBlot);
container.formatAt(1, 2, 'color', false);
expect(container.children.length).toEqual(3);
let targetNode = boldBlot.next.domNode;
let targetNode = boldBlot.next?.domNode as HTMLElement;
expect(targetNode.style.color).toEqual('');
container.formatAt(1, 2, 'size', false);
expect(targetNode.style.fontSize).toEqual('');
@ -111,17 +122,17 @@ describe('Attributor', function () {
});
it('remove nonexistent', function () {
let container = ctx.scroll.create('block');
let container = ctx.scroll.create('block') as BlockBlot;
let node = document.createElement('strong');
node.innerHTML = 'Bold';
let boldBlot = ctx.scroll.create(node);
let boldBlot = ctx.scroll.create(node) as BoldBlot;
container.appendChild(boldBlot);
boldBlot.format('color', false);
expect(container.domNode.innerHTML).toEqual('<strong>Bold</strong>');
});
it('keep class attribute after removal', function () {
let boldBlot = ctx.scroll.create('bold');
let boldBlot = ctx.scroll.create('bold') as BoldBlot;
boldBlot.domNode.classList.add('blot');
boldBlot.format('indent', 2);
boldBlot.format('indent', false);
@ -129,7 +140,7 @@ describe('Attributor', function () {
});
it('move attribute', function () {
let container = ctx.scroll.create('block');
let container = ctx.scroll.create('block') as BlockBlot;
let node = document.createElement('strong');
node.innerHTML = 'Bold';
node.style.color = 'red';
@ -137,41 +148,41 @@ describe('Attributor', function () {
container.appendChild(boldBlot);
container.formatAt(1, 2, 'bold', false);
expect(container.children.length).toEqual(3);
expect(boldBlot.next.statics.blotName).toEqual('inline');
expect(boldBlot.next.formats().color).toEqual('red');
expect(boldBlot.next?.statics.blotName).toEqual('inline');
expect((boldBlot.next as Formattable)?.formats().color).toEqual('red');
});
it('wrap with inline', function () {
let container = ctx.scroll.create('block');
let container = ctx.scroll.create('block') as BlockBlot;
let node = document.createElement('strong');
node.style.color = 'red';
let boldBlot = ctx.scroll.create(node);
container.appendChild(boldBlot);
boldBlot.wrap('italic');
expect(node.style.color).toBeFalsy();
expect(node.parentNode.style.color).toBe('red');
expect(node.parentElement?.style.color).toBe('red');
});
it('wrap with block', function () {
let container = ctx.scroll.create('block');
let container = ctx.scroll.create('block') as BlockBlot;
let node = document.createElement('strong');
node.style.color = 'red';
let boldBlot = ctx.scroll.create(node);
let boldBlot = ctx.scroll.create(node) as BoldBlot;
container.appendChild(boldBlot);
boldBlot.wrap('block');
expect(node.style.color).toBe('red');
expect(node.parentNode.style.color).toBeFalsy();
expect(node.parentElement?.style.color).toBeFalsy();
});
it('add to block', function () {
let container = ctx.scroll.create('block');
let block = ctx.scroll.create('header', 'h1');
let container = ctx.scroll.create('block') as BlockBlot;
let block = ctx.scroll.create('header', 'h1') as HeaderBlot;
container.appendChild(block);
block.format('align', 'right');
expect(container.domNode.innerHTML).toBe(
'<h1 style="text-align: right;"></h1>',
);
expect(container.children.head.formats()).toEqual({
expect((container.children.head as Formattable)?.formats()).toEqual({
header: 'h1',
align: 'right',
});
@ -179,20 +190,17 @@ describe('Attributor', function () {
it('missing class value', function () {
let block = ctx.scroll.create('block');
let indentAttributor = ctx.scroll.query('indent');
expect(indentAttributor.value(block.domNode)).toBeFalsy();
let indentAttributor = ctx.scroll.query('indent') as Attributor;
expect(indentAttributor.value(block.domNode as HTMLElement)).toBeFalsy();
});
it('removes quotes from attribute value when checking if canAdd', function () {
let bold = ctx.scroll.create('bold');
let familyAttributor = ctx.scroll.query('family');
expect(familyAttributor.canAdd(bold.domNode, 'Arial')).toBeTruthy();
expect(
familyAttributor.canAdd(bold.domNode, '"Times New Roman"'),
).toBeTruthy();
expect(familyAttributor.canAdd(bold.domNode, 'monotype')).toBeFalsy();
expect(
familyAttributor.canAdd(bold.domNode, '"Lucida Grande"'),
).toBeFalsy();
let familyAttributor = ctx.scroll.query('family') as Attributor;
const domNode = bold.domNode as HTMLElement;
expect(familyAttributor.canAdd(domNode, 'Arial')).toBeTruthy();
expect(familyAttributor.canAdd(domNode, '"Times New Roman"')).toBeTruthy();
expect(familyAttributor.canAdd(domNode, 'monotype')).toBeFalsy();
expect(familyAttributor.canAdd(domNode, '"Lucida Grande"')).toBeFalsy();
});
});

View File

@ -1,3 +1,6 @@
import { describe, it, expect } from 'vitest';
import type { BlockBlot } from '../../src/parchment';
import type { HeaderBlot } from '../registry/block';
import { setupContextBeforeEach } from '../setup';
describe('Block', function () {
@ -5,38 +8,41 @@ describe('Block', function () {
describe('format', function () {
it('add', function () {
let block = ctx.scroll.create('block');
let block = ctx.scroll.create('block') as BlockBlot;
ctx.scroll.appendChild(block);
block.format('header', 'h1');
expect(ctx.scroll.domNode.innerHTML).toBe('<h1></h1>');
expect(ctx.scroll.children.head.statics.blotName).toBe('header');
expect(ctx.scroll.children.head.formats()).toEqual({ header: 'h1' });
const childrenHead = ctx.scroll.children.head as HeaderBlot;
expect(childrenHead.statics.blotName).toBe('header');
expect(childrenHead.formats()).toEqual({ header: 'h1' });
});
it('remove', function () {
let block = ctx.scroll.create('header', 'h1');
let block = ctx.scroll.create('header', 'h1') as HeaderBlot;
ctx.scroll.appendChild(block);
block.format('header', false);
expect(ctx.scroll.domNode.innerHTML).toBe('<p></p>');
expect(ctx.scroll.children.head.statics.blotName).toBe('block');
expect(ctx.scroll.children.head.formats()).toEqual({});
const childrenHead = ctx.scroll.children.head as BlockBlot;
expect(childrenHead.statics.blotName).toBe('block');
expect(childrenHead.formats()).toEqual({});
});
it('change', function () {
let block = ctx.scroll.create('block');
let block = ctx.scroll.create('block') as BlockBlot;
let text = ctx.scroll.create('text', 'Test');
block.appendChild(text);
ctx.scroll.appendChild(block);
block.format('header', 'h2');
expect(ctx.scroll.domNode.innerHTML).toBe('<h2>Test</h2>');
expect(ctx.scroll.children.head.statics.blotName).toBe('header');
expect(ctx.scroll.children.head.formats()).toEqual({ header: 'h2' });
expect(ctx.scroll.children.head.children.length).toBe(1);
expect(ctx.scroll.children.head.children.head).toBe(text);
const childrenHead = ctx.scroll.children.head as HeaderBlot;
expect(childrenHead.statics.blotName).toBe('header');
expect(childrenHead.formats()).toEqual({ header: 'h2' });
expect(childrenHead.children.length).toBe(1);
expect(childrenHead.children.head).toBe(text);
});
it('split', function () {
let block = ctx.scroll.create('block');
let block = ctx.scroll.create('block') as BlockBlot;
let text = ctx.scroll.create('text', 'Test');
block.appendChild(text);
ctx.scroll.appendChild(block);
@ -46,16 +52,17 @@ describe('Block', function () {
`<p>Te</p><video src="${src}"></video><p>st</p>`,
);
expect(ctx.scroll.children.length).toBe(3);
expect(ctx.scroll.children.head.next.statics.blotName).toBe('video');
expect(ctx.scroll.children.head?.next?.statics.blotName).toBe('video');
});
it('ignore inline', function () {
let block = ctx.scroll.create('header', 1);
let block = ctx.scroll.create('header', 1) as HeaderBlot;
ctx.scroll.appendChild(block);
block.format('bold', true);
expect(ctx.scroll.domNode.innerHTML).toBe('<h1></h1>');
expect(ctx.scroll.children.head.statics.blotName).toBe('header');
expect(ctx.scroll.children.head.formats()).toEqual({ header: 'h1' });
const childrenHead = ctx.scroll.children.head as HeaderBlot;
expect(childrenHead.statics.blotName).toBe('header');
expect(childrenHead.formats()).toEqual({ header: 'h1' });
});
});
});

View File

@ -1,4 +1,7 @@
import { describe, it, expect } from 'vitest';
import type { BlockBlot, Parent } from '../../src/parchment';
import Registry from '../../src/registry';
import type { ItalicBlot } from '../registry/inline';
import { setupContextBeforeEach } from '../setup';
describe('Blot', function () {
@ -7,10 +10,10 @@ describe('Blot', function () {
it('offset()', function () {
let blockNode = document.createElement('p');
blockNode.innerHTML = '<span>01</span><em>23<strong>45</strong></em>';
let blockBlot = ctx.scroll.create(blockNode);
let boldBlot = blockBlot.children.tail.children.tail;
expect(boldBlot.offset()).toEqual(2);
expect(boldBlot.offset(blockBlot)).toEqual(4);
let blockBlot = ctx.scroll.create(blockNode) as BlockBlot;
let boldBlot = (blockBlot.children.tail as Parent)?.children.tail;
expect(boldBlot?.offset()).toEqual(2);
expect(boldBlot?.offset(blockBlot)).toEqual(4);
});
it('detach()', function () {
@ -21,7 +24,7 @@ describe('Blot', function () {
});
it('remove()', function () {
let blot = ctx.scroll.create('block');
let blot = ctx.scroll.create('block') as BlockBlot;
let text = ctx.scroll.create('text', 'Test');
blot.appendChild(text);
expect(blot.children.head).toBe(text);
@ -32,7 +35,7 @@ describe('Blot', function () {
});
it('wrap()', function () {
let parent = ctx.scroll.create('block');
let parent = ctx.scroll.create('block') as BlockBlot;
let head = ctx.scroll.create('bold');
let text = ctx.scroll.create('text', 'Test');
let tail = ctx.scroll.create('bold');
@ -47,14 +50,14 @@ describe('Blot', function () {
'<strong></strong><em>Test</em><strong></strong>',
);
expect(parent.children.head).toEqual(head);
expect(parent.children.head.next).toEqual(wrapper);
expect(parent.children.head?.next).toEqual(wrapper);
expect(parent.children.tail).toEqual(tail);
});
it('wrap() with blot', function () {
let parent = ctx.scroll.create('block');
let parent = ctx.scroll.create('block') as BlockBlot;
let text = ctx.scroll.create('text', 'Test');
let italic = ctx.scroll.create('italic');
let italic = ctx.scroll.create('italic') as ItalicBlot;
parent.appendChild(text);
text.wrap(italic);
expect(parent.domNode.innerHTML).toEqual('<em>Test</em>');

View File

@ -1,3 +1,4 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { setupContextBeforeEach } from '../setup';
describe('Container', function () {
@ -10,8 +11,8 @@ describe('Container', function () {
describe('enforceAllowedChildren()', function () {
it('keep allowed', function () {
const li = document.createElement('li');
li.innerHTML = 2;
ctx.scroll.domNode.firstChild.appendChild(li);
li.innerHTML = '2';
ctx.scroll.domNode.firstChild?.appendChild(li);
ctx.scroll.update();
expect(ctx.scroll.domNode.innerHTML).toEqual(
'<ol><li>1</li><li>2</li></ol>',
@ -20,16 +21,16 @@ describe('Container', function () {
it('remove unallowed child', function () {
const strong = document.createElement('strong');
strong.innerHTML = 2;
ctx.scroll.domNode.firstChild.appendChild(strong);
strong.innerHTML = '2';
ctx.scroll.domNode.firstChild?.appendChild(strong);
ctx.scroll.update();
expect(ctx.scroll.domNode.innerHTML).toEqual('<ol><li>1</li></ol>');
});
it('isolate block', function () {
const header = document.createElement('h1');
header.innerHTML = 2;
ctx.scroll.domNode.firstChild.appendChild(header);
header.innerHTML = '2';
ctx.scroll.domNode.firstChild?.appendChild(header);
ctx.scroll.update();
expect(ctx.scroll.domNode.innerHTML).toEqual(
'<ol><li>1</li></ol><h1>2</h1>',

View File

@ -1,18 +1,21 @@
import { describe, it, expect } from 'vitest';
import type { BlockBlot, InlineBlot } from '../../src/parchment';
import type { ImageBlot } from '../registry/embed';
import { setupContextBeforeEach } from '../setup';
describe('EmbedBlot', function () {
const ctx = setupContextBeforeEach();
it('value()', function () {
let imageBlot = ctx.scroll.create('image', 'favicon.ico');
let imageBlot = ctx.scroll.create('image', 'favicon.ico') as ImageBlot;
expect(imageBlot.value()).toEqual({
image: 'favicon.ico',
});
});
it('deleteAt()', function () {
let container = ctx.scroll.create('block');
let imageBlot = ctx.scroll.create('image');
let container = ctx.scroll.create('block') as BlockBlot;
let imageBlot = ctx.scroll.create('image') as ImageBlot;
container.appendChild(imageBlot);
container.insertAt(1, '!');
container.deleteAt(0, 1);
@ -22,23 +25,23 @@ describe('EmbedBlot', function () {
});
it('format()', function () {
let container = ctx.scroll.create('block');
let imageBlot = ctx.scroll.create('image');
let container = ctx.scroll.create('block') as BlockBlot;
let imageBlot = ctx.scroll.create('image') as ImageBlot;
container.appendChild(imageBlot);
imageBlot.format('alt', 'Quill Icon');
expect(imageBlot.formats()).toEqual({ alt: 'Quill Icon' });
});
it('formatAt()', function () {
let container = ctx.scroll.create('block');
let container = ctx.scroll.create('block') as BlockBlot;
let imageBlot = ctx.scroll.create('image');
container.appendChild(imageBlot);
container.formatAt(0, 1, 'color', 'red');
expect(container.children.head.statics.blotName).toBe('inline');
expect(container.children.head?.statics.blotName).toBe('inline');
});
it('insertAt()', function () {
let container = ctx.scroll.create('inline');
let container = ctx.scroll.create('inline') as InlineBlot;
let imageBlot = ctx.scroll.create('image');
container.appendChild(imageBlot);
imageBlot.insertAt(0, 'image', true);
@ -50,22 +53,22 @@ describe('EmbedBlot', function () {
it('split()', function () {
let blockNode = document.createElement('p');
blockNode.innerHTML = '<em>Te</em><img><strong>st</strong>';
let blockBlot = ctx.scroll.create(blockNode);
let imageBlot = blockBlot.children.head.next;
expect(imageBlot.split(0)).toBe(imageBlot);
expect(imageBlot.split(1)).toBe(blockBlot.children.tail);
let blockBlot = ctx.scroll.create(blockNode) as BlockBlot;
let imageBlot = blockBlot.children.head?.next;
expect(imageBlot?.split(0)).toBe(imageBlot);
expect(imageBlot?.split(1)).toBe(blockBlot.children.tail);
});
it('index()', function () {
let imageBlot = ctx.scroll.create('image');
let imageBlot = ctx.scroll.create('image') as ImageBlot;
expect(imageBlot.index(imageBlot.domNode, 0)).toEqual(0);
expect(imageBlot.index(imageBlot.domNode, 1)).toEqual(1);
expect(imageBlot.index(document.body, 1)).toEqual(-1);
});
it('position()', function () {
let container = ctx.scroll.create('block');
let imageBlot = ctx.scroll.create('image');
let container = ctx.scroll.create('block') as BlockBlot;
let imageBlot = ctx.scroll.create('image') as ImageBlot;
container.appendChild(imageBlot);
let [node, offset] = imageBlot.position(1, true);
expect(node).toEqual(container.domNode);

View File

@ -1,10 +1,13 @@
import { describe, it, expect } from 'vitest';
import type { BlockBlot, Leaf } from '../../src/parchment';
import type { BoldBlot, ItalicBlot, ScriptBlot } from '../registry/inline';
import { setupContextBeforeEach } from '../setup';
describe('InlineBlot', function () {
const ctx = setupContextBeforeEach();
it('format addition', function () {
let italicBlot = ctx.scroll.create('italic');
let italicBlot = ctx.scroll.create('italic') as ItalicBlot;
italicBlot.appendChild(ctx.scroll.create('text', 'Test'));
italicBlot.formatAt(1, 2, 'bold', true);
expect(italicBlot.domNode.outerHTML).toEqual(
@ -13,7 +16,7 @@ describe('InlineBlot', function () {
});
it('format invalid', function () {
let boldBlot = ctx.scroll.create('bold');
let boldBlot = ctx.scroll.create('bold') as BoldBlot;
boldBlot.appendChild(ctx.scroll.create('text', 'Test'));
let original = boldBlot.domNode.outerHTML;
expect(function () {
@ -23,8 +26,8 @@ describe('InlineBlot', function () {
});
it('format existing', function () {
let italicBlot = ctx.scroll.create('italic');
let boldBlot = ctx.scroll.create('bold');
let italicBlot = ctx.scroll.create('italic') as ItalicBlot;
let boldBlot = ctx.scroll.create('bold') as BoldBlot;
boldBlot.appendChild(ctx.scroll.create('text', 'Test'));
italicBlot.appendChild(boldBlot);
let original = italicBlot.domNode.outerHTML;
@ -36,8 +39,8 @@ describe('InlineBlot', function () {
});
it('format removal nonexistent', function () {
let container = ctx.scroll.create('block');
let italicBlot = ctx.scroll.create('italic');
let container = ctx.scroll.create('block') as BlockBlot;
let italicBlot = ctx.scroll.create('italic') as ItalicBlot;
italicBlot.appendChild(ctx.scroll.create('text', 'Test'));
container.appendChild(italicBlot);
let original = italicBlot.domNode.outerHTML;
@ -50,25 +53,27 @@ describe('InlineBlot', function () {
it('delete + unwrap', function () {
let node = document.createElement('p');
node.innerHTML = '<em><strong>Test</strong></em>!';
let container = ctx.scroll.create(node);
let container = ctx.scroll.create(node) as BlockBlot;
container.deleteAt(0, 4);
expect(container.children.head.value()).toEqual('!');
expect((container.children.head as Leaf).value()).toEqual('!');
});
it('formats()', function () {
let italic = document.createElement('em');
italic.style.color = 'red';
italic.innerHTML = '<strong>Test</strong>!';
let blot = ctx.scroll.create(italic);
let blot = ctx.scroll.create(italic) as ItalicBlot;
expect(blot.formats()).toEqual({ italic: true, color: 'red' });
});
it('change', function () {
let container = ctx.scroll.create('block');
let script = ctx.scroll.create('script', 'sup');
let container = ctx.scroll.create('block') as BlockBlot;
let script = ctx.scroll.create('script', 'sup') as ScriptBlot;
container.appendChild(script);
script.format('script', 'sub');
expect(container.domNode.innerHTML).toEqual('<sub></sub>');
expect(container.children.head.formats()).toEqual({ script: 'sub' });
expect((container.children.head as ScriptBlot).formats()).toEqual({
script: 'sub',
});
});
});

View File

@ -1,7 +1,15 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import LeafBlot from '../../src/blot/abstract/leaf';
import ShadowBlot from '../../src/blot/abstract/shadow';
import type {
BlockBlot,
Blot,
InlineBlot,
TextBlot,
} from '../../src/parchment';
import { HeaderBlot } from '../registry/block';
import { ImageBlot } from '../registry/embed';
import type { ItalicBlot } from '../registry/inline';
import { BoldBlot } from '../registry/inline';
import { setupContextBeforeEach } from '../setup';
@ -18,21 +26,21 @@ describe('Lifecycle', function () {
it('array tagName index', function () {
let node = HeaderBlot.create(2);
expect(node).toBeTruthy();
let blot = ctx.scroll.create(node);
let blot = ctx.scroll.create(node) as HeaderBlot;
expect(blot.formats()).toEqual({ header: 'h2' });
});
it('array tagName value', function () {
let node = HeaderBlot.create('h2');
expect(node).toBeTruthy();
let blot = ctx.scroll.create(node);
let blot = ctx.scroll.create(node) as HeaderBlot;
expect(blot.formats()).toEqual({ header: 'h2' });
});
it('array tagName default', function () {
let node = HeaderBlot.create();
expect(node).toBeTruthy();
let blot = ctx.scroll.create(node);
let blot = ctx.scroll.create(node) as HeaderBlot;
expect(blot.formats()).toEqual({ header: 'h1' });
});
@ -42,9 +50,13 @@ describe('Lifecycle', function () {
});
it('className', function () {
class ClassBlot extends ShadowBlot {}
ClassBlot.className = 'test';
ClassBlot.tagName = 'span';
class ClassBlot extends ShadowBlot {
static className = 'test';
static tagName = 'span';
static create() {
return super.create() as HTMLElement;
}
}
let node = ClassBlot.create();
expect(node).toBeTruthy();
expect(node.classList.contains('test')).toBe(true);
@ -59,7 +71,7 @@ describe('Lifecycle', function () {
'<span style="color: red;"><strong>Te</strong><em>st</em></span>';
let block = ctx.scroll.create(node);
ctx.scroll.appendChild(block);
let span = ctx.scroll.find(node.querySelector('span'));
let span = ctx.scroll.find(node.querySelector('span')) as InlineBlot;
span.format('color', false);
ctx.scroll.optimize();
expect(ctx.container.innerHTML).toEqual(
@ -72,8 +84,10 @@ describe('Lifecycle', function () {
node.innerHTML = '<em><strong>Test</strong></em>';
let block = ctx.scroll.create(node);
ctx.scroll.appendChild(block);
let text = ctx.scroll.find(node.querySelector('strong').firstChild);
text.deleteAt(0, 4);
let text = ctx.scroll.find(
node.querySelector('strong')?.firstChild as HTMLElement,
);
text?.deleteAt(0, 4);
ctx.scroll.optimize();
expect(ctx.container.innerHTML).toEqual('');
});
@ -84,10 +98,10 @@ describe('Lifecycle', function () {
let block = ctx.scroll.create(node);
ctx.scroll.appendChild(block);
let text = ctx.scroll.find(node.childNodes[1]);
text.formatAt(0, 2, 'bold', true);
text?.formatAt(0, 2, 'bold', true);
ctx.scroll.optimize();
expect(ctx.container.innerHTML).toEqual('<p><strong>Test</strong></p>');
expect(ctx.container.querySelector('strong').childNodes.length).toBe(1);
expect(ctx.container.querySelector('strong')?.childNodes.length).toBe(1);
});
it('format recursive merge', function () {
@ -97,12 +111,12 @@ describe('Lifecycle', function () {
let block = ctx.scroll.create(node);
ctx.scroll.appendChild(block);
let target = ctx.scroll.find(node.childNodes[1]);
target.wrap('italic', true);
target?.wrap('italic', true);
ctx.scroll.optimize();
expect(ctx.container.innerHTML).toEqual(
'<p><em><strong>Test</strong></em></p>',
);
expect(ctx.container.querySelector('strong').childNodes.length).toBe(1);
expect(ctx.container.querySelector('strong')?.childNodes.length).toBe(1);
});
it('remove format merge', function () {
@ -114,7 +128,7 @@ describe('Lifecycle', function () {
block.formatAt(1, 2, 'italic', false);
ctx.scroll.optimize();
expect(ctx.container.innerHTML).toEqual('<p><strong>Test</strong></p>');
expect(ctx.container.querySelector('strong').childNodes.length).toBe(1);
expect(ctx.container.querySelector('strong')?.childNodes.length).toBe(1);
});
it('remove attribute merge', function () {
@ -125,7 +139,7 @@ describe('Lifecycle', function () {
block.formatAt(1, 2, 'color', false);
ctx.scroll.optimize();
expect(ctx.container.innerHTML).toEqual('<p><em>Test</em></p>');
expect(ctx.container.querySelector('em').childNodes.length).toBe(1);
expect(ctx.container.querySelector('em')?.childNodes.length).toBe(1);
});
it('format no merge attribute mismatch', function () {
@ -149,7 +163,7 @@ describe('Lifecycle', function () {
block.deleteAt(1, 2);
ctx.scroll.optimize();
expect(ctx.container.innerHTML).toEqual('<p><em>Tt</em></p>');
expect(ctx.container.querySelector('em').childNodes.length).toBe(1);
expect(ctx.container.querySelector('em')?.childNodes.length).toBe(1);
});
it('unwrap + recursive merge', function () {
@ -162,7 +176,7 @@ describe('Lifecycle', function () {
block.formatAt(1, 2, 'color', false);
ctx.scroll.optimize();
expect(ctx.container.innerHTML).toEqual('<p><strong>Test</strong></p>');
expect(ctx.container.querySelector('strong').childNodes.length).toBe(1);
expect(ctx.container.querySelector('strong')?.childNodes.length).toBe(1);
});
it('remove text + recursive merge', function () {
@ -170,15 +184,15 @@ describe('Lifecycle', function () {
node.innerHTML = '<em>Te</em>|<em>st</em>';
let block = ctx.scroll.create(node);
ctx.scroll.appendChild(block);
node.childNodes[1].data = '';
(node.childNodes[1] as Text).data = '';
ctx.scroll.optimize();
expect(ctx.container.innerHTML).toEqual('<p><em>Test</em></p>');
expect(ctx.container.firstChild.firstChild.childNodes.length).toBe(1);
expect(ctx.container.firstChild?.firstChild?.childNodes.length).toBe(1);
});
it('insert default child', function () {
HeaderBlot.defaultChild = ImageBlot;
let blot = ctx.scroll.create('header');
let blot = ctx.scroll.create('header') as HeaderBlot;
expect(blot.domNode.innerHTML).toEqual('');
blot.optimize();
HeaderBlot.defaultChild = undefined;
@ -187,17 +201,35 @@ describe('Lifecycle', function () {
});
describe('update()', function () {
// [p, em, strong, text, image, text, p, em, text]
const ContentFixture =
'<p><em style="color: red;"><strong>Test</strong><img>ing</em></p><p><em>!</em></p>';
type Blots /* corresponds to ContentFixture */ = [
BlockBlot,
ItalicBlot,
BoldBlot,
TextBlot,
ImageBlot,
TextBlot,
BlockBlot,
ItalicBlot,
TextBlot,
];
type UpdateTestContext = {
checkUpdateCalls: (called: Blot | Blot[]) => void;
checkValues: (expected: any[]) => void;
descendants: Blots;
};
let updateCtx = {} as UpdateTestContext;
beforeEach(function () {
ctx.container.innerHTML =
'<p><em style="color: red;"><strong>Test</strong><img>ing</em></p><p><em>!</em></p>';
ctx.container.innerHTML = ContentFixture;
ctx.scroll.update();
// [p, em, strong, text, image, text, p, em, text]
this.descendants = ctx.scroll.descendants(ShadowBlot);
this.descendants.forEach(function (blot) {
spyOn(blot, 'update').and.callThrough();
updateCtx.descendants = ctx.scroll.descendants(ShadowBlot) as Blots;
updateCtx.descendants.forEach(function (blot: ShadowBlot) {
vi.spyOn(blot, 'update');
});
this.checkUpdateCalls = (called) => {
this.descendants.forEach(function (blot) {
updateCtx.checkUpdateCalls = (called) => {
updateCtx.descendants.forEach(function (blot) {
if (
called === blot ||
(Array.isArray(called) && called.indexOf(blot) > -1)
@ -208,7 +240,7 @@ describe('Lifecycle', function () {
}
});
};
this.checkValues = (expected) => {
updateCtx.checkValues = (expected) => {
let values = ctx.scroll.descendants(LeafBlot).map(function (leaf) {
return leaf.value();
});
@ -220,14 +252,14 @@ describe('Lifecycle', function () {
it('insert text', function () {
ctx.scroll.insertAt(2, '|');
ctx.scroll.optimize();
this.checkValues(['Te|st', { image: true }, 'ing', '!']);
updateCtx.checkValues(['Te|st', { image: true }, 'ing', '!']);
expect(ctx.scroll.observer.takeRecords()).toEqual([]);
});
it('insert embed', function () {
ctx.scroll.insertAt(2, 'image', true);
ctx.scroll.optimize();
this.checkValues([
updateCtx.checkValues([
'Te',
{ image: true },
'st',
@ -241,24 +273,24 @@ describe('Lifecycle', function () {
it('delete', function () {
ctx.scroll.deleteAt(2, 5);
ctx.scroll.optimize();
this.checkValues(['Te', 'g', '!']);
updateCtx.checkValues(['Te', 'g', '!']);
expect(ctx.scroll.observer.takeRecords()).toEqual([]);
});
it('format', function () {
ctx.scroll.formatAt(2, 5, 'size', '24px');
ctx.scroll.optimize();
this.checkValues(['Te', 'st', { image: true }, 'in', 'g', '!']);
updateCtx.checkValues(['Te', 'st', { image: true }, 'in', 'g', '!']);
expect(ctx.scroll.observer.takeRecords()).toEqual([]);
});
});
describe('dom', function () {
it('change text', function () {
let textBlot = this.descendants[3];
let textBlot = updateCtx.descendants[3];
textBlot.domNode.data = 'Te|st';
ctx.scroll.update();
this.checkUpdateCalls(textBlot);
updateCtx.checkUpdateCalls(textBlot);
expect(textBlot.value()).toEqual('Te|st');
});
@ -269,14 +301,14 @@ describe('Lifecycle', function () {
unknownElement.appendChild(unknownElement2);
ctx.scroll.domNode.removeChild(unknownElement);
ctx.scroll.update();
this.checkValues(['Test', { image: true }, 'ing', '!']);
updateCtx.checkValues(['Test', { image: true }, 'ing', '!']);
});
it('add attribute', function () {
let attrBlot = this.descendants[1];
let attrBlot = updateCtx.descendants[1];
attrBlot.domNode.setAttribute('id', 'blot');
ctx.scroll.update();
this.checkUpdateCalls(attrBlot);
updateCtx.checkUpdateCalls(attrBlot);
expect(attrBlot.formats()).toEqual({
color: 'red',
italic: true,
@ -285,88 +317,88 @@ describe('Lifecycle', function () {
});
it('add embed attribute', function () {
let imageBlot = this.descendants[4];
let imageBlot = updateCtx.descendants[4];
imageBlot.domNode.setAttribute('alt', 'image');
ctx.scroll.update();
this.checkUpdateCalls(imageBlot);
updateCtx.checkUpdateCalls(imageBlot);
});
it('change attributes', function () {
let attrBlot = this.descendants[1];
let attrBlot = updateCtx.descendants[1];
attrBlot.domNode.style.color = 'blue';
ctx.scroll.update();
this.checkUpdateCalls(attrBlot);
updateCtx.checkUpdateCalls(attrBlot);
expect(attrBlot.formats()).toEqual({ color: 'blue', italic: true });
});
it('remove attribute', function () {
let attrBlot = this.descendants[1];
let attrBlot = updateCtx.descendants[1];
attrBlot.domNode.removeAttribute('style');
ctx.scroll.update();
this.checkUpdateCalls(attrBlot);
updateCtx.checkUpdateCalls(attrBlot);
expect(attrBlot.formats()).toEqual({ italic: true });
});
it('add child node', function () {
let italicBlot = this.descendants[1];
let italicBlot = updateCtx.descendants[1];
italicBlot.domNode.appendChild(document.createTextNode('|'));
ctx.scroll.update();
this.checkUpdateCalls(italicBlot);
this.checkValues(['Test', { image: true }, 'ing|', '!']);
updateCtx.checkUpdateCalls(italicBlot);
updateCtx.checkValues(['Test', { image: true }, 'ing|', '!']);
});
it('add empty family', function () {
let blockBlot = this.descendants[0];
let blockBlot = updateCtx.descendants[0];
let boldNode = document.createElement('strong');
let html = ctx.scroll.innerHTML;
let html = ctx.scroll.domNode.innerHTML;
boldNode.appendChild(document.createTextNode(''));
blockBlot.domNode.appendChild(boldNode);
ctx.scroll.update();
this.checkUpdateCalls(blockBlot);
expect(ctx.scroll.innerHTML).toBe(html);
updateCtx.checkUpdateCalls(blockBlot);
expect(ctx.scroll.domNode.innerHTML).toBe(html);
expect(ctx.scroll.descendants(ShadowBlot).length).toEqual(
this.descendants.length,
updateCtx.descendants.length,
);
});
it('move node up', function () {
let imageBlot = this.descendants[4];
imageBlot.domNode.parentNode.insertBefore(
let imageBlot = updateCtx.descendants[4];
imageBlot.domNode.parentNode?.insertBefore(
imageBlot.domNode,
imageBlot.domNode.previousSibling,
);
ctx.scroll.update();
this.checkUpdateCalls(imageBlot.parent);
this.checkValues([{ image: true }, 'Test', 'ing', '!']);
updateCtx.checkUpdateCalls(imageBlot.parent);
updateCtx.checkValues([{ image: true }, 'Test', 'ing', '!']);
});
it('move node down', function () {
let imageBlot = this.descendants[4];
imageBlot.domNode.parentNode.insertBefore(
imageBlot.domNode.nextSibling,
let imageBlot = updateCtx.descendants[4];
imageBlot.domNode.parentNode?.insertBefore(
imageBlot.domNode.nextSibling!,
imageBlot.domNode,
);
ctx.scroll.update();
this.checkUpdateCalls(imageBlot.parent);
this.checkValues(['Test', 'ing', { image: true }, '!']);
updateCtx.checkUpdateCalls(imageBlot.parent);
updateCtx.checkValues(['Test', 'ing', { image: true }, '!']);
});
it('move node and change', function () {
let firstBlockBlot = this.descendants[0];
let lastItalicBlot = this.descendants[7];
let firstBlockBlot = updateCtx.descendants[0];
let lastItalicBlot = updateCtx.descendants[7];
firstBlockBlot.domNode.appendChild(lastItalicBlot.domNode);
lastItalicBlot.domNode.innerHTML = '?';
ctx.scroll.update();
this.checkUpdateCalls([
updateCtx.checkUpdateCalls([
firstBlockBlot,
this.descendants[6],
this.descendants[7],
updateCtx.descendants[6],
updateCtx.descendants[7],
]);
this.checkValues(['Test', { image: true }, 'ing', '?']);
updateCtx.checkValues(['Test', { image: true }, 'ing', '?']);
});
it('add and remove consecutive nodes', function () {
let italicBlot = this.descendants[1];
let italicBlot = updateCtx.descendants[1];
let imageNode = document.createElement('img');
let textNode = document.createTextNode('|');
let refNode = italicBlot.domNode.childNodes[1]; // Old img
@ -374,68 +406,70 @@ describe('Lifecycle', function () {
italicBlot.domNode.insertBefore(imageNode, textNode);
italicBlot.domNode.removeChild(refNode);
ctx.scroll.update();
this.checkUpdateCalls(italicBlot);
this.checkValues(['Test', { image: true }, '|ing', '!']);
updateCtx.checkUpdateCalls(italicBlot);
updateCtx.checkValues(['Test', { image: true }, '|ing', '!']);
});
it('wrap text', function () {
let textNode = this.descendants[5].domNode;
let textNode = updateCtx.descendants[5].domNode;
let spanNode = document.createElement('span');
textNode.parentNode.removeChild(textNode);
ctx.scroll.domNode.lastChild.appendChild(spanNode);
textNode.parentNode?.removeChild(textNode);
ctx.scroll.domNode.lastChild?.appendChild(spanNode);
spanNode.appendChild(textNode);
ctx.scroll.update();
this.checkValues(['Test', { image: true }, '!', 'ing']);
updateCtx.checkValues(['Test', { image: true }, '!', 'ing']);
});
it('add then remove same node', function () {
let italicBlot = this.descendants[1];
let italicBlot = updateCtx.descendants[1];
let textNode = document.createTextNode('|');
italicBlot.domNode.appendChild(textNode);
italicBlot.domNode.removeChild(textNode);
ctx.scroll.update();
this.checkUpdateCalls(italicBlot);
this.checkValues(['Test', { image: true }, 'ing', '!']);
updateCtx.checkUpdateCalls(italicBlot);
updateCtx.checkValues(['Test', { image: true }, 'ing', '!']);
});
it('remove child node', function () {
let imageBlot = this.descendants[4];
imageBlot.domNode.parentNode.removeChild(imageBlot.domNode);
let imageBlot = updateCtx.descendants[4];
imageBlot.domNode.parentNode?.removeChild(imageBlot.domNode);
ctx.scroll.update();
this.checkUpdateCalls(this.descendants[1]);
this.checkValues(['Test', 'ing', '!']);
updateCtx.checkUpdateCalls(updateCtx.descendants[1]);
updateCtx.checkValues(['Test', 'ing', '!']);
});
it('change and remove node', function () {
let italicBlot = this.descendants[1];
let italicBlot = updateCtx.descendants[1];
// @ts-expect-error This simulates ignored dom mutation
italicBlot.domNode.color = 'blue';
italicBlot.domNode.parentNode.removeChild(italicBlot.domNode);
italicBlot.domNode.parentNode?.removeChild(italicBlot.domNode);
ctx.scroll.update();
this.checkUpdateCalls(italicBlot.parent);
this.checkValues(['!']);
updateCtx.checkUpdateCalls(italicBlot.parent);
updateCtx.checkValues(['!']);
});
it('change and remove parent', function () {
let blockBlot = this.descendants[0];
let italicBlot = this.descendants[1];
let blockBlot = updateCtx.descendants[0];
let italicBlot = updateCtx.descendants[1];
// @ts-expect-error This simulates ignored dom mutation
italicBlot.domNode.color = 'blue';
ctx.scroll.domNode.removeChild(blockBlot.domNode);
ctx.scroll.update();
this.checkUpdateCalls([]);
this.checkValues(['!']);
updateCtx.checkUpdateCalls([]);
updateCtx.checkValues(['!']);
});
it('different changes to same blot', function () {
let attrBlot = this.descendants[1];
let attrBlot = updateCtx.descendants[1];
attrBlot.domNode.style.color = 'blue';
attrBlot.domNode.insertBefore(
document.createTextNode('|'),
attrBlot.domNode.childNodes[1],
);
ctx.scroll.update();
this.checkUpdateCalls(attrBlot);
updateCtx.checkUpdateCalls(attrBlot);
expect(attrBlot.formats()).toEqual({ color: 'blue', italic: true });
this.checkValues(['Test', '|', { image: true }, 'ing', '!']);
updateCtx.checkValues(['Test', '|', { image: true }, 'ing', '!']);
});
});
});

View File

@ -1,249 +1,259 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import LinkedList from '../../src/collection/linked-list';
import type { LinkedNode } from '../../src/parchment';
interface StrNode extends LinkedNode {
str: string;
}
const setupContextBeforeEach = () => {
const getContext = () => {
const length = () => 3;
return {
list: new LinkedList<StrNode>(),
a: { str: 'a', length } as StrNode,
b: { str: 'b', length } as StrNode,
c: { str: 'c', length } as StrNode,
zero: { str: '!', length: () => 0 } as StrNode,
};
};
let ctx = getContext();
beforeEach(function () {
Object.assign(ctx, getContext());
});
return ctx;
};
describe('LinkedList', function () {
beforeEach(function () {
this.list = new LinkedList();
this.a = { str: 'a' };
this.b = { str: 'b' };
this.c = { str: 'c' };
this.zero = {
str: '!',
length: function () {
return 0;
},
};
this.a.length = this.b.length = this.c.length = () => 3;
});
const ctx = setupContextBeforeEach();
describe('manipulation', function () {
it('append to empty list', function () {
this.list.append(this.a);
expect(this.list.length).toBe(1);
expect(this.list.head).toBe(this.a);
expect(this.list.tail).toBe(this.a);
expect(this.a.prev).toBeNull();
expect(this.a.next).toBeNull();
ctx.list.append(ctx.a);
expect(ctx.list.length).toBe(1);
expect(ctx.list.head).toBe(ctx.a);
expect(ctx.list.tail).toBe(ctx.a);
expect(ctx.a.prev).toBeNull();
expect(ctx.a.next).toBeNull();
});
it('insert to become head', function () {
this.list.append(this.b);
this.list.insertBefore(this.a, this.b);
expect(this.list.length).toBe(2);
expect(this.list.head).toBe(this.a);
expect(this.list.tail).toBe(this.b);
expect(this.a.prev).toBeNull();
expect(this.a.next).toBe(this.b);
expect(this.b.prev).toBe(this.a);
expect(this.b.next).toBeNull();
ctx.list.append(ctx.b);
ctx.list.insertBefore(ctx.a, ctx.b);
expect(ctx.list.length).toBe(2);
expect(ctx.list.head).toBe(ctx.a);
expect(ctx.list.tail).toBe(ctx.b);
expect(ctx.a.prev).toBeNull();
expect(ctx.a.next).toBe(ctx.b);
expect(ctx.b.prev).toBe(ctx.a);
expect(ctx.b.next).toBeNull();
});
it('insert to become tail', function () {
this.list.append(this.a);
this.list.insertBefore(this.b, null);
expect(this.list.length).toBe(2);
expect(this.list.head).toBe(this.a);
expect(this.list.tail).toBe(this.b);
expect(this.a.prev).toBeNull();
expect(this.a.next).toBe(this.b);
expect(this.b.prev).toBe(this.a);
expect(this.b.next).toBeNull();
ctx.list.append(ctx.a);
ctx.list.insertBefore(ctx.b, null);
expect(ctx.list.length).toBe(2);
expect(ctx.list.head).toBe(ctx.a);
expect(ctx.list.tail).toBe(ctx.b);
expect(ctx.a.prev).toBeNull();
expect(ctx.a.next).toBe(ctx.b);
expect(ctx.b.prev).toBe(ctx.a);
expect(ctx.b.next).toBeNull();
});
it('insert in middle', function () {
this.list.append(this.a, this.c);
this.list.insertBefore(this.b, this.c);
expect(this.list.length).toBe(3);
expect(this.list.head).toBe(this.a);
expect(this.a.next).toBe(this.b);
expect(this.b.next).toBe(this.c);
expect(this.list.tail).toBe(this.c);
ctx.list.append(ctx.a, ctx.c);
ctx.list.insertBefore(ctx.b, ctx.c);
expect(ctx.list.length).toBe(3);
expect(ctx.list.head).toBe(ctx.a);
expect(ctx.a.next).toBe(ctx.b);
expect(ctx.b.next).toBe(ctx.c);
expect(ctx.list.tail).toBe(ctx.c);
});
it('remove head', function () {
this.list.append(this.a, this.b);
this.list.remove(this.a);
expect(this.list.length).toBe(1);
expect(this.list.head).toBe(this.b);
expect(this.list.tail).toBe(this.b);
expect(this.list.head.prev).toBeNull();
expect(this.list.tail.next).toBeNull();
ctx.list.append(ctx.a, ctx.b);
ctx.list.remove(ctx.a);
expect(ctx.list.length).toBe(1);
expect(ctx.list.head).toBe(ctx.b);
expect(ctx.list.tail).toBe(ctx.b);
expect(ctx.list.head?.prev).toBeNull();
expect(ctx.list.tail?.next).toBeNull();
});
it('remove tail', function () {
this.list.append(this.a, this.b);
this.list.remove(this.b);
expect(this.list.length).toBe(1);
expect(this.list.head).toBe(this.a);
expect(this.list.tail).toBe(this.a);
expect(this.list.head.prev).toBeNull();
expect(this.list.tail.next).toBeNull();
ctx.list.append(ctx.a, ctx.b);
ctx.list.remove(ctx.b);
expect(ctx.list.length).toBe(1);
expect(ctx.list.head).toBe(ctx.a);
expect(ctx.list.tail).toBe(ctx.a);
expect(ctx.list.head?.prev).toBeNull();
expect(ctx.list.tail?.next).toBeNull();
});
it('remove inner', function () {
this.list.append(this.a, this.b, this.c);
this.list.remove(this.b);
expect(this.list.length).toBe(2);
expect(this.list.head).toBe(this.a);
expect(this.list.tail).toBe(this.c);
expect(this.list.head.prev).toBeNull();
expect(this.list.tail.next).toBeNull();
expect(this.a.next).toBe(this.c);
expect(this.c.prev).toBe(this.a);
ctx.list.append(ctx.a, ctx.b, ctx.c);
ctx.list.remove(ctx.b);
expect(ctx.list.length).toBe(2);
expect(ctx.list.head).toBe(ctx.a);
expect(ctx.list.tail).toBe(ctx.c);
expect(ctx.list.head?.prev).toBeNull();
expect(ctx.list.tail?.next).toBeNull();
expect(ctx.a.next).toBe(ctx.c);
expect(ctx.c.prev).toBe(ctx.a);
// Maintain references
expect(this.b.prev).toBe(this.a);
expect(this.b.next).toBe(this.c);
expect(ctx.b.prev).toBe(ctx.a);
expect(ctx.b.next).toBe(ctx.c);
});
it('remove only node', function () {
this.list.append(this.a);
this.list.remove(this.a);
expect(this.list.length).toBe(0);
expect(this.list.head).toBeNull();
expect(this.list.tail).toBeNull();
ctx.list.append(ctx.a);
ctx.list.remove(ctx.a);
expect(ctx.list.length).toBe(0);
expect(ctx.list.head).toBeNull();
expect(ctx.list.tail).toBeNull();
});
it('contains', function () {
this.list.append(this.a, this.b);
expect(this.list.contains(this.a)).toBe(true);
expect(this.list.contains(this.b)).toBe(true);
expect(this.list.contains(this.c)).toBe(false);
ctx.list.append(ctx.a, ctx.b);
expect(ctx.list.contains(ctx.a)).toBe(true);
expect(ctx.list.contains(ctx.b)).toBe(true);
expect(ctx.list.contains(ctx.c)).toBe(false);
});
it('move', function () {
this.list.append(this.a, this.b, this.c);
this.list.remove(this.b);
this.list.remove(this.a);
this.list.remove(this.c);
this.list.append(this.b);
expect(this.b.prev).toBeNull();
expect(this.b.next).toBeNull();
ctx.list.append(ctx.a, ctx.b, ctx.c);
ctx.list.remove(ctx.b);
ctx.list.remove(ctx.a);
ctx.list.remove(ctx.c);
ctx.list.append(ctx.b);
expect(ctx.b.prev).toBeNull();
expect(ctx.b.next).toBeNull();
});
});
describe('iteration', function () {
const spy = vi.fn();
beforeEach(function () {
this.spy = {
callback: function () {
return arguments;
},
};
spyOn(this.spy, 'callback');
spy.mockReset();
});
it('iterate over empty list', function () {
this.list.forEach(this.spy.callback);
expect(this.spy.callback.calls.count()).toBe(0);
ctx.list.forEach(spy);
expect(spy.mock.calls.length).toBe(0);
});
it('iterate non-head start', function () {
this.list.append(this.a, this.b, this.c);
let next = this.list.iterator(this.b);
ctx.list.append(ctx.a, ctx.b, ctx.c);
let next = ctx.list.iterator(ctx.b);
let b = next();
let c = next();
let d = next();
expect(b).toBe(this.b);
expect(c).toBe(this.c);
expect(b).toBe(ctx.b);
expect(c).toBe(ctx.c);
expect(d).toBeNull();
});
it('find', function () {
this.list.append(this.a, this.b, this.zero, this.c);
expect(this.list.find(0)).toEqual([this.a, 0]);
expect(this.list.find(2)).toEqual([this.a, 2]);
expect(this.list.find(6)).toEqual([this.c, 0]);
expect(this.list.find(3, true)).toEqual([this.a, 3]);
expect(this.list.find(6, true)).toEqual([this.zero, 0]);
expect(this.list.find(3)).toEqual([this.b, 0]);
expect(this.list.find(4)).toEqual([this.b, 1]);
expect(this.list.find(10)).toEqual([null, 0]);
ctx.list.append(ctx.a, ctx.b, ctx.zero, ctx.c);
expect(ctx.list.find(0)).toEqual([ctx.a, 0]);
expect(ctx.list.find(2)).toEqual([ctx.a, 2]);
expect(ctx.list.find(6)).toEqual([ctx.c, 0]);
expect(ctx.list.find(3, true)).toEqual([ctx.a, 3]);
expect(ctx.list.find(6, true)).toEqual([ctx.zero, 0]);
expect(ctx.list.find(3)).toEqual([ctx.b, 0]);
expect(ctx.list.find(4)).toEqual([ctx.b, 1]);
expect(ctx.list.find(10)).toEqual([null, 0]);
});
it('offset', function () {
this.list.append(this.a, this.b, this.c);
expect(this.list.offset(this.a)).toBe(0);
expect(this.list.offset(this.b)).toBe(3);
expect(this.list.offset(this.c)).toBe(6);
expect(this.list.offset({})).toBe(-1);
ctx.list.append(ctx.a, ctx.b, ctx.c);
expect(ctx.list.offset(ctx.a)).toBe(0);
expect(ctx.list.offset(ctx.b)).toBe(3);
expect(ctx.list.offset(ctx.c)).toBe(6);
// @ts-ignore This tests invalid usage
expect(ctx.list.offset({})).toBe(-1);
});
it('forEach', function () {
this.list.append(this.a, this.b, this.c);
this.list.forEach(this.spy.callback);
expect(this.spy.callback.calls.count()).toBe(3);
let result = this.spy.callback.calls.all().reduce(function (memo, call) {
return memo + call.args[0].str;
}, '');
ctx.list.append(ctx.a, ctx.b, ctx.c);
ctx.list.forEach(spy);
expect(spy.mock.calls.length).toBe(3);
const result = spy.mock.calls.reduce(
(memo: string, call: StrNode[]) => memo + call[0].str,
'',
);
expect(result).toBe('abc');
});
it('destructive modification', function () {
this.list.append(this.a, this.b, this.c);
let arr = [];
this.list.forEach((node) => {
ctx.list.append(ctx.a, ctx.b, ctx.c);
let arr: string[] = [];
ctx.list.forEach((node) => {
arr.push(node.str);
if (node === this.a) {
this.list.remove(this.a);
this.list.remove(this.b);
this.list.append(this.a);
if (node === ctx.a) {
ctx.list.remove(ctx.a);
ctx.list.remove(ctx.b);
ctx.list.append(ctx.a);
}
});
expect(arr).toEqual(['a', 'b', 'c', 'a']);
});
it('map', function () {
this.list.append(this.a, this.b, this.c);
let arr = this.list.map(function (node) {
ctx.list.append(ctx.a, ctx.b, ctx.c);
let arr = ctx.list.map(function (node) {
return node.str;
});
expect(arr).toEqual(['a', 'b', 'c']);
});
it('reduce', function () {
this.list.append(this.a, this.b, this.c);
let memo = this.list.reduce(function (memo, node) {
ctx.list.append(ctx.a, ctx.b, ctx.c);
let memo = ctx.list.reduce(function (memo, node) {
return memo + node.str;
}, '');
expect(memo).toBe('abc');
});
it('forEachAt', function () {
this.list.append(this.a, this.b, this.c);
this.list.forEachAt(3, 3, this.spy.callback);
expect(this.spy.callback.calls.count()).toBe(1);
expect(this.spy.callback.calls.first().args).toEqual([this.b, 0, 3]);
ctx.list.append(ctx.a, ctx.b, ctx.c);
ctx.list.forEachAt(3, 3, spy);
expect(spy.mock.calls.length).toBe(1);
expect(spy.mock.calls[0]).toEqual([ctx.b, 0, 3]);
});
it('forEachAt zero length nodes', function () {
this.list.append(this.a, this.zero, this.c);
this.list.forEachAt(2, 2, this.spy.callback);
expect(this.spy.callback.calls.count()).toBe(3);
let calls = this.spy.callback.calls.all();
expect(calls[0].args).toEqual([this.a, 2, 1]);
expect(calls[1].args).toEqual([this.zero, 0, 0]);
expect(calls[2].args).toEqual([this.c, 0, 1]);
ctx.list.append(ctx.a, ctx.zero, ctx.c);
ctx.list.forEachAt(2, 2, spy);
expect(spy.mock.calls.length).toBe(3);
let calls = spy.mock.calls;
expect(calls[0]).toEqual([ctx.a, 2, 1]);
expect(calls[1]).toEqual([ctx.zero, 0, 0]);
expect(calls[2]).toEqual([ctx.c, 0, 1]);
});
it('forEachAt none', function () {
this.list.append(this.a, this.b);
this.list.forEachAt(1, 0, this.spy.callback);
expect(this.spy.callback.calls.count()).toBe(0);
ctx.list.append(ctx.a, ctx.b);
ctx.list.forEachAt(1, 0, spy);
expect(spy.mock.calls.length).toBe(0);
});
it('forEachAt partial nodes', function () {
this.list.append(this.a, this.b, this.c);
this.list.forEachAt(1, 7, this.spy.callback);
expect(this.spy.callback.calls.count()).toBe(3);
let calls = this.spy.callback.calls.all();
expect(calls[0].args).toEqual([this.a, 1, 2]);
expect(calls[1].args).toEqual([this.b, 0, 3]);
expect(calls[2].args).toEqual([this.c, 0, 2]);
ctx.list.append(ctx.a, ctx.b, ctx.c);
ctx.list.forEachAt(1, 7, spy);
expect(spy.mock.calls.length).toBe(3);
let calls = spy.mock.calls;
expect(calls[0]).toEqual([ctx.a, 1, 2]);
expect(calls[1]).toEqual([ctx.b, 0, 3]);
expect(calls[2]).toEqual([ctx.c, 0, 2]);
});
it('forEachAt at part of single node', function () {
this.list.append(this.a, this.b, this.c);
this.list.forEachAt(4, 1, this.spy.callback);
expect(this.spy.callback.calls.count()).toBe(1);
expect(this.spy.callback.calls.first().args).toEqual([this.b, 1, 1]);
ctx.list.append(ctx.a, ctx.b, ctx.c);
ctx.list.forEachAt(4, 1, spy);
expect(spy.mock.calls.length).toBe(1);
expect(spy.mock.calls[0]).toEqual([ctx.b, 1, 1]);
});
});
});

View File

@ -1,3 +1,4 @@
import { describe, it, expect, beforeEach } from 'vitest';
import LeafBlot from '../../src/blot/abstract/leaf';
import ParentBlot from '../../src/blot/abstract/parent';
import ShadowBlot from '../../src/blot/abstract/shadow';
@ -9,41 +10,44 @@ import { ItalicBlot } from '../registry/inline';
import Registry from '../../src/registry';
import TextBlot from '../../src/blot/text';
import { setupContextBeforeEach } from '../setup';
import type { BlockBlot, Blot } from '../../src/parchment';
describe('Parent', function () {
const ctx = setupContextBeforeEach();
let testBlot!: BlockBlot;
beforeEach(function () {
let node = document.createElement('p');
node.innerHTML = '<span>0</span><em>1<strong>2</strong><img></em>4';
this.blot = ctx.registry.create(ctx.scroll, node);
testBlot = ctx.registry.create(ctx.scroll, node) as BlockBlot;
});
describe('descendants()', function () {
it('all', function () {
expect(this.blot.descendants(ShadowBlot).length).toEqual(8);
expect(testBlot.descendants(ShadowBlot).length).toEqual(8);
});
it('container', function () {
expect(this.blot.descendants(ParentBlot).length).toEqual(3);
expect(testBlot.descendants(ParentBlot).length).toEqual(3);
});
it('leaf', function () {
expect(this.blot.descendants(LeafBlot).length).toEqual(5);
expect(testBlot.descendants(LeafBlot).length).toEqual(5);
});
it('embed', function () {
expect(this.blot.descendants(EmbedBlot).length).toEqual(1);
expect(testBlot.descendants(EmbedBlot).length).toEqual(1);
});
it('range', function () {
expect(this.blot.descendants(TextBlot, 1, 3).length).toEqual(2);
expect(testBlot.descendants(TextBlot, 1, 3).length).toEqual(2);
});
it('function match', function () {
expect(
this.blot.descendants(
function (blot) {
testBlot.descendants(
function (blot: Blot) {
return blot instanceof TextBlot;
},
1,
@ -55,13 +59,13 @@ describe('Parent', function () {
describe('descendant', function () {
it('index', function () {
let [blot, offset] = this.blot.descendant(ItalicBlot, 3);
let [blot, offset] = testBlot.descendant(ItalicBlot, 3);
expect(blot instanceof ItalicBlot).toBe(true);
expect(offset).toEqual(2);
});
it('function match', function () {
let [blot, offset] = this.blot.descendant(function (blot) {
let [blot, offset] = testBlot.descendant(function (blot: Blot) {
return blot instanceof ItalicBlot;
}, 3);
expect(blot instanceof ItalicBlot).toBe(true);
@ -69,18 +73,18 @@ describe('Parent', function () {
});
it('no match', function () {
let [blot, offset] = this.blot.descendant(VideoBlot, 1);
let [blot, offset] = testBlot.descendant(VideoBlot, 1);
expect(blot).toEqual(null);
expect(offset).toEqual(-1);
});
});
it('detach()', function () {
expect(Registry.blots.get(this.blot.domNode)).toEqual(this.blot);
expect(this.blot.descendants(ShadowBlot).length).toEqual(8);
this.blot.detach();
expect(Registry.blots.has(this.blot.domNode)).toBe(false);
this.blot.descendants(ShadowBlot).forEach((blot) => {
expect(Registry.blots.get(testBlot.domNode)).toEqual(testBlot);
expect(testBlot.descendants(ShadowBlot).length).toEqual(8);
testBlot.detach();
expect(Registry.blots.has(testBlot.domNode)).toBe(false);
testBlot.descendants(ShadowBlot).forEach((blot) => {
expect(Registry.blots.has(blot.domNode)).toBe(false);
});
});
@ -94,8 +98,8 @@ describe('Parent', function () {
});
it('ignore added uiNode', function () {
ctx.scroll.appendChild(this.blot);
this.blot.attachUI(document.createElement('div'));
ctx.scroll.appendChild(testBlot);
testBlot.attachUI(document.createElement('div'));
ctx.scroll.update();
expect(ctx.scroll.domNode.innerHTML).toEqual(
'<p><div contenteditable="false"></div>0<em>1<strong>2</strong><img></em>4</p>',

View File

@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import Scope from '../../src/scope';
import { HeaderBlot } from '../registry/block';
import { AuthorBlot, BoldBlot, ItalicBlot } from '../registry/inline';
@ -5,6 +6,8 @@ import { AuthorBlot, BoldBlot, ItalicBlot } from '../registry/inline';
import ShadowBlot from '../../src/blot/abstract/shadow';
import InlineBlot from '../../src/blot/inline';
import BlockBlot from '../../src/blot/block';
import type { Parent } from '../../src/parchment';
import { setupContextBeforeEach } from '../setup';
describe('ctx.registry', function () {
@ -38,12 +41,14 @@ describe('ctx.registry', function () {
it('string index', function () {
let blot = ctx.registry.create(ctx.scroll, 'header', '2');
expect(blot instanceof HeaderBlot).toBe(true);
expect(blot.formats()).toEqual({ header: 'h2' });
expect(blot instanceof HeaderBlot && blot.formats()).toEqual({
header: 'h2',
});
});
it('invalid', function () {
expect(() => {
// @ts-expect-error This tests invalid usage
ctx.registry.create(ctx.scroll, BoldBlot);
}).toThrowError(/\[Parchment\]/);
});
@ -52,6 +57,7 @@ describe('ctx.registry', function () {
describe('register()', function () {
it('invalid', function () {
expect(function () {
// @ts-expect-error This tests invalid usage
ctx.registry.register({});
}).toThrowError(/\[Parchment\]/);
});
@ -67,7 +73,7 @@ describe('ctx.registry', function () {
it('exact', function () {
let blockNode = document.createElement('p');
blockNode.innerHTML = '<span>01</span><em>23<strong>45</strong></em>';
let blockBlot = ctx.registry.create(ctx.scroll, blockNode);
let blockBlot = ctx.registry.create(ctx.scroll, blockNode) as BlockBlot;
expect(ctx.registry.find(document.body)).toBeFalsy();
expect(ctx.registry.find(blockNode)).toBe(blockBlot);
expect(ctx.registry.find(blockNode.querySelector('span'))).toBe(
@ -77,11 +83,12 @@ describe('ctx.registry', function () {
blockBlot.children.tail,
);
expect(ctx.registry.find(blockNode.querySelector('strong'))).toBe(
blockBlot.children.tail.children.tail,
(blockBlot.children.tail as Parent)?.children.tail,
);
let text01 = blockBlot.children.head.children.head;
let text23 = blockBlot.children.tail.children.head;
let text45 = blockBlot.children.tail.children.tail.children.head;
let text01 = (blockBlot.children.head as Parent).children.head!;
let text23 = (blockBlot.children.tail as Parent).children.head!;
let text45 = ((blockBlot.children.tail as Parent).children.tail as Parent)
.children.head!;
expect(ctx.registry.find(text01.domNode)).toBe(text01);
expect(ctx.registry.find(text23.domNode)).toBe(text23);
expect(ctx.registry.find(text45.domNode)).toBe(text45);

View File

@ -1,3 +1,4 @@
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { setupContextBeforeEach } from '../setup';
describe('scroll', function () {
@ -11,14 +12,14 @@ describe('scroll', function () {
describe('path()', function () {
it('middle', function () {
let path = ctx.scroll.path(7);
let expected = [
const path = ctx.scroll.path(7);
const expected = [
['scroll', 7],
['block', 7],
['italic', 2],
['bold', 2],
['text', 2],
];
] as const;
expect(path.length).toEqual(expected.length);
path.forEach(function (position, i) {
expect(position[0].statics.blotName).toEqual(expected[i][0]);
@ -27,14 +28,14 @@ describe('scroll', function () {
});
it('between blots', function () {
let path = ctx.scroll.path(5);
let expected = [
const path = ctx.scroll.path(5);
const expected = [
['scroll', 5],
['block', 5],
['italic', 0],
['bold', 0],
['text', 0],
];
] as const;
expect(path.length).toEqual(expected.length);
path.forEach(function (position, i) {
expect(position[0].statics.blotName).toEqual(expected[i][0]);
@ -43,13 +44,13 @@ describe('scroll', function () {
});
it('inclusive', function () {
let path = ctx.scroll.path(3, true);
let expected = [
const path = ctx.scroll.path(3, true);
const expected = [
['scroll', 3],
['block', 3],
['bold', 3],
['text', 3],
];
] as const;
expect(path.length).toEqual(expected.length);
path.forEach(function (position, i) {
expect(position[0].statics.blotName).toEqual(expected[i][0]);
@ -58,8 +59,8 @@ describe('scroll', function () {
});
it('last', function () {
let path = ctx.scroll.path(9);
let expected = [['scroll', 9]];
const path = ctx.scroll.path(9);
const expected = [['scroll', 9]] as const;
expect(path.length).toEqual(expected.length);
path.forEach(function (position, i) {
expect(position[0].statics.blotName).toEqual(expected[i][0]);
@ -75,26 +76,28 @@ describe('scroll', function () {
expect(wrapper.firstChild).toEqual(ctx.scroll.domNode);
});
it('detach', function (done) {
spyOn(ctx.scroll, 'optimize').and.callThrough();
it('detach', async function () {
vi.spyOn(ctx.scroll, 'optimize');
ctx.scroll.domNode.innerHTML = 'Test';
setTimeout(() => {
expect(ctx.scroll.optimize).toHaveBeenCalledTimes(1);
ctx.scroll.detach();
ctx.scroll.domNode.innerHTML = '!';
await new Promise<void>((resolve) => {
setTimeout(() => {
expect(ctx.scroll.optimize).toHaveBeenCalledTimes(1);
done();
ctx.scroll.detach();
ctx.scroll.domNode.innerHTML = '!';
setTimeout(() => {
expect(ctx.scroll.optimize).toHaveBeenCalledTimes(1);
resolve();
}, 1);
}, 1);
}, 1);
});
});
describe('scroll reference', function () {
it('initialization', function () {
expect(ctx.scroll).toEqual(ctx.scroll);
ctx.scroll.descendants((blot) => {
expect(blot.scroll).toEqual(ctx.scroll);
});
ctx.scroll
.descendants(() => true)
.forEach((blot) => expect(blot.scroll).toEqual(ctx.scroll));
});
it('api change', function () {
@ -106,9 +109,9 @@ describe('scroll', function () {
it('user change', function () {
ctx.scroll.domNode.innerHTML = '<p><em>01</em>23</p>';
ctx.scroll.update();
ctx.scroll.descendants((blot) => {
expect(blot.scroll).toEqual(ctx.scroll);
});
ctx.scroll
.descendants(() => true)
.forEach((blot) => expect(blot.scroll).toEqual(ctx.scroll));
});
});
});

View File

@ -1,4 +1,6 @@
import { describe, it, expect } from 'vitest';
import TextBlot from '../../src/blot/text';
import type { BlockBlot, InlineBlot } from '../../src/parchment';
import { setupContextBeforeEach } from '../setup';
describe('TextBlot', function () {
@ -7,20 +9,20 @@ describe('TextBlot', function () {
it('constructor(node)', function () {
let node = document.createTextNode('Test');
let blot = new TextBlot(ctx.scroll, node);
expect(blot.text).toEqual('Test');
expect(blot['text']).toEqual('Test');
expect(blot.domNode.data).toEqual('Test');
});
it('deleteAt() partial', function () {
let blot = ctx.scroll.create('text', 'Test');
let blot = ctx.scroll.create('text', 'Test') as TextBlot;
blot.deleteAt(1, 2);
expect(blot.value()).toEqual('Tt');
expect(blot.length()).toEqual(2);
});
it('deleteAt() all', function () {
let container = ctx.scroll.create('inline');
let textBlot = ctx.scroll.create('text', 'Test');
let container = ctx.scroll.create('inline') as InlineBlot;
let textBlot = ctx.scroll.create('text', 'Test') as TextBlot;
container.appendChild(textBlot);
expect(container.domNode.firstChild).toEqual(textBlot.domNode);
textBlot.deleteAt(0, 4);
@ -28,35 +30,36 @@ describe('TextBlot', function () {
});
it('insertAt() text', function () {
let textBlot = ctx.scroll.create('text', 'Test');
let textBlot = ctx.scroll.create('text', 'Test') as TextBlot;
textBlot.insertAt(1, 'ough');
expect(textBlot.value()).toEqual('Toughest');
});
it('insertAt() other', function () {
let container = ctx.scroll.create('inline');
let textBlot = ctx.scroll.create('text', 'Test');
let container = ctx.scroll.create('inline') as InlineBlot;
let textBlot = ctx.scroll.create('text', 'Test') as TextBlot;
container.appendChild(textBlot);
textBlot.insertAt(2, 'image', {});
expect(textBlot.value()).toEqual('Te');
expect(textBlot.next.statics.blotName).toEqual('image');
expect(textBlot.next.next.value()).toEqual('st');
expect(textBlot.next?.statics.blotName).toEqual('image');
const nextNext = textBlot.next?.next;
expect(nextNext instanceof TextBlot && nextNext.value()).toEqual('st');
});
it('split() middle', function () {
let container = ctx.scroll.create('inline');
let textBlot = ctx.scroll.create('text', 'Test');
let container = ctx.scroll.create('inline') as InlineBlot;
let textBlot = ctx.scroll.create('text', 'Test') as TextBlot;
container.appendChild(textBlot);
let after = textBlot.split(2);
expect(textBlot.value()).toEqual('Te');
expect(after.value()).toEqual('st');
expect(after instanceof TextBlot && after.value()).toEqual('st');
expect(textBlot.next).toEqual(after);
expect(after.prev).toEqual(textBlot);
expect(after instanceof TextBlot && after.prev).toEqual(textBlot);
});
it('split() noop', function () {
let container = ctx.scroll.create('inline');
let textBlot = ctx.scroll.create('text', 'Test');
let container = ctx.scroll.create('inline') as InlineBlot;
let textBlot = ctx.scroll.create('text', 'Test') as TextBlot;
container.appendChild(textBlot);
let before = textBlot.split(0);
let after = textBlot.split(4);
@ -65,52 +68,52 @@ describe('TextBlot', function () {
});
it('split() force', function () {
let container = ctx.scroll.create('inline');
let textBlot = ctx.scroll.create('text', 'Test');
let container = ctx.scroll.create('inline') as InlineBlot;
let textBlot = ctx.scroll.create('text', 'Test') as TextBlot;
container.appendChild(textBlot);
let after = textBlot.split(4, true);
expect(after).not.toEqual(textBlot);
expect(after.value()).toEqual('');
expect(after instanceof TextBlot && after.value()).toEqual('');
expect(textBlot.next).toEqual(after);
expect(after.prev).toEqual(textBlot);
expect(after?.prev).toEqual(textBlot);
});
it('format wrap', function () {
let container = ctx.scroll.create('inline');
let textBlot = ctx.scroll.create('text', 'Test');
let container = ctx.scroll.create('inline') as InlineBlot;
let textBlot = ctx.scroll.create('text', 'Test') as TextBlot;
container.appendChild(textBlot);
textBlot.formatAt(0, 4, 'bold', true);
expect(textBlot.domNode.parentNode.tagName).toEqual('STRONG');
expect(textBlot.domNode.parentElement?.tagName).toEqual('STRONG');
expect(textBlot.value()).toEqual('Test');
});
it('format null', function () {
let container = ctx.scroll.create('inline');
let textBlot = ctx.scroll.create('text', 'Test');
let container = ctx.scroll.create('inline') as InlineBlot;
let textBlot = ctx.scroll.create('text', 'Test') as TextBlot;
container.appendChild(textBlot);
textBlot.formatAt(0, 4, 'bold', null);
expect(textBlot.domNode.parentNode.tagName).toEqual('SPAN');
expect(textBlot.domNode.parentElement?.tagName).toEqual('SPAN');
expect(textBlot.value()).toEqual('Test');
});
it('format split', function () {
let container = ctx.scroll.create('block');
let textBlot = ctx.scroll.create('text', 'Test');
let container = ctx.scroll.create('block') as BlockBlot;
let textBlot = ctx.scroll.create('text', 'Test') as TextBlot;
container.appendChild(textBlot);
textBlot.formatAt(1, 2, 'bold', true);
expect(container.domNode.innerHTML).toEqual('T<strong>es</strong>t');
expect(textBlot.next.statics.blotName).toEqual('bold');
expect(textBlot.next?.statics.blotName).toEqual('bold');
expect(textBlot.value()).toEqual('T');
});
it('index()', function () {
let textBlot = ctx.scroll.create('text', 'Test');
let textBlot = ctx.scroll.create('text', 'Test') as TextBlot;
expect(textBlot.index(textBlot.domNode, 2)).toEqual(2);
expect(textBlot.index(document.body, 2)).toEqual(-1);
});
it('position()', function () {
let textBlot = ctx.scroll.create('text', 'Test');
let textBlot = ctx.scroll.create('text', 'Test') as TextBlot;
let [node, offset] = textBlot.position(2);
expect(node).toEqual(textBlot.domNode);
expect(offset).toEqual(2);

View File

@ -9,7 +9,8 @@
"strict": true,
"moduleResolution": "node",
"noUnusedLocals": true,
"noUnusedParameters": true
"noUnusedParameters": true,
"verbatimModuleSyntax": true
},
"include": ["src"],
"include": ["src", "test"],
}

View File

@ -1,3 +1,4 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite';
export default defineConfig({
@ -10,4 +11,12 @@ export default defineConfig({
},
sourcemap: true,
},
test: {
include: ['./test/unit/*'],
browser: {
enabled: true,
provider: 'playwright',
name: 'chromium',
},
},
});