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
8 changes: 8 additions & 0 deletions src/firefox/snapshot/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ export function formatSnapshotTree(
attrs.push(`tag=${node.tag}`);
}

if (node.type) {
attrs.push(`type=${node.type}`);
}

if (node.checked !== undefined) {
attrs.push(node.checked ? 'checked' : 'unchecked');
}

// Value
if (node.value) {
attrs.push(`value="${truncate(node.value, maxAttrLength)}"`);
Expand Down
24 changes: 17 additions & 7 deletions src/firefox/snapshot/injected/attributeCollector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,11 @@ export function getElementName(el: Element): string | undefined {
return el.getAttribute('aria-label') || undefined;
}

// label for input
const htmlEl = el as HTMLElement;
const elId = htmlEl.id;
if (elId) {
const label = document.querySelector(`label[for="${elId}"]`);
if (label?.textContent) {
return label.textContent.trim();
const label = (el as HTMLInputElement).labels?.[0];
if (label) {
const name = getLabelText(label);
if (name) {
return name;
}
}

Expand Down Expand Up @@ -54,6 +52,18 @@ export function getElementName(el: Element): string | undefined {
return undefined;
}

/**
* Text of a label without the controls it wraps, so that a select does not name itself after its
* options
*/
function getLabelText(label: HTMLLabelElement): string | undefined {
const copy = label.cloneNode(true) as HTMLLabelElement;
for (const control of copy.querySelectorAll('input, select, textarea, button')) {
control.remove();
}
return copy.textContent?.replace(/\s+/g, ' ').trim() || undefined;
}

/**
* Get direct text content (not from deep children)
*/
Expand Down
6 changes: 5 additions & 1 deletion src/firefox/snapshot/injected/treeWalker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ export function walkTree(
const roleAttr = el.getAttribute('role');
const nameAttr = getElementName(el);
const textAttr = getTextContent(el);
const valueAttr = (htmlEl as any).value;
const inputType = tag === 'input' ? (el as HTMLInputElement).type : undefined;
const isToggle = inputType === 'checkbox' || inputType === 'radio';
const valueAttr = inputType === 'password' ? undefined : (htmlEl as any).value;
const hrefAttr = (htmlEl as any).href;
const srcAttr = (htmlEl as any).src;
const ariaAttr = getAriaAttributes(el);
Expand All @@ -171,6 +173,8 @@ export function walkTree(
...(idAttr && { id: idAttr }),
...(roleAttr && { role: roleAttr }),
...(nameAttr && { name: nameAttr }),
...(inputType && inputType !== 'text' && { type: inputType }),
...(isToggle && { checked: (el as HTMLInputElement).checked }),
...(valueAttr && { value: valueAttr }),
...(hrefAttr && { href: hrefAttr }),
...(srcAttr && { src: srcAttr }),
Expand Down
4 changes: 4 additions & 0 deletions src/firefox/snapshot/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export interface SnapshotNode {
id?: string;
role?: string;
name?: string;
/** Input type, left out for text inputs */
type?: string;
/** Checked state of a checkbox or radio button */
checked?: boolean;
value?: string;
href?: string;
src?: string;
Expand Down
69 changes: 69 additions & 0 deletions tests/integration/firefox/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,73 @@ describe('Snapshot Integration Tests', () => {

expect(hasInvisibleButton).toBe(false);
}, 10000);

describe('form controls', () => {
const CONTROLS_PAGE =
'data:text/html,<label>Username <input id=user></label>' +
'<label>Password <input id=pass type=password value=secret></label>' +
'<label><input id=remember type=checkbox> Remember me</label>' +
'<label>Team <select id=team><option>Red</option><option>Blue</option></select></label>' +
'<label for=mail>Mail</label><input id=mail type=email>';

function lineOf(text: string, name: string): string | undefined {
return text.split('\n').find((line) => line.includes(`"${name}"`));
}

it('should name a control after the label that wraps it', async () => {
await firefox.navigate(CONTROLS_PAGE);

const { text } = await firefox.takeSnapshot();

expect(lineOf(text, 'Username')).toContain('input');
expect(lineOf(text, 'Remember me')).toContain('input');
}, 10000);

it('should leave the options out of the name of a select', async () => {
await firefox.navigate(CONTROLS_PAGE);

const { text } = await firefox.takeSnapshot();

expect(lineOf(text, 'Team')).toContain('select');
}, 10000);

it('should still name a control after a label pointing at it by id', async () => {
await firefox.navigate(CONTROLS_PAGE);

const { text } = await firefox.takeSnapshot();

expect(lineOf(text, 'Mail')).toContain('input');
}, 10000);

it('should show the input type', async () => {
await firefox.navigate(CONTROLS_PAGE);

const { text } = await firefox.takeSnapshot();

expect(lineOf(text, 'Mail')).toContain('type=email');
expect(lineOf(text, 'Remember me')).toContain('type=checkbox');
}, 10000);

it('should show whether a checkbox is checked', async () => {
await firefox.navigate(CONTROLS_PAGE);

const before = await firefox.takeSnapshot();
const { uid } = findNodeInSnapshot(before.json.root, (n) => n.id === 'remember')!;
await firefox.clickByUid(uid);
const after = await firefox.takeSnapshot();

expect(lineOf(before.text, 'Remember me')).toContain('unchecked');
expect(lineOf(after.text, 'Remember me')).toMatch(/ checked( |$)/);
expect(lineOf(after.text, 'Remember me')).not.toContain('unchecked');
}, 10000);

it('should keep the value of a password field out of the snapshot', async () => {
await firefox.navigate(CONTROLS_PAGE);

const { text } = await firefox.takeSnapshot();

expect(lineOf(text, 'Password')).toContain('type=password');
expect(text).not.toContain('secret');
}, 10000);
});
});