diff --git a/demo/smoke_binding_picker.mjs b/demo/smoke_binding_picker.mjs new file mode 100644 index 00000000..159bf100 --- /dev/null +++ b/demo/smoke_binding_picker.mjs @@ -0,0 +1,96 @@ +// Пикер привязок: что пользователь видит в списке «Добавить» (#263). +// +// Зачем смок, а не юнит-тест. `_bindingCandidates` — приватный метод карточки, +// читающий `_planHass`, `_devices`, `_markers` и состояние диалога. Переписать +// его логику в тесте значит проверять копию: ровно так дефект #262 и дожил до +// отчёта пользователя — надгробия были покрыты со всех сторон, а список никто +// не спрашивал. Здесь дёргается настоящий метод собранного бандла. +import { launch, checkAll, finish } from './serve.mjs'; +const { page, browser } = await launch(); +const out = {}; + +Object.assign(out, await page.evaluate(async () => { + const o = {}; + const c = window.__card; + const nativeConfirm = window.confirm; + + // Пикер читает `showEntities` и `bindingFilter` из открытого диалога, поэтому + // спрашивать его при закрытом бессмысленно: отдельные сущности устройств не + // перечисляются вовсе, и «сущность вернулась» ничего не значило бы. + const openAdd = async (showEntities) => { + c._openMarkerDialog(); + await c.updateComplete; + c._markerDialog = { ...c._markerDialog, showEntities: !!showEntities, bindingFilter: '' }; + await c.updateComplete; + }; + const offered = async (showEntities) => { + await openAdd(showEntities); + return c._bindingCandidates().map((item) => item.value); + }; + const remove = async (device) => { + c._openMarkerDialog(device); + await c.updateComplete; + window.confirm = () => true; + await c._deleteMarker(); + await c.updateComplete; + window.confirm = nativeConfirm; + }; + const tombstones = () => (c._serverCfg.markers || []).filter((m) => m.removed === true); + + // --- 1. удалили устройство — оно снова предлагается ---------------------- + const device = c._devices.find((d) => d.bindingKind === 'device' && d.bindingRef && !d.hidden); + o.standHasDeviceMarker = !!device; + const deviceBinding = `device:${device.bindingRef}`; + const childEntity = Object.entries(c._planHass.entities) + .find(([, reg]) => reg.device_id === device.bindingRef)?.[0]; + o.standHasChildEntity = !!childEntity; + + o.placedDeviceNotOffered = !(await offered(false)).includes(deviceBinding); + await remove(device); + o.deleteLeavesTombstone = tombstones().some((m) => m.binding === deviceBinding); + o.deletedDeviceOfferedAgain = (await offered(false)).includes(deviceBinding); + + // --- 2. чекбокс «показывать сущности» и есть ловушка --------------------- + // «Нет в списке» и «спрятано за галкой» для пользователя неразличимы: для + // нового маркера галка выключена, и обычная сущность устройства отсутствует. + const withoutCheckbox = await offered(false); + const withCheckbox = await offered(true); + const plainEntity = Object.entries(c._planHass.entities) + .find(([eid, reg]) => reg.device_id && reg.device_id !== device.bindingRef + && !withoutCheckbox.includes(`entity:${eid}`))?.[0]; + o.standHasPlainEntity = !!plainEntity; + o.plainEntityHiddenWithoutCheckbox = !withoutCheckbox.includes(`entity:${plainEntity}`); + o.plainEntityShownWithCheckbox = withCheckbox.includes(`entity:${plainEntity}`); + o.checkboxOffForNewMarker = (await (async () => { + c._openMarkerDialog(); + await c.updateComplete; + return c._markerDialog.showEntities; + })()) === false; + + // --- 3. известный дефект #262 ------------------------------------------- + // Надгробие ключуется по точной строке привязки, поэтому `device:` + // разблокирует только само устройство: `isRemovedPlanEntity` продолжает + // выкидывать из списка все его дочерние сущности. Здесь закреплено ТЕКУЩЕЕ + // поведение — починят #262, и смок покраснеет, потребовав перевернуть + // проверку. Молча пройти фикс мимо покрытия не сможет. + o.knownDefect262ChildEntityBlocked = + !(await offered(true)).includes(`entity:${childEntity}`); + + // --- 4. повторное добавление возвращает устройство в список размещённых -- + await openAdd(false); + c._markerDialog = { ...c._markerDialog, bindingMode: 'ha', binding: deviceBinding, + name: device.name }; + await c._saveMarker(); + await c.updateComplete; + o.readdReplacesTombstone = !tombstones().some((m) => m.binding === deviceBinding) + && (c._serverCfg.markers || []).some((m) => m.binding === deviceBinding); + o.readdRemovesFromPicker = !(await offered(false)).includes(deviceBinding); + + c._markerDialog = null; + c._setMode('view'); + await c.updateComplete; + return o; +})); + +checkAll(out); +await finish(browser, out); diff --git a/scripts/smoke-links.mjs b/scripts/smoke-links.mjs index 0a84049a..3627d5ef 100644 --- a/scripts/smoke-links.mjs +++ b/scripts/smoke-links.mjs @@ -27,6 +27,19 @@ /** @type {SmokeLink[]} */ export const SMOKE_LINKS = [ + { + // #263: пикер называет себя (`_bindingCandidates`) и потому находится + // прямым совпадением. Регистрируются только чистые помощники надгробий: + // смок не произносит ни одного их имени — он видит лишь список, который + // они отфильтровали. Проверено зондом: правка одного `src/devices.ts` без + // этой записи не выбирает ни одного смока. + symbols: ['removedPlanBindings', 'isRemovedPlanEntity', 'deletePlanMarkerRecords'], + smokes: ['smoke_binding_picker.mjs'], + because: 'смок проверяет, что удалённая привязка снова предлагается в списке «Добавить», ' + + 'что отдельные сущности устройств живут за галкой и что надгробие устройства ' + + 'по-прежнему прячет его дочерние сущности (#262) — всё это следствия фильтров ' + + 'надгробий, чьи имена в браузерный сценарий не попадают', + }, { // #258: the browser sees only the resulting path/caches; it cannot call // the pure identity helpers by name through the production bundle.