Skip to content

Commit 837fb68

Browse files
fralongoFrancesco Longo
andauthored
feat: Add support for portals in page scanner utility (#142)
Co-authored-by: Francesco Longo <[email protected]>
1 parent bc718ac commit 837fb68

File tree

6 files changed

+198
-37
lines changed

6 files changed

+198
-37
lines changed

src/internal/analytics-metadata/__tests__/dom-utils.test.tsx

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import React from 'react';
55
import { render } from '@testing-library/react';
66
import { activateAnalyticsMetadata, getAnalyticsMetadataAttribute, METADATA_ATTRIBUTE } from '../attributes';
7-
import { findLogicalParent, isNodeComponent, findComponentUp, findSelectorUp } from '../dom-utils';
7+
import { findLogicalParent, isNodeComponent, findComponentUpUntil, findSelectorUp } from '../dom-utils';
88

99
beforeAll(() => {
1010
activateAnalyticsMetadata(true);
@@ -78,17 +78,17 @@ describe('isNodeComponent', () => {
7878
});
7979
});
8080

81-
describe('findComponentUp', () => {
81+
describe('findComponentUpUntil', () => {
8282
test('returns null when input is null', () => {
83-
expect(findComponentUp(null)).toBeNull();
83+
expect(findComponentUpUntil(null)).toBeNull();
8484
});
8585
test('returns parent component element', () => {
8686
const { container } = render(
8787
<div id="component-element" {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentName' } })}>
8888
<div id="target-element"></div>
8989
</div>
9090
);
91-
expect(findComponentUp(container.querySelector('#target-element'))!.id).toBe('component-element');
91+
expect(findComponentUpUntil(container.querySelector('#target-element'))!.id).toBe('component-element');
9292
});
9393
test('returns parent component element with portals', () => {
9494
const { container } = render(
@@ -101,15 +101,62 @@ describe('findComponentUp', () => {
101101
</div>
102102
</div>
103103
);
104-
expect(findComponentUp(container.querySelector('#target-element'))!.id).toBe('component-element');
104+
expect(findComponentUpUntil(container.querySelector('#target-element'))!.id).toBe('component-element');
105105
});
106106
test('returns null when element has no parent component', () => {
107107
const { container } = render(
108108
<div>
109109
<div id="target-element"></div>
110110
</div>
111111
);
112-
expect(findComponentUp(container.querySelector('#target-element'))).toBeNull();
112+
expect(findComponentUpUntil(container.querySelector('#target-element'))).toBeNull();
113+
});
114+
test('with `until` argument', () => {
115+
const { container } = render(
116+
<>
117+
<div id="outer-element">
118+
<div id="component-element" {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentName' } })}>
119+
<div id="another-until">
120+
<div id=":rr5:"></div>
121+
</div>
122+
<div id="target-element"></div>
123+
</div>
124+
</div>
125+
<div data-awsui-referrer-id=":rr5:">
126+
<div id="another-target-element"></div>
127+
</div>
128+
</>
129+
);
130+
expect(
131+
findComponentUpUntil(
132+
container.querySelector('#target-element'),
133+
container.querySelector('#outer-element') as HTMLElement
134+
)!.id
135+
).toBe('component-element');
136+
expect(
137+
findComponentUpUntil(
138+
container.querySelector('#target-element'),
139+
container.querySelector('#target-element') as HTMLElement
140+
)
141+
).toBeNull();
142+
expect(
143+
findComponentUpUntil(
144+
container.querySelector('#target-element'),
145+
container.querySelector('#component-element') as HTMLElement
146+
)!.id
147+
).toBe('component-element');
148+
expect(
149+
findComponentUpUntil(
150+
container.querySelector('#another-target-element'),
151+
container.querySelector('#outer-element') as HTMLElement
152+
)!.id
153+
).toBe('component-element');
154+
expect(
155+
findComponentUpUntil(
156+
container.querySelector('#another-target-element'),
157+
container.querySelector('#another-until') as HTMLElement
158+
)
159+
).toBeNull();
113160
});
114161
});
115162

src/internal/analytics-metadata/__tests__/page-scanner-utils.test.tsx

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import React from 'react';
55
import { render } from '@testing-library/react';
66
import { getComponentsTree } from '../utils';
7-
import { METADATA_ATTRIBUTE, activateAnalyticsMetadata } from '../attributes';
7+
import { METADATA_ATTRIBUTE, activateAnalyticsMetadata, getAnalyticsMetadataAttribute } from '../attributes';
88
import { ComponentOne, ComponentTwo, ComponentThree } from './components';
99

1010
describe('getComponentsTree', () => {
@@ -55,8 +55,11 @@ describe('getComponentsTree', () => {
5555
{
5656
name: 'ComponentThree',
5757
children: [
58-
{ name: 'ComponentTwo', label: 'sub label' },
59-
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
58+
{
59+
name: 'ComponentTwo',
60+
label: 'sub label',
61+
children: [{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } }],
62+
},
6063
],
6164
},
6265
{ name: 'ComponentTwo', label: 'sub label' },
@@ -66,23 +69,20 @@ describe('getComponentsTree', () => {
6669
const { container } = render(
6770
<div id="outer-target-1">
6871
<ComponentThree>
69-
<ComponentThree />
72+
<ComponentTwo />
7073
</ComponentThree>
7174
</div>
7275
);
7376
expect(getComponentsTree(container.querySelector('#outer-target-1') as HTMLElement)).toEqual([
7477
{
7578
name: 'ComponentThree',
7679
children: [
77-
{ name: 'ComponentTwo', label: 'sub label' },
78-
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
7980
{
80-
name: 'ComponentThree',
81-
children: [
82-
{ name: 'ComponentTwo', label: 'sub label' },
83-
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
84-
],
81+
name: 'ComponentTwo',
82+
label: 'sub label',
83+
children: [{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } }],
8584
},
85+
{ name: 'ComponentTwo', label: 'sub label' },
8686
],
8787
},
8888
]);
@@ -98,8 +98,11 @@ describe('getComponentsTree', () => {
9898
{
9999
name: 'ComponentThree',
100100
children: [
101-
{ name: 'ComponentTwo', label: 'sub label' },
102-
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
101+
{
102+
name: 'ComponentTwo',
103+
label: 'sub label',
104+
children: [{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } }],
105+
},
103106
],
104107
},
105108
{ name: 'ComponentTwo', label: 'sub label' },
@@ -116,6 +119,83 @@ describe('getComponentsTree', () => {
116119
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
117120
]);
118121
});
122+
describe('with portals', () => {
123+
test('returns an empty array when portal outside of the node element', () => {
124+
const { container } = render(
125+
<div id="outer-target">
126+
<div id="id:portal-1"></div>
127+
<div data-awsui-referrer-id="id:portal-1">
128+
<ComponentOne />
129+
</div>
130+
<div id="inner-target"></div>
131+
</div>
132+
);
133+
const target = container.querySelector('#inner-target') as HTMLElement;
134+
expect(getComponentsTree(target)).toEqual([]);
135+
});
136+
test('returns nested portal correctly', () => {
137+
const { container } = render(
138+
<div id="outer-target">
139+
<div data-awsui-referrer-id="id:portal-1">
140+
<ComponentOne />
141+
</div>
142+
<div id="inner-target">
143+
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentFour' } })}>
144+
<ComponentTwo />
145+
<div id="id:portal-1"></div>
146+
</div>
147+
</div>
148+
</div>
149+
);
150+
expect(getComponentsTree(container.querySelector('#outer-target') as HTMLElement)).toEqual([
151+
{
152+
name: 'ComponentFour',
153+
children: [
154+
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
155+
{ name: 'ComponentTwo', label: 'sub label' },
156+
],
157+
},
158+
]);
159+
expect(getComponentsTree(container.querySelector('#inner-target') as HTMLElement)).toEqual([
160+
{
161+
name: 'ComponentFour',
162+
children: [
163+
{ name: 'ComponentTwo', label: 'sub label' },
164+
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
165+
],
166+
},
167+
]);
168+
});
169+
test('returns recursively nested portals', () => {
170+
const { container } = render(
171+
<div id="outer-target">
172+
<div data-awsui-referrer-id="id:portal-1">
173+
<ComponentOne />
174+
<div id="id:portal-2"></div>
175+
</div>
176+
<div data-awsui-referrer-id="id:portal-2">
177+
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentFive' } })} />
178+
</div>
179+
<div id="inner-target">
180+
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentFour' } })}>
181+
<ComponentTwo />
182+
<div id="id:portal-1"></div>
183+
</div>
184+
</div>
185+
</div>
186+
);
187+
expect(getComponentsTree(container.querySelector('#inner-target') as HTMLElement)).toEqual([
188+
{
189+
name: 'ComponentFour',
190+
children: [
191+
{ name: 'ComponentTwo', label: 'sub label' },
192+
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
193+
{ name: 'ComponentFive' },
194+
],
195+
},
196+
]);
197+
});
198+
});
119199
});
120200

121201
describe('with inactive analytics metadata', () => {

src/internal/analytics-metadata/attributes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { getGlobalFlag } from '../global-flags';
77
export const METADATA_DATA_ATTRIBUTE = 'awsuiAnalytics';
88
export const METADATA_ATTRIBUTE = 'data-awsui-analytics';
99
export const LABEL_DATA_ATTRIBUTE = 'awsuiAnalyticsLabel';
10+
export const REFERRER_DATA_ATTRIBUTE = 'awsuiReferrerId';
11+
export const REFERRER_ATTRIBUTE = 'data-awsui-referrer-id';
1012
const LABEL_ATTRIBUTE = 'data-awsui-analytics-label';
1113

1214
let activated = getGlobalFlag('analyticsMetadata');

src/internal/analytics-metadata/dom-utils.ts

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

4-
import { METADATA_DATA_ATTRIBUTE } from './attributes';
4+
import { METADATA_DATA_ATTRIBUTE, REFERRER_DATA_ATTRIBUTE } from './attributes';
55

66
export const findLogicalParent = (node: HTMLElement): HTMLElement | null => {
77
try {
8-
const referrer = node.dataset.awsuiReferrerId;
8+
const referrer = node.dataset[REFERRER_DATA_ATTRIBUTE];
99
if (referrer) {
1010
return document.querySelector(`[id="${referrer}"]`);
1111
}
@@ -15,12 +15,12 @@ export const findLogicalParent = (node: HTMLElement): HTMLElement | null => {
1515
}
1616
};
1717

18-
export function findComponentUp(node: HTMLElement | null): HTMLElement | null {
18+
export function findComponentUpUntil(node: HTMLElement | null, until: HTMLElement = document.body): HTMLElement | null {
1919
let firstComponentElement = node;
20-
while (firstComponentElement && firstComponentElement.tagName !== 'body' && !isNodeComponent(firstComponentElement)) {
20+
while (firstComponentElement && firstComponentElement !== until && !isNodeComponent(firstComponentElement)) {
2121
firstComponentElement = findLogicalParent(firstComponentElement);
2222
}
23-
return firstComponentElement && firstComponentElement.tagName !== 'body' ? firstComponentElement : null;
23+
return firstComponentElement && isNodeComponent(firstComponentElement) ? firstComponentElement : null;
2424
}
2525

2626
export const isNodeComponent = (node: HTMLElement): boolean => {

src/internal/analytics-metadata/labels-utils.ts

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

44
import { LABEL_DATA_ATTRIBUTE } from './attributes';
5-
import { findSelectorUp, findComponentUp } from './dom-utils';
5+
import { findSelectorUp, findComponentUpUntil } from './dom-utils';
66
import { LabelIdentifier } from './interfaces';
77

88
export const processLabel = (node: HTMLElement | null, labelIdentifier: string | LabelIdentifier | null): string => {
@@ -52,7 +52,7 @@ const processSingleLabel = (
5252
return processSingleLabel(findSelectorUp(node, rootSelector), labelSelector);
5353
}
5454
if (root === 'component') {
55-
return processSingleLabel(findComponentUp(node), labelSelector);
55+
return processSingleLabel(findComponentUpUntil(node), labelSelector);
5656
}
5757
if (root === 'body') {
5858
return processSingleLabel(document.body, labelSelector);
Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { METADATA_ATTRIBUTE } from './attributes';
5-
import { isNodeComponent } from './dom-utils';
4+
import { METADATA_ATTRIBUTE, REFERRER_ATTRIBUTE, REFERRER_DATA_ATTRIBUTE } from './attributes';
5+
import { findComponentUpUntil, isNodeComponent } from './dom-utils';
66
import { getGeneratedAnalyticsMetadata } from './utils';
77

88
interface GeneratedAnalyticsMetadataComponentTree {
@@ -12,26 +12,57 @@ interface GeneratedAnalyticsMetadataComponentTree {
1212
children?: Array<GeneratedAnalyticsMetadataComponentTree>;
1313
}
1414

15+
interface ComponentsMap {
16+
roots: Array<HTMLElement>;
17+
parents: Map<HTMLElement, Array<HTMLElement>>;
18+
}
19+
20+
const findPortalsOutsideOfNode = (node: HTMLElement): Array<HTMLElement> =>
21+
(Array.from(document.querySelectorAll(`[${REFERRER_ATTRIBUTE}]`)) as Array<HTMLElement>).filter(element => {
22+
const referrer = element.dataset[REFERRER_DATA_ATTRIBUTE];
23+
return !!node.querySelector(`[id="${referrer}"]`) && !node.querySelector(`[${REFERRER_ATTRIBUTE}="${referrer}"]`);
24+
});
25+
1526
const getComponentsArray = (node: HTMLElement | Document = document) => {
1627
const elementsWithMetadata = Array.from(node.querySelectorAll(`[${METADATA_ATTRIBUTE}]`)) as Array<HTMLElement>;
28+
findPortalsOutsideOfNode(node as HTMLElement).forEach(portal => {
29+
elementsWithMetadata.push(...getComponentsArray(portal));
30+
});
1731
return elementsWithMetadata.filter(isNodeComponent);
1832
};
1933

34+
const buildComponentsMap = (node: HTMLElement | Document = document) => {
35+
const componentsArray = getComponentsArray(node);
36+
const map: ComponentsMap = {
37+
roots: [],
38+
parents: new Map<HTMLElement, Array<HTMLElement>>(),
39+
};
40+
componentsArray.forEach(element => {
41+
const parent = element.parentElement ? findComponentUpUntil(element.parentElement, node as HTMLElement) : null;
42+
if (!parent) {
43+
map.roots.push(element);
44+
} else {
45+
if (!map.parents.has(parent)) {
46+
map.parents.set(parent, []);
47+
}
48+
map.parents.get(parent)?.push(element);
49+
}
50+
});
51+
return map;
52+
};
53+
2054
const getComponentsTreeRecursive = (
21-
node: HTMLElement | Document,
22-
visited: Set<HTMLElement>
55+
componentNodes: Array<HTMLElement>,
56+
parentsMap: Map<HTMLElement, Array<HTMLElement>>
2357
): Array<GeneratedAnalyticsMetadataComponentTree> => {
2458
const tree: Array<GeneratedAnalyticsMetadataComponentTree> = [];
25-
const componentNodes = getComponentsArray(node);
2659
componentNodes.forEach(componentNode => {
27-
if (visited.has(componentNode)) {
28-
return;
29-
}
30-
visited.add(componentNode);
3160
const treeItem: GeneratedAnalyticsMetadataComponentTree = {
3261
...getGeneratedAnalyticsMetadata(componentNode).contexts[0].detail,
3362
};
34-
const children = getComponentsTreeRecursive(componentNode, visited);
63+
const children = parentsMap.has(componentNode)
64+
? getComponentsTreeRecursive(parentsMap.get(componentNode)!, parentsMap)
65+
: [];
3566
if (children.length > 0) {
3667
treeItem.children = children;
3768
}
@@ -46,5 +77,6 @@ export const getComponentsTree = (
4677
if (!node) {
4778
return [];
4879
}
49-
return getComponentsTreeRecursive(node, new Set());
80+
const { roots, parents } = buildComponentsMap(node);
81+
return getComponentsTreeRecursive(roots, parents);
5082
};

0 commit comments

Comments
 (0)