Add extension snapshot testing support to frontend-test-utils
Adds the snapshot() method to ExtensionTester, enabling snapshot testing of extension tree structures. The snapshots use a tree-shaped format that mirrors the extension hierarchy, with empty fields and default values omitted for clarity. Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -188,4 +188,161 @@ describe('createExtensionTester', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
it('should return a snapshot of the extension tree', () => {
|
||||
const extension = createExtension({
|
||||
name: 'root',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: [stringDataRef],
|
||||
factory: () => [stringDataRef('test-text')],
|
||||
});
|
||||
|
||||
const tester = createExtensionTester(extension);
|
||||
|
||||
expect(tester.snapshot()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": "root",
|
||||
"outputs": [
|
||||
"test.string",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should include child extensions in the tree', () => {
|
||||
const childInput = createExtensionInput([stringDataRef]);
|
||||
|
||||
const rootExtension = createExtension({
|
||||
name: 'root',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
children: childInput,
|
||||
},
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: () => [coreExtensionData.reactElement(<div>root</div>)],
|
||||
});
|
||||
|
||||
const childExtension = createExtension({
|
||||
name: 'child',
|
||||
attachTo: { id: 'root', input: 'children' },
|
||||
output: [stringDataRef],
|
||||
factory: () => [stringDataRef('child-data')],
|
||||
});
|
||||
|
||||
const tester = createExtensionTester(rootExtension).add(childExtension);
|
||||
|
||||
expect(tester.snapshot()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"children": {
|
||||
"children": [
|
||||
{
|
||||
"id": "child",
|
||||
"outputs": [
|
||||
"test.string",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
"id": "root",
|
||||
"outputs": [
|
||||
"core.reactElement",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should include multiple children in sorted order', () => {
|
||||
const childInput = createExtensionInput([stringDataRef]);
|
||||
|
||||
const rootExtension = createExtension({
|
||||
name: 'root',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
children: childInput,
|
||||
},
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: () => [coreExtensionData.reactElement(<div>root</div>)],
|
||||
});
|
||||
|
||||
const child1 = createExtension({
|
||||
name: 'child1',
|
||||
attachTo: { id: 'root', input: 'children' },
|
||||
output: [stringDataRef],
|
||||
factory: () => [stringDataRef('child1-data')],
|
||||
});
|
||||
|
||||
const child2 = createExtension({
|
||||
name: 'child2',
|
||||
attachTo: { id: 'root', input: 'children' },
|
||||
output: [stringDataRef],
|
||||
factory: () => [stringDataRef('child2-data')],
|
||||
});
|
||||
|
||||
const tester = createExtensionTester(rootExtension)
|
||||
.add(child1)
|
||||
.add(child2);
|
||||
|
||||
expect(tester.snapshot()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"children": {
|
||||
"children": [
|
||||
{
|
||||
"id": "child1",
|
||||
"outputs": [
|
||||
"test.string",
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "child2",
|
||||
"outputs": [
|
||||
"test.string",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
"id": "root",
|
||||
"outputs": [
|
||||
"core.reactElement",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should omit empty children and outputs', () => {
|
||||
const extension = createExtension({
|
||||
name: 'root',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: [],
|
||||
factory: () => [],
|
||||
});
|
||||
|
||||
const tester = createExtensionTester(extension);
|
||||
|
||||
expect(tester.snapshot()).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": "root",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should produce serializable snapshot data', () => {
|
||||
const extension = createExtension({
|
||||
name: 'root',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: [stringDataRef],
|
||||
factory: () => [stringDataRef('test-text')],
|
||||
});
|
||||
|
||||
const tester = createExtensionTester(extension);
|
||||
const snapshot = tester.snapshot();
|
||||
|
||||
expect(snapshot).toEqual({
|
||||
id: 'root',
|
||||
outputs: ['test.string'],
|
||||
});
|
||||
|
||||
expect(JSON.parse(JSON.stringify(snapshot))).toEqual(snapshot);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,22 @@ import { createErrorCollector } from '../../../frontend-app-api/src/wiring/creat
|
||||
import { OpaqueExtensionDefinition } from '@internal/frontend';
|
||||
import { TestApiRegistry, type TestApiPairs } from '../utils';
|
||||
|
||||
/**
|
||||
* Represents a snapshot of an extension in the app tree.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ExtensionSnapshotNode {
|
||||
/** The ID of the extension */
|
||||
id: string;
|
||||
/** The IDs of output data refs produced by this extension */
|
||||
outputs?: string[];
|
||||
/** Child extensions organized by input name */
|
||||
children?: Record<string, ExtensionSnapshotNode[]>;
|
||||
/** Whether this extension is disabled */
|
||||
disabled?: true;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export class ExtensionQuery<UOutput extends ExtensionDataRef> {
|
||||
#node: AppNode;
|
||||
@@ -192,6 +208,54 @@ export class ExtensionTester<UOutput extends ExtensionDataRef> {
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a snapshot of the extension tree structure for testing and debugging.
|
||||
* Convenient to use with Jest's inline snapshot testing.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const tester = createExtensionTester(myExtension);
|
||||
* expect(tester.snapshot()).toMatchInlineSnapshot();
|
||||
* ```
|
||||
*/
|
||||
snapshot(): ExtensionSnapshotNode {
|
||||
const tree = this.#resolveTree();
|
||||
|
||||
const buildNode = (node: AppNode): ExtensionSnapshotNode => {
|
||||
const outputs = node.instance
|
||||
? Array.from(node.instance.getDataRefs())
|
||||
.map(ref => ref.id)
|
||||
.sort()
|
||||
: [];
|
||||
|
||||
const children: Record<string, ExtensionSnapshotNode[]> = {};
|
||||
for (const [inputName, attachedNodes] of node.edges.attachments) {
|
||||
children[inputName] = attachedNodes
|
||||
.map(n => buildNode(n))
|
||||
.sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
const result: ExtensionSnapshotNode = {
|
||||
id: node.spec.id,
|
||||
};
|
||||
|
||||
// Only include non-empty/non-default fields
|
||||
if (outputs.length > 0) {
|
||||
result.outputs = outputs;
|
||||
}
|
||||
if (Object.keys(children).length > 0) {
|
||||
result.children = children;
|
||||
}
|
||||
if (node.spec.disabled) {
|
||||
result.disabled = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return buildNode(tree.root);
|
||||
}
|
||||
|
||||
#resolveTree() {
|
||||
if (this.#tree) {
|
||||
return this.#tree;
|
||||
|
||||
@@ -18,6 +18,7 @@ export {
|
||||
createExtensionTester,
|
||||
type ExtensionTester,
|
||||
type ExtensionQuery,
|
||||
type ExtensionSnapshotNode,
|
||||
} from './createExtensionTester';
|
||||
|
||||
export { renderInTestApp, type TestAppOptions } from './renderInTestApp';
|
||||
|
||||
Reference in New Issue
Block a user