mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
237 lines
9.9 KiB
JavaScript
237 lines
9.9 KiB
JavaScript
// #140: opening and physical-object properties share the existing medium
|
|
// dialog shell. Measure real localized buttons rather than merely checking
|
|
// the `wide` attribute, because the contract is usable spare width plus safe
|
|
// wrapping when the viewport becomes narrow.
|
|
import { launch, checkAll, finish } from './serve.mjs';
|
|
|
|
const DESKTOP_WIDTH = 1000;
|
|
const NARROW_WIDTH = 320;
|
|
const { page, browser } = await launch({ width: DESKTOP_WIDTH, height: 820 }, 1);
|
|
|
|
const desktop = await page.evaluate(async () => {
|
|
const card = window.__card;
|
|
const settle = async () => {
|
|
await card.updateComplete;
|
|
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
};
|
|
const settleLanguage = async (language) => {
|
|
for (let attempt = 0; language === 'de' && card._t('btn.save') !== 'Speichern'
|
|
&& attempt < 50; attempt++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
await settle();
|
|
}
|
|
};
|
|
card._serverStorage = true;
|
|
card._serverCfg = {
|
|
spaces: [{
|
|
id: 'dialog-layout', title: 'Dialog layout', cell_cm: 5,
|
|
view_box: [0, 0, 1, 1], rooms: [], settings: {},
|
|
}],
|
|
markers: [], settings: {},
|
|
};
|
|
card._space = 'dialog-layout';
|
|
card._mode = 'plan';
|
|
card._modelCache = null;
|
|
card._saveConfig = () => {};
|
|
await settle();
|
|
|
|
const layout = (kind, language) => {
|
|
const root = card.shadowRoot || card.renderRoot;
|
|
const dialog = root.querySelector('hp-dialog');
|
|
const surface = dialog?.shadowRoot?.querySelector('.surface');
|
|
const footer = dialog?.querySelector('.dialog-action-footer');
|
|
const danger = footer?.querySelector('.dialog-action-danger');
|
|
const commit = footer?.querySelector('.dialog-action-commit');
|
|
const surfaceRect = surface?.getBoundingClientRect();
|
|
const footerRect = footer?.getBoundingClientRect();
|
|
const footerStyle = footer ? getComputedStyle(footer) : null;
|
|
const dangerRect = danger?.getBoundingClientRect();
|
|
const commitRect = commit?.getBoundingClientRect();
|
|
const buttons = [...(footer?.querySelectorAll('button') || [])];
|
|
const innerWidth = footerRect && footerStyle
|
|
? footerRect.width - parseFloat(footerStyle.paddingLeft) - parseFloat(footerStyle.paddingRight)
|
|
: 0;
|
|
const contentWidth = (dangerRect?.width || 0) + (commitRect?.width || 0);
|
|
return {
|
|
kind,
|
|
language,
|
|
widePreset: dialog?.wide === true && dialog.hasAttribute('wide'),
|
|
surfaceWidth: Number((surfaceRect?.width || 0).toFixed(2)),
|
|
innerWidth: Number(innerWidth.toFixed(2)),
|
|
contentWidth: Number(contentWidth.toFixed(2)),
|
|
spareWidth: Number((innerWidth - contentWidth).toFixed(2)),
|
|
buttons: buttons.length,
|
|
oneRow: !!dangerRect && !!commitRect && Math.abs(dangerRect.top - commitRect.top) <= 1,
|
|
noHorizontalOverflow: !!surface && !!footer
|
|
&& surface.scrollWidth <= surface.clientWidth + 1
|
|
&& footer.scrollWidth <= footer.clientWidth + 1,
|
|
};
|
|
};
|
|
|
|
const open = async (kind, language) => {
|
|
card._config = { ...(card._config || {}), language };
|
|
card._openingDialog = null;
|
|
card._physicalDialog = null;
|
|
card._spaceDialog = null;
|
|
if (kind === 'opening') {
|
|
card._openingDialog = {
|
|
id: 'opening-1', type: 'door', lengthCm: 90,
|
|
contact: '', lock: '', invert: false, flipH: false, flipV: false,
|
|
x: 200, y: 200, angle: 0,
|
|
};
|
|
} else if (kind === 'physical') {
|
|
card._physicalDialog = {
|
|
kind: 'partition', id: 'partition-1', cm: '12', length: '1 m',
|
|
};
|
|
} else {
|
|
card._openSpaceDialog('edit', 'dialog-layout');
|
|
}
|
|
await settle();
|
|
await settleLanguage(language);
|
|
const result = layout(kind, language);
|
|
card._openingDialog = null;
|
|
card._physicalDialog = null;
|
|
card._spaceDialog = null;
|
|
await settle();
|
|
return result;
|
|
};
|
|
|
|
const metrics = [];
|
|
for (const language of ['en', 'ru', 'de']) {
|
|
metrics.push(await open('opening', language));
|
|
metrics.push(await open('physical', language));
|
|
}
|
|
const space = await open('space', 'ru');
|
|
return { metrics, space };
|
|
});
|
|
|
|
await page.setViewportSize({ width: NARROW_WIDTH, height: 820 });
|
|
|
|
const narrow = await page.evaluate(async () => {
|
|
const card = window.__card;
|
|
const settle = async () => {
|
|
await card.updateComplete;
|
|
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
};
|
|
const settleLanguage = async (language) => {
|
|
for (let attempt = 0; language === 'de' && card._t('btn.save') !== 'Speichern'
|
|
&& attempt < 50; attempt++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
await settle();
|
|
}
|
|
};
|
|
const measure = async (kind, language = 'ru') => {
|
|
card._config = { ...(card._config || {}), language };
|
|
card._openingDialog = null;
|
|
card._physicalDialog = null;
|
|
card._spaceDialog = null;
|
|
if (kind === 'opening') {
|
|
card._openingDialog = {
|
|
id: 'opening-1', type: 'door', lengthCm: 90,
|
|
contact: '', lock: '', invert: false, flipH: false, flipV: false,
|
|
x: 200, y: 200, angle: 0,
|
|
};
|
|
} else if (kind === 'physical') {
|
|
card._physicalDialog = {
|
|
kind: 'partition', id: 'partition-1', cm: '12', length: '1 m',
|
|
};
|
|
} else {
|
|
card._openSpaceDialog('edit', 'dialog-layout');
|
|
}
|
|
await settle();
|
|
await settleLanguage(language);
|
|
const root = card.shadowRoot || card.renderRoot;
|
|
const dialog = root.querySelector('hp-dialog');
|
|
const surface = dialog?.shadowRoot?.querySelector('.surface');
|
|
const footer = dialog?.querySelector('.dialog-action-footer');
|
|
const danger = footer?.querySelector('.dialog-action-danger');
|
|
const commit = footer?.querySelector('.dialog-action-commit');
|
|
const surfaceRect = surface?.getBoundingClientRect();
|
|
const footerRect = footer?.getBoundingClientRect();
|
|
const footerStyle = footer ? getComputedStyle(footer) : null;
|
|
const dangerRect = danger?.getBoundingClientRect();
|
|
const commitRect = commit?.getBoundingClientRect();
|
|
const innerLeft = footerRect && footerStyle
|
|
? footerRect.left + parseFloat(footerStyle.paddingLeft) : 0;
|
|
const innerRight = footerRect && footerStyle
|
|
? footerRect.right - parseFloat(footerStyle.paddingRight) : 0;
|
|
const buttons = [...(footer?.querySelectorAll('button') || [])];
|
|
const result = {
|
|
kind,
|
|
language,
|
|
surfaceWidth: Number((surfaceRect?.width || 0).toFixed(2)),
|
|
insideViewport: !!surfaceRect && surfaceRect.left >= -1
|
|
&& surfaceRect.right <= innerWidth + 1,
|
|
noHorizontalOverflow: !!surface && !!footer
|
|
&& surface.scrollWidth <= surface.clientWidth + 1
|
|
&& footer.scrollWidth <= footer.clientWidth + 1,
|
|
buttonsContained: !!footerRect && buttons.every((button) => {
|
|
const rect = button.getBoundingClientRect();
|
|
return rect.left >= innerLeft - 1 && rect.right <= innerRight + 1
|
|
&& rect.top >= footerRect.top - 1 && rect.bottom <= footerRect.bottom + 1;
|
|
}),
|
|
wrapped: !!dangerRect && !!commitRect && commitRect.top >= dangerRect.bottom - 1,
|
|
oneRow: buttons.length > 0 && buttons.every((button) =>
|
|
Math.abs(button.getBoundingClientRect().top - buttons[0].getBoundingClientRect().top) <= 1),
|
|
iconOnlyDestructive: kind !== 'space' || [...footer.querySelectorAll('.hpf-mobile-icon')].every((button) =>
|
|
button.getAttribute('aria-label') && getComputedStyle(button.querySelector('.hpf-action-label')).display === 'none'),
|
|
buttons: buttons.length,
|
|
};
|
|
card._openingDialog = null;
|
|
card._physicalDialog = null;
|
|
card._spaceDialog = null;
|
|
await settle();
|
|
return result;
|
|
};
|
|
return {
|
|
opening: await measure('opening'),
|
|
physical: await measure('physical'),
|
|
space: await measure('space'),
|
|
opening_de: await measure('opening', 'de'),
|
|
physical_de: await measure('physical', 'de'),
|
|
space_de: await measure('space', 'de'),
|
|
};
|
|
});
|
|
|
|
const checks = {};
|
|
for (const metric of desktop.metrics) {
|
|
const key = `${metric.kind}_${metric.language}`;
|
|
checks[`${key}_medium_shell`] = metric.widePreset
|
|
&& metric.surfaceWidth >= 499 && metric.surfaceWidth <= 501;
|
|
checks[`${key}_three_actions_one_row`] = metric.buttons === 3 && metric.oneRow;
|
|
checks[`${key}_positive_localization_headroom`] = metric.spareWidth > 0;
|
|
checks[`${key}_no_horizontal_overflow`] = metric.noHorizontalOverflow;
|
|
}
|
|
// #600 Q7: диалоги настроек (`form-shell`) — 560 px по референсу; остальные
|
|
// wide-диалоги остаются на 500 и проверяются циклом выше.
|
|
checks.space_dialog_uses_reference_width = desktop.space.widePreset
|
|
&& desktop.space.surfaceWidth >= 559 && desktop.space.surfaceWidth <= 561;
|
|
checks.space_dialog_footer_not_regressed = desktop.space.buttons === 4
|
|
&& desktop.space.oneRow && desktop.space.noHorizontalOverflow;
|
|
|
|
for (const kind of ['opening', 'physical', 'opening_de', 'physical_de']) {
|
|
const metric = narrow[kind];
|
|
checks[`${kind}_narrow_fits_viewport`] = metric.surfaceWidth <= NARROW_WIDTH * 0.94 + 1
|
|
&& metric.insideViewport && metric.noHorizontalOverflow && metric.buttonsContained;
|
|
checks[`${kind}_narrow_keeps_responsive_wrap`] = metric.buttons === 3 && metric.wrapped;
|
|
}
|
|
checks.space_narrow_not_regressed = narrow.space.buttons === 4
|
|
&& narrow.space.insideViewport && narrow.space.noHorizontalOverflow
|
|
&& narrow.space.buttonsContained && narrow.space.oneRow && narrow.space.iconOnlyDestructive;
|
|
checks.space_de_narrow_not_regressed = narrow.space_de.buttons === 4
|
|
&& narrow.space_de.insideViewport && narrow.space_de.noHorizontalOverflow
|
|
&& narrow.space_de.buttonsContained && narrow.space_de.oneRow && narrow.space_de.iconOnlyDestructive;
|
|
|
|
checkAll(checks);
|
|
await finish(browser, {
|
|
checks,
|
|
desktop: {
|
|
dialogs: desktop.metrics,
|
|
longest: desktop.metrics.reduce((longest, metric) => (
|
|
metric.contentWidth > longest.contentWidth ? metric : longest
|
|
)),
|
|
space: desktop.space,
|
|
},
|
|
narrow,
|
|
});
|