├── test ├── assets │ ├── sw.js │ ├── empty.html │ ├── simple.json │ ├── file-to-upload.txt │ ├── frames │ │ ├── script.js │ │ ├── style.css │ │ ├── frame.html │ │ ├── two-frames.html │ │ └── nested-frames.html │ ├── injectedstyle.css │ ├── one-style.css │ ├── tamperable.html │ ├── injectedfile.js │ ├── pptr.png │ ├── mobile.html │ ├── digits │ │ ├── 0.png │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ ├── one-style.html │ ├── input │ │ ├── fileupload.html │ │ ├── button.html │ │ ├── textarea.html │ │ ├── scrollable.html │ │ ├── touches.html │ │ ├── keyboard.html │ │ ├── mouse-helper.js │ │ └── select.html │ ├── error.html │ ├── playground.html │ ├── networkidle.html │ ├── grid.html │ └── detect-touch.html ├── golden │ ├── grid-cell-0.png │ ├── grid-cell-1.png │ ├── grid-cell-2.png │ ├── grid-cell-3.png │ ├── transparent.png │ ├── screenshot-sanity.png │ ├── mock-binary-response.png │ ├── screenshot-clip-rect.png │ ├── screenshot-clip-odd-size.png │ ├── screenshot-grid-fullpage.png │ ├── screenshot-element-rotate.png │ ├── screenshot-offscreen-clip.png │ ├── screenshot-element-bounding-box.png │ ├── screenshot-element-padding-border.png │ ├── nested-frames.txt │ └── reconnect-nested-frames.txt ├── diffstyle.css ├── server │ ├── cert.pem │ ├── key.pem │ └── SimpleServer.js ├── frame-utils.js └── golden-utils.js ├── utils ├── doclint │ ├── .gitignore │ ├── check_public_api │ │ ├── test │ │ │ ├── diff-classes │ │ │ │ ├── foo.js │ │ │ │ ├── other.js │ │ │ │ ├── doc.md │ │ │ │ └── result.txt │ │ │ ├── .gitignore │ │ │ ├── diff-properties │ │ │ │ ├── doc.md │ │ │ │ ├── result.txt │ │ │ │ └── foo.js │ │ │ ├── diff-events │ │ │ │ ├── doc.md │ │ │ │ ├── foo.js │ │ │ │ └── result.txt │ │ │ ├── check-duplicates │ │ │ │ ├── foo.js │ │ │ │ ├── result.txt │ │ │ │ └── doc.md │ │ │ ├── diff-methods │ │ │ │ ├── result.txt │ │ │ │ ├── doc.md │ │ │ │ └── foo.js │ │ │ ├── diff-arguments │ │ │ │ ├── foo.js │ │ │ │ ├── doc.md │ │ │ │ └── result.txt │ │ │ ├── check-returns │ │ │ │ ├── doc.md │ │ │ │ ├── foo.js │ │ │ │ └── result.txt │ │ │ ├── check-sorting │ │ │ │ ├── doc.md │ │ │ │ ├── foo.js │ │ │ │ └── result.txt │ │ │ ├── js-builder-common │ │ │ │ ├── foo.js │ │ │ │ └── result.txt │ │ │ ├── js-builder-inheritance │ │ │ │ ├── foo.js │ │ │ │ └── result.txt │ │ │ ├── md-builder-common │ │ │ │ ├── doc.md │ │ │ │ └── result.txt │ │ │ └── test.js │ │ ├── Documentation.js │ │ ├── MDBuilder.js │ │ └── JSBuilder.js │ ├── README.md │ ├── toc.js │ ├── Message.js │ ├── preprocessor │ │ ├── test.js │ │ └── index.js │ ├── cli.js │ └── SourceFactory.js ├── node6-transform │ ├── index.js │ ├── test │ │ └── test.js │ └── TransformAsyncFunctions.js ├── ESTreeWalker.js ├── check_availability.js └── fetch_devices.js ├── .eslintignore ├── .editorconfig ├── tsconfig.json ├── .gitignore ├── .npmignore ├── .appveyor.yml ├── lib ├── .eslintrc.js ├── Puppeteer.js ├── externs.d.ts ├── Dialog.js ├── Multimap.js ├── Tracing.js ├── EmulationManager.js ├── NavigatorWatcher.js ├── ElementHandle.js ├── Browser.js ├── Connection.js ├── ExecutionContext.js └── helper.js ├── examples ├── .eslintrc.js ├── README.md ├── screenshot.js ├── screenshot-fullpage.js ├── proxy.js ├── pdf.js ├── block-images.js ├── search.js ├── detect-sniff.js └── custom-event.js ├── .travis.yml ├── index.js ├── docs ├── issue_template.md └── troubleshooting.md ├── package.json ├── install.js ├── .eslintrc.js └── CONTRIBUTING.md /test/assets/sw.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/assets/empty.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/doclint/.gitignore: -------------------------------------------------------------------------------- 1 | output/ 2 | -------------------------------------------------------------------------------- /test/assets/simple.json: -------------------------------------------------------------------------------- 1 | {"foo": "bar"} 2 | -------------------------------------------------------------------------------- /test/assets/file-to-upload.txt: -------------------------------------------------------------------------------- 1 | contents of the file -------------------------------------------------------------------------------- /test/assets/frames/script.js: -------------------------------------------------------------------------------- 1 | console.log('Cheers!'); 2 | -------------------------------------------------------------------------------- /test/assets/frames/style.css: -------------------------------------------------------------------------------- 1 | div { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /test/assets/injectedstyle.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/assets/one-style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: pink; 3 | } 4 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/diff-classes/foo.js: -------------------------------------------------------------------------------- 1 | class Foo { 2 | } 3 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/diff-classes/other.js: -------------------------------------------------------------------------------- 1 | class Other { 2 | } 3 | -------------------------------------------------------------------------------- /test/assets/tamperable.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/.gitignore: -------------------------------------------------------------------------------- 1 | result-actual.txt 2 | result-diff.html 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | third_party/* 2 | utils/doclint/check_public_api/test/ 3 | node6/* 4 | node6-test/* -------------------------------------------------------------------------------- /test/assets/injectedfile.js: -------------------------------------------------------------------------------- 1 | window.__injected = 42; 2 | window.__injectedError = new Error('hi'); -------------------------------------------------------------------------------- /test/assets/pptr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/assets/pptr.png -------------------------------------------------------------------------------- /test/assets/mobile.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/assets/digits/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/assets/digits/0.png -------------------------------------------------------------------------------- /test/assets/digits/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/assets/digits/1.png -------------------------------------------------------------------------------- /test/assets/digits/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/assets/digits/2.png -------------------------------------------------------------------------------- /test/assets/digits/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/assets/digits/3.png -------------------------------------------------------------------------------- /test/assets/digits/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/assets/digits/4.png -------------------------------------------------------------------------------- /test/assets/digits/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/assets/digits/5.png -------------------------------------------------------------------------------- /test/assets/digits/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/assets/digits/6.png -------------------------------------------------------------------------------- /test/assets/digits/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/assets/digits/7.png -------------------------------------------------------------------------------- /test/assets/digits/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/assets/digits/8.png -------------------------------------------------------------------------------- /test/assets/digits/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/assets/digits/9.png -------------------------------------------------------------------------------- /test/assets/one-style.html: -------------------------------------------------------------------------------- 1 | 2 |
hello, world!
3 | -------------------------------------------------------------------------------- /test/golden/grid-cell-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/golden/grid-cell-0.png -------------------------------------------------------------------------------- /test/golden/grid-cell-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/golden/grid-cell-1.png -------------------------------------------------------------------------------- /test/golden/grid-cell-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/golden/grid-cell-2.png -------------------------------------------------------------------------------- /test/golden/grid-cell-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/golden/grid-cell-3.png -------------------------------------------------------------------------------- /test/golden/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/golden/transparent.png -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/diff-properties/doc.md: -------------------------------------------------------------------------------- 1 | ### class: Foo 2 | 3 | #### foo.a 4 | 5 | #### foo.c 6 | -------------------------------------------------------------------------------- /test/golden/screenshot-sanity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/golden/screenshot-sanity.png -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/diff-classes/doc.md: -------------------------------------------------------------------------------- 1 | ### class: Foo 2 | 3 | ### class: Bar 4 | 5 | ### class: Baz 6 | -------------------------------------------------------------------------------- /test/golden/mock-binary-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/golden/mock-binary-response.png -------------------------------------------------------------------------------- /test/golden/screenshot-clip-rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/golden/screenshot-clip-rect.png -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/diff-events/doc.md: -------------------------------------------------------------------------------- 1 | ### class: Foo 2 | 3 | #### event: 'start' 4 | 5 | #### event: 'stop' 6 | -------------------------------------------------------------------------------- /test/golden/screenshot-clip-odd-size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/golden/screenshot-clip-odd-size.png -------------------------------------------------------------------------------- /test/golden/screenshot-grid-fullpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/golden/screenshot-grid-fullpage.png -------------------------------------------------------------------------------- /test/golden/screenshot-element-rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/golden/screenshot-element-rotate.png -------------------------------------------------------------------------------- /test/golden/screenshot-offscreen-clip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/golden/screenshot-offscreen-clip.png -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/diff-properties/result.txt: -------------------------------------------------------------------------------- 1 | [MarkDown] Non-existing property found: Foo.c 2 | [MarkDown] Property not found: Foo.b -------------------------------------------------------------------------------- /test/golden/screenshot-element-bounding-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/golden/screenshot-element-bounding-box.png -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/diff-events/foo.js: -------------------------------------------------------------------------------- 1 | class Foo { 2 | } 3 | 4 | Foo.Events = { 5 | Start: 'start', 6 | Finish: 'finish', 7 | }; 8 | -------------------------------------------------------------------------------- /test/golden/screenshot-element-padding-border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timleland/puppeteer/master/test/golden/screenshot-element-padding-border.png -------------------------------------------------------------------------------- /test/assets/frames/frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Hi, I'm frame
4 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/diff-events/result.txt: -------------------------------------------------------------------------------- 1 | [MarkDown] Non-existing event found in class Foo: 'stop' 2 | [MarkDown] Event not found in class Foo: 'finish' -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/check-duplicates/foo.js: -------------------------------------------------------------------------------- 1 | class Foo { 2 | test() { 3 | } 4 | 5 | title(arg) { 6 | } 7 | } 8 | 9 | class Bar { 10 | } 11 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/diff-classes/result.txt: -------------------------------------------------------------------------------- 1 | [MarkDown] Non-existing class found: Bar 2 | [MarkDown] Non-existing class found: Baz 3 | [MarkDown] Class not found: Other -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/diff-properties/foo.js: -------------------------------------------------------------------------------- 1 | class Foo { 2 | constructor() { 3 | this.a = 42; 4 | this.b = 'hello'; 5 | this.emit('done'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/assets/input/fileupload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | File upload test 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/diff-methods/result.txt: -------------------------------------------------------------------------------- 1 | [MarkDown] Non-existing method found: Foo.proceed() 2 | [MarkDown] Method not found: Foo.stop() 3 | [MarkDown] Property not found: Foo.zzz -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/diff-methods/doc.md: -------------------------------------------------------------------------------- 1 | ### class: Foo 2 | 3 | #### foo.$() 4 | 5 | #### foo.money$$money() 6 | 7 | #### foo.proceed() 8 | 9 | #### foo.start() 10 | 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/diff-arguments/foo.js: -------------------------------------------------------------------------------- 1 | class Foo { 2 | foo(arg1, arg3 = {}) { 3 | } 4 | 5 | test(...filePaths) { 6 | } 7 | 8 | bar({visibility}) { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "allowJs": true, 5 | "checkJs": true, 6 | "target": "es2017" 7 | }, 8 | "include": [ 9 | "lib" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /test/output 3 | /test/test-user-data-dir* 4 | /.local-chromium/ 5 | /.dev_profile* 6 | .DS_Store 7 | *.swp 8 | *.pyc 9 | .vscode 10 | package-lock.json 11 | /node6 12 | /node6-test 13 | -------------------------------------------------------------------------------- /test/assets/error.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/check-duplicates/result.txt: -------------------------------------------------------------------------------- 1 | [MarkDown] Duplicate declaration of method Foo.test() 2 | [MarkDown] Duplicate declaration of argument Foo.title "arg" 3 | [MarkDown] Duplicate declaration of class Bar -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/diff-methods/foo.js: -------------------------------------------------------------------------------- 1 | class Foo { 2 | start() { 3 | } 4 | 5 | stop() { 6 | } 7 | 8 | get zzz() { 9 | } 10 | 11 | $() { 12 | } 13 | 14 | money$$money() { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/diffstyle.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: monospace; 3 | white-space: pre; 4 | } 5 | 6 | ins { 7 | background-color: #9cffa0; 8 | text-decoration: none; 9 | } 10 | 11 | del { 12 | background-color: #ff9e9e; 13 | } 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # exclude all tests 2 | test 3 | utils/node6-transform 4 | 5 | # repeats from .gitignore 6 | node_modules 7 | .local-chromium 8 | .dev_profile* 9 | .DS_Store 10 | *.swp 11 | *.pyc 12 | .vscode 13 | package-lock.json 14 | /node6-test 15 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/check-returns/doc.md: -------------------------------------------------------------------------------- 1 | ### class: Foo 2 | 3 | #### foo.asyncFunction() 4 | 5 | #### foo.return42() 6 | 7 | #### foo.returnNothing() 8 | - returns: <[number]> 9 | 10 | #### foo.www() 11 | - returns <[string]> 12 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/check-sorting/doc.md: -------------------------------------------------------------------------------- 1 | ### class: Foo 2 | 3 | #### event: 'c' 4 | 5 | #### event: 'a' 6 | 7 | #### foo.aaa() 8 | 9 | #### event: 'b' 10 | 11 | #### foo.ddd 12 | 13 | #### foo.ccc() 14 | 15 | #### foo.bbb() 16 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/check-duplicates/doc.md: -------------------------------------------------------------------------------- 1 | ### class: Bar 2 | 3 | ### class: Foo 4 | 5 | #### foo.test() 6 | 7 | #### foo.test() 8 | 9 | #### foo.title(arg, arg) 10 | - `arg` <[number]> 11 | - `arg` <[number]> 12 | 13 | ### class: Bar 14 | 15 | -------------------------------------------------------------------------------- /test/golden/nested-frames.txt: -------------------------------------------------------------------------------- 1 | http://localhost:8907/frames/nested-frames.html 2 | http://localhost:8907/frames/two-frames.html 3 | http://localhost:8907/frames/frame.html 4 | http://localhost:8907/frames/frame.html 5 | http://localhost:8907/frames/frame.html -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/check-sorting/foo.js: -------------------------------------------------------------------------------- 1 | class Foo { 2 | constructor() { 3 | this.ddd = 10; 4 | } 5 | 6 | aaa() {} 7 | 8 | bbb() {} 9 | 10 | ccc() {} 11 | } 12 | 13 | Foo.Events = { 14 | a: 'a', 15 | b: 'b', 16 | c: 'c' 17 | } 18 | -------------------------------------------------------------------------------- /test/golden/reconnect-nested-frames.txt: -------------------------------------------------------------------------------- 1 | http://localhost:8907/frames/nested-frames.html 2 | http://localhost:8907/frames/two-frames.html 3 | http://localhost:8907/frames/frame.html 4 | http://localhost:8907/frames/frame.html 5 | http://localhost:8907/frames/frame.html -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/diff-arguments/doc.md: -------------------------------------------------------------------------------- 1 | ### class: Foo 2 | #### foo.bar(options) 3 | - `options` <[Object]> 4 | 5 | #### foo.foo(arg1, arg2) 6 | - `arg1` <[string]> 7 | - `arg2` <[string]> 8 | 9 | #### foo.test(...files) 10 | - `...filePaths` <[string]> 11 | 12 | -------------------------------------------------------------------------------- /test/assets/frames/two-frames.html: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/diff-arguments/result.txt: -------------------------------------------------------------------------------- 1 | [MarkDown] Heading arguments for "foo.test(...files)" do not match described ones, i.e. "...files" != "...filePaths" 2 | [MarkDown] Method Foo.foo() fails to describe its parameters: 3 | - Argument not found: arg3 4 | - Non-existing argument found: arg2 -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/js-builder-common/foo.js: -------------------------------------------------------------------------------- 1 | class A { 2 | constructor(delegate) { 3 | this.property1 = 1; 4 | this._property2 = 2; 5 | } 6 | 7 | get getter() { 8 | return null; 9 | } 10 | 11 | async method(foo, bar) { 12 | } 13 | } 14 | 15 | A.Events = { 16 | AnEvent: 'anevent' 17 | }; 18 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/js-builder-inheritance/foo.js: -------------------------------------------------------------------------------- 1 | class A { 2 | constructor() { 3 | } 4 | 5 | foo(a) { 6 | } 7 | 8 | bar() { 9 | } 10 | } 11 | 12 | class B extends A { 13 | bar(override) { 14 | } 15 | } 16 | 17 | B.Events = { 18 | // Event with the same name as a super class method. 19 | foo: 'foo' 20 | }; 21 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/check-returns/foo.js: -------------------------------------------------------------------------------- 1 | class Foo { 2 | return42() { 3 | return 42; 4 | } 5 | 6 | returnNothing() { 7 | let e = () => { 8 | return 10; 9 | } 10 | e(); 11 | } 12 | 13 | www() { 14 | if (1 === 1) 15 | return 'df'; 16 | } 17 | 18 | async asyncFunction() { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/check-sorting/result.txt: -------------------------------------------------------------------------------- 1 | [MarkDown] Events should go first. Event 'b' in class Foo breaks order 2 | [MarkDown] Event 'c' in class Foo breaks alphabetic ordering of events 3 | [MarkDown] Bad alphabetic ordering of Foo members: Foo.ddd should go after Foo.ccc() 4 | [MarkDown] Bad alphabetic ordering of Foo members: Foo.ccc() should go after Foo.bbb() -------------------------------------------------------------------------------- /test/assets/playground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Playground 5 | 6 | 7 | 8 | 9 |
First div
10 |
11 | Second div 12 | Inner span 13 |
14 | 15 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/check-returns/result.txt: -------------------------------------------------------------------------------- 1 | [MarkDown] foo.www() has mistyped 'return' type declaration: expected exactly 'returns: ', found 'returns '. 2 | [MarkDown] Async method Foo.asyncFunction should describe return type Promise 3 | [MarkDown] Method Foo.return42 is missing return type description 4 | [MarkDown] Method Foo.returnNothing has unneeded description of return type -------------------------------------------------------------------------------- /test/assets/input/button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Button test 5 | 6 | 7 | 8 | 9 | 15 | 16 | -------------------------------------------------------------------------------- /test/assets/input/textarea.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Textarea test 5 | 6 | 7 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/md-builder-common/doc.md: -------------------------------------------------------------------------------- 1 | ### class: Foo 2 | 3 | This is a class. 4 | 5 | #### event: 'frame' 6 | - <[Frame]> 7 | 8 | This event is dispatched. 9 | 10 | #### foo.$(selector) 11 | - `selector` <[string]> A selector to query page for 12 | - returns: <[Promise]<[ElementHandle]>> 13 | 14 | The method runs document.querySelector. 15 | 16 | #### foo.url 17 | - <[string]> 18 | 19 | Contains the URL of the request. 20 | -------------------------------------------------------------------------------- /test/assets/networkidle.html: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /.appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "6" 4 | - nodejs_version: "7" 5 | 6 | build: off 7 | 8 | install: 9 | - ps: Install-Product node $env:nodejs_version 10 | - yarn install 11 | - if "%nodejs_version%" == "7" ( 12 | yarn run lint & 13 | yarn run coverage & 14 | yarn run test-doclint 15 | ) else ( 16 | yarn run test-node6-transformer & 17 | yarn run build & 18 | yarn run unit-node6 19 | ) 20 | -------------------------------------------------------------------------------- /lib/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "../.eslintrc.js", 3 | /** 4 | * ESLint rules 5 | * 6 | * All available rules: http://eslint.org/docs/rules/ 7 | * 8 | * Rules take the following form: 9 | * "rule-name", [severity, { opts }] 10 | * Severity: 2 == error, 1 == warning, 0 == off. 11 | */ 12 | "rules": { 13 | "no-console": [2, { "allow": ["warn", "error", "assert", "timeStamp", "time", "timeEnd"] }], 14 | "no-debugger": 0, 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /examples/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "../.eslintrc.js", 3 | /** 4 | * ESLint rules 5 | * 6 | * All available rules: http://eslint.org/docs/rules/ 7 | * 8 | * Rules take the following form: 9 | * "rule-name", [severity, { opts }] 10 | * Severity: 2 == error, 1 == warning, 0 == off. 11 | */ 12 | "rules": { 13 | "indent": [2, 2, { "SwitchCase": 1, "CallExpression": {"arguments": 2}, "MemberExpression": 2, "outerIIFEBody": 0 }], 14 | "strict": [2, "global"], 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /test/assets/frames/nested-frames.html: -------------------------------------------------------------------------------- 1 | 11 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/md-builder-common/result.txt: -------------------------------------------------------------------------------- 1 | { 2 | "classes": [ 3 | { 4 | "name": "Foo", 5 | "members": [ 6 | { 7 | "name": "frame", 8 | "type": "event", 9 | "hasReturn": false, 10 | "async": false, 11 | "args": [] 12 | }, 13 | { 14 | "name": "$", 15 | "type": "method", 16 | "hasReturn": true, 17 | "async": false, 18 | "args": [ 19 | "selector" 20 | ] 21 | }, 22 | { 23 | "name": "url", 24 | "type": "property", 25 | "hasReturn": false, 26 | "async": false, 27 | "args": [] 28 | } 29 | ] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /utils/doclint/README.md: -------------------------------------------------------------------------------- 1 | # DocLint 2 | 3 | **Doclint** is a small program that lints Puppeteer's documentation against 4 | Puppeteer's source code. 5 | 6 | Doclint works in a few steps: 7 | 8 | 1. Read sources in `lib/` folder, parse AST trees and extract public API 9 | 2. Read sources in `docs/` folder, render markdown to HTML, use puppeteer to traverse the HTML 10 | and extract described API 11 | 3. Compare one API to another 12 | 13 | Doclint is also responsible for general markdown checks, most notably for the table of contents 14 | relevancy. 15 | 16 | ## Running 17 | 18 | ```bash 19 | npm run doc 20 | ``` 21 | 22 | ## Tests 23 | 24 | Doclint has its own set of jasmine tests, located at `utils/doclint/test` folder. 25 | 26 | To execute tests, run: 27 | 28 | ```bash 29 | npm run test-doclint 30 | ``` 31 | -------------------------------------------------------------------------------- /test/assets/input/scrollable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scrollable test 5 | 6 | 7 | 8 | 22 | 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: trusty 3 | addons: 4 | apt: 5 | packages: 6 | # This is required to run new chrome on old trusty 7 | - libnss3 8 | cache: 9 | yarn: true 10 | directories: 11 | - node_modules 12 | install: 13 | - yarn install 14 | # puppeteer's install script downloads Chrome 15 | script: 16 | - 'if [ "$NODE7" = "true" ]; then yarn run lint; fi' 17 | - 'if [ "$NODE7" = "true" ]; then yarn run coverage; fi' 18 | - 'if [ "$NODE7" = "true" ]; then yarn run test-doclint; fi' 19 | - 'if [ "$NODE6" = "true" ]; then yarn run test-node6-transformer; fi' 20 | - 'if [ "$NODE6" = "true" ]; then yarn run build; fi' 21 | - 'if [ "$NODE6" = "true" ]; then yarn run unit-node6; fi' 22 | jobs: 23 | include: 24 | - node_js: "7.6.0" 25 | env: NODE7=true 26 | - node_js: "6.4.0" 27 | env: NODE6=true 28 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Other resources 2 | 3 | > Other useful tools, articles, and projects that use Puppeteer. 4 | 5 | ## Web scraping 6 | - [Puppetron](https://github.com/cheeaun/puppetron) - Demo site that shows how to use Puppeteer and Headless Chrome to render pages. Inspired by [GoogleChrome/rendertron](https://github.com/GoogleChrome/rendertron). 7 | - [Thal](https://medium.com/@e_mad_ehsan/getting-started-with-puppeteer-and-chrome-headless-for-web-scrapping-6bf5979dee3e "An article on medium") - Getting started with Puppeteer and Chrome Headless for Web Scraping. 8 | 9 | ## Testing 10 | - [angular-puppeteer-demo](https://github.com/Quramy/angular-puppeteer-demo) - Demo repository explaining how to use Puppeteer in Karma. 11 | - [mocha-headless-chrome](https://github.com/direct-adv-interfaces/mocha-headless-chrome) - Tool which runs client-side **mocha** tests in the command line through headless Chrome. 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | // If node does not support async await, use the compiled version. 18 | let folder = 'lib'; 19 | try { 20 | new Function('async function test(){await 1}'); 21 | } catch (error) { 22 | folder = 'node6'; 23 | } 24 | 25 | module.exports = require(`./${folder}/Puppeteer`); 26 | -------------------------------------------------------------------------------- /examples/screenshot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | 'use strict'; 18 | 19 | const puppeteer = require('puppeteer'); 20 | 21 | (async() => { 22 | 23 | const browser = await puppeteer.launch(); 24 | const page = await browser.newPage(); 25 | await page.goto('http://example.com'); 26 | await page.screenshot({path: 'example.png'}); 27 | 28 | await browser.close(); 29 | 30 | })(); 31 | -------------------------------------------------------------------------------- /examples/screenshot-fullpage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | 'use strict'; 18 | 19 | const puppeteer = require('puppeteer'); 20 | const devices = require('puppeteer/DeviceDescriptors'); 21 | 22 | (async() => { 23 | 24 | const browser = await puppeteer.launch(); 25 | const page = await browser.newPage(); 26 | await page.emulate(devices['iPhone 6']); 27 | await page.goto('https://www.nytimes.com/'); 28 | await page.screenshot({path: 'full.png', fullPage: true}); 29 | await browser.close(); 30 | 31 | })(); 32 | -------------------------------------------------------------------------------- /examples/proxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | 'use strict'; 18 | 19 | const puppeteer = require('puppeteer'); 20 | 21 | (async() => { 22 | 23 | const browser = await puppeteer.launch({ 24 | // Launch chromium using a proxy server on port 9876. 25 | // More on proxying: 26 | // https://www.chromium.org/developers/design-documents/network-settings 27 | args: [ '--proxy-server=127.0.0.1:9876' ] 28 | }); 29 | const page = await browser.newPage(); 30 | await page.goto('https://google.com'); 31 | await browser.close(); 32 | 33 | })(); 34 | -------------------------------------------------------------------------------- /docs/issue_template.md: -------------------------------------------------------------------------------- 1 | 18 | 19 | ### Steps to reproduce 20 | 21 | **Tell us about your environment:** 22 | 23 | * Puppeteer version: 24 | * Platform / OS version: 25 | * URLs (if applicable): 26 | 27 | **What steps will reproduce the problem?** 28 | 29 | _Please include code that reproduces the issue._ 30 | 31 | 1. 32 | 2. 33 | 3. 34 | 35 | **What is the expected result?** 36 | 37 | 38 | **What happens instead?** 39 | 40 | -------------------------------------------------------------------------------- /examples/pdf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | 'use strict'; 18 | 19 | const puppeteer = require('puppeteer'); 20 | 21 | (async() => { 22 | 23 | const browser = await puppeteer.launch(); 24 | const page = await browser.newPage(); 25 | await page.goto('https://news.ycombinator.com', {waitUntil: 'networkidle2'}); 26 | // page.pdf() is currently supported only in headless mode. 27 | // @see https://bugs.chromium.org/p/chromium/issues/detail?id=753118 28 | await page.pdf({ 29 | path: 'hn.pdf', 30 | format: 'letter' 31 | }); 32 | 33 | await browser.close(); 34 | 35 | })(); 36 | -------------------------------------------------------------------------------- /utils/doclint/toc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 markdownToc = require('markdown-toc'); 18 | const Message = require('./Message'); 19 | 20 | /** 21 | * @param {!Array} sources 22 | * @return {!Array} 23 | */ 24 | module.exports = function(sources) { 25 | const warnings = []; 26 | for (const source of sources) { 27 | const newText = markdownToc.insert(source.text()); 28 | if (source.setText(newText)) 29 | warnings.push('Regenerated table-of-contexts: ' + source.projectPath()); 30 | } 31 | return warnings.map(warning => Message.warning(warning)); 32 | }; 33 | -------------------------------------------------------------------------------- /test/assets/input/touches.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Touch test 5 | 6 | 7 | 8 | 9 | 34 | 35 | -------------------------------------------------------------------------------- /examples/block-images.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc., PhantomJS Authors All rights reserved. 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 | * http://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 | 'use strict'; 18 | 19 | const puppeteer = require('puppeteer'); 20 | 21 | (async() => { 22 | 23 | const browser = await puppeteer.launch(); 24 | const page = await browser.newPage(); 25 | await page.setRequestInterception(true); 26 | page.on('request', request => { 27 | if (request.resourceType === 'image') 28 | request.abort(); 29 | else 30 | request.continue(); 31 | }); 32 | await page.goto('https://news.google.com/news/'); 33 | await page.screenshot({path: 'news.png', fullPage: true}); 34 | 35 | await browser.close(); 36 | 37 | })(); 38 | 39 | -------------------------------------------------------------------------------- /utils/doclint/Message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | class Message { 18 | /** 19 | * @param {string} type 20 | * @param {string} text 21 | */ 22 | constructor(type, text) { 23 | this.type = type; 24 | this.text = text; 25 | } 26 | 27 | /** 28 | * @param {string} text 29 | * @return {!Message} 30 | */ 31 | static error(text) { 32 | return new Message('error', text); 33 | } 34 | 35 | /** 36 | * @param {string} text 37 | * @return {!Message} 38 | */ 39 | static warning(text) { 40 | return new Message('warning', text); 41 | } 42 | } 43 | 44 | module.exports = Message; 45 | -------------------------------------------------------------------------------- /test/assets/grid.html: -------------------------------------------------------------------------------- 1 | 28 | 29 | 49 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/js-builder-common/result.txt: -------------------------------------------------------------------------------- 1 | { 2 | "classes": [ 3 | { 4 | "name": "A", 5 | "members": [ 6 | { 7 | "name": "property1", 8 | "type": "property", 9 | "hasReturn": false, 10 | "async": false, 11 | "args": [] 12 | }, 13 | { 14 | "name": "_property2", 15 | "type": "property", 16 | "hasReturn": false, 17 | "async": false, 18 | "args": [] 19 | }, 20 | { 21 | "name": "constructor", 22 | "type": "method", 23 | "hasReturn": false, 24 | "async": false, 25 | "args": [ 26 | "delegate" 27 | ] 28 | }, 29 | { 30 | "name": "getter", 31 | "type": "property", 32 | "hasReturn": false, 33 | "async": false, 34 | "args": [] 35 | }, 36 | { 37 | "name": "method", 38 | "type": "method", 39 | "hasReturn": true, 40 | "async": true, 41 | "args": [ 42 | "foo", 43 | "bar" 44 | ] 45 | }, 46 | { 47 | "name": "anevent", 48 | "type": "event", 49 | "hasReturn": false, 50 | "async": false, 51 | "args": [] 52 | } 53 | ] 54 | } 55 | ] 56 | } -------------------------------------------------------------------------------- /lib/Puppeteer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | const {helper} = require('./helper'); 17 | const Launcher = require('./Launcher'); 18 | 19 | class Puppeteer { 20 | /** 21 | * @param {!Object=} options 22 | * @return {!Promise} 23 | */ 24 | static launch(options) { 25 | return Launcher.launch(options); 26 | } 27 | 28 | /** 29 | * @param {{browserWSEndpoint: string, ignoreHTTPSErrors: boolean}} options 30 | * @return {!Promise} 31 | */ 32 | static connect(options) { 33 | return Launcher.connect(options); 34 | } 35 | 36 | /** 37 | * @return {string} 38 | */ 39 | static executablePath() { 40 | return Launcher.executablePath(); 41 | } 42 | } 43 | 44 | module.exports = Puppeteer; 45 | helper.tracePublicAPI(Puppeteer); 46 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/js-builder-inheritance/result.txt: -------------------------------------------------------------------------------- 1 | { 2 | "classes": [ 3 | { 4 | "name": "A", 5 | "members": [ 6 | { 7 | "name": "constructor", 8 | "type": "method", 9 | "hasReturn": false, 10 | "async": false, 11 | "args": [] 12 | }, 13 | { 14 | "name": "foo", 15 | "type": "method", 16 | "hasReturn": false, 17 | "async": false, 18 | "args": [ 19 | "a" 20 | ] 21 | }, 22 | { 23 | "name": "bar", 24 | "type": "method", 25 | "hasReturn": false, 26 | "async": false, 27 | "args": [] 28 | } 29 | ] 30 | }, 31 | { 32 | "name": "B", 33 | "members": [ 34 | { 35 | "name": "bar", 36 | "type": "method", 37 | "hasReturn": false, 38 | "async": false, 39 | "args": [ 40 | "override" 41 | ] 42 | }, 43 | { 44 | "name": "foo", 45 | "type": "event", 46 | "hasReturn": false, 47 | "async": false, 48 | "args": [] 49 | }, 50 | { 51 | "name": "foo", 52 | "type": "method", 53 | "hasReturn": false, 54 | "async": false, 55 | "args": [ 56 | "a" 57 | ] 58 | } 59 | ] 60 | } 61 | ] 62 | } -------------------------------------------------------------------------------- /test/assets/input/keyboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Keyboard test 5 | 6 | 7 | 8 | 43 | 44 | -------------------------------------------------------------------------------- /examples/search.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | 'use strict'; 18 | 19 | const puppeteer = require('puppeteer'); 20 | 21 | (async() => { 22 | 23 | const browser = await puppeteer.launch(); 24 | const page = await browser.newPage(); 25 | await page.goto('https://google.com', {waitUntil: 'networkidle2'}); 26 | 27 | await page.waitFor('input[name=q]'); 28 | // Type our query into the search bar 29 | await page.type('input[name=q]', 'puppeteer'); 30 | 31 | await page.click('input[type="submit"]'); 32 | 33 | // Wait for the results to show up 34 | await page.waitForSelector('h3 a'); 35 | 36 | // Extract the results from the page 37 | const links = await page.evaluate(() => { 38 | const anchors = Array.from(document.querySelectorAll('h3 a')); 39 | return anchors.map(anchor => anchor.textContent); 40 | }); 41 | console.log(links.join('\n')); 42 | await browser.close(); 43 | 44 | })(); 45 | -------------------------------------------------------------------------------- /lib/externs.d.ts: -------------------------------------------------------------------------------- 1 | import { Connection as RealConnection, Session as RealSession } from './Connection.js'; 2 | import {Browser as RealBrowser, TaskQueue as RealTaskQueue} from './Browser.js'; 3 | import * as RealPage from './Page.js'; 4 | import {Mouse as RealMouse, Keyboard as RealKeyboard, Touchscreen as RealTouchscreen} from './Input.js'; 5 | import {Frame as RealFrame, FrameManager as RealFrameManager} from './FrameManager.js'; 6 | import {JSHandle as RealJSHandle, ExecutionContext as RealExecutionContext} from './ExecutionContext.js'; 7 | import * as RealElementHandle from './ElementHandle.js'; 8 | import * as RealNetworkManager from './NetworkManager.js'; 9 | import * as child_process from 'child_process'; 10 | export as namespace Puppeteer; 11 | 12 | export class Connection extends RealConnection {} 13 | export class Session extends RealSession {} 14 | export class Mouse extends RealMouse {} 15 | export class Keyboard extends RealKeyboard {} 16 | export class Touchscreen extends RealTouchscreen {} 17 | export class TaskQueue extends RealTaskQueue {} 18 | export class Browser extends RealBrowser {} 19 | export class Frame extends RealFrame {} 20 | export class FrameManager extends RealFrameManager {} 21 | export class NetworkManager extends RealNetworkManager {} 22 | export class ElementHandle extends RealElementHandle {} 23 | export class JSHandle extends RealJSHandle {} 24 | export class ExecutionContext extends RealExecutionContext {} 25 | export class Page extends RealPage {} 26 | 27 | 28 | export interface ChildProcess extends child_process.ChildProcess {} 29 | -------------------------------------------------------------------------------- /examples/detect-sniff.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc., PhantomJS Authors All rights reserved. 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 | * http://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 | 'use strict'; 18 | 19 | const puppeteer = require('puppeteer'); 20 | 21 | function sniffDetector() { 22 | const userAgent = window.navigator.userAgent; 23 | const platform = window.navigator.platform; 24 | 25 | window.navigator.__defineGetter__('userAgent', function() { 26 | window.navigator.sniffed = true; 27 | return userAgent; 28 | }); 29 | 30 | window.navigator.__defineGetter__('platform', function() { 31 | window.navigator.sniffed = true; 32 | return platform; 33 | }); 34 | } 35 | 36 | (async() => { 37 | 38 | const browser = await puppeteer.launch(); 39 | const page = await browser.newPage(); 40 | await page.evaluateOnNewDocument(sniffDetector); 41 | await page.goto('https://www.google.com', {waitUntil: 'networkidle2'}); 42 | console.log('Sniffed: ' + (await page.evaluate(() => !!navigator.sniffed))); 43 | 44 | await browser.close(); 45 | 46 | })(); 47 | -------------------------------------------------------------------------------- /examples/custom-event.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | 'use strict'; 18 | 19 | const puppeteer = require('puppeteer'); 20 | 21 | (async() => { 22 | 23 | const browser = await puppeteer.launch(); 24 | const page = await browser.newPage(); 25 | 26 | // Define a window.onCustomEvent function on the page. 27 | await page.exposeFunction('onCustomEvent', e => { 28 | console.log(`${e.type} fired`, e.detail || ''); 29 | }); 30 | 31 | /** 32 | * Attach an event listener to page to capture a custom event on page load/navigation. 33 | * @param {string} type Event name. 34 | * @return {!Promise} 35 | */ 36 | function listenFor(type) { 37 | return page.evaluateOnNewDocument(type => { 38 | document.addEventListener(type, e => { 39 | window.onCustomEvent({type, detail: e.detail}); 40 | }); 41 | }, type); 42 | } 43 | 44 | await listenFor('app-ready'); // Listen for "app-ready" custom event on page load. 45 | 46 | await page.goto('https://www.chromestatus.com/features', {waitUntil: 'networkidle2'}); 47 | 48 | await browser.close(); 49 | 50 | })(); 51 | -------------------------------------------------------------------------------- /utils/node6-transform/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 fs = require('fs'); 18 | const path = require('path'); 19 | const removeRecursive = require('rimraf').sync; 20 | const transformAsyncFunctions = require('./TransformAsyncFunctions'); 21 | 22 | copyFolder(path.join(__dirname, '..', '..', 'lib'), path.join(__dirname, '..', '..', 'node6')); 23 | copyFolder(path.join(__dirname, '..', '..', 'test'), path.join(__dirname, '..', '..', 'node6-test')); 24 | 25 | 26 | function copyFolder(source, target) { 27 | if (fs.existsSync(target)) 28 | removeRecursive(target); 29 | fs.mkdirSync(target); 30 | 31 | fs.readdirSync(source).forEach(file => { 32 | const from = path.join(source, file); 33 | const to = path.join(target, file); 34 | if (fs.lstatSync(from).isDirectory()) { 35 | copyFolder(from, to); 36 | } else { 37 | let text = fs.readFileSync(from); 38 | if (file.endsWith('.js')) 39 | text = transformAsyncFunctions(text.toString()).replace(/require\('\.\.\/lib\//g, `require('../node6/`); 40 | fs.writeFileSync(to, text); 41 | } 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/assets/input/mouse-helper.js: -------------------------------------------------------------------------------- 1 | // This injects a box into the page that moves with the mouse; 2 | // Useful for debugging 3 | (function(){ 4 | const box = document.createElement('div'); 5 | box.classList.add('mouse-helper'); 6 | const styleElement = document.createElement('style'); 7 | styleElement.innerHTML = ` 8 | .mouse-helper { 9 | pointer-events: none; 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | width: 20px; 14 | height: 20px; 15 | background: rgba(0,0,0,.4); 16 | border: 1px solid white; 17 | border-radius: 10px; 18 | margin-left: -10px; 19 | margin-top: -10px; 20 | transition: background .2s, border-radius .2s, border-color .2s; 21 | } 22 | .mouse-helper.button-1 { 23 | transition: none; 24 | background: rgba(0,0,0,0.9); 25 | } 26 | .mouse-helper.button-2 { 27 | transition: none; 28 | border-color: rgba(0,0,255,0.9); 29 | } 30 | .mouse-helper.button-3 { 31 | transition: none; 32 | border-radius: 4px; 33 | } 34 | .mouse-helper.button-4 { 35 | transition: none; 36 | border-color: rgba(255,0,0,0.9); 37 | } 38 | .mouse-helper.button-5 { 39 | transition: none; 40 | border-color: rgba(0,255,0,0.9); 41 | } 42 | `; 43 | document.head.appendChild(styleElement); 44 | document.body.appendChild(box); 45 | document.addEventListener('mousemove', event => { 46 | box.style.left = event.pageX + 'px'; 47 | box.style.top = event.pageY + 'px'; 48 | updateButtons(event.buttons); 49 | }, true); 50 | document.addEventListener('mousedown', event => { 51 | updateButtons(event.buttons); 52 | box.classList.add('button-' + event.which); 53 | }, true); 54 | document.addEventListener('mouseup', event => { 55 | updateButtons(event.buttons); 56 | box.classList.remove('button-' + event.which); 57 | }, true); 58 | function updateButtons(buttons) { 59 | for (let i = 0; i < 5; i++) 60 | box.classList.toggle('button-' + i, buttons & (1 << i)); 61 | } 62 | })(); 63 | -------------------------------------------------------------------------------- /test/server/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFXzCCA0egAwIBAgIJAM+8uXXn61zZMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQwIBcNMTcwNzEwMjI1MDA2WhgPMzAxNjExMTAyMjUwMDZa 5 | MEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJ 6 | bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw 7 | ggIKAoICAQCfsIbw1Q91wooUdSu5tDiyrSYB6ZQmY49Y141KQ0ll0ZXzf1sPTpPg 8 | OuBjJE8Fre2Wn3jJ0SfLFyQBMvE49hqWyY/U5Xc367ujqKFQVoItnoV5MM2TPu5J 9 | /zhtf26Vq0Pcrujt5LfRe1JSYKdJ21Tquqa0MAGI0HghaCdSdtyo2xuotnukirKb 10 | QrvP/YNa+ONZT6KW8MFAwfoCOJMo1idrkBA1Wve5xcCd7J9Oy5mWCBxTSR67W2vQ 11 | izoOTSkzD0xoXpFF5V/34zJGWU9t6Z5qytV/w5ROY3Tk9EaKs0NcQmXlCxSmkXil 12 | KSTlZ/VDeDliI92jGn4hT+apisglm3aaTnVVAP0EbZ/CF9Fwb601M7IcAP9ejaeB 13 | EEs+smXpuzhAfxPa5SpZCWeaXLcFq6Ewi2LXrMaChWvbu9AUi3QjuT3u9PW3B0w5 14 | C54FLfvcy9X9dQQ/jCgydF3eyhiO3SuLZqrhofHUn53z4UCEYgbC7uQSv08ep2UD 15 | kT2ARN6aetXVgiQBYS8hcGaFrdsUTSAmT0flt0g8ZoHn+NmuAWxbAx8UnPd0p/EP 16 | B4cZByDOUDGgDMSOEluheiCFlZBQEJnvOhim6rwSje87EzQazGkwRpOrBtzGIGcM 17 | xmotG9IrMbzKb4Z+yg5pOEW2WKEy3f2h8P2bisbbHwYa/tgTHVWbGwIDAQABo1Aw 18 | TjAdBgNVHQ4EFgQUZvIOJVkyQTAd0fpUkpqgcXVID+4wHwYDVR0jBBgwFoAUZvIO 19 | JVkyQTAd0fpUkpqgcXVID+4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC 20 | AgEASmqzfr4TOVFbNo3r85QlCQB8W4xaArRpY6k7ugxJm2/9iyyovMKMhCcRvmPV 21 | HZXDTV8WEbLDg3uNF56WU39YmtfjghnsYcGQ/UhnH2PoHQxGIKzOoalLew7kP0Uc 22 | HCmXi7OPjqeFQgjWVhLkL/RIzZ3vOVFQfqodTVIdYDe0O1cNRD0U0rtGa0MHzZlD 23 | y/ZaksiB0E3UL/GsgyJoCxCAF1E80PU9aw1K9COyTOChpUKnhSHC964KlhnJKyOe 24 | HKCYtb5RVrF7ryBcGneLTcQm/oNUp8aRyiwyQIDH7djFSp11MakXBF+EeGR523pp 25 | u+sleR7ZFBGyb3Z5K9NdRdUk0SWu7Hu4jQtJHn+PmIZ1qjfbPv5qDfVd1vmFwqsu 26 | 7NfsLoNm0dQNu5WOMLISQHmQiT86AH2wWQ3l5Sm+g8/BdNLQLklhtZcRhp2efyiL 27 | ciUmGugKqoX+nPIZ36kuoRTZy++AnTiid011vZFe1qrfug/ykWiqWmBSvD/cfRU4 28 | ydoK87cfjIixqmpRZ7j2q+/cDK2SbYN0t/Xrufw3L6TjDgUEL7ZCImcwqqWJz9I8 29 | ASnnL5PhX8bbsUrtE21Ugqk2zYnVnqRO5FjINtlTb07y9pGC/QpBkb1AasF5okhe 30 | 7lw/sMiryKKzS4A10nRV/+gErDBsIBj+cpGPM8brLfZuy2o= 31 | -----END CERTIFICATE----- 32 | -------------------------------------------------------------------------------- /lib/Dialog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 {helper} = require('./helper'); 18 | 19 | class Dialog { 20 | /** 21 | * @param {!Puppeteer.Session} client 22 | * @param {string} type 23 | * @param {string} message 24 | * @param {(string|undefined)} defaultValue 25 | */ 26 | constructor(client, type, message, defaultValue = '') { 27 | this._client = client; 28 | this.type = type; 29 | this._message = message; 30 | this._handled = false; 31 | this._defaultValue = defaultValue; 32 | } 33 | 34 | /** 35 | * @return {string} 36 | */ 37 | message() { 38 | return this._message; 39 | } 40 | 41 | /** 42 | * @return {string} 43 | */ 44 | defaultValue() { 45 | return this._defaultValue; 46 | } 47 | 48 | /** 49 | * @param {string=} promptText 50 | */ 51 | async accept(promptText) { 52 | console.assert(!this._handled, 'Cannot accept dialog which is already handled!'); 53 | this._handled = true; 54 | await this._client.send('Page.handleJavaScriptDialog', { 55 | accept: true, 56 | promptText: promptText 57 | }); 58 | } 59 | 60 | async dismiss() { 61 | console.assert(!this._handled, 'Cannot dismiss dialog which is already handled!'); 62 | this._handled = true; 63 | await this._client.send('Page.handleJavaScriptDialog', { 64 | accept: false 65 | }); 66 | } 67 | } 68 | 69 | Dialog.Type = { 70 | Alert: 'alert', 71 | BeforeUnload: 'beforeunload', 72 | Confirm: 'confirm', 73 | Prompt: 'prompt' 74 | }; 75 | 76 | module.exports = Dialog; 77 | helper.tracePublicAPI(Dialog); 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppeteer", 3 | "version": "0.13.0-alpha", 4 | "description": "A high-level API to control headless Chrome over the DevTools Protocol", 5 | "main": "index.js", 6 | "repository": "github:GoogleChrome/puppeteer", 7 | "engines": { 8 | "node": ">=6.4.0" 9 | }, 10 | "scripts": { 11 | "unit": "jasmine test/test.js", 12 | "debug-unit": "cross-env DEBUG_TEST=true node --inspect-brk ./node_modules/jasmine/bin/jasmine.js test/test.js", 13 | "test-doclint": "jasmine utils/doclint/check_public_api/test/test.js && jasmine utils/doclint/preprocessor/test.js", 14 | "test": "npm run lint --silent && npm run coverage && npm run test-doclint && npm run test-node6-transformer", 15 | "install": "node install.js", 16 | "lint": "([ \"$CI\" = true ] && eslint --quiet -f codeframe . || eslint .) && npm run tsc && npm run doc", 17 | "doc": "node utils/doclint/cli.js", 18 | "coverage": "cross-env COVERAGE=true npm run unit", 19 | "test-node6-transformer": "jasmine utils/node6-transform/test/test.js", 20 | "build": "node utils/node6-transform/index.js", 21 | "unit-node6": "jasmine node6-test/test.js", 22 | "tsc": "tsc -p ." 23 | }, 24 | "author": "The Chromium Authors", 25 | "license": "Apache-2.0", 26 | "dependencies": { 27 | "debug": "^2.6.8", 28 | "extract-zip": "^1.6.5", 29 | "https-proxy-agent": "^2.1.0", 30 | "mime": "^1.3.4", 31 | "progress": "^2.0.0", 32 | "proxy-from-env": "^1.0.0", 33 | "rimraf": "^2.6.1", 34 | "ws": "^3.0.0" 35 | }, 36 | "puppeteer": { 37 | "chromium_revision": "511134" 38 | }, 39 | "devDependencies": { 40 | "@types/debug": "0.0.30", 41 | "@types/extract-zip": "^1.6.2", 42 | "@types/mime": "^1.3.1", 43 | "@types/node": "^8.0.26", 44 | "@types/rimraf": "^2.0.2", 45 | "@types/ws": "^3.0.2", 46 | "commonmark": "^0.27.0", 47 | "cross-env": "^5.0.5", 48 | "eslint": "^4.0.0", 49 | "esprima": "^4.0.0", 50 | "jasmine": "^2.6.0", 51 | "markdown-toc": "^1.1.0", 52 | "minimist": "^1.2.0", 53 | "ncp": "^2.0.0", 54 | "pdfjs-dist": "^1.8.595", 55 | "pixelmatch": "^4.0.2", 56 | "pngjs": "^3.2.0", 57 | "text-diff": "^1.0.1", 58 | "typescript": "^2.6.0-rc" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/Multimap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | class Multimap { 18 | constructor() { 19 | this._map = new Map(); 20 | } 21 | 22 | set(key, value) { 23 | let set = this._map.get(key); 24 | if (!set) { 25 | set = new Set(); 26 | this._map.set(key, set); 27 | } 28 | set.add(value); 29 | } 30 | 31 | get(key) { 32 | let result = this._map.get(key); 33 | if (!result) 34 | result = new Set(); 35 | return result; 36 | } 37 | 38 | has(key) { 39 | return this._map.has(key); 40 | } 41 | 42 | hasValue(key, value) { 43 | const set = this._map.get(key); 44 | if (!set) 45 | return false; 46 | return set.has(value); 47 | } 48 | 49 | /** 50 | * @return {number} 51 | */ 52 | get size() { 53 | return this._map.size; 54 | } 55 | 56 | delete(key, value) { 57 | const values = this.get(key); 58 | const result = values.delete(value); 59 | if (!values.size) 60 | this._map.delete(key); 61 | return result; 62 | } 63 | 64 | deleteAll(key) { 65 | this._map.delete(key); 66 | } 67 | 68 | firstValue(key) { 69 | const set = this._map.get(key); 70 | if (!set) 71 | return null; 72 | return set.values().next().value; 73 | } 74 | 75 | firstKey() { 76 | return this._map.keys().next().value; 77 | } 78 | 79 | valuesArray() { 80 | const result = []; 81 | for (const key of this._map.keys()) 82 | result.push(...Array.from(this._map.get(key).values())); 83 | return result; 84 | } 85 | 86 | keysArray() { 87 | return Array.from(this._map.keys()); 88 | } 89 | 90 | clear() { 91 | this._map.clear(); 92 | } 93 | } 94 | 95 | module.exports = Multimap; 96 | -------------------------------------------------------------------------------- /test/assets/input/select.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Selection Test 5 | 6 | 7 | 24 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /test/frame-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 utils = module.exports = { 18 | /** 19 | * @param {!Page} page 20 | * @param {string} frameId 21 | * @param {string} url 22 | */ 23 | attachFrame: async function(page, frameId, url) { 24 | await page.evaluate(attachFrame, frameId, url); 25 | 26 | function attachFrame(frameId, url) { 27 | const frame = document.createElement('iframe'); 28 | frame.src = url; 29 | frame.id = frameId; 30 | document.body.appendChild(frame); 31 | return new Promise(x => frame.onload = x); 32 | } 33 | }, 34 | 35 | /** 36 | * @param {!Page} page 37 | * @param {string} frameId 38 | */ 39 | detachFrame: async function(page, frameId) { 40 | await page.evaluate(detachFrame, frameId); 41 | 42 | function detachFrame(frameId) { 43 | const frame = document.getElementById(frameId); 44 | frame.remove(); 45 | } 46 | }, 47 | 48 | /** 49 | * @param {!Page} page 50 | * @param {string} frameId 51 | * @param {string} url 52 | */ 53 | navigateFrame: async function(page, frameId, url) { 54 | await page.evaluate(navigateFrame, frameId, url); 55 | 56 | function navigateFrame(frameId, url) { 57 | const frame = document.getElementById(frameId); 58 | frame.src = url; 59 | return new Promise(x => frame.onload = x); 60 | } 61 | }, 62 | 63 | /** 64 | * @param {!Frame} frame 65 | * @param {string=} indentation 66 | * @return {string} 67 | */ 68 | dumpFrames: function(frame, indentation) { 69 | indentation = indentation || ''; 70 | let result = indentation + frame.url(); 71 | for (const child of frame.childFrames()) 72 | result += '\n' + utils.dumpFrames(child, ' ' + indentation); 73 | return result; 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /utils/doclint/preprocessor/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 preprocessor = require('.'); 18 | const SourceFactory = require('../SourceFactory'); 19 | const factory = new SourceFactory(); 20 | const VERSION = require('../../../package.json').version; 21 | 22 | describe('preprocessor', function() { 23 | it('should throw for unknown command', function() { 24 | const source = factory.createForTest('doc.md', getCommand('unknownCommand()')); 25 | const messages = preprocessor([source]); 26 | expect(source.hasUpdatedText()).toBe(false); 27 | expect(messages.length).toBe(1); 28 | expect(messages[0].type).toBe('error'); 29 | expect(messages[0].text).toContain('Unknown command'); 30 | }); 31 | describe('gen:version', function() { 32 | it('should work', function() { 33 | const source = factory.createForTest('doc.md', `Puppeteer v${getCommand('version')}`); 34 | const messages = preprocessor([source]); 35 | expect(messages.length).toBe(1); 36 | expect(messages[0].type).toBe('warning'); 37 | expect(messages[0].text).toContain('doc.md'); 38 | expect(source.text()).toBe(`Puppeteer v${getCommand('version', VERSION)}`); 39 | }); 40 | it('should tolerate different writing', function() { 41 | const source = factory.createForTest('doc.md', `Puppeteer vWHAT 42 | `); 43 | preprocessor([source]); 44 | expect(source.text()).toBe(`Puppeteer v${VERSION}`); 45 | }); 46 | it('should not tolerate missing gen:stop', function() { 47 | const source = factory.createForTest('doc.md', ``); 48 | const messages = preprocessor([source]); 49 | expect(source.hasUpdatedText()).toBe(false); 50 | expect(messages.length).toBe(1); 51 | expect(messages[0].type).toBe('error'); 52 | expect(messages[0].text).toContain(`Failed to find 'gen:stop'`); 53 | }); 54 | }); 55 | }); 56 | 57 | function getCommand(name, body = '') { 58 | return `${body}`; 59 | } 60 | -------------------------------------------------------------------------------- /test/assets/detect-touch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Detect Touch Test 5 | 10 | 11 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /lib/Tracing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | const {helper} = require('./helper'); 17 | const fs = require('fs'); 18 | 19 | const openAsync = helper.promisify(fs.open); 20 | const writeAsync = helper.promisify(fs.write); 21 | const closeAsync = helper.promisify(fs.close); 22 | 23 | class Tracing { 24 | /** 25 | * @param {!Puppeteer.Session} client 26 | */ 27 | constructor(client) { 28 | this._client = client; 29 | this._recording = false; 30 | this._path = ''; 31 | } 32 | 33 | /** 34 | * @param {!Object} options 35 | */ 36 | async start(options) { 37 | console.assert(!this._recording, 'Cannot start recording trace while already recording trace.'); 38 | console.assert(options.path, 'Must specify a path to write trace file to.'); 39 | 40 | const categoriesArray = [ 41 | '-*', 'devtools.timeline', 'v8.execute', 'disabled-by-default-devtools.timeline', 42 | 'disabled-by-default-devtools.timeline.frame', 'toplevel', 43 | 'blink.console', 'blink.user_timing', 'latencyInfo', 'disabled-by-default-devtools.timeline.stack', 44 | 'disabled-by-default-v8.cpu_profiler' 45 | ]; 46 | 47 | if (options.screenshots) 48 | categoriesArray.push('disabled-by-default-devtools.screenshot'); 49 | 50 | this._path = options.path; 51 | this._recording = true; 52 | await this._client.send('Tracing.start', { 53 | transferMode: 'ReturnAsStream', 54 | categories: categoriesArray.join(',') 55 | }); 56 | } 57 | 58 | async stop() { 59 | let fulfill; 60 | const contentPromise = new Promise(x => fulfill = x); 61 | this._client.once('Tracing.tracingComplete', event => { 62 | this._readStream(event.stream, this._path).then(fulfill); 63 | }); 64 | await this._client.send('Tracing.end'); 65 | this._recording = false; 66 | return contentPromise; 67 | } 68 | 69 | /** 70 | * @param {string} handle 71 | * @param {string} path 72 | */ 73 | async _readStream(handle, path) { 74 | let eof = false; 75 | const file = await openAsync(path, 'w'); 76 | while (!eof) { 77 | const response = await this._client.send('IO.read', {handle}); 78 | eof = response.eof; 79 | if (path) 80 | await writeAsync(file, response.data); 81 | } 82 | await closeAsync(file); 83 | await this._client.send('IO.close', {handle}); 84 | } 85 | } 86 | helper.tracePublicAPI(Tracing); 87 | 88 | module.exports = Tracing; 89 | -------------------------------------------------------------------------------- /utils/doclint/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright 2017 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | const puppeteer = require('../..'); 19 | const path = require('path'); 20 | const SourceFactory = require('./SourceFactory'); 21 | 22 | const PROJECT_DIR = path.join(__dirname, '..', '..'); 23 | 24 | const RED_COLOR = '\x1b[31m'; 25 | const YELLOW_COLOR = '\x1b[33m'; 26 | const RESET_COLOR = '\x1b[0m'; 27 | 28 | run(); 29 | 30 | async function run() { 31 | const startTime = Date.now(); 32 | 33 | const sourceFactory = new SourceFactory(); 34 | /** @type {!Array} */ 35 | const messages = []; 36 | 37 | // Documentation checks. 38 | { 39 | const mdSources = await sourceFactory.readdir(path.join(PROJECT_DIR, 'docs'), '.md'); 40 | 41 | const toc = require('./toc'); 42 | messages.push(...await toc(mdSources)); 43 | 44 | const preprocessor = require('./preprocessor'); 45 | messages.push(...await preprocessor(mdSources)); 46 | 47 | const browser = await puppeteer.launch({args: ['--no-sandbox']}); 48 | const page = await browser.newPage(); 49 | const checkPublicAPI = require('./check_public_api'); 50 | const jsSources = await sourceFactory.readdir(path.join(PROJECT_DIR, 'lib'), '.js'); 51 | messages.push(...await checkPublicAPI(page, mdSources, jsSources)); 52 | await browser.close(); 53 | } 54 | 55 | // Report results. 56 | const errors = messages.filter(message => message.type === 'error'); 57 | if (errors.length) { 58 | console.log('DocLint Failures:'); 59 | for (let i = 0; i < errors.length; ++i) { 60 | let error = errors[i].text; 61 | error = error.split('\n').join('\n '); 62 | console.log(` ${i + 1}) ${RED_COLOR}${error}${RESET_COLOR}`); 63 | } 64 | } 65 | const warnings = messages.filter(message => message.type === 'warning'); 66 | if (warnings.length) { 67 | console.log('DocLint Warnings:'); 68 | for (let i = 0; i < warnings.length; ++i) { 69 | let warning = warnings[i].text; 70 | warning = warning.split('\n').join('\n '); 71 | console.log(` ${i + 1}) ${YELLOW_COLOR}${warning}${RESET_COLOR}`); 72 | } 73 | } 74 | let clearExit = messages.length === 0; 75 | if (await sourceFactory.saveChangedSources()) { 76 | if (clearExit) 77 | console.log(`${YELLOW_COLOR}Some files were updated.${RESET_COLOR}`); 78 | clearExit = false; 79 | } 80 | console.log(`${errors.length} failures, ${warnings.length} warnings.`); 81 | const runningTime = Date.now() - startTime; 82 | console.log(`DocLint Finished in ${runningTime / 1000} seconds`); 83 | process.exit(clearExit ? 0 : 1); 84 | } 85 | -------------------------------------------------------------------------------- /install.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | if (process.env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD) { 18 | console.log('**INFO** Skipping Chromium download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" environment variable was found.'); 19 | return; 20 | } 21 | if (process.env.NPM_CONFIG_PUPPETEER_SKIP_CHROMIUM_DOWNLOAD || process.env.npm_config_puppeteer_skip_chromium_download) { 22 | console.log('**INFO** Skipping Chromium download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" was set in npm config.'); 23 | return; 24 | } 25 | 26 | const Downloader = require('./utils/ChromiumDownloader'); 27 | const platform = Downloader.currentPlatform(); 28 | const revision = require('./package').puppeteer.chromium_revision; 29 | const ProgressBar = require('progress'); 30 | 31 | const revisionInfo = Downloader.revisionInfo(platform, revision); 32 | // Do nothing if the revision is already downloaded. 33 | if (revisionInfo.downloaded) 34 | return; 35 | 36 | // Override current environment proxy settings with npm configuration, if any. 37 | const NPM_HTTPS_PROXY = process.env.npm_config_https_proxy || process.env.npm_config_proxy; 38 | const NPM_HTTP_PROXY = process.env.npm_config_http_proxy || process.env.npm_config_proxy; 39 | if (NPM_HTTPS_PROXY) 40 | process.env.HTTPS_PROXY = NPM_HTTPS_PROXY; 41 | if (NPM_HTTP_PROXY) 42 | process.env.HTTP_PROXY = NPM_HTTP_PROXY; 43 | 44 | const allRevisions = Downloader.downloadedRevisions(); 45 | const DOWNLOAD_HOST = process.env.PUPPETEER_DOWNLOAD_HOST || process.env.npm_config_puppeteer_download_host; 46 | Downloader.downloadRevision(platform, revision, DOWNLOAD_HOST, onProgress) 47 | .then(onSuccess) 48 | .catch(onError); 49 | 50 | /** 51 | * @return {!Promise} 52 | */ 53 | function onSuccess() { 54 | console.log('Chromium downloaded to ' + revisionInfo.folderPath); 55 | // Remove previous chromium revisions. 56 | const cleanupOldVersions = allRevisions.map(({platform, revision}) => Downloader.removeRevision(platform, revision)); 57 | return Promise.all(cleanupOldVersions); 58 | } 59 | 60 | /** 61 | * @param {!Error} error 62 | */ 63 | function onError(error) { 64 | console.error(`ERROR: Failed to download Chromium r${revision}! Set "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" env variable to skip download.`); 65 | console.error(error); 66 | process.exit(1); 67 | } 68 | 69 | let progressBar = null; 70 | function onProgress(bytesTotal, delta) { 71 | if (!progressBar) { 72 | progressBar = new ProgressBar(`Downloading Chromium r${revision} - ${toMegabytes(bytesTotal)} [:bar] :percent :etas `, { 73 | complete: '=', 74 | incomplete: ' ', 75 | width: 20, 76 | total: bytesTotal, 77 | }); 78 | } 79 | progressBar.tick(delta); 80 | } 81 | 82 | function toMegabytes(bytes) { 83 | const mb = bytes / 1024 / 1024; 84 | return `${Math.round(mb * 10) / 10} Mb`; 85 | } 86 | 87 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/Documentation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | class Documentation { 18 | /** 19 | * @param {!Array} clasesArray 20 | */ 21 | constructor(classesArray) { 22 | this.classesArray = classesArray; 23 | this.classes = new Map(); 24 | for (const cls of classesArray) 25 | this.classes.set(cls.name, cls); 26 | } 27 | } 28 | 29 | Documentation.Class = class { 30 | /** 31 | * @param {string} name 32 | * @param {!Array} membersArray 33 | */ 34 | constructor(name, membersArray) { 35 | this.name = name; 36 | this.membersArray = membersArray; 37 | this.members = new Map(); 38 | this.properties = new Map(); 39 | this.methods = new Map(); 40 | this.events = new Map(); 41 | for (const member of membersArray) { 42 | this.members.set(member.name, member); 43 | if (member.type === 'method') 44 | this.methods.set(member.name, member); 45 | else if (member.type === 'property') 46 | this.properties.set(member.name, member); 47 | else if (member.type === 'event') 48 | this.events.set(member.name, member); 49 | } 50 | } 51 | }; 52 | 53 | Documentation.Member = class { 54 | /** 55 | * @param {string} type 56 | * @param {string} name 57 | * @param {!Array} argsArray 58 | * @param {boolean} hasReturn 59 | * @param {boolean} async 60 | */ 61 | constructor(type, name, argsArray, hasReturn, async) { 62 | this.type = type; 63 | this.name = name; 64 | this.argsArray = argsArray; 65 | this.args = new Map(); 66 | this.hasReturn = hasReturn; 67 | this.async = async; 68 | for (const arg of argsArray) 69 | this.args.set(arg.name, arg); 70 | } 71 | 72 | /** 73 | * @param {string} name 74 | * @param {!Array} argsArray 75 | * @param {boolean} hasReturn 76 | * @return {!Documentation.Member} 77 | */ 78 | static createMethod(name, argsArray, hasReturn, async) { 79 | return new Documentation.Member('method', name, argsArray, hasReturn, async); 80 | } 81 | 82 | /** 83 | * @param {string} name 84 | * @return {!Documentation.Member} 85 | */ 86 | static createProperty(name) { 87 | return new Documentation.Member('property', name, [], false, false); 88 | } 89 | 90 | /** 91 | * @param {string} name 92 | * @return {!Documentation.Member} 93 | */ 94 | static createEvent(name) { 95 | return new Documentation.Member('event', name, [], false, false); 96 | } 97 | }; 98 | 99 | Documentation.Argument = class { 100 | /** 101 | * @param {string} name 102 | */ 103 | constructor(name) { 104 | this.name = name; 105 | } 106 | }; 107 | 108 | module.exports = Documentation; 109 | 110 | -------------------------------------------------------------------------------- /utils/doclint/preprocessor/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 Message = require('../Message'); 18 | 19 | const PUPPETEER_VERSION = require('../../../package.json').version; 20 | 21 | module.exports = function(sources) { 22 | const messages = []; 23 | let commands = []; 24 | for (const source of sources) { 25 | const text = source.text(); 26 | const commandStartRegex = //ig; 27 | const commandEndRegex = //ig; 28 | let start; 29 | 30 | while (start = commandStartRegex.exec(text)) { // eslint-disable-line no-cond-assign 31 | commandEndRegex.lastIndex = commandStartRegex.lastIndex; 32 | const end = commandEndRegex.exec(text); 33 | if (!end) { 34 | messages.push(Message.error(`Failed to find 'gen:stop' for comamnd ${start[0]}`)); 35 | break; 36 | } 37 | const name = start[1]; 38 | const arg = start[2]; 39 | const from = commandStartRegex.lastIndex; 40 | const to = end.index; 41 | commandStartRegex.lastIndex = commandEndRegex.lastIndex; 42 | commands.push({name, arg, from, to, source}); 43 | } 44 | } 45 | 46 | commands = validateCommands(commands, messages); 47 | 48 | const changedSources = new Set(); 49 | // Iterate commands in reverse order so that edits don't conflict. 50 | commands.sort((a, b) => b.from - a.from); 51 | for (const command of commands) { 52 | let newText = command.source.text(); 53 | if (command.name === 'version') 54 | newText = replaceInText(newText, command.from, command.to, PUPPETEER_VERSION); 55 | if (command.source.setText(newText)) 56 | changedSources.add(command.source); 57 | } 58 | for (const source of changedSources) 59 | messages.push(Message.warning(`GEN: updated ${source.projectPath()}`)); 60 | return messages; 61 | }; 62 | 63 | /** 64 | * @param {!Array} commands 65 | * @param {!Array} outMessages 66 | * @return {!Array} 67 | */ 68 | function validateCommands(commands, outMessages) { 69 | // Filter sane commands 70 | const goodCommands = commands.filter(command => { 71 | if (command.name === 'version') 72 | return check(command, !command.arg, `"gen:version" should not have argument`); 73 | check(command, false, `Unknown command: "gen:${command.name}"`); 74 | }); 75 | 76 | return goodCommands; 77 | 78 | function check(command, condition, message) { 79 | if (condition) 80 | return true; 81 | outMessages.push(Message.error(`${command.source.projectPath()}: ${message}`)); 82 | return false; 83 | } 84 | } 85 | 86 | /** 87 | * @param {string} text 88 | * @param {number} from 89 | * @param {number} to 90 | * @param {string} newText 91 | * @return {string} 92 | */ 93 | function replaceInText(text, from, to, newText) { 94 | return text.substring(0, from) + newText + text.substring(to); 95 | } 96 | -------------------------------------------------------------------------------- /lib/EmulationManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | class EmulationManager { 18 | /** 19 | * @param {!Puppeteer.Session} client 20 | */ 21 | constructor(client) { 22 | this._client = client; 23 | this._emulatingMobile = false; 24 | this._injectedTouchScriptId = null; 25 | } 26 | 27 | /** 28 | * @param {!EmulationManager.Viewport} viewport 29 | * @return {Promise} 30 | */ 31 | async emulateViewport(client, viewport) { 32 | const mobile = viewport.isMobile || false; 33 | const width = viewport.width; 34 | const height = viewport.height; 35 | const deviceScaleFactor = viewport.deviceScaleFactor || 1; 36 | const screenOrientation = viewport.isLandscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' }; 37 | 38 | await Promise.all([ 39 | this._client.send('Emulation.setDeviceMetricsOverride', { mobile, width, height, deviceScaleFactor, screenOrientation }), 40 | this._client.send('Emulation.setTouchEmulationEnabled', { 41 | enabled: viewport.hasTouch || false, 42 | configuration: viewport.isMobile ? 'mobile' : 'desktop' 43 | }) 44 | ]); 45 | 46 | let reloadNeeded = false; 47 | if (viewport.hasTouch && !this._injectedTouchScriptId) { 48 | const source = `(${injectedTouchEventsFunction})()`; 49 | this._injectedTouchScriptId = (await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source })).identifier; 50 | reloadNeeded = true; 51 | } else if (!viewport.hasTouch && this._injectedTouchScriptId) { 52 | await this._client.send('Page.removeScriptToEvaluateOnNewDocument', {identifier: this._injectedTouchScriptId}); 53 | this._injectedTouchScriptId = null; 54 | reloadNeeded = true; 55 | } 56 | 57 | if (this._emulatingMobile !== mobile) 58 | reloadNeeded = true; 59 | this._emulatingMobile = mobile; 60 | return reloadNeeded; 61 | 62 | function injectedTouchEventsFunction() { 63 | const touchEvents = ['ontouchstart', 'ontouchend', 'ontouchmove', 'ontouchcancel']; 64 | // @ts-ignore 65 | const recepients = [window.__proto__, document.__proto__]; 66 | for (let i = 0; i < touchEvents.length; ++i) { 67 | for (let j = 0; j < recepients.length; ++j) { 68 | if (!(touchEvents[i] in recepients[j])) { 69 | Object.defineProperty(recepients[j], touchEvents[i], { 70 | value: null, writable: true, configurable: true, enumerable: true 71 | }); 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * @typedef {Object} EmulationManager.Viewport 81 | * @property {number} width 82 | * @property {number} height 83 | * @property {number=} deviceScaleFactor 84 | * @property {boolean=} isMobile 85 | * @property {boolean=} isLandscape 86 | * @property {boolean=} hasTouch 87 | */ 88 | 89 | module.exports = EmulationManager; 90 | -------------------------------------------------------------------------------- /test/server/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | MIIJjjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIT2qHBljWwsICAggA 3 | MBQGCCqGSIb3DQMHBAh5lMoGsb1UgQSCCUg/FtxOBSUG05DteTzzkdp6FptfOy5p 4 | iirYKWUNck6EhPdZ1RPSuCvs84S+qP6Po5gFfGz9e7tdbNSueOQnMD+vbS8vXpZU 5 | 4KVafxUlKbAfSVj2ZY6SkbU8v/iXiTieUj3G046z5QYsfcvyEdS47Z+WbKAJKemv 6 | BVck4W+/gKtiukbK/94VaehC7kbbO2yp9/kYp9qqk33aB61C8xX/fILOIRdYkvut 7 | 55pHGviN5Eajabm/d83MPAzaqUdv9NiIKej2IFXs+Oz8OYRbVVHwyN93HquHmn+V 8 | WKOknJXzvS5Pa+ViGmB4o6+fpluuIyHXJUUG20z/IY5n0vaBo4jRlyYZ3J1vWTZ2 9 | 1bhLGPUPUlGt1vToY5Ejr8qxaYGxLEOOxvRL3Z1xnlCbv2tQTD3TyV+Yr7i/54s4 10 | 2h0ddnP22KDmKEA8PhxZMZG7i7fsO+yA6hbKeMnS8lGv5Yyw2H7vRm4JwgeuX7A7 11 | gPQ/zQ6Sf6fUCkOMgVD3jJZ4XK7l9GkEQMYJAoowYb+WJ+yHSUQWJLJzPnNqhAPT 12 | aqQGHAO+AjGDnSI6ANDlxUubfU4yjQPg2eVyFczS7G+BISgCrJyQP/Z3Mpzo5rdu 13 | iTVcH8Phvsdm3PBOTkbgqxeHos6tyIiH1aElwlG3hxM1QiM6D9p7b5RyjUqfP/qn 14 | r1CndzroAyvseQ3ET8mJQAqWdGd+qmw9t0Voi7sR9jcDLVw+oLSk+oD8dk6BrItI 15 | rb39DmQAJiP7sLtAZ/zUm3sRpGgytNjElzzDdmijCM1A5oJzLvRwwa4AHwicMBkW 16 | nRnvliockHhFHQnQ/QThNGTPoNOHHgWwUKIidSFAdPIK1tYJP/4te2t5Djm25jS7 17 | ITm/LR1gbUguOMlOqwrDyAQir5Jjjk2pqN30+W9AXgVk/Oq/bWC8eMRv8zsmBlUY 18 | poXBwV52ITCzpNOsywSpz1vWGs1I0WQcLbm1zrHCR3CYxPOB45XdvHtwnHn5zaLF 19 | NB42IS/zK4PYzSJrbwJMsILS51Qb88JNL4PvYMCz43dUd0W6hokX4yCnQjfeEvB1 20 | MIhYQBu/lFK3IsskgoYBeg4zbU950vKSL+oNb5/dzAoayyJ/jRg5a1xMx8tCHazW 21 | 5afYxyIE9FRThwsHQ7K8BGe/4qwMCbvDIh/Fk+tWqXiyJJdsixf6YZLXtQXPkzki 22 | azpN9plUSoWpTVY4i6wNF5IO5LExLWlemDnw1v99lnU5W3SfgEwV8DYAyScA7Qzd 23 | GJQEfZVPSSxMQSyfrAap80PVs5KZWYcJpzKl9vwwdzopA2oj2vvx40r43YFiVFHp 24 | IpG9biWgh6McKKVCT1QThtNktDS9NT4uQZofC6m+RzzbsfI2R9IelozSOrS3xRJd 25 | D1eTiIccuomOsrhnh+/VDc9iuP6LgGaKwjdNSfcryuOn3S2+vho7wGrul/GxRf5k 26 | GrT1C68y5e0C/qbxyKtJPEGcntgLADhVrPr3WiM2M4tZ/imMi9XeyKSZ3i6P0OB/ 27 | QkhfNLrws03nXi3ASpk7/C9EWnnAYGwxQRht2LpEcTOpCUmmZ+6cBM4+dXMjvxqU 28 | SXR93Vm7Rwfs3MSIbtD2lGEXaIvG5mOh/9HXByBOh/UuSATeUIgEWgfW1zn0tvAw 29 | muqOyeS+ngYtoRK0NV979Kp7Pizq7ZHpoemm+C65EVvq2CU/ceRNh6DwCW5ZCHvU 30 | rJdOXqdO9Oef/2rHmLjnkwIjkXS3MJFd9wlCsSJn2SsuNmSLkwXuEkgdbjMKwjW+ 31 | sKzozd6FDp80HBuNw6H+kBn8KhO9tdQmyZnS9EpwO+OLgCTuyDNBiA892aevx9zD 32 | 7WPzNlsppcMEcxudv0mvavMfUJhbQ0wo+9Rp3wRQXIquKt++5vK7EoPvUjhVO39p 33 | 1VJTfx/wnX39VF62hc+OH3rritHmsrCBwzPupDGWSLBwQcMJbmgjFojuMG02pkIM 34 | mR5HIp+Xl4fWmSa9aklVyssdSi1hK+6VRIafTPtCiyA2BfeeYQk1xEsjSai0TYqh 35 | Suw+3wKOORm0xPEV30qX08N3bCP7aqqA+dMXWjfDM7LUBzDV4+MX6WOMdunlBdIS 36 | 6q63DXon6CCLoXPun/IwUEhOZfh43UAF3qEfxJdkhH6AgPdCSYBB5AWHQDdqvrBY 37 | wkhPEXkY2viMF2hOknXQr1GAdx0EjnAaMMxDE7+q01ERdgnO0xRVOD/4DjngdU4K 38 | p935fERs5/1nWe4yq/z8lXEC2W3YdiPR3Ok+pekyzhokh5/b+eG8G0JEnnl4+M8y 39 | qqyZZTJJeDFhasOzxwc5hwq8yqOznF6dEF58FXO9pKqBXbz5NrwbdUPtp67WwNx7 40 | iTRNe9szns5U8/3S5LEg+xGvAUhwbdmWxzYHye2MAt4sObi4YFDb1RVkZzCj2eer 41 | 6hzCQi5VUB3pUqr8in1TJMXPzCrl4ZdK2KoHtNA1gKT7midLzsjP+rXsgKtzcGAO 42 | IcNOkbv8KqzYr2OtcEW+v9z6vgSpKImQ/n+6/WLRGWh9pLCZ+onD06OLcsm55H4y 43 | zNacau+nT+Mf9XQTErFjj2+orjF3p2u3tY9vpoLJN653mMTNzexVZi+k9NgZz7eL 44 | 4m8R3ai0ZeGQezo9rGHQ6ulqlueN0qXGKI07G4VKxM35OZeOyPPnrF95FQzMY5Ox 45 | GkQr1hR0rj+oQJTFWIfy9ffZEBzUqC8fYYvOPJO32I+fe2tvBUtpUe3w7dYHVs3a 46 | lqi/+Wwk0QLE4ItR5f0sZsoIeHkDeXoKYrCOOPl2bnIQA3UT/rszGl6YDY8pU/7W 47 | fziO/G5BzGe7LU7o26ykzkmDF5alUaNdfVnnQcm/iRy8YOsjfKQmqbFfFCKOlRMo 48 | RdTStOcsEzddttI57YlBHv5Gh1GfMjd5acgeQfStSMwHcahwY2Y36WcR4GE4Id/5 49 | ycaikCt+zYEVKUoO+JEZxvzFQIPVhT3oBOjSFj/EeMLmnGMjnohdgEmh2T7+0Ko5 50 | oAMzWW/XNeJlL1DAPPEpya66oSvwZvxUkGDLcIGYWgzSKz0JO8zn6wDTOU7O2YeF 51 | Gm8YymS0oqnK1wE6HJ1YSk2ers6kgZij3kVAnGlbI88YLeMJvteO2oL7GqfEQdSA 52 | HskQklLZUT2Y+m7oinVyCcdbKRGI9frnfm6j3TVkrNHjcc7aKhM+XT5ZNKxT3ftt 53 | 4Ig= 54 | -----END ENCRYPTED PRIVATE KEY----- 55 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | 4 | "env": { 5 | "node": true, 6 | "es6": true 7 | }, 8 | 9 | "parserOptions": { 10 | "ecmaVersion": 8 11 | }, 12 | 13 | /** 14 | * ESLint rules 15 | * 16 | * All available rules: http://eslint.org/docs/rules/ 17 | * 18 | * Rules take the following form: 19 | * "rule-name", [severity, { opts }] 20 | * Severity: 2 == error, 1 == warning, 0 == off. 21 | */ 22 | "rules": { 23 | /** 24 | * Enforced rules 25 | */ 26 | 27 | 28 | // syntax preferences 29 | "quotes": [2, "single", { 30 | "avoidEscape": true, 31 | "allowTemplateLiterals": true 32 | }], 33 | "semi": 2, 34 | "no-extra-semi": 2, 35 | "comma-style": [2, "last"], 36 | "wrap-iife": [2, "inside"], 37 | "spaced-comment": [2, "always", { 38 | "markers": ["*"] 39 | }], 40 | "eqeqeq": [2], 41 | "arrow-body-style": [2, "as-needed"], 42 | "accessor-pairs": [2, { 43 | "getWithoutSet": false, 44 | "setWithoutGet": false 45 | }], 46 | "brace-style": [2, "1tbs", {"allowSingleLine": true}], 47 | "curly": [2, "multi-or-nest", "consistent"], 48 | "new-parens": 2, 49 | "func-call-spacing": 2, 50 | "arrow-parens": [2, "as-needed"], 51 | "prefer-const": 2, 52 | "quote-props": [2, "consistent"], 53 | 54 | // anti-patterns 55 | "no-var": 2, 56 | "no-with": 2, 57 | "no-multi-str": 2, 58 | "no-caller": 2, 59 | "no-implied-eval": 2, 60 | "no-labels": 2, 61 | "no-new-object": 2, 62 | "no-octal-escape": 2, 63 | "no-self-compare": 2, 64 | "no-shadow-restricted-names": 2, 65 | "no-cond-assign": 2, 66 | "no-debugger": 2, 67 | "no-dupe-keys": 2, 68 | "no-duplicate-case": 2, 69 | "no-empty-character-class": 2, 70 | "no-unreachable": 2, 71 | "no-unsafe-negation": 2, 72 | "radix": 2, 73 | "valid-typeof": 2, 74 | "no-unused-vars": [2, { "args": "none", "vars": "local" }], 75 | "no-implicit-globals": [2], 76 | 77 | // es2015 features 78 | "require-yield": 2, 79 | "template-curly-spacing": [2, "never"], 80 | 81 | // spacing details 82 | "space-infix-ops": 2, 83 | "space-in-parens": [2, "never"], 84 | "space-before-function-paren": [2, "never"], 85 | "no-whitespace-before-property": 2, 86 | "keyword-spacing": [2, { 87 | "overrides": { 88 | "if": {"after": true}, 89 | "else": {"after": true}, 90 | "for": {"after": true}, 91 | "while": {"after": true}, 92 | "do": {"after": true}, 93 | "switch": {"after": true}, 94 | "return": {"after": true} 95 | } 96 | }], 97 | "arrow-spacing": [2, { 98 | "after": true, 99 | "before": true 100 | }], 101 | 102 | // file whitespace 103 | "no-multiple-empty-lines": [2, {"max": 2}], 104 | "no-mixed-spaces-and-tabs": 2, 105 | "no-trailing-spaces": 2, 106 | "linebreak-style": [ process.platform === "win32" ? 0 : 2, "unix" ], 107 | "indent": [2, 2, { "SwitchCase": 1, "CallExpression": {"arguments": 2}, "MemberExpression": 2 }], 108 | "key-spacing": [2, { 109 | "beforeColon": false 110 | }] 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /utils/node6-transform/test/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | const transformAsyncFunctions = require('../TransformAsyncFunctions'); 17 | 18 | describe('TransformAsyncFunctions', function() { 19 | it('should convert a function expression', function(done) { 20 | const input = `(async function(){ return 123 })()`; 21 | const output = eval(transformAsyncFunctions(input)); 22 | expect(output instanceof Promise).toBe(true); 23 | output.then(result => expect(result).toBe(123)).then(done); 24 | }); 25 | it('should convert an arrow function', function(done) { 26 | const input = `(async () => 123)()`; 27 | const output = eval(transformAsyncFunctions(input)); 28 | expect(output instanceof Promise).toBe(true); 29 | output.then(result => expect(result).toBe(123)).then(done); 30 | }); 31 | it('should convert an arrow function with curly braces', function(done) { 32 | const input = `(async () => { return 123 })()`; 33 | const output = eval(transformAsyncFunctions(input)); 34 | expect(output instanceof Promise).toBe(true); 35 | output.then(result => expect(result).toBe(123)).then(done); 36 | }); 37 | it('should convert a function declaration', function(done) { 38 | const input = `async function f(){ return 123; } f();`; 39 | const output = eval(transformAsyncFunctions(input)); 40 | expect(output instanceof Promise).toBe(true); 41 | output.then(result => expect(result).toBe(123)).then(done); 42 | }); 43 | it('should convert await', function(done) { 44 | const input = `async function f(){ return 23 + await Promise.resolve(100); } f();`; 45 | const output = eval(transformAsyncFunctions(input)); 46 | expect(output instanceof Promise).toBe(true); 47 | output.then(result => expect(result).toBe(123)).then(done); 48 | }); 49 | it('should convert method', function(done) { 50 | const input = `class X{async f() { return 123 }} (new X()).f();`; 51 | const output = eval(transformAsyncFunctions(input)); 52 | expect(output instanceof Promise).toBe(true); 53 | output.then(result => expect(result).toBe(123)).then(done); 54 | }); 55 | it('should pass arguments', function(done) { 56 | const input = `(async function(a, b){ return await a + await b })(Promise.resolve(100), 23)`; 57 | const output = eval(transformAsyncFunctions(input)); 58 | expect(output instanceof Promise).toBe(true); 59 | output.then(result => expect(result).toBe(123)).then(done); 60 | }); 61 | it('should still work across eval', function(done) { 62 | const input = `var str = (async function(){ return 123; }).toString(); eval('(' + str + ')')();`; 63 | const output = eval(transformAsyncFunctions(input)); 64 | expect(output instanceof Promise).toBe(true); 65 | output.then(result => expect(result).toBe(123)).then(done); 66 | }); 67 | it('should work with double await', function(done) { 68 | const input = `async function f(){ return 23 + await Promise.resolve(50 + await Promise.resolve(50)); } f();`; 69 | const output = eval(transformAsyncFunctions(input)); 70 | expect(output instanceof Promise).toBe(true); 71 | output.then(result => expect(result).toBe(123)).then(done); 72 | }); 73 | it('should work paren around arrow function', function(done) { 74 | const input = `(async x => ( 123))()`; 75 | const output = eval(transformAsyncFunctions(input)); 76 | expect(output instanceof Promise).toBe(true); 77 | output.then(result => expect(result).toBe(123)).then(done); 78 | }); 79 | }); -------------------------------------------------------------------------------- /lib/NavigatorWatcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 {helper} = require('./helper'); 18 | 19 | class NavigatorWatcher { 20 | /** 21 | * @param {!Puppeteer.Session} client 22 | * @param {string} frameId 23 | * @param {boolean} ignoreHTTPSErrors 24 | * @param {!Object=} options 25 | */ 26 | constructor(client, frameId, ignoreHTTPSErrors, options = {}) { 27 | console.assert(options.networkIdleTimeout === undefined, 'ERROR: networkIdleTimeout option is no longer supported.'); 28 | console.assert(options.networkIdleInflight === undefined, 'ERROR: networkIdleInflight option is no longer supported.'); 29 | console.assert(options.waitUntil !== 'networkidle', 'ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead'); 30 | this._client = client; 31 | this._frameId = frameId; 32 | this._ignoreHTTPSErrors = ignoreHTTPSErrors; 33 | this._timeout = typeof options.timeout === 'number' ? options.timeout : 30000; 34 | let waitUntil = ['load']; 35 | if (Array.isArray(options.waitUntil)) 36 | waitUntil = options.waitUntil.slice(); 37 | else if (typeof options.waitUntil === 'string') 38 | waitUntil = [options.waitUntil]; 39 | for (const value of waitUntil) { 40 | const isAllowedValue = value === 'networkidle0' || value === 'networkidle2' || value === 'load' || value === 'domcontentloaded'; 41 | console.assert(isAllowedValue, 'Unknown value for options.waitUntil: ' + value); 42 | } 43 | this._pendingEvents = new Set(waitUntil); 44 | } 45 | 46 | /** 47 | * @return {!Promise} 48 | */ 49 | async waitForNavigation() { 50 | this._eventListeners = []; 51 | 52 | const navigationPromises = []; 53 | if (this._timeout) { 54 | const watchdog = new Promise(fulfill => this._maximumTimer = setTimeout(fulfill, this._timeout)) 55 | .then(() => 'Navigation Timeout Exceeded: ' + this._timeout + 'ms exceeded'); 56 | navigationPromises.push(watchdog); 57 | } 58 | 59 | if (!this._ignoreHTTPSErrors) { 60 | const certificateError = new Promise(fulfill => { 61 | this._eventListeners.push(helper.addEventListener(this._client, 'Security.certificateError', fulfill)); 62 | }).then(error => 'SSL Certificate error: ' + error.errorType); 63 | navigationPromises.push(certificateError); 64 | } 65 | 66 | this._eventListeners.push(helper.addEventListener(this._client, 'Page.lifecycleEvent', this._onLifecycleEvent.bind(this))); 67 | const pendingEventsFired = new Promise(fulfill => this._pendingEventsCallback = fulfill); 68 | navigationPromises.push(pendingEventsFired); 69 | 70 | const error = await Promise.race(navigationPromises); 71 | this._cleanup(); 72 | return error ? new Error(error) : null; 73 | } 74 | 75 | /** 76 | * @param {!{frameId: string, name: string}} event 77 | */ 78 | _onLifecycleEvent(event) { 79 | if (event.frameId !== this._frameId) 80 | return; 81 | const pptrName = protocolLifecycleToPuppeteer[event.name]; 82 | if (!pptrName) 83 | return; 84 | this._pendingEvents.delete(pptrName); 85 | if (this._pendingEvents.size === 0) 86 | this._pendingEventsCallback(); 87 | } 88 | 89 | cancel() { 90 | this._cleanup(); 91 | } 92 | 93 | _cleanup() { 94 | helper.removeEventListeners(this._eventListeners); 95 | clearTimeout(this._maximumTimer); 96 | } 97 | } 98 | 99 | const protocolLifecycleToPuppeteer = { 100 | 'load': 'load', 101 | 'DOMContentLoaded': 'domcontentloaded', 102 | 'networkIdle': 'networkidle0', 103 | 'networkAlmostIdle': 'networkidle2', 104 | }; 105 | 106 | module.exports = NavigatorWatcher; 107 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/test/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 fs = require('fs'); 18 | const rm = require('rimraf').sync; 19 | const path = require('path'); 20 | const puppeteer = require('../../../..'); 21 | const checkPublicAPI = require('..'); 22 | const SourceFactory = require('../../SourceFactory'); 23 | const mdBuilder = require('../MDBuilder'); 24 | const jsBuilder = require('../JSBuilder'); 25 | const GoldenUtils = require('../../../../test/golden-utils'); 26 | 27 | const OUTPUT_DIR = path.join(__dirname, 'output'); 28 | const GOLDEN_DIR = path.join(__dirname, 'golden'); 29 | 30 | let browser; 31 | let page; 32 | let specName; 33 | 34 | jasmine.getEnv().addReporter({ 35 | specStarted: result => specName = result.description 36 | }); 37 | 38 | beforeAll(SX(async function() { 39 | browser = await puppeteer.launch({args: ['--no-sandbox']}); 40 | page = await browser.newPage(); 41 | if (fs.existsSync(OUTPUT_DIR)) 42 | rm(OUTPUT_DIR); 43 | })); 44 | 45 | afterAll(SX(async function() { 46 | await browser.close(); 47 | })); 48 | 49 | describe('checkPublicAPI', function() { 50 | it('diff-classes', SX(testLint)); 51 | it('diff-methods', SX(testLint)); 52 | it('diff-properties', SX(testLint)); 53 | it('diff-arguments', SX(testLint)); 54 | it('diff-events', SX(testLint)); 55 | it('check-duplicates', SX(testLint)); 56 | it('check-sorting', SX(testLint)); 57 | it('check-returns', SX(testLint)); 58 | it('js-builder-common', SX(testJSBuilder)); 59 | it('js-builder-inheritance', SX(testJSBuilder)); 60 | it('md-builder-common', SX(testMDBuilder)); 61 | }); 62 | 63 | async function testLint() { 64 | const dirPath = path.join(__dirname, specName); 65 | GoldenUtils.addMatchers(jasmine, dirPath, dirPath); 66 | const factory = new SourceFactory(); 67 | const mdSources = await factory.readdir(dirPath, '.md'); 68 | const jsSources = await factory.readdir(dirPath, '.js'); 69 | const messages = await checkPublicAPI(page, mdSources, jsSources); 70 | const errors = messages.map(message => message.text); 71 | expect(errors.join('\n')).toBeGolden('result.txt'); 72 | } 73 | 74 | async function testMDBuilder() { 75 | const dirPath = path.join(__dirname, specName); 76 | GoldenUtils.addMatchers(jasmine, dirPath, dirPath); 77 | const factory = new SourceFactory(); 78 | const sources = await factory.readdir(dirPath, '.md'); 79 | const {documentation} = await mdBuilder(page, sources); 80 | expect(serialize(documentation)).toBeGolden('result.txt'); 81 | } 82 | 83 | async function testJSBuilder() { 84 | const dirPath = path.join(__dirname, specName); 85 | GoldenUtils.addMatchers(jasmine, dirPath, dirPath); 86 | const factory = new SourceFactory(); 87 | const sources = await factory.readdir(dirPath, '.js'); 88 | const {documentation} = await jsBuilder(sources); 89 | expect(serialize(documentation)).toBeGolden('result.txt'); 90 | } 91 | 92 | function serialize(doc) { 93 | const result = {classes: []}; 94 | for (let cls of doc.classesArray) { 95 | const classJSON = { 96 | name: cls.name, 97 | members: [] 98 | }; 99 | result.classes.push(classJSON); 100 | for (let member of cls.membersArray) { 101 | classJSON.members.push({ 102 | name: member.name, 103 | type: member.type, 104 | hasReturn: member.hasReturn, 105 | async: member.async, 106 | args: member.argsArray.map(arg => arg.name) 107 | }); 108 | } 109 | } 110 | return JSON.stringify(result, null, 2); 111 | } 112 | 113 | // Since Jasmine doesn't like async functions, they should be wrapped 114 | // in a SX function. 115 | function SX(fun) { 116 | return done => Promise.resolve(fun()).then(done).catch(done.fail); 117 | } 118 | -------------------------------------------------------------------------------- /utils/node6-transform/TransformAsyncFunctions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 esprima = require('esprima'); 18 | const ESTreeWalker = require('../ESTreeWalker'); 19 | 20 | // This is converted from Babel's "transform-async-to-generator" 21 | // https://babeljs.io/docs/plugins/transform-async-to-generator/ 22 | const asyncToGenerator = fn => { 23 | const gen = fn.call(this); 24 | return new Promise((resolve, reject) => { 25 | function step(key, arg) { 26 | let info, value; 27 | try { 28 | info = gen[key](arg); 29 | value = info.value; 30 | } catch (error) { 31 | reject(error); 32 | return; 33 | } 34 | if (info.done) { 35 | resolve(value); 36 | } else { 37 | return Promise.resolve(value).then( 38 | value => { 39 | step('next', value); 40 | }, 41 | err => { 42 | step('throw', err); 43 | }); 44 | } 45 | } 46 | return step('next'); 47 | }); 48 | }; 49 | 50 | /** 51 | * @param {string} text 52 | * @return {string} 53 | */ 54 | function transformAsyncFunctions(text) { 55 | const edits = []; 56 | 57 | const ast = esprima.parseScript(text, {range: true, tolerant: true}); 58 | const walker = new ESTreeWalker(node => { 59 | if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration' || node.type === 'ArrowFunctionExpression') 60 | onFunction(node); 61 | else if (node.type === 'AwaitExpression') 62 | onAwait(node); 63 | }); 64 | walker.walk(ast); 65 | 66 | edits.sort((a, b) => b.from - a.from); 67 | for (const {replacement, from, to} of edits) 68 | text = text.substring(0, from) + replacement + text.substring(to); 69 | 70 | return text; 71 | 72 | /** 73 | * @param {ESTree.Node} node 74 | */ 75 | function onFunction(node) { 76 | if (!node.async) return; 77 | 78 | let range; 79 | if (node.parent.type === 'MethodDefinition') 80 | range = node.parent.range; 81 | else 82 | range = node.range; 83 | const index = text.substring(range[0], range[1]).indexOf('async') + range[0]; 84 | insertText(index, index + 'async'.length, '/* async */'); 85 | 86 | let before = `{return (${asyncToGenerator.toString()})(function*()`; 87 | let after = `);}`; 88 | if (node.body.type !== 'BlockStatement') { 89 | before += `{ return `; 90 | after = `; }` + after; 91 | 92 | // Remove parentheses that might wrap an arrow function 93 | const beforeBody = text.substring(node.range[0], node.body.range[0]); 94 | if (/\(\s*$/.test(beforeBody)) { 95 | const afterBody = text.substring(node.body.range[1], node.range[1]); 96 | const openParen = node.range[0] + beforeBody.lastIndexOf('('); 97 | insertText(openParen, openParen + 1, ' '); 98 | const closeParen = node.body.range[1] + afterBody.indexOf(')'); 99 | insertText(closeParen, closeParen + 1, ' '); 100 | } 101 | } 102 | 103 | 104 | insertText(node.body.range[0], node.body.range[0], before); 105 | insertText(node.body.range[1], node.body.range[1], after); 106 | } 107 | 108 | /** 109 | * @param {ESTree.Node} node 110 | */ 111 | function onAwait(node) { 112 | const index = text.substring(node.range[0], node.range[1]).indexOf('await') + node.range[0]; 113 | insertText(index, index + 'await'.length, '(yield'); 114 | insertText(node.range[1], node.range[1], ')'); 115 | } 116 | 117 | /** 118 | * @param {number} from 119 | * @param {number} to 120 | * @param {string} replacement 121 | */ 122 | function insertText(from, to, replacement) { 123 | edits.push({from, to, replacement}); 124 | } 125 | } 126 | 127 | module.exports = transformAsyncFunctions; -------------------------------------------------------------------------------- /utils/doclint/SourceFactory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 path = require('path'); 18 | const fs = require('fs'); 19 | 20 | const readFileAsync = promisify(fs.readFile); 21 | const readdirAsync = promisify(fs.readdir); 22 | const writeFileAsync = promisify(fs.writeFile); 23 | 24 | const PROJECT_DIR = path.join(__dirname, '..', '..'); 25 | 26 | class Source { 27 | /** 28 | * @param {string} filePath 29 | * @param {string} text 30 | */ 31 | constructor(filePath, text) { 32 | this._filePath = filePath; 33 | this._projectPath = path.relative(PROJECT_DIR, filePath); 34 | this._name = path.basename(filePath); 35 | this._text = text; 36 | this._hasUpdatedText = false; 37 | } 38 | 39 | /** 40 | * @return {string} 41 | */ 42 | filePath() { 43 | return this._filePath; 44 | } 45 | 46 | /** 47 | * @return {string} 48 | */ 49 | projectPath() { 50 | return this._projectPath; 51 | } 52 | 53 | /** 54 | * @return {string} 55 | */ 56 | name() { 57 | return this._name; 58 | } 59 | 60 | /** 61 | * @param {string} text 62 | * @return {boolean} 63 | */ 64 | setText(text) { 65 | if (text === this._text) 66 | return false; 67 | this._hasUpdatedText = true; 68 | this._text = text; 69 | return true; 70 | } 71 | 72 | /** 73 | * @return {string} 74 | */ 75 | text() { 76 | return this._text; 77 | } 78 | 79 | /** 80 | * @return {boolean} 81 | */ 82 | hasUpdatedText() { 83 | return this._hasUpdatedText; 84 | } 85 | } 86 | 87 | class SourceFactory { 88 | constructor() { 89 | this._sources = new Map(); 90 | } 91 | 92 | /** 93 | * @return {!Array} 94 | */ 95 | sources() { 96 | return Array.from(this._sources.values()); 97 | } 98 | 99 | /** 100 | * @return {!Promise} 101 | */ 102 | async saveChangedSources() { 103 | const changedSources = Array.from(this._sources.values()).filter(source => source.hasUpdatedText()); 104 | if (!changedSources.length) 105 | return false; 106 | await Promise.all(changedSources.map(source => writeFileAsync(source.filePath(), source.text()))); 107 | return true; 108 | } 109 | 110 | /** 111 | * @param {string} filePath 112 | * @return {!Promise} 113 | */ 114 | async readFile(filePath) { 115 | filePath = path.resolve(filePath); 116 | let source = this._sources.get(filePath); 117 | if (!source) { 118 | const text = await readFileAsync(filePath, {encoding: 'utf8'}); 119 | source = new Source(filePath, text); 120 | this._sources.set(filePath, source); 121 | } 122 | return source; 123 | } 124 | 125 | /** 126 | * @param {string} dirPath 127 | * @param {string=} extension 128 | * @return {!Promise>} 129 | */ 130 | async readdir(dirPath, extension = '') { 131 | const fileNames = await readdirAsync(dirPath); 132 | const filePaths = fileNames.filter(fileName => fileName.endsWith(extension)).map(fileName => path.join(dirPath, fileName)); 133 | return Promise.all(filePaths.map(filePath => this.readFile(filePath))); 134 | } 135 | 136 | /** 137 | * @param {string} filePath 138 | * @param {string} text 139 | * @return {!Source} 140 | */ 141 | createForTest(filePath, text) { 142 | return new Source(filePath, text); 143 | } 144 | } 145 | 146 | /** 147 | * @param {function(?)} nodeFunction 148 | * @return {function(?):!Promise} 149 | */ 150 | function promisify(nodeFunction) { 151 | /** 152 | * @param {!Array} options 153 | * @return {!Promise} 154 | */ 155 | return function(...options) { 156 | return new Promise(function(fulfill, reject) { 157 | options.push(callback); 158 | nodeFunction.call(null, ...options); 159 | function callback(err, result) { 160 | if (err) 161 | reject(err); 162 | else 163 | fulfill(result); 164 | } 165 | }); 166 | }; 167 | } 168 | 169 | module.exports = SourceFactory; 170 | -------------------------------------------------------------------------------- /utils/ESTreeWalker.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | /** 6 | * @unrestricted 7 | */ 8 | class ESTreeWalker { 9 | /** 10 | * @param {function(!ESTree.Node):(!Object|undefined)} beforeVisit 11 | * @param {function(!ESTree.Node)=} afterVisit 12 | */ 13 | constructor(beforeVisit, afterVisit) { 14 | this._beforeVisit = beforeVisit; 15 | this._afterVisit = afterVisit || new Function(); 16 | } 17 | 18 | /** 19 | * @param {!ESTree.Node} ast 20 | */ 21 | walk(ast) { 22 | this._innerWalk(ast, null); 23 | } 24 | 25 | /** 26 | * @param {!ESTree.Node} node 27 | * @param {?ESTree.Node} parent 28 | */ 29 | _innerWalk(node, parent) { 30 | if (!node) 31 | return; 32 | node.parent = parent; 33 | 34 | if (this._beforeVisit.call(null, node) === ESTreeWalker.SkipSubtree) { 35 | this._afterVisit.call(null, node); 36 | return; 37 | } 38 | 39 | const walkOrder = ESTreeWalker._walkOrder[node.type]; 40 | if (!walkOrder) 41 | return; 42 | 43 | if (node.type === 'TemplateLiteral') { 44 | const templateLiteral = /** @type {!ESTree.TemplateLiteralNode} */ (node); 45 | const expressionsLength = templateLiteral.expressions.length; 46 | for (let i = 0; i < expressionsLength; ++i) { 47 | this._innerWalk(templateLiteral.quasis[i], templateLiteral); 48 | this._innerWalk(templateLiteral.expressions[i], templateLiteral); 49 | } 50 | this._innerWalk(templateLiteral.quasis[expressionsLength], templateLiteral); 51 | } else { 52 | for (let i = 0; i < walkOrder.length; ++i) { 53 | const entity = node[walkOrder[i]]; 54 | if (Array.isArray(entity)) 55 | this._walkArray(entity, node); 56 | else 57 | this._innerWalk(entity, node); 58 | } 59 | } 60 | 61 | this._afterVisit.call(null, node); 62 | } 63 | 64 | /** 65 | * @param {!Array.} nodeArray 66 | * @param {?ESTree.Node} parentNode 67 | */ 68 | _walkArray(nodeArray, parentNode) { 69 | for (let i = 0; i < nodeArray.length; ++i) 70 | this._innerWalk(nodeArray[i], parentNode); 71 | } 72 | } 73 | 74 | /** @typedef {!Object} ESTreeWalker.SkipSubtree */ 75 | ESTreeWalker.SkipSubtree = {}; 76 | 77 | /** @enum {!Array.} */ 78 | ESTreeWalker._walkOrder = { 79 | 'AwaitExpression': ['argument'], 80 | 'ArrayExpression': ['elements'], 81 | 'ArrowFunctionExpression': ['params', 'body'], 82 | 'AssignmentExpression': ['left', 'right'], 83 | 'BinaryExpression': ['left', 'right'], 84 | 'BlockStatement': ['body'], 85 | 'BreakStatement': ['label'], 86 | 'CallExpression': ['callee', 'arguments'], 87 | 'CatchClause': ['param', 'body'], 88 | 'ClassBody': ['body'], 89 | 'ClassDeclaration': ['id', 'superClass', 'body'], 90 | 'ClassExpression': ['id', 'superClass', 'body'], 91 | 'ConditionalExpression': ['test', 'consequent', 'alternate'], 92 | 'ContinueStatement': ['label'], 93 | 'DebuggerStatement': [], 94 | 'DoWhileStatement': ['body', 'test'], 95 | 'EmptyStatement': [], 96 | 'ExpressionStatement': ['expression'], 97 | 'ForInStatement': ['left', 'right', 'body'], 98 | 'ForOfStatement': ['left', 'right', 'body'], 99 | 'ForStatement': ['init', 'test', 'update', 'body'], 100 | 'FunctionDeclaration': ['id', 'params', 'body'], 101 | 'FunctionExpression': ['id', 'params', 'body'], 102 | 'Identifier': [], 103 | 'IfStatement': ['test', 'consequent', 'alternate'], 104 | 'LabeledStatement': ['label', 'body'], 105 | 'Literal': [], 106 | 'LogicalExpression': ['left', 'right'], 107 | 'MemberExpression': ['object', 'property'], 108 | 'MethodDefinition': ['key', 'value'], 109 | 'NewExpression': ['callee', 'arguments'], 110 | 'ObjectExpression': ['properties'], 111 | 'ObjectPattern': ['properties'], 112 | 'ParenthesizedExpression': ['expression'], 113 | 'Program': ['body'], 114 | 'Property': ['key', 'value'], 115 | 'ReturnStatement': ['argument'], 116 | 'SequenceExpression': ['expressions'], 117 | 'Super': [], 118 | 'SwitchCase': ['test', 'consequent'], 119 | 'SwitchStatement': ['discriminant', 'cases'], 120 | 'TaggedTemplateExpression': ['tag', 'quasi'], 121 | 'TemplateElement': [], 122 | 'TemplateLiteral': ['quasis', 'expressions'], 123 | 'ThisExpression': [], 124 | 'ThrowStatement': ['argument'], 125 | 'TryStatement': ['block', 'handler', 'finalizer'], 126 | 'UnaryExpression': ['argument'], 127 | 'UpdateExpression': ['argument'], 128 | 'VariableDeclaration': ['declarations'], 129 | 'VariableDeclarator': ['id', 'init'], 130 | 'WhileStatement': ['test', 'body'], 131 | 'WithStatement': ['object', 'body'], 132 | 'YieldExpression': ['argument'] 133 | }; 134 | 135 | module.exports = ESTreeWalker; 136 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | First of all, thank you for your interest in Puppeteer! 4 | We'd love to accept your patches and contributions! 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Getting setup 19 | 20 | 1. Clone this repository 21 | ```bash 22 | git clone https://github.com/GoogleChrome/puppeteer 23 | cd puppeteer 24 | ``` 25 | 2. Install dependencies 26 | ```bash 27 | yarn # or 'npm install' 28 | ``` 29 | 30 | ## Code reviews 31 | 32 | All submissions, including submissions by project members, require review. We 33 | use GitHub pull requests for this purpose. Consult 34 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 35 | information on using pull requests. 36 | 37 | ## Code Style 38 | 39 | - coding style is fully defined in [.eslintrc](https://github.com/GoogleChrome/puppeteer/blob/master/.eslintrc.js) 40 | - code should be annotated with [closure annotations](https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler) 41 | - comments should be generally avoided. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory 42 | 43 | To run code linter, use: 44 | ``` 45 | npm run lint 46 | ``` 47 | 48 | ## Writing Documentation 49 | 50 | All public API should have a descriptive entry in the [docs/api.md](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md). There's a [documentation linter](https://github.com/GoogleChrome/puppeteer/tree/master/utils/doclint) which makes sure documentation is aligned with the codebase. 51 | 52 | To run documentation linter, use 53 | ``` 54 | npm run doc 55 | ``` 56 | 57 | ## Adding New Dependencies 58 | 59 | For all dependencies (both installation and development): 60 | - **Do not add** a dependency if the desired functionality is easily implementable 61 | - if adding a dependency, it should be well-maintained and trustworthy 62 | 63 | A barrier for introducing new installation dependencies is especially high: 64 | - **do not add** installation dependency unless it's critical to project success 65 | 66 | ## Writing Tests 67 | 68 | - every feature should be accompanied by a test 69 | - every public api event/method should be accompanied by a test 70 | - tests should be *hermetic*. Tests should not depend on external services. 71 | - tests should work on all three platforms: Mac, Linux and Win. This is especially important for screenshot tests. 72 | 73 | Puppeteer tests are located in [test/test.js](https://github.com/GoogleChrome/puppeteer/blob/master/test/test.js) 74 | and are written using [Jasmine](https://jasmine.github.io/) testing framework. Despite being named 'unit', these are integration tests, making sure public API methods and events work as expected. 75 | 76 | - To run all tests: 77 | ``` 78 | npm run unit 79 | ``` 80 | - To filter tests by name: 81 | ``` 82 | npm run unit -- --filter=waitFor 83 | ``` 84 | - To run a specific test, substitute the `it` with `fit` (mnemonic rule: '*focus it*'): 85 | ```js 86 | ... 87 | // Using "fit" to run specific test 88 | fit('should work', SX(async function() { 89 | const response = await page.goto(EMPTY_PAGE); 90 | expect(response.ok).toBe(true); 91 | })) 92 | ``` 93 | - To disable a specific test, substitute the `it` with `xit` (mnemonic rule: '*cross it*'): 94 | ```js 95 | ... 96 | // Using "xit" to skip specific test 97 | xit('should work', SX(async function() { 98 | const response = await page.goto(EMPTY_PAGE); 99 | expect(response.ok).toBe(true); 100 | })) 101 | ``` 102 | - To run tests in non-headless mode: 103 | ``` 104 | HEADLESS=false npm run unit 105 | ``` 106 | - To run tests with custom Chromium executable: 107 | ``` 108 | CHROME= npm run unit 109 | ``` 110 | - To run tests in slow-mode: 111 | ``` 112 | HEADLESS=false SLOW_MO=500 npm run unit 113 | ``` 114 | - To debug a test, "focus" a test first and then run: 115 | ``` 116 | npm run debug-unit 117 | ``` 118 | 119 | ## Public API Coverage 120 | 121 | Every public API method or event should be called at least once in tests. To ensure this, there's a coverage command which tracks calls to public API and reports back if some methods/events were not called. 122 | 123 | Run coverage: 124 | 125 | ``` 126 | npm run coverage 127 | ``` 128 | 129 | ## Debugging Puppeteer 130 | See [Debugging Tips](README.md#debugging-tips) in the readme 131 | 132 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ## Chrome headless doesn't launch 4 | 5 | - Make sure all the necessary dependencies are installed: 6 | 7 |
8 | Debian (e.g. Ubuntu) Dependencies 9 | 10 | ``` 11 | gconf-service 12 | libasound2 13 | libatk1.0-0 14 | libc6 15 | libcairo2 16 | libcups2 17 | libdbus-1-3 18 | libexpat1 19 | libfontconfig1 20 | libgcc1 21 | libgconf-2-4 22 | libgdk-pixbuf2.0-0 23 | libglib2.0-0 24 | libgtk-3-0 25 | libnspr4 26 | libpango-1.0-0 27 | libpangocairo-1.0-0 28 | libstdc++6 29 | libx11-6 30 | libx11-xcb1 31 | libxcb1 32 | libxcomposite1 33 | libxcursor1 34 | libxdamage1 35 | libxext6 36 | libxfixes3 37 | libxi6 38 | libxrandr2 39 | libxrender1 40 | libxss1 41 | libxtst6 42 | ca-certificates 43 | fonts-liberation 44 | libappindicator1 45 | libnss3 46 | lsb-release 47 | xdg-utils 48 | wget 49 | ``` 50 |
51 | 52 |
53 | CentOS Dependencies 54 | 55 | ``` 56 | pango.x86_64 57 | libXcomposite.x86_64 58 | libXcursor.x86_64 59 | libXdamage.x86_64 60 | libXext.x86_64 61 | libXi.x86_64 62 | libXtst.x86_64 63 | cups-libs.x86_64 64 | libXScrnSaver.x86_64 65 | libXrandr.x86_64 66 | GConf2.x86_64 67 | alsa-lib.x86_64 68 | atk.x86_64 69 | gtk3.x86_64 70 | ipa-gothic-fonts 71 | xorg-x11-fonts-100dpi 72 | xorg-x11-fonts-75dpi 73 | xorg-x11-utils 74 | xorg-x11-fonts-cyrillic 75 | xorg-x11-fonts-Type1 76 | xorg-x11-fonts-misc 77 | ``` 78 |
79 | 80 | - Check out discussions: 81 | - [#290](https://github.com/GoogleChrome/puppeteer/issues/290) - Debian troubleshooting 82 | - [#391](https://github.com/GoogleChrome/puppeteer/issues/391) - CentOS troubleshooting 83 | 84 | 85 | ## Chrome Headless fails due to sandbox issues 86 | 87 | - make sure kernel version is up-to-date 88 | - read about linux sandbox here: https://chromium.googlesource.com/chromium/src/+/master/docs/linux_suid_sandbox_development.md 89 | - try running without the sandbox (**Note: running without the sandbox is not recommended due to security reasons!**) 90 | ```js 91 | const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']}); 92 | ``` 93 | 94 | ## Running Puppeteer in Docker 95 | 96 | Using headless Chrome Linux to run Puppeteer in Docker container can be tricky. 97 | The bundled version Chromium that Puppeteer installs is missing the necessary 98 | shared library dependencies. 99 | 100 | To fix this, you'll need to install the latest version of Chrome dev in your 101 | Dockerfile: 102 | 103 | ``` 104 | FROM node:8-slim 105 | 106 | # Install latest chrome dev package. 107 | # Note: this installs the necessary libs to make the bundled version of Chromium that Pupppeteer 108 | # installs, work. 109 | RUN apt-get update && apt-get install -y wget --no-install-recommends \ 110 | && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ 111 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ 112 | && apt-get update \ 113 | && apt-get install -y google-chrome-unstable \ 114 | --no-install-recommends \ 115 | && rm -rf /var/lib/apt/lists/* \ 116 | && apt-get purge --auto-remove -y curl \ 117 | && rm -rf /src/*.deb 118 | 119 | # Uncomment to skip the chromium download when installing puppeteer. If you do, 120 | # you'll need to launch puppeteer with: 121 | # browser.launch({executablePath: 'google-chrome-unstable'}) 122 | # ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true 123 | 124 | # Install puppeteer so it's available in the container. 125 | RUN yarn add puppeteer 126 | 127 | # Add pptr user. 128 | RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \ 129 | && mkdir -p /home/pptruser/Downloads \ 130 | && chown -R pptruser:pptruser /home/pptruser \ 131 | && chown -R pptruser:pptruser /node_modules 132 | 133 | # Run user as non privileged. 134 | USER pptruser 135 | 136 | CMD ["google-chrome-unstable"] 137 | ``` 138 | 139 | Build the container: 140 | 141 | ```bash 142 | docker build -t puppeteer-chrome-linux . 143 | ``` 144 | 145 | Run the container by passing `node -e "` as the command: 146 | 147 | ```bash 148 | docker run -i --rm --name puppeteer-chrome puppeteer-chrome-linux node -e "`cat yourscript.js`" 149 | ``` 150 | 151 | There's a full example at https://github.com/ebidel/try-puppeteer that shows 152 | how to run this setup from a webserver running on App Engine Flex (Node). 153 | 154 | ## Running Puppeteer on Heroku 155 | 156 | Running Puppeteer on Heroku requires some additional dependencies that aren't included on the Linux box that Heroku spins up for you. To add the dependencies on deploy, add the Puppeteer Heroku buildpack to the list of buildpacks for your app under Settings > Buildpacks. 157 | 158 | The url for the buildpack is https://github.com/jontewks/puppeteer-heroku-buildpack 159 | 160 | When you click add buildpack, simply paste that url into the input, and click save. On the next deploy, your app will also install the dependencies that Puppeteer needs to run. 161 | 162 | If you need to render Chinese, Japanese, or Korean characters you may need to use a buildpack with additional font files like https://github.com/CoffeeAndCode/puppeteer-heroku-buildpack 163 | -------------------------------------------------------------------------------- /test/golden-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | const path = require('path'); 17 | const fs = require('fs'); 18 | const Diff = require('text-diff'); 19 | const mime = require('mime'); 20 | const PNG = require('pngjs').PNG; 21 | const pixelmatch = require('pixelmatch'); 22 | 23 | module.exports = { 24 | addMatchers: function(jasmine, goldenPath, outputPath) { 25 | jasmine.addMatchers({ 26 | toBeGolden: function(util, customEqualityTesters) { 27 | return { compare: compare.bind(null, goldenPath, outputPath) }; 28 | } 29 | }); 30 | }, 31 | }; 32 | 33 | const GoldenComparators = { 34 | 'image/png': compareImages, 35 | 'text/plain': compareText 36 | }; 37 | 38 | /** 39 | * @param {?Object} actualBuffer 40 | * @param {!Buffer} expectedBuffer 41 | * @return {?{diff: (!Object:undefined), errorMessage: (string|undefined)}} 42 | */ 43 | function compareImages(actualBuffer, expectedBuffer) { 44 | if (!actualBuffer || !(actualBuffer instanceof Buffer)) 45 | return { errorMessage: 'Actual result should be Buffer.' }; 46 | 47 | const actual = PNG.sync.read(actualBuffer); 48 | const expected = PNG.sync.read(expectedBuffer); 49 | if (expected.width !== actual.width || expected.height !== actual.height) { 50 | return { 51 | errorMessage: `Sizes differ: expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px. ` 52 | }; 53 | } 54 | const diff = new PNG({width: expected.width, height: expected.height}); 55 | const count = pixelmatch(expected.data, actual.data, diff.data, expected.width, expected.height, {threshold: 0.1}); 56 | return count > 0 ? { diff: PNG.sync.write(diff) } : null; 57 | } 58 | 59 | /** 60 | * @param {?Object} actual 61 | * @param {!Buffer} expectedBuffer 62 | * @return {?{diff: (!Object:undefined), errorMessage: (string|undefined)}} 63 | */ 64 | function compareText(actual, expectedBuffer) { 65 | if (typeof actual !== 'string') 66 | return { errorMessage: 'Actual result should be string' }; 67 | const expected = expectedBuffer.toString('utf-8'); 68 | if (expected === actual) 69 | return null; 70 | const diff = new Diff(); 71 | const result = diff.main(expected, actual); 72 | diff.cleanupSemantic(result); 73 | let html = diff.prettyHtml(result); 74 | const diffStylePath = path.join(__dirname, 'diffstyle.css'); 75 | html = `` + html; 76 | return { 77 | diff: html, 78 | diffExtension: '.html' 79 | }; 80 | } 81 | 82 | /** 83 | * @param {?Object} actual 84 | * @param {string} goldenName 85 | * @return {!{pass: boolean, message: (undefined|string)}} 86 | */ 87 | function compare(goldenPath, outputPath, actual, goldenName) { 88 | goldenPath = path.normalize(goldenPath); 89 | outputPath = path.normalize(outputPath); 90 | const expectedPath = path.join(goldenPath, goldenName); 91 | const actualPath = path.join(outputPath, goldenName); 92 | 93 | const messageSuffix = 'Output is saved in "' + path.basename(outputPath + '" directory'); 94 | 95 | if (!fs.existsSync(expectedPath)) { 96 | ensureOutputDir(); 97 | fs.writeFileSync(actualPath, actual); 98 | return { 99 | pass: false, 100 | message: goldenName + ' is missing in golden results. ' + messageSuffix 101 | }; 102 | } 103 | const expected = fs.readFileSync(expectedPath); 104 | const comparator = GoldenComparators[mime.lookup(goldenName)]; 105 | if (!comparator) { 106 | return { 107 | pass: false, 108 | message: 'Failed to find comparator with type ' + mime.lookup(goldenName) + ': ' + goldenName 109 | }; 110 | } 111 | const result = comparator(actual, expected); 112 | if (!result) 113 | return { pass: true }; 114 | ensureOutputDir(); 115 | if (goldenPath === outputPath) { 116 | fs.writeFileSync(addSuffix(actualPath, '-actual'), actual); 117 | } else { 118 | fs.writeFileSync(actualPath, actual); 119 | // Copy expected to the output/ folder for convenience. 120 | fs.writeFileSync(addSuffix(actualPath, '-expected'), expected); 121 | } 122 | if (result.diff) { 123 | const diffPath = addSuffix(actualPath, '-diff', result.diffExtension); 124 | fs.writeFileSync(diffPath, result.diff); 125 | } 126 | 127 | let message = goldenName + ' mismatch!'; 128 | if (result.errorMessage) 129 | message += ' ' + result.errorMessage; 130 | return { 131 | pass: false, 132 | message: message + ' ' + messageSuffix 133 | }; 134 | 135 | function ensureOutputDir() { 136 | if (!fs.existsSync(outputPath)) 137 | fs.mkdirSync(outputPath); 138 | } 139 | } 140 | 141 | /** 142 | * @param {string} filePath 143 | * @param {string} suffix 144 | * @param {string=} customExtension 145 | * @return {string} 146 | */ 147 | function addSuffix(filePath, suffix, customExtension) { 148 | const dirname = path.dirname(filePath); 149 | const ext = path.extname(filePath); 150 | const name = path.basename(filePath, ext); 151 | return path.join(dirname, name + suffix + (customExtension || ext)); 152 | } 153 | -------------------------------------------------------------------------------- /lib/ElementHandle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | const path = require('path'); 17 | const {JSHandle} = require('./ExecutionContext'); 18 | const {helper} = require('./helper'); 19 | 20 | class ElementHandle extends JSHandle { 21 | /** 22 | * @param {!Puppeteer.ExecutionContext} context 23 | * @param {!Puppeteer.Session} client 24 | * @param {!Object} remoteObject 25 | * @param {!Puppeteer.Page} page 26 | */ 27 | constructor(context, client, remoteObject, page) { 28 | super(context, client, remoteObject); 29 | this._client = client; 30 | this._remoteObject = remoteObject; 31 | this._page = page; 32 | this._disposed = false; 33 | } 34 | 35 | /** 36 | * @override 37 | * @return {?ElementHandle} 38 | */ 39 | asElement() { 40 | return this; 41 | } 42 | 43 | async _scrollIntoViewIfNeeded() { 44 | const error = await this.executionContext().evaluate(element => { 45 | if (!element.ownerDocument.contains(element)) 46 | return 'Node is detached from document'; 47 | if (element.nodeType !== Node.ELEMENT_NODE) 48 | return 'Node is not of type HTMLElement'; 49 | element.scrollIntoViewIfNeeded(); 50 | return false; 51 | }, this); 52 | if (error) 53 | throw new Error(error); 54 | } 55 | 56 | /** 57 | * @return {!Promise<{x: number, y: number}>} 58 | */ 59 | async _visibleCenter() { 60 | await this._scrollIntoViewIfNeeded(); 61 | const box = await this.boundingBox(); 62 | return { 63 | x: box.x + box.width / 2, 64 | y: box.y + box.height / 2 65 | }; 66 | } 67 | 68 | async hover() { 69 | const {x, y} = await this._visibleCenter(); 70 | await this._page.mouse.move(x, y); 71 | } 72 | 73 | /** 74 | * @param {!Object=} options 75 | */ 76 | async click(options) { 77 | const {x, y} = await this._visibleCenter(); 78 | await this._page.mouse.click(x, y, options); 79 | } 80 | 81 | /** 82 | * @param {!Array} filePaths 83 | * @return {!Promise} 84 | */ 85 | async uploadFile(...filePaths) { 86 | const files = filePaths.map(filePath => path.resolve(filePath)); 87 | const objectId = this._remoteObject.objectId; 88 | return this._client.send('DOM.setFileInputFiles', { objectId, files }); 89 | } 90 | 91 | async tap() { 92 | const {x, y} = await this._visibleCenter(); 93 | await this._page.touchscreen.tap(x, y); 94 | } 95 | 96 | async focus() { 97 | await this.executionContext().evaluate(element => element.focus(), this); 98 | } 99 | 100 | /** 101 | * @param {string} text 102 | * @param {{delay: (number|undefined)}=} options 103 | */ 104 | async type(text, options) { 105 | await this.focus(); 106 | await this._page.keyboard.type(text, options); 107 | } 108 | 109 | /** 110 | * @param {string} key 111 | * @param {!Object=} options 112 | */ 113 | async press(key, options) { 114 | await this.focus(); 115 | await this._page.keyboard.press(key, options); 116 | } 117 | 118 | /** 119 | * @return {!Promise<{x: number, y: number, width: number, height: number}>} 120 | */ 121 | async boundingBox() { 122 | const {model} = await this._client.send('DOM.getBoxModel', { 123 | objectId: this._remoteObject.objectId 124 | }); 125 | if (!model) 126 | throw new Error('Node is detached from document'); 127 | 128 | const quad = model.border; 129 | const x = Math.min(quad[0], quad[2], quad[4], quad[6]); 130 | const y = Math.min(quad[1], quad[3], quad[5], quad[7]); 131 | const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x; 132 | const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y; 133 | 134 | return {x, y, width, height}; 135 | } 136 | 137 | /** 138 | * 139 | * @param {!Object=} options 140 | * @returns {!Promise} 141 | */ 142 | async screenshot(options = {}) { 143 | await this._scrollIntoViewIfNeeded(); 144 | const boundingBox = await this.boundingBox(); 145 | 146 | return await this._page.screenshot(Object.assign({}, { 147 | clip: boundingBox 148 | }, options)); 149 | } 150 | 151 | /** 152 | * @param {string} selector 153 | * @return {!Promise} 154 | */ 155 | async $(selector) { 156 | const handle = await this.executionContext().evaluateHandle( 157 | (element, selector) => element.querySelector(selector), 158 | this, selector 159 | ); 160 | const element = handle.asElement(); 161 | if (element) 162 | return element; 163 | await handle.dispose(); 164 | return null; 165 | } 166 | 167 | /** 168 | * @param {string} selector 169 | * @return {!Promise>} 170 | */ 171 | async $$(selector) { 172 | const arrayHandle = await this.executionContext().evaluateHandle( 173 | (element, selector) => element.querySelectorAll(selector), 174 | this, selector 175 | ); 176 | const properties = await arrayHandle.getProperties(); 177 | await arrayHandle.dispose(); 178 | const result = []; 179 | for (const property of properties.values()) { 180 | const elementHandle = property.asElement(); 181 | if (elementHandle) 182 | result.push(elementHandle); 183 | } 184 | return result; 185 | } 186 | } 187 | 188 | module.exports = ElementHandle; 189 | helper.tracePublicAPI(ElementHandle); 190 | -------------------------------------------------------------------------------- /utils/check_availability.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright 2017 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | const Downloader = require('./ChromiumDownloader'); 19 | const https = require('https'); 20 | const OMAHA_PROXY = 'https://omahaproxy.appspot.com/all.json'; 21 | 22 | const colors = { 23 | reset: '\x1b[0m', 24 | red: '\x1b[31m', 25 | green: '\x1b[32m', 26 | yellow: '\x1b[33m' 27 | }; 28 | 29 | class Table { 30 | /** 31 | * @param {!Array} columnWidths 32 | */ 33 | constructor(columnWidths) { 34 | this.widths = columnWidths; 35 | } 36 | 37 | /** 38 | * @param {!Array} values 39 | */ 40 | drawRow(values) { 41 | console.assert(values.length === this.widths.length); 42 | let row = ''; 43 | for (let i = 0; i < values.length; ++i) 44 | row += padCenter(values[i], this.widths[i]); 45 | console.log(row); 46 | } 47 | } 48 | 49 | if (process.argv.length === 2) { 50 | checkOmahaProxyAvailability(); 51 | return; 52 | } 53 | if (process.argv.length !== 4) { 54 | console.log(` 55 | Usage: node check_revisions.js [fromRevision] [toRevision] 56 | 57 | This script checks availability of different prebuild chromium revisions. 58 | Running command without arguments will check against omahaproxy revisions.`); 59 | return; 60 | } 61 | 62 | const fromRevision = parseInt(process.argv[2], 10); 63 | const toRevision = parseInt(process.argv[3], 10); 64 | checkRangeAvailability(fromRevision, toRevision); 65 | 66 | async function checkOmahaProxyAvailability() { 67 | console.log('Fetching revisions from ' + OMAHA_PROXY); 68 | const platforms = await loadJSON(OMAHA_PROXY); 69 | if (!platforms) { 70 | console.error('ERROR: failed to fetch chromium revisions from omahaproxy.'); 71 | return; 72 | } 73 | const table = new Table([27, 7, 7, 7, 7]); 74 | table.drawRow([''].concat(Downloader.supportedPlatforms())); 75 | for (const platform of platforms) { 76 | // Trust only to the main platforms. 77 | if (platform.os !== 'mac' && platform.os !== 'win' && platform.os !== 'win64' && platform.os !== 'linux') 78 | continue; 79 | const osName = platform.os === 'win' ? 'win32' : platform.os; 80 | for (const version of platform.versions) { 81 | if (version.channel !== 'dev' && version.channel !== 'beta' && version.channel !== 'canary' && version.channel !== 'stable') 82 | continue; 83 | const revisionName = padLeft('[' + osName + ' ' + version.channel + ']', 15); 84 | const revision = parseInt(version.branch_base_position, 10); 85 | await checkAndDrawRevisionAvailability(table, revisionName, revision); 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * @param {number} fromRevision 92 | * @param {number} toRevision 93 | */ 94 | async function checkRangeAvailability(fromRevision, toRevision) { 95 | const table = new Table([10, 7, 7, 7, 7]); 96 | table.drawRow([''].concat(Downloader.supportedPlatforms())); 97 | const inc = fromRevision < toRevision ? 1 : -1; 98 | for (let revision = fromRevision; revision !== toRevision; revision += inc) 99 | await checkAndDrawRevisionAvailability(table, '', revision); 100 | } 101 | 102 | /** 103 | * @param {!Table} table 104 | * @param {string} name 105 | * @param {number} revision 106 | */ 107 | async function checkAndDrawRevisionAvailability(table, name, revision) { 108 | const promises = []; 109 | for (const platform of Downloader.supportedPlatforms()) 110 | promises.push(Downloader.canDownloadRevision(platform, revision)); 111 | const availability = await Promise.all(promises); 112 | const allAvailable = availability.every(e => !!e); 113 | const values = [name + ' ' + (allAvailable ? colors.green + revision + colors.reset : revision)]; 114 | for (let i = 0; i < availability.length; ++i) { 115 | const decoration = availability[i] ? '+' : '-'; 116 | const color = availability[i] ? colors.green : colors.red; 117 | values.push(color + decoration + colors.reset); 118 | } 119 | table.drawRow(values); 120 | } 121 | 122 | /** 123 | * @param {string} url 124 | * @return {!Promise} 125 | */ 126 | function loadJSON(url) { 127 | let resolve; 128 | const promise = new Promise(x => resolve = x); 129 | https.get(url, response => { 130 | if (response.statusCode !== 200) { 131 | resolve(null); 132 | return; 133 | } 134 | let body = ''; 135 | response.on('data', function(chunk){ 136 | body += chunk; 137 | }); 138 | response.on('end', function(){ 139 | const json = JSON.parse(body); 140 | resolve(json); 141 | }); 142 | }).on('error', function(e){ 143 | console.error('Error fetching json: ' + e); 144 | resolve(null); 145 | }); 146 | return promise; 147 | } 148 | 149 | /** 150 | * @param {number} size 151 | * @return {string} 152 | */ 153 | function spaceString(size) { 154 | return new Array(size).fill(' ').join(''); 155 | } 156 | 157 | /** 158 | * @param {string} text 159 | * @return {string} 160 | */ 161 | function filterOutColors(text) { 162 | for (const colorName in colors) { 163 | const color = colors[colorName]; 164 | text = text.replace(color, ''); 165 | } 166 | return text; 167 | } 168 | 169 | /** 170 | * @param {string} text 171 | * @param {number} length 172 | * @return {string} 173 | */ 174 | function padLeft(text, length) { 175 | const printableCharacters = filterOutColors(text); 176 | return printableCharacters.length >= length ? text : spaceString(length - text.length) + text; 177 | } 178 | 179 | /** 180 | * @param {string} text 181 | * @param {number} length 182 | * @return {string} 183 | */ 184 | function padCenter(text, length) { 185 | const printableCharacters = filterOutColors(text); 186 | if (printableCharacters.length >= length) 187 | return text; 188 | const left = Math.floor((length - printableCharacters.length) / 2); 189 | const right = Math.ceil((length - printableCharacters.length) / 2); 190 | return spaceString(left) + text + spaceString(right); 191 | } 192 | -------------------------------------------------------------------------------- /test/server/SimpleServer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 http = require('http'); 18 | const https = require('https'); 19 | const url = require('url'); 20 | const fs = require('fs'); 21 | const path = require('path'); 22 | const mime = require('mime'); 23 | const WebSocketServer = require('ws').Server; 24 | 25 | const fulfillSymbol = Symbol('fullfill callback'); 26 | const rejectSymbol = Symbol('reject callback'); 27 | 28 | class SimpleServer { 29 | /** 30 | * @param {string} dirPath 31 | * @param {number} port 32 | * @return {!SimpleServer} 33 | */ 34 | static async create(dirPath, port) { 35 | const server = new SimpleServer(dirPath, port); 36 | await new Promise(x => server._server.once('listening', x)); 37 | return server; 38 | } 39 | 40 | /** 41 | * @param {string} dirPath 42 | * @param {number} port 43 | * @return {!SimpleServer} 44 | */ 45 | static async createHTTPS(dirPath, port) { 46 | const server = new SimpleServer(dirPath, port, { 47 | key: fs.readFileSync(path.join(__dirname, 'key.pem')), 48 | cert: fs.readFileSync(path.join(__dirname, 'cert.pem')), 49 | passphrase: 'aaaa', 50 | }); 51 | await new Promise(x => server._server.once('listening', x)); 52 | return server; 53 | } 54 | 55 | /** 56 | * @param {string} dirPath 57 | * @param {number} port 58 | * @param {!Object=} sslOptions 59 | */ 60 | constructor(dirPath, port, sslOptions) { 61 | if (sslOptions) 62 | this._server = https.createServer(sslOptions, this._onRequest.bind(this)); 63 | else 64 | this._server = http.createServer(this._onRequest.bind(this)); 65 | this._server.on('connection', socket => this._onSocket(socket)); 66 | this._wsServer = new WebSocketServer({server: this._server}); 67 | this._wsServer.on('connection', this._onWebSocketConnection.bind(this)); 68 | this._server.listen(port); 69 | this._dirPath = dirPath; 70 | 71 | /** @type {!Set} */ 72 | this._sockets = new Set(); 73 | 74 | /** @type {!Map} */ 75 | this._routes = new Map(); 76 | /** @type {!Map} */ 77 | this._auths = new Map(); 78 | /** @type {!Map} */ 79 | this._requestSubscribers = new Map(); 80 | } 81 | 82 | _onSocket(socket) { 83 | this._sockets.add(socket); 84 | // ECONNRESET is a legit error given 85 | // that tab closing simply kills process. 86 | socket.on('error', error => { 87 | if (error.code !== 'ECONNRESET') 88 | throw error; 89 | }); 90 | socket.once('close', () => this._sockets.delete(socket)); 91 | } 92 | 93 | /** 94 | * @param {string} path 95 | * @param {string} username 96 | * @param {string} password 97 | */ 98 | setAuth(path, username, password) { 99 | this._auths.set(path, {username, password}); 100 | } 101 | 102 | async stop() { 103 | this.reset(); 104 | for (const socket of this._sockets) 105 | socket.destroy(); 106 | this._sockets.clear(); 107 | await new Promise(x => this._server.close(x)); 108 | } 109 | 110 | /** 111 | * @param {string} path 112 | * @param {function(!IncomingMessage, !ServerResponse)} handler 113 | */ 114 | setRoute(path, handler) { 115 | this._routes.set(path, handler); 116 | } 117 | 118 | /** 119 | * @param {string} fromPath 120 | * @param {string} toPath 121 | */ 122 | setRedirect(from, to) { 123 | this.setRoute(from, (req, res) => { 124 | res.writeHead(302, { location: to }); 125 | res.end(); 126 | }); 127 | } 128 | 129 | /** 130 | * @param {string} path 131 | * @return {!Promise} 132 | */ 133 | waitForRequest(path) { 134 | let promise = this._requestSubscribers.get(path); 135 | if (promise) 136 | return promise; 137 | let fulfill, reject; 138 | promise = new Promise((f, r) => { 139 | fulfill = f; 140 | reject = r; 141 | }); 142 | promise[fulfillSymbol] = fulfill; 143 | promise[rejectSymbol] = reject; 144 | this._requestSubscribers.set(path, promise); 145 | return promise; 146 | } 147 | 148 | reset() { 149 | this._routes.clear(); 150 | this._auths.clear(); 151 | const error = new Error('Static Server has been reset'); 152 | for (const subscriber of this._requestSubscribers.values()) 153 | subscriber[rejectSymbol].call(null, error); 154 | this._requestSubscribers.clear(); 155 | } 156 | 157 | _onRequest(request, response) { 158 | request.on('error', error => { 159 | if (error.code === 'ECONNRESET') 160 | response.end(); 161 | else 162 | throw error; 163 | }); 164 | const pathName = url.parse(request.url).path; 165 | if (this._auths.has(pathName)) { 166 | const auth = this._auths.get(pathName); 167 | const credentials = new Buffer((request.headers.authorization || '').split(' ')[1] || '', 'base64').toString(); 168 | if (credentials !== `${auth.username}:${auth.password}`) { 169 | response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Secure Area"' }); 170 | response.end('HTTP Error 401 Unauthorized: Access is denied'); 171 | return; 172 | } 173 | } 174 | // Notify request subscriber. 175 | if (this._requestSubscribers.has(pathName)) 176 | this._requestSubscribers.get(pathName)[fulfillSymbol].call(null, request); 177 | const handler = this._routes.get(pathName); 178 | if (handler) 179 | handler.call(null, request, response); 180 | else 181 | this.defaultHandler(request, response); 182 | } 183 | 184 | /** 185 | * @param {!IncomingMessage} request 186 | * @param {!ServerResponse} response 187 | */ 188 | defaultHandler(request, response) { 189 | let pathName = url.parse(request.url).path; 190 | if (pathName === '/') 191 | pathName = '/index.html'; 192 | pathName = path.join(this._dirPath, pathName.substring(1)); 193 | 194 | fs.readFile(pathName, function(err, data) { 195 | if (err) { 196 | response.statusCode = 404; 197 | response.end(`File not found: ${pathName}`); 198 | return; 199 | } 200 | response.setHeader('Content-Type', mime.lookup(pathName)); 201 | response.end(data); 202 | }); 203 | } 204 | 205 | _onWebSocketConnection(connection) { 206 | connection.send('opened'); 207 | } 208 | } 209 | 210 | module.exports = SimpleServer; 211 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/MDBuilder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 Documentation = require('./Documentation'); 18 | const commonmark = require('commonmark'); 19 | 20 | class MDOutline { 21 | /** 22 | * @param {!Page} page 23 | * @param {string} text 24 | * @return {!MDOutline} 25 | */ 26 | static async create(page, text) { 27 | // Render markdown as HTML. 28 | const reader = new commonmark.Parser(); 29 | const parsed = reader.parse(text); 30 | const writer = new commonmark.HtmlRenderer(); 31 | const html = writer.render(parsed); 32 | 33 | // Extract headings. 34 | await page.setContent(html); 35 | const {classes, errors} = await page.evaluate(() => { 36 | const classes = []; 37 | let currentClass = {}; 38 | let member = {}; 39 | const errors = []; 40 | for (const element of document.body.querySelectorAll('h3, h4, h4 + ul > li')) { 41 | if (element.matches('h3')) { 42 | currentClass = { 43 | name: element.textContent, 44 | members: [], 45 | }; 46 | classes.push(currentClass); 47 | } else if (element.matches('h4')) { 48 | member = { 49 | name: element.textContent, 50 | args: [], 51 | hasReturn: false 52 | }; 53 | currentClass.members.push(member); 54 | } else if (element.matches('li') && element.firstChild.matches && element.firstChild.matches('code')) { 55 | member.args.push(element.firstChild.textContent); 56 | } else if (element.matches('li') && element.firstChild.nodeType === Element.TEXT_NODE && element.firstChild.textContent.toLowerCase().startsWith('retur')) { 57 | member.hasReturn = true; 58 | const expectedText = 'returns: '; 59 | let actualText = element.firstChild.textContent; 60 | let angleIndex = actualText.indexOf('<'); 61 | let spaceIndex = actualText.indexOf(' '); 62 | angleIndex = angleIndex === -1 ? actualText.length : angleIndex; 63 | spaceIndex = spaceIndex === -1 ? actualText.length : spaceIndex + 1; 64 | actualText = actualText.substring(0, Math.min(angleIndex, spaceIndex)); 65 | if (actualText !== expectedText) 66 | errors.push(`${member.name} has mistyped 'return' type declaration: expected exactly '${expectedText}', found '${actualText}'.`); 67 | } 68 | } 69 | return {classes, errors}; 70 | }); 71 | return new MDOutline(classes, errors); 72 | } 73 | 74 | constructor(classes, errors) { 75 | this.classes = []; 76 | this.errors = errors; 77 | const classHeading = /^class: (\w+)$/; 78 | const constructorRegex = /^new (\w+)\((.*)\)$/; 79 | const methodRegex = /^(\w+)\.([\w$]+)\((.*)\)$/; 80 | const propertyRegex = /^(\w+)\.(\w+)$/; 81 | const eventRegex = /^event: '(\w+)'$/; 82 | let currentClassName = null; 83 | let currentClassMembers = []; 84 | for (const cls of classes) { 85 | const match = cls.name.match(classHeading); 86 | if (!match) 87 | continue; 88 | currentClassName = match[1]; 89 | for (const member of cls.members) { 90 | if (constructorRegex.test(member.name)) { 91 | const match = member.name.match(constructorRegex); 92 | handleMethod.call(this, member, match[1], 'constructor', match[2]); 93 | } else if (methodRegex.test(member.name)) { 94 | const match = member.name.match(methodRegex); 95 | handleMethod.call(this, member, match[1], match[2], match[3]); 96 | } else if (propertyRegex.test(member.name)) { 97 | const match = member.name.match(propertyRegex); 98 | handleProperty.call(this, member, match[1], match[2]); 99 | } else if (eventRegex.test(member.name)) { 100 | const match = member.name.match(eventRegex); 101 | handleEvent.call(this, member, match[1]); 102 | } 103 | } 104 | flushClassIfNeeded.call(this); 105 | } 106 | 107 | function handleMethod(member, className, methodName, parameters) { 108 | if (!currentClassName || !className || !methodName || className.toLowerCase() !== currentClassName.toLowerCase()) { 109 | this.errors.push(`Failed to process header as method: ${member.name}`); 110 | return; 111 | } 112 | parameters = parameters.trim().replace(/[\[\]]/g, ''); 113 | if (parameters !== member.args.join(', ')) 114 | this.errors.push(`Heading arguments for "${member.name}" do not match described ones, i.e. "${parameters}" != "${member.args.join(', ')}"`); 115 | const args = member.args.map(arg => new Documentation.Argument(arg)); 116 | const method = Documentation.Member.createMethod(methodName, args, member.hasReturn, false); 117 | currentClassMembers.push(method); 118 | } 119 | 120 | function handleProperty(member, className, propertyName) { 121 | if (!currentClassName || !className || !propertyName || className.toLowerCase() !== currentClassName.toLowerCase()) { 122 | this.errors.push(`Failed to process header as property: ${member.name}`); 123 | return; 124 | } 125 | currentClassMembers.push(Documentation.Member.createProperty(propertyName)); 126 | } 127 | 128 | function handleEvent(member, eventName) { 129 | if (!currentClassName || !eventName) { 130 | this.errors.push(`Failed to process header as event: ${member.name}`); 131 | return; 132 | } 133 | currentClassMembers.push(Documentation.Member.createEvent(eventName)); 134 | } 135 | 136 | function flushClassIfNeeded() { 137 | if (currentClassName === null) 138 | return; 139 | this.classes.push(new Documentation.Class(currentClassName, currentClassMembers)); 140 | currentClassName = null; 141 | currentClassMembers = []; 142 | } 143 | } 144 | } 145 | 146 | /** 147 | * @param {!Page} page 148 | * @param {!Array} sources 149 | * @return {!Promise<{documentation: !Documentation, errors: !Array}>} 150 | */ 151 | module.exports = async function(page, sources) { 152 | const classes = []; 153 | const errors = []; 154 | for (const source of sources) { 155 | const outline = await MDOutline.create(page, source.text()); 156 | classes.push(...outline.classes); 157 | errors.push(...outline.errors); 158 | } 159 | const documentation = new Documentation(classes); 160 | return { documentation, errors }; 161 | }; 162 | 163 | -------------------------------------------------------------------------------- /lib/Browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 {helper} = require('./helper'); 18 | const Page = require('./Page'); 19 | const EventEmitter = require('events'); 20 | 21 | class Browser extends EventEmitter { 22 | /** 23 | * @param {!Puppeteer.Connection} connection 24 | * @param {!Object=} options 25 | * @param {(function():Promise)=} closeCallback 26 | */ 27 | constructor(connection, options = {}, closeCallback) { 28 | super(); 29 | this._ignoreHTTPSErrors = !!options.ignoreHTTPSErrors; 30 | this._appMode = !!options.appMode; 31 | this._screenshotTaskQueue = new TaskQueue(); 32 | this._connection = connection; 33 | this._closeCallback = closeCallback || new Function(); 34 | /** @type {Map} */ 35 | this._targets = new Map(); 36 | this._connection.on('Target.targetCreated', this._targetCreated.bind(this)); 37 | this._connection.on('Target.targetDestroyed', this._targetDestroyed.bind(this)); 38 | this._connection.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this)); 39 | } 40 | 41 | /** 42 | * @param {!Puppeteer.Connection} connection 43 | * @param {boolean} ignoreHTTPSErrors 44 | * @param {function()=} closeCallback 45 | */ 46 | static async create(connection, ignoreHTTPSErrors, closeCallback) { 47 | const browser = new Browser(connection, ignoreHTTPSErrors, closeCallback); 48 | await connection.send('Target.setDiscoverTargets', {discover: true}); 49 | return browser; 50 | } 51 | 52 | /** 53 | * @param {{targetInfo: !Target.TargetInfo}} event 54 | */ 55 | async _targetCreated(event) { 56 | const target = new Target(this, event.targetInfo); 57 | console.assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated'); 58 | this._targets.set(event.targetInfo.targetId, target); 59 | 60 | if (await target._initializedPromise) 61 | this.emit(Browser.Events.TargetCreated, target); 62 | } 63 | 64 | /** 65 | * @param {{targetId: string}} event 66 | */ 67 | async _targetDestroyed(event) { 68 | const target = this._targets.get(event.targetId); 69 | target._initializedCallback(false); 70 | this._targets.delete(event.targetId); 71 | if (await target._initializedPromise) 72 | this.emit(Browser.Events.TargetDestroyed, target); 73 | } 74 | 75 | /** 76 | * @param {{targetInfo: !Target.TargetInfo}} event 77 | */ 78 | _targetInfoChanged(event) { 79 | const target = this._targets.get(event.targetInfo.targetId); 80 | console.assert(target, 'target should exist before targetInfoChanged'); 81 | target._targetInfoChanged(event.targetInfo); 82 | } 83 | 84 | /** 85 | * @return {string} 86 | */ 87 | wsEndpoint() { 88 | return this._connection.url(); 89 | } 90 | 91 | /** 92 | * @return {!Promise} 93 | */ 94 | async newPage() { 95 | const {targetId} = await this._connection.send('Target.createTarget', {url: 'about:blank'}); 96 | const target = await this._targets.get(targetId); 97 | console.assert(await target._initializedPromise, 'Failed to create target for page'); 98 | const page = await target.page(); 99 | return page; 100 | } 101 | 102 | /** 103 | * @return {!Array} 104 | */ 105 | targets() { 106 | return Array.from(this._targets.values()).filter(target => target._isInitialized); 107 | } 108 | 109 | /** 110 | * @return {!Promise>} 111 | */ 112 | async pages() { 113 | const pages = await Promise.all(this.targets().map(target => target.page())); 114 | return pages.filter(page => !!page); 115 | } 116 | 117 | /** 118 | * @return {!Promise} 119 | */ 120 | async version() { 121 | const version = await this._connection.send('Browser.getVersion'); 122 | return version.product; 123 | } 124 | 125 | async close() { 126 | await this._closeCallback.call(null); 127 | this.disconnect(); 128 | } 129 | 130 | disconnect() { 131 | this._connection.dispose(); 132 | } 133 | } 134 | 135 | /** @enum {string} */ 136 | Browser.Events = { 137 | TargetCreated: 'targetcreated', 138 | TargetDestroyed: 'targetdestroyed', 139 | TargetChanged: 'targetchanged' 140 | }; 141 | 142 | helper.tracePublicAPI(Browser); 143 | 144 | class TaskQueue { 145 | constructor() { 146 | this._chain = Promise.resolve(); 147 | } 148 | 149 | /** 150 | * @param {function()} task 151 | * @return {!Promise} 152 | */ 153 | postTask(task) { 154 | const result = this._chain.then(task); 155 | this._chain = result.catch(() => {}); 156 | return result; 157 | } 158 | } 159 | 160 | class Target { 161 | /** 162 | * @param {!Browser} browser 163 | * @param {!Target.TargetInfo} targetInfo 164 | */ 165 | constructor(browser, targetInfo) { 166 | this._browser = browser; 167 | this._targetInfo = targetInfo; 168 | /** @type {?Promise} */ 169 | this._pagePromise = null; 170 | this._initializedPromise = new Promise(fulfill => this._initializedCallback = fulfill); 171 | this._isInitialized = this._targetInfo.type !== 'page' || this._targetInfo.url !== ''; 172 | if (this._isInitialized) 173 | this._initializedCallback(true); 174 | } 175 | 176 | /** 177 | * @return {!Promise} 178 | */ 179 | async page() { 180 | if (this._targetInfo.type === 'page' && !this._pagePromise) { 181 | this._pagePromise = this._browser._connection.createSession(this._targetInfo.targetId) 182 | .then(client => Page.create(client, this._browser._ignoreHTTPSErrors, this._browser._appMode, this._browser._screenshotTaskQueue)); 183 | } 184 | return this._pagePromise; 185 | } 186 | 187 | /** 188 | * @return {string} 189 | */ 190 | url() { 191 | return this._targetInfo.url; 192 | } 193 | 194 | /** 195 | * @return {"page"|"service_worker"|"other"} 196 | */ 197 | type() { 198 | const type = this._targetInfo.type; 199 | if (type === 'page' || type === 'service_worker') 200 | return type; 201 | return 'other'; 202 | } 203 | 204 | /** 205 | * @param {!Target.TargetInfo} targetInfo 206 | */ 207 | _targetInfoChanged(targetInfo) { 208 | const previousURL = this._targetInfo.url; 209 | this._targetInfo = targetInfo; 210 | 211 | if (!this._isInitialized && (this._targetInfo.type !== 'page' || this._targetInfo.url !== '')) { 212 | this._isInitialized = true; 213 | this._initializedCallback(true); 214 | return; 215 | } 216 | 217 | if (previousURL !== targetInfo.url) 218 | this._browser.emit(Browser.Events.TargetChanged, this); 219 | } 220 | } 221 | helper.tracePublicAPI(Target); 222 | 223 | /** 224 | * @typedef {Object} Target.TargetInfo 225 | * @property {string} type 226 | * @property {string} targetId 227 | * @property {string} title 228 | * @property {string} url 229 | * @property {boolean} attached 230 | */ 231 | 232 | module.exports = { Browser, TaskQueue }; 233 | -------------------------------------------------------------------------------- /lib/Connection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 | const debugProtocol = require('debug')('puppeteer:protocol'); 17 | const debugSession = require('debug')('puppeteer:session'); 18 | 19 | const EventEmitter = require('events'); 20 | const WebSocket = require('ws'); 21 | 22 | class Connection extends EventEmitter { 23 | /** 24 | * @param {string} url 25 | * @param {number=} delay 26 | * @return {!Promise} 27 | */ 28 | static async create(url, delay = 0) { 29 | return new Promise((resolve, reject) => { 30 | const ws = new WebSocket(url, { perMessageDeflate: false }); 31 | ws.on('open', () => resolve(new Connection(url, ws, delay))); 32 | ws.on('error', reject); 33 | }); 34 | } 35 | 36 | /** 37 | * @param {string} url 38 | * @param {!WebSocket} ws 39 | * @param {number=} delay 40 | */ 41 | constructor(url, ws, delay = 0) { 42 | super(); 43 | this._url = url; 44 | this._lastId = 0; 45 | /** @type {!Map}*/ 46 | this._callbacks = new Map(); 47 | this._delay = delay; 48 | 49 | this._ws = ws; 50 | this._ws.on('message', this._onMessage.bind(this)); 51 | this._ws.on('close', this._onClose.bind(this)); 52 | /** @type {!Map}*/ 53 | this._sessions = new Map(); 54 | } 55 | 56 | /** 57 | * @return {string} 58 | */ 59 | url() { 60 | return this._url; 61 | } 62 | 63 | /** 64 | * @param {string} method 65 | * @param {!Object=} params 66 | * @return {!Promise} 67 | */ 68 | send(method, params = {}) { 69 | const id = ++this._lastId; 70 | const message = JSON.stringify({id, method, params}); 71 | debugProtocol('SEND ► ' + message); 72 | this._ws.send(message); 73 | return new Promise((resolve, reject) => { 74 | this._callbacks.set(id, {resolve, reject, method}); 75 | }); 76 | } 77 | 78 | /** 79 | * @param {string} message 80 | */ 81 | async _onMessage(message) { 82 | if (this._delay) 83 | await new Promise(f => setTimeout(f, this._delay)); 84 | debugProtocol('◀ RECV ' + message); 85 | const object = JSON.parse(message); 86 | if (object.id && this._callbacks.has(object.id)) { 87 | const callback = this._callbacks.get(object.id); 88 | this._callbacks.delete(object.id); 89 | if (object.error) 90 | callback.reject(new Error(`Protocol error (${callback.method}): ${object.error.message} ${object.error.data}`)); 91 | else 92 | callback.resolve(object.result); 93 | } else { 94 | console.assert(!object.id); 95 | if (object.method === 'Target.receivedMessageFromTarget') { 96 | const session = this._sessions.get(object.params.sessionId); 97 | if (session) 98 | session._onMessage(object.params.message); 99 | } else if (object.method === 'Target.detachedFromTarget') { 100 | const session = this._sessions.get(object.params.sessionId); 101 | if (session) 102 | session._onClosed(); 103 | this._sessions.delete(object.params.sessionId); 104 | } else { 105 | this.emit(object.method, object.params); 106 | } 107 | } 108 | } 109 | 110 | _onClose() { 111 | this._ws.removeAllListeners(); 112 | for (const callback of this._callbacks.values()) 113 | callback.reject(new Error(`Protocol error (${callback.method}): Target closed.`)); 114 | this._callbacks.clear(); 115 | for (const session of this._sessions.values()) 116 | session._onClosed(); 117 | this._sessions.clear(); 118 | } 119 | 120 | dispose() { 121 | this._onClose(); 122 | this._ws.close(); 123 | } 124 | 125 | /** 126 | * @param {string} targetId 127 | * @return {!Promise} 128 | */ 129 | async createSession(targetId) { 130 | const {sessionId} = await this.send('Target.attachToTarget', {targetId}); 131 | const session = new Session(this, targetId, sessionId); 132 | this._sessions.set(sessionId, session); 133 | return session; 134 | } 135 | } 136 | 137 | class Session extends EventEmitter { 138 | /** 139 | * @param {!Connection} connection 140 | * @param {string} targetId 141 | * @param {string} sessionId 142 | */ 143 | constructor(connection, targetId, sessionId) { 144 | super(); 145 | this._lastId = 0; 146 | /** @type {!Map}*/ 147 | this._callbacks = new Map(); 148 | this._connection = connection; 149 | this._targetId = targetId; 150 | this._sessionId = sessionId; 151 | } 152 | 153 | /** 154 | * @return {string} 155 | */ 156 | targetId() { 157 | return this._targetId; 158 | } 159 | 160 | /** 161 | * @param {string} method 162 | * @param {!Object=} params 163 | * @return {!Promise} 164 | */ 165 | send(method, params = {}) { 166 | if (!this._connection) 167 | return Promise.reject(new Error(`Protocol error (${method}): Session closed. Most likely the page has been closed.`)); 168 | const id = ++this._lastId; 169 | const message = JSON.stringify({id, method, params}); 170 | debugSession('SEND ► ' + message); 171 | this._connection.send('Target.sendMessageToTarget', {sessionId: this._sessionId, message}).catch(e => { 172 | // The response from target might have been already dispatched. 173 | if (!this._callbacks.has(id)) 174 | return; 175 | const callback = this._callbacks.get(id); 176 | this._callbacks.delete(id); 177 | callback.reject(e); 178 | }); 179 | return new Promise((resolve, reject) => { 180 | this._callbacks.set(id, {resolve, reject, method}); 181 | }); 182 | } 183 | 184 | /** 185 | * @param {string} message 186 | */ 187 | _onMessage(message) { 188 | debugSession('◀ RECV ' + message); 189 | const object = JSON.parse(message); 190 | if (object.id && this._callbacks.has(object.id)) { 191 | const callback = this._callbacks.get(object.id); 192 | this._callbacks.delete(object.id); 193 | if (object.error) 194 | callback.reject(new Error(`Protocol error (${callback.method}): ${object.error.message} ${object.error.data}`)); 195 | else 196 | callback.resolve(object.result); 197 | } else { 198 | console.assert(!object.id); 199 | this.emit(object.method, object.params); 200 | } 201 | } 202 | 203 | async dispose() { 204 | console.assert(!!this._connection, 'Protocol error: Connection closed. Most likely the page has been closed.'); 205 | await this._connection.send('Target.closeTarget', {targetId: this._targetId}); 206 | } 207 | 208 | _onClosed() { 209 | for (const callback of this._callbacks.values()) 210 | callback.reject(new Error(`Protocol error (${callback.method}): Target closed.`)); 211 | this._callbacks.clear(); 212 | this._connection = null; 213 | } 214 | } 215 | 216 | module.exports = {Connection, Session}; 217 | -------------------------------------------------------------------------------- /lib/ExecutionContext.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 {helper} = require('./helper'); 18 | 19 | class ExecutionContext { 20 | /** 21 | * @param {!Puppeteer.Session} client 22 | * @param {string} contextId 23 | * @param {function(*):!JSHandle} objectHandleFactory 24 | */ 25 | constructor(client, contextId, objectHandleFactory) { 26 | this._client = client; 27 | this._contextId = contextId; 28 | this._objectHandleFactory = objectHandleFactory; 29 | } 30 | 31 | /** 32 | * @param {function(*)|string} pageFunction 33 | * @param {...*} args 34 | * @return {!Promise<(!Object|undefined)>} 35 | */ 36 | async evaluate(pageFunction, ...args) { 37 | const handle = await this.evaluateHandle(pageFunction, ...args); 38 | const result = await handle.jsonValue().catch(error => undefined); 39 | await handle.dispose(); 40 | return result; 41 | } 42 | 43 | /** 44 | * @param {function(*)|string} pageFunction 45 | * @param {...*} args 46 | * @return {!Promise} 47 | */ 48 | async evaluateHandle(pageFunction, ...args) { 49 | if (helper.isString(pageFunction)) { 50 | const contextId = this._contextId; 51 | const expression = pageFunction; 52 | const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', { expression, contextId, returnByValue: false, awaitPromise: true}); 53 | if (exceptionDetails) 54 | throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails)); 55 | return this._objectHandleFactory(remoteObject); 56 | } 57 | 58 | const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', { 59 | functionDeclaration: pageFunction.toString(), 60 | executionContextId: this._contextId, 61 | arguments: args.map(convertArgument.bind(this)), 62 | returnByValue: false, 63 | awaitPromise: true 64 | }); 65 | if (exceptionDetails) 66 | throw new Error('Evaluation failed: ' + helper.getExceptionMessage(exceptionDetails)); 67 | return this._objectHandleFactory(remoteObject); 68 | 69 | /** 70 | * @param {*} arg 71 | * @return {*} 72 | * @this {Frame} 73 | */ 74 | function convertArgument(arg) { 75 | if (Object.is(arg, -0)) 76 | return { unserializableValue: '-0' }; 77 | if (Object.is(arg, Infinity)) 78 | return { unserializableValue: 'Infinity' }; 79 | if (Object.is(arg, -Infinity)) 80 | return { unserializableValue: '-Infinity' }; 81 | if (Object.is(arg, NaN)) 82 | return { unserializableValue: 'NaN' }; 83 | const objectHandle = arg && (arg instanceof JSHandle) ? arg : null; 84 | if (objectHandle) { 85 | if (objectHandle._context !== this) 86 | throw new Error('JSHandles can be evaluated only in the context they were created!'); 87 | if (objectHandle._disposed) 88 | throw new Error('JSHandle is disposed!'); 89 | if (objectHandle._remoteObject.unserializableValue) 90 | return { unserializableValue: objectHandle._remoteObject.unserializableValue }; 91 | if (!objectHandle._remoteObject.objectId) 92 | return { value: objectHandle._remoteObject.value }; 93 | return { objectId: objectHandle._remoteObject.objectId }; 94 | } 95 | return { value: arg }; 96 | } 97 | } 98 | 99 | /** 100 | * @param {!JSHandle} prototypeHandle 101 | * @return {!Promise} 102 | */ 103 | async queryObjects(prototypeHandle) { 104 | console.assert(!prototypeHandle._disposed, 'Prototype JSHandle is disposed!'); 105 | console.assert(prototypeHandle._remoteObject.objectId, 'Prototype JSHandle must not be referencing primitive value'); 106 | const response = await this._client.send('Runtime.queryObjects', { 107 | prototypeObjectId: prototypeHandle._remoteObject.objectId 108 | }); 109 | return this._objectHandleFactory(response.objects); 110 | } 111 | } 112 | 113 | class JSHandle { 114 | /** 115 | * @param {!ExecutionContext} context 116 | * @param {!Puppeteer.Session} client 117 | * @param {!Object} remoteObject 118 | */ 119 | constructor(context, client, remoteObject) { 120 | this._context = context; 121 | this._client = client; 122 | this._remoteObject = remoteObject; 123 | this._disposed = false; 124 | } 125 | 126 | /** 127 | * @return {!ExecutionContext} 128 | */ 129 | executionContext() { 130 | return this._context; 131 | } 132 | 133 | /** 134 | * @param {string} propertyName 135 | * @return {!Promise} 136 | */ 137 | async getProperty(propertyName) { 138 | const objectHandle = await this._context.evaluateHandle((object, propertyName) => { 139 | const result = {__proto__: null}; 140 | result[propertyName] = object[propertyName]; 141 | return result; 142 | }, this, propertyName); 143 | const properties = await objectHandle.getProperties(); 144 | const result = properties.get(propertyName) || null; 145 | await objectHandle.dispose(); 146 | return result; 147 | } 148 | 149 | /** 150 | * @return {!Promise>} 151 | */ 152 | async getProperties() { 153 | const response = await this._client.send('Runtime.getProperties', { 154 | objectId: this._remoteObject.objectId, 155 | ownProperties: true 156 | }); 157 | const result = new Map(); 158 | for (const property of response.result) { 159 | if (!property.enumerable) 160 | continue; 161 | result.set(property.name, this._context._objectHandleFactory(property.value)); 162 | } 163 | return result; 164 | } 165 | 166 | /** 167 | * @return {!Promise} 168 | */ 169 | async jsonValue() { 170 | if (this._remoteObject.objectId) { 171 | const response = await this._client.send('Runtime.callFunctionOn', { 172 | functionDeclaration: 'function() { return this; }', 173 | objectId: this._remoteObject.objectId, 174 | returnByValue: true, 175 | awaitPromise: true, 176 | }); 177 | return helper.valueFromRemoteObject(response.result); 178 | } 179 | return helper.valueFromRemoteObject(this._remoteObject); 180 | } 181 | 182 | /** 183 | * @return {?Puppeteer.ElementHandle} 184 | */ 185 | asElement() { 186 | return null; 187 | } 188 | 189 | async dispose() { 190 | if (this._disposed) 191 | return; 192 | this._disposed = true; 193 | await helper.releaseObject(this._client, this._remoteObject); 194 | } 195 | 196 | /** 197 | * @override 198 | * @return {string} 199 | */ 200 | toString() { 201 | if (this._remoteObject.objectId) { 202 | const type = this._remoteObject.subtype || this._remoteObject.type; 203 | return 'JSHandle@' + type; 204 | } 205 | return 'JSHandle:' + helper.valueFromRemoteObject(this._remoteObject); 206 | } 207 | } 208 | 209 | helper.tracePublicAPI(JSHandle); 210 | module.exports = {ExecutionContext, JSHandle}; 211 | -------------------------------------------------------------------------------- /utils/doclint/check_public_api/JSBuilder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 esprima = require('esprima'); 18 | const ESTreeWalker = require('../../ESTreeWalker'); 19 | const Documentation = require('./Documentation'); 20 | 21 | class JSOutline { 22 | constructor(text) { 23 | this.classes = []; 24 | /** @type {!Map} */ 25 | this.inheritance = new Map(); 26 | this.errors = []; 27 | this._eventsByClassName = new Map(); 28 | this._currentClassName = null; 29 | this._currentClassMembers = []; 30 | 31 | this._text = text; 32 | const ast = esprima.parseScript(this._text, {loc: true, range: true}); 33 | const walker = new ESTreeWalker(node => { 34 | if (node.type === 'ClassDeclaration') 35 | this._onClassDeclaration(node); 36 | else if (node.type === 'MethodDefinition') 37 | this._onMethodDefinition(node); 38 | else if (node.type === 'AssignmentExpression') 39 | this._onAssignmentExpression(node); 40 | }); 41 | walker.walk(ast); 42 | this._flushClassIfNeeded(); 43 | this._recreateClassesWithEvents(); 44 | } 45 | 46 | _onClassDeclaration(node) { 47 | this._flushClassIfNeeded(); 48 | this._currentClassName = this._extractText(node.id); 49 | const superClass = this._extractText(node.superClass); 50 | if (superClass) 51 | this.inheritance.set(this._currentClassName, superClass); 52 | } 53 | 54 | _onMethodDefinition(node) { 55 | console.assert(this._currentClassName !== null); 56 | console.assert(node.value.type === 'FunctionExpression'); 57 | const methodName = this._extractText(node.key); 58 | if (node.kind === 'get') { 59 | const property = Documentation.Member.createProperty(methodName); 60 | this._currentClassMembers.push(property); 61 | return; 62 | } 63 | // Async functions have return value. 64 | let hasReturn = node.value.async; 65 | // Extract properties from constructor. 66 | if (node.kind === 'constructor') { 67 | // Extract properties from constructor. 68 | const walker = new ESTreeWalker(node => { 69 | if (node.type !== 'AssignmentExpression') 70 | return; 71 | node = node.left; 72 | if (node.type === 'MemberExpression' && node.object && 73 | node.object.type === 'ThisExpression' && node.property && 74 | node.property.type === 'Identifier') 75 | this._currentClassMembers.push(Documentation.Member.createProperty(node.property.name)); 76 | }); 77 | walker.walk(node); 78 | } else if (!hasReturn) { 79 | const walker = new ESTreeWalker(node => { 80 | if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration' || node.type === 'ArrowFunctionExpression') 81 | return ESTreeWalker.SkipSubtree; 82 | if (node.type === 'ReturnStatement') 83 | hasReturn = hasReturn || !!node.argument; 84 | }); 85 | walker.walk(node.value.body); 86 | } 87 | const args = []; 88 | for (const param of node.value.params) { 89 | if (param.type === 'AssignmentPattern' && param.left.name) 90 | args.push(new Documentation.Argument(param.left.name)); 91 | else if (param.type === 'RestElement') 92 | args.push(new Documentation.Argument('...' + param.argument.name)); 93 | else if (param.type === 'Identifier') 94 | args.push(new Documentation.Argument(param.name)); 95 | else if (param.type === 'ObjectPattern' || param.type === 'AssignmentPattern') 96 | args.push(new Documentation.Argument('options')); 97 | else 98 | this.errors.push(`JS Parsing issue: unsupported syntax to define parameter in ${this._currentClassName}.${methodName}(): ${this._extractText(param)}`); 99 | } 100 | const method = Documentation.Member.createMethod(methodName, args, hasReturn, node.value.async); 101 | this._currentClassMembers.push(method); 102 | return ESTreeWalker.SkipSubtree; 103 | } 104 | 105 | _onAssignmentExpression(node) { 106 | if (node.left.type !== 'MemberExpression' || node.right.type !== 'ObjectExpression') 107 | return; 108 | if (node.left.object.type !== 'Identifier' || node.left.property.type !== 'Identifier' || node.left.property.name !== 'Events') 109 | return; 110 | const className = node.left.object.name; 111 | let events = this._eventsByClassName.get(className); 112 | if (!events) { 113 | events = []; 114 | this._eventsByClassName.set(className, events); 115 | } 116 | for (const property of node.right.properties) { 117 | if (property.type !== 'Property' || property.key.type !== 'Identifier' || property.value.type !== 'Literal') 118 | continue; 119 | events.push(Documentation.Member.createEvent(property.value.value)); 120 | } 121 | } 122 | 123 | _flushClassIfNeeded() { 124 | if (this._currentClassName === null) 125 | return; 126 | const jsClass = new Documentation.Class(this._currentClassName, this._currentClassMembers); 127 | this.classes.push(jsClass); 128 | this._currentClassName = null; 129 | this._currentClassMembers = []; 130 | } 131 | 132 | _recreateClassesWithEvents() { 133 | this.classes = this.classes.map(cls => { 134 | const events = this._eventsByClassName.get(cls.name) || []; 135 | const members = cls.membersArray.concat(events); 136 | return new Documentation.Class(cls.name, members); 137 | }); 138 | } 139 | 140 | _extractText(node) { 141 | if (!node) 142 | return null; 143 | const text = this._text.substring(node.range[0], node.range[1]).trim(); 144 | return text; 145 | } 146 | } 147 | 148 | /** 149 | * @param {!Array} classes 150 | * @param {!Map} inheritance 151 | * @return {!Array} 152 | */ 153 | function recreateClassesWithInheritance(classes, inheritance) { 154 | const classesByName = new Map(classes.map(cls => [cls.name, cls])); 155 | return classes.map(cls => { 156 | const membersMap = new Map(); 157 | for (let wp = cls; wp; wp = classesByName.get(inheritance.get(wp.name))) { 158 | for (const member of wp.membersArray) { 159 | // Member was overridden. 160 | const memberId = member.type + ':' + member.name; 161 | if (membersMap.has(memberId)) 162 | continue; 163 | // Do not inherit constructors 164 | if (wp !== cls && member.name === 'constructor' && member.type === 'method') 165 | continue; 166 | membersMap.set(memberId, member); 167 | } 168 | } 169 | return new Documentation.Class(cls.name, Array.from(membersMap.values())); 170 | }); 171 | } 172 | 173 | /** 174 | * @param {!Array} sources 175 | * @return {!Promise<{documentation: !Documentation, errors: !Array}>} 176 | */ 177 | module.exports = async function(sources) { 178 | const classes = []; 179 | const errors = []; 180 | const inheritance = new Map(); 181 | for (const source of sources) { 182 | const outline = new JSOutline(source.text()); 183 | classes.push(...outline.classes); 184 | errors.push(...outline.errors); 185 | for (const entry of outline.inheritance) 186 | inheritance.set(entry[0], entry[1]); 187 | } 188 | const documentation = new Documentation(recreateClassesWithInheritance(classes, inheritance)); 189 | return { documentation, errors }; 190 | }; 191 | 192 | -------------------------------------------------------------------------------- /lib/helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 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 | * http://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 debugError = require('debug')(`puppeteer:error`); 18 | /** @type {?Map} */ 19 | let apiCoverage = null; 20 | class Helper { 21 | /** 22 | * @param {Function|string} fun 23 | * @param {!Array<*>} args 24 | * @return {string} 25 | */ 26 | static evaluationString(fun, ...args) { 27 | if (Helper.isString(fun)) { 28 | console.assert(args.length === 0, 'Cannot evaluate a string with arguments'); 29 | return /** @type {string} */ (fun); 30 | } 31 | return `(${fun})(${args.map(serializeArgument).join(',')})`; 32 | 33 | /** 34 | * @param {*} arg 35 | * @return {string} 36 | */ 37 | function serializeArgument(arg) { 38 | if (Object.is(arg, undefined)) 39 | return 'undefined'; 40 | return JSON.stringify(arg); 41 | } 42 | } 43 | 44 | /** 45 | * @param {!Object} exceptionDetails 46 | * @return {string} 47 | */ 48 | static getExceptionMessage(exceptionDetails) { 49 | if (exceptionDetails.exception) 50 | return exceptionDetails.exception.description; 51 | let message = exceptionDetails.text; 52 | if (exceptionDetails.stackTrace) { 53 | for (const callframe of exceptionDetails.stackTrace.callFrames) { 54 | const location = callframe.url + ':' + callframe.lineNumber + ':' + callframe.columnNumber; 55 | const functionName = callframe.functionName || ''; 56 | message += `\n at ${functionName} (${location})`; 57 | } 58 | } 59 | return message; 60 | } 61 | 62 | /** 63 | * @param {!Object} remoteObject 64 | * @return {*} 65 | */ 66 | static valueFromRemoteObject(remoteObject) { 67 | console.assert(!remoteObject.objectId, 'Cannot extract value when objectId is given'); 68 | if (remoteObject.unserializableValue) { 69 | switch (remoteObject.unserializableValue) { 70 | case '-0': 71 | return -0; 72 | case 'NaN': 73 | return NaN; 74 | case 'Infinity': 75 | return Infinity; 76 | case '-Infinity': 77 | return -Infinity; 78 | default: 79 | throw new Error('Unsupported unserializable value: ' + remoteObject.unserializableValue); 80 | } 81 | } 82 | return remoteObject.value; 83 | } 84 | 85 | /** 86 | * @param {!Puppeteer.Session} client 87 | * @param {!Object} remoteObject 88 | */ 89 | static async releaseObject(client, remoteObject) { 90 | if (!remoteObject.objectId) 91 | return; 92 | await client.send('Runtime.releaseObject', {objectId: remoteObject.objectId}).catch(error => { 93 | // Exceptions might happen in case of a page been navigated or closed. 94 | // Swallow these since they are harmless and we don't leak anything in this case. 95 | debugError(error); 96 | }); 97 | } 98 | 99 | /** 100 | * @param {!Object} classType 101 | */ 102 | static tracePublicAPI(classType) { 103 | let className = classType.prototype.constructor.name; 104 | className = className.substring(0, 1).toLowerCase() + className.substring(1); 105 | const debug = require('debug')(`puppeteer:${className}`); 106 | if (!debug.enabled && !apiCoverage) 107 | return; 108 | for (const methodName of Reflect.ownKeys(classType.prototype)) { 109 | const method = Reflect.get(classType.prototype, methodName); 110 | if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function') 111 | continue; 112 | if (apiCoverage) 113 | apiCoverage.set(`${className}.${methodName}`, false); 114 | Reflect.set(classType.prototype, methodName, function(...args) { 115 | const argsText = args.map(stringifyArgument).join(', '); 116 | const callsite = `${className}.${methodName}(${argsText})`; 117 | if (debug.enabled) 118 | debug(callsite); 119 | if (apiCoverage) 120 | apiCoverage.set(`${className}.${methodName}`, true); 121 | return method.call(this, ...args); 122 | }); 123 | } 124 | 125 | if (classType.Events) { 126 | if (apiCoverage) { 127 | for (const event of Object.values(classType.Events)) 128 | apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false); 129 | } 130 | const method = Reflect.get(classType.prototype, 'emit'); 131 | Reflect.set(classType.prototype, 'emit', function(event, ...args) { 132 | const argsText = [JSON.stringify(event)].concat(args.map(stringifyArgument)).join(', '); 133 | if (debug.enabled && this.listenerCount(event)) 134 | debug(`${className}.emit(${argsText})`); 135 | if (apiCoverage && this.listenerCount(event)) 136 | apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true); 137 | return method.call(this, event, ...args); 138 | }); 139 | } 140 | 141 | /** 142 | * @param {!Object} arg 143 | * @return {string} 144 | */ 145 | function stringifyArgument(arg) { 146 | if (Helper.isString(arg) || Helper.isNumber(arg) || !arg) 147 | return JSON.stringify(arg); 148 | if (typeof arg === 'function') { 149 | let text = arg.toString().split('\n').map(line => line.trim()).join(''); 150 | if (text.length > 20) 151 | text = text.substring(0, 20) + '…'; 152 | return `"${text}"`; 153 | } 154 | const state = {}; 155 | const keys = Object.keys(arg); 156 | for (const key of keys) { 157 | const value = arg[key]; 158 | if (Helper.isString(value) || Helper.isNumber(value)) 159 | state[key] = JSON.stringify(value); 160 | } 161 | const name = arg.constructor.name === 'Object' ? '' : arg.constructor.name; 162 | return name + JSON.stringify(state); 163 | } 164 | } 165 | 166 | /** 167 | * @param {!NodeJS.EventEmitter} emitter 168 | * @param {string} eventName 169 | * @param {function(?)} handler 170 | * @return {{emitter: !NodeJS.EventEmitter, eventName: string, handler: function(?)}} 171 | */ 172 | static addEventListener(emitter, eventName, handler) { 173 | emitter.on(eventName, handler); 174 | return { emitter, eventName, handler }; 175 | } 176 | 177 | /** 178 | * @param {!Array<{emitter: !NodeJS.EventEmitter, eventName: string, handler: function(?)}>} listeners 179 | */ 180 | static removeEventListeners(listeners) { 181 | for (const listener of listeners) 182 | listener.emitter.removeListener(listener.eventName, listener.handler); 183 | listeners.splice(0, listeners.length); 184 | } 185 | 186 | /** 187 | * @return {?Map} 188 | */ 189 | static publicAPICoverage() { 190 | return apiCoverage; 191 | } 192 | 193 | static recordPublicAPICoverage() { 194 | apiCoverage = new Map(); 195 | } 196 | 197 | /** 198 | * @param {!Object} obj 199 | * @return {boolean} 200 | */ 201 | static isString(obj) { 202 | return typeof obj === 'string' || obj instanceof String; 203 | } 204 | 205 | /** 206 | * @param {!Object} obj 207 | * @return {boolean} 208 | */ 209 | static isNumber(obj) { 210 | return typeof obj === 'number' || obj instanceof Number; 211 | } 212 | 213 | static promisify(nodeFunction) { 214 | function promisified(...args) { 215 | return new Promise((resolve, reject) => { 216 | function callback(err, ...result) { 217 | if (err) 218 | return reject(err); 219 | if (result.length === 1) 220 | return resolve(result[0]); 221 | return resolve(result); 222 | } 223 | nodeFunction.call(null, ...args, callback); 224 | }); 225 | } 226 | return promisified; 227 | } 228 | } 229 | 230 | module.exports = { 231 | helper: Helper, 232 | debugError 233 | }; 234 | -------------------------------------------------------------------------------- /utils/fetch_devices.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright 2017 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | const util = require('util'); 19 | const fs = require('fs'); 20 | const path = require('path'); 21 | const DEVICES_URL = 'https://raw.githubusercontent.com/ChromeDevTools/devtools-frontend/master/front_end/emulated_devices/module.json'; 22 | 23 | const template = `/** 24 | * Copyright 2017 Google Inc. All rights reserved. 25 | * 26 | * Licensed under the Apache License, Version 2.0 (the "License"); 27 | * you may not use this file except in compliance with the License. 28 | * You may obtain a copy of the License at 29 | * 30 | * http://www.apache.org/licenses/LICENSE-2.0 31 | * 32 | * Unless required by applicable law or agreed to in writing, software 33 | * distributed under the License is distributed on an "AS IS" BASIS, 34 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 35 | * See the License for the specific language governing permissions and 36 | * limitations under the License. 37 | */ 38 | 39 | module.exports = %s; 40 | for (let device of module.exports) 41 | module.exports[device.name] = device; 42 | `; 43 | 44 | const help = `Usage: node ${path.basename(__filename)} [-u ] 45 | -u, --url The URL to load devices descriptor from. If not set, 46 | devices will be fetched from the tip-of-tree of DevTools 47 | frontend. 48 | 49 | -h, --help Show this help message 50 | 51 | Fetch Chrome DevTools front-end emulation devices from given URL, convert them to puppeteer 52 | devices and save to the . 53 | `; 54 | 55 | const argv = require('minimist')(process.argv.slice(2), { 56 | alias: { u: 'url', h: 'help' }, 57 | }); 58 | 59 | if (argv.help) { 60 | console.log(help); 61 | return; 62 | } 63 | 64 | const url = argv.url || DEVICES_URL; 65 | const outputPath = argv._[0]; 66 | if (!outputPath) { 67 | console.log('ERROR: output file name is missing. Use --help for help.'); 68 | return; 69 | } 70 | 71 | main(url); 72 | 73 | async function main(url) { 74 | console.log('GET ' + url); 75 | const text = await httpGET(url); 76 | let json = null; 77 | try { 78 | json = JSON.parse(text); 79 | } catch (e) { 80 | console.error(`FAILED: error parsing response - ${e.message}`); 81 | return; 82 | } 83 | const devicePayloads = json.extensions.filter(extension => extension.type === 'emulated-device').map(extension => extension.device); 84 | let devices = []; 85 | for (const payload of devicePayloads) { 86 | const device = createDevice(payload, false); 87 | const landscape = createDevice(payload, true); 88 | devices.push(device); 89 | if (landscape.viewport.width !== device.viewport.width || landscape.viewport.height !== device.viewport.height) 90 | devices.push(landscape); 91 | } 92 | devices = devices.filter(device => device.viewport.isMobile); 93 | devices.sort((a, b) => a.name.localeCompare(b.name)); 94 | // Use single-quotes instead of double-quotes to conform with codestyle. 95 | const serialized = JSON.stringify(devices, null, 2) 96 | .replace(/'/g, `\\'`) 97 | .replace(/"/g, `'`); 98 | const result = util.format(template, serialized); 99 | fs.writeFileSync(outputPath, result, 'utf8'); 100 | } 101 | 102 | /** 103 | * @param {*} descriptor 104 | * @param {boolean} landscape 105 | * @return {!Object} 106 | */ 107 | function createDevice(descriptor, landscape) { 108 | const devicePayload = loadFromJSONV1(descriptor); 109 | const viewportPayload = landscape ? devicePayload.horizontal : devicePayload.vertical; 110 | return { 111 | name: descriptor.title + (landscape ? ' landscape' : ''), 112 | userAgent: devicePayload.userAgent, 113 | viewport: { 114 | width: viewportPayload.width, 115 | height: viewportPayload.height, 116 | deviceScaleFactor: devicePayload.deviceScaleFactor, 117 | isMobile: devicePayload.capabilities.includes('mobile'), 118 | hasTouch: devicePayload.capabilities.includes('touch'), 119 | isLandscape: landscape || false 120 | } 121 | }; 122 | } 123 | 124 | /** 125 | * @param {*} json 126 | * @return {?Object} 127 | */ 128 | function loadFromJSONV1(json) { 129 | /** 130 | * @param {*} object 131 | * @param {string} key 132 | * @param {string} type 133 | * @param {*=} defaultValue 134 | * @return {*} 135 | */ 136 | function parseValue(object, key, type, defaultValue) { 137 | if (typeof object !== 'object' || object === null || !object.hasOwnProperty(key)) { 138 | if (typeof defaultValue !== 'undefined') 139 | return defaultValue; 140 | throw new Error('Emulated device is missing required property \'' + key + '\''); 141 | } 142 | const value = object[key]; 143 | if (typeof value !== type || value === null) 144 | throw new Error('Emulated device property \'' + key + '\' has wrong type \'' + typeof value + '\''); 145 | return value; 146 | } 147 | 148 | /** 149 | * @param {*} object 150 | * @param {string} key 151 | * @return {number} 152 | */ 153 | function parseIntValue(object, key) { 154 | const value = /** @type {number} */ (parseValue(object, key, 'number')); 155 | if (value !== Math.abs(value)) 156 | throw new Error('Emulated device value \'' + key + '\' must be integer'); 157 | return value; 158 | } 159 | 160 | /** 161 | * @param {*} json 162 | * @return {!{width: number, height: number}} 163 | */ 164 | function parseOrientation(json) { 165 | const result = {}; 166 | const minDeviceSize = 50; 167 | const maxDeviceSize = 9999; 168 | result.width = parseIntValue(json, 'width'); 169 | if (result.width < 0 || result.width > maxDeviceSize || 170 | result.width < minDeviceSize) 171 | throw new Error('Emulated device has wrong width: ' + result.width); 172 | 173 | result.height = parseIntValue(json, 'height'); 174 | if (result.height < 0 || result.height > maxDeviceSize || 175 | result.height < minDeviceSize) 176 | throw new Error('Emulated device has wrong height: ' + result.height); 177 | 178 | return /** @type {!{width: number, height: number}} */ (result); 179 | } 180 | 181 | const result = {}; 182 | result.type = /** @type {string} */ (parseValue(json, 'type', 'string')); 183 | result.userAgent = /** @type {string} */ (parseValue(json, 'user-agent', 'string')); 184 | 185 | const capabilities = parseValue(json, 'capabilities', 'object', []); 186 | if (!Array.isArray(capabilities)) 187 | throw new Error('Emulated device capabilities must be an array'); 188 | result.capabilities = []; 189 | for (let i = 0; i < capabilities.length; ++i) { 190 | if (typeof capabilities[i] !== 'string') 191 | throw new Error('Emulated device capability must be a string'); 192 | result.capabilities.push(capabilities[i]); 193 | } 194 | 195 | result.deviceScaleFactor = /** @type {number} */ (parseValue(json['screen'], 'device-pixel-ratio', 'number')); 196 | if (result.deviceScaleFactor < 0 || result.deviceScaleFactor > 100) 197 | throw new Error('Emulated device has wrong deviceScaleFactor: ' + result.deviceScaleFactor); 198 | 199 | result.vertical = parseOrientation(parseValue(json['screen'], 'vertical', 'object')); 200 | result.horizontal = parseOrientation(parseValue(json['screen'], 'horizontal', 'object')); 201 | return result; 202 | } 203 | 204 | /** 205 | * @param {url} 206 | * @return {!Promise} 207 | */ 208 | function httpGET(url) { 209 | let fulfill, reject; 210 | const promise = new Promise((res, rej) => { 211 | fulfill = res; 212 | reject = rej; 213 | }); 214 | const driver = url.startsWith('https://') ? require('https') : require('http'); 215 | const request = driver.get(url, response => { 216 | let data = ''; 217 | response.setEncoding('utf8'); 218 | response.on('data', chunk => data += chunk); 219 | response.on('end', () => fulfill(data)); 220 | response.on('error', reject); 221 | }); 222 | request.on('error', reject); 223 | return promise; 224 | } 225 | --------------------------------------------------------------------------------