/** * The mark on a card for a product the workspace does not own yet. * * A shelf card leads with a UI vignette of the job the product does, which sells * it and does not identify it: the thing the buyer will hunt for in the switcher * next week is the coloured mark, and until now that appeared on Your App and in * the switcher but never here. What has to hold is that every card carries one, in * its own brand colour rather than the theme's, and that adding a picture did not * add anything for a screen reader to read out. */ import { render, screen } from "@testing-library/react" import { MemoryRouter } from "react-router" import { describe, expect, it, vi } from "vitest" import { buildProductHomeInventory, type ProductHomeCard } from "@/lib/product-home" import { PRODUCT_TILE_SIZES } from "@/lib/product-glyph" import { ProductShelves } from "./product-shelves" // The real provider boots a tenant catalogue over `fetch` on mount, and which // product the shell has open is not what is under test. vi.mock("@/contexts/product-context", () => ({ useProduct: () => ({ product: "exxat-prism" }), })) function shelfCards(): ProductHomeCard[] { const cards = buildProductHomeInventory([], []).available if (cards.length === 0) throw new Error("the workspace owns every product") return cards } function renderShelf(cards = shelfCards()) { const view = render( , ) return { ...view, cards } } describe("a More from Exxat card", () => { it("wears a mark for every product on the shelf", () => { const { container, cards } = renderShelf() expect(container.querySelectorAll(".product-tile")).toHaveLength(cards.length) }) // Seven cards on one page, so the colour cannot come from the theme: each tile // hands its own product's brand down to the CSS that paints it. it("paints each tile in that card's brand colour", () => { const { container, cards } = renderShelf() const painted = [...container.querySelectorAll(".product-tile")].map(tile => tile.style.getPropertyValue("--product-brand"), ) expect(painted).toEqual(cards.map(card => card.brandColor)) }) // The mark is white on the brand colour at a measured 3:1. Menu rows and other // highlightable surfaces repaint their descendants' text on hover, which turned // the mark near-black over a saturated tile, so the tile opts its subtree out. it("keeps its own ink where a surface repaints its descendants", () => { const { container } = renderShelf() for (const tile of container.querySelectorAll(".product-tile")) { expect(tile).toHaveAttribute("data-fixed-ink") } }) it("says nothing to a screen reader, since the name is right beside it", () => { const { container } = renderShelf() for (const tile of container.querySelectorAll(".product-tile")) { expect(tile).toHaveAttribute("aria-hidden", "true") } }) // The tile sits in its own column beside a nested flex column now. A heading that // is no longer a heading, or a card that stopped being one link to one place, is // the regression worth catching. it("leaves the card one heading and one link", () => { const { cards } = renderShelf() const [first] = cards expect(screen.getAllByRole("heading", { level: 3 })).toHaveLength(cards.length) expect(screen.getByRole("link", { name: new RegExp(first.label, "i") })).toHaveAttribute( "href", `/home/${first.slug}`, ) }) // Title and tagline stack under the art band — no mark beside the heading. it("keeps the title and the tagline in one column", () => { const { cards } = renderShelf() const [first] = cards const heading = screen.getAllByRole("heading", { level: 3 })[0] const column = heading.parentElement! expect(heading).toHaveTextContent(first.label) expect(column.querySelector("p")?.textContent).toBe(first.tagline) }) it("anchors the mark in the card footer like a directory logo", () => { const { container } = renderShelf() for (const article of container.querySelectorAll("article")) { const tile = article.querySelector(".product-tile") const footer = tile?.parentElement expect(footer?.className).toContain("justify-between") } }) it("gives the card the larger of the two tile sizes", () => { const { container } = renderShelf() for (const tile of container.querySelectorAll(".product-tile")) { expect(tile.className).toContain(PRODUCT_TILE_SIZES.lg.footprint) } }) it("offers Learn more on new products such as Surveys and Course Evaluations", () => { const surveys = shelfCards().find(card => card.product === "exxat-surveys") if (!surveys) return renderShelf([surveys]) expect(screen.getByRole("link", { name: /learn more about surveys/i })).toHaveAttribute( "href", `/home/${surveys.slug}`, ) }) it("types card copy in the sans stack from globals, not the page serif", () => { const { container } = renderShelf() for (const article of container.querySelectorAll("article")) { expect(article.className).toContain("font-sans") } for (const heading of container.querySelectorAll("article h3")) { expect(heading.className).toContain("font-sans") expect(heading.className).not.toContain("font-heading") expect(heading.className).toContain("font-semibold") } }) })