From c89cb147663dd39ac070654be1b9f9d7a60eed9e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 10 Mar 2025 14:45:13 +0100 Subject: [PATCH] core-compat-api: tests and fixes for entity page conversion Signed-off-by: Patrik Oldsberg --- .../src/collectEntityPageContents.test.tsx | 171 ++++++++++++++++++ .../src/collectEntityPageContents.ts | 6 +- .../src/collectLegacyRoutes.test.tsx | 4 +- .../src/collectLegacyRoutes.tsx | 2 +- .../src/convertLegacyApp.test.tsx | 159 ++++++++++++++++ .../core-compat-api/src/convertLegacyApp.ts | 2 +- 6 files changed, 337 insertions(+), 7 deletions(-) create mode 100644 packages/core-compat-api/src/collectEntityPageContents.test.tsx diff --git a/packages/core-compat-api/src/collectEntityPageContents.test.tsx b/packages/core-compat-api/src/collectEntityPageContents.test.tsx new file mode 100644 index 0000000000..53aa90f247 --- /dev/null +++ b/packages/core-compat-api/src/collectEntityPageContents.test.tsx @@ -0,0 +1,171 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ExtensionAttachToSpec } from '@backstage/frontend-plugin-api'; +import { EntityLayout, EntitySwitch, isKind } from '@backstage/plugin-catalog'; +import React from 'react'; +import { collectEntityPageContents } from './collectEntityPageContents'; +import { + createComponentExtension, + createPlugin, +} from '@backstage/core-plugin-api'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { + resolveExtensionDefinition, + toInternalExtension, +} from '../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; + +const fooPlugin = createPlugin({ + id: 'foo', +}); + +const FooContent = fooPlugin.provide( + createComponentExtension({ + name: 'FooContent', + component: { sync: () =>
foo content
}, + }), +); +const OtherFooContent = fooPlugin.provide( + createComponentExtension({ + name: 'OtherFooContent', + component: { sync: () =>
other foo content
}, + }), +); + +const simpleTestContent = ( + + +
overview content
+
+ + + + +
bar content
+
+
+); + +const otherTestContent = ( + + +
other overview content
+
+ + + +
+); + +function collect(element: React.JSX.Element) { + const result = new Array<{ + id: string; + attachTo: ExtensionAttachToSpec; + }>(); + + collectEntityPageContents(element, { + discoverExtension(extension, plugin) { + const ext = toInternalExtension( + resolveExtensionDefinition(extension, { + namespace: plugin?.getId() ?? 'test', + }), + ); + result.push({ id: ext.id, attachTo: ext.attachTo }); + }, + }); + return result; +} + +describe('collectEntityPageContents', () => { + it('should collect contents from a simple entity page', () => { + expect(collect(simpleTestContent)).toMatchInlineSnapshot(` + [ + { + "attachTo": { + "id": "entity-content:catalog/overview", + "input": "cards", + }, + "id": "entity-card:test/discovered-1", + }, + { + "attachTo": { + "id": "page:catalog/entity", + "input": "contents", + }, + "id": "entity-content:foo/discovered-1", + }, + { + "attachTo": { + "id": "page:catalog/entity", + "input": "contents", + }, + "id": "entity-content:test/discovered-2", + }, + ] + `); + }); + + it('should collect contents from an entity page with an entity switch', () => { + expect( + collect( + + + {simpleTestContent} + + {otherTestContent} + , + ), + ).toMatchInlineSnapshot(` + [ + { + "attachTo": { + "id": "entity-content:catalog/overview", + "input": "cards", + }, + "id": "entity-card:test/discovered-1", + }, + { + "attachTo": { + "id": "page:catalog/entity", + "input": "contents", + }, + "id": "entity-content:foo/discovered-1", + }, + { + "attachTo": { + "id": "page:catalog/entity", + "input": "contents", + }, + "id": "entity-content:test/discovered-2", + }, + { + "attachTo": { + "id": "entity-content:catalog/overview", + "input": "cards", + }, + "id": "entity-card:test/discovered-2", + }, + { + "attachTo": { + "id": "page:catalog/entity", + "input": "contents", + }, + "id": "entity-content:foo/discovered-3", + }, + ] + `); + }); +}); diff --git a/packages/core-compat-api/src/collectEntityPageContents.ts b/packages/core-compat-api/src/collectEntityPageContents.ts index c2a3ba2145..7c26cd29ef 100644 --- a/packages/core-compat-api/src/collectEntityPageContents.ts +++ b/packages/core-compat-api/src/collectEntityPageContents.ts @@ -74,7 +74,7 @@ function invertFilter(ifFunc?: EntityFilter): EntityFilter { export function collectEntityPageContents( entityPageElement: React.JSX.Element, context: { - discoveryExtension( + discoverExtension( extension: ExtensionDefinition, plugin?: LegacyBackstagePlugin, ): void; @@ -94,7 +94,7 @@ export function collectEntityPageContents( const mergedIf = allFilters(parentFilter, pageNode.if); if (pageNode.path === '/') { - context.discoveryExtension( + context.discoverExtension( EntityCardBlueprint.makeWithOverrides({ name: `discovered-${cardCounter++}`, factory(originalFactory, { apis }) { @@ -109,7 +109,7 @@ export function collectEntityPageContents( } else { const name = `discovered-${routeCounter++}`; - context.discoveryExtension( + context.discoverExtension( EntityContentBlueprint.makeWithOverrides({ name, factory(originalFactory, { apis }) { diff --git a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx index ce0b055c00..2d64f88535 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx @@ -57,7 +57,7 @@ describe('collectLegacyRoutes', () => { expect( collected.map(p => ({ - id: p.id, + id: p.$$type === '@backstage/FrontendPlugin' ? p.id : p.pluginId, extensions: OpaqueFrontendPlugin.toInternal(p).extensions.map(e => ({ id: e.id, attachTo: e.attachTo, @@ -177,7 +177,7 @@ describe('collectLegacyRoutes', () => { expect( collected.map(p => ({ - id: p.id, + id: p.$$type === '@backstage/FrontendPlugin' ? p.id : p.pluginId, extensions: OpaqueFrontendPlugin.toInternal(p).extensions.map(e => ({ id: e.id, attachTo: e.attachTo, diff --git a/packages/core-compat-api/src/collectLegacyRoutes.tsx b/packages/core-compat-api/src/collectLegacyRoutes.tsx index 3cf16b96c0..f6408b8b3f 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.tsx @@ -270,7 +270,7 @@ export function collectLegacyRoutes( if (entityPage) { collectEntityPageContents(entityPage, { - discoveryExtension(extension, plugin) { + discoverExtension(extension, plugin) { if (!plugin || plugin.getId() === 'catalog') { getPluginExtensions(orphanRoutesPlugin).push(extension); } else { diff --git a/packages/core-compat-api/src/convertLegacyApp.test.tsx b/packages/core-compat-api/src/convertLegacyApp.test.tsx index 7153fcb4be..c532c6da1a 100644 --- a/packages/core-compat-api/src/convertLegacyApp.test.tsx +++ b/packages/core-compat-api/src/convertLegacyApp.test.tsx @@ -21,6 +21,16 @@ import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card'; import React, { ReactNode } from 'react'; import { Route } from 'react-router-dom'; import { convertLegacyApp } from './convertLegacyApp'; +import { + createApiFactory, + createComponentExtension, + createPlugin, +} from '@backstage/core-plugin-api'; +import { EntityLayout, EntitySwitch, isKind } from '@backstage/plugin-catalog'; +import { renderInTestApp } from '@backstage/frontend-test-utils'; +import { default as catalogPlugin } from '@backstage/plugin-catalog/alpha'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; const Root = ({ children }: { children: ReactNode }) => <>{children}; @@ -159,4 +169,153 @@ describe('convertLegacyApp', () => { }), ]); }); + + it('should convert entity pages', async () => { + const fooPlugin = createPlugin({ + id: 'foo', + }); + + const FooContent = fooPlugin.provide( + createComponentExtension({ + name: 'FooContent', + component: { sync: () =>
foo content
}, + }), + ); + const OtherFooContent = fooPlugin.provide( + createComponentExtension({ + name: 'OtherFooContent', + component: { sync: () =>
other foo content
}, + }), + ); + + const simpleTestContent = ( + + +
overview content
+
+ + + + +
bar content
+
+
+ ); + + const otherTestContent = ( + + +
other overview content
+
+ + + +
+ ); + + const entityPage = ( + + + {simpleTestContent} + + {otherTestContent} + + ); + + const converted = convertLegacyApp( + + test} /> + , + { entityPage }, + ); + + const catalogOverride = catalogPlugin.withOverrides({ + extensions: [ + catalogPlugin.getExtension('api:catalog').override({ + params: { + factory: createApiFactory( + catalogApiRef, + catalogApiMock({ + entities: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'test', + metadata: { + name: 'x', + }, + spec: {}, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'other', + metadata: { + name: 'x', + }, + spec: {}, + }, + ], + }), + ), + }, + }), + ], + }); + + // Overview + const renderOverviewTest = await renderInTestApp(
, { + features: [catalogOverride, ...converted], + initialRouteEntries: ['/catalog/default/test/x'], + }); + await expect( + renderOverviewTest.findByText('overview content'), + ).resolves.toBeInTheDocument(); + renderOverviewTest.unmount(); + + const renderOverviewOther = await renderInTestApp(
, { + features: [catalogOverride, ...converted], + initialRouteEntries: ['/catalog/default/other/x'], + }); + await expect( + renderOverviewOther.findByText('other overview content'), + ).resolves.toBeInTheDocument(); + renderOverviewOther.unmount(); + + // Foo tab + const renderFooTest = await renderInTestApp(
, { + features: [catalogOverride, ...converted], + initialRouteEntries: ['/catalog/default/test/x/foo'], + }); + await expect( + renderFooTest.findByText('foo content'), + ).resolves.toBeInTheDocument(); + renderFooTest.unmount(); + + const renderFooOther = await renderInTestApp(
, { + features: [catalogOverride, ...converted], + initialRouteEntries: ['/catalog/default/other/x/foo'], + }); + await expect( + renderFooOther.findByText('other foo content'), + ).resolves.toBeInTheDocument(); + renderFooOther.unmount(); + + // Bar tab + const renderBarTest = await renderInTestApp(
, { + features: [catalogOverride, ...converted], + initialRouteEntries: ['/catalog/default/test/x/bar'], + }); + await expect( + renderBarTest.findByText('bar content'), + ).resolves.toBeInTheDocument(); + renderBarTest.unmount(); + + const renderBarOther = await renderInTestApp(
, { + features: [catalogOverride, ...converted], + initialRouteEntries: ['/catalog/default/other/x/bar'], + }); + await expect( + renderBarOther.findByText('other overview content'), + ).resolves.toBeInTheDocument(); // /bar does not exist, fall back to rendering overview + renderBarOther.unmount(); + }); }); diff --git a/packages/core-compat-api/src/convertLegacyApp.ts b/packages/core-compat-api/src/convertLegacyApp.ts index eb72cda204..dac9849aac 100644 --- a/packages/core-compat-api/src/convertLegacyApp.ts +++ b/packages/core-compat-api/src/convertLegacyApp.ts @@ -86,7 +86,7 @@ export function convertLegacyApp( options: ConvertLegacyAppOptions = {}, ): (FrontendPlugin | FrontendModule)[] { if (getComponentData(rootElement, 'core.type') === 'FlatRoutes') { - return collectLegacyRoutes(rootElement); + return collectLegacyRoutes(rootElement, options?.entityPage); } const appRouterEls = selectChildren(