// @vitest-environment jsdom /** * What the Card page claims, pinned (Katerina's rulings, 14 September): every * card has 8px corners and a hairline that follows its fill; a card with an * href is a link whose hairline goes one step stronger on hover; a feed card * lights up; the one you are on, or the one being changed, does not answer the * pointer; attention recolours the hairline; a card that cannot be read is * dashed; and the caller's className — placement, padding, layout — lands last. */ import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Card } from './Card' afterEach(cleanup) const classesOf = (el: Element) => el.getAttribute('class')?.split(' ').filter(Boolean) ?? [] const card = (ui: React.ReactElement) => { render(ui) return screen.getByText('Item one').closest('div, a')! } describe('Card', () => { it('has 8px corners and a hairline in every fill', () => { for (const fill of ['surface', 'elevated', 'inset', 'none'] as const) { const classes = classesOf(card(Item one)) expect(classes).toContain('rounded-lg') expect(classes).toContain('border') cleanup() } }) it('the hairline follows the fill: default on surface and elevated, subtle on inset and none', () => { const expected = { surface: ['bg-bg-surface', 'border-border-default'], elevated: ['bg-bg-elevated', 'border-border-default'], inset: ['bg-bg-inset', 'border-border-subtle'], none: ['border-border-subtle'] } as const for (const [fill, classes] of Object.entries(expected)) { const actual = classesOf(card(Item one)) for (const c of classes) expect(actual).toContain(c) if (fill === 'none') expect(actual.some((c) => c.startsWith('bg-'))).toBe(false) cleanup() } }) it('is still by default: nothing answers the pointer, and nothing eases', () => { const classes = classesOf(card(Item one)) expect(classes.some((c) => c.startsWith('hover:'))).toBe(false) expect(classes).not.toContain('transition-colors') cleanup() expect(classesOf(card(Item one))).toContain('transition-colors') }) it('with an href it is a link, its hairline one step stronger on hover, and a router takes the click', async () => { const onClick = vi.fn((event: { preventDefault: () => void }) => event.preventDefault()) render( Item one , ) const link = screen.getByRole('link', { name: 'Item one' }) expect(link.getAttribute('href')).toBe('/somewhere') expect(classesOf(link)).toContain('hover:border-border-strong') await userEvent.click(link) expect(onClick).toHaveBeenCalledTimes(1) cleanup() expect(classesOf(card(Item one))).toContain('hover:border-border-default') }) it('a feed card lights up on hover, and can keep its hairline hidden until then', () => { const classes = classesOf(card( {}}>Item one)) expect(classes).toContain('border-transparent') expect(classes).toContain('hover:bg-bg-hover') expect(classes).toContain('hover:border-border-default') expect(classes).toContain('cursor-pointer') }) it('the one you are on, and the one being changed, do not answer the pointer', () => { const selected = classesOf(card(Item one)) expect(selected).toContain('bg-bg-selected') expect(selected).toContain('border-border-subtle') expect(selected.some((c) => c.startsWith('hover:'))).toBe(false) cleanup() const active = classesOf(card(Item one)) expect(active).toContain('border-accent-primary') expect(active).not.toContain('border-accent-muted') expect(active.some((c) => c.startsWith('hover:'))).toBe(false) }) it('hovered holds the hover look without the pointer: a feed card stays lit, a link card keeps its stronger hairline', () => { const feed = classesOf(card( {}}>Item one)) expect(feed).toContain('bg-bg-hover') expect(feed).toContain('border-border-default') expect(feed).not.toContain('bg-bg-surface') expect(feed).not.toContain('border-transparent') cleanup() const link = classesOf(card(Item one)) expect(link).toContain('border-border-strong') cleanup() const inset = classesOf(card(Item one)) expect(inset).toContain('border-border-default') expect(inset).not.toContain('border-border-subtle') cleanup() // Nothing to hold on a card that does not answer the pointer. const still = classesOf(card(Item one)) expect(still).toContain('bg-bg-surface') expect(still).not.toContain('bg-bg-hover') }) it('hovered is ignored by the one you are on and the one being changed; attention still colours the hairline', () => { const selected = classesOf(card(Item one)) expect(selected).toContain('bg-bg-selected') expect(selected).not.toContain('bg-bg-hover') cleanup() const active = classesOf(card(Item one)) expect(active).toContain('border-accent-primary') expect(active).not.toContain('bg-bg-hover') cleanup() const urgent = classesOf(card(Item one)) expect(urgent).toContain('bg-bg-hover') expect(urgent).toContain('border-warning-muted') expect(urgent).not.toContain('border-border-default') }) it('the one you are on can still be clicked, and says so; the one being changed does not', () => { expect(classesOf(card( {}}>Item one))).toContain('cursor-pointer') cleanup() expect(classesOf(card( {}}>Item one))).not.toContain('cursor-pointer') }) it('attention recolours the hairline, at rest and on hover', () => { const classes = classesOf(card(Item one)) expect(classes).toContain('border-warning-muted') expect(classes).toContain('hover:border-warning-muted') expect(classes).not.toContain('border-transparent') }) it('a card that cannot be read is dashed, and keeps its fill', () => { const classes = classesOf(card(Item one)) expect(classes).toContain('border-dashed') expect(classes).toContain('bg-bg-surface') }) it("the caller's className lands last", () => { const classes = classesOf(card(Item one)) expect(classes).toContain('flex') expect(classes).not.toContain('block') expect(classes).toContain('p-3') }) })