@7365admin1/module-hygiene 4.28.1-staging.7 → 4.28.1-staging.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@7365admin1/module-hygiene",
3
3
  "license": "MIT",
4
- "version": "4.28.1-staging.7",
4
+ "version": "4.28.1-staging.8",
5
5
  "author": "7365admin1",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -12,7 +12,8 @@
12
12
  "scripts": {
13
13
  "build": "tsup src/index.ts --format cjs,esm --dts",
14
14
  "release": "yarn run build && changeset publish",
15
- "lint": "tsc"
15
+ "lint": "tsc",
16
+ "test": "tsup src/utils/hygiene-dashboard-metrics.util.ts --format esm --out-dir test/.build --no-dts --silent && node --test \"test/*.test.mjs\""
16
17
  },
17
18
  "devDependencies": {
18
19
  "@changesets/cli": "^2.26.0",
@@ -0,0 +1,146 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import {
5
+ DASHBOARD_TIMEZONE,
6
+ SUPPLY_ALERT_HAS_NO_PRIOR_PERIOD,
7
+ calculatePercentageChange,
8
+ getPeriodRange,
9
+ getPreviousPeriodRange,
10
+ } from "./.build/hygiene-dashboard-metrics.util.mjs";
11
+
12
+ /*
13
+ THE CLOCK IS PINNED, IN EVERY TEST THAT HAS ONE.
14
+
15
+ 17:00 UTC on the 19th is 01:00 SGT on the 20th — a deliberately chosen instant
16
+ where the host's calendar day and Singapore's calendar day DISAGREE. A boundary
17
+ that quietly inherited the host's timezone would land on the 19th here and the
18
+ assertions below would fail. Nothing in this file reads the wall clock, so the
19
+ answers do not change with the hour the suite runs at, or with the machine's TZ.
20
+ */
21
+ const LATE_EVENING_UTC = "2026-08-19T17:00:00.000Z";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // The day boundary is Singapore's, not the host's.
25
+ // ---------------------------------------------------------------------------
26
+
27
+ test("the day starts at Singapore midnight even when the host is a day behind", () => {
28
+ const range = getPeriodRange("today", LATE_EVENING_UTC);
29
+
30
+ // SGT is already on the 20th at this instant.
31
+ assert.equal(range.$gte.toISOString(), "2026-08-19T16:00:00.000Z");
32
+ assert.equal(range.$lte.toISOString(), "2026-08-20T15:59:59.999Z");
33
+ });
34
+
35
+ test("every boundary carries the UTC+8 signature, whatever timezone this test runs in", () => {
36
+ // 16:00 UTC IS midnight in Singapore. Asserting the offset rather than a local
37
+ // string keeps this true on a UTC container, an SGT container and a laptop.
38
+ for (const period of ["today", "thisWeek", "thisMonth"]) {
39
+ for (const range of [
40
+ getPeriodRange(period, LATE_EVENING_UTC),
41
+ getPreviousPeriodRange(period, LATE_EVENING_UTC),
42
+ ]) {
43
+ assert.equal(range.$gte.getUTCHours(), 16, `${period} start`);
44
+ assert.equal(range.$lte.getUTCHours(), 15, `${period} end`);
45
+ }
46
+ }
47
+ });
48
+
49
+ test("an unknown period falls back to a day, as it always did", () => {
50
+ assert.deepEqual(
51
+ getPeriodRange("whatever", LATE_EVENING_UTC),
52
+ getPeriodRange("today", LATE_EVENING_UTC),
53
+ );
54
+ });
55
+
56
+ test("the timezone is stated, not inherited", () => {
57
+ assert.equal(DASHBOARD_TIMEZONE, "Asia/Singapore");
58
+ });
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // The trend compares the period the user actually selected.
62
+ // ---------------------------------------------------------------------------
63
+
64
+ test("this week compares against the PREVIOUS ISO week, not yesterday", () => {
65
+ // 20 Aug 2026 SGT is a Thursday, so the ISO week runs Mon 17 -> Sun 23.
66
+ const current = getPeriodRange("thisWeek", LATE_EVENING_UTC);
67
+ const prior = getPreviousPeriodRange("thisWeek", LATE_EVENING_UTC);
68
+
69
+ assert.equal(current.$gte.toISOString(), "2026-08-16T16:00:00.000Z"); // Mon 17 SGT
70
+ assert.equal(prior.$gte.toISOString(), "2026-08-09T16:00:00.000Z"); // Mon 10 SGT
71
+ assert.equal(prior.$lte.toISOString(), "2026-08-16T15:59:59.999Z"); // Sun 16 SGT
72
+
73
+ // The two ranges are adjacent and do not overlap.
74
+ assert.equal(prior.$lte.getTime() + 1, current.$gte.getTime());
75
+ });
76
+
77
+ test("this month compares against the PREVIOUS month, not yesterday", () => {
78
+ const current = getPeriodRange("thisMonth", LATE_EVENING_UTC);
79
+ const prior = getPreviousPeriodRange("thisMonth", LATE_EVENING_UTC);
80
+
81
+ assert.equal(current.$gte.toISOString(), "2026-07-31T16:00:00.000Z"); // 1 Aug SGT
82
+ assert.equal(prior.$gte.toISOString(), "2026-06-30T16:00:00.000Z"); // 1 Jul SGT
83
+ assert.equal(prior.$lte.getTime() + 1, current.$gte.getTime());
84
+ });
85
+
86
+ test("today still compares against yesterday", () => {
87
+ const current = getPeriodRange("today", LATE_EVENING_UTC);
88
+ const prior = getPreviousPeriodRange("today", LATE_EVENING_UTC);
89
+
90
+ assert.equal(prior.$gte.toISOString(), "2026-08-18T16:00:00.000Z");
91
+ assert.equal(prior.$lte.getTime() + 1, current.$gte.getTime());
92
+ });
93
+
94
+ test("stepping a month back from the 31st does not skip a month", () => {
95
+ // 31 Aug SGT minus one month is 31 July in moment's arithmetic, which snaps
96
+ // to July — the trap the core suite caught, re-asserted here.
97
+ const prior = getPreviousPeriodRange("thisMonth", "2026-08-30T20:00:00.000Z");
98
+ assert.equal(prior.$gte.toISOString(), "2026-06-30T16:00:00.000Z");
99
+ });
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // A missing prior period yields NO percentage — never 0%, never 100%.
103
+ // ---------------------------------------------------------------------------
104
+
105
+ test("no prior period means no percentage, not 100%", () => {
106
+ assert.equal(calculatePercentageChange(7, 0), null);
107
+ });
108
+
109
+ test("two empty periods mean no percentage, not a measured 0%", () => {
110
+ assert.equal(calculatePercentageChange(0, 0), null);
111
+ });
112
+
113
+ test("a real comparison still returns a real number", () => {
114
+ assert.equal(calculatePercentageChange(3, 2), 50);
115
+ assert.equal(calculatePercentageChange(1, 3), -66.67);
116
+ assert.equal(calculatePercentageChange(2, 2), 0); // a genuine measured 0%
117
+ });
118
+
119
+ test("a non-number on either side is not silently treated as zero", () => {
120
+ for (const bad of [null, undefined, NaN, Infinity, "4"]) {
121
+ assert.equal(calculatePercentageChange(bad, 5), null, `current=${bad}`);
122
+ assert.equal(calculatePercentageChange(5, bad), null, `previous=${bad}`);
123
+ }
124
+ });
125
+
126
+ test("the percentage is never a constant — it moves with its inputs", () => {
127
+ const answers = new Set(
128
+ [
129
+ [10, 5],
130
+ [5, 10],
131
+ [3, 2],
132
+ [100, 25],
133
+ ].map(([a, b]) => calculatePercentageChange(a, b)),
134
+ );
135
+ assert.equal(answers.size, 4);
136
+ assert.ok(!answers.has(null));
137
+ });
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Supply alert: an honest count, and an explicit not-available beside it.
141
+ // ---------------------------------------------------------------------------
142
+
143
+ test("the supply alert reason says why there is no percentage, in plain words", () => {
144
+ assert.match(SUPPLY_ALERT_HAS_NO_PRIOR_PERIOD, /out of stock right now/);
145
+ assert.match(SUPPLY_ALERT_HAS_NO_PRIOR_PERIOD, /no earlier period/);
146
+ });