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"