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;
}