| 1 |
// Net-new helper in the rebuild — mounts a single body-level <div> for |
| 2 |
// wp-components portals. The React root is lazy and reused across |
| 3 |
// mountModal calls; closeModal tears down both the React root and the |
| 4 |
// DOM node and optionally reloads the page. |
| 5 |
|
| 6 |
jest.mock('@wordpress/element', () => { |
| 7 |
const render = jest.fn(); |
| 8 |
const unmount = jest.fn(); |
| 9 |
const createRoot = jest.fn(() => ({ render, unmount })); |
| 10 |
return { |
| 11 |
createRoot, |
| 12 |
__getRoot: () => ({ render, unmount, createRoot }), |
| 13 |
}; |
| 14 |
}); |
| 15 |
|
| 16 |
let renderFn; |
| 17 |
let unmountFn; |
| 18 |
let createRootFn; |
| 19 |
|
| 20 |
beforeEach(() => { |
| 21 |
jest.resetModules(); |
| 22 |
document.body.innerHTML = ''; |
| 23 |
const handles = require('@wordpress/element').__getRoot(); |
| 24 |
renderFn = handles.render; |
| 25 |
unmountFn = handles.unmount; |
| 26 |
createRootFn = handles.createRoot; |
| 27 |
renderFn.mockClear(); |
| 28 |
unmountFn.mockClear(); |
| 29 |
createRootFn.mockClear(); |
| 30 |
}); |
| 31 |
|
| 32 |
describe('mountModal', () => { |
| 33 |
it('creates the body-level root node with id="extendify-quick-edit-modal-root"', async () => { |
| 34 |
const { mountModal } = await import('@quick-edit/lib/modal-root'); |
| 35 |
mountModal(<div>hi</div>); |
| 36 |
const node = document.getElementById('extendify-quick-edit-modal-root'); |
| 37 |
expect(node).not.toBeNull(); |
| 38 |
expect(node.parentElement).toBe(document.body); |
| 39 |
}); |
| 40 |
|
| 41 |
it('calls createRoot once and reuses it across repeated mounts', async () => { |
| 42 |
const { mountModal } = await import('@quick-edit/lib/modal-root'); |
| 43 |
mountModal(<div>a</div>); |
| 44 |
mountModal(<div>b</div>); |
| 45 |
mountModal(<div>c</div>); |
| 46 |
expect(createRootFn).toHaveBeenCalledTimes(1); |
| 47 |
expect(renderFn).toHaveBeenCalledTimes(3); |
| 48 |
}); |
| 49 |
|
| 50 |
it('forwards the React element to the root.render call', async () => { |
| 51 |
const { mountModal } = await import('@quick-edit/lib/modal-root'); |
| 52 |
const element = <span data-x="1">hi</span>; |
| 53 |
mountModal(element); |
| 54 |
expect(renderFn).toHaveBeenCalledWith(element); |
| 55 |
}); |
| 56 |
|
| 57 |
it('re-creates the node when it was removed from the DOM externally', async () => { |
| 58 |
const { mountModal } = await import('@quick-edit/lib/modal-root'); |
| 59 |
mountModal(<div>a</div>); |
| 60 |
document.getElementById('extendify-quick-edit-modal-root').remove(); |
| 61 |
mountModal(<div>b</div>); |
| 62 |
expect( |
| 63 |
document.getElementById('extendify-quick-edit-modal-root'), |
| 64 |
).not.toBeNull(); |
| 65 |
}); |
| 66 |
}); |
| 67 |
|
| 68 |
describe('closeModal', () => { |
| 69 |
it('unmounts the React root and removes the DOM node when no reload arg is passed', async () => { |
| 70 |
const { mountModal, closeModal } = await import( |
| 71 |
'@quick-edit/lib/modal-root' |
| 72 |
); |
| 73 |
mountModal(<div>hi</div>); |
| 74 |
closeModal(); |
| 75 |
expect(unmountFn).toHaveBeenCalledTimes(1); |
| 76 |
expect( |
| 77 |
document.getElementById('extendify-quick-edit-modal-root'), |
| 78 |
).toBeNull(); |
| 79 |
}); |
| 80 |
|
| 81 |
// closeModal(true) → window.location.reload() is one trivial line that |
| 82 |
// jsdom can't drive without monkey-patching a non-configurable Location. |
| 83 |
// reload-flows.spec.ts E2E covers the reload-after-save path. |
| 84 |
|
| 85 |
it('is safe to call when nothing was mounted', async () => { |
| 86 |
const { closeModal } = await import('@quick-edit/lib/modal-root'); |
| 87 |
expect(() => closeModal()).not.toThrow(); |
| 88 |
}); |
| 89 |
|
| 90 |
it('drops the React root reference so the next mountModal creates a fresh one', async () => { |
| 91 |
const { mountModal, closeModal } = await import( |
| 92 |
'@quick-edit/lib/modal-root' |
| 93 |
); |
| 94 |
mountModal(<div>a</div>); |
| 95 |
closeModal(); |
| 96 |
mountModal(<div>b</div>); |
| 97 |
expect(createRootFn).toHaveBeenCalledTimes(2); |
| 98 |
}); |
| 99 |
}); |
| 100 |
|