@antelopejs/dms 0.7.9 → 0.7.10
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/CHANGELOG.md +29 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/implementations/dms/api-base-url.d.ts +1 -0
- package/dist/implementations/dms/api-base-url.js +6 -0
- package/dist/implementations/dms/api-base-url.js.map +1 -0
- package/dist/implementations/dms/frontend-modules.js +7 -6
- package/dist/implementations/dms/frontend-modules.js.map +1 -1
- package/dist/implementations/dms/page.js +8 -2
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/test/unit/api-base-url.test.d.ts +1 -0
- package/dist/test/unit/api-base-url.test.js +31 -0
- package/dist/test/unit/api-base-url.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js +106 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -0
- package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js +46 -0
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +26 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js +10 -0
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/guard.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/guard.test.js +53 -0
- package/dist/test/unit/interfaces/dms-base/guard.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js +23 -0
- package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/page-live.test.js +55 -0
- package/dist/test/unit/interfaces/dms-base/page-live.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js +17 -0
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +2 -0
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +28 -0
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/build/composables/user/preferencesCookie.ts +149 -0
- package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +321 -0
- package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +4 -3
- package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +4 -3
- package/frontend-vue/layers/dms-core/app/composables/period/types.ts +3 -1
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +2 -2
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +9 -2
- package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +48 -16
- package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +21 -4
- package/frontend-vue/layers/dms-core/app/composables/user/usePreferences.ts +81 -49
- package/frontend-vue/layers/dms-core/app/types/confirm-dialog.ts +35 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +10 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +32 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +13 -3
- package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +27 -3
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/rowSidePanel.ts +16 -0
- package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +75 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +8 -1
- package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +13 -2
- package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +7 -0
- package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +7 -0
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +7 -4
- package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +59 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +5 -1
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +5 -1
- package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockInlineStatus.vue +67 -0
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockStatus.vue +60 -20
- package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +15 -8
- package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +14 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +29 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +76 -28
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +26 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useGuard.ts +115 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +58 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/secondaryTarget.ts +86 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +6 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveCadence.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveHolds.ts +66 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveStorage.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/missedTally.ts +65 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/pageLiveController.ts +247 -88
- package/frontend-vue/layers/dms-ui/app/build/composables/live/useLivePeriodSpan.ts +33 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +79 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparisonControl.ts +70 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +41 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +22 -9
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodLocation.ts +13 -6
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +27 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +18 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +19 -26
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +66 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +7 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +65 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTablePreferences.ts +149 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +39 -8
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +16 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +39 -16
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +29 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +18 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/sortableColumns.ts +17 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/fetchNotFound.ts +79 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +46 -1
- package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +4 -0
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +40 -2
- package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +12 -2
- package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +129 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +6 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +6 -1
- package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +33 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/ChoiceCards.vue +22 -4
- package/frontend-vue/layers/dms-ui/app/components/guard/Guard.vue +74 -0
- package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +12 -17
- package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +15 -0
- package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +12 -15
- package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +104 -16
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +104 -98
- package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +11 -1
- package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +15 -1
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +86 -10
- package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewShiftF.ts +20 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +12 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/useTableViewEmptyState.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/types/live.ts +47 -1
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +13 -4
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +12 -3
- package/frontend-vue/tests/activity-feed-fill-height.test.ts +1 -0
- package/frontend-vue/tests/activity-feed-head.test.ts +140 -0
- package/frontend-vue/tests/block-not-found.test.ts +220 -0
- package/frontend-vue/tests/choice-cards-columns.test.ts +60 -0
- package/frontend-vue/tests/confirm-secondary-action.test.ts +269 -0
- package/frontend-vue/tests/guard.test.ts +242 -0
- package/frontend-vue/tests/header-action-page-query.test.ts +286 -0
- package/frontend-vue/tests/live-toggle.test.ts +87 -1
- package/frontend-vue/tests/page-layout-registration.test.ts +1 -0
- package/frontend-vue/tests/page-live-parity.test.ts +367 -0
- package/frontend-vue/tests/page-realtime-remount.test.ts +185 -0
- package/frontend-vue/tests/period-selector-parity.test.ts +138 -0
- package/frontend-vue/tests/period-selector-url.test.ts +84 -6
- package/frontend-vue/tests/period-url-query.test.ts +52 -0
- package/frontend-vue/tests/preferences-cookie-size.test.ts +311 -0
- package/frontend-vue/tests/row-side-panel.test.ts +109 -10
- package/frontend-vue/tests/table-actions-search-label.test.ts +57 -1
- package/frontend-vue/tests/table-clear-filters.test.ts +99 -1
- package/frontend-vue/tests/table-density-menu.test.ts +4 -3
- package/frontend-vue/tests/table-live-hold.test.ts +95 -5
- package/frontend-vue/tests/table-row-action-labels.test.ts +181 -0
- package/frontend-vue/tests/table-source-period-scope.test.ts +31 -3
- package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +14 -17
- package/frontend-vue/tests/table-view-pinned-rows.test.ts +143 -0
- package/frontend-vue/tests/table-view-sortable-columns.test.ts +45 -0
- package/frontend-vue/tests/table-view-views.test.ts +56 -15
- package/frontend-vue/tests/top-list-card-value-suffix.test.ts +107 -0
- package/frontend-vue/tests/vue-inventory.test.ts +1 -1
- package/package.json +2 -2
- package/skills/dms/SKILL.md +9 -3
- package/skills/dms-module/SKILL.md +5 -0
- package/skills/dms-pages/REFERENCE.md +23 -2
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* The live mode beyond Live / Pause: what a pause counts (published rows, not
|
|
4
|
+
* messages), the holds a component takes with a reason, the hidden tab, the
|
|
5
|
+
* `?live=0` URL, the shared `rememberKey`, the polling while the stream is down and
|
|
6
|
+
* its slower cadence over a long period, and the remembered pause applied only
|
|
7
|
+
* once the page is mounted.
|
|
8
|
+
*/
|
|
9
|
+
import { createApp, defineComponent, h, nextTick, ref } from "vue";
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
11
|
+
import type { RealtimeTopicHandler } from "../layers/dms-core/app/composables/realtime/useUserRealtime";
|
|
12
|
+
import {
|
|
13
|
+
createPageLiveController,
|
|
14
|
+
type PageLiveController,
|
|
15
|
+
} from "../layers/dms-ui/app/build/composables/live/pageLiveController";
|
|
16
|
+
import { setCurrentPageLive } from "../layers/dms-ui/app/build/composables/live/currentPageLive";
|
|
17
|
+
import {
|
|
18
|
+
bindPageLiveEnvironment,
|
|
19
|
+
usePageLiveHost,
|
|
20
|
+
} from "../layers/dms-ui/app/build/composables/live/usePageLiveHost";
|
|
21
|
+
import {
|
|
22
|
+
liveIntervalMs,
|
|
23
|
+
livePeriodMs,
|
|
24
|
+
} from "../layers/dms-ui/app/build/composables/live/liveCadence";
|
|
25
|
+
import { MissedTally } from "../layers/dms-ui/app/build/composables/live/missedTally";
|
|
26
|
+
import { usePageLive } from "../layers/dms-ui/app/composables/live/usePageLive";
|
|
27
|
+
import { currentPageLive } from "../layers/dms-ui/app/build/composables/live/currentPageLive";
|
|
28
|
+
|
|
29
|
+
const topicHandlers = new Map<string, RealtimeTopicHandler[]>();
|
|
30
|
+
|
|
31
|
+
vi.mock("../layers/dms-core/app/composables/realtime/useRealtimeTopic", () => ({
|
|
32
|
+
useRealtimeTopic: (topic: string, handler: RealtimeTopicHandler) => {
|
|
33
|
+
topicHandlers.set(topic, [...(topicHandlers.get(topic) ?? []), handler]);
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const { useRefreshTriggers } = await import(
|
|
38
|
+
"../layers/dms-ui/app/build/composables/blocks/useRefreshTriggers"
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const PAGE = "modules.api.logs";
|
|
42
|
+
const TOPIC = "api:monitor:requests";
|
|
43
|
+
const ROW_TOPIC = "dms:table:requests";
|
|
44
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
45
|
+
const mounted: Array<{ unmount: () => void }> = [];
|
|
46
|
+
const controllers: PageLiveController[] = [];
|
|
47
|
+
|
|
48
|
+
function publish(payload?: Record<string, unknown>, topic = TOPIC) {
|
|
49
|
+
const event = { topic, type: "requests", ts: Date.now(), payload };
|
|
50
|
+
for (const handler of topicHandlers.get(topic) ?? []) handler(event);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mountBlock(topic: string = TOPIC) {
|
|
54
|
+
const onRefresh = vi.fn();
|
|
55
|
+
const app = createApp(
|
|
56
|
+
defineComponent({
|
|
57
|
+
setup() {
|
|
58
|
+
useRefreshTriggers(topic, onRefresh);
|
|
59
|
+
return () => h("div");
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
app.mount(document.createElement("div"));
|
|
64
|
+
mounted.push(app);
|
|
65
|
+
return onRefresh;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function install(
|
|
69
|
+
options: Parameters<typeof createPageLiveController>[1] = {},
|
|
70
|
+
start?: Parameters<typeof createPageLiveController>[2],
|
|
71
|
+
pageId = PAGE,
|
|
72
|
+
): PageLiveController {
|
|
73
|
+
const controller = createPageLiveController(pageId, options, start);
|
|
74
|
+
controllers.push(controller);
|
|
75
|
+
setCurrentPageLive(controller);
|
|
76
|
+
return controller;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function setVisibility(state: "visible" | "hidden") {
|
|
80
|
+
Object.defineProperty(document, "visibilityState", {
|
|
81
|
+
configurable: true,
|
|
82
|
+
get: () => state,
|
|
83
|
+
});
|
|
84
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
vi.useFakeTimers();
|
|
89
|
+
topicHandlers.clear();
|
|
90
|
+
window.localStorage.clear();
|
|
91
|
+
window.history.replaceState(null, "", "/");
|
|
92
|
+
setVisibility("visible");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
mounted.splice(0).forEach((app) => app.unmount());
|
|
97
|
+
controllers.splice(0).forEach((controller) => controller.dispose());
|
|
98
|
+
setCurrentPageLive(null);
|
|
99
|
+
vi.useRealTimers();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("the pause counter", () => {
|
|
103
|
+
// The dms-api regression: the capture publishes one message per 1.5 s with
|
|
104
|
+
// the number of requests it stands for; 40 requests over two messages read
|
|
105
|
+
// "2 new" before.
|
|
106
|
+
it("counts the requests a batch announces, not the messages", () => {
|
|
107
|
+
const live = install({ maxRate: 5 });
|
|
108
|
+
const table = mountBlock();
|
|
109
|
+
mountBlock();
|
|
110
|
+
live.pause();
|
|
111
|
+
|
|
112
|
+
publish({ count: 25, server5xx: 2, tester: 0 });
|
|
113
|
+
publish({ count: 15, server5xx: 0, tester: 1 });
|
|
114
|
+
|
|
115
|
+
expect(live.missedCount.value).toBe(40);
|
|
116
|
+
live.resume();
|
|
117
|
+
expect(table).toHaveBeenCalledTimes(1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("counts each published row once, across messages", () => {
|
|
121
|
+
const live = install();
|
|
122
|
+
mountBlock(ROW_TOPIC);
|
|
123
|
+
live.pause();
|
|
124
|
+
|
|
125
|
+
publish({ ids: ["a", "b", "c"] }, ROW_TOPIC);
|
|
126
|
+
publish({ ids: ["b", "d"] }, ROW_TOPIC);
|
|
127
|
+
|
|
128
|
+
expect(live.missedCount.value).toBe(4);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("counts a message without ids nor count as one", () => {
|
|
132
|
+
const tally = new MissedTally();
|
|
133
|
+
tally.add({ topic: TOPIC, payload: { count: 0 } });
|
|
134
|
+
tally.add({ topic: TOPIC, payload: { count: 2.5 } });
|
|
135
|
+
tally.add({ topic: TOPIC });
|
|
136
|
+
tally.add(undefined);
|
|
137
|
+
expect(tally.total.value).toBe(4);
|
|
138
|
+
tally.reset();
|
|
139
|
+
expect(tally.total.value).toBe(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("counts every event a pause cut short, not only the first", () => {
|
|
143
|
+
const live = install({ maxRate: 5 });
|
|
144
|
+
mountBlock();
|
|
145
|
+
publish({ count: 1 });
|
|
146
|
+
publish({ count: 3 });
|
|
147
|
+
publish({ count: 4 });
|
|
148
|
+
live.pause();
|
|
149
|
+
expect(live.missedCount.value).toBe(7);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("holds", () => {
|
|
154
|
+
it("pauses with a reason while a component holds, and resumes on release", () => {
|
|
155
|
+
const live = install();
|
|
156
|
+
const block = mountBlock();
|
|
157
|
+
const tester = usePageLive();
|
|
158
|
+
|
|
159
|
+
tester.hold("$api.live.typing_in_tester");
|
|
160
|
+
expect(live.isPaused.value).toBe(true);
|
|
161
|
+
expect(live.pauseReason.value).toEqual({
|
|
162
|
+
text: "$api.live.typing_in_tester",
|
|
163
|
+
});
|
|
164
|
+
publish({ count: 2 });
|
|
165
|
+
expect(block).not.toHaveBeenCalled();
|
|
166
|
+
|
|
167
|
+
tester.release();
|
|
168
|
+
expect(live.isPaused.value).toBe(false);
|
|
169
|
+
expect(block).toHaveBeenCalledTimes(1);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("stays paused on release when the user paused", () => {
|
|
173
|
+
const live = install();
|
|
174
|
+
const tester = usePageLive();
|
|
175
|
+
tester.hold("typing");
|
|
176
|
+
live.pause();
|
|
177
|
+
expect(live.pauseReason.value).toBeNull();
|
|
178
|
+
tester.release();
|
|
179
|
+
expect(live.isPaused.value).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("lets the user resume over a hold until it is taken anew", () => {
|
|
183
|
+
const live = install();
|
|
184
|
+
const tester = usePageLive();
|
|
185
|
+
tester.hold("typing");
|
|
186
|
+
live.resume();
|
|
187
|
+
expect(live.isPaused.value).toBe(false);
|
|
188
|
+
|
|
189
|
+
tester.hold("still typing");
|
|
190
|
+
expect(live.isPaused.value).toBe(false);
|
|
191
|
+
|
|
192
|
+
tester.release();
|
|
193
|
+
tester.hold("typing again");
|
|
194
|
+
expect(live.isPaused.value).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("releases a hold when its component goes", () => {
|
|
198
|
+
const live = install();
|
|
199
|
+
const app = createApp(
|
|
200
|
+
defineComponent({
|
|
201
|
+
setup() {
|
|
202
|
+
usePageLive().hold("typing");
|
|
203
|
+
return () => h("div");
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
app.mount(document.createElement("div"));
|
|
208
|
+
expect(live.isPaused.value).toBe(true);
|
|
209
|
+
app.unmount();
|
|
210
|
+
expect(live.isPaused.value).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("keeps a module's breakdown of the missed events until resume", () => {
|
|
214
|
+
const live = install();
|
|
215
|
+
const state = usePageLive();
|
|
216
|
+
live.pause();
|
|
217
|
+
state.setMissedDetail({
|
|
218
|
+
text: "$api.live.in_5xx",
|
|
219
|
+
params: { count: 2 },
|
|
220
|
+
tone: "error",
|
|
221
|
+
});
|
|
222
|
+
expect(state.missedDetail.value?.tone).toBe("error");
|
|
223
|
+
live.resume();
|
|
224
|
+
expect(state.missedDetail.value).toBeNull();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("the browser around the page", () => {
|
|
229
|
+
it("pauses while the tab is hidden and resumes on return", () => {
|
|
230
|
+
const live = install();
|
|
231
|
+
const block = mountBlock();
|
|
232
|
+
const unbind = bindPageLiveEnvironment(live, {});
|
|
233
|
+
|
|
234
|
+
setVisibility("hidden");
|
|
235
|
+
expect(live.isPaused.value).toBe(true);
|
|
236
|
+
expect(live.pauseReason.value?.text).toBe("$dms.live.reason_hidden");
|
|
237
|
+
publish();
|
|
238
|
+
|
|
239
|
+
setVisibility("visible");
|
|
240
|
+
expect(live.isPaused.value).toBe(false);
|
|
241
|
+
expect(block).toHaveBeenCalledTimes(1);
|
|
242
|
+
unbind();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("stays paused on return when the user had paused", () => {
|
|
246
|
+
const live = install();
|
|
247
|
+
const unbind = bindPageLiveEnvironment(live, {});
|
|
248
|
+
live.pause();
|
|
249
|
+
setVisibility("hidden");
|
|
250
|
+
setVisibility("visible");
|
|
251
|
+
expect(live.isPaused.value).toBe(true);
|
|
252
|
+
unbind();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("opens paused with ?live=0, unremembered, and drops it on resume", async () => {
|
|
256
|
+
window.history.replaceState(null, "", "/logs?live=0&tab=5xx");
|
|
257
|
+
const { isPausedByUrl } = await import(
|
|
258
|
+
"../layers/dms-ui/app/build/composables/live/usePageLiveHost"
|
|
259
|
+
);
|
|
260
|
+
const live = install({}, { isPausedByUrl: isPausedByUrl() });
|
|
261
|
+
const unbind = bindPageLiveEnvironment(live, {});
|
|
262
|
+
expect(live.isPaused.value).toBe(true);
|
|
263
|
+
expect(window.localStorage.length).toBe(0);
|
|
264
|
+
|
|
265
|
+
live.resume();
|
|
266
|
+
await nextTick();
|
|
267
|
+
expect(window.location.search).toBe("?tab=5xx");
|
|
268
|
+
unbind();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("shares the remembered choice between the pages of one remember key", () => {
|
|
272
|
+
install({ rememberKey: "api" }).pause();
|
|
273
|
+
expect(
|
|
274
|
+
install({ rememberKey: "api" }, {}, "modules.api.routes").isPaused.value,
|
|
275
|
+
).toBe(true);
|
|
276
|
+
expect(install({}, {}, "modules.api.routes").isPaused.value).toBe(false);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("applies the remembered pause only once the page is mounted", async () => {
|
|
280
|
+
window.localStorage.setItem(`dms.live.paused:${PAGE}`, "1");
|
|
281
|
+
let pausedAtSetup: boolean | undefined;
|
|
282
|
+
const app = createApp(
|
|
283
|
+
defineComponent({
|
|
284
|
+
setup() {
|
|
285
|
+
usePageLiveHost(() => ({ pageId: PAGE, live: {} }));
|
|
286
|
+
pausedAtSetup = currentPageLive()?.isPaused.value;
|
|
287
|
+
return () => h("div");
|
|
288
|
+
},
|
|
289
|
+
}),
|
|
290
|
+
);
|
|
291
|
+
app.mount(document.createElement("div"));
|
|
292
|
+
await nextTick();
|
|
293
|
+
expect(pausedAtSetup).toBe(false);
|
|
294
|
+
expect(currentPageLive()?.isPaused.value).toBe(true);
|
|
295
|
+
app.unmount();
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("the cadence", () => {
|
|
300
|
+
it("polls at maxRate while the stream is down, not while paused", () => {
|
|
301
|
+
const poll = vi.fn();
|
|
302
|
+
const live = install({ maxRate: 5 }, { poll });
|
|
303
|
+
live.setStreamConnected(false);
|
|
304
|
+
expect(live.isPolling.value).toBe(true);
|
|
305
|
+
|
|
306
|
+
vi.advanceTimersByTime(5000);
|
|
307
|
+
vi.advanceTimersByTime(5000);
|
|
308
|
+
expect(poll).toHaveBeenCalledTimes(2);
|
|
309
|
+
|
|
310
|
+
live.pause();
|
|
311
|
+
vi.advanceTimersByTime(20_000);
|
|
312
|
+
expect(poll).toHaveBeenCalledTimes(2);
|
|
313
|
+
|
|
314
|
+
live.resume();
|
|
315
|
+
live.setStreamConnected(true);
|
|
316
|
+
vi.advanceTimersByTime(20_000);
|
|
317
|
+
expect(poll).toHaveBeenCalledTimes(2);
|
|
318
|
+
expect(live.isPolling.value).toBe(false);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("does not poll a page without a cadence", () => {
|
|
322
|
+
const poll = vi.fn();
|
|
323
|
+
const live = install({}, { poll });
|
|
324
|
+
live.setStreamConnected(false);
|
|
325
|
+
vi.advanceTimersByTime(60_000);
|
|
326
|
+
expect(poll).not.toHaveBeenCalled();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("slows down once the period shown reaches slowRateAfter", () => {
|
|
330
|
+
const options = {
|
|
331
|
+
maxRate: 5,
|
|
332
|
+
slowRateAfter: { period: "7d" as const, maxRate: 60 },
|
|
333
|
+
};
|
|
334
|
+
expect(livePeriodMs("7d")).toBe(7 * DAY_MS);
|
|
335
|
+
expect(livePeriodMs("30m")).toBe(30 * 60 * 1000);
|
|
336
|
+
expect(livePeriodMs("week")).toBeNull();
|
|
337
|
+
expect(liveIntervalMs(options, null)).toBe(5000);
|
|
338
|
+
expect(liveIntervalMs(options, DAY_MS)).toBe(5000);
|
|
339
|
+
expect(liveIntervalMs(options, 7 * DAY_MS)).toBe(60_000);
|
|
340
|
+
expect(liveIntervalMs(options, Number.POSITIVE_INFINITY)).toBe(60_000);
|
|
341
|
+
|
|
342
|
+
const poll = vi.fn();
|
|
343
|
+
const live = install(options, { poll });
|
|
344
|
+
live.reportPeriodSpan(Symbol("period"), 30 * DAY_MS);
|
|
345
|
+
live.setStreamConnected(false);
|
|
346
|
+
vi.advanceTimersByTime(59_000);
|
|
347
|
+
expect(poll).not.toHaveBeenCalled();
|
|
348
|
+
vi.advanceTimersByTime(1000);
|
|
349
|
+
expect(poll).toHaveBeenCalledTimes(1);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("gates a block at the slower cadence over a long period", () => {
|
|
353
|
+
const live = install({
|
|
354
|
+
maxRate: 5,
|
|
355
|
+
slowRateAfter: { period: "7d", maxRate: 60 },
|
|
356
|
+
});
|
|
357
|
+
const block = mountBlock();
|
|
358
|
+
const span = ref(7 * DAY_MS);
|
|
359
|
+
live.reportPeriodSpan(Symbol("period"), span.value);
|
|
360
|
+
publish();
|
|
361
|
+
publish();
|
|
362
|
+
vi.advanceTimersByTime(5000);
|
|
363
|
+
expect(block).toHaveBeenCalledTimes(1);
|
|
364
|
+
vi.advanceTimersByTime(55_000);
|
|
365
|
+
expect(block).toHaveBeenCalledTimes(2);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
effectScope,
|
|
5
|
+
nextTick,
|
|
6
|
+
onScopeDispose,
|
|
7
|
+
ref,
|
|
8
|
+
toValue,
|
|
9
|
+
watch,
|
|
10
|
+
type EffectScope,
|
|
11
|
+
} from "vue";
|
|
12
|
+
import type { UserRealtimeApi } from "../layers/dms-core/app/composables/realtime/useUserRealtime";
|
|
13
|
+
|
|
14
|
+
const BASE_URL = "http://api";
|
|
15
|
+
const USER_STREAM_URL = `${BASE_URL}/api/realtime/user`;
|
|
16
|
+
const SUBSCRIBE_URL_PREFIX = `${BASE_URL}/api/realtime/subscribe/`;
|
|
17
|
+
const UNSUBSCRIBE_URL = `${BASE_URL}/api/realtime/unsubscribe`;
|
|
18
|
+
const PAGE_ID = "lang.translations";
|
|
19
|
+
const OTHER_PAGE_ID = "lang.overview";
|
|
20
|
+
const SESSION_ID = "session-1";
|
|
21
|
+
|
|
22
|
+
type PageCall = () => void;
|
|
23
|
+
|
|
24
|
+
interface FakeServer {
|
|
25
|
+
/** The page the server holds the session's topics for. */
|
|
26
|
+
page: string | null;
|
|
27
|
+
isHoldingCalls: boolean;
|
|
28
|
+
heldCalls: PageCall[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const encoder = new TextEncoder();
|
|
32
|
+
|
|
33
|
+
const openUserStream = () => {
|
|
34
|
+
const body = new ReadableStream<Uint8Array>({
|
|
35
|
+
start(controller) {
|
|
36
|
+
controller.enqueue(
|
|
37
|
+
encoder.encode(
|
|
38
|
+
`event: hello\ndata: ${JSON.stringify({ sessionId: SESSION_ID })}\n\n`,
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
return { ok: true, status: 200, body };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const applyPageCall = (server: FakeServer, url: string): void => {
|
|
47
|
+
server.page =
|
|
48
|
+
url === UNSUBSCRIBE_URL ? null : url.slice(SUBSCRIBE_URL_PREFIX.length);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const installFetch = (server: FakeServer): void => {
|
|
52
|
+
globalThis.fetch = vi.fn(async (url: string) => {
|
|
53
|
+
if (url === USER_STREAM_URL) return openUserStream();
|
|
54
|
+
if (!server.isHoldingCalls) {
|
|
55
|
+
applyPageCall(server, url);
|
|
56
|
+
return { ok: true, status: 200 };
|
|
57
|
+
}
|
|
58
|
+
await new Promise<void>((resolve) =>
|
|
59
|
+
server.heldCalls.push(() => {
|
|
60
|
+
applyPageCall(server, url);
|
|
61
|
+
resolve();
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
return { ok: true, status: 200 };
|
|
65
|
+
}) as unknown as typeof fetch;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const settle = async (): Promise<void> => {
|
|
69
|
+
for (let round = 0; round < 5; round += 1) {
|
|
70
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
71
|
+
await nextTick();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const realFetch = globalThis.fetch;
|
|
76
|
+
let loggedIn = ref(false);
|
|
77
|
+
let server: FakeServer;
|
|
78
|
+
let user: UserRealtimeApi;
|
|
79
|
+
let mountPage: (pageId: string) => EffectScope;
|
|
80
|
+
|
|
81
|
+
const releaseHeldCalls = async (): Promise<void> => {
|
|
82
|
+
server.isHoldingCalls = false;
|
|
83
|
+
while (server.heldCalls.length) {
|
|
84
|
+
server.heldCalls.shift()!();
|
|
85
|
+
await settle();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
beforeEach(async () => {
|
|
90
|
+
vi.resetModules();
|
|
91
|
+
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
|
92
|
+
vi.stubEnv("SSR", false);
|
|
93
|
+
vi.stubGlobal("ref", ref);
|
|
94
|
+
vi.stubGlobal("watch", watch);
|
|
95
|
+
vi.stubGlobal("computed", computed);
|
|
96
|
+
vi.stubGlobal("toValue", toValue);
|
|
97
|
+
vi.stubGlobal("onScopeDispose", onScopeDispose);
|
|
98
|
+
vi.stubGlobal("useDmsState", (_key: string, init: () => unknown) =>
|
|
99
|
+
ref(init()),
|
|
100
|
+
);
|
|
101
|
+
vi.stubGlobal("useDmsRuntimeConfig", () => ({
|
|
102
|
+
public: { dms: { baseURL: BASE_URL } },
|
|
103
|
+
}));
|
|
104
|
+
vi.stubGlobal("useUserSession", () => ({ loggedIn }));
|
|
105
|
+
vi.stubGlobal("useSessionRecovery", () => ({
|
|
106
|
+
refreshSession: async () => true,
|
|
107
|
+
redirectToAuth: async () => {},
|
|
108
|
+
}));
|
|
109
|
+
server = { page: null, isHoldingCalls: false, heldCalls: [] };
|
|
110
|
+
installFetch(server);
|
|
111
|
+
loggedIn = ref(true);
|
|
112
|
+
const userModule = await import(
|
|
113
|
+
"../layers/dms-core/app/composables/realtime/useUserRealtime"
|
|
114
|
+
);
|
|
115
|
+
vi.stubGlobal("useUserRealtime", userModule.useUserRealtime);
|
|
116
|
+
const pageModule = await import(
|
|
117
|
+
"../layers/dms-core/app/composables/realtime/usePageRealtime"
|
|
118
|
+
);
|
|
119
|
+
user = userModule.useUserRealtime();
|
|
120
|
+
mountPage = (pageId) => {
|
|
121
|
+
const scope = effectScope(true);
|
|
122
|
+
scope.run(() => pageModule.usePageRealtime(pageId));
|
|
123
|
+
return scope;
|
|
124
|
+
};
|
|
125
|
+
await settle();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
afterEach(async () => {
|
|
129
|
+
loggedIn.value = false;
|
|
130
|
+
await settle();
|
|
131
|
+
globalThis.fetch = realFetch;
|
|
132
|
+
vi.useRealTimers();
|
|
133
|
+
vi.unstubAllGlobals();
|
|
134
|
+
vi.unstubAllEnvs();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("usePageRealtime remount at the same path", () => {
|
|
138
|
+
it("keeps the page when the new instance mounts before the old one unmounts", async () => {
|
|
139
|
+
const previous = mountPage(PAGE_ID);
|
|
140
|
+
await settle();
|
|
141
|
+
expect(server.page).toBe(PAGE_ID);
|
|
142
|
+
|
|
143
|
+
const next = mountPage(PAGE_ID);
|
|
144
|
+
previous.stop();
|
|
145
|
+
await settle();
|
|
146
|
+
|
|
147
|
+
expect(user.pageId.value).toBe(PAGE_ID);
|
|
148
|
+
expect(server.page).toBe(PAGE_ID);
|
|
149
|
+
next.stop();
|
|
150
|
+
await settle();
|
|
151
|
+
expect(server.page).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("re-subscribes when the new instance mounts while the old unsubscribe is in flight", async () => {
|
|
155
|
+
const previous = mountPage(PAGE_ID);
|
|
156
|
+
await settle();
|
|
157
|
+
|
|
158
|
+
server.isHoldingCalls = true;
|
|
159
|
+
previous.stop();
|
|
160
|
+
await settle();
|
|
161
|
+
const next = mountPage(PAGE_ID);
|
|
162
|
+
await settle();
|
|
163
|
+
await releaseHeldCalls();
|
|
164
|
+
|
|
165
|
+
expect(user.pageId.value).toBe(PAGE_ID);
|
|
166
|
+
expect(server.page).toBe(PAGE_ID);
|
|
167
|
+
next.stop();
|
|
168
|
+
await settle();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("keeps the new page when navigating away unmounts the old one last", async () => {
|
|
172
|
+
const previous = mountPage(OTHER_PAGE_ID);
|
|
173
|
+
await settle();
|
|
174
|
+
|
|
175
|
+
const next = mountPage(PAGE_ID);
|
|
176
|
+
await settle();
|
|
177
|
+
previous.stop();
|
|
178
|
+
await settle();
|
|
179
|
+
|
|
180
|
+
expect(user.pageId.value).toBe(PAGE_ID);
|
|
181
|
+
expect(server.page).toBe(PAGE_ID);
|
|
182
|
+
next.stop();
|
|
183
|
+
await settle();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a PeriodSelector offers besides its presets: every period (`all`, no
|
|
3
|
+
* bounds sent), a window labelled relative to now ("since yesterday 11:52"),
|
|
4
|
+
* and a comparison drawn as a menu, a toggle or nothing.
|
|
5
|
+
*/
|
|
6
|
+
import { effectScope } from "vue";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { resolvePresetRange } from "../layers/dms-core/app/composables/period/presetResolvers";
|
|
9
|
+
import { resolveComparisonRange } from "../layers/dms-core/app/composables/period/compareResolvers";
|
|
10
|
+
import { usePeriod } from "../layers/dms-core/app/composables/period/usePeriod";
|
|
11
|
+
import { appendPeriodToUrl } from "../layers/dms-core/app/composables/period/usePeriodScope";
|
|
12
|
+
import { usePeriodDraft } from "../layers/dms-ui/app/build/composables/period/usePeriodDraft";
|
|
13
|
+
import { formatRelativeInstant } from "../layers/dms-ui/app/build/composables/period/periodDisplay";
|
|
14
|
+
import { resolveComparisonControl } from "../layers/dms-ui/app/build/composables/period/periodComparisonControl";
|
|
15
|
+
|
|
16
|
+
const NOW = new Date(2026, 9, 11, 19, 54);
|
|
17
|
+
const FALLBACK = { from: new Date(2026, 0, 1), to: new Date(2026, 0, 31) };
|
|
18
|
+
|
|
19
|
+
function allTimePeriod() {
|
|
20
|
+
const scope = effectScope();
|
|
21
|
+
const result = scope.run(() => {
|
|
22
|
+
const period = usePeriod({
|
|
23
|
+
defaultPreset: "all",
|
|
24
|
+
defaultComparison: "previous-period",
|
|
25
|
+
presets: ["last-24h", "all", "custom"],
|
|
26
|
+
});
|
|
27
|
+
return { period, draft: usePeriodDraft(period) };
|
|
28
|
+
});
|
|
29
|
+
if (!result) throw new Error("scope did not run");
|
|
30
|
+
return { ...result, stop: () => scope.stop() };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("every period", () => {
|
|
34
|
+
it("resolves to no bounds, and nothing to compare with", () => {
|
|
35
|
+
expect(resolvePresetRange("all", NOW, FALLBACK)).toBeNull();
|
|
36
|
+
expect(resolveComparisonRange("previous-period", null, null)).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("is published with no range, and sends no from / to", () => {
|
|
40
|
+
const { period, stop } = allTimePeriod();
|
|
41
|
+
const { state } = period;
|
|
42
|
+
expect(state.value.range).toBeNull();
|
|
43
|
+
expect(state.value.compareRange).toBeNull();
|
|
44
|
+
|
|
45
|
+
const url = appendPeriodToUrl("/api/requests?route=1", state.value)!;
|
|
46
|
+
const params = new URL(url, "http://dms.local").searchParams;
|
|
47
|
+
expect(params.has("from")).toBe(false);
|
|
48
|
+
expect(params.has("to")).toBe(false);
|
|
49
|
+
expect(params.has("compareFrom")).toBe(false);
|
|
50
|
+
expect(params.get("preset")).toBe("all");
|
|
51
|
+
expect(params.get("route")).toBe("1");
|
|
52
|
+
stop();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("starts a custom range from today in the popover", () => {
|
|
56
|
+
const { draft, stop } = allTimePeriod();
|
|
57
|
+
expect(draft.range.value).toBeNull();
|
|
58
|
+
draft.selectPreset("custom");
|
|
59
|
+
const today = new Date();
|
|
60
|
+
expect(draft.range.value!.from.getDate()).toBe(today.getDate());
|
|
61
|
+
expect(draft.range.value!.to.getHours()).toBe(23);
|
|
62
|
+
stop();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("a window labelled relative to now", () => {
|
|
67
|
+
it("gives the time alone today", () => {
|
|
68
|
+
const start = new Date(2026, 9, 11, 11, 52);
|
|
69
|
+
expect(formatRelativeInstant(start, NOW, "en-GB")).toBe("11:52");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("says yesterday in the reader's language", () => {
|
|
73
|
+
const start = new Date(2026, 9, 10, 11, 52);
|
|
74
|
+
expect(formatRelativeInstant(start, NOW, "en-GB")).toBe("yesterday 11:52");
|
|
75
|
+
expect(formatRelativeInstant(start, NOW, "fr-FR")).toBe("hier 11:52");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("gives the weekday within a week", () => {
|
|
79
|
+
const start = new Date(2026, 9, 7, 11, 52);
|
|
80
|
+
expect(formatRelativeInstant(start, NOW, "en-GB")).toMatch(/^Wed 11:52$/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("gives the date beyond a week, and its year when not this one's", () => {
|
|
84
|
+
expect(
|
|
85
|
+
formatRelativeInstant(new Date(2026, 9, 4, 11, 52), NOW, "en-US"),
|
|
86
|
+
).toBe("Oct 4, 11:52 AM");
|
|
87
|
+
expect(
|
|
88
|
+
formatRelativeInstant(new Date(2025, 9, 4, 11, 52), NOW, "en-US"),
|
|
89
|
+
).toContain("2025");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("the comparison control", () => {
|
|
94
|
+
it("is a menu of several comparisons by default", () => {
|
|
95
|
+
expect(resolveComparisonControl({}).control).toBe("menu");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("is a toggle when a single comparison is offered, none accepted too", () => {
|
|
99
|
+
const resolved = resolveComparisonControl({
|
|
100
|
+
comparisons: ["previous-period"],
|
|
101
|
+
});
|
|
102
|
+
expect(resolved).toMatchObject({
|
|
103
|
+
control: "toggle",
|
|
104
|
+
toggled: "previous-period",
|
|
105
|
+
comparisons: ["none", "previous-period"],
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("toggles the default comparison when asked for a toggle among several", () => {
|
|
110
|
+
const resolved = resolveComparisonControl({
|
|
111
|
+
comparisonControl: "toggle",
|
|
112
|
+
defaultComparison: "previous-year",
|
|
113
|
+
});
|
|
114
|
+
expect(resolved.toggled).toBe("previous-year");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("keeps the comparison on when hidden", () => {
|
|
118
|
+
const resolved = resolveComparisonControl({
|
|
119
|
+
comparisonControl: "hidden",
|
|
120
|
+
comparisons: ["previous-period"],
|
|
121
|
+
});
|
|
122
|
+
expect(resolved).toMatchObject({
|
|
123
|
+
control: "hidden",
|
|
124
|
+
toggled: null,
|
|
125
|
+
defaultComparison: "previous-period",
|
|
126
|
+
comparisons: ["previous-period"],
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("draws nothing when nothing but none is offered", () => {
|
|
131
|
+
expect(
|
|
132
|
+
resolveComparisonControl({
|
|
133
|
+
comparisons: ["none"],
|
|
134
|
+
comparisonControl: "menu",
|
|
135
|
+
}).control,
|
|
136
|
+
).toBe("hidden");
|
|
137
|
+
});
|
|
138
|
+
});
|