Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
47 changes: 47 additions & 0 deletions src/internal/analytics-metadata/__tests__/dom-utils.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,53 @@ describe('findComponentUp', () => {
);
expect(findComponentUp(container.querySelector('#target-element'))).toBeNull();
});
test('with `until` argument', () => {
const { container } = render(
<>
<div id="outer-element">
<div id="component-element" {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentName' } })}>
<div id="another-until">
<div id=":rr5:"></div>
</div>
<div id="target-element"></div>
</div>
</div>
<div data-awsui-referrer-id=":rr5:">
<div id="another-target-element"></div>
</div>
</>
);
expect(
findComponentUp(
container.querySelector('#target-element'),
container.querySelector('#outer-element') as HTMLElement
)!.id
).toBe('component-element');
expect(
findComponentUp(
container.querySelector('#target-element'),
container.querySelector('#target-element') as HTMLElement
)
).toBeNull();
expect(
findComponentUp(
container.querySelector('#target-element'),
container.querySelector('#component-element') as HTMLElement
)!.id
).toBe('component-element');
expect(
findComponentUp(
container.querySelector('#another-target-element'),
container.querySelector('#outer-element') as HTMLElement
)!.id
).toBe('component-element');
expect(
findComponentUp(
container.querySelector('#another-target-element'),
container.querySelector('#another-until') as HTMLElement
)
).toBeNull();
});
});

describe('findSelectorUp', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { getComponentsTree } from '../utils';
import { METADATA_ATTRIBUTE, activateAnalyticsMetadata } from '../attributes';
import { METADATA_ATTRIBUTE, activateAnalyticsMetadata, getAnalyticsMetadataAttribute } from '../attributes';
import { ComponentOne, ComponentTwo, ComponentThree } from './components';

describe('getComponentsTree', () => {
Expand Down Expand Up @@ -55,8 +55,11 @@ describe('getComponentsTree', () => {
{
name: 'ComponentThree',
children: [
{ name: 'ComponentTwo', label: 'sub label' },
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
{
name: 'ComponentTwo',
label: 'sub label',
children: [{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } }],
},
],
},
{ name: 'ComponentTwo', label: 'sub label' },
Expand All @@ -66,23 +69,20 @@ describe('getComponentsTree', () => {
const { container } = render(
<div id="outer-target-1">
<ComponentThree>
<ComponentThree />
<ComponentTwo />
</ComponentThree>
</div>
);
expect(getComponentsTree(container.querySelector('#outer-target-1') as HTMLElement)).toEqual([
{
name: 'ComponentThree',
children: [
{ name: 'ComponentTwo', label: 'sub label' },
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
{
name: 'ComponentThree',
children: [
{ name: 'ComponentTwo', label: 'sub label' },
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
],
name: 'ComponentTwo',
label: 'sub label',
children: [{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } }],
},
{ name: 'ComponentTwo', label: 'sub label' },
],
},
]);
Expand All @@ -98,8 +98,11 @@ describe('getComponentsTree', () => {
{
name: 'ComponentThree',
children: [
{ name: 'ComponentTwo', label: 'sub label' },
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
{
name: 'ComponentTwo',
label: 'sub label',
children: [{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } }],
},
],
},
{ name: 'ComponentTwo', label: 'sub label' },
Expand All @@ -116,6 +119,83 @@ describe('getComponentsTree', () => {
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
]);
});
describe('with portals', () => {
test('returns an empty array when portal outside of the node element', () => {
const { container } = render(
<div id="outer-target">
<div id="id:portal-1"></div>
<div data-awsui-referrer-id="id:portal-1">
<ComponentOne />
</div>
<div id="inner-target"></div>
</div>
);
const target = container.querySelector('#inner-target') as HTMLElement;
expect(getComponentsTree(target)).toEqual([]);
});
test('returns nested portal correctly', () => {
const { container } = render(
<div id="outer-target">
<div data-awsui-referrer-id="id:portal-1">
<ComponentOne />
</div>
<div id="inner-target">
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentFour' } })}>
<ComponentTwo />
<div id="id:portal-1"></div>
</div>
</div>
</div>
);
expect(getComponentsTree(container.querySelector('#outer-target') as HTMLElement)).toEqual([
{
name: 'ComponentFour',
children: [
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
{ name: 'ComponentTwo', label: 'sub label' },
],
},
]);
expect(getComponentsTree(container.querySelector('#inner-target') as HTMLElement)).toEqual([
{
name: 'ComponentFour',
children: [
{ name: 'ComponentTwo', label: 'sub label' },
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
],
},
]);
});
test('returns recursively nested portals', () => {
const { container } = render(
<div id="outer-target">
<div data-awsui-referrer-id="id:portal-1">
<ComponentOne />
<div id="id:portal-2"></div>
</div>
<div data-awsui-referrer-id="id:portal-2">
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentFive' } })} />
</div>
<div id="inner-target">
<div {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentFour' } })}>
<ComponentTwo />
<div id="id:portal-1"></div>
</div>
</div>
</div>
);
expect(getComponentsTree(container.querySelector('#inner-target') as HTMLElement)).toEqual([
{
name: 'ComponentFour',
children: [
{ name: 'ComponentTwo', label: 'sub label' },
{ name: 'ComponentOne', label: 'component label', properties: { multi: 'true' } },
{ name: 'ComponentFive' },
],
},
]);
});
});
});

describe('with inactive analytics metadata', () => {
Expand Down
2 changes: 2 additions & 0 deletions src/internal/analytics-metadata/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { getGlobalFlag } from '../global-flags';
export const METADATA_DATA_ATTRIBUTE = 'awsuiAnalytics';
export const METADATA_ATTRIBUTE = 'data-awsui-analytics';
export const LABEL_DATA_ATTRIBUTE = 'awsuiAnalyticsLabel';
export const REFERRER_DATA_ATTRIBUTE = 'awsuiReferrerId';
export const REFERRER_ATTRIBUTE = 'data-awsui-referrer-id';
const LABEL_ATTRIBUTE = 'data-awsui-analytics-label';

let activated = getGlobalFlag('analyticsMetadata');
Expand Down
10 changes: 5 additions & 5 deletions src/internal/analytics-metadata/dom-utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { METADATA_DATA_ATTRIBUTE } from './attributes';
import { METADATA_DATA_ATTRIBUTE, REFERRER_DATA_ATTRIBUTE } from './attributes';

export const findLogicalParent = (node: HTMLElement): HTMLElement | null => {
try {
const referrer = node.dataset.awsuiReferrerId;
const referrer = node.dataset[REFERRER_DATA_ATTRIBUTE];
if (referrer) {
return document.querySelector(`[id="${referrer}"]`);
}
Expand All @@ -15,12 +15,12 @@ export const findLogicalParent = (node: HTMLElement): HTMLElement | null => {
}
};

export function findComponentUp(node: HTMLElement | null): HTMLElement | null {
export function findComponentUp(node: HTMLElement | null, until: HTMLElement = document.body): HTMLElement | null {
let firstComponentElement = node;
while (firstComponentElement && firstComponentElement.tagName !== 'body' && !isNodeComponent(firstComponentElement)) {
while (firstComponentElement && firstComponentElement !== until && !isNodeComponent(firstComponentElement)) {
firstComponentElement = findLogicalParent(firstComponentElement);
}
return firstComponentElement && firstComponentElement.tagName !== 'body' ? firstComponentElement : null;
return firstComponentElement && isNodeComponent(firstComponentElement) ? firstComponentElement : null;
}

export const isNodeComponent = (node: HTMLElement): boolean => {
Expand Down
54 changes: 43 additions & 11 deletions src/internal/analytics-metadata/page-scanner-utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { METADATA_ATTRIBUTE } from './attributes';
import { isNodeComponent } from './dom-utils';
import { METADATA_ATTRIBUTE, REFERRER_ATTRIBUTE, REFERRER_DATA_ATTRIBUTE } from './attributes';
import { findComponentUp, isNodeComponent } from './dom-utils';
import { getGeneratedAnalyticsMetadata } from './utils';

interface GeneratedAnalyticsMetadataComponentTree {
Expand All @@ -12,26 +12,57 @@ interface GeneratedAnalyticsMetadataComponentTree {
children?: Array<GeneratedAnalyticsMetadataComponentTree>;
}

interface ComponentsMap {
roots: Array<HTMLElement>;
parents: Map<HTMLElement, Array<HTMLElement>>;
}

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}"]`);
});

const getComponentsArray = (node: HTMLElement | Document = document) => {
const elementsWithMetadata = Array.from(node.querySelectorAll(`[${METADATA_ATTRIBUTE}]`)) as Array<HTMLElement>;
findPortalsOutsideOfNode(node as HTMLElement).forEach(portal => {
elementsWithMetadata.push(...getComponentsArray(portal));
});
return elementsWithMetadata.filter(isNodeComponent);
};

const buildComponentsMap = (node: HTMLElement | Document = document) => {
const componentsArray = getComponentsArray(node);
const map: ComponentsMap = {
roots: [],
parents: new Map<HTMLElement, Array<HTMLElement>>(),
};
componentsArray.forEach(element => {
const parent = element.parentElement ? findComponentUp(element.parentElement, node as HTMLElement) : null;
if (!parent) {
map.roots.push(element);
} else {
if (!map.parents.has(parent)) {
map.parents.set(parent, []);
}
map.parents.get(parent)?.push(element);
}
});
return map;
};

const getComponentsTreeRecursive = (
node: HTMLElement | Document,
visited: Set<HTMLElement>
componentNodes: Array<HTMLElement>,
parentsMap: Map<HTMLElement, Array<HTMLElement>>
): Array<GeneratedAnalyticsMetadataComponentTree> => {
const tree: Array<GeneratedAnalyticsMetadataComponentTree> = [];
const componentNodes = getComponentsArray(node);
componentNodes.forEach(componentNode => {
if (visited.has(componentNode)) {
return;
}
visited.add(componentNode);
const treeItem: GeneratedAnalyticsMetadataComponentTree = {
...getGeneratedAnalyticsMetadata(componentNode).contexts[0].detail,
};
const children = getComponentsTreeRecursive(componentNode, visited);
const children = parentsMap.has(componentNode)
? getComponentsTreeRecursive(parentsMap.get(componentNode)!, parentsMap)
: [];
if (children.length > 0) {
treeItem.children = children;
}
Expand All @@ -46,5 +77,6 @@ export const getComponentsTree = (
if (!node) {
return [];
}
return getComponentsTreeRecursive(node, new Set());
const { roots, parents } = buildComponentsMap(node);
return getComponentsTreeRecursive(roots, parents);
};
Loading