diff --git a/packages/super-editor/src/core/commands/insertContent.test.js b/packages/super-editor/src/core/commands/insertContent.test.js
index 2557b3bc8a..e6d3e7b56d 100644
--- a/packages/super-editor/src/core/commands/insertContent.test.js
+++ b/packages/super-editor/src/core/commands/insertContent.test.js
@@ -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';
@@ -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');
@@ -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;
@@ -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();
@@ -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();
@@ -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('
- First
- Second
', { contentType: 'html' });
await Promise.resolve();
@@ -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();
@@ -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('', { contentType: 'html' });
await Promise.resolve();
@@ -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(
'',
{ contentType: 'html' },
@@ -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();
@@ -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(
'| Search Query | Findings / Assessment |
|---|
| A | B |
',
{ contentType: 'html' },
@@ -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;
@@ -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('
', { contentType: 'html' });
@@ -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' });
@@ -409,7 +403,7 @@ describe.skipIf(!process.env.CI)('insertContent (integration) horizontal rule',
});
it('insertContent with bare
(no contentType) creates a horizontal rule', async () => {
- const editor = await setupEditor();
+ const editor = setupEditor();
expect(countHorizontalRules(editor)).toBe(0);
editor.commands.insertContent('
');
diff --git a/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts b/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts
index a5043f0138..fe992b6552 100644
--- a/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts
+++ b/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts
@@ -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,
@@ -10395,11 +10395,19 @@ const dryRunVectors: Partial unknown>> = {
},
};
-beforeEach(() => {
+beforeAll(() => {
registerBuiltInExecutors();
registerPartDescriptor(numberingPartDescriptor);
registerPartDescriptor(settingsPartDescriptor);
registerPartDescriptor(stylesPartDescriptor);
+});
+
+afterAll(() => {
+ clearPartDescriptors();
+ clearInvalidationHandlers();
+});
+
+const resetMocks = () => {
vi.restoreAllMocks();
mockedDeps.resolveCommentAnchorsById.mockReset();
mockedDeps.resolveCommentAnchorsById.mockImplementation(() => []);
@@ -10425,11 +10433,10 @@ beforeEach(() => {
refResolverMocks.getSourcesFromConverter.mockImplementation(() => []);
refResolverMocks.findAllAuthorities.mockImplementation(() => []);
refResolverMocks.findAllAuthorityEntries.mockImplementation(() => []);
-});
+};
-afterEach(() => {
- clearPartDescriptors();
- clearInvalidationHandlers();
+beforeEach(() => {
+ resetMocks();
});
describe('document-api adapter conformance', () => {
diff --git a/packages/super-editor/src/extensions/field-annotation/cleanup-commands/cleanUpParagraphWithAnnotation.test.js b/packages/super-editor/src/extensions/field-annotation/cleanup-commands/cleanUpParagraphWithAnnotation.test.js
index d215a004f2..632ab204a4 100644
--- a/packages/super-editor/src/extensions/field-annotation/cleanup-commands/cleanUpParagraphWithAnnotation.test.js
+++ b/packages/super-editor/src/extensions/field-annotation/cleanup-commands/cleanUpParagraphWithAnnotation.test.js
@@ -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;
@@ -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 });
@@ -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')]),
@@ -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
@@ -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",
, "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 });
@@ -111,7 +99,7 @@ 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);
@@ -119,13 +107,8 @@ describe('cleanUpParagraphWithAnnotations – original behavior', () => {
// 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 });
@@ -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')]);
@@ -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 });
diff --git a/packages/super-editor/src/extensions/image/imageHelpers/startImageUpload.collaboration.test.js b/packages/super-editor/src/extensions/image/imageHelpers/startImageUpload.collaboration.test.js
new file mode 100644
index 0000000000..68c082aa1a
--- /dev/null
+++ b/packages/super-editor/src/extensions/image/imageHelpers/startImageUpload.collaboration.test.js
@@ -0,0 +1,256 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+
+// Controllable mock implementations — configure per test
+let mockFindPlaceholder = vi.fn(() => 0);
+let mockRemoveImagePlaceholder = vi.fn((_state, tr) => tr);
+let mockFindOrCreateRelationship = vi.fn(() => 'rId100');
+let mockDefaultUpload = vi.fn();
+let mockGenerateDocxRandomId = vi.fn();
+
+vi.mock('./imageRegistrationPlugin.js', () => ({
+ findPlaceholder: (...args) => mockFindPlaceholder(...args),
+ removeImagePlaceholder: (...args) => mockRemoveImagePlaceholder(...args),
+ addImagePlaceholder: vi.fn(),
+}));
+
+vi.mock('@core/parts/adapters/relationships-mutation.js', () => ({
+ findOrCreateRelationship: (...args) => mockFindOrCreateRelationship(...args),
+}));
+
+vi.mock('./handleImageUpload.js', () => ({
+ handleImageUpload: (...args) => mockDefaultUpload(...args),
+}));
+
+vi.mock('@core/helpers/index.js', () => ({
+ generateDocxRandomId: (...args) => mockGenerateDocxRandomId(...args),
+}));
+
+// Import after vi.mock (hoisted)
+const { uploadAndInsertImage } = await import('./startImageUpload.js');
+
+describe('uploadAndInsertImage collaboration branch (isolated)', () => {
+ beforeEach(() => {
+ mockFindPlaceholder = vi.fn(() => 0);
+ mockRemoveImagePlaceholder = vi.fn((_state, tr) => tr);
+ mockFindOrCreateRelationship = vi.fn(() => 'rId100');
+ mockDefaultUpload = vi.fn();
+ mockGenerateDocxRandomId = vi.fn();
+ });
+
+ it('calls addImageToCollaboration when ydoc is provided', async () => {
+ const collabSpy = vi.fn();
+
+ const editor = {
+ options: {
+ handleImageUpload: vi.fn().mockResolvedValue('http://example.com/image.png'),
+ mode: 'docx',
+ ydoc: {},
+ },
+ commands: {
+ addImageToCollaboration: collabSpy,
+ },
+ storage: {
+ image: { media: {} },
+ },
+ };
+
+ const tr = {
+ replaceWith: vi.fn(() => tr),
+ };
+
+ const view = {
+ state: {
+ tr,
+ schema: {
+ nodes: {
+ image: {
+ create: vi.fn(() => ({ attrs: {} })),
+ },
+ },
+ },
+ },
+ dispatch: vi.fn(),
+ };
+
+ const file = new File([new Uint8Array([1])], 'collab.png', { type: 'image/png' });
+
+ await uploadAndInsertImage({
+ editor,
+ view,
+ file,
+ size: { width: 10, height: 10 },
+ id: {},
+ });
+
+ expect(collabSpy).toHaveBeenCalledWith({
+ mediaPath: 'word/media/collab.png',
+ fileData: 'http://example.com/image.png',
+ });
+ });
+
+ it('falls back when media is unset and file lacks lastModified', async () => {
+ mockFindOrCreateRelationship = vi.fn(() => 'rId200');
+
+ const OriginalFile = globalThis.File;
+ const fileCtorSpy = vi.fn();
+
+ class MockFile {
+ constructor(parts, name, options = {}) {
+ fileCtorSpy({ parts, name, options });
+ this.name = name;
+ this.type = options.type;
+ }
+ }
+
+ globalThis.File = MockFile;
+ const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(123456);
+
+ const editor = {
+ options: {
+ handleImageUpload: vi.fn().mockResolvedValue('data:image/png;base64,CCC'),
+ mode: 'docx',
+ },
+ commands: {
+ addImageToCollaboration: vi.fn(),
+ },
+ storage: {
+ image: {},
+ },
+ state: {
+ doc: {
+ descendants: () => {},
+ },
+ },
+ };
+
+ const backingMedia = {};
+ let firstAccess = true;
+ Object.defineProperty(editor.storage.image, 'media', {
+ configurable: true,
+ get() {
+ if (firstAccess) {
+ firstAccess = false;
+ return undefined;
+ }
+ return backingMedia;
+ },
+ set(value) {
+ Object.assign(backingMedia, value);
+ },
+ });
+
+ const tr = {
+ replaceWith: vi.fn(() => tr),
+ };
+
+ const view = {
+ state: {
+ tr,
+ schema: {
+ nodes: {
+ image: {
+ create: vi.fn(() => ({ attrs: {} })),
+ },
+ },
+ },
+ },
+ dispatch: vi.fn(),
+ };
+
+ const sourceFile = { name: 'Screenshot 2025.png', type: 'image/png', size: 10 };
+
+ try {
+ await uploadAndInsertImage({
+ editor,
+ view,
+ file: sourceFile,
+ size: { width: 10, height: 10 },
+ id: {},
+ });
+ } finally {
+ globalThis.File = OriginalFile;
+ nowSpy.mockRestore();
+ delete editor.storage.image.media;
+ editor.storage.image.media = backingMedia;
+ }
+
+ expect(fileCtorSpy).toHaveBeenCalledTimes(1);
+ const [[callArgs]] = fileCtorSpy.mock.calls;
+ expect(callArgs.name).toBe('Screenshot_2025.png');
+ expect(callArgs.options.lastModified).toBe(123456);
+
+ expect(editor.options.handleImageUpload).toHaveBeenCalledWith(expect.any(MockFile));
+ expect(backingMedia).toHaveProperty('word/media/Screenshot_2025.png');
+ expect(mockFindPlaceholder).toHaveBeenCalled();
+ expect(mockRemoveImagePlaceholder).toHaveBeenCalled();
+ });
+
+ it('uses default upload handler and skips duplicate docPr ids', async () => {
+ mockDefaultUpload.mockResolvedValue('data:image/png;base64,DDD');
+ const relationshipSpy = vi.fn(() => 'rId500');
+ mockFindOrCreateRelationship = relationshipSpy;
+ mockGenerateDocxRandomId.mockReturnValueOnce('0000007b').mockReturnValueOnce('0000007c');
+
+ const imageCreateSpy = vi.fn(() => ({ attrs: {} }));
+
+ const editor = {
+ options: {
+ mode: 'docx',
+ },
+ commands: {
+ addImageToCollaboration: vi.fn(),
+ },
+ storage: {
+ image: { media: {} },
+ },
+ state: {
+ doc: {
+ descendants: (callback) => {
+ callback({
+ type: { name: 'image' },
+ attrs: { id: '123' },
+ });
+ },
+ },
+ },
+ };
+
+ const tr = {
+ replaceWith: vi.fn(() => tr),
+ };
+
+ const view = {
+ state: {
+ tr,
+ schema: {
+ nodes: {
+ image: {
+ create: imageCreateSpy,
+ },
+ },
+ },
+ },
+ dispatch: vi.fn(),
+ };
+
+ const basicFile = new File([new Uint8Array([1])], 'image.png', { type: 'image/png' });
+
+ await uploadAndInsertImage({
+ editor,
+ view,
+ file: basicFile,
+ size: { width: 20, height: 20 },
+ id: {},
+ });
+
+ expect(mockDefaultUpload).toHaveBeenCalledTimes(1);
+ expect(relationshipSpy).toHaveBeenCalledWith(editor, 'startImageUpload:addImageRelationship', {
+ target: 'media/image.png',
+ type: 'image',
+ });
+ const createdNodeAttrs = imageCreateSpy.mock.calls[0][0];
+ expect(createdNodeAttrs.id).toBe('124');
+
+ expect(mockGenerateDocxRandomId).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/packages/super-editor/src/extensions/image/imageHelpers/startImageUpload.test.js b/packages/super-editor/src/extensions/image/imageHelpers/startImageUpload.test.js
index ce36fe4b63..fff31f6fb6 100644
--- a/packages/super-editor/src/extensions/image/imageHelpers/startImageUpload.test.js
+++ b/packages/super-editor/src/extensions/image/imageHelpers/startImageUpload.test.js
@@ -275,272 +275,3 @@ describe('image upload helpers integration', () => {
uploadStub.mockRestore();
});
});
-
-describe('uploadAndInsertImage collaboration branch (isolated)', () => {
- afterEach(() => {
- vi.resetModules();
- });
-
- it('calls addImageToCollaboration when ydoc is provided', async () => {
- vi.resetModules();
-
- vi.doMock('./imageRegistrationPlugin.js', () => ({
- findPlaceholder: () => 0,
- removeImagePlaceholder: (_state, tr) => tr,
- addImagePlaceholder: vi.fn(),
- }));
-
- vi.doMock('@core/parts/adapters/relationships-mutation.js', () => ({
- findOrCreateRelationship: vi.fn(() => 'rId100'),
- }));
-
- const { uploadAndInsertImage } = await import('./startImageUpload.js');
-
- const collabSpy = vi.fn();
-
- const editor = {
- options: {
- handleImageUpload: vi.fn().mockResolvedValue('http://example.com/image.png'),
- mode: 'docx',
- ydoc: {},
- },
- commands: {
- addImageToCollaboration: collabSpy,
- },
- storage: {
- image: { media: {} },
- },
- };
-
- const tr = {
- replaceWith: vi.fn(() => tr),
- };
-
- const view = {
- state: {
- tr,
- schema: {
- nodes: {
- image: {
- create: vi.fn(() => ({ attrs: {} })),
- },
- },
- },
- },
- dispatch: vi.fn(),
- };
-
- const file = new File([new Uint8Array([1])], 'collab.png', { type: 'image/png' });
-
- await uploadAndInsertImage({
- editor,
- view,
- file,
- size: { width: 10, height: 10 },
- id: {},
- });
-
- expect(collabSpy).toHaveBeenCalledWith({
- mediaPath: 'word/media/collab.png',
- fileData: 'http://example.com/image.png',
- });
- });
-
- it('falls back when media is unset and file lacks lastModified', async () => {
- vi.resetModules();
-
- const findPlaceholder = vi.fn(() => 0);
- const removeImagePlaceholder = vi.fn((_, tr) => tr);
-
- vi.doMock('./imageRegistrationPlugin.js', () => ({
- findPlaceholder,
- removeImagePlaceholder,
- addImagePlaceholder: vi.fn(),
- }));
-
- vi.doMock('@core/parts/adapters/relationships-mutation.js', () => ({
- findOrCreateRelationship: vi.fn(() => 'rId200'),
- }));
-
- const OriginalFile = globalThis.File;
- const fileCtorSpy = vi.fn();
-
- class MockFile {
- constructor(parts, name, options = {}) {
- fileCtorSpy({ parts, name, options });
- this.name = name;
- this.type = options.type;
- }
- }
-
- globalThis.File = MockFile;
- const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(123456);
-
- const { uploadAndInsertImage } = await import('./startImageUpload.js');
-
- const editor = {
- options: {
- handleImageUpload: vi.fn().mockResolvedValue('data:image/png;base64,CCC'),
- mode: 'docx',
- },
- commands: {
- addImageToCollaboration: vi.fn(),
- },
- storage: {
- image: {},
- },
- state: {
- doc: {
- descendants: () => {},
- },
- },
- };
-
- const backingMedia = {};
- let firstAccess = true;
- Object.defineProperty(editor.storage.image, 'media', {
- configurable: true,
- get() {
- if (firstAccess) {
- firstAccess = false;
- return undefined;
- }
- return backingMedia;
- },
- set(value) {
- Object.assign(backingMedia, value);
- },
- });
-
- const tr = {
- replaceWith: vi.fn(() => tr),
- };
-
- const view = {
- state: {
- tr,
- schema: {
- nodes: {
- image: {
- create: vi.fn(() => ({ attrs: {} })),
- },
- },
- },
- },
- dispatch: vi.fn(),
- };
-
- const sourceFile = { name: 'Screenshot 2025.png', type: 'image/png', size: 10 };
-
- try {
- await uploadAndInsertImage({
- editor,
- view,
- file: sourceFile,
- size: { width: 10, height: 10 },
- id: {},
- });
- } finally {
- globalThis.File = OriginalFile;
- nowSpy.mockRestore();
- delete editor.storage.image.media;
- editor.storage.image.media = backingMedia;
- }
-
- expect(fileCtorSpy).toHaveBeenCalledTimes(1);
- const [[callArgs]] = fileCtorSpy.mock.calls;
- expect(callArgs.name).toBe('Screenshot_2025.png');
- expect(callArgs.options.lastModified).toBe(123456);
-
- expect(editor.options.handleImageUpload).toHaveBeenCalledWith(expect.any(MockFile));
- expect(backingMedia).toHaveProperty('word/media/Screenshot_2025.png');
- expect(findPlaceholder).toHaveBeenCalled();
- expect(removeImagePlaceholder).toHaveBeenCalled();
- });
-
- it('uses default upload handler and skips duplicate docPr ids', async () => {
- vi.resetModules();
-
- const defaultUpload = vi.fn().mockResolvedValue('data:image/png;base64,DDD');
- vi.doMock('./handleImageUpload.js', () => ({
- handleImageUpload: defaultUpload,
- }));
- vi.doMock('./imageRegistrationPlugin.js', () => ({
- findPlaceholder: () => 0,
- removeImagePlaceholder: (_state, tr) => tr,
- addImagePlaceholder: vi.fn(),
- }));
- const relationshipSpy = vi.fn(() => 'rId500');
- vi.doMock('@core/parts/adapters/relationships-mutation.js', () => ({
- findOrCreateRelationship: relationshipSpy,
- }));
- const randomSpy = vi.fn().mockReturnValueOnce('0000007b').mockReturnValueOnce('0000007c');
- vi.doMock('@core/helpers/index.js', () => ({
- generateDocxRandomId: randomSpy,
- }));
-
- const { uploadAndInsertImage } = await import('./startImageUpload.js');
-
- const imageCreateSpy = vi.fn(() => ({ attrs: {} }));
-
- const editor = {
- options: {
- mode: 'docx',
- },
- commands: {
- addImageToCollaboration: vi.fn(),
- },
- storage: {
- image: { media: {} },
- },
- state: {
- doc: {
- descendants: (callback) => {
- callback({
- type: { name: 'image' },
- attrs: { id: '123' },
- });
- },
- },
- },
- };
-
- const tr = {
- replaceWith: vi.fn(() => tr),
- };
-
- const view = {
- state: {
- tr,
- schema: {
- nodes: {
- image: {
- create: imageCreateSpy,
- },
- },
- },
- },
- dispatch: vi.fn(),
- };
-
- const basicFile = new File([new Uint8Array([1])], 'image.png', { type: 'image/png' });
-
- await uploadAndInsertImage({
- editor,
- view,
- file: basicFile,
- size: { width: 20, height: 20 },
- id: {},
- });
-
- expect(defaultUpload).toHaveBeenCalledTimes(1);
- expect(relationshipSpy).toHaveBeenCalledWith(editor, 'startImageUpload:addImageRelationship', {
- target: 'media/image.png',
- type: 'image',
- });
- const createdNodeAttrs = imageCreateSpy.mock.calls[0][0];
- expect(createdNodeAttrs.id).toBe('124');
-
- expect(randomSpy).toHaveBeenCalledTimes(2);
- });
-});