@equinor/fusion-framework-react-app 14.0.2 → 14.0.3-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +9 -2
- package/dist/esm/ag-grid/community.js +12 -0
- package/dist/esm/ag-grid/community.js.map +1 -0
- package/dist/esm/ag-grid/enterprise.js +12 -0
- package/dist/esm/ag-grid/enterprise.js.map +1 -0
- package/dist/esm/ag-grid/react.js +12 -0
- package/dist/esm/ag-grid/react.js.map +1 -0
- package/dist/esm/ag-grid/testing.js +19 -0
- package/dist/esm/ag-grid/testing.js.map +1 -0
- package/dist/esm/ag-grid/theme.js +13 -0
- package/dist/esm/ag-grid/theme.js.map +1 -0
- package/dist/esm/create-component.js +1 -1
- package/dist/esm/msal/useToken.js +12 -3
- package/dist/esm/msal/useToken.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/ag-grid/community.d.ts +11 -0
- package/dist/types/ag-grid/enterprise.d.ts +11 -0
- package/dist/types/ag-grid/react.d.ts +11 -0
- package/dist/types/ag-grid/testing.d.ts +18 -0
- package/dist/types/ag-grid/theme.d.ts +13 -0
- package/dist/types/create-component.d.ts +1 -1
- package/dist/types/version.d.ts +1 -1
- package/docs/bookmark.md +7 -0
- package/docs/context.md +3 -1
- package/docs/framework.md +2 -2
- package/docs/msal.md +3 -3
- package/package.json +77 -25
- package/src/__tests__/Apploader.test.tsx +51 -0
- package/src/__tests__/fixtures/apploader-child-script.ts +9 -0
- package/src/__tests__/testApp.test.tsx +76 -0
- package/src/__tests__/useAccessToken.test.tsx +51 -0
- package/src/__tests__/useAppSetting.test.tsx +133 -0
- package/src/__tests__/useAppSettings.test.tsx +147 -0
- package/src/__tests__/useCurrentAccount.test.tsx +32 -0
- package/src/__tests__/useCurrentBookmark.test.tsx +108 -0
- package/src/__tests__/useCurrentContext.test.tsx +72 -0
- package/src/__tests__/useFeature.test.tsx +104 -0
- package/src/__tests__/useHelpCenter.test.tsx +64 -0
- package/src/__tests__/useStateSyncEvents.test.ts +12 -11
- package/src/__tests__/useToken.test.tsx +71 -0
- package/src/__tests__/useTrackFeature.test.tsx +83 -0
- package/src/ag-grid/community.ts +11 -0
- package/src/ag-grid/enterprise.ts +11 -0
- package/src/ag-grid/react.ts +11 -0
- package/src/ag-grid/testing.ts +19 -0
- package/src/ag-grid/theme.ts +17 -0
- package/src/create-component.tsx +1 -1
- package/src/msal/useToken.ts +14 -3
- package/src/version.ts +1 -1
- package/tsconfig.json +6 -0
- package/vitest.config.ts +2 -8
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { act } from 'react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { mockFramework } from '@equinor/fusion-framework/mock';
|
|
5
|
+
import { enableAppManifestMock } from '@equinor/fusion-framework-app/mock';
|
|
6
|
+
import type { AppModule } from '@equinor/fusion-framework-module-app';
|
|
7
|
+
import { createRouterMiddleware } from '@equinor/fusion-framework-module-http/mock';
|
|
8
|
+
|
|
9
|
+
import { useAppSettings } from '../settings/useAppSettings';
|
|
10
|
+
import { renderAppHook } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
|
|
11
|
+
|
|
12
|
+
const env = {
|
|
13
|
+
manifest: {
|
|
14
|
+
appKey: 'test-app',
|
|
15
|
+
displayName: 'Test App',
|
|
16
|
+
description: 'A test application',
|
|
17
|
+
type: 'standalone' as const,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Boots a parent framework whose `apps` client serves per-user settings from an
|
|
23
|
+
* in-memory store, so `useAppSettings` exercises the real fetch/update round-trip
|
|
24
|
+
* instead of a stubbed provider.
|
|
25
|
+
*
|
|
26
|
+
* @param initial - The settings the store starts seeded with.
|
|
27
|
+
* @param putResponse - Overrides the response a PUT request resolves to, to simulate a failed update.
|
|
28
|
+
*/
|
|
29
|
+
const mockSettingsFusion = async (
|
|
30
|
+
initial: Record<string, unknown> = {},
|
|
31
|
+
putResponse?: () => Response,
|
|
32
|
+
) => {
|
|
33
|
+
const store: Record<string, unknown> = { ...initial };
|
|
34
|
+
const fusion = await mockFramework<[AppModule]>((configurator) => {
|
|
35
|
+
configurator.http.addMiddleware(
|
|
36
|
+
createRouterMiddleware('https://apps.fusion.test', (router) => {
|
|
37
|
+
router.get('/persons/me/apps/:appKey/settings', () => Response.json(store));
|
|
38
|
+
router.put('/persons/me/apps/:appKey/settings', async ({ request }) => {
|
|
39
|
+
// a caller-supplied response simulates a failed persistence request
|
|
40
|
+
if (putResponse) return putResponse();
|
|
41
|
+
// the PUT body replaces the seeded keys it targets, leaving the rest of the store untouched
|
|
42
|
+
Object.assign(store, await request.json());
|
|
43
|
+
return Response.json(store);
|
|
44
|
+
});
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
enableAppManifestMock(configurator, env);
|
|
48
|
+
});
|
|
49
|
+
fusion.modules.app.setCurrentApp(env.manifest.appKey);
|
|
50
|
+
return fusion;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
interface TestSettings extends Record<string, unknown> {
|
|
54
|
+
theme: string;
|
|
55
|
+
layout: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('useAppSettings', () => {
|
|
59
|
+
it('resolves the full persisted settings object', async () => {
|
|
60
|
+
const fusion = await mockSettingsFusion({ theme: 'dark', layout: 'grid' });
|
|
61
|
+
|
|
62
|
+
const { result } = await renderAppHook(() => useAppSettings<TestSettings>(), { env, fusion });
|
|
63
|
+
|
|
64
|
+
await vi.waitFor(() =>
|
|
65
|
+
expect(result.current[0]).toMatchObject({ theme: 'dark', layout: 'grid' }),
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('falls back to the default value until the persisted settings resolve', async () => {
|
|
70
|
+
const fusion = await mockSettingsFusion({ theme: 'dark', layout: 'grid' });
|
|
71
|
+
|
|
72
|
+
const { result } = await renderAppHook(
|
|
73
|
+
() => useAppSettings<TestSettings>({ theme: 'light', layout: 'list' }),
|
|
74
|
+
{ env, fusion },
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(result.current[0]).toMatchObject({ theme: 'light', layout: 'list' });
|
|
78
|
+
await vi.waitFor(() =>
|
|
79
|
+
expect(result.current[0]).toMatchObject({ theme: 'dark', layout: 'grid' }),
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('persists an updated settings object and notifies onUpdated', async () => {
|
|
84
|
+
const fusion = await mockSettingsFusion({ theme: 'dark', layout: 'grid' });
|
|
85
|
+
const onUpdated = vi.fn();
|
|
86
|
+
|
|
87
|
+
const { result } = await renderAppHook(
|
|
88
|
+
() => useAppSettings<TestSettings>(undefined, { onUpdated }),
|
|
89
|
+
{ env, fusion },
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
await vi.waitFor(() =>
|
|
93
|
+
expect(result.current[0]).toMatchObject({ theme: 'dark', layout: 'grid' }),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
act(() => {
|
|
97
|
+
result.current[1]({ theme: 'light', layout: 'grid' });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await vi.waitFor(() =>
|
|
101
|
+
expect(result.current[0]).toMatchObject({ theme: 'light', layout: 'grid' }),
|
|
102
|
+
);
|
|
103
|
+
await vi.waitFor(() => expect(onUpdated).toHaveBeenCalled());
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('resolves the next settings object from the previous one when given an updater function', async () => {
|
|
107
|
+
const fusion = await mockSettingsFusion({ theme: 'dark', layout: 'grid' });
|
|
108
|
+
|
|
109
|
+
const { result } = await renderAppHook(() => useAppSettings<TestSettings>(), { env, fusion });
|
|
110
|
+
|
|
111
|
+
await vi.waitFor(() =>
|
|
112
|
+
expect(result.current[0]).toMatchObject({ theme: 'dark', layout: 'grid' }),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
act(() => {
|
|
116
|
+
// `current` is typed as possibly undefined even though it's already resolved by this point
|
|
117
|
+
result.current[1]((current) => ({ theme: current?.theme ?? 'dark', layout: 'list' }));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await vi.waitFor(() =>
|
|
121
|
+
expect(result.current[0]).toMatchObject({ theme: 'dark', layout: 'list' }),
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('surfaces a persistence failure through onError instead of throwing', async () => {
|
|
126
|
+
const fusion = await mockSettingsFusion(
|
|
127
|
+
{ theme: 'dark', layout: 'grid' },
|
|
128
|
+
() => new Response(null, { status: 500 }),
|
|
129
|
+
);
|
|
130
|
+
const onError = vi.fn();
|
|
131
|
+
|
|
132
|
+
const { result } = await renderAppHook(
|
|
133
|
+
() => useAppSettings<TestSettings>(undefined, { onError }),
|
|
134
|
+
{ env, fusion },
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
await vi.waitFor(() =>
|
|
138
|
+
expect(result.current[0]).toMatchObject({ theme: 'dark', layout: 'grid' }),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
act(() => {
|
|
142
|
+
result.current[1]({ theme: 'light', layout: 'grid' });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await vi.waitFor(() => expect(onError).toHaveBeenCalledWith(expect.any(Error)));
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { useCurrentAccount } from '../msal/useCurrentAccount';
|
|
4
|
+
import { renderAppHook } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
|
|
5
|
+
|
|
6
|
+
describe('useCurrentAccount', () => {
|
|
7
|
+
it('returns the default mock user signed in by the app scope’s auth module', async () => {
|
|
8
|
+
const { result } = await renderAppHook(() => useCurrentAccount());
|
|
9
|
+
|
|
10
|
+
expect(result.current).toMatchObject({
|
|
11
|
+
name: 'Test User',
|
|
12
|
+
username: 'test.user@equinor.com',
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('returns the account configured through the msal mock builder', async () => {
|
|
17
|
+
const { result } = await renderAppHook(() => useCurrentAccount(), {
|
|
18
|
+
configure: (configurator) =>
|
|
19
|
+
configurator.msal.setAccount({ name: 'Ada Lovelace', username: 'ada@equinor.com' }),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(result.current).toMatchObject({ name: 'Ada Lovelace', username: 'ada@equinor.com' });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns undefined when no account is signed in', async () => {
|
|
26
|
+
const { result } = await renderAppHook(() => useCurrentAccount(), {
|
|
27
|
+
configure: (configurator) => configurator.msal.setAccount(null),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(result.current).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { mockFramework } from '@equinor/fusion-framework/mock';
|
|
4
|
+
import { enableAppManifestMock } from '@equinor/fusion-framework-app/mock';
|
|
5
|
+
import type { AppMockConfigureFn } from '@equinor/fusion-framework-app/mock';
|
|
6
|
+
import type { AppModule } from '@equinor/fusion-framework-module-app';
|
|
7
|
+
import { enableBookmarkMock } from '@equinor/fusion-framework-module-bookmark/mock';
|
|
8
|
+
import type { Bookmark, BookmarkModule } from '@equinor/fusion-framework-module-bookmark';
|
|
9
|
+
|
|
10
|
+
import { useCurrentBookmark } from '../bookmark/useCurrentBookmark';
|
|
11
|
+
import { renderAppHook } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
|
|
12
|
+
import { useAppModules } from '../useAppModules';
|
|
13
|
+
|
|
14
|
+
const env = {
|
|
15
|
+
manifest: {
|
|
16
|
+
appKey: 'test-app',
|
|
17
|
+
displayName: 'Test App',
|
|
18
|
+
description: 'A test application',
|
|
19
|
+
type: 'standalone' as const,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** A fully-formed seeded bookmark, so tests only override what they care about. */
|
|
24
|
+
const createBookmark = (overrides: Partial<Bookmark> = {}): Bookmark => ({
|
|
25
|
+
id: 'bookmark-1',
|
|
26
|
+
name: 'My Bookmark',
|
|
27
|
+
appKey: 'test-app',
|
|
28
|
+
created: new Date('2024-01-01T00:00:00.000Z'),
|
|
29
|
+
createdBy: { id: 'seed-user', name: 'Seed User' },
|
|
30
|
+
...overrides,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('useCurrentBookmark', () => {
|
|
34
|
+
it('returns the app-scoped current bookmark once it belongs to the active app', async () => {
|
|
35
|
+
const bookmark = createBookmark();
|
|
36
|
+
const fusion = await mockFramework<[AppModule]>((configurator) =>
|
|
37
|
+
enableAppManifestMock(configurator, env),
|
|
38
|
+
);
|
|
39
|
+
fusion.modules.app.setCurrentApp(env.manifest.appKey);
|
|
40
|
+
|
|
41
|
+
const configure: AppMockConfigureFn<[BookmarkModule]> = (configurator) =>
|
|
42
|
+
enableBookmarkMock(configurator, (builder) => {
|
|
43
|
+
builder.setBookmarks([bookmark]);
|
|
44
|
+
builder.setCurrentBookmark(bookmark.id);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const { result } = await renderAppHook(() => useCurrentBookmark(), { env, fusion, configure });
|
|
48
|
+
|
|
49
|
+
await vi.waitFor(() =>
|
|
50
|
+
expect(result.current.currentBookmark).toMatchObject({ id: bookmark.id }),
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('hides the current bookmark once it belongs to a different app', async () => {
|
|
55
|
+
const bookmark = createBookmark({ appKey: 'some-other-app' });
|
|
56
|
+
const fusion = await mockFramework<[AppModule]>((configurator) =>
|
|
57
|
+
enableAppManifestMock(configurator, env),
|
|
58
|
+
);
|
|
59
|
+
fusion.modules.app.setCurrentApp(env.manifest.appKey);
|
|
60
|
+
|
|
61
|
+
const configure: AppMockConfigureFn<[BookmarkModule]> = (configurator) =>
|
|
62
|
+
enableBookmarkMock(configurator, (builder) => {
|
|
63
|
+
builder.setBookmarks([bookmark]);
|
|
64
|
+
builder.setCurrentBookmark(bookmark.id);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const { result } = await renderAppHook(
|
|
68
|
+
() => ({
|
|
69
|
+
bookmark: useCurrentBookmark(),
|
|
70
|
+
// unfiltered provider state, so we can tell the seeded bookmark actually
|
|
71
|
+
// resolved rather than the filter trivially matching a still-pending value
|
|
72
|
+
provider: useAppModules<[BookmarkModule]>().bookmark,
|
|
73
|
+
}),
|
|
74
|
+
{ env, fusion, configure },
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
await vi.waitFor(() =>
|
|
78
|
+
expect(result.current.provider.currentBookmark).toMatchObject({ id: bookmark.id }),
|
|
79
|
+
);
|
|
80
|
+
expect(result.current.bookmark.currentBookmark).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('falls back to the framework-scoped bookmark provider, warning about the deprecation', async () => {
|
|
84
|
+
const bookmark = createBookmark();
|
|
85
|
+
const fusion = await mockFramework<[AppModule, BookmarkModule]>((configurator) => {
|
|
86
|
+
enableAppManifestMock(configurator, env);
|
|
87
|
+
enableBookmarkMock(configurator, (builder) => {
|
|
88
|
+
builder.setBookmarks([bookmark]);
|
|
89
|
+
builder.setCurrentBookmark(bookmark.id);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
fusion.modules.app.setCurrentApp(env.manifest.appKey);
|
|
93
|
+
|
|
94
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
95
|
+
|
|
96
|
+
const { result } = await renderAppHook(() => useCurrentBookmark(), { env, fusion });
|
|
97
|
+
|
|
98
|
+
await vi.waitFor(() =>
|
|
99
|
+
expect(result.current.currentBookmark).toMatchObject({ id: bookmark.id }),
|
|
100
|
+
);
|
|
101
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
102
|
+
'@deprecation',
|
|
103
|
+
expect.stringContaining('has not enabled bookmarks'),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
warnSpy.mockRestore();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { act } from 'react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { mockFramework } from '@equinor/fusion-framework/mock';
|
|
5
|
+
import { enableAppManifestMock } from '@equinor/fusion-framework-app/mock';
|
|
6
|
+
import type { AppMockConfigureFn } from '@equinor/fusion-framework-app/mock';
|
|
7
|
+
import type { AppModule } from '@equinor/fusion-framework-module-app';
|
|
8
|
+
import { enableContextMock } from '@equinor/fusion-framework-module-context/mock';
|
|
9
|
+
import type { ContextItem, ContextModule } from '@equinor/fusion-framework-module-context';
|
|
10
|
+
|
|
11
|
+
import { useCurrentContext } from '../context/useCurrentContext';
|
|
12
|
+
import { renderAppHook } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
|
|
13
|
+
|
|
14
|
+
const env = {
|
|
15
|
+
manifest: {
|
|
16
|
+
appKey: 'test-app',
|
|
17
|
+
displayName: 'Test App',
|
|
18
|
+
description: 'A test application',
|
|
19
|
+
type: 'standalone' as const,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const project: ContextItem = {
|
|
24
|
+
id: 'ctx-1',
|
|
25
|
+
title: 'My project',
|
|
26
|
+
type: { id: 'ProjectMaster' },
|
|
27
|
+
value: {},
|
|
28
|
+
};
|
|
29
|
+
const facility: ContextItem = {
|
|
30
|
+
id: 'ctx-2',
|
|
31
|
+
title: 'My facility',
|
|
32
|
+
type: { id: 'Facility' },
|
|
33
|
+
value: {},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('useCurrentContext', () => {
|
|
37
|
+
it('resolves the context item selected on startup from the app-scoped context module', async () => {
|
|
38
|
+
const fusion = await mockFramework<[AppModule]>((configurator) =>
|
|
39
|
+
enableAppManifestMock(configurator, env),
|
|
40
|
+
);
|
|
41
|
+
const configure: AppMockConfigureFn<[ContextModule]> = (configurator) =>
|
|
42
|
+
enableContextMock(configurator, (mock) => {
|
|
43
|
+
mock.setCurrentContext(project);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const { result } = await renderAppHook(() => useCurrentContext(), { env, fusion, configure });
|
|
47
|
+
|
|
48
|
+
await vi.waitFor(() => expect(result.current.currentContext).toMatchObject({ id: project.id }));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('switches the current context when setCurrentContext is called with another seeded item', async () => {
|
|
52
|
+
const fusion = await mockFramework<[AppModule]>((configurator) =>
|
|
53
|
+
enableAppManifestMock(configurator, env),
|
|
54
|
+
);
|
|
55
|
+
const configure: AppMockConfigureFn<[ContextModule]> = (configurator) =>
|
|
56
|
+
enableContextMock(configurator, (mock) => {
|
|
57
|
+
mock.setCurrentContext(project);
|
|
58
|
+
mock.addContext(facility);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const { result } = await renderAppHook(() => useCurrentContext(), { env, fusion, configure });
|
|
62
|
+
await vi.waitFor(() => expect(result.current.currentContext).toMatchObject({ id: project.id }));
|
|
63
|
+
|
|
64
|
+
await act(async () => {
|
|
65
|
+
await result.current.setCurrentContext(facility.id);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await vi.waitFor(() =>
|
|
69
|
+
expect(result.current.currentContext).toMatchObject({ id: facility.id }),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { act } from 'react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { mockFramework } from '@equinor/fusion-framework/mock';
|
|
5
|
+
import { enableAppManifestMock } from '@equinor/fusion-framework-app/mock';
|
|
6
|
+
import type { AppMockConfigureFn } from '@equinor/fusion-framework-app/mock';
|
|
7
|
+
import type { AppModule } from '@equinor/fusion-framework-module-app';
|
|
8
|
+
import { enableFeatureFlagMock } from '@equinor/fusion-framework-module-feature-flag/mock';
|
|
9
|
+
import type { FeatureFlagModule } from '@equinor/fusion-framework-module-feature-flag';
|
|
10
|
+
|
|
11
|
+
import { useFeature } from '../feature-flag/useFeature';
|
|
12
|
+
import { renderAppHook } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
|
|
13
|
+
|
|
14
|
+
const env = {
|
|
15
|
+
manifest: {
|
|
16
|
+
appKey: 'test-app',
|
|
17
|
+
displayName: 'Test App',
|
|
18
|
+
description: 'A test application',
|
|
19
|
+
type: 'standalone' as const,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('useFeature', () => {
|
|
24
|
+
it('resolves a flag seeded on the app scope when the framework has no feature-flag module', async () => {
|
|
25
|
+
const fusion = await mockFramework<[AppModule]>((configurator) =>
|
|
26
|
+
enableAppManifestMock(configurator, env),
|
|
27
|
+
);
|
|
28
|
+
const configure: AppMockConfigureFn<[FeatureFlagModule]> = (configurator) => {
|
|
29
|
+
enableFeatureFlagMock(configurator, (mock) => {
|
|
30
|
+
mock.addFeature({ key: 'dark-mode', enabled: true });
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const { result } = await renderAppHook(() => useFeature('dark-mode'), {
|
|
35
|
+
env,
|
|
36
|
+
fusion,
|
|
37
|
+
configure,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await vi.waitFor(() => expect(result.current.feature?.enabled).toBe(true));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('lets an app-scoped flag override a framework-scoped flag with the same key', async () => {
|
|
44
|
+
const fusion = await mockFramework<[AppModule, FeatureFlagModule]>((configurator) => {
|
|
45
|
+
enableAppManifestMock(configurator, env);
|
|
46
|
+
enableFeatureFlagMock(configurator, (mock) => {
|
|
47
|
+
mock.addFeature({ key: 'dark-mode', enabled: false });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
const configure: AppMockConfigureFn<[FeatureFlagModule]> = (configurator) => {
|
|
51
|
+
enableFeatureFlagMock(configurator, (mock) => {
|
|
52
|
+
mock.addFeature({ key: 'dark-mode', enabled: true });
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const { result } = await renderAppHook(() => useFeature('dark-mode'), {
|
|
57
|
+
env,
|
|
58
|
+
fusion,
|
|
59
|
+
configure,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await vi.waitFor(() => expect(result.current.feature?.enabled).toBe(true));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('exposes a framework-only flag through the merged stream when the app has not seeded it', async () => {
|
|
66
|
+
const fusion = await mockFramework<[AppModule, FeatureFlagModule]>((configurator) => {
|
|
67
|
+
enableAppManifestMock(configurator, env);
|
|
68
|
+
enableFeatureFlagMock(configurator, (mock) => {
|
|
69
|
+
mock.addFeature({ key: 'framework-only', enabled: true });
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
const configure: AppMockConfigureFn<[FeatureFlagModule]> = (configurator) => {
|
|
73
|
+
enableFeatureFlagMock(configurator);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const { result } = await renderAppHook(() => useFeature('framework-only'), {
|
|
77
|
+
env,
|
|
78
|
+
fusion,
|
|
79
|
+
configure,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await vi.waitFor(() => expect(result.current.feature?.enabled).toBe(true));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('inverts the current value when toggleFeature is called without an explicit value', async () => {
|
|
86
|
+
const fusion = await mockFramework<[AppModule]>((configurator) =>
|
|
87
|
+
enableAppManifestMock(configurator, env),
|
|
88
|
+
);
|
|
89
|
+
const configure: AppMockConfigureFn<[FeatureFlagModule]> = (configurator) => {
|
|
90
|
+
enableFeatureFlagMock(configurator, (mock) => {
|
|
91
|
+
mock.addFeature({ key: 'my-flag', enabled: false });
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const { result } = await renderAppHook(() => useFeature('my-flag'), { env, fusion, configure });
|
|
96
|
+
await vi.waitFor(() => expect(result.current.feature?.enabled).toBe(false));
|
|
97
|
+
|
|
98
|
+
act(() => {
|
|
99
|
+
result.current.toggleFeature();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await vi.waitFor(() => expect(result.current.feature?.enabled).toBe(true));
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { mockFramework } from '@equinor/fusion-framework/mock';
|
|
4
|
+
import { enableAppManifestMock } from '@equinor/fusion-framework-app/mock';
|
|
5
|
+
import type { AppModule } from '@equinor/fusion-framework-module-app';
|
|
6
|
+
|
|
7
|
+
import useAppModule from '../useAppModule';
|
|
8
|
+
import { useHelpCenter } from '../help-center/useHelpCenter';
|
|
9
|
+
import { EVENT_NAME } from '../help-center/event-name.js';
|
|
10
|
+
import { renderAppHook } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
|
|
11
|
+
|
|
12
|
+
const env = {
|
|
13
|
+
manifest: {
|
|
14
|
+
appKey: 'test-app',
|
|
15
|
+
displayName: 'Test App',
|
|
16
|
+
description: 'A test application',
|
|
17
|
+
type: 'standalone' as const,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe('useHelpCenter', () => {
|
|
22
|
+
it('dispatches the expected page and detail for every help-center action', async () => {
|
|
23
|
+
const fusion = await mockFramework<[AppModule]>((configurator) =>
|
|
24
|
+
enableAppManifestMock(configurator, env),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const { result } = await renderAppHook(
|
|
28
|
+
() => ({ helpCenter: useHelpCenter(), eventModule: useAppModule('event') }),
|
|
29
|
+
{ env, fusion },
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const received: Array<{ page: string } & Record<string, unknown>> = [];
|
|
33
|
+
result.current.eventModule.addEventListener(EVENT_NAME, (event) => {
|
|
34
|
+
received.push(event.detail as { page: string } & Record<string, unknown>);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
result.current.helpCenter.openHelp();
|
|
38
|
+
await vi.waitFor(() => expect(received).toHaveLength(1));
|
|
39
|
+
|
|
40
|
+
result.current.helpCenter.openArticle('my-article');
|
|
41
|
+
await vi.waitFor(() => expect(received).toHaveLength(2));
|
|
42
|
+
|
|
43
|
+
result.current.helpCenter.openFaqs();
|
|
44
|
+
await vi.waitFor(() => expect(received).toHaveLength(3));
|
|
45
|
+
|
|
46
|
+
result.current.helpCenter.openSearch('my search');
|
|
47
|
+
await vi.waitFor(() => expect(received).toHaveLength(4));
|
|
48
|
+
|
|
49
|
+
result.current.helpCenter.openGovernance();
|
|
50
|
+
await vi.waitFor(() => expect(received).toHaveLength(5));
|
|
51
|
+
|
|
52
|
+
result.current.helpCenter.openReleaseNotes();
|
|
53
|
+
await vi.waitFor(() => expect(received).toHaveLength(6));
|
|
54
|
+
|
|
55
|
+
expect(received).toEqual([
|
|
56
|
+
{ page: 'home' },
|
|
57
|
+
{ page: 'article', articleId: 'my-article' },
|
|
58
|
+
{ page: 'faqs' },
|
|
59
|
+
{ page: 'search', search: 'my search' },
|
|
60
|
+
{ page: 'governance' },
|
|
61
|
+
{ page: 'release-notes' },
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { act } from 'react';
|
|
1
2
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
3
|
import { Subject } from 'rxjs';
|
|
3
4
|
|
|
4
|
-
import { act, renderHook } from '@testing-library/react';
|
|
5
|
-
|
|
6
5
|
import { StateSyncEvent, type StateSyncEventType } from '@equinor/fusion-framework-module-state';
|
|
6
|
+
import { renderAppHook } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
|
|
7
7
|
|
|
8
8
|
const event$ = new Subject<StateSyncEventType>();
|
|
9
9
|
|
|
@@ -14,8 +14,8 @@ vi.mock('../useAppModule', () => ({
|
|
|
14
14
|
import { useStateSyncEvents } from '../state/useStateSyncEvents';
|
|
15
15
|
|
|
16
16
|
describe('useStateSyncEvents', () => {
|
|
17
|
-
it('collects dispatched onStateSync.* events, oldest first', () => {
|
|
18
|
-
const { result } =
|
|
17
|
+
it('collects dispatched onStateSync.* events, oldest first', async () => {
|
|
18
|
+
const { result } = await renderAppHook(() => useStateSyncEvents(10));
|
|
19
19
|
|
|
20
20
|
expect(result.current).toEqual([]);
|
|
21
21
|
|
|
@@ -24,13 +24,14 @@ describe('useStateSyncEvents', () => {
|
|
|
24
24
|
event$.next(new StateSyncEvent.Status({ detail: { status: 'paused' } }));
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
expect(result.current).
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
expect(result.current).toMatchObject([
|
|
28
|
+
{ detail: { status: 'active' } },
|
|
29
|
+
{ detail: { status: 'paused' } },
|
|
30
|
+
]);
|
|
30
31
|
});
|
|
31
32
|
|
|
32
|
-
it('ignores events unrelated to state sync and trims the log to the given limit', () => {
|
|
33
|
-
const { result } =
|
|
33
|
+
it('ignores events unrelated to state sync and trims the log to the given limit', async () => {
|
|
34
|
+
const { result } = await renderAppHook(() => useStateSyncEvents(1));
|
|
34
35
|
|
|
35
36
|
act(() => {
|
|
36
37
|
event$.next(new StateSyncEvent.Status({ detail: { status: 'active' } }));
|
|
@@ -43,8 +44,8 @@ describe('useStateSyncEvents', () => {
|
|
|
43
44
|
expect(result.current[0]).toBeInstanceOf(StateSyncEvent.Error);
|
|
44
45
|
});
|
|
45
46
|
|
|
46
|
-
it('unsubscribes from the event stream on unmount', () => {
|
|
47
|
-
const { result, unmount } =
|
|
47
|
+
it('unsubscribes from the event stream on unmount', async () => {
|
|
48
|
+
const { result, unmount } = await renderAppHook(() => useStateSyncEvents(10));
|
|
48
49
|
|
|
49
50
|
unmount();
|
|
50
51
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { mockFramework } from '@equinor/fusion-framework/mock';
|
|
4
|
+
|
|
5
|
+
import { useToken } from '../msal/useToken';
|
|
6
|
+
import { renderAppHook } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
|
|
7
|
+
|
|
8
|
+
describe('useToken', () => {
|
|
9
|
+
it('resolves a full AuthenticationResult from the app scope’s real, mock-backed auth module', async () => {
|
|
10
|
+
const { result, unmount } = await renderAppHook(() => useToken({ scopes: ['User.Read'] }));
|
|
11
|
+
|
|
12
|
+
// `renderAppHook` awaits the render, and the mock resolves near-instantly,
|
|
13
|
+
// so the pending state may have already settled by the time we can observe it
|
|
14
|
+
await vi.waitFor(() => expect(result.current.pending).toBe(false));
|
|
15
|
+
|
|
16
|
+
expect(result.current.error).toBeNull();
|
|
17
|
+
expect(result.current.token?.scopes).toEqual(['User.Read']);
|
|
18
|
+
// a structurally valid JWT has three dot-separated segments
|
|
19
|
+
expect(result.current.token?.accessToken.split('.')).toHaveLength(3);
|
|
20
|
+
expect(result.current.token?.account).toMatchObject({ username: 'test.user@equinor.com' });
|
|
21
|
+
|
|
22
|
+
// unmount before the next test's environment tears down, so no acquisition
|
|
23
|
+
// effect can flush a state update against an already-destroyed `window`
|
|
24
|
+
await unmount();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('surfaces an acquisition error instead of throwing', async () => {
|
|
28
|
+
const fusion = await mockFramework();
|
|
29
|
+
// persistent, not `-Once`: the app module hoists via a version-compat proxy that
|
|
30
|
+
// may call through before the hook's own effect does, consuming a one-shot mock
|
|
31
|
+
vi.spyOn(fusion.modules.auth, 'acquireToken').mockRejectedValue(
|
|
32
|
+
new Error('acquisition failed'),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const { result, unmount } = await renderAppHook(() => useToken({ scopes: ['User.Read'] }), {
|
|
36
|
+
fusion,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await vi.waitFor(() => expect(result.current.pending).toBe(false));
|
|
40
|
+
|
|
41
|
+
expect(result.current.token).toBeUndefined();
|
|
42
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
43
|
+
|
|
44
|
+
await unmount();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('only re-acquires the token when the scopes\u2019 contents change, not on every re-render', async () => {
|
|
48
|
+
const fusion = await mockFramework();
|
|
49
|
+
const acquireToken = vi.spyOn(fusion.modules.auth, 'acquireToken');
|
|
50
|
+
|
|
51
|
+
const { result, rerender, unmount } = await renderAppHook((props) => useToken(props), {
|
|
52
|
+
initialProps: { scopes: ['User.Read'] },
|
|
53
|
+
fusion,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await vi.waitFor(() => expect(result.current.pending).toBe(false));
|
|
57
|
+
expect(acquireToken).toHaveBeenCalledTimes(1);
|
|
58
|
+
|
|
59
|
+
// a fresh array literal with the same contents must not trigger a re-acquisition
|
|
60
|
+
await rerender({ scopes: ['User.Read'] });
|
|
61
|
+
await vi.waitFor(() => expect(result.current.pending).toBe(false));
|
|
62
|
+
expect(acquireToken).toHaveBeenCalledTimes(1);
|
|
63
|
+
|
|
64
|
+
// changed scope contents must trigger a second acquisition
|
|
65
|
+
await rerender({ scopes: ['User.Read', 'Mail.Read'] });
|
|
66
|
+
await vi.waitFor(() => expect(result.current.pending).toBe(false));
|
|
67
|
+
expect(acquireToken).toHaveBeenCalledTimes(2);
|
|
68
|
+
|
|
69
|
+
await unmount();
|
|
70
|
+
});
|
|
71
|
+
});
|