diff --git a/src/Frontend/src/components/audit/AuditList.spec.ts b/src/Frontend/src/components/audit/AuditList.spec.ts index 6a1d98384..622a6ca16 100644 --- a/src/Frontend/src/components/audit/AuditList.spec.ts +++ b/src/Frontend/src/components/audit/AuditList.spec.ts @@ -5,6 +5,7 @@ import { createRouter, createMemoryHistory } from "vue-router"; import { ref, shallowReadonly, nextTick, type Ref } from "vue"; import { flushPromises } from "@vue/test-utils"; import AuditList from "@/components/audit/AuditList.vue"; +import { useAuditStore } from "@/stores/AuditStore"; import { type default as Message, MessageStatus } from "@/resources/Message"; /** @@ -47,6 +48,8 @@ interface QueryStateAssertions { interface RenderResult { verify: QueryStateAssertions; isRefreshing: Ref; + refreshNow: ReturnType; + store: ReturnType; } // ==================== DOM Query Helpers ==================== @@ -104,9 +107,10 @@ function createMessage(id = "msg-1"): Message { async function renderAuditList(messages: Message[] = []): Promise { const isRefreshing = ref(false); + const refreshNow = vi.fn().mockResolvedValue(undefined); vi.mocked(useFetchWithAutoRefresh).mockReturnValue({ - refreshNow: vi.fn().mockResolvedValue(undefined), + refreshNow, isRefreshing: shallowReadonly(isRefreshing), updateInterval: vi.fn(), isActive: ref(false), @@ -176,7 +180,18 @@ async function renderAuditList(messages: Message[] = []): Promise }, }; - return { verify, isRefreshing }; + return { verify, isRefreshing, refreshNow, store: useAuditStore(pinia) }; +} + +// A control change reaches the fetch via two async hops (controls watcher -> router.push -> route watcher), +// so settle the microtask queue a few times before counting queries. +async function waitForRouteDrivenQuery() { + await flushPromises(); + await nextTick(); + await flushPromises(); + await nextTick(); + await flushPromises(); + await nextTick(); } async function waitForFirstLoadToComplete() { @@ -298,4 +313,30 @@ describe("FEATURE: Audit Messages Query State", () => { verify.overlayIsNotVisible(); }); }); + + describe("RULE: A query-control change results in exactly one query", () => { + test("EXAMPLE: Changing the filter text fires a single query", async () => { + const { refreshNow, store } = await renderAuditList([createMessage()]); + + await waitForFirstLoadToComplete(); + const queriesAfterFirstLoad = refreshNow.mock.calls.length; + + store.messageFilterString = "orders"; + await waitForRouteDrivenQuery(); + + expect(refreshNow.mock.calls.length - queriesAfterFirstLoad).toBe(1); + }); + + test("EXAMPLE: Changing the endpoint fires a single query", async () => { + const { refreshNow, store } = await renderAuditList([createMessage()]); + + await waitForFirstLoadToComplete(); + const queriesAfterFirstLoad = refreshNow.mock.calls.length; + + store.selectedEndpointName = "Sales.Endpoint"; + await waitForRouteDrivenQuery(); + + expect(refreshNow.mock.calls.length - queriesAfterFirstLoad).toBe(1); + }); + }); }); diff --git a/src/Frontend/src/components/audit/AuditList.vue b/src/Frontend/src/components/audit/AuditList.vue index 134c2e48b..19dbd77a7 100644 --- a/src/Frontend/src/components/audit/AuditList.vue +++ b/src/Frontend/src/components/audit/AuditList.vue @@ -72,16 +72,18 @@ onBeforeMount(() => { }, 0); }); +// The route is the single source of truth for the query: control changes only push to the router, +// and only a route change triggers a fetch. Having the fetch in both watchers (and the route in the +// controls watcher) made a single control change fire the same query up to three times. watch( - () => router.currentRoute.value.query, + () => route.query, async () => { setQuery(); await refreshNow(); - }, - { deep: true } + } ); -const watchHandle = watch([() => route.query, itemsPerPage, sortBy, messageFilterString, selectedEndpointName, dateRange], async () => { +const watchHandle = watch([itemsPerPage, sortBy, messageFilterString, selectedEndpointName, dateRange], async () => { if (firstLoad.value) { return; } @@ -101,8 +103,6 @@ const watchHandle = watch([() => route.query, itemsPerPage, sortBy, messageFilte pageSize: itemsPerPage.value, }, }); - - await refreshNow(); }); function setQuery() {