Skip to content

Commit b6d9a86

Browse files
committed
Add react boundary tests
1 parent 3e910f8 commit b6d9a86

12 files changed

Lines changed: 233 additions & 0 deletions

File tree

.github/copilot-instructions.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,32 @@ Managed by Renovate (`config:js-lib` preset):
6060
- **100% code coverage** required across all build formats
6161
- Always run `npm test` after changes; use `npm run test:src` for quick
6262
source-only feedback during development
63+
- Use `npm run test:react` for the full React version matrix independently.
64+
It also runs as part of `npm test` (via the `test:*` glob).
65+
66+
### React version matrix
67+
68+
We test boundary versions only: first and last minor of each supported
69+
major. See `test/react/` for current versions.
70+
71+
Current boundaries: 16.14, 17.0, 18.0, 18.3, 19.0.
72+
73+
React 16.14 is the practical lower bound. Hooks require 16.8 and
74+
`@testing-library/react-hooks` requires 16.9.
75+
76+
When adding a new boundary:
77+
78+
1. Add `test/react/<version>/package.json` with correct `react`,
79+
`react-dom`, and `@testing-library/react` (12.x for React 16–17,
80+
16.x for React 18+). React 16–17 also need
81+
`@testing-library/react-hooks` (8.x) and `react-test-renderer`.
82+
2. Replace the previous "latest minor" for that major.
83+
3. Verify with a single-version run before the full matrix:
84+
```bash
85+
cd test/react/<version> && npm i --no-package-lock --quiet --no-progress
86+
REACT_VERSION=<version> npx jest --config ./scripts/jest/config.src.js --coverage false
87+
```
88+
4. Update the boundary list above.
6389

6490
## Examples
6591

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"clean:compiled": "shx rm -rf compiled",
2121
"clean:coverage": "shx rm -rf coverage",
2222
"clean:dist": "shx rm -rf dist",
23+
"clean:react": "node ./scripts/clean-react.js",
2324
"compile": "tsc -p tsconfig.base.json",
2425
"format": "npm run lint -- --fix && prettier --write \"**/*.{js,ts,tsx}\"",
2526
"lint": "eslint .",
@@ -30,6 +31,7 @@
3031
"test:cjs": "jest --config ./scripts/jest/config.cjs.js",
3132
"test:cjsprod": "jest --config ./scripts/jest/config.cjsprod.js",
3233
"test:es": "jest --config ./scripts/jest/config.es.js",
34+
"test:react": "node ./scripts/test-react.js",
3335
"test:src": "jest --config ./scripts/jest/config.src.js",
3436
"test:umd": "jest --config ./scripts/jest/config.umd.js",
3537
"test:umdprod": "jest --config ./scripts/jest/config.umdprod.js",

scripts/clean-react.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Cleans node_modules from each test/react/<version> directory while preserving
2+
// the package.json file.
3+
const fs = require('fs')
4+
const os = require('os')
5+
const path = require('path')
6+
7+
const reactDir = path.join(process.cwd(), 'test', 'react')
8+
9+
fs.readdirSync(reactDir, { withFileTypes: true })
10+
.filter((entry) => entry.isDirectory())
11+
.forEach((entry) => {
12+
const dir = path.join(reactDir, entry.name)
13+
const pkgJsonPath = path.join(dir, 'package.json')
14+
15+
if (!fs.existsSync(pkgJsonPath)) {
16+
return
17+
}
18+
19+
// Preserve package.json by copying to a temporary location.
20+
const tmpPath = path.join(os.tmpdir(), `${entry.name}-package.json`)
21+
fs.copyFileSync(pkgJsonPath, tmpPath)
22+
fs.rmSync(dir, { force: true, recursive: true })
23+
fs.mkdirSync(dir, { recursive: true })
24+
fs.copyFileSync(tmpPath, pkgJsonPath)
25+
})

scripts/jest/config.src.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,74 @@
1+
const path = require('path')
2+
3+
// When REACT_VERSION is set (e.g. "18.0"), resolve react, react-dom and
4+
// @testing-library/react from the matching test/react/<version> directory so
5+
// the test suite runs against that specific React version.
6+
const generateReactVersionMappings = (reactVersion) => {
7+
if (!reactVersion) {
8+
return {}
9+
}
10+
11+
const testDir = path.join(process.cwd(), 'test', 'react', reactVersion)
12+
const [major] = reactVersion.split('.').map(Number)
13+
14+
const reactDir = path.dirname(
15+
require.resolve('react/package.json', { paths: [testDir] }),
16+
)
17+
const reactDomDir = path.dirname(
18+
require.resolve('react-dom/package.json', { paths: [testDir] }),
19+
)
20+
21+
const mappings = {
22+
'^react$': require.resolve('react', { paths: [testDir] }),
23+
'^react-dom$': require.resolve('react-dom', { paths: [testDir] }),
24+
'^react-dom/(.*)$': `${reactDomDir}/$1`,
25+
'^react/(.*)$': `${reactDir}/$1`,
26+
}
27+
28+
// React 16 and 17 use @testing-library/react 12.x which does not export
29+
// renderHook. A shim re-exports it from @testing-library/react-hooks instead.
30+
if (major < 18) {
31+
mappings['^@testing-library/react$'] = path.join(
32+
testDir,
33+
'..',
34+
'testing-library-shim.js',
35+
)
36+
} else {
37+
mappings['^@testing-library/react$'] = require.resolve(
38+
'@testing-library/react',
39+
{ paths: [testDir] },
40+
)
41+
}
42+
43+
return mappings
44+
}
45+
46+
// React 16 and 17 boundary tests produce harmless "wrong act()" warnings from
47+
// @testing-library/react-hooks. A setup file suppresses them.
48+
const generateSetupFiles = (reactVersion) => {
49+
if (!reactVersion) {
50+
return []
51+
}
52+
53+
const [major] = reactVersion.split('.').map(Number)
54+
if (major < 18) {
55+
return [path.join(__dirname, 'setupReact.js')]
56+
}
57+
58+
return []
59+
}
60+
161
module.exports = {
262
collectCoverage: true,
363
collectCoverageFrom: ['src/*.{ts,tsx}'],
464
moduleFileExtensions: ['ts', 'tsx', 'js'],
65+
moduleNameMapper: {
66+
...generateReactVersionMappings(process.env.REACT_VERSION),
67+
},
568
preset: 'ts-jest',
669
rootDir: process.cwd(),
770
roots: ['<rootDir>/test'],
71+
setupFilesAfterEnv: generateSetupFiles(process.env.REACT_VERSION),
872
testEnvironment: 'jsdom',
973
testMatch: ['<rootDir>/test/*.spec.ts?(x)'],
1074
transform: { '^.+\\.(js|tsx?)$': 'ts-jest' },

scripts/jest/setupReact.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Suppresses the "wrong act()" console.error warnings that
2+
// @testing-library/react-hooks emits on React 16 and 17. The package uses
3+
// react-test-renderer internally while state updates flow through react-dom,
4+
// triggering a harmless mismatch warning.
5+
const originalError = console.error
6+
7+
console.error = (...args) => {
8+
if (
9+
typeof args[0] === 'string' &&
10+
args[0].includes("It looks like you're using the wrong act()")
11+
) {
12+
return
13+
}
14+
originalError.call(console, ...args)
15+
}

scripts/test-react.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Installs dependencies and runs the src test suite against each React version
2+
// defined in test/react/<version>.
3+
const { execSync } = require('child_process')
4+
const fs = require('fs')
5+
const path = require('path')
6+
7+
const reactDir = path.join(process.cwd(), 'test', 'react')
8+
9+
const versions = fs
10+
.readdirSync(reactDir, { withFileTypes: true })
11+
.filter((entry) => entry.isDirectory())
12+
.filter((entry) =>
13+
fs.existsSync(path.join(reactDir, entry.name, 'package.json')),
14+
)
15+
.map((entry) => entry.name)
16+
.sort()
17+
18+
for (const version of versions) {
19+
const dir = path.join(reactDir, version)
20+
21+
console.log(`Starting React ${version} tests`)
22+
23+
execSync('npm i --no-package-lock --quiet --no-progress', {
24+
cwd: dir,
25+
stdio: 'inherit',
26+
})
27+
28+
try {
29+
execSync(
30+
`REACT_VERSION=${version} npx jest --config ./scripts/jest/config.src.js --coverage false`,
31+
{ stdio: 'inherit' },
32+
)
33+
} catch {
34+
console.error(`Fail testing React ${version}`)
35+
process.exit(1)
36+
}
37+
38+
console.log(`Success testing React ${version}`)
39+
}

test/react/16.14/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"private": true,
3+
"devDependencies": {
4+
"@testing-library/react": "12.x",
5+
"@testing-library/react-hooks": "8.x",
6+
"react": "16.14.x",
7+
"react-dom": "16.14.x",
8+
"react-test-renderer": "16.14.x"
9+
}
10+
}

test/react/17.0/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"private": true,
3+
"devDependencies": {
4+
"@testing-library/react": "12.x",
5+
"@testing-library/react-hooks": "8.x",
6+
"react": "17.0.x",
7+
"react-dom": "17.0.x",
8+
"react-test-renderer": "17.0.x"
9+
}
10+
}

test/react/18.0/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"private": true,
3+
"devDependencies": {
4+
"@testing-library/react": "16.x",
5+
"react": "18.0.x",
6+
"react-dom": "18.0.x"
7+
}
8+
}

test/react/18.3/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"private": true,
3+
"devDependencies": {
4+
"@testing-library/react": "16.x",
5+
"react": "18.3.x",
6+
"react-dom": "18.3.x"
7+
}
8+
}

0 commit comments

Comments
 (0)