Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import useBrowser from '@cloudscape-design/browser-test-tools/use-browser';
import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects';
import { Browser } from 'webdriverio';
import { getComponentsTree } from '../page-scanner-utils';

// Extend the Window interface to include the getComponentsTree property
declare global {
interface Window {
getComponentsTree: typeof getComponentsTree;
}
}

describe('getComponentsTree', () => {
const setupTest = (testFn: (page: BasePageObject, browser: Browser) => Promise<void>) => {
return useBrowser(async browser => {
const page = new BasePageObject(browser);
await browser.url('/with-iframe');
await testFn(page, browser);
});
};

test(
'gets component metadata, including iframes',
setupTest(async (page, browser) => {
await page.runInsideIframe('#iframe-1', true, async () => {
await page.runInsideIframe('#iframe-2', true, async () => {
await page.waitForVisible('#sub-sub-target');
});
});
const tree = await browser.execute(() => {
return window.getComponentsTree();
});
expect(tree).toEqual([
{
name: 'ComponentOne',
children: [
{
name: 'ComponentTwo',
children: [
{ name: 'ComponentTwoInPortal' },
{ name: 'ComponentThree', children: [{ name: 'ComponentThreeInPortal' }] },
],
},
],
},
]);
})
);
test(
'gets component metadata of sub-tree inside an iframe',
setupTest(async (page, browser) => {
await page.runInsideIframe('#iframe-1', true, async () => {
await page.runInsideIframe('#iframe-2', true, async () => {
await page.waitForVisible('#sub-sub-target');
});
});
const tree = await browser.execute(() => {
const iframe = document.querySelector('#iframe-1') as HTMLIFrameElement;
const iframeDocument = iframe!.contentDocument;
const subTarget = iframeDocument!.querySelector('#sub-target') as HTMLElement;
return window.getComponentsTree(subTarget);
});
expect(tree).toEqual([
{ name: 'ComponentTwoInPortal' },
{ name: 'ComponentThree', children: [{ name: 'ComponentThreeInPortal' }] },
]);
})
);
});
82 changes: 81 additions & 1 deletion src/internal/analytics-metadata/__tests__/components.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

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

export const ComponentOne = ({ malformed }: { malformed?: boolean }) => (
<div
Expand Down Expand Up @@ -65,3 +66,82 @@ export const ComponentThree = ({ children }: { children?: ReactNode }) => (
</div>
</div>
);

const NestedIframe = () => {
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
const container = ref.current;
if (!container) {
return;
}
const iframeEl = container.ownerDocument.createElement('iframe');
iframeEl.id = 'iframe-2';
container.appendChild(iframeEl);

const iframeDocument = iframeEl.contentDocument!;
iframeDocument.open();
iframeDocument.writeln('<!DOCTYPE html>');
iframeDocument.close();
iframeDocument.body.innerHTML =
'<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>';

return () => {
container.removeChild(iframeEl);
};
});

return (
<>
<h1>Nested title</h1>
<div
{...getAnalyticsMetadataAttribute({
component: { name: 'ComponentTwo', label: { selector: 'h1', root: 'body' } },
})}
>
<div>inside iframe</div>
<div id="sub-target">
<div ref={ref}></div>;<div id="id:portal-1"></div>
</div>
</div>
<div data-awsui-referrer-id="id:portal-1">
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentTwoInPortal' } })}> </div>
</div>
</>
);
};

export const AppWithIframe = () => {
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
const container = ref.current;
if (!container) {
return;
}
const iframeEl = container.ownerDocument.createElement('iframe');
iframeEl.id = 'iframe-1';
container.appendChild(iframeEl);

const iframeDocument = iframeEl.contentDocument!;
iframeDocument.open();
iframeDocument.writeln('<!DOCTYPE html>');
iframeDocument.close();

const innerAppRoot = iframeDocument.createElement('div');
iframeDocument.body.appendChild(innerAppRoot);
ReactDOM.render(<NestedIframe />, innerAppRoot);
return () => {
ReactDOM.unmountComponentAtNode(innerAppRoot);
container.removeChild(iframeEl);
};
});

return (
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentOne' } })}>
<h1>Main title</h1>
<div ref={ref}></div>;
<iframe src="https://www.amazon.com/" />
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React from 'react';
import { render } from '@testing-library/react';
import { getComponentsTree } from '../utils';
import { METADATA_ATTRIBUTE, activateAnalyticsMetadata, getAnalyticsMetadataAttribute } from '../attributes';
import { ComponentOne, ComponentTwo, ComponentThree } from './components';
import { AppWithIframe, ComponentOne, ComponentTwo, ComponentThree } from './components';

describe('getComponentsTree', () => {
describe('with active analytics metadata', () => {
Expand Down Expand Up @@ -196,6 +196,31 @@ describe('getComponentsTree', () => {
]);
});
});
test('with iframes', () => {
const { container } = render(<AppWithIframe />);
expect(getComponentsTree()).toEqual([
{
name: 'ComponentOne',
children: [
{
name: 'ComponentTwo',
label: 'Nested title',
children: [
{ name: 'ComponentTwoInPortal' },
{ name: 'ComponentThree', children: [{ name: 'ComponentThreeInPortal' }] },
],
},
],
},
]);
const subTarget = (container.querySelector('#iframe-1') as HTMLIFrameElement)!.contentDocument!.querySelector(
'#sub-target'
)!;
expect(getComponentsTree(subTarget as HTMLElement)).toEqual([
{ name: 'ComponentTwoInPortal' },
{ name: 'ComponentThree', children: [{ name: 'ComponentThreeInPortal' }] },
]);
});
});

describe('with inactive analytics metadata', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/internal/analytics-metadata/dom-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const findLogicalParent = (node: HTMLElement): HTMLElement | null => {
try {
const referrer = node.dataset[REFERRER_DATA_ATTRIBUTE];
if (referrer) {
return document.querySelector(`[id="${referrer}"]`);
return (node.ownerDocument || node).querySelector(`[id="${referrer}"]`);
}
return node.parentElement;
} catch (ex) {
Expand Down
2 changes: 1 addition & 1 deletion src/internal/analytics-metadata/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

export { GeneratedAnalyticsMetadataFragment, GeneratedAnalyticsMetadata, LabelIdentifier } from './interfaces';
export type { GeneratedAnalyticsMetadataFragment, GeneratedAnalyticsMetadata, LabelIdentifier } from './interfaces';
export {
getAnalyticsMetadataAttribute,
copyAnalyticsMetadataAttribute,
Expand Down
2 changes: 1 addition & 1 deletion src/internal/analytics-metadata/labels-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const processSingleLabel = (
return processSingleLabel(findComponentUpUntil(node), labelSelector);
}
if (root === 'body') {
return processSingleLabel(document.body, labelSelector);
return processSingleLabel(node.ownerDocument.body, labelSelector);
}
let labelElement: HTMLElement | null = node;
if (labelSelector) {
Expand Down
40 changes: 36 additions & 4 deletions src/internal/analytics-metadata/page-scanner-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ interface ComponentsMap {
}

const findPortalsOutsideOfNode = (node: HTMLElement): Array<HTMLElement> =>
(Array.from(document.querySelectorAll(`[${REFERRER_ATTRIBUTE}]`)) as Array<HTMLElement>).filter(element => {
const referrer = element.dataset[REFERRER_DATA_ATTRIBUTE];
return !!node.querySelector(`[id="${referrer}"]`) && !node.querySelector(`[${REFERRER_ATTRIBUTE}="${referrer}"]`);
});
(Array.from((node.ownerDocument || node).querySelectorAll(`[${REFERRER_ATTRIBUTE}]`)) as Array<HTMLElement>).filter(
element => {
const referrer = element.dataset[REFERRER_DATA_ATTRIBUTE];
return !!node.querySelector(`[id="${referrer}"]`) && !node.querySelector(`[${REFERRER_ATTRIBUTE}="${referrer}"]`);
}
);

const findAccessibleIframes = (node: HTMLElement | Document): Array<HTMLIFrameElement> =>
Array.from(node.querySelectorAll('iframe')).filter(iframe => !!iframe.contentDocument);

const getComponentsArray = (node: HTMLElement | Document = document) => {
const elementsWithMetadata = Array.from(node.querySelectorAll(`[${METADATA_ATTRIBUTE}]`)) as Array<HTMLElement>;
Expand All @@ -48,9 +53,36 @@ const buildComponentsMap = (node: HTMLElement | Document = document) => {
map.parents.get(parent)?.push(element);
}
});
findAccessibleIframes(node).forEach(
iframe =>
iframe.contentDocument &&
mergeComponentsMaps(
map,
findComponentUpUntil(iframe, node as HTMLElement),
buildComponentsMap(iframe.contentDocument)
)
);
return map;
};

const mergeComponentsMaps = (
parentMap: ComponentsMap,
parentComponent: HTMLElement | null,
childMap: ComponentsMap
): void => {
parentMap.parents = new Map([...parentMap.parents, ...childMap.parents]);
if (childMap.roots.length > 0) {
if (parentComponent) {
if (!parentMap.parents.has(parentComponent)) {
parentMap.parents.set(parentComponent, []);
}
childMap.roots.forEach(root => parentMap.parents.get(parentComponent)?.push(root));
} else {
parentMap.roots = [...parentMap.roots, ...childMap.roots];
}
}
};

const getComponentsTreeRecursive = (
componentNodes: Array<HTMLElement>,
parentsMap: Map<HTMLElement, Array<HTMLElement>>
Expand Down
20 changes: 20 additions & 0 deletions test-pages/src/pages/another-nested-iframe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import * as React from 'react';
import { activateAnalyticsMetadata, getAnalyticsMetadataAttribute } from '../../../src/internal/analytics-metadata/';

export default function NestedIFrame() {
activateAnalyticsMetadata(true);
return (
<>
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentThree' } })}>
<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 {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentThreeInPortal' } })}> </div>
</div>
</>
);
}
23 changes: 23 additions & 0 deletions test-pages/src/pages/nested-iframe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import * as React from 'react';
import { activateAnalyticsMetadata, getAnalyticsMetadataAttribute } from '../../../src/internal/analytics-metadata/';

export default function NestedIFrame() {
activateAnalyticsMetadata(true);
return (
<>
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentTwo' } })}>
<div>inside iframe</div>
<div id="sub-target">
<iframe id="iframe-2" src="/another-nested-iframe" />
<div id="id:portal-1"></div>
</div>
</div>
<div data-awsui-referrer-id="id:portal-1">
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentTwoInPortal' } })}> </div>
</div>
</>
);
}
26 changes: 26 additions & 0 deletions test-pages/src/pages/with-iframe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import * as React from 'react';
import { activateAnalyticsMetadata, getAnalyticsMetadataAttribute } from '../../../src/internal/analytics-metadata/';
import { getComponentsTree } from '../../../src/internal/analytics-metadata/utils';

// Extend the Window interface to include the getComponentsTree property
declare global {
interface Window {
getComponentsTree: typeof getComponentsTree;
}
}

export default function PageWithIFrame() {
window.getComponentsTree = getComponentsTree;
activateAnalyticsMetadata(true);
return (
<>
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentOne' } })}>
<iframe id="iframe-1" src="/nested-iframe" />
<iframe src="https://www.amazon.com/" />
</div>
</>
);
}
Loading