diff --git a/components/Onboarding/MeasuringCatalogPanel.tsx b/components/Onboarding/MeasuringCatalogPanel.tsx index 40f2325aa..550b44b82 100644 --- a/components/Onboarding/MeasuringCatalogPanel.tsx +++ b/components/Onboarding/MeasuringCatalogPanel.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { buttonVariants } from "@/components/ui/button"; import { cn } from "@/lib/utils"; +import { MEASURING_BODY, MEASURING_TITLE } from "@/lib/catalog/measuringCopy"; /** * Terminal state for `/setup/valuation` while a catalog exists but has no @@ -15,12 +16,9 @@ import { cn } from "@/lib/utils"; */ const MeasuringCatalogPanel = () => (
-

- Measuring your catalog -

+

{MEASURING_TITLE}

- We are pulling play counts for every track. This usually takes a minute. - Your baseline value appears here as soon as it is ready. + {MEASURING_BODY} Your baseline value appears here as soon as it is ready.

Set up your weekly report diff --git a/hooks/useAddSpotifyArtist.ts b/hooks/useAddSpotifyArtist.ts index 0631f2d2a..54534561c 100644 --- a/hooks/useAddSpotifyArtist.ts +++ b/hooks/useAddSpotifyArtist.ts @@ -6,6 +6,11 @@ import { useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { addSpotifyArtist } from "@/lib/artists/addSpotifyArtist"; import { runValuation } from "@/lib/valuation/runValuation"; +import { + MEASURING_TOAST_ERROR, + MEASURING_TOAST_SUCCESS, + measuringToastLoading, +} from "@/lib/catalog/measuringCopy"; import { useArtistProvider } from "@/providers/ArtistProvider"; import { useOrganization } from "@/providers/OrganizationProvider"; import useCatalogs from "@/hooks/useCatalogs"; @@ -56,10 +61,9 @@ export function useAddSpotifyArtist() { queryClient.invalidateQueries({ queryKey: ["catalogs"] }); }), { - loading: `Valuing ${artist.name}'s catalog…`, - success: "Your catalog is ready", - error: - "Couldn't value the catalog automatically — you can claim it later", + loading: measuringToastLoading(artist.name), + success: MEASURING_TOAST_SUCCESS, + error: MEASURING_TOAST_ERROR, }, ); } diff --git a/lib/catalog/__tests__/measuringCopy.test.ts b/lib/catalog/__tests__/measuringCopy.test.ts new file mode 100644 index 000000000..47b878c4d --- /dev/null +++ b/lib/catalog/__tests__/measuringCopy.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "vitest"; +import { + MEASURING_TITLE, + MEASURING_ESTIMATE, + MEASURING_BODY, + MEASURING_TOAST_SUCCESS, + MEASURING_TOAST_ERROR, + measuringToastLoading, +} from "@/lib/catalog/measuringCopy"; +import { getCatalogReportEmptyCopy } from "@/lib/catalog/getCatalogReportEmptyCopy"; + +describe("measuringCopy", () => { + // chat#1912 row 10: the same minute of waiting was described four different + // ways across the seeding toast, /setup/valuation, /catalogs/{id} and + // marketing, with three different time estimates. The estimate a customer + // gets should not depend on which surface they happen to be looking at. + it("states the time estimate once", () => { + expect(MEASURING_ESTIMATE).toContain("about a minute"); + expect(MEASURING_BODY).toContain(MEASURING_ESTIMATE); + }); + + it("is used by the catalog report's measuring state", () => { + const copy = getCatalogReportEmptyCopy("measuring", { hasOwnCatalogs: true }); + + expect(copy.title).toBe(MEASURING_TITLE); + expect(copy.body).toContain(MEASURING_ESTIMATE); + }); + + it("keeps the report's own reassurance without restating the estimate", () => { + const copy = getCatalogReportEmptyCopy("measuring", { hasOwnCatalogs: true }); + + expect(copy.body).toContain("no need to run the valuation again"); + // The estimate must appear once in the sentence, not twice. + expect(copy.body.split("about a minute").length - 1).toBe(1); + }); + + // Derived from the title rather than hard-coded, so the assertion fails if + // either side drifts — the point is that they agree, not that they are any + // particular word. + it("uses the same verb in the seeding toast as on the pages", () => { + const verb = MEASURING_TITLE.split(" ")[0]; + + expect(verb).toBe("Measuring"); + expect(measuringToastLoading("BennyJ504")).toBe(`${verb} BennyJ504's catalog…`); + }); + + it("states the toast outcomes", () => { + expect(MEASURING_TOAST_SUCCESS).toBe("Your catalog is ready"); + expect(MEASURING_TOAST_ERROR).toContain("claim it later"); + }); + + // House style: em dashes read as machine-written in product copy. + it("keeps the shared copy free of em dashes", () => { + const all = [MEASURING_TITLE, MEASURING_ESTIMATE, MEASURING_BODY, MEASURING_TOAST_SUCCESS, MEASURING_TOAST_ERROR].join(" "); + expect(all).not.toMatch(/[—–]/); + }); +}); diff --git a/lib/catalog/getCatalogReportEmptyCopy.ts b/lib/catalog/getCatalogReportEmptyCopy.ts index 91a627b16..7e0a132c6 100644 --- a/lib/catalog/getCatalogReportEmptyCopy.ts +++ b/lib/catalog/getCatalogReportEmptyCopy.ts @@ -1,4 +1,5 @@ import type { CatalogReportState } from "./getCatalogReportState"; +import { MEASURING_BODY, MEASURING_TITLE } from "./measuringCopy"; export type CatalogReportEmptyState = Exclude< CatalogReportState, @@ -24,8 +25,8 @@ const COPY: Record = { cta: { label: "Sign in", action: "login" }, }, measuring: { - title: "Measuring your catalog", - body: "We are pulling live play counts for every track. This usually takes about a minute, and the report appears here on its own. There is no need to run the valuation again.", + title: MEASURING_TITLE, + body: `${MEASURING_BODY} The report appears here on its own, so there is no need to run the valuation again.`, }, "other-account": { title: "This catalog was measured by another account", diff --git a/lib/catalog/measuringCopy.ts b/lib/catalog/measuringCopy.ts new file mode 100644 index 000000000..aee9878d4 --- /dev/null +++ b/lib/catalog/measuringCopy.ts @@ -0,0 +1,30 @@ +/** + * One description of one wait. + * + * The minute between seeding a catalog and its measurements landing is shown on + * three chat surfaces — the `/setup/artists` seeding toast, `/setup/valuation`, + * and the `/catalogs/{id}` report — and each had drifted into its own wording + * and its own time estimate ("a minute", "about a minute", "a minute or two"). + * The estimate a customer is given should not depend on which surface they + * happen to be looking at (chat#1912 row 10). + * + * Marketing keeps its own copy of these strings, since it is a separate + * deployment; the contract is that the text matches, not that the module is + * imported across repos. + */ +export const MEASURING_TITLE = "Measuring your catalog"; + +/** The single source for how long this takes. Change it here, nowhere else. */ +export const MEASURING_ESTIMATE = + "This usually takes about a minute, and longer for large catalogs."; + +export const MEASURING_BODY = `We are pulling live play counts for every track. ${MEASURING_ESTIMATE}`; + +/** Seeding runs in the background, so its toast uses the same verb as the pages. */ +export const measuringToastLoading = (artistName: string) => + `Measuring ${artistName}'s catalog…`; + +export const MEASURING_TOAST_SUCCESS = "Your catalog is ready"; + +export const MEASURING_TOAST_ERROR = + "Couldn't measure the catalog automatically, you can claim it later";