docs(storybook): make Expanded OrgChart a preview-only page
Deploy Storybook / deploy (push) Successful in 59s

Remove the data, layout, interaction and employee-popup setting panels
from the Expanded OrgChart Story. The chart now renders a fixed sample
(7 companies x 20, TB, all expanded, collapse/zoom/pan on, Drag & Drop
off, HM Dialog) right under the page title. Story ID, OrgChart packages
and public API are unchanged.

Align the Story browser checks (P7, P8, P9, P9.2-P9.5, geometry, popup
parity, hierarchy, HM Story) with the preview: setting-only scenarios
are removed, chart rendering, collapse, Zoom/Pan/Fit and HM Dialog checks
run on the fixed state. Move/Drag & Drop coverage stays in the Vanilla
Core checks (verify-p9-3-core, verify-p9-4-core).

Verified: tsc, test:guard 94/94, node --check on 11 scripts, changed
browser sections PASS. Pre-existing P7 story-menu, P8 Department
long-name, P9 Expanded tb/runner failures remain separate issues.
verify-p9-5-popup-parity was not run (needs the Vanilla example server).
This commit is contained in:
shkim4 committed 2026-10-22 17:51:21 +09:00
1 parent 4a54cfbeb2
commit f4ddd5ac4b
14 files changed
+163 -1134

No files matched your search

+16 -149
View File
@@ -1,5 +1,7 @@
// Focused P9.4 Storybook deltas. Run named sections to repair only failed probes.
// Existing P7-P9.3 evidence is preserved and is not replayed by this harness.
// Expanded is preview-only (2026-10-22): company/data/layout settings and the Drag & Drop switch were
// removed, so data/dropdown/on/cancel/modes sections were retired (Core ON/cancel stays in verify-p9-4-core.mjs).
import fs from 'node:fs/promises';
import { writeFileSync } from 'node:fs';
import path from 'node:path';
@@ -26,11 +28,7 @@ page.on('console', message => { if (message.type() === 'error') { errors.push({
const viewport = () => page.locator('.hm-orgchart-viewport');
const employee = id => page.locator(`.hm-orgchart-employee[data-employee-id="${id}"]`);
const title = id => page.locator(`.hm-orgchart-expanded-toggle[data-organization-id="${id}"]`);
const companyTrigger = () => page.getByRole('button', { name: /^회사 선택/ });
const companyPanel = () => page.getByRole('dialog', { name: '회사 선택', exact: true });
const companyCheckbox = name => companyPanel().getByRole('checkbox', { name, exact: true });
const dialog = () => page.locator('[data-component="HMOrgChartEmployeeDialog"]');
const editSwitch = () => page.getByRole('switch', { name: /Drag & Drop|조직 이동/ });
const settle = () => page.evaluate(async () => { await document.fonts.ready; for (let i = 0; i < 8; i++) await new Promise(requestAnimationFrame); });
async function ready(count) {
await page.waitForFunction(count => {
@@ -39,40 +37,10 @@ async function ready(count) {
&& (count === undefined || shadow.querySelectorAll('.hm-orgchart-employee').length === count);
}, count, { timeout: 45000 }); await settle();
}
async function select(label, option) {
await page.getByRole('combobox', { name: label, exact: true }).click();
await page.getByRole('option', { name: option, exact: true }).click(); await ready();
}
async function countPerCompany(count, expected) {
await select('회사당 직원 수', `${count}명`);
const apply = page.getByRole('button', { name: '데이터 적용', exact: true });
if (await apply.count() && await apply.isEnabled()) await apply.click();
await ready(expected);
}
async function chooseCompanies(names) {
await companyTrigger().click(); await companyPanel().waitFor();
const all = companyCheckbox('전체 선택');
for (let i = 0; i < 2 && await all.getAttribute('aria-checked') !== 'false'; i++) { await all.click(); await ready(); }
await ready(0);
for (const name of names) { await companyCheckbox(name).click(); await ready(); }
await page.keyboard.press('Escape'); await companyPanel().waitFor({ state: 'hidden' }); await ready();
}
async function toggle(enabled, { programmatic = false } = {}) {
const control = editSwitch();
if (await control.getAttribute('aria-checked') !== String(enabled)) {
// A live pointer must remain down while an external settings change cancels
// it. DOM click is the same React handler and does not synthesize pointerup.
if (programmatic) await control.evaluate(element => element.click()); else await control.click();
}
await settle();
}
async function setup({ companies = ['한맥기술'], layout = 'tb', enabled = false } = {}) {
async function setup() {
await page.setViewportSize({ width: 1920, height: 1700 });
await page.goto(`${origin}/iframe.html?id=orgchart--expanded-org-chart&viewMode=story`);
await companyTrigger().waitFor(); await ready(140);
await chooseCompanies(companies); await ready(companies.length * 20);
if (layout !== 'tb') await select('조직도 표시 유형', layout === 'lr' ? '가로형 LR' : '혼합형 Hybrid');
await toggle(enabled); await viewport().scrollIntoViewIfNeeded();
await ready(140); await viewport().scrollIntoViewIfNeeded();
await page.getByRole('button', { name: 'Fit to View', exact: true }).click(); await settle();
}
async function state() {
@@ -96,7 +64,7 @@ async function state() {
drop: [...shadow.querySelectorAll('[data-drop-state]')].map(element => ({ id: element.dataset.organizationId, state: element.dataset.dropState, hover: element.dataset.dropHover })),
dragging: viewport.dataset.editDragging, panDragging: viewport.dataset.dragging, activeEmployee: shadow.activeElement?.dataset.employeeId,
activeOrganization: shadow.activeElement?.dataset.organizationId, dragDropEnabled: viewport.dataset.dragDropEnabled,
countText: document.querySelector('[data-component="OrgChartExampleCounts"]')?.textContent.trim(), dialogs: document.querySelectorAll('[data-component="HMOrgChartEmployeeDialog"]').length,
dialogs: document.querySelectorAll('[data-component="HMOrgChartEmployeeDialog"]').length,
companyRoots: nodes.filter(org => org.parentId === '').map(org => org.companyId), documentWidth: document.documentElement.scrollWidth,
};
});
@@ -140,14 +108,6 @@ async function section(name, action) {
if (only.length && !only.includes(name)) return;
try { await action(); } catch (error) { check(false, `${name}: execution completed`, { message: error.message, stack: error.stack }); await page.mouse.up().catch(() => {}); await snap(`failure-${name}`, true).catch(() => {}); }
}
function counts(s, companies, perCompany, name) {
const grouped = Object.fromEntries(companies.map(id => [id, s.cards.filter(card => card.companyId === id).length]));
check(s.cards.length === companies.length * perCompany && new Set(s.cards.map(card => card.id)).size === s.cards.length
&& new Set(s.nodes.map(org => org.id)).size === s.nodes.length && Object.values(grouped).every(count => count === perCompany), `${name}: unique IDs and exact per-company employee counts`, grouped);
check(s.companyRoots.length === companies.length && s.nodes.filter(org => org.parentId === '').every(org => org.inclusive === perCompany), `${name}: root totals count organizational leaders exactly once`);
check(s.countText?.includes(`합계 ${(companies.length * perCompany).toLocaleString('ko-KR')}명`), `${name}: displayed total matches Employee IDs`, s.countText);
}
await section('department', async () => {
await page.setViewportSize({ width: 1440, height: 1000 });
await page.goto(`${origin}/iframe.html?id=orgchart--department-colors&viewMode=story`); await ready();
@@ -250,36 +210,9 @@ await section('department-names', async () => {
await snap('department-names',true); measurements.push({ name:'Department naming correction',current });
});
await section('data', async () => {
await setup();
check(await page.getByRole('combobox', { name: '데이터 범위', exact: true }).count() === 0 && !await page.getByText('전체 회사 합산', { exact: true }).count(), 'Expanded removes both data-scope choices');
counts(await state(), ['hanmac'], 20, 'one company x20');
for (const count of [100, 300]) { await countPerCompany(count, count); counts(await state(), ['hanmac'], count, `one company x${count}`); }
await countPerCompany(100, 100); await chooseCompanies(['한맥기술', '삼안']); await ready(200);
counts(await state(), ['hanmac', 'saman'], 100, 'two companies x100'); await snap('expanded-scope-removed', true);
await companyTrigger().click(); await companyPanel().waitFor(); await companyCheckbox('전체 선택').click(); await ready(700);
await page.keyboard.press('Escape'); await companyPanel().waitFor({ state: 'hidden' }); await countPerCompany(300, 2100);
const allIds = ['hanmac','saman','jangheon-industry','jangheon','ptc','halla','baron'];
counts(await state(), allIds, 300, 'seven companies x300'); measurements.push({ name: 'seven x300', state: await state() });
await chooseCompanies([]); await ready(0); const empty = await state();
check(empty.cards.length === 0 && empty.companyRoots.length === 0 && empty.countText.includes('0개 회사') && await page.locator('.hm-orgchart-expanded-empty').count() === 1, 'Zero selected companies renders explicit empty state');
await snap('expanded-empty'); await chooseCompanies(['한맥기술']); await ready(300); counts(await state(), ['hanmac'], 300, 'selection after empty');
});
await section('dropdown', async () => {
await setup(); await chooseCompanies(['한맥기술']); await companyTrigger().focus(); await page.keyboard.press('Enter'); await companyPanel().waitFor();
check(await companyCheckbox('전체 선택').evaluate(element => element === document.activeElement), 'Company dropdown keyboard opening focuses select-all');
await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); await page.keyboard.press('Space'); await ready(40);
check(await companyCheckbox('삼안').getAttribute('aria-checked') === 'true' && await companyCheckbox('전체 선택').getAttribute('aria-checked') === 'mixed', 'Company keyboard Space selects second company and mixed state');
await snap('company-dropdown'); await page.keyboard.press('Escape'); await companyPanel().waitFor({ state: 'hidden' });
check(await companyTrigger().evaluate(element => element === document.activeElement), 'Company ESC closes and restores trigger focus');
await companyTrigger().click(); await companyPanel().waitFor(); await page.locator('.sb-page-title').click(); await companyPanel().waitFor({ state: 'hidden' });
check(true, 'Company outside click closes existing HM Popover');
});
await section('off', async () => {
await setup({ companies: ['한맥기술', '삼안'] });
check(await editSwitch().getAttribute('aria-checked') === 'false', 'Drag & Drop defaults OFF'); await snap('drag-drop-off', true);
await setup();
check(await viewport().getAttribute('data-drag-drop-enabled') === 'false', 'Drag & Drop fixed OFF'); await snap('drag-drop-off', true);
for (const [name, source, target] of [
['employee within company', employee('virtual-022'), title('hanmac-office-sibling')],
['group within company', title('hanmac-office-0'), title('hanmac-office-1')],
@@ -296,7 +229,7 @@ await section('off', async () => {
await viewport().scrollIntoViewIfNeeded(); const before = await state(); await pan(70, -35); const after = await state();
check(Math.abs(after.panX - before.panX - 70) < 1 && Math.abs(after.panY - before.panY + 35) < 1 && relations(before) === relations(after), 'OFF preserves blank-space Pan');
await title('hanmac-office-sibling').click(); await ready(); check(await title('hanmac-office-sibling').getAttribute('aria-expanded') === 'false', 'OFF group click collapses');
await title('hanmac-office-sibling').press('Space'); await ready(40); check(await title('hanmac-office-sibling').getAttribute('aria-expanded') === 'true', 'OFF group Space expands');
await title('hanmac-office-sibling').press('Space'); await ready(140); check(await title('hanmac-office-sibling').getAttribute('aria-expanded') === 'true', 'OFF group Space expands');
await employee('virtual-022').click(); await dialog().waitFor(); check(await dialog().getAttribute('data-employee-id') === 'virtual-022', 'OFF employee click opens HM Dialog');
await page.keyboard.press('Escape'); await dialog().waitFor({ state: 'hidden' }); await settle();
check((await state()).activeEmployee === 'virtual-022', 'OFF Dialog closes and restores employee focus');
@@ -306,66 +239,12 @@ await section('off', async () => {
measurements.push({ name: 'OFF browsing', state: fit });
});
await section('on', async () => {
await setup({ companies: ['한맥기술', '삼안'], enabled: true }); await snap('drag-drop-on', true);
const inside = await drag(employee('virtual-022'), title('hanmac-office-sibling'));
check(inside.after.cards.find(card => card.id === 'virtual-022')?.organizationId === 'hanmac-office-sibling' && inside.after.feedback.status === 'success', 'ON employee move within company');
const groupInside = await drag(title('hanmac-office-0'), title('hanmac-office-1'));
check(groupInside.after.nodes.find(org => org.id === 'hanmac-office-0')?.parentId === 'hanmac-office-1' && groupInside.after.feedback.status === 'success', 'ON group move within company');
const cross = await drag(employee('virtual-022'), title('saman-office-0'));
const moved = cross.after.cards.find(card => card.id === 'virtual-022');
check(moved?.companyId === 'saman' && moved.organizationId === 'saman-office-0' && moved.border[moved.direction] === 'rgb(240, 131, 0)' && cross.after.feedback.status === 'success', 'ON cross-company employee move updates actual company, organization and approved accent');
const crossGroup = await drag(title('hanmac-office-0'), title('saman-office-1'));
check(crossGroup.after.nodes.find(org => org.id === 'hanmac-office-0')?.parentId === 'saman-office-1'
&& ['hanmac-office-0','hanmac-cell-0','hanmac-team-0','hanmac-team-sibling'].every(id => crossGroup.after.nodes.find(org => org.id === id)?.companyId === 'saman') && crossGroup.after.feedback.status === 'success', 'ON cross-company group move preserves and recolors complete subtree');
check(crossGroup.after.cards.length === 40 && new Set(crossGroup.after.cards.map(card => card.id)).size === 40 && crossGroup.after.nodes.filter(org => org.parentId === '').reduce((sum, org) => sum + org.inclusive, 0) === 40, 'ON cross-company moves preserve unique Employee IDs and aggregate total');
await snap('cross-company-moved', true);
const beforeOff = await state(); await toggle(false); const afterOff = await state();
check(relations(beforeOff) === relations(afterOff) && afterOff.drop.length === 0 && !afterOff.dragging, 'Turning OFF retains completed employee and group moves');
await toggle(true); check(relations(beforeOff) === relations(await state()), 'Turning ON again preserves completed moves without reloading data');
await employee('virtual-022').click(); await dialog().waitFor(); check(await dialog().getAttribute('data-employee-id') === 'virtual-022', 'ON moved employee remains clickable');
await page.keyboard.press('Escape'); await dialog().waitFor({ state: 'hidden' });
await title('hanmac-office-0').click(); await ready(); check(await title('hanmac-office-0').getAttribute('aria-expanded') === 'false', 'ON group click still collapses');
await title('hanmac-office-0').press('Space'); await ready(40); check(await title('hanmac-office-0').getAttribute('aria-expanded') === 'true', 'ON group Space still expands');
await countPerCompany(100, 200); const fresh = await state();
check(fresh.cards.find(card => card.id === 'virtual-022')?.organizationId === 'hanmac-office-0' && fresh.nodes.find(org => org.id === 'hanmac-office-0')?.parentId === 'hanmac-division-0' && fresh.drop.length === 0 && !fresh.dragging, 'Data Apply rebuilds fixture without mixing completed session moves or drag state');
measurements.push({ name: 'ON transfers', inside, groupInside, cross, crossGroup, afterOff });
});
await section('cancel', async () => {
await setup({ enabled: true });
let cancelledDuringPointer;
const pointer = await drag(employee('virtual-022'), title('hanmac-office-sibling'), async () => { await toggle(false, { programmatic: true }); cancelledDuringPointer = await state(); });
check(pointer.hover.dragging === 'true' && pointer.hover.drop.some(target => target.hover === 'allowed'), 'ON pointer edit is active before disable');
check(!cancelledDuringPointer.dragging && cancelledDuringPointer.drop.length === 0 && relations(pointer.before) === relations(pointer.after) && pointer.after.dialogs === 0, 'OFF during pointer move cancels edit/highlights and suppresses release click');
await toggle(true); await employee('virtual-022').focus(); await page.keyboard.press('Alt+m'); await settle(); const opened = await state();
check(opened.dragging === 'true' && opened.drop.some(target => target.hover === 'allowed'), 'ON Alt+M starts keyboard edit');
await toggle(false, { programmatic: true }); await page.keyboard.press('Enter'); await settle(); const cancelled = await state();
check(!cancelled.dragging && cancelled.drop.length === 0 && cancelled.cards.find(card => card.id === 'virtual-022')?.organizationId === 'hanmac-office-0' && cancelled.dialogs === 0, 'OFF cancels active keyboard edit and blocks stale Enter commit');
await toggle(true); await employee('virtual-022').focus(); await page.keyboard.press('Alt+m'); await settle();
for (let i = 0; i < 40 && (await state()).drop.find(target => target.hover)?.id !== 'hanmac-office-sibling'; i++) { await page.keyboard.press('ArrowRight'); await settle(); }
await page.keyboard.press('Enter'); await ready(20); const keyboard = await state();
check(keyboard.cards.find(card => card.id === 'virtual-022')?.organizationId === 'hanmac-office-sibling' && keyboard.feedback.status === 'success' && keyboard.dialogs === 0, 'Re-enabled keyboard move commits through common ON policy');
measurements.push({ name: 'disable cancellation', pointer, cancelledDuringPointer, opened, cancelled, keyboard });
});
await section('modes', async () => {
await setup({ companies: ['한맥기술', '삼안'] });
for (const [layout, option] of [['tb','세로형 TB'],['lr','가로형 LR'],['hybrid','혼합형 Hybrid']]) {
if (layout !== 'tb') await select('조직도 표시 유형', option); await ready(40); const s = await state();
check(s.layout === layout && s.cards.length === 40 && s.cards.every(card => ['top','left','right'].includes(card.direction)), `${layout}: layout and existing directional cards work with default OFF`);
}
});
await section('responsive', async () => {
for (const width of [390,768,1280,1440]) {
await page.setViewportSize({ width, height: 1100 }); await page.goto(`${origin}/iframe.html?id=orgchart--expanded-org-chart&viewMode=story`); await companyTrigger().waitFor(); await ready(140);
const metric = await editSwitch().evaluate(element => ({ label: element.getAttribute('aria-label') || element.getAttribute('aria-labelledby') || document.querySelector(`label[for="${element.id}"]`)?.textContent, checked: element.getAttribute('aria-checked'), scrollWidth: document.documentElement.scrollWidth, switchVisible: element.getBoundingClientRect().width > 0 }));
check(metric.scrollWidth <= width + 1 && metric.switchVisible && metric.checked === 'false' && Boolean(metric.label), `${width}: OFF switch is named and settings have no horizontal overflow`, metric);
await snap(`expanded-${width}`, true); await companyTrigger().click(); await companyPanel().waitFor();
const bounds = await companyPanel().evaluate(element => { const r = element.getBoundingClientRect(); return { x: r.left, right: r.right, y: r.top, bottom: r.bottom, scrollWidth: element.scrollWidth, clientWidth: element.clientWidth }; });
check(bounds.x >= -1 && bounds.right <= width + 1 && bounds.y >= -1 && bounds.bottom <= 1101 && bounds.scrollWidth <= bounds.clientWidth + 1, `${width}: retained company dropdown fits viewport`, bounds);
await page.keyboard.press('Escape'); await companyPanel().waitFor({ state: 'hidden' });
await page.setViewportSize({ width, height: 1100 }); await page.goto(`${origin}/iframe.html?id=orgchart--expanded-org-chart&viewMode=story`); await ready(140);
const metric = await page.evaluate(() => ({ width: innerWidth, scrollWidth: document.documentElement.scrollWidth }));
check(metric.scrollWidth <= width + 1, `${width}: preview-only Expanded has no horizontal overflow`, metric);
await snap(`expanded-${width}`, true);
await page.goto(`${origin}/iframe.html?id=orgchart--department-colors&viewMode=story`); await ready();
const departmentOverflow = await page.evaluate(() => ({ width: innerWidth, scrollWidth: document.documentElement.scrollWidth }));
check(departmentOverflow.scrollWidth <= width + 1, `${width}: Department preview containers fit viewport`, departmentOverflow);
@@ -375,13 +254,13 @@ await section('responsive', async () => {
await section('expanded-frame', async () => {
// Later outer-frame style correction only. Existing layout/move suites remain
// the evidence for those features; moving a sample below is for fresh capture.
// the evidence for those features.
await page.setViewportSize({ width:1440,height:1100 });
await page.goto(`${origin}/iframe.html?id=center-graphic-facility--tunnel&viewMode=story`); await page.locator('.tunnel-reference__sample').first().waitFor(); await settle();
const reference = await page.locator('.tunnel-reference__sample').first().evaluate(element => {
const css = getComputedStyle(element); return { backgroundColor:css.backgroundColor,border:css.border,borderRadius:css.borderRadius };
});
await page.goto(`${origin}/iframe.html?id=orgchart--expanded-org-chart&viewMode=story`); await companyTrigger().waitFor(); await ready(140);
await page.goto(`${origin}/iframe.html?id=orgchart--expanded-org-chart&viewMode=story`); await ready(140);
const frameSnapshot = () => page.evaluate(() => {
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot, root = shadow.querySelector('.hm-orgchart-expanded'), css = getComputedStyle(root);
return { frame:{ backgroundColor:css.backgroundColor,border:css.border,borderRadius:css.borderRadius },
@@ -396,24 +275,12 @@ await section('expanded-frame', async () => {
check(JSON.stringify(before.frame) === JSON.stringify(reference) && Object.values(before.inputs).every(value => value.length > 0), 'Expanded outer frame matches actual Tunnel shared background, border and radius', { expanded:before.frame,reference,inputs:before.inputs });
const expectedColors = { hanmac:'rgb(203, 46, 40)',saman:'rgb(240, 131, 0)','jangheon-industry':'rgb(69, 128, 166)',jangheon:'rgb(29, 65, 141)',ptc:'rgb(132, 51, 142)',halla:'rgb(0, 104, 183)',baron:'rgb(12, 54, 127)' };
check(before.cards.length === 140 && before.cards.every(card => card.background === 'rgb(255, 255, 255)') && before.titles.every(title => title.color === 'rgb(255, 255, 255)' && title.background === expectedColors[title.company]), 'Expanded outer-frame style preserves white employee backgrounds and approved company/white group titles');
check(before.layout === 'tb' && before.editing === 'false' && await editSwitch().getAttribute('aria-checked') === 'false'
&& await page.getByRole('checkbox',{ name:'Pan 사용',exact:true }).getAttribute('aria-checked') === 'true'
&& await page.getByRole('checkbox',{ name:'Zoom 사용',exact:true }).getAttribute('aria-checked') === 'true'
&& await page.getByRole('checkbox',{ name:'접기·펼치기 사용',exact:true }).getAttribute('aria-checked') === 'true', 'Expanded frame correction preserves TB, default OFF and enabled browsing controls');
check(before.layout === 'tb' && before.editing === 'false' && before.pan === 'true' && before.zoom === 'true', 'Expanded frame correction preserves TB, fixed OFF and enabled browsing', before);
await snap('expanded-frame-off',true); await snap('expanded-frame-scope-removed',true);
await companyTrigger().click(); await companyPanel().waitFor(); await snap('expanded-frame-company-dropdown');
await page.keyboard.press('Escape'); await companyPanel().waitFor({ state:'hidden' });
await toggle(true); const on = await frameSnapshot();
check(JSON.stringify(on.frame) === JSON.stringify(before.frame) && JSON.stringify(on.cards) === JSON.stringify(before.cards) && JSON.stringify(on.titles) === JSON.stringify(before.titles), 'ON toggle leaves corrected outer frame and all card/title styling unchanged');
await snap('expanded-frame-on',true);
await page.setViewportSize({ width:390,height:1100 }); await settle(); const narrow = await frameSnapshot();
check(narrow.documentWidth <= 391 && JSON.stringify(narrow.frame) === JSON.stringify(reference), '390px corrected Expanded frame fits page and retains shared style', { width:narrow.width,scrollWidth:narrow.documentWidth,frame:narrow.frame });
await snap('expanded-frame-390',true);
await setup({ companies:['한맥기술','삼안'],enabled:true });
const captureMove = await drag(employee('virtual-022'),title('saman-office-0'));
await page.getByRole('button',{ name:'Fit to View',exact:true }).click(); await settle(); await snap('expanded-frame-company-moved',true);
measurements.push({ name:'Expanded outer-frame correction',reference,before,on,narrow,
captureOnlyMove:{ status:captureMove.after.feedback.status,employee:captureMove.after.cards.find(card => card.id === 'virtual-022'),countText:captureMove.after.countText,note:'Fresh moved sample screenshot; previous P9.4 movement PASS is retained without rerunning its assertions.' } });
measurements.push({ name:'Expanded outer-frame correction',reference,before,narrow });
});
await section('names-mobile', async () => {