Skip to content

Commit c43a9f1

Browse files
fralongoFrancesco Longo
andauthored
feat: Implement initial components tree utility (#137)
Co-authored-by: Francesco Longo <[email protected]>
1 parent b420fe8 commit c43a9f1

File tree

5 files changed

+181
-4
lines changed

5 files changed

+181
-4
lines changed

src/internal/analytics-metadata/__tests__/components.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import React from 'react';
4+
import React, { ReactNode } from 'react';
55
import { METADATA_ATTRIBUTE, getAnalyticsMetadataAttribute, getAnalyticsLabelAttribute } from '../attributes';
66

77
export const ComponentOne = ({ malformed }: { malformed?: boolean }) => (
@@ -33,7 +33,7 @@ export const ComponentOne = ({ malformed }: { malformed?: boolean }) => (
3333
</div>
3434
);
3535

36-
const ComponentTwo = () => (
36+
export const ComponentTwo = () => (
3737
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentTwo', label: '.component-label' } })}>
3838
<div className="component-label" {...getAnalyticsLabelAttribute('.sub-label')}>
3939
<div className="sub-label">sub label</div>
@@ -43,7 +43,7 @@ const ComponentTwo = () => (
4343
</div>
4444
);
4545

46-
export const ComponentThree = () => (
46+
export const ComponentThree = ({ children }: { children?: ReactNode }) => (
4747
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentThree' } })}>
4848
<div
4949
{...getAnalyticsMetadataAttribute({
@@ -61,6 +61,7 @@ export const ComponentThree = () => (
6161
<div data-awsui-referrer-id="id:nested:portal">
6262
<ComponentOne />
6363
</div>
64+
{children}
6465
</div>
6566
</div>
6667
);
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React from 'react';
5+
import { render } from '@testing-library/react';
6+
import { getComponentsTree } from '../utils';
7+
import { METADATA_ATTRIBUTE, activateAnalyticsMetadata } from '../attributes';
8+
import { ComponentOne, ComponentTwo, ComponentThree } from './components';
9+
10+
describe('getComponentsTree', () => {
11+
describe('with active analytics metadata', () => {
12+
beforeAll(() => {
13+
activateAnalyticsMetadata(true);
14+
});
15+
test('returns an empty array when input is null', () => {
16+
expect(getComponentsTree(null)).toEqual([]);
17+
});
18+
test('skips metadata that does not refer to a component', () => {
19+
const { container } = render(
20+
<div id="outer-target">
21+
<ComponentOne />
22+
</div>
23+
);
24+
const target = container.querySelector('#outer-target') as HTMLElement;
25+
expect(getComponentsTree(target)).toEqual([
26+
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' }, children: [] },
27+
]);
28+
});
29+
test('only includes components inside the specified element', () => {
30+
const { container } = render(
31+
<>
32+
<div id="outer-target-1">
33+
<ComponentOne />
34+
</div>
35+
<div id="outer-target-2">
36+
<ComponentTwo />
37+
</div>
38+
</>
39+
);
40+
expect(getComponentsTree(container.querySelector('#outer-target-1') as HTMLElement)).toEqual([
41+
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' }, children: [] },
42+
]);
43+
expect(getComponentsTree(container.querySelector('#outer-target-2') as HTMLElement)).toEqual([
44+
{ name: 'ComponentTwo', label: 'sub label', children: [] },
45+
]);
46+
});
47+
test('can include multiple components', () => {
48+
const { container } = render(
49+
<div id="outer-target-1">
50+
<ComponentThree />
51+
<ComponentTwo />
52+
</div>
53+
);
54+
expect(getComponentsTree(container.querySelector('#outer-target-1') as HTMLElement)).toEqual([
55+
{
56+
name: 'ComponentThree',
57+
children: [
58+
{ name: 'ComponentTwo', label: 'sub label', children: [] },
59+
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' }, children: [] },
60+
],
61+
},
62+
{ name: 'ComponentTwo', label: 'sub label', children: [] },
63+
]);
64+
});
65+
test('can include multiple nested components', () => {
66+
const { container } = render(
67+
<div id="outer-target-1">
68+
<ComponentThree>
69+
<ComponentThree />
70+
</ComponentThree>
71+
</div>
72+
);
73+
expect(getComponentsTree(container.querySelector('#outer-target-1') as HTMLElement)).toEqual([
74+
{
75+
name: 'ComponentThree',
76+
children: [
77+
{ name: 'ComponentTwo', label: 'sub label', children: [] },
78+
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' }, children: [] },
79+
{
80+
name: 'ComponentThree',
81+
children: [
82+
{ name: 'ComponentTwo', label: 'sub label', children: [] },
83+
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' }, children: [] },
84+
],
85+
},
86+
],
87+
},
88+
]);
89+
});
90+
test('use document as default element', () => {
91+
render(
92+
<>
93+
<ComponentThree />
94+
<ComponentTwo />
95+
</>
96+
);
97+
expect(getComponentsTree()).toEqual([
98+
{
99+
name: 'ComponentThree',
100+
children: [
101+
{ name: 'ComponentTwo', label: 'sub label', children: [] },
102+
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' }, children: [] },
103+
],
104+
},
105+
{ name: 'ComponentTwo', label: 'sub label', children: [] },
106+
]);
107+
});
108+
test('skips malformed metadata', () => {
109+
const { container } = render(
110+
<div id="outer-target" {...{ [METADATA_ATTRIBUTE]: "{'corruptedJSON':}" }}>
111+
<ComponentOne malformed={true} />
112+
</div>
113+
);
114+
const target = container.querySelector('#outer-target') as HTMLElement;
115+
expect(getComponentsTree(target)).toEqual([
116+
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' }, children: [] },
117+
]);
118+
});
119+
});
120+
121+
describe('with inactive analytics metadata', () => {
122+
beforeAll(() => {
123+
activateAnalyticsMetadata(false);
124+
});
125+
test('returns an empty object', () => {
126+
const { container } = render(<ComponentThree />);
127+
const target = container.querySelector('#target') as HTMLElement;
128+
expect(getComponentsTree(target)).toEqual([]);
129+
});
130+
});
131+
});

src/internal/analytics-metadata/interfaces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export interface GeneratedAnalyticsMetadata {
1212
contexts: Array<GeneratedAnalyticsMetadataComponentContext>;
1313
}
1414

15-
interface GeneratedAnalyticsMetadataComponent {
15+
export interface GeneratedAnalyticsMetadataComponent {
1616
// name of the component. For example: "awsui.RadioGroup". We prefix the actual name with awsui to account for future tagging of custom components
1717
name: string;
1818

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { METADATA_ATTRIBUTE } from './attributes';
5+
import { isNodeComponent } from './dom-utils';
6+
import { GeneratedAnalyticsMetadataComponent } from './interfaces';
7+
import { getGeneratedAnalyticsMetadata } from './utils';
8+
9+
interface GeneratedAnalyticsMetadataComponentTree extends GeneratedAnalyticsMetadataComponent {
10+
children?: Array<GeneratedAnalyticsMetadataComponentTree>;
11+
}
12+
13+
const getComponentsArray = (node: HTMLElement | Document = document) => {
14+
const elementsWithMetadata = Array.from(node.querySelectorAll(`[${METADATA_ATTRIBUTE}]`)) as Array<HTMLElement>;
15+
return elementsWithMetadata.filter(isNodeComponent);
16+
};
17+
18+
const getComponentsTreeRecursive = (
19+
node: HTMLElement | Document,
20+
visited: Set<HTMLElement>
21+
): Array<GeneratedAnalyticsMetadataComponentTree> => {
22+
const tree: Array<GeneratedAnalyticsMetadataComponentTree> = [];
23+
const componentNodes = getComponentsArray(node);
24+
componentNodes.forEach(componentNode => {
25+
if (visited.has(componentNode)) {
26+
return;
27+
}
28+
visited.add(componentNode);
29+
tree.push({
30+
...getGeneratedAnalyticsMetadata(componentNode).contexts[0].detail,
31+
children: getComponentsTreeRecursive(componentNode, visited),
32+
});
33+
});
34+
return tree;
35+
};
36+
37+
export const getComponentsTree = (
38+
node: HTMLElement | Document | null = document
39+
): Array<GeneratedAnalyticsMetadataComponentTree> => {
40+
if (!node) {
41+
return [];
42+
}
43+
return getComponentsTreeRecursive(node, new Set());
44+
};

src/internal/analytics-metadata/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
export { getRawAnalyticsMetadata } from './testing-utils';
5+
export { getComponentsTree } from './page-scanner-utils';
56

67
import { METADATA_DATA_ATTRIBUTE } from './attributes';
78
import { GeneratedAnalyticsMetadata, GeneratedAnalyticsMetadataFragment } from './interfaces';

0 commit comments

Comments
 (0)