tap action: run an automation, a script or a scene — with a confirm guard

Owner's spec (2026-07-29), agreed points: one 'Run' action covering the
three runnable domains of HA (a script is the idiomatic 'action' — with
automations alone people would build trigger-less dummies); the confirm
checkbox guards BOTH toggle and run; covers and valves join the card-wide
toggle so curtains work natively.

- marker.tap_action gains 'run'; marker.tap_target (schema-bounded to
  automation./script./scene. ids); marker.tap_confirm.
- the dialog: a searchable picker over the three domains (friendly name +
  kind), save refuses a run action without a target, a vanished target gets
  a warning hint; the checkbox shows for any actionable tap (explicit or
  effective-default toggle).
- the tap: automation.trigger / script.turn_on / scene.turn_on, started/
  error toasts; with confirm on — our own dialog (not window.confirm, it
  must work on a wall tablet), Esc/backdrop/Cancel = no call. The guard
  covers the controls-toggle path too.
- 'run' is explicit-only by construction: it needs a per-marker target, so
  it can never arrive as a card-wide default.
- covers: the old test pinned 'garage stays shut' — that intent survives as
  COVER_GUARDED_CLASSES (garage/door/gate stay out of the CARD-WIDE toggle;
  an explicit per-device toggle remains the owner's conscious choice).
  Locks/alarms stay forbidden everywhere, run included is not affected —
  we do not inspect automation contents, same trust as HA's own Run button.

Tests: unit resolveTapAction/runServiceFor + cover guard, backend schema
parity picks 'run' automatically + tap_target bounds, smoke_tap_run with 11
assertions (picker, search, save guard, confirm cancel/ok, per-domain
services, missing target). smoke_tap_ctx: 4 options now.
Inventory: 148 / 52 / 43 / 72.
This commit is contained in:
Matysh
2026-07-30 01:26:30 +03:00
parent f56bceef27
commit 3e976562ff
13 changed files with 434 additions and 75 deletions
+26 -3
View File
@@ -15,7 +15,7 @@ import {
contentUrl, chunk, referencedContentUrls, MAX_SIGN_PATHS,
interiorPoint,
segmentCm, formatLength, roomEdges, roomPoly, pointOnBoundary, pointStrictlyInside, roomsOverlap,
mergeRooms, splitRoom, polygonArea, closestPointOnBoundary, isActiveState, snapToWall, openingAmount, fillColorsOf, lerpColor, roomFillStyle, stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices, poleOfInaccessibility } from '../test-build/logic.js';
mergeRooms, splitRoom, polygonArea, closestPointOnBoundary, isActiveState, snapToWall, openingAmount, fillColorsOf, lerpColor, roomFillStyle, stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices, poleOfInaccessibility, runServiceFor, TOGGLE_SAFE_DOMAINS } from '../test-build/logic.js';
import {
iconFor, compileIconRules, isValidPattern, iconFromDeviceClasses,
} from '../test-build/rules.js';
@@ -190,8 +190,13 @@ test('tap action: defaults — info everywhere except pure lights (v1.39.0)', ()
test('tap action: card-wide toggle only touches safe domains', () => {
assert.equal(resolveTapAction(null, 'toggle', 'light'), 'toggle');
assert.equal(resolveTapAction(null, 'toggle', 'switch'), 'toggle');
assert.equal(resolveTapAction(null, 'toggle', 'cover'), 'info'); // garage stays shut
assert.equal(resolveTapAction(null, 'toggle', 'valve'), 'info');
// covers/valves joined the safe set for curtains (owner, 2026-07-29)…
assert.equal(resolveTapAction(null, 'toggle', 'cover', 'curtain'), 'toggle');
assert.equal(resolveTapAction(null, 'toggle', 'valve'), 'toggle');
// …but the garage door is a cover too, and it STAYS SHUT on a default tap
assert.equal(resolveTapAction(null, 'toggle', 'cover', 'garage'), 'info');
assert.equal(resolveTapAction(null, 'toggle', 'cover', 'gate'), 'info');
assert.equal(resolveTapAction('toggle', null, 'cover', 'garage'), 'toggle', 'explicit stays a conscious choice');
assert.equal(resolveTapAction(null, 'toggle', 'sensor'), 'info');
});
@@ -936,3 +941,21 @@ test('poleOfInaccessibility: the VISUAL centre, not just any interior point', ()
assert.ok(q[1] < 8, 'in the thick slab (y < 8), not down the column: ' + q);
assert.ok(Math.abs(q[0] - 11.3) < 2.5, 'near the centroid x within the slab: ' + q);
});
test('tap action run: explicit-only, with runnable targets (owner spec 2026-07-29)', () => {
// 'run' resolves only as an explicit per-marker action
assert.equal(resolveTapAction('run', undefined, 'switch'), 'run');
assert.equal(resolveTapAction(null, 'run', 'switch'), 'info', 'never a card-wide default');
// runnable domains map to their services; anything else is not runnable
assert.deepEqual(runServiceFor('automation.morning'), { domain: 'automation', service: 'trigger' });
assert.deepEqual(runServiceFor('script.curtains'), { domain: 'script', service: 'turn_on' });
assert.deepEqual(runServiceFor('scene.movie'), { domain: 'scene', service: 'turn_on' });
assert.equal(runServiceFor('light.lamp'), null);
assert.equal(runServiceFor(''), null);
// covers and valves joined the card-wide toggle domains
assert.ok(TOGGLE_SAFE_DOMAINS.has('cover') && TOGGLE_SAFE_DOMAINS.has('valve'));
assert.equal(resolveTapAction(null, 'toggle', 'cover', 'shutter'), 'toggle');
// locks and alarms stay forbidden, run or not
assert.equal(resolveTapAction('toggle', undefined, 'lock'), 'info');
});