Skip to content

Commit e0960f8

Browse files
adamloblerclaude
andcommitted
test(ui-card,regression-test): add Card unit tests and visual regression page
Unit tests assert concrete token values (the 20rem and 40rem width breakpoints, and padding and radius relationships per variant) instead of only asserting that some value is set, so a swapped size or variant token fails the suite rather than passing silently. Adds the /card regression page and its axe check to the Cypress spec. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 34db8bc commit e0960f8

4 files changed

Lines changed: 232 additions & 0 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2015 - present Instructure, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
import { render, screen } from '@testing-library/react'
26+
import { runAxeCheck } from '@instructure/ui-axe-check'
27+
28+
import '@testing-library/jest-dom'
29+
import { Card } from '@instructure/ui-card/latest'
30+
31+
// Values come from the generated Card theme tokens, see
32+
// packages/ui-themes/src/themes/newThemeTokens/<theme>/components/card.ts
33+
const BREAKPOINT_MD = '20rem'
34+
const BREAKPOINT_LG = '40rem'
35+
36+
const renderCard = (props = {}) => {
37+
const { container } = render(<Card {...props}>content</Card>)
38+
const card = container.querySelector('div')!
39+
return { card, style: getComputedStyle(card) }
40+
}
41+
42+
describe('<Card />', () => {
43+
describe('for a11y', () => {
44+
it('should be accessible', async () => {
45+
const { container } = render(<Card>content</Card>)
46+
const axeCheck = await runAxeCheck(container)
47+
expect(axeCheck).toBe(true)
48+
})
49+
})
50+
51+
it('should render children', () => {
52+
render(<Card>Hello world</Card>)
53+
expect(screen.getByText('Hello world')).toBeInTheDocument()
54+
})
55+
56+
it('should render a div', () => {
57+
const { card } = renderCard()
58+
expect(card.tagName).toBe('DIV')
59+
})
60+
61+
it('should pass through arbitrary HTML attributes', () => {
62+
render(<Card id="my-card">content</Card>)
63+
expect(document.getElementById('my-card')).toBeInTheDocument()
64+
})
65+
66+
it('should not leak component props onto the DOM node', () => {
67+
const { card } = renderCard({ variant: 'nested', size: 'lg' })
68+
expect(card).not.toHaveAttribute('variant')
69+
expect(card).not.toHaveAttribute('size')
70+
})
71+
72+
describe('variant', () => {
73+
it('base should render a background and a box-shadow', () => {
74+
const { style } = renderCard({ variant: 'base' })
75+
// an opaque surface colour, not the transparent jsdom default
76+
expect(style.backgroundColor).not.toBe('')
77+
expect(style.backgroundColor).not.toBe('rgba(0, 0, 0, 0)')
78+
expect(style.boxShadow).toContain('rgba')
79+
})
80+
81+
it('nested should render a border instead of a background and shadow', () => {
82+
const { style } = renderCard({ variant: 'nested' })
83+
// positively assert the border the nested variant *does* apply
84+
expect(style.borderStyle).toBe('solid')
85+
expect(style.borderWidth).toBe('1px')
86+
expect(style.borderColor).not.toBe('')
87+
// and that it opts out of the base surface treatment
88+
expect(style.backgroundColor).toBe('rgba(0, 0, 0, 0)')
89+
expect(style.boxShadow).toBe('')
90+
})
91+
92+
it('should apply a smaller padding for nested than for base at the same size', () => {
93+
const base = renderCard({ variant: 'base', size: 'md' })
94+
const nested = renderCard({ variant: 'nested', size: 'md' })
95+
expect(base.style.padding).not.toBe(nested.style.padding)
96+
expect(parseFloat(nested.style.padding)).toBeLessThan(
97+
parseFloat(base.style.padding)
98+
)
99+
})
100+
})
101+
102+
describe('size', () => {
103+
it('sm should cap width at the md breakpoint with no min-width', () => {
104+
const { style } = renderCard({ size: 'sm' })
105+
expect(style.maxWidth).toBe(BREAKPOINT_MD)
106+
expect(style.minWidth).toBe('auto')
107+
})
108+
109+
it('md should span the md and lg breakpoints', () => {
110+
const { style } = renderCard({ size: 'md' })
111+
expect(style.minWidth).toBe(BREAKPOINT_MD)
112+
expect(style.maxWidth).toBe(BREAKPOINT_LG)
113+
})
114+
115+
it('lg should start at the lg breakpoint with no max-width', () => {
116+
const { style } = renderCard({ size: 'lg' })
117+
expect(style.minWidth).toBe(BREAKPOINT_LG)
118+
expect(style.maxWidth).toBe('none')
119+
})
120+
121+
it('should grow padding from sm through lg', () => {
122+
const sm = parseFloat(renderCard({ size: 'sm' }).style.padding)
123+
const md = parseFloat(renderCard({ size: 'md' }).style.padding)
124+
const lg = parseFloat(renderCard({ size: 'lg' }).style.padding)
125+
expect(sm).toBeLessThan(md)
126+
expect(md).toBeLessThan(lg)
127+
})
128+
129+
// The documented contract is that `nested` uses a smaller radius than
130+
// `base`. Absolute radii are deliberately not asserted: the legacy Canvas
131+
// themes flatten every radius token to the same value, so only the
132+
// relationship between the two variants holds across all four themes.
133+
it.each(['sm', 'md', 'lg'] as const)(
134+
'nested should not have a larger border-radius than base at size %s',
135+
(size) => {
136+
const base = parseFloat(
137+
renderCard({ variant: 'base', size }).style.borderRadius
138+
)
139+
const nested = parseFloat(
140+
renderCard({ variant: 'nested', size }).style.borderRadius
141+
)
142+
expect(nested).toBeLessThanOrEqual(base)
143+
}
144+
)
145+
})
146+
147+
describe('defaults', () => {
148+
it('should default to the base variant at the md size', () => {
149+
const bare = renderCard()
150+
const explicit = renderCard({ variant: 'base', size: 'md' })
151+
expect(bare.style.padding).toBe(explicit.style.padding)
152+
expect(bare.style.minWidth).toBe(BREAKPOINT_MD)
153+
expect(bare.style.maxWidth).toBe(BREAKPOINT_LG)
154+
expect(bare.style.boxShadow).toContain('rgba')
155+
})
156+
})
157+
})

regression-test/cypress/e2e/spec.cy.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ describe('visual regression test', () => {
131131
cy.checkA11y('.axe-test', axeOptions, terminalLog)
132132
})
133133

134+
it('Card', () => {
135+
cy.visit('http://localhost:3000/card')
136+
cy.injectAxe()
137+
cy.checkA11y('.axe-test', axeOptions, terminalLog)
138+
})
139+
134140
it('Checkbox', () => {
135141
cy.visit('http://localhost:3000/checkbox')
136142
cy.wait(100) // needed so checkbox dont trigger axe check fails
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2015 - present Instructure, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
'use client'
25+
import React from 'react'
26+
import { Card as c, Text as t } from '@instructure/ui/latest'
27+
28+
// alias to avoid TS/SSR friction like other pages
29+
const Card = c as any
30+
const Text = t as any
31+
32+
export default function CardPage() {
33+
return (
34+
<main className="flex gap-8 p-8 flex-col items-start axe-test">
35+
{/* Sizes */}
36+
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
37+
<Card size="sm">
38+
<Text variant="content">Small base card</Text>
39+
</Card>
40+
<Card size="md">
41+
<Text variant="content">Medium base card</Text>
42+
</Card>
43+
<Card size="lg">
44+
<Text variant="content">Large base card</Text>
45+
</Card>
46+
</div>
47+
48+
{/* Nested cards */}
49+
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
50+
<Card variant="base" size="sm">
51+
<Card variant="nested" size="sm">
52+
<Text variant="content">Nested small card</Text>
53+
</Card>
54+
</Card>
55+
<Card variant="base" size="md">
56+
<Card variant="nested" size="md">
57+
<Text variant="content">Nested medium card</Text>
58+
</Card>
59+
</Card>
60+
<Card variant="base" size="lg">
61+
<Card variant="nested" size="lg">
62+
<Text variant="content">Nested large card</Text>
63+
</Card>
64+
</Card>
65+
</div>
66+
</main>
67+
)
68+
}

regression-test/src/app/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const components = [
3737
'tooltip',
3838
'byline',
3939
'calendar',
40+
'card',
4041
'checkbox',
4142
'checkboxgroup',
4243
'colorpicker',

0 commit comments

Comments
 (0)