@equinor/fusion-framework-module-app 8.0.5 → 8.0.6-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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -18,6 +18,7 @@
18
18
  */
19
19
  export { AppModuleConfig, AppConfigurator, IAppConfigurator, type AppModuleConfig as IAppModuleConfig, } from './AppConfigurator';
20
20
  export { AppClient, type IAppClient } from './AppClient';
21
+ export { AppConfig } from './AppConfig';
21
22
  export { AppModuleProvider } from './AppModuleProvider';
22
23
  export { IApp } from './app/App';
23
24
  export * from './events';
@@ -0,0 +1,56 @@
1
+ import type { Observable } from 'rxjs';
2
+ import type { IHttpClient } from '@equinor/fusion-framework-module-http';
3
+ import { AppClient } from '../AppClient.js';
4
+ import type { AppConfig, AppManifest, ConfigEnvironment } from '../types.js';
5
+ /**
6
+ * An {@link AppClient} that answers `getAppManifest` and `getAppConfig` for one
7
+ * known app locally, delegating everything else — other app keys, tagged
8
+ * requests, builds, settings — to the real client it wraps.
9
+ *
10
+ * @remarks
11
+ * Whatever `client` was resolved to (a pre-configured http client, or one created
12
+ * through service discovery) still backs every method this class doesn't
13
+ * override, so pointing service discovery at a different registry or a real
14
+ * local mock server keeps working unchanged.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * builder.setClient(async ({ requireInstance }) => {
19
+ * const http = await requireInstance('http');
20
+ * return new MockAppClient(http.createClient('apps'), manifest, config);
21
+ * });
22
+ * ```
23
+ */
24
+ export declare class MockAppClient extends AppClient {
25
+ #private;
26
+ /**
27
+ * @param client - The real {@link IHttpClient} to delegate all other requests to.
28
+ * @param manifest - The manifest to return for `getAppManifest({ appKey: manifest.appKey })`.
29
+ * @param config - The config to return for `getAppConfig({ appKey: manifest.appKey })`, if any.
30
+ */
31
+ constructor(client: IHttpClient, manifest: AppManifest, config?: AppConfig);
32
+ /**
33
+ * Answers with the manifest passed to the constructor when `args` matches
34
+ * this client's own app key and no tag; otherwise delegates to the real client.
35
+ *
36
+ * @param args - The app key and optional tag to resolve a manifest for.
37
+ * @returns An observable of the resolved {@link AppManifest}.
38
+ */
39
+ getAppManifest(args: {
40
+ appKey: string;
41
+ tag?: string;
42
+ }): Observable<AppManifest>;
43
+ /**
44
+ * Answers with the config passed to the constructor when `args` matches this
45
+ * client's own app key and tag; otherwise delegates to the real client.
46
+ *
47
+ * @template TType - The shape of the config's `environment` data.
48
+ * @param args - The app key and optional tag to resolve config for.
49
+ * @returns An observable of the resolved {@link AppConfig}.
50
+ */
51
+ getAppConfig<TType extends ConfigEnvironment = ConfigEnvironment>(args: {
52
+ appKey: string;
53
+ tag?: string;
54
+ }): Observable<AppConfig<TType>>;
55
+ }
56
+ export default MockAppClient;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Test doubles for the app module.
3
+ *
4
+ * @remarks
5
+ * Imported from `@equinor/fusion-framework-module-app/mock`, so the mock ships
6
+ * and versions with the implementation it stands in for.
7
+ *
8
+ * This entry point has no dependency on any test runner.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+ export { MockAppClient } from './MockAppClient.js';
@@ -1 +1 @@
1
- export declare const version = "8.0.5";
1
+ export declare const version = "8.0.6-next.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@equinor/fusion-framework-module-app",
3
- "version": "8.0.5",
3
+ "version": "8.0.6-next.0",
4
4
  "description": "",
5
5
  "main": "dist/esm/index.js",
6
6
  "exports": {
@@ -22,6 +22,10 @@
22
22
  },
23
23
  "./app/*.js": {
24
24
  "import": "./dist/esm/app/*.js"
25
+ },
26
+ "./mock": {
27
+ "import": "./dist/esm/mock/index.js",
28
+ "types": "./dist/types/mock/index.d.ts"
25
29
  }
26
30
  },
27
31
  "types": "dist/types/index.d.ts",
@@ -41,6 +45,9 @@
41
45
  ],
42
46
  "app/*.js": [
43
47
  "dist/types/app/*.d.ts"
48
+ ],
49
+ "mock": [
50
+ "dist/types/mock/index.d.ts"
44
51
  ]
45
52
  }
46
53
  },
@@ -61,18 +68,21 @@
61
68
  "rxjs": "^7.8.1",
62
69
  "uuid": "^14.0.0",
63
70
  "zod": "^4.4.3",
64
- "@equinor/fusion-observable": "^9.1.1",
65
- "@equinor/fusion-query": "^7.0.3"
71
+ "@equinor/fusion-observable": "^9.1.2-next.1",
72
+ "@equinor/fusion-query": "^7.0.4-next.0"
66
73
  },
67
74
  "devDependencies": {
68
75
  "typescript": "^7.0.2",
69
- "@equinor/fusion-framework-module": "^6.1.2",
70
- "@equinor/fusion-framework-module-event": "^6.0.1",
71
- "@equinor/fusion-framework-module-http": "^8.0.5",
72
- "@equinor/fusion-framework-module-msal": "^10.0.2",
73
- "@equinor/fusion-framework-module-service-discovery": "^10.0.2"
76
+ "vitest": "^4.1.10",
77
+ "@equinor/fusion-framework": "^8.1.0-next.1",
78
+ "@equinor/fusion-framework-module": "^6.1.3-next.0",
79
+ "@equinor/fusion-framework-module-http": "^8.1.0-next.0",
80
+ "@equinor/fusion-framework-module-service-discovery": "^10.1.0-next.0",
81
+ "@equinor/fusion-framework-module-event": "^6.1.0-next.0",
82
+ "@equinor/fusion-framework-module-msal": "^11.0.0-next.0"
74
83
  },
75
84
  "scripts": {
76
- "build": "tsc -b"
85
+ "build": "tsc -b",
86
+ "test": "vitest run"
77
87
  }
78
88
  }
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { mockFramework } from '@equinor/fusion-framework/mock';
4
+ import { createRouterMiddleware } from '@equinor/fusion-framework-module-http/mock';
5
+
6
+ import { enableAppModule } from '../enable-app-module';
7
+ import type { AppModule } from '../module';
8
+
9
+ /**
10
+ * Boots a full portal-shaped framework instance the same way `packages/dev-portal`'s
11
+ * `configure.ts` does — `enableAppModule` registered directly on the portal's own
12
+ * configurator, alongside its real `http`, `event`, `msal`, etc. — rather than a
13
+ * standalone module graph assembled just for this test. Only the network call
14
+ * itself is faked; the app module creates its client eagerly during its own
15
+ * `requireInstance('http')` in the SAME configure→initialize pipeline, so this also
16
+ * verifies middleware registered before `initialize()` reaches that client.
17
+ */
18
+ const initializePortalWith = () =>
19
+ mockFramework<[AppModule]>((configurator) => {
20
+ configurator.http.configureClient('apps', { baseUri: 'https://apps.example.com' });
21
+ configurator.http.addMiddleware(
22
+ createRouterMiddleware('https://apps.example.com', (router) => {
23
+ router.get('/persons/me/apps/:appKey', ({ params }) =>
24
+ Response.json({
25
+ appKey: params.appKey,
26
+ displayName: 'Test App',
27
+ description: 'A test application',
28
+ type: 'standalone',
29
+ }),
30
+ );
31
+ }),
32
+ );
33
+ enableAppModule(configurator);
34
+ });
35
+
36
+ describe('AppModuleProvider', () => {
37
+ it("fetches the manifest through the portal's own http client once a current app is set", async () => {
38
+ const fusion = await initializePortalWith();
39
+
40
+ fusion.modules.app.setCurrentApp('test-app');
41
+
42
+ await expect(fusion.modules.app.current?.getManifestAsync()).resolves.toMatchObject({
43
+ appKey: 'test-app',
44
+ displayName: 'Test App',
45
+ });
46
+ });
47
+ });
@@ -0,0 +1,97 @@
1
+ import { firstValueFrom, of } from 'rxjs';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import type { IHttpClient } from '@equinor/fusion-framework-module-http';
5
+
6
+ import { AppClient } from '../AppClient.js';
7
+ import { AppConfig } from '../AppConfig.js';
8
+ import { MockAppClient } from '../mock/MockAppClient.js';
9
+ import type { AppManifest } from '../types.js';
10
+
11
+ const manifest: AppManifest = {
12
+ appKey: 'my-app',
13
+ displayName: 'My App',
14
+ description: 'My app',
15
+ type: 'standalone',
16
+ build: { version: '1.2.3', entryPoint: 'index.js' },
17
+ };
18
+
19
+ const config = new AppConfig({ environment: { key: 'value' } });
20
+
21
+ // never invoked directly -- both overrides either answer locally or delegate through
22
+ // the spied `AppClient.prototype` methods below, so the real client is never called
23
+ const client = {} as IHttpClient;
24
+
25
+ describe('MockAppClient', () => {
26
+ describe('getAppManifest', () => {
27
+ it('answers locally for its own app key with no tag', async () => {
28
+ const mockClient = new MockAppClient(client, manifest);
29
+
30
+ const result = mockClient.getAppManifest({ appKey: 'my-app' });
31
+
32
+ await expect(firstValueFrom(result)).resolves.toBe(manifest);
33
+ });
34
+
35
+ it('delegates for its own app key with an explicit empty-string tag', () => {
36
+ const delegate = vi
37
+ .spyOn(AppClient.prototype, 'getAppManifest')
38
+ .mockReturnValue(of(manifest));
39
+ const mockClient = new MockAppClient(client, manifest);
40
+
41
+ mockClient.getAppManifest({ appKey: 'my-app', tag: '' });
42
+
43
+ expect(delegate).toHaveBeenCalledWith({ appKey: 'my-app', tag: '' });
44
+ delegate.mockRestore();
45
+ });
46
+
47
+ it('delegates for a different app key', () => {
48
+ const delegate = vi
49
+ .spyOn(AppClient.prototype, 'getAppManifest')
50
+ .mockReturnValue(of(manifest));
51
+ const mockClient = new MockAppClient(client, manifest);
52
+
53
+ mockClient.getAppManifest({ appKey: 'other-app' });
54
+
55
+ expect(delegate).toHaveBeenCalledWith({ appKey: 'other-app' });
56
+ delegate.mockRestore();
57
+ });
58
+ });
59
+
60
+ describe('getAppConfig', () => {
61
+ it('answers locally for its own app key with no tag', async () => {
62
+ const mockClient = new MockAppClient(client, manifest, config);
63
+
64
+ const result = mockClient.getAppConfig({ appKey: 'my-app' });
65
+
66
+ await expect(firstValueFrom(result)).resolves.toBe(config);
67
+ });
68
+
69
+ it("answers locally for its own app key with a tag matching the manifest's build version", async () => {
70
+ const mockClient = new MockAppClient(client, manifest, config);
71
+
72
+ const result = mockClient.getAppConfig({ appKey: 'my-app', tag: '1.2.3' });
73
+
74
+ await expect(firstValueFrom(result)).resolves.toBe(config);
75
+ });
76
+
77
+ it('delegates for its own app key with an explicit empty-string tag', () => {
78
+ const delegate = vi.spyOn(AppClient.prototype, 'getAppConfig').mockReturnValue(of(config));
79
+ const mockClient = new MockAppClient(client, manifest, config);
80
+
81
+ mockClient.getAppConfig({ appKey: 'my-app', tag: '' });
82
+
83
+ expect(delegate).toHaveBeenCalledWith({ appKey: 'my-app', tag: '' });
84
+ delegate.mockRestore();
85
+ });
86
+
87
+ it('delegates for its own app key with a tag not matching the manifest build version', () => {
88
+ const delegate = vi.spyOn(AppClient.prototype, 'getAppConfig').mockReturnValue(of(config));
89
+ const mockClient = new MockAppClient(client, manifest, config);
90
+
91
+ mockClient.getAppConfig({ appKey: 'my-app', tag: '9.9.9' });
92
+
93
+ expect(delegate).toHaveBeenCalledWith({ appKey: 'my-app', tag: '9.9.9' });
94
+ delegate.mockRestore();
95
+ });
96
+ });
97
+ });
package/src/index.ts CHANGED
@@ -26,6 +26,8 @@ export {
26
26
 
27
27
  export { AppClient, type IAppClient } from './AppClient';
28
28
 
29
+ export { AppConfig } from './AppConfig';
30
+
29
31
  export { AppModuleProvider } from './AppModuleProvider';
30
32
 
31
33
  export { IApp } from './app/App';
@@ -0,0 +1,77 @@
1
+ import type { Observable } from 'rxjs';
2
+ import { of } from 'rxjs';
3
+
4
+ import type { IHttpClient } from '@equinor/fusion-framework-module-http';
5
+
6
+ import { AppClient } from '../AppClient.js';
7
+ import type { AppConfig, AppManifest, ConfigEnvironment } from '../types.js';
8
+
9
+ /**
10
+ * An {@link AppClient} that answers `getAppManifest` and `getAppConfig` for one
11
+ * known app locally, delegating everything else — other app keys, tagged
12
+ * requests, builds, settings — to the real client it wraps.
13
+ *
14
+ * @remarks
15
+ * Whatever `client` was resolved to (a pre-configured http client, or one created
16
+ * through service discovery) still backs every method this class doesn't
17
+ * override, so pointing service discovery at a different registry or a real
18
+ * local mock server keeps working unchanged.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * builder.setClient(async ({ requireInstance }) => {
23
+ * const http = await requireInstance('http');
24
+ * return new MockAppClient(http.createClient('apps'), manifest, config);
25
+ * });
26
+ * ```
27
+ */
28
+ export class MockAppClient extends AppClient {
29
+ #manifest: AppManifest;
30
+ #config?: AppConfig;
31
+
32
+ /**
33
+ * @param client - The real {@link IHttpClient} to delegate all other requests to.
34
+ * @param manifest - The manifest to return for `getAppManifest({ appKey: manifest.appKey })`.
35
+ * @param config - The config to return for `getAppConfig({ appKey: manifest.appKey })`, if any.
36
+ */
37
+ constructor(client: IHttpClient, manifest: AppManifest, config?: AppConfig) {
38
+ super(client);
39
+ this.#manifest = manifest;
40
+ this.#config = config;
41
+ }
42
+
43
+ /**
44
+ * Answers with the manifest passed to the constructor when `args` matches
45
+ * this client's own app key and no tag; otherwise delegates to the real client.
46
+ *
47
+ * @param args - The app key and optional tag to resolve a manifest for.
48
+ * @returns An observable of the resolved {@link AppManifest}.
49
+ */
50
+ override getAppManifest(args: { appKey: string; tag?: string }): Observable<AppManifest> {
51
+ return args.appKey === this.#manifest.appKey && args.tag === undefined
52
+ ? of(this.#manifest)
53
+ : super.getAppManifest(args);
54
+ }
55
+
56
+ /**
57
+ * Answers with the config passed to the constructor when `args` matches this
58
+ * client's own app key and tag; otherwise delegates to the real client.
59
+ *
60
+ * @template TType - The shape of the config's `environment` data.
61
+ * @param args - The app key and optional tag to resolve config for.
62
+ * @returns An observable of the resolved {@link AppConfig}.
63
+ */
64
+ override getAppConfig<TType extends ConfigEnvironment = ConfigEnvironment>(args: {
65
+ appKey: string;
66
+ tag?: string;
67
+ }): Observable<AppConfig<TType>> {
68
+ // config is fetched against the manifest's own build version, not an explicit override tag,
69
+ // so a matching tag is treated the same as an absent one -- an explicit empty string is not
70
+ const isOwnTag = args.tag === undefined || args.tag === this.#manifest.build?.version;
71
+ return args.appKey === this.#manifest.appKey && isOwnTag && this.#config
72
+ ? of(this.#config as AppConfig<TType>)
73
+ : super.getAppConfig(args);
74
+ }
75
+ }
76
+
77
+ export default MockAppClient;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Test doubles for the app module.
3
+ *
4
+ * @remarks
5
+ * Imported from `@equinor/fusion-framework-module-app/mock`, so the mock ships
6
+ * and versions with the implementation it stands in for.
7
+ *
8
+ * This entry point has no dependency on any test runner.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+
13
+ export { MockAppClient } from './MockAppClient.js';
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '8.0.5';
2
+ export const version = '8.0.6-next.0';
package/tsconfig.json CHANGED
@@ -9,6 +9,9 @@
9
9
  {
10
10
  "path": "../../utils/query"
11
11
  },
12
+ {
13
+ "path": "../../framework"
14
+ },
12
15
  {
13
16
  "path": "../module"
14
17
  },
@@ -0,0 +1,11 @@
1
+ import { defineProject } from 'vitest/config';
2
+
3
+ import { name, version } from './package.json' with { type: 'json' };
4
+
5
+ export default defineProject({
6
+ test: {
7
+ environment: 'node',
8
+ include: ['src/__tests__/**/*.test.ts'],
9
+ name: `${name}@${version}`,
10
+ },
11
+ });