Skip to content

feat(module-analytics): add MockAnalyticsAdapter for recording tracked analytics events - #5242

Merged
odinr merged 6 commits into
feat/fusion-testingfrom
feat/analytics-mock-adapter
Aug 9, 2026
Merged

feat(module-analytics): add MockAnalyticsAdapter for recording tracked analytics events#5242
odinr merged 6 commits into
feat/fusion-testingfrom
feat/analytics-mock-adapter

Conversation

@odinr

@odinr odinr commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Why is this change needed?
Analytics-emitting code (collectors, custom adapters, app code calling provider.trackAnalytic) has no supported way to assert on tracked events in tests. Every real adapter forwards events to an external backend (console, OTLP endpoint), so there is nothing to inspect from a test.

What is the current behavior?
There is no adapter designed for test use. Asserting on tracked analytics requires either mocking IAnalyticsAdapter ad hoc in every test, or spying on collector internals.

What is the new behavior?
Adds MockAnalyticsAdapter, a genuine IAnalyticsAdapter implementation that records every tracked event in-memory instead of exporting it, exposed via a new @equinor/fusion-framework-module-analytics/mock subpath. It is registered through the module's existing setAdapter configuration mechanism — the same real, designed extension point every other adapter uses — so it does not interfere with other adapters registered alongside it.

  • getAnalytics(matcher?) — returns recorded events synchronously, filtered by event name, an array of names, or a predicate.
  • waitForAnalytic(matcher, options?) — resolves with the first matching event (immediately if already recorded, otherwise waits), with optional timeout and AbortSignal support so a test can't hang indefinitely.
  • [Symbol.dispose]() — completes the internal event stream, rejecting any pending waitForAnalytic calls with a clear error.

What is the intended behavior or invariant?
waitForAnalytic must resolve immediately for already-recorded events without waiting on the internal RxJS stream, and must never leave a pending promise unresolved once the adapter is disposed, a timeout elapses, or an AbortSignal fires — each path resolves/rejects exactly once and cleans up its timer/subscription.

Does this PR introduce a breaking change?
No.

Impact assessment:

  • Breaking changes: No
  • Version bump: Minor (new ./mock subpath and exported class)
  • Consumer impact: Purely additive/opt-in; existing adapters and collectors are unaffected
  • Downstream impact: None — new subpath export only

Review guidance:

  • packages/modules/analytics/src/mock/MockAnalyticsAdapter.ts is the core implementation — the waitForAnalytic promise/cleanup logic is the most sensitive part (declares sub before subscribing to avoid a TDZ bug if the source completes synchronously on an already-disposed adapter).
  • Added packages/modules/analytics/vitest.config.ts — this package previously had no vitest.config.ts and so was never picked up as a discoverable Vitest project by the workspace-root projects glob; tests now live under src/__tests__/, matching the convention used by sibling packages/modules/* packages.
  • Two test files, deliberately at two different levels: src/__tests__/MockAnalyticsAdapter.test.ts unit-tests the adapter's own logic in isolation (matching, timeout, abort, disposal). src/__tests__/mock/analytics-mock-adapter.test.ts wires it through the real enableAnalyticsModulesConfiguratorAnalyticsProvider.initialize() pipeline (mirroring the pattern already used by msal's and service-discovery's own mock tests) to prove events from provider.trackAnalytic and a real registered collector actually reach it, and that it doesn't shadow or break a ConsoleAnalyticsAdapter registered alongside it.

Additional context
Modeled on packages/modules/msal/src/mock/MsalMockClient.ts — a real alternate implementation registered through the module's own configuration mechanism — rather than subclassing/faking an internal singleton.

Related issues
closes equinor/fusion-core-tasks#1657

Note: this PR's base is feat/fusion-testing, not the default branch, so merging it will not immediately auto-close #1657 — GitHub only processes closing keywords on commits landing on the default branch.

Checklist

  • Confirm completion of the self-review checklist
  • Confirm TSDoc captures intent for functions, hooks, components, classes, and named arrow functions
  • Confirm iterator blocks, decision gates, RxJS chains, and complex decisions explain why they exist
  • Confirm React logic and derived values are resolved before markup when applicable
  • Confirm README/docs are updated for user-facing changes
  • Confirm changes to target branch validation
    • Included files validated
    • No new linting warnings
    • Not a duplicate PR (check existing)
  • Confirm adherence to code of conduct

…d analytics events

- Add MockAnalyticsAdapter implementing IAnalyticsAdapter, recording events
  in-memory for test assertions (getAnalytics, waitForAnalytic)
- Add ./mock subpath export
- Add vitest.config.ts to make the package a discoverable vitest project
- Document the new adapter in README.md and add a changeset

Implements equinor/fusion-core-tasks#1657
@odinr
odinr requested a review from a team as a code owner August 9, 2026 10:59
@changeset-bot

changeset-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3bed439

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 7 packages
Name Type
@equinor/fusion-framework-docs Patch
@equinor/fusion-framework-module-analytics Minor
@equinor/fusion-framework-dev-portal Major
portal-analytics Patch
@equinor/fusion-framework-cli Patch
poc-portal Patch
portal Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added 📚 documentation Improvements or additions to documentation 🚀 feature New feature or request 🧬 Modules labels Aug 9, 2026
odinr added 3 commits August 9, 2026 13:06
…module pipeline

The existing unit tests only exercise MockAnalyticsAdapter's own logic in
isolation. Add an integration test, mirroring the pattern used by msal and
service-discovery's mock tests, that wires it through the real
enableAnalytics -> ModulesConfigurator -> AnalyticsProvider.initialize()
pipeline to prove events from provider.trackAnalytic and a real collector
actually reach it, and that it doesn't interfere with other adapters.
…in vue-press

Adopt the docs/ convention already used by the event, http, and module
packages: extract the README's inline Testing section into
packages/modules/analytics/docs/testing.md, link to it from README, and
add a vue-press mirror page (@include) with a sidebar entry.
…to docs/

Move Adapters, Collectors, and Tracking Events Manually out of the README
into docs/adapters.md, docs/collectors.md, and docs/tracking-events.md,
matching the event/http/module docs/ convention. Drops duplicate
Configuration/Creating Custom Collectors content that had accumulated in
the old README's Tracking Events Manually section. README is now a slim
overview + entry points + documentation table + quick start, linking out
to docs/ for the domain reference. Mirrors and sidebar entries added in
vue-press.
@odinr
odinr requested a balanced review from Copilot August 9, 2026 11:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an in-memory analytics adapter for test assertions, exposed through a new package subpath.

Changes:

  • Adds MockAnalyticsAdapter with filtering, waiting, timeout, abort, and disposal support.
  • Adds unit and module-pipeline tests plus Vitest configuration.
  • Restructures and publishes analytics documentation.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
.changeset/docs_analytics-docs-restructure.md Records documentation changes.
.changeset/module-analytics_mock-adapter.md Records the new minor feature.
packages/modules/analytics/package.json Exports the ./mock subpath.
packages/modules/analytics/README.md Adds testing entry point and documentation links.
packages/modules/analytics/docs/adapters.md Documents analytics adapters.
packages/modules/analytics/docs/collectors.md Documents analytics collectors.
packages/modules/analytics/docs/testing.md Documents mock-adapter testing.
packages/modules/analytics/docs/tracking-events.md Documents manual event tracking.
packages/modules/analytics/src/__tests__/MockAnalyticsAdapter.test.ts Unit-tests adapter behavior.
packages/modules/analytics/src/__tests__/mock/analytics-mock-adapter.test.ts Tests real module integration.
packages/modules/analytics/src/mock/MockAnalyticsAdapter.ts Implements event recording and waiting.
packages/modules/analytics/src/mock/index.ts Exposes the mock API.
packages/modules/analytics/vitest.config.ts Registers package tests with Vitest.
vue-press/src/.vuepress/sidebar.ts Adds analytics documentation navigation.
vue-press/src/modules/analytics/docs/adapters.md Publishes adapter documentation.
vue-press/src/modules/analytics/docs/collectors.md Publishes collector documentation.
vue-press/src/modules/analytics/docs/testing.md Publishes testing documentation.
vue-press/src/modules/analytics/docs/tracking-events.md Publishes tracking documentation.
Suppressed comments (2)

packages/modules/analytics/src/mock/MockAnalyticsAdapter.ts:133

  • If waitForAnalytic is called after disposal, Subject.subscribe invokes complete synchronously, but execution then continues and allocates the timeout and abort listener after the promise was rejected. A long timeout can keep the test process alive; return when the subscription is already closed before allocating either resource.
      // Only arm a timeout when the caller opted in.
      if (ms !== undefined) {

packages/modules/analytics/src/mock/MockAnalyticsAdapter.ts:149

  • Successful matches and timeouts call cleanup, but the abort listener remains attached until the signal itself aborts because { once: true } only removes an invoked listener. Reusing a long-lived signal for many waits therefore retains one closure per completed wait; use a named abort handler and remove it from every cleanup path.
      if (signal) {
        signal.addEventListener(
          'abort',
          () => {
            cleanup();
            reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
          },
          { once: true },
        );

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/modules/analytics/src/mock/MockAnalyticsAdapter.ts
Comment thread packages/modules/analytics/docs/testing.md Outdated
Comment thread packages/modules/analytics/docs/testing.md Outdated
odinr added 2 commits August 9, 2026 13:39
…r throws

A throwing predicate matcher surfaced on RxJS's error channel, but the
subscribe observer had no error handler — the exception was reported
globally and the returned promise stayed pending forever. Add an error
handler that rejects through the same cleanup path, and cover it with a test.
…ples in testing.md

The abort example never called controller.abort() (so copying it hangs
indefinitely) and redeclared `event` from the preceding example in the
same code block. The bespoke ModulesConfigurator example destructured
`analytics` from initialize() without a type — enableAnalytics only
registers the module at runtime, so this doesn't typecheck as written;
cast to IAnalyticsProvider, matching what the integration test does.
@github-actions github-actions Bot added the 🐞 bug Something isn't working label Aug 9, 2026
@odinr
odinr merged commit 977db4f into feat/fusion-testing Aug 9, 2026
10 checks passed
@odinr
odinr deleted the feat/analytics-mock-adapter branch August 9, 2026 11:42
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 74.44% 4031 / 5415
🔵 Statements 73.42% 4844 / 6597
🔵 Functions 60.61% 1559 / 2572
🔵 Branches 64.33% 2296 / 3569
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/modules/analytics/src/mock/MockAnalyticsAdapter.ts 100% 90% 100% 100%
Generated in workflow #15203 for commit 3bed439 by the Vitest Coverage Report Action

odinr added a commit that referenced this pull request Aug 13, 2026
…d analytics events (#5242)

* feat(module-analytics): add MockAnalyticsAdapter for recording tracked analytics events

- Add MockAnalyticsAdapter implementing IAnalyticsAdapter, recording events
  in-memory for test assertions (getAnalytics, waitForAnalytic)
- Add ./mock subpath export
- Add vitest.config.ts to make the package a discoverable vitest project
- Document the new adapter in README.md and add a changeset

Implements equinor/fusion-core-tasks#1657

* test(module-analytics): verify MockAnalyticsAdapter through the real module pipeline

The existing unit tests only exercise MockAnalyticsAdapter's own logic in
isolation. Add an integration test, mirroring the pattern used by msal and
service-discovery's mock tests, that wires it through the real
enableAnalytics -> ModulesConfigurator -> AnalyticsProvider.initialize()
pipeline to prove events from provider.trackAnalytic and a real collector
actually reach it, and that it doesn't interfere with other adapters.

* docs(module-analytics): move testing docs to docs/testing.md, mirror in vue-press

Adopt the docs/ convention already used by the event, http, and module
packages: extract the README's inline Testing section into
packages/modules/analytics/docs/testing.md, link to it from README, and
add a vue-press mirror page (@include) with a sidebar entry.

* docs(module-analytics): slim README to overview, split domain docs into docs/

Move Adapters, Collectors, and Tracking Events Manually out of the README
into docs/adapters.md, docs/collectors.md, and docs/tracking-events.md,
matching the event/http/module docs/ convention. Drops duplicate
Configuration/Creating Custom Collectors content that had accumulated in
the old README's Tracking Events Manually section. README is now a slim
overview + entry points + documentation table + quick start, linking out
to docs/ for the domain reference. Mirrors and sidebar entries added in
vue-press.

* fix(module-analytics): reject waitForAnalytic when a predicate matcher throws

A throwing predicate matcher surfaced on RxJS's error channel, but the
subscribe observer had no error handler — the exception was reported
globally and the returned promise stayed pending forever. Add an error
handler that rejects through the same cleanup path, and cover it with a test.

* docs(module-analytics): fix broken abort and ModulesConfigurator examples in testing.md

The abort example never called controller.abort() (so copying it hangs
indefinitely) and redeclared `event` from the preceding example in the
same code block. The bespoke ModulesConfigurator example destructured
`analytics` from initialize() without a type — enableAnalytics only
registers the module at runtime, so this doesn't typecheck as written;
cast to IAnalyticsProvider, matching what the integration test does.
odinr added a commit that referenced this pull request Aug 14, 2026
…d analytics events (#5242)

* feat(module-analytics): add MockAnalyticsAdapter for recording tracked analytics events

- Add MockAnalyticsAdapter implementing IAnalyticsAdapter, recording events
  in-memory for test assertions (getAnalytics, waitForAnalytic)
- Add ./mock subpath export
- Add vitest.config.ts to make the package a discoverable vitest project
- Document the new adapter in README.md and add a changeset

Implements equinor/fusion-core-tasks#1657

* test(module-analytics): verify MockAnalyticsAdapter through the real module pipeline

The existing unit tests only exercise MockAnalyticsAdapter's own logic in
isolation. Add an integration test, mirroring the pattern used by msal and
service-discovery's mock tests, that wires it through the real
enableAnalytics -> ModulesConfigurator -> AnalyticsProvider.initialize()
pipeline to prove events from provider.trackAnalytic and a real collector
actually reach it, and that it doesn't interfere with other adapters.

* docs(module-analytics): move testing docs to docs/testing.md, mirror in vue-press

Adopt the docs/ convention already used by the event, http, and module
packages: extract the README's inline Testing section into
packages/modules/analytics/docs/testing.md, link to it from README, and
add a vue-press mirror page (@include) with a sidebar entry.

* docs(module-analytics): slim README to overview, split domain docs into docs/

Move Adapters, Collectors, and Tracking Events Manually out of the README
into docs/adapters.md, docs/collectors.md, and docs/tracking-events.md,
matching the event/http/module docs/ convention. Drops duplicate
Configuration/Creating Custom Collectors content that had accumulated in
the old README's Tracking Events Manually section. README is now a slim
overview + entry points + documentation table + quick start, linking out
to docs/ for the domain reference. Mirrors and sidebar entries added in
vue-press.

* fix(module-analytics): reject waitForAnalytic when a predicate matcher throws

A throwing predicate matcher surfaced on RxJS's error channel, but the
subscribe observer had no error handler — the exception was reported
globally and the returned promise stayed pending forever. Add an error
handler that rejects through the same cleanup path, and cover it with a test.

* docs(module-analytics): fix broken abort and ModulesConfigurator examples in testing.md

The abort example never called controller.abort() (so copying it hangs
indefinitely) and redeclared `event` from the preceding example in the
same code block. The bespoke ModulesConfigurator example destructured
`analytics` from initialize() without a type — enableAnalytics only
registers the module at runtime, so this doesn't typecheck as written;
cast to IAnalyticsProvider, matching what the integration test does.
odinr added a commit that referenced this pull request Aug 16, 2026
…d analytics events (#5242)

* feat(module-analytics): add MockAnalyticsAdapter for recording tracked analytics events

- Add MockAnalyticsAdapter implementing IAnalyticsAdapter, recording events
  in-memory for test assertions (getAnalytics, waitForAnalytic)
- Add ./mock subpath export
- Add vitest.config.ts to make the package a discoverable vitest project
- Document the new adapter in README.md and add a changeset

Implements equinor/fusion-core-tasks#1657

* test(module-analytics): verify MockAnalyticsAdapter through the real module pipeline

The existing unit tests only exercise MockAnalyticsAdapter's own logic in
isolation. Add an integration test, mirroring the pattern used by msal and
service-discovery's mock tests, that wires it through the real
enableAnalytics -> ModulesConfigurator -> AnalyticsProvider.initialize()
pipeline to prove events from provider.trackAnalytic and a real collector
actually reach it, and that it doesn't interfere with other adapters.

* docs(module-analytics): move testing docs to docs/testing.md, mirror in vue-press

Adopt the docs/ convention already used by the event, http, and module
packages: extract the README's inline Testing section into
packages/modules/analytics/docs/testing.md, link to it from README, and
add a vue-press mirror page (@include) with a sidebar entry.

* docs(module-analytics): slim README to overview, split domain docs into docs/

Move Adapters, Collectors, and Tracking Events Manually out of the README
into docs/adapters.md, docs/collectors.md, and docs/tracking-events.md,
matching the event/http/module docs/ convention. Drops duplicate
Configuration/Creating Custom Collectors content that had accumulated in
the old README's Tracking Events Manually section. README is now a slim
overview + entry points + documentation table + quick start, linking out
to docs/ for the domain reference. Mirrors and sidebar entries added in
vue-press.

* fix(module-analytics): reject waitForAnalytic when a predicate matcher throws

A throwing predicate matcher surfaced on RxJS's error channel, but the
subscribe observer had no error handler — the exception was reported
globally and the returned promise stayed pending forever. Add an error
handler that rejects through the same cleanup path, and cover it with a test.

* docs(module-analytics): fix broken abort and ModulesConfigurator examples in testing.md

The abort example never called controller.abort() (so copying it hangs
indefinitely) and redeclared `event` from the preceding example in the
same code block. The bespoke ModulesConfigurator example destructured
`analytics` from initialize() without a type — enableAnalytics only
registers the module at runtime, so this doesn't typecheck as written;
cast to IAnalyticsProvider, matching what the integration test does.
odinr added a commit that referenced this pull request Aug 16, 2026
…kbooks (#5297)

* feat(openapi-mock): add package for generating OpenAPI-shaped fake responses

Adds @equinor/fusion-openapi-mock, a framework-agnostic utility that fakes
OpenAPI 3 responses straight from a parsed spec document. Every operation
with an operationId is faked from its declared success response schema,
with overrides/register for edge cases, seed for repeatable output, and
a FieldFakerMap for realistic per-field values via @faker-js/faker.

No dependency on any HTTP or routing framework: resolve({ method, path,
query }) returns a plain { status, mock }, so it composes with
@equinor/fusion-framework-module-http's mock router or any other server.

Related: equinor/fusion-core-tasks#1660

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore(deps): update lockfile for @equinor/fusion-openapi-mock

* fix(openapi-mock): address review feedback on route matching, response fallback, and validation

- sort operations by specificity so literal routes win over templated ones
- fall back to lowest declared numeric status when no 2xx/default response exists
- validate parsed YAML faker maps and fetched documents are plain objects
- use an isolated faker instance per generation instead of mutating the global singleton
- refactor createOpenApiMock and apply-field-fakers into smaller, focused functions

* refactor(openapi-mock): favor readability and avoid mutable locals

- replace && shorthand and let-based branches with explicit if/early-return
- extract resolveResponseSchema, parseDocumentText helpers
- rewrite resolvePointer's pointer walk with Array#reduce instead of a mutable loop variable

* feat(module-msal): add a mock entry point for testing without credentials or network

Adds ./mock, so applications can run against the auth module without
credentials or network access:

    import { enableMsalMock, createMsalMockClient } from '@equinor/fusion-framework-module-msal/mock';

    enableMsalMock(configurator, (builder) => {
      builder.setAccount({ name: 'Ada Lovelace' });
    });

Only the MSAL client is substituted:

- MsalMockClient resolves tokens in-process and takes the same
  MsalClientConfig as the real MsalClient, keeps a real account cache
  keyed by homeAccountId (matching MSAL's getAccount/getAllAccounts
  semantics), and produces structurally valid, unsigned JWTs.
- The client construction seam (_createClient/_createClientConfig) is
  extracted from MsalConfigurator._processConfig, giving a supported
  seam for authenticating through something other than Entra ID. No
  client is built when the module is hoisted onto a host application's
  provider, since a client built during configuration would be
  discarded or shadow the host's signed-in user (_isHoisted).
- MsalMockConfigurator.setAccount lets a test declare the signed-in
  user on the builder; the account is applied before MsalProvider
  initializes, so the provider's own start-up path (including
  automatic login when signedOut + requiresAuth) acts on it.
- The config schema moved into MsalConfig.schema.ts with
  MsalConfigExtension, an extension point mock configuration merges
  into without the builder needing to cast past its type.

MsalProvider, MsalConfigurator and IMsalProvider are otherwise
untouched; the mock module's configure differs from the real module,
initialize does not.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(module-http): add a mock entry point with middleware and adapters

Adds @equinor/fusion-framework-module-http/mock, so HTTP clients answer
requests from registered route handlers instead of the network:

    import { enableHttpMock } from '@equinor/fusion-framework-module-http/mock';

    enableHttpMock(configurator, (builder) => {
      builder.configureClient('catalog', { baseUri: 'https://api.example.com' });
      builder.get('/items', () => Response.json([{ id: 1 }]));
    });

Route handlers are Fetch-standard middleware -
(request: Request) => Response | undefined | Promise<...> - tried in
registration order, with undefined falling through to the next one.
One router is shared across every named client a HttpMockConfigurator
builds, matched against each request's fully resolved URL.

HttpClient gains a _performFetch(uri, init) seam, isolated from
_fetch$ so a test double replaces only the network call while request
preparation, the response pipeline, and abort handling run unchanged.

Two adapters drop in an existing backend with no hard dependency on it:

- fromExpressStyleHandler(handler) adapts an Express-style (req, res)
  handler (or a whole framework built from them, like openapi-backend).
- fromOpenApiMock(openApiMock) adapts an @equinor/fusion-openapi-mock
  instance (duck-typed, no package dependency), so a real
  openapi.json/openapi.yaml fakes every response with no handlers
  written at all.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(module-service-discovery): add a mock entry point backed by an in-memory registry

Adds @equinor/fusion-framework-module-service-discovery/mock, so service
discovery resolves from an in-memory registry instead of the network:

    import { mockServiceDiscovery } from '@equinor/fusion-framework-module-service-discovery/mock';

    mockServiceDiscovery(configurator, { services: [{ key: 'apps', uri: 'https://apps.test' }] });

ServiceDiscoveryMockConfigurator builds the registry on the builder
itself - setBaseUri, addService, addServices, removeService,
setServices, setResolveUnknownServices - and the client is constructed
from that registry when the module builds its config, so a test never
has to construct a client just to add a service.

setBaseUri also lets the default service endpoints point at a local
mock server such as http://localhost:3000, so an application can make
real HTTP calls against Mockoon, Prism or the dev server without a
service worker intercepting requests.

Exposes the discovery client on the provider as .client, mirroring
MsalProvider.client, so a test can spy on it directly:

    vi.spyOn(fusion.modules.serviceDiscovery.client, 'resolveService').mockResolvedValue(service);

configureServiceDiscovery now also accepts a synchronous callback - the
underlying builder always allowed it, only the exported type required
a promise.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(module): fix configurator phase ordering and dot-path optional branches

Two independent fixes needed by the framework mock work:

- Module re-registration now replaces the prior module's configure,
  afterConfig, and afterInit callbacks instead of appending to them,
  keyed by module name (_dedupeModulesByName /
  _removeModuleCallbacks). This prevents stale callback execution
  when a mock module (e.g. enableMsalMock) overrides a real module
  registration, and fixes configurator phases running out of order
  or skipping post-configure hooks in certain initialization paths.
- DotPath now unwraps an optional object property with NonNullable
  the same way DotPathType already did, so a path BaseConfigBuilder
  understands is one _set no longer refuses. Given
  { foo?: { bar: string } }, 'foo.bar' is now a valid path, matching
  'foo' which was already allowed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(framework): add a mock entry point that composes each module's own test double

Adds @equinor/fusion-framework/mock for initializing the framework in a
test:

    import { mockFramework } from '@equinor/fusion-framework/mock';

    const fusion = await mockFramework((configurator) => {
      configurator.msal.setAccount({ name: 'Ada Lovelace' });
      configurator.serviceDiscovery.setBaseUri('http://localhost:6669');
    });

mockFramework runs the real configure -> initialize pipeline with the
real built-in modules and substitutes only the boundaries that leave
the process - the MSAL client and the service discovery client.
Module wiring, configuration validation and lifecycle hooks behave as
they do in production.

FrameworkMockConfigurator gains _pin/_getConfig, so a module supplied
through TModules gets the same kind of named accessor .msal and
.serviceDiscovery already have, rather than that pinning being
hand-written once per built-in mock. .services, .context and
.telemetry are exposed the same way, though they still perform real
I/O until those modules have a test double of their own. event is
intentionally left out - its configure factory reads ref to wire
event bubbling to a parent event provider when hoisted, and pinning
it would call configure() with ref always undefined, silently
disabling that bubbling.

The entry point owns no mock logic of its own: each module exports
its own test double from its own ./mock entry point, and this one
only composes the built-in set. It has no test-runner dependency and
provides no mocking API, since replacing an individual call belongs
to the test runner.

init() no longer throws when no DOM is present - the window.Fusion
assignment (for portal shells and widgets) is now skipped when
window is undefined, so a test runner using the node environment or
a server-side render no longer fails with
'ReferenceError: window is not defined'.

Also restructures documentation so each README is an entry point
rather than a manual: long-form content (including each package's
testing.md) moved into per-package docs/ folders, matching the
convention already used by @equinor/fusion-framework-module and
@equinor/fusion-framework-module-http.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address Copilot review comments on PR #5205

- msal-config-schema: reject unparseable version instead of coercing to "null"
- HttpMockRouter: reset RegExp lastIndex so global/sticky match patterns are reusable across requests
- extract decodeJwtSegment into its own file, fix atob/UTF-8 round-trip for non-ASCII claims
- service-discovery docs: dedupe heading, close dangling code fence

* fix(http-mock): await handleRequest in fromExpressStyleHandler

Prevents an async Express-style handler's rejection from becoming an
unhandled promise rejection instead of failing the middleware call.

* feat(module-event): add EventModuleConfigurator, waitForEvent/watchEvents, and operators subpath (#5241)

* feat(module-event): add EventModuleConfigurator, waitForEvent/watchEvents, and operators subpath

Add EventModuleConfigurator, a BaseConfigBuilder-based configurator with fluent
setOnDispatch/setOnBubble setters, replacing direct property assignment on the
config object (kept as a deprecated path, no major bump needed).

IEventModuleConfigurator is renamed to EventModuleConfig and converted from an
interface to a type; IEventModuleConfigurator is kept as a deprecated alias.

Narrow the dispatchEvent return type for registered FrameworkEventMap keys and
pre-constructed event instances so callers get back the specific event type.

Fix event$ to emit only after listeners and the bubble hook have run, matching
its documented contract (it previously emitted before dispatch).

Add waitForEvent/watchEvents test helpers and a ./operators subpath export for
filterEvent (moved from src/filter-event.ts to src/operators/filter-event.ts).

Rename configurator.ts -> EventModuleConfigurator.ts and provider.ts ->
EventModuleProvider.ts for filename-convention compliance, and add a docs/
folder (configuration, observable-patterns, lifecycle, testing).

* docs(vue-press): align event module docs with docs/ folder, move React page

Trim vue-press/src/modules/event/README.md to an @include of the package
README, add a docs/ mirror (configuration, observable-patterns, lifecycle,
testing) that @includes the package's own docs, matching the http/module
pattern.

Move the event React bindings page from event/react.md to
react/event/README.md, alongside react/router/, as an @include of
@equinor/fusion-framework-react-module-event's README. Update the sidebar to
match.

* style(module-event): apply Biome formatting fixes flagged by reviewdog

* fix(module-event): fix waitForEvent TDZ on sync completion, correct doc inaccuracies

* feat(module-analytics): add MockAnalyticsAdapter for recording tracked analytics events (#5242)

* feat(module-analytics): add MockAnalyticsAdapter for recording tracked analytics events

- Add MockAnalyticsAdapter implementing IAnalyticsAdapter, recording events
  in-memory for test assertions (getAnalytics, waitForAnalytic)
- Add ./mock subpath export
- Add vitest.config.ts to make the package a discoverable vitest project
- Document the new adapter in README.md and add a changeset

Implements equinor/fusion-core-tasks#1657

* test(module-analytics): verify MockAnalyticsAdapter through the real module pipeline

The existing unit tests only exercise MockAnalyticsAdapter's own logic in
isolation. Add an integration test, mirroring the pattern used by msal and
service-discovery's mock tests, that wires it through the real
enableAnalytics -> ModulesConfigurator -> AnalyticsProvider.initialize()
pipeline to prove events from provider.trackAnalytic and a real collector
actually reach it, and that it doesn't interfere with other adapters.

* docs(module-analytics): move testing docs to docs/testing.md, mirror in vue-press

Adopt the docs/ convention already used by the event, http, and module
packages: extract the README's inline Testing section into
packages/modules/analytics/docs/testing.md, link to it from README, and
add a vue-press mirror page (@include) with a sidebar entry.

* docs(module-analytics): slim README to overview, split domain docs into docs/

Move Adapters, Collectors, and Tracking Events Manually out of the README
into docs/adapters.md, docs/collectors.md, and docs/tracking-events.md,
matching the event/http/module docs/ convention. Drops duplicate
Configuration/Creating Custom Collectors content that had accumulated in
the old README's Tracking Events Manually section. README is now a slim
overview + entry points + documentation table + quick start, linking out
to docs/ for the domain reference. Mirrors and sidebar entries added in
vue-press.

* fix(module-analytics): reject waitForAnalytic when a predicate matcher throws

A throwing predicate matcher surfaced on RxJS's error channel, but the
subscribe observer had no error handler — the exception was reported
globally and the returned promise stayed pending forever. Add an error
handler that rejects through the same cleanup path, and cover it with a test.

* docs(module-analytics): fix broken abort and ModulesConfigurator examples in testing.md

The abort example never called controller.abort() (so copying it hangs
indefinitely) and redeclared `event` from the preceding example in the
same code block. The bespoke ModulesConfigurator example destructured
`analytics` from initialize() without a type — enableAnalytics only
registers the module at runtime, so this doesn't typecheck as written;
cast to IAnalyticsProvider, matching what the integration test does.

* feat(azure-identity,msal-node): add MockAuthProvider test double for auth module (#5244)

* feat(azure-identity,msal-node): add MockAuthProvider test double for auth module

Add a configurable IAuthProvider test double, exported from a new /mock
subpath in each package (@equinor/fusion-framework-module-azure-identity/mock,
@equinor/fusion-framework-module-msal-node/mock).

Unlike token_only mode's static-token providers (AuthProviderTokenOnly /
AuthTokenProvider), MockAuthProvider actually implements login/logout,
moving between signed-in and signed-out state, and exposes setAccount,
setAccessToken and setExpiresOn so a test can control the returned token
and simulate an expired token to exercise refresh logic. No real network
calls are made.

Implemented as two mirrored (twin) implementations rather than a shared
package, since azure-identity's and msal-node's IAuthProvider interfaces
differ in login's return type (AuthRecord vs. AuthenticationResult) and
neither package depends on the other.

Registers via enableAuthMock(configurator, configure?), a full 'auth'
module replacement mirroring @equinor/fusion-framework-module-msal/mock's
enableMsalMock, so no special-cased wiring is needed in either module.

Closes equinor/fusion-core-tasks#1665.

* fix(azure-identity,msal-node): make enableAuthMock generic over TModules/TRef

Replaces the IModulesConfigurator<any, any> parameter with a generic
<TModules extends Array<AnyModule> = Array<AnyModule>, TRef = unknown>
signature, so the optional configure callback keeps the caller's actual
TRef type instead of erasing it to unknown.

Addresses review feedback on PR #5244 (copilot-pull-request-reviewer).

* fix(azure-identity,msal-node): fix double-registration in mock README example

The Testing section called enableAuthMock(configurator) once for its
return value, then again with a configure callback to seed the account.
The second call replaces the first provider (enableAuthMock always
registers last), so the retained `auth` reference from the first call
was no longer the active provider by the time login/acquireAccessToken
ran against it.

Collapse to a single enableAuthMock call that both seeds the account
and returns the provider that stays registered.

Addresses review feedback on PR #5244 (copilot-pull-request-reviewer).

* feat(module-bookmark): add BookmarkMockClient and enableBookmarkMock (#5245)

* feat(module-bookmark): add BookmarkMockClient and enableBookmarkMock

Add a purpose-built mock for the bookmark module, exported from a new
`/mock` subpath (`@equinor/fusion-framework-module-bookmark/mock`),
following the client-boundary-swap pattern already used by msal's
`enableMsalMock`.

- `BookmarkMockClient`: in-memory `IBookmarkClient` backed by a Map of
  seeded bookmarks and a Set of favorite ids.
- `BookmarkMockConfigurator`: extends the real `BookmarkModuleConfigurator`
  with `setBookmarks`/`setCurrentBookmark`/`setFavorite`, and falls back to
  trivial application/context resolvers when no real `app`/`context`
  module is registered (both are required by the config schema).
- `enableBookmarkMock`/`bookmarkMockModule`: module descriptor mirroring
  the real module, only swapping `configure`.

Create, update, delete, and favorite calls all flow through the real
`BookmarkProvider` and `bookmark-flows/` epics unmodified.

Related: equinor/fusion-core-tasks#1667

* fix(module-bookmark): match sourceSystem, strip list payloads, normalize null payload in mock client

- matchesFilter now checks filter.sourceSystem (identifier/name/subSystem),
  since BookmarkProvider.getAllBookmarks always passes sourceSystem through
  the filter.
- getAllBookmarks now strips payloads from returned bookmarks, matching the
  real BookmarkClient's list behavior.
- updateBookmark now normalizes an explicit payload: null clear to undefined,
  matching setBookmarkData and avoiding a later getBookmarkData() emitting
  null as T.

Addresses PR #5245 review comments on BookmarkMockClient.ts:44, :140, :270.

* fix(module-bookmark): respect inherited resolve.application/context on mock fallback

Gate the mock's fallback application/context resolvers on initial?.resolve
as well as this._has(...), so a nested mock configurator does not overwrite
resolvers already inherited from a parent config.

Addresses PR #5245 review comment on BookmarkMockConfigurator.ts:150.

* test(module-bookmark): assert getBookmark rejects for an unknown id

Adds a Vitest assertion exercising the async error path through the real
BookmarkProvider.getBookmark fetch flow, so a regression that stops missing
ids from surfacing as observable errors would fail this suite.

Addresses PR #5245 review comment on BookmarkMockClient.ts:125.

* docs(module-bookmark): add required created/createdBy fields to mock seed example

The README's setBookmarks example omitted created/createdBy, which are
required on Bookmark, so copying the snippet as-is produced a type error.

Addresses PR #5245 review comment on README.md:245.

* docs(module-bookmark): add required created/createdBy fields to changeset example

The changeset's setBookmarks example omitted created/createdBy, which are
required on Bookmark, so the advertised snippet did not type-check.

Addresses PR #5245 review comment on .changeset/module-bookmark_mock-entry-point.md:15.

* refactor(module-context): extend BaseConfigBuilder and reorganize internal files

- ContextModuleConfigurator now extends BaseConfigBuilder (createConfigAsync),
  matching the pattern used by module-event/module-http/module-msal
- rename configurator.ts -> ContextModuleConfigurator.ts, split its exported
  types into ContextModuleConfig.ts and ContextModuleConfigurator.interface.ts
- move get/query/related-context-selector.ts into src/selectors/
- move parse-context-item.ts into src/utils/ and export it from utils/index.ts
- delete unused ContextConfigBuilder.ts
- add ContextModuleConfigurator.test.ts covering the new builder surface

No public export names change; index.ts still exports the same
ContextModuleConfigurator/IContextModuleConfigurator/ContextModuleConfig.

* feat(module-context): add mock and mock/fixtures entry points

Add ./mock (ContextMockConfigurator, enableContextMock) — a
ContextModuleConfigurator backed by an in-memory pool instead of a real
client. Only the data source is substituted; validateContext, resolveContext,
and parent-context propagation still run through the real ContextProvider.
Friendly seeding methods (setCurrentContext/setContexts/addContext/
setRelatedContexts) cover the common cases; setResolver is the escape hatch.

Add ./mock/fixtures (createContextItems, createContextItemFactory) — realistic
ContextItem generators backed by an optional @faker-js/faker peer dependency,
kept on a separate entry point so enableContextMock never pulls faker in.

Adds an http workspace dependency (for the vitest project setup) and a
package-level vitest.config.ts/test script.

* docs(module-context): restructure README into docs/ and mirror in vue-press

Move long-form content out of README.md into docs/data-model.md (ContextItem/
ContextItemType shape, query/related parameter types), docs/lifecycle.md
(setting/resolving context, initial-context resolution, parent/child
propagation) and docs/recipes.md (OData query params, path rewriting,
accepting related context type families, skipping initial-context lookup,
custom search errors), matching the module/http convention.

README.md keeps the elevator pitch, a new "How it fits together" section
covering the module's dependency graph (services required, event/navigation
optional and auto-detected) and hierarchy-aware parent connection, and a
documentation table linking to the three new pages.

vue-press/src/modules/context mirrors the split with @include stubs, and the
sidebar gains an Overview/Data model/Lifecycle/Recipes breakdown for Context.

* feat(framework): back mock context accessor with ContextMockConfigurator

FrameworkMockConfigurator.context now pins contextMockModule (from
@equinor/fusion-framework-module-context/mock) instead of the real context
module, so `.context` seeding methods (setCurrentContext/setContexts/
addContext/setRelatedContexts/setResolver) work the same way `.http` and
`.serviceDiscovery` already do — no more real I/O through context in tests.

docs/testing.md, docs/testing-extending.md and docs/testing-api.md document
both context mocking strategies: the in-memory ContextMockConfigurator above,
and mocking the context API's HTTP responses directly for tests that need to
exercise the real configurator/services/HTTP pipeline.

* chore: add changesets for context mock entry point and docs restructure

* fix(module-context): document createConfig as a breaking change

Extending BaseConfigBuilder changes the public createConfig method from
Promise<ContextModuleConfig> to Observable<ContextModuleConfig>. Bump
the changeset to major with migration guidance to createConfigAsync,
matching the precedent set when service-discovery/msal adopted the
same base class.

Addresses review comment on PR #5243 (discussion_r3744095539).

* docs(module-context): clarify setResolver only affects id-based lookup

setResolver overrides ContextClient.resolveContext(id) (used by
setCurrentContextById and the mock's initial context), not
ContextProvider.resolveContext(item), which always resolves related
context through setRelatedContexts regardless of setResolver. Reword
the class and method docs so the two same-named methods aren't
conflated.

Addresses review comment on PR #5243 (discussion_r3744095551).

* test(framework): assert seeding via .context reaches the built framework

The existing test only checked that .context is defined, which also
passed with the real ContextModuleConfigurator. Add a case that seeds
via configurator.context.setCurrentContext(...), initializes the
framework, and asserts fusion.modules.context.currentContext, covering
the _pin(contextMockModule) composition path.

Addresses review comment on PR #5243 (discussion_r3744095562).

* feat(module-telemetry): add MockTelemetryAdapter and wire it into FrameworkMockConfigurator

Closes equinor/fusion-core-tasks#1708

Adds a collecting MockTelemetryAdapter + TelemetryMockConfigurator behind a
new ./mock subpath, following the MockAnalyticsAdapter shape. Wires
FrameworkMockConfigurator.telemetry to the mock configurator so telemetry
tracked through a mocked framework instance never reaches Application
Insights or any real endpoint.

* fix(module-telemetry): close waitForItem resource leaks and simplify changeset example

* feat(module-feature-flag): add mock entry point for seeding test feature flags

Add `FeatureFlagMockConfigurator`, `enableFeatureFlagMock`, and a `./mock`
subpath so a test can seed flags in-memory via `addFeature`/`setFeatures`,
with no `localStorage`/URL dependency. Toggling still runs through the real
`FeatureFlagProvider`.

Also fixes a missing `telemetry` row in the framework's testing.md accessor
table (pre-existing gap, unrelated to this change).

Closes equinor/fusion-core-tasks#1707.

* test(module-feature-flag): assert async configure callbacks are awaited before initialize resolves

Addresses review comment on module.ts:49.

* docs(module-feature-flag): document the ./mock subpath in the README's API Reference

Addresses review comment on package.json:23.

* chore: add changeset for the testing.md telemetry accessor fix

Addresses review comment on testing.md:30.

* refactor(module-http): replace router-based mock with addMiddleware on the real configurator

Closes equinor/fusion-core-tasks#1706

Replaces enableHttpMock/HttpMockConfigurator/HttpMockRouter with addMiddleware
on the real HttpClientConfigurator, via a new HttpMiddlewareHandler that wraps
_performFetch instead of swapping the configurator out. A middleware calling
next(uri, init) falls through to the real call (or the next middleware)
unchanged, so a test exercises the exact same client and configuration a real
app registers.

createRouterMiddleware and createOpenApiMockMiddleware replace the old router
and its Express-style adapters, covering the same handler/OpenAPI-fake cases
without depending on a real routing library.

* docs(module-http): mirror the testing guide into vue-press

Adds the http module's docs/testing.md via the same @include pattern used
by client-configuration.md and server-sent-events.md, with a matching
sidebar entry.

* fix(framework): pin FrameworkMockConfigurator.http to the real HttpClientConfigurator

FrameworkMockConfigurator.http returns the real IHttpClientConfigurator now
that module-http's mock support is addMiddleware on that real configurator,
not a separate mock-specific one. Fake a response with
.http.addMiddleware(...) instead of swapping the module out.

Updates testing.md/testing-api.md/testing-extending.md to match.

* test(module-context): migrate off the removed enableHttpMock API

enableHttpMock/http.get(...) no longer exist; rebuilds the fixture with
configureHttp + createRouterMiddleware against configurator.addConfig,
matching the addMiddleware-based mocking module-http now exposes.

Updates the README's mention of the HTTP-mock alternative to match.

* feat(module-app): add MockAppClient mock entry point

Part of equinor/fusion-core-tasks#1706

Adds MockAppClient, exported from a new ./mock subpath, so a test can serve
one app's own manifest and config locally instead of contacting the app
service:

- getAppManifest resolves locally only for manifest.appKey with no tag.
- getAppConfig resolves locally for manifest.appKey when tag is absent or
  equal to manifest.build?.version.
- Every other request still goes through the real AppClient it wraps.

Also exports AppConfig as a value from the package root (previously
type-only), so a test can construct one directly.

* feat(app): add mockAppModules and AppMockConfigurator /mock entry point

Closes equinor/fusion-core-tasks#1706

Adds a ./mock entry point: mockAppModules runs an application's real module
pipeline — the real event/http/msal modules, the real AppConfigurator
configuration pipeline and real lifecycle — against a mocked parent Fusion
instance, so a test exercises the wiring an application actually depends on
instead of a reimplementation of it.

AppMockConfigurator extends the real AppConfigurator, pinning http and msal
to the same test doubles FrameworkMockConfigurator uses so .http/.msal are
reachable synchronously; event is deliberately left unpinned since its
configure factory needs a real ref. Its own addConfig override redirects a
pinned module's later registrations at the pinned descriptor. The base
AppConfigurator constructor's own call into _configureHttpClientsFromAppConfig
is deferred (via a no-op override + an explicit super call after pinning) so
it never runs before this class's own fields exist.

enableAppManifestMock registers the app module on a parent mockFramework
configurator, serving one app's manifest/config locally while delegating
everything else to service discovery. mockAppModules uses it to build its
zero-configuration default parent.

Restructures README.md into an entry point pointing at docs/http-clients.md,
docs/bookmarks.md and docs/testing.md, matching the convention already used
by module-http and module-msal.

* fix(app): remove dead initialize-app-configurator.ts duplicate

initialize-app-modules.ts superseded this file when initializeAppModules gained the TConfigurator type parameter, but the old file was left in place -- nothing imported it, so it was dead code that still compiled and still linted (fusion-lint's require-tsdoc flagged it for the missing @template tags the newer file already has).

* style: apply biome formatting fixes

Long import lists wrapped onto multiple lines, a ternary split across lines,
and a single-quoted string containing an apostrophe rewritten with double
quotes -- no behavior change.

* refactor(module-http): flatten mock/adapters into mock/, fix lint warnings

mock/ only ever had one barrel (index.ts) plus an adapters/ subfolder holding
the 3 files it re-exported -- two barrels doing the same job for exactly one
category of file. Moved create-open-api-mock-middleware.ts,
create-router-middleware.ts and resolve-open-api-mock-response.ts up into
mock/ directly and dropped the adapters/ barrel, fixing relative import
depths and OpenApiMockLike's re-export path along the way.

Also fixes the fusion-lint warnings the files carried before the move:
renamed each file to match its exported symbol (filename-convention),
added the missing intent comments on create-router-middleware.ts's
if-blocks and .map() (require-intent-comment), and replaced a comma-operator
expression in compilePath with an explicit if/return (noCommaOperator).

* fix(module-app): treat only an absent tag as 'no tag' in MockAppClient

getAppManifest's !args.tag and getAppConfig's isOwnTag both treated an
explicit empty-string tag the same as an absent one, so getAppManifest({
appKey, tag: '' }) and getAppConfig({ appKey, tag: '' }) answered locally
instead of delegating, contradicting the documented 'tag is absent' rule.
Compare against undefined explicitly instead of a falsy check.

Adds MockAppClient.test.ts covering both methods' local-vs-delegate
decision, including the explicit-empty-string-tag case.

* docs(vue-press): add changeset for the http module testing page

The new vue-press page and sidebar entry is a consumer-facing docs change
to the published @equinor/fusion-framework-docs package, which needed a
changeset like every other vue-press page addition in this repo.

* chore(changesets): bump http mock changesets to major

enableHttpMock, HttpMockConfigurator, and HttpMockRouter were removed
from @equinor/fusion-framework-module-http's /mock entry point, and
FrameworkMockConfigurator.http's return type changed from a mock-specific
configurator to the real IHttpClientConfigurator -- both are breaking
changes for existing consumers and need a major bump, not minor.

* docs(app): correct mockAppModules' network-access claim

The http module registered by AppMockConfigurator is the real
HttpClientConfigurator -- addMiddleware only short-circuits requests a
middleware matches with a response, so an unmatched client or a middleware
that calls next still reaches the real network. msal is the only module
genuinely backed by a test double. Reworded the doc comment to describe
what is actually substituted instead of promising no network access.

* fix(module-http): abort the real network call through middleware on abort()

next(...) resolves through a Promise (HttpMiddlewareHandler's toPromise),
so a middleware calling it creates a subscription to _performFetch that
sits outside the tree takeUntil(this._abort$) tears down -- the outer
request settled on abort(), but the underlying fetch kept running.

abort() now also aborts a per-request AbortSignal combined into the
request init, so _performFetch (fromFetch by default) is cancelled
directly regardless of whether middleware severed the RxJS teardown
chain. Adds a regression test with a pass-through middleware asserting
the underlying fetch's signal is aborted.

* test: add react-app hook testing utilities and msal proxy/hoisting coverage

- add render-app-hook testing utility + useAccessToken hook test in react/app
- add msal-hoisting coverage in packages/app
- add createProxyProvider behavior coverage in modules/msal

* test(react-app): add useTrackFeature hook tests

Add @equinor/fusion-framework-module-telemetry as a devDependency so tests
can read the mock telemetry adapter via fusion.modules.telemetry.getAdapter('mock').

* test(react-app): render Apploader against a real dynamic-import child app script

Add renderAppComponent, a component-level counterpart to renderAppHook, and
use it in a new Apploader.test.tsx that exercises the real useApploader ->
App.initialize() -> dynamic import() pipeline (no vi.mock of createApp or
the import mechanism), backed by a fixture child app script.

* test(react-app): add useCurrentBookmark hook tests

Covers the app-scoped happy path, filtering out a bookmark that belongs to a different app, and the framework-scoped fallback with its deprecation warning. Adds @equinor/fusion-framework-module-bookmark as a devDependency for the mock helpers.

* test(react-app): add useCurrentContext hook tests

Covers resolving the seeded initial context and switching to another seeded item via setCurrentContext, against the app-scoped context module mock. Adds @equinor/fusion-framework-module-context as a devDependency for the mock helpers.

* test(react-app): add useFeature hook tests

Covers resolving an app-scoped flag, app-over-framework override on a shared key, a framework-only flag surfacing through the merged stream, and toggleFeature inverting the current value with no explicit argument.

* test(react-app): add useHelpCenter hook tests

Covers every open* action, asserting the framework event dispatched on the app's event module carries the expected page and payload.

* test(react-app): add useToken and useCurrentAccount hook tests

Completes coverage of the msal folder alongside the existing useAccessToken tests: useToken's full AuthenticationResult happy/error paths, and useCurrentAccount against the default mock user, a custom msal.setAccount() override, and the signed-out case.

* test(react-app): add useAppSetting and useAppSettings hook tests

Covers the settings folder's public hooks against the real fetch/update round-trip (via an in-memory apps-client mock): resolving persisted values, default-value fallback before resolution, persisting an update (direct value and updater-function forms), and surfacing a persistence failure through onError instead of throwing. useAppSettingsStatus is internal (not exported from settings/index.ts) and is exercised indirectly through these onLoading/onUpdating-adjacent paths.

* docs(react-app): fix HTTP config example and nullable/async return types

Fixes issues found while writing hook tests: README/createComponent examples used configurator.http.configureClient (test-only mock API) instead of the real configurator.configureHttpClient; setCurrentContext/useFrameworkCurrentContext were documented as void-returning when they actually return void | Promise<ContextItem | null>; AuthenticationResult.account/expiresOn were documented as non-nullable; useCurrentBookmark's deprecated framework-scope fallback and its console warning were undocumented.

* docs(react-app): add app testing guide

* fix(react-app): unmount rendered hooks in msal token tests

Prevents the async token-acquisition effect from flushing a state
update after happy-dom's window is torn down at the end of the test
file, which CI surfaced as an uncaught 'window is not defined'
exception (all assertions still passed, but the run was marked
failed).

* test: apply Biome line-length formatting suggestions

Collapse two waitFor assertions that fit within the line-length limit
back onto a single line, per Biome/reviewdog suggestions on PR #5271.

* fix(app): treat empty-string assetUri as an explicit override

if (assetUri) silently ignored a caller-supplied empty string, which is a meaningful value (selects a root-relative script path). Check against undefined instead so the override always applies when the caller passes the option.

* docs(react-app): clarify network-boundary faking scope; add error-state example

The docs overstated that renderAppHook/renderAppComponent fake the entire network boundary. Only requests a seeded router middleware answers are faked; unmatched requests still reach the real network. Also documents renderAppComponent under the /testing subpath table, and adds an error-state test example mirroring Apploader.test.tsx.

* docs(react-app): remove invalid @template Result tag from renderAppComponent

renderAppComponent has no Result type parameter; the tag referenced a generic that does not exist on this function and would fail TSDoc validation.

* test(react-app): assert the seeded bookmark actually resolved before checking the app-key filter

The previous waitFor(() => expect(result.current).toBeDefined()) was a no-op since result.current is always a defined object. Now waits on the unfiltered useAppModules().bookmark provider to resolve the seeded bookmark first, so the assertion that useCurrentBookmark hides it can't pass trivially against a still-pending value.

* docs(changeset): qualify cross-repo issue reference

Closes #1716 resolved to an unrelated PR in this repo. Qualifying as equinor/fusion-core-tasks#1716 points at the correct issue while preserving auto-close-on-merge.

* test: apply Biome line-length formatting suggestions

Reformat two msal-hoisting.test.ts call expressions and two useCurrentContext.test.tsx object literals that exceed the line-length limit, per Biome/reviewdog suggestions on PR #5271.

* chore(deps): bump vitest to ^4.1.10

Bumps vitest and @vitest/coverage-v8 across the workspace, and migrates
vitest.config.ts package.json imports to the `with { type: 'json' }`
import attribute required by the newer vitest/vite toolchain.

* feat(cli): add `ffc app test` command

Runs the application's own Vitest suite with manifest/config/module-configurator resolved
the same way `ffc app build`/`ffc app dev` do, exposed via a Vite plugin's virtual modules
to @equinor/fusion-framework-react-app/vitest's test/render fixtures.

Renamed packages/cli/src/lib/testing -> lib/vitest and the ./testing export -> ./vitest.

* Update packages/cli/src/cli/commands/app/test.command.ts

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(cli): throw on an explicit but missing --configure path

An explicitly supplied configure path is a user request, not a convention lookup — a typo
should fail loudly instead of silently running the test suite without the app's modules.

* refactor: clean up code formatting for better readability

* feat(react-app): vitest render/test-app fixtures under src/vitest

- rename src/testing -> src/vitest, add test-app/scope fixture chain
  (test-app.tsx, test.tsx, scope/*) for rendering app components and
  hooks with a fully wired Fusion test environment
- fix(react): useFrameworkModule warns with the correct module name
- fix(module-context): stop warning when there's no initial context
  to resolve
- fix(module-msal): drop stray debug console.log in resolve-version
- fix(log): guard process.env access for Vitest Browser Mode
- chore: biome format fixes surfaced by pnpm check (unrelated pre-existing
  drift in cli/linting/event packages)

* refactor: extract react-app vitest helpers into vitest-plugin-react-app

Move renderAppHook/renderAppComponent/testApp and the app-test Vite plugin out
of @equinor/fusion-framework-react-app's ./vitest entry-point and
@equinor/fusion-framework-cli's ./vitest entry-point into a new standalone
package, @equinor/fusion-framework-vitest-plugin-react-app, built on
vitest-browser-react instead of @testing-library/react.

- Remove the ffc app test CLI command and cli/src/lib/vitest (superseded by
  appTestVitePlugin registered directly in a project's vitest.config.ts).
- Add packages/vitest-plugin/react-app with README, appTestVitePlugin,
  renderAppHook, renderAppComponent, testApp, and a /test entry-point
  exporting pre-seeded test/render fixtures.
- Update react-app's README/docs and package.json to drop ./vitest and
  point at the new package instead.
- Fix stray console.log in msal's resolveVersion mismatch warning, and guard
  fusion-log's process.env access for environments without a Node process
  global (Vitest Browser Mode).
- Install Playwright browsers in CI for the new package's and react-app's
  Vitest Browser Mode tests.
- Add CODEMAP.md entry for the new package.

Changesets included for all affected packages.

* ci: fix Playwright install invocation to filter into a package that depends on it

pnpm exec at the repo root only resolves bins from root's own node_modules,
and playwright is a devDependency of react-app/vitest-plugin-react-app only,
not the workspace root - the previous 'pnpm exec playwright install' failed
with ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL / 'Command "playwright" not found'.

* fix(vitest-plugin-react-app): declare vitest as a peer dependency

The package uses vitest's expect/vi APIs at test-authoring time but only listed it as a devDependency, so consumers weren't told to install it themselves.

* fix(vitest-plugin-react-app): correct tsconfig project reference

src/index.ts and src/resolve-app-test-env.ts import @equinor/fusion-imports (utils/imports), but the tsconfig referenced utils/observable, an unrelated package.

* test(vitest-plugin-react-app): cover resolveAppTestEnv and appTestVitePlugin

The package's own resolution/virtual-module logic had no test coverage and no vitest.config.ts, so it wasn't even picked up as a project by the root Vitest run.

* docs: add missing changeset for CLI ./vitest entry-point removal

The removal of ffc app test, testApplication, the ./vitest entry-point, and resolveAppTestEnv from @equinor/fusion-framework-cli had no changeset.

* fix(module-context): make telemetry an optional peer dependency

The context module only reads telemetry defensively via an injected instance (never imports the package directly), matching the optional pattern already used by modules/msal.

* docs(vitest-plugin-react-app): fix quick-start snippet and fixture wording

Add the missing 'import { expect } from vitest' to the quick-start test snippet, and reword the fixture-sharing bullet to clarify only seeded defaults are shared, not fusion/app state between tests.

* docs(vitest-plugin-react-app): clarify testApp fixture-sharing wording

Match the README wording fix: testApp's JSDoc previously implied one shared mocked scope across tests, when each test actually gets fresh fusion/app instances and only the seeded defaults are shared.

* fix(react-app): remove stale vitest-browser-react peer dependency

vitest-browser-react was only used by the now-removed ./vitest entry-point; src/ has no remaining references to it, so it stays a devDependency only.

* test(module-context): cover telemetry tracking for initial-context resolution

Adds coverage for the resolver-error and successful-resolution telemetry events raised by module.ts's postInitialize, alongside the existing console.warn-fallback test for when no telemetry module is registered.

* test(react-app): add useToken rerender regression test

Asserts acquireToken is not re-invoked when scopes are re-rendered with the same contents (a fresh array literal each time), but is re-invoked when the contents actually change.

* refactor(tests): format code for better readability in useToken and ContextModuleConfigurator tests

* feat(vitest-plugin-react-app): add defineProject config helper, fix entrypoint inference

- New `/config` entry-point exporting defineProject: a drop-in for Vitest's own
  defineProject, pre-wired with appTestVitePlugin and the
  @vitest/browser-playwright/chromium provider.
- appTestVitePlugin now infers entrypoint from the Vitest project's own root
  (via configResolved) instead of process.cwd() when none is given.
- @vitest/browser-playwright and playwright are now explicit peer dependencies.

* fix(module): keep addConfig additive when the same module descriptor is re-registered

ModulesConfigurator.addConfig discarded previously registered configure/afterConfig/
afterInit callbacks whenever it was called again for an already-registered module
name, even when the descriptor itself was the exact same object.

This broke config functions that register more than one named client against a
shared module singleton, e.g. calling configureHttpClient/useFrameworkServiceClient
more than once from @equinor/fusion-framework-module-http - only the last call's
client survived; earlier ones threw "No registered http client for key [...]" at
createClient() time, with no error at configuration time.

addConfig now only discards previous callbacks when a genuinely different module
descriptor replaces the old one for that name; re-registering the same shared
descriptor stays additive.

* feat(openapi-mock): split loadFakerMap into a Node-only /node entry point

loadFakerMap reads sidecar files from disk (node:fs) and, for a .ts/.js sidecar, shells
out to esbuild via @equinor/fusion-imports' importConfig - neither of which exists in
a browser (or browser-mode Vitest) runtime.

Move it to a separate @equinor/fusion-openapi-mock/node entry point (src/node.ts) so
importing the package's main entry never pulls Node-only code into a browser bundle.
A fields map built in code, or loaded ahead of time some other way, still works with
createOpenApiMock from the main entry point regardless of runtime.

Also bumps the read-pkg pnpm override (10.0.0 -> 10.1.0) picked up while resolving
this package's dependency tree.

* fix(vitest-plugin-react-app): warm up all source to avoid mid-test Vite reload

defineProject only transformed source as each test file requested it, so a route
component reached solely through a lazy/code-split import (e.g. React.lazy or a
route-DSL component path) could be discovered mid-run instead of up front - Vite
then reloads the page to pick it up, which fails the in-flight test file's import.

Set server.warmup.clientFiles: ['src/**/*.{ts,tsx}'] so all source under src/ is
pre-transformed before the first test request.

* chore: exclude cookbook test files from AI indexing, exempt generators.ts from single-export lint

- fusion-ai.config.ts: exclude cookbooks/**/__tests__/** and *.test|spec.{ts,tsx}
  from both the patterns and rawPatterns indexes - test files aren't consumer-facing
  API surface and were polluting cookbook search results.
- fusion-lint.config.json: add generators.ts to single-export-per-file's
  excludePattern, matching the existing convention for deliberately multi-export
  utility filenames (utils.ts, helpers.ts, options.ts, etc).

* test(cookbook-app-react): add Vitest coverage via vitest-plugin-react-app

- vitest.config.ts uses defineProject from
  @equinor/fusion-framework-vitest-plugin-react-app/config.
- App.test.tsx covers the App component via renderAppComponent.
- Root vitest.config.ts now globs cookbooks/**/vitest.config.ts.

* test(cookbook-app-react-context): add Vitest coverage via vitest-plugin-react-app

- vitest.config.ts uses defineProject from
  @equinor/fusion-framework-vitest-plugin-react-app/config.
- App.test.tsx covers seeded initial context, related-context items, and
  switching context via the app's own context module ref.

* test(cookbook-app-react-feature-flag): add Vitest coverage via vitest-plugin-react-app

- FeatureFlag/FeatureFlags moved into src/components/, useFeatureLogger moved
  into src/hooks/, each alongside its own vitest.config.ts uses defineProject
  from @equinor/fusion-framework-vitest-plugin-react-app/config.
- FeatureFlag.test.tsx / FeatureFlags.test.tsx cover rendering and toggling.

* test(cookbook-app-react-msal): add Vitest coverage via vitest-plugin-react-app

- vitest.config.ts uses defineProject from
  @equinor/fusion-framework-vitest-plugin-react-app/config.
- App.test.tsx covers the default signed-in mock user, overriding configure
  with a custom mock account, and resolving/rendering an access token.
- Removes the unused demo src/config.ts (dev-only console logging).

* test(cookbook-app-react-router): add Vitest coverage via vitest-plugin-react-app

- vitest.config.ts uses defineProject from
  @equinor/fusion-framework-vitest-plugin-react-app/config.
- Component and route-page tests via testWithRouter/createRouteProps, with
  fixtures built from seeded Faker.js generators (src/mocks/generators.ts)
  instead of hand-written literals.
- src/mocks/{openapi.json,fields.faker.ts,api-mock.ts} + testWithApiMock
  wire an OpenAPI-mock-backed HTTP middleware for full-router integration
  tests (routing-with-api-mock.test.tsx) with deterministic, schema-shaped
  responses.
- README.md documents the testing approach (Testing section).

* test(cookbook-app-react-module): add Vitest coverage via vitest-plugin-react-app

- vitest.config.ts uses defineProject from
  @equinor/fusion-framework-vitest-plugin-react-app/config.
- App.test.tsx covers the demo module's resolved foo/bar values, and overriding
  configure to seed a different demo configuration.
- fix(config.ts): setBar(() => 69) returned a plain number, not a valid
  ObservableInput, so BaseConfigBuilder silently discarded it and bar fell
  through to the module's own 10-second delayed default (5) instead of 69.
  setBar(async () => 69) matches setFoo's existing pattern and resolves
  immediately.

* style: apply biome format/lint fixes repo-wide

Runs `biome check --write --unsafe .` across the whole repo:

- Wraps long lines exceeding the configured line width (mostly test files with
  chained assertions/JSX props).
- cookbooks/app-react/src/config.ts: prefixes 3 intentionally-unused callback
  parameters (env, config, instance) with `_`, per
  lint/correctness/noUnusedFunctionParameters.

No behavioral changes.

* test(vitest-config): include coverage tracking for packages only

* docs(cookbook-app-react-msal): clarify config.ts removal has no functional impact

Corrects the test's doc comment, which claimed composing with a real
configure that no longer exists, and documents the intentional removal
in the changeset instead of silently dropping the note.

Addresses review comment on PR #5293 (discussion_r3778886987).

* docs(cookbook-app-react): document silenced lifecycle console.log in changeset

The demo console.log calls were intentionally left commented out to
keep Vitest output clean; the changeset previously claimed 'no runtime
changes', which wasn't accurate. Documents the behavior change instead
of restoring the logging.

Addresses review comment on PR #5293 (discussion_r3778887017).

* fix(cookbook-app-react-router): fix faker.date.past() with unfixed refDate

faker.seed() only makes the pseudo-random offset deterministic; without
a fixed refDate, date.past() still resolves relative to the current
wall-clock time, so generated joinDate values drift across days despite
the seeded-fixture contract. Adds a shared fakerRefDate constant used by
both generateUser and the User.joinDate field faker.

Addresses review comments on PR #5293 (discussion_r3778887040, discussion_r3778887110).

* docs(cookbook-app-react-context): fix changeset API names

The changeset named renderAppComponent and renderAppComponent.fusion.app,
neither of which exists in the added tests; they use the plugin's
render/app fixtures instead.

Addresses review comment on PR #5293 (discussion_r3778887093).

* fix(cookbook-app-react-router): add missing tsconfig project reference

Adds a reference to packages/vite-plugins/raw-imports matching the new
workspace dependency in package.json, so isolated package builds resolve
it correctly.

Addresses review comment on PR #5293 (discussion_r3778887191).

* fix(turbo): include bin/build in cli package's cached build outputs

The build task only declared dist/** as cacheable output, but
packages/cli's rollup build writes its bundled CLI binaries to
bin/build/*.mjs (gitignored, not under dist/). On a turbo cache hit
in CI, bin/build/ was never restored, so any cookbook invoking the
fusion-framework-cli binary during its own build failed with
ERR_MODULE_NOT_FOUND for bin/build/cli.mjs. Reproduced locally by
deleting bin/build and re-running turbo build without --force (cache
hit skipped restoring it); fixed by adding bin/build/** to outputs.

* test: migrate React tests to Vitest browser

* docs(testing): publish Fusion app testing guide (#5296)

* docs(testing): add canonical package guidance

* feat(docs): map package links in VuePress

* docs(testing): publish the app testing journey

* fix(docs): repair internal documentation links

* test(module-state): use browser PouchDB adapter

* test: decouple browser test projects

* fix(docs): scope package link rewrites

* Refactor code structure for improved readability and maintainability

* fix(pnpm): remove duplicate '@remix-run/router' entries from lockfile

* fix(define-project): enhance dependency optimization for esbuild scanner

* refactor(memfs-plugin): move mocks for fs/promises and import-script to the top for better organization

* docs(advanced): document `test.override(...)` for fixture overriding in test files

* chore: drop spurious react-app major changeset; ./vitest never shipped

* chore: drop spurious cli major changeset; ffc app test never shipped

* chore: fix two more false-major changesets; mock infra never released

* Remove coverage configuration from vitest.config.ts

* feat: add migration guides for transitioning to Fusion Vitest and explain Browser Mode default

* test(cookbook-app-react-router): await styling before asserting active link color

* style(vue-press): add missing trailing newline in vitest config

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
odinr added a commit that referenced this pull request Aug 16, 2026
…kbooks (#5297)

* feat(openapi-mock): add package for generating OpenAPI-shaped fake responses

Adds @equinor/fusion-openapi-mock, a framework-agnostic utility that fakes
OpenAPI 3 responses straight from a parsed spec document. Every operation
with an operationId is faked from its declared success response schema,
with overrides/register for edge cases, seed for repeatable output, and
a FieldFakerMap for realistic per-field values via @faker-js/faker.

No dependency on any HTTP or routing framework: resolve({ method, path,
query }) returns a plain { status, mock }, so it composes with
@equinor/fusion-framework-module-http's mock router or any other server.

Related: equinor/fusion-core-tasks#1660

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore(deps): update lockfile for @equinor/fusion-openapi-mock

* fix(openapi-mock): address review feedback on route matching, response fallback, and validation

- sort operations by specificity so literal routes win over templated ones
- fall back to lowest declared numeric status when no 2xx/default response exists
- validate parsed YAML faker maps and fetched documents are plain objects
- use an isolated faker instance per generation instead of mutating the global singleton
- refactor createOpenApiMock and apply-field-fakers into smaller, focused functions

* refactor(openapi-mock): favor readability and avoid mutable locals

- replace && shorthand and let-based branches with explicit if/early-return
- extract resolveResponseSchema, parseDocumentText helpers
- rewrite resolvePointer's pointer walk with Array#reduce instead of a mutable loop variable

* feat(module-msal): add a mock entry point for testing without credentials or network

Adds ./mock, so applications can run against the auth module without
credentials or network access:

    import { enableMsalMock, createMsalMockClient } from '@equinor/fusion-framework-module-msal/mock';

    enableMsalMock(configurator, (builder) => {
      builder.setAccount({ name: 'Ada Lovelace' });
    });

Only the MSAL client is substituted:

- MsalMockClient resolves tokens in-process and takes the same
  MsalClientConfig as the real MsalClient, keeps a real account cache
  keyed by homeAccountId (matching MSAL's getAccount/getAllAccounts
  semantics), and produces structurally valid, unsigned JWTs.
- The client construction seam (_createClient/_createClientConfig) is
  extracted from MsalConfigurator._processConfig, giving a supported
  seam for authenticating through something other than Entra ID. No
  client is built when the module is hoisted onto a host application's
  provider, since a client built during configuration would be
  discarded or shadow the host's signed-in user (_isHoisted).
- MsalMockConfigurator.setAccount lets a test declare the signed-in
  user on the builder; the account is applied before MsalProvider
  initializes, so the provider's own start-up path (including
  automatic login when signedOut + requiresAuth) acts on it.
- The config schema moved into MsalConfig.schema.ts with
  MsalConfigExtension, an extension point mock configuration merges
  into without the builder needing to cast past its type.

MsalProvider, MsalConfigurator and IMsalProvider are otherwise
untouched; the mock module's configure differs from the real module,
initialize does not.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(module-http): add a mock entry point with middleware and adapters

Adds @equinor/fusion-framework-module-http/mock, so HTTP clients answer
requests from registered route handlers instead of the network:

    import { enableHttpMock } from '@equinor/fusion-framework-module-http/mock';

    enableHttpMock(configurator, (builder) => {
      builder.configureClient('catalog', { baseUri: 'https://api.example.com' });
      builder.get('/items', () => Response.json([{ id: 1 }]));
    });

Route handlers are Fetch-standard middleware -
(request: Request) => Response | undefined | Promise<...> - tried in
registration order, with undefined falling through to the next one.
One router is shared across every named client a HttpMockConfigurator
builds, matched against each request's fully resolved URL.

HttpClient gains a _performFetch(uri, init) seam, isolated from
_fetch$ so a test double replaces only the network call while request
preparation, the response pipeline, and abort handling run unchanged.

Two adapters drop in an existing backend with no hard dependency on it:

- fromExpressStyleHandler(handler) adapts an Express-style (req, res)
  handler (or a whole framework built from them, like openapi-backend).
- fromOpenApiMock(openApiMock) adapts an @equinor/fusion-openapi-mock
  instance (duck-typed, no package dependency), so a real
  openapi.json/openapi.yaml fakes every response with no handlers
  written at all.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(module-service-discovery): add a mock entry point backed by an in-memory registry

Adds @equinor/fusion-framework-module-service-discovery/mock, so service
discovery resolves from an in-memory registry instead of the network:

    import { mockServiceDiscovery } from '@equinor/fusion-framework-module-service-discovery/mock';

    mockServiceDiscovery(configurator, { services: [{ key: 'apps', uri: 'https://apps.test' }] });

ServiceDiscoveryMockConfigurator builds the registry on the builder
itself - setBaseUri, addService, addServices, removeService,
setServices, setResolveUnknownServices - and the client is constructed
from that registry when the module builds its config, so a test never
has to construct a client just to add a service.

setBaseUri also lets the default service endpoints point at a local
mock server such as http://localhost:3000, so an application can make
real HTTP calls against Mockoon, Prism or the dev server without a
service worker intercepting requests.

Exposes the discovery client on the provider as .client, mirroring
MsalProvider.client, so a test can spy on it directly:

    vi.spyOn(fusion.modules.serviceDiscovery.client, 'resolveService').mockResolvedValue(service);

configureServiceDiscovery now also accepts a synchronous callback - the
underlying builder always allowed it, only the exported type required
a promise.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(module): fix configurator phase ordering and dot-path optional branches

Two independent fixes needed by the framework mock work:

- Module re-registration now replaces the prior module's configure,
  afterConfig, and afterInit callbacks instead of appending to them,
  keyed by module name (_dedupeModulesByName /
  _removeModuleCallbacks). This prevents stale callback execution
  when a mock module (e.g. enableMsalMock) overrides a real module
  registration, and fixes configurator phases running out of order
  or skipping post-configure hooks in certain initialization paths.
- DotPath now unwraps an optional object property with NonNullable
  the same way DotPathType already did, so a path BaseConfigBuilder
  understands is one _set no longer refuses. Given
  { foo?: { bar: string } }, 'foo.bar' is now a valid path, matching
  'foo' which was already allowed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(framework): add a mock entry point that composes each module's own test double

Adds @equinor/fusion-framework/mock for initializing the framework in a
test:

    import { mockFramework } from '@equinor/fusion-framework/mock';

    const fusion = await mockFramework((configurator) => {
      configurator.msal.setAccount({ name: 'Ada Lovelace' });
      configurator.serviceDiscovery.setBaseUri('http://localhost:6669');
    });

mockFramework runs the real configure -> initialize pipeline with the
real built-in modules and substitutes only the boundaries that leave
the process - the MSAL client and the service discovery client.
Module wiring, configuration validation and lifecycle hooks behave as
they do in production.

FrameworkMockConfigurator gains _pin/_getConfig, so a module supplied
through TModules gets the same kind of named accessor .msal and
.serviceDiscovery already have, rather than that pinning being
hand-written once per built-in mock. .services, .context and
.telemetry are exposed the same way, though they still perform real
I/O until those modules have a test double of their own. event is
intentionally left out - its configure factory reads ref to wire
event bubbling to a parent event provider when hoisted, and pinning
it would call configure() with ref always undefined, silently
disabling that bubbling.

The entry point owns no mock logic of its own: each module exports
its own test double from its own ./mock entry point, and this one
only composes the built-in set. It has no test-runner dependency and
provides no mocking API, since replacing an individual call belongs
to the test runner.

init() no longer throws when no DOM is present - the window.Fusion
assignment (for portal shells and widgets) is now skipped when
window is undefined, so a test runner using the node environment or
a server-side render no longer fails with
'ReferenceError: window is not defined'.

Also restructures documentation so each README is an entry point
rather than a manual: long-form content (including each package's
testing.md) moved into per-package docs/ folders, matching the
convention already used by @equinor/fusion-framework-module and
@equinor/fusion-framework-module-http.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address Copilot review comments on PR #5205

- msal-config-schema: reject unparseable version instead of coercing to "null"
- HttpMockRouter: reset RegExp lastIndex so global/sticky match patterns are reusable across requests
- extract decodeJwtSegment into its own file, fix atob/UTF-8 round-trip for non-ASCII claims
- service-discovery docs: dedupe heading, close dangling code fence

* fix(http-mock): await handleRequest in fromExpressStyleHandler

Prevents an async Express-style handler's rejection from becoming an
unhandled promise rejection instead of failing the middleware call.

* feat(module-event): add EventModuleConfigurator, waitForEvent/watchEvents, and operators subpath (#5241)

* feat(module-event): add EventModuleConfigurator, waitForEvent/watchEvents, and operators subpath

Add EventModuleConfigurator, a BaseConfigBuilder-based configurator with fluent
setOnDispatch/setOnBubble setters, replacing direct property assignment on the
config object (kept as a deprecated path, no major bump needed).

IEventModuleConfigurator is renamed to EventModuleConfig and converted from an
interface to a type; IEventModuleConfigurator is kept as a deprecated alias.

Narrow the dispatchEvent return type for registered FrameworkEventMap keys and
pre-constructed event instances so callers get back the specific event type.

Fix event$ to emit only after listeners and the bubble hook have run, matching
its documented contract (it previously emitted before dispatch).

Add waitForEvent/watchEvents test helpers and a ./operators subpath export for
filterEvent (moved from src/filter-event.ts to src/operators/filter-event.ts).

Rename configurator.ts -> EventModuleConfigurator.ts and provider.ts ->
EventModuleProvider.ts for filename-convention compliance, and add a docs/
folder (configuration, observable-patterns, lifecycle, testing).

* docs(vue-press): align event module docs with docs/ folder, move React page

Trim vue-press/src/modules/event/README.md to an @include of the package
README, add a docs/ mirror (configuration, observable-patterns, lifecycle,
testing) that @includes the package's own docs, matching the http/module
pattern.

Move the event React bindings page from event/react.md to
react/event/README.md, alongside react/router/, as an @include of
@equinor/fusion-framework-react-module-event's README. Update the sidebar to
match.

* style(module-event): apply Biome formatting fixes flagged by reviewdog

* fix(module-event): fix waitForEvent TDZ on sync completion, correct doc inaccuracies

* feat(module-analytics): add MockAnalyticsAdapter for recording tracked analytics events (#5242)

* feat(module-analytics): add MockAnalyticsAdapter for recording tracked analytics events

- Add MockAnalyticsAdapter implementing IAnalyticsAdapter, recording events
  in-memory for test assertions (getAnalytics, waitForAnalytic)
- Add ./mock subpath export
- Add vitest.config.ts to make the package a discoverable vitest project
- Document the new adapter in README.md and add a changeset

Implements equinor/fusion-core-tasks#1657

* test(module-analytics): verify MockAnalyticsAdapter through the real module pipeline

The existing unit tests only exercise MockAnalyticsAdapter's own logic in
isolation. Add an integration test, mirroring the pattern used by msal and
service-discovery's mock tests, that wires it through the real
enableAnalytics -> ModulesConfigurator -> AnalyticsProvider.initialize()
pipeline to prove events from provider.trackAnalytic and a real collector
actually reach it, and that it doesn't interfere with other adapters.

* docs(module-analytics): move testing docs to docs/testing.md, mirror in vue-press

Adopt the docs/ convention already used by the event, http, and module
packages: extract the README's inline Testing section into
packages/modules/analytics/docs/testing.md, link to it from README, and
add a vue-press mirror page (@include) with a sidebar entry.

* docs(module-analytics): slim README to overview, split domain docs into docs/

Move Adapters, Collectors, and Tracking Events Manually out of the README
into docs/adapters.md, docs/collectors.md, and docs/tracking-events.md,
matching the event/http/module docs/ convention. Drops duplicate
Configuration/Creating Custom Collectors content that had accumulated in
the old README's Tracking Events Manually section. README is now a slim
overview + entry points + documentation table + quick start, linking out
to docs/ for the domain reference. Mirrors and sidebar entries added in
vue-press.

* fix(module-analytics): reject waitForAnalytic when a predicate matcher throws

A throwing predicate matcher surfaced on RxJS's error channel, but the
subscribe observer had no error handler — the exception was reported
globally and the returned promise stayed pending forever. Add an error
handler that rejects through the same cleanup path, and cover it with a test.

* docs(module-analytics): fix broken abort and ModulesConfigurator examples in testing.md

The abort example never called controller.abort() (so copying it hangs
indefinitely) and redeclared `event` from the preceding example in the
same code block. The bespoke ModulesConfigurator example destructured
`analytics` from initialize() without a type — enableAnalytics only
registers the module at runtime, so this doesn't typecheck as written;
cast to IAnalyticsProvider, matching what the integration test does.

* feat(azure-identity,msal-node): add MockAuthProvider test double for auth module (#5244)

* feat(azure-identity,msal-node): add MockAuthProvider test double for auth module

Add a configurable IAuthProvider test double, exported from a new /mock
subpath in each package (@equinor/fusion-framework-module-azure-identity/mock,
@equinor/fusion-framework-module-msal-node/mock).

Unlike token_only mode's static-token providers (AuthProviderTokenOnly /
AuthTokenProvider), MockAuthProvider actually implements login/logout,
moving between signed-in and signed-out state, and exposes setAccount,
setAccessToken and setExpiresOn so a test can control the returned token
and simulate an expired token to exercise refresh logic. No real network
calls are made.

Implemented as two mirrored (twin) implementations rather than a shared
package, since azure-identity's and msal-node's IAuthProvider interfaces
differ in login's return type (AuthRecord vs. AuthenticationResult) and
neither package depends on the other.

Registers via enableAuthMock(configurator, configure?), a full 'auth'
module replacement mirroring @equinor/fusion-framework-module-msal/mock's
enableMsalMock, so no special-cased wiring is needed in either module.

Closes equinor/fusion-core-tasks#1665.

* fix(azure-identity,msal-node): make enableAuthMock generic over TModules/TRef

Replaces the IModulesConfigurator<any, any> parameter with a generic
<TModules extends Array<AnyModule> = Array<AnyModule>, TRef = unknown>
signature, so the optional configure callback keeps the caller's actual
TRef type instead of erasing it to unknown.

Addresses review feedback on PR #5244 (copilot-pull-request-reviewer).

* fix(azure-identity,msal-node): fix double-registration in mock README example

The Testing section called enableAuthMock(configurator) once for its
return value, then again with a configure callback to seed the account.
The second call replaces the first provider (enableAuthMock always
registers last), so the retained `auth` reference from the first call
was no longer the active provider by the time login/acquireAccessToken
ran against it.

Collapse to a single enableAuthMock call that both seeds the account
and returns the provider that stays registered.

Addresses review feedback on PR #5244 (copilot-pull-request-reviewer).

* feat(module-bookmark): add BookmarkMockClient and enableBookmarkMock (#5245)

* feat(module-bookmark): add BookmarkMockClient and enableBookmarkMock

Add a purpose-built mock for the bookmark module, exported from a new
`/mock` subpath (`@equinor/fusion-framework-module-bookmark/mock`),
following the client-boundary-swap pattern already used by msal's
`enableMsalMock`.

- `BookmarkMockClient`: in-memory `IBookmarkClient` backed by a Map of
  seeded bookmarks and a Set of favorite ids.
- `BookmarkMockConfigurator`: extends the real `BookmarkModuleConfigurator`
  with `setBookmarks`/`setCurrentBookmark`/`setFavorite`, and falls back to
  trivial application/context resolvers when no real `app`/`context`
  module is registered (both are required by the config schema).
- `enableBookmarkMock`/`bookmarkMockModule`: module descriptor mirroring
  the real module, only swapping `configure`.

Create, update, delete, and favorite calls all flow through the real
`BookmarkProvider` and `bookmark-flows/` epics unmodified.

Related: equinor/fusion-core-tasks#1667

* fix(module-bookmark): match sourceSystem, strip list payloads, normalize null payload in mock client

- matchesFilter now checks filter.sourceSystem (identifier/name/subSystem),
  since BookmarkProvider.getAllBookmarks always passes sourceSystem through
  the filter.
- getAllBookmarks now strips payloads from returned bookmarks, matching the
  real BookmarkClient's list behavior.
- updateBookmark now normalizes an explicit payload: null clear to undefined,
  matching setBookmarkData and avoiding a later getBookmarkData() emitting
  null as T.

Addresses PR #5245 review comments on BookmarkMockClient.ts:44, :140, :270.

* fix(module-bookmark): respect inherited resolve.application/context on mock fallback

Gate the mock's fallback application/context resolvers on initial?.resolve
as well as this._has(...), so a nested mock configurator does not overwrite
resolvers already inherited from a parent config.

Addresses PR #5245 review comment on BookmarkMockConfigurator.ts:150.

* test(module-bookmark): assert getBookmark rejects for an unknown id

Adds a Vitest assertion exercising the async error path through the real
BookmarkProvider.getBookmark fetch flow, so a regression that stops missing
ids from surfacing as observable errors would fail this suite.

Addresses PR #5245 review comment on BookmarkMockClient.ts:125.

* docs(module-bookmark): add required created/createdBy fields to mock seed example

The README's setBookmarks example omitted created/createdBy, which are
required on Bookmark, so copying the snippet as-is produced a type error.

Addresses PR #5245 review comment on README.md:245.

* docs(module-bookmark): add required created/createdBy fields to changeset example

The changeset's setBookmarks example omitted created/createdBy, which are
required on Bookmark, so the advertised snippet did not type-check.

Addresses PR #5245 review comment on .changeset/module-bookmark_mock-entry-point.md:15.

* refactor(module-context): extend BaseConfigBuilder and reorganize internal files

- ContextModuleConfigurator now extends BaseConfigBuilder (createConfigAsync),
  matching the pattern used by module-event/module-http/module-msal
- rename configurator.ts -> ContextModuleConfigurator.ts, split its exported
  types into ContextModuleConfig.ts and ContextModuleConfigurator.interface.ts
- move get/query/related-context-selector.ts into src/selectors/
- move parse-context-item.ts into src/utils/ and export it from utils/index.ts
- delete unused ContextConfigBuilder.ts
- add ContextModuleConfigurator.test.ts covering the new builder surface

No public export names change; index.ts still exports the same
ContextModuleConfigurator/IContextModuleConfigurator/ContextModuleConfig.

* feat(module-context): add mock and mock/fixtures entry points

Add ./mock (ContextMockConfigurator, enableContextMock) — a
ContextModuleConfigurator backed by an in-memory pool instead of a real
client. Only the data source is substituted; validateContext, resolveContext,
and parent-context propagation still run through the real ContextProvider.
Friendly seeding methods (setCurrentContext/setContexts/addContext/
setRelatedContexts) cover the common cases; setResolver is the escape hatch.

Add ./mock/fixtures (createContextItems, createContextItemFactory) — realistic
ContextItem generators backed by an optional @faker-js/faker peer dependency,
kept on a separate entry point so enableContextMock never pulls faker in.

Adds an http workspace dependency (for the vitest project setup) and a
package-level vitest.config.ts/test script.

* docs(module-context): restructure README into docs/ and mirror in vue-press

Move long-form content out of README.md into docs/data-model.md (ContextItem/
ContextItemType shape, query/related parameter types), docs/lifecycle.md
(setting/resolving context, initial-context resolution, parent/child
propagation) and docs/recipes.md (OData query params, path rewriting,
accepting related context type families, skipping initial-context lookup,
custom search errors), matching the module/http convention.

README.md keeps the elevator pitch, a new "How it fits together" section
covering the module's dependency graph (services required, event/navigation
optional and auto-detected) and hierarchy-aware parent connection, and a
documentation table linking to the three new pages.

vue-press/src/modules/context mirrors the split with @include stubs, and the
sidebar gains an Overview/Data model/Lifecycle/Recipes breakdown for Context.

* feat(framework): back mock context accessor with ContextMockConfigurator

FrameworkMockConfigurator.context now pins contextMockModule (from
@equinor/fusion-framework-module-context/mock) instead of the real context
module, so `.context` seeding methods (setCurrentContext/setContexts/
addContext/setRelatedContexts/setResolver) work the same way `.http` and
`.serviceDiscovery` already do — no more real I/O through context in tests.

docs/testing.md, docs/testing-extending.md and docs/testing-api.md document
both context mocking strategies: the in-memory ContextMockConfigurator above,
and mocking the context API's HTTP responses directly for tests that need to
exercise the real configurator/services/HTTP pipeline.

* chore: add changesets for context mock entry point and docs restructure

* fix(module-context): document createConfig as a breaking change

Extending BaseConfigBuilder changes the public createConfig method from
Promise<ContextModuleConfig> to Observable<ContextModuleConfig>. Bump
the changeset to major with migration guidance to createConfigAsync,
matching the precedent set when service-discovery/msal adopted the
same base class.

Addresses review comment on PR #5243 (discussion_r3744095539).

* docs(module-context): clarify setResolver only affects id-based lookup

setResolver overrides ContextClient.resolveContext(id) (used by
setCurrentContextById and the mock's initial context), not
ContextProvider.resolveContext(item), which always resolves related
context through setRelatedContexts regardless of setResolver. Reword
the class and method docs so the two same-named methods aren't
conflated.

Addresses review comment on PR #5243 (discussion_r3744095551).

* test(framework): assert seeding via .context reaches the built framework

The existing test only checked that .context is defined, which also
passed with the real ContextModuleConfigurator. Add a case that seeds
via configurator.context.setCurrentContext(...), initializes the
framework, and asserts fusion.modules.context.currentContext, covering
the _pin(contextMockModule) composition path.

Addresses review comment on PR #5243 (discussion_r3744095562).

* feat(module-telemetry): add MockTelemetryAdapter and wire it into FrameworkMockConfigurator

Closes equinor/fusion-core-tasks#1708

Adds a collecting MockTelemetryAdapter + TelemetryMockConfigurator behind a
new ./mock subpath, following the MockAnalyticsAdapter shape. Wires
FrameworkMockConfigurator.telemetry to the mock configurator so telemetry
tracked through a mocked framework instance never reaches Application
Insights or any real endpoint.

* fix(module-telemetry): close waitForItem resource leaks and simplify changeset example

* feat(module-feature-flag): add mock entry point for seeding test feature flags

Add `FeatureFlagMockConfigurator`, `enableFeatureFlagMock`, and a `./mock`
subpath so a test can seed flags in-memory via `addFeature`/`setFeatures`,
with no `localStorage`/URL dependency. Toggling still runs through the real
`FeatureFlagProvider`.

Also fixes a missing `telemetry` row in the framework's testing.md accessor
table (pre-existing gap, unrelated to this change).

Closes equinor/fusion-core-tasks#1707.

* test(module-feature-flag): assert async configure callbacks are awaited before initialize resolves

Addresses review comment on module.ts:49.

* docs(module-feature-flag): document the ./mock subpath in the README's API Reference

Addresses review comment on package.json:23.

* chore: add changeset for the testing.md telemetry accessor fix

Addresses review comment on testing.md:30.

* refactor(module-http): replace router-based mock with addMiddleware on the real configurator

Closes equinor/fusion-core-tasks#1706

Replaces enableHttpMock/HttpMockConfigurator/HttpMockRouter with addMiddleware
on the real HttpClientConfigurator, via a new HttpMiddlewareHandler that wraps
_performFetch instead of swapping the configurator out. A middleware calling
next(uri, init) falls through to the real call (or the next middleware)
unchanged, so a test exercises the exact same client and configuration a real
app registers.

createRouterMiddleware and createOpenApiMockMiddleware replace the old router
and its Express-style adapters, covering the same handler/OpenAPI-fake cases
without depending on a real routing library.

* docs(module-http): mirror the testing guide into vue-press

Adds the http module's docs/testing.md via the same @include pattern used
by client-configuration.md and server-sent-events.md, with a matching
sidebar entry.

* fix(framework): pin FrameworkMockConfigurator.http to the real HttpClientConfigurator

FrameworkMockConfigurator.http returns the real IHttpClientConfigurator now
that module-http's mock support is addMiddleware on that real configurator,
not a separate mock-specific one. Fake a response with
.http.addMiddleware(...) instead of swapping the module out.

Updates testing.md/testing-api.md/testing-extending.md to match.

* test(module-context): migrate off the removed enableHttpMock API

enableHttpMock/http.get(...) no longer exist; rebuilds the fixture with
configureHttp + createRouterMiddleware against configurator.addConfig,
matching the addMiddleware-based mocking module-http now exposes.

Updates the README's mention of the HTTP-mock alternative to match.

* feat(module-app): add MockAppClient mock entry point

Part of equinor/fusion-core-tasks#1706

Adds MockAppClient, exported from a new ./mock subpath, so a test can serve
one app's own manifest and config locally instead of contacting the app
service:

- getAppManifest resolves locally only for manifest.appKey with no tag.
- getAppConfig resolves locally for manifest.appKey when tag is absent or
  equal to manifest.build?.version.
- Every other request still goes through the real AppClient it wraps.

Also exports AppConfig as a value from the package root (previously
type-only), so a test can construct one directly.

* feat(app): add mockAppModules and AppMockConfigurator /mock entry point

Closes equinor/fusion-core-tasks#1706

Adds a ./mock entry point: mockAppModules runs an application's real module
pipeline — the real event/http/msal modules, the real AppConfigurator
configuration pipeline and real lifecycle — against a mocked parent Fusion
instance, so a test exercises the wiring an application actually depends on
instead of a reimplementation of it.

AppMockConfigurator extends the real AppConfigurator, pinning http and msal
to the same test doubles FrameworkMockConfigurator uses so .http/.msal are
reachable synchronously; event is deliberately left unpinned since its
configure factory needs a real ref. Its own addConfig override redirects a
pinned module's later registrations at the pinned descriptor. The base
AppConfigurator constructor's own call into _configureHttpClientsFromAppConfig
is deferred (via a no-op override + an explicit super call after pinning) so
it never runs before this class's own fields exist.

enableAppManifestMock registers the app module on a parent mockFramework
configurator, serving one app's manifest/config locally while delegating
everything else to service discovery. mockAppModules uses it to build its
zero-configuration default parent.

Restructures README.md into an entry point pointing at docs/http-clients.md,
docs/bookmarks.md and docs/testing.md, matching the convention already used
by module-http and module-msal.

* fix(app): remove dead initialize-app-configurator.ts duplicate

initialize-app-modules.ts superseded this file when initializeAppModules gained the TConfigurator type parameter, but the old file was left in place -- nothing imported it, so it was dead code that still compiled and still linted (fusion-lint's require-tsdoc flagged it for the missing @template tags the newer file already has).

* style: apply biome formatting fixes

Long import lists wrapped onto multiple lines, a ternary split across lines,
and a single-quoted string containing an apostrophe rewritten with double
quotes -- no behavior change.

* refactor(module-http): flatten mock/adapters into mock/, fix lint warnings

mock/ only ever had one barrel (index.ts) plus an adapters/ subfolder holding
the 3 files it re-exported -- two barrels doing the same job for exactly one
category of file. Moved create-open-api-mock-middleware.ts,
create-router-middleware.ts and resolve-open-api-mock-response.ts up into
mock/ directly and dropped the adapters/ barrel, fixing relative import
depths and OpenApiMockLike's re-export path along the way.

Also fixes the fusion-lint warnings the files carried before the move:
renamed each file to match its exported symbol (filename-convention),
added the missing intent comments on create-router-middleware.ts's
if-blocks and .map() (require-intent-comment), and replaced a comma-operator
expression in compilePath with an explicit if/return (noCommaOperator).

* fix(module-app): treat only an absent tag as 'no tag' in MockAppClient

getAppManifest's !args.tag and getAppConfig's isOwnTag both treated an
explicit empty-string tag the same as an absent one, so getAppManifest({
appKey, tag: '' }) and getAppConfig({ appKey, tag: '' }) answered locally
instead of delegating, contradicting the documented 'tag is absent' rule.
Compare against undefined explicitly instead of a falsy check.

Adds MockAppClient.test.ts covering both methods' local-vs-delegate
decision, including the explicit-empty-string-tag case.

* docs(vue-press): add changeset for the http module testing page

The new vue-press page and sidebar entry is a consumer-facing docs change
to the published @equinor/fusion-framework-docs package, which needed a
changeset like every other vue-press page addition in this repo.

* chore(changesets): bump http mock changesets to major

enableHttpMock, HttpMockConfigurator, and HttpMockRouter were removed
from @equinor/fusion-framework-module-http's /mock entry point, and
FrameworkMockConfigurator.http's return type changed from a mock-specific
configurator to the real IHttpClientConfigurator -- both are breaking
changes for existing consumers and need a major bump, not minor.

* docs(app): correct mockAppModules' network-access claim

The http module registered by AppMockConfigurator is the real
HttpClientConfigurator -- addMiddleware only short-circuits requests a
middleware matches with a response, so an unmatched client or a middleware
that calls next still reaches the real network. msal is the only module
genuinely backed by a test double. Reworded the doc comment to describe
what is actually substituted instead of promising no network access.

* fix(module-http): abort the real network call through middleware on abort()

next(...) resolves through a Promise (HttpMiddlewareHandler's toPromise),
so a middleware calling it creates a subscription to _performFetch that
sits outside the tree takeUntil(this._abort$) tears down -- the outer
request settled on abort(), but the underlying fetch kept running.

abort() now also aborts a per-request AbortSignal combined into the
request init, so _performFetch (fromFetch by default) is cancelled
directly regardless of whether middleware severed the RxJS teardown
chain. Adds a regression test with a pass-through middleware asserting
the underlying fetch's signal is aborted.

* test: add react-app hook testing utilities and msal proxy/hoisting coverage

- add render-app-hook testing utility + useAccessToken hook test in react/app
- add msal-hoisting coverage in packages/app
- add createProxyProvider behavior coverage in modules/msal

* test(react-app): add useTrackFeature hook tests

Add @equinor/fusion-framework-module-telemetry as a devDependency so tests
can read the mock telemetry adapter via fusion.modules.telemetry.getAdapter('mock').

* test(react-app): render Apploader against a real dynamic-import child app script

Add renderAppComponent, a component-level counterpart to renderAppHook, and
use it in a new Apploader.test.tsx that exercises the real useApploader ->
App.initialize() -> dynamic import() pipeline (no vi.mock of createApp or
the import mechanism), backed by a fixture child app script.

* test(react-app): add useCurrentBookmark hook tests

Covers the app-scoped happy path, filtering out a bookmark that belongs to a different app, and the framework-scoped fallback with its deprecation warning. Adds @equinor/fusion-framework-module-bookmark as a devDependency for the mock helpers.

* test(react-app): add useCurrentContext hook tests

Covers resolving the seeded initial context and switching to another seeded item via setCurrentContext, against the app-scoped context module mock. Adds @equinor/fusion-framework-module-context as a devDependency for the mock helpers.

* test(react-app): add useFeature hook tests

Covers resolving an app-scoped flag, app-over-framework override on a shared key, a framework-only flag surfacing through the merged stream, and toggleFeature inverting the current value with no explicit argument.

* test(react-app): add useHelpCenter hook tests

Covers every open* action, asserting the framework event dispatched on the app's event module carries the expected page and payload.

* test(react-app): add useToken and useCurrentAccount hook tests

Completes coverage of the msal folder alongside the existing useAccessToken tests: useToken's full AuthenticationResult happy/error paths, and useCurrentAccount against the default mock user, a custom msal.setAccount() override, and the signed-out case.

* test(react-app): add useAppSetting and useAppSettings hook tests

Covers the settings folder's public hooks against the real fetch/update round-trip (via an in-memory apps-client mock): resolving persisted values, default-value fallback before resolution, persisting an update (direct value and updater-function forms), and surfacing a persistence failure through onError instead of throwing. useAppSettingsStatus is internal (not exported from settings/index.ts) and is exercised indirectly through these onLoading/onUpdating-adjacent paths.

* docs(react-app): fix HTTP config example and nullable/async return types

Fixes issues found while writing hook tests: README/createComponent examples used configurator.http.configureClient (test-only mock API) instead of the real configurator.configureHttpClient; setCurrentContext/useFrameworkCurrentContext were documented as void-returning when they actually return void | Promise<ContextItem | null>; AuthenticationResult.account/expiresOn were documented as non-nullable; useCurrentBookmark's deprecated framework-scope fallback and its console warning were undocumented.

* docs(react-app): add app testing guide

* fix(react-app): unmount rendered hooks in msal token tests

Prevents the async token-acquisition effect from flushing a state
update after happy-dom's window is torn down at the end of the test
file, which CI surfaced as an uncaught 'window is not defined'
exception (all assertions still passed, but the run was marked
failed).

* test: apply Biome line-length formatting suggestions

Collapse two waitFor assertions that fit within the line-length limit
back onto a single line, per Biome/reviewdog suggestions on PR #5271.

* fix(app): treat empty-string assetUri as an explicit override

if (assetUri) silently ignored a caller-supplied empty string, which is a meaningful value (selects a root-relative script path). Check against undefined instead so the override always applies when the caller passes the option.

* docs(react-app): clarify network-boundary faking scope; add error-state example

The docs overstated that renderAppHook/renderAppComponent fake the entire network boundary. Only requests a seeded router middleware answers are faked; unmatched requests still reach the real network. Also documents renderAppComponent under the /testing subpath table, and adds an error-state test example mirroring Apploader.test.tsx.

* docs(react-app): remove invalid @template Result tag from renderAppComponent

renderAppComponent has no Result type parameter; the tag referenced a generic that does not exist on this function and would fail TSDoc validation.

* test(react-app): assert the seeded bookmark actually resolved before checking the app-key filter

The previous waitFor(() => expect(result.current).toBeDefined()) was a no-op since result.current is always a defined object. Now waits on the unfiltered useAppModules().bookmark provider to resolve the seeded bookmark first, so the assertion that useCurrentBookmark hides it can't pass trivially against a still-pending value.

* docs(changeset): qualify cross-repo issue reference

Closes #1716 resolved to an unrelated PR in this repo. Qualifying as equinor/fusion-core-tasks#1716 points at the correct issue while preserving auto-close-on-merge.

* test: apply Biome line-length formatting suggestions

Reformat two msal-hoisting.test.ts call expressions and two useCurrentContext.test.tsx object literals that exceed the line-length limit, per Biome/reviewdog suggestions on PR #5271.

* chore(deps): bump vitest to ^4.1.10

Bumps vitest and @vitest/coverage-v8 across the workspace, and migrates
vitest.config.ts package.json imports to the `with { type: 'json' }`
import attribute required by the newer vitest/vite toolchain.

* feat(cli): add `ffc app test` command

Runs the application's own Vitest suite with manifest/config/module-configurator resolved
the same way `ffc app build`/`ffc app dev` do, exposed via a Vite plugin's virtual modules
to @equinor/fusion-framework-react-app/vitest's test/render fixtures.

Renamed packages/cli/src/lib/testing -> lib/vitest and the ./testing export -> ./vitest.

* Update packages/cli/src/cli/commands/app/test.command.ts

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(cli): throw on an explicit but missing --configure path

An explicitly supplied configure path is a user request, not a convention lookup — a typo
should fail loudly instead of silently running the test suite without the app's modules.

* refactor: clean up code formatting for better readability

* feat(react-app): vitest render/test-app fixtures under src/vitest

- rename src/testing -> src/vitest, add test-app/scope fixture chain
  (test-app.tsx, test.tsx, scope/*) for rendering app components and
  hooks with a fully wired Fusion test environment
- fix(react): useFrameworkModule warns with the correct module name
- fix(module-context): stop warning when there's no initial context
  to resolve
- fix(module-msal): drop stray debug console.log in resolve-version
- fix(log): guard process.env access for Vitest Browser Mode
- chore: biome format fixes surfaced by pnpm check (unrelated pre-existing
  drift in cli/linting/event packages)

* refactor: extract react-app vitest helpers into vitest-plugin-react-app

Move renderAppHook/renderAppComponent/testApp and the app-test Vite plugin out
of @equinor/fusion-framework-react-app's ./vitest entry-point and
@equinor/fusion-framework-cli's ./vitest entry-point into a new standalone
package, @equinor/fusion-framework-vitest-plugin-react-app, built on
vitest-browser-react instead of @testing-library/react.

- Remove the ffc app test CLI command and cli/src/lib/vitest (superseded by
  appTestVitePlugin registered directly in a project's vitest.config.ts).
- Add packages/vitest-plugin/react-app with README, appTestVitePlugin,
  renderAppHook, renderAppComponent, testApp, and a /test entry-point
  exporting pre-seeded test/render fixtures.
- Update react-app's README/docs and package.json to drop ./vitest and
  point at the new package instead.
- Fix stray console.log in msal's resolveVersion mismatch warning, and guard
  fusion-log's process.env access for environments without a Node process
  global (Vitest Browser Mode).
- Install Playwright browsers in CI for the new package's and react-app's
  Vitest Browser Mode tests.
- Add CODEMAP.md entry for the new package.

Changesets included for all affected packages.

* ci: fix Playwright install invocation to filter into a package that depends on it

pnpm exec at the repo root only resolves bins from root's own node_modules,
and playwright is a devDependency of react-app/vitest-plugin-react-app only,
not the workspace root - the previous 'pnpm exec playwright install' failed
with ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL / 'Command "playwright" not found'.

* fix(vitest-plugin-react-app): declare vitest as a peer dependency

The package uses vitest's expect/vi APIs at test-authoring time but only listed it as a devDependency, so consumers weren't told to install it themselves.

* fix(vitest-plugin-react-app): correct tsconfig project reference

src/index.ts and src/resolve-app-test-env.ts import @equinor/fusion-imports (utils/imports), but the tsconfig referenced utils/observable, an unrelated package.

* test(vitest-plugin-react-app): cover resolveAppTestEnv and appTestVitePlugin

The package's own resolution/virtual-module logic had no test coverage and no vitest.config.ts, so it wasn't even picked up as a project by the root Vitest run.

* docs: add missing changeset for CLI ./vitest entry-point removal

The removal of ffc app test, testApplication, the ./vitest entry-point, and resolveAppTestEnv from @equinor/fusion-framework-cli had no changeset.

* fix(module-context): make telemetry an optional peer dependency

The context module only reads telemetry defensively via an injected instance (never imports the package directly), matching the optional pattern already used by modules/msal.

* docs(vitest-plugin-react-app): fix quick-start snippet and fixture wording

Add the missing 'import { expect } from vitest' to the quick-start test snippet, and reword the fixture-sharing bullet to clarify only seeded defaults are shared, not fusion/app state between tests.

* docs(vitest-plugin-react-app): clarify testApp fixture-sharing wording

Match the README wording fix: testApp's JSDoc previously implied one shared mocked scope across tests, when each test actually gets fresh fusion/app instances and only the seeded defaults are shared.

* fix(react-app): remove stale vitest-browser-react peer dependency

vitest-browser-react was only used by the now-removed ./vitest entry-point; src/ has no remaining references to it, so it stays a devDependency only.

* test(module-context): cover telemetry tracking for initial-context resolution

Adds coverage for the resolver-error and successful-resolution telemetry events raised by module.ts's postInitialize, alongside the existing console.warn-fallback test for when no telemetry module is registered.

* test(react-app): add useToken rerender regression test

Asserts acquireToken is not re-invoked when scopes are re-rendered with the same contents (a fresh array literal each time), but is re-invoked when the contents actually change.

* refactor(tests): format code for better readability in useToken and ContextModuleConfigurator tests

* feat(vitest-plugin-react-app): add defineProject config helper, fix entrypoint inference

- New `/config` entry-point exporting defineProject: a drop-in for Vitest's own
  defineProject, pre-wired with appTestVitePlugin and the
  @vitest/browser-playwright/chromium provider.
- appTestVitePlugin now infers entrypoint from the Vitest project's own root
  (via configResolved) instead of process.cwd() when none is given.
- @vitest/browser-playwright and playwright are now explicit peer dependencies.

* fix(module): keep addConfig additive when the same module descriptor is re-registered

ModulesConfigurator.addConfig discarded previously registered configure/afterConfig/
afterInit callbacks whenever it was called again for an already-registered module
name, even when the descriptor itself was the exact same object.

This broke config functions that register more than one named client against a
shared module singleton, e.g. calling configureHttpClient/useFrameworkServiceClient
more than once from @equinor/fusion-framework-module-http - only the last call's
client survived; earlier ones threw "No registered http client for key [...]" at
createClient() time, with no error at configuration time.

addConfig now only discards previous callbacks when a genuinely different module
descriptor replaces the old one for that name; re-registering the same shared
descriptor stays additive.

* feat(openapi-mock): split loadFakerMap into a Node-only /node entry point

loadFakerMap reads sidecar files from disk (node:fs) and, for a .ts/.js sidecar, shells
out to esbuild via @equinor/fusion-imports' importConfig - neither of which exists in
a browser (or browser-mode Vitest) runtime.

Move it to a separate @equinor/fusion-openapi-mock/node entry point (src/node.ts) so
importing the package's main entry never pulls Node-only code into a browser bundle.
A fields map built in code, or loaded ahead of time some other way, still works with
createOpenApiMock from the main entry point regardless of runtime.

Also bumps the read-pkg pnpm override (10.0.0 -> 10.1.0) picked up while resolving
this package's dependency tree.

* fix(vitest-plugin-react-app): warm up all source to avoid mid-test Vite reload

defineProject only transformed source as each test file requested it, so a route
component reached solely through a lazy/code-split import (e.g. React.lazy or a
route-DSL component path) could be discovered mid-run instead of up front - Vite
then reloads the page to pick it up, which fails the in-flight test file's import.

Set server.warmup.clientFiles: ['src/**/*.{ts,tsx}'] so all source under src/ is
pre-transformed before the first test request.

* chore: exclude cookbook test files from AI indexing, exempt generators.ts from single-export lint

- fusion-ai.config.ts: exclude cookbooks/**/__tests__/** and *.test|spec.{ts,tsx}
  from both the patterns and rawPatterns indexes - test files aren't consumer-facing
  API surface and were polluting cookbook search results.
- fusion-lint.config.json: add generators.ts to single-export-per-file's
  excludePattern, matching the existing convention for deliberately multi-export
  utility filenames (utils.ts, helpers.ts, options.ts, etc).

* test(cookbook-app-react): add Vitest coverage via vitest-plugin-react-app

- vitest.config.ts uses defineProject from
  @equinor/fusion-framework-vitest-plugin-react-app/config.
- App.test.tsx covers the App component via renderAppComponent.
- Root vitest.config.ts now globs cookbooks/**/vitest.config.ts.

* test(cookbook-app-react-context): add Vitest coverage via vitest-plugin-react-app

- vitest.config.ts uses defineProject from
  @equinor/fusion-framework-vitest-plugin-react-app/config.
- App.test.tsx covers seeded initial context, related-context items, and
  switching context via the app's own context module ref.

* test(cookbook-app-react-feature-flag): add Vitest coverage via vitest-plugin-react-app

- FeatureFlag/FeatureFlags moved into src/components/, useFeatureLogger moved
  into src/hooks/, each alongside its own vitest.config.ts uses defineProject
  from @equinor/fusion-framework-vitest-plugin-react-app/config.
- FeatureFlag.test.tsx / FeatureFlags.test.tsx cover rendering and toggling.

* test(cookbook-app-react-msal): add Vitest coverage via vitest-plugin-react-app

- vitest.config.ts uses defineProject from
  @equinor/fusion-framework-vitest-plugin-react-app/config.
- App.test.tsx covers the default signed-in mock user, overriding configure
  with a custom mock account, and resolving/rendering an access token.
- Removes the unused demo src/config.ts (dev-only console logging).

* test(cookbook-app-react-router): add Vitest coverage via vitest-plugin-react-app

- vitest.config.ts uses defineProject from
  @equinor/fusion-framework-vitest-plugin-react-app/config.
- Component and route-page tests via testWithRouter/createRouteProps, with
  fixtures built from seeded Faker.js generators (src/mocks/generators.ts)
  instead of hand-written literals.
- src/mocks/{openapi.json,fields.faker.ts,api-mock.ts} + testWithApiMock
  wire an OpenAPI-mock-backed HTTP middleware for full-router integration
  tests (routing-with-api-mock.test.tsx) with deterministic, schema-shaped
  responses.
- README.md documents the testing approach (Testing section).

* test(cookbook-app-react-module): add Vitest coverage via vitest-plugin-react-app

- vitest.config.ts uses defineProject from
  @equinor/fusion-framework-vitest-plugin-react-app/config.
- App.test.tsx covers the demo module's resolved foo/bar values, and overriding
  configure to seed a different demo configuration.
- fix(config.ts): setBar(() => 69) returned a plain number, not a valid
  ObservableInput, so BaseConfigBuilder silently discarded it and bar fell
  through to the module's own 10-second delayed default (5) instead of 69.
  setBar(async () => 69) matches setFoo's existing pattern and resolves
  immediately.

* style: apply biome format/lint fixes repo-wide

Runs `biome check --write --unsafe .` across the whole repo:

- Wraps long lines exceeding the configured line width (mostly test files with
  chained assertions/JSX props).
- cookbooks/app-react/src/config.ts: prefixes 3 intentionally-unused callback
  parameters (env, config, instance) with `_`, per
  lint/correctness/noUnusedFunctionParameters.

No behavioral changes.

* test(vitest-config): include coverage tracking for packages only

* docs(cookbook-app-react-msal): clarify config.ts removal has no functional impact

Corrects the test's doc comment, which claimed composing with a real
configure that no longer exists, and documents the intentional removal
in the changeset instead of silently dropping the note.

Addresses review comment on PR #5293 (discussion_r3778886987).

* docs(cookbook-app-react): document silenced lifecycle console.log in changeset

The demo console.log calls were intentionally left commented out to
keep Vitest output clean; the changeset previously claimed 'no runtime
changes', which wasn't accurate. Documents the behavior change instead
of restoring the logging.

Addresses review comment on PR #5293 (discussion_r3778887017).

* fix(cookbook-app-react-router): fix faker.date.past() with unfixed refDate

faker.seed() only makes the pseudo-random offset deterministic; without
a fixed refDate, date.past() still resolves relative to the current
wall-clock time, so generated joinDate values drift across days despite
the seeded-fixture contract. Adds a shared fakerRefDate constant used by
both generateUser and the User.joinDate field faker.

Addresses review comments on PR #5293 (discussion_r3778887040, discussion_r3778887110).

* docs(cookbook-app-react-context): fix changeset API names

The changeset named renderAppComponent and renderAppComponent.fusion.app,
neither of which exists in the added tests; they use the plugin's
render/app fixtures instead.

Addresses review comment on PR #5293 (discussion_r3778887093).

* fix(cookbook-app-react-router): add missing tsconfig project reference

Adds a reference to packages/vite-plugins/raw-imports matching the new
workspace dependency in package.json, so isolated package builds resolve
it correctly.

Addresses review comment on PR #5293 (discussion_r3778887191).

* fix(turbo): include bin/build in cli package's cached build outputs

The build task only declared dist/** as cacheable output, but
packages/cli's rollup build writes its bundled CLI binaries to
bin/build/*.mjs (gitignored, not under dist/). On a turbo cache hit
in CI, bin/build/ was never restored, so any cookbook invoking the
fusion-framework-cli binary during its own build failed with
ERR_MODULE_NOT_FOUND for bin/build/cli.mjs. Reproduced locally by
deleting bin/build and re-running turbo build without --force (cache
hit skipped restoring it); fixed by adding bin/build/** to outputs.

* test: migrate React tests to Vitest browser

* docs(testing): publish Fusion app testing guide (#5296)

* docs(testing): add canonical package guidance

* feat(docs): map package links in VuePress

* docs(testing): publish the app testing journey

* fix(docs): repair internal documentation links

* test(module-state): use browser PouchDB adapter

* test: decouple browser test projects

* fix(docs): scope package link rewrites

* Refactor code structure for improved readability and maintainability

* fix(pnpm): remove duplicate '@remix-run/router' entries from lockfile

* fix(define-project): enhance dependency optimization for esbuild scanner

* refactor(memfs-plugin): move mocks for fs/promises and import-script to the top for better organization

* docs(advanced): document `test.override(...)` for fixture overriding in test files

* chore: drop spurious react-app major changeset; ./vitest never shipped

* chore: drop spurious cli major changeset; ffc app test never shipped

* chore: fix two more false-major changesets; mock infra never released

* Remove coverage configuration from vitest.config.ts

* feat: add migration guides for transitioning to Fusion Vitest and explain Browser Mode default

* test(cookbook-app-react-router): await styling before asserting active link color

* style(vue-press): add missing trailing newline in vitest config

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
odinr added a commit that referenced this pull request Aug 19, 2026
…kbooks (#5297)

* feat(openapi-mock): add package for generating OpenAPI-shaped fake responses

Adds @equinor/fusion-openapi-mock, a framework-agnostic utility that fakes
OpenAPI 3 responses straight from a parsed spec document. Every operation
with an operationId is faked from its declared success response schema,
with overrides/register for edge cases, seed for repeatable output, and
a FieldFakerMap for realistic per-field values via @faker-js/faker.

No dependency on any HTTP or routing framework: resolve({ method, path,
query }) returns a plain { status, mock }, so it composes with
@equinor/fusion-framework-module-http's mock router or any other server.

Related: equinor/fusion-core-tasks#1660

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore(deps): update lockfile for @equinor/fusion-openapi-mock

* fix(openapi-mock): address review feedback on route matching, response fallback, and validation

- sort operations by specificity so literal routes win over templated ones
- fall back to lowest declared numeric status when no 2xx/default response exists
- validate parsed YAML faker maps and fetched documents are plain objects
- use an isolated faker instance per generation instead of mutating the global singleton
- refactor createOpenApiMock and apply-field-fakers into smaller, focused functions

* refactor(openapi-mock): favor readability and avoid mutable locals

- replace && shorthand and let-based branches with explicit if/early-return
- extract resolveResponseSchema, parseDocumentText helpers
- rewrite resolvePointer's pointer walk with Array#reduce instead of a mutable loop variable

* feat(module-msal): add a mock entry point for testing without credentials or network

Adds ./mock, so applications can run against the auth module without
credentials or network access:

    import { enableMsalMock, createMsalMockClient } from '@equinor/fusion-framework-module-msal/mock';

    enableMsalMock(configurator, (builder) => {
      builder.setAccount({ name: 'Ada Lovelace' });
    });

Only the MSAL client is substituted:

- MsalMockClient resolves tokens in-process and takes the same
  MsalClientConfig as the real MsalClient, keeps a real account cache
  keyed by homeAccountId (matching MSAL's getAccount/getAllAccounts
  semantics), and produces structurally valid, unsigned JWTs.
- The client construction seam (_createClient/_createClientConfig) is
  extracted from MsalConfigurator._processConfig, giving a supported
  seam for authenticating through something other than Entra ID. No
  client is built when the module is hoisted onto a host application's
  provider, since a client built during configuration would be
  discarded or shadow the host's signed-in user (_isHoisted).
- MsalMockConfigurator.setAccount lets a test declare the signed-in
  user on the builder; the account is applied before MsalProvider
  initializes, so the provider's own start-up path (including
  automatic login when signedOut + requiresAuth) acts on it.
- The config schema moved into MsalConfig.schema.ts with
  MsalConfigExtension, an extension point mock configuration merges
  into without the builder needing to cast past its type.

MsalProvider, MsalConfigurator and IMsalProvider are otherwise
untouched; the mock module's configure differs from the real module,
initialize does not.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(module-http): add a mock entry point with middleware and adapters

Adds @equinor/fusion-framework-module-http/mock, so HTTP clients answer
requests from registered route handlers instead of the network:

    import { enableHttpMock } from '@equinor/fusion-framework-module-http/mock';

    enableHttpMock(configurator, (builder) => {
      builder.configureClient('catalog', { baseUri: 'https://api.example.com' });
      builder.get('/items', () => Response.json([{ id: 1 }]));
    });

Route handlers are Fetch-standard middleware -
(request: Request) => Response | undefined | Promise<...> - tried in
registration order, with undefined falling through to the next one.
One router is shared across every named client a HttpMockConfigurator
builds, matched against each request's fully resolved URL.

HttpClient gains a _performFetch(uri, init) seam, isolated from
_fetch$ so a test double replaces only the network call while request
preparation, the response pipeline, and abort handling run unchanged.

Two adapters drop in an existing backend with no hard dependency on it:

- fromExpressStyleHandler(handler) adapts an Express-style (req, res)
  handler (or a whole framework built from them, like openapi-backend).
- fromOpenApiMock(openApiMock) adapts an @equinor/fusion-openapi-mock
  instance (duck-typed, no package dependency), so a real
  openapi.json/openapi.yaml fakes every response with no handlers
  written at all.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(module-service-discovery): add a mock entry point backed by an in-memory registry

Adds @equinor/fusion-framework-module-service-discovery/mock, so service
discovery resolves from an in-memory registry instead of the network:

    import { mockServiceDiscovery } from '@equinor/fusion-framework-module-service-discovery/mock';

    mockServiceDiscovery(configurator, { services: [{ key: 'apps', uri: 'https://apps.test' }] });

ServiceDiscoveryMockConfigurator builds the registry on the builder
itself - setBaseUri, addService, addServices, removeService,
setServices, setResolveUnknownServices - and the client is constructed
from that registry when the module builds its config, so a test never
has to construct a client just to add a service.

setBaseUri also lets the default service endpoints point at a local
mock server such as http://localhost:3000, so an application can make
real HTTP calls against Mockoon, Prism or the dev server without a
service worker intercepting requests.

Exposes the discovery client on the provider as .client, mirroring
MsalProvider.client, so a test can spy on it directly:

    vi.spyOn(fusion.modules.serviceDiscovery.client, 'resolveService').mockResolvedValue(service);

configureServiceDiscovery now also accepts a synchronous callback - the
underlying builder always allowed it, only the exported type required
a promise.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(module): fix configurator phase ordering and dot-path optional branches

Two independent fixes needed by the framework mock work:

- Module re-registration now replaces the prior module's configure,
  afterConfig, and afterInit callbacks instead of appending to them,
  keyed by module name (_dedupeModulesByName /
  _removeModuleCallbacks). This prevents stale callback execution
  when a mock module (e.g. enableMsalMock) overrides a real module
  registration, and fixes configurator phases running out of order
  or skipping post-configure hooks in certain initialization paths.
- DotPath now unwraps an optional object property with NonNullable
  the same way DotPathType already did, so a path BaseConfigBuilder
  understands is one _set no longer refuses. Given
  { foo?: { bar: string } }, 'foo.bar' is now a valid path, matching
  'foo' which was already allowed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(framework): add a mock entry point that composes each module's own test double

Adds @equinor/fusion-framework/mock for initializing the framework in a
test:

    import { mockFramework } from '@equinor/fusion-framework/mock';

    const fusion = await mockFramework((configurator) => {
      configurator.msal.setAccount({ name: 'Ada Lovelace' });
      configurator.serviceDiscovery.setBaseUri('http://localhost:6669');
    });

mockFramework runs the real configure -> initialize pipeline with the
real built-in modules and substitutes only the boundaries that leave
the process - the MSAL client and the service discovery client.
Module wiring, configuration validation and lifecycle hooks behave as
they do in production.

FrameworkMockConfigurator gains _pin/_getConfig, so a module supplied
through TModules gets the same kind of named accessor .msal and
.serviceDiscovery already have, rather than that pinning being
hand-written once per built-in mock. .services, .context and
.telemetry are exposed the same way, though they still perform real
I/O until those modules have a test double of their own. event is
intentionally left out - its configure factory reads ref to wire
event bubbling to a parent event provider when hoisted, and pinning
it would call configure() with ref always undefined, silently
disabling that bubbling.

The entry point owns no mock logic of its own: each module exports
its own test double from its own ./mock entry point, and this one
only composes the built-in set. It has no test-runner dependency and
provides no mocking API, since replacing an individual call belongs
to the test runner.

init() no longer throws when no DOM is present - the window.Fusion
assignment (for portal shells and widgets) is now skipped when
window is undefined, so a test runner using the node environment or
a server-side render no longer fails with
'ReferenceError: window is not defined'.

Also restructures documentation so each README is an entry point
rather than a manual: long-form content (including each package's
testing.md) moved into per-package docs/ folders, matching the
convention already used by @equinor/fusion-framework-module and
@equinor/fusion-framework-module-http.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address Copilot review comments on PR #5205

- msal-config-schema: reject unparseable version instead of coercing to "null"
- HttpMockRouter: reset RegExp lastIndex so global/sticky match patterns are reusable across requests
- extract decodeJwtSegment into its own file, fix atob/UTF-8 round-trip for non-ASCII claims
- service-discovery docs: dedupe heading, close dangling code fence

* fix(http-mock): await handleRequest in fromExpressStyleHandler

Prevents an async Express-style handler's rejection from becoming an
unhandled promise rejection instead of failing the middleware call.

* feat(module-event): add EventModuleConfigurator, waitForEvent/watchEvents, and operators subpath (#5241)

* feat(module-event): add EventModuleConfigurator, waitForEvent/watchEvents, and operators subpath

Add EventModuleConfigurator, a BaseConfigBuilder-based configurator with fluent
setOnDispatch/setOnBubble setters, replacing direct property assignment on the
config object (kept as a deprecated path, no major bump needed).

IEventModuleConfigurator is renamed to EventModuleConfig and converted from an
interface to a type; IEventModuleConfigurator is kept as a deprecated alias.

Narrow the dispatchEvent return type for registered FrameworkEventMap keys and
pre-constructed event instances so callers get back the specific event type.

Fix event$ to emit only after listeners and the bubble hook have run, matching
its documented contract (it previously emitted before dispatch).

Add waitForEvent/watchEvents test helpers and a ./operators subpath export for
filterEvent (moved from src/filter-event.ts to src/operators/filter-event.ts).

Rename configurator.ts -> EventModuleConfigurator.ts and provider.ts ->
EventModuleProvider.ts for filename-convention compliance, and add a docs/
folder (configuration, observable-patterns, lifecycle, testing).

* docs(vue-press): align event module docs with docs/ folder, move React page

Trim vue-press/src/modules/event/README.md to an @include of the package
README, add a docs/ mirror (configuration, observable-patterns, lifecycle,
testing) that @includes the package's own docs, matching the http/module
pattern.

Move the event React bindings page from event/react.md to
react/event/README.md, alongside react/router/, as an @include of
@equinor/fusion-framework-react-module-event's README. Update the sidebar to
match.

* style(module-event): apply Biome formatting fixes flagged by reviewdog

* fix(module-event): fix waitForEvent TDZ on sync completion, correct doc inaccuracies

* feat(module-analytics): add MockAnalyticsAdapter for recording tracked analytics events (#5242)

* feat(module-analytics): add MockAnalyticsAdapter for recording tracked analytics events

- Add MockAnalyticsAdapter implementing IAnalyticsAdapter, recording events
  in-memory for test assertions (getAnalytics, waitForAnalytic)
- Add ./mock subpath export
- Add vitest.config.ts to make the package a discoverable vitest project
- Document the new adapter in README.md and add a changeset

Implements equinor/fusion-core-tasks#1657

* test(module-analytics): verify MockAnalyticsAdapter through the real module pipeline

The existing unit tests only exercise MockAnalyticsAdapter's own logic in
isolation. Add an integration test, mirroring the pattern used by msal and
service-discovery's mock tests, that wires it through the real
enableAnalytics -> ModulesConfigurator -> AnalyticsProvider.initialize()
pipeline to prove events from provider.trackAnalytic and a real collector
actually reach it, and that it doesn't interfere with other adapters.

* docs(module-analytics): move testing docs to docs/testing.md, mirror in vue-press

Adopt the docs/ convention already used by the event, http, and module
packages: extract the README's inline Testing section into
packages/modules/analytics/docs/testing.md, link to it from README, and
add a vue-press mirror page (@include) with a sidebar entry.

* docs(module-analytics): slim README to overview, split domain docs into docs/

Move Adapters, Collectors, and Tracking Events Manually out of the README
into docs/adapters.md, docs/collectors.md, and docs/tracking-events.md,
matching the event/http/module docs/ convention. Drops duplicate
Configuration/Creating Custom Collectors content that had accumulated in
the old README's Tracking Events Manually section. README is now a slim
overview + entry points + documentation table + quick start, linking out
to docs/ for the domain reference. Mirrors and sidebar entries added in
vue-press.

* fix(module-analytics): reject waitForAnalytic when a predicate matcher throws

A throwing predicate matcher surfaced on RxJS's error channel, but the
subscribe observer had no error handler — the exception was reported
globally and the returned promise stayed pending forever. Add an error
handler that rejects through the same cleanup path, and cover it with a test.

* docs(module-analytics): fix broken abort and ModulesConfigurator examples in testing.md

The abort example never called controller.abort() (so copying it hangs
indefinitely) and redeclared `event` from the preceding example in the
same code block. The bespoke ModulesConfigurator example destructured
`analytics` from initialize() without a type — enableAnalytics only
registers the module at runtime, so this doesn't typecheck as written;
cast to IAnalyticsProvider, matching what the integration test does.

* feat(azure-identity,msal-node): add MockAuthProvider test double for auth module (#5244)

* feat(azure-identity,msal-node): add MockAuthProvider test double for auth module

Add a configurable IAuthProvider test double, exported from a new /mock
subpath in each package (@equinor/fusion-framework-module-azure-identity/mock,
@equinor/fusion-framework-module-msal-node/mock).

Unlike token_only mode's static-token providers (AuthProviderTokenOnly /
AuthTokenProvider), MockAuthProvider actually implements login/logout,
moving between signed-in and signed-out state, and exposes setAccount,
setAccessToken and setExpiresOn so a test can control the returned token
and simulate an expired token to exercise refresh logic. No real network
calls are made.

Implemented as two mirrored (twin) implementations rather than a shared
package, since azure-identity's and msal-node's IAuthProvider interfaces
differ in login's return type (AuthRecord vs. AuthenticationResult) and
neither package depends on the other.

Registers via enableAuthMock(configurator, configure?), a full 'auth'
module replacement mirroring @equinor/fusion-framework-module-msal/mock's
enableMsalMock, so no special-cased wiring is needed in either module.

Closes equinor/fusion-core-tasks#1665.

* fix(azure-identity,msal-node): make enableAuthMock generic over TModules/TRef

Replaces the IModulesConfigurator<any, any> parameter with a generic
<TModules extends Array<AnyModule> = Array<AnyModule>, TRef = unknown>
signature, so the optional configure callback keeps the caller's actual
TRef type instead of erasing it to unknown.

Addresses review feedback on PR #5244 (copilot-pull-request-reviewer).

* fix(azure-identity,msal-node): fix double-registration in mock README example

The Testing section called enableAuthMock(configurator) once for its
return value, then again with a configure callback to seed the account.
The second call replaces the first provider (enableAuthMock always
registers last), so the retained `auth` reference from the first call
was no longer the active provider by the time login/acquireAccessToken
ran against it.

Collapse to a single enableAuthMock call that both seeds the account
and returns the provider that stays registered.

Addresses review feedback on PR #5244 (copilot-pull-request-reviewer).

* feat(module-bookmark): add BookmarkMockClient and enableBookmarkMock (#5245)

* feat(module-bookmark): add BookmarkMockClient and enableBookmarkMock

Add a purpose-built mock for the bookmark module, exported from a new
`/mock` subpath (`@equinor/fusion-framework-module-bookmark/mock`),
following the client-boundary-swap pattern already used by msal's
`enableMsalMock`.

- `BookmarkMockClient`: in-memory `IBookmarkClient` backed by a Map of
  seeded bookmarks and a Set of favorite ids.
- `BookmarkMockConfigurator`: extends the real `BookmarkModuleConfigurator`
  with `setBookmarks`/`setCurrentBookmark`/`setFavorite`, and falls back to
  trivial application/context resolvers when no real `app`/`context`
  module is registered (both are required by the config schema).
- `enableBookmarkMock`/`bookmarkMockModule`: module descriptor mirroring
  the real module, only swapping `configure`.

Create, update, delete, and favorite calls all flow through the real
`BookmarkProvider` and `bookmark-flows/` epics unmodified.

Related: equinor/fusion-core-tasks#1667

* fix(module-bookmark): match sourceSystem, strip list payloads, normalize null payload in mock client

- matchesFilter now checks filter.sourceSystem (identifier/name/subSystem),
  since BookmarkProvider.getAllBookmarks always passes sourceSystem through
  the filter.
- getAllBookmarks now strips payloads from returned bookmarks, matching the
  real BookmarkClient's list behavior.
- updateBookmark now normalizes an explicit payload: null clear to undefined,
  matching setBookmarkData and avoiding a later getBookmarkData() emitting
  null as T.

Addresses PR #5245 review comments on BookmarkMockClient.ts:44, :140, :270.

* fix(module-bookmark): respect inherited resolve.application/context on mock fallback

Gate the mock's fallback application/context resolvers on initial?.resolve
as well as this._has(...), so a nested mock configurator does not overwrite
resolvers already inherited from a parent config.

Addresses PR #5245 review comment on BookmarkMockConfigurator.ts:150.

* test(module-bookmark): assert getBookmark rejects for an unknown id

Adds a Vitest assertion exercising the async error path through the real
BookmarkProvider.getBookmark fetch flow, so a regression that stops missing
ids from surfacing as observable errors would fail this suite.

Addresses PR #5245 review comment on BookmarkMockClient.ts:125.

* docs(module-bookmark): add required created/createdBy fields to mock seed example

The README's setBookmarks example omitted created/createdBy, which are
required on Bookmark, so copying the snippet as-is produced a type error.

Addresses PR #5245 review comment on README.md:245.

* docs(module-bookmark): add required created/createdBy fields to changeset example

The changeset's setBookmarks example omitted created/createdBy, which are
required on Bookmark, so the advertised snippet did not type-check.

Addresses PR #5245 review comment on .changeset/module-bookmark_mock-entry-point.md:15.

* refactor(module-context): extend BaseConfigBuilder and reorganize internal files

- ContextModuleConfigurator now extends BaseConfigBuilder (createConfigAsync),
  matching the pattern used by module-event/module-http/module-msal
- rename configurator.ts -> ContextModuleConfigurator.ts, split its exported
  types into ContextModuleConfig.ts and ContextModuleConfigurator.interface.ts
- move get/query/related-context-selector.ts into src/selectors/
- move parse-context-item.ts into src/utils/ and export it from utils/index.ts
- delete unused ContextConfigBuilder.ts
- add ContextModuleConfigurator.test.ts covering the new builder surface

No public export names change; index.ts still exports the same
ContextModuleConfigurator/IContextModuleConfigurator/ContextModuleConfig.

* feat(module-context): add mock and mock/fixtures entry points

Add ./mock (ContextMockConfigurator, enableContextMock) — a
ContextModuleConfigurator backed by an in-memory pool instead of a real
client. Only the data source is substituted; validateContext, resolveContext,
and parent-context propagation still run through the real ContextProvider.
Friendly seeding methods (setCurrentContext/setContexts/addContext/
setRelatedContexts) cover the common cases; setResolver is the escape hatch.

Add ./mock/fixtures (createContextItems, createContextItemFactory) — realistic
ContextItem generators backed by an optional @faker-js/faker peer dependency,
kept on a separate entry point so enableContextMock never pulls faker in.

Adds an http workspace dependency (for the vitest project setup) and a
package-level vitest.config.ts/test script.

* docs(module-context): restructure README into docs/ and mirror in vue-press

Move long-form content out of README.md into docs/data-model.md (ContextItem/
ContextItemType shape, query/related parameter types), docs/lifecycle.md
(setting/resolving context, initial-context resolution, parent/child
propagation) and docs/recipes.md (OData query params, path rewriting,
accepting related context type families, skipping initial-context lookup,
custom search errors), matching the module/http convention.

README.md keeps the elevator pitch, a new "How it fits together" section
covering the module's dependency graph (services required, event/navigation
optional and auto-detected) and hierarchy-aware parent connection, and a
documentation table linking to the three new pages.

vue-press/src/modules/context mirrors the split with @include stubs, and the
sidebar gains an Overview/Data model/Lifecycle/Recipes breakdown for Context.

* feat(framework): back mock context accessor with ContextMockConfigurator

FrameworkMockConfigurator.context now pins contextMockModule (from
@equinor/fusion-framework-module-context/mock) instead of the real context
module, so `.context` seeding methods (setCurrentContext/setContexts/
addContext/setRelatedContexts/setResolver) work the same way `.http` and
`.serviceDiscovery` already do — no more real I/O through context in tests.

docs/testing.md, docs/testing-extending.md and docs/testing-api.md document
both context mocking strategies: the in-memory ContextMockConfigurator above,
and mocking the context API's HTTP responses directly for tests that need to
exercise the real configurator/services/HTTP pipeline.

* chore: add changesets for context mock entry point and docs restructure

* fix(module-context): document createConfig as a breaking change

Extending BaseConfigBuilder changes the public createConfig method from
Promise<ContextModuleConfig> to Observable<ContextModuleConfig>. Bump
the changeset to major with migration guidance to createConfigAsync,
matching the precedent set when service-discovery/msal adopted the
same base class.

Addresses review comment on PR #5243 (discussion_r3744095539).

* docs(module-context): clarify setResolver only affects id-based lookup

setResolver overrides ContextClient.resolveContext(id) (used by
setCurrentContextById and the mock's initial context), not
ContextProvider.resolveContext(item), which always resolves related
context through setRelatedContexts regardless of setResolver. Reword
the class and method docs so the two same-named methods aren't
conflated.

Addresses review comment on PR #5243 (discussion_r3744095551).

* test(framework): assert seeding via .context reaches the built framework

The existing test only checked that .context is defined, which also
passed with the real ContextModuleConfigurator. Add a case that seeds
via configurator.context.setCurrentContext(...), initializes the
framework, and asserts fusion.modules.context.currentContext, covering
the _pin(contextMockModule) composition path.

Addresses review comment on PR #5243 (discussion_r3744095562).

* feat(module-telemetry): add MockTelemetryAdapter and wire it into FrameworkMockConfigurator

Closes equinor/fusion-core-tasks#1708

Adds a collecting MockTelemetryAdapter + TelemetryMockConfigurator behind a
new ./mock subpath, following the MockAnalyticsAdapter shape. Wires
FrameworkMockConfigurator.telemetry to the mock configurator so telemetry
tracked through a mocked framework instance never reaches Application
Insights or any real endpoint.

* fix(module-telemetry): close waitForItem resource leaks and simplify changeset example

* feat(module-feature-flag): add mock entry point for seeding test feature flags

Add `FeatureFlagMockConfigurator`, `enableFeatureFlagMock`, and a `./mock`
subpath so a test can seed flags in-memory via `addFeature`/`setFeatures`,
with no `localStorage`/URL dependency. Toggling still runs through the real
`FeatureFlagProvider`.

Also fixes a missing `telemetry` row in the framework's testing.md accessor
table (pre-existing gap, unrelated to this change).

Closes equinor/fusion-core-tasks#1707.

* test(module-feature-flag): assert async configure callbacks are awaited before initialize resolves

Addresses review comment on module.ts:49.

* docs(module-feature-flag): document the ./mock subpath in the README's API Reference

Addresses review comment on package.json:23.

* chore: add changeset for the testing.md telemetry accessor fix

Addresses review comment on testing.md:30.

* refactor(module-http): replace router-based mock with addMiddleware on the real configurator

Closes equinor/fusion-core-tasks#1706

Replaces enableHttpMock/HttpMockConfigurator/HttpMockRouter with addMiddleware
on the real HttpClientConfigurator, via a new HttpMiddlewareHandler that wraps
_performFetch instead of swapping the configurator out. A middleware calling
next(uri, init) falls through to the real call (or the next middleware)
unchanged, so a test exercises the exact same client and configuration a real
app registers.

createRouterMiddleware and createOpenApiMockMiddleware replace the old router
and its Express-style adapters, covering the same handler/OpenAPI-fake cases
without depending on a real routing library.

* docs(module-http): mirror the testing guide into vue-press

Adds the http module's docs/testing.md via the same @include pattern used
by client-configuration.md and server-sent-events.md, with a matching
sidebar entry.

* fix(framework): pin FrameworkMockConfigurator.http to the real HttpClientConfigurator

FrameworkMockConfigurator.http returns the real IHttpClientConfigurator now
that module-http's mock support is addMiddleware on that real configurator,
not a separate mock-specific one. Fake a response with
.http.addMiddleware(...) instead of swapping the module out.

Updates testing.md/testing-api.md/testing-extending.md to match.

* test(module-context): migrate off the removed enableHttpMock API

enableHttpMock/http.get(...) no longer exist; rebuilds the fixture with
configureHttp + createRouterMiddleware against configurator.addConfig,
matching the addMiddleware-based mocking module-http now exposes.

Updates the README's mention of the HTTP-mock alternative to match.

* feat(module-app): add MockAppClient mock entry point

Part of equinor/fusion-core-tasks#1706

Adds MockAppClient, exported from a new ./mock subpath, so a test can serve
one app's own manifest and config locally instead of contacting the app
service:

- getAppManifest resolves locally only for manifest.appKey with no tag.
- getAppConfig resolves locally for manifest.appKey when tag is absent or
  equal to manifest.build?.version.
- Every other request still goes through the real AppClient it wraps.

Also exports AppConfig as a value from the package root (previously
type-only), so a test can construct one directly.

* feat(app): add mockAppModules and AppMockConfigurator /mock entry point

Closes equinor/fusion-core-tasks#1706

Adds a ./mock entry point: mockAppModules runs an application's real module
pipeline — the real event/http/msal modules, the real AppConfigurator
configuration pipeline and real lifecycle — against a mocked parent Fusion
instance, so a test exercises the wiring an application actually depends on
instead of a reimplementation of it.

AppMockConfigurator extends the real AppConfigurator, pinning http and msal
to the same test doubles FrameworkMockConfigurator uses so .http/.msal are
reachable synchronously; event is deliberately left unpinned since its
configure factory needs a real ref. Its own addConfig override redirects a
pinned module's later registrations at the pinned descriptor. The base
AppConfigurator constructor's own call into _configureHttpClientsFromAppConfig
is deferred (via a no-op override + an explicit super call after pinning) so
it never runs before this class's own fields exist.

enableAppManifestMock registers the app module on a parent mockFramework
configurator, serving one app's manifest/config locally while delegating
everything else to service discovery. mockAppModules uses it to build its
zero-configuration default parent.

Restructures README.md into an entry point pointing at docs/http-clients.md,
docs/bookmarks.md and docs/testing.md, matching the convention already used
by module-http and module-msal.

* fix(app): remove dead initialize-app-configurator.ts duplicate

initialize-app-modules.ts superseded this file when initializeAppModules gained the TConfigurator type parameter, but the old file was left in place -- nothing imported it, so it was dead code that still compiled and still linted (fusion-lint's require-tsdoc flagged it for the missing @template tags the newer file already has).

* style: apply biome formatting fixes

Long import lists wrapped onto multiple lines, a ternary split across lines,
and a single-quoted string containing an apostrophe rewritten with double
quotes -- no behavior change.

* refactor(module-http): flatten mock/adapters into mock/, fix lint warnings

mock/ only ever had one barrel (index.ts) plus an adapters/ subfolder holding
the 3 files it re-exported -- two barrels doing the same job for exactly one
category of file. Moved create-open-api-mock-middleware.ts,
create-router-middleware.ts and resolve-open-api-mock-response.ts up into
mock/ directly and dropped the adapters/ barrel, fixing relative import
depths and OpenApiMockLike's re-export path along the way.

Also fixes the fusion-lint warnings the files carried before the move:
renamed each file to match its exported symbol (filename-convention),
added the missing intent comments on create-router-middleware.ts's
if-blocks and .map() (require-intent-comment), and replaced a comma-operator
expression in compilePath with an explicit if/return (noCommaOperator).

* fix(module-app): treat only an absent tag as 'no tag' in MockAppClient

getAppManifest's !args.tag and getAppConfig's isOwnTag both treated an
explicit empty-string tag the same as an absent one, so getAppManifest({
appKey, tag: '' }) and getAppConfig({ appKey, tag: '' }) answered locally
instead of delegating, contradicting the documented 'tag is absent' rule.
Compare against undefined explicitly instead of a falsy check.

Adds MockAppClient.test.ts covering both methods' local-vs-delegate
decision, including the explicit-empty-string-tag case.

* docs(vue-press): add changeset for the http module testing page

The new vue-press page and sidebar entry is a consumer-facing docs change
to the published @equinor/fusion-framework-docs package, which needed a
changeset like every other vue-press page addition in this repo.

* chore(changesets): bump http mock changesets to major

enableHttpMock, HttpMockConfigurator, and HttpMockRouter were removed
from @equinor/fusion-framework-module-http's /mock entry point, and
FrameworkMockConfigurator.http's return type changed from a mock-specific
configurator to the real IHttpClientConfigurator -- both are breaking
changes for existing consumers and need a major bump, not minor.

* docs(app): correct mockAppModules' network-access claim

The http module registered by AppMockConfigurator is the real
HttpClientConfigurator -- addMiddleware only short-circuits requests a
middleware matches with a response, so an unmatched client or a middleware
that calls next still reaches the real network. msal is the only module
genuinely backed by a test double. Reworded the doc comment to describe
what is actually substituted instead of promising no network access.

* fix(module-http): abort the real network call through middleware on abort()

next(...) resolves through a Promise (HttpMiddlewareHandler's toPromise),
so a middleware calling it creates a subscription to _performFetch that
sits outside the tree takeUntil(this._abort$) tears down -- the outer
request settled on abort(), but the underlying fetch kept running.

abort() now also aborts a per-request AbortSignal combined into the
request init, so _performFetch (fromFetch by default) is cancelled
directly regardless of whether middleware severed the RxJS teardown
chain. Adds a regression test with a pass-through middleware asserting
the underlying fetch's signal is aborted.

* test: add react-app hook testing utilities and msal proxy/hoisting coverage

- add render-app-hook testing utility + useAccessToken hook test in react/app
- add msal-hoisting coverage in packages/app
- add createProxyProvider behavior coverage in modules/msal

* test(react-app): add useTrackFeature hook tests

Add @equinor/fusion-framework-module-telemetry as a devDependency so tests
can read the mock telemetry adapter via fusion.modules.telemetry.getAdapter('mock').

* test(react-app): render Apploader against a real dynamic-import child app script

Add renderAppComponent, a component-level counterpart to renderAppHook, and
use it in a new Apploader.test.tsx that exercises the real useApploader ->
App.initialize() -> dynamic import() pipeline (no vi.mock of createApp or
the import mechanism), backed by a fixture child app script.

* test(react-app): add useCurrentBookmark hook tests

Covers the app-scoped happy path, filtering out a bookmark that belongs to a different app, and the framework-scoped fallback with its deprecation warning. Adds @equinor/fusion-framework-module-bookmark as a devDependency for the mock helpers.

* test(react-app): add useCurrentContext hook tests

Covers resolving the seeded initial context and switching to another seeded item via setCurrentContext, against the app-scoped context module mock. Adds @equinor/fusion-framework-module-context as a devDependency for the mock helpers.

* test(react-app): add useFeature hook tests

Covers resolving an app-scoped flag, app-over-framework override on a shared key, a framework-only flag surfacing through the merged stream, and toggleFeature inverting the current value with no explicit argument.

* test(react-app): add useHelpCenter hook tests

Covers every open* action, asserting the framework event dispatched on the app's event module carries the expected page and payload.

* test(react-app): add useToken and useCurrentAccount hook tests

Completes coverage of the msal folder alongside the existing useAccessToken tests: useToken's full AuthenticationResult happy/error paths, and useCurrentAccount against the default mock user, a custom msal.setAccount() override, and the signed-out case.

* test(react-app): add useAppSetting and useAppSettings hook tests

Covers the settings folder's public hooks against the real fetch/update round-trip (via an in-memory apps-client mock): resolving persisted values, default-value fallback before resolution, persisting an update (direct value and updater-function forms), and surfacing a persistence failure through onError instead of throwing. useAppSettingsStatus is internal (not exported from settings/index.ts) and is exercised indirectly through these onLoading/onUpdating-adjacent paths.

* docs(react-app): fix HTTP config example and nullable/async return types

Fixes issues found while writing hook tests: README/createComponent examples used configurator.http.configureClient (test-only mock API) instead of the real configurator.configureHttpClient; setCurrentContext/useFrameworkCurrentContext were documented as void-returning when they actually return void | Promise<ContextItem | null>; AuthenticationResult.account/expiresOn were documented as non-nullable; useCurrentBookmark's deprecated framework-scope fallback and its console warning were undocumented.

* docs(react-app): add app testing guide

* fix(react-app): unmount rendered hooks in msal token tests

Prevents the async token-acquisition effect from flushing a state
update after happy-dom's window is torn down at the end of the test
file, which CI surfaced as an uncaught 'window is not defined'
exception (all assertions still passed, but the run was marked
failed).

* test: apply Biome line-length formatting suggestions

Collapse two waitFor assertions that fit within the line-length limit
back onto a single line, per Biome/reviewdog suggestions on PR #5271.

* fix(app): treat empty-string assetUri as an explicit override

if (assetUri) silently ignored a caller-supplied empty string, which is a meaningful value (selects a root-relative script path). Check against undefined instead so the override always applies when the caller passes the option.

* docs(react-app): clarify network-boundary faking scope; add error-state example

The docs overstated that renderAppHook/renderAppComponent fake the entire network boundary. Only requests a seeded router middleware answers are faked; unmatched requests still reach the real network. Also documents renderAppComponent under the /testing subpath table, and adds an error-state test example mirroring Apploader.test.tsx.

* docs(react-app): remove invalid @template Result tag from renderAppComponent

renderAppComponent has no Result type parameter; the tag referenced a generic that does not exist on this function and would fail TSDoc validation.

* test(react-app): assert the seeded bookmark actually resolved before checking the app-key filter

The previous waitFor(() => expect(result.current).toBeDefined()) was a no-op since result.current is always a defined object. Now waits on the unfiltered useAppModules().bookmark provider to resolve the seeded bookmark first, so the assertion that useCurrentBookmark hides it can't pass trivially against a still-pending value.

* docs(changeset): qualify cross-repo issue reference

Closes #1716 resolved to an unrelated PR in this repo. Qualifying as equinor/fusion-core-tasks#1716 points at the correct issue while preserving auto-close-on-merge.

* test: apply Biome line-length formatting suggestions

Reformat two msal-hoisting.test.ts call expressions and two useCurrentContext.test.tsx object literals that exceed the line-length limit, per Biome/reviewdog suggestions on PR #5271.

* chore(deps): bump vitest to ^4.1.10

Bumps vitest and @vitest/coverage-v8 across the workspace, and migrates
vitest.config.ts package.json imports to the `with { type: 'json' }`
import attribute required by the newer vitest/vite toolchain.

* feat(cli): add `ffc app test` command

Runs the application's own Vitest suite with manifest/config/module-configurator resolved
the same way `ffc app build`/`ffc app dev` do, exposed via a Vite plugin's virtual modules
to @equinor/fusion-framework-react-app/vitest's test/render fixtures.

Renamed packages/cli/src/lib/testing -> lib/vitest and the ./testing export -> ./vitest.

* Update packages/cli/src/cli/commands/app/test.command.ts

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(cli): throw on an explicit but missing --configure path

An explicitly supplied configure path is a user request, not a convention lookup — a typo
should fail loudly instead of silently running the test suite without the app's modules.

* refactor: clean up code formatting for better readability

* feat(react-app): vitest render/test-app fixtures under src/vitest

- rename src/testing -> src/vitest, add test-app/scope fixture chain
  (test-app.tsx, test.tsx, scope/*) for rendering app components and
  hooks with a fully wired Fusion test environment
- fix(react): useFrameworkModule warns with the correct module name
- fix(module-context): stop warning when there's no initial context
  to resolve
- fix(module-msal): drop stray debug console.log in resolve-version
- fix(log): guard process.env access for Vitest Browser Mode
- chore: biome format fixes surfaced by pnpm check (unrelated pre-existing
  drift in cli/linting/event packages)

* refactor: extract react-app vitest helpers into vitest-plugin-react-app

Move renderAppHook/renderAppComponent/testApp and the app-test Vite plugin out
of @equinor/fusion-framework-react-app's ./vitest entry-point and
@equinor/fusion-framework-cli's ./vitest entry-point into a new standalone
package, @equinor/fusion-framework-vitest-plugin-react-app, built on
vitest-browser-react instead of @testing-library/react.

- Remove the ffc app test CLI command and cli/src/lib/vitest (superseded by
  appTestVitePlugin registered directly in a project's vitest.config.ts).
- Add packages/vitest-plugin/react-app with README, appTestVitePlugin,
  renderAppHook, renderAppComponent, testApp, and a /test entry-point
  exporting pre-seeded test/render fixtures.
- Update react-app's README/docs and package.json to drop ./vitest and
  point at the new package instead.
- Fix stray console.log in msal's resolveVersion mismatch warning, and guard
  fusion-log's process.env access for environments without a Node process
  global (Vitest Browser Mode).
- Install Playwright browsers in CI for the new package's and react-app's
  Vitest Browser Mode tests.
- Add CODEMAP.md entry for the new package.

Changesets included for all affected packages.

* ci: fix Playwright install invocation to filter into a package that depends on it

pnpm exec at the repo root only resolves bins from root's own node_modules,
and playwright is a devDependency of react-app/vitest-plugin-react-app only,
not the workspace root - the previous 'pnpm exec playwright install' failed
with ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL / 'Command "playwright" not found'.

* fix(vitest-plugin-react-app): declare vitest as a peer dependency

The package uses vitest's expect/vi APIs at test-authoring time but only listed it as a devDependency, so consumers weren't told to install it themselves.

* fix(vitest-plugin-react-app): correct tsconfig project reference

src/index.ts and src/resolve-app-test-env.ts import @equinor/fusion-imports (utils/imports), but the tsconfig referenced utils/observable, an unrelated package.

* test(vitest-plugin-react-app): cover resolveAppTestEnv and appTestVitePlugin

The package's own resolution/virtual-module logic had no test coverage and no vitest.config.ts, so it wasn't even picked up as a project by the root Vitest run.

* docs: add missing changeset for CLI ./vitest entry-point removal

The removal of ffc app test, testApplication, the ./vitest entry-point, and resolveAppTestEnv from @equinor/fusion-framework-cli had no changeset.

* fix(module-context): make telemetry an optional peer dependency

The context module only reads telemetry defensively via an injected instance (never imports the package directly), matching the optional pattern already used by modules/msal.

* docs(vitest-plugin-react-app): fix quick-start snippet and fixture wording

Add the missing 'import { expect } from vitest' to the quick-start test snippet, and reword the fixture-sharing bullet to clarify only seeded defaults are shared, not fusion/app state between tests.

* docs(vitest-plugin-react-app): clarify testApp fixture-sharing wording

Match the README wording fix: testApp's JSDoc previously implied one shared mocked scope across tests, when each test actually gets fresh fusion/app instances and only the seeded defaults are shared.

* fix(react-app): remove stale vitest-browser-react peer dependency

vitest-browser-react was only used by the now-removed ./vitest entry-point; src/ has no remaining references to it, so it stays a devDependency only.

* test(module-context): cover telemetry tracking for initial-context resolution

Adds coverage for the resolver-error and successful-resolution telemetry events raised by module.ts's postInitialize, alongside the existing console.warn-fallback test for when no telemetry module is registered.

* test(react-app): add useToken rerender regression test

Asserts acquireToken is not re-invoked when scopes are re-rendered with the same contents (a fresh array literal each time), but is re-invoked when the contents actually change.

* refactor(tests): format code for better readability in useToken and ContextModuleConfigurator tests

* feat(vitest-plugin-react-app): add defineProject config helper, fix entrypoint inference

- New `/config` entry-point exporting defineProject: a drop-in for Vitest's own
  defineProject, pre-wired with appTestVitePlugin and the
  @vitest/browser-playwright/chromium provider.
- appTestVitePlugin now infers entrypoint from the Vitest project's own root
  (via configResolved) instead of process.cwd() when none is given.
- @vitest/browser-playwright and playwright are now explicit peer dependencies.

* fix(module): keep addConfig additive when the same module descriptor is re-registered

ModulesConfigurator.addConfig discarded previously registered configure/afterConfig/
afterInit callbacks whenever it was called again for an already-registered module
name, even when the descriptor itself was the exact same object.

This broke config functions that register more than one named client against a
shared module singleton, e.g. calling configureHttpClient/useFrameworkServiceClient
more than once from @equinor/fusion-framework-module-http - only the last call's
client survived; earlier ones threw "No registered http client for key [...]" at
createClient() time, with no error at configuration time.

addConfig now only discards previous callbacks when a genuinely different module
descriptor replaces the old one for that name; re-registering the same shared
descriptor stays additive.

* feat(openapi-mock): split loadFakerMap into a Node-only /node entry point

loadFakerMap reads sidecar files from disk (node:fs) and, for a .ts/.js sidecar, shells
out to esbuild via @equinor/fusion-imports' importConfig - neither of which exists in
a browser (or browser-mode Vitest) runtime.

Move it to a separate @equinor/fusion-openapi-mock/node entry point (src/node.ts) so
importing the package's main entry never pulls Node-only code into a browser bundle.
A fields map built in code, or loaded ahead of time some other way, still works with
createOpenApiMock from the main entry point regardless of runtime.

Also bumps the read-pkg pnpm override (10.0.0 -> 10.1.0) picked up while resolving
this package's dependency tree.

* fix(vitest-plugin-react-app): warm up all source to avoid mid-test Vite reload

defineProject only transformed source as each test file requested it, so a route
component reached solely through a lazy/code-split import (e.g. React.lazy or a
route-DSL component path) could be discovered mid-run instead of up front - Vite
then reloads the page to pick it up, which fails the in-flight test file's import.

Set server.warmup.clientFiles: ['src/**/*.{ts,tsx}'] so all source under src/ is
pre-transformed before the first test request.

* chore: exclude cookbook test files from AI indexing, exempt generators.ts from single-export lint

- fusion-ai.config.ts: exclude cookbooks/**/__tests__/** and *.test|spec.{ts,tsx}
  from both the patterns and rawPatterns indexes - test files aren't consumer-facing
  API surface and were polluting cookbook search results.
- fusion-lint.config.json: add generators.ts to single-export-per-file's
  excludePattern, matching the existing convention for deliberately multi-export
  utility filenames (utils.ts, helpers.ts, options.ts, etc).

* test(cookbook-app-react): add Vitest coverage via vitest-plugin-react-app

- vitest.config.ts uses defineProject from
  @equinor/fusion-framework-vitest-plugin-react-app/config.
- App.test.tsx covers the App component via renderAppComponent.
- Root vitest.config.ts now globs cookbooks/**/vitest.config.ts.

* test(cookbook-app-react-context): add Vitest coverage via vitest-plugin-react-app

- vitest.config.ts uses defineProject from
  @equinor/fusion-framework-vitest-plugin-react-app/config.
- App.test.tsx covers seeded initial context, related-context items, and
  switching context via the app's own context module ref.

* test(cookbook-app-react-feature-flag): add Vitest coverage via vitest-plugin-react-app

- FeatureFlag/FeatureFlags moved into src/components/, useFeatureLogger moved
  into src/hooks/, each alongside its own vitest.config.ts uses defineProject
  from @equinor/fusion-framework-vitest-plugin-react-app/config.
- FeatureFlag.test.tsx / FeatureFlags.test.tsx cover rendering and toggling.

* test(cookbook-app-react-msal): add Vitest coverage via vitest-plugin-react-app

- vitest.config.ts uses defineProject from
  @equinor/fusion-framework-vitest-plugin-react-app/config.
- App.test.tsx covers the default signed-in mock user, overriding configure
  with a custom mock account, and resolving/rendering an access token.
- Removes the unused demo src/config.ts (dev-only console logging).

* test(cookbook-app-react-router): add Vitest coverage via vitest-plugin-react-app

- vitest.config.ts uses defineProject from
  @equinor/fusion-framework-vitest-plugin-react-app/config.
- Component and route-page tests via testWithRouter/createRouteProps, with
  fixtures built from seeded Faker.js generators (src/mocks/generators.ts)
  instead of hand-written literals.
- src/mocks/{openapi.json,fields.faker.ts,api-mock.ts} + testWithApiMock
  wire an OpenAPI-mock-backed HTTP middleware for full-router integration
  tests (routing-with-api-mock.test.tsx) with deterministic, schema-shaped
  responses.
- README.md documents the testing approach (Testing section).

* test(cookbook-app-react-module): add Vitest coverage via vitest-plugin-react-app

- vitest.config.ts uses defineProject from
  @equinor/fusion-framework-vitest-plugin-react-app/config.
- App.test.tsx covers the demo module's resolved foo/bar values, and overriding
  configure to seed a different demo configuration.
- fix(config.ts): setBar(() => 69) returned a plain number, not a valid
  ObservableInput, so BaseConfigBuilder silently discarded it and bar fell
  through to the module's own 10-second delayed default (5) instead of 69.
  setBar(async () => 69) matches setFoo's existing pattern and resolves
  immediately.

* style: apply biome format/lint fixes repo-wide

Runs `biome check --write --unsafe .` across the whole repo:

- Wraps long lines exceeding the configured line width (mostly test files with
  chained assertions/JSX props).
- cookbooks/app-react/src/config.ts: prefixes 3 intentionally-unused callback
  parameters (env, config, instance) with `_`, per
  lint/correctness/noUnusedFunctionParameters.

No behavioral changes.

* test(vitest-config): include coverage tracking for packages only

* docs(cookbook-app-react-msal): clarify config.ts removal has no functional impact

Corrects the test's doc comment, which claimed composing with a real
configure that no longer exists, and documents the intentional removal
in the changeset instead of silently dropping the note.

Addresses review comment on PR #5293 (discussion_r3778886987).

* docs(cookbook-app-react): document silenced lifecycle console.log in changeset

The demo console.log calls were intentionally left commented out to
keep Vitest output clean; the changeset previously claimed 'no runtime
changes', which wasn't accurate. Documents the behavior change instead
of restoring the logging.

Addresses review comment on PR #5293 (discussion_r3778887017).

* fix(cookbook-app-react-router): fix faker.date.past() with unfixed refDate

faker.seed() only makes the pseudo-random offset deterministic; without
a fixed refDate, date.past() still resolves relative to the current
wall-clock time, so generated joinDate values drift across days despite
the seeded-fixture contract. Adds a shared fakerRefDate constant used by
both generateUser and the User.joinDate field faker.

Addresses review comments on PR #5293 (discussion_r3778887040, discussion_r3778887110).

* docs(cookbook-app-react-context): fix changeset API names

The changeset named renderAppComponent and renderAppComponent.fusion.app,
neither of which exists in the added tests; they use the plugin's
render/app fixtures instead.

Addresses review comment on PR #5293 (discussion_r3778887093).

* fix(cookbook-app-react-router): add missing tsconfig project reference

Adds a reference to packages/vite-plugins/raw-imports matching the new
workspace dependency in package.json, so isolated package builds resolve
it correctly.

Addresses review comment on PR #5293 (discussion_r3778887191).

* fix(turbo): include bin/build in cli package's cached build outputs

The build task only declared dist/** as cacheable output, but
packages/cli's rollup build writes its bundled CLI binaries to
bin/build/*.mjs (gitignored, not under dist/). On a turbo cache hit
in CI, bin/build/ was never restored, so any cookbook invoking the
fusion-framework-cli binary during its own build failed with
ERR_MODULE_NOT_FOUND for bin/build/cli.mjs. Reproduced locally by
deleting bin/build and re-running turbo build without --force (cache
hit skipped restoring it); fixed by adding bin/build/** to outputs.

* test: migrate React tests to Vitest browser

* docs(testing): publish Fusion app testing guide (#5296)

* docs(testing): add canonical package guidance

* feat(docs): map package links in VuePress

* docs(testing): publish the app testing journey

* fix(docs): repair internal documentation links

* test(module-state): use browser PouchDB adapter

* test: decouple browser test projects

* fix(docs): scope package link rewrites

* Refactor code structure for improved readability and maintainability

* fix(pnpm): remove duplicate '@remix-run/router' entries from lockfile

* fix(define-project): enhance dependency optimization for esbuild scanner

* refactor(memfs-plugin): move mocks for fs/promises and import-script to the top for better organization

* docs(advanced): document `test.override(...)` for fixture overriding in test files

* chore: drop spurious react-app major changeset; ./vitest never shipped

* chore: drop spurious cli major changeset; ffc app test never shipped

* chore: fix two more false-major changesets; mock infra never released

* Remove coverage configuration from vitest.config.ts

* feat: add migration guides for transitioning to Fusion Vitest and explain Browser Mode default

* test(cookbook-app-react-router): await styling before asserting active link color

* style(vue-press): add missing trailing newline in vitest config

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 bug Something isn't working 📚 documentation Improvements or additions to documentation 🚀 feature New feature or request 🧬 Modules

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants