chore: wip

Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com>
Co-authored-by: Fredrik Adelöw <freben@users.noreply.github.com>
Co-authored-by: Camila Belo <camilaibs@users.noreply.github.com>
Co-authored-by: Johan Haals <johan@haals.se>
Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2024-08-13 14:49:56 +02:00
parent f1b209b947
commit 4bc4cd969c
3 changed files with 128 additions and 79 deletions
@@ -16,9 +16,11 @@
import React from 'react';
import { SignInPageBlueprint } from './SignInPageBlueprint';
import { createExtensionTester } from '@backstage/frontend-test-utils';
import {
createExtensionTester,
renderInTestApp,
} from '@backstage/frontend-test-utils';
import { screen, waitFor } from '@testing-library/react';
import { coreExtensionData, createExtension } from '../wiring';
describe('SignInPageBlueprint', () => {
it('should create an extension with sensible defaults', () => {
@@ -60,20 +62,11 @@ describe('SignInPageBlueprint', () => {
const tester = createExtensionTester(extension);
expect(tester.data(SignInPageBlueprint.dataRefs.component)).toBeDefined();
const Element = tester.data(SignInPageBlueprint.dataRefs.component)!;
createExtensionTester(
createExtension({
name: 'dummy',
attachTo: { id: 'ignored', input: 'ignored' },
output: {
element: coreExtensionData.reactElement,
},
factory: () => ({ element: <div /> }),
}),
)
.add(extension)
.render();
expect(Element).toBeDefined();
renderInTestApp(<Element onSignInSuccess={() => {}} />);
await waitFor(() => {
expect(screen.getByTestId('mock-sign-in')).toBeInTheDocument();
@@ -49,57 +49,6 @@ import { instantiateAppNodeTree } from '../../../frontend-app-api/src/tree/insta
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { readAppExtensionsConfig } from '../../../frontend-app-api/src/tree/readAppExtensionsConfig';
const NavItem = (props: {
routeRef: RouteRef<undefined>;
title: string;
icon: IconComponent;
}) => {
const { routeRef, title, icon: Icon } = props;
const link = useRouteRef(routeRef);
if (!link) {
return null;
}
return (
<li>
<Link to={link()}>
<Icon /> {title}
</Link>
</li>
);
};
const TestAppNavExtension = createExtension({
namespace: 'app',
name: 'nav',
attachTo: { id: 'app/layout', input: 'nav' },
inputs: {
items: createExtensionInput({
target: createNavItemExtension.targetDataRef,
}),
},
output: {
element: coreExtensionData.reactElement,
},
factory({ inputs }) {
return {
element: (
<nav>
<ul>
{inputs.items.map((item, index) => (
<NavItem
key={index}
icon={item.output.target.icon}
title={item.output.target.title}
routeRef={item.output.target.routeRef}
/>
))}
</ul>
</nav>
),
};
},
});
/** @public */
export class ExtensionQuery {
#node: AppNode;
@@ -245,6 +194,25 @@ export class ExtensionTester {
return new ExtensionQuery(node);
}
element(): JSX.Element {
const tree = this.#resolveTree();
const element = new ExtensionQuery(tree.root).data(
coreExtensionData.reactElement,
);
if (!element) {
throw new Error(
'No element found. Make sure the extension has a `coreExtensionData.reactElement` output, or use the `.get(myComponentDataRef)` method to get the component',
);
}
return element;
}
/**
* @deprecated Switch to using `renderInTestApp` directly and using `.element()` or `.get(myComponentDataRef)` to get the component you would like to wrap up
*/
render(options?: { config?: JsonObject }): RenderResult {
const { config = {} } = options ?? {};
@@ -260,7 +228,6 @@ export class ExtensionTester {
createExtensionOverrides({
extensions: [
...this.#extensions.map(extension => extension.definition),
TestAppNavExtension,
createRouterExtension({
namespace: 'test',
Component: ({ children }) => (
@@ -16,11 +16,24 @@
import React from 'react';
import {
ExtensionDefinition,
IconComponent,
RouteRef,
coreExtensionData,
createExtension,
createExtensionInput,
createExtensionOverrides,
createNavItemExtension,
createRouterExtension,
useRouteRef,
} from '@backstage/frontend-plugin-api';
import { createExtensionTester } from './createExtensionTester';
import { Link, MemoryRouter } from 'react-router-dom';
import { createSpecializedApp } from '@backstage/frontend-app-api';
import { render } from '@testing-library/react';
import { resolveExtensionDefinition } from '@backstage/frontend-plugin-api/src/wiring/resolveExtensionDefinition';
import { resolve } from 'path';
import { ConfigReader } from '@backstage/config';
import { JsonObject } from '@backstage/types';
/**
* Options to customize the behavior of the test app.
@@ -44,8 +57,64 @@ export type TestAppOptions = {
* ```
*/
mountedRoutes?: { [path: string]: RouteRef };
/**
* Additional configuration passed to the app when rendering elements inside it.
*/
config?: JsonObject;
};
const NavItem = (props: {
routeRef: RouteRef<undefined>;
title: string;
icon: IconComponent;
}) => {
const { routeRef, title, icon: Icon } = props;
const link = useRouteRef(routeRef);
if (!link) {
return null;
}
return (
<li>
<Link to={link()}>
<Icon /> {title}
</Link>
</li>
);
};
const TestAppNavExtension = createExtension({
namespace: 'app',
name: 'nav',
attachTo: { id: 'app/layout', input: 'nav' },
inputs: {
items: createExtensionInput({
target: createNavItemExtension.targetDataRef,
}),
},
output: {
element: coreExtensionData.reactElement,
},
factory({ inputs }) {
return {
element: (
<nav>
<ul>
{inputs.items.map((item, index) => (
<NavItem
key={index}
icon={item.output.target.icon}
title={item.output.target.title}
routeRef={item.output.target.routeRef}
/>
))}
</ul>
</nav>
),
};
},
});
/**
* @public
* Renders the given element in a test app, for use in unit tests.
@@ -54,20 +123,28 @@ export function renderInTestApp(
element: JSX.Element,
options?: TestAppOptions,
) {
const extension = createExtension({
namespace: 'test',
attachTo: { id: 'app', input: 'root' },
output: {
element: coreExtensionData.reactElement,
},
factory: () => ({ element }),
});
const tester = createExtensionTester(extension);
const extensions: Array<ExtensionDefinition<any, any>> = [
createExtension({
namespace: 'test',
attachTo: { id: 'app/routes', input: 'routes' },
output: [coreExtensionData.reactElement, coreExtensionData.routePath],
factory: () => {
return [
coreExtensionData.reactElement(element),
coreExtensionData.routePath('/'),
];
},
}),
createRouterExtension({
namespace: 'test',
Component: ({ children }) => <MemoryRouter>{children}</MemoryRouter>,
}),
];
if (options?.mountedRoutes) {
for (const [path, routeRef] of Object.entries(options.mountedRoutes)) {
// TODO(Rugvip): add support for external route refs
tester.add(
extensions.push(
createExtension({
kind: 'test-route',
name: path,
@@ -84,5 +161,17 @@ export function renderInTestApp(
);
}
}
return tester.render();
const app = createSpecializedApp({
features: [
createExtensionOverrides({
extensions,
}),
],
config: ConfigReader.fromConfigs([
{ context: 'render-config', data: options?.config ?? {} },
]),
});
return render(app.createRoot());
}