@equinor/fusion-framework-vitest-plugin-react-app 0.2.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/LICENSE +21 -0
- package/README.md +349 -0
- package/dist/esm/app-test.js +9 -0
- package/dist/esm/app-test.js.map +1 -0
- package/dist/esm/define-project.js +68 -0
- package/dist/esm/define-project.js.map +1 -0
- package/dist/esm/index.js +99 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/render-app-component.js +52 -0
- package/dist/esm/render-app-component.js.map +1 -0
- package/dist/esm/render-app-hook.js +63 -0
- package/dist/esm/render-app-hook.js.map +1 -0
- package/dist/esm/render.js +49 -0
- package/dist/esm/render.js.map +1 -0
- package/dist/esm/resolve-app-test-env.js +85 -0
- package/dist/esm/resolve-app-test-env.js.map +1 -0
- package/dist/esm/scope/create-app-scope-wrapper.js +15 -0
- package/dist/esm/scope/create-app-scope-wrapper.js.map +1 -0
- package/dist/esm/scope/default-app-env.js +12 -0
- package/dist/esm/scope/default-app-env.js.map +1 -0
- package/dist/esm/scope/index.js +5 -0
- package/dist/esm/scope/index.js.map +1 -0
- package/dist/esm/scope/resolve-app-scope.js +21 -0
- package/dist/esm/scope/resolve-app-scope.js.map +1 -0
- package/dist/esm/scope/resolve-fusion.js +18 -0
- package/dist/esm/scope/resolve-fusion.js.map +1 -0
- package/dist/esm/test-app.js +59 -0
- package/dist/esm/test-app.js.map +1 -0
- package/dist/esm/test.js +33 -0
- package/dist/esm/test.js.map +1 -0
- package/dist/esm/version.js +3 -0
- package/dist/esm/version.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/app-test.d.ts +6 -0
- package/dist/types/define-project.d.ts +41 -0
- package/dist/types/index.d.ts +45 -0
- package/dist/types/render-app-component.d.ts +86 -0
- package/dist/types/render-app-hook.d.ts +99 -0
- package/dist/types/render.d.ts +37 -0
- package/dist/types/resolve-app-test-env.d.ts +56 -0
- package/dist/types/scope/create-app-scope-wrapper.d.ts +14 -0
- package/dist/types/scope/default-app-env.d.ts +5 -0
- package/dist/types/scope/index.d.ts +4 -0
- package/dist/types/scope/resolve-app-scope.d.ts +31 -0
- package/dist/types/scope/resolve-fusion.d.ts +13 -0
- package/dist/types/test-app.d.ts +68 -0
- package/dist/types/test.d.ts +66 -0
- package/dist/types/version.d.ts +1 -0
- package/docs/advanced.md +140 -0
- package/docs/configuration.md +109 -0
- package/docs/getting-started.md +66 -0
- package/docs/migrating-an-existing-app.md +197 -0
- package/docs/module-mocks.md +120 -0
- package/docs/overview.md +49 -0
- package/docs/troubleshooting.md +64 -0
- package/docs/why-browser-mode.md +113 -0
- package/package.json +87 -0
- package/src/__tests__/app-test-vite-plugin.test.ts +109 -0
- package/src/__tests__/resolve-app-test-env.test.ts +83 -0
- package/src/app-test.ts +18 -0
- package/src/define-project.ts +84 -0
- package/src/index.ts +124 -0
- package/src/render-app-component.tsx +116 -0
- package/src/render-app-hook.tsx +135 -0
- package/src/render.tsx +64 -0
- package/src/resolve-app-test-env.ts +132 -0
- package/src/scope/create-app-scope-wrapper.tsx +26 -0
- package/src/scope/default-app-env.ts +13 -0
- package/src/scope/index.ts +4 -0
- package/src/scope/resolve-app-scope.ts +46 -0
- package/src/scope/resolve-fusion.ts +29 -0
- package/src/test-app.tsx +72 -0
- package/src/test.tsx +35 -0
- package/src/version.ts +2 -0
- package/src/virtual-modules.d.ts +12 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +10 -0
package/docs/overview.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Test Fusion React apps
|
|
2
|
+
|
|
3
|
+
Fusion Framework uses Vitest for application tests. Vitest shares Vite's module and
|
|
4
|
+
configuration model, runs components in a real browser, and owns the test runner, assertions,
|
|
5
|
+
fixtures, spies, coverage, and editor integration. Fusion provides only the application scope
|
|
6
|
+
and deterministic platform boundaries needed to test production behavior.
|
|
7
|
+
|
|
8
|
+
Use `@equinor/fusion-framework-vitest-plugin-react-app` when a React hook, component, route,
|
|
9
|
+
or complete app consumes Fusion modules. The package resolves app files, initializes the real
|
|
10
|
+
framework and app provider nesting, and substitutes supported external clients without
|
|
11
|
+
introducing a second testing language.
|
|
12
|
+
|
|
13
|
+
## Choose the subject
|
|
14
|
+
|
|
15
|
+
| Subject | Start with | What the test proves |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| Pure function or framework-independent hook | Standard Vitest | Local behavior without a Fusion runtime |
|
|
18
|
+
| Hook that consumes Fusion app modules | `renderAppHook` | Hook behavior inside an initialized app scope |
|
|
19
|
+
| Component that consumes Fusion app modules | `/test` `render` fixture or `renderAppComponent` | Rendered behavior with real providers |
|
|
20
|
+
| Route or complete application | `/test` `test` and `render` | App configuration, navigation, loaders, and module lifecycle together |
|
|
21
|
+
| Framework or module integration without React | Package `/mock` entry points | Production provider behavior with external boundaries substituted |
|
|
22
|
+
|
|
23
|
+
Prefer the smallest layer containing the behavior under assertion. A component test should
|
|
24
|
+
not recreate a portal, while an app lifecycle test should not replace every Fusion hook with
|
|
25
|
+
a JavaScript module mock.
|
|
26
|
+
|
|
27
|
+
## Start here
|
|
28
|
+
|
|
29
|
+
Follow [Getting started](getting-started.md) to install the browser dependencies, configure
|
|
30
|
+
Vitest, and run one deterministic app test without credentials, a portal, or backend services.
|
|
31
|
+
|
|
32
|
+
## Continue by task
|
|
33
|
+
|
|
34
|
+
- [Configure Vitest defaults and non-standard app files](configuration.md)
|
|
35
|
+
- [Compose fixtures and explicit render options](advanced.md)
|
|
36
|
+
- [Seed Fusion module dependencies](module-mocks.md)
|
|
37
|
+
- [Troubleshoot browser, app-resolution, and network failures](troubleshooting.md)
|
|
38
|
+
- [Understand the Browser Mode default or choose another renderer](why-browser-mode.md)
|
|
39
|
+
- [Migrate an existing app from Testing Library and DOM emulation](migrating-an-existing-app.md)
|
|
40
|
+
- [Choose between app, framework, module, and HTTP testing](../../../framework/docs/testing-choosing-a-layer.md)
|
|
41
|
+
|
|
42
|
+
Use the official Vitest documentation for generic runner concepts:
|
|
43
|
+
|
|
44
|
+
- [Why Vitest](https://vitest.dev/guide/why)
|
|
45
|
+
- [Writing tests](https://vitest.dev/guide/learn/writing-tests)
|
|
46
|
+
- [Browser Mode](https://vitest.dev/guide/browser/)
|
|
47
|
+
- [Test context and fixtures](https://vitest.dev/guide/test-context)
|
|
48
|
+
- [Configuration](https://vitest.dev/config/)
|
|
49
|
+
- [Mocking](https://vitest.dev/guide/mocking)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Troubleshoot Fusion React app tests
|
|
2
|
+
|
|
3
|
+
Use this guide when a Fusion app test fails before the assertion, loads real services, cannot
|
|
4
|
+
resolve the app, or behaves differently in Vitest Browser Mode.
|
|
5
|
+
|
|
6
|
+
## Common symptoms
|
|
7
|
+
|
|
8
|
+
| Symptom | Likely cause | Resolution |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| Chromium executable is missing | Playwright installed without its browser binary | Run `pnpm exec playwright install chromium` |
|
|
11
|
+
| The `/test` import cannot resolve Fusion virtual modules | `appTestVitePlugin` is not registered in this Vitest project | Export `defineProject()` from the package's `/config` entry point or register `appTestVitePlugin()` manually |
|
|
12
|
+
| `FileNotFoundError` names a manifest, config, or configurator | An explicit plugin path does not exist | Correct the path relative to the Vitest project root; explicit paths fail instead of falling back |
|
|
13
|
+
| The app's module configuration is not applied | No conventional `src/config.ts(x|js)` file was found | Pass `configure` to `appTestVitePlugin` or override the `configure` fixture |
|
|
14
|
+
| A render result is a Promise or assertions run before initialization | A Fusion render helper was not awaited | Await `render`, `renderHook`, `renderAppComponent`, or `renderAppHook` |
|
|
15
|
+
| A request reaches a live backend or fails with a network error | No HTTP middleware answered it | Add a matching `configurator.http.addMiddleware`; unmatched requests eventually reach the real network |
|
|
16
|
+
| A lazy route import reloads the browser during a test | Source warmup no longer covers the lazy module | Keep `server.warmup.clientFiles` aligned with application source and route locations |
|
|
17
|
+
| No tests are found | Tests are outside the default `src/**/*.{test,spec}.{ts,tsx}` pattern | Override `test.include` in `defineProject` |
|
|
18
|
+
| The app starts signed in unexpectedly | The MSAL mock defaults to `Test User` | Set `configurator.msal.setAccount(null)` before rendering |
|
|
19
|
+
| State configured in one test is missing in another | Framework and app fixtures are test-scoped | Seed the required state per test or publish reusable fixture declarations with `test.extend` |
|
|
20
|
+
| `vi.spyOn` fails on an imported module in Browser Mode | Native ESM module namespace objects are sealed | Use `vi.mock('./module.js', { spy: true })` as documented by Vitest Browser Mode |
|
|
21
|
+
|
|
22
|
+
## Verify the project wiring
|
|
23
|
+
|
|
24
|
+
Start with the smallest configuration:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { defineProject } from '@equinor/fusion-framework-vitest-plugin-react-app/config';
|
|
28
|
+
|
|
29
|
+
export default defineProject();
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then verify the fixture itself before testing application behavior:
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { expect } from 'vitest';
|
|
36
|
+
import { test } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
|
|
37
|
+
|
|
38
|
+
test('initializes a Fusion app scope', async ({ app, render }) => {
|
|
39
|
+
expect(app.auth.account?.name).toBe('Test User');
|
|
40
|
+
const screen = await render(<div>ready</div>);
|
|
41
|
+
await expect.element(screen.getByText('ready')).toBeVisible();
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If this test fails, fix project or app-file resolution before adding module-specific data.
|
|
46
|
+
|
|
47
|
+
## Keep tests offline
|
|
48
|
+
|
|
49
|
+
Fusion mocks replace known external boundaries, but an HTTP middleware is an ordered chain.
|
|
50
|
+
Returning `next(uri, init)` delegates to the next middleware and eventually `fetch`. A test
|
|
51
|
+
that must never use the network should register middleware for every expected request and
|
|
52
|
+
fail deliberately on unexpected requests.
|
|
53
|
+
|
|
54
|
+
Use `createOpenApiMockMiddleware` from
|
|
55
|
+
`@equinor/fusion-framework-module-http/mock` when an OpenAPI document describes many routes.
|
|
56
|
+
Use `vi.fn` around a middleware when the test also needs call assertions.
|
|
57
|
+
|
|
58
|
+
## Related references
|
|
59
|
+
|
|
60
|
+
- [Getting started](getting-started.md)
|
|
61
|
+
- [Configuration and app-file resolution](configuration.md)
|
|
62
|
+
- [Choose a Fusion testing layer](../../../framework/docs/testing-choosing-a-layer.md)
|
|
63
|
+
- [HTTP middleware testing](../../../modules/http/docs/testing.md)
|
|
64
|
+
- [Vitest Browser Mode limitations](https://vitest.dev/guide/browser/#limitations)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Why Browser Mode is the default
|
|
2
|
+
|
|
3
|
+
`@equinor/fusion-framework-vitest-plugin-react-app` runs component and hook tests in headless
|
|
4
|
+
Chromium by default. A real browser gives these tests more accurate DOM behavior, but it is
|
|
5
|
+
slower than `happy-dom` or `jsdom`.
|
|
6
|
+
|
|
7
|
+
This guide explains that tradeoff and shows how to use a different browser or renderer when a
|
|
8
|
+
test does not need Chromium.
|
|
9
|
+
|
|
10
|
+
## Performance
|
|
11
|
+
|
|
12
|
+
A real browser is slower than DOM emulation. `happy-dom` and `jsdom` run in the same Node process
|
|
13
|
+
as the test. Browser Mode starts Chromium through Playwright and communicates with that browser
|
|
14
|
+
process.
|
|
15
|
+
|
|
16
|
+
Choose Browser Mode for fidelity, not speed. Keep fast unit tests on Node or DOM emulation when
|
|
17
|
+
they do not need browser behavior.
|
|
18
|
+
|
|
19
|
+
## Why use a real browser
|
|
20
|
+
|
|
21
|
+
- **React compatibility:** the previous React test setup failed after its React 19 peer
|
|
22
|
+
dependency update. The repository then moved those tests to Vitest Browser Mode. See the
|
|
23
|
+
`packages/utils/observable` 8.4.4 changelog and migration commit `059aefae5d`.
|
|
24
|
+
- **Real framework execution:** tests control external boundaries and seed module state through
|
|
25
|
+
[Module mocks](module-mocks.md), while the actual module configuration, lifecycle, providers,
|
|
26
|
+
and rendering behavior still run.
|
|
27
|
+
- **Browser fidelity:** Chromium provides real layout, `ResizeObserver`, custom elements, and
|
|
28
|
+
Shadow DOM. This can remove DOM polyfills and component replacements that only exist because
|
|
29
|
+
`happy-dom` or `jsdom` cannot provide the required browser behavior.
|
|
30
|
+
|
|
31
|
+
A real browser does not remove every test workaround. AG Grid license messages and Lit dev-mode
|
|
32
|
+
warnings can still occur in Chromium. Component mocks may also be useful because they simplify
|
|
33
|
+
focused tests. Remove a workaround only after running the affected test against the real
|
|
34
|
+
component.
|
|
35
|
+
|
|
36
|
+
## Choose a different runtime
|
|
37
|
+
|
|
38
|
+
You can change either the browser provider or the renderer:
|
|
39
|
+
|
|
40
|
+
- Stay in Browser Mode and replace Playwright with another Vitest browser provider.
|
|
41
|
+
- Keep the Fusion module setup but render with `happy-dom` or `jsdom`.
|
|
42
|
+
|
|
43
|
+
### Use another browser provider
|
|
44
|
+
|
|
45
|
+
Pass `test.browser.provider` to `defineProject` to replace Playwright while staying in Browser
|
|
46
|
+
Mode. This example uses WebdriverIO:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { defineProject } from '@equinor/fusion-framework-vitest-plugin-react-app/config';
|
|
50
|
+
import { webdriverio } from '@vitest/browser-webdriverio';
|
|
51
|
+
|
|
52
|
+
export default defineProject({
|
|
53
|
+
test: { browser: { provider: webdriverio() } },
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
See [Configuration](configuration.md) for the complete `defineProject` behavior.
|
|
58
|
+
|
|
59
|
+
### Use happy-dom or jsdom
|
|
60
|
+
|
|
61
|
+
The Fusion module setup does not depend on Browser Mode. Build the same provider tree and pass it
|
|
62
|
+
to another renderer. The example below uses `@testing-library/react` on `happy-dom`:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { render } from '@testing-library/react';
|
|
66
|
+
import { mockFramework } from '@equinor/fusion-framework/mock';
|
|
67
|
+
import { enableAppManifestMock, mockAppModules } from '@equinor/fusion-framework-app/mock';
|
|
68
|
+
import { FrameworkProvider } from '@equinor/fusion-framework-react';
|
|
69
|
+
import { ModuleProvider } from '@equinor/fusion-framework-react-module';
|
|
70
|
+
import type { AppModule } from '@equinor/fusion-framework-module-app';
|
|
71
|
+
import type { ReactElement } from 'react';
|
|
72
|
+
|
|
73
|
+
const env = {
|
|
74
|
+
manifest: {
|
|
75
|
+
appKey: 'test-app',
|
|
76
|
+
displayName: 'Test App',
|
|
77
|
+
description: 'Test app',
|
|
78
|
+
type: 'standalone',
|
|
79
|
+
},
|
|
80
|
+
} as const;
|
|
81
|
+
|
|
82
|
+
async function renderWithHappyDom(ui: ReactElement) {
|
|
83
|
+
const framework = await mockFramework<[AppModule]>((configurator) =>
|
|
84
|
+
enableAppManifestMock(configurator, env),
|
|
85
|
+
);
|
|
86
|
+
const app = await mockAppModules(undefined, env, framework);
|
|
87
|
+
return render(ui, {
|
|
88
|
+
wrapper: ({ children }) => (
|
|
89
|
+
<FrameworkProvider value={framework}>
|
|
90
|
+
<ModuleProvider value={app}>{children}</ModuleProvider>
|
|
91
|
+
</FrameworkProvider>
|
|
92
|
+
),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Run the test in a Vitest project with `environment: 'happy-dom'` instead of
|
|
98
|
+
`browser.enabled`. Use a separate project if the same app also has Browser Mode tests.
|
|
99
|
+
|
|
100
|
+
Call the same `enable*Mock` functions from the `mockAppModules` configuration callback. These
|
|
101
|
+
mocks configure Fusion modules and do not depend on the renderer. `renderHook` from
|
|
102
|
+
`@testing-library/react` can use the same wrapper for hook tests.
|
|
103
|
+
|
|
104
|
+
Use Browser Mode when browser behavior matters. Use `happy-dom`, `jsdom`, or no DOM for focused
|
|
105
|
+
tests that only need application logic or simple rendering.
|
|
106
|
+
|
|
107
|
+
## Related documentation
|
|
108
|
+
|
|
109
|
+
- [Overview](overview.md)
|
|
110
|
+
- [Configuration](configuration.md)
|
|
111
|
+
- [Module mocks](module-mocks.md)
|
|
112
|
+
- [Migrate an existing app to Fusion Vitest](migrating-an-existing-app.md)
|
|
113
|
+
- [Choose a Fusion testing layer](../../../framework/docs/testing-choosing-a-layer.md)
|
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@equinor/fusion-framework-vitest-plugin-react-app",
|
|
3
|
+
"version": "0.2.0-next.0",
|
|
4
|
+
"description": "Vite plugin resolving an application's manifest, config, and module-configurator as virtual modules for testing.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/esm/index.js",
|
|
9
|
+
"types": "./dist/types/index.d.ts"
|
|
10
|
+
},
|
|
11
|
+
"./test": {
|
|
12
|
+
"types": "./dist/types/app-test.d.ts",
|
|
13
|
+
"import": "./dist/esm/app-test.js"
|
|
14
|
+
},
|
|
15
|
+
"./config": {
|
|
16
|
+
"types": "./dist/types/define-project.d.ts",
|
|
17
|
+
"import": "./dist/esm/define-project.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"types": "dist/types/index.d.ts",
|
|
21
|
+
"typesVersions": {
|
|
22
|
+
"*": {
|
|
23
|
+
"test": [
|
|
24
|
+
"dist/types/app-test.d.ts"
|
|
25
|
+
],
|
|
26
|
+
"config": [
|
|
27
|
+
"dist/types/define-project.d.ts"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"directories": {
|
|
32
|
+
"dist": "dist"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/equinor/fusion-framework.git",
|
|
37
|
+
"directory": "packages/vitest-plugin/react-app"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"vite",
|
|
41
|
+
"vitest",
|
|
42
|
+
"fusion-framework",
|
|
43
|
+
"testing"
|
|
44
|
+
],
|
|
45
|
+
"license": "ISC",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@equinor/fusion-framework": "8.1.0-next.0",
|
|
51
|
+
"@equinor/fusion-framework-app": "14.0.0-next.0",
|
|
52
|
+
"@equinor/fusion-framework-module-app": "8.1.0-next.0",
|
|
53
|
+
"@equinor/fusion-framework-react": "9.0.0-next.0",
|
|
54
|
+
"@equinor/fusion-framework-react-module": "4.0.3-next.0",
|
|
55
|
+
"@equinor/fusion-imports": "2.0.3-next.0",
|
|
56
|
+
"@equinor/fusion-framework-cli": "15.2.8-next.0",
|
|
57
|
+
"@equinor/fusion-framework-module": "6.1.3-next.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/react": "^19.2.7",
|
|
61
|
+
"@types/react-dom": "^19.2.3",
|
|
62
|
+
"@vitest/browser-playwright": "^4.1.0",
|
|
63
|
+
"playwright": "^1.62.1",
|
|
64
|
+
"react": "^19.2.1",
|
|
65
|
+
"react-dom": "^19.2.1",
|
|
66
|
+
"rxjs": "^7.8.1",
|
|
67
|
+
"typescript": "^7.0.2",
|
|
68
|
+
"vite": "^8.0.0",
|
|
69
|
+
"vitest": "^4.1.10",
|
|
70
|
+
"vitest-browser-react": "^2.2.0"
|
|
71
|
+
},
|
|
72
|
+
"peerDependencies": {
|
|
73
|
+
"@types/react": "^18.0.0 || ^19.0.0",
|
|
74
|
+
"@vitest/browser-playwright": "^4.0.0",
|
|
75
|
+
"playwright": "^1.0.0",
|
|
76
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
77
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
78
|
+
"rxjs": "^7.0.0",
|
|
79
|
+
"vite": "^8.0.0",
|
|
80
|
+
"vitest": "^4.0.0",
|
|
81
|
+
"vitest-browser-react": "^2.2.0"
|
|
82
|
+
},
|
|
83
|
+
"scripts": {
|
|
84
|
+
"build": "tsc -b",
|
|
85
|
+
"test": "vitest --run"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { appTestVitePlugin } from '../index.js';
|
|
8
|
+
|
|
9
|
+
type ResolveIdHook = (id: string) => string | null | undefined;
|
|
10
|
+
type LoadHook = (id: string) => string | null | undefined | Promise<string | null | undefined>;
|
|
11
|
+
type ConfigResolvedHook = (config: { root: string }) => void;
|
|
12
|
+
|
|
13
|
+
describe('appTestVitePlugin', () => {
|
|
14
|
+
let dir: string;
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
dir = await mkdtemp(join(tmpdir(), 'fusion-vitest-plugin-react-app-'));
|
|
18
|
+
await writeFile(
|
|
19
|
+
join(dir, 'package.json'),
|
|
20
|
+
JSON.stringify({ name: '@equinor/my-app', version: '1.2.3' }),
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await rm(dir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('claims only its two virtual specifiers, leaving everything else unresolved', () => {
|
|
29
|
+
const plugin = appTestVitePlugin({ entrypoint: dir });
|
|
30
|
+
const resolveId = plugin.resolveId as ResolveIdHook;
|
|
31
|
+
|
|
32
|
+
expect(resolveId('virtual:fusion-app-test-env')).toBe('\0virtual:fusion-app-test-env');
|
|
33
|
+
expect(resolveId('virtual:fusion-app-test-configure')).toBe(
|
|
34
|
+
'\0virtual:fusion-app-test-configure',
|
|
35
|
+
);
|
|
36
|
+
expect(resolveId('some-other-module')).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('serves the resolved manifest/config as the env virtual module', async () => {
|
|
40
|
+
const plugin = appTestVitePlugin({ entrypoint: dir });
|
|
41
|
+
const load = plugin.load as LoadHook;
|
|
42
|
+
|
|
43
|
+
const source = await load('\0virtual:fusion-app-test-env');
|
|
44
|
+
|
|
45
|
+
expect(source).toContain('"appKey":"my-app"');
|
|
46
|
+
expect(source).toContain('export const config = {"environment":{}};');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('serves an undefined configure export when no config file exists', async () => {
|
|
50
|
+
const plugin = appTestVitePlugin({ entrypoint: dir });
|
|
51
|
+
const load = plugin.load as LoadHook;
|
|
52
|
+
|
|
53
|
+
const source = await load('\0virtual:fusion-app-test-configure');
|
|
54
|
+
|
|
55
|
+
expect(source).toBe('export const configure = undefined;');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('resolves the app from the Vitest project root when no entrypoint is provided', async () => {
|
|
59
|
+
await mkdir(join(dir, 'src'), { recursive: true });
|
|
60
|
+
await writeFile(join(dir, 'src', 'index.ts'), 'export {};');
|
|
61
|
+
await writeFile(join(dir, 'src', 'config.ts'), 'export default () => undefined;');
|
|
62
|
+
const plugin = appTestVitePlugin();
|
|
63
|
+
const configResolved = plugin.configResolved as ConfigResolvedHook;
|
|
64
|
+
const load = plugin.load as LoadHook;
|
|
65
|
+
|
|
66
|
+
configResolved({ root: dir });
|
|
67
|
+
|
|
68
|
+
expect(await load('\0virtual:fusion-app-test-env')).toContain('"appKey":"my-app"');
|
|
69
|
+
expect(await load('\0virtual:fusion-app-test-configure')).toBe(
|
|
70
|
+
`export { default as configure } from ${JSON.stringify(join(dir, 'src', 'config.ts'))};`,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('re-exports the default candidate config file as the configure virtual module', async () => {
|
|
75
|
+
await mkdir(join(dir, 'src'), { recursive: true });
|
|
76
|
+
await writeFile(join(dir, 'src', 'config.ts'), 'export default () => undefined;');
|
|
77
|
+
const plugin = appTestVitePlugin({ entrypoint: dir });
|
|
78
|
+
const load = plugin.load as LoadHook;
|
|
79
|
+
|
|
80
|
+
const source = await load('\0virtual:fusion-app-test-configure');
|
|
81
|
+
|
|
82
|
+
expect(source).toBe(
|
|
83
|
+
`export { default as configure } from ${JSON.stringify(join(dir, 'src', 'config.ts'))};`,
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('re-exports an explicitly requested configure file', async () => {
|
|
88
|
+
await writeFile(join(dir, 'my-config.ts'), 'export default () => undefined;');
|
|
89
|
+
const plugin = appTestVitePlugin({ entrypoint: dir, configure: 'my-config.ts' });
|
|
90
|
+
const load = plugin.load as LoadHook;
|
|
91
|
+
|
|
92
|
+
const source = await load('\0virtual:fusion-app-test-configure');
|
|
93
|
+
|
|
94
|
+
expect(source).toBe(
|
|
95
|
+
`export { default as configure } from ${JSON.stringify(join(dir, 'my-config.ts'))};`,
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('throws when an explicitly requested configure file does not exist', () => {
|
|
100
|
+
expect(() => appTestVitePlugin({ entrypoint: dir, configure: 'missing.ts' })).toThrow();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('returns null for module ids other than its own virtual specifiers', async () => {
|
|
104
|
+
const plugin = appTestVitePlugin({ entrypoint: dir });
|
|
105
|
+
const load = plugin.load as LoadHook;
|
|
106
|
+
|
|
107
|
+
expect(await load('some-other-module')).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { resolveAppTestEnv } from '../resolve-app-test-env.js';
|
|
8
|
+
|
|
9
|
+
describe('resolveAppTestEnv', () => {
|
|
10
|
+
let dir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
dir = await mkdtemp(join(tmpdir(), 'fusion-vitest-plugin-react-app-'));
|
|
14
|
+
await writeFile(
|
|
15
|
+
join(dir, 'package.json'),
|
|
16
|
+
JSON.stringify({ name: '@equinor/my-app', version: '1.2.3', description: 'a test app' }),
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await rm(dir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('falls back to a package-derived manifest and an empty config with no local files', async () => {
|
|
25
|
+
const { manifest, config } = await resolveAppTestEnv({ entrypoint: dir });
|
|
26
|
+
|
|
27
|
+
expect(manifest).toMatchObject({
|
|
28
|
+
appKey: 'my-app',
|
|
29
|
+
displayName: '@equinor/my-app',
|
|
30
|
+
description: 'a test app',
|
|
31
|
+
type: 'standalone',
|
|
32
|
+
});
|
|
33
|
+
expect(config).toEqual({ environment: {} });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('merges a local app.manifest.ts and app.config.ts, same as ffc app build', async () => {
|
|
37
|
+
await writeFile(
|
|
38
|
+
join(dir, 'app.manifest.ts'),
|
|
39
|
+
"export default { displayName: 'My Custom App' };",
|
|
40
|
+
);
|
|
41
|
+
await writeFile(
|
|
42
|
+
join(dir, 'app.config.ts'),
|
|
43
|
+
"export default { environment: {}, endpoints: { api: { url: 'https://example.com' } } };",
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const { manifest, config } = await resolveAppTestEnv({ entrypoint: dir });
|
|
47
|
+
|
|
48
|
+
expect(manifest).toMatchObject({ appKey: 'my-app', displayName: 'My Custom App' });
|
|
49
|
+
expect(config).toEqual({
|
|
50
|
+
environment: {},
|
|
51
|
+
endpoints: { api: { url: 'https://example.com', scopes: [] } },
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('applies an inline manifest/config function directly, same as a loaded file default export', async () => {
|
|
56
|
+
const { manifest, config } = await resolveAppTestEnv({
|
|
57
|
+
entrypoint: dir,
|
|
58
|
+
manifest: () => ({ displayName: 'Inline App' }),
|
|
59
|
+
config: () => ({
|
|
60
|
+
environment: {},
|
|
61
|
+
endpoints: { api: { url: 'https://inline.example.com' } },
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(manifest).toMatchObject({ appKey: 'my-app', displayName: 'Inline App' });
|
|
66
|
+
expect(config).toEqual({
|
|
67
|
+
environment: {},
|
|
68
|
+
endpoints: { api: { url: 'https://inline.example.com', scopes: [] } },
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('propagates an error for an explicitly requested manifest file that does not exist', async () => {
|
|
73
|
+
await expect(
|
|
74
|
+
resolveAppTestEnv({ entrypoint: dir, manifest: 'does-not-exist.manifest.ts' }),
|
|
75
|
+
).rejects.toThrow();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('propagates an error for an explicitly requested config file that does not exist', async () => {
|
|
79
|
+
await expect(
|
|
80
|
+
resolveAppTestEnv({ entrypoint: dir, config: 'does-not-exist.config.ts' }),
|
|
81
|
+
).rejects.toThrow();
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/app-test.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
renderAppHook,
|
|
3
|
+
type RenderAppHookOptions,
|
|
4
|
+
type RenderAppHookResult,
|
|
5
|
+
} from './render-app-hook';
|
|
6
|
+
export {
|
|
7
|
+
renderAppComponent,
|
|
8
|
+
type RenderAppComponentOptions,
|
|
9
|
+
type RenderAppComponentResult,
|
|
10
|
+
} from './render-app-component';
|
|
11
|
+
export { testApp } from './test-app';
|
|
12
|
+
export type { AppMockConfigureFn } from '@equinor/fusion-framework-app/mock';
|
|
13
|
+
|
|
14
|
+
// `test`/`render` import virtual modules only served once `appTestVitePlugin`
|
|
15
|
+
// (@equinor/fusion-framework-vitest-plugin-react-app) is registered — using any export from
|
|
16
|
+
// this module requires the plugin registered in your `vitest.config.ts` `plugins`.
|
|
17
|
+
export { test } from './test';
|
|
18
|
+
export { render } from './render';
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { playwright } from '@vitest/browser-playwright';
|
|
2
|
+
import {
|
|
3
|
+
defineProject as defineVitestProject,
|
|
4
|
+
mergeConfig,
|
|
5
|
+
type UserWorkspaceConfig,
|
|
6
|
+
} from 'vitest/config';
|
|
7
|
+
|
|
8
|
+
import { appTestVitePlugin } from './index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A config override for {@link defineProject}: an object is deep-merged onto the default
|
|
12
|
+
* config via Vite's own `mergeConfig`; a function receives the default config and returns the
|
|
13
|
+
* config used outright, for changes `mergeConfig` can't express (e.g. removing a field).
|
|
14
|
+
*/
|
|
15
|
+
export type AppTestConfigOverride =
|
|
16
|
+
| Partial<UserWorkspaceConfig>
|
|
17
|
+
| ((config: UserWorkspaceConfig) => UserWorkspaceConfig);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Builds a ready-to-export Vitest project config for testing a Fusion Framework React app:
|
|
21
|
+
* registers {@link appTestVitePlugin} (with its own defaults) and wires up Vitest browser mode
|
|
22
|
+
* with the Playwright/`chromium` provider, so a consuming app's own `vitest.config.ts` needs no
|
|
23
|
+
* browser-provider boilerplate of its own.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* Drop-in replacement for Vitest's own `defineProject` — same `export default`, just pre-wired.
|
|
27
|
+
* Playwright/`chromium` is only the *default* — pass `override` to change or replace anything
|
|
28
|
+
* (e.g. `test.name`, or swap `test.browser.provider` for a different `@vitest/browser-*`
|
|
29
|
+
* provider) without hand-rolling `appTestVitePlugin`'s own wiring.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { defineProject } from '@equinor/fusion-framework-vitest-plugin-react-app/config';
|
|
34
|
+
* import { name, version } from './package.json' with { type: 'json' };
|
|
35
|
+
*
|
|
36
|
+
* export default defineProject({ test: { name: `${name}@${version}` } });
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* Overriding the browser provider:
|
|
41
|
+
* ```ts
|
|
42
|
+
* import { webdriverio } from '@vitest/browser-webdriverio';
|
|
43
|
+
*
|
|
44
|
+
* export default defineProject({ test: { browser: { provider: webdriverio() } } });
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @param override - Deep-merged onto the default config (a plain object), or applied to it
|
|
48
|
+
* outright (a function receiving the default config); see {@link AppTestConfigOverride}.
|
|
49
|
+
* @returns A Vitest project config, ready to `export default`.
|
|
50
|
+
*/
|
|
51
|
+
export const defineProject = (override?: AppTestConfigOverride): UserWorkspaceConfig => {
|
|
52
|
+
const defaults: UserWorkspaceConfig = {
|
|
53
|
+
plugins: [appTestVitePlugin()],
|
|
54
|
+
test: {
|
|
55
|
+
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
|
56
|
+
browser: {
|
|
57
|
+
enabled: true,
|
|
58
|
+
provider: playwright(),
|
|
59
|
+
headless: true,
|
|
60
|
+
instances: [{ browser: 'chromium' }],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
// pre-transforms all source up front so deps only reached via lazy/code-split imports
|
|
64
|
+
// (e.g. route components) are discovered before the first test request, not mid-run —
|
|
65
|
+
// the latter forces Vite to reload the page and fails the in-flight test file import
|
|
66
|
+
server: { warmup: { clientFiles: ['src/**/*.{ts,tsx}'] } },
|
|
67
|
+
// same reasoning as `server.warmup` above, but for the esbuild dep scanner: without this,
|
|
68
|
+
// its default entry detection can miss code-split route files entirely, so a package only
|
|
69
|
+
// ever imported from one of those (e.g. react-router's own deps) is discovered mid-run
|
|
70
|
+
// instead of up front — statically crawling every source file (which esbuild's scanner
|
|
71
|
+
// follows through dynamic imports too) avoids that with no per-package name needed
|
|
72
|
+
optimizeDeps: { entries: ['src/**/*.{ts,tsx}'] },
|
|
73
|
+
};
|
|
74
|
+
// a function replaces the config outright; a plain object deep-merges onto it via Vite's own mergeConfig
|
|
75
|
+
const resolved =
|
|
76
|
+
typeof override === 'function'
|
|
77
|
+
? override(defaults)
|
|
78
|
+
: override
|
|
79
|
+
? (mergeConfig(defaults, override) as UserWorkspaceConfig)
|
|
80
|
+
: defaults;
|
|
81
|
+
return defineVitestProject(resolved);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default defineProject;
|