diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx index 76196f45cd..cd1356d4cb 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx @@ -1292,357 +1292,6 @@ describe('createSpecializedApp', () => { ); }); - it('should synchronously finalize feature flag predicates without sign-in', async () => { - const featureFlagsApi = { - isActive: jest.fn((name: string) => name === 'test-flag'), - registerFlag: jest.fn(), - getRegisteredFlags: () => [], - save: jest.fn(), - } as unknown as typeof featureFlagsApiRef.T; - const noSignInAppPlugin = appPluginOriginal.withOverrides({ - extensions: [ - appPluginOriginal - .getExtension('sign-in-page:app') - .override({ disabled: true }), - ], - }); - const preparedApp = prepareSpecializedApp({ - features: [ - noSignInAppPlugin, - createFrontendPlugin({ - pluginId: 'test', - featureFlags: [{ name: 'test-flag' }], - extensions: [ - createExtension({ - name: 'bootstrap-element', - attachTo: { id: 'app/root', input: 'elements' }, - output: [coreExtensionData.reactElement], - factory: () => [ - coreExtensionData.reactElement(
Bootstrap Element
), - ], - }), - createExtension({ - name: 'deferred-element', - attachTo: { id: 'app/root', input: 'elements' }, - if: { featureFlags: { $contains: 'test-flag' } }, - output: [coreExtensionData.reactElement], - factory: () => [ - coreExtensionData.reactElement(
Deferred Element
), - ], - }), - ], - }), - createFrontendModule({ - pluginId: 'app', - extensions: [ - ApiBlueprint.make({ - params: defineParams => - defineParams({ - api: featureFlagsApiRef, - deps: {}, - factory: () => featureFlagsApi, - }), - }), - ], - }), - ], - }); - - renderPreparedBootstrap(preparedApp); - - expect(screen.getByText('Bootstrap Element')).toBeInTheDocument(); - expect(screen.queryByText('Deferred Element')).not.toBeInTheDocument(); - - const finalizedApp = preparedApp.finalize(); - render( - finalizedApp.tree.root.instance!.getData( - coreExtensionData.reactElement, - ), - ); - - expect(featureFlagsApi.isActive).toHaveBeenCalledWith('test-flag'); - await expect( - screen.findByText('Deferred Element'), - ).resolves.toBeInTheDocument(); - }); - - it('should defer conditional api roots without resetting visible api instances', () => { - const featureFlagsApi = { - isActive: jest.fn((name: string) => name === 'test-flag'), - registerFlag: jest.fn(), - getRegisteredFlags: () => [], - save: jest.fn(), - } as unknown as typeof featureFlagsApiRef.T; - const visibleApiExtension = ApiBlueprint.make({ - name: 'visible-api', - params: defineParams => - defineParams({ - api: createApiRef<{ value: string }>({ id: 'test.visible-api' }), - deps: {}, - factory: () => ({ value: 'visible' }), - }), - }); - const deferredApiExtension = ApiBlueprint.make({ - name: 'deferred-api', - params: defineParams => - defineParams({ - api: createApiRef<{ value: string }>({ id: 'test.deferred-api' }), - deps: {}, - factory: () => ({ value: 'deferred' }), - }), - }).override({ - if: { featureFlags: { $contains: 'test-flag' } }, - }); - const preparedApp = prepareSpecializedApp({ - features: [ - makeAppPlugin(), - createFrontendPlugin({ - pluginId: 'test', - featureFlags: [{ name: 'test-flag' }], - extensions: [ - visibleApiExtension, - deferredApiExtension, - ApiBlueprint.make({ - name: 'feature-flags', - params: defineParams => - defineParams({ - api: featureFlagsApiRef, - deps: {}, - factory: () => featureFlagsApi, - }), - }), - ], - }), - ], - }); - - const bootstrapTree = preparedApp.getBootstrapApp().tree; - const apiNodes = bootstrapTree.root.edges.attachments.get('apis') ?? []; - const visibleApiNode = apiNodes.find( - node => node.spec.id === 'api:test/visible-api', - ); - const deferredApiNode = apiNodes.find( - node => node.spec.id === 'api:test/deferred-api', - ); - - expect(visibleApiNode?.instance).toBeDefined(); - expect(deferredApiNode?.instance).toBeUndefined(); - - const visibleApiInstance = visibleApiNode?.instance; - preparedApp.finalize(); - - expect(visibleApiNode?.instance).toBe(visibleApiInstance); - expect(deferredApiNode?.instance).toBeDefined(); - }); - - it('should ignore deferred overrides of materialized bootstrap APIs', () => { - const apiRef = createApiRef<{ value: string }>({ - id: 'test.bootstrap-frozen-api', - }); - let bootstrapApiValue: string | undefined; - let finalApiValue: string | undefined; - const featureFlagsApi = { - isActive: jest.fn((name: string) => name === 'test-flag'), - registerFlag: jest.fn(), - getRegisteredFlags: () => [], - save: jest.fn(), - } as unknown as typeof featureFlagsApiRef.T; - const noSignInAppPlugin = appPluginOriginal.withOverrides({ - extensions: [ - appPluginOriginal - .getExtension('sign-in-page:app') - .override({ disabled: true }), - ], - }); - const preparedApp = prepareSpecializedApp({ - features: [ - noSignInAppPlugin, - createFrontendPlugin({ - pluginId: 'test', - featureFlags: [{ name: 'test-flag' }], - extensions: [ - ApiBlueprint.make({ - name: 'bootstrap-api', - params: defineParams => - defineParams({ - api: apiRef, - deps: {}, - factory: () => ({ value: 'bootstrap' }), - }), - }), - ApiBlueprint.make({ - name: 'deferred-api', - params: defineParams => - defineParams({ - api: apiRef, - deps: {}, - factory: () => ({ value: 'final' }), - }), - }).override({ - if: { featureFlags: { $contains: 'test-flag' } }, - }), - createExtension({ - name: 'bootstrap-reader', - attachTo: { id: 'app/root', input: 'elements' }, - output: [coreExtensionData.reactElement], - factory: ({ apis }) => { - bootstrapApiValue = apis.get(apiRef)?.value; - return [ - coreExtensionData.reactElement(
Bootstrap Reader
), - ]; - }, - }), - createExtension({ - name: 'final-reader', - attachTo: { id: 'app/root', input: 'elements' }, - if: { featureFlags: { $contains: 'test-flag' } }, - output: [coreExtensionData.reactElement], - factory: ({ apis }) => { - finalApiValue = apis.get(apiRef)?.value; - return [ - coreExtensionData.reactElement(
Final Reader
), - ]; - }, - }), - ], - }), - createFrontendModule({ - pluginId: 'app', - extensions: [ - ApiBlueprint.make({ - params: defineParams => - defineParams({ - api: featureFlagsApiRef, - deps: {}, - factory: () => featureFlagsApi, - }), - }), - ], - }), - ], - }); - - renderPreparedBootstrap(preparedApp); - expect(bootstrapApiValue).toBe('bootstrap'); - - const finalizedApp = preparedApp.finalize(); - - expect(featureFlagsApi.isActive).toHaveBeenCalledWith('test-flag'); - expect(finalApiValue).toBe('bootstrap'); - expect(finalizedApp.errors).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - code: 'EXTENSION_BOOTSTRAP_API_OVERRIDE_IGNORED', - context: expect.objectContaining({ - apiRefId: apiRef.id, - }), - }), - ]), - ); - }); - - it('should allow deferred overrides of bootstrap APIs that were not materialized', () => { - const apiRef = createApiRef<{ value: string }>({ - id: 'test.bootstrap-overridable-api', - }); - let finalApiValue: string | undefined; - const featureFlagsApi = { - isActive: jest.fn((name: string) => name === 'test-flag'), - registerFlag: jest.fn(), - getRegisteredFlags: () => [], - save: jest.fn(), - } as unknown as typeof featureFlagsApiRef.T; - const noSignInAppPlugin = appPluginOriginal.withOverrides({ - extensions: [ - appPluginOriginal - .getExtension('sign-in-page:app') - .override({ disabled: true }), - ], - }); - const preparedApp = prepareSpecializedApp({ - features: [ - noSignInAppPlugin, - createFrontendPlugin({ - pluginId: 'test', - featureFlags: [{ name: 'test-flag' }], - extensions: [ - ApiBlueprint.make({ - name: 'bootstrap-api', - params: defineParams => - defineParams({ - api: apiRef, - deps: {}, - factory: () => ({ value: 'bootstrap' }), - }), - }), - ApiBlueprint.make({ - name: 'deferred-api', - params: defineParams => - defineParams({ - api: apiRef, - deps: {}, - factory: () => ({ value: 'final' }), - }), - }).override({ - if: { featureFlags: { $contains: 'test-flag' } }, - }), - createExtension({ - name: 'bootstrap-element', - attachTo: { id: 'app/root', input: 'elements' }, - output: [coreExtensionData.reactElement], - factory: () => [ - coreExtensionData.reactElement(
Bootstrap Element
), - ], - }), - createExtension({ - name: 'final-reader', - attachTo: { id: 'app/root', input: 'elements' }, - if: { featureFlags: { $contains: 'test-flag' } }, - output: [coreExtensionData.reactElement], - factory: ({ apis }) => { - finalApiValue = apis.get(apiRef)?.value; - return [ - coreExtensionData.reactElement(
Final Reader
), - ]; - }, - }), - ], - }), - createFrontendModule({ - pluginId: 'app', - extensions: [ - ApiBlueprint.make({ - params: defineParams => - defineParams({ - api: featureFlagsApiRef, - deps: {}, - factory: () => featureFlagsApi, - }), - }), - ], - }), - ], - }); - - renderPreparedBootstrap(preparedApp); - expect(screen.getByText('Bootstrap Element')).toBeInTheDocument(); - - const finalizedApp = preparedApp.finalize(); - - expect(featureFlagsApi.isActive).toHaveBeenCalledWith('test-flag'); - expect(finalApiValue).toBe('final'); - expect(finalizedApp.errors ?? []).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ - code: 'EXTENSION_BOOTSTRAP_API_OVERRIDE_IGNORED', - context: expect.objectContaining({ - apiRefId: apiRef.id, - }), - }), - ]), - ); - }); - it('should defer app root children until finalize', async () => { const identityApi = { getProfileInfo: async () => ({ displayName: 'Test User' }), diff --git a/packages/frontend-defaults/src/createApp.test.tsx b/packages/frontend-defaults/src/createApp.test.tsx index be09c8b8df..4cc9e367f8 100644 --- a/packages/frontend-defaults/src/createApp.test.tsx +++ b/packages/frontend-defaults/src/createApp.test.tsx @@ -32,7 +32,7 @@ import { FrontendPluginInfo, } from '@backstage/frontend-plugin-api'; import { ThemeBlueprint } from '@backstage/plugin-app-react'; -import { screen, waitFor } from '@testing-library/react'; +import { act, screen, waitFor } from '@testing-library/react'; import { createApp } from './createApp'; import { mockApis, renderWithEffects } from '@backstage/test-utils'; import { @@ -229,6 +229,7 @@ describe('createApp', () => { getRegisteredFlags: () => [], save: jest.fn(), } as unknown as typeof featureFlagsApiRef.T; + let onSignInSuccess: ((identity: IdentityApi) => void) | undefined; const app = createApp({ advanced: { @@ -252,9 +253,7 @@ describe('createApp', () => { function SignInPage(props: { onSignInSuccess(identity: IdentityApi): void; }) { - useEffect(() => { - props.onSignInSuccess(identityApi); - }, [props]); + onSignInSuccess = props.onSignInSuccess; return
Custom Sign In
; } @@ -281,6 +280,16 @@ describe('createApp', () => { }); await renderWithEffects(app.createRoot()); + await expect( + screen.findByText('Custom Sign In'), + ).resolves.toBeInTheDocument(); + if (!onSignInSuccess) { + throw new Error('Expected sign-in success callback to be captured'); + } + const triggerSignInSuccess = onSignInSuccess; + act(() => { + triggerSignInSuccess(identityApi); + }); await expect( screen.findByText(/Error in app/), diff --git a/packages/frontend-defaults/src/createApp.tsx b/packages/frontend-defaults/src/createApp.tsx index 728ec29e42..2dd75031c2 100644 --- a/packages/frontend-defaults/src/createApp.tsx +++ b/packages/frontend-defaults/src/createApp.tsx @@ -152,12 +152,22 @@ function PreparedAppRoot(props: { const [finalizedApp, setFinalizedApp] = useState< FinalizedSpecializedApp | undefined >(); + const [bootstrapError, setBootstrapError] = useState(); useEffect( - () => props.preparedApp.onFinalized(setFinalizedApp), + () => props.preparedApp.onFinalized(setFinalizedApp, setBootstrapError), [props.preparedApp], ); + if (bootstrapError) { + return ( + <> +
{bootstrapError.message}
+

Error in app

+ + ); + } + if (!finalizedApp) { return bootstrapApp.element; }