Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
269434b
chore: add className testing util:
francinelucca Nov 22, 2025
7972d0e
Merge branch 'main' of github.com:primer/react into chore/add-base-cl…
francinelucca Nov 25, 2025
4034aca
lint fixes
francinelucca Nov 25, 2025
cae2022
Merge branch 'main' into chore/add-base-class-testing-util
francinelucca Nov 26, 2025
a61c41c
Merge branch 'main' into chore/add-base-class-testing-util
francinelucca Dec 2, 2025
62aae49
Merge branch 'main' of github.com:primer/react into chore/add-base-cl…
francinelucca Dec 4, 2025
69f163c
review comments
francinelucca Dec 4, 2025
bc6b386
Merge branch 'chore/add-base-class-testing-util' of github.com:primer…
francinelucca Dec 4, 2025
82c72dd
add CI to enforce className test
francinelucca Dec 4, 2025
bfe79ca
cleanup!
francinelucca Dec 4, 2025
ac283d1
Merge branch 'main' into chore/add-base-class-testing-util
francinelucca Dec 5, 2025
b7b7b79
Merge branch 'main' into chore/add-base-class-testing-util
francinelucca Dec 5, 2025
64e7843
Merge branch 'main' into chore/add-base-class-testing-util
francinelucca Dec 5, 2025
782ea21
Merge branch 'main' into chore/add-base-class-testing-util
francinelucca Dec 8, 2025
9fa44a1
Merge branch 'main' into chore/add-base-class-testing-util
francinelucca Dec 8, 2025
42223c3
convert script to node
francinelucca Dec 9, 2025
67216ae
add more className utils
francinelucca Dec 9, 2025
ce10e92
add more className utils
francinelucca Dec 9, 2025
1ef18dc
add more className utils
francinelucca Dec 9, 2025
3b197b5
add more className utils
francinelucca Dec 10, 2025
115e1d5
add more className utils
francinelucca Dec 10, 2025
598e866
add more className utils
francinelucca Dec 10, 2025
456b839
test fix
francinelucca Dec 10, 2025
afaf244
use PRC Button in test
francinelucca Dec 10, 2025
ea20e33
fix relative import check condition
francinelucca Dec 10, 2025
a04d375
fix relative import check condition
francinelucca Dec 10, 2025
f8b8fe6
fix relative import check condition
francinelucca Dec 10, 2025
7f1dc19
script correction
francinelucca Dec 10, 2025
fed64bd
add more className utils
francinelucca Dec 10, 2025
d86cc26
Merge branch 'main' into chore/add-base-class-testing-util
francinelucca Dec 10, 2025
1a1e752
add more className utils
francinelucca Dec 10, 2025
86cdc16
remove unused vars
francinelucca Dec 11, 2025
cd19193
add more className utils
francinelucca Dec 11, 2025
e328fb9
lint fix
francinelucca Dec 11, 2025
6bde988
add more className utils
francinelucca Dec 11, 2025
c890576
add ignores
francinelucca Dec 11, 2025
b71e08a
aria-label fix
francinelucca Dec 11, 2025
6538b81
remove redundant checks
francinelucca Dec 11, 2025
7e84707
Merge branch 'main' into chore/add-base-class-testing-util
francinelucca Dec 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ jobs:
run: npm run lint:md
- name: Lint npm packages
run: npx turbo lint:npm
- name: Check className test coverage
run: npm run test:classname-coverage

test:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"test": "vitest",
"test:type-check": "tsc --noEmit",
"test:update": "npm run test -- -u",
"test:classname-coverage": "node script/check-classname-tests.mjs",
"type-check": "tsc --noEmit && turbo type-check",
"release": "npm run build && changeset publish",
"reset": "script/reset",
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/ActionBar/ActionBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import userEvent from '@testing-library/user-event'
import ActionBar from './'
import {BoldIcon, ItalicIcon, CodeIcon} from '@primer/octicons-react'
import {useState} from 'react'
import {implementsClassName} from '../utils/testing'
import classes from './ActionBar.module.css'

describe('ActionBar', () => {
implementsClassName(ActionBar, classes.Nav)
afterEach(() => {
vi.clearAllMocks()
})
Expand Down
18 changes: 7 additions & 11 deletions packages/react/src/ActionList/ActionList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@ import {describe, it, expect, vi} from 'vitest'
import {render as HTMLRender} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {ActionList} from '.'
import {implementsClassName} from '../utils/testing'
import classes from './ActionList.module.css'

describe('ActionList', () => {
implementsClassName(ActionList, classes.ActionList)
implementsClassName(ActionList.LeadingVisual, classes.LeadingVisual)
implementsClassName(ActionList.TrailingVisual, classes.TrailingVisual)
implementsClassName(ActionList.TrailingAction, classes.TrailingAction)
implementsClassName(ActionList.Divider, classes.Divider)
it('should warn when selected is provided without a selectionVariant on parent', async () => {
// we expect console.warn to be called, so we spy on that in the test
const spy = vi.spyOn(console, 'warn').mockImplementation(() => vi.fn())
Expand Down Expand Up @@ -59,17 +66,6 @@ describe('ActionList', () => {
expect(document.activeElement).toHaveTextContent('Option 4')
})

it('should support a custom `className` on the outermost element', () => {
const Element = () => {
return (
<ActionList className="test-class-name">
<ActionList.Item>Item</ActionList.Item>
</ActionList>
)
}
expect(HTMLRender(<Element />).container.querySelector('ul')).toHaveClass('test-class-name')
})

it('divider should support a custom `className`', () => {
const Element = () => {
return (
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/ActionList/Description.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {expect, it, describe} from 'vitest'
import {render as HTMLRender} from '@testing-library/react'
import {ActionList} from '.'
import {implementsClassName} from '../utils/testing'
import classes from './ActionList.module.css'

describe('ActionList.Description', () => {
implementsClassName(ActionList.Description, classes.Description)
it('should render the description as inline without truncation by default', () => {
const {getByText} = HTMLRender(
<ActionList>
Expand Down
29 changes: 14 additions & 15 deletions packages/react/src/ActionList/Group.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,22 @@ import {render as HTMLRender} from '@testing-library/react'
import BaseStyles from '../BaseStyles'
import {ActionList} from '.'
import {ActionMenu} from '..'
import {implementsClassName} from '../utils/testing'
import classes from './Group.module.css'

describe('ActionList.Group', () => {
implementsClassName(
props => (
<ActionList>
<ActionList.Group {...props}>
<ActionList.Item>item</ActionList.Item>
</ActionList.Group>
</ActionList>
),
classes.Group,
)
implementsClassName(ActionList.GroupHeading, classes.GroupHeading)

it('should throw an error when ActionList.GroupHeading has an `as` prop when it is used within ActionMenu context', async () => {
expect(() =>
HTMLRender(
Expand Down Expand Up @@ -122,19 +136,4 @@ describe('ActionList.Group', () => {
const list = container.querySelector(`li[data-test-id='ActionList.Group'] > ul`)
expect(list).toHaveAttribute('aria-label', 'Animals')
})

it('should support a custom `className` on the outermost element', () => {
const Element = () => {
return (
<ActionList>
<ActionList.Group>
<ActionList.GroupHeading as="h2" className="test-class-name">
Test
</ActionList.GroupHeading>
</ActionList.Group>
</ActionList>
)
}
expect(HTMLRender(<Element />).container.querySelector('h2')).toHaveClass('test-class-name')
})
})
24 changes: 13 additions & 11 deletions packages/react/src/ActionList/Heading.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,21 @@ import {render as HTMLRender} from '@testing-library/react'
import BaseStyles from '../BaseStyles'
import {ActionList} from '.'
import {ActionMenu} from '..'
import {implementsClassName} from '../utils/testing'
import classes from './Heading.module.css'

describe('ActionList.Heading', () => {
implementsClassName(
props => (
<ActionList>
<ActionList.Heading as="h1" {...props}>
Heading
</ActionList.Heading>
</ActionList>
),
classes.ActionListHeader,
)

it('should render the ActionList.Heading component as a heading with the given heading level', async () => {
const container = HTMLRender(
<ActionList>
Expand Down Expand Up @@ -47,15 +60,4 @@ describe('ActionList.Heading', () => {
"ActionList.Heading shouldn't be used within an ActionMenu container. Menus are labelled by the menu button's name.",
)
})

it('should support a custom `className` on the outermost element', () => {
const actionList = HTMLRender(
<ActionList>
<ActionList.Heading as="h2" className="test-class-name">
Filter by
</ActionList.Heading>
</ActionList>,
)
expect(actionList.container.querySelector('h2')).toHaveClass('test-class-name')
})
})
4 changes: 4 additions & 0 deletions packages/react/src/ActionList/Item.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import userEvent from '@testing-library/user-event'
import React, {type JSX} from 'react'
import {ActionList} from '.'
import {BookIcon} from '@primer/octicons-react'
import {implementsClassName} from '../utils/testing'
import classes from './ActionList.module.css'

function SimpleActionList(): JSX.Element {
return (
Expand Down Expand Up @@ -60,6 +62,8 @@ function SingleSelectListStory(): JSX.Element {
}

describe('ActionList.Item', () => {
implementsClassName(ActionList.Item, classes.ActionListItem)
implementsClassName(ActionList.LinkItem)
it('should have aria-keyshortcuts applied to the correct element', async () => {
const {container} = HTMLRender(<SimpleActionList />)
const linkOptions = await waitFor(() => container.querySelectorAll('a'))
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/ActionMenu/ActionMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {MixedSelection} from '../ActionMenu/ActionMenu.examples.stories'
import {SearchIcon, KebabHorizontalIcon} from '@primer/octicons-react'

import type {JSX} from 'react'
import {implementsClassName} from '../utils/testing'

function Example(): JSX.Element {
return (
Expand Down Expand Up @@ -120,6 +121,8 @@ function ExampleWithSubmenus(): JSX.Element {
}

describe('ActionMenu', () => {
implementsClassName(ActionMenu.Button)

it('should open Menu on MenuButton click', async () => {
const component = HTMLRender(<Example />)
const button = component.getByRole('button')
Expand Down
8 changes: 8 additions & 0 deletions packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,24 @@ import {AnchoredOverlay} from '../AnchoredOverlay'
import {Button} from '../Button'
import BaseStyles from '../BaseStyles'
import type {AnchorPosition} from '@primer/behaviors'
import {implementsClassName} from '../utils/testing'

import overlayClasses from '../Overlay/Overlay.module.css'

type TestComponentSettings = {
initiallyOpen?: boolean
onOpenCallback?: (gesture: string) => void
onCloseCallback?: (gesture: string) => void
onPositionChange?: ({position}: {position: AnchorPosition}) => void
className?: string
}

const AnchoredOverlayTestComponent = ({
initiallyOpen = false,
onOpenCallback,
onCloseCallback,
onPositionChange,
className,
}: TestComponentSettings = {}) => {
const [open, setOpen] = useState(initiallyOpen)
const onOpen = useCallback(
Expand All @@ -42,6 +48,7 @@ const AnchoredOverlayTestComponent = ({
onClose={onClose}
renderAnchor={props => <Button {...props}>Anchor Button</Button>}
onPositionChange={onPositionChange}
className={className}
>
<button type="button">Focusable Child</button>
</AnchoredOverlay>
Expand All @@ -50,6 +57,7 @@ const AnchoredOverlayTestComponent = ({
}

describe('AnchoredOverlay', () => {
implementsClassName(props => <AnchoredOverlayTestComponent initiallyOpen={true} {...props} />, overlayClasses.Overlay)
it('should call onOpen when the anchor is clicked', async () => {
const mockOpenCallback = vi.fn()
const mockCloseCallback = vi.fn()
Expand Down
36 changes: 15 additions & 21 deletions packages/react/src/Autocomplete/Autocomplete.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {AutocompleteInputProps} from '../Autocomplete'
import Autocomplete from '../Autocomplete'
import type {AutocompleteMenuInternalProps, AutocompleteMenuItem} from '../Autocomplete/AutocompleteMenu'
import BaseStyles from '../BaseStyles'
import {implementsClassName} from '../utils/testing'
import classes from './AutocompleteOverlay.module.css'

const mockItems = [
{text: 'zero', id: '0'},
Expand Down Expand Up @@ -47,6 +49,11 @@ const LabelledAutocomplete = <T extends AutocompleteMenuItem>({

describe('Autocomplete', () => {
describe('Autocomplete.Input', () => {
implementsClassName(props => (
<Autocomplete>
<Autocomplete.Input {...props} />
</Autocomplete>
))
it('calls onChange', async () => {
const user = userEvent.setup()
const onChangeMock = vi.fn()
Expand Down Expand Up @@ -214,15 +221,6 @@ describe('Autocomplete', () => {

expect(getByDisplayValue('0')).toBeDefined()
})

it('should support `className` on the outermost element', () => {
const Element = () => (
<Autocomplete>
<Autocomplete.Input className={'test-class-name'} />
</Autocomplete>
)
expect(render(<Element />).container.firstChild).toHaveClass('test-class-name')
})
})

describe('Autocomplete.Menu', () => {
Expand Down Expand Up @@ -444,23 +442,19 @@ describe('Autocomplete', () => {
})
})

describe('Autocomplete.Overlay', () => {
it('should support `className` on the outermost element', async () => {
const Element = ({className}: {className: string}) => (
// TODO: Enable once className override bug is fixed in Autocomplete.Overlay, also remember to remove from ignore list on script/check-classname-tests.mjs
describe.skip('Autocomplete.Overlay', () => {
Comment on lines +445 to +446
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TODO comment mentions that Autocomplete.Overlay has a className override bug that needs to be fixed before this test can be enabled. However, the file 'packages/react/src/Autocomplete/AutocompleteOverlay.tsx' (line 15) is included in the ignore list with an incorrect path - it should be 'packages/react/src/Autocomplete/Autocomplete.test.tsx' to match the actual test file. This inconsistency could cause confusion about which file is being ignored.

Copilot uses AI. Check for mistakes.
implementsClassName(
props => (
<Autocomplete id="autocompleteId">
<Autocomplete.Input />
<Autocomplete.Overlay className={className} visibility="visible">
<Autocomplete.Overlay {...props} visibility="visible">
hi
</Autocomplete.Overlay>
</Autocomplete>
)
const {container: elementContainer, getByRole} = render(<Element className="test-class-name" />)
const inputNode = getByRole('combobox')
await userEvent.click(inputNode)
await userEvent.keyboard('{ArrowDown}')
// overlay is a sibling of elementContainer
expect(elementContainer.parentElement?.querySelectorAll('.test-class-name')).toHaveLength(1)
})
),
classes.Overlay,
)
})

describe('null context', () => {
Expand Down
7 changes: 3 additions & 4 deletions packages/react/src/Avatar/Avatar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import {describe, expect, it} from 'vitest'
import {render, screen} from '@testing-library/react'
import Avatar from '../Avatar'
import {implementsClassName} from '../utils/testing'
import classes from './Avatar.module.css'

describe('Avatar', () => {
it('should support `className` on the outermost element', () => {
const Element = () => <Avatar src="primer.png" className={'test-class-name'} />
expect(render(<Element />).container.firstChild).toHaveClass('test-class-name')
})
implementsClassName(Avatar, classes.Avatar)

it('renders small by default', () => {
const size = 20
Expand Down
14 changes: 3 additions & 11 deletions packages/react/src/AvatarStack/AvatarStack.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {describe, expect, it} from 'vitest'
import {render} from '@testing-library/react'
import {AvatarStack} from '..'
import {implementsClassName} from '../utils/testing'
import classes from './AvatarStack.module.css'

const avatarComp = (
<AvatarStack>
Expand All @@ -21,17 +23,7 @@ const rightAvatarComp = (
)

describe('AvatarStack', () => {
it('should support `className` on the outermost element', () => {
const Element = () => (
<AvatarStack className={'test-class-name'}>
<img src="https://avatars.githubusercontent.com/primer" alt="" />
<img src="https://avatars.githubusercontent.com/github" alt="" />
<img src="https://avatars.githubusercontent.com/primer" alt="" />
<img src="https://avatars.githubusercontent.com/github" alt="" />
</AvatarStack>
)
expect(render(<Element />).container.firstChild).toHaveClass('test-class-name')
})
implementsClassName(AvatarStack, classes.AvatarStack)

it('respects alignRight props', () => {
const {container} = render(rightAvatarComp)
Expand Down
9 changes: 4 additions & 5 deletions packages/react/src/Banner/Banner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@ import {describe, expect, it, vi} from 'vitest'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {Banner} from '../Banner'
import {implementsClassName} from '../utils/testing'
import classes from './Banner.module.css'

describe('Banner', () => {
implementsClassName(props => <Banner title="test" {...props} />, classes.Banner)

it('should render as a region element', () => {
render(<Banner title="test" />)
expect(screen.getByRole('region', {name: 'Information'})).toBeInTheDocument()
expect(screen.getByRole('heading', {name: 'test'})).toBeInTheDocument()
})

it('should support a custom `className` on the outermost element', () => {
const Element = () => <Banner title="test" className="test-class-name" />
expect(render(<Element />).container.firstChild).toHaveClass('test-class-name')
})

it('should label the landmark element with the corresponding variant label text', () => {
render(<Banner title="test" />)
expect(screen.getByRole('region')).toEqual(screen.getByLabelText('Information'))
Expand Down
7 changes: 3 additions & 4 deletions packages/react/src/Blankslate/Blankslate.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import {describe, expect, it, vi} from 'vitest'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {Blankslate} from '../Blankslate'
import {implementsClassName} from '../utils/testing'
import classes from './Blankslate.module.css'

describe('Blankslate', () => {
it('should support a custom `className` on the outermost, non-container element', () => {
const {container} = render(<Blankslate className="test">Test content</Blankslate>)
expect(container.firstChild!.firstChild).toHaveClass('test')
})
implementsClassName(Blankslate, classes.Blankslate)

it('should render with border when border is true', () => {
const {container} = render(<Blankslate border>Test content</Blankslate>)
Expand Down
Loading
Loading