├── .c8rc ├── .eslintignore ├── .eslintrc.cjs ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── release-please.yml └── workflows │ ├── main.yml │ ├── pre-release.yml │ ├── publish.yml │ └── scorecards-analysis.yml ├── .gitignore ├── .mocharc.cjs ├── .npmrc ├── .prettierignore ├── .release-please-manifest.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── __snapshots__ ├── JSONStringifyExtension.test.ts.js ├── JSONUtils.test.ts.js ├── LighthouseStringifyExtension.test.ts.js ├── PuppeteerReplayStringifyExtension.test.ts.js ├── PuppeteerStringifyExtension.test.ts.js ├── lighthouse.test.ts.js ├── stringify.test.ts.js └── stringifyStep.test.ts.js ├── docs.tsconfig.json ├── docs ├── api │ ├── .nojekyll │ ├── README.md │ ├── classes │ │ ├── JSONStringifyExtension.md │ │ ├── LighthouseRunnerExtension.md │ │ ├── LighthouseStringifyExtension.md │ │ ├── PuppeteerReplayStringifyExtension.md │ │ ├── PuppeteerRunnerExtension.md │ │ ├── PuppeteerRunnerOwningBrowserExtension.md │ │ ├── PuppeteerStringifyExtension.md │ │ ├── Runner.md │ │ ├── RunnerExtension.md │ │ └── StringifyExtension.md │ ├── enums │ │ ├── AssertedEventType.md │ │ ├── Schema.AssertedEventType.md │ │ ├── Schema.SelectorType.md │ │ ├── Schema.StepType.md │ │ ├── SelectorType.md │ │ └── StepType.md │ ├── interfaces │ │ ├── BaseStep.md │ │ ├── ChangeStep.md │ │ ├── ClickAttributes.md │ │ ├── ClickStep.md │ │ ├── CloseStep.md │ │ ├── CustomStepParams.md │ │ ├── DoubleClickStep.md │ │ ├── EmulateNetworkConditionsStep.md │ │ ├── HoverStep.md │ │ ├── KeyDownStep.md │ │ ├── KeyUpStep.md │ │ ├── LineWriter.md │ │ ├── NavigateStep.md │ │ ├── NavigationEvent.md │ │ ├── Schema.BaseStep.md │ │ ├── Schema.ChangeStep.md │ │ ├── Schema.ClickAttributes.md │ │ ├── Schema.ClickStep.md │ │ ├── Schema.CloseStep.md │ │ ├── Schema.CustomStepParams.md │ │ ├── Schema.DoubleClickStep.md │ │ ├── Schema.EmulateNetworkConditionsStep.md │ │ ├── Schema.HoverStep.md │ │ ├── Schema.KeyDownStep.md │ │ ├── Schema.KeyUpStep.md │ │ ├── Schema.NavigateStep.md │ │ ├── Schema.NavigationEvent.md │ │ ├── Schema.ScrollPageStep.md │ │ ├── Schema.SetViewportStep.md │ │ ├── Schema.StepWithFrame.md │ │ ├── Schema.StepWithSelectors.md │ │ ├── Schema.StepWithTarget.md │ │ ├── Schema.UserFlow.md │ │ ├── Schema.WaitForElementStep.md │ │ ├── Schema.WaitForExpressionStep.md │ │ ├── ScrollPageStep.md │ │ ├── SetViewportStep.md │ │ ├── StepWithFrame.md │ │ ├── StepWithSelectors.md │ │ ├── StepWithTarget.md │ │ ├── StringifyOptions.md │ │ ├── UserFlow.md │ │ ├── WaitForElementStep.md │ │ └── WaitForExpressionStep.md │ └── modules │ │ └── Schema.md ├── code-of-conduct.md └── contributing.md ├── examples ├── chrome-extension-replay │ ├── DevToolsPlugin.html │ ├── DevToolsPlugin.js │ ├── README.md │ ├── Replay.html │ ├── Replay.js │ └── manifest.json ├── chrome-extension │ ├── DevToolsPlugin.html │ ├── DevToolsPlugin.js │ └── manifest.json ├── cjs │ ├── main.js │ └── package.json ├── cli-extension │ └── extension.js ├── extend-runner │ └── main.js ├── extend-stringify │ ├── extension.js │ └── main.js ├── replay-from-file-using-puppeteer │ ├── main.js │ └── recording.json └── stringify-as-puppeteer-script │ └── main.js ├── package-lock.json ├── package.json ├── prettier.config.cjs ├── release-please-config.json ├── rollup.config.cjs ├── src ├── CLIUtils.ts ├── InMemoryLineWriter.ts ├── JSONStringifyExtension.ts ├── JSONUtils.ts ├── LineWriter.ts ├── PuppeteerReplayStringifyExtension.ts ├── PuppeteerRunnerExtension.ts ├── PuppeteerStringifyExtension.ts ├── Runner.ts ├── RunnerExtension.ts ├── Schema.ts ├── SchemaUtils.ts ├── Spec.ts ├── StringifyExtension.ts ├── cli.ts ├── extension-test.ts ├── lighthouse │ ├── LighthouseRunnerExtension.ts │ ├── LighthouseStringifyExtension.ts │ └── helpers.ts ├── main.ts ├── stringify.ts ├── types.ts └── vlq.ts ├── test ├── InMemoryLineWriter.test.ts ├── JSONStringifyExtension.test.ts ├── JSONUtils.test.ts ├── PuppeteerReplayStringifyExtension.test.ts ├── PuppeteerStringifyExtension.test.ts ├── SchemaUtils.test.ts ├── cli.test.ts ├── e2e │ ├── cli.test.ts │ └── lighthouse.test.ts ├── lighthouse │ └── LighthouseStringifyExtension.test.ts ├── resources │ ├── checkbox.html │ ├── empty.html │ ├── folder-test │ │ ├── replay-fail.json │ │ └── replay.json │ ├── form.html │ ├── iframe1.html │ ├── iframe2.html │ ├── input.html │ ├── invisible-parent.html │ ├── local-iframe1.html │ ├── local-to-oopif.html │ ├── main.html │ ├── main2.html │ ├── oopif.html │ ├── popup.html │ ├── replay-fail.json │ ├── replay.json │ ├── scroll-into-view.html │ ├── scroll.html │ ├── select.html │ ├── shadow-dynamic.html │ └── svg.html ├── runner.test.ts ├── spec.test.ts ├── stringify.test.ts ├── stringifyStep.test.ts ├── tsconfig.json └── vlq.test.ts ├── third_party └── testserver │ ├── LICENSE │ ├── README.md │ ├── cert.pem │ ├── key.pem │ ├── package.json │ ├── src │ └── index.ts │ └── tsconfig.json ├── tsconfig.base.json ├── tsconfig.cli.json └── tsconfig.json /.c8rc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": ["text", "html"], 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | coverage 4 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | mocha: true, 7 | }, 8 | extends: ['eslint:recommended', 'google', 'plugin:prettier/recommended'], 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | ecmaVersion: 'latest', 12 | sourceType: 'module', 13 | }, 14 | plugins: ['@typescript-eslint', 'eslint-plugin-tsdoc'], 15 | rules: { 16 | 'require-jsdoc': 0, 17 | 'no-redeclare': 0, 18 | 'valid-jsdoc': 0, // Figure jsdoc once we look into documentation. 19 | 'tsdoc/syntax': 'warn', 20 | 'no-unused-vars': 'off', 21 | '@typescript-eslint/no-unused-vars': 'off', 22 | }, 23 | ignorePatterns: ['third_party/**/*'], 24 | }; 25 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Declare files that will always have LF line endings on checkout. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | ## Actual Behavior 4 | 5 | ## Steps to Reproduce the Problem 6 | 7 | 1. 8 | 1. 9 | 1. 10 | 11 | ## Specifications 12 | 13 | - Version: 14 | - Platform: 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | > It's a good idea to open an issue first for discussion. 4 | 5 | - [ ] Tests pass 6 | - [ ] Appropriate changes to README are included in PR 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 3 8 | allow: 9 | - dependency-name: 'puppeteer' 10 | - dependency-name: 'puppeteer-core' 11 | - dependency-type: 'production' 12 | - package-ecosystem: github-actions 13 | directory: '/' 14 | schedule: 15 | interval: daily 16 | open-pull-requests-limit: 2 17 | -------------------------------------------------------------------------------- /.github/release-please.yml: -------------------------------------------------------------------------------- 1 | releaseType: node 2 | primaryBranch: main 3 | handleGHRelease: true 4 | manifest: true 5 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: run-checks 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | merge_group: 8 | push: 9 | branches: 10 | - main 11 | pull_request: 12 | branches: 13 | - main 14 | 15 | concurrency: 16 | group: ${{ github.head_ref || github.run_id }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | main-checks: 21 | runs-on: ${{ matrix.os }} 22 | strategy: 23 | matrix: 24 | # https://github.com/actions/virtual-environments#available-environments 25 | os: [ubuntu-latest, macos-latest, windows-latest] 26 | # https://github.com/nodejs/Release#release-schedule 27 | node: [14, 16, 18] 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | with: 32 | fetch-depth: 2 33 | 34 | - name: Set up Node.js 35 | uses: actions/setup-node@v3 36 | with: 37 | node-version: ${{ matrix.node }} 38 | 39 | - name: Install dependencies 40 | run: | 41 | npm ci 42 | 43 | - name: Build 44 | run: | 45 | npm run build 46 | 47 | - name: Run code checks 48 | run: | 49 | npm run lint 50 | 51 | - name: Run tests 52 | uses: nick-invision/retry@v2 53 | with: 54 | max_attempts: 3 55 | command: npm run test 56 | timeout_minutes: 10 57 | headful-checks: 58 | runs-on: ${{ matrix.os }} 59 | strategy: 60 | matrix: 61 | os: [ubuntu-latest] 62 | node: [16] 63 | steps: 64 | - name: Checkout 65 | uses: actions/checkout@v3 66 | with: 67 | fetch-depth: 2 68 | 69 | - name: Set up Node.js 70 | uses: actions/setup-node@v3 71 | with: 72 | node-version: ${{ matrix.node }} 73 | 74 | - name: Install dependencies 75 | run: | 76 | sudo apt-get install xvfb 77 | npm ci 78 | 79 | - name: Build 80 | run: | 81 | npm run build 82 | 83 | - name: Run tests in headful 84 | uses: nick-invision/retry@v2 85 | with: 86 | max_attempts: 3 87 | command: xvfb-run --auto-servernum npm run test:headful 88 | timeout_minutes: 10 89 | -------------------------------------------------------------------------------- /.github/workflows/pre-release.yml: -------------------------------------------------------------------------------- 1 | name: Pre-release 2 | 3 | on: 4 | push: 5 | branches: 6 | - release-please-* 7 | 8 | jobs: 9 | pre-release: 10 | if: "startsWith(github.event.head_commit.message, 'chore(main): release')" 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} 19 | - name: Install dependencies 20 | run: npm ci 21 | - name: Build library 22 | run: npm run build 23 | - name: Build docs 24 | run: npm run docs 25 | - name: Format 26 | run: npm run format 27 | - name: Commit 28 | run: | 29 | git config --global user.name 'release-please[bot]' 30 | git config --global user.email '55107282+release-please[bot]@users.noreply.github.com' 31 | git add --all 32 | if ! git diff-index --quiet HEAD; then 33 | git commit -m 'chore: generate docs' 34 | git push --no-verify 35 | fi 36 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Install dependencies 17 | run: npm ci 18 | - name: Build 19 | run: npm run build 20 | - name: Publish @puppeteer/replay 21 | env: 22 | NPM_TOKEN: ${{secrets.NPM_TOKEN_REPLAY}} 23 | run: | 24 | npm config set registry 'https://wombat-dressing-room.appspot.com/' 25 | npm config set '//wombat-dressing-room.appspot.com/:_authToken' '${NPM_TOKEN}' 26 | npm publish --access=public 27 | -------------------------------------------------------------------------------- /.github/workflows/scorecards-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Scorecards supply-chain security 2 | on: 3 | # Only the default branch is supported. 4 | branch_protection_rule: 5 | schedule: 6 | - cron: '23 8 * * 6' 7 | push: 8 | branches: [main] 9 | 10 | # Declare default permissions as read only. 11 | permissions: read-all 12 | 13 | jobs: 14 | analysis: 15 | name: Scorecards analysis 16 | runs-on: ubuntu-latest 17 | permissions: 18 | # Needed to upload the results to code-scanning dashboard. 19 | security-events: write 20 | actions: read 21 | contents: read 22 | # Needed to access OIDC token. 23 | id-token: write 24 | 25 | steps: 26 | - name: 'Checkout code' 27 | uses: actions/checkout@v3 # v2.4.0 28 | with: 29 | persist-credentials: false 30 | 31 | - name: 'Run analysis' 32 | uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v1.0.2 33 | with: 34 | results_file: results.sarif 35 | results_format: sarif 36 | repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} 37 | # Publish the results to enable scorecard badges. For more details, see 38 | # https://github.com/ossf/scorecard-action#publishing-results. 39 | publish_results: true 40 | 41 | # Upload the results as artifacts (optional). 42 | - name: 'Upload artifact' 43 | uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v2.3.1 44 | with: 45 | name: SARIF file 46 | path: results.sarif 47 | retention-days: 5 48 | 49 | # Upload the results to GitHub’s code scanning dashboard. 50 | - name: 'Upload to code-scanning' 51 | uses: github/codeql-action/upload-sarif@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v1.0.26 52 | with: 53 | sarif_file: results.sarif 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | .DS_Store 3 | *.swp 4 | *.pyc 5 | .vscode 6 | yarn.lock 7 | lib 8 | coverage/ 9 | tsconfig.tsbuildinfo 10 | .tmp 11 | test/resources/spec.html -------------------------------------------------------------------------------- /.mocharc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reporter: 'spec', 3 | // Allow `console.log`s to show up during test execution 4 | logLevel: 'debug', 5 | exit: !!process.env.CI, 6 | spec: 'test/**/*.test.ts', 7 | extension: ['ts'], 8 | timeout: 25 * 1000, 9 | loader: 'ts-node/esm', 10 | }; 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | third_party/ 4 | vendor/ 5 | package-lock.json 6 | yarn.lock 7 | package.json 8 | docs-dist/ 9 | website/ 10 | coverage/ 11 | CHANGELOG.md -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "2.10.2" 3 | } 4 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | The @puppeteer/replay project takes security very seriously. Please use Chromium's process to report security issues. 4 | 5 | ## Reporting a Vulnerability 6 | 7 | See https://www.chromium.org/Home/chromium-security/reporting-security-bugs/ 8 | -------------------------------------------------------------------------------- /__snapshots__/JSONStringifyExtension.test.ts.js: -------------------------------------------------------------------------------- 1 | exports['JSONStringifyExtension should print the script for a click step 1'] = ` 2 | { 3 | "type": "click", 4 | "target": "main", 5 | "selectors": [ 6 | "aria/Test" 7 | ], 8 | "offsetX": 1, 9 | "offsetY": 1, 10 | "assertedEvents": [ 11 | { 12 | "type": "navigation" 13 | } 14 | ] 15 | } 16 | 17 | `; 18 | 19 | exports['JSONStringifyExtension should print an entire script 1'] = ` 20 | { 21 | "title": "test", 22 | "steps": [ 23 | { 24 | "type": "click", 25 | "target": "main", 26 | "selectors": [ 27 | "aria/Test" 28 | ], 29 | "offsetX": 1, 30 | "offsetY": 1, 31 | "assertedEvents": [ 32 | { 33 | "type": "navigation" 34 | } 35 | ] 36 | }, 37 | { 38 | "type": "click", 39 | "target": "main", 40 | "selectors": [ 41 | "aria/Test" 42 | ], 43 | "offsetX": 1, 44 | "offsetY": 1, 45 | "assertedEvents": [ 46 | { 47 | "type": "navigation" 48 | } 49 | ] 50 | } 51 | ] 52 | } 53 | //# recorderSourceMap=BDORO 54 | 55 | `; 56 | -------------------------------------------------------------------------------- /__snapshots__/JSONUtils.test.ts.js: -------------------------------------------------------------------------------- 1 | exports['JSONUtils should format JSON as JS 1'] = ` 2 | { 3 | title: 'test', 4 | test: true, 5 | steps: [ 6 | { 7 | type: 'click', 8 | target: 'main', 9 | selectors: [ 10 | 'aria/Test', 11 | [ 12 | '.cls', 13 | '.cls' 14 | ] 15 | ], 16 | offsetX: 1, 17 | offsetY: 1, 18 | assertedEvents: [ 19 | { 20 | type: 'navigation' 21 | } 22 | ] 23 | }, 24 | { 25 | type: 'click', 26 | target: 'main', 27 | selectors: [ 28 | 'aria/Test' 29 | ], 30 | offsetX: 1, 31 | offsetY: 1, 32 | assertedEvents: [ 33 | { 34 | type: 'navigation' 35 | } 36 | ] 37 | } 38 | ], 39 | otherTest: 1.234, 40 | nullTest: null 41 | } 42 | `; 43 | 44 | exports['JSONUtils should properly escape 2 | -------------------------------------------------------------------------------- /examples/chrome-extension-replay/DevToolsPlugin.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const view = await chrome.devtools.recorder.createView( 3 | /* name= */ 'CoffeeTest', 4 | /* pagePath= */ 'Replay.html' 5 | ); 6 | 7 | let latestRecording; 8 | 9 | view.onShown.addListener(() => { 10 | // Recorder has shown the view. Send additional data to the view if needed. 11 | chrome.runtime.sendMessage(JSON.stringify(latestRecording)); 12 | }); 13 | 14 | view.onHidden.addListener(() => { 15 | // Recorder has hidden the view. 16 | }); 17 | 18 | export class RecorderPlugin { 19 | replay(recording) { 20 | // Share the data with the view. 21 | latestRecording = recording; 22 | // Request to show the view. 23 | view.show(); 24 | } 25 | } 26 | 27 | /* eslint-disable no-undef */ 28 | chrome.devtools.recorder.registerRecorderExtensionPlugin( 29 | new RecorderPlugin(), 30 | /* name=*/ 'CoffeeTest' 31 | ); 32 | -------------------------------------------------------------------------------- /examples/chrome-extension-replay/README.md: -------------------------------------------------------------------------------- 1 | # Example extension demonstrating how to extend the replay button in DevTools 2 | 3 | To test the extension: 4 | 5 | 1. [Load your extension locally](https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked). 6 | 2. Open a new tab and a new DevTools window. 7 | 3. In the Recorder panel, create a new recording. 8 | 4. Expand the replay menu and press **Via CoffeeTest** 9 | -------------------------------------------------------------------------------- /examples/chrome-extension-replay/Replay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 |
12 | CoffeeTest is a dummy extension that emulates a basic auth flow and displays 13 | the recording in DOM. 14 |
15 | 16 |
17 | 18 | -------------------------------------------------------------------------------- /examples/chrome-extension-replay/Replay.js: -------------------------------------------------------------------------------- 1 | // Wait for the message from the extension to replay. 2 | // You can also provide static content via the HTML page 3 | // or have a different workflow around the events. 4 | /* eslint-disable no-undef */ 5 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 6 | localStorage.setItem('recording', request); 7 | main(); 8 | }); 9 | 10 | const initHTML = document.body.innerHTML; 11 | 12 | async function main() { 13 | const recording = JSON.parse(localStorage.getItem('recording')); 14 | // Dummy auth flow. Please use the best practices for authenticating users. 15 | const token = localStorage.getItem('token'); 16 | if (!token) { 17 | document.body.innerHTML = ` 18 |
19 | Name: 20 | Password: 21 | 22 |
23 | `; 24 | document.querySelector('form').onsubmit = (event) => { 25 | event.preventDefault(); 26 | fetch('http://example.com', { mode: 'no-cors' }) 27 | .then((response) => { 28 | localStorage.setItem('token', 'token'); 29 | main(); 30 | }) 31 | .catch((err) => console.log(err)); 32 | }; 33 | return; 34 | } 35 | 36 | document.body.innerHTML = initHTML; 37 | document.querySelector('button').addEventListener('click', () => { 38 | localStorage.removeItem('token'); 39 | main(); 40 | }); 41 | 42 | // Emulating replay 43 | document.querySelector( 44 | '#recording' 45 | ).innerHTML = `Running ${recording.title}
`; 46 | for (let i = 0; i < recording.steps.length; i++) { 47 | document.querySelector( 48 | '#recording' 49 | ).innerHTML += `Running step ${i}: ${JSON.stringify( 50 | recording.steps[i] 51 | )}
`; 52 | await new Promise((resolve) => setTimeout(resolve, 1000)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/chrome-extension-replay/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "version": "0.1.0", 4 | "name": "CoffeeTestExtension", 5 | "description": "DevTools extension showing how to extend the replay functionality of the Recorder panel in Chrome DevTools.", 6 | "permissions": [], 7 | "devtools_page": "DevToolsPlugin.html", 8 | "content_security_policy": { 9 | "extension_pages": "script-src 'self'; object-src 'self'" 10 | }, 11 | "minimum_chrome_version": "112.0.5569.0" 12 | } 13 | -------------------------------------------------------------------------------- /examples/chrome-extension/DevToolsPlugin.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/chrome-extension/DevToolsPlugin.js: -------------------------------------------------------------------------------- 1 | export class RecorderPlugin { 2 | async stringify(recording) { 3 | return JSON.stringify(recording, null, 2); 4 | } 5 | async stringifyStep(step) { 6 | return JSON.stringify(step, null, 2); 7 | } 8 | } 9 | 10 | /* eslint-disable no-undef */ 11 | chrome.devtools.recorder.registerRecorderExtensionPlugin( 12 | new RecorderPlugin(), 13 | /* name=*/ 'Custom JSON', 14 | /* mediaType=*/ 'application/json' 15 | ); 16 | -------------------------------------------------------------------------------- /examples/chrome-extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "version": "0.1.0", 4 | "name": "CustomJSONExtension", 5 | "description": "DevTools extensions showing how to extend the Recorder panel.", 6 | "permissions": [], 7 | "devtools_page": "DevToolsPlugin.html", 8 | "content_security_policy": { 9 | "extension_pages": "script-src 'self'; object-src 'self'" 10 | }, 11 | "minimum_chrome_version": "104.0.5107.0" 12 | } 13 | -------------------------------------------------------------------------------- /examples/cjs/main.js: -------------------------------------------------------------------------------- 1 | const { 2 | createRunner, 3 | PuppeteerRunnerExtension, 4 | } = require('../../lib/cjs/main.js'); 5 | const puppeteer = require('puppeteer'); 6 | 7 | async function main() { 8 | const browser = await puppeteer.launch({ 9 | headless: true, 10 | }); 11 | 12 | const page = await browser.newPage(); 13 | 14 | class Extension extends PuppeteerRunnerExtension { 15 | async beforeAllSteps(flow) { 16 | await super.beforeAllSteps(flow); 17 | console.log('starting'); 18 | } 19 | 20 | async beforeEachStep(step, flow) { 21 | await super.beforeEachStep(step, flow); 22 | console.log('before', step); 23 | } 24 | 25 | async afterEachStep(step, flow) { 26 | await super.afterEachStep(step, flow); 27 | console.log('after', step); 28 | } 29 | 30 | async afterAllSteps(flow) { 31 | await super.afterAllSteps(flow); 32 | console.log('done'); 33 | } 34 | } 35 | 36 | const runner = await createRunner( 37 | { 38 | title: 'Test recording', 39 | steps: [ 40 | { 41 | type: 'navigate', 42 | url: 'https://wikipedia.org', 43 | }, 44 | ], 45 | }, 46 | new Extension(browser, page, 7000) 47 | ); 48 | 49 | await runner.run(); 50 | 51 | await browser.close(); 52 | } 53 | 54 | main(); 55 | -------------------------------------------------------------------------------- /examples/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /examples/cli-extension/extension.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | import { PuppeteerRunnerExtension } from '../../lib/main.js'; 3 | 4 | export default class Extension extends PuppeteerRunnerExtension { 5 | async beforeAllSteps(flow) { 6 | if (!this.browser) { 7 | this.browser = await puppeteer.launch(); 8 | } 9 | if (!this.page) { 10 | this.page = await this.browser.newPage(); 11 | } 12 | await super.beforeAllSteps(flow); 13 | console.log('starting'); 14 | } 15 | 16 | async beforeEachStep(step, flow) { 17 | await super.beforeEachStep(step, flow); 18 | console.log('before', step); 19 | } 20 | 21 | async afterEachStep(step, flow) { 22 | await super.afterEachStep(step, flow); 23 | console.log('after', step); 24 | } 25 | 26 | async afterAllSteps(flow) { 27 | await super.afterAllSteps(flow); 28 | console.log('done'); 29 | await this.browser.close(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/extend-runner/main.js: -------------------------------------------------------------------------------- 1 | import { createRunner, PuppeteerRunnerExtension } from '../../lib/main.js'; 2 | import puppeteer from 'puppeteer'; 3 | 4 | const browser = await puppeteer.launch({ 5 | headless: true, 6 | }); 7 | 8 | const page = await browser.newPage(); 9 | 10 | class Extension extends PuppeteerRunnerExtension { 11 | async beforeAllSteps(flow) { 12 | await super.beforeAllSteps(flow); 13 | console.log('starting'); 14 | } 15 | 16 | async beforeEachStep(step, flow) { 17 | await super.beforeEachStep(step, flow); 18 | console.log('before', step); 19 | } 20 | 21 | async afterEachStep(step, flow) { 22 | await super.afterEachStep(step, flow); 23 | console.log('after', step); 24 | } 25 | 26 | async afterAllSteps(flow) { 27 | await super.afterAllSteps(flow); 28 | console.log('done'); 29 | } 30 | } 31 | 32 | const runner = await createRunner( 33 | { 34 | title: 'Test recording', 35 | steps: [ 36 | { 37 | type: 'navigate', 38 | url: 'https://wikipedia.org', 39 | }, 40 | ], 41 | }, 42 | new Extension(browser, page, 7000) 43 | ); 44 | 45 | await runner.run(); 46 | 47 | await browser.close(); 48 | -------------------------------------------------------------------------------- /examples/extend-stringify/extension.js: -------------------------------------------------------------------------------- 1 | import { PuppeteerStringifyExtension } from '../../lib/main.js'; 2 | 3 | export default class Extension extends PuppeteerStringifyExtension { 4 | // beforeAllSteps?(out: LineWriter, flow: UserFlow): Promise; 5 | async beforeAllSteps(...args) { 6 | await super.beforeAllSteps(...args); 7 | args[0].appendLine('console.log("starting");'); 8 | } 9 | 10 | // beforeEachStep?(out: LineWriter, step: Step, flow: UserFlow): Promise; 11 | async beforeEachStep(...args) { 12 | await super.beforeEachStep(...args); 13 | const [out, step] = args; 14 | out.appendLine(`console.log("about to execute step ${step.type}")`); 15 | } 16 | 17 | // afterEachStep?(out: LineWriter, step: Step, flow: UserFlow): Promise; 18 | async afterEachStep(...args) { 19 | const [out, step] = args; 20 | out.appendLine(`console.log("finished step ${step.type}")`); 21 | await super.afterEachStep(...args); 22 | } 23 | 24 | // afterAllSteps?(out: LineWriter, flow: UserFlow): Promise; 25 | async afterAllSteps(...args) { 26 | args[0].appendLine('console.log("finished");'); 27 | await super.afterAllSteps(...args); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/extend-stringify/main.js: -------------------------------------------------------------------------------- 1 | import { stringify } from '../../lib/main.js'; 2 | 3 | import Extension from './extension.js'; 4 | 5 | console.log( 6 | await stringify( 7 | { 8 | title: 'Test recording', 9 | steps: [ 10 | { 11 | type: 'navigate', 12 | url: 'https://wikipedia.org', 13 | }, 14 | ], 15 | }, 16 | { 17 | extension: new Extension(), 18 | indentation: ' ', // use tab to indent lines 19 | } 20 | ) 21 | ); 22 | -------------------------------------------------------------------------------- /examples/replay-from-file-using-puppeteer/main.js: -------------------------------------------------------------------------------- 1 | import { createRunner, parse } from '../../lib/main.js'; 2 | import fs from 'fs'; 3 | 4 | // Read recording for a file. 5 | const recordingText = fs.readFileSync('./recording.json', 'utf8'); 6 | // Validate & parse the file. 7 | const recording = parse(JSON.parse(recordingText)); 8 | // Create a runner and execute the script. 9 | const runner = await createRunner(recording); 10 | await runner.run(); 11 | -------------------------------------------------------------------------------- /examples/replay-from-file-using-puppeteer/recording.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Test recording", 3 | "steps": [ 4 | { 5 | "type": "navigate", 6 | "url": "https://wikipedia.org" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /examples/stringify-as-puppeteer-script/main.js: -------------------------------------------------------------------------------- 1 | import { stringify } from '../../lib/main.js'; 2 | 3 | console.log( 4 | await stringify({ 5 | title: 'Test recording', 6 | steps: [], 7 | }) 8 | ); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@puppeteer/replay", 3 | "version": "2.10.2", 4 | "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/)", 5 | "main": "lib/cjs/main.cjs", 6 | "types": "lib/main.d.ts", 7 | "bin": { 8 | "@puppeteer/replay": "lib/cli.js", 9 | "replay-extension-test": "lib/extension-test.js" 10 | }, 11 | "exports": { 12 | ".": { 13 | "import": { 14 | "types": "./lib/main.d.ts", 15 | "default": "./lib/main.js" 16 | }, 17 | "require": { 18 | "types": "./lib/cjs/main.d.cts", 19 | "default": "./lib/cjs/main.cjs" 20 | } 21 | } 22 | }, 23 | "repository": "github:puppeteer/replay", 24 | "directories": { 25 | "doc": "docs" 26 | }, 27 | "engines": { 28 | "node": ">=14" 29 | }, 30 | "scripts": { 31 | "test": "npm run build:testserver && cross-env TS_NODE_PROJECT=test/tsconfig.json mocha --config .mocharc.cjs", 32 | "test:headful": "cross-env PUPPETEER_HEADFUL=true npm run test", 33 | "test:coverage": "c8 npm run test", 34 | "test:fast": "npm run test -- --ignore=test/e2e/**/*", 35 | "lint": "npm run lint:format", 36 | "lint:format": "prettier --check . && npm run eslint", 37 | "format": "prettier --write . && eslint --ext js --ext ts --fix .", 38 | "eslint": "([ \"$CI\" = true ] && eslint --ext js --ext ts --quiet -f codeframe . || eslint --ext js --ext ts .)", 39 | "docs": "typedoc --tsconfig docs.tsconfig.json --readme none --gitRevision main --externalPattern --excludeExternals --excludeProtected --excludePrivate --plugin typedoc-plugin-markdown --out docs/api src/main.ts && npm run format", 40 | "clean": "rimraf lib", 41 | "build": "npm run clean && rollup --config rollup.config.cjs", 42 | "build:testserver": "cd third_party/testserver && npm run build" 43 | }, 44 | "files": [ 45 | "lib", 46 | "!lib/**/*.d.ts", 47 | "!lib/**/*.d.ts.map", 48 | "lib/main.d.ts", 49 | "lib/cjs/main.d.cts" 50 | ], 51 | "author": "Chrome DevTools authors", 52 | "license": "Apache-2.0", 53 | "type": "module", 54 | "devDependencies": { 55 | "@rollup/plugin-typescript": "9.0.1", 56 | "@types/chai": "4.3.3", 57 | "@types/mime": "3.0.1", 58 | "@types/mocha": "10.0.0", 59 | "@types/node": "18.11.0", 60 | "@types/ws": "8.5.3", 61 | "@types/yargs": "17.0.13", 62 | "@typescript-eslint/eslint-plugin": "5.40.0", 63 | "@typescript-eslint/parser": "5.40.0", 64 | "c8": "7.12.0", 65 | "chai": "4.3.6", 66 | "cross-env": "7.0.3", 67 | "eslint": "8.25.0", 68 | "eslint-config-google": "0.14.0", 69 | "eslint-config-prettier": "8.5.0", 70 | "eslint-plugin-prettier": "4.2.1", 71 | "eslint-plugin-tsdoc": "0.2.17", 72 | "lighthouse": "^10.0.1", 73 | "mime": "3.0.0", 74 | "mocha": "10.1.0", 75 | "prettier": "2.7.1", 76 | "puppeteer": "19.9.0", 77 | "puppeteer-core": "19.8.5", 78 | "rimraf": "3.0.2", 79 | "rollup": "3.2.2", 80 | "rollup-plugin-dts": "5.0.0", 81 | "snap-shot-it": "7.9.6", 82 | "ts-node": "10.9.1", 83 | "tslib": "2.4.0", 84 | "typedoc": "0.23.16", 85 | "typedoc-plugin-markdown": "3.13.6", 86 | "typescript": "4.8.4" 87 | }, 88 | "peerDependencies": { 89 | "puppeteer": ">=18.0.5", 90 | "lighthouse": ">=10.0.0" 91 | }, 92 | "peerDependenciesMeta": { 93 | "puppeteer": { 94 | "optional": true 95 | }, 96 | "lighthouse": { 97 | "optional": true 98 | } 99 | }, 100 | "dependencies": { 101 | "cli-table3": "0.6.3", 102 | "colorette": "2.0.19", 103 | "yargs": "17.6.2" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'es5', 4 | singleQuote: true, 5 | }; 6 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "last-release-sha": "11d33e5439d98c6f248bf6f6c0b27043936d67a1", 3 | "packages": { 4 | ".": { 5 | "releaseType": "node", 6 | "include-component-in-tag": false 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /rollup.config.cjs: -------------------------------------------------------------------------------- 1 | const typescript = require('@rollup/plugin-typescript').default; 2 | const dts = require('rollup-plugin-dts').default; 3 | const pkg = require('./package.json'); 4 | 5 | module.exports = [ 6 | { 7 | input: 'src/main.ts', 8 | output: [ 9 | { 10 | file: 'lib/main.js', 11 | sourcemap: true, 12 | format: 'es', 13 | }, 14 | { 15 | file: 'lib/cjs/main.cjs', 16 | sourcemap: true, 17 | format: 'cjs', 18 | }, 19 | ], 20 | external: Object.keys(pkg.peerDependencies), 21 | plugins: [typescript({ module: 'NodeNext' })], 22 | }, 23 | { 24 | input: 'src/main.ts', 25 | output: [ 26 | { 27 | file: 'lib/main.d.ts', 28 | format: 'es', 29 | }, 30 | { 31 | file: 'lib/cjs/main.d.cts', 32 | format: 'cjs', 33 | }, 34 | ], 35 | external: [ 36 | 'devtools-protocol', 37 | 'devtools-protocol/types/protocol-mapping.js', 38 | ], 39 | plugins: [ 40 | dts({ 41 | compilerOptions: { declaration: true }, 42 | }), 43 | ], 44 | }, 45 | { 46 | input: 'src/cli.ts', 47 | output: { 48 | file: 'lib/cli.js', 49 | format: 'es', 50 | sourcemap: true, 51 | banner: '#!/usr/bin/env node', 52 | }, 53 | external: [ 54 | ...Object.keys({ ...pkg.dependencies, ...pkg.peerDependencies }), 55 | '../lib/main.js', 56 | 'fs', 57 | 'path', 58 | 'url', 59 | 'process', 60 | 'yargs/helpers', 61 | ], 62 | plugins: [typescript({ tsconfig: 'tsconfig.cli.json' })], 63 | }, 64 | { 65 | input: 'src/extension-test.ts', 66 | output: { 67 | file: 'lib/extension-test.js', 68 | format: 'es', 69 | sourcemap: true, 70 | banner: '#!/usr/bin/env node', 71 | }, 72 | external: [ 73 | ...Object.keys({ ...pkg.dependencies, ...pkg.peerDependencies }), 74 | '../lib/main.js', 75 | 'fs', 76 | 'path', 77 | 'url', 78 | 'process', 79 | 'yargs/helpers', 80 | 'http', 81 | ], 82 | plugins: [typescript({ tsconfig: 'tsconfig.cli.json' })], 83 | }, 84 | ]; 85 | -------------------------------------------------------------------------------- /src/InMemoryLineWriter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { LineWriter } from './LineWriter.js'; 18 | 19 | export class InMemoryLineWriter implements LineWriter { 20 | #indentation: string; 21 | #currentIndentation = 0; 22 | #lines: string[] = []; 23 | 24 | constructor(indentation: string) { 25 | this.#indentation = indentation; 26 | } 27 | 28 | appendLine(line: string): LineWriter { 29 | const lines = line.split('\n').map((line) => { 30 | const indentedLine = line 31 | ? this.#indentation.repeat(this.#currentIndentation) + line.trimEnd() 32 | : ''; 33 | return indentedLine; 34 | }); 35 | 36 | this.#lines.push(...lines); 37 | return this; 38 | } 39 | 40 | startBlock(): LineWriter { 41 | this.#currentIndentation++; 42 | return this; 43 | } 44 | 45 | endBlock(): LineWriter { 46 | this.#currentIndentation--; 47 | return this; 48 | } 49 | 50 | toString(): string { 51 | // Scripts should end with a final blank line. 52 | return this.#lines.join('\n') + '\n'; 53 | } 54 | 55 | getIndent(): string { 56 | return this.#indentation; 57 | } 58 | 59 | getSize(): number { 60 | return this.#lines.length; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/JSONStringifyExtension.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import type { LineWriter } from './LineWriter.js'; 18 | import type { Step, UserFlow } from './Schema.js'; 19 | import { StringifyExtension } from './StringifyExtension.js'; 20 | 21 | /** 22 | * Stringifies a user flow to JSON with source maps. 23 | * 24 | * You probably want to strip the source map because not all 25 | * parsers support comments in JSON. 26 | */ 27 | export class JSONStringifyExtension extends StringifyExtension { 28 | override async beforeAllSteps(out: LineWriter, flow: UserFlow) { 29 | const copy = { 30 | ...flow, 31 | steps: undefined, 32 | }; 33 | // Stringify top-level attributes. 34 | const text = JSON.stringify(copy, null, out.getIndent()); 35 | const lines = text.split('\n'); 36 | lines.pop(); 37 | lines[lines.length - 1] += ','; 38 | lines.push(out.getIndent() + `"steps": [`); 39 | out.appendLine(lines.join('\n')).startBlock().startBlock(); 40 | } 41 | 42 | override async afterAllSteps(out: LineWriter) { 43 | out 44 | .endBlock() 45 | .endBlock() 46 | .appendLine(out.getIndent() + `]`) 47 | .appendLine('}'); 48 | } 49 | 50 | override async stringifyStep(out: LineWriter, step: Step, flow?: UserFlow) { 51 | const stepText = JSON.stringify(step, null, out.getIndent()); 52 | if (!flow) { 53 | out.appendLine(stepText); 54 | return; 55 | } 56 | const separator = 57 | flow.steps.lastIndexOf(step) === flow.steps.length - 1 ? '' : ','; 58 | out.appendLine(stepText + separator); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/LineWriter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | export interface LineWriter { 18 | appendLine(line: string): LineWriter; 19 | startBlock(): LineWriter; 20 | endBlock(): LineWriter; 21 | getIndent(): string; 22 | getSize(): number; 23 | } 24 | -------------------------------------------------------------------------------- /src/PuppeteerReplayStringifyExtension.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import type { LineWriter } from './LineWriter.js'; 18 | import type { Step } from './Schema.js'; 19 | import { StringifyExtension } from './StringifyExtension.js'; 20 | import { formatJSONAsJS } from './JSONUtils.js'; 21 | 22 | /** 23 | * Stringifies a user flow to a script that uses \@puppeteer/replay's own API. 24 | */ 25 | export class PuppeteerReplayStringifyExtension extends StringifyExtension { 26 | override async beforeAllSteps(out: LineWriter) { 27 | out.appendLine("import url from 'url';"); 28 | out.appendLine("import { createRunner } from '@puppeteer/replay';"); 29 | out.appendLine(''); 30 | out.appendLine('export async function run(extension) {').startBlock(); 31 | out.appendLine('const runner = await createRunner(extension);'); 32 | out.appendLine(''); 33 | out.appendLine('await runner.runBeforeAllSteps();'); 34 | out.appendLine(''); 35 | } 36 | 37 | override async afterAllSteps(out: LineWriter) { 38 | out.appendLine(''); 39 | out 40 | .appendLine('await runner.runAfterAllSteps();') 41 | .endBlock() 42 | .appendLine('}'); 43 | out.appendLine(''); 44 | out 45 | .appendLine( 46 | 'if (process && import.meta.url === url.pathToFileURL(process.argv[1]).href) {' 47 | ) 48 | .startBlock() 49 | .appendLine('run()') 50 | .endBlock() 51 | .appendLine('}'); 52 | } 53 | 54 | override async stringifyStep(out: LineWriter, step: Step) { 55 | out.appendLine( 56 | `await runner.runStep(${formatJSONAsJS(step, out.getIndent())});` 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/RunnerExtension.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { UserFlow, Step } from './Schema.js'; 18 | 19 | export class RunnerExtension { 20 | async beforeAllSteps?(flow?: UserFlow): Promise {} 21 | async afterAllSteps?(flow?: UserFlow): Promise {} 22 | async beforeEachStep?(step: Step, flow?: UserFlow): Promise {} 23 | async runStep(step: Step, flow?: UserFlow): Promise {} 24 | async afterEachStep?(step: Step, flow?: UserFlow): Promise {} 25 | } 26 | -------------------------------------------------------------------------------- /src/StringifyExtension.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { LineWriter } from './LineWriter.js'; 18 | import { Step, UserFlow } from './Schema.js'; 19 | 20 | export class StringifyExtension { 21 | async beforeAllSteps?(out: LineWriter, flow: UserFlow): Promise {} 22 | async afterAllSteps?(out: LineWriter, flow: UserFlow): Promise {} 23 | async beforeEachStep?( 24 | out: LineWriter, 25 | step: Step, 26 | flow?: UserFlow 27 | ): Promise {} 28 | async stringifyStep( 29 | out: LineWriter, 30 | step: Step, 31 | flow?: UserFlow 32 | ): Promise {} 33 | async afterEachStep?( 34 | out: LineWriter, 35 | step: Step, 36 | flow?: UserFlow 37 | ): Promise {} 38 | } 39 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import yargs from 'yargs'; 18 | import { hideBin } from 'yargs/helpers'; 19 | import { getHeadlessEnvVar, getRecordingPaths, runFiles } from './CLIUtils.js'; 20 | 21 | interface Arguments { 22 | files: string[]; 23 | extension?: string; 24 | headless?: string; 25 | } 26 | 27 | yargs(hideBin(process.argv)) 28 | .command( 29 | '$0 ', 30 | 'run files', 31 | () => {}, 32 | async (argv) => { 33 | const args = argv as unknown as Arguments; 34 | const recordingPaths = getRecordingPaths(args.files); 35 | 36 | await runFiles(recordingPaths, { 37 | log: true, 38 | headless: getHeadlessEnvVar( 39 | args.headless || process.env['PUPPETEER_HEADLESS'] 40 | ), 41 | extension: args.extension, 42 | }); 43 | } 44 | ) 45 | .option('headless', { 46 | type: 'string', 47 | description: "Run using the browser's headless mode.", 48 | choices: ['new', 'true', '1', '0', 'false'], 49 | }) 50 | .option('extension', { 51 | alias: 'ext', 52 | type: 'string', 53 | description: 'Run using an extension identified by the path.', 54 | }) 55 | .parse(); 56 | -------------------------------------------------------------------------------- /src/lighthouse/LighthouseRunnerExtension.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { PuppeteerRunnerExtension } from '../PuppeteerRunnerExtension.js'; 18 | import type { Step, UserFlow } from '../Schema.js'; 19 | import { StepType } from '../Schema.js'; 20 | import { isMobileFlow, isNavigationStep } from './helpers.js'; 21 | 22 | export class LighthouseRunnerExtension extends PuppeteerRunnerExtension { 23 | #isTimespanRunning = false; 24 | #isNavigationRunning = false; 25 | #lhFlow?: import('lighthouse').UserFlow; 26 | 27 | async createFlowResult(): Promise { 28 | if (!this.#lhFlow) { 29 | throw new Error('Cannot get flow result before running the flow'); 30 | } 31 | return this.#lhFlow.createFlowResult(); 32 | } 33 | 34 | override async beforeAllSteps(flow: UserFlow) { 35 | await super.beforeAllSteps?.(flow); 36 | 37 | const { startFlow, desktopConfig } = await import('lighthouse'); 38 | 39 | let config = undefined; 40 | if (!isMobileFlow(flow)) { 41 | config = desktopConfig; 42 | } 43 | 44 | this.#lhFlow = await startFlow(this.page, { 45 | config, 46 | flags: { screenEmulation: { disabled: true } }, 47 | name: flow.title, 48 | }); 49 | } 50 | 51 | override async beforeEachStep(step: Step, flow?: UserFlow) { 52 | await super.beforeEachStep?.(step, flow); 53 | if (step.type === StepType.SetViewport) return; 54 | 55 | if (isNavigationStep(step)) { 56 | if (this.#isTimespanRunning) { 57 | await this.#lhFlow!.endTimespan(); 58 | this.#isTimespanRunning = false; 59 | } 60 | await this.#lhFlow!.startNavigation(); 61 | this.#isNavigationRunning = true; 62 | } else if (!this.#isTimespanRunning) { 63 | await this.#lhFlow!.startTimespan(); 64 | this.#isTimespanRunning = true; 65 | } 66 | } 67 | 68 | override async afterEachStep(step: Step, flow?: UserFlow) { 69 | if (this.#isNavigationRunning) { 70 | await this.#lhFlow!.endNavigation(); 71 | this.#isNavigationRunning = false; 72 | } 73 | await super.afterEachStep?.(step, flow); 74 | } 75 | 76 | override async afterAllSteps(flow: UserFlow) { 77 | if (this.#isTimespanRunning) { 78 | await this.#lhFlow!.endTimespan(); 79 | } 80 | await super.afterAllSteps?.(flow); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/lighthouse/LighthouseStringifyExtension.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { PuppeteerStringifyExtension } from '../PuppeteerStringifyExtension.js'; 18 | 19 | import type { LineWriter } from '../LineWriter.js'; 20 | import { Step, StepType, UserFlow } from '../Schema.js'; 21 | 22 | import { isNavigationStep, isMobileFlow } from './helpers.js'; 23 | import { formatJSONAsJS } from '../JSONUtils.js'; 24 | 25 | export class LighthouseStringifyExtension extends PuppeteerStringifyExtension { 26 | #isProcessingTimespan = false; 27 | 28 | override async beforeAllSteps(out: LineWriter, flow: UserFlow) { 29 | out.appendLine(`const fs = require('fs');`); 30 | 31 | await super.beforeAllSteps(out, flow); 32 | 33 | out.appendLine( 34 | `const lhApi = await import('lighthouse'); // v10.0.0 or later` 35 | ); 36 | 37 | const flags = { 38 | screenEmulation: { 39 | disabled: true, 40 | }, 41 | }; 42 | out.appendLine(`const flags = ${formatJSONAsJS(flags, out.getIndent())}`); 43 | 44 | if (isMobileFlow(flow)) { 45 | out.appendLine(`const config = undefined;`); 46 | } else { 47 | out.appendLine('const config = lhApi.desktopConfig;'); 48 | } 49 | 50 | out.appendLine( 51 | `const lhFlow = await lhApi.startFlow(page, {name: ${formatJSONAsJS( 52 | flow.title, 53 | out.getIndent() 54 | )}, config, flags});` 55 | ); 56 | } 57 | 58 | override async stringifyStep(out: LineWriter, step: Step, flow: UserFlow) { 59 | if (step.type === StepType.SetViewport) { 60 | await super.stringifyStep(out, step, flow); 61 | return; 62 | } 63 | 64 | const isNavigation = isNavigationStep(step); 65 | 66 | if (isNavigation) { 67 | if (this.#isProcessingTimespan) { 68 | out.appendLine(`await lhFlow.endTimespan();`); 69 | this.#isProcessingTimespan = false; 70 | } 71 | out.appendLine(`await lhFlow.startNavigation();`); 72 | } else if (!this.#isProcessingTimespan) { 73 | out.appendLine(`await lhFlow.startTimespan();`); 74 | this.#isProcessingTimespan = true; 75 | } 76 | 77 | await super.stringifyStep(out, step, flow); 78 | 79 | if (isNavigation) { 80 | out.appendLine(`await lhFlow.endNavigation();`); 81 | } 82 | } 83 | 84 | override async afterAllSteps(out: LineWriter, flow: UserFlow) { 85 | if (this.#isProcessingTimespan) { 86 | out.appendLine(`await lhFlow.endTimespan();`); 87 | } 88 | out.appendLine(`const lhFlowReport = await lhFlow.generateReport();`); 89 | out.appendLine( 90 | `fs.writeFileSync(__dirname + '/flow.report.html', lhFlowReport)` 91 | ); 92 | await super.afterAllSteps(out, flow); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/lighthouse/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import type { Step, UserFlow } from '../Schema.js'; 18 | import { AssertedEventType, StepType } from '../Schema.js'; 19 | 20 | export function isNavigationStep(step: Step): boolean { 21 | return Boolean( 22 | step.type === StepType.Navigate || 23 | step.assertedEvents?.some( 24 | (event) => event.type === AssertedEventType.Navigation 25 | ) 26 | ); 27 | } 28 | 29 | export function isMobileFlow(flow: UserFlow): boolean { 30 | for (const step of flow.steps) { 31 | if (step.type === StepType.SetViewport) { 32 | return step.isMobile; 33 | } 34 | } 35 | return false; 36 | } 37 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * @packageDocumentation 19 | */ 20 | export * from './Schema.js'; 21 | export * as Schema from './Schema.js'; 22 | export * from './SchemaUtils.js'; 23 | export { StringifyExtension } from './StringifyExtension.js'; 24 | export { JSONStringifyExtension } from './JSONStringifyExtension.js'; 25 | export { 26 | stringify, 27 | stringifyStep, 28 | parseSourceMap, 29 | stripSourceMap, 30 | StringifyOptions, 31 | SourceMap, 32 | } from './stringify.js'; 33 | export { LineWriter } from './LineWriter.js'; 34 | export { RunnerExtension } from './RunnerExtension.js'; 35 | export { createRunner, Runner } from './Runner.js'; 36 | export { PuppeteerRunnerExtension } from './PuppeteerRunnerExtension.js'; 37 | export { PuppeteerRunnerOwningBrowserExtension } from './PuppeteerRunnerExtension.js'; 38 | export { PuppeteerStringifyExtension } from './PuppeteerStringifyExtension.js'; 39 | export { PuppeteerReplayStringifyExtension } from './PuppeteerReplayStringifyExtension.js'; 40 | export { LighthouseStringifyExtension } from './lighthouse/LighthouseStringifyExtension.js'; 41 | export { LighthouseRunnerExtension } from './lighthouse/LighthouseRunnerExtension.js'; 42 | export * from './JSONUtils.js'; 43 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type ExcludeType = { 2 | [K in keyof T]-?: T[K] extends U ? K : never; 3 | }[keyof T]; 4 | 5 | export type PickType = Pick>; 6 | 7 | export type JSONValue = 8 | | null 9 | | string 10 | | number 11 | | boolean 12 | | JSONObject 13 | | JSONArray; 14 | 15 | export interface JSONObject { 16 | [key: string]: JSONValue; 17 | } 18 | 19 | export type JSONArray = JSONValue[]; 20 | 21 | export type JSONSerializable = PickType< 22 | Object, 23 | JSONValue 24 | >; 25 | -------------------------------------------------------------------------------- /src/vlq.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const alpha = 18 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 19 | 20 | const charToIdx = alpha.split('').reduce((acc, char, idx) => { 21 | acc.set(char, idx); 22 | return acc; 23 | }, new Map()); 24 | 25 | const LEAST_5_BIT_MASK = 0b011111; 26 | const CONTINUATION_BIT_MASK = 0b100000; 27 | const MAX_INT = 2147483647; 28 | 29 | /** 30 | * Encoding variable length integer into base64 (6-bit): 31 | * 32 | * 1 N N N N N | 0 N N N N N 33 | * 34 | * The first bit indicates if there is more data for the int. 35 | */ 36 | export function encodeInt(num: number) { 37 | if (num < 0) { 38 | throw new Error('Only postive integers and zero are supported'); 39 | } 40 | if (num > MAX_INT) { 41 | throw new Error( 42 | 'Only integers between 0 and ' + MAX_INT + ' are supported' 43 | ); 44 | } 45 | const result = []; 46 | do { 47 | let payload = num & LEAST_5_BIT_MASK; 48 | num >>>= 5; 49 | if (num > 0) payload |= CONTINUATION_BIT_MASK; 50 | result.push(alpha[payload]); 51 | } while (num !== 0); 52 | return result.join(''); 53 | } 54 | 55 | export function encode(nums: number[]): string { 56 | const parts = []; 57 | for (const num of nums) { 58 | parts.push(encodeInt(num)); 59 | } 60 | return parts.join(''); 61 | } 62 | 63 | export function decode(str: string) { 64 | const results = []; 65 | const chrs = str.split(''); 66 | 67 | let result = 0; 68 | let shift = 0; 69 | for (const ch of chrs) { 70 | const num = charToIdx.get(ch); 71 | result |= (num & LEAST_5_BIT_MASK) << shift; 72 | shift += 5; 73 | const hasMore = num & CONTINUATION_BIT_MASK; 74 | if (!hasMore) { 75 | results.push(result); 76 | result = 0; 77 | shift = 0; 78 | } 79 | } 80 | 81 | return results; 82 | } 83 | -------------------------------------------------------------------------------- /test/InMemoryLineWriter.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { InMemoryLineWriter } from '../src/InMemoryLineWriter.js'; 18 | import { assert } from 'chai'; 19 | 20 | describe('InMemoryLineWriter', () => { 21 | it('should open and close blocks', () => { 22 | const out = new InMemoryLineWriter(' '); 23 | out.appendLine('{').startBlock(); 24 | out.appendLine('console.log("test");'); 25 | out.endBlock().appendLine('}'); 26 | assert.strictEqual( 27 | out.toString(), 28 | `{ 29 | console.log("test"); 30 | } 31 | ` 32 | ); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/JSONStringifyExtension.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import snapshot from 'snap-shot-it'; 18 | import { stringify } from '../src/stringify.js'; 19 | import { InMemoryLineWriter } from '../src/InMemoryLineWriter.js'; 20 | import { JSONStringifyExtension } from '../src/JSONStringifyExtension.js'; 21 | import { StepType, AssertedEventType } from '../src/Schema.js'; 22 | 23 | describe('JSONStringifyExtension', () => { 24 | const ext = new JSONStringifyExtension(); 25 | 26 | it('should print the script for a click step', async () => { 27 | const step = { 28 | type: StepType.Click as const, 29 | target: 'main', 30 | selectors: ['aria/Test'], 31 | offsetX: 1, 32 | offsetY: 1, 33 | assertedEvents: [{ type: AssertedEventType.Navigation as const }], 34 | }; 35 | const writer = new InMemoryLineWriter(' '); 36 | await ext.stringifyStep(writer, step); 37 | snapshot(writer.toString()); 38 | }); 39 | 40 | it('should print an entire script', async () => { 41 | const flow = { 42 | title: 'test', 43 | steps: [ 44 | { 45 | type: StepType.Click as const, 46 | target: 'main', 47 | selectors: ['aria/Test'], 48 | offsetX: 1, 49 | offsetY: 1, 50 | assertedEvents: [{ type: AssertedEventType.Navigation as const }], 51 | }, 52 | { 53 | type: StepType.Click as const, 54 | target: 'main', 55 | selectors: ['aria/Test'], 56 | offsetX: 1, 57 | offsetY: 1, 58 | assertedEvents: [{ type: AssertedEventType.Navigation as const }], 59 | }, 60 | ], 61 | }; 62 | snapshot( 63 | await stringify(flow, { 64 | extension: ext, 65 | }) 66 | ); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/JSONUtils.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import snapshot from 'snap-shot-it'; 18 | import { assert } from 'chai'; 19 | import { formatJSONAsJS } from '../src/JSONUtils.js'; 20 | import { StepType, AssertedEventType } from '../src/Schema.js'; 21 | 22 | describe('JSONUtils', () => { 23 | it('should format JSON as JS', async () => { 24 | const json = { 25 | title: 'test', 26 | test: true, 27 | steps: [ 28 | { 29 | type: StepType.Click as const, 30 | target: 'main', 31 | selectors: ['aria/Test', ['.cls', '.cls']], 32 | offsetX: 1, 33 | offsetY: 1, 34 | assertedEvents: [{ type: AssertedEventType.Navigation as const }], 35 | }, 36 | { 37 | type: StepType.Click as const, 38 | target: 'main', 39 | selectors: ['aria/Test'], 40 | offsetX: 1, 41 | offsetY: 1, 42 | assertedEvents: [{ type: AssertedEventType.Navigation as const }], 43 | }, 44 | ], 45 | otherTest: 1.234, 46 | undefinedTest: undefined, 47 | nullTest: null, 48 | }; 49 | const str = formatJSONAsJS(json, ' '); 50 | snapshot(str); 51 | delete json['undefinedTest']; 52 | assert.deepStrictEqual(Function(`'use strict';return (${str})`)(), json); 53 | }); 54 | 55 | it('should properly escape ', 59 | '' 60 | ) 61 | ); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/PuppeteerReplayStringifyExtension.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import snapshot from 'snap-shot-it'; 18 | import { stringify } from '../src/stringify.js'; 19 | import { InMemoryLineWriter } from '../src/InMemoryLineWriter.js'; 20 | import { PuppeteerReplayStringifyExtension } from '../src/PuppeteerReplayStringifyExtension.js'; 21 | import { StepType, AssertedEventType } from '../src/Schema.js'; 22 | 23 | describe('PuppeteerReplayStringifyExtension', () => { 24 | const ext = new PuppeteerReplayStringifyExtension(); 25 | 26 | it('should print the script for a click step', async () => { 27 | const step = { 28 | type: StepType.Click as const, 29 | target: 'main', 30 | selectors: ['aria/Test'], 31 | offsetX: 1, 32 | offsetY: 1, 33 | assertedEvents: [{ type: AssertedEventType.Navigation as const }], 34 | }; 35 | const writer = new InMemoryLineWriter(' '); 36 | await ext.stringifyStep(writer, step); 37 | snapshot(writer.toString()); 38 | }); 39 | 40 | it('should print an entire script', async () => { 41 | const step = { 42 | type: StepType.Click as const, 43 | target: 'main', 44 | selectors: ['aria/Test'], 45 | offsetX: 1, 46 | offsetY: 1, 47 | assertedEvents: [{ type: AssertedEventType.Navigation as const }], 48 | }; 49 | const flow = { title: 'test', steps: [step] }; 50 | snapshot( 51 | await stringify(flow, { 52 | extension: ext, 53 | }) 54 | ); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/PuppeteerStringifyExtension.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import snapshot from 'snap-shot-it'; 18 | import { InMemoryLineWriter } from '../src/InMemoryLineWriter.js'; 19 | import { PuppeteerStringifyExtension } from '../src/PuppeteerStringifyExtension.js'; 20 | import { AssertedEventType, StepType } from '../src/Schema.js'; 21 | 22 | describe('PuppeteerStringifyExtension', () => { 23 | const ext = new PuppeteerStringifyExtension(); 24 | 25 | it('should print the correct script for a click step', async () => { 26 | const step = { 27 | type: StepType.Click as const, 28 | target: 'main', 29 | selectors: ['aria/Test'], 30 | offsetX: 1, 31 | offsetY: 1, 32 | }; 33 | const flow = { title: 'test', steps: [step] }; 34 | 35 | const writer = new InMemoryLineWriter(' '); 36 | await ext.stringifyStep(writer, step, flow); 37 | snapshot(writer.toString()); 38 | }); 39 | 40 | it('should print the correct script for asserted events', async () => { 41 | const step = { 42 | type: StepType.Click as const, 43 | target: 'main', 44 | selectors: ['aria/Test'], 45 | offsetX: 1, 46 | offsetY: 1, 47 | assertedEvents: [{ type: AssertedEventType.Navigation as const }], 48 | }; 49 | const flow = { title: 'test', steps: [step] }; 50 | 51 | const writer = new InMemoryLineWriter(' '); 52 | await ext.stringifyStep(writer, step, flow); 53 | snapshot(writer.toString()); 54 | }); 55 | 56 | it('should print the correct script with a chain selector', async () => { 57 | const step = { 58 | type: StepType.Click as const, 59 | target: 'main', 60 | selectors: [['aria/Test', 'aria/Test2']], 61 | offsetX: 1, 62 | offsetY: 1, 63 | }; 64 | const flow = { title: 'test', steps: [step] }; 65 | 66 | const writer = new InMemoryLineWriter(' '); 67 | await ext.stringifyStep(writer, step, flow); 68 | snapshot(writer.toString()); 69 | }); 70 | 71 | it('should print the correct script for a change step', async () => { 72 | const step = { 73 | type: StepType.Change as const, 74 | target: 'main', 75 | selectors: ['aria/Test'], 76 | value: 'Hello World', 77 | }; 78 | const flow = { title: 'test', steps: [step] }; 79 | 80 | const writer = new InMemoryLineWriter(' '); 81 | await ext.stringifyStep(writer, step, flow); 82 | snapshot(writer.toString()); 83 | }); 84 | 85 | it('should print the correct script for a change step for non-text inputs', async () => { 86 | const step = { 87 | type: StepType.Change as const, 88 | target: 'main', 89 | selectors: ['aria/Test'], 90 | value: '#333333', 91 | }; 92 | const flow = { title: 'test', steps: [step] }; 93 | 94 | const writer = new InMemoryLineWriter(' '); 95 | await ext.stringifyStep(writer, step, flow); 96 | snapshot(writer.toString()); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/cli.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { 18 | getHeadlessEnvVar, 19 | getRecordingPaths, 20 | getJSONFilesFromFolder, 21 | } from '../src/CLIUtils.js'; 22 | import { assert } from 'chai'; 23 | import path from 'path'; 24 | import url from 'url'; 25 | 26 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 27 | 28 | describe('cli', () => { 29 | describe('getHeadlessEnvVar', () => { 30 | it('extracts the headless parameter from process.argv', () => { 31 | assert.strictEqual(getHeadlessEnvVar(undefined), true); 32 | assert.strictEqual(getHeadlessEnvVar('1'), true); 33 | assert.strictEqual(getHeadlessEnvVar('true'), true); 34 | assert.strictEqual(getHeadlessEnvVar('0'), false); 35 | assert.strictEqual(getHeadlessEnvVar('false'), false); 36 | assert.strictEqual(getHeadlessEnvVar('new'), 'new'); 37 | assert.strictEqual(getHeadlessEnvVar('True'), true); 38 | assert.strictEqual(getHeadlessEnvVar('False'), false); 39 | }); 40 | }); 41 | 42 | describe('getRecordingPaths', () => { 43 | it('is able to get recordings from a directory', () => { 44 | const recordingsFolderPath = 'test/resources/folder-test'; 45 | const recordingPaths = getRecordingPaths([recordingsFolderPath]); 46 | 47 | assert.isTrue( 48 | !!recordingPaths.find((path) => path.includes('replay.json')) 49 | ); 50 | assert.isTrue( 51 | !!recordingPaths.find((path) => path.includes('replay-fail.json')) 52 | ); 53 | }); 54 | }); 55 | 56 | describe('getJSONFilesFromFolder', () => { 57 | it('is able to return json files from a directory', () => { 58 | const files = getJSONFilesFromFolder( 59 | path.join(__dirname, 'resources', 'folder-test') 60 | ); 61 | 62 | assert.isTrue(files.every((file) => file.endsWith('.json'))); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/resources/checkbox.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 21 | -------------------------------------------------------------------------------- /test/resources/empty.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sofiayem/replay/821ff6e1fcb53fca8a255760a253584fb4eab71a/test/resources/empty.html -------------------------------------------------------------------------------- /test/resources/folder-test/replay-fail.json: -------------------------------------------------------------------------------- 1 | { 2 | "steps": [ 3 | { 4 | "type": "navigate", 5 | "url": "https://www.wikipedia.org/" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/resources/folder-test/replay.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "wiki", 3 | "steps": [ 4 | { 5 | "type": "navigate", 6 | "url": "https://www.wikipedia.org/" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test/resources/form.html: -------------------------------------------------------------------------------- 1 | 2 | 17 |
18 | 19 | 20 |
21 | -------------------------------------------------------------------------------- /test/resources/iframe1.html: -------------------------------------------------------------------------------- 1 | 16 |

iframe 1

17 | To iframe 2 18 | -------------------------------------------------------------------------------- /test/resources/iframe2.html: -------------------------------------------------------------------------------- 1 | 16 |

iframe 2

17 | To iframe 1 18 | -------------------------------------------------------------------------------- /test/resources/input.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |

38 | 
43 | 


--------------------------------------------------------------------------------
/test/resources/invisible-parent.html:
--------------------------------------------------------------------------------
 1 | 
15 | 
16 | 
17 | 18 |
19 | -------------------------------------------------------------------------------- /test/resources/local-iframe1.html: -------------------------------------------------------------------------------- 1 | 16 |

iframe 1

17 | 23 | 24 | -------------------------------------------------------------------------------- /test/resources/local-to-oopif.html: -------------------------------------------------------------------------------- 1 | 16 |

Navigation in local frame leading to an OOPIF

17 | 18 | -------------------------------------------------------------------------------- /test/resources/main.html: -------------------------------------------------------------------------------- 1 | 2 | 17 |
18 | 19 |
20 | 21 |
22 |
23 |
24 | 25 |
26 |
27 |
Hello World
28 |
29 | 30 |
31 | Hello  32 | 33 | 34 | Page 2 35 | 36 | 37 | 40 | 41 |
42 | 104 | -------------------------------------------------------------------------------- /test/resources/main2.html: -------------------------------------------------------------------------------- 1 | 2 | 17 |

Page 2

18 | Back to Page 1 19 | 20 | 28 | -------------------------------------------------------------------------------- /test/resources/oopif.html: -------------------------------------------------------------------------------- 1 | 16 |

Navigation in OOPIF iframes

17 | 23 | -------------------------------------------------------------------------------- /test/resources/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 17 |

Popup

18 | 19 | -------------------------------------------------------------------------------- /test/resources/replay-fail.json: -------------------------------------------------------------------------------- 1 | { 2 | "steps": [ 3 | { 4 | "type": "navigate", 5 | "url": "https://www.wikipedia.org/" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/resources/replay.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "wiki", 3 | "steps": [ 4 | { 5 | "type": "navigate", 6 | "url": "https://www.wikipedia.org/" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test/resources/scroll-into-view.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 26 | 36 | -------------------------------------------------------------------------------- /test/resources/scroll.html: -------------------------------------------------------------------------------- 1 | 2 | 17 |
18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /test/resources/select.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 24 | -------------------------------------------------------------------------------- /test/resources/shadow-dynamic.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 33 | 34 | -------------------------------------------------------------------------------- /test/resources/svg.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | -------------------------------------------------------------------------------- /test/stringifyStep.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { stringifyStep } from '../src/stringify.js'; 18 | import { assert } from 'chai'; 19 | import { StringifyExtension } from '../src/StringifyExtension.js'; 20 | import { Step, StepType, UserFlow } from '../src/Schema.js'; 21 | import { LineWriter } from '../src/LineWriter.js'; 22 | import snapshot from 'snap-shot-it'; 23 | 24 | describe('stringifyStep', () => { 25 | it('should stringify a single step', async () => { 26 | snapshot( 27 | await stringifyStep({ 28 | type: StepType.Navigate as const, 29 | url: 'https://localhost/', 30 | }) 31 | ); 32 | }); 33 | 34 | it('invokes all hooks in extensions relevant for stringifyStep', async () => { 35 | class DummyExtension implements StringifyExtension { 36 | async beforeAllSteps(out: LineWriter): Promise { 37 | out.appendLine('beforeAll'); 38 | } 39 | 40 | async beforeEachStep( 41 | out: LineWriter, 42 | step: Step, 43 | flow?: UserFlow 44 | ): Promise { 45 | out.appendLine('beforeStep ' + flow); 46 | } 47 | 48 | async stringifyStep( 49 | out: LineWriter, 50 | step: Step, 51 | flow?: UserFlow 52 | ): Promise { 53 | out.appendLine('stringifyStep ' + flow); 54 | } 55 | 56 | async afterEachStep( 57 | out: LineWriter, 58 | step: Step, 59 | flow?: UserFlow 60 | ): Promise { 61 | out.appendLine('afterStep ' + flow); 62 | } 63 | 64 | async afterAllSteps(out: LineWriter): Promise { 65 | out.appendLine('afterAll'); 66 | } 67 | } 68 | 69 | const extension = new DummyExtension(); 70 | assert.strictEqual( 71 | await stringifyStep( 72 | { type: StepType.CustomStep, name: 'test', parameters: {} }, 73 | { extension } 74 | ), 75 | [ 76 | 'beforeStep undefined', 77 | 'stringifyStep undefined', 78 | 'afterStep undefined', 79 | '', 80 | ].join('\n') 81 | ); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": false, 6 | "noEmit": true 7 | }, 8 | "include": ["**/*.ts"], 9 | "references": [{ "path": "../third_party/testserver/tsconfig.json" }] 10 | } 11 | -------------------------------------------------------------------------------- /test/vlq.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2022 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { encodeInt, decode, encode } from '../src/vlq.js'; 18 | import { assert } from 'chai'; 19 | 20 | describe('vlq', () => { 21 | it('should encode', () => { 22 | assert.strictEqual(encodeInt(0), 'A'); 23 | assert.strictEqual(encodeInt(1), 'B'); 24 | assert.strictEqual(encodeInt(123), '7D'); 25 | assert.strictEqual(encodeInt(123) + encodeInt(123456789), '7D1oz31D'); 26 | assert.strictEqual(encodeInt(123456789), '1oz31D'); 27 | assert.strictEqual(encodeInt(2147483647), '//////B'); 28 | }); 29 | it('should decode', () => { 30 | assert.deepStrictEqual(decode('A'), [0]); 31 | assert.deepStrictEqual(decode('C'), [2]); 32 | assert.deepStrictEqual(decode('D'), [3]); 33 | assert.deepStrictEqual(decode('7D'), [123]); 34 | assert.deepStrictEqual(decode('1oz31D'), [123456789]); 35 | assert.deepStrictEqual(decode('7D1oz31D'), [123, 123456789]); 36 | assert.deepStrictEqual(decode('//////B'), [2147483647]); 37 | }); 38 | it('should encode array', () => { 39 | assert.strictEqual(encode([0, 1, 123]), 'AB7D'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /third_party/testserver/README.md: -------------------------------------------------------------------------------- 1 | # TestServer 2 | 3 | This test server is used internally by Puppeteer to test Puppeteer itself. 4 | 5 | ### Example 6 | 7 | ```js 8 | const {TestServer} = require('@pptr/testserver'); 9 | 10 | (async(() => { 11 | const httpServer = await TestServer.create(__dirname, 8000), 12 | const httpsServer = await TestServer.createHTTPS(__dirname, 8001) 13 | httpServer.setRoute('/hello', (req, res) => { 14 | res.end('Hello, world!'); 15 | }); 16 | console.log('HTTP and HTTPS servers are running!'); 17 | })(); 18 | ``` 19 | -------------------------------------------------------------------------------- /third_party/testserver/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDWDCCAkCgAwIBAgIUM8Tmw+D1j+eVz9x9So4zRVqFsKowDQYJKoZIhvcNAQEL 3 | BQAwGjEYMBYGA1UEAwwPcHVwcGV0ZWVyLXRlc3RzMB4XDTIwMDUxMzA4MDQyOVoX 4 | DTMwMDUxMTA4MDQyOVowGjEYMBYGA1UEAwwPcHVwcGV0ZWVyLXRlc3RzMIIBIjAN 5 | BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApWbbhgc6CnWywd8xGETT1mfLi3wi 6 | KIbpAUHghLF4sj0jXz8vLh/4oicpQ12d6bsz+IAi7qrdXNh11P5nEej6/Gx4fWzB 7 | gGdrJFGPqsvXuhYdzZAmy6xOaWcLIJeQ543bXv3YeST7EGRXJBc/ocTo2jIGTGjq 8 | hksFaid910VQlX3KGOLTDMUCk00TeEYBTTUx47PWoIsxVqbl2RzVXRSWL5hlPWlW 9 | 29/BQtBGmsXxZyWtqqHudiUulGBSr4LcPyicZLI8nqCqD0ioS0TEmGh61nRBuwBa 10 | xmLCvPmpt0+sDuOU+1bme3w8juvTVToBIFxGB86rADd3ys+8NeZzXqi+bQIDAQAB 11 | o4GVMIGSMB0GA1UdDgQWBBT/m3vdkZpQyVQFdYrKHVoAHXDFODAfBgNVHSMEGDAW 12 | gBT/m3vdkZpQyVQFdYrKHVoAHXDFODAPBgNVHRMBAf8EBTADAQH/MD8GA1UdEQQ4 13 | MDaCGHd3dy5wdXBwZXRlZXItdGVzdHMudGVzdIIad3d3LnB1cHBldGVlci10ZXN0 14 | cy0xLnRlc3QwDQYJKoZIhvcNAQELBQADggEBAI1qp5ZppV1R3e8XxzwwkFDPFN8W 15 | Pe3AoqhAKyJnJl1NUn9q3sroEeSQRhODWUHCd7lENzhsT+3mzonNNkN9B/hq0rpK 16 | KHHczXILDqdyuxH3LxQ1VHGE8VN2NbdkfobtzAsA3woiJxOuGeusXJnKB4kJQeIP 17 | V+BMEZWeaSDC2PREkG7GOezmE1/WDUCYaorPw2whdCA5wJvTW3zXpJjYhfsld+5z 18 | KuErx4OCxRJij73/BD9SpLxDEY1cdl819F1IvxsRGhmTIaSly2hQLrhOgo1jgZtV 19 | FGCa6DSlXnQGLaV+N+ssR0lkCksNrNBVDfA1bP5bT/4VCcwUWwm9TUeF0Qo= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /third_party/testserver/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQClZtuGBzoKdbLB 3 | 3zEYRNPWZ8uLfCIohukBQeCEsXiyPSNfPy8uH/iiJylDXZ3puzP4gCLuqt1c2HXU 4 | /mcR6Pr8bHh9bMGAZ2skUY+qy9e6Fh3NkCbLrE5pZwsgl5Dnjdte/dh5JPsQZFck 5 | Fz+hxOjaMgZMaOqGSwVqJ33XRVCVfcoY4tMMxQKTTRN4RgFNNTHjs9agizFWpuXZ 6 | HNVdFJYvmGU9aVbb38FC0EaaxfFnJa2qoe52JS6UYFKvgtw/KJxksjyeoKoPSKhL 7 | RMSYaHrWdEG7AFrGYsK8+am3T6wO45T7VuZ7fDyO69NVOgEgXEYHzqsAN3fKz7w1 8 | 5nNeqL5tAgMBAAECggEAKPveo0xBHnxhidZzBM9xKixX7D0a/a3IKI6ZQmfzPz8U 9 | 97HhT+2OHyfS+qVEzribPRULEtZ1uV7Ne7R5958iKc/63yFGpTl6++nVzn1p++sl 10 | AV2Zr1gHqehlgnLr7eRhmh0OOZ5nM32ZdhDorH3tMLu6gc5xZktKkS4t6Vx8hj3a 11 | Docx+rbawp8GRd0p7I6vzIE3bsDab8hC+RTRO63q2G0BqgKwV9ZNtJxQgcDJ5L8N 12 | 6gtM2z5nKXAIOCbCQYa1PsrDh3IRA/ZNxEeA9G3YQjwlZYCWmdRRplgDraYxcTBO 13 | oQGjaLwICNdcprMacPD6cCSgrI+PadzyMsAuk9SgpQKBgQDO9PT4gK40Pm+Damxv 14 | +tWYBFmvn3vasmyolc1zVDltsxQbQTjKhVpTLXTTGmrIhDXEIIV9I4rg164WptQs 15 | 6Brp2EwYR7ZJIrjvXs/9i2QTW1ZXvhdiWpB3s+RXD5VHGovHUadcI6wOgw2Cl+Jk 16 | zXjSIgyXKM99N1MAonuR7DyzTwKBgQDMmPX+9vWZMpS/gc6JLQiPPoGszE6tYjXg 17 | W3LpRUNqmO0/bDDjslbebDgrGAmhlkJlxzH6gz96VmGm1evEGPEet3euy8S9zuM3 18 | LCgEM9Ulqa3JbInwtKupmKv76Im+XWLLSxAXbfiel1zFRRwxI99A3ad0QRZ6Bov5 19 | 3cHJBwvzgwKBgAU5HW2gIcVjxgC1EOOKmxVpFrJd/gw48JEYpsTAXWqtWFaPwNUr 20 | pGnw/b/OLN++pnS6tWPBH+Ioz1X3A+fWO8enE9SRCsKxw6UW6XzmpbHvXjB8ta5f 21 | xsGeoqan2AahXuG659RlehQrro2bM7WDkgcLoPG3r/TjDo83ipLWOXn1AoGAKWiL 22 | 4R56dpcWI+xRsNG8ecFc3Ww8QDswTEg16aBrFJf+7GcpPexKSJn+hDpJOLsAlTjL 23 | lLgbkNcKzIlfPkEOC/l175quJvxIYFI/hxo2eXjuA2ZERMNMOvb7V/CocC7WX+7B 24 | Qvyu5OodjI+ANTHdbXNvAMhrlCbfDaMkJVuXv6ECgYBzvY4aYmVoFsr+72/EfLls 25 | Dz9pi55tUUWc61w6ovd+iliawvXeGi4wibtTH4iGj/C2sJIaMmOD99NQ7Oi/x89D 26 | oMgSUemkoFL8FGsZGyZ7szqxyON1jP42Bm2MQrW5kIf7Y4yaIGhoak5JNxn2JUyV 27 | gupVbY1mQ1GTPByxHeLh1w== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /third_party/testserver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pptr/testserver", 3 | "version": "0.5.0", 4 | "description": "testing server", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "tsc" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/puppeteer/puppeteer/tree/main/utils/testserver" 12 | }, 13 | "author": "The Chromium Authors", 14 | "license": "Apache-2.0" 15 | } 16 | -------------------------------------------------------------------------------- /third_party/testserver/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "composite": true, 6 | "module": "CommonJS", 7 | "outDir": "lib", 8 | "rootDir": "src" 9 | }, 10 | "include": ["src"] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "alwaysStrict": true, 5 | "checkJs": true, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "module": "ESNext", 10 | "moduleResolution": "NodeNext", 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitAny": true, 13 | "noImplicitOverride": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noPropertyAccessFromIndexSignature": true, 17 | "noUncheckedIndexedAccess": true, 18 | "noUnusedLocals": true, 19 | "resolveJsonModule": true, 20 | "sourceMap": true, 21 | "strict": true, 22 | "strictBindCallApply": true, 23 | "strictFunctionTypes": true, 24 | "strictNullChecks": true, 25 | "strictPropertyInitialization": true, 26 | "target": "ES2019", 27 | "useUnknownInCatchVariables": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/cli.ts", 5 | "src/CLIUtils.ts", 6 | "src/extension-test.ts", 7 | "src/Spec.ts" 8 | ], 9 | "exclude": [] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "stripInternal": true 6 | }, 7 | "include": ["src"], 8 | "exclude": [ 9 | "src/cli.ts", 10 | "src/CLIUtils.ts", 11 | "src/extension-test.ts", 12 | "src/Spec.ts" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------