import { describe, expect, it } from 'vitest'; import { decodeProps, encodeValue, hasProps, paramKey } from './prop-params'; import type { Axes, PropBag } from '~/registry/types'; /* * The one part of the share link that can break silently. * * Everything else in the feature fails loudly — a missing catalogue key fails * typecheck, a broken route renders a 404 — but a codec that hands `"3"` to a * component expecting `3`, or that quietly ignores a param, produces a link that * *looks* like it worked and shows the wrong thing. `vitest.config.ts` includes * `showcase/src/**` for this file alone; the showcase has no other tests. * * `vitest`'s helpers are imported by name rather than taken from globals: * `showcase/tsconfig.json` declares `"types": []`, and this file is not worth * pulling the whole jsdom/jest-dom type surface into that program for. */ const axes: Axes = { variant: ['solid', 'outline'], size: [1, 2, 3], disabled: [false, true], /* Named after a global toggle on purpose: the `p.` prefix is what keeps the two apart, and this is the row that proves it. */ theme: ['flat', 'raised'], }; const defaults: PropBag = { variant: 'solid', size: 2, disabled: false, theme: 'flat' }; function decode(query: string) { return decodeProps(axes, defaults, new URLSearchParams(query)); } describe('encodeValue', () => { it('writes booleans as 1/0 and everything else as-is', () => { expect(encodeValue(true)).toBe('1'); expect(encodeValue(false)).toBe('0'); expect(encodeValue(3)).toBe('3'); expect(encodeValue('outline')).toBe('outline'); }); }); describe('decodeProps', () => { it('falls back to the entry defaults when the query is empty', () => { expect(decode('')).toEqual(defaults); }); it('restores a numeric axis as a number, not a string', () => { const props = decode(`${paramKey('size')}=3`); expect(props.size).toBe(3); }); it('restores a boolean axis from 1/0', () => { expect(decode(`${paramKey('disabled')}=1`).disabled).toBe(true); expect(decode(`${paramKey('disabled')}=0`).disabled).toBe(false); }); it('ignores a value the axis does not offer', () => { expect(decode(`${paramKey('variant')}=bogus`).variant).toBe('solid'); }); it('reads its own prefixed key, never a global toggle of the same name', () => { /* `theme=dark` is the site's light/dark switch and must not reach the props. */ const props = decode('theme=dark&p.theme=raised'); expect(props.theme).toBe('raised'); }); it('layers several params over the defaults at once', () => { expect(decode('p.variant=outline&p.size=1&p.disabled=1')).toEqual({ variant: 'outline', size: 1, disabled: true, theme: 'flat', }); }); it('survives an entry with no axes', () => { expect(decodeProps(undefined, defaults, new URLSearchParams('p.variant=outline'))).toEqual( defaults ); }); }); /* * `hasProps` decides whether the playground's Reset control exists at all, so * getting it wrong is the same class of silent failure as the codec above: a * button that is always there and usually does nothing, or one that never * appears when the URL is full of state. */ describe('hasProps', () => { it('is false for an empty query and for global toggles alone', () => { expect(hasProps(new URLSearchParams(''))).toBe(false); expect(hasProps(new URLSearchParams('theme=dark&lang=ru&a11y=on'))).toBe(false); }); it('is true as soon as one prefixed key is present', () => { expect(hasProps(new URLSearchParams('p.variant=outline'))).toBe(true); expect(hasProps(new URLSearchParams('theme=dark&p.size=3'))).toBe(true); }); it('does not mistake a global toggle that merely starts with the letter p', () => { /* `page` is not `p.age` — the prefix includes the dot for exactly this. */ expect(hasProps(new URLSearchParams('page=2&parity=findings'))).toBe(false); }); });