| 1 |
// Mock zustand's persist middleware so it runs synchronously and we don't |
| 2 |
// have to coordinate hydration timing per test. |
| 3 |
jest.mock('zustand/middleware', () => ({ |
| 4 |
devtools: (fn) => fn, |
| 5 |
persist: (config) => config, |
| 6 |
})); |
| 7 |
|
| 8 |
jest.mock('@quick-edit/lib/insights', () => ({ |
| 9 |
track: jest.fn(), |
| 10 |
})); |
| 11 |
|
| 12 |
const trackedDocListeners = []; |
| 13 |
const originalDocAddEventListener = document.addEventListener.bind(document); |
| 14 |
document.addEventListener = (type, handler, opts) => { |
| 15 |
trackedDocListeners.push([type, handler, opts]); |
| 16 |
return originalDocAddEventListener(type, handler, opts); |
| 17 |
}; |
| 18 |
|
| 19 |
const NODE_ID = 'wp-admin-bar-extendify-quick-edit-toggle'; |
| 20 |
|
| 21 |
const renderAdminBar = () => { |
| 22 |
document.body.innerHTML = ` |
| 23 |
<ul id="wpadminbar"> |
| 24 |
<li id="${NODE_ID}"> |
| 25 |
<a href="#" role="switch" aria-label="Quick Edit">Quick Edit</a> |
| 26 |
</li> |
| 27 |
</ul> |
| 28 |
`; |
| 29 |
return document.querySelector(`#${NODE_ID} a`); |
| 30 |
}; |
| 31 |
|
| 32 |
const loadModules = async () => { |
| 33 |
const { useEditModeStore } = await import('@quick-edit/state/edit-mode'); |
| 34 |
const { bindAdminBarToggle } = await import('@quick-edit/lib/admin-bar'); |
| 35 |
return { useEditModeStore, bindAdminBarToggle }; |
| 36 |
}; |
| 37 |
|
| 38 |
beforeEach(() => { |
| 39 |
jest.resetModules(); |
| 40 |
jest.clearAllMocks(); |
| 41 |
document.documentElement.className = ''; |
| 42 |
document.body.innerHTML = ''; |
| 43 |
window.extQuickEditData = undefined; |
| 44 |
}); |
| 45 |
|
| 46 |
afterEach(() => { |
| 47 |
for (const [type, handler, opts] of trackedDocListeners) { |
| 48 |
document.removeEventListener(type, handler, opts); |
| 49 |
} |
| 50 |
trackedDocListeners.length = 0; |
| 51 |
}); |
| 52 |
|
| 53 |
describe('bindAdminBarToggle — no-op when pill is missing', () => { |
| 54 |
it('returns silently when the admin-bar node is not present', async () => { |
| 55 |
const { bindAdminBarToggle, useEditModeStore } = await loadModules(); |
| 56 |
expect(() => bindAdminBarToggle()).not.toThrow(); |
| 57 |
expect(useEditModeStore.getState().on).toBe(false); |
| 58 |
}); |
| 59 |
}); |
| 60 |
|
| 61 |
describe('bindAdminBarToggle — initial aria-checked sync', () => { |
| 62 |
it('writes aria-checked="false" when edit mode is off', async () => { |
| 63 |
const link = renderAdminBar(); |
| 64 |
const { bindAdminBarToggle } = await loadModules(); |
| 65 |
bindAdminBarToggle(); |
| 66 |
expect(link.getAttribute('aria-checked')).toBe('false'); |
| 67 |
}); |
| 68 |
|
| 69 |
it('writes aria-checked="true" when edit mode is on at bind time', async () => { |
| 70 |
window.extQuickEditData = { defaultOn: true }; |
| 71 |
const link = renderAdminBar(); |
| 72 |
const { bindAdminBarToggle } = await loadModules(); |
| 73 |
bindAdminBarToggle(); |
| 74 |
expect(link.getAttribute('aria-checked')).toBe('true'); |
| 75 |
}); |
| 76 |
}); |
| 77 |
|
| 78 |
describe('bindAdminBarToggle — click handler', () => { |
| 79 |
it('toggles the edit-mode store and prevents default navigation', async () => { |
| 80 |
const link = renderAdminBar(); |
| 81 |
const { bindAdminBarToggle, useEditModeStore } = await loadModules(); |
| 82 |
bindAdminBarToggle(); |
| 83 |
|
| 84 |
const event = new MouseEvent('click', { bubbles: true, cancelable: true }); |
| 85 |
const preventSpy = jest.spyOn(event, 'preventDefault'); |
| 86 |
link.dispatchEvent(event); |
| 87 |
|
| 88 |
expect(preventSpy).toHaveBeenCalled(); |
| 89 |
expect(useEditModeStore.getState().on).toBe(true); |
| 90 |
}); |
| 91 |
|
| 92 |
it('a second click toggles back to off', async () => { |
| 93 |
const link = renderAdminBar(); |
| 94 |
const { bindAdminBarToggle, useEditModeStore } = await loadModules(); |
| 95 |
bindAdminBarToggle(); |
| 96 |
|
| 97 |
link.dispatchEvent( |
| 98 |
new MouseEvent('click', { bubbles: true, cancelable: true }), |
| 99 |
); |
| 100 |
link.dispatchEvent( |
| 101 |
new MouseEvent('click', { bubbles: true, cancelable: true }), |
| 102 |
); |
| 103 |
|
| 104 |
expect(useEditModeStore.getState().on).toBe(false); |
| 105 |
}); |
| 106 |
}); |
| 107 |
|
| 108 |
describe('bindAdminBarToggle — store subscription mirrors aria-checked', () => { |
| 109 |
it('updates aria-checked when the store flips externally', async () => { |
| 110 |
const link = renderAdminBar(); |
| 111 |
const { bindAdminBarToggle, useEditModeStore } = await loadModules(); |
| 112 |
bindAdminBarToggle(); |
| 113 |
|
| 114 |
useEditModeStore.getState().setOn(true); |
| 115 |
expect(link.getAttribute('aria-checked')).toBe('true'); |
| 116 |
|
| 117 |
useEditModeStore.getState().setOn(false); |
| 118 |
expect(link.getAttribute('aria-checked')).toBe('false'); |
| 119 |
}); |
| 120 |
}); |
| 121 |
|
| 122 |
describe('bindAdminBarToggle — idempotency', () => { |
| 123 |
it('only binds the click handler once across repeated calls', async () => { |
| 124 |
const link = renderAdminBar(); |
| 125 |
const { bindAdminBarToggle, useEditModeStore } = await loadModules(); |
| 126 |
bindAdminBarToggle(); |
| 127 |
bindAdminBarToggle(); |
| 128 |
bindAdminBarToggle(); |
| 129 |
|
| 130 |
link.dispatchEvent( |
| 131 |
new MouseEvent('click', { bubbles: true, cancelable: true }), |
| 132 |
); |
| 133 |
|
| 134 |
// One click → one toggle. If the listener double-bound, on would be |
| 135 |
// false again after two toggles. |
| 136 |
expect(useEditModeStore.getState().on).toBe(true); |
| 137 |
}); |
| 138 |
}); |
| 139 |
|