test: cover the space copy workflow

Issue: #456
User-Visible: no
Release: v1.72.0-beta.4
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33948778925
This commit is contained in:
Sergey Matyunin
2026-09-05 09:25:08 +03:00
parent 5108202419
commit 7dd5b4798a
3 changed files with 146 additions and 4 deletions
+5 -4
View File
@@ -1,11 +1,12 @@
{
"schema": 1,
"matrixVersion": 54,
"acceptedAt": "2026-09-04T13:10:56.492Z",
"sourceFingerprint": "7acb1b9d3bb384bc1a0c70fb01347386c157ae88b12b00ccd876b7658fc84a1c",
"acceptedAt": "2026-09-05T06:21:56.683Z",
"sourceFingerprint": "814851e1e157fd126309de061c720fb1203ddd6d63824cd37289542baefca406",
"chromium": "151.0.7922.34",
"platform": "linux",
"witnesses": {
"count": 127,
"count": 134,
"floor": 10
},
"scenarios": {
@@ -152,7 +153,7 @@
"support-relay-error-light-en": "e9a9ad906e106eedfa6e84c592325f1604eb64c89c51135c076ac87a6a0766b2",
"support-tablet-preview-dark-ru": "1264e005cd764408edb9b285a5dfbe92ceb90cb925f624b8f65cd47edb96c0c9",
"device-ripple-color-popover-mobile-ru": "d9ebf83ad0d2995b46f6174daf6e21ca5404952afa308ddd3a66ecefcee565d0",
"space-room-color-popover-desktop-ru": "9bbbcf9c5ea6ff5490b500be8cb3086af9d368d14ca50c304f42a697e296ece1",
"space-room-color-popover-desktop-ru": "04bf325ddb449001f6c0b459761dcb84eda6432dca2fbd10ce5a129484d0756c",
"backup-full-preview-desktop-en": "0fbd23a92121668001faf5c9ed555a31107704f3074d1791709c8436517d3d1f",
"backup-plan-only-export-desktop-en": "ce66fb57edbc87d56355b0d73a2d4203a67c275d5927d400ef1758ac0b607b34",
"backup-space-preview-mobile-ru": "82cd93cb495ad77b92d635b4a3715601b1ae0e5e2974edcbbd461fe872c75bad",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

+141
View File
@@ -0,0 +1,141 @@
// #456: Copy is a real two-write workflow when the plan needs Optimize and a
// one-write workflow when it does not. Exercise the production bundle through
// the visible dialogs while delegating every request to the demo backend.
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch({ width: 820, height: 780 });
const out = await page.evaluate(async () => {
const result = {};
const card = window.__card;
const root = () => card.shadowRoot || card.renderRoot;
const clone = (value) => structuredClone(value);
const settle = async () => {
await card.updateComplete;
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
};
const waitFor = async (predicate, label) => {
for (let attempt = 0; attempt < 120; attempt++) {
await settle();
if (predicate()) return;
await new Promise((resolve) => setTimeout(resolve, 10));
}
throw new Error(`space-copy smoke timed out: ${label}`);
};
const currentDialog = () => root().querySelector('hp-dialog');
const buttonWithIcon = (dialog, icon) => [...(dialog?.querySelectorAll('button') || [])]
.find((button) => button.querySelector(`ha-icon[icon="${icon}"]`));
const openCopy = async () => {
card._openSpaceDialog('edit', 'f1');
await settle();
const settingsDialog = currentDialog();
const button = buttonWithIcon(settingsDialog, 'mdi:content-copy');
const outsideDanger = !!button && !button.closest('.dialog-action-danger');
button?.click();
await waitFor(() => !!card._spaceDialog?.copy, 'copy name dialog');
const copyDialog = currentDialog();
return { outsideDanger, copyDialog };
};
const nameCopy = async (dialog, title) => {
const input = dialog?.querySelector('#space-copy-name');
if (input) {
input.value = title;
input.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
}
await settle();
return input;
};
const submitCopy = (dialog) => buttonWithIcon(dialog, 'mdi:content-copy')?.click();
const confirmUi = () => {
const component = root().querySelector('hp-confirm');
const dialog = component?.querySelector('hp-dialog');
return {
component,
dialog,
buttons: [...(dialog?.querySelectorAll('.danger-confirm-footer button') || [])],
};
};
const baseCall = card.hass.callWS.bind(card.hass);
const calls = [];
card.hass = {
...card.hass,
callWS: async (message) => {
calls.push(clone(message));
return baseCall(message);
},
};
await settle();
// The demo's original f1 needs Optimize. Cancel must leave the name dialog
// intact and must not make either durable write.
const cancelStart = calls.length;
const cancel = await openCopy();
const proposedName = cancel.copyDialog?.querySelector('#space-copy-name')?.value || '';
await nameCopy(cancel.copyDialog, 'Cancelled copy');
submitCopy(cancel.copyDialog);
await waitFor(() => confirmUi().buttons.length === 2, 'Optimize warning for Cancel');
const cancelWarning = confirmUi();
const warningText = cancelWarning.dialog?.textContent || '';
cancelWarning.buttons[0].click();
await waitFor(() => !confirmUi().component && card._spaceDialog?.copy?.busy === false,
'Cancel returns to name');
const cancelWrites = calls.slice(cancelStart)
.filter((call) => call.type === 'houseplan/plan/optimize'
|| call.type === 'houseplan/config/set');
result.copyButtonIsNeutralAndDefaultNameIsNumbered = cancel.outsideDanger
&& proposedName.endsWith(' (2)');
result.optimizeWarningNamesTheWholePlan = warningText.includes('entire plan')
&& warningText.includes('including other spaces');
result.optimizeCancelMakesZeroWritesAndKeepsName = cancelWrites.length === 0
&& card._spaceDialog?.copy?.title === 'Cancelled copy';
card._spaceDialog = null;
await settle();
// Accept uses the real demo backend in the promised order and enters the
// roomless copy after both writes succeed.
const acceptStart = calls.length;
const accepted = await openCopy();
await nameCopy(accepted.copyDialog, 'Optimized copy');
submitCopy(accepted.copyDialog);
await waitFor(() => confirmUi().buttons.length === 2, 'Optimize warning for Accept');
confirmUi().buttons[1].click();
await waitFor(() => card._spaceDialog === null && card._space !== 'f1',
'accepted copy transition');
const acceptedCalls = calls.slice(acceptStart)
.filter((call) => call.type === 'houseplan/plan/optimize'
|| call.type === 'houseplan/config/set');
const optimizedCopy = card._serverCfg.spaces.find((space) => space.title === 'Optimized copy');
result.optimizeAcceptWritesInOrder = acceptedCalls.length === 2
&& acceptedCalls[0].type === 'houseplan/plan/optimize'
&& acceptedCalls[1].type === 'houseplan/config/set';
result.acceptCreatesAndEntersRoomlessCopy = !!optimizedCopy
&& optimizedCopy.id === card._space
&& optimizedCopy.rooms?.length === 0
&& optimizedCopy.wall_segments?.length === 0
&& optimizedCopy.partitions?.length > 0
&& card._mode === 'plan' && card._tool === 'draw';
// The accepted config is now optimized. Submitting a second name is itself
// the only confirmation and produces exactly one config write.
card._commitSpace('f1', true);
await settle();
const cleanStart = calls.length;
const clean = await openCopy();
await nameCopy(clean.copyDialog, 'Clean copy');
submitCopy(clean.copyDialog);
await waitFor(() => card._spaceDialog === null && card._space !== 'f1',
'clean copy transition');
const cleanCalls = calls.slice(cleanStart)
.filter((call) => call.type === 'houseplan/plan/optimize'
|| call.type === 'houseplan/config/set');
const cleanCopy = card._serverCfg.spaces.find((space) => space.title === 'Clean copy');
result.cleanCopyNeedsNoExtraConfirmation = !confirmUi().component
&& cleanCalls.length === 1 && cleanCalls[0].type === 'houseplan/config/set';
result.cleanCopyIsInsertedAfterSourceAndSelected = !!cleanCopy
&& card._serverCfg.spaces[1]?.id === cleanCopy.id
&& card._space === cleanCopy.id;
return result;
});
checkAll(out);
await finish(browser, out);