@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`**                                                    |