anthodev/lapin-futepublic Fork
⑂ 0cc3c0b
Commits
⬇ Clone ▾
git clone https://git.rickub.com/anthodev/lapin-fute.git
git clone ssh://git@rickub.com/anthodev/lapin-fute.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat(display): prefix metro and tram line labels on watch badges (#24) (#25)Unverified

Cédric Anthony committed 2026-09-13T20:35:07+02:00 Browse files
0cc3c0b parent: 1889044
modified package-lock.json +6 -6
@@ -1,12 +1,12 @@
11 {
22 "name": "lapin-fute",
3- "version": "1.0.1",
3+ "version": "1.0.2",
44 "lockfileVersion": 3,
55 "requires": true,
66 "packages": {
77 "": {
88 "name": "lapin-fute",
9- "version": "1.0.1",
9+ "version": "1.0.2",
1010 "license": "MIT",
1111 "workspaces": [
1212 "packages/*"
@@ -57,7 +57,7 @@
5757 },
5858 "packages/catalog": {
5959 "name": "@lapin-fute/catalog",
60- "version": "1.0.1",
60+ "version": "1.0.2",
6161 "license": "MIT",
6262 "dependencies": {
6363 "csv-parse": "7.0.2",
@@ -66,17 +66,17 @@
6666 },
6767 "packages/companion": {
6868 "name": "@lapin-fute/companion",
69- "version": "1.0.1",
69+ "version": "1.0.2",
7070 "license": "MIT"
7171 },
7272 "packages/config-page": {
7373 "name": "@lapin-fute/config-page",
74- "version": "1.0.1",
74+ "version": "1.0.2",
7575 "license": "MIT"
7676 },
7777 "packages/watch": {
7878 "name": "lapin-fute-watch",
79- "version": "1.0.1",
79+ "version": "1.0.2",
8080 "license": "MIT"
8181 }
8282 }
@@ -1,12 +1,12 @@
1 {1 {
2 "name": "lapin-fute",2 "name": "lapin-fute",
3- "version": "1.0.1",3+ "version": "1.0.2",
4 "lockfileVersion": 3,4 "lockfileVersion": 3,
5 "requires": true,5 "requires": true,
6 "packages": {6 "packages": {
7 "": {7 "": {
8 "name": "lapin-fute",8 "name": "lapin-fute",
9- "version": "1.0.1",9+ "version": "1.0.2",
10 "license": "MIT",10 "license": "MIT",
11 "workspaces": [11 "workspaces": [
12 "packages/*"12 "packages/*"
@@ -57,7 +57,7 @@
57 },57 },
58 "packages/catalog": {58 "packages/catalog": {
59 "name": "@lapin-fute/catalog",59 "name": "@lapin-fute/catalog",
60- "version": "1.0.1",60+ "version": "1.0.2",
61 "license": "MIT",61 "license": "MIT",
62 "dependencies": {62 "dependencies": {
63 "csv-parse": "7.0.2",63 "csv-parse": "7.0.2",
@@ -66,17 +66,17 @@
66 },66 },
67 "packages/companion": {67 "packages/companion": {
68 "name": "@lapin-fute/companion",68 "name": "@lapin-fute/companion",
69- "version": "1.0.1",69+ "version": "1.0.2",
70 "license": "MIT"70 "license": "MIT"
71 },71 },
72 "packages/config-page": {72 "packages/config-page": {
73 "name": "@lapin-fute/config-page",73 "name": "@lapin-fute/config-page",
74- "version": "1.0.1",74+ "version": "1.0.2",
75 "license": "MIT"75 "license": "MIT"
76 },76 },
77 "packages/watch": {77 "packages/watch": {
78 "name": "lapin-fute-watch",78 "name": "lapin-fute-watch",
79- "version": "1.0.1",79+ "version": "1.0.2",
80 "license": "MIT"80 "license": "MIT"
81 }81 }
82 }82 }
modified package.json +1 -1
@@ -1,6 +1,6 @@
11 {
22 "name": "lapin-fute",
3- "version": "1.0.1",
3+ "version": "1.0.2",
44 "license": "MIT",
55 "private": true,
66 "type": "module",
@@ -1,6 +1,6 @@
1 {1 {
2 "name": "lapin-fute",2 "name": "lapin-fute",
3- "version": "1.0.1",3+ "version": "1.0.2",
4 "license": "MIT",4 "license": "MIT",
5 "private": true,5 "private": true,
6 "type": "module",6 "type": "module",
modified packages/catalog/package.json +1 -1
@@ -1,6 +1,6 @@
11 {
22 "name": "@lapin-fute/catalog",
3- "version": "1.0.1",
3+ "version": "1.0.2",
44 "license": "MIT",
55 "private": true,
66 "type": "module",
@@ -1,6 +1,6 @@
1 {1 {
2 "name": "@lapin-fute/catalog",2 "name": "@lapin-fute/catalog",
3- "version": "1.0.1",3+ "version": "1.0.2",
4 "license": "MIT",4 "license": "MIT",
5 "private": true,5 "private": true,
6 "type": "module",6 "type": "module",
modified packages/companion/package.json +1 -1
@@ -1,6 +1,6 @@
11 {
22 "name": "@lapin-fute/companion",
3- "version": "1.0.1",
3+ "version": "1.0.2",
44 "license": "MIT",
55 "private": true,
66 "type": "commonjs",
@@ -1,6 +1,6 @@
1 {1 {
2 "name": "@lapin-fute/companion",2 "name": "@lapin-fute/companion",
3- "version": "1.0.1",3+ "version": "1.0.2",
4 "license": "MIT",4 "license": "MIT",
5 "private": true,5 "private": true,
6 "type": "commonjs",6 "type": "commonjs",
modified packages/companion/src/display-layout.js +11 -2
@@ -162,6 +162,15 @@ function prepareAppearance(favorite, profile, language) {
162162 for (const key of ["id", "lineLabel", "stopLabel", "destinationLabel"]) {
163163 if (sourceBytes(favorite[key], key === "id" ? 64 : 96) === 0) throw new TypeError("Empty appearance label");
164164 }
165+ // Display label shared by the line slots and the serialized field: metro and
166+ // tram digit lines gain their mode prefix, and metro "bis" contracts to "b".
167+ let lineLabel = favorite.lineLabel;
168+ if (favorite.lineMode === "METRO") {
169+ const bis = /^M?([0-9]+)\s*bis$/i.exec(lineLabel);
170+ lineLabel = bis ? "M" + bis[1] + "b" : /^[0-9]/.test(lineLabel) ? "M" + lineLabel : lineLabel;
171+ } else if (favorite.lineMode === "TRAM" && /^[0-9]/.test(lineLabel)) {
172+ lineLabel = "T" + lineLabel;
173+ }
165174 const hasBackground = favorite.lineColor !== undefined, hasForeground = favorite.lineTextColor !== undefined;
166175 if (hasBackground !== hasForeground || (hasBackground && (!/^#[0-9a-f]{6}$/.test(favorite.lineColor)
167176 || !/^#[0-9a-f]{6}$/.test(favorite.lineTextColor)))) throw new TypeError("Invalid appearance colors");
@@ -169,13 +178,13 @@ function prepareAppearance(favorite, profile, language) {
169178 profile ? 67 : 84, profile ? 54 : 71, profile ? 116 : 132, profile ? 86 : 74, profile ? 73 : 61, profile ? 106 : 122];
170179 let ends = "", mask = 0;
171180 for (let slot = 0; slot < 12; slot++) {
172- const label = slot < 4 ? favorite.lineLabel : slot < 9 ? favorite.stopLabel : favorite.destinationLabel;
181+ const label = slot < 4 ? lineLabel : slot < 9 ? favorite.stopLabel : favorite.destinationLabel;
173182 const fit = clip(label, slot === 3 || slot >= 9 ? 0 : 1, widths[slot]);
174183 ends += fixed(scalarLength(label.slice(0, fit.end)), 2);
175184 if (fit.ellipsis) mask |= 1 << slot;
176185 }
177186 const id = lp3(favorite.id);
178- const content = lp3(favorite.lineLabel) + lp3(favorite.stopLabel) + lp3(favorite.destinationLabel)
187+ const content = lp3(lineLabel) + lp3(favorite.stopLabel) + lp3(favorite.destinationLabel)
179188 + (hasBackground ? favorite.lineColor.slice(1) : "52616f")
180189 + (hasForeground ? favorite.lineTextColor.slice(1) : "ffffff") + ends + fixed(mask, 3);
181190 const hashInput = "D2" + profile + language + id + content;
@@ -162,6 +162,15 @@ function prepareAppearance(favorite, profile, language) {
162 for (const key of ["id", "lineLabel", "stopLabel", "destinationLabel"]) {162 for (const key of ["id", "lineLabel", "stopLabel", "destinationLabel"]) {
163 if (sourceBytes(favorite[key], key === "id" ? 64 : 96) === 0) throw new TypeError("Empty appearance label");163 if (sourceBytes(favorite[key], key === "id" ? 64 : 96) === 0) throw new TypeError("Empty appearance label");
164 }164 }
165+ // Display label shared by the line slots and the serialized field: metro and
166+ // tram digit lines gain their mode prefix, and metro "bis" contracts to "b".
167+ let lineLabel = favorite.lineLabel;
168+ if (favorite.lineMode === "METRO") {
169+ const bis = /^M?([0-9]+)\s*bis$/i.exec(lineLabel);
170+ lineLabel = bis ? "M" + bis[1] + "b" : /^[0-9]/.test(lineLabel) ? "M" + lineLabel : lineLabel;
171+ } else if (favorite.lineMode === "TRAM" && /^[0-9]/.test(lineLabel)) {
172+ lineLabel = "T" + lineLabel;
173+ }
165 const hasBackground = favorite.lineColor !== undefined, hasForeground = favorite.lineTextColor !== undefined;174 const hasBackground = favorite.lineColor !== undefined, hasForeground = favorite.lineTextColor !== undefined;
166 if (hasBackground !== hasForeground || (hasBackground && (!/^#[0-9a-f]{6}$/.test(favorite.lineColor)175 if (hasBackground !== hasForeground || (hasBackground && (!/^#[0-9a-f]{6}$/.test(favorite.lineColor)
167 || !/^#[0-9a-f]{6}$/.test(favorite.lineTextColor)))) throw new TypeError("Invalid appearance colors");176 || !/^#[0-9a-f]{6}$/.test(favorite.lineTextColor)))) throw new TypeError("Invalid appearance colors");
@@ -169,13 +178,13 @@ function prepareAppearance(favorite, profile, language) {
169 profile ? 67 : 84, profile ? 54 : 71, profile ? 116 : 132, profile ? 86 : 74, profile ? 73 : 61, profile ? 106 : 122];178 profile ? 67 : 84, profile ? 54 : 71, profile ? 116 : 132, profile ? 86 : 74, profile ? 73 : 61, profile ? 106 : 122];
170 let ends = "", mask = 0;179 let ends = "", mask = 0;
171 for (let slot = 0; slot < 12; slot++) {180 for (let slot = 0; slot < 12; slot++) {
172- const label = slot < 4 ? favorite.lineLabel : slot < 9 ? favorite.stopLabel : favorite.destinationLabel;181+ const label = slot < 4 ? lineLabel : slot < 9 ? favorite.stopLabel : favorite.destinationLabel;
173 const fit = clip(label, slot === 3 || slot >= 9 ? 0 : 1, widths[slot]);182 const fit = clip(label, slot === 3 || slot >= 9 ? 0 : 1, widths[slot]);
174 ends += fixed(scalarLength(label.slice(0, fit.end)), 2);183 ends += fixed(scalarLength(label.slice(0, fit.end)), 2);
175 if (fit.ellipsis) mask |= 1 << slot;184 if (fit.ellipsis) mask |= 1 << slot;
176 }185 }
177 const id = lp3(favorite.id);186 const id = lp3(favorite.id);
178- const content = lp3(favorite.lineLabel) + lp3(favorite.stopLabel) + lp3(favorite.destinationLabel)187+ const content = lp3(lineLabel) + lp3(favorite.stopLabel) + lp3(favorite.destinationLabel)
179 + (hasBackground ? favorite.lineColor.slice(1) : "52616f")188 + (hasBackground ? favorite.lineColor.slice(1) : "52616f")
180 + (hasForeground ? favorite.lineTextColor.slice(1) : "ffffff") + ends + fixed(mask, 3);189 + (hasForeground ? favorite.lineTextColor.slice(1) : "ffffff") + ends + fixed(mask, 3);
181 const hashInput = "D2" + profile + language + id + content;190 const hashInput = "D2" + profile + language + id + content;
modified packages/config-page/index.html +2 -2
@@ -146,7 +146,7 @@
146146 </div>
147147
148148 <div id="about-en" lang="en">
149- <p class="about-version">Version 1.0.1</p>
149+ <p class="about-version">Version 1.0.2</p>
150150 <h3>Publisher and contact</h3>
151151 <p><strong>Author and publisher:</strong> Anthodev, a private individual acting on a non-professional basis.</p>
152152 <p><strong>Publication director:</strong> Anthodev.</p>
@@ -168,7 +168,7 @@
168168 </div>
169169
170170 <div id="about-fr" lang="fr" hidden>
171- <p class="about-version">Version 1.0.1</p>
171+ <p class="about-version">Version 1.0.2</p>
172172 <h3>Édition et contact</h3>
173173 <p><strong>Auteur et éditeur :</strong> Anthodev, personne physique agissant à titre privé et non professionnel.</p>
174174 <p><strong>Directeur de la publication :</strong> Anthodev.</p>
@@ -146,7 +146,7 @@
146 </div>146 </div>
147 147
148 <div id="about-en" lang="en">148 <div id="about-en" lang="en">
149- <p class="about-version">Version 1.0.1</p>149+ <p class="about-version">Version 1.0.2</p>
150 <h3>Publisher and contact</h3>150 <h3>Publisher and contact</h3>
151 <p><strong>Author and publisher:</strong> Anthodev, a private individual acting on a non-professional basis.</p>151 <p><strong>Author and publisher:</strong> Anthodev, a private individual acting on a non-professional basis.</p>
152 <p><strong>Publication director:</strong> Anthodev.</p>152 <p><strong>Publication director:</strong> Anthodev.</p>
@@ -168,7 +168,7 @@
168 </div>168 </div>
169 169
170 <div id="about-fr" lang="fr" hidden>170 <div id="about-fr" lang="fr" hidden>
171- <p class="about-version">Version 1.0.1</p>171+ <p class="about-version">Version 1.0.2</p>
172 <h3>Édition et contact</h3>172 <h3>Édition et contact</h3>
173 <p><strong>Auteur et éditeur :</strong> Anthodev, personne physique agissant à titre privé et non professionnel.</p>173 <p><strong>Auteur et éditeur :</strong> Anthodev, personne physique agissant à titre privé et non professionnel.</p>
174 <p><strong>Directeur de la publication :</strong> Anthodev.</p>174 <p><strong>Directeur de la publication :</strong> Anthodev.</p>
modified packages/config-page/package.json +1 -1
@@ -1,6 +1,6 @@
11 {
22 "name": "@lapin-fute/config-page",
3- "version": "1.0.1",
3+ "version": "1.0.2",
44 "license": "MIT",
55 "private": true,
66 "type": "module",
@@ -1,6 +1,6 @@
1 {1 {
2 "name": "@lapin-fute/config-page",2 "name": "@lapin-fute/config-page",
3- "version": "1.0.1",3+ "version": "1.0.2",
4 "license": "MIT",4 "license": "MIT",
5 "private": true,5 "private": true,
6 "type": "module",6 "type": "module",
modified packages/watch/package.json +1 -1
@@ -1,7 +1,7 @@
11 {
22 "name": "lapin-fute-watch",
33 "author": "Anthodev",
4- "version": "1.0.1",
4+ "version": "1.0.2",
55 "license": "MIT",
66 "keywords": [
77 "pebble-app"
@@ -1,7 +1,7 @@
1 {1 {
2 "name": "lapin-fute-watch",2 "name": "lapin-fute-watch",
3 "author": "Anthodev",3 "author": "Anthodev",
4- "version": "1.0.1",4+ "version": "1.0.2",
5 "license": "MIT",5 "license": "MIT",
6 "keywords": [6 "keywords": [
7 "pebble-app"7 "pebble-app"
modified packages/watch/test/packed.test.js +59 -0
@@ -7,6 +7,8 @@ const {
77 trafficError, pages, stale
88 } = await watchModule("packed");
99 import { NOW, appearance, departure, trafficDocument, scalars } from "./d2-records.js";
10+import layout from "../../companion/src/display-layout.js";
11+const { prepareAppearance } = layout;
1012
1113 test("appearance records round trip and validate", () => {
1214 const record = appearance({ id: "home:rer:a" });
@@ -61,6 +63,63 @@ test("clipped honours scalar endpoints and the ellipsis mask", () => {
6163 assert(!appearanceValid(shortened, 0, "fr"), "shortened ends without mask bits reject");
6264 });
6365
66+// Regression: the phone derives one display label (metro/tram prefixes, metro
67+// "bis" contraction) that must reach both the cut slots and the serialized
68+// field the watch validates, with the favorite left untouched.
69+test("prepared appearances prefix metro and tram labels consistently", () => {
70+ const favorite = (id, line, mode) => ({
71+ id, lineLabel: line, stopLabel: "Saint-Lazare", destinationLabel: "Montfermeil",
72+ ...(mode ? { lineMode: mode, lineColor: "#d9ebde", lineTextColor: "#174d31" } : {})
73+ });
74+ const cases = [
75+ [favorite("pfx:metro", "13", "METRO"), "M13"],
76+ [favorite("pfx:tram", "11", "TRAM"), "T11"],
77+ [favorite("pfx:tram-prefixed", "T11", "TRAM"), "T11"],
78+ [favorite("pfx:metro-prefixed", "M13", "METRO"), "M13"],
79+ [favorite("pfx:tram-suffix", "T3b", "TRAM"), "T3b"],
80+ [favorite("pfx:bus", "13", "BUS"), "13"],
81+ [favorite("pfx:rer", "A", "RER"), "A"],
82+ [favorite("pfx:transilien", "L", "TRANSILIEN"), "L"],
83+ [favorite("pfx:untyped", "13", undefined), "13"]
84+ ];
85+ for (const profile of [0, 1]) for (const [fav, label] of cases) {
86+ const snapshot = structuredClone(fav);
87+ const record = prepareAppearance(fav, profile, "fr");
88+ assert(appearanceValid(record, profile, "fr"), `${fav.id} profile ${profile}`);
89+ assert.equal(field(record, 2), label, `${fav.id} serialized line field`);
90+ for (const slot of [0, 1, 2, 3]) assert.equal(clipped(record, slot), label, `${fav.id} slot ${slot}`);
91+ assert.deepEqual(fav, snapshot);
92+ }
93+});
94+
95+test("prepared appearances contract metro bis lines without clipping", () => {
96+ const favorite = (id, line, mode) => ({
97+ id, lineLabel: line, stopLabel: "Saint-Lazare", destinationLabel: "Montfermeil",
98+ lineMode: mode, lineColor: "#d9ebde", lineTextColor: "#174d31"
99+ });
100+ const cases = [
101+ [favorite("bis:plain", "3bis", "METRO"), "M3b"],
102+ [favorite("bis:seven", "7bis", "METRO"), "M7b"],
103+ [favorite("bis:spaced", "3 bis", "METRO"), "M3b"],
104+ [favorite("bis:prefixed", "M3bis", "METRO"), "M3b"],
105+ [favorite("bis:short", "M3b", "METRO"), "M3b"],
106+ // The contraction is metro-only: a bus named 3bis keeps its exact label.
107+ [favorite("bis:bus", "3bis", "BUS"), "3bis"]
108+ ];
109+ for (const profile of [0, 1]) for (const [fav, label] of cases) {
110+ const snapshot = structuredClone(fav);
111+ const record = prepareAppearance(fav, profile, "fr");
112+ assert(appearanceValid(record, profile, "fr"), `${fav.id} profile ${profile}`);
113+ assert.equal(field(record, 2), label, `${fav.id} serialized line field`);
114+ for (const slot of [0, 1, 2, 3]) {
115+ const shown = clipped(record, slot);
116+ assert.equal(shown, label, `${fav.id} slot ${slot}`);
117+ assert(!shown.includes("…"), `${fav.id} slot ${slot} must not truncate`);
118+ }
119+ assert.deepEqual(fav, snapshot);
120+ }
121+});
122+
64123 test("departure records validate bounds and pairing", () => {
65124 const good = departure({});
66125 assert(departureValid(good, 1));
@@ -7,6 +7,8 @@ const {
7 trafficError, pages, stale7 trafficError, pages, stale
8 } = await watchModule("packed");8 } = await watchModule("packed");
9 import { NOW, appearance, departure, trafficDocument, scalars } from "./d2-records.js";9 import { NOW, appearance, departure, trafficDocument, scalars } from "./d2-records.js";
10+import layout from "../../companion/src/display-layout.js";
11+const { prepareAppearance } = layout;
10 12
11 test("appearance records round trip and validate", () => {13 test("appearance records round trip and validate", () => {
12 const record = appearance({ id: "home:rer:a" });14 const record = appearance({ id: "home:rer:a" });
@@ -61,6 +63,63 @@ test("clipped honours scalar endpoints and the ellipsis mask", () => {
61 assert(!appearanceValid(shortened, 0, "fr"), "shortened ends without mask bits reject");63 assert(!appearanceValid(shortened, 0, "fr"), "shortened ends without mask bits reject");
62 });64 });
63 65
66+// Regression: the phone derives one display label (metro/tram prefixes, metro
67+// "bis" contraction) that must reach both the cut slots and the serialized
68+// field the watch validates, with the favorite left untouched.
69+test("prepared appearances prefix metro and tram labels consistently", () => {
70+ const favorite = (id, line, mode) => ({
71+ id, lineLabel: line, stopLabel: "Saint-Lazare", destinationLabel: "Montfermeil",
72+ ...(mode ? { lineMode: mode, lineColor: "#d9ebde", lineTextColor: "#174d31" } : {})
73+ });
74+ const cases = [
75+ [favorite("pfx:metro", "13", "METRO"), "M13"],
76+ [favorite("pfx:tram", "11", "TRAM"), "T11"],
77+ [favorite("pfx:tram-prefixed", "T11", "TRAM"), "T11"],
78+ [favorite("pfx:metro-prefixed", "M13", "METRO"), "M13"],
79+ [favorite("pfx:tram-suffix", "T3b", "TRAM"), "T3b"],
80+ [favorite("pfx:bus", "13", "BUS"), "13"],
81+ [favorite("pfx:rer", "A", "RER"), "A"],
82+ [favorite("pfx:transilien", "L", "TRANSILIEN"), "L"],
83+ [favorite("pfx:untyped", "13", undefined), "13"]
84+ ];
85+ for (const profile of [0, 1]) for (const [fav, label] of cases) {
86+ const snapshot = structuredClone(fav);
87+ const record = prepareAppearance(fav, profile, "fr");
88+ assert(appearanceValid(record, profile, "fr"), `${fav.id} profile ${profile}`);
89+ assert.equal(field(record, 2), label, `${fav.id} serialized line field`);
90+ for (const slot of [0, 1, 2, 3]) assert.equal(clipped(record, slot), label, `${fav.id} slot ${slot}`);
91+ assert.deepEqual(fav, snapshot);
92+ }
93+});
94+
95+test("prepared appearances contract metro bis lines without clipping", () => {
96+ const favorite = (id, line, mode) => ({
97+ id, lineLabel: line, stopLabel: "Saint-Lazare", destinationLabel: "Montfermeil",
98+ lineMode: mode, lineColor: "#d9ebde", lineTextColor: "#174d31"
99+ });
100+ const cases = [
101+ [favorite("bis:plain", "3bis", "METRO"), "M3b"],
102+ [favorite("bis:seven", "7bis", "METRO"), "M7b"],
103+ [favorite("bis:spaced", "3 bis", "METRO"), "M3b"],
104+ [favorite("bis:prefixed", "M3bis", "METRO"), "M3b"],
105+ [favorite("bis:short", "M3b", "METRO"), "M3b"],
106+ // The contraction is metro-only: a bus named 3bis keeps its exact label.
107+ [favorite("bis:bus", "3bis", "BUS"), "3bis"]
108+ ];
109+ for (const profile of [0, 1]) for (const [fav, label] of cases) {
110+ const snapshot = structuredClone(fav);
111+ const record = prepareAppearance(fav, profile, "fr");
112+ assert(appearanceValid(record, profile, "fr"), `${fav.id} profile ${profile}`);
113+ assert.equal(field(record, 2), label, `${fav.id} serialized line field`);
114+ for (const slot of [0, 1, 2, 3]) {
115+ const shown = clipped(record, slot);
116+ assert.equal(shown, label, `${fav.id} slot ${slot}`);
117+ assert(!shown.includes("…"), `${fav.id} slot ${slot} must not truncate`);
118+ }
119+ assert.deepEqual(fav, snapshot);
120+ }
121+});
122+
64 test("departure records validate bounds and pairing", () => {123 test("departure records validate bounds and pairing", () => {
65 const good = departure({});124 const good = departure({});
66 assert(departureValid(good, 1));125 assert(departureValid(good, 1));