Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Added
- Report elements that have cursor pointer style.
- Report elements with specific `role` attribute values.
- Include ARIA attributes for uniquely identifiable elements lacking an `id`.

## 0.1.8 - 2025-12-12

Expand Down
54 changes: 54 additions & 0 deletions source/ContentScript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,56 @@ function reportPointerElements(
});
}

const STANDARD_INTERACTIVE_TAGS = [
'A',
'BUTTON',
'INPUT',
'SELECT',
'TEXTAREA',
'FORM',
];

const INTERACTIVE_ARIA_ROLES = [
'button',
'link',
'checkbox',
'radio',
'switch',
'tab',
'menuitem',
'menuitemcheckbox',
'menuitemradio',
'option',
'treeitem',
'combobox',
'listbox',
'slider',
'spinbutton',
'searchbox',
'textbox',
];

function hasInteractiveAriaRole(element: Element): boolean {
const role = element.getAttribute('role');
return role !== null && INTERACTIVE_ARIA_ROLES.includes(role.toLowerCase());
}

function reportAriaElements(
source: Element | Document,
fn: (re: ReportedObject) => void
): void {
const url = window.location.href;

source.querySelectorAll('[role]').forEach((element) => {
if (
hasInteractiveAriaRole(element) &&
!STANDARD_INTERACTIVE_TAGS.includes(element.tagName.toUpperCase())
) {
fn(new ReportedElement(element, url));
}
});
}

function reportPageLoaded(
doc: Document,
fn: (re: ReportedObject) => void
Expand All @@ -219,6 +269,7 @@ function reportPageLoaded(
reportElements(doc.getElementsByTagName('input'), fn);
reportElements(doc.getElementsByTagName('button'), fn);
reportPointerElements(doc, fn);
reportAriaElements(doc, fn);
reportStorage(LOCAL_STORAGE, localStorage, fn);
reportStorage(SESSION_STORAGE, sessionStorage, fn);
}
Expand All @@ -232,12 +283,14 @@ const domMutated = function domMutation(
reportPageLinks(document, reportObject);
reportPageForms(document, reportObject);
reportPointerElements(document, reportObject);
reportAriaElements(document, reportObject);
for (const mutation of mutationList) {
if (mutation.type === 'childList') {
reportNodeElements(mutation.target, 'input', reportObject);
reportNodeElements(mutation.target, 'button', reportObject);
if (mutation.target.nodeType === Node.ELEMENT_NODE) {
reportPointerElements(mutation.target as Element, reportObject);
reportAriaElements(mutation.target as Element, reportObject);
}
}
}
Expand Down Expand Up @@ -350,6 +403,7 @@ export {
reportPageForms,
reportNodeElements,
reportStorage,
reportAriaElements,
ReportedElement,
ReportedObject,
ReportedStorage,
Expand Down
56 changes: 53 additions & 3 deletions source/types/ReportedModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class ReportedObject {
}

public toShortString(): string {
return JSON.stringify(this, function replacer(k: string, v: string) {
return JSON.stringify(this, function replacer(k: string, v: unknown) {
if (k === 'xpath') {
// Dont return the xpath value - it can change too often in many cases
return undefined;
Expand All @@ -69,7 +69,7 @@ class ReportedObject {

// Use this for tests
public toNonTimestampString(): string {
return JSON.stringify(this, function replacer(k: string, v: string) {
return JSON.stringify(this, function replacer(k: string, v: unknown) {
if (k === 'timestamp') {
return undefined;
}
Expand Down Expand Up @@ -100,6 +100,10 @@ class ReportedElement extends ReportedObject {

public formId: number | null;

public role: string | null;

public ariaIdentification: Record<string, string> | null;

public constructor(element: Element, url: string) {
super(
'nodeAdded',
Expand Down Expand Up @@ -128,10 +132,56 @@ class ReportedElement extends ReportedObject {
} else if (element.hasAttribute('href')) {
this.href = element.getAttribute('href');
}

this.captureAriaInfo(element);
}

private captureAriaInfo(element: Element): void {
const ariaLabel = element.getAttribute('aria-label');
if (ariaLabel !== null) {
this.text = ariaLabel;
}

const role = element.getAttribute('role');
if (role !== null) {
this.role = role;
}

if (!this.id) {
const ariaAttrs: Record<string, string> = {};

Array.from(element.attributes)
.filter((attr) => attr.name.startsWith('aria-'))
.forEach((attr) => {
ariaAttrs[attr.name] = attr.value;
});

if (Object.keys(ariaAttrs).length > 0) {
if (this.isAriaSelectorUnique(ariaAttrs, element)) {
this.ariaIdentification = ariaAttrs;
}
}
}
}

private isAriaSelectorUnique(
ariaAttrs: Record<string, string>,
element: Element
): boolean {
const doc = element.ownerDocument;
if (!doc || !element.isConnected) {
return true;
}

const selector = Object.entries(ariaAttrs)
.map(([key, value]) => `[${key}="${CSS.escape(value)}"]`)
.join('');
const elements = doc.querySelectorAll(selector);
return elements.length === 1;
}

public toShortString(): string {
return JSON.stringify(this, function replacer(k: string, v: string) {
return JSON.stringify(this, function replacer(k: string, v: unknown) {
if (k === 'timestamp') {
// No point reporting the same element lots of times
return undefined;
Expand Down
139 changes: 139 additions & 0 deletions test/ContentScript/integrationTests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,145 @@ function integrationTests(
]);
});

test('Should report ARIA elements', async () => {
// Given
await enableZapEvents(server, driver);
server.setRecordZapEvents(false);
const wd = await driver.getWebDriver();
// When
await wd.get(`http://localhost:${_HTTPPORT}/webpages/ariaElements.html`);
await eventsProcessed();
// Then
expect(actualData).toEqual(
expect.arrayContaining([
reportEvent(
'pageLoad',
'http://localhost:1801/webpages/ariaElements.html'
),
reportObject(
'nodeAdded',
'DIV',
'aria-button-1',
'DIV',
'http://localhost:1801/webpages/ariaElements.html',
undefined,
'Submit Form',
undefined,
'button'
),
reportObject(
'nodeAdded',
'SPAN',
'aria-button-2',
'SPAN',
'http://localhost:1801/webpages/ariaElements.html',
undefined,
expect.stringContaining('Toggle Button'),
undefined,
'button'
),
reportObject(
'nodeAdded',
'DIV',
'aria-link-1',
'DIV',
'http://localhost:1801/webpages/ariaElements.html',
undefined,
'Go to homepage',
undefined,
'link'
),
reportObject(
'nodeAdded',
'DIV',
'aria-checkbox-1',
'DIV',
'http://localhost:1801/webpages/ariaElements.html',
undefined,
'Accept terms',
undefined,
'checkbox'
),
reportObject(
'nodeAdded',
'DIV',
'aria-tab-1',
'DIV',
'http://localhost:1801/webpages/ariaElements.html',
undefined,
expect.stringContaining('Tab 1'),
undefined,
'tab'
),
reportObject(
'nodeAdded',
'DIV',
'aria-menuitem-1',
'DIV',
'http://localhost:1801/webpages/ariaElements.html',
undefined,
expect.stringContaining('Edit'),
undefined,
'menuitem'
),
reportObject(
'nodeAdded',
'DIV',
'',
'DIV',
'http://localhost:1801/webpages/ariaElements.html',
undefined,
'No ID Button',
{
'aria-label': 'No ID Button',
'aria-pressed': 'false',
},
'button'
),
reportObject(
'nodeAdded',
'DIV',
'',
'DIV',
'http://localhost:1801/webpages/ariaElements.html',
undefined,
'Duplicate Action',
undefined,
'button'
),
reportObject(
'nodeAdded',
'DIV',
'',
'DIV',
'http://localhost:1801/webpages/ariaElements.html',
undefined,
'Duplicate Action',
undefined,
'button'
),
reportObject(
'nodeAdded',
'BUTTON',
'standard-button',
'BUTTON',
'http://localhost:1801/webpages/ariaElements.html',
undefined,
expect.stringContaining('Standard Button')
),
reportObject(
'nodeAdded',
'A',
'standard-link',
'A',
'http://localhost:1801/webpages/ariaElements.html',
'http://localhost:1801/webpages/ariaElements.html#test',
expect.stringContaining('Standard Link')
),
])
);
});

test('Should ignore ZAP div', async () => {
// Given / When
await driver.toggleRecording();
Expand Down
12 changes: 11 additions & 1 deletion test/ContentScript/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ export function reportObject(
nodeName: string,
url: string,
href: string | undefined,
text: string
text: string,
ariaIdentification?: Record<string, string>,
role?: string
): object {
const data = {
action: {action: 'reportObject'},
Expand All @@ -103,13 +105,21 @@ export function reportObject(
url,
href,
text,
role,
ariaIdentification,
},
apikey: 'not set',
},
};
if (href === undefined) {
delete data.body.objectJson.href;
}
if (ariaIdentification === undefined) {
delete data.body.objectJson.ariaIdentification;
}
if (role === undefined) {
delete data.body.objectJson.role;
}
return data;
}

Expand Down
Loading