frontend-app-api: integrate predicate context into app phases
Compute and cache extension predicate context as part of app phase transitions so sign-in and non-sign-in flows finalize against the same gating state. Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -1731,6 +1731,86 @@ describe('createSpecializedApp', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should reuse predicate context gathered during sign-in completion', async () => {
|
||||
const identityApi = {
|
||||
getProfileInfo: async () => ({ displayName: 'Test User' }),
|
||||
getBackstageIdentity: async () => ({
|
||||
type: 'user' as const,
|
||||
userEntityRef: 'user:default/test-user',
|
||||
ownershipEntityRefs: ['user:default/test-user'],
|
||||
}),
|
||||
getCredentials: async () => ({ token: 'token' }),
|
||||
signOut: 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 gatedAppPlugin = appPluginOriginal.withOverrides({
|
||||
extensions: [
|
||||
appPluginOriginal.getExtension('app/layout').override({
|
||||
if: { featureFlags: { $contains: 'test-flag' } },
|
||||
factory: () => [
|
||||
coreExtensionData.reactElement(<div>Flagged Layout</div>),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const preparedApp = prepareSpecializedApp({
|
||||
features: [
|
||||
gatedAppPlugin,
|
||||
createFrontendModule({
|
||||
pluginId: 'app',
|
||||
extensions: [
|
||||
ApiBlueprint.make({
|
||||
params: defineParams =>
|
||||
defineParams({
|
||||
api: featureFlagsApiRef,
|
||||
deps: {},
|
||||
factory: () => featureFlagsApi,
|
||||
}),
|
||||
}),
|
||||
gatedAppPlugin.getExtension('sign-in-page:app').override({
|
||||
factory: () => {
|
||||
function SignInPage(props: {
|
||||
onSignInSuccess(identity: IdentityApi): void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
props.onSignInSuccess(identityApi);
|
||||
}, [props]);
|
||||
return <div>Custom Sign In</div>;
|
||||
}
|
||||
|
||||
return [signInPageComponentDataRef(SignInPage)];
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const signIn = preparedApp.getSignIn();
|
||||
render(signIn!.element);
|
||||
await expect(
|
||||
screen.findByText('Custom Sign In'),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
await signIn!.complete;
|
||||
expect(featureFlagsApi.isActive).toHaveBeenCalledWith('test-flag');
|
||||
|
||||
const finalizedApp = preparedApp.finalize();
|
||||
render(
|
||||
finalizedApp.tree.root.instance!.getData(
|
||||
coreExtensionData.reactElement,
|
||||
),
|
||||
);
|
||||
|
||||
expect(screen.getByText('Flagged Layout')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should gate finalize behind internal async sign-in finalization', async () => {
|
||||
const identityApi = {
|
||||
getProfileInfo: async () => ({ displayName: 'Test User' }),
|
||||
|
||||
@@ -386,6 +386,56 @@ describe('createApp', () => {
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should evaluate extension if predicates before rendering apps 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 app = createApp({
|
||||
advanced: {
|
||||
configLoader: async () => ({ config: mockApis.config() }),
|
||||
},
|
||||
features: [
|
||||
appPlugin,
|
||||
createFrontendModule({
|
||||
pluginId: 'app',
|
||||
extensions: [
|
||||
ApiBlueprint.make({
|
||||
params: defineParams =>
|
||||
defineParams({
|
||||
api: featureFlagsApiRef,
|
||||
deps: {},
|
||||
factory: () => featureFlagsApi,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
createFrontendPlugin({
|
||||
pluginId: 'test',
|
||||
featureFlags: [{ name: 'test-flag' }],
|
||||
extensions: [
|
||||
PageBlueprint.make({
|
||||
if: { featureFlags: { $contains: 'test-flag' } },
|
||||
params: {
|
||||
path: '/',
|
||||
loader: async () => <div>Flagged Page</div>,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
await renderWithEffects(app.createRoot());
|
||||
|
||||
await expect(
|
||||
screen.findByText('Flagged Page'),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(featureFlagsApi.isActive).toHaveBeenCalledWith('test-flag');
|
||||
});
|
||||
|
||||
it('should make the app structure available through the AppTreeApi', async () => {
|
||||
let appTreeApi: AppTreeApi | undefined = undefined;
|
||||
|
||||
|
||||
@@ -133,6 +133,8 @@ export function createApp(options?: CreateAppOptions): {
|
||||
};
|
||||
}
|
||||
|
||||
await preparedApp.buildPredicateContext();
|
||||
|
||||
return {
|
||||
default: () => renderFinalizedApp(preparedApp.finalize()),
|
||||
};
|
||||
@@ -164,12 +166,15 @@ function PreparedAppRoot(props: {
|
||||
let cancelled = false;
|
||||
const runFinalize = async () => {
|
||||
try {
|
||||
const predicateContext =
|
||||
if (signIn) {
|
||||
await signIn.complete;
|
||||
} else {
|
||||
await props.preparedApp.buildPredicateContext();
|
||||
}
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setFinalizedApp(props.preparedApp.finalize(predicateContext));
|
||||
setFinalizedApp(props.preparedApp.finalize());
|
||||
} catch (error) {
|
||||
if (cancelled) {
|
||||
return;
|
||||
@@ -177,23 +182,7 @@ function PreparedAppRoot(props: {
|
||||
setFinalizeError(error as Error);
|
||||
}
|
||||
};
|
||||
if (signIn) {
|
||||
void signIn.complete
|
||||
.then(() => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
void runFinalize();
|
||||
})
|
||||
.catch(error => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setFinalizeError(error);
|
||||
});
|
||||
} else {
|
||||
setFinalizedApp(props.preparedApp.finalize());
|
||||
}
|
||||
void runFinalize();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user