@7365admin1/layer-common 4.0.3-staging.233 → 4.0.3-staging.235

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,437 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { test } from "node:test";
3
-
4
- import {
5
- promoExpiryDate,
6
- promoCodeFormValues,
7
- promoCodeUpdatePayload,
8
- validatePromoCodeEdit,
9
- promoCodeApiError,
10
- } from "./promo-code-form.ts";
11
-
12
- /**
13
- * THE PROMO CODE EDITOR AND THE THREE WRITE CALLS BEHIND IT.
14
- *
15
- * Two halves, both pinned here:
16
- *
17
- * 1. the rules the edit form applies before it sends anything - taken from
18
- * the MERGED backend (`promoCodeUpdateSchema` in `@7365admin1/core`), not
19
- * from a guess about it;
20
- * 2. what `usePromoCode()` actually puts on the wire. The composable is three
21
- * `$api` calls, and the thing that can silently be wrong about it is the
22
- * verb, the path or the body - a `PUT` to the wrong path, or a body that
23
- * carries `code` and so looks as though it renamed a code it cannot.
24
- *
25
- * The composable reaches for `useNuxtApp()`, which only exists inside a Nuxt
26
- * app, so one stub stands in for it and records the call.
27
- */
28
-
29
- const NOW = new Date(2026, 7, 20, 12, 0, 0); // 20 Aug 2026, midday
30
-
31
- // ---------------------------------------------------------------------------
32
- // The expiry rule
33
- // ---------------------------------------------------------------------------
34
-
35
- test("an expiry date is read as the END of its day, in both stored formats", () => {
36
- // A code dated today is good for all of today, so a check at midday passes.
37
- const usFormat = promoExpiryDate("08/20/2026");
38
- assert.ok(usFormat);
39
- assert.equal(usFormat!.getHours(), 23);
40
- assert.ok(usFormat!.getTime() > NOW.getTime());
41
-
42
- const iso = promoExpiryDate("2026-08-20");
43
- assert.ok(iso);
44
- assert.equal(iso!.getTime(), usFormat!.getTime());
45
- });
46
-
47
- test("a date that is not a date is no expiry at all, never an Invalid Date", () => {
48
- assert.equal(promoExpiryDate(""), null);
49
- assert.equal(promoExpiryDate(null), null);
50
- assert.equal(promoExpiryDate("whenever"), null);
51
- // Month 13 rolls into the next year in JavaScript rather than failing, so
52
- // it is caught explicitly - otherwise 13/01/2026 would read as Jan 2027.
53
- assert.equal(promoExpiryDate("13/01/2026"), null);
54
- });
55
-
56
- // ---------------------------------------------------------------------------
57
- // Loading a record into the form, and building the body back out
58
- // ---------------------------------------------------------------------------
59
-
60
- test("the form loads from the record and does NOT carry the code", () => {
61
- const values = promoCodeFormValues({
62
- _id: "652f1a2b3c4d5e6f70819293",
63
- code: "WELCOME2026",
64
- description: "Launch offer",
65
- type: "fixed",
66
- fixed_rate: 12,
67
- expiresAt: "12/31/2026",
68
- status: "active",
69
- });
70
-
71
- assert.equal((values as any).code, undefined);
72
- assert.deepEqual(values, {
73
- description: "Launch offer",
74
- type: "fixed",
75
- fixed_rate: 12,
76
- expiresAt: "12/31/2026",
77
- tiers: [],
78
- });
79
- });
80
-
81
- test("a tiered record keeps its bands; a fixed one is given none", () => {
82
- const tiered = promoCodeFormValues({
83
- type: "tiered",
84
- tiers: [{ min: 1, max: 10, price: 9 }, { min: 11, max: 0, price: 7 }],
85
- });
86
- assert.equal(tiered.tiers.length, 2);
87
- assert.deepEqual(tiered.tiers[1], { min: 11, max: 0, price: 7 });
88
-
89
- // A code stored as fixed but carrying leftover bands must not send them:
90
- // `promoCodeUpdateSchema` FORBIDS `tiers` on a fixed code and would 400.
91
- const fixed = promoCodeFormValues({
92
- type: "fixed",
93
- fixed_rate: 5,
94
- tiers: [{ min: 1, max: 10, price: 9 }],
95
- });
96
- assert.deepEqual(fixed.tiers, []);
97
- });
98
-
99
- test("an empty record gives a usable, valid, fixed-price form", () => {
100
- const values = promoCodeFormValues(null);
101
- assert.deepEqual(values, {
102
- description: "",
103
- type: "fixed",
104
- fixed_rate: 0,
105
- expiresAt: "",
106
- tiers: [],
107
- });
108
- assert.equal(validatePromoCodeEdit(promoCodeUpdatePayload(values), NOW), null);
109
- });
110
-
111
- test("the payload never contains code, _id or status", () => {
112
- const payload = promoCodeUpdatePayload({
113
- description: "Launch offer",
114
- type: "fixed",
115
- fixed_rate: 12,
116
- expiresAt: "12/31/2026",
117
- tiers: [],
118
- });
119
-
120
- assert.deepEqual(Object.keys(payload).sort(), [
121
- "description",
122
- "expiresAt",
123
- "fixed_rate",
124
- "type",
125
- ]);
126
- });
127
-
128
- test("a tiered payload sends bands and no fixed_rate, and the reverse", () => {
129
- const tiered = promoCodeUpdatePayload({
130
- description: "",
131
- type: "tiered",
132
- fixed_rate: 99,
133
- expiresAt: "",
134
- tiers: [{ min: 1, max: 10, price: 9 }],
135
- });
136
- assert.deepEqual(tiered.tiers, [{ min: 1, max: 10, price: 9 }]);
137
- assert.equal("fixed_rate" in tiered, false);
138
-
139
- const fixed = promoCodeUpdatePayload({
140
- description: "",
141
- type: "fixed",
142
- fixed_rate: 9,
143
- expiresAt: "",
144
- tiers: [{ min: 1, max: 10, price: 9 }],
145
- });
146
- assert.equal("tiers" in fixed, false);
147
- assert.equal(fixed.fixed_rate, 9);
148
- });
149
-
150
- test("a price nobody has typed is NOT quietly saved as free", () => {
151
- // `Number("")` is 0. Coercing a blank field would turn "not filled in" into
152
- // "every seat is free" - a real price, saved without anyone choosing it.
153
- const payload = promoCodeUpdatePayload({
154
- description: "",
155
- type: "fixed",
156
- fixed_rate: "" as any,
157
- expiresAt: "",
158
- tiers: [],
159
- });
160
- assert.equal(payload.fixed_rate, "");
161
- assert.match(
162
- String(validatePromoCodeEdit(payload, NOW)),
163
- /Enter a price per seat/
164
- );
165
-
166
- // A deliberate 0, however, is a real answer and goes through.
167
- const free = promoCodeUpdatePayload({
168
- description: "",
169
- type: "fixed",
170
- fixed_rate: 0,
171
- expiresAt: "",
172
- tiers: [],
173
- });
174
- assert.equal(free.fixed_rate, 0);
175
- assert.equal(validatePromoCodeEdit(free, NOW), null);
176
- });
177
-
178
- test("band numbers typed as text reach the API as numbers", () => {
179
- const payload = promoCodeUpdatePayload({
180
- description: "",
181
- type: "tiered",
182
- fixed_rate: 0,
183
- expiresAt: "",
184
- tiers: [{ min: "1", max: "10", price: "9.5" } as any],
185
- });
186
- assert.deepEqual(payload.tiers, [{ min: 1, max: 10, price: 9.5 }]);
187
- });
188
-
189
- // ---------------------------------------------------------------------------
190
- // What the form refuses, in the API's terms
191
- // ---------------------------------------------------------------------------
192
-
193
- test("a valid edit is refused nothing", () => {
194
- assert.equal(
195
- validatePromoCodeEdit(
196
- { type: "fixed", fixed_rate: 0, expiresAt: "", description: "" },
197
- NOW
198
- ),
199
- null
200
- );
201
- assert.equal(
202
- validatePromoCodeEdit(
203
- {
204
- type: "tiered",
205
- tiers: [{ min: 1, max: 10, price: 9 }, { min: 11, max: 0, price: 7 }],
206
- expiresAt: "12/31/2026",
207
- },
208
- NOW
209
- ),
210
- null
211
- );
212
- });
213
-
214
- test("an expiry already in the past is refused, and says what to do", () => {
215
- const message = validatePromoCodeEdit(
216
- { type: "fixed", fixed_rate: 5, expiresAt: "01/01/2020" },
217
- NOW
218
- );
219
- assert.match(String(message), /already passed/);
220
- assert.match(String(message), /clear the date/);
221
- });
222
-
223
- test("TODAY is not in the past - a code expiring today is still savable", () => {
224
- assert.equal(
225
- validatePromoCodeEdit(
226
- { type: "fixed", fixed_rate: 5, expiresAt: "08/20/2026" },
227
- NOW
228
- ),
229
- null
230
- );
231
- });
232
-
233
- test("no expiry at all is allowed - that is a code with no end date", () => {
234
- assert.equal(
235
- validatePromoCodeEdit({ type: "fixed", fixed_rate: 5, expiresAt: "" }, NOW),
236
- null
237
- );
238
- });
239
-
240
- test("an unreadable expiry names the format instead of guessing", () => {
241
- assert.match(
242
- String(
243
- validatePromoCodeEdit(
244
- { type: "fixed", fixed_rate: 5, expiresAt: "next Tuesday" },
245
- NOW
246
- )
247
- ),
248
- /MM\/DD\/YYYY/
249
- );
250
- });
251
-
252
- test("a free BAND is refused, because Joi's positive() refuses it", () => {
253
- const message = validatePromoCodeEdit(
254
- { type: "tiered", tiers: [{ min: 1, max: 10, price: 0 }] },
255
- NOW
256
- );
257
- assert.match(String(message), /^Band 1: /);
258
- assert.match(String(message), /above 0/);
259
- // ...and it says the way to actually make seats free, which is the other
260
- // pricing shape. `fixed_rate` is `min(0)`, not `positive()`.
261
- assert.equal(
262
- validatePromoCodeEdit({ type: "fixed", fixed_rate: 0 }, NOW),
263
- null
264
- );
265
- });
266
-
267
- test("the refused band is named, not just 'a band'", () => {
268
- assert.match(
269
- String(
270
- validatePromoCodeEdit(
271
- {
272
- type: "tiered",
273
- tiers: [
274
- { min: 1, max: 10, price: 9 },
275
- { min: 11, max: 5, price: 7 },
276
- ],
277
- },
278
- NOW
279
- )
280
- ),
281
- /^Band 2: /
282
- );
283
- });
284
-
285
- test("a band with no upper limit is allowed - 0 means 'and above'", () => {
286
- assert.equal(
287
- validatePromoCodeEdit(
288
- { type: "tiered", tiers: [{ min: 5, max: 0, price: 7 }] },
289
- NOW
290
- ),
291
- null
292
- );
293
- });
294
-
295
- test("a tiered code with no bands is refused, and offers the other shape", () => {
296
- assert.match(
297
- String(validatePromoCodeEdit({ type: "tiered", tiers: [] }, NOW)),
298
- /at least one quantity band/
299
- );
300
- });
301
-
302
- test("a negative fixed price is refused; a missing one is refused too", () => {
303
- assert.match(
304
- String(validatePromoCodeEdit({ type: "fixed", fixed_rate: -1 }, NOW)),
305
- /cannot be less than 0/
306
- );
307
- assert.match(
308
- String(validatePromoCodeEdit({ type: "fixed", fixed_rate: "" }, NOW)),
309
- /Enter a price per seat/
310
- );
311
- });
312
-
313
- test("a code with no pricing shape is refused first, before anything else", () => {
314
- assert.equal(
315
- validatePromoCodeEdit({ expiresAt: "01/01/2020" }, NOW),
316
- "Choose how this code prices seats."
317
- );
318
- });
319
-
320
- // ---------------------------------------------------------------------------
321
- // What a refusal from the server reads like
322
- // ---------------------------------------------------------------------------
323
-
324
- test("the API's own wording is what the screen shows", () => {
325
- assert.equal(
326
- promoCodeApiError(
327
- { response: { _data: { message: "Promo code not found." } } },
328
- "Could not save."
329
- ),
330
- "Promo code not found."
331
- );
332
- });
333
-
334
- test("'Not authorized.' is replaced with something a person can act on", () => {
335
- const message = promoCodeApiError(
336
- { data: { message: "Not authorized." } },
337
- "Could not save."
338
- );
339
- assert.match(message, /Seven365 staff account/);
340
- assert.match(message, /Ask a Seven365 administrator/);
341
- });
342
-
343
- test("an unreadable failure falls back rather than dumping the error", () => {
344
- assert.equal(
345
- promoCodeApiError(new Error("fetch failed"), "Could not save the changes."),
346
- "Could not save the changes."
347
- );
348
- });
349
-
350
- // ---------------------------------------------------------------------------
351
- // The composable: verb, path and body of every write
352
- // ---------------------------------------------------------------------------
353
-
354
- const calls: Array<{ path: string; options: any }> = [];
355
-
356
- (globalThis as any).useNuxtApp = () => ({
357
- $api: (path: string, options: any = {}) => {
358
- calls.push({ path, options });
359
- return Promise.resolve({ message: "ok" });
360
- },
361
- });
362
-
363
- const promoCodeApi = (await import("../composables/usePromoCode.ts")).default();
364
-
365
- function lastCall() {
366
- return calls[calls.length - 1];
367
- }
368
-
369
- const ID = "652f1a2b3c4d5e6f70819293";
370
-
371
- test("getById reads the one record, from the path the API mounts", async () => {
372
- await promoCodeApi.getById(ID);
373
- assert.equal(lastCall().path, `/api/promo-codes/id/${ID}`);
374
- assert.equal(lastCall().options.method, "GET");
375
- });
376
-
377
- test("update is a PUT to /api/promo-codes/:id", async () => {
378
- await promoCodeApi.update(ID, {
379
- description: "Launch offer",
380
- type: "fixed",
381
- fixed_rate: 12,
382
- expiresAt: "12/31/2026",
383
- } as any);
384
-
385
- assert.equal(lastCall().path, `/api/promo-codes/${ID}`);
386
- assert.equal(lastCall().options.method, "PUT");
387
- assert.equal(lastCall().options.body.description, "Launch offer");
388
- });
389
-
390
- test("update NEVER sends code, however it is called", async () => {
391
- // The screen loads a record and could easily send it back whole. `code` is
392
- // immutable on the server; sending it would be accepted and dropped, and
393
- // the person would watch a rename succeed and change nothing.
394
- await promoCodeApi.update(ID, {
395
- code: "RENAMED",
396
- type: "fixed",
397
- fixed_rate: 1,
398
- } as any);
399
-
400
- assert.equal("code" in lastCall().options.body, false);
401
- assert.equal(lastCall().options.body.type, "fixed");
402
- });
403
-
404
- test("updateStatus is a PATCH carrying only the new status", async () => {
405
- await promoCodeApi.updateStatus(ID, "disabled");
406
- assert.equal(lastCall().path, `/api/promo-codes/${ID}/status`);
407
- assert.equal(lastCall().options.method, "PATCH");
408
- assert.deepEqual(lastCall().options.body, { status: "disabled" });
409
-
410
- await promoCodeApi.updateStatus(ID, "active");
411
- assert.deepEqual(lastCall().options.body, { status: "active" });
412
- });
413
-
414
- test("remove is a DELETE, with no body to get wrong", async () => {
415
- await promoCodeApi.remove(ID);
416
- assert.equal(lastCall().path, `/api/promo-codes/${ID}`);
417
- assert.equal(lastCall().options.method, "DELETE");
418
- assert.equal(lastCall().options.body, undefined);
419
- });
420
-
421
- test("the four reads and writes are all on the composable", () => {
422
- for (const name of [
423
- "add",
424
- "getPromoCodes",
425
- "getByCode",
426
- "getById",
427
- "update",
428
- "updateStatus",
429
- "remove",
430
- ]) {
431
- assert.equal(
432
- typeof (promoCodeApi as any)[name],
433
- "function",
434
- `usePromoCode() is missing ${name}`
435
- );
436
- }
437
- });
@@ -1,69 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { readFileSync } from "node:fs";
3
- import { test } from "node:test";
4
-
5
- import { showSiteField } from "./role.ts";
6
-
7
- /**
8
- * QA on the admin app (PR #18) reported "no site options" on Create Role. The
9
- * Site field is read-only by design - it shows the site a role belongs to -
10
- * but an admin role has no site, so it drew permanently disabled and empty.
11
- * `RolePermissionFormCreate` defaulted `siteState` to `true` and
12
- * `RolePermissionMain` neither declared nor forwarded it, and its root is a
13
- * `<div>`, so a fallthrough attribute could not reach the dialog either: no
14
- * consuming app could switch the field off.
15
- */
16
- test("an admin role never shows the site field", () => {
17
- assert.equal(showSiteField(true, "admin"), false);
18
- assert.equal(showSiteField(false, "admin"), false);
19
- });
20
-
21
- test("every other role type keeps what the app asked for", () => {
22
- for (const type of [
23
- "app",
24
- "organization",
25
- "security_agency",
26
- "cleaning_services",
27
- ]) {
28
- assert.equal(showSiteField(true, type), true, type);
29
- assert.equal(showSiteField(false, type), false, type);
30
- }
31
- });
32
-
33
- test("RolePermissionMain declares site-state and forwards it to the dialog", () => {
34
- const src = readFileSync(
35
- new URL("../components/RolePermissionMain.vue", import.meta.url),
36
- "utf8"
37
- );
38
-
39
- assert.match(src, /siteState:\s*\{/, "site-state must be a declared prop");
40
- assert.match(
41
- src,
42
- /:site-state="siteFieldShown"/,
43
- "the dialog must receive the resolved value, not a fallthrough attribute"
44
- );
45
- });
46
-
47
- /**
48
- * The category expand toggle carried no class, so it fell back to raw Vuetify
49
- * `inset` styling: a white thumb on a white card, measured 1.02:1 light /
50
- * 1.04:1 dark against the dialog surface. `.app-switch` (primitives.css) is
51
- * this layer's switch shape and is what makes the thumb visible. `inset` is
52
- * removed with it - Vuetify's inset dimensions beat the `.app-switch` rules on
53
- * source order and collapse the control to zero width.
54
- */
55
- for (const file of [
56
- "RolePermissionFormCreate.vue",
57
- "RolePermissionFormPreviewUpdate.vue",
58
- ]) {
59
- test(`${file} styles its category toggle with .app-switch`, () => {
60
- const src = readFileSync(
61
- new URL(`../components/${file}`, import.meta.url),
62
- "utf8"
63
- );
64
- const sw = src.slice(src.indexOf("<v-switch"), src.indexOf("/>", src.indexOf("<v-switch")));
65
-
66
- assert.ok(sw.includes('class="app-switch"'), "missing .app-switch");
67
- assert.ok(!/\binset\b/.test(sw), "inset fights the .app-switch dimensions");
68
- });
69
- }
@@ -1,85 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { readFileSync, readdirSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { test } from "node:test";
6
-
7
- import { pickRouteName } from "./route-name.ts";
8
-
9
- const routerWith = (...names: string[]) => ({
10
- hasRoute: (name: string) => names.includes(name),
11
- });
12
-
13
- test("picks the first candidate this app actually has", () => {
14
- const router = routerWith("org-site-keys-visitor-pass-add");
15
- assert.equal(
16
- pickRouteName(router, "org-site-keys-visitor-pass-add", "keys-visitor-pass-add"),
17
- "org-site-keys-visitor-pass-add"
18
- );
19
- });
20
-
21
- test("falls through to a later candidate for a flatter app", () => {
22
- const router = routerWith("keys-visitor-pass-add");
23
- assert.equal(
24
- pickRouteName(router, "org-site-keys-visitor-pass-add", "keys-visitor-pass-add"),
25
- "keys-visitor-pass-add"
26
- );
27
- });
28
-
29
- test("candidate order wins when an app has both", () => {
30
- const router = routerWith("keys-visitor-pass-add", "org-site-keys-visitor-pass-add");
31
- assert.equal(
32
- pickRouteName(router, "org-site-keys-visitor-pass-add", "keys-visitor-pass-add"),
33
- "org-site-keys-visitor-pass-add"
34
- );
35
- });
36
-
37
- test("null when the app has no such page - the caller hides the control", () => {
38
- assert.equal(pickRouteName(routerWith("org-site-work-orders"), "org-site-work-orders-id"), null);
39
- assert.equal(pickRouteName(routerWith()), null);
40
- });
41
-
42
- // The five names two app-side audits found dead. Each is now resolved through
43
- // `pickRouteName`, so none may reappear as a literal anywhere in the layer.
44
- // `organizations-create` is deliberately absent: it is real in web-app-org and
45
- // is only reached there, so it was never dead.
46
- const DEAD_NAMES = [
47
- "keys-visitor-pass-add",
48
- "org-organizations-customers-add",
49
- "work-order-details",
50
- "org-site-service-provider-mgmt-billing",
51
- ];
52
-
53
- function sourceFiles(dir: string, out: string[] = []): string[] {
54
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
55
- const path = join(dir, entry.name);
56
- if (entry.isDirectory()) sourceFiles(path, out);
57
- else if (/\.(vue|ts)$/.test(entry.name)) out.push(path);
58
- }
59
- return out;
60
- }
61
-
62
- test("no dead route name survives as a literal in the layer", () => {
63
- const root = fileURLToPath(new URL("..", import.meta.url));
64
- const files = ["components", "pages", "layouts", "composables", "middleware", "plugins"]
65
- .flatMap((d) => sourceFiles(join(root, d)))
66
- .filter((f) => !f.endsWith(".test.ts"));
67
-
68
- assert.ok(files.length > 100, `expected to scan the layer, scanned ${files.length} files`);
69
-
70
- const offenders: string[] = [];
71
- for (const file of files) {
72
- const source = readFileSync(file, "utf8");
73
- for (const dead of DEAD_NAMES) {
74
- // Only a `name:` BINDING counts. The same string also appears as a bare
75
- // argument in a `pickRouteName(...)` candidate list, which is the fix, not
76
- // the fault. Plain string matching on purpose - a backslash class inside a
77
- // template literal silently loses its escape and matches nothing.
78
- const bound = [`"${dead}"`, `'${dead}'`].some(
79
- (quoted) => source.includes(`name: ${quoted}`) || source.includes(`name:${quoted}`)
80
- );
81
- if (bound) offenders.push(`${file.slice(root.length)} -> ${dead}`);
82
- }
83
- }
84
- assert.deepEqual(offenders, [], `dead route names still bound:\n${offenders.join("\n")}`);
85
- });
@@ -1,110 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { test } from "node:test";
3
-
4
- import { statusTone, statusToneClass } from "./status.ts";
5
-
6
- test("the handoff's mapping, word for word", () => {
7
- for (const s of ["Completed", "Paid", "Available", "Open"]) {
8
- assert.equal(statusTone(s), "ok", s);
9
- }
10
-
11
- assert.equal(statusTone("Pending"), "warn");
12
-
13
- for (const s of ["In Use", "Closed", "Checkout"]) {
14
- assert.equal(statusTone(s), "err", s);
15
- }
16
-
17
- assert.equal(statusTone("Role"), "info");
18
- });
19
-
20
- /** The same status arrives spelled three ways from three endpoints. */
21
- test("casing, spacing and underscores do not change a status's colour", () => {
22
- for (const s of ["in use", "IN USE", "In_Use", "in-use", " In Use "]) {
23
- assert.equal(statusTone(s), "err", s);
24
- }
25
- });
26
-
27
- /**
28
- * The important one. A status the design never named must NOT be guessed into
29
- * a colour - a wrong green on a failed job reads as success. Neutral is
30
- * visible, honest, and gets it noticed.
31
- */
32
- test("an unmapped status comes out neutral rather than guessed", () => {
33
- for (const s of ["Cancelled", "Overdue", "Draft", "", null, undefined]) {
34
- assert.equal(statusTone(s), "neutral", String(s));
35
- }
36
- });
37
-
38
- /**
39
- * PHASE 4. Each of these words is on a module screen today with a colour a
40
- * per-screen `getStatusColor` gave it. The list has to keep giving it the SAME
41
- * tone, or adopting the shared chip would quietly repaint a status.
42
- */
43
- test("the product's own status words keep the tone the screens already gave them", () => {
44
- for (const s of ["Accepted", "Approved", "Resolved", "Active"]) {
45
- assert.equal(statusTone(s), "ok", s);
46
- }
47
-
48
- for (const s of ["Rejected", "Deleted", "Suspended"]) {
49
- assert.equal(statusTone(s), "err", s);
50
- }
51
-
52
- for (const s of ["In Progress", "In-Progress", "To-Do", "Awaiting Approval", "Replaced", "Returned"]) {
53
- assert.equal(statusTone(s), "warn", s);
54
- }
55
-
56
- for (const s of ["Ongoing", "For Review", "Assigned"]) {
57
- assert.equal(statusTone(s), "info", s);
58
- }
59
-
60
- for (const s of ["Inactive", "Expired"]) {
61
- assert.equal(statusTone(s), "neutral", s);
62
- }
63
- });
64
-
65
- /**
66
- * HID enrolment. These two are the whole point of the Status column on
67
- * `HidUserEnrollment` - if they were left unlisted they would BOTH be neutral
68
- * and the column would stop distinguishing anything, so they are pinned.
69
- */
70
- test("Mapped and Unmapped keep the tones the enrolment screen already gave them", () => {
71
- assert.equal(statusTone("Mapped"), "ok");
72
- assert.equal(statusTone("Unmapped"), "warn");
73
- });
74
-
75
- /**
76
- * HID intercom SIP state. "Disabled" and "Not connected" must stay NEUTRAL -
77
- * painting an intentionally-off intercom red would read as a fault.
78
- */
79
- /**
80
- * HID access authorization, landing on `staging` in b0667a7. Pinned before that
81
- * merge so the words cannot arrive and quietly render neutral - the difference
82
- * between a granted and a denied door is the whole point of the column.
83
- */
84
- test("HID authorization states are known to the shared map before b0667a7 merges", () => {
85
- assert.equal(statusTone("Authorized"), "ok");
86
- assert.equal(statusTone("Not Authorized"), "err");
87
- assert.equal(statusTone("Unknown"), "warn");
88
- });
89
-
90
- test("intercom SIP states keep the tones the intercom screen already gave them", () => {
91
- assert.equal(statusTone("Connected"), "ok");
92
- assert.equal(statusTone("Connecting"), "warn");
93
- assert.equal(statusTone("Not connected"), "neutral");
94
- assert.equal(statusTone("Disabled"), "neutral");
95
- assert.equal(statusTone("Failed"), "err");
96
- });
97
-
98
- /**
99
- * The one place the handoff and the old per-screen colour disagree: `Open` was
100
- * grey on the cleaning schedules and the design names it `ok`. Pinned so the
101
- * disagreement is a decision on the record rather than a surprise.
102
- */
103
- test("Open follows the design, not the old grey", () => {
104
- assert.equal(statusTone("open"), "ok");
105
- });
106
-
107
- test("the class name matches what tokens.css actually defines", () => {
108
- assert.equal(statusToneClass("Paid"), "tone-ok");
109
- assert.equal(statusToneClass("Whatever"), "tone-neutral");
110
- });