// Focused public Vanilla API and dynamic-option parity with React Story evidence. import fs from 'node:fs/promises'; import path from 'node:path'; import { launchChromium } from '../../scripts/orgchart-browser-runtime.mjs'; const output = path.resolve('test-results/org-chart-p9-4'); const origin = (process.env.ORGCHART_CORE_ORIGIN ?? 'http://127.0.0.1:6074').replace(/\/$/, ''); const only = process.argv.slice(2), run = name => !only.length || only.includes(name), suffix = only.length ? `-${only.join('-')}` : ''; const results = [], errors = [], measurements = [], startedAt = new Date().toISOString(); await fs.mkdir(output, { recursive:true }); const check = (pass, name, detail) => { results.push({ name, status:pass ? 'PASS':'FAIL', ...(detail === undefined ? {} : { detail }) }); if (!pass) console.log('FAIL', name, JSON.stringify(detail)); }; const browser = await launchChromium({ headless:true }), page = await browser.newPage({ viewport:{ width:1440,height:1000 } }); page.setDefaultTimeout(15000); page.on('pageerror', error => errors.push({ type:'pageerror', message:error.message })); page.on('console', message => { if (message.type() === 'error') errors.push({ type:'console', message:message.text() }); }); const settle = () => page.evaluate(async () => { await document.fonts.ready; for (let i = 0; i < 8; i++) await new Promise(requestAnimationFrame); }); async function ready() { await page.waitForFunction(() => window.fixtureReady && window.chart?.getState().status === 'ready' && [...window.shadow.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet)); await settle(); } async function fresh(extra, mode = 'auto') { await page.goto(`${origin}/test-results/org-chart-p9-4/vanilla.html`); await ready(); if (extra || mode !== 'auto') { await page.evaluate(({ extra, mode }) => window.mountChart(mode, extra ?? {}), { extra,mode }); await ready(); } } async function section(name, action) { if (!run(name)) return; try { await action(); } catch (error) { check(false, `${name}: execution completed`, { message:error.message,stack:error.stack }); await page.mouse.up().catch(() => {}); await page.screenshot({ path:path.join(output,`core-failure-${name}.png`) }).catch(() => {}); } } const employee = id => page.locator(`.hm-orgchart-employee[data-employee-id="${id}"]`); const requests = [ { kind:'employee',sourceId:'virtual-022',targetOrganizationId:'hanmac-office-sibling' }, { kind:'organization',sourceId:'hanmac-office-0',targetOrganizationId:'hanmac-office-1' }, { kind:'employee',sourceId:'virtual-022',targetOrganizationId:'saman-office-0' }, { kind:'organization',sourceId:'hanmac-office-0',targetOrganizationId:'saman-office-1' }, ]; await section('api', async () => { await fresh(); const before = await page.evaluate(() => ({ enabled:window.chart.getDragDropEnabled(), data:JSON.stringify(window.chart.getData()), loads:window.loads })); check(before.enabled === false && before.loads === 1, 'Vanilla omitted option defaults OFF and loads source once', before.loads); for (const request of requests) { const rejection = await page.evaluate(request => window.chart.move(request), request); check(!rejection.moved && rejection.code === 'EDITING_DISABLED' && await page.evaluate(data => JSON.stringify(window.chart.getData()) === data, before.data), `OFF public move rejects ${request.kind} to ${request.targetOrganizationId} without changing data`, rejection); } const pure = await page.evaluate(request => window.mod.applyOrgChartMove(window.fixture, request), requests[0]); check(pure.moved, 'Pure data transformation remains compatible outside interactive OFF policy', pure.moved ? pure.request : pure); const legacy = await page.evaluate(() => { const original = JSON.stringify(window.template); const total = window.mod.createOrgChartExampleDataset(window.template, { scope:'total',employeeCount:100 }); const perCompany = window.mod.createOrgChartExampleDataset(window.template, { scope:'per-company',employeeCount:20,companyIds:['hanmac','saman'] }); return { total:total.employees.length,totalValid:window.mod.validateOrgChartDataset(total).valid,perCompany:perCompany.employees.length,sourceUnchanged:JSON.stringify(window.template) === original }; }); check(legacy.total === 100 && legacy.totalValid && legacy.perCompany === 40 && legacy.sourceUnchanged, 'Public total scope remains compatible while Expanded uses company counts', legacy); const toggle = await page.evaluate(() => { const data = window.chart.getData(), forest = window.shadow.querySelector('.hm-orgchart-forest'), viewport = window.chart.getViewportState(); window.chart.setDragDropEnabled(true); const on = window.chart.getDragDropEnabled(); window.chart.setDragDropEnabled(false); const off = window.chart.getDragDropEnabled(); return { on,off,dataSame:data === window.chart.getData(),forestSame:forest === window.shadow.querySelector('.hm-orgchart-forest'),viewportSame:JSON.stringify(viewport) === JSON.stringify(window.chart.getViewportState()),loads:window.loads }; }); check(toggle.on && !toggle.off && toggle.dataSame && toggle.forestSame && toggle.viewportSame && toggle.loads === before.loads, 'Vanilla setter changes policy without data reload, DOM remount or viewport reset', toggle); await page.evaluate(() => window.chart.setDragDropEnabled(true)); for (const request of requests) { const moved = await page.evaluate(request => window.chart.move(request), request); await ready(); check(moved.moved, `ON public move permits ${request.kind} to ${request.targetOrganizationId}`, moved.moved ? moved.request : moved); } const after = await page.evaluate(() => { const data = window.chart.getData(), before = JSON.stringify(data), forest = window.shadow.querySelector('.hm-orgchart-forest'); window.chart.setDragDropEnabled(false); return { same:JSON.stringify(window.chart.getData()) === before,owned:data === window.chart.getData(),forestSame:forest === window.shadow.querySelector('.hm-orgchart-forest'), unique:new Set(data.employees.map(employee => employee.id)).size === data.employees.length,total:data.employees.length,valid:window.mod.validateOrgChartDataset(data).valid, employee:data.employees.find(employee => employee.id === 'virtual-022'),group:data.organizations.find(group => group.id === 'hanmac-office-0'),fixturePreserved:JSON.stringify(window.fixture) === window.fixtureOriginal,loads:window.loads }; }); check(after.same && after.owned && after.forestSame && after.unique && after.total === 40 && after.valid && after.fixturePreserved && after.loads === 1 && after.employee.organizationId === 'saman-office-0' && after.group.parentId === 'saman-office-1', 'Vanilla OFF preserves completed cross-company moves, unique IDs and original fixture without reload', after); await page.screenshot({ path:path.join(output,'vanilla-on-off-preserved.png'),fullPage:true }); measurements.push({ name:'public API',before,toggle,after }); }); await section('cancel', async () => { await fresh({ dragDropEnabled:true }); await page.evaluate(() => { window.pointerId = undefined; window.shadow.querySelector('.hm-orgchart-viewport').addEventListener('pointerdown', event => { window.pointerId = event.pointerId; }, { capture:true }); }); const box = await employee('virtual-022').boundingBox(); await page.mouse.move(box.x + box.width/2,box.y + box.height/2); await page.mouse.down(); await page.mouse.move(box.x + box.width/2 + 12,box.y + box.height/2,{ steps:2 }); await settle(); const before = await page.evaluate(() => { const v = window.shadow.querySelector('.hm-orgchart-viewport'); return { dragging:v.dataset.editDragging,captured:v.hasPointerCapture(window.pointerId),data:JSON.stringify(window.chart.getData()) }; }); const after = await page.evaluate(() => { window.chart.setDragDropEnabled(false); const v = window.shadow.querySelector('.hm-orgchart-viewport'); return { dragging:v.dataset.editDragging,captured:v.hasPointerCapture(window.pointerId),highlight:window.shadow.querySelectorAll('[data-drop-state],[data-drop-hover],[data-edit-source]').length,data:JSON.stringify(window.chart.getData()) }; }); await page.mouse.up(); await settle(); check(before.dragging === 'true' && before.captured && !after.dragging && !after.captured && after.highlight === 0 && before.data === after.data, 'Vanilla live pointer OFF releases capture and clears all pending/highlight state', { before:{ ...before,data:undefined },after:{ ...after,data:undefined } }); check(await page.locator('.hm-orgchart-employee-dialog[open]').count() === 0 && await page.evaluate(data => JSON.stringify(window.chart.getData()) === data,before.data), 'Vanilla canceled pointer release neither commits nor opens Dialog'); await page.evaluate(() => window.chart.setDragDropEnabled(true)); await employee('virtual-022').focus(); await page.keyboard.press('Alt+m'); await settle(); const keyboardBefore = await page.evaluate(() => window.shadow.querySelector('.hm-orgchart-viewport').dataset.editDragging); await page.evaluate(() => window.chart.setDragDropEnabled(false)); await page.keyboard.press('Enter'); await settle(); check(keyboardBefore === 'true' && await page.evaluate(data => !window.shadow.querySelector('[data-edit-dragging],[data-drop-state],[data-drop-hover]') && JSON.stringify(window.chart.getData()) === data,before.data), 'Vanilla active keyboard move is canceled and stale Enter cannot commit'); }); await section('default', async () => { for (const [flag,expected] of [[undefined,false],[false,false],[true,true]]) { await fresh(flag === undefined ? undefined : { dragDropEnabled:flag }); const actual = await page.evaluate(() => ({ value:window.chart.getDragDropEnabled(),attribute:window.shadow.querySelector('.hm-orgchart-viewport').dataset.dragDropEnabled,shortcuts:[...window.shadow.querySelectorAll('.hm-orgchart-employee,.hm-orgchart-expanded-toggle')].map(element => element.getAttribute('aria-keyshortcuts')) })); check(actual.value === expected && actual.attribute === String(expected) && actual.shortcuts.every(value => expected ? value === 'Alt+M' : value === null), `Vanilla option ${String(flag)} matches React default/setter policy and keyboard affordances`, actual.value); } }); await section('fixed', async () => { await fresh({ dragDropEnabled:true },'fixed'); const fixed = await page.evaluate(async () => { let setterRejected = false; try { window.chart.setDragDropEnabled(true); } catch (error) { setterRejected = /Auto/.test(error.message); } let moveRejected = false; try { await window.chart.move({ kind:'employee',sourceId:'emp-001',targetOrganizationId:'management-plan' }); } catch (error) { moveRejected = /Auto/.test(error.message); } return { enabled:window.chart.getDragDropEnabled(),setterRejected,moveRejected,editingUI:window.shadow.querySelectorAll('[data-drag-drop-enabled],[data-edit-dragging],[data-drop-state],.hm-orgchart-move-feedback').length,loads:window.loads }; }); check(!fixed.enabled && fixed.setterRejected && fixed.moveRejected && fixed.editingUI === 0, 'Fixed ignores dragDropEnabled option, rejects Auto setter/move and adds no editing UI', fixed); }); check(errors.length === 0,'Vanilla new console/page errors: zero',errors); const result = { startedAt,completedAt:new Date().toISOString(),browser:browser.version(),origin,sections:only,results,measurements,errors,pass:results.filter(item => item.status === 'PASS').length,fail:results.filter(item => item.status === 'FAIL').length }; await fs.writeFile(path.join(output,`core${suffix}.json`),JSON.stringify(result,null,2)); await browser.close(); console.log(JSON.stringify({ pass:result.pass,fail:result.fail,output:`test-results/org-chart-p9-4/core${suffix}.json` })); if (result.fail) process.exitCode = 1;