Skip to content

Commit

Permalink
feat: CLI to test extension implementations (#435)
Browse files Browse the repository at this point in the history
* feat: cli to test extension implementations

* feat: cli command to test extensions
  • Loading branch information
OrKoN authored Jan 24, 2023
1 parent 5555e8a commit edd9628
Show file tree
Hide file tree
Showing 17 changed files with 429 additions and 229 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ lib
coverage/
tsconfig.tsbuildinfo
.tmp
test/resources/spec.html
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,19 @@ console.log(

## Others

### Test your extensions using the replay lib

The replay lib offers a canonical recording and a test page that allows to
verify that your extension produces all expected side effects on a page.

The test command supports both stringify and runner extensions. The stringify
extension will be tested by running the stringified script using node. Run the
test using the following command.

```
npx -p @puppeteer/replay replay-extension-test --ext path-to-your-extension-js
```

### Create a Chrome extension for Recorder (Available from Chrome 104 onwards)

You can create a Chrome extension for [Recorder](https://goo.gle/devtools-recorder). Refer to the [Chrome Extensions documentation](https://developer.chrome.com/docs/extensions/mv3/devtools/) for more details on how to extend DevTools.
Expand Down
7 changes: 7 additions & 0 deletions examples/cli-extension/extension.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import puppeteer from 'puppeteer';
import { PuppeteerRunnerExtension } from '../../lib/main.js';

export default class Extension extends PuppeteerRunnerExtension {
async beforeAllSteps(flow) {
if (!this.browser) {
this.browser = await puppeteer.launch();
}
if (!this.page) {
this.page = await this.browser.newPage();
}
await super.beforeAllSteps(flow);
console.log('starting');
}
Expand Down
29 changes: 29 additions & 0 deletions examples/extend-stringify/extension.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { PuppeteerStringifyExtension } from '../../lib/main.js';

export default class Extension extends PuppeteerStringifyExtension {
// beforeAllSteps?(out: LineWriter, flow: UserFlow): Promise<void>;
async beforeAllSteps(...args) {
await super.beforeAllSteps(...args);
args[0].appendLine('console.log("starting");');
}

// beforeEachStep?(out: LineWriter, step: Step, flow: UserFlow): Promise<void>;
async beforeEachStep(...args) {
await super.beforeEachStep(...args);
const [out, step] = args;
out.appendLine(`console.log("about to execute step ${step.type}")`);
}

// afterEachStep?(out: LineWriter, step: Step, flow: UserFlow): Promise<void>;
async afterEachStep(...args) {
const [out, step] = args;
out.appendLine(`console.log("finished step ${step.type}")`);
await super.afterEachStep(...args);
}

// afterAllSteps?(out: LineWriter, flow: UserFlow): Promise<void>;
async afterAllSteps(...args) {
args[0].appendLine('console.log("finished");');
await super.afterAllSteps(...args);
}
}
30 changes: 2 additions & 28 deletions examples/extend-stringify/main.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,6 @@
import { stringify, PuppeteerStringifyExtension } from '../../lib/main.js';
import { stringify } from '../../lib/main.js';

class Extension extends PuppeteerStringifyExtension {
// beforeAllSteps?(out: LineWriter, flow: UserFlow): Promise<void>;
async beforeAllSteps(...args) {
await super.beforeAllSteps(...args);
args[0].appendLine('console.log("starting");');
}

// beforeEachStep?(out: LineWriter, step: Step, flow: UserFlow): Promise<void>;
async beforeEachStep(...args) {
await super.beforeEachStep(...args);
const [out, step] = args;
out.appendLine(`console.log("about to execute step ${step.type}")`);
}

// afterEachStep?(out: LineWriter, step: Step, flow: UserFlow): Promise<void>;
async afterEachStep(...args) {
const [out, step] = args;
out.appendLine(`console.log("finished step ${step.type}")`);
await super.afterEachStep(...args);
}

// afterAllSteps?(out: LineWriter, flow: UserFlow): Promise<void>;
async afterAllSteps(...args) {
args[0].appendLine('console.log("finished");');
await super.afterAllSteps(...args);
}
}
import Extension from './extension.js';

console.log(
await stringify(
Expand Down
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
"description": "Replay is a library which provides an API to replay and stringify recordings created using Chrome DevTools Recorder](https://developer.chrome.com/docs/devtools/recorder/)",
"main": "lib/cjs/main.cjs",
"types": "lib/main.d.ts",
"bin": "lib/cli.js",
"bin": {
"@puppeteer/replay": "lib/cli.js",
"replay-extension-test": "lib/extension-test.js"
},
"exports": {
".": {
"import": {
Expand Down
20 changes: 20 additions & 0 deletions rollup.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,24 @@ module.exports = [
],
plugins: [typescript({ tsconfig: 'tsconfig.cli.json' })],
},
{
input: 'src/extension-test.ts',
output: {
file: 'lib/extension-test.js',
format: 'es',
sourcemap: true,
banner: '#!/usr/bin/env node',
},
external: [
...Object.keys({ ...pkg.dependencies, ...pkg.peerDependencies }),
'../lib/main.js',
'fs',
'path',
'url',
'process',
'yargs/helpers',
'http',
],
plugins: [typescript({ tsconfig: 'tsconfig.cli.json' })],
},
];
16 changes: 8 additions & 8 deletions src/CLIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ export function createStatusReport(results: Result[]): Table.Table {
return table;
}

export async function importExtensionFromPath(path: string): Promise<any> {
const module = await import(
pathToFileURL(isAbsolute(path) ? path : join(cwd(), path)).toString()
);
return module.default;
}

export async function runFiles(
files: string[],
opts: { log: boolean; headless: boolean | 'new'; extension?: string } = {
Expand All @@ -142,14 +149,7 @@ export async function runFiles(
let browser: Browser | undefined;

if (opts.extension) {
const module = await import(
pathToFileURL(
isAbsolute(opts.extension)
? opts.extension
: join(cwd(), opts.extension)
).toString()
);
Extension = module.default;
Extension = await importExtensionFromPath(opts.extension);
}

const results: Result[] = [];
Expand Down
190 changes: 190 additions & 0 deletions src/Spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
export 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'],
['#button'],
['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',
},
],
};

export const expectedLog = `window dimensions 900x700
click targetId=button button=0 value=
click targetId=button button=0 value=
dblclick targetId=button button=0 value=
change targetId=input button=undefined value=test
change targetId=input-prefilled button=undefined value=testSuffix
contextmenu targetId=input button=2 value=test
mouseenter targetId=hover button=0 value=
change targetId=select button=undefined value=optionB
`.trim();

export 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 +
' value=' +
event.target.value
);
}
logStr(\`window dimensions \${window.innerWidth}x\${window.innerHeight}\`);
input.addEventListener('contextmenu', (e) => e.preventDefault(), false);
</script>`,
],
]);
Loading

3 comments on commit edd9628

@badraly1
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

package-lock.json

@badraly1
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

npm install @puppeteer/replay --save

@Hmadhmry31

This comment was marked as off-topic.

Please sign in to comment.