Skip to content

Commit cf64198

Browse files
csandmanclaude
andauthored
Add an integration test suite (#412)
* test: add vitest test suite with ported react-select tests Adds vitest + jsdom + @testing-library/{react,user-event,jest-dom} as devDependencies and configures a Chakra-provider-wrapped render helper. Ports the upstream react-select tests (Select, Async, Creatable, AsyncCreatable, StateManaged + the constants fixture) from react-select@5.10.2 with the adaptations the wrapper needs: imports from the package source, userEvent v14 async migration, and a small jest-in-case shim that honors per-case skip/only flags. Adds dedicated coverage for the props this package layers on top of react-select (size, variant, invalid, disabled, readOnly, chakraStyles, tagColorPalette, tagVariant, selectedOptionStyle, selectedOptionColorPalette, focusRingColor, useChakraSelectProps), using a probe pattern that captures chakraStyles slot state to bypass jsdom CSS-engine flakiness. Also fixes a small accessibility gap surfaced while writing tests: passing \`invalid\` directly to Select now propagates to aria-invalid on the rendered input, matching how Field.Root invalid inheritance already worked. A CI workflow runs the suite on push to main/v5 and on PR. Final tally: 271 passing, 4 skipped (upstream's own QUESTION-marked tests and intentional Chakra divergences). The .oxlintrc.json ignores src/tests/ since upstream's style differs from our strict project rules. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: limit test workflow push trigger to main Each branch's workflow only knows about its code, so listing both [main, v5] in a single workflow file is a footgun — only the branch whose name matches will actually run, and pushing to the other branch silently fires nothing useful from this file. Matches the existing convention used by lint.yml, pkg-pr.yml, and zizmor.yml. A parallel copy in the v5 branch should reference [v5] only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: extract lint-staged config and filter ignored paths Moves the lint-staged config from package.json into .lintstagedrc.cjs so we can use a function to filter file lists before invoking oxlint. oxlint exits non-zero with "No files found to lint" when every staged path matches its ignorePatterns (e.g. a commit touching only files under src/tests/), which broke the pre-commit hook for test-only changes. The filter drops src/tests/ paths before invoking oxlint; oxfmt still formats them via the catch-all glob. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: pin react-select source version in ported test files Each ported test file now carries a header comment with a permalink to the exact upstream source at react-select@5.10.2 plus a brief re-port checklist for future dep bumps. constants.ts gets a verbatim note since its data fixtures should stay bit-identical with upstream. No behavior change — comments only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: run tests across Node 20 and 24 Adds a Node version matrix to the test workflow so we catch regressions that depend on the runtime — particularly relevant given the test suite relies on jsdom whose CSS serialization differs across versions (we already hit a jsdom 22 -> 25 change in caret-color handling). 20 is the current LTS, 24 is the latest stable. fail-fast: false so a failure on one runner doesn't cancel the other and obscure which is broken. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: document the test suite in CONTRIBUTING Adds a `pnpm test` bullet to the pre-PR checklist and a new Tests section explaining the suite layout, the file naming convention, and the manual re-port checklist for `react-select` dep bumps. The intent is to make the porting boundary explicit so future contributors don't fold chakra-specific assertions back into the ported files or skip the upstream-source comments. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: switch lint-staged config to ESM `.cjs` was unnecessary in a `type: commonjs` project — a plain `.js` file already would have been CommonJS — and ESM is the better default for new config files. Behavior unchanged from the previous .cjs version. Uses `import.meta.dirname` (Node 20.11+); our CI matrix requires Node 20+. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: lint-clean .lintstagedrc.mjs Two layers: * In source: name the default export (satisfies import/no-anonymous-default-export) and add braces to the bare if/return (satisfies eslint/curly). Both fixes are easy improvements that match project style. * In config: add a *.mjs override that disables rules which fundamentally don't apply to root tooling configs — import/no-nodejs-modules (Node tooling legitimately imports node:built-ins) and the three typescript/no-unsafe-* rules (a .mjs file has no TS declarations, so type-aware oxlint sees everything as `any`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: enable oxlint on the test suite Removes `src/tests/` from `.oxlintrc.json` `ignorePatterns` so the tests now get linted alongside source. A scoped override exempts the small set of rules that legitimately don't apply to spy/mock-style test code: TS unsafe checks, the `cases()` non-null assertions, react-perf inline-prop rules, jsx-a11y rules for synthetic fixtures, and a handful of unicorn opinions that don't fit our jsdom env. Also adds the `vitest` plugin and enables most of its rules project-wide; the disables for vitest stylistic preferences (`prefer-to-be*`, `prefer-lowercase-title`, etc.) live at the top level since those rules are no-ops outside test files anyway. Source side: a sweep that converts the upstream-ported style to project conventions — `let` → `const` where appropriate, type-only import splits, `toHaveLength`/`toStrictEqual` matcher upgrades, function declarations to expressions, a few prefer-destructuring rewrites, regex `u` flags, etc. Two intentional patterns get per-line `oxlint-disable-next-line` comments (definite-assignment `let event!:`, the 4th-element index in state-managed-select). `init-declarations` ends up off project-wide. It conflicts with `unicorn/no-useless-undefined` (one wants `= undefined`, the other forbids it); we pick the no-useless-undefined style so `let captured: T | undefined;` stays uninitialised. The change goes at the top level rather than in the test override because the preference is consistent for source code too. Test counts: 271 passing, 4 skipped, 0 lint errors. Remaining lint output is 4 `no-warning-comments` warnings (2 pre-existing project TODOs in src/chakra-components, 2 upstream-ported `TODO: Cover more scenarios` in select.test.tsx) — kept as warnings intentionally to keep TODOs visible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 49a968b commit cf64198

18 files changed

Lines changed: 6161 additions & 16 deletions

.github/workflows/test.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
permissions: {}
9+
10+
jobs:
11+
test:
12+
runs-on: ubuntu-latest
13+
permissions:
14+
contents: read
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
node-version: [20, 24]
19+
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
23+
with:
24+
persist-credentials: false
25+
26+
- name: Install pnpm
27+
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
28+
29+
- name: Use Node.js ${{ matrix.node-version }}
30+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
31+
with:
32+
node-version: ${{ matrix.node-version }}
33+
cache: pnpm
34+
35+
- name: Install Dependencies
36+
run: pnpm install
37+
38+
- name: Run tests
39+
run: pnpm test

.lintstagedrc.mjs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// lint-staged config — moved here from package.json so we can use a
2+
// function to filter file lists before invoking oxlint. The default glob
3+
// picks up files under src/tests/, but oxlint excludes that directory via
4+
// ignorePatterns in .oxlintrc.json; passing it nothing-but-ignored files
5+
// makes oxlint exit non-zero with "No files found to lint", which breaks
6+
// the pre-commit hook for test-only changes. The filter drops src/tests/
7+
// paths before invoking oxlint; oxfmt still formats them.
8+
9+
import path from "node:path";
10+
11+
const IGNORED = ["src/tests/"];
12+
13+
const filterLintable = (files) =>
14+
files.filter((file) => {
15+
const rel = path.relative(import.meta.dirname, file);
16+
return !IGNORED.some((prefix) => rel.startsWith(prefix));
17+
});
18+
19+
const config = {
20+
"demo/**/*.{js,jsx,ts,tsx}":
21+
"oxlint -c demo/.oxlintrc.json --disable-nested-config --fix",
22+
"!(demo|codemod)/**/*.{js,jsx,ts,tsx}": (files) => {
23+
const lintable = filterLintable(files);
24+
if (lintable.length === 0) {
25+
return [];
26+
}
27+
return `oxlint -c .oxlintrc.json --disable-nested-config --fix ${lintable
28+
.map((f) => JSON.stringify(f))
29+
.join(" ")}`;
30+
},
31+
"*": "oxfmt",
32+
};
33+
34+
export default config;

.oxlintrc.json

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"jsdoc",
1212
"jsx-a11y",
1313
"node",
14-
"promise"
14+
"promise",
15+
"vitest"
1516
],
1617
"categories": {
1718
"correctness": "error",
@@ -58,7 +59,15 @@
5859
"no-proto": "error",
5960
"no-regex-spaces": "warn",
6061
"no-sequences": "error",
61-
"no-unused-vars": ["error", { "ignoreRestSiblings": true }],
62+
"no-unused-vars": [
63+
"error",
64+
{
65+
"ignoreRestSiblings": true,
66+
"argsIgnorePattern": "^_",
67+
"varsIgnorePattern": "^_",
68+
"caughtErrorsIgnorePattern": "^_"
69+
}
70+
],
6271
"no-var": "error",
6372
"no-void": "warn",
6473
"prefer-const": "error",
@@ -94,6 +103,7 @@
94103
"@typescript-eslint/strict-boolean-expressions": "off",
95104
"capitalized-comments": "off",
96105
"id-length": "off",
106+
"init-declarations": "off",
97107
"import/exports-last": "off",
98108
"import/group-exports": "off",
99109
"import/no-named-export": "off",
@@ -122,7 +132,60 @@
122132
"react-perf/jsx-no-new-function-as-prop": "off",
123133
"sort-imports": "off",
124134
"sort-keys": "off",
125-
"unicorn/no-null": "off"
135+
"unicorn/no-null": "off",
136+
137+
"vitest/consistent-test-it": [
138+
"error",
139+
{ "fn": "test", "withinDescribe": "test" }
140+
],
141+
"vitest/max-expects": "off",
142+
"vitest/no-conditional-in-test": "off",
143+
"vitest/no-hooks": "off",
144+
"vitest/no-importing-vitest-globals": "off",
145+
"vitest/no-standalone-expect": "off",
146+
"vitest/prefer-called-times": "off",
147+
"vitest/prefer-called-with": "off",
148+
"vitest/prefer-expect-assertions": "off",
149+
"vitest/prefer-lowercase-title": "off",
150+
"vitest/prefer-strict-boolean-matchers": "off",
151+
"vitest/prefer-to-be": "off",
152+
"vitest/prefer-to-be-falsy": "off",
153+
"vitest/prefer-to-be-truthy": "off",
154+
"vitest/require-hook": "off",
155+
"vitest/require-mock-type-parameters": "off"
126156
},
127-
"overrides": []
157+
"overrides": [
158+
{
159+
"files": ["*.mjs"],
160+
"rules": {
161+
"import/no-nodejs-modules": "off",
162+
"@typescript-eslint/no-unsafe-argument": "off",
163+
"@typescript-eslint/no-unsafe-call": "off",
164+
"@typescript-eslint/no-unsafe-return": "off"
165+
}
166+
},
167+
{
168+
"files": ["src/tests/**/*.{ts,tsx}"],
169+
"rules": {
170+
// ─── TS strictness relaxed for spy/mock-style test code ───
171+
"@typescript-eslint/no-non-null-assertion": "off",
172+
"@typescript-eslint/no-useless-default-assignment": "off",
173+
174+
// ─── General style/correctness rules tests legitimately break ───
175+
"no-console": "off",
176+
"no-param-reassign": "off",
177+
"no-underscore-dangle": "off",
178+
179+
// ─── JSX/a11y/perf rules not applicable to test fixtures ───
180+
"jsx-a11y/no-autofocus": "off",
181+
"react-perf/jsx-no-new-array-as-prop": "off",
182+
"react-perf/jsx-no-new-object-as-prop": "off",
183+
184+
// ─── Unicorn opinions that don't fit test patterns ───
185+
"unicorn/consistent-function-scoping": "off",
186+
"unicorn/prefer-at": "off",
187+
"unicorn/prefer-global-this": "off"
188+
}
189+
}
190+
]
128191
}

CONTRIBUTING.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Before you open a PR:
1515
- Please ensure all the examples work correctly after your change.
1616
- Also run `pnpm lint` to ensure that the change meets the projects code style
1717
setup.
18+
- Run `pnpm test` to verify the test suite still passes. The suite covers both
19+
behavior ported from `react-select` and chakra-specific extensions — see the
20+
[Tests](#tests) section below.
1821
- Make sure there's an issue open for any work you take on and intend to submit
1922
as a pull request - it helps core members review your concept and direction
2023
early and is a good way to discuss what you're planning to do.
@@ -23,3 +26,45 @@ Before you open a PR:
2326
your hard work.
2427
- All new features and changes need documentation. If you don't have time to
2528
write any, leave a note in your PR.
29+
30+
## Tests
31+
32+
The test suite lives in [`src/tests/`](./src/tests/) and runs with
33+
[Vitest](https://vitest.dev) against a `jsdom` environment plus
34+
`@testing-library/react`:
35+
36+
- `select.test.tsx`, `async-select.test.tsx`, `creatable-select.test.tsx`,
37+
`async-creatable-select.test.tsx`, `state-managed-select.test.tsx` — ported
38+
from `react-select`'s own `__tests__` directory. Each file's header
39+
comment carries a permalink to the upstream source at the pinned version.
40+
- `chakra-specific.test.tsx` — exercises the props this package adds on
41+
top of `react-select` (`size`, `variant`, `invalid`, `chakraStyles`,
42+
`tagColorPalette`, `selectedOptionStyle`, etc.).
43+
- `constants.ts` — option fixtures, copied verbatim from upstream.
44+
- `render.tsx`, `setup.ts`, `cases.ts` — local helpers (Chakra-wrapped
45+
render, jsdom polyfills, `jest-in-case` shim).
46+
47+
Useful commands:
48+
49+
- `pnpm test` — run the suite once
50+
- `pnpm test:watch` — re-run on file changes
51+
52+
### When bumping the `react-select` dependency
53+
54+
If a `react-select` bump introduces test changes, port them in as a manual
55+
step rather than rewriting the ported files from scratch:
56+
57+
1. Diff the upstream
58+
[`__tests__/` folder](https://github.com/JedWatson/react-select/tree/master/packages/react-select/src/__tests__)
59+
between the old and new tag — the header comment in each ported file links
60+
to the source at the currently pinned tag.
61+
2. For each upstream test that's new or whose body changed, port it
62+
following the same adaptations already in use:
63+
- imports from `../index` (not `../Select` etc.)
64+
- `render` from `./render` (provides Chakra context)
65+
- `userEvent.setup()` + `await user.click/type` (v14 async API)
66+
- `vi.fn` instead of `jest.fn`
67+
- skip snapshot tests
68+
3. Update each ported file's header permalink to point at the new tag.
69+
4. Leave chakra-specific tests in `chakra-specific.test.tsx` — don't fold
70+
chakra-only assertions into the ported files.

package.json

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,38 +62,42 @@
6262
"lint:types": "tsc",
6363
"lint-fix": "oxlint -c .oxlintrc.json --fix",
6464
"prepare": "husky",
65-
"prepublishOnly": "pnpm build && pnpm lint",
65+
"prepublishOnly": "pnpm build && pnpm lint && pnpm test",
6666
"postpublish": "git push --follow-tags",
67-
"lint:publish": "publint"
67+
"lint:publish": "publint",
68+
"test": "vitest run",
69+
"test:watch": "vitest"
6870
},
6971
"dependencies": {
7072
"react-select": "^5.10.2"
7173
},
7274
"devDependencies": {
7375
"@arethetypeswrong/cli": "^0.18.2",
7476
"@chakra-ui/react": "~3.35.0",
77+
"@testing-library/jest-dom": "^6.6.3",
78+
"@testing-library/react": "^16.3.0",
79+
"@testing-library/user-event": "^14.6.1",
7580
"@types/react": "^19.2.14",
81+
"@types/react-dom": "^19.2.0",
7682
"concurrently": "^9.2.1",
7783
"husky": "^9.1.7",
84+
"jsdom": "^25.0.1",
7885
"lint-staged": "^17.0.4",
7986
"next-themes": "~0.4.6",
8087
"oxfmt": "^0.49.0",
8188
"oxlint": "^1.64.0",
8289
"oxlint-tsgolint": "^0.22.1",
8390
"publint": "^0.3.21",
8491
"react": "^19.2.6",
92+
"react-dom": "^19.2.6",
8593
"tsup": "^8.5.1",
86-
"typescript": "^6.0.3"
94+
"typescript": "^6.0.3",
95+
"vitest": "^4.1.6"
8796
},
8897
"peerDependencies": {
8998
"@chakra-ui/react": "3.x",
9099
"next-themes": "0.x",
91100
"react": "18.x || 19.x"
92101
},
93-
"lint-staged": {
94-
"demo/**/*.{js,jsx,ts,tsx}": "oxlint -c demo/.oxlintrc.json --disable-nested-config --fix",
95-
"!(demo|codemod)/**/*.{js,jsx,ts,tsx}": "oxlint -c .oxlintrc.json --disable-nested-config --fix",
96-
"*": "oxfmt"
97-
},
98102
"packageManager": "pnpm@11.1.1+sha512.d1fdf5f73c617b64fa1a56a81c3c8dfe0e966e33a6010aa256b517ae77be21d93e05affc0de1a83b0e4f29d569f68b446ae8f068cd7247c0bb3df0fb4d7bdf9a"
99103
}

0 commit comments

Comments
 (0)