/* Drives the real admin editor in Firefox and reports whether dragging a Page actually reorders it and reaches the server. This is the only automated check that reaches the drag path: the order is committed from dragover, because browsers suppress mouse events during a drag and dragend is not guaranteed. Needs a running dev server and geckodriver on PATH: npm run dev # in one terminal geckodriver --port 4444 & # in another npm run check-dnd -- [native|synthetic|none] Use "none" to inspect a set's affordances without dragging: an Archive should report no draggable rows and no grab handles. */ const GECKO = 'http://127.0.0.1:4444'; const SITE = process.env.SITE ?? 'http://127.0.0.1:3000'; const PASSWORD = process.env.DEV_PASSWORD ?? 'menu-dev-password'; const setId = process.argv[2]; const gesture = process.argv[3] ?? 'native'; if (!setId) throw new Error('usage: node .tmp/dnd.mjs [native|synthetic|none]'); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const KEY = 'element-6066-11e4-a52e-4f735466cecf'; async function cmd(method, path, body) { const init = { method }; if (body) { init.headers = { 'content-type': 'application/json' }; init.body = JSON.stringify(body); } const response = await fetch(GECKO + path, init); const json = JSON.parse(await response.text()); if (json.value && json.value.error) throw new Error(`${json.value.error}: ${json.value.message}`); return json.value; } const find = async (sid, selector) => (await cmd('POST', `/session/${sid}/element`, { using: 'css selector', value: selector }))[KEY]; const findAll = async (sid, selector) => (await cmd('POST', `/session/${sid}/elements`, { using: 'css selector', value: selector })).map( (e) => e[KEY], ); const click = (sid, element) => cmd('POST', `/session/${sid}/element/${element}/click`, {}); const type = (sid, element, value) => cmd('POST', `/session/${sid}/element/${element}/value`, { text: value }); const script = (sid, body, args = []) => cmd('POST', `/session/${sid}/execute/sync`, { script: body, args }); async function pageState(sid) { return script( sid, `return { rows: Array.from(document.querySelectorAll('[data-page-id]')).map(function (li) { var handle = li.querySelector('.page-admin__handle'); return { id: li.getAttribute('data-page-id'), draggable: li.draggable, hasHandle: !!handle, cursor: handle ? getComputedStyle(handle).cursor : null, formCount: li.querySelectorAll('form').length }; }), hasList: !!document.querySelector('[data-pages-admin]'), scriptLoaded: !!document.querySelector('script[src="/assets/admin.js"]'), dnd: window.__dnd || null };`, ); } const session = await cmd('POST', '/session', { capabilities: { alwaysMatch: { 'moz:firefoxOptions': { args: ['-headless'] } } }, }); const sid = session.sessionId; try { await cmd('POST', `/session/${sid}/window/rect`, { width: 430, height: 1000 }); await cmd('POST', `/session/${sid}/url`, { url: `${SITE}/admin/login` }); await sleep(500); await type(sid, await find(sid, 'input[name=password]'), PASSWORD); await click(sid, await find(sid, 'button[type=submit]')); await sleep(800); await cmd('POST', `/session/${sid}/url`, { url: `${SITE}/admin/sets/${setId}` }); await sleep(1000); await script( sid, `window.__dnd = { dragstart: 0, dragover: 0, drop: 0, dragend: 0 }; ['dragstart','dragover','drop','dragend'].forEach(function (name) { document.addEventListener(name, function () { window.__dnd[name] += 1; }, true); }); return true;`, ); const before = await pageState(sid); console.log(`rows: ${before.rows.length}`); console.log(`draggable rows: ${before.rows.filter((r) => r.draggable).length}`); console.log(`rows with a grab handle: ${before.rows.filter((r) => r.hasHandle).length}`); console.log(`handle cursor: ${before.rows[0]?.cursor}`); console.log(`per-row up/down/delete forms: ${before.rows[0]?.formCount}`); console.log(`[data-pages-admin] list present: ${before.hasList}`); console.log(`admin.js included: ${before.scriptLoaded}`); const beforeIds = before.rows.map((r) => r.id); if (gesture === 'native' && before.rows.length >= 3) { const elements = await findAll(sid, '[data-page-id]'); const origin = (element) => ({ [KEY]: element }); const from = elements[0]; const to = elements[2]; await cmd('POST', `/session/${sid}/actions`, { actions: [ { type: 'pointer', id: 'mouse', parameters: { pointerType: 'mouse' }, actions: [ { type: 'pointerMove', duration: 0, origin: origin(from), x: 5, y: 5 }, { type: 'pointerDown', button: 0 }, { type: 'pause', duration: 200 }, { type: 'pointerMove', duration: 300, origin: origin(to), x: 5, y: 40 }, { type: 'pause', duration: 300 }, { type: 'pointerUp', button: 0 }, ], }, ], }); await sleep(1200); } if (gesture === 'synthetic' && before.rows.length >= 3) { // Exercises our handlers without the browser's own drag machinery. await script( sid, `var rows = document.querySelectorAll('[data-page-id]'); var from = rows[0], to = rows[2]; var rect = to.getBoundingClientRect(); function fire(target, type, extra) { var event = new DragEvent(type, Object.assign({ bubbles: true, cancelable: true, dataTransfer: new DataTransfer() }, extra || {})); target.dispatchEvent(event); } fire(from, 'dragstart'); fire(to, 'dragover', { clientY: rect.bottom - 2 }); fire(to, 'drop', { clientY: rect.bottom - 2 }); fire(from, 'dragend'); return true;`, ); await sleep(1200); } const after = await pageState(sid); const afterIds = after.rows.map((r) => r.id); console.log(`\nevents seen: ${JSON.stringify(after.dnd)}`); const domChanged = JSON.stringify(beforeIds) !== JSON.stringify(afterIds); console.log(`DOM order changed by the drag: ${domChanged}`); if (domChanged) { console.log(` was: ${beforeIds.slice(0, 3).join(', ')} ...`); console.log(` now: ${afterIds.slice(0, 3).join(', ')} ...`); } // What the server holds now, read back through the editor. await cmd('POST', `/session/${sid}/url`, { url: `${SITE}/admin/sets/${setId}` }); await sleep(800); const reloaded = await pageState(sid); const serverIds = reloaded.rows.map((r) => r.id); const persisted = JSON.stringify(serverIds) !== JSON.stringify(beforeIds); console.log(`server order changed (after reload): ${persisted}`); if (persisted) console.log(` server now: ${serverIds.slice(0, 3).join(', ')} ...`); console.log( `\nVERDICT: ${domChanged && persisted ? 'PASS' : 'FAIL'} (drag reorders and persists)`, ); } finally { await cmd('DELETE', `/session/${sid}`); }