diff --git a/demo/smoke_moon_status.mjs b/demo/smoke_moon_status.mjs index e72ab085..11317684 100644 --- a/demo/smoke_moon_status.mjs +++ b/demo/smoke_moon_status.mjs @@ -80,6 +80,29 @@ const closeDialog = (page) => page.evaluate(async () => { const result = await window.__hpTest.close(dialog); return { closed: result.closed, confirm: !!result.confirm }; }); +/** + * #731: Lovelace replaces the card — remove, then a new element with the same + * config in the same slot (as in smoke_warm_dialogs); `window.__card` follows + * the successor. True once the successor has revived the dialog of `kind`. + */ +const remount = (page, kind) => page.evaluate(async (kind) => { + const old = window.__card; + const host = old.parentNode; + const hass = old.hass; + old.remove(); + await new Promise((done) => setTimeout(done, 20)); + const card = document.createElement('houseplan-card'); + card.setConfig({ type: 'custom:houseplan-card', title: 'House Plan', icon_size: 3.4 }); + card.hass = hass; + host.appendChild(card); + window.__card = card; + const revived = () => !!card.renderRoot?.querySelector(`hp-dialog[data-kind="${kind}"]`); + // The page clock is fixed: count steps, not milliseconds. + for (let step = 0; step < 250 && !revived(); step++) await new Promise((done) => setTimeout(done, 20)); + await card.updateComplete; + return revived(); +}, kind); +const draftOf = (page) => page.evaluate(() => JSON.stringify(window.__card._settingsDialog)); // ─── AC11 and AC12 (chunk already loaded by the View at night) ─── { @@ -161,6 +184,74 @@ const closeDialog = (page) => page.evaluate(async () => { await browser.close(); } +// ─── #731 AC1/AC2: a warm revive is an opening of its own ─── +{ + const { page, browser } = await open({ moon: true }); + await openDialog(page); + const opened = await line(page); + const draft = await draftOf(page); + // The sun rises while the dialog is open: this opening keeps its snapshot… + await pushHass(page, { sun: { azimuth: 200, elevation: 25.4, rising: false } }); + const stale = await line(page); + // …and the card is replaced. The revived dialog is a new opening: its own + // snapshot now, nothing carried over from the dead instance. + const revived = await remount(page, 'settings'); + const arrived = await waitLine(page, 4000); + const after = await line(page); + const draftAfter = await draftOf(page); + await closeDialog(page); + // A regular opening at the same moment (the page clock is fixed). + await openDialog(page); + const regular = await line(page); + report.r731 = { opened, stale, revived, after, regular }; + checks.r731_ac1_revivedDialogHasTheLine = revived && arrived && after.count === 1; + checks.r731_ac1_sameAsARegularOpening = after.text === regular.text && after.reason === regular.reason + && after.text === DAY_SUN; + checks.r731_ac2_reviveTakesItsOwnSnapshot = opened.reason === 'shown' && stale.reason === 'shown' + && after.reason === 'day_sun'; + checks.r731_ac2_lineLeavesTheRevivedDraftClean = after.saveDisabled && draftAfter === draft; + await closeDialog(page); + // In an editor the successor first waits for its own lazy runtime, then + // adopts the mode and only then revives the dialog. + await page.evaluate(() => window.__hpTest.setMode('plan')); + await openDialog(page); + const revivedInPlan = await remount(page, 'settings'); + const arrivedInPlan = await waitLine(page, 4000); + const inPlan = await line(page); + const planMode = await page.evaluate(() => window.__card._mode); + report.r731.plan = { revivedInPlan, planMode, inPlan }; + checks.r731_ac1_revivedInAnEditorHasTheLine = revivedInPlan && arrivedInPlan && planMode === 'plan' + && inPlan.count === 1 && inPlan.text === regular.text && inPlan.saveDisabled; + await closeDialog(page); + await browser.close(); +} + +// ─── #731 AC3: other revives never ask for the chunk; a revive while it loads ─── +{ + const delay = async (route) => { await new Promise((done) => setTimeout(done, 1000)); await route.fallback(); }; + const { page, browser, moonRequests } = await open({ route: delay }); + const space = await page.evaluate(() => window.__card._space); + await page.evaluate((id) => window.__hpTest.openSpaceDialog('edit', id), space); + const revivedSpace = await remount(page, 'space'); + await page.waitForTimeout(600); + const afterSpace = moonRequests.length; + await page.evaluate(async () => { + const dialog = window.__card.renderRoot.querySelector('hp-dialog[data-kind="space"]'); + if (dialog) await window.__hpTest.close(dialog); + }); + // The opening asks for the chunk; the card is replaced before it lands. + await openDialog(page); + const revived = await remount(page, 'settings'); + const arrived = await waitLine(page, 3000); + const late = await line(page); + report.r731.ac3 = { revivedSpace, afterSpace, revived, late, requests: moonRequests.length }; + checks.r731_ac3_otherRevivesLeaveTheChunkAlone = revivedSpace && afterSpace === 0; + checks.r731_ac3_reviveWhileTheChunkLoads = revived && arrived && late.count === 1 + && late.reason === 'shown' && late.text === SHOWN && late.saveDisabled && moonRequests.length === 1; + await closeDialog(page); + await browser.close(); +} + // ─── AC12: the chunk request is refused ─── { const { page, browser } = await open({ route: (route) => route.abort() }); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7ea3840d..92f9e690 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,6 +12,9 @@ that switch a new line tells whether the moon is shown right now and, if not, why — daytime, the moon below 3°, new moon or no home location in Home Assistant ([#718](https://github.com/Matysh/houseplan-card/issues/718)). +- General settings restored after the card reloads — for example on returning + to the tab — now show the moon's "Now: …" line too, worked out afresh at that + moment ([#731](https://github.com/Matysh/houseplan-card/issues/731)). ## v1.79.0-beta.1 — 2026-09-30 diff --git a/docs/CHANGELOG.ru.md b/docs/CHANGELOG.ru.md index d8999e46..35c604bb 100755 --- a/docs/CHANGELOG.ru.md +++ b/docs/CHANGELOG.ru.md @@ -18,6 +18,10 @@ появилась строка: видна ли луна сейчас, а если нет — почему (день, луна ниже 3°, новолуние или в Home Assistant не заданы координаты дома) ([#718](https://github.com/Matysh/houseplan-card/issues/718)). +- Общие настройки, восстановленные после перезагрузки карточки — например, при + возврате на вкладку, — тоже показывают строку луны «Сейчас: …», рассчитанную + заново на этот момент + ([#731](https://github.com/Matysh/houseplan-card/issues/731)). ## v1.79.0-beta.1 — 2026-09-30 diff --git a/docs/SUN.md b/docs/SUN.md index a400d4c0..ee890895 100644 --- a/docs/SUN.md +++ b/docs/SUN.md @@ -494,7 +494,10 @@ do not change it. `moonStatus` in `src/moon.ts` decides «shown» with the same month), rounds to whole numbers and keeps a hidden reason's number below its threshold (2.6° reads «2°»). The status lives beside the draft, never in it: the line arriving leaves «Save» disabled. While the chunk loads, or when it -failed, there is no line; a closed opening's result is dropped. The line +failed, there is no line; a closed opening's result is dropped. A warm revive +of the dialog (the card replaced, `docs/WARM-REMOUNT.md`) is an opening too: +its own snapshot, asked through the lazy editor runtime once it is there; +nothing is carried over from the replaced card (#731). The line belongs to the browser the dialog is open in — a wall tablet with another clock or time zone may differ. diff --git a/docs/WARM-REMOUNT.md b/docs/WARM-REMOUNT.md index 981e83cf..f68ed0bb 100644 --- a/docs/WARM-REMOUNT.md +++ b/docs/WARM-REMOUNT.md @@ -231,6 +231,10 @@ hover. После долгого сна общий `VisualContinuityController` обвязкой (`_roomEditId`, `_roomFill`, источники температуры и влажности, масштабы подписей, `_areaSel`/`_nameSel`, `_pendingSplit`, `_path`). +Строка статуса луны в общих настройках (#718 K7) живёт вне черновика и не +переносится: восстановление — новое открытие, статус снимается заново, как +только у нового экземпляра есть ленивый рантайм редактора (#731). + Информационные попапы — `_infoCard` (карточка устройства) и `_openingInfo` — восстанавливаются **по id**, а не по объекту: конфиг мог перезагрузиться под нами, и карточка, отрисованная из устаревшего объекта, была бы враньём. Если diff --git a/scripts/smoke-links.mjs b/scripts/smoke-links.mjs index 92ec8a09..43f30383 100644 --- a/scripts/smoke-links.mjs +++ b/scripts/smoke-links.mjs @@ -43,7 +43,7 @@ export const SMOKE_LINKS = [ 'moonLayer', 'renderMoon', 'moonTick', 'moonView', 'moonShownAt', 'moonPhasePath', 'moonPosition', 'moonIllumination', 'moonFingerprint', 'renderDayCycleEnvironment', 'withMoon', 'moonSkyState', 'dayCycleClock', 'renderMoonSky', '_moonSkyState', '_dayCycleClock', - 'moonStatus', 'moonStatusOf', 'openMoonStatus', 'moonStatusText', + 'moonStatus', 'moonStatusOf', 'openMoonStatus', 'moonStatusText', '_reviveMoonStatus', '_openMoonStatus', ], smokes: ['smoke_moon.mjs', 'smoke_moon_static.mjs', 'smoke_moon_status.mjs', 'smoke_daycycle_layer_budget.mjs'], because: '#661: the moon is observed only as `.hp-moon` in the production bundle — the lazy ' @@ -52,6 +52,7 @@ export const SMOKE_LINKS = [ + '#718: the same over a static background as `.hp-moon-sky` (the clock ticker at 08:00 and ' + '18:00, tab and background switches, the #101 View weight, houseplan-space-card) and the ' + 'General settings line as `[data-moon-status]` (delayed, refused and preloaded chunk); ' + + '#731: that line in a dialog revived after a warm remount (View, plan editor, chunk loading); ' + 'no smoke names the functions', }, { diff --git a/src/houseplan-card.ts b/src/houseplan-card.ts index 4ccce6a5..7347982e 100755 --- a/src/houseplan-card.ts +++ b/src/houseplan-card.ts @@ -3497,7 +3497,7 @@ export class HouseplanCard extends LitElement { switch (d.kind) { case 'space': this._spaceDialog = { ...d.data, busy: false, savedBusy: false }; break; case 'marker': this._markerDialog = { ...d.data, busy: false }; break; - case 'settings': this._settingsDialog = { ...d.data, busy: false }; break; + case 'settings': this._settingsDialog = { ...d.data, busy: false }; this._reviveMoonStatus(); break; case 'rules': this._rulesDialog = { ...d.data, busy: false }; break; case 'opening': this._openingDialog = { ...d.data }; break; case 'backdrop': this._backdropDialog = { ...d.data }; break; @@ -10125,6 +10125,15 @@ export class HouseplanCard extends LitElement { return this._editorRuntime._openSettingsDialog(); } + /** #731: a warm revive is an opening too (#718 K7) — its own status, asked once the lazy runtime is here. */ + private _reviveMoonStatus(dialog = this._settingsDialog): void { + if (!this._editorRuntime) { + void this._ensureEditorRuntime().then((ready) => { if (ready) this._reviveMoonStatus(dialog); }); + return; + } + if (dialog && this._settingsDialog === dialog) this._editorRuntime._openMoonStatus(); + } + private _openSupportDialog = (): void => { if (!this._editorRuntime) { void this._ensureEditorRuntime().then((ready) => { diff --git a/src/houseplan-editor-runtime.ts b/src/houseplan-editor-runtime.ts index 426e7bdd..f5f3d65f 100644 --- a/src/houseplan-editor-runtime.ts +++ b/src/houseplan-editor-runtime.ts @@ -8468,6 +8468,9 @@ public _openSettingsDialog = (): void => { openMoonStatus(this.host); // #718 K7: the status line, outside the draft }; +/** #731: the card revived General settings — a new opening, so a new snapshot. */ +public _openMoonStatus(): void { openMoonStatus(this.host); } + public _openSupportDialog = (): void => { if (!this.host._norm || !this.host._canEdit) return; clearTimeout(this._supportExpiryTimer); diff --git a/test/moon-settings.test.mjs b/test/moon-settings.test.mjs index db335732..36acad57 100644 --- a/test/moon-settings.test.mjs +++ b/test/moon-settings.test.mjs @@ -1,12 +1,16 @@ // #661 AC10: the moon switch in General settings — «Sun and Moon» section, // reset to on, save writes `true` or removes the key; its strings exist in all -// four dictionaries. #718: the status line under it (AC14, K7). The dialog is -// a lazy Lit module (its layout is read as a source contract); reading and -// writing the key and the status of an opening are executed. +// four dictionaries. #718: the status line under it (AC14, K7); #731: a warm +// revive of the dialog is an opening of its own. The dialog is a lazy Lit +// module (its layout is read as a source contract); reading and writing the +// key and the status of an opening are executed. import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; -import { generalDraftKey, moonDraftOf, writeMoonSetting } from '../test-build/editors/general-form-state.js'; +import { + generalDirty, generalDraftKey, moonDraftOf, rememberGeneralBaseline, writeMoonSetting, +} from '../test-build/editors/general-form-state.js'; +import { restoreWarmDialogBaseline, warmDialogBaseline } from '../test-build/editors/dialog-baseline.js'; import { moonStatusOf, openMoonStatus } from '../test-build/editors/moon-status.js'; const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8'); @@ -155,3 +159,35 @@ test('#718 K7: one snapshot per opening, outside the draft; a closed opening’s openMoonStatus(host, new Date(2026, 9, 21, 12, 0)); assert.deepEqual(moonStatusOf(host), { reason: 'day_clock' }); }); + +test('#731 AC2: a warm revive is a new opening — its own snapshot, nothing carried over, the draft untouched', async () => { + const at = new Date('2026-10-21T18:00:00Z'); + // The instance Lovelace replaced: opened at night, judged, its draft clean. + const dead = dialogHost(hassWith(NIGHT)); + rememberGeneralBaseline(dead, dead._settingsDialog); + openMoonStatus(dead, at); + await settle(); + assert.deepEqual(moonStatusOf(dead), { reason: 'shown', alt: 24, pct: 79 }); + // Its successor, as `_warmReviveDialog` hands the dialog over — the draft + // copied, the baseline restored — after the sun has risen. + const live = dialogHost(hassWith({ azimuth: 180, elevation: 40, rising: false })); + live._settingsDialog = { ...dead._settingsDialog, busy: false }; + restoreWarmDialogBaseline(live, 'settings', warmDialogBaseline(dead, 'settings')); + assert.equal(moonStatusOf(live), undefined, 'the dead opening’s result is not carried over'); + const key = generalDraftKey(live._settingsDialog); + assert.equal(generalDirty(live, live._settingsDialog), false); + // The revive's own opening (the runtime's `_openMoonStatus`): the snapshot of now. + openMoonStatus(live, at); + assert.deepEqual(moonStatusOf(live), { reason: 'day_sun', sun: 40 }); + assert.equal(live.updates, 1); + assert.equal(generalDraftKey(live._settingsDialog), key, 'the line stays outside the draft'); + assert.equal(generalDirty(live, live._settingsDialog), false, 'the line does not make it dirty'); + // A dirty draft travels the same way: the line does not clean it either. + live._settingsDialog = { ...live._settingsDialog, moon: false }; + openMoonStatus(live, at); + assert.equal(generalDirty(live, live._settingsDialog), true); + // The same element re-attached: its earlier opening is replaced, not reused. + live.hass = hassWith(NIGHT); + openMoonStatus(live, at); + assert.deepEqual(moonStatusOf(live), { reason: 'shown', alt: 24, pct: 79 }); +});