Skip to content

Commit 71430ed

Browse files
committed
test(image): add unit tests for relative image registration edge cases
Add three test cases for relative URL image registration: - Duplicate relative URLs: Verify that when two images with the same relative src arrive during loading, urlToFile is only called once due to pendingRelativeRegistrations deduplication guard. - Fetch failure: Verify that when urlToFile returns null (network error, 404, etc.), no media store entry or rId is created, and no transaction is dispatched. - Exact fallback size values: Update the existing fallback size test to assert exact EMU values (300×200 px fallback) instead of just truthy, catching regressions if defaults are changed. Follows Conventional Commits style per CONTRIBUTING.md guidelines.
1 parent 7bef180 commit 71430ed

1 file changed

Lines changed: 48 additions & 2 deletions

File tree

packages/super-editor/src/extensions/image/imageHelpers/imageRegistrationPlugin.browser.test.js

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { flushPromises } from '@vue/test-utils';
23

34
// ── Hoisted mock objects (available to vi.mock factories) ─────────────
45
const { mockDecoSet, mockPluginKeyInstance } = vi.hoisted(() => {
@@ -69,8 +70,6 @@ import { urlToFile, validateUrlAccessibility } from './handleUrl';
6970
import { addImageRelationship } from '@extensions/image/imageHelpers/startImageUpload.js';
7071

7172
// ── Helpers ───────────────────────────────────────────────────────────
72-
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0));
73-
7473
const createImageNode = (attrs) => ({
7574
type: { name: 'image' },
7675
attrs,
@@ -241,4 +240,51 @@ describe('registerRelativeImages (via handleBrowserPath)', () => {
241240
});
242241
expect(view.dispatch).toHaveBeenCalled();
243242
});
243+
244+
it('skips duplicate relative URL when the first is still being registered', async () => {
245+
// Create a deferred promise so the first urlToFile call hangs
246+
let resolveFirst;
247+
const firstCall = new Promise((r) => (resolveFirst = r));
248+
urlToFile.mockReturnValueOnce(firstCall);
249+
250+
const imageA = { node: createImageNode({ src: 'assets/dup.png' }), pos: 0, id: {} };
251+
const imageB = { node: createImageNode({ src: 'assets/dup.png' }), pos: 5, id: {} };
252+
253+
// Both arrive in the same batch
254+
handleBrowserPath([imageA, imageB], editor, view, { tr: createTrStub() });
255+
await flushPromises();
256+
257+
// Only one fetch is initiated — the second is blocked by pendingRelativeRegistrations
258+
expect(urlToFile).toHaveBeenCalledTimes(1);
259+
260+
// Complete the first registration
261+
resolveFirst(null);
262+
await flushPromises();
263+
264+
// pendingRelativeRegistrations is cleaned up so a future insert could register again
265+
expect(editor.storage.image.pendingRelativeRegistrations.size).toBe(0);
266+
});
267+
268+
it('does not register or dispatch when urlToFile returns null (fetch failure)', async () => {
269+
urlToFile.mockResolvedValueOnce(null);
270+
271+
const foundImages = [{ node: createImageNode({ src: 'assets/missing.png' }), pos: 0, id: {} }];
272+
273+
handleBrowserPath(foundImages, editor, view, { tr: createTrStub() });
274+
await flushPromises();
275+
276+
expect(urlToFile).toHaveBeenCalledWith('assets/missing.png', 'missing.png');
277+
278+
// No media stored
279+
expect(Object.keys(editor.storage.image.media)).toHaveLength(0);
280+
281+
// No relationship created
282+
expect(addImageRelationship).not.toHaveBeenCalled();
283+
284+
// No transaction dispatched
285+
expect(view.dispatch).not.toHaveBeenCalled();
286+
287+
// Pending set cleaned up so the src can be retried later
288+
expect(editor.storage.image.pendingRelativeRegistrations.has('assets/missing.png')).toBe(false);
289+
});
244290
});

0 commit comments

Comments
 (0)