Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
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
38 changes: 35 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,38 @@ 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) {
this.ariaIdentification = ariaAttrs;
Comment thread
thc202 marked this conversation as resolved.
Outdated
}
}
}

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
117 changes: 117 additions & 0 deletions test/ContentScript/integrationTests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,123 @@ 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',
'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
40 changes: 37 additions & 3 deletions test/ContentScript/unitTests.test.ts
Comment thread
thc202 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,31 @@ test('ReportedElement A toString as expected', () => {
);
});

test('ReportedElement with ARIA attributes', () => {
const btnWithId: Element = document.createElement('button');
btnWithId.setAttribute('id', 'close-btn');
btnWithId.setAttribute('aria-label', 'Close dialog');
const roWithId: src.ReportedElement = new src.ReportedElement(
btnWithId,
'http://localhost/'
);
expect(roWithId.toNonTimestampString()).toBe(
'{"type":"nodeAdded","tagName":"BUTTON","id":"close-btn","nodeName":"BUTTON","url":"http://localhost/","text":"Close dialog"}'
);

const divNoId: Element = document.createElement('div');
divNoId.setAttribute('role', 'button');
divNoId.setAttribute('aria-label', 'Submit');
divNoId.setAttribute('aria-controls', 'form1');
const roNoId: src.ReportedElement = new src.ReportedElement(
divNoId,
'http://localhost/'
);
expect(roNoId.toNonTimestampString()).toBe(
'{"type":"nodeAdded","tagName":"DIV","id":"","nodeName":"DIV","url":"http://localhost/","text":"Submit","role":"button","ariaIdentification":{"aria-label":"Submit","aria-controls":"form1"}}'
);
});

test('Report no document links', () => {
// Given
const dom: JSDOM = new JSDOM(
Expand Down Expand Up @@ -245,7 +270,10 @@ test('Reported page loaded', () => {
'<button id="button1">Button</button>' +
'<input id="input1" value="default"/>' +
'<area href="https://www.example.com/1">' +
'<input id="submit" type="submit" value="Submit"/>'
'<input id="submit" type="submit" value="Submit"/>' +
'<div role="button" aria-label="ARIA Button">Click</div>' +
'<span role="link" aria-pressed="true">ARIA Link</span>' +
'</body>'
);
const mockFn = jest.fn();
localStorage.setItem('lsKey', 'value1');
Expand All @@ -255,7 +283,7 @@ test('Reported page loaded', () => {
src.reportPageLoaded(dom.window.document, mockFn);

// Then
expect(mockFn.mock.calls.length).toBe(8);
expect(mockFn.mock.calls.length).toBe(10);
expect(mockFn.mock.calls[0][0].toNonTimestampString()).toBe(
'{"type":"nodeAdded","tagName":"A","id":"","nodeName":"A","url":"http://localhost/","href":"https://www.example.com/1","text":"link1"}'
);
Expand All @@ -275,9 +303,15 @@ test('Reported page loaded', () => {
'{"type":"nodeAdded","tagName":"BUTTON","id":"button1","nodeName":"BUTTON","url":"http://localhost/","text":"Button"}'
);
expect(mockFn.mock.calls[6][0].toNonTimestampString()).toBe(
'{"type":"localStorage","tagName":"","id":"lsKey","nodeName":"","url":"http://localhost/","text":"value1"}'
'{"type":"nodeAdded","tagName":"DIV","id":"","nodeName":"DIV","url":"http://localhost/","text":"ARIA Button","role":"button","ariaIdentification":{"aria-label":"ARIA Button"}}'
);
expect(mockFn.mock.calls[7][0].toNonTimestampString()).toBe(
'{"type":"nodeAdded","tagName":"SPAN","id":"","nodeName":"SPAN","url":"http://localhost/","text":"ARIA Link","role":"link","ariaIdentification":{"aria-pressed":"true"}}'
);
expect(mockFn.mock.calls[8][0].toNonTimestampString()).toBe(
'{"type":"localStorage","tagName":"","id":"lsKey","nodeName":"","url":"http://localhost/","text":"value1"}'
);
expect(mockFn.mock.calls[9][0].toNonTimestampString()).toBe(
'{"type":"sessionStorage","tagName":"","id":"ssKey","nodeName":"","url":"http://localhost/","text":"value2"}'
);

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