@equinor/fusion-framework-vitest-plugin-react-app 1.0.0-next.4 → 1.0.0-next.6

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.
@@ -9,11 +9,14 @@ import type { NavigationModule } from '@equinor/fusion-framework-module-navigati
9
9
  * given.
10
10
  *
11
11
  * @template TEnv - The application environment descriptor.
12
- * @param options - `env` seeds the built-in app manifest mock; navigation defaults to real
13
- * browser history, matching Browser Mode. `configure` runs afterwards on the same
14
- * configurator e.g. call `enableNavigation` again to override the history, or register
15
- * extra modules and service discovery entries. `fusion`, an already-built parent instance,
16
- * is reused as-is and skips `configure` entirely.
12
+ * @param options - `env` seeds the built-in app manifest mock; navigation defaults to in-memory
13
+ * history, so tests don't leak URL/history state between runs. Feature flags default to the
14
+ * in-memory feature-flag mock, with none enabled, when the optional
15
+ * `@equinor/fusion-framework-module-feature-flag` peer dependency is installed. `configure`
16
+ * runs afterwards on the same configurator — e.g. call `enableNavigation` or
17
+ * `enableFeatureFlagMock` again to override either, or register extra modules and service
18
+ * discovery entries. `fusion`, an already-built parent instance, is reused as-is and skips
19
+ * `configure` entirely.
17
20
  * @returns The given `fusion`, or a fresh mocked parent Fusion instance.
18
21
  */
19
22
  export declare function resolveFusion<TEnv extends AppEnv = AppEnv>(options?: {
@@ -45,8 +45,12 @@ import type { FrameworkMockConfigureFn } from '@equinor/fusion-framework/mock';
45
45
  * @example Seed a module for every test in a suite
46
46
  * ```tsx
47
47
  * describe('with a seeded context module', () => {
48
- * const test = testApp.extend('configureApp', { injected: true }, () =>
49
- * (configurator) => enableContextMock(configurator, (mock) => mock.setCurrentContext(projectA)),
48
+ * const test = testApp.extend(
49
+ * 'configureApp',
50
+ * { injected: true },
51
+ * (): AppMockConfigureFn =>
52
+ * (configurator) =>
53
+ * enableContextMock(configurator, (mock) => mock.setCurrentContext(projectA)),
50
54
  * );
51
55
  *
52
56
  * test('starts on the seeded context', async ({ render }) => {
@@ -56,16 +60,46 @@ import type { FrameworkMockConfigureFn } from '@equinor/fusion-framework/mock';
56
60
  * });
57
61
  * ```
58
62
  *
63
+ * @example Type `app`'s registered modules for consumers
64
+ * ```tsx
65
+ * // `configureApp`'s `TModules` generic only types that callback's own `configurator` parameter.
66
+ * // `app`'s type was already fixed (to `unknown`) when the base `testApp` chain defined it, so a
67
+ * // later `configureApp` override does not retroactively change what `app` is typed as. Re-extend
68
+ * // `app` too, with the same `TModules`, to get a typed `app.navigation` in tests.
69
+ * const test = testApp
70
+ * .extend(
71
+ * 'configureApp',
72
+ * { injected: true },
73
+ * ({ configureApp }): AppMockConfigureFn<[NavigationModule]> =>
74
+ * async (configurator, args) => {
75
+ * await configureApp?.(configurator, args);
76
+ * enableNavigation(configurator, '/app');
77
+ * },
78
+ * )
79
+ * .extend('app', ({ configureApp, appEnv, fusion }) =>
80
+ * mockAppModules<[NavigationModule], AppEnv>(configureApp, appEnv as AppEnv, fusion),
81
+ * );
82
+ *
83
+ * test('exposes the navigation module', async ({ app }) => {
84
+ * expect(app.navigation).toBeDefined();
85
+ * });
86
+ * ```
87
+ *
59
88
  * @example Extend the parent framework mock with an application module
60
89
  * ```tsx
61
- * const test = testApp.extend('configureFusion', { injected: true }, () =>
62
- * (configurator) => {
63
- * enableFeatureFlagMock(configurator);
64
- * configurator.serviceDiscovery.addServices([
65
- * { key: 'people', uri: baseUrl('people') },
66
- * { key: 'context', uri: baseUrl('context') },
67
- * ]);
68
- * },
90
+ * const test = testApp.extend(
91
+ * 'configureFusion',
92
+ * { injected: true },
93
+ * (): FrameworkMockConfigureFn<[AppModule, NavigationModule]> =>
94
+ * (configurator) => {
95
+ * // seed a specific flag rather than merely enabling the mock — that's already the
96
+ * // default when the optional feature-flag peer dependency is installed
97
+ * enableFeatureFlagMock(configurator, (mock) => mock.addFeature({ key: 'new-search', enabled: true }));
98
+ * configurator.serviceDiscovery.addServices([
99
+ * { key: 'people', uri: baseUrl('people') },
100
+ * { key: 'context', uri: baseUrl('context') },
101
+ * ]);
102
+ * },
69
103
  * );
70
104
  * ```
71
105
  */
@@ -1 +1 @@
1
- export declare const version = "1.0.0-next.4";
1
+ export declare const version = "1.0.0-next.6";
package/docs/advanced.md CHANGED
@@ -147,15 +147,20 @@ when the custom parent must serve the app's manifest and config. Use
147
147
  ## Extend the parent framework mock with `configureFusion`
148
148
 
149
149
  `fusion` (built by [`resolveFusion`](../src/scope/resolve-fusion.ts)) always carries a mocked
150
- `app` module (serving this app's manifest) and a `navigation` module with real browser history,
151
- matching Browser Mode. `configureFusion` runs on the same configurator afterwards, so a test can
152
- register extra framework-level modules or override that setup, without reimplementing it:
150
+ `app` module (serving this app's manifest) and a `navigation` module with in-memory history, so
151
+ tests don't leak URL/history state between runs. It also mocks the `featureFlag` module with
152
+ no flags enabled, so `useFeature` needs no `localStorage` or URL seeding but only when
153
+ `@equinor/fusion-framework-module-feature-flag` (an optional peer dependency) is actually
154
+ installed; apps that don't use feature flags aren't forced to add it. `configureFusion` runs on
155
+ the same configurator afterwards, so a test can register extra framework-level modules or
156
+ override that setup, without reimplementing it:
153
157
 
154
158
  ```tsx
155
159
  import { enableFeatureFlagMock } from '@equinor/fusion-framework-module-feature-flag/mock';
156
160
 
157
161
  const test = baseTest.extend('configureFusion', { injected: true }, () => (configurator) => {
158
- enableFeatureFlagMock(configurator);
162
+ // seed a specific flag rather than merely enabling the mock — that's already the default
163
+ enableFeatureFlagMock(configurator, (mock) => mock.addFeature({ key: 'new-search', enabled: true }));
159
164
  configurator.serviceDiscovery.addServices([{ key: 'people', uri: baseUrl('people') }]);
160
165
  });
161
166
  ```
@@ -172,7 +177,7 @@ import { enableNavigation, createHistory } from '@equinor/fusion-framework-modul
172
177
 
173
178
  test.override('configureFusion', { injected: true }, () => (configurator) =>
174
179
  enableNavigation(configurator, {
175
- configure: (config) => config.setHistory(createHistory('memory')),
180
+ configure: (config) => config.setHistory(createHistory('browser')),
176
181
  }),
177
182
  );
178
183
  ```
@@ -92,7 +92,7 @@ export default defineProject({
92
92
  instances: [{ browser: 'chromium' }],
93
93
  },
94
94
  },
95
- server: { warmup: { clientFiles: ['src/**/*.{ts,tsx}'] } },
95
+ server: { warmup: { clientFiles: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'] } },
96
96
  });
97
97
  ```
98
98
 
@@ -178,6 +178,11 @@ aren't Fusion-specific.
178
178
  Review polyfills and component replacements that may only exist for `jsdom` or `happy-dom`.
179
179
  Remove one workaround at a time and run the affected tests against the real component.
180
180
 
181
+ A common one: a no-op `window.ResizeObserver` stub carried over from `jsdom`/`happy-dom` setup.
182
+ Real Chromium doesn't need the stub, but leaving it in place silently breaks any component that
183
+ measures itself through `ResizeObserver` before rendering — for example a virtualized popover
184
+ list. Remove the stub rather than porting it.
185
+
181
186
  Do not remove unrelated suppressions automatically. AG Grid license messages, Lit dev-mode
182
187
  warnings, network behavior, and app-specific test doubles may still apply in Browser Mode.
183
188
 
@@ -18,6 +18,9 @@ resolve the app, or behaves differently in Vitest Browser Mode.
18
18
  | The app starts signed in unexpectedly | The MSAL mock defaults to `Test User` | Set `configurator.msal.setAccount(null)` before rendering |
19
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
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
+ | A `<Router>` route with a catch-all or parameterized `path` never renders; every query on it times out | The navigation module's current location does not match that path yet | Push the target location through the navigation module (`app.modules.navigation.push(path)`) before rendering, not after |
22
+ | A virtualized list or popover (for example an EDS `Autocomplete` built on `@tanstack/react-virtual`) never renders its rows | `window.ResizeObserver` was stubbed as a no-op in test setup | Remove the stub; real Chromium's `ResizeObserver` is required for virtualized layout to measure correctly |
23
+ | `test.each` runs but a fixture in its per-case callback is `undefined` | `test.each` does not forward `test.extend` fixture context to each case | Use Vitest's `test.for` instead, which does receive fixture context |
21
24
 
22
25
  ## Verify the project wiring
23
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@equinor/fusion-framework-vitest-plugin-react-app",
3
- "version": "1.0.0-next.4",
3
+ "version": "1.0.0-next.6",
4
4
  "description": "Vite plugin resolving an application's manifest, config, and module-configurator as virtual modules for testing.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -47,15 +47,15 @@
47
47
  "access": "public"
48
48
  },
49
49
  "dependencies": {
50
- "@equinor/fusion-framework": "8.1.0-next.1",
51
- "@equinor/fusion-framework-cli": "15.2.8-next.2",
50
+ "@equinor/fusion-framework": "8.1.0-next.2",
51
+ "@equinor/fusion-framework-app": "13.0.3-next.1",
52
+ "@equinor/fusion-framework-cli": "15.3.0-next.1",
52
53
  "@equinor/fusion-framework-module": "6.1.3-next.0",
53
- "@equinor/fusion-framework-module-app": "8.1.0-next.0",
54
- "@equinor/fusion-framework-module-navigation": "7.0.8-next.0",
55
- "@equinor/fusion-framework-react": "9.0.0-next.0",
56
- "@equinor/fusion-framework-app": "14.0.0-next.1",
54
+ "@equinor/fusion-framework-module-app": "8.0.6-next.0",
55
+ "@equinor/fusion-framework-module-navigation": "7.0.9-next.0",
57
56
  "@equinor/fusion-framework-react-module": "4.0.3-next.0",
58
- "@equinor/fusion-imports": "2.0.3-next.0"
57
+ "@equinor/fusion-imports": "2.0.3-next.1",
58
+ "@equinor/fusion-framework-react": "9.0.0-next.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@types/react": "^19.2.7",
@@ -68,7 +68,8 @@
68
68
  "typescript": "^7.0.2",
69
69
  "vite": "^8.0.0",
70
70
  "vitest": "^4.1.10",
71
- "vitest-browser-react": "^2.2.0"
71
+ "vitest-browser-react": "^2.2.0",
72
+ "@equinor/fusion-framework-module-feature-flag": "2.1.0-next.1"
72
73
  },
73
74
  "peerDependencies": {
74
75
  "@types/react": "^18.0.0 || ^19.0.0",
@@ -79,7 +80,13 @@
79
80
  "rxjs": "^7.0.0",
80
81
  "vite": "^8.0.0",
81
82
  "vitest": "^4.0.0",
82
- "vitest-browser-react": "^2.2.0"
83
+ "vitest-browser-react": "^2.2.0",
84
+ "@equinor/fusion-framework-module-feature-flag": "2.1.0-next.1"
85
+ },
86
+ "peerDependenciesMeta": {
87
+ "@equinor/fusion-framework-module-feature-flag": {
88
+ "optional": true
89
+ }
83
90
  },
84
91
  "scripts": {
85
92
  "build": "tsc -b",
@@ -68,13 +68,15 @@ export const defineProject = (override?: AppTestConfigOverride): UserWorkspaceCo
68
68
  // pre-transforms all source up front so deps only reached via lazy/code-split imports
69
69
  // (e.g. route components) are discovered before the first test request, not mid-run —
70
70
  // the latter forces Vite to reload the page and fails the in-flight test file import
71
- server: { warmup: { clientFiles: ['src/**/*.{ts,tsx}'] } },
71
+ // `.d.ts` files are excluded: a CJS-style declaration file (e.g. one using `export =`)
72
+ // fails the warmup scan when loaded as ESM
73
+ server: { warmup: { clientFiles: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'] } },
72
74
  // same reasoning as `server.warmup` above, but for the esbuild dep scanner: without this,
73
75
  // its default entry detection can miss code-split route files entirely, so a package only
74
76
  // ever imported from one of those (e.g. react-router's own deps) is discovered mid-run
75
77
  // instead of up front — statically crawling every source file (which esbuild's scanner
76
78
  // follows through dynamic imports too) avoids that with no per-package name needed
77
- optimizeDeps: { entries: ['src/**/*.{ts,tsx}'] },
79
+ optimizeDeps: { entries: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'] },
78
80
  };
79
81
  // a function replaces the config outright; a plain object deep-merges onto it via Vite's own mergeConfig
80
82
  const resolved =
@@ -1,6 +1,10 @@
1
1
  import type { AppEnv } from '@equinor/fusion-framework-app';
2
2
  import type { Fusion } from '@equinor/fusion-framework';
3
- import { mockFramework, type FrameworkMockConfigureFn } from '@equinor/fusion-framework/mock';
3
+ import {
4
+ mockFramework,
5
+ type FrameworkMockConfigureFn,
6
+ type FrameworkMockConfigurator,
7
+ } from '@equinor/fusion-framework/mock';
4
8
  import { enableAppManifestMock } from '@equinor/fusion-framework-app/mock';
5
9
  import type { AppModule } from '@equinor/fusion-framework-module-app';
6
10
  import { enableNavigation, createHistory } from '@equinor/fusion-framework-module-navigation';
@@ -8,17 +12,46 @@ import type { NavigationModule } from '@equinor/fusion-framework-module-navigati
8
12
 
9
13
  import { defaultAppEnv } from './default-app-env';
10
14
 
15
+ /**
16
+ * Enables the feature-flag mock only when `@equinor/fusion-framework-module-feature-flag` — an
17
+ * optional peer dependency — is actually installed, so apps that don't use feature flags aren't
18
+ * forced to install a module they never reference.
19
+ */
20
+ async function enableFeatureFlagMockIfAvailable(
21
+ configurator: FrameworkMockConfigurator<[AppModule, NavigationModule]>,
22
+ ): Promise<void> {
23
+ try {
24
+ const { enableFeatureFlagMock } = await import(
25
+ '@equinor/fusion-framework-module-feature-flag/mock'
26
+ );
27
+ enableFeatureFlagMock(configurator);
28
+ } catch (error) {
29
+ // re-throw anything other than the optional peer being missing, so a real
30
+ // registration failure isn't silently swallowed
31
+ if (
32
+ error instanceof Error &&
33
+ (error.message.includes('Cannot find module') || error.message.includes('MODULE_NOT_FOUND'))
34
+ ) {
35
+ return;
36
+ }
37
+ throw error;
38
+ }
39
+ }
40
+
11
41
  /**
12
42
  * Resolves the parent Fusion instance backing an application module scope, building a
13
43
  * fresh {@link mockFramework} instance with this app's own manifest served when none is
14
44
  * given.
15
45
  *
16
46
  * @template TEnv - The application environment descriptor.
17
- * @param options - `env` seeds the built-in app manifest mock; navigation defaults to real
18
- * browser history, matching Browser Mode. `configure` runs afterwards on the same
19
- * configurator e.g. call `enableNavigation` again to override the history, or register
20
- * extra modules and service discovery entries. `fusion`, an already-built parent instance,
21
- * is reused as-is and skips `configure` entirely.
47
+ * @param options - `env` seeds the built-in app manifest mock; navigation defaults to in-memory
48
+ * history, so tests don't leak URL/history state between runs. Feature flags default to the
49
+ * in-memory feature-flag mock, with none enabled, when the optional
50
+ * `@equinor/fusion-framework-module-feature-flag` peer dependency is installed. `configure`
51
+ * runs afterwards on the same configurator — e.g. call `enableNavigation` or
52
+ * `enableFeatureFlagMock` again to override either, or register extra modules and service
53
+ * discovery entries. `fusion`, an already-built parent instance, is reused as-is and skips
54
+ * `configure` entirely.
22
55
  * @returns The given `fusion`, or a fresh mocked parent Fusion instance.
23
56
  */
24
57
  export async function resolveFusion<TEnv extends AppEnv = AppEnv>(options?: {
@@ -32,8 +65,9 @@ export async function resolveFusion<TEnv extends AppEnv = AppEnv>(options?: {
32
65
  mockFramework<[AppModule, NavigationModule]>(async (configurator) => {
33
66
  enableAppManifestMock(configurator, env ?? (defaultAppEnv as TEnv));
34
67
  enableNavigation(configurator, {
35
- configure: (config) => config.setHistory(createHistory('browser')),
68
+ configure: (config) => config.setHistory(createHistory('memory')),
36
69
  });
70
+ await enableFeatureFlagMockIfAvailable(configurator);
37
71
  await configure?.(configurator);
38
72
  })
39
73
  );
package/src/test-app.tsx CHANGED
@@ -52,8 +52,12 @@ import { defaultAppEnv, resolveFusion, createAppScopeWrapper } from './scope';
52
52
  * @example Seed a module for every test in a suite
53
53
  * ```tsx
54
54
  * describe('with a seeded context module', () => {
55
- * const test = testApp.extend('configureApp', { injected: true }, () =>
56
- * (configurator) => enableContextMock(configurator, (mock) => mock.setCurrentContext(projectA)),
55
+ * const test = testApp.extend(
56
+ * 'configureApp',
57
+ * { injected: true },
58
+ * (): AppMockConfigureFn =>
59
+ * (configurator) =>
60
+ * enableContextMock(configurator, (mock) => mock.setCurrentContext(projectA)),
57
61
  * );
58
62
  *
59
63
  * test('starts on the seeded context', async ({ render }) => {
@@ -63,16 +67,46 @@ import { defaultAppEnv, resolveFusion, createAppScopeWrapper } from './scope';
63
67
  * });
64
68
  * ```
65
69
  *
70
+ * @example Type `app`'s registered modules for consumers
71
+ * ```tsx
72
+ * // `configureApp`'s `TModules` generic only types that callback's own `configurator` parameter.
73
+ * // `app`'s type was already fixed (to `unknown`) when the base `testApp` chain defined it, so a
74
+ * // later `configureApp` override does not retroactively change what `app` is typed as. Re-extend
75
+ * // `app` too, with the same `TModules`, to get a typed `app.navigation` in tests.
76
+ * const test = testApp
77
+ * .extend(
78
+ * 'configureApp',
79
+ * { injected: true },
80
+ * ({ configureApp }): AppMockConfigureFn<[NavigationModule]> =>
81
+ * async (configurator, args) => {
82
+ * await configureApp?.(configurator, args);
83
+ * enableNavigation(configurator, '/app');
84
+ * },
85
+ * )
86
+ * .extend('app', ({ configureApp, appEnv, fusion }) =>
87
+ * mockAppModules<[NavigationModule], AppEnv>(configureApp, appEnv as AppEnv, fusion),
88
+ * );
89
+ *
90
+ * test('exposes the navigation module', async ({ app }) => {
91
+ * expect(app.navigation).toBeDefined();
92
+ * });
93
+ * ```
94
+ *
66
95
  * @example Extend the parent framework mock with an application module
67
96
  * ```tsx
68
- * const test = testApp.extend('configureFusion', { injected: true }, () =>
69
- * (configurator) => {
70
- * enableFeatureFlagMock(configurator);
71
- * configurator.serviceDiscovery.addServices([
72
- * { key: 'people', uri: baseUrl('people') },
73
- * { key: 'context', uri: baseUrl('context') },
74
- * ]);
75
- * },
97
+ * const test = testApp.extend(
98
+ * 'configureFusion',
99
+ * { injected: true },
100
+ * (): FrameworkMockConfigureFn<[AppModule, NavigationModule]> =>
101
+ * (configurator) => {
102
+ * // seed a specific flag rather than merely enabling the mock — that's already the
103
+ * // default when the optional feature-flag peer dependency is installed
104
+ * enableFeatureFlagMock(configurator, (mock) => mock.addFeature({ key: 'new-search', enabled: true }));
105
+ * configurator.serviceDiscovery.addServices([
106
+ * { key: 'people', uri: baseUrl('people') },
107
+ * { key: 'context', uri: baseUrl('context') },
108
+ * ]);
109
+ * },
76
110
  * );
77
111
  * ```
78
112
  */
@@ -81,9 +115,9 @@ export const testApp = baseTest
81
115
  // `test.extend`'s plain-`value` overload rejects function types (ambiguous with the
82
116
  // resolver-`fn` overload), so a function-typed fixture default must go through `fn` instead.
83
117
  .extend('configureApp', { injected: true }, () => undefined as AppMockConfigureFn | undefined)
84
- // Runs after the built-in app manifest/navigation setup, so a test can register extra
85
- // framework modules, service discovery entries, or call `enableNavigation` again to
86
- // override the history, without reimplementing the base setup.
118
+ // Runs after the built-in app manifest/navigation/feature-flag setup, so a test can register
119
+ // extra framework modules, service discovery entries, or call `enableNavigation`/
120
+ // `enableFeatureFlagMock` again to override either, without reimplementing the base setup.
87
121
  .extend(
88
122
  'configureFusion',
89
123
  { injected: true },
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '1.0.0-next.4';
2
+ export const version = '1.0.0-next.6';
package/tsconfig.json CHANGED
@@ -16,6 +16,7 @@
16
16
  { "path": "../../modules/module" },
17
17
  { "path": "../../modules/app" },
18
18
  { "path": "../../modules/navigation" },
19
+ { "path": "../../modules/feature-flag" },
19
20
  { "path": "../../react/framework" },
20
21
  { "path": "../../react/modules/module" },
21
22
  { "path": "../../utils/imports" }