diff --git a/src/internal/analytics-metadata/__tests__/components.tsx b/src/internal/analytics-metadata/__tests__/components.tsx
index 2005400..ad371cc 100644
--- a/src/internal/analytics-metadata/__tests__/components.tsx
+++ b/src/internal/analytics-metadata/__tests__/components.tsx
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
-import React from 'react';
+import React, { ReactNode } from 'react';
import { METADATA_ATTRIBUTE, getAnalyticsMetadataAttribute, getAnalyticsLabelAttribute } from '../attributes';
export const ComponentOne = ({ malformed }: { malformed?: boolean }) => (
@@ -33,7 +33,7 @@ export const ComponentOne = ({ malformed }: { malformed?: boolean }) => (
);
-const ComponentTwo = () => (
+export const ComponentTwo = () => (
sub label
@@ -43,7 +43,7 @@ const ComponentTwo = () => (
);
-export const ComponentThree = () => (
+export const ComponentThree = ({ children }: { children?: ReactNode }) => (
);
diff --git a/src/internal/analytics-metadata/__tests__/page-scanner-utils.test.tsx b/src/internal/analytics-metadata/__tests__/page-scanner-utils.test.tsx
new file mode 100644
index 0000000..df6b636
--- /dev/null
+++ b/src/internal/analytics-metadata/__tests__/page-scanner-utils.test.tsx
@@ -0,0 +1,131 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React from 'react';
+import { render } from '@testing-library/react';
+import { getComponentsTree } from '../utils';
+import { METADATA_ATTRIBUTE, activateAnalyticsMetadata } from '../attributes';
+import { ComponentOne, ComponentTwo, ComponentThree } from './components';
+
+describe('getComponentsTree', () => {
+ describe('with active analytics metadata', () => {
+ beforeAll(() => {
+ activateAnalyticsMetadata(true);
+ });
+ test('returns an empty array when input is null', () => {
+ expect(getComponentsTree(null)).toEqual([]);
+ });
+ test('skips metadata that does not refer to a component', () => {
+ const { container } = render(
+
+
+
+ );
+ const target = container.querySelector('#outer-target') as HTMLElement;
+ expect(getComponentsTree(target)).toEqual([
+ { name: 'ComponentOne', label: 'component label', properties: { multi: 'true' }, children: [] },
+ ]);
+ });
+ test('only includes components inside the specified element', () => {
+ const { container } = render(
+ <>
+
+
+
+
+
+
+ >
+ );
+ expect(getComponentsTree(container.querySelector('#outer-target-1') as HTMLElement)).toEqual([
+ { name: 'ComponentOne', label: 'component label', properties: { multi: 'true' }, children: [] },
+ ]);
+ expect(getComponentsTree(container.querySelector('#outer-target-2') as HTMLElement)).toEqual([
+ { name: 'ComponentTwo', label: 'sub label', children: [] },
+ ]);
+ });
+ test('can include multiple components', () => {
+ const { container } = render(
+
+
+
+
+ );
+ expect(getComponentsTree(container.querySelector('#outer-target-1') as HTMLElement)).toEqual([
+ {
+ name: 'ComponentThree',
+ children: [
+ { name: 'ComponentTwo', label: 'sub label', children: [] },
+ { name: 'ComponentOne', label: 'component label', properties: { multi: 'true' }, children: [] },
+ ],
+ },
+ { name: 'ComponentTwo', label: 'sub label', children: [] },
+ ]);
+ });
+ test('can include multiple nested components', () => {
+ const { container } = render(
+
+
+
+
+
+ );
+ expect(getComponentsTree(container.querySelector('#outer-target-1') as HTMLElement)).toEqual([
+ {
+ name: 'ComponentThree',
+ children: [
+ { name: 'ComponentTwo', label: 'sub label', children: [] },
+ { name: 'ComponentOne', label: 'component label', properties: { multi: 'true' }, children: [] },
+ {
+ name: 'ComponentThree',
+ children: [
+ { name: 'ComponentTwo', label: 'sub label', children: [] },
+ { name: 'ComponentOne', label: 'component label', properties: { multi: 'true' }, children: [] },
+ ],
+ },
+ ],
+ },
+ ]);
+ });
+ test('use document as default element', () => {
+ render(
+ <>
+
+
+ >
+ );
+ expect(getComponentsTree()).toEqual([
+ {
+ name: 'ComponentThree',
+ children: [
+ { name: 'ComponentTwo', label: 'sub label', children: [] },
+ { name: 'ComponentOne', label: 'component label', properties: { multi: 'true' }, children: [] },
+ ],
+ },
+ { name: 'ComponentTwo', label: 'sub label', children: [] },
+ ]);
+ });
+ test('skips malformed metadata', () => {
+ const { container } = render(
+
+
+
+ );
+ const target = container.querySelector('#outer-target') as HTMLElement;
+ expect(getComponentsTree(target)).toEqual([
+ { name: 'ComponentOne', label: 'component label', properties: { multi: 'true' }, children: [] },
+ ]);
+ });
+ });
+
+ describe('with inactive analytics metadata', () => {
+ beforeAll(() => {
+ activateAnalyticsMetadata(false);
+ });
+ test('returns an empty object', () => {
+ const { container } = render(
);
+ const target = container.querySelector('#target') as HTMLElement;
+ expect(getComponentsTree(target)).toEqual([]);
+ });
+ });
+});
diff --git a/src/internal/analytics-metadata/interfaces.ts b/src/internal/analytics-metadata/interfaces.ts
index 383dd3c..297e244 100644
--- a/src/internal/analytics-metadata/interfaces.ts
+++ b/src/internal/analytics-metadata/interfaces.ts
@@ -12,7 +12,7 @@ export interface GeneratedAnalyticsMetadata {
contexts: Array
;
}
-interface GeneratedAnalyticsMetadataComponent {
+export interface GeneratedAnalyticsMetadataComponent {
// name of the component. For example: "awsui.RadioGroup". We prefix the actual name with awsui to account for future tagging of custom components
name: string;
diff --git a/src/internal/analytics-metadata/page-scanner-utils.ts b/src/internal/analytics-metadata/page-scanner-utils.ts
new file mode 100644
index 0000000..615b32e
--- /dev/null
+++ b/src/internal/analytics-metadata/page-scanner-utils.ts
@@ -0,0 +1,44 @@
+// 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 { GeneratedAnalyticsMetadataComponent } from './interfaces';
+import { getGeneratedAnalyticsMetadata } from './utils';
+
+interface GeneratedAnalyticsMetadataComponentTree extends GeneratedAnalyticsMetadataComponent {
+ children?: Array;
+}
+
+const getComponentsArray = (node: HTMLElement | Document = document) => {
+ const elementsWithMetadata = Array.from(node.querySelectorAll(`[${METADATA_ATTRIBUTE}]`)) as Array;
+ return elementsWithMetadata.filter(isNodeComponent);
+};
+
+const getComponentsTreeRecursive = (
+ node: HTMLElement | Document,
+ visited: Set
+): Array => {
+ const tree: Array = [];
+ const componentNodes = getComponentsArray(node);
+ componentNodes.forEach(componentNode => {
+ if (visited.has(componentNode)) {
+ return;
+ }
+ visited.add(componentNode);
+ tree.push({
+ ...getGeneratedAnalyticsMetadata(componentNode).contexts[0].detail,
+ children: getComponentsTreeRecursive(componentNode, visited),
+ });
+ });
+ return tree;
+};
+
+export const getComponentsTree = (
+ node: HTMLElement | Document | null = document
+): Array => {
+ if (!node) {
+ return [];
+ }
+ return getComponentsTreeRecursive(node, new Set());
+};
diff --git a/src/internal/analytics-metadata/utils.ts b/src/internal/analytics-metadata/utils.ts
index 05c1205..19bb318 100644
--- a/src/internal/analytics-metadata/utils.ts
+++ b/src/internal/analytics-metadata/utils.ts
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
export { getRawAnalyticsMetadata } from './testing-utils';
+export { getComponentsTree } from './page-scanner-utils';
import { METADATA_DATA_ATTRIBUTE } from './attributes';
import { GeneratedAnalyticsMetadata, GeneratedAnalyticsMetadataFragment } from './interfaces';