@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,104 @@
1
+ # @equinor/fusion-framework-vitest-plugin-react-app
2
+
3
+ ## 0.2.0-next.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2836e0b: Add a `/config` entry-point exporting `defineProject`: a drop-in for Vitest's own `defineProject`, pre-wired with `appTestVitePlugin` and the `@vitest/browser-playwright`/`chromium` browser provider, so a consuming app's own `vitest.config.ts` needs no browser-provider boilerplate.
8
+
9
+ ```ts
10
+ import { defineProject } from "@equinor/fusion-framework-vitest-plugin-react-app/config";
11
+ import { name, version } from "./package.json" with { type: "json" };
12
+
13
+ export default defineProject({ test: { name: `${name}@${version}` } });
14
+ ```
15
+
16
+ `override` is deep-merged onto the default config via Vite's own `mergeConfig` (a plain object), or applied to the default config outright (a function receiving it) — for changes `mergeConfig` can't express, such as swapping `test.browser.provider` for a different `@vitest/browser-*` provider.
17
+
18
+ `@vitest/browser-playwright` and `playwright` are now explicit peer dependencies of the package.
19
+
20
+ Also fixes `appTestVitePlugin`'s `entrypoint` inference: when no `entrypoint` option is given, it now resolves from the Vitest project's own root (via `configResolved`) instead of `process.cwd()`, so each cookbook's `vitest.config.ts` no longer needs to pass `entrypoint` explicitly when run as part of a multi-project Vitest run.
21
+
22
+ - 2836e0b: Add `@equinor/fusion-framework-vitest-plugin-react-app`: Vitest/`vitest-browser-react` helpers for testing a Fusion Framework React application inside a real, mock-backed application module scope — the same `FrameworkProvider` + `ModuleProvider` nesting `renderApp`/`createComponent` wire up in production, backed by `mockFramework` and `mockAppModules` (`@equinor/fusion-framework-app/mock`).
23
+
24
+ ```tsx
25
+ import { renderAppHook } from "@equinor/fusion-framework-vitest-plugin-react-app";
26
+ import { useAccessToken } from "@equinor/fusion-framework-react-app/msal";
27
+
28
+ const { result } = await renderAppHook(() =>
29
+ useAccessToken({ scopes: ["User.Read"] }),
30
+ );
31
+ await vi.waitFor(() => expect(result.current.pending).toBe(false));
32
+ ```
33
+
34
+ Highlights:
35
+
36
+ - `renderAppHook`/`renderAppComponent` — render a hook or component against the real `event`/`http`/`msal` module pipeline, with only the network boundary faked; the result carries a nested `fusion: { framework, app }` for driving a module directly after the initial render.
37
+ - `testApp` — a `vitest` `test` extended with `env`/`configure`/`app`/`render`/`renderHook` fixtures, for a test file whose cases share one mocked scope.
38
+ - `appTestVitePlugin` — a Vite plugin resolving an application's own manifest, config, and module-configurator (the same pipeline `ffc app build`/`ffc app dev` use) as virtual modules.
39
+ - A `/test` entry-point exporting `test`/`render`, pre-seeded from the resolved manifest/config/configure once `appTestVitePlugin` is registered — no per-test `env`/`configure` wiring.
40
+
41
+ Package documentation provides task-focused guides for setup, configuration, advanced fixtures,
42
+ module mocks, and troubleshooting so npm, GitHub, and retrieval clients use the same canonical
43
+ source as the VuePress site.
44
+
45
+ ### Patch Changes
46
+
47
+ - e8aae1f: Internal: publish every package on the `next` pre-release tag so the whole framework can be installed as a coherent set.
48
+
49
+ Packages without their own changes are bumped only to receive a `-next.N` version and the `next` dist-tag on npm. Install with:
50
+
51
+ ```bash
52
+ pnpm add @equinor/fusion-framework-react-app@next
53
+ ```
54
+
55
+ - 2836e0b: Fix `defineProject` not pre-transforming lazily/code-split-imported source (e.g. route
56
+ components reached only through dynamic `import()`), which could force Vite to reload the
57
+ page mid-test and fail the in-flight test file's import.
58
+
59
+ `defineProject`'s Vite config now sets `server.warmup.clientFiles: ['src/**/*.{ts,tsx}']`, so
60
+ all source under `src/` is transformed up front instead of on first request.
61
+
62
+ - 2836e0b: Document `test.override(...)` for overriding a fixture (`configure`, `fusion`, etc.) within a
63
+ single test file or `describe` block, alongside the existing `test.extend(...)` guide, in the
64
+ package's "Advanced usage" doc (`docs/advanced.md`).
65
+ - 2836e0b: Add a "Migrate an existing app" guide (`docs/migrating-an-existing-app.md`) covering how to
66
+ move a suite from `@testing-library/react`/jsdom/happy-dom onto this package: installing
67
+ Browser Mode, replacing hand-rolled `vi.mock`s of Fusion hooks with each module's `enable*Mock`
68
+ entry point, porting a hand-written mock HTTP server to `createRouterMiddleware`, composing a
69
+ router fixture with a domain-state fixture in one file, and updating renders/assertions to the
70
+ async `vitest-browser-react` API. Linked from the package README and from
71
+ `docs/overview.md` and `testing-choosing-a-layer.md`.
72
+ - 2836e0b: Add a "Why Browser Mode is the default" guide (`docs/why-browser-mode.md`) explaining
73
+ the rationale for defaulting to real Chromium over DOM emulation (the documented React 19 peer
74
+ dependency crash in the previous renderer, plus the framework's real-module testing philosophy),
75
+ the actual performance tradeoff (a real browser is slower, not faster — this was a fidelity
76
+ choice, not a speed optimization), and how to bring your own renderer for a single test file by
77
+ composing `mockFramework`/`mockAppModules`/`FrameworkProvider`/`ModuleProvider` directly with a
78
+ different render function (e.g. `@testing-library/react` on `happy-dom`). Linked from the
79
+ package README and `docs/overview.md`.
80
+ - Updated dependencies [e8aae1f]
81
+ - Updated dependencies [2836e0b]
82
+ - Updated dependencies [2836e0b]
83
+ - Updated dependencies [2836e0b]
84
+ - Updated dependencies [2836e0b]
85
+ - Updated dependencies [2836e0b]
86
+ - Updated dependencies [2836e0b]
87
+ - Updated dependencies [2836e0b]
88
+ - Updated dependencies [2836e0b]
89
+ - Updated dependencies [2836e0b]
90
+ - Updated dependencies [2836e0b]
91
+ - Updated dependencies [2836e0b]
92
+ - Updated dependencies [2836e0b]
93
+ - Updated dependencies [2836e0b]
94
+ - Updated dependencies [2836e0b]
95
+ - Updated dependencies [2836e0b]
96
+ - Updated dependencies [2836e0b]
97
+ - @equinor/fusion-framework@8.1.0-next.0
98
+ - @equinor/fusion-framework-app@14.0.0-next.0
99
+ - @equinor/fusion-framework-cli@15.2.8-next.0
100
+ - @equinor/fusion-framework-module@6.1.3-next.0
101
+ - @equinor/fusion-framework-module-app@8.1.0-next.0
102
+ - @equinor/fusion-framework-react@9.0.0-next.0
103
+ - @equinor/fusion-framework-react-module@4.0.3-next.0
104
+ - @equinor/fusion-imports@2.0.3-next.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Equinor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,349 @@
1
+ # @equinor/fusion-framework-vitest-plugin-react-app
2
+
3
+ Vite plugin and Vitest helpers for testing Fusion Framework React applications inside a real,
4
+ mock-backed application module scope — the same `FrameworkProvider` + `ModuleProvider` nesting
5
+ `renderApp`/`createComponent` wire up in production, with the real `event`/`http`/`msal` module
6
+ pipeline and only the network boundary faked.
7
+
8
+ > **Note:** Requires `vitest-browser-react` (peer dependency) and Vitest's
9
+ > [Browser Mode](https://vitest.dev/guide/browser/) configured for the project — these render
10
+ > helpers mount real components in a real browser, not `happy-dom`/`jsdom`.
11
+
12
+ Use this package when you need to:
13
+
14
+ - **Render a component or hook against real application modules** — `useAppModule`,
15
+ `useAccessToken`, `useCurrentContext`, `useCurrentBookmark`, `useAppSetting`, and similar
16
+ hooks resolve for real, instead of being hand-mocked per test.
17
+ - **Test against the application's own manifest, config, and module-configurator** — the same
18
+ resolution pipeline `ffc app build`/`ffc app dev` use, served as virtual modules by
19
+ `appTestVitePlugin`.
20
+ - **Share seeded fixture defaults across a test file** — `testApp`, a `vitest` `test` extended
21
+ with `env`/`configure`/`app`/`render`/`renderHook` fixtures, instead of repeating options on
22
+ every call. Each test still gets its own fresh `fusion`/`app` instances; only the seeded
23
+ defaults are shared, not state between tests.
24
+
25
+ ## Installation
26
+
27
+ ```sh
28
+ pnpm add -D vitest playwright @vitest/browser-playwright vitest-browser-react \
29
+ @equinor/fusion-framework-vitest-plugin-react-app
30
+ pnpm exec playwright install chromium
31
+ ```
32
+
33
+ React, React DOM, RxJS, and Vite are peer dependencies normally supplied by the app.
34
+
35
+ ## Documentation
36
+
37
+ | Task | Guide |
38
+ | --- | --- |
39
+ | Understand the package and choose a subject | [Overview](docs/overview.md) |
40
+ | Install, configure, and run the first app test | [Getting started](docs/getting-started.md) |
41
+ | Override Vitest defaults or resolve non-standard app files | [Configuration](docs/configuration.md) |
42
+ | Compose fixtures, explicit render options, and app lifecycle tests | [Advanced usage](docs/advanced.md) |
43
+ | Diagnose setup, browser, app-resolution, and network failures | [Troubleshooting](docs/troubleshooting.md) |
44
+ | Seed authentication, context, bookmarks, feature flags, HTTP, analytics, and telemetry | [Module mocks](docs/module-mocks.md) |
45
+ | Understand the Browser Mode default or choose another renderer | [Why Browser Mode is the default](docs/why-browser-mode.md) |
46
+ | Move an existing `@testing-library/react`/jsdom suite onto this package | [Migrate an existing app](docs/migrating-an-existing-app.md) |
47
+ | Choose between app, framework, module, HTTP, and runner-level tests | [Choose a testing layer](../../framework/docs/testing-choosing-a-layer.md) |
48
+
49
+ ## Overview
50
+
51
+ Any hook or component that reads from the application module scope — `useAppModule`,
52
+ `useAccessToken`, `useCurrentContext`, `useCurrentBookmark`, `useAppSetting`, and so on — or
53
+ from the parent framework via `useFramework`, needs to run inside the same provider nesting
54
+ `renderApp`/`createComponent` wire up in production: a `FrameworkProvider` (the parent Fusion
55
+ instance) around a `ModuleProvider` (the app's own modules).
56
+
57
+ `renderAppHook` and `renderAppComponent` build that nesting for you, using `mockFramework`
58
+ (`@equinor/fusion-framework/mock`) and `mockAppModules` (`@equinor/fusion-framework-app/mock`) —
59
+ the **real** `event`/`http`/`msal` module pipeline. Only requests a seeded middleware answers
60
+ are faked; a request with no matching middleware still reaches the real network. This means:
61
+
62
+ - MSAL signs in a default "Test User" with zero configuration — `useAccessToken`/`useToken`
63
+ resolve a real, structurally-valid (unsigned) JWT out of the box.
64
+ - `event` dispatches and listens for real `FrameworkEvent`s.
65
+ - Anything that talks to an HTTP endpoint (e.g. an app's own manifest, settings) is answered
66
+ by a mock client rather than a real network call — see each hook's own docs for what's
67
+ pre-wired versus what you need to seed yourself (e.g. via `enableBookmarkMock`,
68
+ `enableContextMock`, `enableFeatureFlagMock`, or a `configurator.http.addMiddleware(...)`
69
+ router).
70
+
71
+ Use `renderAppHook` for a hook in isolation; use `renderAppComponent` when you need to assert
72
+ on rendered output (e.g. loading/error states, DOM structure); use `testApp` or the `/test`
73
+ entry-point's `test`/`render` when several cases in a file share one seeded scope.
74
+
75
+ ## Entry points
76
+
77
+ - **`@equinor/fusion-framework-vitest-plugin-react-app`** — `appTestVitePlugin`,
78
+ `renderAppComponent`, `renderAppHook`, `testApp`. No app-specific resolution; you pass
79
+ `env`/`configure` yourself.
80
+ - **`@equinor/fusion-framework-vitest-plugin-react-app/test`** — `test`, `render`. Require
81
+ `appTestVitePlugin` registered in `vitest.config.ts`; `env`/`configure` are resolved
82
+ automatically from the application's own manifest, config, and module-configurator.
83
+ - **`@equinor/fusion-framework-vitest-plugin-react-app/config`** — `defineProject`. Registers
84
+ `appTestVitePlugin`, headless Playwright/Chromium, test-file inclusion, and lazy-import
85
+ warmup while accepting ordinary Vitest configuration overrides.
86
+
87
+ ## Quick start
88
+
89
+ Use the browser-ready project config in your app's `vitest.config.ts`:
90
+
91
+ ```ts
92
+ import { defineProject } from '@equinor/fusion-framework-vitest-plugin-react-app/config';
93
+
94
+ export default defineProject();
95
+ ```
96
+
97
+ Then use the pre-seeded `test` fixture — no per-test `env`/`configure` wiring:
98
+
99
+ ```tsx
100
+ import { expect } from 'vitest';
101
+ import { test } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
102
+ import { App } from '../App';
103
+
104
+ test('renders the app', async ({ render }) => {
105
+ const screen = await render(<App />);
106
+ await expect.element(screen.getByRole('heading')).toBeVisible();
107
+ });
108
+ ```
109
+
110
+ Or, in a plain `describe`/`it` file, the equivalent `render` function:
111
+
112
+ ```tsx
113
+ import { describe, expect, it } from 'vitest';
114
+ import { render } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
115
+ import { App } from '../App';
116
+
117
+ describe('App', () => {
118
+ it('renders the app', async () => {
119
+ const { getByRole } = await render(<App />);
120
+ await expect.element(getByRole('heading')).toBeVisible();
121
+ });
122
+ });
123
+ ```
124
+
125
+ ## renderAppHook
126
+
127
+ Renders a hook inside a real, mock-backed application module scope.
128
+
129
+ **Signature:**
130
+
131
+ ```ts
132
+ function renderAppHook<Result, Props = undefined, TModules = unknown, TEnv extends AppEnv = AppEnv>(
133
+ render: (initialProps?: Props) => Result,
134
+ options?: {
135
+ configure?: AppMockConfigureFn<TModules, TEnv>;
136
+ env?: TEnv;
137
+ fusion?: Fusion;
138
+ } & Omit<RenderHookOptions<Props>, 'wrapper'>,
139
+ ): Promise<RenderHookResult<Result, Props> & { fusion: { framework: Fusion; app: AppModulesInstance<TModules> } }>;
140
+ ```
141
+
142
+ | Option | Description |
143
+ | ----------- | -------------------------------------------------------------------------------------------------------------------|
144
+ | `configure` | Callback forwarded to `mockAppModules` for seeding app-scope modules (bookmark, context, feature-flag, msal, etc.) |
145
+ | `env` | The application environment (manifest); defaults to a generic standalone `test-app` |
146
+ | `fusion` | The parent Fusion instance; defaults to a fresh `mockFramework` instance serving this app's own manifest |
147
+
148
+ Any other `renderHook` option (e.g. `initialProps`) is forwarded as-is. The result carries the
149
+ usual `renderHook` return values (`result`, `rerender`, `unmount`) plus `fusion` — the same
150
+ instances the hook rendered against — for driving a module the hook itself doesn't return.
151
+
152
+ ### Basic usage
153
+
154
+ ```tsx
155
+ import { renderAppHook } from '@equinor/fusion-framework-vitest-plugin-react-app';
156
+ import { useAccessToken } from '@equinor/fusion-framework-react-app/msal';
157
+
158
+ test('resolves an access token', async () => {
159
+ const { result } = await renderAppHook(() => useAccessToken({ scopes: ['User.Read'] }));
160
+ await vi.waitFor(() => expect(result.current.pending).toBe(false));
161
+ expect(result.current.token).toBeDefined();
162
+ });
163
+ ```
164
+
165
+ ### Sign in a named user
166
+
167
+ Pass `configure` to reach the msal mock's builder before the hook renders:
168
+
169
+ ```tsx
170
+ import { renderAppHook } from '@equinor/fusion-framework-vitest-plugin-react-app';
171
+ import { useCurrentAccount } from '@equinor/fusion-framework-react-app/msal';
172
+
173
+ test('reads the configured account', async () => {
174
+ const { result } = await renderAppHook(() => useCurrentAccount(), {
175
+ configure: (configurator) => configurator.msal.setAccount({ name: 'Ada Lovelace' }),
176
+ });
177
+ expect(result.current).toMatchObject({ name: 'Ada Lovelace' });
178
+ });
179
+ ```
180
+
181
+ ### Reuse a pre-built parent Fusion instance
182
+
183
+ Build the `fusion` instance yourself when a test needs to pre-configure parent-level modules
184
+ (`http`, `context`, `serviceDiscovery`) or share one instance across multiple render calls:
185
+
186
+ ```tsx
187
+ import { mockFramework } from '@equinor/fusion-framework/mock';
188
+ import { enableAppManifestMock } from '@equinor/fusion-framework-app/mock';
189
+ import type { AppModule } from '@equinor/fusion-framework-module-app';
190
+ import { renderAppHook } from '@equinor/fusion-framework-vitest-plugin-react-app';
191
+ import { useAccessToken, useCurrentAccount } from '@equinor/fusion-framework-react-app/msal';
192
+
193
+ const env = { manifest: { appKey: 'test-app', displayName: 'Test App', description: '', type: 'standalone' as const } };
194
+
195
+ test('shares one fusion instance across two renders', async () => {
196
+ const fusion = await mockFramework<[AppModule]>((configurator) =>
197
+ enableAppManifestMock(configurator, env),
198
+ );
199
+
200
+ const { result: a } = await renderAppHook(() => useAccessToken({ scopes: ['User.Read'] }), { fusion });
201
+ const { result: b } = await renderAppHook(() => useCurrentAccount(), { fusion });
202
+ });
203
+ ```
204
+
205
+ ## renderAppComponent
206
+
207
+ Renders a component inside the same real, mock-backed application module scope as
208
+ `renderAppHook`. Use this when you need to assert on rendered output rather than a hook's
209
+ return value — e.g. a component with its own loading/error states.
210
+
211
+ **Signature:**
212
+
213
+ ```ts
214
+ function renderAppComponent<TModules = unknown, TEnv extends AppEnv = AppEnv>(
215
+ ui: ReactElement,
216
+ options?: {
217
+ configure?: AppMockConfigureFn<TModules, TEnv>;
218
+ env?: TEnv;
219
+ fusion?: Fusion;
220
+ } & Omit<RenderOptions, 'wrapper'>,
221
+ ): Promise<RenderResult & { fusion: { framework: Fusion; app: AppModulesInstance<TModules> } }>;
222
+ ```
223
+
224
+ Options are the same shape as `renderAppHook`'s — `configure`, `env`, `fusion` — plus any other
225
+ `vitest-browser-react` `render` option. The result carries the usual `render` return values
226
+ (`getByText`, `container`, `unmount`, ...) plus `fusion` — nested rather than spread directly
227
+ onto the result, so `vitest-browser-react`'s own return shape stays free to evolve without ever
228
+ colliding with it. `fusion.app` is the same application module instance the rendered component
229
+ reads through `useAppModule`/`useAppModules`, and `fusion.framework` is the parent Fusion
230
+ instance. Drive a module directly through `fusion.app` to exercise a state change after the
231
+ initial render, without hand-wiring `mockAppModules`/`ModuleProvider`:
232
+
233
+ ```tsx
234
+ import { enableContextMock } from '@equinor/fusion-framework-module-context/mock';
235
+ import type { ContextModule } from '@equinor/fusion-framework-module-context';
236
+ import { act } from 'react';
237
+ import { renderAppComponent } from '@equinor/fusion-framework-vitest-plugin-react-app';
238
+
239
+ test('reacts when the current context switches', async () => {
240
+ const { getByText, fusion } = await renderAppComponent<[ContextModule]>(<App />, {
241
+ configure: (configurator) =>
242
+ enableContextMock(configurator, (mock) => mock.setCurrentContext(projectA)),
243
+ });
244
+
245
+ await act(() => fusion.app.context.setCurrentContextByIdAsync(projectB.id));
246
+ await expect.element(getByText(/project-b/)).toBeVisible();
247
+ });
248
+ ```
249
+
250
+ ### Example: asserting loading and error states
251
+
252
+ ```tsx
253
+ import { mockFramework } from '@equinor/fusion-framework/mock';
254
+ import { enableAppManifestMock } from '@equinor/fusion-framework-app/mock';
255
+ import type { AppManifest, AppModule } from '@equinor/fusion-framework-module-app';
256
+ import { renderAppComponent } from '@equinor/fusion-framework-vitest-plugin-react-app';
257
+ import { Apploader } from '@equinor/fusion-framework-react-app/apploader';
258
+
259
+ test('mounts the child app once its script loads', async () => {
260
+ const manifest: AppManifest = {
261
+ appKey: 'child-app',
262
+ displayName: 'Child App',
263
+ description: 'A child application',
264
+ type: 'standalone',
265
+ build: { version: '1.0.0', entryPoint: 'child-script.ts', assetPath: '' },
266
+ };
267
+ const fusion = await mockFramework<[AppModule]>((configurator) =>
268
+ enableAppManifestMock(configurator, { manifest }, someFixturesUri),
269
+ );
270
+
271
+ const { container } = await renderAppComponent(<Apploader appKey="child-app" />, { fusion });
272
+
273
+ // the loading state renders synchronously, before the script's dynamic import resolves
274
+ expect(container.textContent).toContain('Loading child-app');
275
+ await vi.waitFor(() => expect(container.textContent).toContain('mounted: child-app'));
276
+ });
277
+ ```
278
+
279
+ ## testApp
280
+
281
+ `vitest`'s `test`, extended with an application module scope fixture — an alternative to
282
+ `renderAppComponent`/`renderAppHook` for a test file whose cases share one mocked scope:
283
+ `env`/`configure` become suite-level concerns, overridden once per file (or per `describe`
284
+ block) with `testApp.extend(...)`, rather than an options object repeated on every call.
285
+ `fusion`/`app` resolve lazily — a test that only destructures `app` never pays for rendering
286
+ anything, and one that only destructures `render`/`renderHook` gets the same mocked scope
287
+ wired in automatically.
288
+
289
+ ```tsx
290
+ import { testApp } from '@equinor/fusion-framework-vitest-plugin-react-app';
291
+
292
+ testApp('resolves current context', async ({ app, render }) => {
293
+ const screen = await render(<App />);
294
+ expect(app.context).toBeDefined();
295
+ });
296
+ ```
297
+
298
+ Seed a module for every test in a suite:
299
+
300
+ ```tsx
301
+ describe('with a seeded context module', () => {
302
+ const test = testApp.extend('configure', { injected: true }, () =>
303
+ (configurator) => enableContextMock(configurator, (mock) => mock.setCurrentContext(projectA)),
304
+ );
305
+
306
+ test('starts on the seeded context', async ({ render }) => {
307
+ const screen = await render(<App />);
308
+ await expect.element(screen.getByText(projectA.title)).toBeVisible();
309
+ });
310
+ });
311
+ ```
312
+
313
+ ## appTestVitePlugin
314
+
315
+ A plain Vite plugin — Vitest configs are Vite configs, so this registers directly in your own
316
+ `vitest.config.ts`, no CLI command required. It serves an application's manifest/config
317
+ (resolved the same way `ffc app build`/`ffc app dev` do) and its own module-configurator
318
+ export as virtual modules, so the `/test` entry-point's `test`/`render` need no per-test
319
+ `env`/`configure` wiring.
320
+
321
+ ```ts
322
+ appTestVitePlugin({
323
+ entrypoint?: string; // defaults to the resolved Vite/Vitest project root
324
+ manifest?: string | AppManifestFn;
325
+ config?: string | AppConfigFn;
326
+ configure?: string; // defaults to the first of src/config.ts, src/config.tsx, src/config.js
327
+ });
328
+ ```
329
+
330
+ `entrypoint`, `manifest`, and `config` are forwarded to the same manifest/config resolution
331
+ `ffc app build`/`ffc app dev` use (a base manifest generated from `package.json`, merged with a
332
+ local `app.manifest.ts` if one exists, plus `app.config.ts` for endpoints). `configure` points
333
+ at the application's module-configurator file — unlike `manifest`/`config`, it must be a real
334
+ file on disk, since it's re-exported as live application code into the test bundle rather than
335
+ JSON-serialized data. An explicitly requested `manifest`/`config`/`configure` file that doesn't
336
+ exist throws `FileNotFoundError`; the convention-based lookups fail silently and fall back to
337
+ defaults instead.
338
+
339
+ ## Notes
340
+
341
+ - Every render/hook helper is `async` — always `await` it, since the mocked application module
342
+ scope (app manifest, module initialization) resolves asynchronously before the first render.
343
+ - Seeding a module beyond the default set (bookmark, context, feature-flag) requires its own
344
+ mock enabler passed through `configure` — e.g. `enableBookmarkMock`, `enableContextMock`,
345
+ `enableFeatureFlagMock` from that module's own `/mock` sub-path.
346
+ - `msal` ships enabled by default with a signed-in "Test User" — no `configure` needed unless
347
+ a test cares about a specific account or a signed-out state.
348
+ - Prefer these helpers over hand-wiring `mockFramework`, `mockAppModules`, `FrameworkProvider`,
349
+ and `ModuleProvider` directly in every test.
@@ -0,0 +1,9 @@
1
+ export { renderAppHook, } from './render-app-hook';
2
+ export { renderAppComponent, } from './render-app-component';
3
+ export { testApp } from './test-app';
4
+ // `test`/`render` import virtual modules only served once `appTestVitePlugin`
5
+ // (@equinor/fusion-framework-vitest-plugin-react-app) is registered — using any export from
6
+ // this module requires the plugin registered in your `vitest.config.ts` `plugins`.
7
+ export { test } from './test';
8
+ export { render } from './render';
9
+ //# sourceMappingURL=app-test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-test.js","sourceRoot":"","sources":["../../src/app-test.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,GAGd,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,kBAAkB,GAGnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAGrC,8EAA8E;AAC9E,4FAA4F;AAC5F,mFAAmF;AACnF,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1,68 @@
1
+ import { playwright } from '@vitest/browser-playwright';
2
+ import { defineProject as defineVitestProject, mergeConfig, } from 'vitest/config';
3
+ import { appTestVitePlugin } from './index.js';
4
+ /**
5
+ * Builds a ready-to-export Vitest project config for testing a Fusion Framework React app:
6
+ * registers {@link appTestVitePlugin} (with its own defaults) and wires up Vitest browser mode
7
+ * with the Playwright/`chromium` provider, so a consuming app's own `vitest.config.ts` needs no
8
+ * browser-provider boilerplate of its own.
9
+ *
10
+ * @remarks
11
+ * Drop-in replacement for Vitest's own `defineProject` — same `export default`, just pre-wired.
12
+ * Playwright/`chromium` is only the *default* — pass `override` to change or replace anything
13
+ * (e.g. `test.name`, or swap `test.browser.provider` for a different `@vitest/browser-*`
14
+ * provider) without hand-rolling `appTestVitePlugin`'s own wiring.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { defineProject } from '@equinor/fusion-framework-vitest-plugin-react-app/config';
19
+ * import { name, version } from './package.json' with { type: 'json' };
20
+ *
21
+ * export default defineProject({ test: { name: `${name}@${version}` } });
22
+ * ```
23
+ *
24
+ * @example
25
+ * Overriding the browser provider:
26
+ * ```ts
27
+ * import { webdriverio } from '@vitest/browser-webdriverio';
28
+ *
29
+ * export default defineProject({ test: { browser: { provider: webdriverio() } } });
30
+ * ```
31
+ *
32
+ * @param override - Deep-merged onto the default config (a plain object), or applied to it
33
+ * outright (a function receiving the default config); see {@link AppTestConfigOverride}.
34
+ * @returns A Vitest project config, ready to `export default`.
35
+ */
36
+ export const defineProject = (override) => {
37
+ const defaults = {
38
+ plugins: [appTestVitePlugin()],
39
+ test: {
40
+ include: ['src/**/*.{test,spec}.{ts,tsx}'],
41
+ browser: {
42
+ enabled: true,
43
+ provider: playwright(),
44
+ headless: true,
45
+ instances: [{ browser: 'chromium' }],
46
+ },
47
+ },
48
+ // pre-transforms all source up front so deps only reached via lazy/code-split imports
49
+ // (e.g. route components) are discovered before the first test request, not mid-run —
50
+ // the latter forces Vite to reload the page and fails the in-flight test file import
51
+ server: { warmup: { clientFiles: ['src/**/*.{ts,tsx}'] } },
52
+ // same reasoning as `server.warmup` above, but for the esbuild dep scanner: without this,
53
+ // its default entry detection can miss code-split route files entirely, so a package only
54
+ // ever imported from one of those (e.g. react-router's own deps) is discovered mid-run
55
+ // instead of up front — statically crawling every source file (which esbuild's scanner
56
+ // follows through dynamic imports too) avoids that with no per-package name needed
57
+ optimizeDeps: { entries: ['src/**/*.{ts,tsx}'] },
58
+ };
59
+ // a function replaces the config outright; a plain object deep-merges onto it via Vite's own mergeConfig
60
+ const resolved = typeof override === 'function'
61
+ ? override(defaults)
62
+ : override
63
+ ? mergeConfig(defaults, override)
64
+ : defaults;
65
+ return defineVitestProject(resolved);
66
+ };
67
+ export default defineProject;
68
+ //# sourceMappingURL=define-project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-project.js","sourceRoot":"","sources":["../../src/define-project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EACL,aAAa,IAAI,mBAAmB,EACpC,WAAW,GAEZ,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAW/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,QAAgC,EAAuB,EAAE;IACrF,MAAM,QAAQ,GAAwB;QACpC,OAAO,EAAE,CAAC,iBAAiB,EAAE,CAAC;QAC9B,IAAI,EAAE;YACJ,OAAO,EAAE,CAAC,+BAA+B,CAAC;YAC1C,OAAO,EAAE;gBACP,OAAO,EAAE,IAAI;gBACb,QAAQ,EAAE,UAAU,EAAE;gBACtB,QAAQ,EAAE,IAAI;gBACd,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;aACrC;SACF;QACD,sFAAsF;QACtF,sFAAsF;QACtF,qFAAqF;QACrF,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,CAAC,mBAAmB,CAAC,EAAE,EAAE;QAC1D,0FAA0F;QAC1F,0FAA0F;QAC1F,uFAAuF;QACvF,uFAAuF;QACvF,mFAAmF;QACnF,YAAY,EAAE,EAAE,OAAO,EAAE,CAAC,mBAAmB,CAAC,EAAE;KACjD,CAAC;IACF,yGAAyG;IACzG,MAAM,QAAQ,GACZ,OAAO,QAAQ,KAAK,UAAU;QAC5B,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACpB,CAAC,CAAC,QAAQ;YACR,CAAC,CAAE,WAAW,CAAC,QAAQ,EAAE,QAAQ,CAAyB;YAC1D,CAAC,CAAC,QAAQ,CAAC;IACjB,OAAO,mBAAmB,CAAC,QAAQ,CAAC,CAAC;AACvC,CAAC,CAAC;AAEF,eAAe,aAAa,CAAC"}
@@ -0,0 +1,99 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { FileNotFoundError } from '@equinor/fusion-imports';
4
+ import { resolveAppTestEnv } from './resolve-app-test-env.js';
5
+ const ENV_MODULE_ID = 'virtual:fusion-app-test-env';
6
+ const CONFIGURE_MODULE_ID = 'virtual:fusion-app-test-configure';
7
+ const RESOLVED_ENV_MODULE_ID = `\0${ENV_MODULE_ID}`;
8
+ const RESOLVED_CONFIGURE_MODULE_ID = `\0${CONFIGURE_MODULE_ID}`;
9
+ const DEFAULT_CONFIGURE_CANDIDATES = ['src/config.ts', 'src/config.tsx', 'src/config.js'];
10
+ /**
11
+ * Vite plugin serving an application's manifest/config (resolved the same way `ffc app build`/
12
+ * `ffc app dev` do) and its own module-configurator export as virtual modules, so
13
+ * `@equinor/fusion-framework-vitest-plugin-react-app/test`'s `test`/`render` need no per-test
14
+ * `env`/`configure` wiring.
15
+ *
16
+ * @remarks
17
+ * A plain Vite plugin — Vitest configs are Vite configs, so this registers directly in your
18
+ * own `vitest.config.ts`, no CLI command required:
19
+ * ```ts
20
+ * import { defineConfig } from 'vitest/config';
21
+ * import { appTestVitePlugin } from '@equinor/fusion-framework-vitest-plugin-react-app';
22
+ *
23
+ * export default defineConfig({
24
+ * plugins: [appTestVitePlugin()],
25
+ * // ...your own browser-mode config
26
+ * });
27
+ * ```
28
+ * Exposes two virtual modules: `virtual:fusion-app-test-env` (`manifest`/`config`, as JSON) and
29
+ * `virtual:fusion-app-test-configure` (a re-export of the resolved `configure` module, or
30
+ * `undefined` if none exists). Not intended to be imported directly by application code.
31
+ *
32
+ * @param options - Resolution options; `entrypoint` defaults to the current working directory.
33
+ * @returns A Vite plugin instance.
34
+ */
35
+ export const appTestVitePlugin = (options) => {
36
+ const explicitEntrypoint = options?.entrypoint;
37
+ let entrypoint = explicitEntrypoint;
38
+ let configureModulePath = explicitEntrypoint
39
+ ? resolveConfigureModulePath(explicitEntrypoint, options?.configure)
40
+ : undefined;
41
+ return {
42
+ name: 'fusion:app-test',
43
+ configResolved(config) {
44
+ // Vitest resolves each workspace project's root from its config file, which identifies the app containing src/index.ts.
45
+ entrypoint ??= config.root;
46
+ configureModulePath ??= resolveConfigureModulePath(entrypoint, options?.configure);
47
+ },
48
+ resolveId(id) {
49
+ // claim only our two virtual specifiers, leave everything else to the normal resolvers
50
+ if (id === ENV_MODULE_ID)
51
+ return RESOLVED_ENV_MODULE_ID;
52
+ // second virtual specifier, same rule as above
53
+ if (id === CONFIGURE_MODULE_ID)
54
+ return RESOLVED_CONFIGURE_MODULE_ID;
55
+ return null;
56
+ },
57
+ async load(id) {
58
+ // serves manifest/config resolved lazily here, not at plugin-creation time, so options.entrypoint changes between test runs are respected
59
+ if (id === RESOLVED_ENV_MODULE_ID) {
60
+ const { manifest, config } = await resolveAppTestEnv({ ...options, entrypoint });
61
+ return [
62
+ `export const manifest = ${JSON.stringify(manifest)};`,
63
+ `export const config = ${JSON.stringify(config)};`,
64
+ ].join('\n');
65
+ }
66
+ // no conventional module means the app registers no extra modules, same as omitting `configure` from `makeComponent`
67
+ if (id === RESOLVED_CONFIGURE_MODULE_ID) {
68
+ return configureModulePath
69
+ ? `export { default as configure } from ${JSON.stringify(configureModulePath)};`
70
+ : 'export const configure = undefined;';
71
+ }
72
+ return null;
73
+ },
74
+ };
75
+ };
76
+ /**
77
+ * Resolves the app's module-configurator file: the explicit `file` if given, otherwise the
78
+ * first existing candidate in {@link DEFAULT_CONFIGURE_CANDIDATES}.
79
+ *
80
+ * @throws {@link FileNotFoundError} If an explicitly requested `file` does not exist — unlike
81
+ * the convention-based lookup, a typo here should fail loudly instead of silently running the
82
+ * test suite without the application's modules.
83
+ */
84
+ const resolveConfigureModulePath = (cwd, file) => {
85
+ // an explicit path is a user request, not a convention lookup, so a typo must fail loudly
86
+ if (file) {
87
+ const resolved = resolve(cwd, file);
88
+ // fail fast rather than silently falling back to "no configurator"
89
+ if (!existsSync(resolved)) {
90
+ throw new FileNotFoundError(`Configure module not found: ${resolved}`);
91
+ }
92
+ return resolved;
93
+ }
94
+ // first candidate that exists wins; none existing is a valid "no configurator" state
95
+ const found = DEFAULT_CONFIGURE_CANDIDATES.find((candidate) => existsSync(resolve(cwd, candidate)));
96
+ return found ? resolve(cwd, found) : undefined;
97
+ };
98
+ export default appTestVitePlugin;
99
+ //# sourceMappingURL=index.js.map