@equinor/fusion-framework 8.0.14 → 8.1.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 (41) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/README.md +30 -0
  3. package/dist/esm/__tests__/mock/application-module.test.js +53 -0
  4. package/dist/esm/__tests__/mock/application-module.test.js.map +1 -0
  5. package/dist/esm/__tests__/mock/documented-usage.test.js +73 -0
  6. package/dist/esm/__tests__/mock/documented-usage.test.js.map +1 -0
  7. package/dist/esm/__tests__/mock/mock-framework.test.js +212 -0
  8. package/dist/esm/__tests__/mock/mock-framework.test.js.map +1 -0
  9. package/dist/esm/init.js +7 -2
  10. package/dist/esm/init.js.map +1 -1
  11. package/dist/esm/mock/FrameworkMockConfigurator.js +233 -0
  12. package/dist/esm/mock/FrameworkMockConfigurator.js.map +1 -0
  13. package/dist/esm/mock/index.js +32 -0
  14. package/dist/esm/mock/index.js.map +1 -0
  15. package/dist/esm/mock/mock-framework.js +55 -0
  16. package/dist/esm/mock/mock-framework.js.map +1 -0
  17. package/dist/esm/version.js +1 -1
  18. package/dist/esm/version.js.map +1 -1
  19. package/dist/tsconfig.tsbuildinfo +1 -1
  20. package/dist/types/__tests__/mock/application-module.test.d.ts +1 -0
  21. package/dist/types/__tests__/mock/documented-usage.test.d.ts +1 -0
  22. package/dist/types/__tests__/mock/mock-framework.test.d.ts +1 -0
  23. package/dist/types/mock/FrameworkMockConfigurator.d.ts +185 -0
  24. package/dist/types/mock/index.d.ts +29 -0
  25. package/dist/types/mock/mock-framework.d.ts +58 -0
  26. package/dist/types/version.d.ts +1 -1
  27. package/docs/testing-api.md +25 -0
  28. package/docs/testing-choosing-a-layer.md +90 -0
  29. package/docs/testing-design.md +63 -0
  30. package/docs/testing-extending.md +102 -0
  31. package/docs/testing.md +178 -0
  32. package/package.json +17 -11
  33. package/src/__tests__/mock/application-module.test.ts +87 -0
  34. package/src/__tests__/mock/documented-usage.test.ts +98 -0
  35. package/src/__tests__/mock/mock-framework.test.ts +278 -0
  36. package/src/init.ts +7 -2
  37. package/src/mock/FrameworkMockConfigurator.ts +270 -0
  38. package/src/mock/index.ts +62 -0
  39. package/src/mock/mock-framework.ts +72 -0
  40. package/src/version.ts +1 -1
  41. package/vitest.config.ts +1 -1
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,185 @@
1
+ import type { AnyModule } from '@equinor/fusion-framework-module';
2
+ import { type ContextMockConfigurator } from '@equinor/fusion-framework-module-context/mock';
3
+ import { type IHttpClientConfigurator } from '@equinor/fusion-framework-module-http';
4
+ import { type MsalMockConfigurator } from '@equinor/fusion-framework-module-msal/mock';
5
+ import { type ServiceDiscoveryMockConfigurator } from '@equinor/fusion-framework-module-service-discovery/mock';
6
+ import { type IApiConfigurator } from '@equinor/fusion-framework-module-services';
7
+ import { type TelemetryMockConfigurator } from '@equinor/fusion-framework-module-telemetry/mock';
8
+ import { FrameworkConfigurator } from '../FrameworkConfigurator.js';
9
+ /**
10
+ * The real framework configurator, with every built-in module that reaches
11
+ * outside the process backed by a test double, and every other built-in
12
+ * module reachable the same way.
13
+ *
14
+ * @remarks
15
+ * Nothing else changes: the same module set, the same configuration pipeline and
16
+ * the same lifecycle are used. Only the boundaries that would need credentials or
17
+ * network access are substituted, so a test still exercises module wiring,
18
+ * configuration validation and lifecycle hooks.
19
+ *
20
+ * Every built-in module exposes its own configurator as a property, so a test
21
+ * reaches it directly instead of registering a callback to receive it. `http`
22
+ * is the real configurator — fake a response by registering a
23
+ * short-circuiting middleware through `.http.addMiddleware(...)` instead of
24
+ * swapping the module out; see `@equinor/fusion-framework-module-http/mock`'s
25
+ * `createOpenApiMockMiddleware` for faking a whole `@equinor/fusion-openapi-mock`
26
+ * document that way. `services` is not backed by a test double yet either, so
27
+ * calls through its configurator still reach the network — but
28
+ * the configurator itself is reachable the same way `.msal` is, since its
29
+ * `configure` factory takes no `ref` and so loses nothing by being pinned early.
30
+ *
31
+ * `event` is deliberately not pinned: its `configure` factory reads `ref` to
32
+ * wire bubbling to a parent event provider when this configurator is hoisted
33
+ * inside a host framework, and pinning would freeze that decision before a
34
+ * `ref` could ever be known.
35
+ *
36
+ * Because this *is* a `FrameworkConfigurator`, every `enableX` helper an
37
+ * application already uses accepts it unchanged — including the ones an
38
+ * application team writes for their own modules.
39
+ *
40
+ * @typeParam TModules - Module descriptors beyond the built-in set. Supply this
41
+ * when a test registers application modules, so they are typed on the resulting
42
+ * instance.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * const configurator = new FrameworkMockConfigurator();
47
+ *
48
+ * configurator.msal.setAccount({ name: 'Ada Lovelace' });
49
+ * configurator.serviceDiscovery.setBaseUri('http://localhost:6669');
50
+ *
51
+ * const fusion = await init(configurator);
52
+ * ```
53
+ */
54
+ export declare class FrameworkMockConfigurator<TModules extends Array<AnyModule> = []> extends FrameworkConfigurator<TModules> {
55
+ #private;
56
+ static readonly className: string;
57
+ /**
58
+ * Creates a framework configurator backed by the built-in mock modules.
59
+ */
60
+ constructor();
61
+ /**
62
+ * Pins a module to a single configurator instance for the lifetime of this
63
+ * configurator, so it can be reached by name through {@link _getConfig}.
64
+ *
65
+ * @remarks
66
+ * The module system otherwise builds a fresh configurator from its own
67
+ * `configure` factory during the configure phase — too late for a test to
68
+ * reach, and a new instance on every call besides. This replaces that
69
+ * factory with one that always returns the same instance, and registers the
70
+ * result under the module's own name.
71
+ *
72
+ * A subclass registering a module supplied through {@link TModules} uses
73
+ * this the same way `.msal` and `.serviceDiscovery` do, to expose its own
74
+ * named accessor:
75
+ *
76
+ * ```typescript
77
+ * class MyMockConfigurator extends FrameworkMockConfigurator<[InvoiceModule]> {
78
+ * constructor() {
79
+ * super();
80
+ * this._pin(invoiceMockModule);
81
+ * }
82
+ *
83
+ * public get invoices(): InvoiceMockConfigurator {
84
+ * return this._getConfig('invoices');
85
+ * }
86
+ * }
87
+ * ```
88
+ *
89
+ * @param module - The module descriptor to pin a configurator for.
90
+ * @template TModule - The specific module descriptor type being pinned.
91
+ * @throws {Error} If the module declares no `configure` factory to pin, or
92
+ * the factory returns a promise instead of a configurator — pinning is
93
+ * synchronous, so a test can reach the accessor immediately.
94
+ */
95
+ protected _pin<TModule extends AnyModule>(module: TModule): void;
96
+ /**
97
+ * Returns the configurator pinned for a module by name.
98
+ *
99
+ * @param name - The module's name, as passed to {@link _pin}.
100
+ * @template TConfig - The specific configurator type expected for this module.
101
+ * @returns The configurator pinned under `name`.
102
+ * @throws {Error} If no configurator has been pinned for that name.
103
+ */
104
+ protected _getConfig<TConfig>(name: string): TConfig;
105
+ /**
106
+ * Configures the user the framework signs in.
107
+ *
108
+ * @remarks
109
+ * The same {@link MsalMockConfigurator} the auth module is configured from, so
110
+ * a change made here is what the module sees.
111
+ *
112
+ * @returns The MSAL mock configurator.
113
+ */
114
+ get msal(): MsalMockConfigurator;
115
+ /**
116
+ * Configures the registry services are resolved from.
117
+ *
118
+ * @remarks
119
+ * The same {@link ServiceDiscoveryMockConfigurator} the service discovery
120
+ * module is configured from, so a change made here is what the module sees.
121
+ *
122
+ * @returns The service discovery mock configurator.
123
+ */
124
+ get serviceDiscovery(): ServiceDiscoveryMockConfigurator;
125
+ /**
126
+ * Configures the HTTP module's named clients.
127
+ *
128
+ * @remarks
129
+ * The same {@link IHttpClientConfigurator} the HTTP module is configured
130
+ * from — the real one, not a test double. Register a short-circuiting
131
+ * {@link HttpMiddleware} through `addMiddleware` to answer from that
132
+ * instead of the network.
133
+ *
134
+ * @returns The real HTTP configurator.
135
+ */
136
+ get http(): IHttpClientConfigurator;
137
+ /**
138
+ * Configures the typed API clients the `services` module builds.
139
+ *
140
+ * @remarks
141
+ * The real configurator — `services` has no test double yet.
142
+ *
143
+ * @returns The real API configurator.
144
+ */
145
+ get services(): IApiConfigurator;
146
+ /**
147
+ * Configures context resolution.
148
+ *
149
+ * @remarks
150
+ * The same {@link ContextMockConfigurator} the context module is configured
151
+ * from, so seeding an item here is what `fusion.modules.context` resolves.
152
+ *
153
+ * @returns The context mock configurator.
154
+ */
155
+ get context(): ContextMockConfigurator;
156
+ /**
157
+ * Configures telemetry.
158
+ *
159
+ * @remarks
160
+ * The same {@link TelemetryMockConfigurator} the telemetry module is
161
+ * configured from, so a tracked event or measurement can be read back from
162
+ * its adapter instead of reaching Application Insights.
163
+ *
164
+ * @returns The telemetry mock configurator.
165
+ */
166
+ get telemetry(): TelemetryMockConfigurator;
167
+ /**
168
+ * Registers a module through its own enabler.
169
+ *
170
+ * @remarks
171
+ * Sugar for calling the enabler directly — `enableMyModuleMock(configurator)`
172
+ * works just as well, because this class *is* a `FrameworkConfigurator`. Use
173
+ * whichever reads better at the call site.
174
+ *
175
+ * @param configure - Callback receiving this configurator.
176
+ * @returns This configurator, for chaining.
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * configurator.addModule((c) => enableMyModuleMock(c, { total: 42 }));
181
+ * ```
182
+ */
183
+ addModule(configure: (configurator: this) => void): this;
184
+ }
185
+ export default FrameworkMockConfigurator;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Zero-configuration Fusion Framework instances for tests.
3
+ *
4
+ * @remarks
5
+ * Lets an application initialize the real framework — real modules, real
6
+ * configuration pipeline, real lifecycle — while the boundaries that reach
7
+ * outside the process are substituted with deterministic fakes. No credentials,
8
+ * no network access and no configuration are required.
9
+ *
10
+ * This entry point holds **no mock logic**. Each module owns and exports its own
11
+ * test double from its `./mock` entry point; this entry point only composes the
12
+ * built-in set into a ready-to-use instance. An application module follows the
13
+ * same pattern and plugs in identically.
14
+ *
15
+ * This entry point is test-runner agnostic: it contains no dependency on Vitest
16
+ * or any other test framework, so the same helpers work under any runner.
17
+ *
18
+ * Fixture generators with realistic fake data (e.g. `createContextItems`) live
19
+ * on each module's own `/mock/fixtures` entry point instead of here, since they
20
+ * pull in an extra dependency ({@link https://fakerjs.dev/ | faker}) that
21
+ * plain configurator mocking doesn't need.
22
+ *
23
+ * @packageDocumentation
24
+ */
25
+ export { mockFramework, type FrameworkMockConfigureFn } from './mock-framework.js';
26
+ export { FrameworkMockConfigurator } from './FrameworkMockConfigurator.js';
27
+ export { enableMsalMock, msalMockModule, MsalMockConfigurator, MsalMockClient, createMsalMockClient, createMockToken, type AuthConfigMockFn, type MsalMockUser, type MockTokenClaims, } from '@equinor/fusion-framework-module-msal/mock';
28
+ export { mockServiceDiscovery, enableServiceDiscoveryMock, serviceDiscoveryMockModule, ServiceDiscoveryMockClient, ServiceDiscoveryMockConfigurator, createMockService, defaultServiceDiscoveryMockServices, type ServiceDiscoveryConfigMockFn, type MockService, type ServiceDiscoveryMockClientOptions, } from '@equinor/fusion-framework-module-service-discovery/mock';
29
+ export { enableContextMock, contextMockModule, ContextMockConfigurator, type ContextMockConfigFn, type ContextResolverFn, } from '@equinor/fusion-framework-module-context/mock';
@@ -0,0 +1,58 @@
1
+ import type { AnyModule } from '@equinor/fusion-framework-module';
2
+ import type { Fusion } from '../types.js';
3
+ import { FrameworkMockConfigurator } from './FrameworkMockConfigurator.js';
4
+ /**
5
+ * Configures a mocked framework before it is initialized.
6
+ *
7
+ * @typeParam TModules - Module descriptors beyond the built-in set.
8
+ * @param configurator - The configurator to configure.
9
+ */
10
+ export type FrameworkMockConfigureFn<TModules extends Array<AnyModule> = []> = (configurator: FrameworkMockConfigurator<TModules>) => void | Promise<void>;
11
+ /**
12
+ * Starts a Fusion framework instance that needs no credentials and no network.
13
+ *
14
+ * @remarks
15
+ * The real framework is started: the real module set, the real configuration
16
+ * pipeline and the real lifecycle. Only the boundaries that would need
17
+ * credentials or network access are substituted, so a test exercises the wiring
18
+ * an application actually depends on rather than a reimplementation of it.
19
+ *
20
+ * The configurator passed to `configure` is a {@link FrameworkMockConfigurator},
21
+ * which *is* a `FrameworkConfigurator`. Every `enableX` helper therefore accepts
22
+ * it unchanged — including the ones an application team writes for their own
23
+ * modules.
24
+ *
25
+ * Spying on individual calls is left to the test runner. The framework makes the
26
+ * runtime substitutable; `vi.spyOn`, `bun:test` `spyOn` and `t.mock.method` all
27
+ * work against the resulting instance with no framework support.
28
+ *
29
+ * @typeParam TModules - Module descriptors beyond the built-in set. Supply this
30
+ * when a test registers application modules, so they are typed on the result.
31
+ * @template TModules - Module descriptors beyond the built-in set.
32
+ * @param configure - Callback that configures the framework before it starts.
33
+ * @returns The initialized framework instance.
34
+ *
35
+ * @example Zero configuration
36
+ * ```typescript
37
+ * const fusion = await mockFramework();
38
+ * ```
39
+ *
40
+ * @example Configure the built-in mocks
41
+ * ```typescript
42
+ * const fusion = await mockFramework((configurator) => {
43
+ * configurator.msal.setAccount({ name: 'Ada Lovelace' });
44
+ * configurator.serviceDiscovery.setBaseUri('http://localhost:6669');
45
+ * });
46
+ * ```
47
+ *
48
+ * @example Register an application module
49
+ * ```typescript
50
+ * const fusion = await mockFramework<[InvoiceModule]>((configurator) => {
51
+ * enableInvoicesMock(configurator, { total: 42 });
52
+ * });
53
+ *
54
+ * await fusion.modules.invoices.getInvoice('1');
55
+ * ```
56
+ */
57
+ export declare function mockFramework<TModules extends Array<AnyModule> = []>(configure?: FrameworkMockConfigureFn<TModules>): Promise<Fusion<TModules>>;
58
+ export default mockFramework;
@@ -1 +1 @@
1
- export declare const version = "8.0.14";
1
+ export declare const version = "8.1.0-next.0";
@@ -0,0 +1,25 @@
1
+ # API
2
+
3
+ | Export | Owner | Purpose |
4
+ | --- | --- | --- |
5
+ | `mockFramework<TModules>(configure?)` | `/mock` | Build and initialize a framework instance for a test |
6
+ | `FrameworkMockConfigurator<TModules>` | `/mock` | `FrameworkConfigurator` whose outward boundaries are mocked, exposing `msal`, `serviceDiscovery`, `http` and `context` |
7
+ | `enableMsalMock(configurator, configure?)` | `-module-msal/mock` | Register the auth module with an in-process MSAL client |
8
+ | `msalMockModule` | `-module-msal/mock` | The auth module with a mock client, for manual registration |
9
+ | `MsalMockConfigurator` | `-module-msal/mock` | `MsalConfigurator` backed by a mock client (`setAccount`, `setClient`, …) |
10
+ | `MsalMockClient(config)` | `-module-msal/mock` | Build the in-process MSAL client on its own, from the same `MsalClientConfig` as `MsalClient` |
11
+ | `createMsalMockClient(config, user?)` | `-module-msal/mock` | Convenience alias for `new MsalMockClient(config)`, optionally signing a user in |
12
+ | `createMockToken(claims?)` | `-module-msal/mock` | Mint a deterministic JWT |
13
+ | `mockServiceDiscovery(configurator, options?, configure?)` | `-module-service-discovery/mock` | Replace service discovery with an in-memory registry |
14
+ | `enableServiceDiscoveryMock(configurator, configure?)` | `-module-service-discovery/mock` | Register the discovery module with an in-memory registry |
15
+ | `ServiceDiscoveryMockConfigurator` | `-module-service-discovery/mock` | `ServiceDiscoveryConfigurator` that builds an in-memory registry (`setBaseUri`, `addService`, …) |
16
+ | `ServiceDiscoveryMockClient(options?)` | `-module-service-discovery/mock` | Build the in-memory discovery client on its own |
17
+ | `defaultServiceDiscoveryMockServices` | `-module-service-discovery/mock` | Baseline services a Fusion app resolves at start-up |
18
+ | `createMockService(service, baseUri?)` | `-module-service-discovery/mock` | Expand a sparse service declaration into a full `Service` |
19
+ | `configureHttp(configure)` | `-module-http` | Build a module config that pairs `configureClient`/`configureHttpClient` and `addMiddleware` for `configurator.addConfig(...)` — the real module, no mock client |
20
+ | `createOpenApiMockMiddleware(mock)` | `-module-http/mock` | Adapt an `@equinor/fusion-openapi-mock` instance into an `HttpMiddleware`, for use with `configurator.http.addMiddleware(...)` |
21
+ | `enableContextMock(configurator, configure?)` | `-module-context/mock` | Register the context module with an in-memory seeded pool |
22
+ | `contextMockModule` | `-module-context/mock` | The context module with a mock client, for manual registration |
23
+ | `ContextMockConfigurator` | `-module-context/mock` | `ContextModuleConfigurator` backed by in-memory context items (`setCurrentContext`, `setContexts`, `addContext`, `setRelatedContexts`, `setResolver`) |
24
+
25
+ Module-owned exports are re-exported from `@equinor/fusion-framework/mock` for convenience; importing them from their own package is equally valid.
@@ -0,0 +1,90 @@
1
+ # Choose a Fusion testing layer
2
+
3
+ Start with the smallest layer that initializes the production behavior the test needs. Fusion
4
+ test utilities substitute external boundaries; Vitest still owns tests, assertions, lifecycle
5
+ hooks, spies, timers, coverage, and general module mocking.
6
+
7
+ ## Which import should I use?
8
+
9
+ | Testing intent | Start with | Import |
10
+ | --- | --- | --- |
11
+ | Pure function or framework-independent hook | Standard Vitest | `vitest` |
12
+ | React hook that consumes app modules | `renderAppHook` | `@equinor/fusion-framework-vitest-plugin-react-app` |
13
+ | React component, route, or complete Fusion app | Extended `test` and `render` fixture | `@equinor/fusion-framework-vitest-plugin-react-app/test` |
14
+ | Reusable app fixture without automatic app-file resolution | `testApp` | `@equinor/fusion-framework-vitest-plugin-react-app` |
15
+ | Application module configuration without React | `mockAppModules` | `@equinor/fusion-framework-app/mock` |
16
+ | Parent framework or portal-level modules | `mockFramework` | `@equinor/fusion-framework/mock` |
17
+ | One module in a bespoke module graph | That module's `enable*Mock` helper | `@equinor/fusion-framework-module-*/mock` |
18
+ | Named HTTP clients and a few deterministic responses | `configurator.http.addMiddleware` | Real HTTP configurator |
19
+ | Many HTTP operations described by OpenAPI | `createOpenApiMockMiddleware` | `@equinor/fusion-framework-module-http/mock` |
20
+ | One method call, timer, global, or JavaScript module | Vitest's `vi` APIs | `vitest` |
21
+
22
+ ## React app tests
23
+
24
+ Use `@equinor/fusion-framework-vitest-plugin-react-app/test` for the normal application path.
25
+ Its `test` and `render` exports resolve the app's manifest, `app.config.ts`, and module
26
+ configurator, then initialize a fresh framework and app scope for each test.
27
+
28
+ ```tsx
29
+ import { expect } from 'vitest';
30
+ import { test } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
31
+ import { App } from './App';
32
+
33
+ test('renders the app', async ({ render }) => {
34
+ const screen = await render(<App />);
35
+ await expect.element(screen.getByRole('heading')).toBeVisible();
36
+ });
37
+ ```
38
+
39
+ Use the root entry point's `renderAppHook`, `renderAppComponent`, or `testApp` only when the
40
+ test should provide `env`, `configure`, or a parent `fusion` instance explicitly. See the
41
+ [`@equinor/fusion-framework-vitest-plugin-react-app` documentation](../../vitest-plugin/react-app/README.md).
42
+
43
+ ## Framework and module tests
44
+
45
+ Use `mockFramework` when code consumes the parent framework directly or when the test needs
46
+ to seed several built-in framework modules together:
47
+
48
+ ```ts
49
+ import { mockFramework } from '@equinor/fusion-framework/mock';
50
+
51
+ const fusion = await mockFramework((configurator) => {
52
+ configurator.msal.setAccount({ name: 'Ada Lovelace' });
53
+ configurator.context.setCurrentContext({
54
+ id: 'project-a',
55
+ title: 'Project A',
56
+ type: { id: 'ProjectMaster' },
57
+ value: {},
58
+ });
59
+ configurator.serviceDiscovery.addService({ key: 'catalog' });
60
+ });
61
+ ```
62
+
63
+ Use a module-owned `/mock` entry point when assembling a custom module graph or testing the
64
+ module without the standard framework set. Module mocks keep the real provider,
65
+ configurator, validation, and lifecycle; only the client or data source that leaves the
66
+ process is substituted.
67
+
68
+ ## Choosing how to fake data
69
+
70
+ | Need | Preferred boundary | Why |
71
+ | --- | --- | --- |
72
+ | One known context item | `enableContextMock` | Seeds domain data without transport setup |
73
+ | Context service integration | HTTP middleware | Exercises service discovery, HTTP, and context client behavior together |
74
+ | One or two HTTP routes | Hand-written `HttpMiddleware` | Keeps the response behavior explicit |
75
+ | An API described by OpenAPI | `createOpenApiMockMiddleware` | Generates deterministic responses for the whole specification |
76
+ | An individual client call | `vi.spyOn` | Uses the test runner's call assertions and reset semantics |
77
+
78
+ An HTTP middleware handles only matching requests. Calling `next(uri, init)` continues to the
79
+ next middleware and eventually the real network. Tests intended to be fully offline should
80
+ fail or answer every request they expect.
81
+
82
+ ## Related package documentation
83
+
84
+ - [Framework mock usage](testing.md)
85
+ - [Framework mock design](testing-design.md)
86
+ - [Framework mock exports](testing-api.md)
87
+ - [Add a mock for a custom module](testing-extending.md)
88
+ - [Application module testing](../../app/docs/testing.md)
89
+ - [HTTP testing](../../modules/http/docs/testing.md)
90
+ - [Migrate an existing app to Fusion Vitest](../../vitest-plugin/react-app/docs/migrating-an-existing-app.md)
@@ -0,0 +1,63 @@
1
+ # Design
2
+
3
+ Why this entry point exists, what it substitutes, and the rules that keep it from growing into a mocking library.
4
+
5
+ ## Why this exists
6
+
7
+ Fusion Framework cannot start without authenticating a user and resolving services from a registry. Both reach outside the process, so an application test either has to supply real credentials or hand-build a replacement for every built-in module.
8
+
9
+ This entry point removes that work. It runs the **real** configure → initialize pipeline with the **real** built-in modules, and substitutes only the boundaries that leave the process.
10
+
11
+ That distinction matters: module wiring, configuration validation and lifecycle hooks behave exactly as they do in production, so a test still catches wiring mistakes.
12
+
13
+ ## How mocks are organised
14
+
15
+ > [!IMPORTANT]
16
+ > This entry point contains **no mock logic**. Every module owns and exports its own test double from a `./mock` entry point. This entry point only composes the built-in set.
17
+
18
+ ```
19
+ @equinor/fusion-framework-module-msal/mock -> enableMsalMock
20
+ @equinor/fusion-framework-module-service-discovery/mock -> mockServiceDiscovery
21
+ @equinor/fusion-framework/mock -> composes the above
22
+ ```
23
+
24
+ Three consequences follow, and they are the reason for the split:
25
+
26
+ - **Mocks cannot drift.** A test double lives beside the implementation it stands in for, so a change to the real interface breaks it in the same package, in the same build.
27
+ - **Mocks version with their module.** Installing msal `v10` gets msal `v10`'s mock. There is no version matrix to reconcile.
28
+ - **Application modules work the same way.** A team's own module exposes its own mock entry point and composes without this entry point knowing it exists.
29
+
30
+ Adding mock support to another Fusion module means adding a `src/mock/` folder to that module — not editing this entry point.
31
+
32
+ ## What is actually substituted
33
+
34
+ > [!IMPORTANT]
35
+ > Only the **client** — the object that performs network I/O — is replaced. Providers, configurators, schema validation and module `initialize` are all real.
36
+
37
+ For authentication this means `MsalProvider` itself runs. A test therefore observes real provider behaviour, including decisions the provider makes on the caller's behalf:
38
+
39
+ ```typescript
40
+ const fusion = await mockFramework((configurator) => {
41
+ configurator.msal.setClientConfig({ auth: { clientId: 'my-app', tenantId: 'my-tenant' } });
42
+ });
43
+
44
+ const token = await fusion.modules.auth.acquireAccessToken();
45
+ // scope is 'my-app/.default' — resolved by the real provider, not by the test double
46
+ ```
47
+
48
+ A double that replaced the provider would have skipped that logic and quietly reported whatever it was told to.
49
+
50
+ ## Determinism
51
+
52
+ Tokens and resolved services are identical across runs and across machines. `createMockToken` uses a fixed issue time, so a token can be compared or snapshotted directly.
53
+
54
+ ## Test-runner support
55
+
56
+ The package has **no test-runner dependency**. It builds a real framework instance and returns it; assertions are the caller's concern. It works under Vitest today and would work unchanged under another runner.
57
+
58
+ `vitest` appears only in `devDependencies`, for this entry point's own tests.
59
+
60
+ > [!IMPORTANT]
61
+ > This is deliberate, and it is why there is **no Fusion mocking API**. The framework's job is making the runtime *substitutable* — a real configurator that validates, an I/O-boundary client that needs no network or credentials, a registry you compose on the builder. Replacing an individual call is your runner's job, and it does it better: call assertions, argument matchers and reset semantics you already know.
62
+ >
63
+ > Mock clients are therefore plain classes with ordinary methods. `vi.spyOn`, `bun:test`'s `spyOn` and Node's `t.mock.method` all work on them directly.
@@ -0,0 +1,102 @@
1
+ # Adding a mock for another module
2
+
3
+ How to give a module a test double, whether it is one of ours or one of yours.
4
+
5
+ ## Mocking your own module
6
+
7
+ An application module needs no support from this entry point. Ship a test double next to the module and apply it alongside the framework mocks.
8
+
9
+ Two seams matter, and the second is the one teams miss:
10
+
11
+ 1. **A client seam** — `setClient`, the way `MsalConfigurator.setClient` and `setServiceDiscoveryClient` do — so the object performing I/O can be swapped.
12
+ 2. **Configuration on the builder** — so a test can adjust behaviour *without* constructing a client at all. `ServiceDiscoveryMockConfigurator.addService` is the reference: the configurator accumulates config, and the client is built from it when the module assembles its config.
13
+
14
+ ```typescript
15
+ // @my-app/module-invoices/mock
16
+ export const mockInvoices = (configurator, options = {}) => {
17
+ configurator.addConfig(
18
+ configureInvoices((builder) => {
19
+ builder.setClient({
20
+ getInvoice: async (id) => ({ id, total: options.total ?? 100 }),
21
+ });
22
+ }),
23
+ );
24
+ };
25
+ ```
26
+
27
+ ```typescript
28
+ // @my-app/module-invoices — the module's own type, exported for tests
29
+ export type InvoiceModule = Module<'invoices', InvoiceClient, InvoiceConfigurator>;
30
+ ```
31
+
32
+ Pass that descriptor to `mockFramework` as a type argument, and the module is typed on both the configurator and the returned instance:
33
+
34
+ ```typescript
35
+ const fusion = await mockFramework<[InvoiceModule]>((configurator) => {
36
+ enableInvoicesMock(configurator, { total: 42 });
37
+ });
38
+
39
+ await fusion.modules.invoices.getInvoice('inv-1'); // typed, no cast
40
+ ```
41
+
42
+ ### Giving your module the same accessor as `.msal` and `.serviceDiscovery`
43
+
44
+ `enableInvoicesMock(configurator, options)` is enough on its own — the configurator it builds is discarded once configuration runs, which is fine when a test only ever sets options up front. Reach for an accessor when a test needs the configurator itself, for example to assert against it after the fact.
45
+
46
+ Subclass `FrameworkMockConfigurator` and use the protected `_pin`/`_getConfig` pair it exposes for exactly this — the same mechanism `.msal` and `.serviceDiscovery` are built from:
47
+
48
+ ```typescript
49
+ class AppMockConfigurator extends FrameworkMockConfigurator<[InvoiceModule]> {
50
+ constructor() {
51
+ super();
52
+ this._pin(invoiceMockModule);
53
+ }
54
+
55
+ get invoices(): InvoiceMockConfigurator {
56
+ return this._getConfig('invoices');
57
+ }
58
+ }
59
+ ```
60
+
61
+ `_pin` replaces the module's own `configure` factory with one that always returns the same instance — pinning it before initialization runs is what lets a test reach `.invoices` synchronously and have it be the configurator the module is actually built from. `_getConfig` looks that instance up by name, throwing if nothing was pinned for it.
62
+
63
+ ## What is not covered yet
64
+
65
+ `.services` and `.telemetry` are already reachable on `FrameworkMockConfigurator` — their `configure` factories take no `ref`, so they were safe to pin the same way `.msal` and `.serviceDiscovery` are. What is missing is a test double behind them: neither module has a `src/mock/` folder yet, so anything issuing an actual request through them still reaches the network. Adding one means creating that folder in **that module**, then pinning its mock configurator with `_pin` and exposing it with `_getConfig` on `FrameworkMockConfigurator`, replacing the real module descriptor pinned there today.
66
+
67
+ `event` is not pinned at all, deliberately: its `configure` factory reads `ref` to wire event bubbling to a parent event provider when `FrameworkMockConfigurator` is hoisted inside a host framework. Pinning would call `configure()` with `ref` always `undefined`, silently breaking that bubbling — so it is left to build the normal way, from the module system's own configure phase, where `ref` is actually known.
68
+
69
+ ## `.http`
70
+
71
+ `.http` is the real `HttpClientConfigurator` built from `@equinor/fusion-framework-module-http` — there is no separate mock client. Register a short-circuiting middleware through `addMiddleware` to answer requests without touching the network:
72
+
73
+ ```typescript
74
+ configurator.http.configureClient('catalog', { baseUri: 'https://api.example.com' });
75
+ configurator.http.addMiddleware(async (uri, init, next) =>
76
+ uri === 'https://api.example.com/items' ? Response.json([{ id: 1 }]) : next(uri, init),
77
+ );
78
+
79
+ const items = await fusion.modules.http.createClient('catalog').json('/items');
80
+ ```
81
+
82
+ A middleware is `(uri, init, next) => Response | Promise<Response> | Observable<Response>` — return a `Response` to answer the request, or call and return `next(uri, init)` to fall through to whichever middleware (or the real network call) is registered next. Two ways to fill that seam:
83
+
84
+ - **`addMiddleware`** — a hand-rolled middleware for a handful of routes, as above.
85
+ - **`createOpenApiMockMiddleware`** (`@equinor/fusion-framework-module-http/mock`) — adapts an `@equinor/fusion-openapi-mock` instance (`createOpenApiMock(document)`), so a real `openapi.json`/`openapi.yaml` fakes every response with no handlers written at all until an edge case needs overriding.
86
+
87
+ ## `.context`
88
+
89
+ `.context` is backed by `ContextMockConfigurator` (`@equinor/fusion-framework-module-context/mock`): context items live in an in-memory pool instead of a real context API, so a test needs no HTTP mock and no service-discovery mock to seed a known item.
90
+
91
+ ```typescript
92
+ configurator.context.setCurrentContext({ id: 'my-ctx', type: { id: 'ProjectMaster' }, value: {} });
93
+
94
+ fusion.modules.context.currentContext; // the seeded item, resolved on startup
95
+ ```
96
+
97
+ Real `ContextProvider` behaviour — `validateContext`, `resolveContext`, parent-context propagation — still runs against the seeded data; only the data source is substituted. Two layers cover different needs:
98
+
99
+ - **`setCurrentContext`/`setContexts`/`addContext`/`setRelatedContexts`** — a friendly, context-domain vocabulary for the common case: seed a known item, get it back.
100
+ - **`setResolver`** — an escape hatch for a custom `resolveContext` strategy or a shape the friendly layer did not anticipate.
101
+
102
+ Seeding a context item this way is one of two ways to fake context in a test — the other is mocking the context API's HTTP responses directly (with `.http`, optionally paired with `createOpenApiMockMiddleware`), which exercises the real `ContextModuleConfigurator`/services/HTTP pipeline instead of substituting it. Reach for `.context` to seed one known item with no transport involved; reach for `.http` when the test needs to cover that pipeline itself.