added tests to createPlugin, plus minor refactors

Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com>
Co-authored-by: Johan Haals <johan.haals@gmail.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2023-08-30 14:57:38 +02:00
parent 65c7c64666
commit 9758240395
14 changed files with 394 additions and 45 deletions
@@ -29,5 +29,5 @@ export const GraphiqlPage = createPageExtension({
export const graphiqlPlugin = createPlugin({
id: 'graphiql',
defaultExtensions: [GraphiqlPage],
extensions: [GraphiqlPage],
});
+3 -3
View File
@@ -43,7 +43,7 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
// apply config to adjust default extension instances and add more
const extensionParams = mergeExtensionParameters(
[
...options.plugins.flatMap(plugin => plugin.defaultExtensions),
...options.plugins.flatMap(plugin => plugin.extensions),
...builtinExtensions,
],
readAppExtensionParameters(appConfig),
@@ -116,8 +116,8 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
);
return (
<>
{rootComponents.map(Component => (
<Component />
{rootComponents.map((Component, i) => (
<Component key={i} />
))}
</>
);
+1 -1
View File
@@ -15,7 +15,7 @@
*/
/**
* TODO
* Core API used by Backstage frontend apps.
*
* @packageDocumentation
*/
+10 -10
View File
@@ -19,15 +19,7 @@ export interface BackstagePlugin {
// (undocumented)
$$type: 'backstage-plugin';
// (undocumented)
defaultExtensions: Extension<unknown>[];
// (undocumented)
id: string;
}
// @public (undocumented)
export interface BackstagePluginOptions {
// (undocumented)
defaultExtensions?: Extension<unknown>[];
extensions: Extension<unknown>[];
// (undocumented)
id: string;
}
@@ -121,7 +113,7 @@ export function createPageExtension<
): Extension<TConfig>;
// @public (undocumented)
export function createPlugin(options: BackstagePluginOptions): BackstagePlugin;
export function createPlugin(options: PluginOptions): BackstagePlugin;
// @public (undocumented)
export function createSchemaFromZod<TOutput, TInput>(
@@ -174,6 +166,14 @@ export type ExtensionDataValue<TData extends AnyExtensionDataMap> = {
[K in keyof TData]: TData[K]['T'];
};
// @public (undocumented)
export interface PluginOptions {
// (undocumented)
extensions?: Extension<unknown>[];
// (undocumented)
id: string;
}
// @public (undocumented)
export type PortableSchema<TOutput> = {
parse: (input: unknown) => TOutput;
+3 -1
View File
@@ -24,7 +24,9 @@
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@testing-library/jest-dom": "^5.10.1"
"@backstage/frontend-app-api": "workspace:^",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3"
},
"files": [
"dist"
@@ -0,0 +1,100 @@
/*
* Copyright 2023 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 React from 'react';
import { PortableSchema } from '../createSchemaFromZod';
import { coreExtensionData } from '../types';
import { createPageExtension } from './createPageExtension';
describe('createPageExtension', () => {
it('creates the extension properly', () => {
const configSchema: PortableSchema<{ path: string }> = {
parse: jest.fn(),
schema: {} as any,
};
expect(
createPageExtension({
id: 'test',
configSchema,
component: async () => <div />,
}),
).toEqual({
$$type: 'extension',
id: 'test',
at: 'core.router/routes',
configSchema: expect.anything(),
disabled: false,
inputs: {},
output: {
component: expect.anything(),
path: expect.anything(),
},
factory: expect.any(Function),
});
expect(
createPageExtension({
id: 'test',
at: 'other/place',
disabled: true,
configSchema,
inputs: {
first: {
extensionData: { component: coreExtensionData.reactComponent },
},
},
component: async () => <div />,
}),
).toEqual({
$$type: 'extension',
id: 'test',
at: 'other/place',
configSchema: expect.anything(),
disabled: true,
inputs: {
first: {
extensionData: { component: coreExtensionData.reactComponent },
},
},
output: {
component: expect.anything(),
path: expect.anything(),
},
factory: expect.any(Function),
});
expect(
createPageExtension({
id: 'test',
defaultPath: '/here',
component: async () => <div />,
}),
).toEqual({
$$type: 'extension',
id: 'test',
at: 'core.router/routes',
configSchema: expect.anything(),
disabled: false,
inputs: {},
output: {
component: expect.anything(),
path: expect.anything(),
},
factory: expect.any(Function),
});
});
});
@@ -15,14 +15,14 @@
*/
import React from 'react';
import { createSchemaFromZod, PortableSchema } from './createSchemaFromZod';
import { createSchemaFromZod, PortableSchema } from '../createSchemaFromZod';
import {
AnyExtensionDataMap,
coreExtensionData,
createExtension,
Extension,
ExtensionDataValue,
} from './types';
} from '../types';
/**
* Helper for creating extensions for a routable React page component.
@@ -0,0 +1,17 @@
/*
* Copyright 2023 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.
*/
export { createPageExtension } from './createPageExtension';
+3 -5
View File
@@ -15,26 +15,24 @@
*/
/**
* TODO
* Core API used by Backstage frontend plugins.
*
* @packageDocumentation
*/
export { createPageExtension } from './createPageExtension';
export {
createSchemaFromZod,
type PortableSchema,
} from './createSchemaFromZod';
export * from './extensions';
export {
coreExtensionData,
createExtension,
createPlugin,
type AnyExtensionDataMap,
type BackstagePlugin,
type BackstagePluginOptions,
type CreateExtensionOptions,
type Extension,
type ExtensionDataBind,
type ExtensionDataRef,
type ExtensionDataValue,
} from './types';
export * from './wiring';
+1 -22
View File
@@ -25,6 +25,7 @@ export type ExtensionDataRef<T> = {
};
/** @public */
// TODO: change to options object with ID.
export function createExtensionDataRef<T>(id: string): ExtensionDataRef<T> {
return { id, $$type: 'extension-data' } as ExtensionDataRef<T>;
}
@@ -100,25 +101,3 @@ export function createExtension<
inputs: options.inputs ?? {},
};
}
/** @public */
export interface BackstagePluginOptions {
id: string;
defaultExtensions?: Extension<unknown>[];
}
/** @public */
export interface BackstagePlugin {
$$type: 'backstage-plugin';
id: string;
defaultExtensions: Extension<unknown>[];
}
/** @public */
export function createPlugin(options: BackstagePluginOptions): BackstagePlugin {
return {
...options,
$$type: 'backstage-plugin',
defaultExtensions: options.defaultExtensions ?? [],
};
}
@@ -0,0 +1,191 @@
/*
* Copyright 2023 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 React from 'react';
import { createApp } from '@backstage/frontend-app-api';
import { render, screen } from '@testing-library/react';
import { createSchemaFromZod } from '../createSchemaFromZod';
import {
createExtension,
coreExtensionData,
createExtensionDataRef,
} from '../types';
import { createPlugin, BackstagePlugin } from './createPlugin';
import { JsonObject } from '@backstage/types';
const nameExtensionDataRef = createExtensionDataRef<string>('name');
const TechRadarPage = createExtension({
id: 'plugin.techradar.page',
at: 'test.output/names',
output: {
name: nameExtensionDataRef,
},
factory({ bind }) {
bind.name('TechRadar');
},
});
const CatalogPage = createExtension({
id: 'plugin.catalog.page',
at: 'test.output/names',
output: {
name: nameExtensionDataRef,
},
configSchema: createSchemaFromZod(z =>
z.object({ name: z.string().default('Catalog') }),
),
factory({ bind, config }) {
bind.name(config.name);
},
});
const TechDocsAddon = createExtension({
id: 'plugin.techdocs.addon.example',
at: 'plugin.techdocs.page/addons',
output: {
name: nameExtensionDataRef,
},
configSchema: createSchemaFromZod(z =>
z.object({ name: z.string().default('TechDocsAddon') }),
),
factory({ bind, config }) {
bind.name(config.name);
},
});
const TechDocsPage = createExtension({
id: 'plugin.techdocs.page',
at: 'test.output/names',
inputs: {
addons: {
extensionData: {
name: nameExtensionDataRef,
},
},
},
output: {
name: nameExtensionDataRef,
},
factory({ bind, inputs }) {
bind.name(`TechDocs-${inputs.addons.map(n => n.name).join('-')}`);
},
});
const outputExtension = createExtension({
id: 'test.output',
at: 'root',
inputs: {
names: {
extensionData: {
name: nameExtensionDataRef,
},
},
},
output: {
component: coreExtensionData.reactComponent,
},
factory({ bind, inputs }) {
bind.component(() =>
React.createElement('span', {}, [
`Names: ${inputs.names.map(n => n.name).join(', ')}`,
]),
);
},
});
function createTestAppRoot({
plugins,
config = {},
}: {
plugins: BackstagePlugin[];
config: JsonObject;
}) {
Object.defineProperty(process.env, 'APP_CONFIG', {
value: [
{
data: config,
context: 'test',
},
],
configurable: true,
});
return createApp({ plugins: plugins }).createRoot();
}
describe('createPlugin', () => {
it('should create an empty plugin', () => {
const plugin = createPlugin({ id: 'empty' });
expect(plugin).toBeDefined();
});
it('should create a plugin with extension instances', () => {
const plugin = createPlugin({
id: 'empty',
extensions: [TechRadarPage, CatalogPage, outputExtension],
});
expect(plugin).toBeDefined();
render(
createTestAppRoot({
plugins: [plugin],
config: { app: { extensions: [{ 'core.router': false }] } },
}),
);
expect(screen.getByText('Names: TechRadar, Catalog')).toBeInTheDocument();
});
it('should create a plugin with nested extension instances', () => {
const plugin = createPlugin({
id: 'empty',
extensions: [
TechRadarPage,
CatalogPage,
TechDocsPage,
TechDocsAddon,
outputExtension,
],
});
expect(plugin).toBeDefined();
render(
createTestAppRoot({
plugins: [plugin],
config: {
app: {
extensions: [
{ 'core.router': false },
{
'plugin.catalog.page': {
config: { name: 'CatalogRenamed' },
},
},
],
},
},
}),
);
expect(
screen.getByText(
'Names: TechRadar, CatalogRenamed, TechDocs-TechDocsAddon',
),
).toBeInTheDocument();
});
});
@@ -0,0 +1,39 @@
/*
* Copyright 2023 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 { Extension } from '../types';
/** @public */
export interface PluginOptions {
id: string;
extensions?: Extension<unknown>[];
}
/** @public */
export interface BackstagePlugin {
$$type: 'backstage-plugin';
id: string;
extensions: Extension<unknown>[];
}
/** @public */
export function createPlugin(options: PluginOptions): BackstagePlugin {
return {
...options,
$$type: 'backstage-plugin',
extensions: options.extensions ?? [],
};
}
@@ -0,0 +1,21 @@
/*
* Copyright 2023 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.
*/
export {
createPlugin,
type BackstagePlugin,
type PluginOptions,
} from './createPlugin';
+2
View File
@@ -4298,8 +4298,10 @@ __metadata:
resolution: "@backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/frontend-app-api": "workspace:^"
"@backstage/types": "workspace:^"
"@testing-library/jest-dom": ^5.10.1
"@testing-library/react": ^12.1.3
lodash: ^4.17.21
zod: ^3.21.4
zod-to-json-schema: ^3.21.4