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
59 changes: 53 additions & 6 deletions src/internal/analytics-metadata/__tests__/dom-utils.test.tsx
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 { activateAnalyticsMetadata, getAnalyticsMetadataAttribute, METADATA_ATTRIBUTE } from '../attributes';
import { findLogicalParent, isNodeComponent, findComponentUp, findSelectorUp } from '../dom-utils';
import { findLogicalParent, isNodeComponent, findComponentUpUntil, findSelectorUp } from '../dom-utils';

beforeAll(() => {
activateAnalyticsMetadata(true);
Expand Down Expand Up @@ -78,17 +78,17 @@ describe('isNodeComponent', () => {
});
});

describe('findComponentUp', () => {
describe('findComponentUpUntil', () => {
test('returns null when input is null', () => {
expect(findComponentUp(null)).toBeNull();
expect(findComponentUpUntil(null)).toBeNull();
});
test('returns parent component element', () => {
const { container } = render(
<div id="component-element" {...getAnalyticsMetadataAttribute({ component: { name: 'ComponentName' } })}>
<div id="target-element"></div>
</div>
);
expect(findComponentUp(container.querySelector('#target-element'))!.id).toBe('component-element');
expect(findComponentUpUntil(container.querySelector('#target-element'))!.id).toBe('component-element');
});
test('returns parent component element with portals', () => {
const { container } = render(
Expand All @@ -101,15 +101,62 @@ describe('findComponentUp', () => {
</div>
</div>
);
expect(findComponentUp(container.querySelector('#target-element'))!.id).toBe('component-element');
expect(findComponentUpUntil(container.querySelector('#target-element'))!.id).toBe('component-element');
});
test('returns null when element has no parent component', () => {
const { container } = render(
<div>
<div id="target-element"></div>
</div>
);
expect(findComponentUp(container.querySelector('#target-element'))).toBeNull();
expect(findComponentUpUntil(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(
findComponentUpUntil(
container.querySelector('#target-element'),
container.querySelector('#outer-element') as HTMLElement
)!.id
).toBe('component-element');
expect(
findComponentUpUntil(
container.querySelector('#target-element'),
container.querySelector('#target-element') as HTMLElement
)
).toBeNull();
expect(
findComponentUpUntil(
container.querySelector('#target-element'),
container.querySelector('#component-element') as HTMLElement
)!.id
).toBe('component-element');
expect(
findComponentUpUntil(
container.querySelector('#another-target-element'),
container.querySelector('#outer-element') as HTMLElement
)!.id
).toBe('component-element');
expect(
findComponentUpUntil(
container.querySelector('#another-target-element'),
container.querySelector('#another-until') as HTMLElement
)
).toBeNull();
});
});

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 findComponentUpUntil(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
4 changes: 2 additions & 2 deletions src/internal/analytics-metadata/labels-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { LABEL_DATA_ATTRIBUTE } from './attributes';
import { findSelectorUp, findComponentUp } from './dom-utils';
import { findSelectorUp, findComponentUpUntil } from './dom-utils';
import { LabelIdentifier } from './interfaces';

export const processLabel = (node: HTMLElement | null, labelIdentifier: string | LabelIdentifier | null): string => {
Expand Down Expand Up @@ -52,7 +52,7 @@ const processSingleLabel = (
return processSingleLabel(findSelectorUp(node, rootSelector), labelSelector);
}
if (root === 'component') {
return processSingleLabel(findComponentUp(node), labelSelector);
return processSingleLabel(findComponentUpUntil(node), labelSelector);
}
if (root === 'body') {
return processSingleLabel(document.body, labelSelector);
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 { findComponentUpUntil, 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 ? findComponentUpUntil(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