mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 20:29:00 +00:00
feat: подсказка к настройке связей Zigbee
User-Visible: yes Issue: #459
This commit is contained in:
@@ -6,6 +6,14 @@
|
||||
incoming arrows identify devices behind a router, while a short bubble
|
||||
truthfully ends the path when the next node is in another space or is not
|
||||
placed ([#457](https://github.com/Matysh/houseplan-card/issues/457)).
|
||||
|
||||
- The «Zigbee links» setting now carries contextual help: it explains the
|
||||
feature in one breath, spells out the legend — line colour as link quality,
|
||||
a dashed line as unreported quality, arrows as the way to the coordinator —
|
||||
and states the limits: data comes from the last load and goes stale, and
|
||||
links are shown with a mouse only
|
||||
([#459](https://github.com/Matysh/houseplan-card/issues/459)).
|
||||
|
||||
- Space settings can now create a room-free copy of the current space under a
|
||||
new name, preserving walls, openings, columns, Background objects, image
|
||||
transform, scale and display settings while leaving device placement behind;
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
координатора; входящие стрелки показывают устройства за роутером, а если
|
||||
следующий узел находится на другом плане или не размещён, короткая подпись
|
||||
честно завершает путь ([#457](https://github.com/Matysh/houseplan-card/issues/457)).
|
||||
|
||||
- У настройки «Связи Zigbee» появилась контекстная подсказка: она коротко
|
||||
объясняет функцию, разбирает легенду — цвет линии как качество связи, пунктир
|
||||
как несообщённое качество, стрелки как путь к координатору — и называет
|
||||
ограничения: данные из последней загрузки устаревают, а связи видны только
|
||||
при работе мышью
|
||||
([#459](https://github.com/Matysh/houseplan-card/issues/459)).
|
||||
|
||||
- В настройках пространства теперь можно создать под новым именем копию без
|
||||
комнат: стены, проёмы, колонны, элементы подложки, положение фонового плана,
|
||||
масштаб и настройки отображения сохраняются, а размещение устройств не
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"fixture": "synthetic-only",
|
||||
"chromium": "151.0.7922.34",
|
||||
"oxipng": "oxipng 10.2.0",
|
||||
"sourceFingerprint": "997a6ed2829850e0bd02ffbe6aaef8fdfc3a1e22b8828d7cbe207b426a694b3b",
|
||||
"sourceFingerprint": "baf74cdb584726b1925079b3ee326edc1afc8b10f362bffb11be205cdab379d5",
|
||||
"captureScriptSha256": "cadb8e1bcab9f1dcdd7d75b3b90ddcbaaeb2b8c2a098f575a21f39ff70f5c59c",
|
||||
"command": "npm run build && node demo/docs/capture.mjs",
|
||||
"scenarios": {
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "997a6ed2829850e0bd02ffbe6aaef8fdfc3a1e22b8828d7cbe207b426a694b3b",
|
||||
"sourceSha256": "baf74cdb584726b1925079b3ee326edc1afc8b10f362bffb11be205cdab379d5",
|
||||
"imageSha256": "80a70361dc18dd0461568df332062e6482c633af5d280954f8b675701418a76d"
|
||||
},
|
||||
"view-touch": {
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "997a6ed2829850e0bd02ffbe6aaef8fdfc3a1e22b8828d7cbe207b426a694b3b",
|
||||
"sourceSha256": "baf74cdb584726b1925079b3ee326edc1afc8b10f362bffb11be205cdab379d5",
|
||||
"imageSha256": "4106cc28847047505f46921ff95765d8abdf5b382d9d17d4c5e4ad129dd8f6be"
|
||||
},
|
||||
"space-create": {
|
||||
@@ -37,7 +37,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "997a6ed2829850e0bd02ffbe6aaef8fdfc3a1e22b8828d7cbe207b426a694b3b",
|
||||
"sourceSha256": "baf74cdb584726b1925079b3ee326edc1afc8b10f362bffb11be205cdab379d5",
|
||||
"imageSha256": "617b51b3648498787b5039980c9f3eceb75ba56ed63a1a20e616bc05bc304362"
|
||||
},
|
||||
"room-contour-close": {
|
||||
@@ -48,7 +48,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "997a6ed2829850e0bd02ffbe6aaef8fdfc3a1e22b8828d7cbe207b426a694b3b",
|
||||
"sourceSha256": "baf74cdb584726b1925079b3ee326edc1afc8b10f362bffb11be205cdab379d5",
|
||||
"imageSha256": "1dab6cc3b9d1bf7d8c40f0e5137f8c688683c9b7eabc5167da99d41ecfdd5b79"
|
||||
},
|
||||
"plan-context-tray": {
|
||||
@@ -59,7 +59,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "997a6ed2829850e0bd02ffbe6aaef8fdfc3a1e22b8828d7cbe207b426a694b3b",
|
||||
"sourceSha256": "baf74cdb584726b1925079b3ee326edc1afc8b10f362bffb11be205cdab379d5",
|
||||
"imageSha256": "94ef50762753c0ac6ddc84d2521c4232a3f9ecd89843810d2df61c517592bed7"
|
||||
},
|
||||
"device-editor": {
|
||||
@@ -70,7 +70,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "997a6ed2829850e0bd02ffbe6aaef8fdfc3a1e22b8828d7cbe207b426a694b3b",
|
||||
"sourceSha256": "baf74cdb584726b1925079b3ee326edc1afc8b10f362bffb11be205cdab379d5",
|
||||
"imageSha256": "7a601769de38aa19c2e280f6ff4cf3695b854b1d1c6d1f8de4e6b5e8f7eb1559"
|
||||
},
|
||||
"device-display-preview": {
|
||||
@@ -81,7 +81,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "997a6ed2829850e0bd02ffbe6aaef8fdfc3a1e22b8828d7cbe207b426a694b3b",
|
||||
"sourceSha256": "baf74cdb584726b1925079b3ee326edc1afc8b10f362bffb11be205cdab379d5",
|
||||
"imageSha256": "0939875f8631694f4de0ef4fd01032010ede8c7d49fb7c0a857612d4b3afff93"
|
||||
},
|
||||
"background-editor": {
|
||||
@@ -92,7 +92,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "997a6ed2829850e0bd02ffbe6aaef8fdfc3a1e22b8828d7cbe207b426a694b3b",
|
||||
"sourceSha256": "baf74cdb584726b1925079b3ee326edc1afc8b10f362bffb11be205cdab379d5",
|
||||
"imageSha256": "054170fd9ef45762b602b4d5c9c3b9ea9724858be61af137970c246f485c13bb"
|
||||
},
|
||||
"room-card": {
|
||||
@@ -103,7 +103,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "997a6ed2829850e0bd02ffbe6aaef8fdfc3a1e22b8828d7cbe207b426a694b3b",
|
||||
"sourceSha256": "baf74cdb584726b1925079b3ee326edc1afc8b10f362bffb11be205cdab379d5",
|
||||
"imageSha256": "2ae4a58853d98e10d12456b2078ec2f6a0b597722c8310d7b016abd2bc42561e"
|
||||
},
|
||||
"device-info": {
|
||||
@@ -114,12 +114,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "997a6ed2829850e0bd02ffbe6aaef8fdfc3a1e22b8828d7cbe207b426a694b3b",
|
||||
"sourceSha256": "baf74cdb584726b1925079b3ee326edc1afc8b10f362bffb11be205cdab379d5",
|
||||
"imageSha256": "cb37f7eefd936f98ef44969b21dfe8e40ee27d1f558abe8d53c0223f6488b8a6"
|
||||
}
|
||||
},
|
||||
"acceptedOn": "linux",
|
||||
"acceptance": {
|
||||
"lastWriteWasFingerprintOnly": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5793,6 +5793,32 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' if (candidates.length >= 1) placements.set(node.key, {',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'topology-help-renders-without-aria',
|
||||
guard: 'node --test --test-name-pattern="кружок справки не рисуется|вызывается из блока" '
|
||||
+ 'test/zigbee-topology.test.mjs',
|
||||
because: 'a help circle that opens without an accessible name is worse than no circle, and '
|
||||
+ 'topologyT answers a missing key with the key itself — so absence has to be asked of the '
|
||||
+ 'dictionary, not of the resolved string (#459 AC2)',
|
||||
patches: [{
|
||||
file: 'src/i18n/topology.ts',
|
||||
find: ' const value = DICTIONARIES[lang]?.[key] ?? en[key];',
|
||||
replace: ' const value = DICTIONARIES[lang]?.[key] ?? en[key] ?? key;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'topology-help-drops-arrow-legend',
|
||||
guard: 'node --test --test-name-pattern="все шесть пунктов легенды|не путь пакета" '
|
||||
+ 'test/zigbee-topology.test.mjs',
|
||||
because: 'a hint that names only colour and dashes passes «the help exists» while answering '
|
||||
+ 'none of the questions the arrows raise — the very content this task waited for #457 to '
|
||||
+ 'write (#459 AC3, AC3b)',
|
||||
patches: [{
|
||||
file: 'src/i18n/topology/ru.json',
|
||||
find: ' Стрелка ведёт к следующему устройству по пути к координатору: исходящая одна, входящие — те, кто ходит через это устройство. Линия без стрелки — запасной сосед. Это дерево маршрутов, которое строит House Plan, а не путь пакета в эту секунду: подпись на конце стрелки значит, что цель не на этом плане, а отсутствие стрелки — что путь неизвестен.',
|
||||
replace: '',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'zigbee-topology-z2m-camelcase-node-rejected',
|
||||
guard: 'node --test --test-name-pattern="real anonymized camelCase" test/zigbee-topology.test.mjs',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LitElement, css, html, nothing, type PropertyValues } from 'lit';
|
||||
import { LitElement, css, html, nothing, type PropertyValues, type TemplateResult } from 'lit';
|
||||
import { langOf } from './i18n';
|
||||
import { topologyT, type TopologyI18nKey } from './i18n/topology';
|
||||
import { hasTopologyTranslation, topologyT, type TopologyI18nKey } from './i18n/topology';
|
||||
import './hp-help';
|
||||
import {
|
||||
normalizeZ2mBaseTopic, type ZigbeeTopologySettings,
|
||||
} from './zigbee-topology-settings';
|
||||
@@ -35,7 +36,13 @@ export class HpZigbeeTopologySettings extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
:host { display: block; color: inherit; font: inherit; }
|
||||
.section { margin-top: 18px; font-weight: 700; }
|
||||
.section {
|
||||
margin-top: 18px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
.toggle { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
|
||||
.hint, .status { color: var(--secondary-text-color, #9aa0aa); font-size: 12px; line-height: 1.45; }
|
||||
.hint { margin-top: 6px; }
|
||||
@@ -86,6 +93,26 @@ export class HpZigbeeTopologySettings extends LitElement {
|
||||
return topologyT(langOf(this.hass), key, vars);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contextual help for the feature, not for the checkbox (#459).
|
||||
*
|
||||
* `_help()` of the editor runtime is typed against the MAIN dictionary
|
||||
* (`I18nKey`), and this component owns its own `topology` namespace — so the
|
||||
* affordance is rebuilt here rather than imported. The fail-closed rule is
|
||||
* copied deliberately: a missing text or accessible name renders nothing at
|
||||
* all, because a help circle that opens an empty surface is worse than no
|
||||
* circle. Absence is asked of the dictionaries (`hasTopologyTranslation`),
|
||||
* not of the resolved string: `topologyT` answers a missing key with the key
|
||||
* itself, and «help» is a perfectly non-empty string.
|
||||
*/
|
||||
private _help(): TemplateResult | typeof nothing {
|
||||
const lang = langOf(this.hass);
|
||||
if (!hasTopologyTranslation(lang, 'help')
|
||||
|| !hasTopologyTranslation(lang, 'help_aria')) return nothing;
|
||||
return html`<hp-help .text=${this._t('help')}
|
||||
.ariaLabel=${this._t('help_aria')}></hp-help>`;
|
||||
}
|
||||
|
||||
private async _ensureRuntime(): Promise<typeof import('./zigbee-topology-runtime')> {
|
||||
if (!this._runtime) {
|
||||
this._runtime = await import('./zigbee-topology-runtime');
|
||||
@@ -157,7 +184,7 @@ export class HpZigbeeTopologySettings extends LitElement {
|
||||
const mayLoad = admin && this.savedEnabled;
|
||||
const topics = this._topics();
|
||||
return html`
|
||||
<div class="section">${this._t('title')}</div>
|
||||
<div class="section">${this._t('title')}${this._help()}</div>
|
||||
<label class="toggle">
|
||||
${customElements.get('ha-switch')
|
||||
? html`<ha-switch .checked=${enabled} .disabled=${!admin}
|
||||
|
||||
@@ -9,6 +9,21 @@ export type TopologyI18nKey = keyof typeof en;
|
||||
|
||||
const DICTIONARIES: Record<Lang, Record<TopologyI18nKey, string>> = { en, ru, de, fr };
|
||||
|
||||
/**
|
||||
* Whether the namespace really carries this string (#459).
|
||||
*
|
||||
* `topologyT` never fails: a missing key comes back as the key itself, so a
|
||||
* caller that only looks at the resolved value cannot tell «no translation»
|
||||
* from «translated to the word help». Affordances that must fail closed —
|
||||
* the contextual help circle is one — ask this instead. English is the
|
||||
* fallback layer exactly as in `topologyT`, so a key present in English and
|
||||
* missing in one locale still counts as available.
|
||||
*/
|
||||
export function hasTopologyTranslation(lang: Lang, key: TopologyI18nKey): boolean {
|
||||
const value = DICTIONARIES[lang]?.[key] ?? en[key];
|
||||
return typeof value === 'string' && value.trim().length > 0;
|
||||
}
|
||||
|
||||
export function topologyT(
|
||||
lang: Lang, key: TopologyI18nKey, vars?: Record<string, string | number>,
|
||||
): string {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
"title": "Zigbee-Verbindungen",
|
||||
"toggle": "Zigbee-Verbindungen beim Darüberfahren über ein Gerät anzeigen",
|
||||
"hint": "Es werden nur beobachtete direkte Nachbarn angezeigt. Beim Darüberfahren werden keine Daten abgerufen.",
|
||||
"help": "Zeigt, mit welchen Zigbee-Geräten jedes Gerät direkt verbunden ist. Die Linienfarbe ist die Verbindungsqualität (LQI) auf derselben Skala wie der LQI-Wert des Geräts: rot bei 40 und darunter, grün bei 180 und darüber. Eine gestrichelte Linie bedeutet, dass die Qualität nicht gemeldet wurde. Ein Pfeil zeigt auf das nächste Gerät auf dem Weg zum Koordinator: ein Pfeil führt hinaus, eingehende Pfeile sind die Geräte, die über dieses Gerät routen. Eine Linie ohne Pfeil ist ein Ersatznachbar. Das ist der Routenbaum, den House Plan bildet, nicht der Weg eines Pakets in diesem Moment: eine Beschriftung am Pfeilende bedeutet, dass das Ziel nicht auf diesem Plan liegt, und ein fehlender Pfeil, dass die Route unbekannt ist. Die Daten stammen aus dem letzten Laden und veralten; Verbindungen sind nur mit der Maus sichtbar.",
|
||||
"help_aria": "Hilfe: Zigbee-Verbindungen",
|
||||
"admin_only": "Topologiedaten sind nur für Home-Assistant-Administratoren verfügbar.",
|
||||
"save_first": "Speichere diese Einstellung, bevor Topologiedaten geladen werden.",
|
||||
"zha": "ZHA",
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
"title": "Zigbee links",
|
||||
"toggle": "Show Zigbee links when hovering over a device",
|
||||
"hint": "Shows observed direct neighbours only. Nothing is fetched while you hover.",
|
||||
"help": "Shows which Zigbee devices each device talks to directly. Line colour is link quality (LQI) on the same scale as the device's LQI reading: red at 40 and below, green at 180 and above. A dashed line means quality was not reported. An arrow points to the next device on the way to the coordinator: one arrow leads out, incoming arrows are the devices that route through this one. A line without an arrow is a spare neighbour. This is the route tree House Plan builds, not the path a packet takes right now: a label at the end of an arrow means its target is not on this plan, and no arrow at all means the route is unknown. Data comes from the last load and goes stale; links are shown with a mouse only.",
|
||||
"help_aria": "Help: Zigbee links",
|
||||
"admin_only": "Topology data is available to Home Assistant administrators only.",
|
||||
"save_first": "Save this setting before loading topology data.",
|
||||
"zha": "ZHA",
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
"title": "Liens Zigbee",
|
||||
"toggle": "Afficher les liens Zigbee au survol d’un appareil",
|
||||
"hint": "Seuls les voisins directs observés sont affichés. Le survol ne lance aucune requête.",
|
||||
"help": "Montre avec quels appareils Zigbee chaque appareil communique directement. La couleur de la ligne est la qualité du lien (LQI) sur la même échelle que l'indicateur LQI de l'appareil : rouge à 40 et en dessous, vert à 180 et au-dessus. Une ligne pointillée signifie que la qualité n'a pas été communiquée. Une flèche pointe vers l'appareil suivant sur le chemin du coordinateur : une flèche part, les flèches entrantes sont les appareils qui passent par celui-ci. Une ligne sans flèche est un voisin de secours. C'est l'arbre de routes que construit House Plan, pas le trajet d'un paquet à cet instant : une étiquette au bout d'une flèche signifie que sa cible n'est pas sur ce plan, et l'absence de flèche que la route est inconnue. Les données proviennent du dernier chargement et vieillissent ; les liens ne sont visibles qu'à la souris.",
|
||||
"help_aria": "Aide : liens Zigbee",
|
||||
"admin_only": "Les données de topologie sont réservées aux administrateurs Home Assistant.",
|
||||
"save_first": "Enregistrez ce réglage avant de charger les données de topologie.",
|
||||
"zha": "ZHA",
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
"title": "Связи Zigbee",
|
||||
"toggle": "Показывать связи Zigbee при наведении на устройство",
|
||||
"hint": "Показываются только наблюдаемые прямые соседи. Наведение ничего не запрашивает.",
|
||||
"help": "Показывает, с какими устройствами Zigbee каждое связано напрямую. Цвет линии — качество связи (LQI) по той же шкале, что у показателя LQI устройства: красный при 40 и ниже, зелёный при 180 и выше. Пунктир — качество не сообщено. Стрелка ведёт к следующему устройству по пути к координатору: исходящая одна, входящие — те, кто ходит через это устройство. Линия без стрелки — запасной сосед. Это дерево маршрутов, которое строит House Plan, а не путь пакета в эту секунду: подпись на конце стрелки значит, что цель не на этом плане, а отсутствие стрелки — что путь неизвестен. Данные из последней загрузки и устаревают, связи видны только при работе мышью.",
|
||||
"help_aria": "Справка: связи Zigbee",
|
||||
"admin_only": "Данные топологии доступны только администраторам Home Assistant.",
|
||||
"save_first": "Сохраните настройку перед загрузкой данных топологии.",
|
||||
"zha": "ZHA",
|
||||
|
||||
@@ -401,3 +401,106 @@ test('Z2M runtime refuses an unconfirmed base topic without publishing and still
|
||||
assert.equal(cleanups, 2);
|
||||
assert.equal(zigbeeTopologyRuntimeSnapshot(hass).states['z2m:zigbee2mqtt'].error, 'timeout');
|
||||
});
|
||||
|
||||
// #459. Подсказка «Связи Zigbee»: легенда живёт в словаре, и её содержание —
|
||||
// защитный контракт. Текст, называющий только цвет и пунктир, формально
|
||||
// «подсказка есть», но не отвечает ни на один вопрос про стрелки — ради
|
||||
// которых задача и ждала #457. Поэтому проверяется КАЖДЫЙ пункт легенды.
|
||||
|
||||
const topologyDict = (code) => JSON.parse(readFileSync(
|
||||
new URL(`../src/i18n/topology/${code}.json`, import.meta.url), 'utf8',
|
||||
));
|
||||
const TOPOLOGY_LANGS = ['en', 'ru', 'de', 'fr'];
|
||||
|
||||
test('подсказка называет все шесть пунктов легенды (#459 AC3)', () => {
|
||||
const help = topologyDict('ru').help;
|
||||
const claims = [
|
||||
// шкала LQI — обе границы, и они не выдуманы, а взяты из lqiColor (AC4)
|
||||
[/\bLQI\b/, 'качество связи названо аббревиатурой LQI'],
|
||||
[/\b40\b/, 'нижняя граница шкалы'],
|
||||
[/\b180\b/, 'верхняя граница шкалы'],
|
||||
[/[Пп]унктир/, 'пунктир как отдельное состояние линии'],
|
||||
[/исходящ/i, 'исходящая стрелка'],
|
||||
[/координатор/i, 'исходящая стрелка ведёт к координатору'],
|
||||
[/входящ/i, 'входящие стрелки'],
|
||||
[/без стрелки/i, 'линия без стрелки — запасной сосед'],
|
||||
[/подпись на конце стрелки/i, 'подпись = цель не на этом плане'],
|
||||
[/отсутствие стрелки/i, 'нет стрелки = путь неизвестен'],
|
||||
];
|
||||
for (const [pattern, why] of claims) {
|
||||
assert.match(help, pattern, `подсказка не называет: ${why}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('подсказка предупреждает, что стрелки — не путь пакета (#459 AC3b)', () => {
|
||||
// Оговорка унаследована от §6 ТЗ #457: дерево аплинков строим мы, и между
|
||||
// роутерами это приближение. Без неё администратор примет стрелку за истину.
|
||||
const help = topologyDict('ru').help;
|
||||
// `\w` в JS-регулярке ASCII-словесный: «дерев\w+» на кириллице не совпадёт
|
||||
// никогда. Ловушка та же, что с `\b` в счётчике раундов ревью (#454).
|
||||
assert.match(help, /дерево маршрут/i);
|
||||
assert.match(help, /не путь пакета/i);
|
||||
});
|
||||
|
||||
test('границы шкалы в подсказке — те же, что у lqiColor (#459 AC4)', async () => {
|
||||
const { lqiColor } = await import('../test-build/logic.js');
|
||||
const hueOf = (value) => Number(/hsl\((\d+)/.exec(lqiColor(value))[1]);
|
||||
// Красный край и зелёный край берутся из функции, а не из константы в тесте:
|
||||
// сдвинется реализация — тест назовёт другие числа и подсказка разойдётся.
|
||||
assert.equal(hueOf(40), 0, 'красный край шкалы');
|
||||
assert.equal(hueOf(180), 120, 'зелёный край шкалы');
|
||||
const help = topologyDict('ru').help;
|
||||
assert.match(help, new RegExp(`\\b40\\b`));
|
||||
assert.match(help, new RegExp(`\\b180\\b`));
|
||||
});
|
||||
|
||||
test('подсказка не полагается на переносы строк (#459 AC5)', () => {
|
||||
// hp-help кладёт .text текстовым узлом: \n схлопнется в пробел.
|
||||
for (const code of TOPOLOGY_LANGS) {
|
||||
const dict = topologyDict(code);
|
||||
assert.ok(!dict.help.includes('\n'), `${code}: перенос строки в help`);
|
||||
assert.ok(dict.help.trim().length > 0, `${code}: пустая подсказка`);
|
||||
assert.ok(dict.help_aria.trim().length > 0, `${code}: пустая подпись для скринридера`);
|
||||
}
|
||||
});
|
||||
|
||||
test('словари topology несут один и тот же набор ключей (#459 AC6)', () => {
|
||||
// Гейт, которого не было: i18n-dead-keys и i18n.test знают основной словарь,
|
||||
// бэкендные переводы и support, но не namespace topology.
|
||||
const english = Object.keys(topologyDict('en')).sort();
|
||||
const placeholders = (value) => (String(value).match(/\{\w+\}/gu) || []).sort();
|
||||
const en = topologyDict('en');
|
||||
for (const code of TOPOLOGY_LANGS) {
|
||||
const dict = topologyDict(code);
|
||||
assert.deepEqual(Object.keys(dict).sort(), english, `${code}: набор ключей расходится`);
|
||||
for (const key of english) {
|
||||
assert.deepEqual(placeholders(dict[key]), placeholders(en[key]),
|
||||
`${code}: плейсхолдеры расходятся в ${key}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('кружок справки не рисуется без подписи для скринридера (#459 AC2)', async () => {
|
||||
const { hasTopologyTranslation } = await import('../test-build/i18n/topology.js');
|
||||
// Проверка идёт по СЛОВАРЮ, а не по строке: topologyT на отсутствующий ключ
|
||||
// отвечает именем ключа, и «help» — вполне непустая строка.
|
||||
assert.equal(hasTopologyTranslation('ru', 'help'), true);
|
||||
assert.equal(hasTopologyTranslation('ru', 'help_aria'), true);
|
||||
assert.equal(hasTopologyTranslation('ru', 'no_such_key'), false);
|
||||
// Английский — слой фолбэка: ключ, которого нет в локали, но есть в en,
|
||||
// доступен, как и в topologyT.
|
||||
assert.equal(hasTopologyTranslation('de', 'help'), true);
|
||||
});
|
||||
|
||||
test('подсказка вызывается из блока настройки топологии (#459 AC1)', () => {
|
||||
const source = readFileSync(
|
||||
new URL('../src/hp-zigbee-topology-settings.ts', import.meta.url), 'utf8',
|
||||
);
|
||||
// Кружок стоит у ЗАГОЛОВКА функции, а не у тумблера: он объясняет функцию,
|
||||
// а не то, что делает галочка.
|
||||
assert.match(source, /<div class="section">\$\{this\._t\('title'\)\}\$\{this\._help\(\)\}<\/div>/);
|
||||
assert.match(source, /<hp-help \.text=/);
|
||||
// Fail-closed: оба ключа обязательны, иначе не рисуется ничего.
|
||||
assert.match(source, /hasTopologyTranslation\(lang, 'help'\)/);
|
||||
assert.match(source, /hasTopologyTranslation\(lang, 'help_aria'\)/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user