diff --git a/packages/frontend-defaults/src/resolveFeatures.test.ts b/packages/frontend-defaults/src/resolveFeatures.test.ts new file mode 100644 index 0000000000..b91e6710b6 --- /dev/null +++ b/packages/frontend-defaults/src/resolveFeatures.test.ts @@ -0,0 +1,140 @@ +/* + * 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 { + createFrontendPlugin, + PageBlueprint, +} from '@backstage/frontend-plugin-api'; +import { CreateAppFeatureLoader } from './createApp'; +import { resolveFeatures } from './resolveFeatures'; +import { mockApis } from '@backstage/test-utils'; + +describe('resolveFeatures', () => { + it('returns empty array when no features are provided', async () => { + const features = await resolveFeatures({ + config: mockApis.config(), + }); + + expect(features).toEqual([]); + }); + + it('returns expected array when features are directly provided', async () => { + const features = await resolveFeatures({ + config: mockApis.config(), + features: [ + createFrontendPlugin({ + id: 'test-feature', + extensions: [ + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: () => new Promise(() => {}), + }, + }), + ], + }), + ], + }); + + expect(features).toMatchObject([ + { + $$type: '@backstage/FrontendPlugin', + id: 'test-feature', + version: 'v1', + extensions: [ + { + $$type: '@backstage/Extension', + id: 'page:test-feature', + version: 'v2', + attachTo: { + id: 'app/routes', + input: 'routes', + }, + }, + ], + }, + ]); + }); + + it('supports feature loaders', async () => { + const loader: CreateAppFeatureLoader = { + getLoaderName() { + return 'test-loader'; + }, + async load() { + return { + features: [ + createFrontendPlugin({ + id: 'test', + extensions: [ + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: () => new Promise(() => {}), + }, + }), + ], + }), + ], + }; + }, + }; + + const features = await resolveFeatures({ + config: mockApis.config(), + features: [loader], + }); + + expect(features).toMatchObject([ + { + $$type: '@backstage/FrontendPlugin', + id: 'test', + version: 'v1', + extensions: [ + { + $$type: '@backstage/Extension', + id: 'page:test', + version: 'v2', + attachTo: { + id: 'app/routes', + input: 'routes', + }, + }, + ], + }, + ]); + }); + + it('should propagate errors thrown by feature loaders', async () => { + const loader: CreateAppFeatureLoader = { + getLoaderName() { + return 'test-loader'; + }, + async load() { + throw new TypeError('boom'); + }, + }; + + await expect(() => + resolveFeatures({ + config: mockApis.config(), + features: [loader], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to read frontend features from loader 'test-loader', TypeError: boom"`, + ); + }); +});