From da2d2630de7f16b377eb6faf6b5f965c711504c5 Mon Sep 17 00:00:00 2001 From: AJ Slater Date: Wed, 2 Sep 2026 00:20:38 -0700 Subject: [PATCH] Derive stats tab section titles from the component d88f4f110 split the "User Settings" table into "Settings by Session" and "Settings by User", but the spec kept its own copy of the section titles and still asserted the old name, so it failed. Rather than retype the new names into the test, the tab now declares its tables once as a SECTIONS list the template loops over, and exports the titles. The spec compares the rendered captions to that export, so a rename cannot leave a stale copy behind again. The assertion is the whole ordered caption list rather than a containment check per title. With the titles derived, containment would pass vacuously; comparing the list still catches a section that stops rendering, one rendered twice, or an empty title. Co-Authored-By: Claude Opus 5 --- .../src/components/admin/tabs/stats-tab.vue | 69 ++++++++++--------- frontend/tests/unit/stats-tab.test.js | 36 ++++------ 2 files changed, 51 insertions(+), 54 deletions(-) diff --git a/frontend/src/components/admin/tabs/stats-tab.vue b/frontend/src/components/admin/tabs/stats-tab.vue index 094d69f2b..466a2fa4f 100644 --- a/frontend/src/components/admin/tabs/stats-tab.vue +++ b/frontend/src/components/admin/tabs/stats-tab.vue @@ -1,38 +1,11 @@ @@ -174,6 +147,34 @@ const INDENT_KEYS = Object.freeze( const SET_SUFFIXES = ["Set", "Custom", "Configured", "Credentials"]; const NONE = "None"; +// Every stats table, in render order: the caption it wears and the computed +// that fills it. Exported so a test enumerates the sections from here instead +// of keeping a second copy of the titles that goes stale when one is renamed. +const SECTIONS = Object.freeze([ + ["Platform", "platformTable"], + // The API key is deliberately absent: the payload still carries + // ``stats.config.apiKey``, but configTable drops it from the rendered keys. + // The key itself and its regenerate button live on the Settings tab. + ["Config", "configTable"], + ["File Types", "fileTypesTable"], + // Two denominators, named apart. Sessions counts settings rows, one per user + // and per anonymous session; per user counts people, one vote each. Titling + // either of them "User Settings" invited reading one as the other. + ["Settings by Session", "sessionSettingsTable"], + ["Settings by User", "perUserSettingsTable"], + ["Browser Collections", "browserCollectionsTable"], + ["Tags", "metadataTable"], + ["Reading", "usageTable"], + ["Identifiers", "identifiersTable"], + ["Admin Flags", "adminFlagsTable"], + ["Online Tagging", "taggingTable"], + ["Authentication", "authTable"], + ["Email", "emailTable"], + ["Rate Limits", "throttleTable"], + ["Deployment", "deploymentTable"], +]); +export const SECTION_TITLES = Object.freeze(SECTIONS.map(([title]) => title)); + export default { name: "AdminStatsTab", components: { @@ -185,6 +186,12 @@ export default { }; }, computed: { + sections() { + return SECTIONS.map(([title, items]) => ({ + title, + items: this[items], + })); + }, ...mapState(useCommonStore, {}), ...mapState(useAdminStore, { stats: (state) => state.stats, diff --git a/frontend/tests/unit/stats-tab.test.js b/frontend/tests/unit/stats-tab.test.js index 9a3207162..17fc9d297 100644 --- a/frontend/tests/unit/stats-tab.test.js +++ b/frontend/tests/unit/stats-tab.test.js @@ -3,7 +3,9 @@ * * The tab is what an administrator sees of the anonymous stats report, so * behavior locked in here: - * - Every section the API returns is rendered under a titled table. + * - Every section the component declares is rendered under a titled + * table. The titles come from the component so renaming one there + * does not leave a stale copy here. * - Toggle booleans read as Yes/No, and "have you configured this" * booleans read as Set/Not set, so nobody mistakes one for the other. * - The API key is never rendered, even though the payload carries it. @@ -12,7 +14,9 @@ import { createTestingPinia } from "@pinia/testing"; import { mount } from "@vue/test-utils"; import { describe, expect, test } from "vitest"; -import StatsTab from "@/components/admin/tabs/stats-tab.vue"; +import StatsTab, { + SECTION_TITLES, +} from "@/components/admin/tabs/stats-tab.vue"; import vuetify from "@/plugins/vuetify"; const STATS = { @@ -104,23 +108,6 @@ const STATS = { }, }; -const SECTION_TITLES = [ - "Platform", - "Config", - "File Types", - "User Settings", - "Browser Collections", - "Tags", - "Reading", - "Identifiers", - "Admin Flags", - "Online Tagging", - "Authentication", - "Email", - "Rate Limits", - "Deployment", -]; - function mountTab(stats = STATS) { const pinia = createTestingPinia({ initialState: { admin: { stats } } }); return mount(StatsTab, { @@ -130,10 +117,13 @@ function mountTab(stats = STATS) { describe("AdminStatsTab", () => { test("renders a table for every stats section", () => { - const text = mountTab().text(); - for (const title of SECTION_TITLES) { - expect(text).toContain(title); - } + const captions = mountTab() + .findAll(".adminKvCaption") + .map((caption) => caption.text()); + // The whole ordered list, not per-title containment: a section that stops + // rendering, one rendered twice, and an empty title all have to fail here, + // which a loop of toContain over titles taken from the component cannot. + expect(captions).toStrictEqual([...SECTION_TITLES]); }); test("never renders the api key", () => {