@equinor/fusion-framework-vitest-plugin-react-app

中文文档
2.0.5发布于 2026/11/12ISCTypeScriptES Module近 30 天下载 2965

Vite plugin resolving an application's manifest, config, and module-configurator as virtual modules for testing.

原始使用文档
文档目录14 节
  1. Installation
  2. Documentation
  3. Overview
  4. Entry points
  5. Quick start
  6. renderAppHook
  7. Basic usage
  8. Acquire a token for a named user
  9. Reuse a pre-built parent Fusion instance
  10. renderAppComponent
  11. Example: asserting loading and error states
  12. testApp
  13. appTestVitePlugin
  14. Notes

@equinor/fusion-framework-vitest-plugin-react-app

Vite plugin and Vitest helpers for testing Fusion Framework React applications inside a real, mock-backed application module scope — the same FrameworkProvider + ModuleProvider nesting renderApp/createComponent wire up in production, with the real event/http/msal module pipeline and only the network boundary faked.

Note: Requires vitest-browser-react (peer dependency) and Vitest's Browser Mode configured for the project — these render helpers mount real components in a real browser, not happy-dom/jsdom.

Use this package when you need to:

  • Render a component or hook against real application modules — useAppModule, useAccessToken, useCurrentContext, useCurrentBookmark, useAppSetting, and similar hooks resolve for real, instead of being hand-mocked per test.
  • Test against the application's own manifest, config, and module-configurator — the same resolution pipeline ffc app build/ffc app dev use, served as virtual modules by appTestVitePlugin.
  • Share seeded fixture defaults across a test file — testApp, a vitest test extended with appEnv/configureApp/app/render/renderHook fixtures, instead of repeating options on every call. Each test still gets its own fresh fusion/app instances; only the seeded defaults are shared, not state between tests.
  • Fake one endpoint's URL without losing the rest of the config — mergeEnvConfig merges environment/endpoints overrides into an AppEnv's config; a plain object spread over AppConfig (which stores both behind private fields) silently drops everything.

Installation

pnpm add -D vitest@^5 playwright @vitest/browser-playwright@^5 vitest-browser-react@^2.3.0 \
  @equinor/fusion-framework-vitest-plugin-react-app
pnpm exec playwright install chromium

React, React DOM, RxJS, and Vite are peer dependencies normally supplied by the app.

Documentation

TaskGuide
Understand the package and choose a subjectOverview
Install, configure, and run the first app testGetting started
Override Vitest defaults or resolve non-standard app filesConfiguration
Compose fixtures, explicit render options, and app lifecycle testsAdvanced usage
Diagnose setup, browser, app-resolution, and network failuresTroubleshooting
Seed authentication, context, bookmarks, feature flags, HTTP, analytics, and telemetryModule mocks
Understand the Browser Mode default or choose another rendererWhy Browser Mode is the default
Move an existing @testing-library/react/jsdom suite onto this packageMigrate an existing app
Choose between app, framework, module, HTTP, and runner-level testsChoose a testing layer

Overview

Any hook or component that reads from the application module scope — useAppModule, useAccessToken, useCurrentContext, useCurrentBookmark, useAppSetting, and so on — or from the parent framework via useFramework, needs to run inside the same provider nesting renderApp/createComponent wire up in production: a FrameworkProvider (the parent Fusion instance) around a ModuleProvider (the app's own modules).

renderAppHook and renderAppComponent build that nesting for you, using mockFramework (@equinor/fusion-framework/mock) and mockAppModules (@equinor/fusion-framework-app/mock) — the real event/http/msal module pipeline. Only requests a seeded middleware answers are faked; a request with no matching middleware still reaches the real network. This means:

  • MSAL signs in a default "Test User" with zero configuration — useAccessToken/useToken resolve a real, structurally-valid (unsigned) JWT out of the box.
  • event dispatches and listens for real FrameworkEvents.
  • Anything that talks to an HTTP endpoint (e.g. an app's own manifest, settings) is answered by a mock client rather than a real network call — see each hook's own docs for what's pre-wired versus what you need to seed yourself (e.g. via enableBookmarkMock, enableContextMock, enableFeatureFlagMock, or a configurator.http.addMiddleware(...) router).

Use renderAppHook for a hook in isolation; use renderAppComponent when you need to assert on rendered output (e.g. loading/error states, DOM structure); use testApp or the /test entry-point's test/render when several cases in a file share one seeded scope.

Entry points

  • @equinor/fusion-framework-vitest-plugin-react-app — appTestVitePlugin, renderAppComponent, renderAppHook, testApp. No app-specific resolution; you pass env/configure yourself.
  • @equinor/fusion-framework-vitest-plugin-react-app/test — test, render. Require appTestVitePlugin registered in vitest.config.ts; appEnv/configureApp are resolved automatically from the application's own manifest, config, and module-configurator.
  • @equinor/fusion-framework-vitest-plugin-react-app/config — defineProject. Registers appTestVitePlugin, headless Playwright/Chromium, test-file inclusion, and lazy-import warmup while accepting ordinary Vitest configuration overrides.

Quick start

Use the browser-ready project config in your app's vitest.config.ts:

import { defineProject } from '@equinor/fusion-framework-vitest-plugin-react-app/config';

export default defineProject();

Then use the pre-seeded test fixture — no per-test appEnv/configureApp wiring:

import { expect } from 'vitest';
import { test } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
import { App } from '../App';

test('renders the app', async ({ render }) => {
  const screen = await render(<App />);
  await expect.element(screen.getByRole('heading')).toBeVisible();
});

Or, in a plain describe/it file, the equivalent render function:

import { describe, expect, it } from 'vitest';
import { render } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
import { App } from '../App';

describe('App', () => {
  it('renders the app', async () => {
    const { getByRole } = await render(<App />);
    await expect.element(getByRole('heading')).toBeVisible();
  });
});

renderAppHook

Renders a hook inside a real, mock-backed application module scope.

Signature:

function renderAppHook<Result, Props = undefined, TModules = unknown, TEnv extends AppEnv = AppEnv>(
  render: (initialProps?: Props) => Result,
  options?: {
    configure?: AppMockConfigureFn<TModules, TEnv>;
    env?: TEnv;
    fusion?: Fusion;
  } & Omit<RenderHookOptions<Props>, 'wrapper'>,
): Promise<RenderHookResult<Result, Props> & { fusion: { framework: Fusion; app: AppModulesInstance<TModules> } }>;
OptionDescription
configureCallback forwarded to mockAppModules for seeding app-scope modules (bookmark, context, feature-flag, msal, etc.)
envThe application environment (manifest); defaults to a generic standalone test-app
fusionThe parent Fusion instance; defaults to a fresh mockFramework instance serving this app's own manifest

Any other renderHook option (e.g. initialProps) is forwarded as-is. The result carries the usual renderHook return values (result, rerender, unmount) plus fusion — the same instances the hook rendered against — for driving a module the hook itself doesn't return.

Basic usage

import { renderAppHook } from '@equinor/fusion-framework-vitest-plugin-react-app';
import { useAccessToken } from '@equinor/fusion-framework-react-app/msal';

test('resolves an access token', async () => {
  const { result } = await renderAppHook(() => useAccessToken({ scopes: ['User.Read'] }));
  await vi.waitFor(() => expect(result.current.pending).toBe(false));
  expect(result.current.token).toBeDefined();
});

Acquire a token for a named user

Configure the MSAL mock's token source when a test needs identity claims other than the default Test User. The acquired token becomes both the access token and active account:

import { createMockToken } from '@equinor/fusion-framework-module-msal/mock';
import { useAccessToken } from '@equinor/fusion-framework-react-app/msal';
import { renderAppHook } from '@equinor/fusion-framework-vitest-plugin-react-app';

test('acquires a token for a named user', async () => {
  const { result, fusion } = await renderAppHook(
    () => useAccessToken({ scopes: ['User.Read'] }),
    {
      configure: (configurator) =>
        configurator.msal.setAcquireToken(({ clientId, scopes }) =>
          createMockToken({
            aud: clientId,
            scp: scopes.join(' '),
            name: 'Ada Lovelace',
            preferred_username: '[email protected]',
            oid: 'ada-lovelace',
          }),
        ),
    },
  );

  await vi.waitFor(() => expect(result.current.pending).toBe(false));
  expect(fusion.framework.modules.auth.account?.name).toBe('Ada Lovelace');
});

Reuse a pre-built parent Fusion instance

Build the fusion instance yourself when a test needs to pre-configure parent-level modules (http, context, serviceDiscovery) or share one instance across multiple render calls:

import { mockFramework } from '@equinor/fusion-framework/mock';
import { enableAppManifestMock } from '@equinor/fusion-framework-app/mock';
import type { AppModule } from '@equinor/fusion-framework-module-app';
import { renderAppHook } from '@equinor/fusion-framework-vitest-plugin-react-app';
import { useAccessToken, useCurrentAccount } from '@equinor/fusion-framework-react-app/msal';

const env = { manifest: { appKey: 'test-app', displayName: 'Test App', description: '', type: 'standalone' as const } };

test('shares one fusion instance across two renders', async () => {
  const fusion = await mockFramework<[AppModule]>((configurator) =>
    enableAppManifestMock(configurator, env),
  );

  const { result: a } = await renderAppHook(() => useAccessToken({ scopes: ['User.Read'] }), { fusion });
  const { result: b } = await renderAppHook(() => useCurrentAccount(), { fusion });
});

renderAppComponent

Renders a component inside the same real, mock-backed application module scope as renderAppHook. Use this when you need to assert on rendered output rather than a hook's return value — e.g. a component with its own loading/error states.

Signature:

function renderAppComponent<TModules = unknown, TEnv extends AppEnv = AppEnv>(
  ui: ReactElement,
  options?: {
    configure?: AppMockConfigureFn<TModules, TEnv>;
    env?: TEnv;
    fusion?: Fusion;
  } & Omit<RenderOptions, 'wrapper'>,
): Promise<RenderResult & { fusion: { framework: Fusion; app: AppModulesInstance<TModules> } }>;

Options are the same shape as renderAppHook's — configure, env, fusion — plus any other vitest-browser-react render option. The result carries the usual render return values (getByText, container, unmount, ...) plus fusion — nested rather than spread directly onto the result, so vitest-browser-react's own return shape stays free to evolve without ever colliding with it. fusion.app is the same application module instance the rendered component reads through useAppModule/useAppModules, and fusion.framework is the parent Fusion instance. Drive a module directly through fusion.app to exercise a state change after the initial render, without hand-wiring mockAppModules/ModuleProvider:

import { enableContextMock } from '@equinor/fusion-framework-module-context/mock';
import type { ContextModule } from '@equinor/fusion-framework-module-context';
import { act } from 'react';
import { renderAppComponent } from '@equinor/fusion-framework-vitest-plugin-react-app';

test('reacts when the current context switches', async () => {
  const { getByText, fusion } = await renderAppComponent<[ContextModule]>(<App />, {
    configure: (configurator) =>
      enableContextMock(configurator, (mock) => mock.setCurrentContext(projectA)),
  });

  await act(() => fusion.app.context.setCurrentContextByIdAsync(projectB.id));
  await expect.element(getByText(/project-b/)).toBeVisible();
});

Example: asserting loading and error states

import { mockFramework } from '@equinor/fusion-framework/mock';
import { enableAppManifestMock } from '@equinor/fusion-framework-app/mock';
import type { AppManifest, AppModule } from '@equinor/fusion-framework-module-app';
import { renderAppComponent } from '@equinor/fusion-framework-vitest-plugin-react-app';
import { Apploader } from '@equinor/fusion-framework-react-app/apploader';

test('mounts the child app once its script loads', async () => {
  const manifest: AppManifest = {
    appKey: 'child-app',
    displayName: 'Child App',
    description: 'A child application',
    type: 'standalone',
    build: { version: '1.0.0', entryPoint: 'child-script.ts', assetPath: '' },
  };
  const fusion = await mockFramework<[AppModule]>((configurator) =>
    enableAppManifestMock(configurator, { manifest }, someFixturesUri),
  );

  const { container } = await renderAppComponent(<Apploader appKey="child-app" />, { fusion });

  // the loading state renders synchronously, before the script's dynamic import resolves
  expect(container.textContent).toContain('Loading child-app');
  await vi.waitFor(() => expect(container.textContent).toContain('mounted: child-app'));
});

testApp

vitest's test, extended with an application module scope fixture — an alternative to renderAppComponent/renderAppHook for a test file whose cases share one mocked scope: appEnv/configureApp become suite-level concerns, overridden once per file (or per describe block) with testApp.extend(...), rather than an options object repeated on every call. fusion/app resolve lazily — a test that only destructures app never pays for rendering anything, and one that only destructures render/renderHook gets the same mocked scope wired in automatically.

import { testApp } from '@equinor/fusion-framework-vitest-plugin-react-app';

testApp('resolves current context', async ({ app, render }) => {
  const screen = await render(<App />);
  expect(app.context).toBeDefined();
});

Seed a module for every test in a suite:

describe('with a seeded context module', () => {
  const test = testApp.extend(
    'configureApp',
    { injected: true },
    (): AppMockConfigureFn =>
      (configurator) =>
        enableContextMock(configurator, (mock) => mock.setCurrentContext(projectA)),
  );

  test('starts on the seeded context', async ({ render }) => {
    const screen = await render(<App />);
    await expect.element(screen.getByText(projectA.title)).toBeVisible();
  });
});

appTestVitePlugin

A plain Vite plugin — Vitest configs are Vite configs, so this registers directly in your own vitest.config.ts, no CLI command required. It serves an application's manifest/config (resolved the same way ffc app build/ffc app dev do) and its own module-configurator export as virtual modules, so the /test entry-point's test/render need no per-test appEnv/configureApp wiring.

appTestVitePlugin({
  entrypoint?: string; // defaults to the resolved Vite/Vitest project root
  manifest?: string | AppManifestFn;
  config?: string | AppConfigFn;
  configure?: string; // defaults to the first of src/config.ts, src/config.tsx, src/config.js
});

entrypoint, manifest, and config are forwarded to the same manifest/config resolution ffc app build/ffc app dev use (a base manifest generated from package.json, merged with a local app.manifest.ts if one exists, plus app.config.ts for endpoints). configure points at the application's module-configurator file — unlike manifest/config, it must be a real file on disk, since it's re-exported as live application code into the test bundle rather than JSON-serialized data. An explicitly requested manifest/config/configure file that doesn't exist throws FileNotFoundError; the convention-based lookups fail silently and fall back to defaults instead.

Notes

  • Every render/hook helper is async — always await it, since the mocked application module scope (app manifest, module initialization) resolves asynchronously before the first render.
  • Seeding a module beyond the default set (bookmark, context, feature-flag) requires its own mock enabler passed through configure — e.g. enableBookmarkMock, enableContextMock, enableFeatureFlagMock from that module's own /mock sub-path.
  • msal ships enabled by default with a signed-in "Test User" — no configure needed unless a test cares about a specific account or a signed-out state.
  • Prefer these helpers over hand-wiring mockFramework, mockAppModules, FrameworkProvider, and ModuleProvider directly in every test.

版本与发布记录

@equinor/fusion-framework-vitest-plugin-react-app 共发布 34 个版本,首次发布于 2026/08/16;过去 12 个月发布了 34 个版本。

版本发布日期距今
2.0.5latest2026/11/121 天前
2.0.5-next.0next2026/11/112 天前
2.0.42026/11/112 天前
2.0.4-next.22026/10/2915 天前
2.0.4-next.12026/10/2816 天前
2.0.4-next.02026/10/2816 天前

其他发布通道:trusted-publishing-20261015 → 1.0.16-trusted-publishing-20261015-9dfe7602cd2a49a8a9df00d62c26e7304a82a85a,next → 2.0.5-next.0。

关于 @equinor/fusion-framework-vitest-plugin-react-app 的常见问题

@equinor/fusion-framework-vitest-plugin-react-app 是什么?

Vite plugin resolving an application's manifest, config, and module-configurator as virtual modules for testing.

如何安装 @equinor/fusion-framework-vitest-plugin-react-app?

在项目目录运行 npm install @equinor/fusion-framework-vitest-plugin-react-app,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:rxjs(^7.0.0)、vite(^8.0.0)、react(^18.0.0 || ^19.0.0)、vitest(^5.0.0)、react-dom(^18.0.0 || ^19.0.0)、playwright(^1.0.0)、@types/react(^18.0.0 || ^19.0.0)、vitest-browser-react(^2.3.0)、@vitest/browser-playwright(^5.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@equinor/fusion-framework-vitest-plugin-react-app 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@equinor/fusion-framework-vitest-plugin-react-app 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@equinor/fusion-framework-vitest-plugin-react-app 可以商用吗?

@equinor/fusion-framework-vitest-plugin-react-app 使用 ISC 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@equinor/fusion-framework-vitest-plugin-react-app 的下载量有多少?

根据 npm 官方下载统计,@equinor/fusion-framework-vitest-plugin-react-app 在 2026/10/14 – 2026/11/12 这 30 天内被下载 2,965 次,日均约 99 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@equinor/fusion-framework-vitest-plugin-react-app 还在维护吗?

最新版本 2.0.5 发布于 2026/11/12(1 天前);至今共发布 34 个版本,其中过去 12 个月发布了 34 个;首次发布于 2026/08/16。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。