Files
houseplan-card/demo/smoke_color_picker.mjs
T
Claude 82a48ee0ce perf(i18n): словари settings/support/topology — ru/de/fr ленивыми чанками; потолок онбординга (#627)
- ru/de/fr трёх словарей пространств — девять ленивых чанков (по одному на
  пару «пространство × язык»), английский статически как слой отката;
  рантайм — тот же LanguageRuntime (две попытки, отпечаток сборки, fallback,
  общий тост отказа), src/i18n/namespace-language.ts.
- Загрузчики рантаймов онбординга и редактора отдают поверхность в рендер
  только после того, как словарь текущего языка осел; гейт хоста учитывает
  словари загруженной на нём поверхности (смена языка на лету — ветка warm).
  Оверлей Zigbee ждёт topology до первой отрисовки.
- bundle-manifest: граф lazyNamespaceLocaleFiles, девять retry-токенов со
  строгим счётом «ровно один». bundle-budget: потолок онбординга 28 500
  (замер 27 470), редактор 245 400 → 228 900 (замер 227 869), проверки
  ленивости и владения по содержимому.
- Смок smoke_lazy_admin_locale (AC4–AC7); регресс-смоки, переключавшие язык
  синхронно, ждут гейт вместо ослабления проверок; пять мутантов.
- monolith-baseline bundleBytes 2 500 387 → 2 508 676: +10 чанков (rollup intro
  и export на каждый) и записи манифеста; gzip каждой поверхности при этом
  уменьшился.

Issue: #627
User-Visible: no
2026-09-24 14:51:18 +03:00

215 lines
9.1 KiB
JavaScript

import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch(
{ width: 390, height: 760 }, 1, [], { hasTouch: true, isMobile: true },
);
const result = await page.evaluate(async () => {
const out = {};
const card = window.__card;
const root = () => card.shadowRoot || card.renderRoot;
const frame = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
const settleMode = async () => {
const started = performance.now();
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
while (card._modeTransitionBusy && performance.now() - started < 1500);
await card.updateComplete;
};
[...root().querySelectorAll('.modetab')][2]?.click();
await settleMode();
card._decorTool = 'line';
card.requestUpdate();
await card.updateComplete;
const picker = root().querySelector('.editor-secondary hp-color-opacity');
const trigger = picker?.shadowRoot?.querySelector('.trigger');
const events = [];
picker?.addEventListener('hp-color-opacity-change', (event) => events.push(event.detail));
trigger?.click();
await picker?.updateComplete;
await frame();
const surface = () => picker?.shadowRoot?.querySelector('.picker');
const ranges = () => [...(surface()?.querySelectorAll('input[type="range"]') || [])];
out.oneSurfaceNoNativePicker = !!surface()
&& surface().getAttribute('role') === 'dialog'
&& !surface().querySelector('input[type="color"]');
out.allDimensionsVisible = !!surface()?.querySelector('.sv-field')
&& ranges().length === 4
&& !!surface()?.querySelector('input[type="text"]')
&& !!surface()?.querySelector('input[type="number"]');
out.englishLabels = surface()?.getAttribute('aria-label') === 'Color picker'
&& ranges().map((input) => input.getAttribute('aria-label')).join('|')
=== 'Hue|Saturation|Brightness|Opacity'
&& surface()?.querySelector('.confirm')?.textContent.trim() === 'OK';
const rect = surface()?.getBoundingClientRect();
out.narrowViewportFits = !!rect && rect.left >= 0 && rect.right <= innerWidth
&& rect.top >= 0 && rect.bottom <= innerHeight
&& surface().scrollWidth <= surface().clientWidth;
const hue = ranges()[0];
const hueStyle = getComputedStyle(hue);
out.hueTrackContract = hue?.classList.contains('hue-range')
&& hue.min === '0' && hue.max === '359' && hue.step === '1'
&& hueStyle.getPropertyValue('--hp-picker-hue-track').includes('linear-gradient')
&& hue.getBoundingClientRect().height >= 40;
const thumbShadow = hueStyle.getPropertyValue('--hp-picker-hue-thumb-shadow');
out.hueThumbContrastContract = thumbShadow.includes('0 0 0 2px')
&& thumbShadow.includes('0 0 0 3px');
ranges()[1].value = '100';
ranges()[1].dispatchEvent(new Event('input', { bubbles: true, composed: true }));
ranges()[2].value = '100';
ranges()[2].dispatchEvent(new Event('input', { bubbles: true, composed: true }));
hue.value = '120';
hue.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
await picker.updateComplete;
out.hueUpdatesSharedDraft = picker.color === '#00ff00'
&& card._decorStyle.color === '#00ff00';
const beforeShift = Number(ranges()[0].value);
ranges()[0].dispatchEvent(new KeyboardEvent('keydown', {
key: 'ArrowRight', shiftKey: true, bubbles: true, composed: true, cancelable: true,
}));
await picker.updateComplete;
out.shiftArrowUsesTenStep = Math.round(picker._hue) === beforeShift + 10;
const opacity = ranges().at(-1);
opacity.value = '37';
opacity.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
await picker.updateComplete;
out.opacitySharesTheSameSurface = picker.opacity === 0.37
&& card._decorStyle.opacity === 0.37;
let hex = surface().querySelector('input[type="text"]');
hex.value = '#0af';
hex.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
await picker.updateComplete;
out.shortHexNormalizesAndEmits = picker.color === '#00aaff'
&& events.at(-1)?.color === '#00aaff';
let parentClicks = 0;
card.addEventListener('click', () => { parentClicks += 1; });
let confirm = surface().querySelector('.confirm');
const confirmRect = confirm?.getBoundingClientRect();
const surfaceRect = surface()?.getBoundingClientRect();
out.confirmIsFullWidthTouchTarget = !!confirmRect && !!surfaceRect
&& confirmRect.height >= 40
&& Math.abs(confirmRect.width - (surface().clientWidth - 24)) <= 1
&& surface()?.lastElementChild === confirm;
const eventsBeforeConfirm = events.length;
const parentClicksBeforeConfirm = parentClicks;
confirm?.click();
await picker.updateComplete;
await frame();
out.confirmClosesWithoutDuplicateOrClickThrough = trigger?.getAttribute('aria-expanded') === 'false'
&& picker.shadowRoot.activeElement === trigger
&& events.length === eventsBeforeConfirm
&& parentClicks === parentClicksBeforeConfirm;
trigger?.click();
await picker.updateComplete;
await frame();
const eventCount = events.length;
hex = surface().querySelector('input[type="text"]');
hex.value = '#12';
hex.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
await picker.updateComplete;
hex.dispatchEvent(new FocusEvent('blur'));
await picker.updateComplete;
hex = surface().querySelector('input[type="text"]');
out.invalidHexNeverEmits = events.length === eventCount;
out.invalidHexNormalizesDraft = hex.value === '#00aaff';
out.invalidHexKeepsVisibleError = hex.getAttribute('aria-invalid') === 'true';
confirm = surface().querySelector('.confirm');
confirm?.click();
await picker.updateComplete;
await frame();
confirm = surface().querySelector('.confirm');
confirm?.click();
await picker.updateComplete;
await frame();
hex = surface().querySelector('input[type="text"]');
out.repeatedConfirmCannotBypassInvalidHex = trigger?.getAttribute('aria-expanded') === 'true'
&& events.length === eventCount
&& hex?.getAttribute('aria-invalid') === 'true'
&& picker.shadowRoot.activeElement === hex;
hex.value = '#123456';
hex.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
await picker.updateComplete;
const eventsAfterCorrection = events.length;
surface().querySelector('.confirm')?.click();
await picker.updateComplete;
await frame();
out.validCorrectionAllowsConfirm = trigger?.getAttribute('aria-expanded') === 'false'
&& picker.shadowRoot.activeElement === trigger
&& picker.color === '#123456'
&& events.length === eventsAfterCorrection;
trigger?.click();
await picker.updateComplete;
await frame();
const field = surface().querySelector('.sv-field');
const fieldRect = field.getBoundingClientRect();
const eventsBeforePointer = events.length;
field.dispatchEvent(new PointerEvent('pointerdown', {
pointerId: 41, pointerType: 'touch', clientX: fieldRect.left + fieldRect.width * .25,
clientY: fieldRect.top + fieldRect.height * .25, bubbles: true, composed: true, cancelable: true,
}));
field.dispatchEvent(new PointerEvent('pointermove', {
pointerId: 41, pointerType: 'touch', clientX: fieldRect.right + 20,
clientY: fieldRect.bottom + 20, bubbles: true, composed: true, cancelable: true,
}));
field.dispatchEvent(new PointerEvent('pointercancel', {
pointerId: 41, pointerType: 'touch', bubbles: true, composed: true, cancelable: true,
}));
await frame();
out.pointerCancelAddsNoEvent = events.length === eventsBeforePointer + 1
&& events.at(-1)?.color === picker.color
&& picker._activePointerId === null;
picker.showOpacity = false;
await picker.updateComplete;
out.colorOnlyKeepsColorControls = ranges().length === 3
&& !surface().querySelector('input[type="number"]')
&& surface()?.lastElementChild?.classList.contains('confirm');
picker.showOpacity = true;
await picker.updateComplete;
card._config = { ...card._config, language: 'ru' };
card.requestUpdate();
await card.updateComplete;
// #627: editor-dialog ru strings are a lazy chunk; the live switch holds the
// previous frame (inert + aria-busy) until it settles — wait for the gate.
for (let i = 0; i < 250 && card.hasAttribute('aria-busy'); i++) await new Promise((r) => setTimeout(r, 20));
await card.updateComplete;
await picker.updateComplete;
out.cardLanguageOwnsCopy = surface()?.getAttribute('aria-label') === 'Выбор цвета'
&& surface()?.querySelector('input[type="text"]')?.getAttribute('aria-label') === 'Цвет HEX'
&& surface()?.querySelector('.confirm')?.textContent.trim() === 'ОК';
const escapeControl = surface()?.querySelector('input[type="range"]');
escapeControl?.focus();
escapeControl?.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Escape', bubbles: true, composed: true, cancelable: true,
}));
await picker.updateComplete;
await frame();
out.escapeClosesFirstAndRefocuses = trigger?.getAttribute('aria-expanded') === 'false'
&& picker.shadowRoot.activeElement === trigger;
picker.disabled = true;
await picker.updateComplete;
trigger?.click();
await picker.updateComplete;
out.disabledDoesNotOpen = trigger?.getAttribute('aria-expanded') === 'false';
return out;
});
checkAll(result);
await finish(browser, result);