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", () => {