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
104 changes: 104 additions & 0 deletions src/internal/analytics-metadata/__tests__/metadata-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<span id="desc-1">Must be 3-20 characters</span>
<input aria-describedby="desc-1" />
`;
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 = `
<span id="desc-a">Enter your work email</span>
<span id="desc-b">Must end with @amazon.com</span>
<input aria-describedby="desc-a desc-b" />
`;
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 = `
<label class="label">Form field label</label>
<div id="ff-desc">This is a description</div>
<input aria-describedby="ff-desc" />
`;
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 = `
<span id="desc-1">Some description</span>
<input aria-describedby="desc-1" />
`;
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 = `
<div id="ff-desc">This is a description</div>
<input aria-describedby="ff-desc" />
`;
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 = `
<span id="desc-exists">Constraint text</span>
<input aria-describedby="desc-missing desc-exists" />
`;
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 = `
<span id="desc-1">Help text</span>
<span class="header">Title</span>
<button class="trigger" aria-describedby="desc-1">Select</button>
`;
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', () => {
Expand Down
36 changes: 35 additions & 1 deletion src/internal/analytics-metadata/metadata-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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$/)) {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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<OptionItem> => {
const inputs = Array.from(node.querySelectorAll('input[type="radio"]')) as HTMLElement[];
return inputs
Expand Down
1 change: 1 addition & 0 deletions src/internal/analytics-metadata/page-scanner-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getGeneratedAnalyticsMetadata } from './utils.js';
interface GeneratedAnalyticsMetadataComponentTree {
name: string;
label: string;
description?: string;
properties?: Record<string, string | Array<string> | Array<Array<string>> | Array<OptionItem> | Array<TabItem>>;
children?: Array<GeneratedAnalyticsMetadataComponentTree>;
}
Expand Down
Loading