diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index f24a9e17f3..a000e016be 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -925,6 +925,24 @@ describe('instantiateAppNodeTree', () => { }); }); + it('should create an extension passed through an extension factory middleware', () => { + const attachments = new Map(); + const instance = createAppNodeInstance({ + node: makeNode(simpleExtension), + attachments, + apis: testApis, + *extensionFactoryMiddleware(originalFactory) { + const output = originalFactory({ + config: { output: 'modified' }, + }); + yield* output; + }, + }); + + expect(Array.from(instance.getDataRefs())).toEqual([testDataRef]); + expect(instance.getData(testDataRef)).toEqual('modified'); + }); + it('should refuse to create an extension with invalid config', () => { expect(() => createAppNodeInstance({ diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx index a1399d786a..e269961d52 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx @@ -29,7 +29,7 @@ import { } from '@backstage/frontend-plugin-api'; import { screen, render } from '@testing-library/react'; import { createSpecializedApp } from './createSpecializedApp'; -import { mockApis } from '@backstage/test-utils'; +import { mockApis, TestApiRegistry } from '@backstage/test-utils'; import React from 'react'; import { configApiRef, @@ -37,7 +37,7 @@ import { featureFlagsApiRef, } from '@backstage/core-plugin-api'; import { MemoryRouter } from 'react-router-dom'; -import { ApiProvider } from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; describe('createSpecializedApp', () => { it('should render the root app', () => { @@ -166,6 +166,81 @@ describe('createSpecializedApp', () => { render(app.createRoot()); expect(screen.getByText('flags:test=a,test=b')).toBeInTheDocument(); + + expect(app.apis).toMatchInlineSnapshot(` + ApiResolver { + "apis": Map { + "core.featureflags" => { + "getRegisteredFlags": [Function], + "registerFlag": [Function], + }, + }, + "factories": ApiFactoryRegistry { + "factories": Map { + "core.featureflags" => { + "factory": { + "api": ApiRefImpl { + "config": { + "id": "core.featureflags", + }, + }, + "deps": {}, + "factory": [Function], + }, + "priority": 10, + }, + "core.app-tree" => { + "factory": { + "api": ApiRefImpl { + "config": { + "id": "core.app-tree", + }, + }, + "deps": {}, + "factory": [Function], + }, + "priority": 100, + }, + "core.config" => { + "factory": { + "api": ApiRefImpl { + "config": { + "id": "core.config", + }, + }, + "deps": {}, + "factory": [Function], + }, + "priority": 100, + }, + "core.route-resolution" => { + "factory": { + "api": ApiRefImpl { + "config": { + "id": "core.route-resolution", + }, + }, + "deps": {}, + "factory": [Function], + }, + "priority": 100, + }, + "core.identity" => { + "factory": { + "api": ApiRefImpl { + "config": { + "id": "core.identity", + }, + }, + "deps": {}, + "factory": [Function], + }, + "priority": 100, + }, + }, + }, + } + `); }); it('should intitialize the APIs in the correct order to allow for overrides', () => { @@ -237,10 +312,59 @@ describe('createSpecializedApp', () => { expect(mockAnalyticsApi).toHaveBeenCalled(); }); + it('should use provided apis', async () => { + const app = createSpecializedApp({ + apis: TestApiRegistry.from([ + configApiRef, + new ConfigReader({ anything: 'config' }), + ]), + features: [ + createFrontendPlugin({ + id: 'test', + extensions: [ + createExtension({ + attachTo: { id: 'root', input: 'app' }, + output: [coreExtensionData.reactElement], + factory: ({ apis }) => [ + coreExtensionData.reactElement( +
+ providedApis: + {apis.get(configApiRef)!.getString('anything')} +
, + ), + ], + }), + ], + }), + ], + }); + + render(app.createRoot()); + + expect(screen.getByText('providedApis:config')).toBeInTheDocument(); + + expect(app.apis).toMatchInlineSnapshot(` + TestApiRegistry { + "apis": Map { + "core.config" => ConfigReader { + "context": "mock-config", + "data": { + "anything": "config", + }, + "fallback": undefined, + "filteredKeys": undefined, + "notifiedFilteredKeys": Set {}, + "prefix": "", + }, + }, + } + `); + }); + it('should make the app structure available through the AppTreeApi', async () => { let appTreeApi: AppTreeApi | undefined = undefined; - createSpecializedApp({ + const { tree } = createSpecializedApp({ features: [ createFrontendPlugin({ id: 'test', @@ -265,6 +389,55 @@ describe('createSpecializedApp', () => { ] " `); + + expect(tree).toMatchInlineSnapshot(` + { + "nodes": Map { + "test" => { + "attachments": undefined, + "id": "test", + "output": [ + "core.reactElement", + ], + }, + "root" => { + "attachments": { + "app": [ + { + "attachments": undefined, + "id": "test", + "output": [ + "core.reactElement", + ], + }, + ], + }, + "id": "root", + "output": [ + "core.reactElement", + ], + }, + }, + "orphans": [], + "root": { + "attachments": { + "app": [ + { + "attachments": undefined, + "id": "test", + "output": [ + "core.reactElement", + ], + }, + ], + }, + "id": "root", + "output": [ + "core.reactElement", + ], + }, + } + `); }); it('should support route bindings', async () => { diff --git a/packages/frontend-defaults/src/createApp.test.tsx b/packages/frontend-defaults/src/createApp.test.tsx index 899d5d1640..c11bb138e0 100644 --- a/packages/frontend-defaults/src/createApp.test.tsx +++ b/packages/frontend-defaults/src/createApp.test.tsx @@ -395,6 +395,46 @@ describe('createApp', () => { expect(screen.queryByText('Custom app root element')).toBeNull(); }); + it('should use a custom extensionFactoryMiddleware', async () => { + const app = createApp({ + configLoader: async () => ({ config: mockApis.config() }), + features: [ + appPlugin, + createFrontendPlugin({ + id: 'test-plugin', + extensions: [ + PageBlueprint.make({ + name: 'test-page', + params: { + defaultPath: '/', + loader: async () => <>Test Page, + }, + }), + ], + }), + ], + *extensionFactoryMiddleware(originalFactory, context) { + const output = originalFactory(); + yield* output; + const element = output.get(coreExtensionData.reactElement); + + if (element) { + yield coreExtensionData.reactElement( +
+ {element} +
, + ); + } + }, + }); + + await renderWithEffects(app.createRoot()); + + await expect( + screen.findByTestId('wrapped(page:test-plugin/test-page)'), + ).resolves.toHaveTextContent('Test Page'); + }); + describe('modules', () => { it('should be able to override extensions with a plugin extension override', async () => { const mod = createFrontendModule({