Merge pull request #25992 from backstage/mob/fet
NFS: Deprecate `.render()` of `createExtensionTester`
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
---
|
||||
'@backstage/frontend-test-utils': patch
|
||||
---
|
||||
|
||||
Deprecate the `.render` method of the `createExtensionTester` in favour of using `renderInTestApp` directly.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
renderInTestApp,
|
||||
createExtensionTester,
|
||||
} from '@backstage/frontend-test-utils';
|
||||
|
||||
const tester = createExtensionTester(extension);
|
||||
|
||||
const { getByTestId } = renderInTestApp(tester.reactElement());
|
||||
|
||||
// or if you're not using `coreExtensionData.reactElement` as the output ref
|
||||
const { getByTestId } = renderInTestApp(tester.get(myComponentRef));
|
||||
```
|
||||
@@ -77,7 +77,7 @@ describe('NavItemBlueprint', () => {
|
||||
|
||||
const tester = createExtensionTester(extension);
|
||||
|
||||
expect(tester.data(NavItemBlueprint.dataRefs.target)).toEqual({
|
||||
expect(tester.get(NavItemBlueprint.dataRefs.target)).toEqual({
|
||||
title: 'TEST',
|
||||
icon: MockIcon,
|
||||
routeRef: mockRouteRef,
|
||||
@@ -97,7 +97,7 @@ describe('NavItemBlueprint', () => {
|
||||
config: { title: 'OVERRIDDEN' },
|
||||
});
|
||||
|
||||
expect(tester.data(NavItemBlueprint.dataRefs.target)).toEqual({
|
||||
expect(tester.get(NavItemBlueprint.dataRefs.target)).toEqual({
|
||||
title: 'OVERRIDDEN',
|
||||
icon: MockIcon,
|
||||
routeRef: mockRouteRef,
|
||||
|
||||
@@ -64,7 +64,7 @@ describe('NavLogoBlueprint', () => {
|
||||
|
||||
const tester = createExtensionTester(extension);
|
||||
|
||||
expect(tester.data(NavLogoBlueprint.dataRefs.logoElements)).toEqual({
|
||||
expect(tester.get(NavLogoBlueprint.dataRefs.logoElements)).toEqual({
|
||||
logoFull,
|
||||
logoIcon,
|
||||
});
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
import React from 'react';
|
||||
import { createRouteRef } from '../routing';
|
||||
import { PageBlueprint } from './PageBlueprint';
|
||||
import { createExtensionTester } from '@backstage/frontend-test-utils';
|
||||
import {
|
||||
createExtensionTester,
|
||||
renderInTestApp,
|
||||
} from '@backstage/frontend-test-utils';
|
||||
import {
|
||||
coreExtensionData,
|
||||
createExtensionBlueprint,
|
||||
@@ -98,7 +101,7 @@ describe('PageBlueprint', () => {
|
||||
// TODO(blam): test for the routePath output doesn't work, due to the the way the test harness works
|
||||
// expect(tester.data(coreExtensionData.routePath)).toBe('/test');
|
||||
|
||||
expect(tester.data(coreExtensionData.routeRef)).toBe(mockRouteRef);
|
||||
expect(tester.get(coreExtensionData.routeRef)).toBe(mockRouteRef);
|
||||
|
||||
const { getByTestId } = tester.render();
|
||||
|
||||
@@ -144,7 +147,7 @@ describe('PageBlueprint', () => {
|
||||
CardBlueprint.make({ name: 'card', params: {} }),
|
||||
);
|
||||
|
||||
const { getByTestId, getByText } = tester.render();
|
||||
const { getByTestId, getByText } = renderInTestApp(tester.reactElement());
|
||||
|
||||
await waitFor(() => expect(getByTestId('card')).toBeInTheDocument());
|
||||
await waitFor(() =>
|
||||
|
||||
@@ -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,9 @@ describe('SignInPageBlueprint', () => {
|
||||
|
||||
const tester = createExtensionTester(extension);
|
||||
|
||||
expect(tester.data(SignInPageBlueprint.dataRefs.component)).toBeDefined();
|
||||
const Component = tester.get(SignInPageBlueprint.dataRefs.component);
|
||||
|
||||
createExtensionTester(
|
||||
createExtension({
|
||||
name: 'dummy',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
},
|
||||
factory: () => ({ element: <div /> }),
|
||||
}),
|
||||
)
|
||||
.add(extension)
|
||||
.render();
|
||||
renderInTestApp(<Component onSignInSuccess={() => {}} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('mock-sign-in')).toBeInTheDocument();
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('ThemeBlueprint', () => {
|
||||
const extension = ThemeBlueprint.make({ params: { theme } });
|
||||
|
||||
expect(
|
||||
createExtensionTester(extension).data(ThemeBlueprint.dataRefs.theme),
|
||||
createExtensionTester(extension).get(ThemeBlueprint.dataRefs.theme),
|
||||
).toEqual(theme);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('TranslationBlueprint', () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
createExtensionTester(extension).data(
|
||||
createExtensionTester(extension).get(
|
||||
TranslationBlueprint.dataRefs.translation,
|
||||
),
|
||||
).toBe(messages);
|
||||
|
||||
@@ -688,7 +688,7 @@ describe('createExtension', () => {
|
||||
|
||||
const tester = createExtensionTester(overridden);
|
||||
|
||||
expect(tester.data(numberDataRef)).toBe(43);
|
||||
expect(tester.get(numberDataRef)).toBe(43);
|
||||
});
|
||||
|
||||
it('should work functionally with overrides', () => {
|
||||
@@ -722,14 +722,14 @@ describe('createExtension', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(createExtensionTester(overriden).data(stringDataRef)).toBe(
|
||||
expect(createExtensionTester(overriden).get(stringDataRef)).toBe(
|
||||
'foo-boom-override-hello',
|
||||
);
|
||||
|
||||
expect(
|
||||
createExtensionTester(overriden, {
|
||||
config: { foo: 'hello', bar: 'world' },
|
||||
}).data(stringDataRef),
|
||||
}).get(stringDataRef),
|
||||
).toBe('foo-hello-override-world');
|
||||
});
|
||||
|
||||
@@ -809,7 +809,7 @@ describe('createExtension', () => {
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
.get(outputRef),
|
||||
).toEqual({
|
||||
opt: 'orig-opt',
|
||||
single: 'orig-single',
|
||||
@@ -836,7 +836,7 @@ describe('createExtension', () => {
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
.get(outputRef),
|
||||
).toEqual({
|
||||
opt: 'opt',
|
||||
single: 'single',
|
||||
@@ -862,7 +862,7 @@ describe('createExtension', () => {
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
.get(outputRef),
|
||||
).toEqual({
|
||||
opt: 'none',
|
||||
single: 'single',
|
||||
@@ -885,7 +885,7 @@ describe('createExtension', () => {
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
.get(outputRef),
|
||||
).toEqual({
|
||||
opt: 'orig-opt',
|
||||
single: 'orig-single',
|
||||
@@ -912,7 +912,7 @@ describe('createExtension', () => {
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
.get(outputRef),
|
||||
).toEqual({
|
||||
opt: 'orig-opt',
|
||||
single: 'orig-single',
|
||||
@@ -939,7 +939,7 @@ describe('createExtension', () => {
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
.get(outputRef),
|
||||
).toEqual({
|
||||
opt: 'orig-opt',
|
||||
single: 'orig-single',
|
||||
@@ -966,7 +966,7 @@ describe('createExtension', () => {
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
.get(outputRef),
|
||||
).toEqual({
|
||||
opt: 'orig-opt',
|
||||
single: 'orig-single',
|
||||
@@ -997,7 +997,7 @@ describe('createExtension', () => {
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
.get(outputRef),
|
||||
).toEqual({
|
||||
opt: 'none',
|
||||
single: 'override-orig-single',
|
||||
@@ -1023,7 +1023,7 @@ describe('createExtension', () => {
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
.get(outputRef),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Failed to instantiate extension 'subject', override data provided for input 'multi' must match the length of the original inputs"`,
|
||||
);
|
||||
@@ -1046,7 +1046,7 @@ describe('createExtension', () => {
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
.get(outputRef),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Failed to instantiate extension 'subject', override data for input 'multi' may not mix forwarded inputs with data overrides"`,
|
||||
);
|
||||
@@ -1069,7 +1069,7 @@ describe('createExtension', () => {
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
.get(outputRef),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Failed to instantiate extension 'subject', missing required extension data value(s) 'test1'"`,
|
||||
);
|
||||
@@ -1098,7 +1098,7 @@ describe('createExtension', () => {
|
||||
.add(singleExt)
|
||||
.add(multi1Ext)
|
||||
.add(multi2Ext)
|
||||
.data(outputRef),
|
||||
.get(outputRef),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Failed to instantiate extension 'subject', extension data 'test2' was provided but not declared"`,
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { AnalyticsApi } from '@backstage/frontend-plugin-api';
|
||||
import { AnalyticsEvent } from '@backstage/frontend-plugin-api';
|
||||
import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { AppNode } from '@backstage/frontend-plugin-api';
|
||||
import { AppNodeInstance } from '@backstage/frontend-plugin-api';
|
||||
import { ErrorWithContext } from '@backstage/test-utils';
|
||||
@@ -30,20 +31,30 @@ import { TestApiRegistry } from '@backstage/test-utils';
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
|
||||
// @public (undocumented)
|
||||
export function createExtensionTester<TConfig>(
|
||||
subject: ExtensionDefinition<TConfig>,
|
||||
export function createExtensionTester<
|
||||
TConfig,
|
||||
TConfigInput,
|
||||
UOutput extends AnyExtensionDataRef,
|
||||
>(
|
||||
subject: ExtensionDefinition<TConfig, TConfigInput, UOutput>,
|
||||
options?: {
|
||||
config?: TConfig;
|
||||
config?: TConfigInput;
|
||||
},
|
||||
): ExtensionTester;
|
||||
): ExtensionTester<UOutput>;
|
||||
|
||||
export { ErrorWithContext };
|
||||
|
||||
// @public (undocumented)
|
||||
export class ExtensionQuery {
|
||||
export class ExtensionQuery<UOutput extends AnyExtensionDataRef> {
|
||||
constructor(node: AppNode);
|
||||
// (undocumented)
|
||||
data<T>(ref: ExtensionDataRef<T>): T | undefined;
|
||||
get<TId extends UOutput['id']>(
|
||||
ref: ExtensionDataRef<any, TId, any>,
|
||||
): UOutput extends ExtensionDataRef<infer IData, TId, infer IConfig>
|
||||
? IConfig['optional'] extends true
|
||||
? IData | undefined
|
||||
: IData
|
||||
: never;
|
||||
// (undocumented)
|
||||
get instance(): AppNodeInstance;
|
||||
// (undocumented)
|
||||
@@ -51,19 +62,29 @@ export class ExtensionQuery {
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export class ExtensionTester {
|
||||
export class ExtensionTester<UOutput extends AnyExtensionDataRef> {
|
||||
// (undocumented)
|
||||
add<TConfig, TConfigInput>(
|
||||
extension: ExtensionDefinition<TConfig, TConfigInput>,
|
||||
options?: {
|
||||
config?: TConfigInput;
|
||||
},
|
||||
): ExtensionTester;
|
||||
): ExtensionTester<UOutput>;
|
||||
// (undocumented)
|
||||
data<T>(ref: ExtensionDataRef<T>): T | undefined;
|
||||
get<TId extends UOutput['id']>(
|
||||
ref: ExtensionDataRef<any, TId, any>,
|
||||
): UOutput extends ExtensionDataRef<infer IData, TId, infer IConfig>
|
||||
? IConfig['optional'] extends true
|
||||
? IData | undefined
|
||||
: IData
|
||||
: never;
|
||||
// (undocumented)
|
||||
query(id: string | ExtensionDefinition<any, any>): ExtensionQuery;
|
||||
query<UQueryExtensionOutput extends AnyExtensionDataRef>(
|
||||
extension: ExtensionDefinition<any, any, UQueryExtensionOutput>,
|
||||
): ExtensionQuery<UQueryExtensionOutput>;
|
||||
// (undocumented)
|
||||
reactElement(): JSX.Element;
|
||||
// @deprecated (undocumented)
|
||||
render(options?: { config?: JsonObject }): RenderResult;
|
||||
}
|
||||
|
||||
@@ -117,6 +138,7 @@ export type TestAppOptions = {
|
||||
mountedRoutes?: {
|
||||
[path: string]: RouteRef;
|
||||
};
|
||||
config?: JsonObject;
|
||||
};
|
||||
|
||||
export { withLogCollector };
|
||||
|
||||
@@ -240,13 +240,13 @@ describe('createExtensionTester', () => {
|
||||
const extension = createExtension({
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: { text: stringDataRef },
|
||||
factory: () => ({ text: 'test-text' }),
|
||||
output: [stringDataRef],
|
||||
factory: () => [stringDataRef('test-text')],
|
||||
});
|
||||
|
||||
const tester = createExtensionTester(extension);
|
||||
|
||||
expect(tester.data(stringDataRef)).toBe('test-text');
|
||||
expect(tester.get(stringDataRef)).toBe('test-text');
|
||||
});
|
||||
|
||||
it('should throw an error if trying to access an instance not provided to the tester', () => {
|
||||
@@ -297,6 +297,79 @@ describe('createExtensionTester', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not allow getting extension data for an output that was not defined in the extension', () => {
|
||||
const internalRef = createExtensionDataRef<number>().with({
|
||||
id: 'test.internal',
|
||||
});
|
||||
|
||||
const internalRef2 = createExtensionDataRef<number>().with({
|
||||
id: 'test.internal2',
|
||||
});
|
||||
|
||||
const extension = createExtension({
|
||||
namespace: 'test',
|
||||
name: 'e1',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: [stringDataRef, internalRef.optional()],
|
||||
factory: () => [stringDataRef('test-text')],
|
||||
});
|
||||
|
||||
const tester = createExtensionTester(extension);
|
||||
|
||||
const test: string = tester.get(stringDataRef);
|
||||
|
||||
// @ts-expect-error - internalRef is optional
|
||||
const test2: number = tester.get(internalRef);
|
||||
|
||||
// @ts-expect-error - internalRef2 is not defined in the extension
|
||||
const test3: number = tester.get(internalRef2);
|
||||
|
||||
expect([test, test2, test3]).toBeDefined();
|
||||
});
|
||||
|
||||
it('should support getting outputs from a query response', () => {
|
||||
const internalRef = createExtensionDataRef<number>().with({
|
||||
id: 'test.internal',
|
||||
});
|
||||
|
||||
const internalRef2 = createExtensionDataRef<number>().with({
|
||||
id: 'test.internal2',
|
||||
});
|
||||
|
||||
const extension = createExtension({
|
||||
namespace: 'test',
|
||||
name: 'e1',
|
||||
inputs: {
|
||||
ignored: createExtensionInput([stringDataRef]),
|
||||
},
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: () => [coreExtensionData.reactElement(<div>bob</div>)],
|
||||
});
|
||||
|
||||
const extraExtension = createExtension({
|
||||
namespace: 'test',
|
||||
name: 'e2',
|
||||
attachTo: { id: 'test/e1', input: 'ignored' },
|
||||
output: [stringDataRef, internalRef.optional()],
|
||||
factory: () => [stringDataRef('test-text')],
|
||||
});
|
||||
|
||||
const tester = createExtensionTester(extension)
|
||||
.add(extraExtension)
|
||||
.query(extraExtension);
|
||||
|
||||
const test: string = tester.get(stringDataRef);
|
||||
|
||||
// @ts-expect-error - internalRef is optional
|
||||
const test2: number = tester.get(internalRef);
|
||||
|
||||
// @ts-expect-error - internalRef2 is not defined in the extension
|
||||
const test3: number = tester.get(internalRef2);
|
||||
|
||||
expect([test, test2, test3]).toBeDefined();
|
||||
});
|
||||
|
||||
// TODO: this should be implemented
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('should allow querying an extension and getting outputs', () => {
|
||||
@@ -328,8 +401,10 @@ describe('createExtensionTester', () => {
|
||||
|
||||
const tester = createExtensionTester(extension).add(extension2);
|
||||
|
||||
expect(tester.query(extension).data(stringDataRef)).toBe('nest-test-text');
|
||||
expect(tester.query(extension2).data(stringDataRef)).toBe('test-text');
|
||||
// @ts-expect-error
|
||||
expect(tester.query(extension).get(stringDataRef)).toBe('nest-test-text');
|
||||
// @ts-expect-error
|
||||
expect(tester.query(extension2).get(stringDataRef)).toBe('test-text');
|
||||
// @ts-expect-error
|
||||
expect(tester.query(extension).input('input').data(stringDataRef)).toBe(
|
||||
'nest-test-text',
|
||||
@@ -362,6 +437,7 @@ describe('createExtensionTester', () => {
|
||||
inputs: { input: 'test-text' },
|
||||
});
|
||||
|
||||
expect(tester.query(extension).data(stringDataRef)).toBe('nest-test-text');
|
||||
// @ts-expect-error
|
||||
expect(tester.query(extension).get(stringDataRef)).toBe('nest-test-text');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import { MemoryRouter, Link } from 'react-router-dom';
|
||||
import { RenderResult, render } from '@testing-library/react';
|
||||
import { createSpecializedApp } from '@backstage/frontend-app-api';
|
||||
import {
|
||||
AnyExtensionDataRef,
|
||||
AppNode,
|
||||
AppTree,
|
||||
Extension,
|
||||
@@ -101,7 +102,7 @@ const TestAppNavExtension = createExtension({
|
||||
});
|
||||
|
||||
/** @public */
|
||||
export class ExtensionQuery {
|
||||
export class ExtensionQuery<UOutput extends AnyExtensionDataRef> {
|
||||
#node: AppNode;
|
||||
|
||||
constructor(node: AppNode) {
|
||||
@@ -124,18 +125,24 @@ export class ExtensionQuery {
|
||||
return instance;
|
||||
}
|
||||
|
||||
data<T>(ref: ExtensionDataRef<T>): T | undefined {
|
||||
get<TId extends UOutput['id']>(
|
||||
ref: ExtensionDataRef<any, TId, any>,
|
||||
): UOutput extends ExtensionDataRef<infer IData, TId, infer IConfig>
|
||||
? IConfig['optional'] extends true
|
||||
? IData | undefined
|
||||
: IData
|
||||
: never {
|
||||
return this.instance.getData(ref);
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export class ExtensionTester {
|
||||
export class ExtensionTester<UOutput extends AnyExtensionDataRef> {
|
||||
/** @internal */
|
||||
static forSubject<TConfig, TConfigInput>(
|
||||
static forSubject<TConfig, TConfigInput, UOutput extends AnyExtensionDataRef>(
|
||||
subject: ExtensionDefinition<TConfig, TConfigInput>,
|
||||
options?: { config?: TConfigInput },
|
||||
): ExtensionTester {
|
||||
): ExtensionTester<UOutput> {
|
||||
const tester = new ExtensionTester();
|
||||
const internal = toInternalExtensionDefinition(subject);
|
||||
|
||||
@@ -192,7 +199,7 @@ export class ExtensionTester {
|
||||
add<TConfig, TConfigInput>(
|
||||
extension: ExtensionDefinition<TConfig, TConfigInput>,
|
||||
options?: { config?: TConfigInput },
|
||||
): ExtensionTester {
|
||||
): ExtensionTester<UOutput> {
|
||||
if (this.#tree) {
|
||||
throw new Error(
|
||||
'Cannot add more extensions accessing the extension tree',
|
||||
@@ -219,17 +226,24 @@ export class ExtensionTester {
|
||||
return this;
|
||||
}
|
||||
|
||||
data<T>(ref: ExtensionDataRef<T>): T | undefined {
|
||||
get<TId extends UOutput['id']>(
|
||||
ref: ExtensionDataRef<any, TId, any>,
|
||||
): UOutput extends ExtensionDataRef<infer IData, TId, infer IConfig>
|
||||
? IConfig['optional'] extends true
|
||||
? IData | undefined
|
||||
: IData
|
||||
: never {
|
||||
const tree = this.#resolveTree();
|
||||
|
||||
return new ExtensionQuery(tree.root).data(ref);
|
||||
return new ExtensionQuery(tree.root).get(ref);
|
||||
}
|
||||
|
||||
query(id: string | ExtensionDefinition<any, any>): ExtensionQuery {
|
||||
query<UQueryExtensionOutput extends AnyExtensionDataRef>(
|
||||
extension: ExtensionDefinition<any, any, UQueryExtensionOutput>,
|
||||
): ExtensionQuery<UQueryExtensionOutput> {
|
||||
const tree = this.#resolveTree();
|
||||
|
||||
const actualId =
|
||||
typeof id === 'string' ? id : resolveExtensionDefinition(id).id;
|
||||
const actualId = resolveExtensionDefinition(extension).id;
|
||||
|
||||
const node = tree.nodes.get(actualId);
|
||||
|
||||
@@ -245,6 +259,25 @@ export class ExtensionTester {
|
||||
return new ExtensionQuery(node);
|
||||
}
|
||||
|
||||
reactElement(): JSX.Element {
|
||||
const tree = this.#resolveTree();
|
||||
|
||||
const element = new ExtensionQuery(tree.root).get(
|
||||
coreExtensionData.reactElement,
|
||||
);
|
||||
|
||||
if (!element) {
|
||||
throw new Error(
|
||||
'No element found. Make sure the extension has a `coreExtensionData.reactElement` output, or use the `.get(...)` to access output data directly instead',
|
||||
);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Switch to using `renderInTestApp` directly and using `.reactElement()` or `.get(...)` to get the component you w
|
||||
*/
|
||||
render(options?: { config?: JsonObject }): RenderResult {
|
||||
const { config = {} } = options ?? {};
|
||||
|
||||
@@ -336,9 +369,13 @@ export class ExtensionTester {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function createExtensionTester<TConfig>(
|
||||
subject: ExtensionDefinition<TConfig>,
|
||||
options?: { config?: TConfig },
|
||||
): ExtensionTester {
|
||||
export function createExtensionTester<
|
||||
TConfig,
|
||||
TConfigInput,
|
||||
UOutput extends AnyExtensionDataRef,
|
||||
>(
|
||||
subject: ExtensionDefinition<TConfig, TConfigInput, UOutput>,
|
||||
options?: { config?: TConfigInput },
|
||||
): ExtensionTester<UOutput> {
|
||||
return ExtensionTester.forSubject(subject, options);
|
||||
}
|
||||
|
||||
@@ -15,12 +15,23 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Link, MemoryRouter } from 'react-router-dom';
|
||||
import { createSpecializedApp } from '@backstage/frontend-app-api';
|
||||
import { RenderResult, render } from '@testing-library/react';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import {
|
||||
RouteRef,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
createExtensionOverrides,
|
||||
ExtensionDefinition,
|
||||
coreExtensionData,
|
||||
RouteRef,
|
||||
useRouteRef,
|
||||
createExtensionInput,
|
||||
IconComponent,
|
||||
createNavItemExtension,
|
||||
RouterBlueprint,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { createExtensionTester } from './createExtensionTester';
|
||||
|
||||
/**
|
||||
* Options to customize the behavior of the test app.
|
||||
@@ -44,8 +55,66 @@ 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>
|
||||
);
|
||||
};
|
||||
|
||||
export const TestAppNavExtension = createExtension({
|
||||
namespace: 'app',
|
||||
name: 'nav',
|
||||
attachTo: { id: 'app/layout', input: 'nav' },
|
||||
inputs: {
|
||||
items: createExtensionInput([createNavItemExtension.targetDataRef]),
|
||||
},
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory({ inputs }) {
|
||||
return [
|
||||
coreExtensionData.reactElement(
|
||||
<nav>
|
||||
<ul>
|
||||
{inputs.items.map((item, index) => {
|
||||
const { icon, title, routeRef } = item.get(
|
||||
createNavItemExtension.targetDataRef,
|
||||
);
|
||||
|
||||
return (
|
||||
<NavItem
|
||||
key={index}
|
||||
icon={icon}
|
||||
title={title}
|
||||
routeRef={routeRef}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>,
|
||||
),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Renders the given element in a test app, for use in unit tests.
|
||||
@@ -53,21 +122,32 @@ export type TestAppOptions = {
|
||||
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);
|
||||
): RenderResult {
|
||||
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('/'),
|
||||
];
|
||||
},
|
||||
}),
|
||||
RouterBlueprint.make({
|
||||
namespace: 'test',
|
||||
params: {
|
||||
Component: ({ children }) => <MemoryRouter>{children}</MemoryRouter>,
|
||||
},
|
||||
}),
|
||||
TestAppNavExtension,
|
||||
];
|
||||
|
||||
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 +164,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());
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ describe('EntityCardBlueprint', () => {
|
||||
filter: 'test',
|
||||
},
|
||||
}),
|
||||
).data(EntityCardBlueprint.dataRefs.filterExpression),
|
||||
).get(EntityCardBlueprint.dataRefs.filterExpression),
|
||||
).toBe('test');
|
||||
|
||||
expect(
|
||||
@@ -112,7 +112,7 @@ describe('EntityCardBlueprint', () => {
|
||||
},
|
||||
}),
|
||||
{ config: { filter: 'test' } },
|
||||
).data(EntityCardBlueprint.dataRefs.filterExpression),
|
||||
).get(EntityCardBlueprint.dataRefs.filterExpression),
|
||||
).toBe('test');
|
||||
|
||||
expect(
|
||||
@@ -124,7 +124,7 @@ describe('EntityCardBlueprint', () => {
|
||||
loader: async () => <div>Test!</div>,
|
||||
},
|
||||
}),
|
||||
).data(EntityCardBlueprint.dataRefs.filterFunction),
|
||||
).get(EntityCardBlueprint.dataRefs.filterFunction),
|
||||
).toBe(mockFilter);
|
||||
});
|
||||
|
||||
|
||||
@@ -122,10 +122,10 @@ describe('EntityContentBlueprint', () => {
|
||||
const tester = createExtensionTester(extension);
|
||||
|
||||
// todo(blam): route paths are always set to / in the createExtensionTester. This will work eventually.
|
||||
// expect(tester.data(coreExtensionData.routePath)).toBe('/test');
|
||||
// expect(tester.get(coreExtensionData.routePath)).toBe('/test');
|
||||
|
||||
expect(tester.data(coreExtensionData.routeRef)).toBe(mockRouteRef);
|
||||
expect(tester.data(EntityContentBlueprint.dataRefs.title)).toBe('Test');
|
||||
expect(tester.get(coreExtensionData.routeRef)).toBe(mockRouteRef);
|
||||
expect(tester.get(EntityContentBlueprint.dataRefs.title)).toBe('Test');
|
||||
});
|
||||
|
||||
it('should emit the correct filter output', () => {
|
||||
@@ -142,7 +142,7 @@ describe('EntityContentBlueprint', () => {
|
||||
filter: 'test',
|
||||
},
|
||||
}),
|
||||
).data(EntityContentBlueprint.dataRefs.filterExpression),
|
||||
).get(EntityContentBlueprint.dataRefs.filterExpression),
|
||||
).toBe('test');
|
||||
|
||||
expect(
|
||||
@@ -156,7 +156,7 @@ describe('EntityContentBlueprint', () => {
|
||||
},
|
||||
}),
|
||||
{ config: { filter: 'test' } },
|
||||
).data(EntityContentBlueprint.dataRefs.filterExpression),
|
||||
).get(EntityContentBlueprint.dataRefs.filterExpression),
|
||||
).toBe('test');
|
||||
|
||||
expect(
|
||||
@@ -170,7 +170,7 @@ describe('EntityContentBlueprint', () => {
|
||||
loader: async () => <div>Test!</div>,
|
||||
},
|
||||
}),
|
||||
).data(EntityContentBlueprint.dataRefs.filterFunction),
|
||||
).get(EntityContentBlueprint.dataRefs.filterFunction),
|
||||
).toBe(mockFilter);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user