From a15585a8e6960cbed794bbffadb8e6d6dbdc1471 Mon Sep 17 00:00:00 2001 From: Scrubs McBarkley Date: Sun, 22 Mar 2026 00:12:57 +0000 Subject: [PATCH] fix: address QA feedback on site theming PR (GH #91) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix gradient regression in ReportCards.tsx: use distinct color stops (--color-accent-lighter → --color-accent-light) to restore subtle gradient - Fix BrandingContext meta tag accumulation: cache ref with useRef instead of querying DOM on every render to prevent duplicate elements on remount - Add BrandingContext.test.tsx: verify CSS vars applied, theme-color meta created/updated, and no duplicate meta tags on rerender Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/BrandingContext.tsx | 17 +-- .../src/__tests__/BrandingContext.test.tsx | 106 ++++++++++++++++++ apps/web/src/portal/sections/ReportCards.tsx | 2 +- 3 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/__tests__/BrandingContext.test.tsx diff --git a/apps/web/src/BrandingContext.tsx b/apps/web/src/BrandingContext.tsx index ec5fad3..1420e02 100644 --- a/apps/web/src/BrandingContext.tsx +++ b/apps/web/src/BrandingContext.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useEffect, useState, useCallback } from "react"; +import { createContext, useContext, useEffect, useRef, useState, useCallback } from "react"; export interface Branding { businessName: string; @@ -27,6 +27,7 @@ export function useBranding() { export function BrandingProvider({ children }: { children: React.ReactNode }) { const [branding, setBranding] = useState(DEFAULT_BRANDING); + const metaThemeColorRef = useRef(null); const fetchBranding = useCallback(() => { fetch("/api/branding") @@ -46,13 +47,15 @@ export function BrandingProvider({ children }: { children: React.ReactNode }) { document.documentElement.style.setProperty("--color-primary", branding.primaryColor); document.documentElement.style.setProperty("--color-accent", branding.accentColor); // Keep PWA theme-color meta tag in sync with primary color - let metaThemeColor = document.querySelector("meta[name='theme-color']"); - if (!metaThemeColor) { - metaThemeColor = document.createElement("meta"); - metaThemeColor.name = "theme-color"; - document.head.appendChild(metaThemeColor); + if (!metaThemeColorRef.current) { + metaThemeColorRef.current = document.querySelector("meta[name='theme-color']"); + if (!metaThemeColorRef.current) { + metaThemeColorRef.current = document.createElement("meta"); + metaThemeColorRef.current.name = "theme-color"; + document.head.appendChild(metaThemeColorRef.current); + } } - metaThemeColor.content = branding.primaryColor; + metaThemeColorRef.current.content = branding.primaryColor; }, [branding.primaryColor, branding.accentColor]); return ( diff --git a/apps/web/src/__tests__/BrandingContext.test.tsx b/apps/web/src/__tests__/BrandingContext.test.tsx new file mode 100644 index 0000000..2e7816c --- /dev/null +++ b/apps/web/src/__tests__/BrandingContext.test.tsx @@ -0,0 +1,106 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, waitFor } from "@testing-library/react"; +import { BrandingProvider, useBranding } from "../BrandingContext.js"; + +function BrandingConsumer() { + const { branding } = useBranding(); + return ( +
+ {branding.primaryColor} + {branding.accentColor} +
+ ); +} + +beforeEach(() => { + vi.restoreAllMocks(); + document.documentElement.style.removeProperty("--color-primary"); + document.documentElement.style.removeProperty("--color-accent"); + // Remove any theme-color meta tags + document.querySelectorAll("meta[name='theme-color']").forEach((el) => el.remove()); +}); + +describe("BrandingProvider", () => { + it("applies CSS vars to document root when branding loads", async () => { + const branding = { + businessName: "Test Salon", + primaryColor: "#123456", + accentColor: "#654321", + logoBase64: null, + logoMimeType: null, + }; + global.fetch = vi.fn(() => + Promise.resolve({ ok: true, json: async () => branding } as Response) + ) as unknown as typeof fetch; + + render( + + + + ); + + await waitFor(() => { + expect(document.documentElement.style.getPropertyValue("--color-primary")).toBe("#123456"); + expect(document.documentElement.style.getPropertyValue("--color-accent")).toBe("#654321"); + }); + }); + + it("creates and updates meta[name=theme-color]", async () => { + const branding = { + businessName: "Test Salon", + primaryColor: "#abcdef", + accentColor: "#fedcba", + logoBase64: null, + logoMimeType: null, + }; + global.fetch = vi.fn(() => + Promise.resolve({ ok: true, json: async () => branding } as Response) + ) as unknown as typeof fetch; + + render( + + + + ); + + await waitFor(() => { + const meta = document.querySelector("meta[name='theme-color']"); + expect(meta).not.toBeNull(); + expect(meta!.content).toBe("#abcdef"); + }); + }); + + it("does not create duplicate meta[name=theme-color] tags on rerender", async () => { + const branding = { + businessName: "Test Salon", + primaryColor: "#111111", + accentColor: "#222222", + logoBase64: null, + logoMimeType: null, + }; + global.fetch = vi.fn(() => + Promise.resolve({ ok: true, json: async () => branding } as Response) + ) as unknown as typeof fetch; + + const { rerender } = render( + + + + ); + + await waitFor(() => { + expect(document.querySelector("meta[name='theme-color']")).not.toBeNull(); + }); + + rerender( + + + + ); + + await waitFor(() => { + const metas = document.querySelectorAll("meta[name='theme-color']"); + expect(metas.length).toBe(1); + }); + }); +}); diff --git a/apps/web/src/portal/sections/ReportCards.tsx b/apps/web/src/portal/sections/ReportCards.tsx index 88172ec..8336285 100644 --- a/apps/web/src/portal/sections/ReportCards.tsx +++ b/apps/web/src/portal/sections/ReportCards.tsx @@ -76,7 +76,7 @@ function ReportCardDetail({ card, onBack }: { card: ReportCard; onBack: () => vo
{/* Header */} -
+

{card.petName}'s Grooming Report