fix(frontend-test-utils): enable testing more than one route

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2023-12-09 12:25:19 +01:00
parent d653c139e6
commit 4158ef3301
5 changed files with 274 additions and 59 deletions
+3 -1
View File
@@ -24,12 +24,14 @@
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@testing-library/jest-dom": "^6.0.0"
"@testing-library/jest-dom": "^6.0.0",
"@types/react": "*"
},
"files": [
"dist"
],
"dependencies": {
"@backstage/core-components": "workspace:^",
"@backstage/frontend-app-api": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/test-utils": "workspace:^",
@@ -14,55 +14,215 @@
* limitations under the License.
*/
import React, { useCallback } from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import {
analyticsApiRef,
configApiRef,
coreExtensionData,
createApiExtension,
createApiFactory,
createExtension,
createSchemaFromZod,
useAnalytics,
useApi,
} from '@backstage/frontend-plugin-api';
import { screen } from '@testing-library/react';
import React from 'react';
import { Link } from '@backstage/core-components';
import { MockAnalyticsApi } from '../apis';
import { createExtensionTester } from './createExtensionTester';
describe('createExtensionTester', () => {
it('should render a simple extension', async () => {
createExtensionTester(
createExtension({
namespace: 'test',
attachTo: { id: 'ignored', input: 'ignored' },
output: { element: coreExtensionData.reactElement },
factory: () => ({ element: <div>test</div> }),
}),
).render();
const defaultDefinition = {
namespace: 'test',
attachTo: { id: 'ignored', input: 'ignored' },
output: { element: coreExtensionData.reactElement },
factory: () => ({ element: <div>test</div> }),
};
it('should render a simple extension', async () => {
const extension = createExtension(defaultDefinition);
const tester = createExtensionTester(extension);
tester.render();
await expect(screen.findByText('test')).resolves.toBeInTheDocument();
});
it('should render an extension even if disabled by default', async () => {
createExtensionTester(
createExtension({
namespace: 'test',
attachTo: { id: 'ignored', input: 'ignored' },
disabled: true,
output: { element: coreExtensionData.reactElement },
factory: () => ({ element: <div>test</div> }),
}),
).render();
const extension = createExtension({
...defaultDefinition,
disabled: true,
});
const tester = createExtensionTester(extension);
tester.render();
await expect(screen.findByText('test')).resolves.toBeInTheDocument();
});
it("should fail to render an extension that doesn't output a react element", async () => {
expect(() =>
createExtensionTester(
createExtension({
namespace: 'test',
attachTo: { id: 'ignored', input: 'ignored' },
disabled: true,
output: { path: coreExtensionData.routePath },
factory: () => ({ path: '/foo' }),
}),
).render(),
).toThrow(
"Failed to instantiate extension 'core/router', input 'children' did not receive required extension data 'core.reactElement' from extension 'test'",
const extension = createExtension({
...defaultDefinition,
output: { path: coreExtensionData.routePath },
factory: () => ({ path: '/foo' }),
});
const tester = createExtensionTester(extension);
expect(() => tester.render()).toThrow(
"Failed to instantiate extension 'core/routes', input 'routes' did not receive required extension data 'core.reactElement' from extension 'test'",
);
});
it('should render multiple extensions', async () => {
const indexPageExtension = createExtension({
...defaultDefinition,
factory: () => ({
element: (
<div>
Index page <Link to="/details">See details</Link>
</div>
),
}),
});
const detailsPageExtension = createExtension({
...defaultDefinition,
name: 'details',
attachTo: { id: 'core/routes', input: 'routes' },
output: {
path: coreExtensionData.routePath,
element: coreExtensionData.reactElement,
},
factory: () => ({ path: '/details', element: <div>Details page</div> }),
});
const tester = createExtensionTester(indexPageExtension);
tester.add(detailsPageExtension);
tester.render();
await expect(screen.findByText('Index page')).resolves.toBeInTheDocument();
fireEvent.click(screen.getByRole('link', { name: 'See details' }));
await expect(
screen.findByText('Details page'),
).resolves.toBeInTheDocument();
});
it.skip('should accepts a custom config', async () => {
const indexPageExtension = createExtension({
...defaultDefinition,
configSchema: createSchemaFromZod(z =>
z.object({ title: z.string().optional() }),
),
factory: ({ config }) => {
const Component = () => {
const configApi = useApi(configApiRef);
const appTitle = configApi.getOptionalString('app.title');
return (
<div>
<h2>{appTitle ?? 'Backstafe app'}</h2>
<h3>{config.title ?? 'Index page'}</h3>
<Link to="/details">See details</Link>
</div>
);
};
return {
element: <Component />,
};
},
});
const detailsPageExtension = createExtension({
...defaultDefinition,
name: 'details',
attachTo: { id: 'core/routes', input: 'routes' },
configSchema: createSchemaFromZod(z =>
z.object({ title: z.string().optional() }),
),
output: {
path: coreExtensionData.routePath,
element: coreExtensionData.reactElement,
},
factory: ({ config }) => ({
path: '/details',
element: <div>{config.title ?? 'Details page'}</div>,
}),
});
const tester = createExtensionTester(indexPageExtension, {
config: { title: 'Custom index' },
});
tester.add(detailsPageExtension, {
config: { title: 'Custom details' },
});
tester.render({
config: {
app: {
title: 'Custom app',
},
},
});
await expect(
screen.findByRole('heading', { name: 'Custom app' }),
).resolves.toBeInTheDocument();
await expect(
screen.findByRole('heading', { name: 'Custom index' }),
).resolves.toBeInTheDocument();
fireEvent.click(screen.getByRole('link', { name: 'See details' }));
await expect(
screen.findByText('Custom details'),
).resolves.toBeInTheDocument();
});
it.skip('should capture click events in anylitics', async () => {
// Mocking the analytics api implementation
const analyticsApiMock = new MockAnalyticsApi();
const analyticsApiOverride = createApiExtension({
factory: createApiFactory({
api: analyticsApiRef,
deps: {},
factory: () => analyticsApiMock,
}),
});
const indexPageExtension = createExtension({
...defaultDefinition,
factory: () => {
const Component = () => {
const analyticsApi = useAnalytics();
const handleClick = useCallback(() => {
analyticsApi.captureEvent('click', 'See details');
}, [analyticsApi]);
return (
<div>
Index Page
<button onClick={handleClick}>See details</button>
</div>
);
};
return {
element: <Component />,
};
},
});
const tester = createExtensionTester(indexPageExtension);
// Overriding the analytics api extension
tester.add(analyticsApiOverride);
tester.render();
fireEvent.click(await screen.findByRole('button', { name: 'See details' }));
await waitFor(() =>
expect(analyticsApiMock.getEvents()[1]).toMatchObject({
action: 'click',
subject: 'See details',
}),
);
});
});
@@ -17,6 +17,8 @@
import { createSpecializedApp } from '@backstage/frontend-app-api';
import {
ExtensionDefinition,
coreExtensionData,
createExtension,
createExtensionOverrides,
} from '@backstage/frontend-plugin-api';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
@@ -33,13 +35,27 @@ export class ExtensionTester {
options?: { config?: TConfig },
): ExtensionTester {
const tester = new ExtensionTester();
tester.add(subject, options);
const { output, factory, ...rest } = subject;
// attaching to core/routes to render as index route
const extension = createExtension({
...rest,
attachTo: { id: 'core/routes', input: 'routes' },
output: {
...output,
path: coreExtensionData.routePath,
},
factory: params => ({
...factory(params),
path: '/',
}),
});
tester.add(extension, options);
return tester;
}
readonly #extensions = new Array<{
id: string;
extension: ExtensionDefinition<any>;
definition: ExtensionDefinition<any>;
config?: JsonValue;
}>();
@@ -47,13 +63,19 @@ export class ExtensionTester {
extension: ExtensionDefinition<TConfig>,
options?: { config?: TConfig },
): ExtensionTester {
const withNamespace = {
const { name, namespace } = extension;
const definition = {
...extension,
name: !extension.namespace && !extension.name ? 'test' : extension.name,
// setting name "test" as fallback
name: !namespace && !name ? 'test' : name,
};
const { id } = resolveExtensionDefinition(definition);
this.#extensions.push({
id: resolveExtensionDefinition(withNamespace).id,
extension: withNamespace,
id,
definition,
config: options?.config as JsonValue,
});
@@ -71,27 +93,17 @@ export class ExtensionTester {
}
const extensionsConfig: JsonArray = [
...rest.map(entry => ({
[entry.id]: {
config: entry.config,
...rest.map(extension => ({
[extension.id]: {
config: extension.config,
},
})),
{
[subject.id]: {
attachTo: { id: 'core/router', input: 'children' },
config: subject.config,
disabled: false,
},
},
{
'core/layout': false,
},
{
'core/nav': false,
},
{
'core/routes': false,
},
];
const finalConfig = {
@@ -105,7 +117,7 @@ export class ExtensionTester {
const app = createSpecializedApp({
features: [
createExtensionOverrides({
extensions: this.#extensions.map(entry => entry.extension),
extensions: this.#extensions.map(extension => extension.definition),
}),
],
config: new MockConfigApi(finalConfig),
@@ -14,14 +14,53 @@
* limitations under the License.
*/
import React from 'react';
import { screen } from '@testing-library/react';
import React, { useCallback } from 'react';
import { screen, fireEvent } from '@testing-library/react';
import {
MockAnalyticsApi,
TestApiProvider,
} from '@backstage/frontend-test-utils';
import { analyticsApiRef, useAnalytics } from '@backstage/frontend-plugin-api';
import { renderInTestApp } from './renderInTestApp';
describe('renderInTestApp', () => {
it('should render the given component in a page', async () => {
const Component = () => <div>Test</div>;
renderInTestApp(<Component />);
expect(screen.getByText('Test')).toBeInTheDocument();
const IndexPage = () => <div>Index Page</div>;
renderInTestApp(<IndexPage />);
expect(screen.getByText('Index Page')).toBeInTheDocument();
});
it('should works with apis provider', async () => {
const IndexPage = () => {
const analyticsApi = useAnalytics();
const handleClick = useCallback(() => {
analyticsApi.captureEvent('click', 'See details');
}, [analyticsApi]);
return (
<div>
Index Page
<a href="/details" onClick={handleClick}>
See details
</a>
</div>
);
};
const analyticsApiMock = new MockAnalyticsApi();
renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, analyticsApiMock]]}>
<IndexPage />
</TestApiProvider>,
);
fireEvent.click(screen.getByRole('link', { name: 'See details' }));
const events = analyticsApiMock.getEvents();
expect(events[1]).toMatchObject({
action: 'click',
subject: 'See details',
});
});
});
+2
View File
@@ -4227,11 +4227,13 @@ __metadata:
resolution: "@backstage/frontend-test-utils@workspace:packages/frontend-test-utils"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/core-components": "workspace:^"
"@backstage/frontend-app-api": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/types": "workspace:^"
"@testing-library/jest-dom": ^6.0.0
"@types/react": "*"
peerDependencies:
"@testing-library/react": ^12.1.3 || ^13.0.0 || ^14.0.0
react: ^16.13.1 || ^17.0.0 || ^18.0.0