PluginProbe
Extendify / 3.1.1
Extendify v3.1.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / tests / unit / QuickEdit / lib / save-bridge.test.js

save-bridge.test.js in Extendify 3.1.1, at tests/unit/QuickEdit/lib/save-bridge.test.js

112 lines 3.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Pins the save-bridge contract: register/unregister, saveSelected is a
2 // no-op without a registered saver, the agent-submit window event fires
3 // the saver with alsoClear: true.
4
5 import {
6 AGENT_SUBMIT_EVENT,
7 hasSaver,
8 registerSaver,
9 saveSelected,
10 unregisterSaver,
11 } from '@quick-edit/lib/save-bridge';
12
13 afterEach(() => {
14 unregisterSaver(unregisterSaver);
15 });
16
17 describe('save-bridge', () => {
18 it('hasSaver is false before any registration', () => {
19 expect(hasSaver()).toBe(false);
20 });
21
22 it('register + saveSelected fires the registered fn', async () => {
23 const fn = jest.fn().mockResolvedValue('ok');
24 registerSaver(fn);
25 expect(hasSaver()).toBe(true);
26 await saveSelected();
27 expect(fn).toHaveBeenCalledTimes(1);
28 expect(fn).toHaveBeenCalledWith({});
29 unregisterSaver(fn);
30 });
31
32 it('saveSelected forwards options through to the saver', async () => {
33 const fn = jest.fn().mockResolvedValue('ok');
34 registerSaver(fn);
35 await saveSelected({ alsoClear: false });
36 expect(fn).toHaveBeenCalledWith({ alsoClear: false });
37 unregisterSaver(fn);
38 });
39
40 it('unregister leaves saveSelected a no-op', async () => {
41 const fn = jest.fn();
42 registerSaver(fn);
43 unregisterSaver(fn);
44 expect(hasSaver()).toBe(false);
45 await saveSelected();
46 expect(fn).not.toHaveBeenCalled();
47 });
48
49 it('agent-submit window event fires the saver with alsoClear: true', () => {
50 const fn = jest.fn();
51 registerSaver(fn);
52 window.dispatchEvent(new CustomEvent(AGENT_SUBMIT_EVENT));
53 expect(fn).toHaveBeenCalledWith({ alsoClear: true });
54 unregisterSaver(fn);
55 });
56
57 // Same-origin trigger surface: the agent-submit listener
58 // must NOT read event.detail. A hostile same-origin script (a second
59 // plugin, a stored-XSS elsewhere) can dispatch this event, but it can only
60 // flush the editor's already-staged content — never inject its own. If the
61 // listener ever forwarded e.detail, an attacker could choose what gets
62 // saved without the user's intent. Dispatch a poisoned payload and assert
63 // the saver still receives exactly { alsoClear: true } and nothing else.
64 it('agent-submit ignores attacker-supplied event detail', () => {
65 const fn = jest.fn();
66 registerSaver(fn);
67 window.dispatchEvent(
68 new CustomEvent(AGENT_SUBMIT_EVENT, {
69 detail: {
70 alsoClear: false,
71 rawBlock: '<img src=x onerror=alert(1)>',
72 source: { kind: 'post', id: 999 },
73 },
74 }),
75 );
76 expect(fn).toHaveBeenCalledTimes(1);
77 const args = fn.mock.calls[0][0];
78 expect(args).toEqual({ alsoClear: true });
79 expect(args).not.toHaveProperty('rawBlock');
80 expect(args).not.toHaveProperty('source');
81 unregisterSaver(fn);
82 });
83
84 it('agent-submit event is a no-op when no saver is registered', () => {
85 expect(() => {
86 window.dispatchEvent(new CustomEvent(AGENT_SUBMIT_EVENT));
87 }).not.toThrow();
88 });
89
90 it('register twice replaces — only the latest saver fires', () => {
91 const first = jest.fn();
92 const second = jest.fn();
93 registerSaver(first);
94 registerSaver(second);
95 window.dispatchEvent(new CustomEvent(AGENT_SUBMIT_EVENT));
96 expect(first).not.toHaveBeenCalled();
97 expect(second).toHaveBeenCalledTimes(1);
98 unregisterSaver(second);
99 });
100
101 it('unregister of a different fn does not clear the active saver', async () => {
102 const active = jest.fn();
103 const ghost = jest.fn();
104 registerSaver(active);
105 unregisterSaver(ghost);
106 expect(hasSaver()).toBe(true);
107 await saveSelected();
108 expect(active).toHaveBeenCalledTimes(1);
109 unregisterSaver(active);
110 });
111 });
112