mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Issue: #456 User-Visible: no Release: v1.72.0-beta.4 Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33948778925
142 lines
6.1 KiB
JavaScript
142 lines
6.1 KiB
JavaScript
// #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);
|