From 37ff85614238bf4e695ab642f5f69c197c5ccd1f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 15 Mar 2026 01:08:30 +0100 Subject: [PATCH] frontend-defaults: cover conditional page usage patterns Add createApp coverage for the new page-level and page-child predicate patterns so the example app is backed by implementation-adjacent tests. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../frontend-defaults/src/createApp.test.tsx | 459 ++++++++++++++++++ 1 file changed, 459 insertions(+) diff --git a/packages/frontend-defaults/src/createApp.test.tsx b/packages/frontend-defaults/src/createApp.test.tsx index 9b3733b5af..c4a4861e1b 100644 --- a/packages/frontend-defaults/src/createApp.test.tsx +++ b/packages/frontend-defaults/src/createApp.test.tsx @@ -22,6 +22,8 @@ import { createApiRef, createExtensionDataRef, createExtension, + createExtensionBlueprint, + createExtensionInput, PageBlueprint, createFrontendPlugin, createFrontendFeatureLoader, @@ -40,6 +42,8 @@ import { } from '@backstage/core-plugin-api'; import { default as appPluginOriginal } from '@backstage/plugin-app'; import { ComponentType, useState, useEffect } from 'react'; +import { permissionApiRef } from '@backstage/plugin-permission-react'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; const signInPageComponentDataRef = createExtensionDataRef< ComponentType<{ onSignInSuccess(identity: IdentityApi): void }> @@ -54,6 +58,25 @@ describe('createApp', () => { ], }); + function createFeatureFlagsApi(activeFlags: string[]) { + return { + isActive: jest.fn((name: string) => activeFlags.includes(name)), + registerFlag: jest.fn(), + getRegisteredFlags: () => [], + save: jest.fn(), + } as unknown as typeof featureFlagsApiRef.T; + } + + function createPermissionApi(allowedPermissions: string[]) { + return { + authorize: jest.fn(async request => ({ + result: allowedPermissions.includes(request.permission.name) + ? AuthorizeResult.ALLOW + : AuthorizeResult.DENY, + })), + } as typeof permissionApiRef.T; + } + it('should allow themes to be installed', async () => { const app = createApp({ advanced: { @@ -516,6 +539,442 @@ describe('createApp', () => { expect(featureFlagsApi.isActive).toHaveBeenCalledWith('test-flag'); }); + it('should support $all feature flag predicates on pages', async () => { + const partialFlagsApi = createFeatureFlagsApi(['experimental-features']); + const partialFlagsApp = createApp({ + advanced: { + configLoader: async () => ({ config: mockApis.config() }), + }, + features: [ + appPlugin, + createFrontendModule({ + pluginId: 'app', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: featureFlagsApiRef, + deps: {}, + factory: () => partialFlagsApi, + }), + }), + ], + }), + createFrontendPlugin({ + pluginId: 'test', + featureFlags: [ + { name: 'experimental-features' }, + { name: 'advanced-features' }, + ], + extensions: [ + PageBlueprint.make({ + if: { + $all: [ + { featureFlags: { $contains: 'experimental-features' } }, + { featureFlags: { $contains: 'advanced-features' } }, + ], + }, + params: { + path: '/', + loader: async () =>
All Flags Page
, + }, + }), + ], + }), + ], + }); + + const partialRender = await renderWithEffects(partialFlagsApp.createRoot()); + await waitFor(() => + expect(screen.queryByText('All Flags Page')).not.toBeInTheDocument(), + ); + partialRender.unmount(); + + const allFlagsApi = createFeatureFlagsApi([ + 'experimental-features', + 'advanced-features', + ]); + const allFlagsApp = createApp({ + advanced: { + configLoader: async () => ({ config: mockApis.config() }), + }, + features: [ + appPlugin, + createFrontendModule({ + pluginId: 'app', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: featureFlagsApiRef, + deps: {}, + factory: () => allFlagsApi, + }), + }), + ], + }), + createFrontendPlugin({ + pluginId: 'test', + featureFlags: [ + { name: 'experimental-features' }, + { name: 'advanced-features' }, + ], + extensions: [ + PageBlueprint.make({ + if: { + $all: [ + { featureFlags: { $contains: 'experimental-features' } }, + { featureFlags: { $contains: 'advanced-features' } }, + ], + }, + params: { + path: '/', + loader: async () =>
All Flags Page
, + }, + }), + ], + }), + ], + }); + + await renderWithEffects(allFlagsApp.createRoot()); + await expect( + screen.findByText('All Flags Page'), + ).resolves.toBeInTheDocument(); + expect(allFlagsApi.isActive).toHaveBeenCalledWith('experimental-features'); + expect(allFlagsApi.isActive).toHaveBeenCalledWith('advanced-features'); + }); + + it('should support $any feature flag predicates on pages', async () => { + const noFlagsApi = createFeatureFlagsApi([]); + const noFlagsApp = createApp({ + advanced: { + configLoader: async () => ({ config: mockApis.config() }), + }, + features: [ + appPlugin, + createFrontendModule({ + pluginId: 'app', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: featureFlagsApiRef, + deps: {}, + factory: () => noFlagsApi, + }), + }), + ], + }), + createFrontendPlugin({ + pluginId: 'test', + featureFlags: [ + { name: 'experimental-features' }, + { name: 'beta-access' }, + ], + extensions: [ + PageBlueprint.make({ + if: { + $any: [ + { featureFlags: { $contains: 'experimental-features' } }, + { featureFlags: { $contains: 'beta-access' } }, + ], + }, + params: { + path: '/', + loader: async () =>
Any Flag Page
, + }, + }), + ], + }), + ], + }); + + const noFlagsRender = await renderWithEffects(noFlagsApp.createRoot()); + await waitFor(() => + expect(screen.queryByText('Any Flag Page')).not.toBeInTheDocument(), + ); + noFlagsRender.unmount(); + + const oneFlagApi = createFeatureFlagsApi(['beta-access']); + const oneFlagApp = createApp({ + advanced: { + configLoader: async () => ({ config: mockApis.config() }), + }, + features: [ + appPlugin, + createFrontendModule({ + pluginId: 'app', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: featureFlagsApiRef, + deps: {}, + factory: () => oneFlagApi, + }), + }), + ], + }), + createFrontendPlugin({ + pluginId: 'test', + featureFlags: [ + { name: 'experimental-features' }, + { name: 'beta-access' }, + ], + extensions: [ + PageBlueprint.make({ + if: { + $any: [ + { featureFlags: { $contains: 'experimental-features' } }, + { featureFlags: { $contains: 'beta-access' } }, + ], + }, + params: { + path: '/', + loader: async () =>
Any Flag Page
, + }, + }), + ], + }), + ], + }); + + await renderWithEffects(oneFlagApp.createRoot()); + await expect( + screen.findByText('Any Flag Page'), + ).resolves.toBeInTheDocument(); + expect(oneFlagApi.isActive).toHaveBeenCalledWith('experimental-features'); + expect(oneFlagApi.isActive).toHaveBeenCalledWith('beta-access'); + }); + + it('should support permission predicates on pages', async () => { + const deniedPermissionApi = createPermissionApi([]); + const deniedApp = createApp({ + advanced: { + configLoader: async () => ({ config: mockApis.config() }), + }, + features: [ + appPlugin, + createFrontendModule({ + pluginId: 'app', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: permissionApiRef, + deps: {}, + factory: () => deniedPermissionApi, + }), + }), + ], + }), + createFrontendPlugin({ + pluginId: 'test', + extensions: [ + PageBlueprint.make({ + if: { permissions: { $contains: 'catalog.entity.create' } }, + params: { + path: '/', + loader: async () =>
Permission Page
, + }, + }), + ], + }), + ], + }); + + const deniedRender = await renderWithEffects(deniedApp.createRoot()); + await waitFor(() => + expect(screen.queryByText('Permission Page')).not.toBeInTheDocument(), + ); + deniedRender.unmount(); + + const allowedPermissionApi = createPermissionApi(['catalog.entity.create']); + const allowedApp = createApp({ + advanced: { + configLoader: async () => ({ config: mockApis.config() }), + }, + features: [ + appPlugin, + createFrontendModule({ + pluginId: 'app', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: permissionApiRef, + deps: {}, + factory: () => allowedPermissionApi, + }), + }), + ], + }), + createFrontendPlugin({ + pluginId: 'test', + extensions: [ + PageBlueprint.make({ + if: { permissions: { $contains: 'catalog.entity.create' } }, + params: { + path: '/', + loader: async () =>
Permission Page
, + }, + }), + ], + }), + ], + }); + + await renderWithEffects(allowedApp.createRoot()); + await expect( + screen.findByText('Permission Page'), + ).resolves.toBeInTheDocument(); + expect(allowedPermissionApi.authorize).toHaveBeenCalledWith({ + permission: { name: 'catalog.entity.create', type: 'basic', attributes: {} }, + }); + }); + + it('should support conditional child extensions attached to pages', async () => { + const CardBlueprint = createExtensionBlueprint({ + kind: 'card', + attachTo: { id: 'page:test/card-page', input: 'cards' }, + output: [coreExtensionData.reactElement], + *factory(params: { title: string }) { + yield coreExtensionData.reactElement(
{params.title}
); + }, + }); + + const page = PageBlueprint.makeWithOverrides({ + name: 'card-page', + inputs: { + cards: createExtensionInput([coreExtensionData.reactElement], { + optional: false, + singleton: false, + }), + }, + factory(originalFactory, { inputs }) { + return originalFactory({ + path: '/', + loader: async () => ( +
+ {inputs.cards.map(card => card.get(coreExtensionData.reactElement))} +
+ ), + }); + }, + }); + + const publicCard = CardBlueprint.make({ + name: 'public', + params: { title: 'Public Card' }, + }); + const permissionCard = CardBlueprint.make({ + name: 'permission', + params: { title: 'Permission Card' }, + if: { permissions: { $contains: 'catalog.entity.create' } }, + }); + const featureFlagCard = CardBlueprint.make({ + name: 'feature-flag', + params: { title: 'Feature Flag Card' }, + if: { featureFlags: { $contains: 'experimental-card' } }, + }); + + const hiddenCardsApp = createApp({ + advanced: { + configLoader: async () => ({ config: mockApis.config() }), + }, + features: [ + appPlugin, + createFrontendModule({ + pluginId: 'app', + extensions: [ + ApiBlueprint.make({ + name: 'permission-api', + params: defineParams => + defineParams({ + api: permissionApiRef, + deps: {}, + factory: () => createPermissionApi([]), + }), + }), + ApiBlueprint.make({ + name: 'feature-flags-api', + params: defineParams => + defineParams({ + api: featureFlagsApiRef, + deps: {}, + factory: () => createFeatureFlagsApi([]), + }), + }), + ], + }), + createFrontendPlugin({ + pluginId: 'test', + featureFlags: [{ name: 'experimental-card' }], + extensions: [page, publicCard, permissionCard, featureFlagCard], + }), + ], + }); + + const hiddenCardsRender = await renderWithEffects(hiddenCardsApp.createRoot()); + await expect( + screen.findByText('Public Card'), + ).resolves.toBeInTheDocument(); + await waitFor(() => + expect(screen.queryByText('Permission Card')).not.toBeInTheDocument(), + ); + await waitFor(() => + expect(screen.queryByText('Feature Flag Card')).not.toBeInTheDocument(), + ); + hiddenCardsRender.unmount(); + + const visibleCardsApp = createApp({ + advanced: { + configLoader: async () => ({ config: mockApis.config() }), + }, + features: [ + appPlugin, + createFrontendModule({ + pluginId: 'app', + extensions: [ + ApiBlueprint.make({ + name: 'permission-api', + params: defineParams => + defineParams({ + api: permissionApiRef, + deps: {}, + factory: () => + createPermissionApi(['catalog.entity.create']), + }), + }), + ApiBlueprint.make({ + name: 'feature-flags-api', + params: defineParams => + defineParams({ + api: featureFlagsApiRef, + deps: {}, + factory: () => createFeatureFlagsApi(['experimental-card']), + }), + }), + ], + }), + createFrontendPlugin({ + pluginId: 'test', + featureFlags: [{ name: 'experimental-card' }], + extensions: [page, publicCard, permissionCard, featureFlagCard], + }), + ], + }); + + await renderWithEffects(visibleCardsApp.createRoot()); + await expect( + screen.findByText('Permission Card'), + ).resolves.toBeInTheDocument(); + await expect( + screen.findByText('Feature Flag Card'), + ).resolves.toBeInTheDocument(); + }); + it('should make the app structure available through the AppTreeApi', async () => { let appTreeApi: AppTreeApi | undefined = undefined;