Skip to content

Commit 9cb7d0e

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

File tree

10 files changed

+287
-9
lines changed

10 files changed

+287
-9
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import useBrowser from '@cloudscape-design/browser-test-tools/use-browser';
5+
import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects';
6+
import { Browser } from 'webdriverio';
7+
import { getComponentsTree } from '../page-scanner-utils';
8+
9+
// Extend the Window interface to include the getComponentsTree property
10+
declare global {
11+
interface Window {
12+
getComponentsTree: typeof getComponentsTree;
13+
}
14+
}
15+
16+
describe('getComponentsTree', () => {
17+
const setupTest = (testFn: (page: BasePageObject, browser: Browser) => Promise<void>) => {
18+
return useBrowser(async browser => {
19+
const page = new BasePageObject(browser);
20+
await browser.url('/with-iframe');
21+
await testFn(page, browser);
22+
});
23+
};
24+
25+
test(
26+
'gets component metadata, including iframes',
27+
setupTest(async (page, browser) => {
28+
await page.runInsideIframe('#iframe-1', true, async () => {
29+
await page.runInsideIframe('#iframe-2', true, async () => {
30+
await page.waitForVisible('#sub-sub-target');
31+
});
32+
});
33+
const tree = await browser.execute(() => {
34+
return window.getComponentsTree();
35+
});
36+
expect(tree).toEqual([
37+
{
38+
name: 'ComponentOne',
39+
children: [
40+
{
41+
name: 'ComponentTwo',
42+
children: [
43+
{ name: 'ComponentTwoInPortal' },
44+
{ name: 'ComponentThree', children: [{ name: 'ComponentThreeInPortal' }] },
45+
],
46+
},
47+
],
48+
},
49+
]);
50+
})
51+
);
52+
test(
53+
'gets component metadata of sub-tree inside an iframe',
54+
setupTest(async (page, browser) => {
55+
await page.runInsideIframe('#iframe-1', true, async () => {
56+
await page.runInsideIframe('#iframe-2', true, async () => {
57+
await page.waitForVisible('#sub-sub-target');
58+
});
59+
});
60+
const tree = await browser.execute(() => {
61+
const iframe = document.querySelector('#iframe-1') as HTMLIFrameElement;
62+
const iframeDocument = iframe!.contentDocument;
63+
const subTarget = iframeDocument!.querySelector('#sub-target') as HTMLElement;
64+
return window.getComponentsTree(subTarget);
65+
});
66+
expect(tree).toEqual([
67+
{ name: 'ComponentTwoInPortal' },
68+
{ name: 'ComponentThree', children: [{ name: 'ComponentThreeInPortal' }] },
69+
]);
70+
})
71+
);
72+
});

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

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

4-
import React, { ReactNode } from 'react';
4+
import React, { ReactNode, useEffect, useRef } from 'react';
55
import { METADATA_ATTRIBUTE, getAnalyticsMetadataAttribute, getAnalyticsLabelAttribute } from '../attributes';
6+
import ReactDOM from 'react-dom';
67

78
export const ComponentOne = ({ malformed }: { malformed?: boolean }) => (
89
<div
@@ -65,3 +66,82 @@ export const ComponentThree = ({ children }: { children?: ReactNode }) => (
6566
</div>
6667
</div>
6768
);
69+
70+
const NestedIframe = () => {
71+
const ref = useRef<HTMLDivElement>(null);
72+
73+
useEffect(() => {
74+
const container = ref.current;
75+
if (!container) {
76+
return;
77+
}
78+
const iframeEl = container.ownerDocument.createElement('iframe');
79+
iframeEl.id = 'iframe-2';
80+
container.appendChild(iframeEl);
81+
82+
const iframeDocument = iframeEl.contentDocument!;
83+
iframeDocument.open();
84+
iframeDocument.writeln('<!DOCTYPE html>');
85+
iframeDocument.close();
86+
iframeDocument.body.innerHTML =
87+
'<div><div data-awsui-analytics="{&quot;component&quot;:{&quot;name&quot;:&quot;ComponentThree&quot;}}"><div id="sub-sub-target">inside iframe inside iframe</div><div id="id:portal-2"></div></div><div data-awsui-referrer-id="id:portal-2"><div data-awsui-analytics="{&quot;component&quot;:{&quot;name&quot;:&quot;ComponentThreeInPortal&quot;}}"> </div></div></div>';
88+
89+
return () => {
90+
container.removeChild(iframeEl);
91+
};
92+
});
93+
94+
return (
95+
<>
96+
<h1>Nested title</h1>
97+
<div
98+
{...getAnalyticsMetadataAttribute({
99+
component: { name: 'ComponentTwo', label: { selector: 'h1', root: 'body' } },
100+
})}
101+
>
102+
<div>inside iframe</div>
103+
<div id="sub-target">
104+
<div ref={ref}></div>;<div id="id:portal-1"></div>
105+
</div>
106+
</div>
107+
<div data-awsui-referrer-id="id:portal-1">
108+
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentTwoInPortal' } })}> </div>
109+
</div>
110+
</>
111+
);
112+
};
113+
114+
export const AppWithIframe = () => {
115+
const ref = useRef<HTMLDivElement>(null);
116+
117+
useEffect(() => {
118+
const container = ref.current;
119+
if (!container) {
120+
return;
121+
}
122+
const iframeEl = container.ownerDocument.createElement('iframe');
123+
iframeEl.id = 'iframe-1';
124+
container.appendChild(iframeEl);
125+
126+
const iframeDocument = iframeEl.contentDocument!;
127+
iframeDocument.open();
128+
iframeDocument.writeln('<!DOCTYPE html>');
129+
iframeDocument.close();
130+
131+
const innerAppRoot = iframeDocument.createElement('div');
132+
iframeDocument.body.appendChild(innerAppRoot);
133+
ReactDOM.render(<NestedIframe />, innerAppRoot);
134+
return () => {
135+
ReactDOM.unmountComponentAtNode(innerAppRoot);
136+
container.removeChild(iframeEl);
137+
};
138+
});
139+
140+
return (
141+
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentOne' } })}>
142+
<h1>Main title</h1>
143+
<div ref={ref}></div>;
144+
<iframe src="https://www.amazon.com/" />
145+
</div>
146+
);
147+
};

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import React from 'react';
55
import { render } from '@testing-library/react';
66
import { getComponentsTree } from '../utils';
77
import { METADATA_ATTRIBUTE, activateAnalyticsMetadata, getAnalyticsMetadataAttribute } from '../attributes';
8-
import { ComponentOne, ComponentTwo, ComponentThree } from './components';
8+
import { AppWithIframe, ComponentOne, ComponentTwo, ComponentThree } from './components';
99

1010
describe('getComponentsTree', () => {
1111
describe('with active analytics metadata', () => {
@@ -196,6 +196,31 @@ describe('getComponentsTree', () => {
196196
]);
197197
});
198198
});
199+
test('with iframes', () => {
200+
const { container } = render(<AppWithIframe />);
201+
expect(getComponentsTree()).toEqual([
202+
{
203+
name: 'ComponentOne',
204+
children: [
205+
{
206+
name: 'ComponentTwo',
207+
label: 'Nested title',
208+
children: [
209+
{ name: 'ComponentTwoInPortal' },
210+
{ name: 'ComponentThree', children: [{ name: 'ComponentThreeInPortal' }] },
211+
],
212+
},
213+
],
214+
},
215+
]);
216+
const subTarget = (container.querySelector('#iframe-1') as HTMLIFrameElement)!.contentDocument!.querySelector(
217+
'#sub-target'
218+
)!;
219+
expect(getComponentsTree(subTarget as HTMLElement)).toEqual([
220+
{ name: 'ComponentTwoInPortal' },
221+
{ name: 'ComponentThree', children: [{ name: 'ComponentThreeInPortal' }] },
222+
]);
223+
});
199224
});
200225

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const findLogicalParent = (node: HTMLElement): HTMLElement | null => {
77
try {
88
const referrer = node.dataset[REFERRER_DATA_ATTRIBUTE];
99
if (referrer) {
10-
return document.querySelector(`[id="${referrer}"]`);
10+
return (node.ownerDocument || node).querySelector(`[id="${referrer}"]`);
1111
}
1212
return node.parentElement;
1313
} catch (ex) {

src/internal/analytics-metadata/index.ts

Lines changed: 1 addition & 1 deletion
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-
export { GeneratedAnalyticsMetadataFragment, GeneratedAnalyticsMetadata, LabelIdentifier } from './interfaces';
4+
export type { GeneratedAnalyticsMetadataFragment, GeneratedAnalyticsMetadata, LabelIdentifier } from './interfaces';
55
export {
66
getAnalyticsMetadataAttribute,
77
copyAnalyticsMetadataAttribute,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const processSingleLabel = (
5555
return processSingleLabel(findComponentUpUntil(node), labelSelector);
5656
}
5757
if (root === 'body') {
58-
return processSingleLabel(document.body, labelSelector);
58+
return processSingleLabel(node.ownerDocument.body, labelSelector);
5959
}
6060
let labelElement: HTMLElement | null = node;
6161
if (labelSelector) {

src/internal/analytics-metadata/page-scanner-utils.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,15 @@ interface ComponentsMap {
1818
}
1919

2020
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-
});
21+
(Array.from((node.ownerDocument || node).querySelectorAll(`[${REFERRER_ATTRIBUTE}]`)) as Array<HTMLElement>).filter(
22+
element => {
23+
const referrer = element.dataset[REFERRER_DATA_ATTRIBUTE];
24+
return !!node.querySelector(`[id="${referrer}"]`) && !node.querySelector(`[${REFERRER_ATTRIBUTE}="${referrer}"]`);
25+
}
26+
);
27+
28+
const findAccessibleIframes = (node: HTMLElement | Document): Array<HTMLIFrameElement> =>
29+
Array.from(node.querySelectorAll('iframe')).filter(iframe => !!iframe.contentDocument);
2530

2631
const getComponentsArray = (node: HTMLElement | Document = document) => {
2732
const elementsWithMetadata = Array.from(node.querySelectorAll(`[${METADATA_ATTRIBUTE}]`)) as Array<HTMLElement>;
@@ -48,9 +53,36 @@ const buildComponentsMap = (node: HTMLElement | Document = document) => {
4853
map.parents.get(parent)?.push(element);
4954
}
5055
});
56+
findAccessibleIframes(node).forEach(
57+
iframe =>
58+
iframe.contentDocument &&
59+
mergeComponentsMaps(
60+
map,
61+
findComponentUpUntil(iframe, node as HTMLElement),
62+
buildComponentsMap(iframe.contentDocument)
63+
)
64+
);
5165
return map;
5266
};
5367

68+
const mergeComponentsMaps = (
69+
parentMap: ComponentsMap,
70+
parentComponent: HTMLElement | null,
71+
childMap: ComponentsMap
72+
): void => {
73+
parentMap.parents = new Map([...parentMap.parents, ...childMap.parents]);
74+
if (childMap.roots.length > 0) {
75+
if (parentComponent) {
76+
if (!parentMap.parents.has(parentComponent)) {
77+
parentMap.parents.set(parentComponent, []);
78+
}
79+
childMap.roots.forEach(root => parentMap.parents.get(parentComponent)?.push(root));
80+
} else {
81+
parentMap.roots = [...parentMap.roots, ...childMap.roots];
82+
}
83+
}
84+
};
85+
5486
const getComponentsTreeRecursive = (
5587
componentNodes: Array<HTMLElement>,
5688
parentsMap: Map<HTMLElement, Array<HTMLElement>>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import * as React from 'react';
5+
import { activateAnalyticsMetadata, getAnalyticsMetadataAttribute } from '../../../src/internal/analytics-metadata/';
6+
7+
export default function NestedIFrame() {
8+
activateAnalyticsMetadata(true);
9+
return (
10+
<>
11+
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentThree' } })}>
12+
<div id="sub-sub-target">inside iframe inside iframe</div>
13+
<div id="id:portal-2"></div>
14+
</div>
15+
<div data-awsui-referrer-id="id:portal-2">
16+
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentThreeInPortal' } })}> </div>
17+
</div>
18+
</>
19+
);
20+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import * as React from 'react';
5+
import { activateAnalyticsMetadata, getAnalyticsMetadataAttribute } from '../../../src/internal/analytics-metadata/';
6+
7+
export default function NestedIFrame() {
8+
activateAnalyticsMetadata(true);
9+
return (
10+
<>
11+
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentTwo' } })}>
12+
<div>inside iframe</div>
13+
<div id="sub-target">
14+
<iframe id="iframe-2" src="/another-nested-iframe" />
15+
<div id="id:portal-1"></div>
16+
</div>
17+
</div>
18+
<div data-awsui-referrer-id="id:portal-1">
19+
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentTwoInPortal' } })}> </div>
20+
</div>
21+
</>
22+
);
23+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import * as React from 'react';
5+
import { activateAnalyticsMetadata, getAnalyticsMetadataAttribute } from '../../../src/internal/analytics-metadata/';
6+
import { getComponentsTree } from '../../../src/internal/analytics-metadata/utils';
7+
8+
// Extend the Window interface to include the getComponentsTree property
9+
declare global {
10+
interface Window {
11+
getComponentsTree: typeof getComponentsTree;
12+
}
13+
}
14+
15+
export default function PageWithIFrame() {
16+
window.getComponentsTree = getComponentsTree;
17+
activateAnalyticsMetadata(true);
18+
return (
19+
<>
20+
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentOne' } })}>
21+
<iframe id="iframe-1" src="/nested-iframe" />
22+
<iframe src="https://www.amazon.com/" />
23+
</div>
24+
</>
25+
);
26+
}

0 commit comments

Comments
 (0)