@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/LICENSE +21 -0
  3. package/README.md +349 -0
  4. package/dist/esm/app-test.js +9 -0
  5. package/dist/esm/app-test.js.map +1 -0
  6. package/dist/esm/define-project.js +68 -0
  7. package/dist/esm/define-project.js.map +1 -0
  8. package/dist/esm/index.js +99 -0
  9. package/dist/esm/index.js.map +1 -0
  10. package/dist/esm/render-app-component.js +52 -0
  11. package/dist/esm/render-app-component.js.map +1 -0
  12. package/dist/esm/render-app-hook.js +63 -0
  13. package/dist/esm/render-app-hook.js.map +1 -0
  14. package/dist/esm/render.js +49 -0
  15. package/dist/esm/render.js.map +1 -0
  16. package/dist/esm/resolve-app-test-env.js +85 -0
  17. package/dist/esm/resolve-app-test-env.js.map +1 -0
  18. package/dist/esm/scope/create-app-scope-wrapper.js +15 -0
  19. package/dist/esm/scope/create-app-scope-wrapper.js.map +1 -0
  20. package/dist/esm/scope/default-app-env.js +12 -0
  21. package/dist/esm/scope/default-app-env.js.map +1 -0
  22. package/dist/esm/scope/index.js +5 -0
  23. package/dist/esm/scope/index.js.map +1 -0
  24. package/dist/esm/scope/resolve-app-scope.js +21 -0
  25. package/dist/esm/scope/resolve-app-scope.js.map +1 -0
  26. package/dist/esm/scope/resolve-fusion.js +18 -0
  27. package/dist/esm/scope/resolve-fusion.js.map +1 -0
  28. package/dist/esm/test-app.js +59 -0
  29. package/dist/esm/test-app.js.map +1 -0
  30. package/dist/esm/test.js +33 -0
  31. package/dist/esm/test.js.map +1 -0
  32. package/dist/esm/version.js +3 -0
  33. package/dist/esm/version.js.map +1 -0
  34. package/dist/tsconfig.tsbuildinfo +1 -0
  35. package/dist/types/app-test.d.ts +6 -0
  36. package/dist/types/define-project.d.ts +41 -0
  37. package/dist/types/index.d.ts +45 -0
  38. package/dist/types/render-app-component.d.ts +86 -0
  39. package/dist/types/render-app-hook.d.ts +99 -0
  40. package/dist/types/render.d.ts +37 -0
  41. package/dist/types/resolve-app-test-env.d.ts +56 -0
  42. package/dist/types/scope/create-app-scope-wrapper.d.ts +14 -0
  43. package/dist/types/scope/default-app-env.d.ts +5 -0
  44. package/dist/types/scope/index.d.ts +4 -0
  45. package/dist/types/scope/resolve-app-scope.d.ts +31 -0
  46. package/dist/types/scope/resolve-fusion.d.ts +13 -0
  47. package/dist/types/test-app.d.ts +68 -0
  48. package/dist/types/test.d.ts +66 -0
  49. package/dist/types/version.d.ts +1 -0
  50. package/docs/advanced.md +140 -0
  51. package/docs/configuration.md +109 -0
  52. package/docs/getting-started.md +66 -0
  53. package/docs/migrating-an-existing-app.md +197 -0
  54. package/docs/module-mocks.md +120 -0
  55. package/docs/overview.md +49 -0
  56. package/docs/troubleshooting.md +64 -0
  57. package/docs/why-browser-mode.md +113 -0
  58. package/package.json +87 -0
  59. package/src/__tests__/app-test-vite-plugin.test.ts +109 -0
  60. package/src/__tests__/resolve-app-test-env.test.ts +83 -0
  61. package/src/app-test.ts +18 -0
  62. package/src/define-project.ts +84 -0
  63. package/src/index.ts +124 -0
  64. package/src/render-app-component.tsx +116 -0
  65. package/src/render-app-hook.tsx +135 -0
  66. package/src/render.tsx +64 -0
  67. package/src/resolve-app-test-env.ts +132 -0
  68. package/src/scope/create-app-scope-wrapper.tsx +26 -0
  69. package/src/scope/default-app-env.ts +13 -0
  70. package/src/scope/index.ts +4 -0
  71. package/src/scope/resolve-app-scope.ts +46 -0
  72. package/src/scope/resolve-fusion.ts +29 -0
  73. package/src/test-app.tsx +72 -0
  74. package/src/test.tsx +35 -0
  75. package/src/version.ts +2 -0
  76. package/src/virtual-modules.d.ts +12 -0
  77. package/tsconfig.json +24 -0
  78. package/vitest.config.ts +10 -0
@@ -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
+ });
@@ -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;