@codecademy/gamut
68.6.068.6.1-alpha.bc8b32.0
agent-tools/skills/gamut-testing/SKILL.md+
agent-tools/skills/gamut-testing/SKILL.mdNew file+221
Index: package/agent-tools/skills/gamut-testing/SKILL.md
===================================================================
--- package/agent-tools/skills/gamut-testing/SKILL.md
+++ package/agent-tools/skills/gamut-testing/SKILL.md
@@ -0,0 +1,221 @@
+---
+name: gamut-testing
+description: Use this skill when writing or fixing unit tests for React components that use Gamut — prefer setupRtl from @codecademy/gamut-tests, harness patterns for useLogicalProperties and ColorMode, RTL/dir testing, emotion matchers, or removing jest.mock of @codecademy/gamut / gamut-styles.
+---
+
+# Gamut Testing
+
+Source: `@codecademy/gamut-tests` — [`index.tsx`](https://github.com/Codecademy/gamut/blob/main/packages/gamut-tests/src/index.tsx)
+
+---
+
+---
+
+## What `MockGamutProvider` does (under `setupRtl`)
+
+`MockGamutProvider` forwards to `GamutProvider` with:
+
+- `useCache={false}` — stable Emotion output across tests
+- `useGlobals={false}` — no global Reboot/Typography bleed between files
+- `theme={theme}` — full token theme for styled components
+- Optional **`useLogicalProperties`** — forwarded for logical vs physical CSS in variance
+
+You normally **do not** import `MockGamutProvider` for plain component tests; `setupRtl` already wraps the **component under test** once. Import it **inside a harness** when the SUT needs a non-default provider flag or extra wrappers (see below).
+
+---
+
+## Decision guide
+
+| Scenario | Prefer |
+| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| Default unit test for a Gamut (or app) component | **`setupRtl(Component, defaultProps)`** once per file / describe |
+| Vary **`useLogicalProperties`** across cases | **Harness** that accepts `useLogicalProperties` and wraps **`MockGamutProvider`**, then **`setupRtl(Harness, defaults)`**; pass overrides per `it` / `describe.each` |
+| Need **`ColorMode`** (or other context) around the SUT | **Harness** with `<ColorMode>` inside the tree, then **`setupRtl(Harness)`** — no need for raw `render` unless you are testing the provider itself |
+| **`dir` / RTL** behavior (e.g. mirrored layout, `useElementDir`) | Keep using **`setupRtl`** for the component; set **`document.documentElement.setAttribute('dir', 'rtl' \| 'ltr')`** (and scroll/viewport stubs if needed) in **`beforeEach` / `afterEach`**; reset `dir` after tests so suites do not leak |
+| Storybook-only mock, chromatic-style wrapper, or non-RTL harness | **`MockGamutProvider`** (± **`ColorMode`**) in the exported wrapper component |
+
+---
+
+## `setupRtl` — primary pattern
+
+```tsx
+import { setupRtl } from '@codecademy/gamut-tests';
+
+import { MyComponent } from '../MyComponent';
+
+const renderView = setupRtl(MyComponent, {
+ label: 'Default label',
+ onClick: jest.fn(),
+});
+
+it('renders the label', () => {
+ const { view } = renderView();
+ expect(view.getByText('Default label')).toBeInTheDocument();
+});
+
+it('accepts prop overrides', () => {
+ const { view } = renderView({ label: 'Override' });
+ expect(view.getByText('Override')).toBeInTheDocument();
+});
+```
+
+`renderView` returns `{ view, props, update }`:
+
+- **`view`** — RTL `RenderResult` (`getByRole`, `getByLabelText`, `getByText`, …)
+- **`props`** — resolved props (handy for `jest.fn()` assertions)
+- **`update`** — re-render with new props without remounting
+
+### Query and interaction habits (RTL)
+
+- Prefer **`getByRole`**, **`getByLabelText`**, and accessible names over CSS selectors or snapshotting class strings unless you are explicitly testing styling.
+- Prefer **`@testing-library/user-event`** over `fireEvent` when simulating real input (import `userEvent` from **`@testing-library/user-event`** in current major versions).
+
+### Accessing mock functions via `props`
+
+```tsx
+import userEvent from '@testing-library/user-event';
+
+it('calls onClick when clicked', async () => {
+ const { view, props } = renderView();
+ await userEvent.click(view.getByRole('button'));
+ expect(props.onClick).toHaveBeenCalled();
+});
+```
+
+---
+
+## Harness + `setupRtl` when the wrapper is not default
+
+`setupRtl` always wraps with **`MockGamutProvider`** with default props. To vary **`useLogicalProperties`**, add **`ColorMode`**, or compose other providers, define a **small harness** and pass **`setupRtl`** that harness — still one `renderView` factory, still `props` / `update` ergonomics.
+
+### Varying `useLogicalProperties` (logical vs physical CSS)
+
+```tsx
+import { MockGamutProvider, setupRtl } from '@codecademy/gamut-tests';
+
+import { MyComponent } from '../MyComponent';
+
+type HarnessProps = React.ComponentProps<typeof MyComponent> & {
+ useLogicalProperties?: boolean;
+};
+
+const MyHarness = ({ useLogicalProperties, ...rest }: HarnessProps) => (
+ <MockGamutProvider useLogicalProperties={useLogicalProperties}>
+ <MyComponent {...rest} />
+ </MockGamutProvider>
+);
+
+const renderView = setupRtl(MyHarness, { width: '200px' });
+
+describe.each([
+ { useLogicalProperties: true as const, widthProp: 'inlineSize' as const },
+ { useLogicalProperties: false as const, widthProp: 'width' as const },
+])(
+ 'useLogicalProperties=$useLogicalProperties',
+ ({ useLogicalProperties, widthProp }) => {
+ it(`uses ${widthProp}`, () => {
+ const { view } = renderView({ useLogicalProperties });
+ expect(view.getByTestId('my-component-root')).toHaveStyle({
+ [widthProp]: '200px',
+ });
+ });
+ }
+);
+```
+
+The outer `setupRtl` wrapper adds a default **`MockGamutProvider`**; the harness’s inner **`MockGamutProvider`** sets **`useLogicalProperties`** for the subtree under test (nested `GamutProvider` / theme is the nearest one Emotion and variance see).
+
+### `ColorMode` without abandoning `setupRtl`
+
+```tsx
+import { ColorMode } from '@codecademy/gamut-styles';
+import { setupRtl } from '@codecademy/gamut-tests';
+
+const DarkHarness = (props: React.ComponentProps<typeof MyComponent>) => (
+ <ColorMode mode="dark">
+ <MyComponent {...props} />
+ </ColorMode>
+);
+
+const renderDark = setupRtl(DarkHarness, { title: 'Hi' });
+```
+
+Use **`MockGamutProvider`** only inside the harness if you also need a non-default Gamut flag **and** `ColorMode` in the same tree; otherwise **`setupRtl(DarkHarness)`** is enough.
+
+---
+
+## Raw `render` + `MockGamutProvider` — rare
+
+Reserve **`render` from `@testing-library/react`** + manual **`MockGamutProvider`** for cases where a harness would be more obscure than a single inline tree (e.g. highly dynamic one-off trees). If the same wrapper appears more than once, switch to a **harness + `setupRtl`**.
+
+---
+
+## RTL / `dir` and document-level behavior
+
+Some components (e.g. overlays that call **`useElementDir`**) resolve direction from **`document.documentElement`** when there is no real target node. For those tests:
+
+- Set **`document.documentElement.setAttribute('dir', 'rtl')`** (or `'ltr'`) around the scenario, **`unmount`** between LTR and RTL assertions when re-rendering, and restore **`dir`** in **`afterEach`** so other tests start clean.
+- Combine with the harness pattern above when **`useLogicalProperties`** affects which longhand wins (`left` vs `insetInlineStart`, etc.).
+
+---
+
+## Emotion style assertions
+
+Install **`@emotion/jest`** matchers if you absolutely need to enable CSS-in-JS assertions:
+
+```tsx
+import { matchers } from '@emotion/jest';
+
+expect.extend(matchers);
+```
+
+Then assert on styles:
+
+```tsx
+expect(element).toHaveStyle({ borderRadius: '2px' });
+expect(element).toHaveStyleRule('padding', '1rem');
+```
+
+Use **`theme`** from **`@codecademy/gamut-styles`** instead of hardcoding token strings:
+
+```tsx
+import { theme } from '@codecademy/gamut-styles';
+
+expect(element).toHaveStyle({ columnGap: theme.spacing[40] });
+```
+
+---
+
+## Visual test wrappers and Storybook
+
+Exported mocks and stories may wrap with **`MockGamutProvider`** and **`ColorMode`** explicitly (no `setupRtl` in Storybook):
+
+```tsx
+import { MockGamutProvider } from '@codecademy/gamut-tests';
+import { ColorMode } from '@codecademy/gamut-styles';
+
+export const MyComponentMock: React.FC<ComponentProps<typeof MyComponent>> = (
+ props
+) => (
+ <MockGamutProvider>
+ <ColorMode mode="light">
+ <MyComponent {...props} />
+ </ColorMode>
+ </MockGamutProvider>
+);
+```
+
+---
+
+## Common anti-patterns
+
+| Anti-pattern | Fix |
+| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
+| `jest.mock('@codecademy/gamut', () => ({ ... }))` | Remove; use **`setupRtl`** (or harness + **`setupRtl`**) |
+| `jest.mock('@codecademy/gamut-styles', ...)` | Remove; **`MockGamutProvider`** / **`setupRtl`** supplies theme |
+| **`GamutProvider`** in test files | Use **`MockGamutProvider`** only when building a harness or story; default tests go through **`setupRtl`** |
+| **`import { setupRtl } from 'component-test-setup'`** in Gamut / apps | Import **`setupRtl` from `@codecademy/gamut-tests`** so **`MockGamutProvider`** is applied |
+| Repeated **`render(<MockGamutProvider>…`** | **Harness + `setupRtl`**, or a shared **`renderView`** factory |
+| One **`setupRtl`** call per **`it`** | Define **`renderView`** once outside **`describe`**, call it inside each **`it`** |
+| Asserting raw CSS strings for tokens | Use **`theme`** from **`@codecademy/gamut-styles`** |
+| Leaking **`dir="rtl"`** between tests | Reset **`document.documentElement`** in **`afterEach`** |