@antelopejs/dms 0.7.7 → 0.7.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/test/unit/interfaces/dms-base/column-display.test.js +15 -0
  3. package/dist/test/unit/interfaces/dms-base/column-display.test.js.map +1 -1
  4. package/dist/test/unit/interfaces/dms-base/page-live.test.d.ts +1 -0
  5. package/dist/test/unit/interfaces/dms-base/page-live.test.js +66 -0
  6. package/dist/test/unit/interfaces/dms-base/page-live.test.js.map +1 -0
  7. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +16 -0
  8. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
  9. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +23 -3
  10. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -1
  11. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -1
  12. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +134 -0
  13. package/frontend-vue/layers/dms-layout/app/build/composables/layout/rowSidePanel.ts +129 -0
  14. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +6 -0
  15. package/frontend-vue/layers/dms-layout/app/types/page.ts +3 -0
  16. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +5 -1
  17. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +5 -1
  18. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +44 -8
  19. package/frontend-vue/layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue +35 -0
  20. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +17 -0
  21. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +107 -12
  22. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useRefreshTriggers.ts +26 -11
  23. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/cellHelpers.ts +24 -0
  24. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +58 -16
  25. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +30 -3
  26. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +107 -0
  27. package/frontend-vue/layers/dms-ui/app/build/composables/live/formatElapsed.ts +25 -0
  28. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageLiveController.ts +229 -0
  29. package/frontend-vue/layers/dms-ui/app/build/composables/live/shortcuts.ts +8 -0
  30. package/frontend-vue/layers/dms-ui/app/build/composables/live/useLiveToggleShortcut.ts +25 -0
  31. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +46 -0
  32. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +330 -0
  33. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodLocation.ts +189 -0
  34. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +15 -3
  35. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +77 -0
  36. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +53 -18
  37. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +1 -0
  38. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +61 -2
  39. package/frontend-vue/layers/dms-ui/app/build/utils/rowClickTrigger.ts +73 -0
  40. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +92 -0
  41. package/frontend-vue/layers/dms-ui/app/components/field-row/FieldRow.vue +10 -4
  42. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +19 -0
  43. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +90 -12
  44. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +52 -0
  45. package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/index.ts +8 -0
  46. package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewRowPanel.ts +30 -0
  47. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +15 -0
  48. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
  49. package/frontend-vue/layers/dms-ui/app/config/shortcuts-registry.ts +5 -0
  50. package/frontend-vue/layers/dms-ui/app/types/live.ts +8 -0
  51. package/frontend-vue/layers/dms-ui/app/types/row-action.ts +5 -2
  52. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +44 -6
  53. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +28 -0
  54. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +28 -0
  55. package/frontend-vue/tests/chart-live-pause.test.ts +74 -0
  56. package/frontend-vue/tests/field-row.test.ts +14 -0
  57. package/frontend-vue/tests/live-hold-banner.test.ts +37 -0
  58. package/frontend-vue/tests/live-toggle.test.ts +162 -0
  59. package/frontend-vue/tests/page-live-gate.test.ts +241 -0
  60. package/frontend-vue/tests/period-live-pause.test.ts +120 -0
  61. package/frontend-vue/tests/period-selector-url.test.ts +268 -0
  62. package/frontend-vue/tests/period-url-query.test.ts +351 -0
  63. package/frontend-vue/tests/row-side-panel.test.ts +653 -0
  64. package/frontend-vue/tests/table-cell-tones.test.ts +176 -0
  65. package/frontend-vue/tests/table-live-hold.test.ts +204 -0
  66. package/frontend-vue/tests/table-source-period-scope.test.ts +144 -0
  67. package/frontend-vue/tests/table-view-refresh.test.ts +2 -2
  68. package/frontend-vue/tests/table-view-sources.test.ts +40 -0
  69. package/frontend-vue/tests/vue-inventory.test.ts +1 -1
  70. package/package.json +2 -2
  71. package/skills/dms/SKILL.md +9 -2
  72. package/skills/dms-pages/REFERENCE.md +5 -0
@@ -0,0 +1,37 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The strip of a held table: "N new · Show", Show reading the rows again.
4
+ */
5
+ import { createApp, h, type App } from "vue";
6
+ import { afterEach, expect, it, vi } from "vitest";
7
+
8
+ let app: App | undefined;
9
+
10
+ afterEach(() => {
11
+ app?.unmount();
12
+ vi.unstubAllGlobals();
13
+ });
14
+
15
+ it("draws the banner and reads the rows on Show", async () => {
16
+ vi.stubGlobal("useI18n", () => ({
17
+ t: (key: string, params?: { count?: number }) =>
18
+ params ? `${key}:${params.count}` : key,
19
+ }));
20
+ const { default: LiveHoldBanner } = await import(
21
+ "../layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue"
22
+ );
23
+ const onShow = vi.fn();
24
+ const host = document.createElement("div");
25
+ app = createApp({
26
+ render: () => h(LiveHoldBanner, { count: 3, onShow }),
27
+ });
28
+ app.component("UIcon", () => h("i"));
29
+ app.component("UButton", (props: { label: string }, { attrs }) =>
30
+ h("button", attrs, props.label),
31
+ );
32
+ app.mount(host);
33
+
34
+ expect(host.textContent).toContain("dms.live.new_rows:3");
35
+ host.querySelector("button")?.click();
36
+ expect(onShow).toHaveBeenCalledTimes(1);
37
+ });
@@ -0,0 +1,162 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The LiveToggle block: Direct / Pause as a two-segment toggle, "updated N s
4
+ * ago" counted every second in the browser, L switching the page outside a
5
+ * text field, and nothing at all on a page without `live`.
6
+ */
7
+ import { createApp, h, nextTick, type App } from "vue";
8
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
9
+ import { defineShortcuts } from "@nuxt/ui/runtime/composables/defineShortcuts.js";
10
+ import {
11
+ createPageLiveController,
12
+ type PageLiveController,
13
+ } from "../layers/dms-ui/app/build/composables/live/pageLiveController";
14
+ import { setCurrentPageLive } from "../layers/dms-ui/app/build/composables/live/currentPageLive";
15
+ import { formatElapsed } from "../layers/dms-ui/app/build/composables/live/formatElapsed";
16
+
17
+ let app: App | undefined;
18
+ let host: HTMLDivElement;
19
+ let live: PageLiveController | undefined;
20
+
21
+ const t = (key: string, params?: { count?: number }) =>
22
+ params?.count === undefined ? key : `${key}:${params.count}`;
23
+
24
+ async function createToggle(
25
+ target: HTMLElement,
26
+ props: Record<string, unknown> = {},
27
+ ): Promise<App> {
28
+ const { default: LiveToggleBlock } = await import(
29
+ "../layers/dms-ui/app/components/blocks/LiveToggleBlock.vue"
30
+ );
31
+ const toggle = createApp({
32
+ render: () =>
33
+ h(LiveToggleBlock, { componentId: "live", pageId: "api.logs", ...props }),
34
+ });
35
+ toggle.component("UIcon", () => h("i"));
36
+ toggle.mount(target);
37
+ await nextTick();
38
+ return toggle;
39
+ }
40
+
41
+ async function mountToggle(props: Record<string, unknown> = {}) {
42
+ app = await createToggle(host, props);
43
+ }
44
+
45
+ const segment = (value: string) =>
46
+ host.querySelector<HTMLButtonElement>(
47
+ `[role="radio"][data-value="${value}"]`,
48
+ ) ??
49
+ [...host.querySelectorAll<HTMLButtonElement>('[role="radio"]')].find(
50
+ (button) => button.textContent?.includes(`dms.live.${value}`),
51
+ );
52
+
53
+ const pressL = (target: EventTarget = document) =>
54
+ target.dispatchEvent(
55
+ new KeyboardEvent("keydown", { key: "l", bubbles: true }),
56
+ );
57
+
58
+ beforeEach(() => {
59
+ vi.useFakeTimers();
60
+ vi.stubGlobal("useI18n", () => ({ t }));
61
+ vi.stubGlobal("defineShortcuts", defineShortcuts);
62
+ host = document.createElement("div");
63
+ document.body.append(host);
64
+ window.localStorage.clear();
65
+ });
66
+
67
+ afterEach(() => {
68
+ app?.unmount();
69
+ app = undefined;
70
+ host.remove();
71
+ live?.dispose();
72
+ live = undefined;
73
+ setCurrentPageLive(null);
74
+ vi.unstubAllGlobals();
75
+ vi.useRealTimers();
76
+ });
77
+
78
+ function installLive() {
79
+ live = createPageLiveController("api.logs", { maxRate: 2 });
80
+ setCurrentPageLive(live);
81
+ return live;
82
+ }
83
+
84
+ it("renders nothing on a page without live", async () => {
85
+ await mountToggle();
86
+ expect(host.querySelector(".dms-live-toggle")).toBeNull();
87
+ });
88
+
89
+ it("shows the page's state and switches it", async () => {
90
+ const controller = installLive();
91
+ await mountToggle();
92
+ expect(segment("direct")?.getAttribute("aria-checked")).toBe("true");
93
+
94
+ segment("pause")?.click();
95
+ await nextTick();
96
+ expect(controller.isPaused.value).toBe(true);
97
+ expect(segment("pause")?.getAttribute("aria-checked")).toBe("true");
98
+
99
+ segment("direct")?.click();
100
+ await nextTick();
101
+ expect(controller.isPaused.value).toBe(false);
102
+ });
103
+
104
+ it("counts the seconds since the last update, every second", async () => {
105
+ installLive();
106
+ await mountToggle();
107
+ expect(host.textContent).toContain("dms.live.updated_seconds:0");
108
+
109
+ vi.advanceTimersByTime(3000);
110
+ await nextTick();
111
+ expect(host.textContent).toContain("dms.live.updated_seconds:3");
112
+
113
+ vi.advanceTimersByTime(60_000);
114
+ await nextTick();
115
+ expect(host.textContent).toContain("dms.live.updated_minutes:1");
116
+ });
117
+
118
+ it("hides the time when asked to", async () => {
119
+ installLive();
120
+ await mountToggle({ showUpdatedAt: false });
121
+ expect(host.textContent).not.toContain("dms.live.updated");
122
+ });
123
+
124
+ it("switches on L, but not while typing", async () => {
125
+ const controller = installLive();
126
+ await mountToggle();
127
+
128
+ pressL();
129
+ expect(controller.isPaused.value).toBe(true);
130
+ pressL();
131
+ expect(controller.isPaused.value).toBe(false);
132
+
133
+ const field = document.createElement("input");
134
+ document.body.append(field);
135
+ field.focus();
136
+ await nextTick();
137
+ pressL(field);
138
+ expect(controller.isPaused.value).toBe(false);
139
+ field.remove();
140
+ });
141
+
142
+ it("reads elapsed times in seconds, minutes, then hours", () => {
143
+ expect(formatElapsed(-5)).toEqual({ unit: "seconds", count: 0 });
144
+ expect(formatElapsed(59_999)).toEqual({ unit: "seconds", count: 59 });
145
+ expect(formatElapsed(60_000)).toEqual({ unit: "minutes", count: 1 });
146
+ expect(formatElapsed(2 * 3_600_000 + 1)).toEqual({ unit: "hours", count: 2 });
147
+ });
148
+
149
+ it("switches once per L with two toggles on the page", async () => {
150
+ const controller = installLive();
151
+ await mountToggle();
152
+ const second = await createToggle(document.createElement("div"));
153
+
154
+ pressL();
155
+ expect(controller.isPaused.value).toBe(true);
156
+
157
+ app?.unmount();
158
+ app = undefined;
159
+ pressL();
160
+ expect(controller.isPaused.value).toBe(false);
161
+ second.unmount();
162
+ });
@@ -0,0 +1,241 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The Direct / Pause valve of a page declared with `live`: in Direct a block
4
+ * refetches on its events at most once per `maxRate` (the last event of a
5
+ * burst included), paused it requests nothing and counts what it missed, and
6
+ * resuming refetches once each block that missed something. The choice is
7
+ * kept per page in the browser, and a page without `live` is untouched.
8
+ */
9
+ import { createApp, defineComponent, h } from "vue";
10
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
11
+ import type { RealtimeTopicHandler } from "../layers/dms-core/app/composables/realtime/useUserRealtime";
12
+ import {
13
+ refreshLivePageBlocks,
14
+ refreshPageBlocks,
15
+ } from "../layers/dms-ui/app/utils/blockRefresh";
16
+ import {
17
+ createPageLiveController,
18
+ type PageLiveController,
19
+ } from "../layers/dms-ui/app/build/composables/live/pageLiveController";
20
+ import { setCurrentPageLive } from "../layers/dms-ui/app/build/composables/live/currentPageLive";
21
+ import { usePageLive } from "../layers/dms-ui/app/composables/live/usePageLive";
22
+
23
+ const topicHandlers = new Map<string, RealtimeTopicHandler[]>();
24
+
25
+ vi.mock("../layers/dms-core/app/composables/realtime/useRealtimeTopic", () => ({
26
+ useRealtimeTopic: (topic: string, handler: RealtimeTopicHandler) => {
27
+ topicHandlers.set(topic, [...(topicHandlers.get(topic) ?? []), handler]);
28
+ },
29
+ }));
30
+
31
+ const { useRefreshTriggers } = await import(
32
+ "../layers/dms-ui/app/build/composables/blocks/useRefreshTriggers"
33
+ );
34
+
35
+ const PAGE = "monitoring.logs";
36
+ const TOPIC = "api:requests";
37
+ const MAX_RATE_SECONDS = 5;
38
+ const MAX_RATE_MS = MAX_RATE_SECONDS * 1000;
39
+ const mounted: Array<{ unmount: () => void }> = [];
40
+ const controllers: PageLiveController[] = [];
41
+
42
+ function publish(topic = TOPIC) {
43
+ const event = { topic, type: "request", ts: Date.now() };
44
+ for (const handler of topicHandlers.get(topic) ?? []) handler(event);
45
+ }
46
+
47
+ function mountBlock(topic: string | undefined = TOPIC) {
48
+ const onRefresh = vi.fn();
49
+ const app = createApp(
50
+ defineComponent({
51
+ setup() {
52
+ useRefreshTriggers(topic, onRefresh);
53
+ return () => h("div");
54
+ },
55
+ }),
56
+ );
57
+ app.mount(document.createElement("div"));
58
+ mounted.push(app);
59
+ return onRefresh;
60
+ }
61
+
62
+ function installLive(maxRate?: number): PageLiveController {
63
+ const controller = createPageLiveController(PAGE, { maxRate });
64
+ controllers.push(controller);
65
+ setCurrentPageLive(controller);
66
+ return controller;
67
+ }
68
+
69
+ beforeEach(() => {
70
+ vi.useFakeTimers();
71
+ topicHandlers.clear();
72
+ window.localStorage.clear();
73
+ });
74
+
75
+ afterEach(() => {
76
+ mounted.splice(0).forEach((app) => app.unmount());
77
+ controllers.splice(0).forEach((controller) => controller.dispose());
78
+ setCurrentPageLive(null);
79
+ vi.useRealTimers();
80
+ });
81
+
82
+ it("leaves a page without live as it was: every event refetches", () => {
83
+ const onRefresh = mountBlock();
84
+ publish();
85
+ publish();
86
+ refreshPageBlocks();
87
+ refreshLivePageBlocks();
88
+ expect(onRefresh).toHaveBeenCalledTimes(4);
89
+ expect(usePageLive().isEnabled.value).toBe(false);
90
+ expect(usePageLive().isLive.value).toBe(true);
91
+ });
92
+
93
+ it("refetches on each event in Direct when no maxRate is set", () => {
94
+ installLive();
95
+ const onRefresh = mountBlock();
96
+ publish();
97
+ publish();
98
+ expect(onRefresh).toHaveBeenCalledTimes(2);
99
+ });
100
+
101
+ it("in Direct, refetches at most once per maxRate, the last event trailing", () => {
102
+ installLive(MAX_RATE_SECONDS);
103
+ const onRefresh = mountBlock();
104
+
105
+ publish();
106
+ expect(onRefresh).toHaveBeenCalledTimes(1);
107
+
108
+ vi.advanceTimersByTime(1000);
109
+ publish();
110
+ vi.advanceTimersByTime(1000);
111
+ publish();
112
+ expect(onRefresh).toHaveBeenCalledTimes(1);
113
+
114
+ vi.advanceTimersByTime(MAX_RATE_MS - 2000);
115
+ expect(onRefresh).toHaveBeenCalledTimes(2);
116
+
117
+ vi.advanceTimersByTime(MAX_RATE_MS * 2);
118
+ expect(onRefresh).toHaveBeenCalledTimes(2);
119
+ });
120
+
121
+ it("caps each block on its own", () => {
122
+ installLive(MAX_RATE_SECONDS);
123
+ const first = mountBlock();
124
+ const second = mountBlock("api:routes");
125
+ publish();
126
+ publish("api:routes");
127
+ expect(first).toHaveBeenCalledTimes(1);
128
+ expect(second).toHaveBeenCalledTimes(1);
129
+ });
130
+
131
+ it("in Pause, requests nothing and counts the events per block", () => {
132
+ const live = installLive();
133
+ const logs = mountBlock();
134
+ const chart = mountBlock();
135
+ const routes = mountBlock("api:routes");
136
+ live.pause();
137
+
138
+ publish();
139
+ publish();
140
+ refreshLivePageBlocks();
141
+ vi.advanceTimersByTime(60_000);
142
+
143
+ expect(logs).not.toHaveBeenCalled();
144
+ expect(chart).not.toHaveBeenCalled();
145
+ expect(routes).not.toHaveBeenCalled();
146
+ // Two events and a live refresh, each counted once for the page although
147
+ // two blocks (three for the refresh) received them.
148
+ expect(live.missedCount.value).toBe(3);
149
+ expect(usePageLive().isPaused.value).toBe(true);
150
+ });
151
+
152
+ it("refreshes on a user's refreshPageBlocks even while paused", () => {
153
+ const live = installLive(MAX_RATE_SECONDS);
154
+ const onRefresh = mountBlock();
155
+ live.pause();
156
+ publish();
157
+ expect(live.missedCount.value).toBe(1);
158
+
159
+ refreshPageBlocks();
160
+ expect(onRefresh).toHaveBeenCalledTimes(1);
161
+ expect(live.isPaused.value).toBe(true);
162
+
163
+ // Read by the user, the block has nothing left to read on resume.
164
+ live.resume();
165
+ expect(onRefresh).toHaveBeenCalledTimes(1);
166
+ });
167
+
168
+ it("caps a live refresh like a realtime event in Direct", () => {
169
+ installLive(MAX_RATE_SECONDS);
170
+ const onRefresh = mountBlock(undefined);
171
+ refreshLivePageBlocks();
172
+ refreshLivePageBlocks();
173
+ expect(onRefresh).toHaveBeenCalledTimes(1);
174
+ vi.advanceTimersByTime(MAX_RATE_MS);
175
+ expect(onRefresh).toHaveBeenCalledTimes(2);
176
+ });
177
+
178
+ it("on resume, refetches once each block that missed an event", () => {
179
+ const live = installLive(MAX_RATE_SECONDS);
180
+ const logs = mountBlock();
181
+ const routes = mountBlock("api:routes");
182
+ publish("api:routes");
183
+ routes.mockClear();
184
+ live.pause();
185
+
186
+ publish();
187
+ publish();
188
+ publish();
189
+ live.resume();
190
+
191
+ expect(logs).toHaveBeenCalledTimes(1);
192
+ expect(routes).not.toHaveBeenCalled();
193
+ expect(live.missedCount.value).toBe(0);
194
+ vi.advanceTimersByTime(MAX_RATE_MS);
195
+ expect(logs).toHaveBeenCalledTimes(1);
196
+ });
197
+
198
+ it("keeps a trailing refetch the pause interrupted for the resume", () => {
199
+ const live = installLive(MAX_RATE_SECONDS);
200
+ const onRefresh = mountBlock();
201
+ publish();
202
+ publish();
203
+ live.pause();
204
+ vi.advanceTimersByTime(MAX_RATE_MS);
205
+ expect(onRefresh).toHaveBeenCalledTimes(1);
206
+ expect(live.missedCount.value).toBe(1);
207
+
208
+ live.resume();
209
+ expect(onRefresh).toHaveBeenCalledTimes(2);
210
+ });
211
+
212
+ it("dates the page's last update from its refetches", () => {
213
+ const live = installLive();
214
+ mountBlock();
215
+ const before = live.lastUpdatedAt.value;
216
+ vi.advanceTimersByTime(4000);
217
+ publish();
218
+ expect(live.lastUpdatedAt.value).toBe(before + 4000);
219
+ });
220
+
221
+ it("remembers the choice per page in the browser", () => {
222
+ installLive().pause();
223
+ expect(createPageLiveController(PAGE, {}).isPaused.value).toBe(true);
224
+ expect(createPageLiveController("monitoring.routes", {}).isPaused.value).toBe(
225
+ false,
226
+ );
227
+
228
+ createPageLiveController(PAGE, {}).resume();
229
+ expect(createPageLiveController(PAGE, {}).isPaused.value).toBe(false);
230
+ });
231
+
232
+ it("follows the page through usePageLive", () => {
233
+ installLive();
234
+ const state = usePageLive();
235
+ expect(state.isEnabled.value).toBe(true);
236
+ state.pause();
237
+ expect(state.isPaused.value).toBe(true);
238
+ expect(state.isLive.value).toBe(false);
239
+ state.toggle();
240
+ expect(state.isPaused.value).toBe(false);
241
+ });
@@ -0,0 +1,120 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * A rolling period (`last-hour`, `last-24h`) slides every minute, and every
4
+ * block reading it refetches. On a paused live page the period holds still,
5
+ * so nothing refetches on the clock; a period the user picks still applies at
6
+ * once, and resuming catches the skipped minutes up in one read.
7
+ */
8
+ import { readFileSync } from "node:fs";
9
+ import { resolve } from "node:path";
10
+ import { createApp, defineComponent, h, ref } from "vue";
11
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
12
+ import { createPageLiveController } from "../layers/dms-ui/app/build/composables/live/pageLiveController";
13
+ import { setCurrentPageLive } from "../layers/dms-ui/app/build/composables/live/currentPageLive";
14
+ import { usePageLive } from "../layers/dms-ui/app/composables/live/usePageLive";
15
+ import {
16
+ usePeriod,
17
+ type UsePeriodReturn,
18
+ } from "../layers/dms-core/app/composables/period/usePeriod";
19
+ import { registerPeriodScope } from "../layers/dms-core/app/composables/period/usePeriodScope";
20
+
21
+ const requests: string[] = [];
22
+
23
+ vi.stubGlobal("useAuthFetch", () => ({
24
+ $authFetch: async (url: string) => {
25
+ requests.push(url);
26
+ return { value: requests.length };
27
+ },
28
+ }));
29
+ vi.stubGlobal("useDmsRoute", () => ({ query: {} }));
30
+ vi.stubGlobal("ref", ref);
31
+
32
+ const { useChartFetch } = await import(
33
+ "../layers/dms-ui/app/composables/chart/useChartFetch"
34
+ );
35
+
36
+ const SCOPE = "live-period";
37
+ const MINUTE_MS = 60_000;
38
+ const apps: Array<() => void> = [];
39
+
40
+ function mountPageWithChart(): UsePeriodReturn {
41
+ let period: UsePeriodReturn | undefined;
42
+ const app = createApp(
43
+ defineComponent({
44
+ setup() {
45
+ const { isPaused } = usePageLive();
46
+ // As the PeriodSelector wires it.
47
+ period = usePeriod({
48
+ defaultPreset: "last-hour",
49
+ holdClock: () => isPaused.value,
50
+ });
51
+ registerPeriodScope(SCOPE, period.state);
52
+ useChartFetch({ fetchUrl: "/api/chart", periodScope: SCOPE });
53
+ return () => h("div");
54
+ },
55
+ }),
56
+ );
57
+ app.mount(document.createElement("div"));
58
+ apps.push(() => app.unmount());
59
+ return period!;
60
+ }
61
+
62
+ beforeEach(() => {
63
+ vi.useFakeTimers();
64
+ requests.length = 0;
65
+ window.localStorage.clear();
66
+ });
67
+
68
+ afterEach(() => {
69
+ apps.splice(0).forEach((unmount) => unmount());
70
+ setCurrentPageLive(null);
71
+ vi.useRealTimers();
72
+ });
73
+
74
+ it("slides and refetches every minute in Direct", async () => {
75
+ setCurrentPageLive(createPageLiveController("api.overview", {}));
76
+ mountPageWithChart();
77
+ await vi.advanceTimersByTimeAsync(1000);
78
+ expect(requests).toHaveLength(1);
79
+
80
+ await vi.advanceTimersByTimeAsync(2 * MINUTE_MS);
81
+ expect(requests).toHaveLength(3);
82
+ });
83
+
84
+ it("holds the period while paused, but follows the user's pick", async () => {
85
+ const live = createPageLiveController("api.overview", {});
86
+ setCurrentPageLive(live);
87
+ const period = mountPageWithChart();
88
+ await vi.advanceTimersByTimeAsync(1000);
89
+ expect(requests).toHaveLength(1);
90
+
91
+ live.pause();
92
+ const heldKey = period.state.value.key;
93
+ await vi.advanceTimersByTimeAsync(3 * MINUTE_MS);
94
+ expect(requests).toHaveLength(1);
95
+ expect(period.state.value.key).toBe(heldKey);
96
+
97
+ period.setPreset("last-24h");
98
+ await vi.advanceTimersByTimeAsync(1000);
99
+ expect(requests).toHaveLength(2);
100
+ expect(requests[1]).toContain("preset=last-24h");
101
+
102
+ await vi.advanceTimersByTimeAsync(2 * MINUTE_MS);
103
+ expect(requests).toHaveLength(2);
104
+
105
+ live.resume();
106
+ await vi.advanceTimersByTimeAsync(1000);
107
+ expect(requests).toHaveLength(3);
108
+ expect(period.state.value.key).not.toBe(heldKey);
109
+ });
110
+
111
+ it("holds the selector's period on the page's pause", () => {
112
+ const source = readFileSync(
113
+ resolve(
114
+ __dirname,
115
+ "../layers/dms-ui/app/components/period/PeriodSelector.vue",
116
+ ),
117
+ "utf8",
118
+ );
119
+ expect(source).toContain("holdClock: () => isPagePaused.value,");
120
+ });