@7365admin1/layer-common 4.0.3-staging.233 → 4.0.3-staging.234
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.
- package/package.json +3 -2
- package/utils/breadcrumb.test.ts +0 -108
- package/utils/camera-wall.test.ts +0 -634
- package/utils/client-nature.test.ts +0 -77
- package/utils/client-subscription.test.ts +0 -240
- package/utils/console-tier.test.ts +0 -87
- package/utils/dashboard.test.ts +0 -118
- package/utils/data.test.ts +0 -179
- package/utils/hid-discover.test.ts +0 -115
- package/utils/list-page.test.ts +0 -60
- package/utils/nav-sections.test.ts +0 -190
- package/utils/nfc-patrol-report.test.ts +0 -130
- package/utils/promo-code-form.test.ts +0 -437
- package/utils/role.test.ts +0 -69
- package/utils/route-name.test.ts +0 -85
- package/utils/status.test.ts +0 -110
- package/utils/subscription-form.test.ts +0 -284
- package/utils/table-card.test.ts +0 -84
- package/utils/theme.test.ts +0 -679
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
BILLING_COMPLIMENTARY,
|
|
6
|
-
BILLING_PAID,
|
|
7
|
-
buildConsolePayload,
|
|
8
|
-
formValuesFrom,
|
|
9
|
-
formatMoney,
|
|
10
|
-
monthlyValue,
|
|
11
|
-
planOptionLabel,
|
|
12
|
-
readApiError,
|
|
13
|
-
todayISO,
|
|
14
|
-
validateConsoleSubscription,
|
|
15
|
-
} from "./subscription-form.ts";
|
|
16
|
-
|
|
17
|
-
const PLANS = [
|
|
18
|
-
{ _id: "aaaaaaaaaaaaaaaaaaaaaaaa", name: "Standard Plan", price: 25, status: "active" },
|
|
19
|
-
{ _id: "bbbbbbbbbbbbbbbbbbbbbbbb", name: "Retired Plan", price: 40, status: "deactive" },
|
|
20
|
-
{ _id: "cccccccccccccccccccccccc", name: "Free Bundle", price: null, status: "active" },
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
const STANDARD = PLANS[0]._id;
|
|
24
|
-
|
|
25
|
-
function form(overrides: Record<string, any> = {}) {
|
|
26
|
-
return {
|
|
27
|
-
plan: STANDARD,
|
|
28
|
-
billingMode: BILLING_COMPLIMENTARY,
|
|
29
|
-
startDate: "2026-09-01",
|
|
30
|
-
endDate: "2027-08-31",
|
|
31
|
-
...overrides,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/* ── WHAT A CLIENT IS WORTH PER MONTH ── */
|
|
36
|
-
|
|
37
|
-
test("a plan is priced per site, per month", () => {
|
|
38
|
-
assert.equal(monthlyValue(PLANS[0], 4), 100);
|
|
39
|
-
assert.equal(monthlyValue(PLANS[0], 1), 25);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("no sites is worth nothing, and is not an error", () => {
|
|
43
|
-
assert.equal(monthlyValue(PLANS[0], 0), 0);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("a plan with no price is worth nothing rather than NaN", () => {
|
|
47
|
-
assert.equal(monthlyValue(PLANS[2], 7), 0);
|
|
48
|
-
assert.equal(monthlyValue(undefined, 7), 0);
|
|
49
|
-
assert.equal(monthlyValue({ price: "not a number" } as any, 7), 0);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("the monthly figure reads as Singapore money", () => {
|
|
53
|
-
assert.equal(formatMoney(100), "S$100.00");
|
|
54
|
-
assert.equal(formatMoney(0), "S$0.00");
|
|
55
|
-
assert.equal(formatMoney(NaN), "S$0.00");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("a plan is offered with its per-site price beside its name", () => {
|
|
59
|
-
assert.equal(
|
|
60
|
-
planOptionLabel(PLANS[0]),
|
|
61
|
-
"Standard Plan - S$25.00 per site, per month",
|
|
62
|
-
);
|
|
63
|
-
assert.equal(planOptionLabel(PLANS[2]), "Free Bundle - no per-site price set");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
/* ── WHAT GETS SENT ── */
|
|
67
|
-
|
|
68
|
-
test("a complimentary subscription is sent with no price at all", () => {
|
|
69
|
-
const payload = buildConsolePayload(form(), PLANS, 4);
|
|
70
|
-
assert.equal(payload.amount, 0);
|
|
71
|
-
assert.equal(payload.billingMode, BILLING_COMPLIMENTARY);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("a paying subscription is sent the plan price times the site count", () => {
|
|
75
|
-
const payload = buildConsolePayload(form({ billingMode: BILLING_PAID }), PLANS, 4);
|
|
76
|
-
assert.equal(payload.amount, 100);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("an empty start date is sent as today, the same default the API uses", () => {
|
|
80
|
-
const payload = buildConsolePayload(form({ startDate: "" }), PLANS, 1);
|
|
81
|
-
assert.equal(payload.startDate, todayISO());
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("the payload carries nothing but the five fields the API accepts", () => {
|
|
85
|
-
const payload = buildConsolePayload(form({ billingMode: BILLING_PAID }), PLANS, 2);
|
|
86
|
-
assert.deepEqual(Object.keys(payload).sort(), [
|
|
87
|
-
"amount",
|
|
88
|
-
"billingMode",
|
|
89
|
-
"endDate",
|
|
90
|
-
"plan",
|
|
91
|
-
"startDate",
|
|
92
|
-
]);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("no status is ever sent - suspend and reactivate are Phase 3", () => {
|
|
96
|
-
const payload: Record<string, any> = buildConsolePayload(form(), PLANS, 2);
|
|
97
|
-
assert.equal(payload.status, undefined);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("nothing about a card, an invoice or a gateway is ever sent", () => {
|
|
101
|
-
const sent = JSON.stringify(buildConsolePayload(form({ billingMode: BILLING_PAID }), PLANS, 3));
|
|
102
|
-
for (const forbidden of ["card", "cvv", "invoice", "reddot", "payment_method"]) {
|
|
103
|
-
assert.equal(sent.toLowerCase().includes(forbidden), false, forbidden);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
/* ── WHAT IS REFUSED, BEFORE THE REQUEST GOES OUT ── */
|
|
108
|
-
|
|
109
|
-
test("a form with everything answered is not refused", () => {
|
|
110
|
-
assert.equal(
|
|
111
|
-
validateConsoleSubscription(buildConsolePayload(form(), PLANS, 4), PLANS),
|
|
112
|
-
null,
|
|
113
|
-
);
|
|
114
|
-
assert.equal(
|
|
115
|
-
validateConsoleSubscription(
|
|
116
|
-
buildConsolePayload(form({ billingMode: BILLING_PAID }), PLANS, 4),
|
|
117
|
-
PLANS,
|
|
118
|
-
),
|
|
119
|
-
null,
|
|
120
|
-
);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("no plan chosen is refused", () => {
|
|
124
|
-
assert.equal(
|
|
125
|
-
validateConsoleSubscription(buildConsolePayload(form({ plan: "" }), PLANS, 1), PLANS),
|
|
126
|
-
"Choose a plan.",
|
|
127
|
-
);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("a plan that does not exist is refused", () => {
|
|
131
|
-
assert.equal(
|
|
132
|
-
validateConsoleSubscription(
|
|
133
|
-
buildConsolePayload(form({ plan: "dddddddddddddddddddddddd" }), PLANS, 1),
|
|
134
|
-
PLANS,
|
|
135
|
-
),
|
|
136
|
-
"That plan could not be found. Pick one from the list.",
|
|
137
|
-
);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("a plan that is no longer active is refused, and named", () => {
|
|
141
|
-
assert.equal(
|
|
142
|
-
validateConsoleSubscription(
|
|
143
|
-
buildConsolePayload(form({ plan: PLANS[1]._id }), PLANS, 1),
|
|
144
|
-
PLANS,
|
|
145
|
-
),
|
|
146
|
-
"The plan Retired Plan is no longer active, so it cannot be given to a client. Pick an active plan.",
|
|
147
|
-
);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test("an end date before the start date is refused", () => {
|
|
151
|
-
assert.equal(
|
|
152
|
-
validateConsoleSubscription(
|
|
153
|
-
buildConsolePayload(form({ startDate: "2026-09-01", endDate: "2026-08-31" }), PLANS, 1),
|
|
154
|
-
PLANS,
|
|
155
|
-
),
|
|
156
|
-
"The end date must be after the start date.",
|
|
157
|
-
);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("an end date equal to the start date is refused too - the API refuses it", () => {
|
|
161
|
-
assert.equal(
|
|
162
|
-
validateConsoleSubscription(
|
|
163
|
-
buildConsolePayload(form({ startDate: "2026-09-01", endDate: "2026-09-01" }), PLANS, 1),
|
|
164
|
-
PLANS,
|
|
165
|
-
),
|
|
166
|
-
"The end date must be after the start date.",
|
|
167
|
-
);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test("a missing end date is refused - every client has one, free or not", () => {
|
|
171
|
-
assert.equal(
|
|
172
|
-
validateConsoleSubscription(buildConsolePayload(form({ endDate: "" }), PLANS, 1), PLANS),
|
|
173
|
-
"Enter an end date. Every client has one, free or not.",
|
|
174
|
-
);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("an unreadable date is refused as a date, not as something else", () => {
|
|
178
|
-
assert.equal(
|
|
179
|
-
validateConsoleSubscription(
|
|
180
|
-
buildConsolePayload(form({ endDate: "31/08/2027" }), PLANS, 1),
|
|
181
|
-
PLANS,
|
|
182
|
-
),
|
|
183
|
-
"The end date is not a valid date.",
|
|
184
|
-
);
|
|
185
|
-
assert.equal(
|
|
186
|
-
validateConsoleSubscription(
|
|
187
|
-
buildConsolePayload(form({ startDate: "not-a-date" }), PLANS, 1),
|
|
188
|
-
PLANS,
|
|
189
|
-
),
|
|
190
|
-
"The start date is not a valid date.",
|
|
191
|
-
);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("a complimentary subscription carrying a price is refused, not silently zeroed", () => {
|
|
195
|
-
// The form can only produce this by disagreeing with itself - which is
|
|
196
|
-
// exactly what this rule is here to catch before it reaches the API.
|
|
197
|
-
const payload = {
|
|
198
|
-
...buildConsolePayload(form({ billingMode: BILLING_PAID }), PLANS, 4),
|
|
199
|
-
billingMode: BILLING_COMPLIMENTARY,
|
|
200
|
-
};
|
|
201
|
-
assert.equal(payload.amount, 100);
|
|
202
|
-
assert.equal(
|
|
203
|
-
validateConsoleSubscription(payload, PLANS),
|
|
204
|
-
"A complimentary subscription is not charged, so it cannot carry a price. Set the price to 0, or change this client to paying.",
|
|
205
|
-
);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test("a charging arrangement that is neither is refused", () => {
|
|
209
|
-
assert.equal(
|
|
210
|
-
validateConsoleSubscription(buildConsolePayload(form({ billingMode: "" }), PLANS, 1), PLANS),
|
|
211
|
-
"Choose whether this client is complimentary or paying.",
|
|
212
|
-
);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("a complimentary client with no sites is allowed - most clients are exactly this", () => {
|
|
216
|
-
assert.equal(
|
|
217
|
-
validateConsoleSubscription(buildConsolePayload(form(), PLANS, 0), PLANS),
|
|
218
|
-
null,
|
|
219
|
-
);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
/* ── PRE-FILLING THE FORM ── */
|
|
223
|
-
|
|
224
|
-
test("a client with no subscription opens complimentary, starting today, with no end date", () => {
|
|
225
|
-
const values = formValuesFrom(null, new Date("2026-08-20T10:00:00"));
|
|
226
|
-
assert.equal(values.plan, "");
|
|
227
|
-
assert.equal(values.billingMode, BILLING_COMPLIMENTARY);
|
|
228
|
-
assert.equal(values.startDate, todayISO(new Date("2026-08-20T10:00:00")));
|
|
229
|
-
assert.equal(values.endDate, "");
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test("an existing subscription opens with its own plan, mode and dates", () => {
|
|
233
|
-
const values = formValuesFrom({
|
|
234
|
-
_id: "eeeeeeeeeeeeeeeeeeeeeeee",
|
|
235
|
-
plan: STANDARD,
|
|
236
|
-
billingMode: "complimentary",
|
|
237
|
-
startDate: "2026-01-15T00:00:00.000Z",
|
|
238
|
-
endDate: "2026-12-31T00:00:00.000Z",
|
|
239
|
-
});
|
|
240
|
-
assert.equal(values.plan, STANDARD);
|
|
241
|
-
assert.equal(values.billingMode, BILLING_COMPLIMENTARY);
|
|
242
|
-
assert.equal(values.startDate, "2026-01-15");
|
|
243
|
-
assert.equal(values.endDate, "2026-12-31");
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
test("a record with no billingMode opens as paying - every one today came from the paid checkout", () => {
|
|
247
|
-
const values = formValuesFrom({
|
|
248
|
-
_id: "eeeeeeeeeeeeeeeeeeeeeeee",
|
|
249
|
-
createdAt: "2025-03-04T00:00:00.000Z",
|
|
250
|
-
nextBillingDate: "2026-04-05T00:00:00.000Z",
|
|
251
|
-
});
|
|
252
|
-
assert.equal(values.billingMode, BILLING_PAID);
|
|
253
|
-
// The dates fall back to what the record does carry, exactly as the list does.
|
|
254
|
-
assert.equal(values.startDate, "2025-03-04");
|
|
255
|
-
assert.equal(values.endDate, "2026-04-05");
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
test("a complimentary record has no nextBillingDate, so the end date comes from endDate alone", () => {
|
|
259
|
-
const values = formValuesFrom({
|
|
260
|
-
_id: "eeeeeeeeeeeeeeeeeeeeeeee",
|
|
261
|
-
billingMode: "complimentary",
|
|
262
|
-
createdAt: "2026-02-01T00:00:00.000Z",
|
|
263
|
-
endDate: "2027-02-01T00:00:00.000Z",
|
|
264
|
-
});
|
|
265
|
-
assert.equal(values.endDate, "2027-02-01");
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
/* ── WHAT A REFUSAL FROM THE SERVER SAYS ── */
|
|
269
|
-
|
|
270
|
-
test("the API's own wording is what the person is shown", () => {
|
|
271
|
-
assert.equal(
|
|
272
|
-
readApiError(
|
|
273
|
-
{ data: { status: "error", message: "This client already has a subscription. Edit the existing one instead of creating another." } },
|
|
274
|
-
"fallback",
|
|
275
|
-
),
|
|
276
|
-
"This client already has a subscription. Edit the existing one instead of creating another.",
|
|
277
|
-
);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
test("a message that says nothing to a person falls back to one that does", () => {
|
|
281
|
-
assert.equal(readApiError(new Error("fetch failed"), "Try again."), "Try again.");
|
|
282
|
-
assert.equal(readApiError(undefined, "Try again."), "Try again.");
|
|
283
|
-
assert.equal(readApiError({ data: { message: " " } }, "Try again."), "Try again.");
|
|
284
|
-
});
|
package/utils/table-card.test.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* REGRESSION GUARDS FOR THE TABLE CARD'S BUSY SIGNAL.
|
|
3
|
-
*
|
|
4
|
-
* Both faults here are a CSS declaration and a template binding, not a pure
|
|
5
|
-
* function, and this repo has no component test runner (adding one is a
|
|
6
|
-
* dependency change). So these assert the SOURCE CONTRACT: the exact
|
|
7
|
-
* declarations whose absence caused the defect. They fail the moment someone
|
|
8
|
-
* deletes the anchor or the disabled binding again.
|
|
9
|
-
*
|
|
10
|
-
* The behavioural proof is the driven browser measurement recorded on the PR -
|
|
11
|
-
* bar 1440x2 at `top: 0` of the WINDOW before, 1406x2 at the card's own top
|
|
12
|
-
* edge after; refresh button `false -> false -> false` before,
|
|
13
|
-
* `false -> true -> false` after.
|
|
14
|
-
*/
|
|
15
|
-
import { describe, it } from "node:test";
|
|
16
|
-
import assert from "node:assert/strict";
|
|
17
|
-
import { readFileSync } from "node:fs";
|
|
18
|
-
import { fileURLToPath } from "node:url";
|
|
19
|
-
|
|
20
|
-
const read = (rel: string) =>
|
|
21
|
-
readFileSync(fileURLToPath(new URL(`../${rel}`, import.meta.url)), "utf8");
|
|
22
|
-
|
|
23
|
-
/** The declaration block for a selector, comments stripped. */
|
|
24
|
-
const ruleFor = (css: string, selector: string) => {
|
|
25
|
-
const at = css.indexOf(`\n${selector} {`);
|
|
26
|
-
assert.notEqual(at, -1, `no rule for "${selector}"`);
|
|
27
|
-
const open = css.indexOf("{", at);
|
|
28
|
-
const close = css.indexOf("}", open);
|
|
29
|
-
return css
|
|
30
|
-
.slice(open + 1, close)
|
|
31
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
32
|
-
.trim();
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
describe("the loading bar's containing block", () => {
|
|
36
|
-
const screens = read("assets/css/screens.css");
|
|
37
|
-
|
|
38
|
-
it("anchors .table-card__loading to its card, not the viewport", () => {
|
|
39
|
-
// The bar is absolute. Without this the nearest positioned ancestor is
|
|
40
|
-
// the initial containing block, i.e. the browser window: the bar was
|
|
41
|
-
// measured 1440x2 at top:0 while its card sat 61.5px lower.
|
|
42
|
-
assert.match(ruleFor(screens, ".table-card"), /position:\s*relative/);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("still positions the bar out of flow, so a load causes no 2px jump", () => {
|
|
46
|
-
assert.match(ruleFor(screens, ".table-card__loading"), /position:\s*absolute/);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("TableMain's refresh button", () => {
|
|
51
|
-
const sfc = read("components/TableMain.vue");
|
|
52
|
-
// The one `v-btn` carrying the refresh emit.
|
|
53
|
-
const refreshBtn = (() => {
|
|
54
|
-
const at = sfc.indexOf(`@click="emits('refresh')"`);
|
|
55
|
-
assert.notEqual(at, -1, "no refresh button in TableMain");
|
|
56
|
-
const open = sfc.lastIndexOf("<v-btn", at);
|
|
57
|
-
return sfc.slice(open, at);
|
|
58
|
-
})();
|
|
59
|
-
|
|
60
|
-
it("is disabled while the caller's request is in flight", () => {
|
|
61
|
-
// Without this, clicking refresh on a list that comes back identical
|
|
62
|
-
// changed zero pixels - the Work Orders / Duty Officer Book report.
|
|
63
|
-
assert.match(refreshBtn, /:disabled="loading"/);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("reads the same `loading` prop the bar reads, so the two cannot diverge", () => {
|
|
67
|
-
assert.match(sfc, /v-if="loading"[\s\S]{0,200}table-card__loading/);
|
|
68
|
-
assert.match(sfc, /loading:\s*\{\s*type:\s*Boolean,\s*default:\s*false,?\s*\}/);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe(".app-switch's thumb", () => {
|
|
73
|
-
const primitives = read("assets/css/primitives.css");
|
|
74
|
-
|
|
75
|
-
it("beats Vuetify's 3- and 4-class slide on the input", () => {
|
|
76
|
-
// `.v-locale--is-ltr .v-switch .v-selection-control__input` and its
|
|
77
|
-
// `--dirty` twin outrank a 2-class rule, so the plain reset never
|
|
78
|
-
// applied and every thumb sat 10px out of its track.
|
|
79
|
-
assert.match(
|
|
80
|
-
ruleFor(primitives, ".app-switch .v-selection-control__input"),
|
|
81
|
-
/transform:\s*none\s*!important/,
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
|
-
});
|