mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 23:19:14 +00:00
fix(zigbee): scope incomplete caption to unknown non-root uplinks (#816)
Issue: #816 User-Visible: yes
This commit is contained in:
@@ -266,8 +266,14 @@ for (const provider of ['z2m', 'zha']) {
|
||||
await page.evaluate(() => window.__hpTest.switchSpace('f1'));
|
||||
for (const [id, name] of [['d_temp', 'unplaced'], ['d_light1', 'remote-count'], ['d_kettle', 'status-only']]) {
|
||||
await hover(id, 0.25);
|
||||
await root.locator('.route-status').waitFor({ state: 'visible' });
|
||||
// #816 removes only Incomplete data for these confirmed routes; the
|
||||
// parent bubble/incoming remote count remains the exact layout witness.
|
||||
const caption = name === 'status-only' ? '.route-status'
|
||||
: name === 'unplaced' ? '.parent-bubble' : '.remote';
|
||||
await root.locator(caption).waitFor({ state: 'visible' });
|
||||
const value = await witness(`${iso ? 'iso' : 'flat'}_${theme}_${name}`, id);
|
||||
if (name !== 'status-only') out[`knownRouteNoIncomplete_${iso}_${theme}_${name}`]
|
||||
= value.badges.every(badge => !/Incomplete data/i.test(badge.text));
|
||||
out[`actualProjection_${iso}_${theme}_${name}`] = value.projectionIso === iso
|
||||
&& (!iso || (value.isoWalls && value.sourceRaised));
|
||||
if (name === 'remote-count') out[`remoteCount_${iso}_${theme}`] = value.badges.some((badge) => /1/.test(badge.text) && badge.kind === 'remote');
|
||||
|
||||
@@ -239,6 +239,9 @@ const result = await page.evaluate(async () => {
|
||||
const arrowBox = routeArrow?.getBoundingClientRect();
|
||||
out.localRouteArrow = !!routeLine && routeArrow?.getAttribute('data-direction') === 'toward-neighbor'
|
||||
&& !!lineBox && !!arrowBox && arrowBox.width > 3 && arrowBox.height > 3;
|
||||
out.knownLocalRouteOmitsIncomplete = !/incomplete data/i.test(
|
||||
overlay.shadowRoot.querySelector('.route-status')?.textContent || '',
|
||||
);
|
||||
out.onlyConfirmedArrows = overlay.shadowRoot.querySelectorAll('[data-hp="zigbee-topology-arrow"]').length === 2
|
||||
&& !overlay.shadowRoot.querySelector('line[data-direction="none"]')
|
||||
&& !root().querySelector('.dev[data-id="d_kettle"]').hasAttribute('data-hp-zigbee-topology-endpoint');
|
||||
@@ -324,6 +327,9 @@ const result = await page.evaluate(async () => {
|
||||
out.unknownRouteHasNoInventedLine = !overlay.shadowRoot.querySelector('line,polygon')
|
||||
&& /no route data/i.test(overlay.shadowRoot.querySelector('.route-status').textContent)
|
||||
&& !noRoute.hasAttribute('data-hp-zigbee-topology-endpoint');
|
||||
out.unknownRouteRetainsIncomplete = /incomplete data/i.test(
|
||||
overlay.shadowRoot.querySelector('.route-status')?.textContent || '',
|
||||
);
|
||||
unknownNeighbor.dispatchEvent(mouse('pointerover'));
|
||||
await wait(() => overlay.shadowRoot.querySelector('[data-hp="zigbee-topology-arrow"]'), 'coordinator incoming route');
|
||||
out.coordinatorHasOnlyIncoming = !/no route data/i.test(
|
||||
@@ -331,6 +337,9 @@ const result = await page.evaluate(async () => {
|
||||
)
|
||||
&& !overlay.shadowRoot.querySelector('[data-direction="toward-neighbor"]')
|
||||
&& !!overlay.shadowRoot.querySelector('[data-direction="toward-origin"]');
|
||||
out.coordinatorNeverShowsIncomplete = !/incomplete data/i.test(
|
||||
overlay.shadowRoot.querySelector('.route-status')?.textContent || '',
|
||||
);
|
||||
|
||||
card._commitSpace('garden', true);
|
||||
card.requestUpdate();
|
||||
@@ -350,6 +359,9 @@ const result = await page.evaluate(async () => {
|
||||
?.getAttribute('data-kind') === 'remote-space'
|
||||
&& !!activeOverlay.shadowRoot.querySelector('[data-hp="zigbee-topology-parent-arrow"]')
|
||||
&& !activeOverlay.shadowRoot.querySelector('[data-hp="zigbee-topology-remote"]');
|
||||
out.knownRemoteRouteOmitsIncomplete = !/incomplete data/i.test(
|
||||
activeOverlay.shadowRoot.querySelector('.route-status')?.textContent || '',
|
||||
);
|
||||
|
||||
card._commitSpace('f1', true);
|
||||
card.requestUpdate();
|
||||
@@ -363,6 +375,9 @@ const result = await page.evaluate(async () => {
|
||||
const missingParentBubble = activeOverlay.shadowRoot.querySelector('[data-kind="unplaced-device"]');
|
||||
out.unplacedDeviceBubble = missingParentBubble?.textContent.trim()
|
||||
=== 'Device is not on the plan (Hallway <router> & "target")';
|
||||
out.knownUnplacedRouteOmitsIncomplete = !/incomplete data/i.test(
|
||||
activeOverlay.shadowRoot.querySelector('.route-status')?.textContent || '',
|
||||
);
|
||||
out.unplacedNameIsSafeText = !missingParentBubble?.querySelector('router,img,script')
|
||||
&& !missingParentBubble?.textContent.includes('Temperature sensor');
|
||||
|
||||
@@ -562,7 +577,8 @@ for (const theme of ['light', 'dark']) {
|
||||
await overlay.updateComplete;
|
||||
const staleStatus = overlay.shadowRoot.querySelector('.route-status');
|
||||
out.stalePartialRetainsKnownRoute = staleStatus?.getAttribute('data-outgoing') === 'known'
|
||||
&& /stale data/i.test(staleStatus.textContent) && /incomplete data/i.test(staleStatus.textContent)
|
||||
// #816: a globally partial snapshot must not label this confirmed uplink incomplete.
|
||||
&& /stale data/i.test(staleStatus.textContent) && !/incomplete data/i.test(staleStatus.textContent)
|
||||
&& !!overlay.shadowRoot.querySelector('[data-hp="zigbee-topology-arrow"]')
|
||||
&& [...overlay.shadowRoot.querySelectorAll('line,polygon')]
|
||||
.every((node) => getComputedStyle(node).strokeDasharray === 'none');
|
||||
@@ -1062,6 +1078,13 @@ result.realPointerEndpointWins = await page.evaluate(async () => {
|
||||
> Number.parseInt(getComputedStyle(overlay.shadowRoot.querySelector('svg')).zIndex, 10);
|
||||
});
|
||||
|
||||
result.realMouseKnownRouteOmitsIncomplete = await page.evaluate(async () => {
|
||||
const overlay = window.__card.renderRoot.querySelector('hp-zigbee-topology-overlay');
|
||||
await overlay.updateComplete;
|
||||
return !!overlay.shadowRoot.querySelector('[data-direction="toward-neighbor"]')
|
||||
&& !/incomplete data/i.test(overlay.shadowRoot.querySelector('.route-status')?.textContent || '');
|
||||
});
|
||||
|
||||
await page.emulateMedia({ forcedColors: 'active' });
|
||||
result.forcedColorsPreserved = await page.evaluate(() => {
|
||||
const card = window.__card;
|
||||
|
||||
@@ -660,7 +660,11 @@ a black casing under a 2 px grey core and an outer arrow outline. The outline
|
||||
is 1 px at zoom=1 and scales with the plan, including temporary camera CSS
|
||||
projection, per the owner's #798 clarification. Local and remote/unplaced routes share one renderer.
|
||||
Unknown/partial/stale captions never trigger network work; one expiry timer
|
||||
updates age without requiring another HA event. Persistence and privacy:
|
||||
updates age without requiring another HA event. The resolver's transient
|
||||
`showIncomplete` distinguishes an unknown per-device uplink from global snapshot
|
||||
`partial`: known uplinks and every matched coordinator suppress only that caption,
|
||||
even if cross-provider reconciliation makes a coordinator's route unknown (#816).
|
||||
Persistence and privacy:
|
||||
[CONFIG-COMPATIBILITY](CONFIG-COMPATIBILITY.md).
|
||||
|
||||
Zigbee caption layout (#802) notifies the lazy live-hover runtime after the
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Zigbee hover: **Incomplete data** no longer appears on a device with a known
|
||||
outgoing route (including remote/unplaced parents), or on a coordinator.
|
||||
Incoming-only devices can still show it; stale/error statuses and the overall
|
||||
snapshot status are unchanged ([#816](https://github.com/Matysh/houseplan-card/issues/816)).
|
||||
|
||||
- Kiosk: holding an empty spot of the plan with a mouse now opens **This
|
||||
screen's sizes** every time. After the first hold the next one used to zoom
|
||||
the plan instead of opening the dialog, and the plan stopped showing device
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- Zigbee при наведении: **Неполные данные** больше не показываются у устройства
|
||||
с известной исходящей связью (включая родителя на другом этаже или вне плана)
|
||||
и у координатора. Одни входящие связи предупреждение не скрывают; отметки
|
||||
устаревания/ошибок и общий статус снимка не изменились
|
||||
([#816](https://github.com/Matysh/houseplan-card/issues/816)).
|
||||
|
||||
- Киоск: удержание пустого места плана мышью теперь каждый раз открывает
|
||||
**«Размеры на этом экране»**. Раньше после первого удержания следующее вместо
|
||||
диалога увеличивало план, а план переставал показывать изменения состояний
|
||||
|
||||
@@ -64,6 +64,11 @@ topology benchmark must use confirmed route evidence, not an empty graph.
|
||||
Neighbour-only raw fixtures remain negative controls; do not add guessed
|
||||
routes to make them draw. Ordinary LQI badges keep their separate palette.
|
||||
|
||||
The #816 tests in `zigbee-provider-routes.test.mjs` cover per-hover incomplete
|
||||
data suppression (local/remote/unplaced uplinks and conflicting coordinator roles),
|
||||
incoming-only negatives and unchanged global `partial`. The same hover smoke
|
||||
checks the mounted caption, retained stale/error messages and real mouse hover.
|
||||
|
||||
For tooltip layout (#802), also run `node demo/smoke_zigbee_tooltip_layout.mjs`:
|
||||
it uses real mouse movement and screen-space overlap oracles, including a
|
||||
negative control at the previous cursor-relative position. DOM presence alone
|
||||
|
||||
@@ -29,6 +29,7 @@ Everything computable from the tree and git; regenerate, never edit by hand
|
||||
|---|---|
|
||||
| Current local cycle | **v1.80.0-beta.5 candidate** — #792 battery indicators, #804 room creation from a long wall segment and #776 late smoke-error accounting passed review and are merged into `dev`. The release candidate rebuilds the committed bundle and tightens its ratchets; local targeted checks and full exact-SHA Validate precede publication. `main` remains on stable v1.79.0. |
|
||||
| Branches | `main` carries stable releases only; pre-release tags point at `dev`. Work lands on `dev`, which is equal to or ahead of `main`, never behind. |
|
||||
| Zigbee incomplete captions | #816 suppresses only the hovered device's Incomplete data caption for confirmed outgoing routes and every coordinator; global snapshot partial and stale/provider messages stay independent. |
|
||||
| Pipeline reliability | #811 checks the actual post-rebase issue range, parses each review's own verdict/findings, generates the final review index with the accepted tree's generator under the trusted dev controller, and reconciles only numeric browser-inventory cells. Manual content and the existing membership/200 warning policy remain unchanged. Evidence: `docs/analysis/811-pipeline-evidence-2026-10-07.md`. |
|
||||
| Check reliability | #812 preserves mixed code/data input reachability, executes sandbox import closures and real workflow refusal comments, selects both static-card witnesses and verifies actual radar setup gestures. The performance harness honours zero warm-ups, reports the active profile and caps epoch stack capture; budgets, golden baselines and CI/reuse policy stay unchanged. Evidence: `docs/analysis/812-test-evidence-2026-10-07.md`. |
|
||||
| Smoke exception guard | #776 makes `finish()` aggregate delivered page errors after awaited browser teardown, so a late rejection cannot print `EXC` followed by a false `OK`. Deterministic Node lifecycle tests complement the unchanged Chromium guard probes. |
|
||||
|
||||
+5
-1
@@ -379,7 +379,11 @@ when space becomes available. No extra setting is needed. If the
|
||||
needed router or coordinator is not placed on the plan, the bubble says so;
|
||||
an unplaced device's available name is included in parentheses. “No route data”
|
||||
means the outgoing route is unknown or conflicting, not that the device is
|
||||
offline. No substitute line is drawn. Stale and incomplete data are marked:
|
||||
offline. No substitute line is drawn. **Incomplete data** appears only when
|
||||
the snapshot is partial and this device's outgoing route is unknown; incoming
|
||||
links alone do not suppress it. A coordinator never shows it, nor does a device
|
||||
with a known outgoing route, including remote or unplaced parents. The overall
|
||||
partial snapshot status in settings is unchanged. Staleness is marked separately:
|
||||
a snapshot older than one hour is stale, and a failed refresh marks the last
|
||||
snapshot stale at once. Lines and arrows pass over battery indicators but stay
|
||||
under device faces and captions. A recent read of ZHA's
|
||||
|
||||
@@ -408,7 +408,12 @@ LQI: 0 — красный, 128 — жёлтый, 255 — зелёный. Без
|
||||
плане, короткая подпись сообщает об этом; доступное имя отсутствующего
|
||||
устройства добавляется в скобках. «Нет данных о маршруте» означает отсутствие
|
||||
однозначных сведений об исходящей связи, а не недоступность устройства.
|
||||
Подменяющей линии нет. Неполные и устаревшие данные отмечены: снимок старше
|
||||
Подменяющей линии нет. «Неполные данные» рядом с устройством показываются,
|
||||
только если снимок неполный и исходящая связь устройства неизвестна; одних
|
||||
входящих связей недостаточно, чтобы скрыть эту подпись. У координатора её нет
|
||||
никогда, а у устройства с известной исходящей связью её нет даже при родителе
|
||||
на другом этаже или вне плана. Общий статус неполноты в настройках сохраняется.
|
||||
Устаревание отмечается отдельно: снимок старше
|
||||
часа считается устаревшим, а ошибка обновления сразу оставляет последний снимок
|
||||
с отметкой об устаревании. Линии и стрелки проходят поверх индикаторов заряда,
|
||||
но под лицом устройства и подписями. Недавнее чтение кеша ZHA
|
||||
|
||||
@@ -36,8 +36,8 @@ that transition events alone prove disposal.
|
||||
| Paint, cascade and layer composition | 47 | The invariant depends on computed CSS, SVG paint, clipping, stacking or pixels produced by Chromium. |
|
||||
| Pointer geometry and trusted interaction | 53 | The invariant depends on hit testing, pointer capture, touch/keyboard dispatch or live DOM geometry. |
|
||||
| Responsive DOM layout | 39 | The invariant depends on measured element boxes, responsive breakpoints, native/HA dialog shells or focusable target size. |
|
||||
| Custom-element and HA browser lifecycle | 116 | The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy loading or a complete HA-card state transition. |
|
||||
| **Total** | **262 / 200** | Above the guideline `mutation-gate --check` warns rather than fails (#699); each guard above it is held by its own reason in this inventory and its `because`. |
|
||||
| Custom-element and HA browser lifecycle | 117 | The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy loading or a complete HA-card state transition. |
|
||||
| **Total** | **263 / 200** | Above the guideline `mutation-gate --check` warns rather than fails (#699); each guard above it is held by its own reason in this inventory and its `because`. |
|
||||
|
||||
## Measured effect
|
||||
|
||||
@@ -262,6 +262,12 @@ The invariant depends on measured element boxes, responsive breakpoints, native/
|
||||
|
||||
### Custom-element and HA browser lifecycle
|
||||
|
||||
`zigbee-hover-incomplete-uses-global-partial` (#816) checks the mounted lazy
|
||||
overlay consuming the per-device warning under live provider state. Pure
|
||||
resolver negatives cover role/route classification, not its rendered wiring.
|
||||
|
||||
- `zigbee-hover-incomplete-uses-global-partial`
|
||||
|
||||
`wall-face-lineage-skips-post-reconcile-settlement` (#804) needs the production room
|
||||
dialog, post-reconciliation runtime wiring, history, asynchronous `config/set` and
|
||||
reload. Pure lineage-helper tests cannot prove that the editor actually settles
|
||||
|
||||
@@ -9674,6 +9674,35 @@ const MUTANT_DEFINITIONS = [
|
||||
+ ' && contentFingerprint(current) !== attempt.attemptedFingerprint)) return false;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'zigbee-hover-incomplete-uses-global-partial',
|
||||
guard: 'node demo/smoke_zigbee_topology_hover.mjs',
|
||||
because: '#816: the mounted lazy custom element must consume the per-hover warning '
|
||||
+ 'while preserving live provider statuses and arrows; a pure resolver cannot prove the UI wiring',
|
||||
patches: [{
|
||||
file: 'src/hp-zigbee-topology-overlay.ts',
|
||||
find: "hover.showIncomplete ? topologyT(lang, 'route_partial') : ''",
|
||||
replace: "hover.outgoing !== 'not-zigbee' && hover.partial ? topologyT(lang, 'route_partial') : ''",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'zigbee-hover-coordinator-incomplete',
|
||||
guard: 'node --test --test-name-pattern="#816 coordinator never" test/zigbee-provider-routes.test.mjs',
|
||||
patches: [{
|
||||
file: 'src/zigbee-topology.ts',
|
||||
find: "showIncomplete: partial && outgoing === 'unknown' && !isCoordinator,",
|
||||
replace: "showIncomplete: partial && outgoing === 'unknown',",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'zigbee-hover-known-route-incomplete',
|
||||
guard: 'node --test --test-name-pattern="#816 known uplink" test/zigbee-provider-routes.test.mjs',
|
||||
patches: [{
|
||||
file: 'src/zigbee-topology.ts',
|
||||
find: "showIncomplete: partial && outgoing === 'unknown' && !isCoordinator,",
|
||||
replace: 'showIncomplete: partial && !isCoordinator,',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'zigbee-topology-overlay-layer-lowered',
|
||||
guard: 'node demo/smoke_zigbee_topology_hover.mjs',
|
||||
|
||||
@@ -542,7 +542,7 @@ export class HpZigbeeTopologyOverlay extends LitElement {
|
||||
providerStates.some((state) => state?.phase === 'error') ? topologyT(lang, 'error_provider') : '',
|
||||
providerStates.some((state) => state?.phase === 'loading') ? topologyT(lang, 'status_loading') : '',
|
||||
hover.outgoing !== 'not-zigbee' && stale ? topologyT(lang, 'route_stale') : '',
|
||||
hover.outgoing !== 'not-zigbee' && hover.partial ? topologyT(lang, 'route_partial') : '',
|
||||
hover.showIncomplete ? topologyT(lang, 'route_partial') : '',
|
||||
].filter(Boolean).join(' · ');
|
||||
this._setDesiredEndpointIds(lines.length || bubbles.length || hover.remoteCount
|
||||
? [
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface ZigbeeHoverResolution {
|
||||
parentTargets: ZigbeeParentTarget[];
|
||||
outgoing: 'known' | 'unknown' | 'root' | 'not-zigbee';
|
||||
partial: boolean;
|
||||
showIncomplete: boolean;
|
||||
obtainedAt?: number;
|
||||
}
|
||||
export type ZigbeeNodePlacement = { markerId: string; space: string };
|
||||
@@ -150,7 +151,7 @@ export function resolveMappedTopologyHover(
|
||||
const lines = new Map<string, ZigbeeHoverLine>(); const remote = new Set<string>();
|
||||
const targets = new Map<string, ZigbeeParentTarget>(); const omitted = new Set<string>();
|
||||
let outgoing: ZigbeeHoverResolution['outgoing'] = 'not-zigbee';
|
||||
let partial = false; let obtainedAt: number | undefined;
|
||||
let partial = false; let isCoordinator = false; let obtainedAt: number | undefined;
|
||||
for (const map of mappedTopologies) {
|
||||
const hovered = new Set([...map.placements].filter(([, placement]) => (
|
||||
placement.markerId === hoveredMarkerId && placement.space === currentSpace
|
||||
@@ -159,6 +160,7 @@ export function resolveMappedTopologyHover(
|
||||
partial ||= map.partial;
|
||||
obtainedAt = obtainedAt === undefined ? map.topology.obtainedAt : Math.min(obtainedAt, map.topology.obtainedAt);
|
||||
for (const key of hovered) {
|
||||
isCoordinator ||= map.nodes.get(key)?.role === 'coordinator';
|
||||
const route = map.routes.get(key);
|
||||
if (route?.kind === 'known') outgoing = 'known';
|
||||
else if (route?.kind === 'root' && outgoing !== 'known') outgoing = 'root';
|
||||
@@ -191,6 +193,7 @@ export function resolveMappedTopologyHover(
|
||||
}
|
||||
return { lines: [...lines.values()], remoteCount: remote.size, omittedCount: omitted.size,
|
||||
parentTargets: [...targets.values()], outgoing, partial,
|
||||
showIncomplete: partial && outgoing === 'unknown' && !isCoordinator,
|
||||
...(obtainedAt === undefined ? {} : { obtainedAt }) };
|
||||
}
|
||||
export function resolveTopologyHover(
|
||||
|
||||
@@ -234,6 +234,59 @@ test('coordinator is incoming-only; unrelated and unknown hover states stay dist
|
||||
assert.equal(hover([topology], 'm3').partial, true);
|
||||
});
|
||||
|
||||
test('#816 known uplink omits incomplete regardless of placement or LQI; global partial survives', () => {
|
||||
for (const lqi of [undefined, 0, 255]) for (const placement of ['local', 'remote', 'unplaced']) {
|
||||
const topology = zha([row(1, 'Coordinator'), row(2, 'EndDevice', {
|
||||
neighbors: [neighbor(1, { relationship: 'Parent', lqi })],
|
||||
})]);
|
||||
topology.warnings.push({ code: 'invalid_payload' });
|
||||
const devices = placement === 'unplaced' ? markers.filter(m => m.id !== 'm1')
|
||||
: markers.map(m => m.id === 'm1' && placement === 'remote' ? { ...m, space: 'other' } : m);
|
||||
const before = JSON.stringify(topology), result = hover([topology], 'm2', devices);
|
||||
assert.equal(result.outgoing, 'known'); assert.equal(result.partial, true);
|
||||
assert.equal(result.showIncomplete, false);
|
||||
assert.equal(result.lines.length + result.parentTargets.length, 1);
|
||||
assert.equal(JSON.stringify(topology), before, 'presentation does not rewrite provider evidence');
|
||||
}
|
||||
});
|
||||
|
||||
test('#816 coordinator never incomplete even after cross-provider root conflict', () => {
|
||||
const a = zha([row(1, 'Coordinator'), row(2, 'Router', { routes: [route(0)] })]);
|
||||
a.warnings.push({ code: 'invalid_payload' });
|
||||
const b = z2m([zlink(2, 1, { routes: [{ status: 'ACTIVE', destinationAddress: 0, nextHopAddress: 2 }] })], [
|
||||
{ ieeeAddr: ieee(1), networkAddress: 1, type: 'Router' },
|
||||
{ ieeeAddr: ieee(2), networkAddress: 0, type: 'Coordinator' },
|
||||
]);
|
||||
const empty = zha([row(1, 'Coordinator')]); empty.warnings.push({ code: 'invalid_payload' });
|
||||
for (const snapshots of [[a], [empty], [a, b], [b, a]]) {
|
||||
const result = hover(snapshots, 'm1');
|
||||
assert.equal(result.partial, true); assert.equal(result.showIncomplete, false);
|
||||
if (snapshots.length === 2) assert.equal(result.outgoing, 'unknown', 'root conflict is not misreported known');
|
||||
}
|
||||
});
|
||||
|
||||
test('#816 incoming-only/unknown stay incomplete; fresh/non-Zigbee do not', () => {
|
||||
const topology = zha([row(2, 'Router'), row(3, 'EndDevice', {
|
||||
neighbors: [neighbor(2, { relationship: 'Parent' })],
|
||||
})]);
|
||||
topology.warnings.push({ code: 'invalid_payload' });
|
||||
const incoming = hover([topology]);
|
||||
assert.equal(incoming.outgoing, 'unknown'); assert.equal(incoming.showIncomplete, true);
|
||||
assert.deepEqual(incoming.lines.map(line => line.routeDirection), ['toward-origin']);
|
||||
assert.equal(hover([topology], 'm4').showIncomplete, false);
|
||||
const mapped = mapTopologies([topology], markers, registry);
|
||||
for (const map of mapped) map.partial = false;
|
||||
const fresh = resolveMappedTopologyHover(mapped, 'main', 'm2');
|
||||
assert.equal(fresh.outgoing, 'unknown'); assert.equal(fresh.showIncomplete, false);
|
||||
const a = zha([row(1, 'Coordinator'), row(2, 'Router', { routes: [route(0)] }), row(4)]);
|
||||
const b = z2m([zlink(4, 2, { routes: [{ status: 'ACTIVE', destinationAddress: 0, nextHopAddress: 4 }] })], [
|
||||
{ ieeeAddr: ieee(1), networkAddress: 0, type: 'Coordinator' },
|
||||
{ ieeeAddr: ieee(2), networkAddress: 2, type: 'Router' },
|
||||
{ ieeeAddr: ieee(4), networkAddress: 4, type: 'Router' },
|
||||
]);
|
||||
assert.equal(hover([a, b]).outgoing, 'unknown'); assert.equal(hover([a, b]).showIncomplete, true);
|
||||
});
|
||||
|
||||
test('remote/unplaced parent carries exact target name and observed LQI, never source name', () => {
|
||||
const topology = zha([row(2, 'Router', { name: '<b>Provider parent</b>', neighbors: [neighbor(3, { relationship: 'Child', lqi: 128 })] }), row(3, 'EndDevice')]);
|
||||
const onlyChild = markers.filter((device) => device.id === 'm3');
|
||||
|
||||
Reference in New Issue
Block a user