| 1 |
// Pins the capture-phase click rule: |
| 2 |
// anchor → navigate, pill/toolbar → pass through, form control → focus, |
| 3 |
// tagged block → select (with innermost-tagged), outside → clear. |
| 4 |
// |
| 5 |
// The decision function is pure — given a target node, returns the |
| 6 |
// branch the listener should take. The listener wiring (preventDefault, |
| 7 |
// stopPropagation, hover-bar render/clear) lives in hover-bar.js and is |
| 8 |
// exercised by the Playwright spec. |
| 9 |
|
| 10 |
import { decideClickAction } from '@quick-edit/lib/click-rule'; |
| 11 |
|
| 12 |
beforeEach(() => { |
| 13 |
document.body.innerHTML = ''; |
| 14 |
}); |
| 15 |
|
| 16 |
const make = (tag, attrs = {}, classes = []) => { |
| 17 |
const el = document.createElement(tag); |
| 18 |
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v); |
| 19 |
for (const cls of classes) el.classList.add(cls); |
| 20 |
return el; |
| 21 |
}; |
| 22 |
|
| 23 |
describe('decideClickAction — null safety', () => { |
| 24 |
it('returns ignore for null', () => { |
| 25 |
expect(decideClickAction(null)).toEqual({ action: 'ignore' }); |
| 26 |
}); |
| 27 |
|
| 28 |
it('returns ignore for a text node target', () => { |
| 29 |
const text = document.createTextNode('x'); |
| 30 |
expect(decideClickAction(text)).toEqual({ action: 'ignore' }); |
| 31 |
}); |
| 32 |
}); |
| 33 |
|
| 34 |
describe('decideClickAction — navigate beats everything', () => { |
| 35 |
it('returns navigate for an <a href> target', () => { |
| 36 |
const a = make('a', { href: '/about' }); |
| 37 |
document.body.appendChild(a); |
| 38 |
expect(decideClickAction(a)).toEqual({ action: 'navigate' }); |
| 39 |
}); |
| 40 |
|
| 41 |
it('returns navigate for a descendant of <a href>', () => { |
| 42 |
const a = make('a', { href: '/about' }); |
| 43 |
const span = make('span'); |
| 44 |
a.appendChild(span); |
| 45 |
document.body.appendChild(a); |
| 46 |
expect(decideClickAction(span)).toEqual({ action: 'navigate' }); |
| 47 |
}); |
| 48 |
|
| 49 |
it('returns navigate for an anchor inside a tagged block (anchor wins)', () => { |
| 50 |
const block = make('section', { 'data-extendify-agent-block-id': '7' }); |
| 51 |
const a = make('a', { href: '/contact' }); |
| 52 |
block.appendChild(a); |
| 53 |
document.body.appendChild(block); |
| 54 |
expect(decideClickAction(a)).toEqual({ action: 'navigate' }); |
| 55 |
}); |
| 56 |
|
| 57 |
it('does not return navigate for an <a> without href', () => { |
| 58 |
const a = make('a'); |
| 59 |
document.body.appendChild(a); |
| 60 |
expect(decideClickAction(a)).toEqual({ action: 'clear' }); |
| 61 |
}); |
| 62 |
}); |
| 63 |
|
| 64 |
describe('decideClickAction — pill / toolbar pass-through', () => { |
| 65 |
it('returns pill for a button with data-extendify-quick-edit-pill', () => { |
| 66 |
const btn = make('button', { 'data-extendify-quick-edit-pill': '' }); |
| 67 |
document.body.appendChild(btn); |
| 68 |
expect(decideClickAction(btn)).toEqual({ action: 'pill' }); |
| 69 |
}); |
| 70 |
|
| 71 |
it('returns pill for a span inside a data-extendify-quick-edit-bar wrapper', () => { |
| 72 |
const bar = make('div', { 'data-extendify-quick-edit-bar': '' }); |
| 73 |
const inner = make('span'); |
| 74 |
bar.appendChild(inner); |
| 75 |
document.body.appendChild(bar); |
| 76 |
expect(decideClickAction(inner)).toEqual({ action: 'pill' }); |
| 77 |
}); |
| 78 |
|
| 79 |
it('returns pill (not select) when a pill sits inside a tagged block', () => { |
| 80 |
const block = make('div', { 'data-extendify-agent-block-id': '4' }); |
| 81 |
const pill = make('button', { 'data-extendify-quick-edit-pill': '' }); |
| 82 |
block.appendChild(pill); |
| 83 |
document.body.appendChild(block); |
| 84 |
expect(decideClickAction(pill)).toEqual({ action: 'pill' }); |
| 85 |
}); |
| 86 |
}); |
| 87 |
|
| 88 |
describe('decideClickAction — form controls focus', () => { |
| 89 |
it.each([ |
| 90 |
'input', |
| 91 |
'textarea', |
| 92 |
'select', |
| 93 |
'button', |
| 94 |
])('returns focus-control for a bare <%s>', (tag) => { |
| 95 |
const el = make(tag); |
| 96 |
document.body.appendChild(el); |
| 97 |
expect(decideClickAction(el)).toEqual({ action: 'focus-control' }); |
| 98 |
}); |
| 99 |
|
| 100 |
it('returns focus-control for a form control inside a tagged block', () => { |
| 101 |
const block = make('div', { 'data-extendify-agent-block-id': '3' }); |
| 102 |
const input = make('input', { type: 'search' }); |
| 103 |
block.appendChild(input); |
| 104 |
document.body.appendChild(block); |
| 105 |
expect(decideClickAction(input)).toEqual({ action: 'focus-control' }); |
| 106 |
}); |
| 107 |
}); |
| 108 |
|
| 109 |
describe('decideClickAction — tagged-block select', () => { |
| 110 |
it('returns select for a click directly on a tagged block', () => { |
| 111 |
const block = make('div', { 'data-extendify-agent-block-id': '11' }); |
| 112 |
document.body.appendChild(block); |
| 113 |
expect(decideClickAction(block)).toEqual({ action: 'select', el: block }); |
| 114 |
}); |
| 115 |
|
| 116 |
it('returns select with the innermost tagged ancestor', () => { |
| 117 |
const outer = make('section', { 'data-extendify-agent-block-id': '1' }); |
| 118 |
const inner = make('div', { 'data-extendify-part-block-id': '2' }); |
| 119 |
const leaf = make('p'); |
| 120 |
inner.appendChild(leaf); |
| 121 |
outer.appendChild(inner); |
| 122 |
document.body.appendChild(outer); |
| 123 |
expect(decideClickAction(leaf)).toEqual({ action: 'select', el: inner }); |
| 124 |
}); |
| 125 |
|
| 126 |
it('returns select for the WPForm-field-tagged ancestor', () => { |
| 127 |
const block = make('div', { |
| 128 |
'data-extendify-quick-edit-wpform-field-id': '7', |
| 129 |
}); |
| 130 |
const label = make('label'); |
| 131 |
block.appendChild(label); |
| 132 |
document.body.appendChild(block); |
| 133 |
expect(decideClickAction(label)).toEqual({ action: 'select', el: block }); |
| 134 |
}); |
| 135 |
|
| 136 |
it('returns select for the product-tagged ancestor', () => { |
| 137 |
const block = make('div', { |
| 138 |
'data-extendify-quick-edit-product-id': '99', |
| 139 |
}); |
| 140 |
const inner = make('span'); |
| 141 |
block.appendChild(inner); |
| 142 |
document.body.appendChild(block); |
| 143 |
expect(decideClickAction(inner)).toEqual({ action: 'select', el: block }); |
| 144 |
}); |
| 145 |
}); |
| 146 |
|
| 147 |
describe('decideClickAction — clear', () => { |
| 148 |
it('returns clear for a click outside every tagged block', () => { |
| 149 |
const body = make('p', {}, ['random']); |
| 150 |
document.body.appendChild(body); |
| 151 |
expect(decideClickAction(body)).toEqual({ action: 'clear' }); |
| 152 |
}); |
| 153 |
}); |
| 154 |
|