diff --git a/.changeset/breezy-guests-scream.md b/.changeset/breezy-guests-scream.md new file mode 100644 index 0000000000..e33344f992 --- /dev/null +++ b/.changeset/breezy-guests-scream.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +The extension tester will no longer unconditionally enable any additional extensions that have been added. diff --git a/.changeset/three-socks-call.md b/.changeset/three-socks-call.md new file mode 100644 index 0000000000..39b75f2fea --- /dev/null +++ b/.changeset/three-socks-call.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +The `factory` option is no longer required when overriding an extension. diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 75d91d28f0..1b89aeac74 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -1323,7 +1323,7 @@ export type ExtensionDefinition< string}' is already defined in parent schema`; }; }; - factory( + factory?( originalFactory: (context?: { config?: T['config']; inputs?: ResolveInputValueOverrides>; diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 5e8aa1c57a..b5d846871f 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -567,7 +567,7 @@ describe('createExtension', () => { }, }); - // @ts-expect-error - this should fail because string output should be merged? + // @ts-expect-error const override2 = testExtension.override({ output: [numberDataRef], factory(_, { inputs }) { @@ -681,6 +681,69 @@ describe('createExtension', () => { ).toBe('foo-hello-override-world'); }); + it('should be able to disable extension with override', () => { + const subject = createExtension({ + name: 'root', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + input: createExtensionInput([stringDataRef], { + singleton: true, + optional: true, + }), + }, + output: [stringDataRef.optional()], + factory({ inputs }) { + return inputs.input ?? []; + }, + }); + + const attached = createExtension({ + attachTo: { id: 'root', input: 'input' }, + output: [stringDataRef], + factory() { + return [stringDataRef('test')]; + }, + }); + + expect( + createExtensionTester(subject).add(attached).get(stringDataRef), + ).toBe('test'); + + expect( + createExtensionTester(subject) + .add(attached.override({ disabled: true })) + .get(stringDataRef), + ).toBe(undefined); + }); + + it('should complain when overriding with incompatible output', () => { + const testExtension = createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'blob' }, + output: [stringDataRef], + factory() { + return [stringDataRef('0')]; + }, + }); + + // @ts-expect-error - override output is incompatible with factory + const override = testExtension.override({ + output: [numberDataRef], + factory() { + return [stringDataRef('1')]; + }, + }); + expect(override).toBeDefined(); + + expect(() => + testExtension.override({ + output: [numberDataRef], + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Refused to override output without also overriding factory"`, + ); + }); + it('should be able to override input values', () => { const outputRef = createExtensionDataRef().with({ id: 'output', diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 8c79758395..25615098ef 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -191,7 +191,7 @@ export type ExtensionDefinition< string}' is already defined in parent schema`; }; }; - factory( + factory?( originalFactory: (context?: { config?: T['config']; inputs?: ResolveInputValueOverrides>; @@ -518,6 +518,15 @@ export function createExtension< UFactoryOutput >; + // TODO(Rugvip): Making this a type check would be optimal, but it seems + // like it's tricky to add that and still have the type + // inference work correctly for the factory output. + if (overrideOptions.output && !overrideOptions.factory) { + throw new Error( + 'Refused to override output without also overriding factory', + ); + } + return createExtension({ kind: newOptions.kind, namespace: newOptions.namespace, diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index f9c1b3cb89..232b41b435 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -213,11 +213,17 @@ export class ExtensionTester { const [subject, ...rest] = this.#extensions; const extensionsConfig: JsonArray = [ - ...rest.map(extension => ({ - [extension.id]: { - config: extension.config, - }, - })), + ...rest.flatMap(extension => + extension.config + ? [ + { + [extension.id]: { + config: extension.config, + }, + }, + ] + : [], + ), { [subject.id]: { config: subject.config,