diff --git a/src/internal/analytics-metadata/__tests__/metadata-utils.test.ts b/src/internal/analytics-metadata/__tests__/metadata-utils.test.ts index a4d9021..0df4d45 100644 --- a/src/internal/analytics-metadata/__tests__/metadata-utils.test.ts +++ b/src/internal/analytics-metadata/__tests__/metadata-utils.test.ts @@ -337,6 +337,110 @@ describe('processMetadata', () => { document.body.removeChild(mockDiv); }); + + test('extracts component description from aria-describedby on label element', () => { + const mockDiv = document.createElement('div'); + mockDiv.innerHTML = ` + Must be 3-20 characters + + `; + document.body.appendChild(mockDiv); + + const result: any = processMetadata(mockDiv, { name: 'awsui.Input', label: 'input' }); + expect(result.description).toBe('Must be 3-20 characters'); + + document.body.removeChild(mockDiv); + }); + + test('concatenates multiple aria-describedby IDs', () => { + const mockDiv = document.createElement('div'); + mockDiv.innerHTML = ` + Enter your work email + Must end with @amazon.com + + `; + document.body.appendChild(mockDiv); + + const result: any = processMetadata(mockDiv, { name: 'awsui.Input', label: 'input' }); + expect(result.description).toBe('Enter your work email Must end with @amazon.com'); + + document.body.removeChild(mockDiv); + }); + + test('does not extract description when label element has no aria-describedby', () => { + const mockDiv = document.createElement('div'); + mockDiv.innerHTML = ` + +
This is a description
+ + `; + document.body.appendChild(mockDiv); + + const result: any = processMetadata(mockDiv, { name: 'awsui.FormField', label: '.label' }); + expect(result.description).toBeUndefined(); + + document.body.removeChild(mockDiv); + }); + + test('does not extract description when component has no label selector', () => { + const mockDiv = document.createElement('div'); + mockDiv.innerHTML = ` + Some description + + `; + document.body.appendChild(mockDiv); + + const result: any = processMetadata(mockDiv, { name: 'awsui.Input' }); + expect(result.description).toBeUndefined(); + + document.body.removeChild(mockDiv); + }); + + test('does not extract description from child elements', () => { + const mockDiv = document.createElement('div'); + mockDiv.innerHTML = ` +
This is a description
+ + `; + document.body.appendChild(mockDiv); + + const result: any = processMetadata(mockDiv, { name: 'awsui.FormField', label: '.label' }); + expect(result.description).toBeUndefined(); + + document.body.removeChild(mockDiv); + }); + + test('skips missing IDs in aria-describedby', () => { + const mockDiv = document.createElement('div'); + mockDiv.innerHTML = ` + Constraint text + + `; + document.body.appendChild(mockDiv); + + const result: any = processMetadata(mockDiv, { name: 'awsui.Input', label: 'input' }); + expect(result.description).toBe('Constraint text'); + + document.body.removeChild(mockDiv); + }); + + test('extracts description when label is a LabelIdentifier with array selector', () => { + const mockDiv = document.createElement('div'); + mockDiv.innerHTML = ` + Help text + Title + + `; + document.body.appendChild(mockDiv); + + const result: any = processMetadata(mockDiv, { + name: 'awsui.Select', + label: { selector: ['.header', '.trigger'] }, + }); + expect(result.description).toBe('Help text'); + + document.body.removeChild(mockDiv); + }); }); describe('merge', () => { diff --git a/src/internal/analytics-metadata/metadata-utils.ts b/src/internal/analytics-metadata/metadata-utils.ts index 6766283..f1cc3b5 100644 --- a/src/internal/analytics-metadata/metadata-utils.ts +++ b/src/internal/analytics-metadata/metadata-utils.ts @@ -22,7 +22,7 @@ export const processMetadata = ( localMetadata: any, options?: GetComponentsTreeOptions ): GeneratedAnalyticsMetadataFragment => { - return Object.keys(localMetadata).reduce((acc: any, key: string) => { + const result: any = Object.keys(localMetadata).reduce((acc: any, key: string) => { if (key.toLowerCase().match(/labels$/)) { acc[key] = processLabel(node, localMetadata[key], 'multi'); } else if (key.toLowerCase().match(/label$/)) { @@ -68,6 +68,15 @@ export const processMetadata = ( } return acc; }, {}); + + if (result.name && node && localMetadata.label) { + const description = resolveComponentDescription(node, localMetadata.label); + if (description) { + result.description = description; + } + } + + return result; }; const isNil = (value: any) => { @@ -159,6 +168,31 @@ const resolveInputDescription = (root: HTMLElement, input: HTMLElement): string return ''; }; +const resolveComponentDescription = (node: HTMLElement, label: string | { selector?: string | string[] }): string => { + const selectors = + typeof label === 'string' ? [label] : Array.isArray(label.selector) ? label.selector : [label.selector || '']; + const doc = node.ownerDocument || document; + for (const selector of selectors) { + const el = selector ? node.querySelector(selector) : node; + if (!el) { + continue; + } + const describedBy = el.getAttribute('aria-describedby'); + if (!describedBy) { + continue; + } + const description = describedBy + .split(' ') + .map(id => doc.getElementById(id)?.textContent?.trim() || '') + .filter(Boolean) + .join(' '); + if (description) { + return description; + } + } + return ''; +}; + const getRadioGroupOptions = (node: HTMLElement): Array => { const inputs = Array.from(node.querySelectorAll('input[type="radio"]')) as HTMLElement[]; return inputs diff --git a/src/internal/analytics-metadata/page-scanner-utils.ts b/src/internal/analytics-metadata/page-scanner-utils.ts index 88d5f21..ff93e7b 100644 --- a/src/internal/analytics-metadata/page-scanner-utils.ts +++ b/src/internal/analytics-metadata/page-scanner-utils.ts @@ -8,6 +8,7 @@ import { getGeneratedAnalyticsMetadata } from './utils.js'; interface GeneratedAnalyticsMetadataComponentTree { name: string; label: string; + description?: string; properties?: Record | Array> | Array | Array>; children?: Array; }