fix(frontend-test-utils): enable testing more than one route
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user