@equinor/fusion-framework-react-app 14.0.1 → 15.0.0-next.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/CHANGELOG.md +90 -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 +47 -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,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AG Grid community-tier sub-path entry-point.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Re-exports every public symbol from
|
|
6
|
+
* `@equinor/fusion-framework-react-ag-grid/community` so the application
|
|
7
|
+
* resolves a single shared copy of `ag-grid-community`.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
export * from '@equinor/fusion-framework-react-ag-grid/community';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AG Grid enterprise-tier sub-path entry-point.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Re-exports every public symbol from
|
|
6
|
+
* `@equinor/fusion-framework-react-ag-grid/enterprise` so the application
|
|
7
|
+
* resolves a single shared copy of `ag-grid-enterprise`.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
export * from '@equinor/fusion-framework-react-ag-grid/enterprise';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AG Grid React component sub-path entry-point.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Re-exports the AG Grid React bindings from
|
|
6
|
+
* `@equinor/fusion-framework-react-ag-grid/react`, including the
|
|
7
|
+
* `AgGridReact` component, `enableAgGrid`, and their associated types.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
export * from '@equinor/fusion-framework-react-ag-grid/react';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AG Grid test helpers sub-path entry-point.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Re-exports the AG Grid test helpers from
|
|
6
|
+
* `@equinor/fusion-framework-module-ag-grid/testing`. Import from test setup
|
|
7
|
+
* files only — these helpers patch global state and are not meant for
|
|
8
|
+
* application runtime code.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Silences AG Grid Enterprise's unlicensed "License Key Not Found" banner on
|
|
14
|
+
* `console.error` so it doesn't bury real failures in test output.
|
|
15
|
+
*
|
|
16
|
+
* @returns A function that restores the original `console.error`.
|
|
17
|
+
*/
|
|
18
|
+
export { suppressAgGridLicenseBanner } from '@equinor/fusion-framework-module-ag-grid/testing';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AG Grid theme sub-path entry-point.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Re-exports the Fusion AG Grid theme utilities from
|
|
6
|
+
* `@equinor/fusion-framework-module-ag-grid/themes`, together with the
|
|
7
|
+
* application-scoped {@link useTheme} hook.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
export { fusionTheme, createThemeFromTheme, createTheme, } from '@equinor/fusion-framework-module-ag-grid/themes';
|
|
12
|
+
export type { Theme } from '@equinor/fusion-framework-module-ag-grid/themes';
|
|
13
|
+
export { useTheme } from './useTheme';
|
|
@@ -27,7 +27,7 @@ export type ComponentRenderer<TFusion extends Fusion = Fusion, TEnv = AppEnv> =
|
|
|
27
27
|
* @example
|
|
28
28
|
* ```tsx
|
|
29
29
|
* const configCallback: AppConfigurator = (configurator) => {
|
|
30
|
-
* configurator.
|
|
30
|
+
* configurator.configureHttpClient(
|
|
31
31
|
* 'bar', {
|
|
32
32
|
* baseUri: 'https://somewhere-test.com',
|
|
33
33
|
* defaultScopes: ['foo/.default']
|
package/dist/types/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "
|
|
1
|
+
export declare const version = "15.0.0-next.1";
|
package/docs/bookmark.md
CHANGED
|
@@ -78,6 +78,13 @@ const MyApp = () => {
|
|
|
78
78
|
};
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
> [!WARNING]
|
|
82
|
+
> If your app has **not** called `enableBookmark`, `useCurrentBookmark` falls back to the
|
|
83
|
+
> portal's framework-scoped bookmark provider and logs a `@deprecation` console warning
|
|
84
|
+
> (`"application has not enabled bookmarks, this will not work in the future"`). This
|
|
85
|
+
> fallback is deprecated and will be removed — always call `enableBookmark` in apps that
|
|
86
|
+
> use bookmarks.
|
|
87
|
+
|
|
81
88
|
## useBookmark (Deprecated)
|
|
82
89
|
|
|
83
90
|
> [!CAUTION]
|
package/docs/context.md
CHANGED
|
@@ -29,12 +29,14 @@ Returns the currently selected context from the application-scoped context modul
|
|
|
29
29
|
```ts
|
|
30
30
|
function useCurrentContext(): {
|
|
31
31
|
currentContext: ContextItem | null | undefined;
|
|
32
|
-
setCurrentContext: (context?: ContextItem | string | null) => void
|
|
32
|
+
setCurrentContext: (context?: ContextItem | string | null) => void | Promise<ContextItem | null>;
|
|
33
33
|
};
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
**Returns:** An object with the current context (`ContextItem | null | undefined`) and a setter to change it.
|
|
37
37
|
|
|
38
|
+
`setCurrentContext` clears the context when called with no argument (or `null`), resolves an item by `id` when given a `string`, or sets the given `ContextItem` directly. Clearing returns `void`; setting by `id` or by item returns a `Promise` that resolves once the switch completes — `await` it when you need to know the new context has taken effect (e.g. before navigating).
|
|
39
|
+
|
|
38
40
|
### Example
|
|
39
41
|
|
|
40
42
|
```tsx
|
package/docs/framework.md
CHANGED
|
@@ -31,11 +31,11 @@ Use this when your app hasn't configured its own context module but still needs
|
|
|
31
31
|
```ts
|
|
32
32
|
function useFrameworkCurrentContext(): {
|
|
33
33
|
currentContext: ContextItem | undefined;
|
|
34
|
-
setCurrentContext: (entry?: ContextItem | string | null) => void
|
|
34
|
+
setCurrentContext: (entry?: ContextItem | string | null) => void | Promise<ContextItem | null>;
|
|
35
35
|
};
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
**Returns:** An object with `currentContext` (the portal's active context, or `undefined` if none is selected) and `setCurrentContext` to change it.
|
|
38
|
+
**Returns:** An object with `currentContext` (the portal's active context, or `undefined` if none is selected) and `setCurrentContext` to change it. Clearing (no argument or `null`) returns `void`; setting by `id` or by item returns a `Promise` that resolves once the switch completes.
|
|
39
39
|
|
|
40
40
|
**Example:**
|
|
41
41
|
|
package/docs/msal.md
CHANGED
|
@@ -116,7 +116,7 @@ const TokenInfo = () => {
|
|
|
116
116
|
return (
|
|
117
117
|
<div>
|
|
118
118
|
<p>Access Token: {token.accessToken.substring(0, 20)}...</p>
|
|
119
|
-
<p>Expires: {
|
|
119
|
+
<p>Expires: {token.expiresOn?.toLocaleString() ?? 'unknown'}</p>
|
|
120
120
|
<p>Token Type: {token.tokenType}</p>
|
|
121
121
|
<p>Scope: {token.scopes.join(', ')}</p>
|
|
122
122
|
</div>
|
|
@@ -134,8 +134,8 @@ const TokenInfo = () => {
|
|
|
134
134
|
|
|
135
135
|
**Note:** The `AuthenticationResult` type includes:
|
|
136
136
|
- `accessToken: string`
|
|
137
|
-
- `account: AccountInfo`
|
|
138
|
-
- `expiresOn: Date`
|
|
137
|
+
- `account: AccountInfo | null`
|
|
138
|
+
- `expiresOn: Date | null`
|
|
139
139
|
- `tokenType: string`
|
|
140
140
|
- `scopes: string[]`
|
|
141
141
|
- And more...
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@equinor/fusion-framework-react-app",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "15.0.0-next.1",
|
|
4
4
|
"description": "",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "./dist/esm/index.js",
|
|
6
7
|
"types": "./dist/types/index.d.ts",
|
|
7
8
|
"exports": {
|
|
@@ -68,6 +69,26 @@
|
|
|
68
69
|
"./routing": {
|
|
69
70
|
"types": "./dist/types/routing/index.d.ts",
|
|
70
71
|
"import": "./dist/esm/routing/index.js"
|
|
72
|
+
},
|
|
73
|
+
"./ag-grid": {
|
|
74
|
+
"types": "./dist/types/ag-grid/react.d.ts",
|
|
75
|
+
"import": "./dist/esm/ag-grid/react.js"
|
|
76
|
+
},
|
|
77
|
+
"./ag-grid/community": {
|
|
78
|
+
"types": "./dist/types/ag-grid/community.d.ts",
|
|
79
|
+
"import": "./dist/esm/ag-grid/community.js"
|
|
80
|
+
},
|
|
81
|
+
"./ag-grid/enterprise": {
|
|
82
|
+
"types": "./dist/types/ag-grid/enterprise.d.ts",
|
|
83
|
+
"import": "./dist/esm/ag-grid/enterprise.js"
|
|
84
|
+
},
|
|
85
|
+
"./ag-grid/theme": {
|
|
86
|
+
"types": "./dist/types/ag-grid/theme.d.ts",
|
|
87
|
+
"import": "./dist/esm/ag-grid/theme.js"
|
|
88
|
+
},
|
|
89
|
+
"./ag-grid/testing": {
|
|
90
|
+
"types": "./dist/types/ag-grid/testing.d.ts",
|
|
91
|
+
"import": "./dist/esm/ag-grid/testing.js"
|
|
71
92
|
}
|
|
72
93
|
},
|
|
73
94
|
"typesVersions": {
|
|
@@ -116,6 +137,21 @@
|
|
|
116
137
|
],
|
|
117
138
|
"routing": [
|
|
118
139
|
"dist/types/routing/index.d.ts"
|
|
140
|
+
],
|
|
141
|
+
"ag-grid": [
|
|
142
|
+
"dist/types/ag-grid/react.d.ts"
|
|
143
|
+
],
|
|
144
|
+
"ag-grid/community": [
|
|
145
|
+
"dist/types/ag-grid/community.d.ts"
|
|
146
|
+
],
|
|
147
|
+
"ag-grid/enterprise": [
|
|
148
|
+
"dist/types/ag-grid/enterprise.d.ts"
|
|
149
|
+
],
|
|
150
|
+
"ag-grid/theme": [
|
|
151
|
+
"dist/types/ag-grid/theme.d.ts"
|
|
152
|
+
],
|
|
153
|
+
"ag-grid/testing": [
|
|
154
|
+
"dist/types/ag-grid/testing.d.ts"
|
|
119
155
|
]
|
|
120
156
|
}
|
|
121
157
|
},
|
|
@@ -131,46 +167,62 @@
|
|
|
131
167
|
"directory": "packages/react"
|
|
132
168
|
},
|
|
133
169
|
"dependencies": {
|
|
134
|
-
"@equinor/fusion-framework-
|
|
135
|
-
"@equinor/fusion-framework
|
|
136
|
-
"@equinor/fusion-framework-module": "6.1.
|
|
137
|
-
"@equinor/fusion-framework-
|
|
138
|
-
"@equinor/fusion-framework-
|
|
139
|
-
"@equinor/fusion-framework-module
|
|
140
|
-
"@equinor/fusion-framework-react
|
|
141
|
-
"@equinor/fusion-framework-react-module-http": "
|
|
170
|
+
"@equinor/fusion-framework-app": "14.0.0-next.0",
|
|
171
|
+
"@equinor/fusion-framework": "8.1.0-next.0",
|
|
172
|
+
"@equinor/fusion-framework-module": "6.1.3-next.0",
|
|
173
|
+
"@equinor/fusion-framework-module-app": "8.1.0-next.0",
|
|
174
|
+
"@equinor/fusion-framework-module-http": "8.1.0-next.0",
|
|
175
|
+
"@equinor/fusion-framework-react-module": "4.0.3-next.0",
|
|
176
|
+
"@equinor/fusion-framework-react": "9.0.0-next.0",
|
|
177
|
+
"@equinor/fusion-framework-react-module-http": "12.0.0-next.0",
|
|
178
|
+
"@equinor/fusion-framework-module-navigation": "7.0.8-next.0"
|
|
142
179
|
},
|
|
143
180
|
"devDependencies": {
|
|
144
|
-
"@
|
|
181
|
+
"@remix-run/router": "^1.23.3",
|
|
145
182
|
"@types/react": "^19.2.7",
|
|
146
183
|
"@types/react-dom": "^19.2.3",
|
|
147
|
-
"
|
|
184
|
+
"@vitest/browser-playwright": "^4.1.0",
|
|
185
|
+
"playwright": "^1.62.1",
|
|
148
186
|
"react": "^19.2.1",
|
|
149
187
|
"react-dom": "^19.2.1",
|
|
150
188
|
"rxjs": "^7.8.1",
|
|
151
189
|
"typescript": "^7.0.2",
|
|
152
|
-
"vitest": "^4.1.
|
|
153
|
-
"
|
|
154
|
-
"@equinor/fusion-framework-module-
|
|
155
|
-
"@equinor/fusion-framework-
|
|
156
|
-
"@equinor/fusion-framework-module-
|
|
157
|
-
"@equinor/fusion-framework-module-
|
|
158
|
-
"@equinor/fusion-framework-module-
|
|
159
|
-
"@equinor/fusion-framework-
|
|
160
|
-
"@equinor/fusion-framework-
|
|
161
|
-
"@equinor/fusion-framework-
|
|
162
|
-
"@equinor/fusion-
|
|
190
|
+
"vitest": "^4.1.10",
|
|
191
|
+
"vitest-browser-react": "^2.2.0",
|
|
192
|
+
"@equinor/fusion-framework-module-ag-grid": "37.1.0-next.1",
|
|
193
|
+
"@equinor/fusion-framework-react-ag-grid": "37.1.0-next.1",
|
|
194
|
+
"@equinor/fusion-framework-module-analytics": "3.1.0-next.0",
|
|
195
|
+
"@equinor/fusion-framework-module-bookmark": "4.1.0-next.0",
|
|
196
|
+
"@equinor/fusion-framework-module-context": "9.0.0-next.0",
|
|
197
|
+
"@equinor/fusion-framework-module-event": "6.1.0-next.0",
|
|
198
|
+
"@equinor/fusion-framework-module-msal": "11.0.0-next.0",
|
|
199
|
+
"@equinor/fusion-framework-module-state": "^2.0.1-next.0",
|
|
200
|
+
"@equinor/fusion-framework-module-feature-flag": "2.1.0-next.0",
|
|
201
|
+
"@equinor/fusion-framework-module-telemetry": "8.0.0-next.0",
|
|
202
|
+
"@equinor/fusion-framework-react-module-bookmark": "7.0.0-next.0",
|
|
203
|
+
"@equinor/fusion-framework-react-module-context": "7.0.3-next.0",
|
|
204
|
+
"@equinor/fusion-framework-react-router": "2.4.1-next.0",
|
|
205
|
+
"@equinor/fusion-framework-vitest-plugin-react-app": "0.2.0-next.1",
|
|
206
|
+
"@equinor/fusion-observable": "9.1.2-next.0"
|
|
163
207
|
},
|
|
164
208
|
"peerDependencies": {
|
|
165
209
|
"@types/react": "^18.0.0 || ^19.0.0",
|
|
166
210
|
"react": "^18.0.0 || ^19.0.0",
|
|
167
211
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
168
212
|
"rxjs": "^7.0.0",
|
|
169
|
-
"@equinor/fusion-framework-module-
|
|
170
|
-
"@equinor/fusion-framework-
|
|
171
|
-
"@equinor/fusion-framework-
|
|
213
|
+
"@equinor/fusion-framework-module-ag-grid": "^37.1.0-next.1",
|
|
214
|
+
"@equinor/fusion-framework-module-msal": "^11.0.0-next.0",
|
|
215
|
+
"@equinor/fusion-framework-react-ag-grid": "^37.1.0-next.1",
|
|
216
|
+
"@equinor/fusion-framework-module-state": "^2.0.1-next.0",
|
|
217
|
+
"@equinor/fusion-framework-react-router": "^2.4.1-next.0"
|
|
172
218
|
},
|
|
173
219
|
"peerDependenciesMeta": {
|
|
220
|
+
"@equinor/fusion-framework-module-ag-grid": {
|
|
221
|
+
"optional": true
|
|
222
|
+
},
|
|
223
|
+
"@equinor/fusion-framework-react-ag-grid": {
|
|
224
|
+
"optional": true
|
|
225
|
+
},
|
|
174
226
|
"@equinor/fusion-framework-react-module-ag-grid": {
|
|
175
227
|
"optional": true
|
|
176
228
|
},
|
|
@@ -0,0 +1,51 @@
|
|
|
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 { AppManifest, AppModule } from '@equinor/fusion-framework-module-app';
|
|
6
|
+
|
|
7
|
+
import { Apploader } from '../apploader/Apploader';
|
|
8
|
+
import { renderAppComponent } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
|
|
9
|
+
|
|
10
|
+
// resolved from this test file rather than hardcoded, so it survives a package move
|
|
11
|
+
const fixturesUri = new URL('./fixtures', import.meta.url).pathname;
|
|
12
|
+
|
|
13
|
+
describe('Apploader', () => {
|
|
14
|
+
it('mounts the child app’s rendered content once its script loads', async () => {
|
|
15
|
+
const manifest: AppManifest = {
|
|
16
|
+
appKey: 'child-app',
|
|
17
|
+
displayName: 'Child App',
|
|
18
|
+
description: 'A child application',
|
|
19
|
+
type: 'standalone',
|
|
20
|
+
build: { version: '1.0.0', entryPoint: 'apploader-child-script.ts', assetPath: '' },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const fusion = await mockFramework<[AppModule]>((configurator) =>
|
|
24
|
+
enableAppManifestMock(configurator, { manifest }, fixturesUri),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const { container } = await renderAppComponent(<Apploader appKey="child-app" />, { fusion });
|
|
28
|
+
|
|
29
|
+
// the loading state renders synchronously, before the script's dynamic import resolves
|
|
30
|
+
expect(container.textContent).toContain('Loading child-app');
|
|
31
|
+
await vi.waitFor(() => expect(container.textContent).toContain('mounted: child-app'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('surfaces the load error instead of throwing when the app’s script fails to import', async () => {
|
|
35
|
+
const manifest: AppManifest = {
|
|
36
|
+
appKey: 'broken-app',
|
|
37
|
+
displayName: 'Broken App',
|
|
38
|
+
description: 'An application whose build entry point does not exist',
|
|
39
|
+
type: 'standalone',
|
|
40
|
+
build: { version: '1.0.0', entryPoint: 'does-not-exist.ts', assetPath: '' },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const fusion = await mockFramework<[AppModule]>((configurator) =>
|
|
44
|
+
enableAppManifestMock(configurator, { manifest }, fixturesUri),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const { container } = await renderAppComponent(<Apploader appKey="broken-app" />, { fusion });
|
|
48
|
+
|
|
49
|
+
await vi.waitFor(() => expect(container.textContent).toContain('Error loading broken-app'));
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AppScriptModule } from '@equinor/fusion-framework-module-app';
|
|
2
|
+
|
|
3
|
+
// a minimal stand-in for a real app bundle's entry point, dynamically imported by `App.initialize()`
|
|
4
|
+
export const renderApp: AppScriptModule['renderApp'] = (el, { env }) => {
|
|
5
|
+
el.textContent = `mounted: ${env.manifest?.appKey}`;
|
|
6
|
+
return () => {
|
|
7
|
+
el.textContent = '';
|
|
8
|
+
};
|
|
9
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { enableContextMock } from '@equinor/fusion-framework-module-context/mock';
|
|
4
|
+
import type { ContextItem } from '@equinor/fusion-framework-module-context';
|
|
5
|
+
import { useCurrentContext } from '@equinor/fusion-framework-react-app/context';
|
|
6
|
+
|
|
7
|
+
import { testApp } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
|
|
8
|
+
|
|
9
|
+
const projectA: ContextItem = {
|
|
10
|
+
id: 'ctx-a',
|
|
11
|
+
title: 'Project A',
|
|
12
|
+
type: { id: 'ProjectMaster' },
|
|
13
|
+
value: {},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const Greeting = () => <p>hello</p>;
|
|
17
|
+
|
|
18
|
+
const CurrentContext = () => {
|
|
19
|
+
const { currentContext } = useCurrentContext();
|
|
20
|
+
return <p>{currentContext?.title ?? 'none'}</p>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('testApp', () => {
|
|
24
|
+
testApp(
|
|
25
|
+
'resolves a fusion/app scope and renders through the public /vitest entry point',
|
|
26
|
+
async ({ render, fusion, app }) => {
|
|
27
|
+
const screen = await render(<Greeting />);
|
|
28
|
+
await expect.element(screen.getByText('hello')).toBeVisible();
|
|
29
|
+
expect(fusion).toBeDefined();
|
|
30
|
+
expect(app).toBeDefined();
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
describe('with a seeded context module', () => {
|
|
35
|
+
const test = testApp.extend(
|
|
36
|
+
'configure',
|
|
37
|
+
{ injected: true },
|
|
38
|
+
() => (configurator) =>
|
|
39
|
+
enableContextMock(configurator, (mock) => mock.setCurrentContext(projectA)),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
test('starts on the seeded context', async ({ render }) => {
|
|
43
|
+
const screen = await render(<CurrentContext />);
|
|
44
|
+
await expect.element(screen.getByText(projectA.title as string)).toBeVisible();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { mockFramework } from '@equinor/fusion-framework/mock';
|
|
4
|
+
|
|
5
|
+
import { useAccessToken } from '../msal/useAccessToken';
|
|
6
|
+
import { renderAppHook } from '@equinor/fusion-framework-vitest-plugin-react-app/test';
|
|
7
|
+
|
|
8
|
+
describe('useAccessToken', () => {
|
|
9
|
+
it('resolves an access token from the app scope’s real, mock-backed auth module', async () => {
|
|
10
|
+
// `renderAppHook` runs the real msal module against an in-process mock client,
|
|
11
|
+
// so `auth` behaves exactly as it would with a signed-in user in production.
|
|
12
|
+
const { result, unmount } = await renderAppHook(() =>
|
|
13
|
+
useAccessToken({ scopes: ['User.Read'] }),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
// `renderAppHook` awaits the render, and the mock resolves near-instantly,
|
|
17
|
+
// so the pending state may have already settled by the time we can observe it
|
|
18
|
+
await vi.waitFor(() => expect(result.current.pending).toBe(false));
|
|
19
|
+
|
|
20
|
+
expect(result.current.error).toBeNull();
|
|
21
|
+
// a structurally valid JWT has three dot-separated segments
|
|
22
|
+
expect(result.current.token?.split('.')).toHaveLength(3);
|
|
23
|
+
|
|
24
|
+
// unmount before the next test's environment tears down, so no acquisition
|
|
25
|
+
// effect can flush a state update against an already-destroyed `window`
|
|
26
|
+
await unmount();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('surfaces an acquisition error instead of throwing', async () => {
|
|
30
|
+
const fusion = await mockFramework();
|
|
31
|
+
// persistent, not `-Once`: the app module hoists via a version-compat proxy that
|
|
32
|
+
// may call through before the hook's own effect does, consuming a one-shot mock
|
|
33
|
+
vi.spyOn(fusion.modules.auth, 'acquireToken').mockRejectedValue(
|
|
34
|
+
new Error('acquisition failed'),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const { result, unmount } = await renderAppHook(
|
|
38
|
+
() => useAccessToken({ scopes: ['User.Read'] }),
|
|
39
|
+
{
|
|
40
|
+
fusion,
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
await vi.waitFor(() => expect(result.current.pending).toBe(false));
|
|
45
|
+
|
|
46
|
+
expect(result.current.token).toBeUndefined();
|
|
47
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
48
|
+
|
|
49
|
+
await unmount();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
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 { useAppSetting } from '../settings/useAppSetting';
|
|
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 `useAppSetting` 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
|
+
describe('useAppSetting', () => {
|
|
54
|
+
it('resolves the persisted value for a single setting key', async () => {
|
|
55
|
+
const fusion = await mockSettingsFusion({ theme: 'dark' });
|
|
56
|
+
|
|
57
|
+
const { result } = await renderAppHook(
|
|
58
|
+
() => useAppSetting<{ theme: string }, 'theme'>('theme'),
|
|
59
|
+
{ env, fusion },
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
await vi.waitFor(() => expect(result.current[0]).toBe('dark'));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('falls back to the default value until the persisted settings resolve', async () => {
|
|
66
|
+
const fusion = await mockSettingsFusion({ theme: 'dark' });
|
|
67
|
+
|
|
68
|
+
const { result } = await renderAppHook(
|
|
69
|
+
() => useAppSetting<{ theme: string }, 'theme'>('theme', 'light'),
|
|
70
|
+
{ env, fusion },
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(result.current[0]).toBe('light');
|
|
74
|
+
await vi.waitFor(() => expect(result.current[0]).toBe('dark'));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('persists an updated value and notifies onUpdated', async () => {
|
|
78
|
+
const fusion = await mockSettingsFusion({ theme: 'dark' });
|
|
79
|
+
const onUpdated = vi.fn();
|
|
80
|
+
|
|
81
|
+
const { result } = await renderAppHook(
|
|
82
|
+
() => useAppSetting<{ theme: string }, 'theme'>('theme', undefined, { onUpdated }),
|
|
83
|
+
{ env, fusion },
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
await vi.waitFor(() => expect(result.current[0]).toBe('dark'));
|
|
87
|
+
|
|
88
|
+
act(() => {
|
|
89
|
+
result.current[1]('light');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await vi.waitFor(() => expect(result.current[0]).toBe('light'));
|
|
93
|
+
await vi.waitFor(() => expect(onUpdated).toHaveBeenCalled());
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('resolves the next value from the previous one when given an updater function', async () => {
|
|
97
|
+
const fusion = await mockSettingsFusion({ count: 1 });
|
|
98
|
+
|
|
99
|
+
const { result } = await renderAppHook(
|
|
100
|
+
() => useAppSetting<{ count: number }, 'count'>('count'),
|
|
101
|
+
{ env, fusion },
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
await vi.waitFor(() => expect(result.current[0]).toBe(1));
|
|
105
|
+
|
|
106
|
+
act(() => {
|
|
107
|
+
result.current[1]((current) => (current ?? 0) + 1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await vi.waitFor(() => expect(result.current[0]).toBe(2));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('surfaces a persistence failure through onError instead of throwing', async () => {
|
|
114
|
+
const fusion = await mockSettingsFusion(
|
|
115
|
+
{ theme: 'dark' },
|
|
116
|
+
() => new Response(null, { status: 500 }),
|
|
117
|
+
);
|
|
118
|
+
const onError = vi.fn();
|
|
119
|
+
|
|
120
|
+
const { result } = await renderAppHook(
|
|
121
|
+
() => useAppSetting<{ theme: string }, 'theme'>('theme', undefined, { onError }),
|
|
122
|
+
{ env, fusion },
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await vi.waitFor(() => expect(result.current[0]).toBe('dark'));
|
|
126
|
+
|
|
127
|
+
act(() => {
|
|
128
|
+
result.current[1]('light');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await vi.waitFor(() => expect(onError).toHaveBeenCalledWith(expect.any(Error)));
|
|
132
|
+
});
|
|
133
|
+
});
|