@westpac/ui 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/AGENTS.md +161 -0
- package/.agents/skills/creating-gel-component/SKILL.md +153 -0
- package/.agents/skills/reviewing-gel-component/SKILL.md +102 -0
- package/.agents/skills/writing-gel-tests/SKILL.md +167 -0
- package/CHANGELOG.md +13 -0
- package/dist/component-type.json +1 -1
- package/dist/components/pass-code/pass-code.component.d.ts +1 -0
- package/dist/components/pass-code/pass-code.component.js +8 -3
- package/dist/components/pass-code/pass-code.types.d.ts +4 -0
- package/dist/css/westpac-ui.css +3 -0
- package/dist/css/westpac-ui.min.css +3 -0
- package/package.json +4 -4
- package/src/components/pass-code/pass-code.component.tsx +11 -4
- package/src/components/pass-code/pass-code.types.ts +4 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# @westpac/ui Component Conventions
|
|
2
|
+
|
|
3
|
+
These conventions apply to all code in `packages/ui/`. Follow them when creating, modifying, or reviewing components.
|
|
4
|
+
|
|
5
|
+
## File Structure
|
|
6
|
+
|
|
7
|
+
Every component lives in `src/components/{kebab-case-name}/` with these files:
|
|
8
|
+
|
|
9
|
+
- `index.ts` — Public exports (component + props type only, NOT styles)
|
|
10
|
+
- `{name}.component.tsx` — Component implementation
|
|
11
|
+
- `{name}.styles.ts` — Tailwind Variants styles
|
|
12
|
+
- `{name}.types.ts` — TypeScript types
|
|
13
|
+
- `{name}.spec.tsx` — Vitest + React Testing Library tests
|
|
14
|
+
- `{name}.stories.tsx` — Storybook stories
|
|
15
|
+
|
|
16
|
+
Optional: `{name}.utils.ts` for helpers, `components/` subdirectory for compound sub-components.
|
|
17
|
+
|
|
18
|
+
## Import Conventions
|
|
19
|
+
|
|
20
|
+
- **Always use `.js` extensions** in relative imports (e.g., `'./button.styles.js'`)
|
|
21
|
+
- Import `ResponsiveVariants` from `../../types/responsive-variants.types.js`
|
|
22
|
+
- Import `useBreakpoint` from `../../hook/breakpoints.hook.js`
|
|
23
|
+
- Import `resolveResponsiveVariant` from `../../utils/breakpoint.util.js`
|
|
24
|
+
- Import `IconProps` from `../icon/index.js`
|
|
25
|
+
|
|
26
|
+
## Types (`*.types.ts`)
|
|
27
|
+
|
|
28
|
+
- Derive variant types: `type Variants = VariantProps<typeof styles>`
|
|
29
|
+
- Wrap responsive props: `ResponsiveVariants<Variants['propName']>`
|
|
30
|
+
- JSDoc comment with `@default` tag on every prop
|
|
31
|
+
- Extend appropriate HTML attributes (`HTMLAttributes<HTMLDivElement>`, `ButtonHTMLAttributes<Element>`, etc.)
|
|
32
|
+
- Use `Omit<>` for conflicting HTML attributes (e.g., `Omit<InputHTMLAttributes, 'size'>`)
|
|
33
|
+
|
|
34
|
+
## Styles (`*.styles.ts`)
|
|
35
|
+
|
|
36
|
+
- Use `tv()` from `tailwind-variants`, exported as `styles`
|
|
37
|
+
- Use `slots` for multi-element components
|
|
38
|
+
- Use `compoundSlots` for variant combinations
|
|
39
|
+
- Use GEL design tokens — never hardcode colors:
|
|
40
|
+
- Text: `text-text-body`, `text-text-muted`, `text-text-link`, `text-text-mono`
|
|
41
|
+
- Surface: `bg-surface-primary`, `bg-surface-hero`, `bg-surface-muted-pale`
|
|
42
|
+
- Hover: `hover:bg-surface-hover-*`
|
|
43
|
+
- Active: `active:bg-surface-active-*`
|
|
44
|
+
- Border: `border-border-primary`, `border-border-hero`, `border-border-muted-soft`
|
|
45
|
+
- Background: `bg-background-white`
|
|
46
|
+
- Typography: `typography-body-*`
|
|
47
|
+
- Focus: `focus-outline` (not custom focus rings)
|
|
48
|
+
|
|
49
|
+
## Components (`*.component.tsx`)
|
|
50
|
+
|
|
51
|
+
- Add `'use client';` directive when using hooks or interactivity
|
|
52
|
+
- Use `forwardRef` pattern when appropriate: `Base{Name}` function + `forwardRef(Base{Name})` export
|
|
53
|
+
- Destructure `className` and pass to `styles({ className })` or `styles.base({ className })`
|
|
54
|
+
- Resolve responsive props: `resolveResponsiveVariant(prop, breakpoint)` with `useBreakpoint()`
|
|
55
|
+
- Use `useFocusRing()` + `mergeProps()` from `react-aria` for interactive components
|
|
56
|
+
- Use plain function declarations — do NOT use `React.FC`
|
|
57
|
+
- Set sensible default values in prop destructuring
|
|
58
|
+
|
|
59
|
+
## Index (`index.ts`)
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
export { ComponentName } from './component-name.component.js';
|
|
63
|
+
export { type ComponentNameProps } from './component-name.types.js';
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Do NOT re-export styles.
|
|
67
|
+
|
|
68
|
+
## Tests (`*.spec.tsx`)
|
|
69
|
+
|
|
70
|
+
- Use `@testing-library/react` for rendering, `userEvent.setup()` for interactions
|
|
71
|
+
- Use `vi.fn()` for mocks, `waitFor()` for async assertions
|
|
72
|
+
- Import from `.component.js` (not index)
|
|
73
|
+
- Minimum: a "renders the component" test
|
|
74
|
+
- Pre-mocked globals (do NOT re-mock): `window.scrollTo`, `window.matchMedia`, `ResizeObserver`, `window.URL.createObjectURL`
|
|
75
|
+
- Style files (`*.styles.ts`) are excluded from test coverage
|
|
76
|
+
|
|
77
|
+
## Stories (`*.stories.tsx`)
|
|
78
|
+
|
|
79
|
+
- Import from `@storybook/react-vite`
|
|
80
|
+
- Include `tags: ['autodocs']` and decorator `[(Story: StoryFn) => <Story />]`
|
|
81
|
+
- JSDoc comments above stories use `>` blockquote format
|
|
82
|
+
- Interactive stories using `useState` are defined as function components
|
|
83
|
+
- Use `fn()` from `storybook/test` for callback props
|
|
84
|
+
|
|
85
|
+
## Registering New Components
|
|
86
|
+
|
|
87
|
+
After creating a component, add it to `src/components/index.ts`:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
export * from './{kebab-case-name}/index.js';
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Key Patterns
|
|
94
|
+
|
|
95
|
+
- **Responsive variants**: Use `ResponsiveVariants<T>` type, resolve with `resolveResponsiveVariant()` + `useBreakpoint()`
|
|
96
|
+
- **Compound components**: Use React Context, place sub-components in `components/` subdirectory
|
|
97
|
+
- **Icon props**: Type as `(props: IconProps) => JSX.Element`
|
|
98
|
+
- **Polymorphic tag**: `tag?: keyof JSX.IntrinsicElements` or a constrained subset
|
|
99
|
+
|
|
100
|
+
## TypeScript Best Practices
|
|
101
|
+
|
|
102
|
+
- **No `any`** — Use proper types. If a type is truly unknown, use `unknown` and narrow it. The only acceptable escape hatch is `as unknown as TargetType` for complex generic interop (e.g., `forwardRef` with generics)
|
|
103
|
+
- **Prefer `type` over `interface`** for props — keeps consistency with `VariantProps` intersections (`&`)
|
|
104
|
+
- **Export types with `export { type ... }`** — use explicit `type` keyword for type-only exports
|
|
105
|
+
- **Derive types from source** — don't duplicate: use `VariantProps<typeof styles>`, `Pick<>`, `Omit<>`, and mapped types
|
|
106
|
+
- **Use `ReactNode` for children** — not `JSX.Element` or `React.ReactElement` (unless narrowing is required for compound components)
|
|
107
|
+
- **Type event handlers precisely** — use `React.MouseEvent<HTMLButtonElement>`, not generic `React.SyntheticEvent`
|
|
108
|
+
- **Avoid type assertions** — prefer type guards and narrowing over `as`. Use `satisfies` for validation without widening
|
|
109
|
+
|
|
110
|
+
## React Best Practices
|
|
111
|
+
|
|
112
|
+
### Component Design
|
|
113
|
+
|
|
114
|
+
- **One component per file** — the main component in `*.component.tsx`, sub-components in their own files under `components/`
|
|
115
|
+
- **Plain functions, not `React.FC`** — function declarations with explicit props typing: `function Button({ children }: ButtonProps)`
|
|
116
|
+
- **Destructure props at the function signature** — not inside the body. Order: `className`, variant props, behavioural props, `children`, `...rest`
|
|
117
|
+
- **Prefer composition over configuration** — use children and compound component patterns rather than deeply nested config objects (see Compacta, Repeater, ButtonGroup)
|
|
118
|
+
- **Keep components pure** — no side effects in render. Move side effects to `useEffect`
|
|
119
|
+
|
|
120
|
+
### Hooks
|
|
121
|
+
|
|
122
|
+
- **`useMemo`** — for expensive computations or referentially stable objects/arrays passed to children. Don't over-use for primitive values
|
|
123
|
+
- **`useCallback`** — for event handlers passed to memoized children or used in dependency arrays
|
|
124
|
+
- **Custom hooks** — extract reusable logic into hooks in `src/hook/`. Prefix with `use`
|
|
125
|
+
- **Dependency arrays** — list all dependencies. Suppress lint warnings only with a comment explaining why
|
|
126
|
+
- **`useEffect` cleanup** — always clean up subscriptions, event listeners, and timers
|
|
127
|
+
|
|
128
|
+
### Refs and DOM
|
|
129
|
+
|
|
130
|
+
- **`forwardRef`** — use for components that render a single native element consumers might need to reference
|
|
131
|
+
- **Type refs precisely** — `Ref<HTMLButtonElement>`, not `Ref<HTMLElement>` or `Ref<any>`
|
|
132
|
+
- **Don't read refs during render** — access `.current` only in effects or event handlers
|
|
133
|
+
|
|
134
|
+
### State Management
|
|
135
|
+
|
|
136
|
+
- **Lift state only when needed** — keep state as close to where it's used as possible
|
|
137
|
+
- **React Context for compound components** — share state between parent and children (Accordion, List, Compacta)
|
|
138
|
+
- **`react-stately` for complex state** — use Adobe's hooks (`useDisclosureGroupState`, `useOverlayTriggerState`, etc.) instead of hand-rolling state machines
|
|
139
|
+
- **Controlled vs uncontrolled** — support both patterns where sensible. Use `defaultValue`/`value` naming convention
|
|
140
|
+
|
|
141
|
+
### Performance
|
|
142
|
+
|
|
143
|
+
- **Don't prematurely optimise** — only add `memo()`, `useMemo`, `useCallback` when there's a measurable problem or the component is used in lists
|
|
144
|
+
- **Avoid inline object/array literals in JSX** — these create new references every render. Extract to `useMemo` or module scope if they're static
|
|
145
|
+
- **Key lists properly** — use stable, unique identifiers, never array index (unless the list is static and never reordered)
|
|
146
|
+
|
|
147
|
+
### Patterns to Avoid
|
|
148
|
+
|
|
149
|
+
- **No `React.FC`** — doesn't support generics well and adds implicit `children`
|
|
150
|
+
- **No `default export` for components** — use named exports for better refactoring support and tree-shaking
|
|
151
|
+
- **No `// @ts-ignore` or `// @ts-expect-error`** — fix the type issue instead. If absolutely unavoidable, add a comment explaining why
|
|
152
|
+
- **No `useEffect` for derived state** — compute derived values directly or with `useMemo`, not by syncing state in effects
|
|
153
|
+
- **No prop drilling beyond 2 levels** — use Context or composition instead
|
|
154
|
+
|
|
155
|
+
## Available Skills
|
|
156
|
+
|
|
157
|
+
Load these skills for specific tasks:
|
|
158
|
+
|
|
159
|
+
- **`creating-gel-component`** — Step-by-step scaffolding workflow with file templates for new components
|
|
160
|
+
- **`reviewing-gel-component`** — Checklist for auditing components against these conventions
|
|
161
|
+
- **`writing-gel-tests`** — Detailed guide for Vitest + React Testing Library test patterns, categories, and coverage targets
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: creating-gel-component
|
|
3
|
+
description: 'Scaffolds a new GEL design system UI component following project conventions. Use when creating a new component, adding a component, or scaffolding a component in packages/ui.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Creating a GEL Component
|
|
7
|
+
|
|
8
|
+
Scaffolds a new React component in `packages/ui/src/components/`. All conventions (file naming, imports, patterns) are defined in the `packages/ui/.agents/AGENTS.md` — follow those automatically.
|
|
9
|
+
|
|
10
|
+
## Step-by-Step Workflow
|
|
11
|
+
|
|
12
|
+
### 1. Create the types file (`{name}.types.ts`)
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { HTMLAttributes } from 'react';
|
|
16
|
+
import { type VariantProps } from 'tailwind-variants';
|
|
17
|
+
|
|
18
|
+
import { ResponsiveVariants } from '../../types/responsive-variants.types.js';
|
|
19
|
+
|
|
20
|
+
import { styles } from './{kebab-case-name}.styles.js';
|
|
21
|
+
|
|
22
|
+
type Variants = VariantProps<typeof styles>;
|
|
23
|
+
|
|
24
|
+
export type {PascalName}Props = {
|
|
25
|
+
/**
|
|
26
|
+
* Description of variant prop
|
|
27
|
+
* @default defaultValue
|
|
28
|
+
*/
|
|
29
|
+
variantProp?: ResponsiveVariants<Variants['variantProp']>;
|
|
30
|
+
} & HTMLAttributes<HTMLElement>;
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. Create the styles file (`{name}.styles.ts`)
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { tv } from 'tailwind-variants';
|
|
37
|
+
|
|
38
|
+
export const styles = tv({
|
|
39
|
+
base: '',
|
|
40
|
+
variants: {},
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 3. Create the component file (`{name}.component.tsx`)
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
'use client';
|
|
48
|
+
|
|
49
|
+
import React, { forwardRef, Ref } from 'react';
|
|
50
|
+
import { mergeProps, useFocusRing } from 'react-aria';
|
|
51
|
+
|
|
52
|
+
import { useBreakpoint } from '../../hook/breakpoints.hook.js';
|
|
53
|
+
import { resolveResponsiveVariant } from '../../utils/breakpoint.util.js';
|
|
54
|
+
|
|
55
|
+
import { styles as componentStyles } from './{kebab-case-name}.styles.js';
|
|
56
|
+
import { type {PascalName}Props } from './{kebab-case-name}.types.js';
|
|
57
|
+
|
|
58
|
+
function Base{PascalName}(
|
|
59
|
+
{ className, variant = 'default', ...props }: {PascalName}Props,
|
|
60
|
+
ref: Ref<HTMLDivElement>,
|
|
61
|
+
) {
|
|
62
|
+
const breakpoint = useBreakpoint();
|
|
63
|
+
const { isFocusVisible, focusProps } = useFocusRing();
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
ref={ref}
|
|
68
|
+
className={componentStyles({
|
|
69
|
+
className,
|
|
70
|
+
variant: resolveResponsiveVariant(variant, breakpoint),
|
|
71
|
+
isFocusVisible,
|
|
72
|
+
})}
|
|
73
|
+
{...mergeProps(props, focusProps)}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const {PascalName} = forwardRef(Base{PascalName});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 4. Create the index file (`index.ts`)
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
export { {PascalName} } from './{kebab-case-name}.component.js';
|
|
85
|
+
export { type {PascalName}Props } from './{kebab-case-name}.types.js';
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 5. Create the test file (`{name}.spec.tsx`)
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import { render } from '@testing-library/react';
|
|
92
|
+
|
|
93
|
+
import { {PascalName} } from './{kebab-case-name}.component.js';
|
|
94
|
+
|
|
95
|
+
describe('{PascalName}', () => {
|
|
96
|
+
it('renders the component', () => {
|
|
97
|
+
const { container } = render(<{PascalName} />);
|
|
98
|
+
expect(container).toBeInTheDocument();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
See the `writing-gel-tests` skill for comprehensive testing guidance.
|
|
104
|
+
|
|
105
|
+
### 6. Create the stories file (`{name}.stories.tsx`)
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import { type Meta, StoryFn, type StoryObj } from '@storybook/react-vite';
|
|
109
|
+
|
|
110
|
+
import { {PascalName} } from './{kebab-case-name}.component.js';
|
|
111
|
+
|
|
112
|
+
const meta: Meta<typeof {PascalName}> = {
|
|
113
|
+
title: 'Components/{PascalName}',
|
|
114
|
+
component: {PascalName},
|
|
115
|
+
tags: ['autodocs'],
|
|
116
|
+
decorators: [(Story: StoryFn) => <Story />],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export default meta;
|
|
120
|
+
type Story = StoryObj<typeof meta>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* > Default usage example
|
|
124
|
+
*/
|
|
125
|
+
export const Default: Story = {
|
|
126
|
+
args: {},
|
|
127
|
+
};
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Interactive stories using `useState` can be defined as function components:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
export const Example = () => {
|
|
134
|
+
const [state, setState] = useState(initialValue);
|
|
135
|
+
|
|
136
|
+
return <Component prop={state} onChange={setState} />;
|
|
137
|
+
};
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 7. Register the component
|
|
141
|
+
|
|
142
|
+
Add the export to `packages/ui/src/components/index.ts`:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
export * from './{kebab-case-name}/index.js';
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 8. Verify
|
|
149
|
+
|
|
150
|
+
Run these commands from `packages/ui/`:
|
|
151
|
+
|
|
152
|
+
- `pnpm vitest run src/components/{kebab-case-name}` — Run tests
|
|
153
|
+
- `pnpm build` — Verify build
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reviewing-gel-component
|
|
3
|
+
description: 'Reviews GEL design system components for convention compliance, accessibility, and best practices. Use when reviewing a component, checking component quality, or auditing GEL components.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Reviewing a GEL Component
|
|
7
|
+
|
|
8
|
+
Reviews components in `packages/ui/src/components/` against the conventions defined in `packages/ui/.agents/AGENTS.md`.
|
|
9
|
+
|
|
10
|
+
## Review Checklist
|
|
11
|
+
|
|
12
|
+
### 1. File Structure
|
|
13
|
+
|
|
14
|
+
- [ ] All required files exist: `index.ts`, `*.component.tsx`, `*.styles.ts`, `*.types.ts`, `*.spec.tsx`, `*.stories.tsx`
|
|
15
|
+
- [ ] File names use kebab-case matching the directory name
|
|
16
|
+
- [ ] Component is exported from `packages/ui/src/components/index.ts`
|
|
17
|
+
|
|
18
|
+
### 2. Types
|
|
19
|
+
|
|
20
|
+
- [ ] `Variants` type alias derived from `VariantProps<typeof styles>`
|
|
21
|
+
- [ ] Responsive props wrapped with `ResponsiveVariants<Variants['...']>`
|
|
22
|
+
- [ ] Every prop has a JSDoc comment with `@default` tag where applicable
|
|
23
|
+
- [ ] Extends correct HTML attributes type
|
|
24
|
+
- [ ] Uses `Omit<>` for conflicting HTML attributes
|
|
25
|
+
- [ ] `.js` extensions on relative imports
|
|
26
|
+
|
|
27
|
+
### 3. Styles
|
|
28
|
+
|
|
29
|
+
- [ ] Uses tailwind styling except when animations/dynamic styles are required
|
|
30
|
+
- [ ] Uses `tv()` from `tailwind-variants`, exported as `styles`
|
|
31
|
+
- [ ] Uses GEL design tokens — no hardcoded colors
|
|
32
|
+
- [ ] Uses `typography-body-*` for text, `focus-outline` for focus
|
|
33
|
+
- [ ] Uses `slots` for multi-element components
|
|
34
|
+
- [ ] Uses `compoundSlots` for variant combinations
|
|
35
|
+
|
|
36
|
+
### 4. Component
|
|
37
|
+
|
|
38
|
+
- [ ] Has `'use client';` if using hooks or client-side rendering
|
|
39
|
+
- [ ] Uses `forwardRef` pattern when appropriate
|
|
40
|
+
- [ ] Destructures `className` and passes to styles
|
|
41
|
+
- [ ] Uses `useBreakpoint()` + `resolveResponsiveVariant()` for responsive props
|
|
42
|
+
- [ ] Uses `useFocusRing()` + `mergeProps()` from `react-aria` where appropriate
|
|
43
|
+
- [ ] All props are used (no unused props)
|
|
44
|
+
- [ ] `.js` extensions on all relative imports
|
|
45
|
+
- [ ] Plain function declarations (not `React.FC`)
|
|
46
|
+
|
|
47
|
+
### 5. Accessibility
|
|
48
|
+
|
|
49
|
+
- [ ] Interactive elements use appropriate ARIA attributes
|
|
50
|
+
- [ ] Decorative icons use `aria-hidden`
|
|
51
|
+
- [ ] `react-aria` hooks used for focus and accessibility where appropriate
|
|
52
|
+
- [ ] Semantic HTML elements used
|
|
53
|
+
- [ ] `react-stately` hooks for state management where applicable
|
|
54
|
+
|
|
55
|
+
### 6. Index
|
|
56
|
+
|
|
57
|
+
- [ ] Exports component and props type only
|
|
58
|
+
- [ ] Does NOT re-export styles
|
|
59
|
+
- [ ] `.js` extensions
|
|
60
|
+
|
|
61
|
+
### 7. Tests
|
|
62
|
+
|
|
63
|
+
- [ ] At minimum a "renders the component" test
|
|
64
|
+
- [ ] Uses `userEvent.setup()` (not `fireEvent`) for interactions
|
|
65
|
+
- [ ] Imports from `.component.js` (not index)
|
|
66
|
+
|
|
67
|
+
### 8. Stories
|
|
68
|
+
|
|
69
|
+
- [ ] Imports from `@storybook/react-vite`
|
|
70
|
+
- [ ] Has `tags: ['autodocs']` and standard decorator
|
|
71
|
+
- [ ] Covers: default state, looks/variants, sizes, responsive, disabled
|
|
72
|
+
- [ ] Interactive stories defined as function components
|
|
73
|
+
|
|
74
|
+
## Severity Levels
|
|
75
|
+
|
|
76
|
+
### Critical
|
|
77
|
+
|
|
78
|
+
- Missing `'use client'` on components with hooks
|
|
79
|
+
- Hardcoded colors instead of design tokens
|
|
80
|
+
- Missing `forwardRef` on components rendering native elements
|
|
81
|
+
- Custom focus styles instead of `focus-outline`
|
|
82
|
+
- Missing `.js` extensions in imports
|
|
83
|
+
|
|
84
|
+
### Warnings
|
|
85
|
+
|
|
86
|
+
- Missing JSDoc `@default` tags
|
|
87
|
+
- No responsive variant support where beneficial
|
|
88
|
+
- Minimal test coverage
|
|
89
|
+
- Missing `aria-*` attributes on interactive elements
|
|
90
|
+
|
|
91
|
+
### Style
|
|
92
|
+
|
|
93
|
+
- Using `React.FC` instead of function declarations
|
|
94
|
+
- Inconsistent prop destructuring order
|
|
95
|
+
- Styles not using `slots` for multi-element components
|
|
96
|
+
|
|
97
|
+
## How to Run
|
|
98
|
+
|
|
99
|
+
1. Read all files in the component directory
|
|
100
|
+
2. Check each item above
|
|
101
|
+
3. Report findings grouped by severity
|
|
102
|
+
4. Suggest specific fixes with code examples
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: writing-gel-tests
|
|
3
|
+
description: 'Writes Vitest + React Testing Library tests for GEL design system components. Use when writing tests, adding tests, or improving test coverage for components in packages/ui.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Writing GEL Component Tests
|
|
7
|
+
|
|
8
|
+
Guides writing tests for components in `packages/ui/src/components/`. Test conventions are also summarised in `packages/ui/.agents/AGENTS.md`.
|
|
9
|
+
|
|
10
|
+
## Test Environment
|
|
11
|
+
|
|
12
|
+
- **Runner**: Vitest with `jsdom` environment
|
|
13
|
+
- **Rendering**: `@testing-library/react` (`render`, `screen`, `waitFor`)
|
|
14
|
+
- **User Events**: `@testing-library/user-event` (`userEvent.setup()`)
|
|
15
|
+
- **Mocks**: `vi` from `vitest`
|
|
16
|
+
- **Matchers**: `@testing-library/jest-dom` (loaded via `vitest.setup.ts`)
|
|
17
|
+
- **Excluded from tests**: `*.styles.ts` files (excluded in vitest config)
|
|
18
|
+
|
|
19
|
+
### Pre-mocked Globals
|
|
20
|
+
|
|
21
|
+
Already mocked in `vitest.setup.ts` — do NOT re-mock:
|
|
22
|
+
|
|
23
|
+
- `window.scrollTo`
|
|
24
|
+
- `window.URL.createObjectURL` / `revokeObjectURL`
|
|
25
|
+
- `window.matchMedia`
|
|
26
|
+
- `ResizeObserver`
|
|
27
|
+
|
|
28
|
+
## Test File Structure
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
32
|
+
import userEvent from '@testing-library/user-event';
|
|
33
|
+
import { vi } from 'vitest';
|
|
34
|
+
|
|
35
|
+
import { ComponentName } from './component-name.component.js';
|
|
36
|
+
|
|
37
|
+
describe('ComponentName', () => {
|
|
38
|
+
const user = userEvent.setup();
|
|
39
|
+
|
|
40
|
+
it('renders the component', () => {
|
|
41
|
+
const { container } = render(<ComponentName />);
|
|
42
|
+
expect(container).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Import from `./component-name.component.js` (NOT from `index`), sub-components from `./components/index.js`, icons from `../icon/index.js`. Always use `.js` extensions.
|
|
48
|
+
|
|
49
|
+
## Test Categories
|
|
50
|
+
|
|
51
|
+
### 1. Render Tests (Required)
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
it('renders the component', () => {
|
|
55
|
+
const { container } = render(<ComponentName />);
|
|
56
|
+
expect(container).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. Variant Rendering
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
it('renders as an anchor tag', () => {
|
|
64
|
+
render(
|
|
65
|
+
<Button tag="a" href="#">
|
|
66
|
+
Link
|
|
67
|
+
</Button>,
|
|
68
|
+
);
|
|
69
|
+
expect(screen.getByRole('link', { name: 'Link' })).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 3. User Interactions
|
|
74
|
+
|
|
75
|
+
Always use `userEvent.setup()` (NOT `fireEvent`):
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
it('calls onClick when clicked', async () => {
|
|
79
|
+
const handleClick = vi.fn();
|
|
80
|
+
render(<Button onClick={handleClick}>Click me</Button>);
|
|
81
|
+
|
|
82
|
+
act(() => {
|
|
83
|
+
user.click(screen.getByRole('button', { name: 'Click me' }));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await waitFor(() => {
|
|
87
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 4. Visibility / State
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
it('shows content when expanded', async () => {
|
|
96
|
+
const { getByText } = render(
|
|
97
|
+
<Accordion>
|
|
98
|
+
<AccordionItem key="item1" id="item1" title="Title">
|
|
99
|
+
Hidden content
|
|
100
|
+
</AccordionItem>
|
|
101
|
+
</Accordion>,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
user.click(getByText('Title'));
|
|
105
|
+
|
|
106
|
+
await waitFor(() => {
|
|
107
|
+
expect(getByText('Hidden content')).toBeVisible();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 5. Icon Rendering
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
it('renders an icon when passed', () => {
|
|
116
|
+
render(<Button iconBefore={ArrowRightIcon} />);
|
|
117
|
+
expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 6. Utility Functions
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
describe('ComponentName utils', () => {
|
|
125
|
+
it('maps correct values', () => {
|
|
126
|
+
expect(utilFunction('input')).toBe('expected');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('handles responsive values', () => {
|
|
130
|
+
expect(utilFunction({ initial: 'small', md: 'large' })).toStrictEqual({ initial: 'xsmall', md: 'small' });
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Querying Elements
|
|
136
|
+
|
|
137
|
+
Prefer accessible queries:
|
|
138
|
+
|
|
139
|
+
1. `screen.getByRole('button', { name: 'Text' })` — interactive elements
|
|
140
|
+
2. `screen.getByText('Text')` — content assertions
|
|
141
|
+
3. `screen.getByLabelText('Label')` — form elements
|
|
142
|
+
4. `container.querySelector()` — last resort only
|
|
143
|
+
|
|
144
|
+
## Coverage Targets
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
branches: 80,
|
|
148
|
+
functions: 80,
|
|
149
|
+
lines: 80,
|
|
150
|
+
statements: 80,
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Running Tests
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
pnpm vitest run src/components/{name} # Specific component
|
|
157
|
+
pnpm vitest run # All tests
|
|
158
|
+
pnpm vitest src/components/{name} # Watch mode
|
|
159
|
+
pnpm vitest run --coverage # With coverage
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## What NOT to Test
|
|
163
|
+
|
|
164
|
+
- Style files (`*.styles.ts`) — excluded in vitest config
|
|
165
|
+
- Tailwind class names — don't assert on specific CSS classes
|
|
166
|
+
- Internal implementation details — test behavior, not implementation
|
|
167
|
+
- Storybook stories — tested separately via Storybook
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @westpac/ui
|
|
2
2
|
|
|
3
|
+
## 1.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2eebb35: - fix issue with font import in style config
|
|
8
|
+
- fix security issues
|
|
9
|
+
|
|
10
|
+
## 1.1.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- bd3422f: add onPasteComplete handler to passcode
|
|
15
|
+
|
|
3
16
|
## 1.0.0
|
|
4
17
|
|
|
5
18
|
### 📦 Major Changes — @westpac/ui & @westpac/style-config
|