// The Logo, as an application meets it. // // Four of ADR 0004's decisions are behaviour rather than prose, and each one is // asked here in the form it would fail in: the surface selection matrix, the // refusal that names a gap instead of papering over it, the clearspace the box // carries whether or not anyone remembers it, and the minimum below which a // Logo is an error rather than a smaller Logo. The `href` form is the fifth — // the accessible home link a navbar wants. import { describe, expect, it } from "vitest"; import Logo from "../src/Logo.svelte"; import { BRAND_RELEASE, LOGO_LAYOUTS, LOGO_SURFACES, MARKS, MarkNotShippedError, selectMark, shipsMark, type LogoLayout, type LogoSurface, } from "../src/logo.marks"; import { CLEARSPACE_RATIO, DEFAULT_SYMBOL_PX, LogoTooSmallError, MINIMUM_SYMBOL_PX, logoBox, } from "../src/logo.box"; import { activeSurface, surfaceOfColorScheme } from "../src/logo.surface"; import { logo, logoMark } from "../src/logo.variants"; import { classes, classesOf, render, rendered, styleOf, withColorScheme } from "./mount"; /** The inside the Logo's box — the Mark itself. */ function markImage(element: HTMLElement): HTMLImageElement { const image = element.querySelector("img"); expect(image, "the Logo rendered no Mark").not.toBeNull(); return image as HTMLImageElement; } /** Every (layout, on) pair, whether or not the release ships a Mark for it. */ const MATRIX: readonly { layout: LogoLayout; on: LogoSurface }[] = LOGO_LAYOUTS.flatMap((layout) => LOGO_SURFACES.map((on) => ({ layout, on })), ); describe("the selection matrix", () => { it("covers the product of both axes, not a list of combinations", () => { // Without this, a silently empty axis would make every case below vacuous. expect(LOGO_LAYOUTS.length).toBeGreaterThan(0); expect(LOGO_SURFACES.length).toBeGreaterThan(0); expect(MATRIX.length).toBe(LOGO_LAYOUTS.length * LOGO_SURFACES.length); }); it("renders the Mark the pinned release ships for each shipped pair", () => { for (const { layout, on } of MATRIX) { if (!shipsMark(layout, on)) continue; const element = rendered(render(Logo, { layout, on })); const mark = selectMark(layout, on); expect(element.getAttribute("data-logo-mark")).toBe(mark.file); expect(markImage(element).getAttribute("src")).toBe(mark.src); // …and says which cell of the matrix it is, which is what makes the // showcase's own matrix readable rather than five similar pictures. expect(element.getAttribute("data-logo-layout")).toBe(layout); expect(element.getAttribute("data-logo-on")).toBe(on); } }); it("takes the Brand's variant for each local surface", () => { // The naming is the Brand's, and the mapping is the whole of what `on` // means: inverse/white are for a dark BACKGROUND, not for a dark Theme. expect(MARKS.horizontal.light?.file).toBe("reddb-horizontal-color.svg"); expect(MARKS.horizontal.dark?.file).toBe("reddb-horizontal-inverse.svg"); expect(MARKS.stacked.light?.file).toBe("reddb-stacked-color.svg"); expect(MARKS.stacked.dark?.file).toBe("reddb-stacked-inverse.svg"); expect(MARKS.icon.light?.file).toBe("reddb-icon-color.svg"); expect(MARKS.symbol.light?.file).toBe("reddb-symbol-black.svg"); expect(MARKS.symbol.dark?.file).toBe("reddb-symbol-white.svg"); expect(MARKS.wordmark.light?.file).toBe("reddb-wordmark-black.svg"); expect(MARKS.wordmark.dark?.file).toBe("reddb-wordmark-white.svg"); }); it("defaults the surface from the active Color Scheme", () => { // ADR 0004's surface default, read off the same root // attribute the cascade reads. No `on` is given anywhere here. withColorScheme("dark", () => { const element = rendered(render(Logo, { layout: "horizontal" })); expect(element.getAttribute("data-logo-on")).toBe("dark"); expect(element.getAttribute("data-logo-mark")).toBe("reddb-horizontal-inverse.svg"); }); withColorScheme("light", () => { const element = rendered(render(Logo, { layout: "horizontal" })); expect(element.getAttribute("data-logo-on")).toBe("light"); expect(element.getAttribute("data-logo-mark")).toBe("reddb-horizontal-color.svg"); }); }); it("follows a Color Scheme change after mounting", async () => { document.documentElement.setAttribute("data-color-scheme", "light"); const element = rendered(render(Logo, { layout: "horizontal" })); const lightSource = element.querySelector("img")?.getAttribute("src"); expect(element.getAttribute("data-logo-on")).toBe("light"); document.documentElement.setAttribute("data-color-scheme", "dark"); await new Promise((resolve) => setTimeout(resolve, 0)); expect(element.getAttribute("data-logo-on")).toBe("dark"); expect(element.getAttribute("data-logo-mark")).toBe("reddb-horizontal-inverse.svg"); expect(element.querySelector("img")?.getAttribute("src")).toBe(MARKS.horizontal.dark?.src); expect(element.querySelector("img")?.getAttribute("src")).not.toBe(lightSource); document.documentElement.removeAttribute("data-color-scheme"); }); it("reads an absent or unknown Color Scheme as the light surface", () => { for (const scheme of [null, "gloaming-not-yet-shipped"]) { withColorScheme(scheme, () => { expect(activeSurface(document)).toBe("light"); expect(rendered(render(Logo, {})).getAttribute("data-logo-on")).toBe("light"); }); } }); it("lets the caller override the Color Scheme, because the surface is local", () => { // The dark hero on a light marketing page (ADR 0004) — the case the prop // exists for, and the one a Theme could never have answered. withColorScheme("light", () => { const element = rendered(render(Logo, { layout: "stacked", on: "dark" })); expect(element.getAttribute("data-logo-on")).toBe("dark"); expect(element.getAttribute("data-logo-mark")).toBe("reddb-stacked-inverse.svg"); }); }); it("answers with no document at all, which is what a server render is", () => { expect(activeSurface(null)).toBe("light"); expect(activeSurface(undefined)).toBe("light"); expect(surfaceOfColorScheme("dark")).toBe("dark"); expect(surfaceOfColorScheme("light")).toBe("light"); }); it("defaults to the horizontal layout", () => { expect(rendered(render(Logo, {})).getAttribute("data-logo-layout")).toBe("horizontal"); }); }); describe("the refusal, when the release ships no such Mark", () => { // ADR 0004: "A missing variant refuses, never falls back and is never // invented locally (today: `icon` has no inverse)." it("is exactly one gap today, and it is the icon on dark", () => { const missing = MATRIX.filter(({ layout, on }) => !shipsMark(layout, on)); expect(missing).toEqual([{ layout: "icon", on: "dark" }]); }); it("throws rather than rendering, when the component is asked for it", () => { expect(() => render(Logo, { layout: "icon", on: "dark" })).toThrow(MarkNotShippedError); }); it("throws under the dark Color Scheme too, where the default chooses it", () => { withColorScheme("dark", () => { expect(() => render(Logo, { layout: "icon" })).toThrow(MarkNotShippedError); }); }); it("names the gap and the pinned release it is missing from", () => { const error = (() => { try { selectMark("icon", "dark"); return null; } catch (err) { return err as MarkNotShippedError; } })(); expect(error).toBeInstanceOf(MarkNotShippedError); expect(error!.layout).toBe("icon"); expect(error!.on).toBe("dark"); expect(error!.release).toBe(BRAND_RELEASE); // The message is the first draft of the Brand proposal, so it has to carry // both facts a reader needs: what is missing, and from which release. expect(error!.message).toContain("icon"); expect(error!.message).toContain("dark"); expect(error!.message).toContain(BRAND_RELEASE); expect(error!.message).toContain("ADR 0004"); // …and what to do meanwhile, which is the only part that is advice. expect(error!.message).toContain('layout="horizontal"'); expect(error!.message).toContain('layout="stacked"'); }); it("never falls back to the other surface's drawing", () => { // The failure this is guarding against is silent, so it is worth asking // directly: no code path returns the light Mark for a dark surface. expect(MARKS.icon.dark).toBeUndefined(); try { selectMark("icon", "dark"); expect.unreachable("selectMark returned a Mark for a variant that does not exist"); } catch (err) { expect(err).toBeInstanceOf(MarkNotShippedError); } }); }); describe("clearspace, which the box carries intrinsically", () => { // ADR 0004: clearspace is 25% of symbol height, it is inside the component's // own box, and there is no opt-out prop. it("is a quarter of the symbol's height, on every side", () => { for (const layout of LOGO_LAYOUTS) { const box = logoBox(selectMark(layout, "light"), 48); expect(box.clearspace).toBe(48 * CLEARSPACE_RATIO); expect(box.clearspace).toBe(12); } }); it("makes the box the Mark plus clearspace on all four sides", () => { const mark = selectMark("horizontal", "light"); const box = logoBox(mark, 32); expect(box.symbol).toBe(32); expect(box.markHeight).toBe(32); expect(box.markWidth).toBeCloseTo(127.26784, 5); expect(box.clearspace).toBe(8); expect(box.width).toBeCloseTo(box.markWidth + 16, 5); expect(box.height).toBe(48); }); it("keeps one size meaning one symbol across layouts of different shapes", () => { // The stacked Mark is taller than the horizontal one at the same `size`, // because its symbol is a smaller share of the drawing — which is exactly // what makes `size` comparable between them. const horizontal = logoBox(selectMark("horizontal", "light"), 32); const stacked = logoBox(selectMark("stacked", "light"), 32); const icon = logoBox(selectMark("icon", "light"), 32); expect(horizontal.symbol).toBe(stacked.symbol); expect(stacked.symbol).toBe(icon.symbol); expect(horizontal.clearspace).toBe(stacked.clearspace); expect(stacked.markHeight).toBeCloseTo(49.41536, 5); expect(stacked.markWidth).toBeCloseTo(44.8, 5); expect(icon.markWidth).toBe(32); expect(icon.markHeight).toBe(32); }); it("renders the clearspace as the box's own padding", () => { const element = rendered(render(Logo, { layout: "horizontal", on: "light", size: 32 })); expect(styleOf(element, "padding")).toBe("8px"); const image = markImage(element); expect(styleOf(image, "width")).toBe("127.268px"); expect(styleOf(image, "height")).toBe("32px"); }); it("has no opt-out, at any size a caller may ask for", () => { // The one property that has to hold for every size rather than for a // chosen one: there is no prop, and no size, that yields a bare Mark. for (const size of [MINIMUM_SYMBOL_PX, 24, DEFAULT_SYMBOL_PX, 64, 200]) { const element = rendered(render(Logo, { layout: "icon", on: "light", size })); expect(styleOf(element, "padding")).toBe(`${size * CLEARSPACE_RATIO}px`); } }); it("defaults to a size a caller did not have to choose", () => { const element = rendered(render(Logo, { layout: "icon", on: "light" })); expect(styleOf(element, "padding")).toBe(`${DEFAULT_SYMBOL_PX * CLEARSPACE_RATIO}px`); expect(styleOf(markImage(element), "height")).toBe(`${DEFAULT_SYMBOL_PX}px`); }); }); describe("the minimum size, which is an error and not a smaller Logo", () => { it("accepts the minimum itself", () => { expect(() => render(Logo, { layout: "icon", on: "light", size: MINIMUM_SYMBOL_PX })).not.toThrow(); }); it("refuses anything under it, from the component and from the arithmetic", () => { expect(() => logoBox(selectMark("icon", "light"), MINIMUM_SYMBOL_PX - 1)).toThrow( LogoTooSmallError, ); expect(() => render(Logo, { layout: "horizontal", on: "light", size: 8 })).toThrow( LogoTooSmallError, ); }); it("refuses a size that is not a length at all", () => { for (const size of [0, -32, Number.NaN, Number.POSITIVE_INFINITY]) { expect(() => logoBox(selectMark("icon", "light"), size)).toThrow(LogoTooSmallError); } }); it("says what was asked for, what the floor is, and why there is no opt-out", () => { try { logoBox(selectMark("horizontal", "light"), 10); expect.unreachable("logoBox accepted a below-minimum size"); } catch (err) { const error = err as LogoTooSmallError; expect(error).toBeInstanceOf(LogoTooSmallError); expect(error.size).toBe(10); expect(error.minimum).toBe(MINIMUM_SYMBOL_PX); expect(error.message).toContain("10"); expect(error.message).toContain(String(MINIMUM_SYMBOL_PX)); expect(error.message).toContain("ADR 0004"); } }); it("is one floor for every layout, because the symbol is one drawing", () => { for (const layout of LOGO_LAYOUTS) { expect(() => logoBox(selectMark(layout, "light"), MINIMUM_SYMBOL_PX)).not.toThrow(); expect(() => logoBox(selectMark(layout, "light"), MINIMUM_SYMBOL_PX - 0.5)).toThrow( LogoTooSmallError, ); } }); }); describe("the pure Mark, without an href", () => { it("renders a carrying the Mark as content", () => { const element = rendered(render(Logo, { layout: "horizontal", on: "light" })); expect(element.tagName).toBe("SPAN"); expect(element.hasAttribute("href")).toBe(false); expect(element.hasAttribute("aria-label")).toBe(false); // On its own the Mark IS the content, so it carries the Brand's name. expect(markImage(element).getAttribute("alt")).toBe("RedDB"); expect(markImage(element).hasAttribute("aria-hidden")).toBe(false); }); it("wears exactly the classes its variants module produces", () => { const element = rendered(render(Logo, {})); expect(classes(element)).toEqual(classesOf(logo({ interactive: false }))); expect(classes(markImage(element))).toEqual(classesOf(logoMark())); }); it("merges a caller's classes over its own", () => { const element = rendered(render(Logo, { class: "align-middle" })); expect(classes(element).has("align-middle")).toBe(true); expect(classes(element).has("inline-flex")).toBe(true); }); it("passes native attributes straight through", () => { const element = rendered(render(Logo, { id: "footer-logo", title: "reddb.io" })); expect(element.id).toBe("footer-logo"); expect(element.getAttribute("title")).toBe("reddb.io"); }); it("carries no script and no handler, so a static render is zero JS", () => { const element = rendered(render(Logo, {})); expect(element.querySelector("script")).toBeNull(); for (const attribute of element.getAttributeNames()) { expect(attribute.startsWith("on")).toBe(false); } }); }); describe("the href form: the accessible home link", () => { it("renders a real pointing where it was told", () => { const element = rendered(render(Logo, { href: "/" })); expect(element.tagName).toBe("A"); expect(element.getAttribute("href")).toBe("/"); }); it("says where it goes, for anyone who cannot see the drawing", () => { const element = rendered(render(Logo, { href: "/" })); expect(element.getAttribute("aria-label")).toBe("reddb.io home"); // …and the Mark stops being content, so the destination is announced once. expect(markImage(element).getAttribute("alt")).toBe(""); expect(markImage(element).getAttribute("aria-hidden")).toBe("true"); }); it("lets the caller say it differently", () => { const element = rendered(render(Logo, { href: "/home", label: "reddb.io — início" })); expect(element.getAttribute("aria-label")).toBe("reddb.io — início"); }); it("draws its focus ring on the box, which puts it outside the clearspace", () => { // The ring is on the element that already INCLUDES the clearspace padding, // so "outside the clearspace" is construction rather than an offset // someone has to keep in step with the clearspace ratio. const element = rendered(render(Logo, { href: "/", size: 32 })); expect(classes(element)).toEqual(classesOf(logo({ interactive: true }))); // Focus is ink (ADR 0023): a 2px outline at a 2px offset. expect(classes(element).has("focus-visible:outline-2")).toBe(true); expect(classes(element).has("focus-visible:outline-focus")).toBe(true); expect(styleOf(element, "padding")).toBe("8px"); }); it("is still the same Logo — same selection, same physics", () => { withColorScheme("dark", () => { const element = rendered(render(Logo, { href: "/", layout: "stacked" })); expect(element.getAttribute("data-logo-mark")).toBe("reddb-stacked-inverse.svg"); }); expect(() => render(Logo, { href: "/", layout: "icon", on: "dark" })).toThrow( MarkNotShippedError, ); expect(() => render(Logo, { href: "/", size: 4 })).toThrow(LogoTooSmallError); }); it("passes anchor attributes straight through", () => { const element = rendered(render(Logo, { href: "https://reddb.io", target: "_blank", rel: "noreferrer" })); expect(element.getAttribute("target")).toBe("_blank"); expect(element.getAttribute("rel")).toBe("noreferrer"); }); it("carries no handler either", () => { const element = rendered(render(Logo, { href: "/" })); for (const attribute of element.getAttributeNames()) { expect(attribute.startsWith("on")).toBe(false); } }); }); describe("how the browser is told to fetch the Mark", () => { // The Logo is the only component in either Kit that renders an image, so it // is the only place these hints can live. They are asked for here in the // form they would fail in: silently, on the wrong element. it("decodes off the parser's thread, in both modes and without being asked", () => { // Fixed rather than a prop: nothing about a Logo needs the decode to land // in the same frame as the markup around it, which is all `sync` buys. for (const props of [{}, { href: "/" }]) { expect(markImage(rendered(render(Logo, props))).getAttribute("decoding")).toBe("async"); } }); it("fetches eagerly by default, because a masthead is where a Logo lives", () => { for (const props of [{}, { href: "/" }]) { expect(markImage(rendered(render(Logo, props))).getAttribute("loading")).toBe("eager"); } }); it("lets a footer Logo defer, which is the caller's call and not the Kit's", () => { const element = rendered(render(Logo, { loading: "lazy" })); expect(markImage(element).getAttribute("loading")).toBe("lazy"); }); it("leaves priority to the browser until someone has a reason to say", () => { // An absent fetchpriority is not the same as `auto`: it is the Kit having // no opinion, which is the honest default for a component that cannot see // the page it is on. for (const props of [{}, { href: "/" }]) { expect(markImage(rendered(render(Logo, props))).hasAttribute("fetchpriority")).toBe(false); } const element = rendered(render(Logo, { href: "/", fetchpriority: "high" })); expect(markImage(element).getAttribute("fetchpriority")).toBe("high"); }); it("puts every one of them on the , never on the box", () => { // The regression this whole block exists for. `rest` is spread on the box, // so before these were named props a caller's `loading="lazy"` landed on a // or an — attributes those elements have no use for, dropped // without a word. A hint that silently does nothing is worse than one that // is missing, because the caller believes it took. for (const props of [{}, { href: "/" }]) { const element = rendered( render(Logo, { ...props, loading: "lazy", fetchpriority: "low" }), ); for (const attribute of ["loading", "fetchpriority", "decoding"]) { expect( element.hasAttribute(attribute), `${attribute} landed on the box, where it does nothing`, ).toBe(false); expect(markImage(element).hasAttribute(attribute)).toBe(true); } } }); it("changes nothing about which Mark is placed, or how big it is", () => { // The hints are about the fetch, never about the drawing — the box // arithmetic and the selection matrix are untouched by them. const plain = rendered(render(Logo, { layout: "stacked", on: "dark", size: 32 })); const hinted = rendered( render(Logo, { layout: "stacked", on: "dark", size: 32, loading: "lazy", fetchpriority: "high", }), ); expect(hinted.getAttribute("data-logo-mark")).toBe(plain.getAttribute("data-logo-mark")); expect(styleOf(hinted, "padding")).toBe(styleOf(plain, "padding")); expect(markImage(hinted).getAttribute("width")).toBe(markImage(plain).getAttribute("width")); expect(markImage(hinted).getAttribute("height")).toBe(markImage(plain).getAttribute("height")); expect(classes(hinted)).toEqual(classes(plain)); }); });