Skip to content
This repository has been archived by the owner on Feb 3, 2022. It is now read-only.

Commit

Permalink
Fix submit events and refactor a bit
Browse files Browse the repository at this point in the history
  • Loading branch information
johanbay committed Oct 23, 2020
1 parent 81ddfd6 commit 9c56851
Show file tree
Hide file tree
Showing 6 changed files with 295 additions and 45 deletions.
18 changes: 13 additions & 5 deletions src/helpers.ts
Expand Up @@ -19,9 +19,16 @@
* https://source.chromium.org/chromium/chromium/src/+/master:third_party/devtools-frontend/src/front_end/elements/DOMPath.js
*/

export function cssPath() {
const node = this;
if (node.nodeType !== Node.ELEMENT_NODE) {
export function isSubmitButton(): boolean {
return (
this.tagName === 'BUTTON' &&
(this as HTMLButtonElement).type === 'submit' &&
(this as HTMLButtonElement).form !== null
);
}

export function cssPath(): string {
if (this.nodeType !== Node.ELEMENT_NODE) {
return '';
}
function idSelector(id: string) {
Expand Down Expand Up @@ -118,9 +125,10 @@ export function cssPath() {
return new Step(result, false);
}
const steps = [];
let currentNode = node;
// eslint-disable-next-line @typescript-eslint/no-this-alias
let currentNode = this;
while (currentNode) {
const step = cssPathStep(currentNode, currentNode === node);
const step = cssPathStep(currentNode, currentNode === this);
if (!step) {
break;
}
Expand Down
97 changes: 61 additions & 36 deletions src/recorder.ts
Expand Up @@ -16,10 +16,9 @@

import * as puppeteer from 'puppeteer';
import { Readable } from 'stream';
import { cssPath } from './helpers';
import { cssPath, isSubmitButton } from './helpers';

let client: puppeteer.CDPSession;
let paused: Promise<void>;

interface RecorderOptions {
wsEndpoint?: string;
Expand All @@ -36,7 +35,7 @@ async function getBrowserInstance(options: RecorderOptions) {
}
}

export async function getSelector(objectId: string) {
export async function getSelector(objectId: string): Promise<string | null> {
const ariaSelector = await getAriaSelector(objectId);
if (ariaSelector) return ariaSelector;
// @ts-ignore
Expand Down Expand Up @@ -66,7 +65,10 @@ export async function getAriaSelector(
return null;
}

export default async (url: string, options: RecorderOptions = {}) => {
export default async (
url: string,
options: RecorderOptions = {}
): Promise<Readable> => {
if (!url.startsWith('http')) {
url = 'https://' + url;
}
Expand All @@ -90,71 +92,94 @@ export default async (url: string, options: RecorderOptions = {}) => {
eventName: 'submit',
});

const findTargetId = async (localFrame, interestingClassNames: string[]) => {
const event = localFrame.find((prop) =>
interestingClassNames.includes(prop.value.className)
);
const eventProperties = await client.send('Runtime.getProperties', {
objectId: event.value.objectId,
});
// @ts-ignore
const target = eventProperties.result.find(
(prop) => prop.name === 'target'
);
return target.value.objectId;
};

const resume = async () => {
await client.send('Debugger.setSkipAllPauses', { skip: true });
await client.send('Debugger.resume', { terminateOnResume: false });
await client.send('Debugger.setSkipAllPauses', { skip: false });
};
const skip = async () => {
await client.send('Debugger.resume', { terminateOnResume: false });
};

const handleClickEvent = async (localFrame) => {
const targetId = await findTargetId(localFrame, [
'MouseEvent',
'PointerEvent',
]);
// @ts-ignore
const pointerEvent = localFrame.find(
(prop) =>
prop.value.className === 'PointerEvent' ||
prop.value.className === 'MouseEvent'
);
const pointerEventProps = await client.send('Runtime.getProperties', {
objectId: pointerEvent.value.objectId,
const isSubmitButtonResponse = await client.send('Runtime.callFunctionOn', {
functionDeclaration: isSubmitButton.toString(),
objectId: targetId,
});
// Let submit handle this case if the click is on a submit button
// @ts-ignore
const target = pointerEventProps.result.find(
(prop) => prop.name === 'target'
);
const selector = await getSelector(target.value.objectId);
if (isSubmitButtonResponse.result.value) {
return skip();
}
const selector = await getSelector(targetId);
if (selector) {
addLineToPuppeteerScript(`await click('${selector}');`);
} else {
console.log(`failed to generate selector`);
}
await resume();
};

const handleSubmitEvent = async (localFrame) => {
const targetId = await findTargetId(localFrame, ['SubmitEvent']);
const selector = await getSelector(targetId);
if (selector) {
addLineToPuppeteerScript(`await submit('${selector}');`);
} else {
console.log(`failed to generate selector`);
}
await resume();
};

const handleChangeEvent = async (localFrame) => {
// @ts-ignore
const changeEvent = localFrame.find(
(prop) => prop.value.className === 'Event'
);
const changeEventProps = await client.send('Runtime.getProperties', {
objectId: changeEvent.value.objectId,
});
// @ts-ignore
const target = changeEventProps.result.find(
(prop) => prop.name === 'target'
);
const targetId = await findTargetId(localFrame, ['Event']);
const targetValue = await client.send('Runtime.callFunctionOn', {
functionDeclaration: 'function() { return this.value }',
objectId: target.value.objectId,
objectId: targetId,
});
// @ts-ignore
const value = targetValue.result.value;
const escapedValue = value.replace(/'/g, "\\'");
const selector = await getAriaSelector(target.value.objectId);
const selector = await getAriaSelector(targetId);
addLineToPuppeteerScript(`await type('${selector}', '${escapedValue}');`);
await resume();
};

client.on('Debugger.paused', async function (params) {
paused = this;
const event = params.data.eventName;
const eventName = params.data.eventName;
const localFrame = params.callFrames[0].scopeChain[0];
// @ts-ignore
const { result } = await client.send('Runtime.getProperties', {
objectId: localFrame.object.objectId,
});
if (event === 'listener:click') {
if (eventName === 'listener:click') {
await handleClickEvent(result);
} else if (event === 'listener:change') {
} else if (eventName === 'listener:submit') {
await handleSubmitEvent(result);
} else if (eventName === 'listener:change') {
await handleChangeEvent(result);
} else {
await skip();
}
await resume();
});

let identation = 0;
Expand All @@ -164,8 +189,9 @@ export default async (url: string, options: RecorderOptions = {}) => {
};

page.evaluateOnNewDocument(() => {
window.addEventListener('change', async (e) => {}, true);
window.addEventListener('click', async (e) => {}, true);
window.addEventListener('change', (e) => {}, true);
window.addEventListener('click', (e) => {}, true);
window.addEventListener('submit', (e) => {}, true);
});

// Setup puppeteer
Expand All @@ -181,7 +207,6 @@ export default async (url: string, options: RecorderOptions = {}) => {
// Add expectations for mainframe navigations
page.on('framenavigated', async (frame: puppeteer.Frame) => {
if (frame.parentFrame()) return;
await paused;
addLineToPuppeteerScript(
`expect(page.url()).resolves.toBe('${frame.url()}');`
);
Expand Down
2 changes: 1 addition & 1 deletion src/runner.ts
Expand Up @@ -23,7 +23,7 @@ export { expect };

declare const __dirname;

const timeout = t => new Promise(cb => timers.setTimeout(cb, t));
const timeout = (t) => new Promise((cb) => timers.setTimeout(cb, t));

let browser, page;
let delay = 100;
Expand Down
136 changes: 136 additions & 0 deletions test/getselector.spec.ts
@@ -0,0 +1,136 @@
/**
* @jest-environment node
*/

/**
* Copyright 2020 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { readFileSync } from 'fs';
import * as path from 'path';
import * as puppeteer from 'puppeteer';

let browser: puppeteer.Browser, page: puppeteer.Page;
let isSubmitButton, getSelector;

describe.skip('DOM', () => {
beforeAll(async () => {
browser = await puppeteer.launch({ defaultViewport: null, headless: true });
page = await browser.newPage();
const script = readFileSync(path.join(__dirname, 'lib/dom-helpers.js'), {
encoding: 'utf-8',
});
await page.evaluate(script);
});

afterAll(async () => {
await browser.close();
});

describe('isSubmitButton', () => {
it('should return true if the button is a submit button', async () => {
await page.setContent(`<form><button id='button' /></form>`);

const element = await page.$('button');
const isSubmitCheck = await element.evaluate((element) =>
isSubmitButton(element)
);
expect(isSubmitCheck).toBe(true);
});

it('should return false if the button is not a submit button', async () => {
await page.setContent(`<button id='button' />`);
const element = await page.$('button');
const isSubmitCheck = await element.evaluate((element) =>
isSubmitButton(element)
);
expect(isSubmitCheck).toBe(false);
});
});

describe('getSelector', () => {
it('should return the aria name if it is available', async () => {
await page.setContent(
`<form><button id='button'>Hello World</button></form>`
);

const element = await page.$('button');
const selector = await element.evaluate((element) =>
getSelector(element)
);
expect(selector).toBe('aria/Hello World[role="button"]');
});

it('should return an aria name selector for the closest link or button', async () => {
await page.setContent(
`<form><button><span id='button'>Hello World</span></button></form>`
);

const element = await page.$('button');
const selector = await element.evaluate((element) =>
getSelector(element)
);
expect(selector).toBe('aria/Hello World[role="button"]');
});

it.skip('should return an aria name selector for the closest link or button if the text is not an exact match', async () => {
await page.setContent(
`<form><button><span id='button'>Hello</span> World</button></form>`
);

const element = await page.$('#button');
const selector = await element.evaluate((element) =>
getSelector(element)
);
expect(selector).toBe('aria/Hello World[role="button"]');
});

it('should return css selector if the element is not identifiable by an aria selector', async () => {
await page.setContent(
`<form><div><span id='button'>Hello</span> World</div></form>`
);

const element = await page.$('#button');
const selector = await element.evaluate((element) =>
getSelector(element)
);
expect(selector).toBe('#button');
});

it('should pierce shadow roots to get an aria name', async () => {
await page.setContent(
`
<script>
window.addEventListener('DOMContentLoaded', () => {
const link = document.createElement('a');
link.setAttribute('role', 'link');
link.textContent = 'Hello ';
document.body.appendChild(link);
const span1 = document.createElement('span');
link.appendChild(span1);
const shadow = span1.attachShadow({mode: 'open'});
const span2 = document.createElement('span');
span2.textContent = 'World';
shadow.appendChild(span2);
});
</script>
`
);
const link = await page.$('a');
const selector = await link.evaluate((element) => getSelector(element));
expect(selector).toBe('aria/Hello World[role="link"]');
});
});
});

0 comments on commit 9c56851

Please sign in to comment.