diff --git a/.changeset/easy-waves-pump.md b/.changeset/easy-waves-pump.md new file mode 100644 index 0000000000..6b6239c210 --- /dev/null +++ b/.changeset/easy-waves-pump.md @@ -0,0 +1,6 @@ +--- +"@lynx-js/react-webpack-plugin": patch +"@lynx-js/react-rsbuild-plugin": patch +--- + +Support rstest for testing library using a dedicated testing loader. diff --git a/.changeset/fair-plums-share.md b/.changeset/fair-plums-share.md new file mode 100644 index 0000000000..a6a64189b7 --- /dev/null +++ b/.changeset/fair-plums-share.md @@ -0,0 +1,11 @@ +--- +"@lynx-js/testing-environment": minor +--- + +**BREAKING CHANGE**: + +Align the public test-environment API around `LynxEnv`. + +`LynxTestingEnv` now expects a `{ window }`-shaped environment instead of relying on a concrete `JSDOM` instance or `global.jsdom`. Callers that construct `LynxTestingEnv` manually or initialize the environment through globals should migrate to `new LynxTestingEnv({ window })` or set `global.lynxEnv`. + +This release also adds the `@lynx-js/testing-environment/env/rstest` entry for running the shared testing-environment suite under rstest. diff --git a/.changeset/ninety-pants-tease.md b/.changeset/ninety-pants-tease.md new file mode 100644 index 0000000000..644f29a76b --- /dev/null +++ b/.changeset/ninety-pants-tease.md @@ -0,0 +1,5 @@ +--- +"create-rspeedy": patch +--- + +Add Rstest ReactLynx Testing Library template. diff --git a/.changeset/red-lamps-arrive.md b/.changeset/red-lamps-arrive.md new file mode 100644 index 0000000000..4ad4b4d80e --- /dev/null +++ b/.changeset/red-lamps-arrive.md @@ -0,0 +1,26 @@ +--- +"@lynx-js/react": patch +--- + +Support rstest for testing library, you can use rstest with RLTL now: + +Create a config file `rstest.config.ts` with the following content: + +```ts +import { defineConfig } from '@rstest/core'; +import { withLynxConfig } from '@lynx-js/react/testing-library/rstest-config'; + +export default defineConfig({ + extends: withLynxConfig(), +}); +``` + +`@lynx-js/react/testing-library/rstest-config` will automatically load your `lynx.config.ts` and apply the same configuration to rstest, so you can keep your test environment consistent with your development environment. + +And then use rstest as usual: + +```bash +$ rstest +``` + +For more usage detail, see https://rstest.rs/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 85af63dbd9..86a67e9b3f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -210,6 +210,15 @@ jobs: pnpm run build --mode development pnpm run lint pnpm run test + cd `mktemp -d` + npx --registry http://localhost:4873 create-rspeedy-canary@latest --template react --dir create-rspeedy-regression-rstest-rltl --tools eslint,rstest-rltl + cd create-rspeedy-regression-rstest-rltl + npx --registry http://localhost:4873 upgrade-rspeedy-canary@latest + pnpm install --registry=http://localhost:4873 + pnpm run build + pnpm run build --mode development + pnpm run lint + pnpm run test test-react: needs: build uses: ./.github/workflows/workflow-test.yml diff --git a/package.json b/package.json index 645114aaa6..9efd05c8e6 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@rsbuild/core": "catalog:rsbuild", "@rslib/core": "^0.19.6", "@rspack/core": "catalog:rspack", + "@rstest/core": "catalog:rstest", "@svitejs/changesets-changelog-github-compact": "^1.2.0", "@tsconfig/node22": "^22.0.5", "@tsconfig/strictest": "^2.0.8", diff --git a/packages/lynx/gesture-runtime/vitest.config.ts b/packages/lynx/gesture-runtime/vitest.config.ts index 3e8c26be58..d7d6117429 100644 --- a/packages/lynx/gesture-runtime/vitest.config.ts +++ b/packages/lynx/gesture-runtime/vitest.config.ts @@ -1,8 +1,10 @@ -import { defineConfig, mergeConfig } from 'vitest/config'; -import { createVitestConfig } from '@lynx-js/react/testing-library/vitest-config'; +import { defineConfig } from 'vitest/config'; +import { vitestTestingLibraryPlugin } from '@lynx-js/react/testing-library/plugins'; -const defaultConfig = await createVitestConfig(); -const config = defineConfig({ +export default defineConfig({ + plugins: [ + vitestTestingLibraryPlugin(), + ], test: { name: 'lynx/gesture-runtime', setupFiles: ['__test__/utils/setup.ts'], @@ -13,5 +15,3 @@ const config = defineConfig({ exclude: ['__test__/utils/**'], }, }); - -export default mergeConfig(defaultConfig, config); diff --git a/packages/motion/vitest.config.ts b/packages/motion/vitest.config.ts index d403130d3b..4f96392ee9 100644 --- a/packages/motion/vitest.config.ts +++ b/packages/motion/vitest.config.ts @@ -1,8 +1,10 @@ -import { defineConfig, mergeConfig } from 'vitest/config'; -import { createVitestConfig } from '@lynx-js/react/testing-library/vitest-config'; +import { defineConfig } from 'vitest/config'; +import { vitestTestingLibraryPlugin } from '@lynx-js/react/testing-library/plugins'; -const defaultConfig = await createVitestConfig(); -const config = defineConfig({ +export default defineConfig({ + plugins: [ + vitestTestingLibraryPlugin(), + ], test: { include: ['__tests__/**/*.test.{js,ts,jsx,tsx}'], exclude: ['__tests__/utils/**'], @@ -11,5 +13,3 @@ const config = defineConfig({ }, }, }); - -export default mergeConfig(defaultConfig, config); diff --git a/packages/react/package.json b/packages/react/package.json index b30bbeded1..95909d372a 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -101,9 +101,17 @@ "default": "./testing-library/dist/pure.js" }, "./testing-library/vitest-config": { - "types": "./testing-library/types/vitest-config.d.ts", + "types": "./testing-library/dist/vitest.config.d.ts", "default": "./testing-library/dist/vitest.config.js" }, + "./testing-library/plugins": { + "types": "./testing-library/dist/plugins/index.d.ts", + "default": "./testing-library/dist/plugins/index.js" + }, + "./testing-library/rstest-config": { + "types": "./testing-library/dist/rstest-config.d.ts", + "default": "./testing-library/dist/rstest-config.js" + }, "./package.json": "./package.json" }, "types": "./types/react.d.ts", diff --git a/packages/react/testing-library/.npmignore b/packages/react/testing-library/.npmignore index cb433b7a07..b1e6104efa 100644 --- a/packages/react/testing-library/.npmignore +++ b/packages/react/testing-library/.npmignore @@ -1,4 +1,6 @@ * -!dist/* -!dist/env/* +* +!dist +!dist/**/* +!types !types/* diff --git a/packages/react/testing-library/README.md b/packages/react/testing-library/README.md index b14b28d9e6..50e173cdeb 100644 --- a/packages/react/testing-library/README.md +++ b/packages/react/testing-library/README.md @@ -8,30 +8,69 @@ Similar to [react-testing-library](https://github.com/testing-library/react-test ## Setup +### Rstest + +Setup rstest with `@lynx-js/react/testing-library/rstest-config`. + +Recommended for library projects: + +```ts +import { defineConfig } from '@rstest/core'; +import { withDefaultConfig } from '@lynx-js/react/testing-library/rstest-config'; + +export default defineConfig({ + extends: withDefaultConfig(), +}); +``` + +Use `withLynxConfig` when you want to reuse your app's `lynx.config.ts`: + +```ts +// rstest.config.ts +import { defineConfig } from '@rstest/core'; +import { withLynxConfig } from '@lynx-js/react/testing-library/rstest-config'; + +export default defineConfig({ + extends: withLynxConfig(), +}); +``` + +Difference between `withLynxConfig` and `withDefaultConfig`: + +- `withLynxConfig`: app-oriented. Loads your `lynx.config.ts` and converts it to rstest config, so rspeedy/lynx settings are reused in tests. +- `withDefaultConfig`: library-oriented. Only applies testing-library defaults (`jsdom`, setup files, globals) and lets you provide the rest via `modifyRstestConfig`. + +Then you can start writing tests and run them with rstest! + +For more usage detail, see https://rstest.rs/ + +### Vitest + Setup vitest: ```js -// vitest.config.js -import { defineConfig, mergeConfig } from 'vitest/config'; -import { createVitestConfig } from '@lynx-js/react/testing-library/vitest-config'; +import { defineConfig } from 'vitest/config'; +import { vitestTestingLibraryPlugin } from '@lynx-js/react/testing-library/plugins'; -const defaultConfig = createVitestConfig(); -const config = defineConfig({ +export default defineConfig({ + plugins: [ + vitestTestingLibraryPlugin(), + ], test: { // ... }, }); - -export default mergeConfig(defaultConfig, config); ``` Then you can start writing tests and run them with vitest! +`createVitestConfig` is still supported for backward compatibility, but is deprecated. + ## Usage -```js +```jsx import '@testing-library/jest-dom'; -import { test, expect } from 'vitest'; +import { test, expect } from 'vitest'; // or '@rstest/core' import { render } from '@lynx-js/react/testing-library'; test('renders options.wrapper around node', async () => { diff --git a/packages/react/testing-library/package.json b/packages/react/testing-library/package.json index cedf70b6a0..cad2bd3569 100644 --- a/packages/react/testing-library/package.json +++ b/packages/react/testing-library/package.json @@ -6,6 +6,7 @@ "scripts": { "build": "rslib build", "dev": "rslib build --watch", + "rstest": "rstest", "test": "npm run test:base && npm run test:3.1", "test:3.1": "vitest --config vitest.3.1.config.ts", "test:base": "vitest", @@ -13,7 +14,11 @@ }, "devDependencies": { "@lynx-js/react": "workspace:*", + "@lynx-js/react-rsbuild-plugin": "workspace:*", + "@lynx-js/rspeedy": "workspace:*", "@lynx-js/testing-environment": "workspace:*", + "@rsbuild/core": "catalog:rsbuild", + "@rstest/adapter-rsbuild": "^0.2.3", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1" } diff --git a/packages/react/testing-library/rslib.config.ts b/packages/react/testing-library/rslib.config.ts index 10790990fe..117f815cda 100644 --- a/packages/react/testing-library/rslib.config.ts +++ b/packages/react/testing-library/rslib.config.ts @@ -9,12 +9,14 @@ export default defineConfig({ { format: 'esm', syntax: 'es2022', - dts: false, + dts: true, bundle: true, source: { entry: { 'pure': './src/pure.jsx', - 'env/vitest': './src/env/vitest.ts', + 'env/index': './src/env/index.ts', + 'plugins/index': './src/plugins/index.ts', + 'rstest-config': './src/rstest-config.ts', }, }, output: { @@ -23,6 +25,9 @@ export default defineConfig({ /^\.\.\/\.\.\/runtime\/lib/, /^preact/, /^vitest/, + '@rstest/core', + '@rsbuild/core', + '@lynx-js/rspeedy', ], }, }, @@ -33,9 +38,13 @@ export default defineConfig({ bundle: false, source: { entry: { - 'index': './src/index.jsx', - 'vitest.config': './src/vitest.config.js', - 'vitest-global-setup': './src/vitest-global-setup.js', + 'index': [ + './src/index.jsx', + './src/vitest.config.ts', + './src/env/vitest.ts', + './src/env/rstest.ts', + './src/setupFiles/**/*.js', + ], }, }, output: { diff --git a/packages/react/testing-library/rstest.config.ts b/packages/react/testing-library/rstest.config.ts new file mode 100644 index 0000000000..803991dfd7 --- /dev/null +++ b/packages/react/testing-library/rstest.config.ts @@ -0,0 +1,44 @@ +import { defineConfig } from '@rstest/core'; +import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin'; +import { withDefaultConfig } from './src/rstest-config.ts'; + +export default defineConfig({ + extends: withDefaultConfig({ + modifyRstestConfig(config) { + return { + ...config, + tools: { + swc: { + jsc: { + transform: { + useDefineForClassFields: true, + }, + }, + }, + }, + plugins: [ + ...(config.plugins || []), + pluginReactLynx(), + ], + source: { + ...config.source, + define: { + ...config.source?.define, + __ALOG__: 'true', + }, + }, + resolve: { + ...config.resolve, + // in order to make our test case work for + // both vitest and rstest, we need to alias + // `vitest` to `@rstest/core` + alias: { + ...config.resolve?.alias, + vitest: require.resolve('./vitest-polyfill.cjs'), + }, + }, + include: ['src/**/*.test.{js,jsx,ts,tsx}', '!src/__tests__/3.1/**/*.{js,jsx,ts,tsx}'], + }; + }, + }), +}); diff --git a/packages/react/testing-library/src/__tests__/act.test.jsx b/packages/react/testing-library/src/__tests__/act.test.jsx index fbd2970ccf..48706037d6 100644 --- a/packages/react/testing-library/src/__tests__/act.test.jsx +++ b/packages/react/testing-library/src/__tests__/act.test.jsx @@ -201,7 +201,7 @@ test('fireEvent triggers useEffect calls', async () => { [ "rLynxChange", { - "data": "{"patchList":[{"snapshotPatch":[0,"__snapshot_e8d0a_test_4",2,4,2,[1],0,null,3,4,3,[0],1,2,3,null,1,-1,2,null],"id":2}]}", + "data": "{"patchList":[{"snapshotPatch":[0,"__snapshot_268b9_test_4",2,4,2,[1],0,null,3,4,3,[0],1,2,3,null,1,-1,2,null],"id":2}]}", "patchOptions": { "isHydration": true, "pipelineOptions": { @@ -236,7 +236,7 @@ test('fireEvent triggers useEffect calls', async () => { ], "extraProps": undefined, "id": 2, - "type": "__snapshot_e8d0a_test_4", + "type": "__snapshot_268b9_test_4", "values": [ "2:0:", ], @@ -261,7 +261,7 @@ test('fireEvent triggers useEffect calls', async () => { ], "extraProps": undefined, "id": 2, - "type": "__snapshot_e8d0a_test_4", + "type": "__snapshot_268b9_test_4", "values": [ "2:0:", ], @@ -285,7 +285,7 @@ test('fireEvent triggers useEffect calls', async () => { [ "rLynxChange", { - "data": "{"patchList":[{"snapshotPatch":[0,"__snapshot_e8d0a_test_4",2,4,2,[1],0,null,3,4,3,[0],1,2,3,null,1,-1,2,null],"id":2}]}", + "data": "{"patchList":[{"snapshotPatch":[0,"__snapshot_268b9_test_4",2,4,2,[1],0,null,3,4,3,[0],1,2,3,null,1,-1,2,null],"id":2}]}", "patchOptions": { "isHydration": true, "pipelineOptions": { diff --git a/packages/react/testing-library/src/__tests__/alog.test.jsx b/packages/react/testing-library/src/__tests__/alog.test.jsx index 2ebadd6119..e4d1acce1a 100644 --- a/packages/react/testing-library/src/__tests__/alog.test.jsx +++ b/packages/react/testing-library/src/__tests__/alog.test.jsx @@ -154,7 +154,98 @@ describe('alog', () => { "[MainThread Component Render] name: App", ], [ - "[ReactLynxDebug] FiberElement API call #32: __OnLifecycleEvent(["rLynxFirstScreen", {"root":"{\\"id\\":-1,\\"type\\":\\"root\\",\\"children\\":[{\\"id\\":-2,\\"type\\":\\"__snapshot_426db_test_1\\",\\"values\\":[\\"-2:0:\\",\\"-2:1:\\"],\\"children\\":[{\\"id\\":-3,\\"type\\":\\"wrapper\\",\\"children\\":[{\\"id\\":-4,\\"type\\":null,\\"values\\":[0]}]},{\\"id\\":-5,\\"type\\":\\"wrapper\\",\\"children\\":[{\\"id\\":-6,\\"type\\":\\"__snapshot_426db_test_2\\"},{\\"id\\":-7,\\"type\\":\\"__snapshot_426db_test_3\\"}]}]}]}","jsReadyEventIdSwap":{}}])", + "[ReactLynxDebug] FiberElement API call #32: __OnLifecycleEvent(["rLynxFirstScreen", {"root":"{\\"id\\":-1,\\"type\\":\\"root\\",\\"children\\":[{\\"id\\":-2,\\"type\\":\\"__snapshot_d6fb6_test_1\\",\\"values\\":[\\"-2:0:\\",\\"-2:1:\\"],\\"children\\":[{\\"id\\":-3,\\"type\\":\\"wrapper\\",\\"children\\":[{\\"id\\":-4,\\"type\\":null,\\"values\\":[0]}]},{\\"id\\":-5,\\"type\\":\\"wrapper\\",\\"children\\":[{\\"id\\":-6,\\"type\\":\\"__snapshot_d6fb6_test_2\\"},{\\"id\\":-7,\\"type\\":\\"__snapshot_d6fb6_test_3\\"}]}]}]}","jsReadyEventIdSwap":{}}])", + ], + [ + "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_d6fb6_test_2, __id: 6", + ], + [ + "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_d6fb6_test_3, __id: 7", + ], + [ + "[BackgroundThread Component Render] name: App, uniqID: __snapshot_d6fb6_test_1, __id: 2", + ], + [ + "[BackgroundThread Component Render] name: Fragment, uniqID: __snapshot_d6fb6_test_1, __id: 2", + ], + [ + "[ReactLynxDebug] MTS -> BTS OnLifecycleEvent: + { + "root": { + "id": -1, + "type": "root", + "children": [ + { + "id": -2, + "type": "__snapshot_d6fb6_test_1", + "values": [ + "-2:0:", + "-2:1:" + ], + "children": [ + { + "id": -3, + "type": "wrapper", + "children": [ + { + "id": -4, + "type": null, + "values": [ + 0 + ] + } + ] + }, + { + "id": -5, + "type": "wrapper", + "children": [ + { + "id": -6, + "type": "__snapshot_d6fb6_test_2" + }, + { + "id": -7, + "type": "__snapshot_d6fb6_test_3" + } + ] + } + ] + } + ] + }, + "jsReadyEventIdSwap": {} + }", + ], + [ + "[ReactLynxDebug] SnapshotInstance tree for first screen hydration: + | -1(root): undefined + | -2(__snapshot_d6fb6_test_1): ["-2:0:","-2:1:"] + | -3(wrapper): undefined + | -4(null): [0] + | -5(wrapper): undefined + | -6(__snapshot_d6fb6_test_2): undefined + | -7(__snapshot_d6fb6_test_3): undefined", + ], + [ + "[ReactLynxDebug] BackgroundSnapshotInstance tree before hydration: + | 1(root): undefined + | 2(__snapshot_d6fb6_test_1): [null,null] + | 3(wrapper): undefined + | 4(null): [0] + | 5(wrapper): undefined + | 6(__snapshot_d6fb6_test_2): undefined + | 7(__snapshot_d6fb6_test_3): undefined", + ], + [ + "[ReactLynxDebug] BackgroundSnapshotInstance after hydration: + | -1(root): undefined + | -2(__snapshot_d6fb6_test_1): [null,null] + | -3(wrapper): undefined + | -4(null): [0] + | -5(wrapper): undefined + | -6(__snapshot_d6fb6_test_2): undefined + | -7(__snapshot_d6fb6_test_3): undefined", ], [ "[ReactLynxDebug] BTS -> MTS updateMainThread: @@ -183,6 +274,33 @@ describe('alog', () => { [ "[ReactLynxDebug] FiberElement API call #33: __FlushElementTree(PAGE#0, {"pipelineOptions":{"pipelineID":"pipelineID","needTimestamps":true,"pipelineOrigin":"reactLynxHydrate","dsl":"reactLynx","stage":"hydrate"}})", ], + [ + "[ReactLynxDebug] BTS received event: + { + "handlerName": "-2:0:", + "type": "bindEvent:tap", + "snapshotType": "__snapshot_d6fb6_test_1", + "jsFunctionName": "" + }", + ], + [ + "[ReactLynxDebug] BTS received event: + { + "handlerName": "-2:1:", + "type": "catchEvent:focus", + "snapshotType": "__snapshot_d6fb6_test_1", + "jsFunctionName": "handleFocus" + }", + ], + [ + "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_d6fb6_test_2, __id: -6", + ], + [ + "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_d6fb6_test_3, __id: -7", + ], + [ + "[BackgroundThread Component Render] name: App, uniqID: __snapshot_d6fb6_test_1, __id: -2", + ], [ "[ReactLynxDebug] BTS -> MTS updateMainThread: { @@ -224,16 +342,121 @@ describe('alog', () => { expect(lynxTestingEnv.backgroundThread.console.alog.mock.calls).toMatchInlineSnapshot(` [ [ - "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_426db_test_2, __id: 6", + "[ReactLynxDebug] FiberElement API call #1: __CreatePage("0", 0) => PAGE#0", + ], + [ + "[ReactLynxDebug] FiberElement API call #2: __GetElementUniqueID(PAGE#0) => 0", + ], + [ + "[ReactLynxDebug] FiberElement API call #3: __SetCSSId([PAGE#0], 0)", + ], + [ + "[ReactLynxDebug] FiberElement API call #4: __CreateView(0) => VIEW#1", + ], + [ + "[ReactLynxDebug] FiberElement API call #5: __CreateText(0) => TEXT#2", + ], + [ + "[ReactLynxDebug] FiberElement API call #6: __AddDataset(TEXT#2, "testid", "count-text")", + ], + [ + "[ReactLynxDebug] FiberElement API call #7: __AppendElement(VIEW#1, TEXT#2)", + ], + [ + "[ReactLynxDebug] FiberElement API call #8: __CreateRawText("count: ") => #text#3", + ], + [ + "[ReactLynxDebug] FiberElement API call #9: __AppendElement(TEXT#2, #text#3)", + ], + [ + "[ReactLynxDebug] FiberElement API call #10: __CreateWrapperElement(0) => WRAPPER#4", + ], + [ + "[ReactLynxDebug] FiberElement API call #11: __AppendElement(TEXT#2, WRAPPER#4)", + ], + [ + "[ReactLynxDebug] FiberElement API call #12: __CreateWrapperElement(0) => WRAPPER#5", + ], + [ + "[ReactLynxDebug] FiberElement API call #13: __AppendElement(VIEW#1, WRAPPER#5)", + ], + [ + "[ReactLynxDebug] FiberElement API call #14: __AppendElement(PAGE#0, VIEW#1)", + ], + [ + "[ReactLynxDebug] FiberElement API call #15: __AddEvent(TEXT#2, "bindEvent", "tap", "-2:0:")", + ], + [ + "[ReactLynxDebug] FiberElement API call #16: __AddEvent(TEXT#2, "catchEvent", "focus", "-2:1:")", + ], + [ + "[ReactLynxDebug] FiberElement API call #17: __CreateWrapperElement(0) => WRAPPER#6", + ], + [ + "[ReactLynxDebug] FiberElement API call #18: __ReplaceElement(WRAPPER#6, WRAPPER#4)", + ], + [ + "[ReactLynxDebug] FiberElement API call #19: __CreateRawText("") => #text#7", + ], + [ + "[ReactLynxDebug] FiberElement API call #20: __SetAttribute(#text#7, "text", 0)", + ], + [ + "[ReactLynxDebug] FiberElement API call #21: __AppendElement(WRAPPER#6, #text#7)", + ], + [ + "[ReactLynxDebug] FiberElement API call #22: __CreateWrapperElement(0) => WRAPPER#8", + ], + [ + "[ReactLynxDebug] FiberElement API call #23: __ReplaceElement(WRAPPER#8, WRAPPER#5)", + ], + [ + "[ReactLynxDebug] FiberElement API call #24: __CreateView(0) => VIEW#9", + ], + [ + "[ReactLynxDebug] FiberElement API call #25: __CreateRawText("Class Component") => #text#10", + ], + [ + "[ReactLynxDebug] FiberElement API call #26: __AppendElement(VIEW#9, #text#10)", + ], + [ + "[ReactLynxDebug] FiberElement API call #27: __AppendElement(WRAPPER#8, VIEW#9)", + ], + [ + "[MainThread Component Render] name: ClassComponent", + ], + [ + "[ReactLynxDebug] FiberElement API call #28: __CreateView(0) => VIEW#11", + ], + [ + "[ReactLynxDebug] FiberElement API call #29: __CreateRawText("Function Component") => #text#12", + ], + [ + "[ReactLynxDebug] FiberElement API call #30: __AppendElement(VIEW#11, #text#12)", + ], + [ + "[ReactLynxDebug] FiberElement API call #31: __AppendElement(WRAPPER#8, VIEW#11)", + ], + [ + "[MainThread Component Render] name: FunctionComponent", + ], + [ + "[MainThread Component Render] name: App", + ], + [ + "[ReactLynxDebug] FiberElement API call #32: __OnLifecycleEvent(["rLynxFirstScreen", {"root":"{\\"id\\":-1,\\"type\\":\\"root\\",\\"children\\":[{\\"id\\":-2,\\"type\\":\\"__snapshot_d6fb6_test_1\\",\\"values\\":[\\"-2:0:\\",\\"-2:1:\\"],\\"children\\":[{\\"id\\":-3,\\"type\\":\\"wrapper\\",\\"children\\":[{\\"id\\":-4,\\"type\\":null,\\"values\\":[0]}]},{\\"id\\":-5,\\"type\\":\\"wrapper\\",\\"children\\":[{\\"id\\":-6,\\"type\\":\\"__snapshot_d6fb6_test_2\\"},{\\"id\\":-7,\\"type\\":\\"__snapshot_d6fb6_test_3\\"}]}]}]}","jsReadyEventIdSwap":{}}])", ], [ - "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_426db_test_3, __id: 7", + "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_d6fb6_test_2, __id: 6", ], [ - "[BackgroundThread Component Render] name: App, uniqID: __snapshot_426db_test_1, __id: 2", + "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_d6fb6_test_3, __id: 7", ], [ - "[BackgroundThread Component Render] name: Fragment, uniqID: __snapshot_426db_test_1, __id: 2", + "[BackgroundThread Component Render] name: App, uniqID: __snapshot_d6fb6_test_1, __id: 2", + ], + [ + "[BackgroundThread Component Render] name: Fragment, uniqID: __snapshot_d6fb6_test_1, __id: 2", ], [ "[ReactLynxDebug] MTS -> BTS OnLifecycleEvent: @@ -244,7 +467,7 @@ describe('alog', () => { "children": [ { "id": -2, - "type": "__snapshot_426db_test_1", + "type": "__snapshot_d6fb6_test_1", "values": [ "-2:0:", "-2:1:" @@ -269,11 +492,11 @@ describe('alog', () => { "children": [ { "id": -6, - "type": "__snapshot_426db_test_2" + "type": "__snapshot_d6fb6_test_2" }, { "id": -7, - "type": "__snapshot_426db_test_3" + "type": "__snapshot_d6fb6_test_3" } ] } @@ -287,39 +510,66 @@ describe('alog', () => { [ "[ReactLynxDebug] SnapshotInstance tree for first screen hydration: | -1(root): undefined - | -2(__snapshot_426db_test_1): ["-2:0:","-2:1:"] + | -2(__snapshot_d6fb6_test_1): ["-2:0:","-2:1:"] | -3(wrapper): undefined | -4(null): [0] | -5(wrapper): undefined - | -6(__snapshot_426db_test_2): undefined - | -7(__snapshot_426db_test_3): undefined", + | -6(__snapshot_d6fb6_test_2): undefined + | -7(__snapshot_d6fb6_test_3): undefined", ], [ "[ReactLynxDebug] BackgroundSnapshotInstance tree before hydration: | 1(root): undefined - | 2(__snapshot_426db_test_1): [null,null] + | 2(__snapshot_d6fb6_test_1): [null,null] | 3(wrapper): undefined | 4(null): [0] | 5(wrapper): undefined - | 6(__snapshot_426db_test_2): undefined - | 7(__snapshot_426db_test_3): undefined", + | 6(__snapshot_d6fb6_test_2): undefined + | 7(__snapshot_d6fb6_test_3): undefined", ], [ "[ReactLynxDebug] BackgroundSnapshotInstance after hydration: | -1(root): undefined - | -2(__snapshot_426db_test_1): [null,null] + | -2(__snapshot_d6fb6_test_1): [null,null] | -3(wrapper): undefined | -4(null): [0] | -5(wrapper): undefined - | -6(__snapshot_426db_test_2): undefined - | -7(__snapshot_426db_test_3): undefined", + | -6(__snapshot_d6fb6_test_2): undefined + | -7(__snapshot_d6fb6_test_3): undefined", + ], + [ + "[ReactLynxDebug] BTS -> MTS updateMainThread: + { + "data": { + "patchList": [ + { + "snapshotPatch": [], + "id": 2 + } + ] + }, + "patchOptions": { + "isHydration": true, + "reloadVersion": 0, + "pipelineOptions": { + "pipelineID": "pipelineID", + "needTimestamps": true, + "pipelineOrigin": "reactLynxHydrate", + "dsl": "reactLynx", + "stage": "hydrate" + } + } + }", + ], + [ + "[ReactLynxDebug] FiberElement API call #33: __FlushElementTree(PAGE#0, {"pipelineOptions":{"pipelineID":"pipelineID","needTimestamps":true,"pipelineOrigin":"reactLynxHydrate","dsl":"reactLynx","stage":"hydrate"}})", ], [ "[ReactLynxDebug] BTS received event: { "handlerName": "-2:0:", "type": "bindEvent:tap", - "snapshotType": "__snapshot_426db_test_1", + "snapshotType": "__snapshot_d6fb6_test_1", "jsFunctionName": "" }", ], @@ -328,18 +578,54 @@ describe('alog', () => { { "handlerName": "-2:1:", "type": "catchEvent:focus", - "snapshotType": "__snapshot_426db_test_1", + "snapshotType": "__snapshot_d6fb6_test_1", "jsFunctionName": "handleFocus" }", ], [ - "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_426db_test_2, __id: -6", + "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_d6fb6_test_2, __id: -6", + ], + [ + "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_d6fb6_test_3, __id: -7", + ], + [ + "[BackgroundThread Component Render] name: App, uniqID: __snapshot_d6fb6_test_1, __id: -2", + ], + [ + "[ReactLynxDebug] BTS -> MTS updateMainThread: + { + "data": { + "patchList": [ + { + "id": 3, + "snapshotPatch": [ + { + "op": "SetAttribute", + "id": -4, + "dynamicPartIndex": 0, + "value": 1 + } + ] + } + ] + }, + "patchOptions": { + "reloadVersion": 0, + "pipelineOptions": { + "pipelineID": "pipelineID", + "needTimestamps": true, + "pipelineOrigin": "reactLynxHydrate", + "dsl": "reactLynx", + "stage": "hydrate" + } + } + }", ], [ - "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_426db_test_3, __id: -7", + "[ReactLynxDebug] FiberElement API call #34: __SetAttribute(#text#7, "text", 1)", ], [ - "[BackgroundThread Component Render] name: App, uniqID: __snapshot_426db_test_1, __id: -2", + "[ReactLynxDebug] FiberElement API call #35: __FlushElementTree(PAGE#0, {"pipelineOptions":{"pipelineID":"pipelineID","needTimestamps":true,"pipelineOrigin":"reactLynxHydrate","dsl":"reactLynx","stage":"hydrate"}})", ], ] `); @@ -353,6 +639,15 @@ describe('alog', () => { expect(lynxTestingEnv.mainThread.console.alog.mock.calls).toMatchInlineSnapshot(` [ + [ + "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_d6fb6_test_2, __id: -6", + ], + [ + "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_d6fb6_test_3, __id: -7", + ], + [ + "[BackgroundThread Component Render] name: App, uniqID: __snapshot_d6fb6_test_1, __id: -2", + ], [ "[ReactLynxDebug] BTS -> MTS updateMainThread: { @@ -394,13 +689,49 @@ describe('alog', () => { expect(lynxTestingEnv.backgroundThread.console.alog.mock.calls).toMatchInlineSnapshot(` [ [ - "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_426db_test_2, __id: -6", + "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_d6fb6_test_2, __id: -6", + ], + [ + "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_d6fb6_test_3, __id: -7", ], [ - "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_426db_test_3, __id: -7", + "[BackgroundThread Component Render] name: App, uniqID: __snapshot_d6fb6_test_1, __id: -2", + ], + [ + "[ReactLynxDebug] BTS -> MTS updateMainThread: + { + "data": { + "patchList": [ + { + "id": 4, + "snapshotPatch": [ + { + "op": "SetAttribute", + "id": -4, + "dynamicPartIndex": 0, + "value": 0 + } + ] + } + ] + }, + "patchOptions": { + "reloadVersion": 0, + "pipelineOptions": { + "pipelineID": "pipelineID", + "needTimestamps": true, + "pipelineOrigin": "reactLynxHydrate", + "dsl": "reactLynx", + "stage": "hydrate" + } + } + }", ], [ - "[BackgroundThread Component Render] name: App, uniqID: __snapshot_426db_test_1, __id: -2", + "[ReactLynxDebug] FiberElement API call #36: __SetAttribute(#text#7, "text", 0)", + ], + [ + "[ReactLynxDebug] FiberElement API call #37: __FlushElementTree(PAGE#0, {"pipelineOptions":{"pipelineID":"pipelineID","needTimestamps":true,"pipelineOrigin":"reactLynxHydrate","dsl":"reactLynx","stage":"hydrate"}})", ], ] `); @@ -414,6 +745,15 @@ describe('alog', () => { expect(lynxTestingEnv.mainThread.console.alog.mock.calls).toMatchInlineSnapshot(` [ + [ + "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_d6fb6_test_2, __id: -6", + ], + [ + "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_d6fb6_test_3, __id: -7", + ], + [ + "[BackgroundThread Component Render] name: App, uniqID: __snapshot_d6fb6_test_1, __id: -2", + ], [ "[ReactLynxDebug] BTS -> MTS updateMainThread: { @@ -455,13 +795,49 @@ describe('alog', () => { expect(lynxTestingEnv.backgroundThread.console.alog.mock.calls).toMatchInlineSnapshot(` [ [ - "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_426db_test_2, __id: -6", + "[BackgroundThread Component Render] name: ClassComponent, uniqID: __snapshot_d6fb6_test_2, __id: -6", + ], + [ + "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_d6fb6_test_3, __id: -7", ], [ - "[BackgroundThread Component Render] name: FunctionComponent, uniqID: __snapshot_426db_test_3, __id: -7", + "[BackgroundThread Component Render] name: App, uniqID: __snapshot_d6fb6_test_1, __id: -2", ], [ - "[BackgroundThread Component Render] name: App, uniqID: __snapshot_426db_test_1, __id: -2", + "[ReactLynxDebug] BTS -> MTS updateMainThread: + { + "data": { + "patchList": [ + { + "id": 5, + "snapshotPatch": [ + { + "op": "SetAttribute", + "id": -4, + "dynamicPartIndex": 0, + "value": 1 + } + ] + } + ] + }, + "patchOptions": { + "reloadVersion": 0, + "pipelineOptions": { + "pipelineID": "pipelineID", + "needTimestamps": true, + "pipelineOrigin": "reactLynxHydrate", + "dsl": "reactLynx", + "stage": "hydrate" + } + } + }", + ], + [ + "[ReactLynxDebug] FiberElement API call #38: __SetAttribute(#text#7, "text", 1)", + ], + [ + "[ReactLynxDebug] FiberElement API call #39: __FlushElementTree(PAGE#0, {"pipelineOptions":{"pipelineID":"pipelineID","needTimestamps":true,"pipelineOrigin":"reactLynxHydrate","dsl":"reactLynx","stage":"hydrate"}})", ], ] `); diff --git a/packages/react/testing-library/src/__tests__/auto-cleanup-skip.test.jsx b/packages/react/testing-library/src/__tests__/auto-cleanup-skip.test.jsx index ac369899e4..d729637359 100644 --- a/packages/react/testing-library/src/__tests__/auto-cleanup-skip.test.jsx +++ b/packages/react/testing-library/src/__tests__/auto-cleanup-skip.test.jsx @@ -1,3 +1,5 @@ +import { beforeAll, test, expect } from 'vitest'; + let render; beforeAll(async () => { diff --git a/packages/react/testing-library/src/__tests__/cleanup.test.jsx b/packages/react/testing-library/src/__tests__/cleanup.test.jsx index 00bd089395..ffbebe04ed 100644 --- a/packages/react/testing-library/src/__tests__/cleanup.test.jsx +++ b/packages/react/testing-library/src/__tests__/cleanup.test.jsx @@ -1,4 +1,4 @@ -import { expect } from 'vitest'; +import { expect, vi } from 'vitest'; import { render, cleanup } from '..'; import { Component } from '@lynx-js/react'; diff --git a/packages/react/testing-library/src/__tests__/css/index.test.jsx b/packages/react/testing-library/src/__tests__/css/index.test.jsx index e7e7c7c99b..86d10c66fe 100644 --- a/packages/react/testing-library/src/__tests__/css/index.test.jsx +++ b/packages/react/testing-library/src/__tests__/css/index.test.jsx @@ -34,8 +34,8 @@ describe('CSS', () => { `); }); it('should render a component with CSS module styles object', () => { - // Assert stable shape (prefix) rather than exact hash - expect(style3.baz).toMatch(/^_baz_[a-z0-9]+$/); + // to be a string + expect(style3.baz).toBeTypeOf('string'); const TestComponent = () => Hello World; const { container } = render(); diff --git a/packages/react/testing-library/src/__tests__/end-to-end.test.jsx b/packages/react/testing-library/src/__tests__/end-to-end.test.jsx index 1308f62876..dd263864a1 100644 --- a/packages/react/testing-library/src/__tests__/end-to-end.test.jsx +++ b/packages/react/testing-library/src/__tests__/end-to-end.test.jsx @@ -1,6 +1,6 @@ import '@testing-library/jest-dom'; import { Component } from 'preact'; -import { expect } from 'vitest'; +import { expect, vi } from 'vitest'; import { render, screen, waitForElementToBeRemoved } from '..'; import { snapshotInstanceManager } from '../../../runtime/lib/snapshot/index.js'; @@ -67,7 +67,7 @@ test('state change will cause re-render', async () => { "children": undefined, "extraProps": undefined, "id": 2, - "type": "__snapshot_354a3_test_1", + "type": "__snapshot_f46c5_test_1", "values": undefined, }, ], @@ -80,7 +80,7 @@ test('state change will cause re-render', async () => { "children": undefined, "extraProps": undefined, "id": 2, - "type": "__snapshot_354a3_test_1", + "type": "__snapshot_f46c5_test_1", "values": undefined, }, } @@ -100,7 +100,7 @@ test('state change will cause re-render', async () => { [ "rLynxChange", { - "data": "{"patchList":[{"snapshotPatch":[0,"__snapshot_354a3_test_1",2,1,-1,2,null],"id":2}]}", + "data": "{"patchList":[{"snapshotPatch":[0,"__snapshot_f46c5_test_1",2,1,-1,2,null],"id":2}]}", "patchOptions": { "isHydration": true, "pipelineOptions": { @@ -118,7 +118,7 @@ test('state change will cause re-render', async () => { [ "rLynxChange", { - "data": "{"patchList":[{"id":3,"snapshotPatch":[2,-1,2,0,"__snapshot_354a3_test_2",3,0,null,4,3,4,0,"Hello World",1,3,4,null,1,-1,3,null]}]}", + "data": "{"patchList":[{"id":3,"snapshotPatch":[2,-1,2,0,"__snapshot_f46c5_test_2",3,0,null,4,3,4,0,"Hello World",1,3,4,null,1,-1,3,null]}]}", "patchOptions": { "pipelineOptions": { "dsl": "reactLynx", diff --git a/packages/react/testing-library/src/__tests__/lazy-bundle/index.test.jsx b/packages/react/testing-library/src/__tests__/lazy-bundle/index.test.jsx index 9e23c0a39b..3e7e8159a4 100644 --- a/packages/react/testing-library/src/__tests__/lazy-bundle/index.test.jsx +++ b/packages/react/testing-library/src/__tests__/lazy-bundle/index.test.jsx @@ -22,7 +22,7 @@ function LazyComponentLoader({ url }) { return ( loading...}> - + {process.env.RSTEST ? null : } ); } @@ -55,7 +55,18 @@ describe('lazy bundle', () => { timeout: 50_000, }); - expect(container.firstChild).toMatchInlineSnapshot(` + if (process.env.RSTEST) { + expect(container.firstChild).toMatchInlineSnapshot(` + + + + Hello from LazyComponent + + + + `); + } else { + expect(container.firstChild).toMatchInlineSnapshot(` @@ -67,6 +78,7 @@ describe('lazy bundle', () => { `); + } }); }); @@ -166,12 +178,12 @@ describe('Suspense', () => { { "id": 2, "op": "CreateElement", - "type": "__snapshot_fffe1_test_3", + "type": "__snapshot_50869_test_3", }, { "id": 7, "op": "CreateElement", - "type": "__snapshot_fffe1_test_4", + "type": "__snapshot_50869_test_4", }, { "beforeId": null, @@ -193,7 +205,7 @@ describe('Suspense', () => { { "id": 2, "op": "CreateElement", - "type": "__snapshot_fffe1_test_3", + "type": "__snapshot_50869_test_3", }, { "id": 8, @@ -203,7 +215,7 @@ describe('Suspense', () => { { "id": 9, "op": "CreateElement", - "type": "__snapshot_fffe1_test_4", + "type": "__snapshot_50869_test_4", }, { "beforeId": null, @@ -294,51 +306,54 @@ describe('Suspense', () => { if (name === 'PreactSuspense') { // is torn down, (it is triggered in first render but delayed 10_000ms to execute, we use `vi.runAllTimers()` to simulate the situation that will cause the bug) // loading... is torn down - expect(tearDownInstances).toMatchInlineSnapshot(` - [ - { - "__id": 3, - "create": "function() { - const pageId = __vite_ssr_import_1__.__pageId; - const el = __CreateView(pageId); - __SetClasses(el, "lazy-wrapper"); - const el1 = __CreateWrapperElement(pageId); - __AppendElement(el, el1); - const el2 = __CreateText(pageId); - __AppendElement(el, el2); - const el3 = __CreateRawText("Hello, ReactLynx"); - __AppendElement(el2, el3); - const el4 = __CreateWrapperElement(pageId); - __AppendElement(el, el4); - return [ - el, - el1, - el2, - el3, - el4 - ]; - }", - "type": "__snapshot_fffe1_test_5", - }, - { - "__id": 7, - "create": "function() { - const pageId = __vite_ssr_import_1__.__pageId; - const el = __CreateText(pageId); - __SetClasses(el, "loading"); - const el1 = __CreateRawText("loading..."); - __AppendElement(el, el1); - return [ - el, - el1 - ]; - }", - "type": "__snapshot_fffe1_test_4", - }, - ] - `); + if (!process.env.RSTEST) { + expect(tearDownInstances).toMatchInlineSnapshot(` + [ + { + "__id": 3, + "create": "function() { + const pageId = __vite_ssr_import_1__.__pageId; + const el = __CreateView(pageId); + __SetClasses(el, "lazy-wrapper"); + const el1 = __CreateWrapperElement(pageId); + __AppendElement(el, el1); + const el2 = __CreateText(pageId); + __AppendElement(el, el2); + const el3 = __CreateRawText("Hello, ReactLynx"); + __AppendElement(el2, el3); + const el4 = __CreateWrapperElement(pageId); + __AppendElement(el, el4); + return [ + el, + el1, + el2, + el3, + el4 + ]; + }", + "type": "__snapshot_50869_test_5", + }, + { + "__id": 7, + "create": "function() { + const pageId = __vite_ssr_import_1__.__pageId; + const el = __CreateText(pageId); + __SetClasses(el, "loading"); + const el1 = __CreateRawText("loading..."); + __AppendElement(el, el1); + return [ + el, + el1 + ]; + }", + "type": "__snapshot_50869_test_4", + }, + ] + `); + } } else { - expect(tearDownInstances).toMatchInlineSnapshot(` + if (!process.env.RSTEST) { + expect(tearDownInstances).toMatchInlineSnapshot(` [ { "__id": 8, @@ -352,6 +367,7 @@ describe('Suspense', () => { }, ] `); + } } act(() => { diff --git a/packages/react/testing-library/src/__tests__/list.test.jsx b/packages/react/testing-library/src/__tests__/list.test.jsx index cb718f18ef..f8b3bfe6cf 100644 --- a/packages/react/testing-library/src/__tests__/list.test.jsx +++ b/packages/react/testing-library/src/__tests__/list.test.jsx @@ -3,7 +3,7 @@ // LICENSE file in the root directory of this source tree. import { act } from 'preact/test-utils'; -import { describe, expect } from 'vitest'; +import { describe, expect, vi } from 'vitest'; import { Component, useState } from '@lynx-js/react'; @@ -31,7 +31,7 @@ describe('list', () => { expect(container).toMatchInlineSnapshot(` `); @@ -42,7 +42,7 @@ describe('list', () => { expect(container).toMatchInlineSnapshot(` { expect(container).toMatchInlineSnapshot(` { expect(container).toMatchInlineSnapshot(` { expect(container).toMatchInlineSnapshot(` { > @@ -359,43 +359,128 @@ describe('list', () => { ] `); expect(__FlushElementTree).toHaveBeenCalledTimes(1); - expect(__FlushElementTree.mock.calls).toMatchInlineSnapshot(` - [ - [ - - - - - 1 - - - 1 - - - - - hello - - - - , - { - "elementID": 33, - "listID": 2, - "operationID": undefined, - "triggerLayout": true, - }, - ], - ] + const [[flushedElement, flushInfo]] = __FlushElementTree.mock.calls; + expect(flushedElement).toMatchInlineSnapshot(` + + + + + 1 + + + 1 + + + + + hello + + + + + `); + expect(flushInfo).toMatchObject({ + listID: 2, + operationID: undefined, + triggerLayout: true, + }); + expect(flushInfo.elementID).toBeTypeOf('number'); + + expect(list).toMatchInlineSnapshot(` + + + + + + 4 + + + 4 + + + + + hello + + + + + + + + + 5 + + + 5 + + + + + hello + + + + + + + + + 2 + + + 2 + + + + + hello + + + + + + + + + 1 + + + 1 + + + + + hello + + + + + `); expect(list).toMatchInlineSnapshot(` should render as normal', () => { `); @@ -585,7 +670,7 @@ describe('list - deferred should render as normal', () => { `); @@ -597,7 +682,7 @@ describe('list - deferred should render as normal', () => { should render as normal', () => { `); @@ -766,7 +851,7 @@ describe('list - deferred should render as normal', () => { `); @@ -786,7 +871,7 @@ describe('list - deferred should render as normal', () => { `); @@ -805,7 +890,7 @@ describe('list - deferred should render as normal', () => { should render as normal', () => { "rLynxFirstScreen", { "jsReadyEventIdSwap": {}, - "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__snapshot_a9e46_test_30","children":[{"id":-3,"type":"__snapshot_a9e46_test_31","values":[{"item-key":0}],"children":[{"id":-4,"type":"__snapshot_a9e46_test_29","values":[{"style":{"backgroundColor":"red","margin":"12px"}}],"children":[{"id":-5,"type":"__snapshot_a9e46_test_32","children":[{"id":-6,"type":null,"values":[0]}]}]}]},{"id":-7,"type":"__snapshot_a9e46_test_31","values":[{"item-key":1}],"children":[{"id":-8,"type":"__snapshot_a9e46_test_29","values":[{"style":{"backgroundColor":"red","margin":"12px"}}],"children":[{"id":-9,"type":"__snapshot_a9e46_test_32","children":[{"id":-10,"type":null,"values":[1]}]}]}]},{"id":-11,"type":"__snapshot_a9e46_test_31","values":[{"item-key":2}],"children":[{"id":-12,"type":"__snapshot_a9e46_test_29","values":[{"style":{"backgroundColor":"red","margin":"12px"}}],"children":[{"id":-13,"type":"__snapshot_a9e46_test_32","children":[{"id":-14,"type":null,"values":[2]}]}]}]}]}]}", + "root": "{"id":-1,"type":"root","children":[{"id":-2,"type":"__snapshot_d0c07_test_30","children":[{"id":-3,"type":"__snapshot_d0c07_test_31","values":[{"item-key":0}],"children":[{"id":-4,"type":"__snapshot_d0c07_test_29","values":[{"style":{"backgroundColor":"red","margin":"12px"}}],"children":[{"id":-5,"type":"__snapshot_d0c07_test_32","children":[{"id":-6,"type":null,"values":[0]}]}]}]},{"id":-7,"type":"__snapshot_d0c07_test_31","values":[{"item-key":1}],"children":[{"id":-8,"type":"__snapshot_d0c07_test_29","values":[{"style":{"backgroundColor":"red","margin":"12px"}}],"children":[{"id":-9,"type":"__snapshot_d0c07_test_32","children":[{"id":-10,"type":null,"values":[1]}]}]}]},{"id":-11,"type":"__snapshot_d0c07_test_31","values":[{"item-key":2}],"children":[{"id":-12,"type":"__snapshot_d0c07_test_29","values":[{"style":{"backgroundColor":"red","margin":"12px"}}],"children":[{"id":-13,"type":"__snapshot_d0c07_test_32","children":[{"id":-14,"type":null,"values":[2]}]}]}]}]}]}", }, ], ], diff --git a/packages/react/testing-library/src/__tests__/lynx.test.jsx b/packages/react/testing-library/src/__tests__/lynx.test.jsx index 6151c8f730..392a35bb88 100644 --- a/packages/react/testing-library/src/__tests__/lynx.test.jsx +++ b/packages/react/testing-library/src/__tests__/lynx.test.jsx @@ -62,7 +62,7 @@ describe('lynx global API', () => { { "id": 2, "op": "CreateElement", - "type": "__snapshot_d4b6f_test_1", + "type": "__snapshot_034cb_test_1", }, { "beforeId": null, diff --git a/packages/react/testing-library/src/__tests__/rerender.test.jsx b/packages/react/testing-library/src/__tests__/rerender.test.jsx index 559e632fc6..df6a9a9bde 100644 --- a/packages/react/testing-library/src/__tests__/rerender.test.jsx +++ b/packages/react/testing-library/src/__tests__/rerender.test.jsx @@ -1,6 +1,6 @@ import '@testing-library/jest-dom'; import { render } from '..'; -import { expect } from 'vitest'; +import { expect, vi } from 'vitest'; import { useEffect, useState } from '@lynx-js/react'; test('rerender will re-render the element', async () => { diff --git a/packages/react/testing-library/src/__tests__/worklet.test.jsx b/packages/react/testing-library/src/__tests__/worklet.test.jsx index d1d50ac624..4caf0b61c0 100644 --- a/packages/react/testing-library/src/__tests__/worklet.test.jsx +++ b/packages/react/testing-library/src/__tests__/worklet.test.jsx @@ -77,7 +77,7 @@ describe('worklet', () => { [ "rLynxChange", { - "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_wkltId":"a45f:test:2","_workletType":"main-thread","_execId":1}],"id":2}]}", + "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_wkltId":"15ab:test:2","_workletType":"main-thread","_execId":1}],"id":2}]}", "patchOptions": { "isHydration": true, "pipelineOptions": { @@ -159,7 +159,7 @@ describe('worklet', () => { [ "rLynxChange", { - "data": "{"patchList":[{"snapshotPatch":[3,-2,1,{"_c":{"props":{"main-thread:onClick":{"_wkltId":"a45f:test:3"}}},"_wkltId":"a45f:test:4","_execId":1}],"id":2}]}", + "data": "{"patchList":[{"snapshotPatch":[3,-2,1,{"_c":{"props":{"main-thread:onClick":{"_wkltId":"15ab:test:3"}}},"_wkltId":"15ab:test:4","_execId":1}],"id":2}]}", "patchOptions": { "isHydration": true, "pipelineOptions": { @@ -249,7 +249,7 @@ describe('worklet', () => { [ "rLynxChange", { - "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_c":{"props":{"main-thread:onScroll":{"_wkltId":"a45f:test:5"}}},"_wkltId":"a45f:test:6","_execId":1}],"id":2}]}", + "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_c":{"props":{"main-thread:onScroll":{"_wkltId":"15ab:test:5"}}},"_wkltId":"15ab:test:6","_execId":1}],"id":2}]}", "patchOptions": { "isHydration": true, "pipelineOptions": { @@ -343,7 +343,7 @@ describe('worklet', () => { [ "rLynxChange", { - "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_wkltId":"a45f:test:8","_jsFn":{"_jsFn1":{"_jsFnId":2,"_fn":"[BackgroundFunction]"}},"_execId":1}],"id":2}]}", + "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_wkltId":"15ab:test:8","_jsFn":{"_jsFn1":{"_jsFnId":2,"_fn":"[BackgroundFunction]"}},"_execId":1}],"id":2}]}", "patchOptions": { "isHydration": true, "pipelineOptions": { @@ -447,7 +447,7 @@ describe('worklet', () => { [ "rLynxChange", { - "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_wvid":1},3,-2,1,{"_c":{"ref":{"_wvid":1},"num":{"_wvid":2}},"_wkltId":"a45f:test:9","_execId":1}],"id":2}]}", + "data": "{"patchList":[{"snapshotPatch":[3,-2,0,{"_wvid":1},3,-2,1,{"_c":{"ref":{"_wvid":1},"num":{"_wvid":2}},"_wkltId":"15ab:test:9","_execId":1}],"id":2}]}", "patchOptions": { "isHydration": true, "pipelineOptions": { diff --git a/packages/react/testing-library/src/env/index.ts b/packages/react/testing-library/src/env/index.ts new file mode 100644 index 0000000000..cd62eb3d54 --- /dev/null +++ b/packages/react/testing-library/src/env/index.ts @@ -0,0 +1,2 @@ +export { LynxTestingEnv, installLynxTestingEnv, uninstallLynxTestingEnv } from '@lynx-js/testing-environment'; +export type { LynxEnv } from '@lynx-js/testing-environment'; diff --git a/packages/react/testing-library/src/env/rstest.ts b/packages/react/testing-library/src/env/rstest.ts new file mode 100644 index 0000000000..de16c1a402 --- /dev/null +++ b/packages/react/testing-library/src/env/rstest.ts @@ -0,0 +1,7 @@ +import { LynxTestingEnv } from './index.js'; + +global.lynxEnv = { + window, +}; +const lynxTestingEnv = new LynxTestingEnv(); +global.lynxTestingEnv = lynxTestingEnv; diff --git a/packages/react/testing-library/src/env/vitest.ts b/packages/react/testing-library/src/env/vitest.ts index cf89eb9fdb..c6ff6608f0 100644 --- a/packages/react/testing-library/src/env/vitest.ts +++ b/packages/react/testing-library/src/env/vitest.ts @@ -1,3 +1,27 @@ -import env from '@lynx-js/testing-environment/env/vitest'; +import { builtinEnvironments, type Environment } from 'vitest/environments'; +import { installLynxTestingEnv, uninstallLynxTestingEnv } from './index.js'; + +const env: Environment = { + name: 'lynxTestingEnv', + transformMode: 'web', + async setup(global) { + const fakeGlobal: { + jsdom?: any; + } = {}; + const jsdomEnvironment = await builtinEnvironments.jsdom.setup( + fakeGlobal, + {}, + ); + + installLynxTestingEnv(global, fakeGlobal.jsdom); + + return { + async teardown(global) { + await jsdomEnvironment.teardown(fakeGlobal); + uninstallLynxTestingEnv(global); + }, + }; + }, +}; export default env; diff --git a/packages/react/testing-library/src/plugins/index.ts b/packages/react/testing-library/src/plugins/index.ts new file mode 100644 index 0000000000..2b150f8d92 --- /dev/null +++ b/packages/react/testing-library/src/plugins/index.ts @@ -0,0 +1,2 @@ +export { testingLibraryPlugin as vitestTestingLibraryPlugin } from './vitest.js'; +export type { TestingLibraryOptions } from './vitest.js'; diff --git a/packages/react/testing-library/src/vitest.config.js b/packages/react/testing-library/src/plugins/vitest.ts similarity index 60% rename from packages/react/testing-library/src/vitest.config.js rename to packages/react/testing-library/src/plugins/vitest.ts index 7c37d9dda5..bf4ae89204 100644 --- a/packages/react/testing-library/src/vitest.config.js +++ b/packages/react/testing-library/src/plugins/vitest.ts @@ -1,62 +1,69 @@ -import { defineConfig } from 'vitest/config'; +// Copyright 2025 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +import type { ResolvedConfig, Vite } from 'vitest/node'; import { VitestPackageInstaller } from 'vitest/node'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { createRequire } from 'module'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const require = createRequire(import.meta.url); -async function ensurePackagesInstalled() { - const installer = new VitestPackageInstaller(); - const installed = await installer.ensureInstalled( - 'jsdom', - process.cwd(), - ); - if (!installed) { - console.log('ReactLynx Testing Library requires jsdom to be installed.'); - process.exit(1); - } -} +export interface TestingLibraryOptions { + /** + * The package name of the ReactLynx runtime package. + * + * @default `@lynx-js/react` + */ + runtimePkgName?: string; + + /** + * The engine version to use for the transform. + * + * @default `''` + */ + engineVersion?: string; -/** - * @returns {import('vitest/config').ViteUserConfig} - */ -export const createVitestConfig = async (options) => { - await ensurePackagesInstalled(); + /** + * Enable experimental React Compiler support. + * + * Requires `@babel/core`, `babel-plugin-react-compiler`, + * `@babel/plugin-syntax-jsx`, and `@babel/plugin-syntax-typescript` + * to be installed in your project. + * + * @default `false` + */ + experimental_enableReactCompiler?: boolean; +} +export function testingLibraryPlugin( + options?: TestingLibraryOptions, +): Vite.PluginOption { const runtimeOSSPkgName = '@lynx-js/react'; const runtimePkgName = options?.runtimePkgName ?? runtimeOSSPkgName; - const runtimeDir = path.dirname(require.resolve(`${runtimePkgName}/package.json`)); + const runtimeDir = path.dirname( + require.resolve(`${runtimePkgName}/package.json`), + ); const runtimeOSSDir = path.dirname( require.resolve(`${runtimeOSSPkgName}/package.json`, { paths: [runtimeDir, __dirname], }), ); + const preactDir = path.dirname( require.resolve('preact/package.json', { paths: [runtimeOSSDir], }), ); - const generateAlias = (pkgName, pkgDir, resolveDir) => { - const pkgExports = require(path.join(pkgDir, 'package.json')).exports; - const pkgAlias = []; - Object.keys(pkgExports).forEach((key) => { - const name = path.posix.join(pkgName, key); - pkgAlias.push({ - find: new RegExp('^' + name + '$'), - replacement: require.resolve(name, { - paths: [resolveDir, __dirname], - }), - }); - }); - return pkgAlias; - }; - - const runtimeOSSAlias = generateAlias(runtimeOSSPkgName, runtimeOSSDir, runtimeDir); - let runtimeAlias = []; + const runtimeOSSAlias = generateAlias( + runtimeOSSPkgName, + runtimeOSSDir, + runtimeDir, + ); + let runtimeAlias: Vite.Alias[] = []; if (runtimePkgName !== runtimeOSSPkgName) { runtimeAlias = generateAlias(runtimePkgName, runtimeDir, __dirname); } @@ -85,11 +92,11 @@ export const createVitestConfig = async (options) => { }, ]; - function transformReactCompilerPlugin() { - let rootContext, compilerDeps, babel; + function transformReactCompilerPlugin(): Vite.Plugin { + let rootContext: string, compilerDeps: ReturnType, babel: any; - function resolveCompilerDeps(rootContext) { - const missingBabelPackages = []; + function resolveCompilerDeps(rootContext: string) { + const missingBabelPackages: string[] = []; const [ babelPath, babelPluginReactCompilerPath, @@ -132,9 +139,9 @@ export const createVitestConfig = async (options) => { name: 'transformReactCompilerPlugin', enforce: 'pre', config(config) { - rootContext = config.root; + rootContext = config.root!; - const reactCompilerRuntimeAlias = []; + const reactCompilerRuntimeAlias: Vite.Alias[] = []; try { reactCompilerRuntimeAlias.push( { @@ -150,14 +157,30 @@ export const createVitestConfig = async (options) => { }, ); } catch (e) { - // console.log('react-compiler-runtime not found, skip alias'); + // react-compiler-runtime not found, skip alias } - config.test.alias.push(...reactCompilerRuntimeAlias); + let mergedAlias: Vite.Alias[] = [...reactCompilerRuntimeAlias]; + if (config.test?.alias) { + if (Array.isArray(config.test.alias)) { + mergedAlias = [...config.test.alias, ...mergedAlias]; + } else { + mergedAlias = [ + ...Object.entries(config.test.alias).map(([key, value]) => ({ + find: key, + replacement: value, + })), + ...mergedAlias, + ]; + } + } + + config.test = config.test || {}; + config.test.alias = mergedAlias; compilerDeps = resolveCompilerDeps(rootContext); const { babelPath } = compilerDeps; - babel = require(babelPath); + babel = require(babelPath!); }, async transform(sourceText, sourcePath) { if (/\.(?:jsx|tsx)$/.test(sourcePath)) { @@ -192,7 +215,7 @@ export const createVitestConfig = async (options) => { ); } } catch (e) { - this.error(e); + this.error(e as Error); } } @@ -201,10 +224,15 @@ export const createVitestConfig = async (options) => { }; } - function transformReactLynxPlugin() { + let config: ResolvedConfig; + + function transformReactLynxPlugin(): Vite.Plugin { return { name: 'transformReactLynxPlugin', enforce: 'pre', + async buildStart() { + await ensurePackagesInstalled(); + }, transform(sourceText, sourcePath) { const id = sourcePath; // Only transform JS files @@ -214,12 +242,11 @@ export const createVitestConfig = async (options) => { const { transformReactLynxSync } = require( '@lynx-js/react/transform', - ); + ) as typeof import('@lynx-js/react/transform'); // relativePath should be stable between different runs with different cwd - const relativePath = normalizeSlashes(path.relative( - __dirname, - sourcePath, - )); + const relativePath = normalizeSlashes( + path.relative(config.root, sourcePath), + ); const basename = path.basename(sourcePath); const result = transformReactLynxSync(sourceText, { mode: 'test', @@ -252,60 +279,88 @@ export const createVitestConfig = async (options) => { refresh: false, cssScope: false, }); + if (result.errors.length > 0) { // https://rollupjs.org/plugin-development/#this-error - result.errors.forEach(error => { - this.error( - error.text, - error.location, - ); + result.errors.forEach((error) => { + this.error(error.text ?? 'Unknown error', { + line: 1, + column: 1, + ...error.location, + }); }); } if (result.warnings.length > 0) { - result.warnings.forEach(warning => { - this.warn( - warning.text, - warning.location, - ); + result.warnings.forEach((warning) => { + this.warn(warning.text ?? 'Unknown warning', { + line: 1, + column: 1, + ...warning.location, + }); }); } return { code: result.code, - map: result.map, + map: result.map!, }; }, + config: () => ({ + test: { + environment: require.resolve( + `${runtimeOSSDir}/testing-library/dist/env/vitest`, + ), + globals: true, + setupFiles: [ + require.resolve('../setupFiles/vitest'), + ], + alias: [...runtimeOSSAlias, ...runtimeAlias, ...preactAlias, ...reactAlias], + }, + }), + configResolved(_config) { + // @ts-ignore + config = _config; + }, }; } - return defineConfig({ - server: { - fs: { - allow: [ - path.join(__dirname, '..'), - ], - }, - }, - plugins: [ - ...(options?.experimental_enableReactCompiler - ? [ - transformReactCompilerPlugin(), - ] - : []), - transformReactLynxPlugin(), - ], - test: { - environment: require.resolve( - './env/vitest', - ), - globals: true, - setupFiles: [path.join(__dirname, 'vitest-global-setup')], - alias: [...runtimeOSSAlias, ...runtimeAlias, ...preactAlias, ...reactAlias], - include: options?.include ?? ['src/**/*.test.{js,jsx,ts,tsx}'], - }, + return [ + ...(options?.experimental_enableReactCompiler + ? [transformReactCompilerPlugin()] + : []), + transformReactLynxPlugin(), + ]; +} + +async function ensurePackagesInstalled() { + const installer = new VitestPackageInstaller(); + const installed = await installer.ensureInstalled('jsdom', process.cwd()); + if (!installed) { + console.log('ReactLynx Testing Library requires jsdom to be installed.'); + process.exit(1); + } +} + +function generateAlias(pkgName: string, pkgDir: string, resolveDir: string) { + const pkgExports = require(path.join(pkgDir, 'package.json')).exports; + if (!pkgExports || typeof pkgExports !== 'object') { + return []; + } + const pkgAlias: Vite.Alias[] = []; + Object.keys(pkgExports).forEach((key) => { + const name = path.posix.join(pkgName, key); + // Escape special regex characters in the package name + const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + pkgAlias.push({ + find: new RegExp('^' + escapedName + '$'), + replacement: require.resolve(name, { + paths: [resolveDir, __dirname], + }), + }); }); -}; + return pkgAlias; +} -function normalizeSlashes(file) { +function normalizeSlashes(file: string) { return file.replaceAll(path.win32.sep, '/'); } diff --git a/packages/react/testing-library/src/pure.jsx b/packages/react/testing-library/src/pure.jsx index 4eaa962281..5eef77f076 100644 --- a/packages/react/testing-library/src/pure.jsx +++ b/packages/react/testing-library/src/pure.jsx @@ -91,7 +91,7 @@ export function render( }); }, asFragment: () => { - const { document } = lynxTestingEnv.jsdom.window; + const { document } = lynxTestingEnv.env.window; const container = lynxTestingEnv.mainThread.elementTree.root; if (typeof document.createRange === 'function') { return document @@ -118,7 +118,7 @@ export function cleanup() { lynxTestingEnv.mainThread.elementTree.root = undefined; clearPage(); - lynxTestingEnv.jsdom.window.document.body.innerHTML = ''; + lynxTestingEnv.env.window.document.body.innerHTML = ''; if (isMainThread) { globalThis.lynxTestingEnv.switchToMainThread(); diff --git a/packages/react/testing-library/src/rstest-config.ts b/packages/react/testing-library/src/rstest-config.ts new file mode 100644 index 0000000000..dc7a053458 --- /dev/null +++ b/packages/react/testing-library/src/rstest-config.ts @@ -0,0 +1,113 @@ +import type { ExtendConfig, ExtendConfigFn } from '@rstest/core'; +import { createRequire } from 'node:module'; +import type { RsbuildConfig } from '@rsbuild/core'; + +export interface LynxConfigOptions { + /** + * The root path of the project. + * + * @default `process.cwd()` + */ + rootPath?: string; + + /** + * The path to the Lynx config file. + * + * @default `lynx.config.ts` + */ + configPath?: string; +} + +export interface RstestConfigOptions { + /** + * Customize the generated rstest config. + */ + modifyRstestConfig?: (config: ExtendConfig) => ExtendConfig | Promise; +} + +export interface LynxRstestConfigOptions extends LynxConfigOptions, RstestConfigOptions {} + +const require = createRequire(import.meta.url); + +function createDefaultRstestConfig(): ExtendConfig { + return { + testEnvironment: 'jsdom', + setupFiles: [require.resolve('./setupFiles/rstest')], + globals: true, + }; +} + +function normalizeSetupFiles( + setupFiles: ExtendConfig['setupFiles'], +): string[] { + if (!setupFiles) { + return []; + } + + return Array.isArray(setupFiles) ? setupFiles : [setupFiles]; +} + +async function applyRstestConfigModifier( + config: ExtendConfig, + modifyRstestConfig?: (config: ExtendConfig) => ExtendConfig | Promise, +): Promise { + if (!modifyRstestConfig) { + return config; + } + + return await modifyRstestConfig(config); +} + +export function withDefaultConfig( + options?: RstestConfigOptions, +): ExtendConfigFn { + return async () => { + return await applyRstestConfigModifier( + createDefaultRstestConfig(), + options?.modifyRstestConfig, + ); + }; +} + +export function withLynxConfig( + options?: LynxRstestConfigOptions, +): ExtendConfigFn { + return async () => { + const { loadConfig } = await import('@lynx-js/rspeedy'); + const lynxConfig = await loadConfig({ + cwd: options?.rootPath, + configPath: options?.configPath, + }); + + const { toRstestConfig } = await import('@rstest/adapter-rsbuild'); + const rstestConfig = toRstestConfig({ + rsbuildConfig: lynxConfig.content as RsbuildConfig, + }); + const defaultConfig = createDefaultRstestConfig(); + const setupFiles = Array.from( + new Set([ + ...normalizeSetupFiles(rstestConfig.setupFiles), + ...normalizeSetupFiles(defaultConfig.setupFiles), + ]), + ); + + const mergedConfig: ExtendConfig = { + ...rstestConfig, + ...defaultConfig, + plugins: [ + ...(rstestConfig.plugins || []), + { + name: 'lynx-adapter:remove-useless-plugins', + remove: ['lynx:rsbuild:qrcode'], + setup: () => {}, + }, + ], + setupFiles, + }; + + return await applyRstestConfigModifier( + mergedConfig, + options?.modifyRstestConfig, + ); + }; +} diff --git a/packages/react/testing-library/src/setupFiles/common/bootstrap.js b/packages/react/testing-library/src/setupFiles/common/bootstrap.js new file mode 100644 index 0000000000..f85071e11f --- /dev/null +++ b/packages/react/testing-library/src/setupFiles/common/bootstrap.js @@ -0,0 +1,6 @@ +globalThis.onInjectMainThreadGlobals( + globalThis.lynxTestingEnv.mainThread.globalThis, +); +globalThis.onInjectBackgroundThreadGlobals( + globalThis.lynxTestingEnv.backgroundThread.globalThis, +); diff --git a/packages/react/testing-library/src/vitest-global-setup.js b/packages/react/testing-library/src/setupFiles/common/runtime-setup.js similarity index 63% rename from packages/react/testing-library/src/vitest-global-setup.js rename to packages/react/testing-library/src/setupFiles/common/runtime-setup.js index 7abf35e436..778435c1de 100644 --- a/packages/react/testing-library/src/vitest-global-setup.js +++ b/packages/react/testing-library/src/setupFiles/common/runtime-setup.js @@ -1,50 +1,24 @@ import { options } from 'preact'; -import { expect } from 'vitest'; - -import { clearCommitTaskId, replaceCommitHook } from '../../runtime/lib/lifecycle/patch/commit.js'; -import { deinitGlobalSnapshotPatch } from '../../runtime/lib/lifecycle/patch/snapshotPatch.js'; -import { injectUpdateMainThread } from '../../runtime/lib/lifecycle/patch/updateMainThread.js'; -import { injectUpdateMTRefInitValue } from '../../runtime/lib/worklet/ref/updateInitValue.js'; -import { injectCalledByNative } from '../../runtime/lib/lynx/calledByNative.js'; -import { flushDelayedLifecycleEvents, injectTt } from '../../runtime/lib/lynx/tt.js'; -import { initElementPAPICallAlog } from '../../runtime/lib/alog/elementPAPICall.js'; -import { addCtxNotFoundEventListener } from '../../runtime/lib/lifecycle/patch/error.js'; -import { setRoot } from '../../runtime/lib/root.js'; + +import { clearCommitTaskId, replaceCommitHook } from '../../../../runtime/lib/lifecycle/patch/commit.js'; +import { deinitGlobalSnapshotPatch } from '../../../../runtime/lib/lifecycle/patch/snapshotPatch.js'; +import { injectUpdateMainThread } from '../../../../runtime/lib/lifecycle/patch/updateMainThread.js'; +import { injectUpdateMTRefInitValue } from '../../../../runtime/lib/worklet/ref/updateInitValue.js'; +import { injectCalledByNative } from '../../../../runtime/lib/lynx/calledByNative.js'; +import { flushDelayedLifecycleEvents, injectTt } from '../../../../runtime/lib/lynx/tt.js'; +import { initElementPAPICallAlog } from '../../../../runtime/lib/alog/elementPAPICall.js'; +import { addCtxNotFoundEventListener } from '../../../../runtime/lib/lifecycle/patch/error.js'; +import { setRoot } from '../../../../runtime/lib/root.js'; import { SnapshotInstance, BackgroundSnapshotInstance, backgroundSnapshotInstanceManager, snapshotInstanceManager, -} from '../../runtime/lib/snapshot/index.js'; -import { destroyWorklet } from '../../runtime/lib/worklet/destroy.js'; -import { initApiEnv } from '../../runtime/lib/worklet-runtime/api/lynxApi.js'; -import { initEventListeners } from '../../runtime/lib/worklet-runtime/listeners.js'; -import { initWorklet } from '../../runtime/lib/worklet-runtime/workletRuntime.js'; - -expect.addSnapshotSerializer({ - test(val) { - return Boolean( - val - && typeof val === 'object' - && Array.isArray(val.refAttr) - && Object.prototype.hasOwnProperty.call(val, 'task') - && typeof val.exec === 'function', - ); - }, - print(val, serialize) { - const printed = serialize({ - refAttr: Array.isArray(val.refAttr) ? [...val.refAttr] : val.refAttr, - task: val.task, - }); - if (printed.startsWith('Object')) { - return printed.replace(/^Object/, 'RefProxy'); - } - if (printed.startsWith('{')) { - return `RefProxy ${printed}`; - } - return printed; - }, -}); +} from '../../../../runtime/lib/snapshot/index.js'; +import { destroyWorklet } from '../../../../runtime/lib/worklet/destroy.js'; +import { initApiEnv } from '../../../../runtime/lib/worklet-runtime/api/lynxApi.js'; +import { initEventListeners } from '../../../../runtime/lib/worklet-runtime/listeners.js'; +import { initWorklet } from '../../../../runtime/lib/worklet-runtime/workletRuntime.js'; const { onInjectMainThreadGlobals, @@ -110,8 +84,12 @@ globalThis.onInjectMainThreadGlobals = (target) => { target.globalPipelineOptions = undefined; - if (typeof __ALOG_ELEMENT_API__ !== 'undefined' && __ALOG_ELEMENT_API__) { + if ( + typeof target.__ALOG_ELEMENT_API__ !== 'undefined' && target.__ALOG_ELEMENT_API__ + && !target.__initElementPAPICallAlogInjected + ) { initElementPAPICallAlog(target); + target.__initElementPAPICallAlogInjected = true; } }; globalThis.onInjectBackgroundThreadGlobals = (target) => { @@ -146,13 +124,16 @@ globalThis.onInjectBackgroundThreadGlobals = (target) => { target._document = setupBackgroundDocument({}); target.globalPipelineOptions = undefined; - target.lynx.requireModuleAsync = async (url, callback) => { - try { - callback(null, await __vite_ssr_dynamic_import__(url)); - } catch (err) { - callback(err, null); - } - }; + // TODO: can we only inject to target(mainThread.globalThis) instead of globalThis? + // packages/react/runtime/src/lynx.ts + // intercept lynxCoreInject assignments to lynxTestingEnv.backgroundThread.globalThis.lynxCoreInject + const oldLynxCoreInject = globalThis.lynxCoreInject; + globalThis.lynxCoreInject = target.lynxCoreInject; + try { + injectTt(); + } finally { + globalThis.lynxCoreInject = oldLynxCoreInject; + } // re-init global snapshot patch to undefined deinitGlobalSnapshotPatch(); diff --git a/packages/react/testing-library/src/setupFiles/inner/rstest.js b/packages/react/testing-library/src/setupFiles/inner/rstest.js new file mode 100644 index 0000000000..26c510b596 --- /dev/null +++ b/packages/react/testing-library/src/setupFiles/inner/rstest.js @@ -0,0 +1,13 @@ +const { + onInjectBackgroundThreadGlobals, +} = globalThis; + +globalThis.onInjectBackgroundThreadGlobals = (target) => { + if (onInjectBackgroundThreadGlobals) { + onInjectBackgroundThreadGlobals(target); + } + + target.lynx.requireModuleAsync = async (url, _callback) => { + throw new Error('lynx.requireModuleAsync not implemented for rstest'); + }; +}; diff --git a/packages/react/testing-library/src/setupFiles/inner/vitest.js b/packages/react/testing-library/src/setupFiles/inner/vitest.js new file mode 100644 index 0000000000..b8421fa88f --- /dev/null +++ b/packages/react/testing-library/src/setupFiles/inner/vitest.js @@ -0,0 +1,17 @@ +const { + onInjectBackgroundThreadGlobals, +} = globalThis; + +globalThis.onInjectBackgroundThreadGlobals = (target) => { + if (onInjectBackgroundThreadGlobals) { + onInjectBackgroundThreadGlobals(target); + } + + target.lynx.requireModuleAsync = async (url, callback) => { + try { + callback(null, await __vite_ssr_dynamic_import__(url)); + } catch (err) { + callback(err, null); + } + }; +}; diff --git a/packages/react/testing-library/src/setupFiles/rstest.js b/packages/react/testing-library/src/setupFiles/rstest.js new file mode 100644 index 0000000000..ef0811e05f --- /dev/null +++ b/packages/react/testing-library/src/setupFiles/rstest.js @@ -0,0 +1,30 @@ +import '../env/rstest.js'; +import './common/runtime-setup.js'; +import './inner/rstest.js'; +import './common/bootstrap.js'; +import { expect } from '@rstest/core'; + +expect.addSnapshotSerializer({ + test(val) { + return Boolean( + val + && typeof val === 'object' + && Array.isArray(val.refAttr) + && Object.prototype.hasOwnProperty.call(val, 'task') + && typeof val.exec === 'function', + ); + }, + print(val, serialize) { + const printed = serialize({ + refAttr: Array.isArray(val.refAttr) ? [...val.refAttr] : val.refAttr, + task: val.task, + }); + if (printed.startsWith('Object')) { + return printed.replace(/^Object/, 'RefProxy'); + } + if (printed.startsWith('{')) { + return `RefProxy ${printed}`; + } + return printed; + }, +}); diff --git a/packages/react/testing-library/src/setupFiles/vitest.js b/packages/react/testing-library/src/setupFiles/vitest.js new file mode 100644 index 0000000000..71f8a24da1 --- /dev/null +++ b/packages/react/testing-library/src/setupFiles/vitest.js @@ -0,0 +1,29 @@ +import './common/runtime-setup.js'; +import './inner/vitest.js'; +import './common/bootstrap.js'; +import { expect } from 'vitest'; + +expect.addSnapshotSerializer({ + test(val) { + return Boolean( + val + && typeof val === 'object' + && Array.isArray(val.refAttr) + && Object.prototype.hasOwnProperty.call(val, 'task') + && typeof val.exec === 'function', + ); + }, + print(val, serialize) { + const printed = serialize({ + refAttr: Array.isArray(val.refAttr) ? [...val.refAttr] : val.refAttr, + task: val.task, + }); + if (printed.startsWith('Object')) { + return printed.replace(/^Object/, 'RefProxy'); + } + if (printed.startsWith('{')) { + return `RefProxy ${printed}`; + } + return printed; + }, +}); diff --git a/packages/react/testing-library/src/vitest.config.ts b/packages/react/testing-library/src/vitest.config.ts new file mode 100644 index 0000000000..56c9e84ba3 --- /dev/null +++ b/packages/react/testing-library/src/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig, type ViteUserConfig } from 'vitest/config'; +import { vitestTestingLibraryPlugin } from './plugins/index.js'; +import type { TestingLibraryOptions } from './plugins/index.js'; + +/** + * @deprecated Use `vitestTestingLibraryPlugin` from `@lynx-js/react/testing-library/plugins` instead. + */ +export function createVitestConfig(options?: TestingLibraryOptions): ViteUserConfig { + return defineConfig({ + plugins: [ + vitestTestingLibraryPlugin(options), + ], + }); +} diff --git a/packages/react/testing-library/tsconfig.json b/packages/react/testing-library/tsconfig.json index aac4b8ccfb..06f9114c72 100644 --- a/packages/react/testing-library/tsconfig.json +++ b/packages/react/testing-library/tsconfig.json @@ -6,7 +6,7 @@ "rootDir": "src", "stripInternal": true, "target": "ESNext", - "lib": ["es2021"], + "lib": ["es2021", "dom"], "module": "Node16", "moduleResolution": "Node16", "resolveJsonModule": true, diff --git a/packages/react/testing-library/types/vitest-config.d.ts b/packages/react/testing-library/types/vitest-config.d.ts deleted file mode 100644 index 3b20fe6013..0000000000 --- a/packages/react/testing-library/types/vitest-config.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ViteUserConfig } from 'vitest/config.js'; - -export interface CreateVitestConfigOptions { - /** - * The package name of the ReactLynx runtime package. - * - * @defaultValue `@lynx-js/react` - */ - runtimePkgName?: string; - /** - * Enable React Compiler for this build. - * - * @link https://react.dev/learn/react-compiler - * - * @defaultValue false - */ - experimental_enableReactCompiler?: boolean; -} - -export function createVitestConfig(options?: CreateVitestConfigOptions): Promise; diff --git a/packages/react/testing-library/vitest-polyfill.cjs b/packages/react/testing-library/vitest-polyfill.cjs new file mode 100644 index 0000000000..647f2e4a8d --- /dev/null +++ b/packages/react/testing-library/vitest-polyfill.cjs @@ -0,0 +1,6 @@ +// in order to make our test case work for +// both vitest and rstest, we need to alias +// `vitest` to `@rstest/core` + +global['@rstest/core'].vi = global['@rstest/core'].rs; +module.exports = global['@rstest/core']; diff --git a/packages/react/testing-library/vitest.3.1.config.ts b/packages/react/testing-library/vitest.3.1.config.ts index 56d18337e2..43706ec69d 100644 --- a/packages/react/testing-library/vitest.3.1.config.ts +++ b/packages/react/testing-library/vitest.3.1.config.ts @@ -1,17 +1,17 @@ -import { defineConfig, mergeConfig } from 'vitest/config'; -import { createVitestConfig } from './dist/vitest.config'; +import { defineConfig } from 'vitest/config'; +import { vitestTestingLibraryPlugin } from './dist/plugins/index.js'; -const defaultConfig = await createVitestConfig({ - runtimePkgName: '@lynx-js/react', - engineVersion: '3.1', - include: [ - 'src/__tests__/3.1/**/*.{js,jsx,ts,tsx}', +export default defineConfig({ + plugins: [ + vitestTestingLibraryPlugin({ + runtimePkgName: '@lynx-js/react', + engineVersion: '3.1', + }), ], -}); -const config = defineConfig({ test: { name: 'react/testing-library/engine-3.1', + include: [ + 'src/__tests__/3.1/**/*.{js,jsx,ts,tsx}', + ], }, }); - -export default mergeConfig(defaultConfig, config); diff --git a/packages/react/testing-library/vitest.config.ts b/packages/react/testing-library/vitest.config.ts index 65fcb43cdb..e2c0b2de2f 100644 --- a/packages/react/testing-library/vitest.config.ts +++ b/packages/react/testing-library/vitest.config.ts @@ -1,14 +1,14 @@ -import { defineConfig, mergeConfig } from 'vitest/config'; -import { createVitestConfig } from './dist/vitest.config'; +import { defineConfig } from 'vitest/config'; +import { vitestTestingLibraryPlugin } from './dist/plugins/index.js'; -const defaultConfig = await createVitestConfig({ - runtimePkgName: '@lynx-js/react', - include: ['src/**/*.test.{js,jsx,ts,tsx}', '!src/__tests__/3.1/**/*.{js,jsx,ts,tsx}'], -}); -const config = defineConfig({ +export default defineConfig({ + plugins: [ + vitestTestingLibraryPlugin({ + runtimePkgName: '@lynx-js/react', + }), + ], test: { name: 'react/testing-library', + include: ['src/**/*.test.{js,jsx,ts,tsx}', '!src/__tests__/3.1/**/*.{js,jsx,ts,tsx}'], }, }); - -export default mergeConfig(defaultConfig, config); diff --git a/packages/rspeedy/create-rspeedy/package.json b/packages/rspeedy/create-rspeedy/package.json index be3e067105..1e65491320 100644 --- a/packages/rspeedy/create-rspeedy/package.json +++ b/packages/rspeedy/create-rspeedy/package.json @@ -42,7 +42,8 @@ "@lynx-js/react": "workspace:^", "@lynx-js/react-rsbuild-plugin": "workspace:^", "@lynx-js/rspeedy": "workspace:^", - "@rsbuild/plugin-type-check": "1.3.4" + "@rsbuild/plugin-type-check": "1.3.4", + "@rstest/core": "catalog:rstest" }, "engines": { "node": ">=18" diff --git a/packages/rspeedy/create-rspeedy/src/index.ts b/packages/rspeedy/create-rspeedy/src/index.ts index 6dac880580..fdfd853223 100644 --- a/packages/rspeedy/create-rspeedy/src/index.ts +++ b/packages/rspeedy/create-rspeedy/src/index.ts @@ -82,7 +82,7 @@ void create({ extraTools: [ { value: 'vitest-rltl', - label: 'ReactLynx Testing Library - unit testing', + label: 'Vitest', order: 'pre', when: (templateName) => templateName === 'react-js' || templateName === 'react-ts', @@ -96,6 +96,22 @@ void create({ addAgentsMdSearchDirs(from) }, }, + { + value: 'rstest-rltl', + label: 'Rstest', + order: 'pre', + when: (templateName) => + templateName === 'react-js' || templateName === 'react-ts', + action: ({ distFolder, addAgentsMdSearchDirs }) => { + const from = path.resolve(__dirname, '..', 'template-react-rstest-rltl') + copyFolder({ + from, + to: distFolder, + isMergePackageJson: true, + }) + addAgentsMdSearchDirs(from) + }, + }, ], extraSkills: [ { diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl/package.json b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl/package.json new file mode 100644 index 0000000000..87105c08e4 --- /dev/null +++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl/package.json @@ -0,0 +1,10 @@ +{ + "scripts": { + "test": "rstest run" + }, + "devDependencies": { + "@rstest/core": "^0.8.1", + "@testing-library/jest-dom": "^6.9.1", + "jsdom": "^27.4.0" + } +} diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl/rstest.config.js b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl/rstest.config.js new file mode 100644 index 0000000000..829e3b650f --- /dev/null +++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl/rstest.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from '@rstest/core' +import { withLynxConfig } from '@lynx-js/react/testing-library/rstest-config' + +export default defineConfig({ + extends: withLynxConfig(), +}) diff --git a/packages/rspeedy/create-rspeedy/template-react-rstest-rltl/src/__tests__/index.test.jsx b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl/src/__tests__/index.test.jsx new file mode 100644 index 0000000000..590db57517 --- /dev/null +++ b/packages/rspeedy/create-rspeedy/template-react-rstest-rltl/src/__tests__/index.test.jsx @@ -0,0 +1,17 @@ +// Copyright 2024 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +import '@testing-library/jest-dom' +import { expect, test } from '@rstest/core' +import { getQueriesForElement, render } from '@lynx-js/react/testing-library' + +import { App } from '../App' + +test('App', async () => { + render() + + const { findByText } = getQueriesForElement(elementTree.root) + const element = await findByText('Tap the logo and have fun!') + + expect(element).toBeInTheDocument() +}) diff --git a/packages/rspeedy/create-rspeedy/template-react-vitest-rltl/vitest.config.js b/packages/rspeedy/create-rspeedy/template-react-vitest-rltl/vitest.config.js index 98425e53c0..a311413043 100644 --- a/packages/rspeedy/create-rspeedy/template-react-vitest-rltl/vitest.config.js +++ b/packages/rspeedy/create-rspeedy/template-react-vitest-rltl/vitest.config.js @@ -1,9 +1,9 @@ -import { defineConfig, mergeConfig } from 'vitest/config' -import { createVitestConfig } from '@lynx-js/react/testing-library/vitest-config' +import { defineConfig } from 'vitest/config' +import { vitestTestingLibraryPlugin } from '@lynx-js/react/testing-library/plugins' -const defaultConfig = await createVitestConfig() -const config = defineConfig({ +export default defineConfig({ + plugins: [ + vitestTestingLibraryPlugin(), + ], test: {}, }) - -export default mergeConfig(defaultConfig, config) diff --git a/packages/rspeedy/plugin-react/package.json b/packages/rspeedy/plugin-react/package.json index 9de644426c..6f9d503a14 100644 --- a/packages/rspeedy/plugin-react/package.json +++ b/packages/rspeedy/plugin-react/package.json @@ -44,7 +44,8 @@ "@lynx-js/runtime-wrapper-webpack-plugin": "workspace:*", "@lynx-js/template-webpack-plugin": "workspace:*", "@lynx-js/use-sync-external-store": "workspace:*", - "background-only": "workspace:^" + "background-only": "workspace:^", + "tiny-invariant": "^1.3.3" }, "devDependencies": { "@lynx-js/config-rsbuild-plugin": "workspace:*", diff --git a/packages/rspeedy/plugin-react/src/loaders.ts b/packages/rspeedy/plugin-react/src/loaders.ts index 2bc826a5fe..78d4b814f6 100644 --- a/packages/rspeedy/plugin-react/src/loaders.ts +++ b/packages/rspeedy/plugin-react/src/loaders.ts @@ -7,19 +7,67 @@ import { LAYERS, ReactWebpackPlugin } from '@lynx-js/react-webpack-plugin' import type { PluginReactLynxOptions } from './pluginReactLynx.js' -export function applyLoaders( +function getLoaderOptions( api: RsbuildPluginAPI, options: Required, -): void { + isMainThread = false, +) { + const { output } = api.getRsbuildConfig() + + const inlineSourcesContent: boolean = output?.sourceMap === true || !( + // `false` + output?.sourceMap === false + // `false` + || output?.sourceMap?.js === false + // explicitly disable source content + || output?.sourceMap?.js?.includes('nosources') + ) + const { compat, enableRemoveCSSScope, shake, defineDCE, engineVersion, + experimental_isLazyBundle, } = options + return { + compat, + enableRemoveCSSScope, + isDynamicComponent: experimental_isLazyBundle, + inlineSourcesContent, + defineDCE, + engineVersion, + ...isMainThread + ? { + shake, + } + : {}, + } +} + +const TESTING_RULE_NAME = 'react:testing' +export function applyTestingLoaders( + api: RsbuildPluginAPI, + options: Required, +): void { + api.modifyBundlerChain((chain, { CHAIN_ID }) => { + const rule = chain.module.rules.get(CHAIN_ID.RULE.JS) + + rule + .use(TESTING_RULE_NAME) + .loader(ReactWebpackPlugin.loaders.TESTING) + .options(getLoaderOptions(api, options)) + .end() + }) +} + +export function applyLoaders( + api: RsbuildPluginAPI, + options: Required, +): void { api.modifyBundlerChain((chain, { CHAIN_ID }) => { const rule = chain.module.rules.get(CHAIN_ID.RULE.JS) // The Rsbuild default loaders: @@ -30,17 +78,6 @@ export function applyLoaders( // - Webpack: None const uses = rule.uses.entries() ?? {} - const { output } = api.getRsbuildConfig() - - const inlineSourcesContent: boolean = output?.sourceMap === true || !( - // `false` - output?.sourceMap === false - // `false` - || output?.sourceMap?.js === false - // explicitly disable source content - || output?.sourceMap?.js?.includes('nosources') - ) - const backgroundRule = rule.oneOf(LAYERS.BACKGROUND) // dprint-ignore backgroundRule @@ -50,14 +87,7 @@ export function applyLoaders( .end() .use(LAYERS.BACKGROUND) .loader(ReactWebpackPlugin.loaders.BACKGROUND) - .options({ - compat, - enableRemoveCSSScope, - isDynamicComponent: experimental_isLazyBundle, - inlineSourcesContent, - defineDCE, - engineVersion, - }) + .options(getLoaderOptions(api, options)) .end() const mainThreadRule = rule.oneOf(LAYERS.MAIN_THREAD) @@ -89,15 +119,7 @@ export function applyLoaders( }) .use(LAYERS.MAIN_THREAD) .loader(ReactWebpackPlugin.loaders.MAIN_THREAD) - .options({ - compat, - enableRemoveCSSScope, - inlineSourcesContent, - isDynamicComponent: experimental_isLazyBundle, - engineVersion, - shake, - defineDCE, - }) + .options(getLoaderOptions(api, options, true)) .end() // Clear the Rsbuild default loader. diff --git a/packages/rspeedy/plugin-react/src/pluginReactLynx.ts b/packages/rspeedy/plugin-react/src/pluginReactLynx.ts index 20761ecfc7..c3435ae75a 100644 --- a/packages/rspeedy/plugin-react/src/pluginReactLynx.ts +++ b/packages/rspeedy/plugin-react/src/pluginReactLynx.ts @@ -28,7 +28,7 @@ import { applyCSS } from './css.js' import { applyEntry } from './entry.js' import { applyGenerator } from './generator.js' import { applyLazy } from './lazy.js' -import { applyLoaders } from './loaders.js' +import { applyLoaders, applyTestingLoaders } from './loaders.js' import { applyNodeEnv } from './nodeEnv.js' import { applyOptimizeBundleSize } from './optimizeBundleSize.js' import { applyRefresh } from './refresh.js' @@ -389,6 +389,7 @@ export function pluginReactLynx( pre: ['lynx:rsbuild:plugin-api', 'lynx:config'], setup(api) { const isRslib = api.context.callerName === 'rslib' + const isRstest = api.context.callerName === 'rstest' const exposedConfig = api.useExposed<{ config: Config }>( Symbol.for('lynx.config'), @@ -403,11 +404,17 @@ export function pluginReactLynx( }) } - applyCSS(api, resolvedOptions) + if (!isRstest) { + applyCSS(api, resolvedOptions) + } applyEntry(api, resolvedOptions) applyBackgroundOnly(api) applyGenerator(api, resolvedOptions) - applyLoaders(api, resolvedOptions) + if (isRstest) { + applyTestingLoaders(api, resolvedOptions) + } else { + applyLoaders(api, resolvedOptions) + } applyRefresh(api) applySplitChunksRule(api) applySWC(api) diff --git a/packages/rspeedy/plugin-react/src/refresh.ts b/packages/rspeedy/plugin-react/src/refresh.ts index 3ea517e03a..a440468d7e 100644 --- a/packages/rspeedy/plugin-react/src/refresh.ts +++ b/packages/rspeedy/plugin-react/src/refresh.ts @@ -9,12 +9,20 @@ import type { RspackChain, } from '@rsbuild/core' -import { ReactRefreshRspackPlugin } from '@lynx-js/react-refresh-webpack-plugin' +import { + ReactRefreshRspackPlugin, + ReactRefreshWebpackPlugin, +} from '@lynx-js/react-refresh-webpack-plugin' import { LAYERS } from '@lynx-js/react-webpack-plugin' const PLUGIN_NAME_REACT_REFRESH = 'lynx:react:refresh' export function applyRefresh(api: RsbuildPluginAPI): void { + api.modifyWebpackChain?.(async (chain, { CHAIN_ID, isProd }) => { + if (!isProd) { + await applyRefreshRules(api, chain, CHAIN_ID, ReactRefreshWebpackPlugin) + } + }) api.modifyBundlerChain(async (chain, { isProd, CHAIN_ID }) => { if (!isProd) { // biome-ignore lint/correctness/useHookAtTopLevel: not react hooks @@ -32,11 +40,12 @@ export function applyRefresh(api: RsbuildPluginAPI): void { }) } -async function applyRefreshRules( +async function applyRefreshRules( api: RsbuildPluginAPI, chain: RspackChain, CHAIN_ID: ChainIdentifier, - ReactRefreshPlugin: typeof ReactRefreshRspackPlugin, + ReactRefreshPlugin: Bundler extends 'rspack' ? typeof ReactRefreshRspackPlugin + : typeof ReactRefreshWebpackPlugin, ) { // biome-ignore lint/correctness/useHookAtTopLevel: not react hooks const { resolve } = api.useExposed< diff --git a/packages/rspeedy/plugin-react/src/rspeedy-api.ts b/packages/rspeedy/plugin-react/src/rspeedy-api.ts new file mode 100644 index 0000000000..5a2de15cc6 --- /dev/null +++ b/packages/rspeedy/plugin-react/src/rspeedy-api.ts @@ -0,0 +1,14 @@ +// Copyright 2025 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +import invariant from 'tiny-invariant' + +import type { ExposedAPI, RsbuildPluginAPI } from '@lynx-js/rspeedy' + +export function getRspeedyAPI(api: RsbuildPluginAPI): ExposedAPI { + const rspeedyAPI = api.useExposed( + Symbol.for('rspeedy.api'), + )! + invariant(rspeedyAPI, 'Should have rspeedy.api') + return rspeedyAPI +} diff --git a/packages/rspeedy/plugin-react/test/config.test.ts b/packages/rspeedy/plugin-react/test/config.test.ts index 9457fb4344..2e28a62396 100644 --- a/packages/rspeedy/plugin-react/test/config.test.ts +++ b/packages/rspeedy/plugin-react/test/config.test.ts @@ -2063,7 +2063,7 @@ describe('Config', () => { vi.stubEnv('NODE_ENV', 'production') const entryName = 'defineDCE' - const rsbuild = await createRspeedy({ + const rsbuild = await createRspeedyWithTempDistRoot({ rspeedyConfig: { source: { entry: { @@ -2096,18 +2096,28 @@ describe('Config', () => { expect.fail('build should succeed') } - const distPath = path.join( - rsbuild.context.distPath, - '.rspeedy', - entryName, - 'main-thread.js', + const candidateOutputPaths = [ + path.join( + rsbuild.context.distPath, + '.rspeedy', + entryName, + 'main-thread.js', + ), + path.join(rsbuild.context.distPath, `${entryName}.lynx.bundle`), + ] + const builtOutputPath = candidateOutputPaths.find( + outputPath => existsSync(outputPath), ) - if (!existsSync(distPath)) { - expect.fail(`Build output should exist at ${distPath}`) + if (!builtOutputPath) { + expect.fail( + `Build output should exist in one of: ${ + candidateOutputPaths.join(', ') + }`, + ) } - const builtCode = readFileSync(distPath, 'utf8') + const builtCode = readFileSync(builtOutputPath, 'utf8') expect(builtCode).not.toContain('profileStart(\'test\')') expect(builtCode).toContain('Config is: profile-off-mode') }) @@ -2740,6 +2750,75 @@ describe('Config', () => { ).toBe('main/background.js') }) }) + + describe('callerName: rstest', async () => { + const { pluginReactLynx } = await import('../src/pluginReactLynx.js') + + const rsbuild = await createRspeedy({ + rspeedyConfig: { + plugins: [ + pluginReactLynx(), + ], + }, + callerName: 'rstest', + }) + const [config] = await rsbuild.initConfigs() + interface Rule { + test?: RegExp + use?: Array<{ loader: string }> + [key: string]: unknown + } + + const rules = config?.module?.rules as Rule[] | undefined + + test('css rules should be rsbuild default', () => { + expect( + rules?.filter((rule: Rule) => + rule + && typeof rule === 'object' + && rule.test + && rule.test.toString() === (/\.css$/).toString() + ).map((rule: Rule) => + (rule?.use?.map((u: { loader: string }) => u.loader)) ?? [] + ), + ).toMatchInlineSnapshot(` + [ + [ + "/node_modules//@rspack/core/dist/cssExtractLoader.js", + "/node_modules//@rsbuild/core/compiled/css-loader/index.js", + "builtin:lightningcss-loader", + ], + [ + "/node_modules//@rsbuild/core/compiled/css-loader/index.js", + "builtin:lightningcss-loader", + ], + [], + ] + `) + }) + test('js loaders should be testing loaders', () => { + expect( + rules?.filter((rule: Rule) => + rule + && typeof rule === 'object' + && rule.test + && rule.test.toString() + === (/\.(?:js|jsx|mjs|cjs|ts|tsx|mts|cts)$/).toString() + ).map((rule: Rule) => + (rule?.use?.map((u: { loader: string }) => u.loader)) ?? [] + ), + ).toMatchInlineSnapshot(` + [ + [ + "builtin:swc-loader", + "/packages/webpack/react-webpack-plugin/lib/loaders/testing.js", + ], + [], + [], + ] + `) + }) + }) }) describe('MPA Config', () => { diff --git a/packages/testing-library/examples/basic/package.json b/packages/testing-library/examples/basic/package.json index 5c0f9c0a15..999c960b68 100644 --- a/packages/testing-library/examples/basic/package.json +++ b/packages/testing-library/examples/basic/package.json @@ -6,6 +6,7 @@ "scripts": { "build": "rspeedy build", "dev": "rspeedy dev", + "rstest": "rstest", "test": "vitest", "test:type": "vitest --typecheck.only" }, diff --git a/packages/testing-library/examples/basic/rstest.config.ts b/packages/testing-library/examples/basic/rstest.config.ts new file mode 100644 index 0000000000..cd0c3bba3a --- /dev/null +++ b/packages/testing-library/examples/basic/rstest.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from '@rstest/core'; +import { withLynxConfig } from '@lynx-js/react/testing-library/rstest-config'; + +export default defineConfig({ + extends: withLynxConfig(), +}); diff --git a/packages/testing-library/examples/basic/src/__tests__/index.test.tsx b/packages/testing-library/examples/basic/src/__tests__/index.test.tsx index d877834a08..1b6a7c4992 100644 --- a/packages/testing-library/examples/basic/src/__tests__/index.test.tsx +++ b/packages/testing-library/examples/basic/src/__tests__/index.test.tsx @@ -2,7 +2,6 @@ // Licensed under the Apache License Version 2.0 that can be found in the // LICENSE file in the root directory of this source tree. import '@testing-library/jest-dom'; -import { expect, test, vi } from 'vitest'; import { render, getQueriesForElement } from '@lynx-js/react/testing-library'; // @ts-expect-error preact is aliased to the dep of @lynx-js/react import { Component as PreacComponent } from 'preact'; @@ -10,8 +9,15 @@ import { Component } from '@lynx-js/react'; import { App } from '../App.jsx'; +let fn; +if (typeof rstest !== 'undefined') { + fn = rstest.fn; +} else { + fn = vi.fn; +} + test('App', async () => { - const cb = vi.fn(); + const cb = fn(); render( +/// +/// diff --git a/packages/testing-library/examples/basic/vitest.config.ts b/packages/testing-library/examples/basic/vitest.config.ts index cbe326697f..37adc389db 100644 --- a/packages/testing-library/examples/basic/vitest.config.ts +++ b/packages/testing-library/examples/basic/vitest.config.ts @@ -1,13 +1,13 @@ -import { defineConfig, mergeConfig } from 'vitest/config'; -import { createVitestConfig } from '@lynx-js/react/testing-library/vitest-config'; +import { defineConfig } from 'vitest/config'; +import { vitestTestingLibraryPlugin } from '@lynx-js/react/testing-library/plugins'; -const defaultConfig = await createVitestConfig({ - runtimePkgName: '@lynx-js/react', -}); -const config = defineConfig({ +export default defineConfig({ + plugins: [ + vitestTestingLibraryPlugin({ + runtimePkgName: '@lynx-js/react', + }), + ], test: { name: 'testing-library/examples/basic', }, }); - -export default mergeConfig(defaultConfig, config); diff --git a/packages/testing-library/examples/library/package.json b/packages/testing-library/examples/library/package.json new file mode 100644 index 0000000000..c5887de3e6 --- /dev/null +++ b/packages/testing-library/examples/library/package.json @@ -0,0 +1,18 @@ +{ + "name": "@lynx-js/testing-library-example-library", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "rstest": "rstest run", + "test": "vitest run", + "test:rstest": "rstest run", + "test:vitest": "vitest run" + }, + "dependencies": { + "@lynx-js/react": "workspace:*" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1" + } +} diff --git a/packages/testing-library/examples/library/rstest.config.ts b/packages/testing-library/examples/library/rstest.config.ts new file mode 100644 index 0000000000..dc8d629688 --- /dev/null +++ b/packages/testing-library/examples/library/rstest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from '@rstest/core'; +import { withDefaultConfig } from '@lynx-js/react/testing-library/rstest-config'; + +export default defineConfig({ + extends: withDefaultConfig(), + name: 'testing-library/examples/library/rstest', +}); diff --git a/packages/testing-library/examples/library/src/useCounter.ts b/packages/testing-library/examples/library/src/useCounter.ts new file mode 100644 index 0000000000..5cffd43900 --- /dev/null +++ b/packages/testing-library/examples/library/src/useCounter.ts @@ -0,0 +1,23 @@ +import { useState } from '@lynx-js/react'; + +export interface UseCounterResult { + count: number; + inc: () => void; + dec: () => void; + reset: () => void; +} + +export function useCounter(initial = 0): UseCounterResult { + const [count, setCount] = useState(initial); + + const inc = (): void => setCount((v) => v + 1); + const dec = (): void => setCount((v) => v - 1); + const reset = (): void => setCount(initial); + + return { + count: count, + inc: inc, + dec: dec, + reset: reset, + }; +} diff --git a/packages/testing-library/examples/library/tests/useCounter.test.ts b/packages/testing-library/examples/library/tests/useCounter.test.ts new file mode 100644 index 0000000000..99e7c8d321 --- /dev/null +++ b/packages/testing-library/examples/library/tests/useCounter.test.ts @@ -0,0 +1,18 @@ +import { act, renderHook } from '@lynx-js/react/testing-library'; +import { useCounter } from '../src/useCounter.js'; + +describe('library example', () => { + it('updates hook state', () => { + const { result } = renderHook(() => useCounter(2)); + + expect(result.current.count).toBe(2); + + act(() => { + result.current.inc(); + result.current.inc(); + result.current.dec(); + }); + + expect(result.current.count).toBe(3); + }); +}); diff --git a/packages/testing-library/examples/library/tsconfig.json b/packages/testing-library/examples/library/tsconfig.json new file mode 100644 index 0000000000..beaf7aee1b --- /dev/null +++ b/packages/testing-library/examples/library/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "@lynx-js/react", + "types": ["vitest/globals", "@rstest/core/globals"], + "noEmit": true, + }, + "include": ["src", "tests"], +} diff --git a/packages/testing-library/examples/library/vitest.config.ts b/packages/testing-library/examples/library/vitest.config.ts new file mode 100644 index 0000000000..8100ef308d --- /dev/null +++ b/packages/testing-library/examples/library/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config'; +import { vitestTestingLibraryPlugin } from '@lynx-js/react/testing-library/plugins'; + +export default defineConfig({ + plugins: [ + vitestTestingLibraryPlugin({ + runtimePkgName: '@lynx-js/react', + }), + ], + test: { + name: 'testing-library/examples/library/vitest', + }, +}); diff --git a/packages/testing-library/examples/react-compiler/lynx.enable.config.ts b/packages/testing-library/examples/react-compiler/lynx.enable.config.ts new file mode 100644 index 0000000000..d2c5176d8a --- /dev/null +++ b/packages/testing-library/examples/react-compiler/lynx.enable.config.ts @@ -0,0 +1,35 @@ +import { pluginBabel } from '@rsbuild/plugin-babel'; + +import { pluginQRCode } from '@lynx-js/qrcode-rsbuild-plugin'; +import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin'; +import { defineConfig } from '@lynx-js/rspeedy'; + +export default defineConfig({ + source: { + entry: './src/index.tsx', + }, + plugins: [ + pluginReactLynx({ + enableRemoveCSSScope: true, + }), + pluginBabel({ + include: /\.(?:jsx|tsx)$/, + babelLoaderOptions(opts) { + opts.plugins?.unshift([ + 'babel-plugin-react-compiler', + // See https://react.dev/reference/react-compiler/configuration for config + { + // ReactLynx only supports target to version 17 + target: '17', + }, + ]); + }, + }), + pluginQRCode({ + schema(url) { + // We use `?fullscreen=true` to open the page in LynxExplorer in full screen mode + return `${url}?fullscreen=true`; + }, + }), + ], +}); diff --git a/packages/testing-library/examples/react-compiler/package.json b/packages/testing-library/examples/react-compiler/package.json index 3a5f1fe235..7824f3f319 100644 --- a/packages/testing-library/examples/react-compiler/package.json +++ b/packages/testing-library/examples/react-compiler/package.json @@ -6,6 +6,7 @@ "scripts": { "build": "rspeedy build", "dev": "rspeedy dev", + "rstest": "rstest", "test": "vitest" }, "dependencies": { @@ -18,9 +19,11 @@ "@babel/plugin-syntax-typescript": "^7.28.6", "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", + "@lynx-js/qrcode-rsbuild-plugin": "workspace:*", "@lynx-js/react-rsbuild-plugin": "workspace:*", "@lynx-js/rspeedy": "workspace:*", "@lynx-js/types": "3.7.0", + "@rsbuild/plugin-babel": "1.1.0", "@testing-library/jest-dom": "^6.9.1", "@types/react": "^18.3.28", "babel-plugin-react-compiler": "0.0.0-experimental-fe727a3-20250909", diff --git a/packages/testing-library/examples/react-compiler/rstest.config.ts b/packages/testing-library/examples/react-compiler/rstest.config.ts new file mode 100644 index 0000000000..36bd78acf3 --- /dev/null +++ b/packages/testing-library/examples/react-compiler/rstest.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from '@rstest/core'; +import { withLynxConfig } from '@lynx-js/react/testing-library/rstest-config'; + +export default defineConfig({ + extends: withLynxConfig({ + configPath: './lynx.enable.config.ts', + }), + resolve: { + alias: { + // not necessary in real projects, just for compatibility with vitest tests in this repo + vitest: require.resolve('./vitest-polyfill.cjs'), + }, + }, + source: { + define: { + __FORGET__: 'true', + }, + }, +}); diff --git a/packages/testing-library/examples/react-compiler/vitest-polyfill.cjs b/packages/testing-library/examples/react-compiler/vitest-polyfill.cjs new file mode 100644 index 0000000000..647f2e4a8d --- /dev/null +++ b/packages/testing-library/examples/react-compiler/vitest-polyfill.cjs @@ -0,0 +1,6 @@ +// in order to make our test case work for +// both vitest and rstest, we need to alias +// `vitest` to `@rstest/core` + +global['@rstest/core'].vi = global['@rstest/core'].rs; +module.exports = global['@rstest/core']; diff --git a/packages/testing-library/examples/react-compiler/vitest.config.compiler-disabled.ts b/packages/testing-library/examples/react-compiler/vitest.config.compiler-disabled.ts index 1505d1e4b9..6a6b5a2207 100644 --- a/packages/testing-library/examples/react-compiler/vitest.config.compiler-disabled.ts +++ b/packages/testing-library/examples/react-compiler/vitest.config.compiler-disabled.ts @@ -1,11 +1,13 @@ -import { defineConfig, mergeConfig } from 'vitest/config'; -import { createVitestConfig } from '@lynx-js/react/testing-library/vitest-config'; +import { defineConfig } from 'vitest/config'; +import { vitestTestingLibraryPlugin } from '@lynx-js/react/testing-library/plugins'; -const defaultConfig = await createVitestConfig({ - runtimePkgName: '@lynx-js/react', - experimental_enableReactCompiler: false, -}); -const config = defineConfig({ +export default defineConfig({ + plugins: [ + vitestTestingLibraryPlugin({ + runtimePkgName: '@lynx-js/react', + experimental_enableReactCompiler: false, + }), + ], define: { __FORGET__: 'false', }, @@ -13,4 +15,3 @@ const config = defineConfig({ name: 'testing-library/examples/react-compiler-disabled', }, }); -export default mergeConfig(defaultConfig, config); diff --git a/packages/testing-library/examples/react-compiler/vitest.config.compiler-enabled.ts b/packages/testing-library/examples/react-compiler/vitest.config.compiler-enabled.ts index 7b66bfae5f..ca058654c0 100644 --- a/packages/testing-library/examples/react-compiler/vitest.config.compiler-enabled.ts +++ b/packages/testing-library/examples/react-compiler/vitest.config.compiler-enabled.ts @@ -1,11 +1,13 @@ -import { defineConfig, mergeConfig } from 'vitest/config'; -import { createVitestConfig } from '@lynx-js/react/testing-library/vitest-config'; +import { defineConfig } from 'vitest/config'; +import { vitestTestingLibraryPlugin } from '@lynx-js/react/testing-library/plugins'; -const defaultConfig = await createVitestConfig({ - runtimePkgName: '@lynx-js/react', - experimental_enableReactCompiler: true, -}); -const config = defineConfig({ +export default defineConfig({ + plugins: [ + vitestTestingLibraryPlugin({ + runtimePkgName: '@lynx-js/react', + experimental_enableReactCompiler: true, + }), + ], define: { __FORGET__: 'true', }, @@ -13,4 +15,3 @@ const config = defineConfig({ name: 'testing-library/examples/react-compiler-enabled', }, }); -export default mergeConfig(defaultConfig, config); diff --git a/packages/testing-library/testing-environment/README.md b/packages/testing-library/testing-environment/README.md index 9a67389df3..9d1cac587e 100644 --- a/packages/testing-library/testing-environment/README.md +++ b/packages/testing-library/testing-environment/README.md @@ -10,7 +10,7 @@ The Element PAPI implementation is based on jsdom, for example `__CreateElement` import { LynxTestingEnv } from '@lynx-js/testing-environment'; import { JSDOM } from 'jsdom'; -const lynxTestingEnv = new LynxTestingEnv(new JSDOM()); +const lynxTestingEnv = new LynxTestingEnv({ window: new JSDOM().window }); ``` To use `@lynx-js/testing-environment`, you will primarily use the `LynxTestingEnv` constructor, which is a named export of the package. You will get back a `LynxTestingEnv` instance, which has a number of methods of useful properties, notably `switchToMainThread` and `switchToBackgroundThread`, which allow you to switch between the main thread and background thread. @@ -66,6 +66,14 @@ If you want to use `@lynx-js/testing-environment` for unit testing in ReactLynx, Please refer to [ReactLynx Testing Library](https://lynxjs.org/react/reactlynx-testing-library.html) to inherit the configuration from `@lynx-js/react/testing-library`. +### Use in Rstest + +If your runner already provides a `window` global via jsdom, you can load the shared Lynx test environment with: + +```js +import '@lynx-js/testing-environment/env/rstest'; +``` + ## Credits Thanks to: diff --git a/packages/testing-library/testing-environment/etc/testing-environment.api.md b/packages/testing-library/testing-environment/etc/testing-environment.api.md index 5e78499041..9b975bd350 100644 --- a/packages/testing-library/testing-environment/etc/testing-environment.api.md +++ b/packages/testing-library/testing-environment/etc/testing-environment.api.md @@ -4,8 +4,6 @@ ```ts -import { JSDOM } from 'jsdom'; - // @public export type ElementTree = ReturnType; @@ -73,6 +71,13 @@ export const initElementTree: () => { __GetElementByUniqueId(uniqueId: number): LynxElement | undefined; }; +// @public +export function installLynxTestingEnv(target: typeof globalThis & { + lynxEnv?: LynxEnv; + lynxTestingEnv?: LynxTestingEnv; + Node?: typeof Node; +}, env: LynxEnv): void; + // @public export interface LynxElement extends HTMLElement { cssId?: string; @@ -87,6 +92,12 @@ export interface LynxElement extends HTMLElement { parentNode: LynxElement; } +// @public +export interface LynxEnv { + // (undocumented) + window: Window & typeof globalThis; +} + // @public export interface LynxGlobalThis { // (undocumented) @@ -96,14 +107,14 @@ export interface LynxGlobalThis { // @public export class LynxTestingEnv { - constructor(jsdom?: JSDOM); + constructor(env?: LynxEnv); backgroundThread: LynxGlobalThis; // (undocumented) clearGlobal(): void; // (undocumented) - injectGlobals(): void; + env: LynxEnv; // (undocumented) - jsdom: JSDOM; + injectGlobals(): void; mainThread: LynxGlobalThis & ElementTreeGlobals; // (undocumented) reset(): void; @@ -116,4 +127,11 @@ export class LynxTestingEnv { // @public (undocumented) export type PickUnderscoreKeys = Pick>; +// @public +export function uninstallLynxTestingEnv(target: typeof globalThis & { + lynxEnv?: LynxEnv; + lynxTestingEnv?: LynxTestingEnv; + Node?: typeof Node; +}): void; + ``` diff --git a/packages/testing-library/testing-environment/package.json b/packages/testing-library/testing-environment/package.json index 9e044c0873..7ae99d6abc 100644 --- a/packages/testing-library/testing-environment/package.json +++ b/packages/testing-library/testing-environment/package.json @@ -30,6 +30,12 @@ "import": "./dist/env/vitest/index.js", "require": "./dist/env/vitest/index.cjs", "default": "./dist/env/vitest/index.js" + }, + "./env/rstest": { + "types": "./dist/env/rstest/index.d.ts", + "import": "./dist/env/rstest/index.js", + "require": "./dist/env/rstest/index.cjs", + "default": "./dist/env/rstest/index.js" } }, "main": "./dist/index.cjs", @@ -39,6 +45,9 @@ "*": { "env/vitest": [ "./dist/env/vitest/index.d.ts" + ], + "env/rstest": [ + "./dist/env/rstest/index.d.ts" ] } }, @@ -50,6 +59,7 @@ "api-extractor": "api-extractor run --verbose", "build": "rslib build", "dev": "rslib build --watch", + "rstest": "rstest", "test": "vitest" }, "devDependencies": { diff --git a/packages/testing-library/testing-environment/rstest.config.ts b/packages/testing-library/testing-environment/rstest.config.ts new file mode 100644 index 0000000000..107b5a06dd --- /dev/null +++ b/packages/testing-library/testing-environment/rstest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + testEnvironment: 'jsdom', + setupFiles: [ + require.resolve('./src/setupFiles/rstest.js'), + ], + globals: true, + resolve: { + alias: { + // Allow the shared test files to keep importing from `vitest`. + vitest: require.resolve('./vitest-polyfill.cjs'), + }, + }, + include: ['src/**/*.test.{js,jsx,ts,tsx}'], +}); diff --git a/packages/testing-library/testing-environment/src/env/rstest/index.ts b/packages/testing-library/testing-environment/src/env/rstest/index.ts new file mode 100644 index 0000000000..b4759c1c42 --- /dev/null +++ b/packages/testing-library/testing-environment/src/env/rstest/index.ts @@ -0,0 +1,3 @@ +import { installLynxTestingEnv } from '../../index.js'; + +installLynxTestingEnv(globalThis, { window }); diff --git a/packages/testing-library/testing-environment/src/env/vitest/index.ts b/packages/testing-library/testing-environment/src/env/vitest/index.ts index e5836eab90..fdd1c9d925 100644 --- a/packages/testing-library/testing-environment/src/env/vitest/index.ts +++ b/packages/testing-library/testing-environment/src/env/vitest/index.ts @@ -1,6 +1,5 @@ -import { builtinEnvironments, Environment } from 'vitest/environments'; -import { LynxTestingEnv } from '@lynx-js/testing-environment'; -import { JSDOM } from 'jsdom'; +import { builtinEnvironments, type Environment } from 'vitest/environments'; +import { installLynxTestingEnv, uninstallLynxTestingEnv } from '../../index.js'; const env = { name: 'lynxTestingEnv', @@ -9,17 +8,17 @@ const env = { const fakeGlobal: { jsdom?: any; } = {}; - await builtinEnvironments.jsdom.setup(fakeGlobal, {}); + const jsdomEnvironment = await builtinEnvironments.jsdom.setup( + fakeGlobal, + {}, + ); - const lynxTestingEnv = new LynxTestingEnv(fakeGlobal.jsdom as JSDOM); - global.lynxTestingEnv = lynxTestingEnv; - global.Node = lynxTestingEnv.jsdom.window.Node; + installLynxTestingEnv(global, fakeGlobal.jsdom); return { - teardown(global) { - delete global.lynxTestingEnv; - delete global.jsdom; - delete global.Node; + async teardown(global) { + await jsdomEnvironment.teardown(fakeGlobal); + uninstallLynxTestingEnv(global); }, }; }, diff --git a/packages/testing-library/testing-environment/src/index.ts b/packages/testing-library/testing-environment/src/index.ts index 8d2aa611ec..bb729f1aff 100644 --- a/packages/testing-library/testing-environment/src/index.ts +++ b/packages/testing-library/testing-environment/src/index.ts @@ -5,15 +5,23 @@ * notably the {@link https://lynxjs.org/api/engine/element-api | Element PAPI} and {@link https://lynxjs.org/guide/spec#dual-threaded-model | Dual-threaded Model} for use with Node.js. */ -import EventEmitter from 'events'; -import { JSDOM } from 'jsdom'; +import EventEmitter from 'node:events'; import { createGlobalThis, LynxGlobalThis } from './lynx/GlobalThis.js'; import { initElementTree, type LynxElement } from './lynx/ElementPAPI.js'; -import { Console } from 'console'; import { GlobalEventEmitter } from './lynx/GlobalEventEmitter.js'; export { initElementTree } from './lynx/ElementPAPI.js'; export type { LynxElement } from './lynx/ElementPAPI.js'; export type { LynxGlobalThis } from './lynx/GlobalThis.js'; + +/** + * The host environment used to initialize `LynxTestingEnv`. + * + * @public + */ +export interface LynxEnv { + window: Window & typeof globalThis; +} + /** * @public * The lynx element tree @@ -36,6 +44,7 @@ export type PickUnderscoreKeys = Pick>; export type ElementTreeGlobals = PickUnderscoreKeys; declare global { + var lynxEnv: LynxEnv; var lynxTestingEnv: LynxTestingEnv; var elementTree: ElementTree; var __JS__: boolean; @@ -55,6 +64,41 @@ declare global { function onInitWorkletRuntime(): void; } +/** + * Installs a `LynxTestingEnv` instance and its required globals onto a target. + * + * @public + */ +export function installLynxTestingEnv( + target: typeof globalThis & { + lynxEnv?: LynxEnv; + lynxTestingEnv?: LynxTestingEnv; + Node?: typeof Node; + }, + env: LynxEnv, +): void { + target.lynxEnv = env; + target.lynxTestingEnv = new LynxTestingEnv(env); + target.Node = env.window.Node; +} + +/** + * Removes the globals installed by `installLynxTestingEnv`. + * + * @public + */ +export function uninstallLynxTestingEnv( + target: typeof globalThis & { + lynxEnv?: LynxEnv; + lynxTestingEnv?: LynxTestingEnv; + Node?: typeof Node; + }, +): void { + delete target.lynxTestingEnv; + delete target.lynxEnv; + delete target.Node; +} + function __injectElementApi(target?: any) { const elementTree = initElementTree(); target.elementTree = elementTree; @@ -212,18 +256,6 @@ function createPolyfills() { }; } -function createPreconfiguredConsole() { - const console = new Console( - process.stdout, - process.stderr, - ); - console.profile = () => {}; - console.profileEnd = () => {}; - // @ts-expect-error Lynx has console.alog - console.alog = () => {}; - return console; -} - function injectMainThreadGlobals(target?: any, polyfills?: any) { __injectElementApi(target); @@ -301,7 +333,10 @@ function injectMainThreadGlobals(target?: any, polyfills?: any) { target.requestAnimationFrame = setTimeout; target.cancelAnimationFrame = clearTimeout; - target.console = createPreconfiguredConsole(); + target.console.profile = console.profile = () => {}; + target.console.profileEnd = console.profileEnd = () => {}; + // @ts-expect-error Lynx has console.alog + target.console.alog = console.alog = () => {}; target.__LoadLepusChunk = __LoadLepusChunk; @@ -407,7 +442,7 @@ function injectBackgroundThreadGlobals(target?: any, polyfills?: any) { }); }, select: function(selector: string) { - const el = lynxTestingEnv.jsdom.window.document.querySelector( + const el = lynxTestingEnv.env.window.document.querySelector( selector, ) as LynxElement; if (!el) { @@ -449,7 +484,10 @@ function injectBackgroundThreadGlobals(target?: any, polyfills?: any) { target.requestAnimationFrame = setTimeout; target.cancelAnimationFrame = clearTimeout; - target.console = createPreconfiguredConsole(); + target.console.profile = console.profile = () => {}; + target.console.profileEnd = console.profileEnd = () => {}; + // @ts-expect-error Lynx has console.alog + target.console.alog = console.alog = () => {}; // TODO: user-configurable target.SystemInfo = { @@ -476,8 +514,9 @@ function injectBackgroundThreadGlobals(target?: any, polyfills?: any) { * * ```ts * import { LynxTestingEnv } from '@lynx-js/testing-environment'; + * import { JSDOM } from 'jsdom'; * - * const lynxTestingEnv = new LynxTestingEnv(new JSDOM()); + * const lynxTestingEnv = new LynxTestingEnv({ window: new JSDOM().window }); * * lynxTestingEnv.switchToMainThread(); * // use the main thread Element PAPI @@ -498,8 +537,9 @@ export class LynxTestingEnv { * * ```ts * import { LynxTestingEnv } from '@lynx-js/testing-environment'; + * import { JSDOM } from 'jsdom'; * - * const lynxTestingEnv = new LynxTestingEnv(new JSDOM()); + * const lynxTestingEnv = new LynxTestingEnv({ window: new JSDOM().window }); * * lynxTestingEnv.switchToBackgroundThread(); * // use the background thread global object @@ -514,8 +554,9 @@ export class LynxTestingEnv { * * ```ts * import { LynxTestingEnv } from '@lynx-js/testing-environment'; + * import { JSDOM } from 'jsdom'; * - * const lynxTestingEnv = new LynxTestingEnv(new JSDOM()); + * const lynxTestingEnv = new LynxTestingEnv({ window: new JSDOM().window }); * * lynxTestingEnv.switchToMainThread(); * // use the main thread global object @@ -525,14 +566,15 @@ export class LynxTestingEnv { * ``` */ mainThread: LynxGlobalThis & ElementTreeGlobals; - jsdom: JSDOM; - constructor(jsdom?: JSDOM) { + env: LynxEnv; + constructor(env?: LynxEnv) { // Prefer explicit instance; fall back to test runner-provided global. - this.jsdom = jsdom ?? global.jsdom; - if (!this.jsdom) { + this.env = (env ?? global.lynxEnv) as LynxEnv; + if (!this.env) { throw new Error( - 'LynxTestingEnv requires a JSDOM instance. Pass one to the constructor, ' - + 'or ensure your test runner sets global.jsdom (e.g., via a setup file).', + 'LynxTestingEnv requires an object with a jsdom-like `window`. Pass ' + + '`{ window }` to the constructor, or ensure your test runner sets ' + + 'global.lynxEnv to that shape (e.g., via a setup file).', ); } @@ -540,13 +582,13 @@ export class LynxTestingEnv { this.mainThread = createGlobalThis() as any; const globalPolyfills = { - console: this.jsdom.window['console'], + console: this.env.window['console'], // `Event` is required by `fireEvent` in `@testing-library/dom` - Event: this.jsdom.window.Event, + Event: this.env.window.Event, // `window` is required by `getDocument` in `@testing-library/dom` - window: this.jsdom.window, + window: this.env.window, // `document` is required by `screen` in `@testing-library/dom` - document: this.jsdom.window.document, + document: this.env.window.document, }; Object.assign( diff --git a/packages/testing-library/testing-environment/src/lynx/ElementPAPI.ts b/packages/testing-library/testing-environment/src/lynx/ElementPAPI.ts index 5503194804..a3f5187c1f 100644 --- a/packages/testing-library/testing-environment/src/lynx/ElementPAPI.ts +++ b/packages/testing-library/testing-environment/src/lynx/ElementPAPI.ts @@ -80,12 +80,12 @@ export const initElementTree = () => { const page = this.__CreateElement('page', parentComponentUniqueId); this.root = page; document.body.innerHTML = ''; - lynxTestingEnv.jsdom.window.document.body.appendChild(page); + lynxTestingEnv.env.window.document.body.appendChild(page); return page; } __CreateRawText(text: string): LynxElement { - const element = lynxTestingEnv.jsdom.window.document + const element = lynxTestingEnv.env.window.document .createTextNode( text, ) as unknown as LynxElement; @@ -110,7 +110,7 @@ export const initElementTree = () => { return this.__CreateRawText(''); } - const element = lynxTestingEnv.jsdom.window.document + const element = lynxTestingEnv.env.window.document .createElement( tag, ) as LynxElement; diff --git a/packages/testing-library/testing-environment/src/setupFiles/rstest.js b/packages/testing-library/testing-environment/src/setupFiles/rstest.js new file mode 100644 index 0000000000..41d82b0219 --- /dev/null +++ b/packages/testing-library/testing-environment/src/setupFiles/rstest.js @@ -0,0 +1 @@ +import '../env/rstest/index.js'; diff --git a/packages/testing-library/testing-environment/tsconfig.json b/packages/testing-library/testing-environment/tsconfig.json index 88f6924cbf..75db74d64e 100644 --- a/packages/testing-library/testing-environment/tsconfig.json +++ b/packages/testing-library/testing-environment/tsconfig.json @@ -5,6 +5,8 @@ "noImplicitAny": false, "isolatedDeclarations": false, "rootDir": "src", + "outDir": "dist", + "declarationDir": "dist", }, "include": [ diff --git a/packages/testing-library/testing-environment/vitest-polyfill.cjs b/packages/testing-library/testing-environment/vitest-polyfill.cjs new file mode 100644 index 0000000000..647f2e4a8d --- /dev/null +++ b/packages/testing-library/testing-environment/vitest-polyfill.cjs @@ -0,0 +1,6 @@ +// in order to make our test case work for +// both vitest and rstest, we need to alias +// `vitest` to `@rstest/core` + +global['@rstest/core'].vi = global['@rstest/core'].rs; +module.exports = global['@rstest/core']; diff --git a/packages/use-sync-external-store/test/use-synx-external-store.test.ts b/packages/use-sync-external-store/test/use-sync-external-store.test.ts similarity index 98% rename from packages/use-sync-external-store/test/use-synx-external-store.test.ts rename to packages/use-sync-external-store/test/use-sync-external-store.test.ts index a5298ca6e0..a1b8a66f97 100644 --- a/packages/use-sync-external-store/test/use-synx-external-store.test.ts +++ b/packages/use-sync-external-store/test/use-sync-external-store.test.ts @@ -180,7 +180,9 @@ describe('useSyncExternalStoreWithSelector', () => { const store = createExternalStore({ items: ['A', 'B'], }); - const shallowEqualArray = (a: T[], b: T[]) => { + // dprint-ignore the comma is required to avoid + // ts treat T as a JSX element + const shallowEqualArray = (a: T[], b: T[]) => { if (a.length !== b.length) { return false; } diff --git a/packages/use-sync-external-store/vitest.config.ts b/packages/use-sync-external-store/vitest.config.ts index 94b3576d8e..9f657d6936 100644 --- a/packages/use-sync-external-store/vitest.config.ts +++ b/packages/use-sync-external-store/vitest.config.ts @@ -1,16 +1,15 @@ -import { defineProject, mergeConfig } from 'vitest/config'; +import { defineProject } from 'vitest/config'; import type { UserWorkspaceConfig } from 'vitest/config'; -import { createVitestConfig } from '@lynx-js/react/testing-library/vitest-config'; - -const defaultConfig = await createVitestConfig(); +import { vitestTestingLibraryPlugin } from '@lynx-js/react/testing-library/plugins'; const config: UserWorkspaceConfig = defineProject({ + plugins: [ + vitestTestingLibraryPlugin(), + ], test: { name: 'use-sync-external-store', }, }); -const mergedConfig: UserWorkspaceConfig = mergeConfig(defaultConfig, config); - -export default mergedConfig; +export default config; diff --git a/packages/webpack/react-webpack-plugin/etc/react-webpack-plugin.api.md b/packages/webpack/react-webpack-plugin/etc/react-webpack-plugin.api.md index 8db9b54706..e9bd1a2435 100644 --- a/packages/webpack/react-webpack-plugin/etc/react-webpack-plugin.api.md +++ b/packages/webpack/react-webpack-plugin/etc/react-webpack-plugin.api.md @@ -38,7 +38,7 @@ export class ReactWebpackPlugin { constructor(options?: ReactWebpackPluginOptions | undefined); apply(compiler: Compiler): void; static defaultOptions: Readonly>; - static loaders: Record; + static loaders: Record; } // @public diff --git a/packages/webpack/react-webpack-plugin/src/ReactWebpackPlugin.ts b/packages/webpack/react-webpack-plugin/src/ReactWebpackPlugin.ts index add85e6212..1057f79b6d 100644 --- a/packages/webpack/react-webpack-plugin/src/ReactWebpackPlugin.ts +++ b/packages/webpack/react-webpack-plugin/src/ReactWebpackPlugin.ts @@ -124,9 +124,10 @@ class ReactWebpackPlugin { * * @public */ - static loaders: Record = { + static loaders: Record = { BACKGROUND: require.resolve('../lib/loaders/background.js'), MAIN_THREAD: require.resolve('../lib/loaders/main-thread.js'), + TESTING: require.resolve('../lib/loaders/testing.js'), }; constructor( diff --git a/packages/webpack/react-webpack-plugin/src/loaders/options.ts b/packages/webpack/react-webpack-plugin/src/loaders/options.ts index 1aede58f07..248015a0a8 100644 --- a/packages/webpack/react-webpack-plugin/src/loaders/options.ts +++ b/packages/webpack/react-webpack-plugin/src/loaders/options.ts @@ -14,12 +14,12 @@ import type { } from '@lynx-js/react/transform'; const PLUGIN_NAME = 'react:webpack'; -const JSX_IMPORT_SOURCE = { +export const JSX_IMPORT_SOURCE = { MAIN_THREAD: '@lynx-js/react/lepus', BACKGROUND: '@lynx-js/react', }; const PUBLIC_RUNTIME_PKG = '@lynx-js/react'; -const RUNTIME_PKG = '@lynx-js/react/internal'; +export const RUNTIME_PKG = '@lynx-js/react/internal'; const OLD_RUNTIME_PKG = '@lynx-js/react-runtime'; const COMPONENT_PKG = '@lynx-js/react-components'; diff --git a/packages/webpack/react-webpack-plugin/src/loaders/testing.ts b/packages/webpack/react-webpack-plugin/src/loaders/testing.ts new file mode 100644 index 0000000000..8028a56c82 --- /dev/null +++ b/packages/webpack/react-webpack-plugin/src/loaders/testing.ts @@ -0,0 +1,126 @@ +// Copyright 2024 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +import { createRequire } from 'node:module'; +import path from 'node:path'; + +import type { LoaderContext } from '@rspack/core'; + +import { JSX_IMPORT_SOURCE, RUNTIME_PKG } from './options.js'; +import type { ReactLoaderOptions } from './options.js'; + +function normalizeSlashes(file: string) { + return file.replaceAll(path.win32.sep, '/'); +} + +function testingLoader( + this: LoaderContext, + content: string, +): void { + const require = createRequire(import.meta.url); + const { + compat = false, + defineDCE = { define: {} }, + engineVersion = '', + shake = false, + transformPath = '@lynx-js/react/transform', + } = this.getOptions(); + const { transformReactLynxSync } = require( + transformPath, + ) as typeof import('@lynx-js/react/transform'); + const filename = normalizeSlashes( + path.relative( + this.rootContext, + this.resourcePath, + ), + ); + const result = transformReactLynxSync( + content, + { + mode: 'test', + pluginName: '', + filename: this.resourcePath, + sourcemap: true, + snapshot: { + preserveJsx: false, + runtimePkg: RUNTIME_PKG, + jsxImportSource: JSX_IMPORT_SOURCE.BACKGROUND, + filename, + target: 'MIXED', + }, + // snapshot: true, + directiveDCE: false, + defineDCE, + shake, + compat, + engineVersion, + worklet: { + filename, + runtimePkg: RUNTIME_PKG, + target: 'MIXED', + }, + refresh: false, + cssScope: false, + }, + ); + + if (result.errors.length > 0) { + for (const error of result.errors) { + if (this.experiments?.emitDiagnostic) { + // Rspack with `emitDiagnostic` API + try { + this.experiments.emitDiagnostic({ + message: error.text!, + sourceCode: content, + location: { + line: error.location?.line ?? 1, + column: error.location?.column ?? 0, + length: error.location?.length ?? 0, + text: error.text ?? '', + }, + severity: 'error', + }); + } catch { + // Rspack may throw on invalid line & column when containing UTF-8. + // We catch it up here. + this.emitError(new Error(error.text)); + } + } else { + // Webpack or legacy Rspack + this.emitError(new Error(error.text)); + } + } + this.callback(new Error('react-transform failed')); + + return; + } + + for (const warning of result.warnings) { + if (this.experiments?.emitDiagnostic) { + // Rspack with `emitDiagnostic` API + try { + this.experiments.emitDiagnostic({ + message: warning.text!, + sourceCode: content, + location: { + line: warning.location?.line ?? 1, + column: warning.location?.column ?? 0, + length: warning.location?.length ?? 0, + text: warning.text ?? '', + }, + severity: 'warning', + }); + } catch { + // Rspack may throw on invalid line & column when containing UTF-8. + // We catch it up here. + this.emitWarning(new Error(warning.text)); + } + } else { + // Webpack or legacy Rspack + this.emitWarning(new Error(warning.text)); + } + } + this.callback(null, result.code, result.map); +} + +export default testingLoader; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 687478f770..d01daf4dd1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,10 @@ catalogs: '@rspack/test-tools': specifier: 1.5.6 version: 1.5.6 + rstest: + '@rstest/core': + specifier: 0.8.1 + version: 0.8.1 overrides: '@rspack/core': 1.7.9 @@ -74,6 +78,9 @@ importers: '@rspack/core': specifier: 1.7.9 version: 1.7.9(@swc/helpers@0.5.21) + '@rstest/core': + specifier: catalog:rstest + version: 0.8.1(jsdom@27.4.0) '@svitejs/changesets-changelog-github-compact': specifier: ^1.2.0 version: 1.2.0 @@ -616,9 +623,21 @@ importers: '@lynx-js/react': specifier: workspace:* version: link:.. + '@lynx-js/react-rsbuild-plugin': + specifier: workspace:* + version: link:../../rspeedy/plugin-react + '@lynx-js/rspeedy': + specifier: workspace:* + version: link:../../rspeedy/core '@lynx-js/testing-environment': specifier: workspace:* version: link:../../testing-library/testing-environment + '@rsbuild/core': + specifier: catalog:rsbuild + version: 1.7.5 + '@rstest/adapter-rsbuild': + specifier: ^0.2.3 + version: 0.2.3(@rsbuild/core@1.7.5)(@rstest/core@0.8.1(jsdom@27.4.0)) '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 @@ -851,6 +870,9 @@ importers: '@rsbuild/plugin-type-check': specifier: 1.3.4 version: 1.3.4(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))(@rspack/core@1.7.9(@swc/helpers@0.5.21))(tslib@2.8.1)(typescript@5.9.3) + '@rstest/core': + specifier: catalog:rstest + version: 0.8.1(jsdom@27.4.0) packages/rspeedy/lynx-bundle-rslib-config: dependencies: @@ -992,6 +1014,9 @@ importers: background-only: specifier: workspace:^ version: link:../../background-only + tiny-invariant: + specifier: ^1.3.3 + version: 1.3.3 devDependencies: '@lynx-js/config-rsbuild-plugin': specifier: workspace:* @@ -1161,6 +1186,16 @@ importers: specifier: ^6.9.1 version: 6.9.1 + packages/testing-library/examples/library: + dependencies: + '@lynx-js/react': + specifier: workspace:* + version: link:../../../react + devDependencies: + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + packages/testing-library/examples/react-compiler: dependencies: '@lynx-js/react': @@ -1185,6 +1220,9 @@ importers: '@babel/preset-typescript': specifier: ^7.28.5 version: 7.28.5(@babel/core@7.29.0) + '@lynx-js/qrcode-rsbuild-plugin': + specifier: workspace:* + version: link:../../../rspeedy/plugin-qrcode '@lynx-js/react-rsbuild-plugin': specifier: workspace:* version: link:../../../rspeedy/plugin-react @@ -1194,6 +1232,9 @@ importers: '@lynx-js/types': specifier: 3.7.0 version: 3.7.0 + '@rsbuild/plugin-babel': + specifier: 1.1.0 + version: 1.1.0(@rsbuild/core@1.7.5) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -3739,6 +3780,11 @@ packages: cpu: [x64] os: [win32] + '@rsbuild/core@1.7.2': + resolution: {integrity: sha512-VAFO6cM+cyg2ntxNW6g3tB2Jc5J5mpLjLluvm7VtW2uceNzyUlVv41o66Yp1t1ikxd3ljtqegViXem62JqzveA==} + engines: {node: '>=18.12.0'} + hasBin: true + '@rsbuild/core@1.7.5': resolution: {integrity: sha512-i37urpoV4y9NSsGiUOuLdoI42KJ5h4gAZ8EG8Ilmsond3bxoAoOCu7YvC+1pJ7p+r16suVPW8cki891ZKHOoXQ==} engines: {node: '>=18.12.0'} @@ -3981,6 +4027,25 @@ packages: '@rstack-dev/doc-ui@1.12.3': resolution: {integrity: sha512-5W70pjRxxwyNT3R4kTYDE8cPaMjsJKXMeZQn7+Q54+RCJ1ahN4pADnpaY7WvSEBWkjXdI4IR4GGvBs7nSU/8MA==} + '@rstest/adapter-rsbuild@0.2.3': + resolution: {integrity: sha512-ZKQkY3wI+PLyPJR41xFrAQ0AmenUivo5l1/g97p00+nk1peGMr9gjUxg5gqt3M7qLQZ0RiJZX/KPkPeOltd4lQ==} + peerDependencies: + '@rsbuild/core': '*' + '@rstest/core': '>=0.7.7' + + '@rstest/core@0.8.1': + resolution: {integrity: sha512-7d/2fm2V91pVx/rRtZ2gl6Zh4hVMivtDl4RgHFhBOrxi//UwhKISeF5gS/CSwpCgfOf10TzJRXqdI17ueUBNMQ==} + engines: {node: '>=18.12.0'} + hasBin: true + peerDependencies: + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + happy-dom: + optional: true + jsdom: + optional: true + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -4050,8 +4115,8 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - '@sinclair/typebox@0.34.37': - resolution: {integrity: sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==} + '@sinclair/typebox@0.34.38': + resolution: {integrity: sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==} '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} @@ -4312,8 +4377,8 @@ packages: '@types/bonjour@3.5.13': resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} - '@types/chai@5.2.2': - resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} '@types/connect-history-api-fallback@1.5.4': resolution: {integrity: sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==} @@ -5225,9 +5290,9 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chai@5.2.0: - resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} - engines: {node: '>=12'} + chai@5.2.1: + resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} + engines: {node: '>=18'} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -11210,7 +11275,7 @@ snapshots: '@jest/schemas@30.0.5': dependencies: - '@sinclair/typebox': 0.34.37 + '@sinclair/typebox': 0.34.38 '@jest/transform@29.7.0': dependencies: @@ -11979,6 +12044,14 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.34.9': optional: true + '@rsbuild/core@1.7.2': + dependencies: + '@rspack/core': 1.7.9(@swc/helpers@0.5.21) + '@rspack/lite-tapable': 1.1.0 + '@swc/helpers': 0.5.21 + core-js: 3.47.0 + jiti: 2.6.1 + '@rsbuild/core@1.7.5': dependencies: '@rspack/core': 1.7.9(@swc/helpers@0.5.21) @@ -12440,6 +12513,19 @@ snapshots: - react - react-dom + '@rstest/adapter-rsbuild@0.2.3(@rsbuild/core@1.7.5)(@rstest/core@0.8.1(jsdom@27.4.0))': + dependencies: + '@rsbuild/core': 1.7.5 + '@rstest/core': 0.8.1(jsdom@27.4.0) + + '@rstest/core@0.8.1(jsdom@27.4.0)': + dependencies: + '@rsbuild/core': 1.7.2 + '@types/chai': 5.2.3 + tinypool: 1.1.1 + optionalDependencies: + jsdom: 27.4.0 + '@rtsao/scc@1.1.0': {} '@rushstack/node-core-library@5.22.0(@types/node@24.10.13)': @@ -12534,7 +12620,7 @@ snapshots: '@sinclair/typebox@0.27.8': {} - '@sinclair/typebox@0.34.37': {} + '@sinclair/typebox@0.34.38': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -12756,9 +12842,10 @@ snapshots: dependencies: '@types/node': 24.10.13 - '@types/chai@5.2.2': + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 '@types/connect-history-api-fallback@1.5.4': dependencies: @@ -13212,10 +13299,10 @@ snapshots: '@vitest/expect@3.2.4': dependencies: - '@types/chai': 5.2.2 + '@types/chai': 5.2.3 '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 - chai: 5.2.0 + chai: 5.2.1 tinyrainbow: 2.0.0 '@vitest/mocker@3.2.4(vite@5.4.2(@types/node@24.10.13)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.31.6))': @@ -13804,7 +13891,7 @@ snapshots: ccount@2.0.1: {} - chai@5.2.0: + chai@5.2.1: dependencies: assertion-error: 2.0.1 check-error: 2.1.1 @@ -19207,7 +19294,7 @@ snapshots: vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.13)(@vitest/ui@3.2.4)(jsdom@27.4.0)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.31.6): dependencies: - '@types/chai': 5.2.2 + '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 '@vitest/mocker': 3.2.4(vite@5.4.2(@types/node@24.10.13)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.31.6)) '@vitest/pretty-format': 3.2.4 @@ -19215,7 +19302,7 @@ snapshots: '@vitest/snapshot': 3.2.4 '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 - chai: 5.2.0 + chai: 5.2.1 debug: 4.4.3 expect-type: 1.2.1 magic-string: 0.30.21 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f28c7ece55..03b9dd5ef4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -57,6 +57,10 @@ catalogs: "@rspack/core": "1.7.9" "@rspack/test-tools": "1.5.6" + # Rstest monorepo packages + rstest: + "@rstest/core": "0.8.1" + overrides: "@rspack/core": "$@rspack/core" "@rsbuild/core>@rspack/core": "$@rspack/core"