From cfd94f29edc9aeb0178e1c23c3e3df03616b2386 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 20 Sep 2023 15:11:05 +0200 Subject: [PATCH] test(frontend-app-api): cover more levels of cyclical dependencies Signed-off-by: Camila Belo --- .../src/wiring/createApp.test.tsx | 176 +++++++++++++++++- 1 file changed, 175 insertions(+), 1 deletion(-) diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index 339948eec1..dcbd996f15 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -14,10 +14,18 @@ * limitations under the License. */ -import { createExtension, createPlugin } from '@backstage/frontend-plugin-api'; +import { + createExtension, + createExtensionDataRef, + createExtensionInput, + createPageExtension, + createPlugin, +} from '@backstage/frontend-plugin-api'; import { createInstances } from './createApp'; import { MockConfigApi } from '@backstage/test-utils'; +import React from 'react'; +import { createRouteRef } from '@backstage/core-plugin-api'; describe('createInstances', () => { it('throws an error when a root extension is parametrized', () => { @@ -63,4 +71,170 @@ describe('createInstances', () => { 'The following plugins are overriding root extensions and root extensions cannot be overridden: plugin', ); }); + + describe('throws an error when immediate cyclical dependencies are found', () => { + it('in an immediate level (e.g., A(plugin.page) -> A(plugin.page))', () => { + const config = new MockConfigApi({}); + + const addonExtensionData = + createExtensionDataRef('plugin.page.addon'); + + const addon = createExtension({ + id: 'plugin.page', // cyclical + at: 'plugin.page/addons', + inputs: {}, + output: { + element: addonExtensionData, + }, + factory({ bind }) { + bind({ + element:
Addon
, + }); + }, + }); + + const page = createPageExtension({ + id: 'plugin.page', + defaultPath: '/', + routeRef: createRouteRef({ id: 'plugins.page.addon' }), + inputs: { + addons: createExtensionInput({ + element: addonExtensionData, + }), + }, + loader: async ({ inputs }) => ( +
Page {inputs.addons.map(({ element }) => element)}
+ ), + }); + + const plugins = [ + createPlugin({ + id: 'plugin', + extensions: [page, addon], + }), + ]; + + expect(() => createInstances({ config, plugins })).toThrow( + 'There is a cyclical dependency with the extension "plugin.page": core.layout → core.routes → plugin.page → plugin.page', + ); + }); + + it('in an intermediate level (e.g., A(core.routes) -> B(plugin.page) -> A(core.routes))', () => { + const config = new MockConfigApi({}); + + const addonExtensionData = + createExtensionDataRef('plugin.page.addon'); + + const addon = createExtension({ + id: 'core.routes', // cyclical + at: 'plugin.page/addons', + inputs: {}, + output: { + element: addonExtensionData, + }, + factory({ bind }) { + bind({ + element:
Addon
, + }); + }, + }); + + const page = createPageExtension({ + id: 'plugin.page', + defaultPath: '/', + routeRef: createRouteRef({ id: 'plugins.page.addon' }), + inputs: { + addons: createExtensionInput({ + element: addonExtensionData, + }), + }, + loader: async ({ inputs }) => ( +
Page {inputs.addons.map(({ element }) => element)}
+ ), + }); + + const plugins = [ + createPlugin({ + id: 'plugin', + extensions: [page, addon], + }), + ]; + + expect(() => createInstances({ config, plugins })).toThrow( + 'There is a cyclical dependency with the extension "core.routes": core.layout → core.routes → plugin.page → core.routes', + ); + }); + + it('in an deep level (e.g., A(core.layout) -> B(core.routes) -> C(plugin.page) -> D(plugin.page.addon) -> B(core.routes))', () => { + const config = new MockConfigApi({}); + + const addonRendererInput = createExtensionDataRef<() => JSX.Element>( + 'plugin.page.addon.renderer', + ); + + const renderer = createExtension({ + id: 'core.routes', // cyclical + at: 'plugin.page.addon/renderer', + inputs: {}, + output: { + renderer: addonRendererInput, + }, + factory({ bind }) { + bind({ + renderer: () =>
Addon
, + }); + }, + }); + + const addonElementInput = createExtensionDataRef( + 'plugin.page.addon.element', + ); + + const addon = createExtension({ + id: 'plugin.page.addon', + at: 'plugin.page/addons', + inputs: { + renderer: createExtensionInput( + { + element: addonRendererInput, + }, + { singleton: true, required: false }, + ), + }, + output: { + element: addonElementInput, + }, + factory({ bind, inputs }) { + bind({ + element: inputs.renderer?.element() ??
Addon
, + }); + }, + }); + + const page = createPageExtension({ + id: 'plugin.page', + defaultPath: '/', + routeRef: createRouteRef({ id: 'plugins.page.addon' }), + inputs: { + addons: createExtensionInput({ + element: addonElementInput, + }), + }, + loader: async ({ inputs }) => ( +
Page {inputs.addons.map(({ element }) => element)}
+ ), + }); + + const plugins = [ + createPlugin({ + id: 'plugin', + extensions: [page, addon, renderer], + }), + ]; + + expect(() => createInstances({ config, plugins })).toThrow( + 'There is a cyclical dependency with the extension "core.routes": core.layout → core.routes → plugin.page → plugin.page.addon → core.routes', + ); + }); + }); });