feat(display): prefix metro and tram line labels on watch badges (#24) (#25)Unverified
0cc3c0b parent: 1889044 modified
package-lock.json +6 -6 | @@ -1,12 +1,12 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "name": "lapin-fute", |
| 3 | - "version": "1.0.1", | |
| 3 | + "version": "1.0.2", | |
| 4 | 4 | "lockfileVersion": 3, |
| 5 | 5 | "requires": true, |
| 6 | 6 | "packages": { |
| 7 | 7 | "": { |
| 8 | 8 | "name": "lapin-fute", |
| 9 | - "version": "1.0.1", | |
| 9 | + "version": "1.0.2", | |
| 10 | 10 | "license": "MIT", |
| 11 | 11 | "workspaces": [ |
| 12 | 12 | "packages/*" |
| @@ -57,7 +57,7 @@ | ||
| 57 | 57 | }, |
| 58 | 58 | "packages/catalog": { |
| 59 | 59 | "name": "@lapin-fute/catalog", |
| 60 | - "version": "1.0.1", | |
| 60 | + "version": "1.0.2", | |
| 61 | 61 | "license": "MIT", |
| 62 | 62 | "dependencies": { |
| 63 | 63 | "csv-parse": "7.0.2", |
| @@ -66,17 +66,17 @@ | ||
| 66 | 66 | }, |
| 67 | 67 | "packages/companion": { |
| 68 | 68 | "name": "@lapin-fute/companion", |
| 69 | - "version": "1.0.1", | |
| 69 | + "version": "1.0.2", | |
| 70 | 70 | "license": "MIT" |
| 71 | 71 | }, |
| 72 | 72 | "packages/config-page": { |
| 73 | 73 | "name": "@lapin-fute/config-page", |
| 74 | - "version": "1.0.1", | |
| 74 | + "version": "1.0.2", | |
| 75 | 75 | "license": "MIT" |
| 76 | 76 | }, |
| 77 | 77 | "packages/watch": { |
| 78 | 78 | "name": "lapin-fute-watch", |
| 79 | - "version": "1.0.1", | |
| 79 | + "version": "1.0.2", | |
| 80 | 80 | "license": "MIT" |
| 81 | 81 | } |
| 82 | 82 | } |
| @@ -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 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "name": "lapin-fute", |
| 3 | - "version": "1.0.1", | |
| 3 | + "version": "1.0.2", | |
| 4 | 4 | "license": "MIT", |
| 5 | 5 | "private": true, |
| 6 | 6 | "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 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "name": "@lapin-fute/catalog", |
| 3 | - "version": "1.0.1", | |
| 3 | + "version": "1.0.2", | |
| 4 | 4 | "license": "MIT", |
| 5 | 5 | "private": true, |
| 6 | 6 | "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 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "name": "@lapin-fute/companion", |
| 3 | - "version": "1.0.1", | |
| 3 | + "version": "1.0.2", | |
| 4 | 4 | "license": "MIT", |
| 5 | 5 | "private": true, |
| 6 | 6 | "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) { | ||
| 162 | 162 | for (const key of ["id", "lineLabel", "stopLabel", "destinationLabel"]) { |
| 163 | 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 | 174 | const hasBackground = favorite.lineColor !== undefined, hasForeground = favorite.lineTextColor !== undefined; |
| 166 | 175 | if (hasBackground !== hasForeground || (hasBackground && (!/^#[0-9a-f]{6}$/.test(favorite.lineColor) |
| 167 | 176 | || !/^#[0-9a-f]{6}$/.test(favorite.lineTextColor)))) throw new TypeError("Invalid appearance colors"); |
| @@ -169,13 +178,13 @@ function prepareAppearance(favorite, profile, language) { | ||
| 169 | 178 | profile ? 67 : 84, profile ? 54 : 71, profile ? 116 : 132, profile ? 86 : 74, profile ? 73 : 61, profile ? 106 : 122]; |
| 170 | 179 | let ends = "", mask = 0; |
| 171 | 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 | 182 | const fit = clip(label, slot === 3 || slot >= 9 ? 0 : 1, widths[slot]); |
| 174 | 183 | ends += fixed(scalarLength(label.slice(0, fit.end)), 2); |
| 175 | 184 | if (fit.ellipsis) mask |= 1 << slot; |
| 176 | 185 | } |
| 177 | 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 | 188 | + (hasBackground ? favorite.lineColor.slice(1) : "52616f") |
| 180 | 189 | + (hasForeground ? favorite.lineTextColor.slice(1) : "ffffff") + ends + fixed(mask, 3); |
| 181 | 190 | 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 @@ | ||
| 146 | 146 | </div> |
| 147 | 147 | |
| 148 | 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 | 150 | <h3>Publisher and contact</h3> |
| 151 | 151 | <p><strong>Author and publisher:</strong> Anthodev, a private individual acting on a non-professional basis.</p> |
| 152 | 152 | <p><strong>Publication director:</strong> Anthodev.</p> |
| @@ -168,7 +168,7 @@ | ||
| 168 | 168 | </div> |
| 169 | 169 | |
| 170 | 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 | 172 | <h3>Édition et contact</h3> |
| 173 | 173 | <p><strong>Auteur et éditeur :</strong> Anthodev, personne physique agissant à titre privé et non professionnel.</p> |
| 174 | 174 | <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 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "name": "@lapin-fute/config-page", |
| 3 | - "version": "1.0.1", | |
| 3 | + "version": "1.0.2", | |
| 4 | 4 | "license": "MIT", |
| 5 | 5 | "private": true, |
| 6 | 6 | "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 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "name": "lapin-fute-watch", |
| 3 | 3 | "author": "Anthodev", |
| 4 | - "version": "1.0.1", | |
| 4 | + "version": "1.0.2", | |
| 5 | 5 | "license": "MIT", |
| 6 | 6 | "keywords": [ |
| 7 | 7 | "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 { | ||
| 7 | 7 | trafficError, pages, stale |
| 8 | 8 | } = await watchModule("packed"); |
| 9 | 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 | 13 | test("appearance records round trip and validate", () => { |
| 12 | 14 | const record = appearance({ id: "home:rer:a" }); |
| @@ -61,6 +63,63 @@ test("clipped honours scalar endpoints and the ellipsis mask", () => { | ||
| 61 | 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 | 123 | test("departure records validate bounds and pairing", () => { |
| 65 | 124 | const good = departure({}); |
| 66 | 125 | assert(departureValid(good, 1)); |
| @@ -7,6 +7,8 @@ const { | |||
| 7 | trafficError, pages, stale | 7 | 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)); |