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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 29 additions & 35 deletions packages/super-editor/src/core/commands/insertContent.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest';
import { insertContent } from './insertContent.js';
import * as contentProcessor from '../helpers/contentProcessor.js';

Expand Down Expand Up @@ -161,11 +161,14 @@ describe('insertContent', () => {

// Integration-style tests that use a real Editor instance to
// insert markdown/HTML lists and verify exported OOXML has list numbering.
//
// These tests need the REAL contentProcessor (not the mock from the unit tests
// above). We use a separate vi.mock-free import path by dynamically importing
// the real insertContent function.
describe('insertContent (integration) list export', () => {
// Cache loaded DOCX data and helpers to avoid repeated file loading
let cachedDocxData = null;
let helpers = null;
let exportHelpers = null;
let cachedDocxData = null;

const getListParagraphs = (result) => {
const body = result.elements?.find((el) => el.name === 'w:body');
Expand All @@ -185,22 +188,16 @@ describe('insertContent (integration) list export', () => {
return { numId, ilvl };
};

const setupEditor = async () => {
// Use real content processor for these tests
// Load helpers and DOCX data once for all integration tests
beforeAll(async () => {
vi.resetModules();
vi.doUnmock('../helpers/contentProcessor.js');
helpers = await import('../../tests/helpers/helpers.js');
cachedDocxData = await helpers.loadTestDataForEditorTests('blank-doc.docx');
exportHelpers = await import('../../tests/export/export-helpers/index.js');
});

// Cache helpers and DOCX data on first call
if (!helpers) {
helpers = await import('../../tests/helpers/helpers.js');
}
if (!cachedDocxData) {
cachedDocxData = await helpers.loadTestDataForEditorTests('blank-doc.docx');
}
if (!exportHelpers) {
exportHelpers = await import('../../tests/export/export-helpers/index.js');
}

const setupEditor = () => {
const { docx, media, mediaFiles, fonts } = cachedDocxData;
const { editor } = helpers.initTestEditor({ content: docx, media, mediaFiles, fonts, mode: 'docx' });
return editor;
Expand All @@ -212,7 +209,7 @@ describe('insertContent (integration) list export', () => {
};

it('exports ordered list from markdown with numId/ilvl', async () => {
const editor = await setupEditor();
const editor = setupEditor();
editor.commands.insertContent('1. One\n2. Two', { contentType: 'markdown' });
await Promise.resolve();

Expand All @@ -230,7 +227,7 @@ describe('insertContent (integration) list export', () => {
});

it('exports unordered list from markdown with numId/ilvl', async () => {
const editor = await setupEditor();
const editor = setupEditor();
editor.commands.insertContent('- Alpha\n- Beta', { contentType: 'markdown' });
await Promise.resolve();

Expand All @@ -248,7 +245,7 @@ describe('insertContent (integration) list export', () => {
});

it('exports ordered list from HTML with numId/ilvl', async () => {
const editor = await setupEditor();
const editor = setupEditor();
editor.commands.insertContent('<ol><li>First</li><li>Second</li></ol>', { contentType: 'html' });
await Promise.resolve();

Expand All @@ -262,7 +259,7 @@ describe('insertContent (integration) list export', () => {
});

it('inserts markdown heading + bold text without creating a table', async () => {
const editor = await setupEditor();
const editor = setupEditor();

editor.commands.insertContent('# Hello\n\nSome **bold** text', { contentType: 'markdown' });
await Promise.resolve();
Expand All @@ -280,7 +277,7 @@ describe('insertContent (integration) list export', () => {
});

it('exports unordered list from HTML with numId/ilvl', async () => {
const editor = await setupEditor();
const editor = setupEditor();
editor.commands.insertContent('<ul><li>Apple</li><li>Banana</li></ul>', { contentType: 'html' });
await Promise.resolve();

Expand All @@ -294,7 +291,7 @@ describe('insertContent (integration) list export', () => {
});

it('defaults imported HTML tables to 100% width', async () => {
const editor = await setupEditor();
const editor = setupEditor();
editor.commands.insertContent(
'<table><tbody><tr><td>Query</td><td>Assessment</td></tr><tr><td>A</td><td>B</td></tr></tbody></table>',
{ contentType: 'html' },
Expand All @@ -310,7 +307,7 @@ describe('insertContent (integration) list export', () => {
});

it('defaults imported markdown tables to 100% width', async () => {
const editor = await setupEditor();
const editor = setupEditor();
editor.commands.insertContent('| Query | Assessment |\n| --- | --- |\n| A | B |', { contentType: 'markdown' });
await Promise.resolve();

Expand All @@ -323,7 +320,7 @@ describe('insertContent (integration) list export', () => {
});

it('does not inject inline cell borders on imported HTML table headers', async () => {
const editor = await setupEditor();
const editor = setupEditor();
editor.commands.insertContent(
'<table><thead><tr><th>Search Query</th><th>Findings / Assessment</th></tr></thead><tbody><tr><td>A</td><td>B</td></tr></tbody></table>',
{ contentType: 'html' },
Expand Down Expand Up @@ -358,17 +355,14 @@ describe.skipIf(!process.env.CI)('insertContent (integration) horizontal rule',
let helpers = null;
let cachedDocxData = null;

const setupEditor = async () => {
beforeAll(async () => {
vi.resetModules();
vi.doUnmock('../helpers/contentProcessor.js');
helpers = await import('../../tests/helpers/helpers.js');
cachedDocxData = await helpers.loadTestDataForEditorTests('blank-doc.docx');
});

if (!helpers) {
helpers = await import('../../tests/helpers/helpers.js');
}
if (!cachedDocxData) {
cachedDocxData = await helpers.loadTestDataForEditorTests('blank-doc.docx');
}

const setupEditor = () => {
const { docx, media, mediaFiles, fonts } = cachedDocxData;
const { editor } = helpers.initTestEditor({ content: docx, media, mediaFiles, fonts, mode: 'docx' });
return editor;
Expand All @@ -391,7 +385,7 @@ describe.skipIf(!process.env.CI)('insertContent (integration) horizontal rule',
};

it('insertContent with contentType html creates a horizontal rule', async () => {
const editor = await setupEditor();
const editor = setupEditor();
expect(countHorizontalRules(editor)).toBe(0);

editor.commands.insertContent('<hr>', { contentType: 'html' });
Expand All @@ -400,7 +394,7 @@ describe.skipIf(!process.env.CI)('insertContent (integration) horizontal rule',
});

it('insertContent with contentType markdown creates a horizontal rule', async () => {
const editor = await setupEditor();
const editor = setupEditor();
expect(countHorizontalRules(editor)).toBe(0);

editor.commands.insertContent('---', { contentType: 'markdown' });
Expand All @@ -409,7 +403,7 @@ describe.skipIf(!process.env.CI)('insertContent (integration) horizontal rule',
});

it('insertContent with bare <hr> (no contentType) creates a horizontal rule', async () => {
const editor = await setupEditor();
const editor = setupEditor();
expect(countHorizontalRules(editor)).toBe(0);

editor.commands.insertContent('<hr>');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Node as ProseMirrorNode } from 'prosemirror-model';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import type { Editor } from '../../core/Editor.js';
import {
COMMAND_CATALOG,
Expand Down Expand Up @@ -10395,11 +10395,19 @@ const dryRunVectors: Partial<Record<OperationId, () => unknown>> = {
},
};

beforeEach(() => {
beforeAll(() => {
registerBuiltInExecutors();
registerPartDescriptor(numberingPartDescriptor);
registerPartDescriptor(settingsPartDescriptor);
registerPartDescriptor(stylesPartDescriptor);
});

afterAll(() => {
clearPartDescriptors();
clearInvalidationHandlers();
});

const resetMocks = () => {
vi.restoreAllMocks();
mockedDeps.resolveCommentAnchorsById.mockReset();
mockedDeps.resolveCommentAnchorsById.mockImplementation(() => []);
Expand All @@ -10425,11 +10433,10 @@ beforeEach(() => {
refResolverMocks.getSourcesFromConverter.mockImplementation(() => []);
refResolverMocks.findAllAuthorities.mockImplementation(() => []);
refResolverMocks.findAllAuthorityEntries.mockImplementation(() => []);
});
};

afterEach(() => {
clearPartDescriptors();
clearInvalidationHandlers();
beforeEach(() => {
resetMocks();
});

describe('document-api adapter conformance', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ if (!Object.getOwnPropertyDescriptor(PMNode.prototype, 'children')) {
});
}

// Controllable mock — set mockAnnotations before each test to control return value
let mockAnnotations = [];

vi.mock('../fieldAnnotationHelpers/index.js', () => ({
findFieldAnnotationsByFieldId: vi.fn(() => mockAnnotations),
}));

// Import AFTER vi.mock so the command picks up the hoisted mock
const { cleanUpParagraphWithAnnotations } = await import('./index.js');

// Local helpers
const schema = basic;
const p = schema.nodes.paragraph;
Expand All @@ -24,27 +34,19 @@ const makeAnnotationAt = (state, pos) => {
};
const hardBreak = schema.nodes.hard_break;
const textContent = (docNode) => docNode.textContent;
const mockHelperPath = '../fieldAnnotationHelpers/index.js';

describe('cleanUpParagraphWithAnnotations - test range error crash', () => {
beforeEach(() => {
vi.resetModules();
});
beforeEach(() => {
mockAnnotations = [];
});

describe('cleanUpParagraphWithAnnotations - test range error crash', () => {
/** Test to fix error in position out of range in original */
it('throws RangeError "Position … out of range" on single-paragraph doc', async () => {
it('throws RangeError "Position … out of range" on single-paragraph doc', () => {
const doc = schema.node('doc', null, [p.createAndFill(null, [text('A')])]);
const state = makeState(doc);
const tr = state.tr;

const ann = makeAnnotationAt(state, 1);

vi.doMock('../fieldAnnotationHelpers/index.js', () => ({
findFieldAnnotationsByFieldId: vi.fn(() => [ann]),
}));

// Import AFTER mocking so the command picks up the mocked helper
const { cleanUpParagraphWithAnnotations } = await import('./index.js');
mockAnnotations = [makeAnnotationAt(state, 1)];

const cmd = cleanUpParagraphWithAnnotations(['field-x']);
const run = () => cmd({ dispatch: () => {}, tr, state });
Expand All @@ -54,11 +56,7 @@ describe('cleanUpParagraphWithAnnotations - test range error crash', () => {
});

describe('cleanUpParagraphWithAnnotations – original behavior', () => {
beforeEach(() => {
vi.resetModules();
});

it('deletes a single-child paragraph (non-last) annotated node', async () => {
it('deletes a single-child paragraph (non-last) annotated node', () => {
// doc: [ p("REMOVE_ME"), p("keep this") ]
const doc = schema.node('doc', null, [
p.createAndFill(null, [text('REMOVE_ME')]),
Expand All @@ -68,13 +66,8 @@ describe('cleanUpParagraphWithAnnotations – original behavior', () => {
const tr = state.tr;

// Annotate inside the first paragraph
const ann = makeAnnotationAt(state, 1);
mockAnnotations = [makeAnnotationAt(state, 1)];

vi.doMock(mockHelperPath, () => ({
findFieldAnnotationsByFieldId: vi.fn(() => [ann]),
}));

const { cleanUpParagraphWithAnnotations } = await import('./index.js');
const cmd = cleanUpParagraphWithAnnotations(['field-x']);

// no-op dispatch is fine; we inspect tr afterwards
Expand All @@ -89,20 +82,15 @@ describe('cleanUpParagraphWithAnnotations – original behavior', () => {
expect(textContent(after)).not.toMatch(/REMOVE_ME/);
});

it('no-ops when parent has >= 2 inline children (e.g., text + hardBreak + text)', async () => {
it('no-ops when parent has >= 2 inline children (e.g., text + hardBreak + text)', () => {
// p("A", <br/>, "B") -> childCount >= 2 -> guard should fail -> no deletion
const para = p.createAndFill(null, [text('A'), hardBreak.create(), text('B')]);
const doc = schema.node('doc', null, [para]);
const state = makeState(doc);
const tr = state.tr;

const ann = makeAnnotationAt(state, 1);

vi.doMock(mockHelperPath, () => ({
findFieldAnnotationsByFieldId: vi.fn(() => [ann]),
}));
mockAnnotations = [makeAnnotationAt(state, 1)];

const { cleanUpParagraphWithAnnotations } = await import('./index.js');
const cmd = cleanUpParagraphWithAnnotations(['field-x']);
const run = () => cmd({ dispatch: () => {}, tr, state });

Expand All @@ -111,21 +99,16 @@ describe('cleanUpParagraphWithAnnotations – original behavior', () => {
expect(textContent(tr.doc)).toContain('AB');
});

it('no-ops when the annotation node does not equal the current node at mapped position', async () => {
it('no-ops when the annotation node does not equal the current node at mapped position', () => {
// doc: [ p("X"), p("Y") ]
const doc = schema.node('doc', null, [p.createAndFill(null, [text('X')]), p.createAndFill(null, [text('Y')])]);
const state = makeState(doc);
const tr = state.tr;

// Build an "annotation" object whose node DOES NOT equal the current node at pos 1
// We fake it by using a different node instance/type (paragraph node) so node.eq(currentNode) is false.
const badAnn = { pos: 1, node: state.doc.child(0) /* paragraph, not text node */ };

vi.doMock(mockHelperPath, () => ({
findFieldAnnotationsByFieldId: vi.fn(() => [badAnn]),
}));
mockAnnotations = [{ pos: 1, node: state.doc.child(0) /* paragraph, not text node */ }];

const { cleanUpParagraphWithAnnotations } = await import('./index.js');
const cmd = cleanUpParagraphWithAnnotations(['field-x']);
const run = () => cmd({ dispatch: () => {}, tr, state });

Expand All @@ -134,7 +117,7 @@ describe('cleanUpParagraphWithAnnotations – original behavior', () => {
expect(textContent(tr.doc)).toMatch(/^XY$/);
});

it('handles multiple annotations by queuing and deleting them in descending order', async () => {
it('handles multiple annotations by queuing and deleting them in descending order', () => {
// doc: [ p("FIRST"), p("MID"), p("SECOND") ]
// Annotate inside FIRST and SECOND (both single-child paragraphs).
const first = p.createAndFill(null, [text('FIRST')]);
Expand All @@ -154,11 +137,8 @@ describe('cleanUpParagraphWithAnnotations – original behavior', () => {
const ann1 = makeAnnotationAt(state, 1);
const ann2 = makeAnnotationAt(state, secondTextPos);

vi.doMock(mockHelperPath, () => ({
findFieldAnnotationsByFieldId: vi.fn(() => [ann1, ann2]),
}));
mockAnnotations = [ann1, ann2];

const { cleanUpParagraphWithAnnotations } = await import('./index.js');
const cmd = cleanUpParagraphWithAnnotations(['field-x']);
const run = () => cmd({ dispatch: () => {}, tr, state });

Expand Down
Loading
Loading