@equinor/fusion-framework-vitest-plugin-react-app 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/index.js +9 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/resolve-app-test-env.js +8 -2
- package/dist/esm/resolve-app-test-env.js.map +1 -1
- package/dist/esm/scope/resolve-fusion.js +22 -24
- package/dist/esm/scope/resolve-fusion.js.map +1 -1
- package/dist/esm/test-app.js +5 -8
- package/dist/esm/test-app.js.map +1 -1
- package/dist/esm/test.js +2 -1
- package/dist/esm/test.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +4 -3
- package/dist/types/resolve-app-test-env.d.ts +3 -1
- package/dist/types/scope/resolve-fusion.d.ts +6 -7
- package/dist/types/test-app.d.ts +12 -11
- package/dist/types/test.d.ts +20 -9
- package/dist/types/version.d.ts +1 -1
- package/package.json +17 -19
- package/CHANGELOG.md +0 -81
- package/docs/advanced.md +0 -209
- package/docs/configuration.md +0 -112
- package/docs/getting-started.md +0 -66
- package/docs/migrating-an-existing-app.md +0 -202
- package/docs/module-mocks.md +0 -120
- package/docs/overview.md +0 -49
- package/docs/troubleshooting.md +0 -67
- package/docs/why-browser-mode.md +0 -128
- package/src/__tests__/app-test-vite-plugin.test.ts +0 -109
- package/src/__tests__/merge-env-config.test.ts +0 -67
- package/src/__tests__/resolve-app-test-env.test.ts +0 -83
- package/src/app-test.ts +0 -19
- package/src/define-project.ts +0 -91
- package/src/index.ts +0 -124
- package/src/merge-env-config.ts +0 -58
- package/src/render-app-component.tsx +0 -116
- package/src/render-app-hook.tsx +0 -135
- package/src/render.tsx +0 -64
- package/src/resolve-app-test-env.ts +0 -132
- package/src/scope/create-app-scope-wrapper.tsx +0 -26
- package/src/scope/default-app-env.ts +0 -13
- package/src/scope/index.ts +0 -4
- package/src/scope/resolve-app-scope.ts +0 -46
- package/src/scope/resolve-fusion.ts +0 -74
- package/src/test-app.tsx +0 -148
- package/src/test.tsx +0 -42
- package/src/version.ts +0 -2
- package/src/virtual-modules.d.ts +0 -12
- package/tsconfig.json +0 -26
- package/vitest.config.ts +0 -10
package/docs/why-browser-mode.md
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
# Why Browser Mode is the default
|
|
2
|
-
|
|
3
|
-
`@equinor/fusion-framework-vitest-plugin-react-app` runs component and hook tests in headless
|
|
4
|
-
Chromium by default. A real browser gives these tests more accurate DOM behavior, but it is
|
|
5
|
-
slower than `happy-dom` or `jsdom`.
|
|
6
|
-
|
|
7
|
-
This guide explains that tradeoff and shows how to use a different browser or renderer when a
|
|
8
|
-
test does not need Chromium.
|
|
9
|
-
|
|
10
|
-
## Performance
|
|
11
|
-
|
|
12
|
-
A real browser is slower than DOM emulation. `happy-dom` and `jsdom` run in the same Node process
|
|
13
|
-
as the test. Browser Mode starts Chromium through Playwright and communicates with that browser
|
|
14
|
-
process.
|
|
15
|
-
|
|
16
|
-
Choose Browser Mode for fidelity, not speed. Keep fast unit tests on Node or DOM emulation when
|
|
17
|
-
they do not need browser behavior.
|
|
18
|
-
|
|
19
|
-
## Why use a real browser
|
|
20
|
-
|
|
21
|
-
- **React compatibility:** the previous React test setup failed after its React 19 peer
|
|
22
|
-
dependency update. The repository then moved those tests to Vitest Browser Mode. See the
|
|
23
|
-
`packages/utils/observable` 8.4.4 changelog and migration commit `059aefae5d`.
|
|
24
|
-
- **Real framework execution:** tests control external boundaries and seed module state through
|
|
25
|
-
[Module mocks](module-mocks.md), while the actual module configuration, lifecycle, providers,
|
|
26
|
-
and rendering behavior still run.
|
|
27
|
-
- **Browser fidelity:** Chromium provides real layout, `ResizeObserver`, custom elements, and
|
|
28
|
-
Shadow DOM — behavior `happy-dom` and `jsdom` only approximate.
|
|
29
|
-
|
|
30
|
-
> [!WARNING]
|
|
31
|
-
> A DOM-emulation gap is usually patched one of two ways: a polyfill standing in for the missing
|
|
32
|
-
> browser API, or a component replacement that avoids exercising it at all. Both are test-only
|
|
33
|
-
> code with no equivalent in production, and both defeat the point of the test: a polyfill can
|
|
34
|
-
> drift from real browser behavior unnoticed, and a mocked component only proves the *mock*
|
|
35
|
-
> renders correctly, never the real one. A passing test built on either can still fail — or
|
|
36
|
-
> silently lie — against the real component. Treat every DOM-emulation workaround as debt to
|
|
37
|
-
> remove, not a pattern to reach for.
|
|
38
|
-
|
|
39
|
-
A real browser does not eliminate every workaround: AG Grid license messages and Lit dev-mode
|
|
40
|
-
warnings are console noise from real Chromium runs, not DOM-emulation gaps, and suppressing them
|
|
41
|
-
carries none of the risk above (see [Module mocks](module-mocks.md) for the supported way to
|
|
42
|
-
seed module state instead of replacing a component). Remove a DOM-emulation-only workaround only
|
|
43
|
-
after confirming the affected test still passes against the real component in Browser Mode.
|
|
44
|
-
|
|
45
|
-
## Choose a different runtime
|
|
46
|
-
|
|
47
|
-
You can change either the browser provider or the renderer:
|
|
48
|
-
|
|
49
|
-
- Stay in Browser Mode and replace Playwright with another Vitest browser provider.
|
|
50
|
-
- Keep the Fusion module setup but render with `happy-dom` or `jsdom`.
|
|
51
|
-
|
|
52
|
-
### Use another browser provider
|
|
53
|
-
|
|
54
|
-
Pass `test.browser.provider` to `defineProject` to replace Playwright while staying in Browser
|
|
55
|
-
Mode. This example uses WebdriverIO:
|
|
56
|
-
|
|
57
|
-
```ts
|
|
58
|
-
import { defineProject } from '@equinor/fusion-framework-vitest-plugin-react-app/config';
|
|
59
|
-
import { webdriverio } from '@vitest/browser-webdriverio';
|
|
60
|
-
|
|
61
|
-
export default defineProject({
|
|
62
|
-
test: { browser: { provider: webdriverio() } },
|
|
63
|
-
});
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
See [Configuration](configuration.md) for the complete `defineProject` behavior.
|
|
67
|
-
|
|
68
|
-
### Use happy-dom or jsdom
|
|
69
|
-
|
|
70
|
-
> [!CAUTION]
|
|
71
|
-
> Rendering with `happy-dom` or `jsdom` reintroduces the DOM-emulation tradeoff above: any gap
|
|
72
|
-
> between the emulation and a real browser has to be closed with a polyfill or a component
|
|
73
|
-
> replacement, not fixed. Only take this path for a component or hook that provably does not
|
|
74
|
-
> need browser fidelity — otherwise stay in Browser Mode.
|
|
75
|
-
|
|
76
|
-
The Fusion module setup does not depend on Browser Mode. Build the same provider tree and pass it
|
|
77
|
-
to another renderer. The example below uses `@testing-library/react` on `happy-dom`:
|
|
78
|
-
|
|
79
|
-
```tsx
|
|
80
|
-
import { render } from '@testing-library/react';
|
|
81
|
-
import { mockFramework } from '@equinor/fusion-framework/mock';
|
|
82
|
-
import { enableAppManifestMock, mockAppModules } from '@equinor/fusion-framework-app/mock';
|
|
83
|
-
import { FrameworkProvider } from '@equinor/fusion-framework-react';
|
|
84
|
-
import { ModuleProvider } from '@equinor/fusion-framework-react-module';
|
|
85
|
-
import type { AppModule } from '@equinor/fusion-framework-module-app';
|
|
86
|
-
import type { ReactElement } from 'react';
|
|
87
|
-
|
|
88
|
-
const env = {
|
|
89
|
-
manifest: {
|
|
90
|
-
appKey: 'test-app',
|
|
91
|
-
displayName: 'Test App',
|
|
92
|
-
description: 'Test app',
|
|
93
|
-
type: 'standalone',
|
|
94
|
-
},
|
|
95
|
-
} as const;
|
|
96
|
-
|
|
97
|
-
async function renderWithHappyDom(ui: ReactElement) {
|
|
98
|
-
const framework = await mockFramework<[AppModule]>((configurator) =>
|
|
99
|
-
enableAppManifestMock(configurator, env),
|
|
100
|
-
);
|
|
101
|
-
const app = await mockAppModules(undefined, env, framework);
|
|
102
|
-
return render(ui, {
|
|
103
|
-
wrapper: ({ children }) => (
|
|
104
|
-
<FrameworkProvider value={framework}>
|
|
105
|
-
<ModuleProvider value={app}>{children}</ModuleProvider>
|
|
106
|
-
</FrameworkProvider>
|
|
107
|
-
),
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
Run the test in a Vitest project with `environment: 'happy-dom'` instead of
|
|
113
|
-
`browser.enabled`. Use a separate project if the same app also has Browser Mode tests.
|
|
114
|
-
|
|
115
|
-
Call the same `enable*Mock` functions from the `mockAppModules` configuration callback. These
|
|
116
|
-
mocks configure Fusion modules and do not depend on the renderer. `renderHook` from
|
|
117
|
-
`@testing-library/react` can use the same wrapper for hook tests.
|
|
118
|
-
|
|
119
|
-
Use Browser Mode when browser behavior matters. Use `happy-dom`, `jsdom`, or no DOM for focused
|
|
120
|
-
tests that only need application logic or simple rendering.
|
|
121
|
-
|
|
122
|
-
## Related documentation
|
|
123
|
-
|
|
124
|
-
- [Overview](overview.md)
|
|
125
|
-
- [Configuration](configuration.md)
|
|
126
|
-
- [Module mocks](module-mocks.md)
|
|
127
|
-
- [Migrate an existing app to Fusion Vitest](migrating-an-existing-app.md)
|
|
128
|
-
- [Choose a Fusion testing layer](../../../framework/docs/testing-choosing-a-layer.md)
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
-
|
|
7
|
-
import { appTestVitePlugin } from '../index.js';
|
|
8
|
-
|
|
9
|
-
type ResolveIdHook = (id: string) => string | null | undefined;
|
|
10
|
-
type LoadHook = (id: string) => string | null | undefined | Promise<string | null | undefined>;
|
|
11
|
-
type ConfigResolvedHook = (config: { root: string }) => void;
|
|
12
|
-
|
|
13
|
-
describe('appTestVitePlugin', () => {
|
|
14
|
-
let dir: string;
|
|
15
|
-
|
|
16
|
-
beforeEach(async () => {
|
|
17
|
-
dir = await mkdtemp(join(tmpdir(), 'fusion-vitest-plugin-react-app-'));
|
|
18
|
-
await writeFile(
|
|
19
|
-
join(dir, 'package.json'),
|
|
20
|
-
JSON.stringify({ name: '@equinor/my-app', version: '1.2.3' }),
|
|
21
|
-
);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
afterEach(async () => {
|
|
25
|
-
await rm(dir, { recursive: true, force: true });
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('claims only its two virtual specifiers, leaving everything else unresolved', () => {
|
|
29
|
-
const plugin = appTestVitePlugin({ entrypoint: dir });
|
|
30
|
-
const resolveId = plugin.resolveId as ResolveIdHook;
|
|
31
|
-
|
|
32
|
-
expect(resolveId('virtual:fusion-app-test-env')).toBe('\0virtual:fusion-app-test-env');
|
|
33
|
-
expect(resolveId('virtual:fusion-app-test-configure')).toBe(
|
|
34
|
-
'\0virtual:fusion-app-test-configure',
|
|
35
|
-
);
|
|
36
|
-
expect(resolveId('some-other-module')).toBeNull();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('serves the resolved manifest/config as the env virtual module', async () => {
|
|
40
|
-
const plugin = appTestVitePlugin({ entrypoint: dir });
|
|
41
|
-
const load = plugin.load as LoadHook;
|
|
42
|
-
|
|
43
|
-
const source = await load('\0virtual:fusion-app-test-env');
|
|
44
|
-
|
|
45
|
-
expect(source).toContain('"appKey":"my-app"');
|
|
46
|
-
expect(source).toContain('export const config = {"environment":{}};');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('serves an undefined configure export when no config file exists', async () => {
|
|
50
|
-
const plugin = appTestVitePlugin({ entrypoint: dir });
|
|
51
|
-
const load = plugin.load as LoadHook;
|
|
52
|
-
|
|
53
|
-
const source = await load('\0virtual:fusion-app-test-configure');
|
|
54
|
-
|
|
55
|
-
expect(source).toBe('export const configure = undefined;');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('resolves the app from the Vitest project root when no entrypoint is provided', async () => {
|
|
59
|
-
await mkdir(join(dir, 'src'), { recursive: true });
|
|
60
|
-
await writeFile(join(dir, 'src', 'index.ts'), 'export {};');
|
|
61
|
-
await writeFile(join(dir, 'src', 'config.ts'), 'export default () => undefined;');
|
|
62
|
-
const plugin = appTestVitePlugin();
|
|
63
|
-
const configResolved = plugin.configResolved as ConfigResolvedHook;
|
|
64
|
-
const load = plugin.load as LoadHook;
|
|
65
|
-
|
|
66
|
-
configResolved({ root: dir });
|
|
67
|
-
|
|
68
|
-
expect(await load('\0virtual:fusion-app-test-env')).toContain('"appKey":"my-app"');
|
|
69
|
-
expect(await load('\0virtual:fusion-app-test-configure')).toBe(
|
|
70
|
-
`export { default as configure } from ${JSON.stringify(join(dir, 'src', 'config.ts'))};`,
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('re-exports the default candidate config file as the configure virtual module', async () => {
|
|
75
|
-
await mkdir(join(dir, 'src'), { recursive: true });
|
|
76
|
-
await writeFile(join(dir, 'src', 'config.ts'), 'export default () => undefined;');
|
|
77
|
-
const plugin = appTestVitePlugin({ entrypoint: dir });
|
|
78
|
-
const load = plugin.load as LoadHook;
|
|
79
|
-
|
|
80
|
-
const source = await load('\0virtual:fusion-app-test-configure');
|
|
81
|
-
|
|
82
|
-
expect(source).toBe(
|
|
83
|
-
`export { default as configure } from ${JSON.stringify(join(dir, 'src', 'config.ts'))};`,
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('re-exports an explicitly requested configure file', async () => {
|
|
88
|
-
await writeFile(join(dir, 'my-config.ts'), 'export default () => undefined;');
|
|
89
|
-
const plugin = appTestVitePlugin({ entrypoint: dir, configure: 'my-config.ts' });
|
|
90
|
-
const load = plugin.load as LoadHook;
|
|
91
|
-
|
|
92
|
-
const source = await load('\0virtual:fusion-app-test-configure');
|
|
93
|
-
|
|
94
|
-
expect(source).toBe(
|
|
95
|
-
`export { default as configure } from ${JSON.stringify(join(dir, 'my-config.ts'))};`,
|
|
96
|
-
);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('throws when an explicitly requested configure file does not exist', () => {
|
|
100
|
-
expect(() => appTestVitePlugin({ entrypoint: dir, configure: 'missing.ts' })).toThrow();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('returns null for module ids other than its own virtual specifiers', async () => {
|
|
104
|
-
const plugin = appTestVitePlugin({ entrypoint: dir });
|
|
105
|
-
const load = plugin.load as LoadHook;
|
|
106
|
-
|
|
107
|
-
expect(await load('some-other-module')).toBeNull();
|
|
108
|
-
});
|
|
109
|
-
});
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { AppConfig } from '@equinor/fusion-framework-module-app';
|
|
2
|
-
import type { AppEnv } from '@equinor/fusion-framework-app';
|
|
3
|
-
import { describe, expect, it } from 'vitest';
|
|
4
|
-
|
|
5
|
-
import { mergeEnvConfig } from '../merge-env-config.js';
|
|
6
|
-
|
|
7
|
-
describe('mergeEnvConfig', () => {
|
|
8
|
-
const baseEnv: AppEnv = {
|
|
9
|
-
manifest: {
|
|
10
|
-
appKey: 'test-app',
|
|
11
|
-
displayName: 'Test App',
|
|
12
|
-
description: 'A test application',
|
|
13
|
-
type: 'standalone',
|
|
14
|
-
},
|
|
15
|
-
config: new AppConfig({
|
|
16
|
-
environment: { foo: 'bar' },
|
|
17
|
-
endpoints: { api: { url: 'https://api.example.com', scopes: ['api.read'] } },
|
|
18
|
-
}),
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
it('adds a new endpoint without dropping existing ones', () => {
|
|
22
|
-
const merged = mergeEnvConfig(baseEnv, {
|
|
23
|
-
endpoints: { 'cpr-api': { url: 'https://cpr.example.com' } },
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
expect(merged.config?.endpoints).toEqual({
|
|
27
|
-
api: { url: 'https://api.example.com', scopes: ['api.read'] },
|
|
28
|
-
'cpr-api': { url: 'https://cpr.example.com', scopes: [] },
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('overrides one field of an existing endpoint, keeping the rest', () => {
|
|
33
|
-
const merged = mergeEnvConfig(baseEnv, {
|
|
34
|
-
endpoints: { api: { url: 'https://fake.example.com' } },
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
expect(merged.config?.endpoints.api).toEqual({
|
|
38
|
-
url: 'https://fake.example.com',
|
|
39
|
-
scopes: ['api.read'],
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('merges environment overrides over the existing environment', () => {
|
|
44
|
-
const merged = mergeEnvConfig(baseEnv, { environment: { baz: 'qux' } });
|
|
45
|
-
|
|
46
|
-
expect(merged.config?.environment).toEqual({ foo: 'bar', baz: 'qux' });
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('leaves the original env and its config untouched', () => {
|
|
50
|
-
mergeEnvConfig(baseEnv, { endpoints: { api: { url: 'https://fake.example.com' } } });
|
|
51
|
-
|
|
52
|
-
expect(baseEnv.config?.endpoints.api).toEqual({
|
|
53
|
-
url: 'https://api.example.com',
|
|
54
|
-
scopes: ['api.read'],
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('handles an env with no existing config', () => {
|
|
59
|
-
const env: AppEnv = { manifest: baseEnv.manifest };
|
|
60
|
-
const merged = mergeEnvConfig(env, { endpoints: { api: { url: 'https://api.example.com' } } });
|
|
61
|
-
|
|
62
|
-
expect(merged.config?.endpoints).toEqual({
|
|
63
|
-
api: { url: 'https://api.example.com', scopes: [] },
|
|
64
|
-
});
|
|
65
|
-
expect(merged.config?.environment).toEqual({});
|
|
66
|
-
});
|
|
67
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
-
|
|
7
|
-
import { resolveAppTestEnv } from '../resolve-app-test-env.js';
|
|
8
|
-
|
|
9
|
-
describe('resolveAppTestEnv', () => {
|
|
10
|
-
let dir: string;
|
|
11
|
-
|
|
12
|
-
beforeEach(async () => {
|
|
13
|
-
dir = await mkdtemp(join(tmpdir(), 'fusion-vitest-plugin-react-app-'));
|
|
14
|
-
await writeFile(
|
|
15
|
-
join(dir, 'package.json'),
|
|
16
|
-
JSON.stringify({ name: '@equinor/my-app', version: '1.2.3', description: 'a test app' }),
|
|
17
|
-
);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(async () => {
|
|
21
|
-
await rm(dir, { recursive: true, force: true });
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('falls back to a package-derived manifest and an empty config with no local files', async () => {
|
|
25
|
-
const { manifest, config } = await resolveAppTestEnv({ entrypoint: dir });
|
|
26
|
-
|
|
27
|
-
expect(manifest).toMatchObject({
|
|
28
|
-
appKey: 'my-app',
|
|
29
|
-
displayName: '@equinor/my-app',
|
|
30
|
-
description: 'a test app',
|
|
31
|
-
type: 'standalone',
|
|
32
|
-
});
|
|
33
|
-
expect(config).toEqual({ environment: {} });
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('merges a local app.manifest.ts and app.config.ts, same as ffc app build', async () => {
|
|
37
|
-
await writeFile(
|
|
38
|
-
join(dir, 'app.manifest.ts'),
|
|
39
|
-
"export default { displayName: 'My Custom App' };",
|
|
40
|
-
);
|
|
41
|
-
await writeFile(
|
|
42
|
-
join(dir, 'app.config.ts'),
|
|
43
|
-
"export default { environment: {}, endpoints: { api: { url: 'https://example.com' } } };",
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const { manifest, config } = await resolveAppTestEnv({ entrypoint: dir });
|
|
47
|
-
|
|
48
|
-
expect(manifest).toMatchObject({ appKey: 'my-app', displayName: 'My Custom App' });
|
|
49
|
-
expect(config).toEqual({
|
|
50
|
-
environment: {},
|
|
51
|
-
endpoints: { api: { url: 'https://example.com', scopes: [] } },
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('applies an inline manifest/config function directly, same as a loaded file default export', async () => {
|
|
56
|
-
const { manifest, config } = await resolveAppTestEnv({
|
|
57
|
-
entrypoint: dir,
|
|
58
|
-
manifest: () => ({ displayName: 'Inline App' }),
|
|
59
|
-
config: () => ({
|
|
60
|
-
environment: {},
|
|
61
|
-
endpoints: { api: { url: 'https://inline.example.com' } },
|
|
62
|
-
}),
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
expect(manifest).toMatchObject({ appKey: 'my-app', displayName: 'Inline App' });
|
|
66
|
-
expect(config).toEqual({
|
|
67
|
-
environment: {},
|
|
68
|
-
endpoints: { api: { url: 'https://inline.example.com', scopes: [] } },
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('propagates an error for an explicitly requested manifest file that does not exist', async () => {
|
|
73
|
-
await expect(
|
|
74
|
-
resolveAppTestEnv({ entrypoint: dir, manifest: 'does-not-exist.manifest.ts' }),
|
|
75
|
-
).rejects.toThrow();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('propagates an error for an explicitly requested config file that does not exist', async () => {
|
|
79
|
-
await expect(
|
|
80
|
-
resolveAppTestEnv({ entrypoint: dir, config: 'does-not-exist.config.ts' }),
|
|
81
|
-
).rejects.toThrow();
|
|
82
|
-
});
|
|
83
|
-
});
|
package/src/app-test.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
renderAppHook,
|
|
3
|
-
type RenderAppHookOptions,
|
|
4
|
-
type RenderAppHookResult,
|
|
5
|
-
} from './render-app-hook';
|
|
6
|
-
export {
|
|
7
|
-
renderAppComponent,
|
|
8
|
-
type RenderAppComponentOptions,
|
|
9
|
-
type RenderAppComponentResult,
|
|
10
|
-
} from './render-app-component';
|
|
11
|
-
export { testApp } from './test-app';
|
|
12
|
-
export { mergeEnvConfig, type MergeEnvConfigOverrides } from './merge-env-config.js';
|
|
13
|
-
export type { AppMockConfigureFn } from '@equinor/fusion-framework-app/mock';
|
|
14
|
-
|
|
15
|
-
// `test`/`render` import virtual modules only served once `appTestVitePlugin`
|
|
16
|
-
// (@equinor/fusion-framework-vitest-plugin-react-app) is registered — using any export from
|
|
17
|
-
// this module requires the plugin registered in your `vitest.config.ts` `plugins`.
|
|
18
|
-
export { test } from './test';
|
|
19
|
-
export { render } from './render';
|
package/src/define-project.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { playwright } from '@vitest/browser-playwright';
|
|
2
|
-
import {
|
|
3
|
-
defineProject as defineVitestProject,
|
|
4
|
-
mergeConfig,
|
|
5
|
-
type UserWorkspaceConfig,
|
|
6
|
-
} from 'vitest/config';
|
|
7
|
-
|
|
8
|
-
import { appTestVitePlugin } from './index.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* A config override for {@link defineProject}: an object is deep-merged onto the default
|
|
12
|
-
* config via Vite's own `mergeConfig`; a function receives the default config and returns the
|
|
13
|
-
* config used outright, for changes `mergeConfig` can't express (e.g. removing a field).
|
|
14
|
-
*/
|
|
15
|
-
export type AppTestConfigOverride =
|
|
16
|
-
| Partial<UserWorkspaceConfig>
|
|
17
|
-
| ((config: UserWorkspaceConfig) => UserWorkspaceConfig);
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Builds a ready-to-export Vitest project config for testing a Fusion Framework React app:
|
|
21
|
-
* registers {@link appTestVitePlugin} (with its own defaults) and wires up Vitest browser mode
|
|
22
|
-
* with the Playwright/`chromium` provider, so a consuming app's own `vitest.config.ts` needs no
|
|
23
|
-
* browser-provider boilerplate of its own.
|
|
24
|
-
*
|
|
25
|
-
* @remarks
|
|
26
|
-
* Drop-in replacement for Vitest's own `defineProject` — same `export default`, just pre-wired.
|
|
27
|
-
* Playwright/`chromium` is only the *default* — pass `override` to change or replace anything
|
|
28
|
-
* (e.g. `test.name`, or swap `test.browser.provider` for a different `@vitest/browser-*`
|
|
29
|
-
* provider) without hand-rolling `appTestVitePlugin`'s own wiring. The default `1024x768`
|
|
30
|
-
* viewport matches a typical low-resolution Citrix session rather than Vitest's own
|
|
31
|
-
* mobile-sized default; pass `test.browser.viewport` to use a different size.
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```ts
|
|
35
|
-
* import { defineProject } from '@equinor/fusion-framework-vitest-plugin-react-app/config';
|
|
36
|
-
* import { name, version } from './package.json' with { type: 'json' };
|
|
37
|
-
*
|
|
38
|
-
* export default defineProject({ test: { name: `${name}@${version}` } });
|
|
39
|
-
* ```
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* Overriding the browser provider:
|
|
43
|
-
* ```ts
|
|
44
|
-
* import { webdriverio } from '@vitest/browser-webdriverio';
|
|
45
|
-
*
|
|
46
|
-
* export default defineProject({ test: { browser: { provider: webdriverio() } } });
|
|
47
|
-
* ```
|
|
48
|
-
*
|
|
49
|
-
* @param override - Deep-merged onto the default config (a plain object), or applied to it
|
|
50
|
-
* outright (a function receiving the default config); see {@link AppTestConfigOverride}.
|
|
51
|
-
* @returns A Vitest project config, ready to `export default`.
|
|
52
|
-
*/
|
|
53
|
-
export const defineProject = (override?: AppTestConfigOverride): UserWorkspaceConfig => {
|
|
54
|
-
const defaults: UserWorkspaceConfig = {
|
|
55
|
-
plugins: [appTestVitePlugin()],
|
|
56
|
-
test: {
|
|
57
|
-
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
|
58
|
-
browser: {
|
|
59
|
-
enabled: true,
|
|
60
|
-
provider: playwright(),
|
|
61
|
-
headless: true,
|
|
62
|
-
// most users run the app through Citrix at a low, fixed resolution rather than a
|
|
63
|
-
// resizable desktop window — default to that instead of Vitest's own mobile-sized viewport
|
|
64
|
-
viewport: { width: 1024, height: 768 },
|
|
65
|
-
instances: [{ browser: 'chromium' }],
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
// pre-transforms all source up front so deps only reached via lazy/code-split imports
|
|
69
|
-
// (e.g. route components) are discovered before the first test request, not mid-run —
|
|
70
|
-
// the latter forces Vite to reload the page and fails the in-flight test file import
|
|
71
|
-
// `.d.ts` files are excluded: a CJS-style declaration file (e.g. one using `export =`)
|
|
72
|
-
// fails the warmup scan when loaded as ESM
|
|
73
|
-
server: { warmup: { clientFiles: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'] } },
|
|
74
|
-
// same reasoning as `server.warmup` above, but for the esbuild dep scanner: without this,
|
|
75
|
-
// its default entry detection can miss code-split route files entirely, so a package only
|
|
76
|
-
// ever imported from one of those (e.g. react-router's own deps) is discovered mid-run
|
|
77
|
-
// instead of up front — statically crawling every source file (which esbuild's scanner
|
|
78
|
-
// follows through dynamic imports too) avoids that with no per-package name needed
|
|
79
|
-
optimizeDeps: { entries: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'] },
|
|
80
|
-
};
|
|
81
|
-
// a function replaces the config outright; a plain object deep-merges onto it via Vite's own mergeConfig
|
|
82
|
-
const resolved =
|
|
83
|
-
typeof override === 'function'
|
|
84
|
-
? override(defaults)
|
|
85
|
-
: override
|
|
86
|
-
? (mergeConfig(defaults, override) as UserWorkspaceConfig)
|
|
87
|
-
: defaults;
|
|
88
|
-
return defineVitestProject(resolved);
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
export default defineProject;
|
package/src/index.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
|
|
4
|
-
import { FileNotFoundError } from '@equinor/fusion-imports';
|
|
5
|
-
import type { Plugin } from 'vite';
|
|
6
|
-
|
|
7
|
-
import { resolveAppTestEnv, type ResolveAppTestEnvOptions } from './resolve-app-test-env.js';
|
|
8
|
-
|
|
9
|
-
const ENV_MODULE_ID = 'virtual:fusion-app-test-env';
|
|
10
|
-
const CONFIGURE_MODULE_ID = 'virtual:fusion-app-test-configure';
|
|
11
|
-
const RESOLVED_ENV_MODULE_ID = `\0${ENV_MODULE_ID}`;
|
|
12
|
-
const RESOLVED_CONFIGURE_MODULE_ID = `\0${CONFIGURE_MODULE_ID}`;
|
|
13
|
-
|
|
14
|
-
const DEFAULT_CONFIGURE_CANDIDATES = ['src/config.ts', 'src/config.tsx', 'src/config.js'];
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Options for {@link appTestVitePlugin}.
|
|
18
|
-
*/
|
|
19
|
-
export type AppTestVitePluginOptions = ResolveAppTestEnvOptions & {
|
|
20
|
-
/**
|
|
21
|
-
* Path (relative to `entrypoint`) to the app's module-configurator export, mirroring the
|
|
22
|
-
* `configure` argument passed to `makeComponent` in the app's own entry point. Defaults to the
|
|
23
|
-
* first of `src/config.ts`, `src/config.tsx`, `src/config.js` that exists.
|
|
24
|
-
*
|
|
25
|
-
* @remarks
|
|
26
|
-
* Unlike `manifest`/`config`, this can't be an inline function: it's live application code
|
|
27
|
-
* (with its own imports and closures) re-exported as-is into the test bundle rather than
|
|
28
|
-
* JSON-serialized data, so Vite needs a real file on disk to resolve and transform.
|
|
29
|
-
*/
|
|
30
|
-
configure?: string;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Vite plugin serving an application's manifest/config (resolved the same way `ffc app build`/
|
|
35
|
-
* `ffc app dev` do) and its own module-configurator export as virtual modules, so
|
|
36
|
-
* `@equinor/fusion-framework-vitest-plugin-react-app/test`'s `test`/`render` need no per-test
|
|
37
|
-
* `env`/`configure` wiring.
|
|
38
|
-
*
|
|
39
|
-
* @remarks
|
|
40
|
-
* A plain Vite plugin — Vitest configs are Vite configs, so this registers directly in your
|
|
41
|
-
* own `vitest.config.ts`, no CLI command required:
|
|
42
|
-
* ```ts
|
|
43
|
-
* import { defineConfig } from 'vitest/config';
|
|
44
|
-
* import { appTestVitePlugin } from '@equinor/fusion-framework-vitest-plugin-react-app';
|
|
45
|
-
*
|
|
46
|
-
* export default defineConfig({
|
|
47
|
-
* plugins: [appTestVitePlugin()],
|
|
48
|
-
* // ...your own browser-mode config
|
|
49
|
-
* });
|
|
50
|
-
* ```
|
|
51
|
-
* Exposes two virtual modules: `virtual:fusion-app-test-env` (`manifest`/`config`, as JSON) and
|
|
52
|
-
* `virtual:fusion-app-test-configure` (a re-export of the resolved `configure` module, or
|
|
53
|
-
* `undefined` if none exists). Not intended to be imported directly by application code.
|
|
54
|
-
*
|
|
55
|
-
* @param options - Resolution options; `entrypoint` defaults to the current working directory.
|
|
56
|
-
* @returns A Vite plugin instance.
|
|
57
|
-
*/
|
|
58
|
-
export const appTestVitePlugin = (options?: AppTestVitePluginOptions): Plugin => {
|
|
59
|
-
const explicitEntrypoint = options?.entrypoint;
|
|
60
|
-
let entrypoint = explicitEntrypoint;
|
|
61
|
-
let configureModulePath = explicitEntrypoint
|
|
62
|
-
? resolveConfigureModulePath(explicitEntrypoint, options?.configure)
|
|
63
|
-
: undefined;
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
name: 'fusion:app-test',
|
|
67
|
-
configResolved(config) {
|
|
68
|
-
// Vitest resolves each workspace project's root from its config file, which identifies the app containing src/index.ts.
|
|
69
|
-
entrypoint ??= config.root;
|
|
70
|
-
configureModulePath ??= resolveConfigureModulePath(entrypoint, options?.configure);
|
|
71
|
-
},
|
|
72
|
-
resolveId(id) {
|
|
73
|
-
// claim only our two virtual specifiers, leave everything else to the normal resolvers
|
|
74
|
-
if (id === ENV_MODULE_ID) return RESOLVED_ENV_MODULE_ID;
|
|
75
|
-
// second virtual specifier, same rule as above
|
|
76
|
-
if (id === CONFIGURE_MODULE_ID) return RESOLVED_CONFIGURE_MODULE_ID;
|
|
77
|
-
return null;
|
|
78
|
-
},
|
|
79
|
-
async load(id) {
|
|
80
|
-
// serves manifest/config resolved lazily here, not at plugin-creation time, so options.entrypoint changes between test runs are respected
|
|
81
|
-
if (id === RESOLVED_ENV_MODULE_ID) {
|
|
82
|
-
const { manifest, config } = await resolveAppTestEnv({ ...options, entrypoint });
|
|
83
|
-
return [
|
|
84
|
-
`export const manifest = ${JSON.stringify(manifest)};`,
|
|
85
|
-
`export const config = ${JSON.stringify(config)};`,
|
|
86
|
-
].join('\n');
|
|
87
|
-
}
|
|
88
|
-
// no conventional module means the app registers no extra modules, same as omitting `configure` from `makeComponent`
|
|
89
|
-
if (id === RESOLVED_CONFIGURE_MODULE_ID) {
|
|
90
|
-
return configureModulePath
|
|
91
|
-
? `export { default as configure } from ${JSON.stringify(configureModulePath)};`
|
|
92
|
-
: 'export const configure = undefined;';
|
|
93
|
-
}
|
|
94
|
-
return null;
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Resolves the app's module-configurator file: the explicit `file` if given, otherwise the
|
|
101
|
-
* first existing candidate in {@link DEFAULT_CONFIGURE_CANDIDATES}.
|
|
102
|
-
*
|
|
103
|
-
* @throws {@link FileNotFoundError} If an explicitly requested `file` does not exist — unlike
|
|
104
|
-
* the convention-based lookup, a typo here should fail loudly instead of silently running the
|
|
105
|
-
* test suite without the application's modules.
|
|
106
|
-
*/
|
|
107
|
-
const resolveConfigureModulePath = (cwd: string, file?: string): string | undefined => {
|
|
108
|
-
// an explicit path is a user request, not a convention lookup, so a typo must fail loudly
|
|
109
|
-
if (file) {
|
|
110
|
-
const resolved = resolve(cwd, file);
|
|
111
|
-
// fail fast rather than silently falling back to "no configurator"
|
|
112
|
-
if (!existsSync(resolved)) {
|
|
113
|
-
throw new FileNotFoundError(`Configure module not found: ${resolved}`);
|
|
114
|
-
}
|
|
115
|
-
return resolved;
|
|
116
|
-
}
|
|
117
|
-
// first candidate that exists wins; none existing is a valid "no configurator" state
|
|
118
|
-
const found = DEFAULT_CONFIGURE_CANDIDATES.find((candidate) =>
|
|
119
|
-
existsSync(resolve(cwd, candidate)),
|
|
120
|
-
);
|
|
121
|
-
return found ? resolve(cwd, found) : undefined;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
export default appTestVitePlugin;
|