chrome-devtools-frontend
Version:
Chrome DevTools UI
331 lines (326 loc) • 9.03 kB
JavaScript
#!/usr/bin/env node
import { parseArgs } from 'node:util';
import { RunnerExtension, createRunner, parse, stringify } from './main.js';
import 'fs';
import { isAbsolute, join } from 'path';
import { pathToFileURL } from 'url';
import { cwd } from 'process';
import http from 'http';
import assert from 'assert/strict';
import { spawn } from 'child_process';
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
async function importExtensionFromPath(path) {
const module = await import(pathToFileURL(isAbsolute(path) ? path : join(cwd(), path)).toString());
return module.default;
}
const recording = {
title: 'spec',
steps: [
{
type: 'setViewport',
width: 900,
height: 700,
deviceScaleFactor: 1,
isMobile: false,
hasTouch: false,
isLandscape: false,
},
{
type: 'navigate',
url: 'http://localhost:8907/spec.html',
assertedEvents: [
{
type: 'navigation',
url: 'http://localhost:8907/spec.html',
title: '',
},
],
},
{
type: 'click',
target: 'main',
selectors: [
['aria/Click'],
['#button'],
['xpath///*[@id="button"]'],
['text/Click'],
],
offsetY: 18,
offsetX: 36,
},
{
type: 'doubleClick',
target: 'main',
selectors: [
['aria/Click'],
['#button'],
['xpath///*[@id="button"]'],
['text/Click'],
],
offsetY: 18,
offsetX: 36,
},
{
type: 'keyDown',
target: 'main',
key: 'Tab',
},
{
type: 'keyUp',
key: 'Tab',
target: 'main',
},
{
type: 'change',
value: 'test',
selectors: [['#input'], ['xpath///*[@id="input"]']],
target: 'main',
},
{
type: 'change',
value: 'testSuffix',
selectors: [['#input-prefilled']],
target: 'main',
},
{
type: 'keyDown',
target: 'main',
key: 'Enter',
},
{
type: 'keyUp',
key: 'Enter',
target: 'main',
},
{
type: 'click',
selectors: [['#input'], ['xpath///*[@id="input"]']],
target: 'main',
button: 'secondary',
offsetX: 1,
offsetY: 1,
},
{
type: 'hover',
target: 'main',
selectors: [
['aria/Hover'],
['#hover'],
['xpath///*[@id="hover"]'],
['text/Hover'],
],
},
{
type: 'waitForExpression',
expression: 'new Promise(resolve => setTimeout(() => resolve(true), 500))',
},
{
type: 'waitForElement',
target: 'main',
selectors: ['#button'],
count: 1,
visible: true,
properties: {
id: 'button',
},
attributes: {
id: 'button',
},
},
{
type: 'change',
value: 'optionB',
selectors: [['#select']],
target: 'main',
},
],
};
const expectedLog = `window dimensions 900x700
click targetId=button button=0 detail=1 value=
click targetId=button button=0 detail=1 value=
click targetId=button button=0 detail=2 value=
dblclick targetId=button button=0 detail=2 value=
change targetId=input button=undefined detail=undefined value=test
change targetId=input-prefilled button=undefined detail=undefined value=testSuffix
contextmenu targetId=input button=2 detail=0 value=test
mouseenter targetId=hover button=0 detail=0 value=
change targetId=select button=undefined detail=undefined value=optionB
`.trim();
const files = new Map([
[
'spec.html',
`<!DOCTYPE html>
<button id="button" onclick="logEvent(event)" ondblclick="logEvent(event)">
Click
</button>
<button
id="hover"
onmouseenter="logEvent(event)"
onmouseleave="logEvent(event)"
>
Hover
</button>
<input id="input" oncontextmenu="logEvent(event)" onchange="logEvent(event)" />
<input id="input-prefilled" onchange="logEvent(event)" value="test" />
<select id="select" onchange="logEvent(event)">
<option value=""></option>
<option value="optionA">Option A</option>
<option value="optionB">Option B</option>
</select>
<pre id="log"></pre>
<script>
function logStr(str) {
log.innerText += str;
const data = { username: 'example' };
fetch('/log', {
method: 'POST',
headers: {
'Content-Type': 'application/text',
},
body: str,
})
.catch((error) => {
console.error(error);
});
}
function logEvent(event) {
logStr(
'\\n' +
event.type +
' targetId=' +
event.target.id +
' button=' +
event.button +
' detail=' +
event.detail +
' value=' +
event.target.value
);
}
logStr(\`window dimensions \${window.innerWidth}x\${window.innerHeight}\`);
input.addEventListener('contextmenu', (e) => e.preventDefault(), false);
</script>`,
],
]);
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
async function startServer() {
const log = {
contents: '',
};
const server = http.createServer(async (req, res) => {
if (req.method === 'GET') {
for (const [file, content] of files.entries()) {
if (req.url?.endsWith(file)) {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(content);
return;
}
}
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not found');
return;
}
if (req.method === 'POST') {
const body = await new Promise((resolve) => {
const body = [];
req
.on('data', (chunk) => {
body.push(chunk);
})
.on('end', () => {
resolve(Buffer.concat(body).toString());
});
});
res.writeHead(200);
res.end();
log.contents += body;
return;
}
res.writeHead(400);
res.end();
});
return new Promise((resolve) => {
server.listen(8907, 'localhost', () => {
resolve({ server, log });
});
});
}
const { values } = parseArgs({
options: {
extension: {
type: 'string',
},
ext: {
type: 'string',
},
help: {
type: 'boolean',
short: 'h',
default: false,
},
},
});
if (values.help) {
console.log(`
Usage: replay-extension-test [options]
Options:
--ext, --extension The path to the extension module. The default export will be used as a Stringify or Runner extension based on instanceOf checks.
-h, --help Show help
`);
process.exit(0);
}
const extensionPath = values.extension ?? values.ext;
if (!extensionPath) {
console.error('Error: Missing required argument: extension');
process.exit(1);
}
const Extension = await importExtensionFromPath(extensionPath);
const ext = new Extension();
let run = async () => { };
if (ext instanceof RunnerExtension) {
console.log('runner');
run = async () => {
const extension = new Extension();
const runner = await createRunner(parse(recording), extension);
await runner.run();
};
}
else {
run = async () => {
const exported = await stringify(parse(recording), {
extension: new Extension(),
});
const childProcess = spawn('node', {
stdio: ['pipe', 'pipe', 'inherit'],
shell: true,
});
childProcess.stdin.write(exported);
childProcess.stdin.end();
await new Promise((resolve, reject) => {
childProcess.on('close', (code) => code
? reject(new Error(`Running node failed with code ${code}`))
: resolve());
});
};
}
const { server, log } = await startServer();
try {
await run();
}
catch (err) {
console.error(err);
}
finally {
server.close();
}
assert.equal(log.contents, expectedLog);
console.log('Run matches the expectations');
//# sourceMappingURL=extension-test.js.map