├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cdp └── raw_websocket.js ├── code_coverage.js ├── connect.js ├── crawlsite.js ├── detect_sound.js ├── element-to-pdf.js ├── fullscreen.js ├── google_search_features.js ├── hash_navigation.js ├── html ├── d3tree.html ├── d3tree_images.html ├── lazyload.html └── speech_synth.html ├── lazyimages_without_scroll_events.js ├── lighthouse ├── chromelauncher_puppeteer.js └── throttling.js ├── monitor_internet_connection.js ├── output └── .keep ├── package.json ├── pacman.js ├── screenshot_element_on_tab.js ├── server.js ├── side-by-side-pageload.js ├── speech.js ├── verify_download.js ├── verify_download2.js ├── verify_sw_caching.js ├── view-source.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | *.pdf 3 | output/* 4 | !output/.keep 5 | tmp/ 6 | user-data-dir/ 7 | node_modules/ 8 | *error.log 9 | lhr_results.html 10 | test_script.js -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | ## Contributor License Agreements 3 | We'd love to accept your sample apps and patches! Before we can take them, we 4 | have to jump a couple of legal hurdles. 5 | Please fill out either the individual or corporate Contributor License Agreement 6 | (CLA). 7 | * If you are an individual writing original source code and you're sure you 8 | own the intellectual property, then you'll need to sign an [individual CLA] 9 | (https://developers.google.com/open-source/cla/individual). 10 | * If you work for a company that wants to allow you to contribute your work, 11 | then you'll need to sign a [corporate CLA] 12 | (https://developers.google.com/open-source/cla/corporate). 13 | Follow either of the two links above to access the appropriate CLA and 14 | instructions for how to sign and return it. Once we receive it, we'll be able to 15 | accept your pull requests. 16 | ## Contributing A Patch 17 | 1. Submit an issue describing your proposed change to the repo in question. 18 | 1. The repo owner will respond to your issue promptly. 19 | 1. If your proposed change is accepted, and you haven't already done so, sign a 20 | Contributor License Agreement (see details above). 21 | 1. Fork the desired repo, develop and test your code changes. 22 | 1. Submit a pull request. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2018 Google Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Useful Puppeteer demos! 2 | 3 | Examples for using [Puppeteer](https://developers.google.com/web/tools/puppeteer/) to do big, bold things. 4 | 5 | Output from some of the examples: 6 | 7 | ### [code_coverage.js](./code_coverage.js) 8 | 9 | Test lazy loading strategy by seeing CSS/JS code coverage usage across page load. 10 | 11 | screen shot 2018-02-26 at 10 20 41 am 12 | 13 | ### [verify_sw_caching.js](./verify_sw_caching.js) 14 | 15 | Verify all the resources you expect are being cached by a service worker for offline. 16 | 17 | screen shot 2018-03-01 at 5 09 32 pm 18 | 19 | ### [google_search_features.js](./google_search_features.js) 20 | 21 | Gut check your page to make sure it renders correctly for Google Search. 22 | 23 | screen shot 2018-03-07 at 12 50 32 pm 24 | 25 | ### [lazyimages_without_scroll_events.js](./lazyimages_without_scroll_events.js) 26 | 27 | Determine if your lazy loaded images will be seen correctly by Google Search. 28 | 29 | screen shot 2018-10-09 at 12 15 10 pm 30 | 31 | ### [speech.js](./speech.js) 32 | 33 | Make your browser talk. 34 | 35 | 36 | Speech demo in action 37 | 38 | 39 | ### [pacman.js](./pacman.js) 40 | 41 | Play the Google Pac-Man Doodle. 42 | 43 | 44 | ePlaying Puppeteer Pacman 45 | 46 | 47 | ### [element-to-pdf.js](./element-to-pdf.js) 48 | 49 | Turn a DOM element into a PDF. 50 | 51 | elemen-to-pdf output example 52 | 53 | ### [crawlsite.js](./crawlsite.js) 54 | 55 | Discover all the URLs on a site and visualize the subpages. 56 | 57 | Crawl a site/SPA 58 | 59 | ### [side-by-side-pageload.js](./side-by-side-pageload.js) 60 | 61 | Load 2 or more pages side-by-side to visually see the difference in page load. Optional desktop viewport and throttling settings. 62 | 63 | Side by side page load 64 | 65 | ### License 66 | 67 | [Apache 2.0](./LICENSE) © 2018 Google Inc. 68 | -------------------------------------------------------------------------------- /cdp/raw_websocket.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Shows how to use raw web sockets to send messages to the page using 21 | * the DevTools protocol. 22 | * See https://chromedevtools.github.io/devtools-protocol/ 23 | */ 24 | 25 | const {URL} = require('url'); 26 | const WebSocket = require('ws'); 27 | const puppeteer = require('puppeteer'); 28 | 29 | const url = process.env.URL || 'https://example.com'; 30 | 31 | (async() => { 32 | 33 | const browser = await puppeteer.launch(); 34 | const page = await browser.newPage(); 35 | 36 | // // 1. createCDPSession() is the easiest way to work with the raw DTP: 37 | // const client = await page.target().createCDPSession(); 38 | // const version = await client.send('Browser.getVersion'); 39 | // console.log(version); 40 | 41 | // 2.But you can also use raw websockets... 42 | 43 | // Pull page's ws:// debugging url. 44 | const wsUrl = new URL(browser.wsEndpoint()); 45 | const resp = await page.goto(`http://localhost:${wsUrl.port}/json`); 46 | const {webSocketDebuggerUrl} = (await resp.json())[0]; 47 | 48 | await page.goto(url); // Navigate to actual page. 49 | 50 | // Connect to page's ws:// endpoint. 51 | const ws = new WebSocket(webSocketDebuggerUrl).on('open', () => { 52 | ws.send(JSON.stringify({ 53 | id: 1, 54 | method: 'Runtime.evaluate', 55 | params: {expression: 'document.title'} // return the page title. 56 | })); 57 | }).on('message', async data => { 58 | console.log('Title of page:', JSON.parse(data).result.result.value); 59 | ws.close(); 60 | await browser.close(); 61 | }); 62 | 63 | })(); 64 | -------------------------------------------------------------------------------- /code_coverage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Shows how to use Puppeteer's code coverage API to measure CSS/JS coverage across 21 | * different points of time during loading. Great for determining if a lazy loading strategy 22 | * is paying off or working correctly. 23 | * 24 | * Install: 25 | * npm i puppeteer chalk cli-table 26 | * Run: 27 | * URL=https://example.com node code_coverage.js 28 | */ 29 | 30 | const puppeteer = require('puppeteer'); 31 | const chalk = require('chalk'); 32 | const Table = require('cli-table'); 33 | 34 | const URL = process.env.URL || 'https://www.chromestatus.com/features'; 35 | 36 | const EVENTS = [ 37 | 'domcontentloaded', 38 | 'load', 39 | // 'networkidle2', 40 | 'networkidle0', 41 | ]; 42 | 43 | function formatBytesToKB(bytes) { 44 | if (bytes > 1024) { 45 | const formattedNum = new Intl.NumberFormat('en-US', {maximumFractionDigits: 1}).format(bytes / 1024); 46 | return `${formattedNum}KB`; 47 | } 48 | return `${bytes} bytes`; 49 | } 50 | 51 | class UsageFormatter { 52 | constructor(stats) { 53 | this.stats = stats; 54 | } 55 | 56 | static eventLabel(event) { 57 | // const maxEventLabelLen = EVENTS.reduce((currMax, event) => Math.max(currMax, event.length), 0); 58 | // const eventLabel = event + ' '.repeat(maxEventLabelLen - event.length); 59 | return chalk.magenta(event); 60 | } 61 | 62 | summary(used = this.stats.usedBytes, total = this.stats.totalBytes) { 63 | const percent = Math.round((used / total) * 100); 64 | return `${formatBytesToKB(used)}/${formatBytesToKB(total)} (${percent}%)`; 65 | } 66 | 67 | shortSummary(used, total = this.stats.totalBytes) { 68 | const percent = Math.round((used / total) * 100); 69 | return used ? `${formatBytesToKB(used)} (${percent}%)` : 0; 70 | } 71 | 72 | /** 73 | * Constructors a bar chart for the % usage of each value. 74 | * @param {!{jsUsed: number, cssUsed: number, totalBytes: number}=} stats Usage stats. 75 | * @return {string} 76 | */ 77 | barGraph(stats = this.stats) { 78 | // const MAX_TERMINAL_CHARS = process.stdout.columns; 79 | const maxBarWidth = 30; 80 | 81 | const jsSegment = ' '.repeat((stats.jsUsed / stats.totalBytes) * maxBarWidth); 82 | const cssSegment = ' '.repeat((stats.cssUsed / stats.totalBytes) * maxBarWidth); 83 | const unusedSegment = ' '.repeat(maxBarWidth - jsSegment.length - cssSegment.length); 84 | 85 | return chalk.bgRedBright(jsSegment) + chalk.bgBlueBright(cssSegment) + 86 | chalk.bgBlackBright(unusedSegment); 87 | } 88 | } 89 | 90 | const stats = new Map(); 91 | 92 | /** 93 | * @param {!Object} coverage 94 | * @param {string} type Either "css" or "js" to indicate which type of coverage. 95 | * @param {string} eventType The page event when the coverage was captured. 96 | */ 97 | function addUsage(coverage, type, eventType) { 98 | for (const entry of coverage) { 99 | if (!stats.has(entry.url)) { 100 | stats.set(entry.url, []); 101 | } 102 | 103 | const urlStats = stats.get(entry.url); 104 | 105 | let eventStats = urlStats.find(item => item.eventType === eventType); 106 | if (!eventStats) { 107 | eventStats = { 108 | cssUsed: 0, 109 | jsUsed: 0, 110 | get usedBytes() { return this.cssUsed + this.jsUsed; }, 111 | totalBytes: 0, 112 | get percentUsed() { 113 | return this.totalBytes ? Math.round(this.usedBytes / this.totalBytes * 100) : 0; 114 | }, 115 | eventType, 116 | url: entry.url, 117 | }; 118 | urlStats.push(eventStats); 119 | } 120 | 121 | eventStats.totalBytes += entry.text.length; 122 | 123 | for (const range of entry.ranges) { 124 | eventStats[`${type}Used`] += range.end - range.start - 1; 125 | } 126 | } 127 | } 128 | 129 | async function collectCoverage() { 130 | const browser = await puppeteer.launch({headless: true}); 131 | 132 | // Do separate load for each event. See 133 | // https://github.com/GoogleChrome/puppeteer/issues/1887 134 | const collectPromises = EVENTS.map(async event => { 135 | console.log(`Collecting coverage @ ${UsageFormatter.eventLabel(event)}...`); 136 | 137 | const page = await browser.newPage(); 138 | 139 | // page.on('response', async response => { 140 | // console.log(response.request().url(), (await response.text()).length); 141 | // }); 142 | 143 | await Promise.all([ 144 | page.coverage.startJSCoverage(), 145 | page.coverage.startCSSCoverage() 146 | ]); 147 | 148 | await page.goto(URL, {waitUntil: event}); 149 | // await page.waitForNavigation({waitUntil: event}); 150 | 151 | const [jsCoverage, cssCoverage] = await Promise.all([ 152 | page.coverage.stopJSCoverage(), 153 | page.coverage.stopCSSCoverage() 154 | ]); 155 | 156 | addUsage(cssCoverage, 'css', event); 157 | addUsage(jsCoverage, 'js', event); 158 | 159 | await page.close(); 160 | }); 161 | 162 | await Promise.all(collectPromises); 163 | 164 | return browser.close(); 165 | } 166 | 167 | (async() => { 168 | 169 | await collectCoverage(); 170 | 171 | for (const [url, vals] of stats) { 172 | console.log('\n' + chalk.cyan(url)); 173 | 174 | const table = new Table({ 175 | // chars: {mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': ''}, 176 | head: [ 177 | 'Event', 178 | `${chalk.bgRedBright(' JS ')} ${chalk.bgBlueBright(' CSS ')} % used`, 179 | 'JS used', 180 | 'CSS used', 181 | 'Total bytes used' 182 | ], 183 | // style : {compact : true, 'padding-left' : 0} 184 | style: {head: ['white'], border: ['grey']} 185 | // colWidths: [20, 20] 186 | }); 187 | 188 | EVENTS.forEach(event => { 189 | const usageForEvent = vals.filter(val => val.eventType === event); 190 | 191 | if (usageForEvent.length) { 192 | for (const stats of usageForEvent) { 193 | // totalBytes += stats.totalBytes; 194 | // totalUsedBytes += stats.usedBytes; 195 | 196 | const formatter = new UsageFormatter(stats); 197 | table.push([ 198 | UsageFormatter.eventLabel(stats.eventType), 199 | formatter.barGraph(), 200 | formatter.shortSummary(stats.jsUsed), // !== 0 ? `${formatBytesToKB(stats.jsUsed)}KB` : 0, 201 | formatter.shortSummary(stats.cssUsed), 202 | formatter.summary() 203 | ]); 204 | } 205 | } else { 206 | table.push([UsageFormatter.eventLabel(event), 'no usage found', '-', '-', '-']); 207 | } 208 | }); 209 | 210 | console.log(table.toString()); 211 | } 212 | 213 | // Print total usage for each event. 214 | // console.log('\n'); 215 | EVENTS.forEach(event => { 216 | let totalBytes = 0; 217 | let totalUsedBytes = 0; 218 | 219 | const metrics = Array.from(stats.values()); 220 | const statsForEvent = metrics.map(eventStatsForUrl => { 221 | const statsForEvent = eventStatsForUrl.filter(stat => stat.eventType === event)[0]; 222 | // TODO: need to sum max totalBytes. Currently ignores stats if event didn't 223 | // have an entry. IOW, all total numerators should be max totalBytes seen for that event. 224 | if (statsForEvent) { 225 | totalBytes += statsForEvent.totalBytes; 226 | totalUsedBytes += statsForEvent.usedBytes; 227 | } 228 | }); 229 | 230 | const percentUsed = Math.round(totalUsedBytes / totalBytes * 100); 231 | 232 | console.log(`Total used @ ${chalk.magenta(event)}: ${formatBytesToKB(totalUsedBytes)}/${formatBytesToKB(totalBytes)} (${percentUsed}%)`); 233 | }); 234 | 235 | })(); 236 | -------------------------------------------------------------------------------- /connect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * @fileoverview 21 | * 22 | * Demonstrates how to use puppeteer.connect() to re-connect to instance of 23 | * Chrome that's already running with remote debugging enabled. 24 | * 25 | * These first time you run this script, it will launch Chrome and print 26 | * the remote debugging websocket url. 27 | * 28 | * node connect.js 29 | * 30 | * The second time you run the script, pass the websocket URL as an env variable. 31 | * Puppeteer will reconnect that to the browser instance running instead of 32 | * launching a new browser. 33 | * 34 | * wsURL=ws://127.0.0.1:9222/devtools/browser/72775377-7f73-4436 node connect.js 35 | */ 36 | 37 | const path = require('path'); 38 | const puppeteer = require('puppeteer'); 39 | 40 | const browserWSEndpoint = process.env.wsURL || null; 41 | 42 | (async() => { 43 | 44 | if (!browserWSEndpoint) { 45 | const browser = await puppeteer.launch({ 46 | handleSIGINT: false, // so Chrome doesn't exit when we quit Node. 47 | headless: false // to see what's happening 48 | }); 49 | 50 | console.log('1. Quit this script (cmd/ctrl+C).'); 51 | console.log('2. Chrome will still be running.'); 52 | console.log('4. Re-return the script with:'); 53 | console.log(` wsURL=${browser.wsEndpoint()} node ${path.basename(__filename)}`); 54 | console.log('5. Puppeteer will reconnect to the existing Chrome instead of launching a new browser.'); 55 | 56 | return; 57 | } 58 | 59 | console.log('Reconnecting to existing Chrome....'); 60 | const browser = await puppeteer.connect({browserWSEndpoint}); 61 | const page = await browser.newPage(); 62 | await page.goto('https://example.com'); 63 | 64 | console.log(`Page title:`, await page.title()); 65 | 66 | await browser.close(); 67 | })(); 68 | -------------------------------------------------------------------------------- /crawlsite.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Discovers all the pages in site or single page app (SPA) and creates 21 | * a tree of the result in ./output/ { 60 | const currentPath = parentPath + dirName; 61 | if (!fs.existsSync(currentPath)) { 62 | fs.mkdirSync(currentPath); 63 | } 64 | return currentPath + '/'; 65 | }, ''); 66 | } catch (err) { 67 | if (err.code !== 'EEXIST') { 68 | throw err; 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * Finds all anchors on the page, inclusive of those within shadow roots. 75 | * Note: Intended to be run in the context of the page. 76 | * @param {boolean=} sameOrigin When true, only considers links from the same origin as the app. 77 | * @return {!Array} List of anchor hrefs. 78 | */ 79 | function collectAllSameOriginAnchorsDeep(sameOrigin = true) { 80 | const allElements = []; 81 | 82 | const findAllElements = function(nodes) { 83 | for (let i = 0, el; el = nodes[i]; ++i) { 84 | allElements.push(el); 85 | // If the element has a shadow root, dig deeper. 86 | if (el.shadowRoot) { 87 | findAllElements(el.shadowRoot.querySelectorAll('*')); 88 | } 89 | } 90 | }; 91 | 92 | findAllElements(document.querySelectorAll('*')); 93 | 94 | const filtered = allElements 95 | .filter(el => el.localName === 'a' && el.href) // element is an anchor with an href. 96 | .filter(el => el.href !== location.href) // link doesn't point to page's own URL. 97 | .filter(el => { 98 | if (sameOrigin) { 99 | return new URL(location).origin === new URL(el.href).origin; 100 | } 101 | return true; 102 | }) 103 | .map(a => a.href); 104 | 105 | return Array.from(new Set(filtered)); 106 | } 107 | 108 | /** 109 | * Crawls a URL by visiting an url, then recursively visiting any child subpages. 110 | * @param {!Browser} browser 111 | * @param {{url: string, title: string, img?: string, children: !Array}} page Current page. 112 | * @param {number=} depth Current subtree depth of crawl. 113 | */ 114 | async function crawl(browser, page, depth = 0) { 115 | if (depth > maxDepth) { 116 | return; 117 | } 118 | 119 | // If we've already crawled the URL, we know its children. 120 | if (crawledPages.has(page.url)) { 121 | console.log(`Reusing route: ${page.url}`); 122 | const item = crawledPages.get(page.url); 123 | page.title = item.title; 124 | page.img = item.img; 125 | page.children = item.children; 126 | // Fill in the children with details (if they already exist). 127 | page.children.forEach(c => { 128 | const item = crawledPages.get(c.url); 129 | c.title = item ? item.title : ''; 130 | c.img = item ? item.img : null; 131 | }); 132 | return; 133 | } else { 134 | console.log(`Loading: ${page.url}`); 135 | 136 | const newPage = await browser.newPage(); 137 | await newPage.goto(page.url, {waitUntil: 'networkidle2'}); 138 | 139 | let anchors = await newPage.evaluate(collectAllSameOriginAnchorsDeep); 140 | anchors = anchors.filter(a => a !== URL) // link doesn't point to start url of crawl. 141 | 142 | page.title = await newPage.evaluate('document.title'); 143 | page.children = anchors.map(url => ({url})); 144 | 145 | if (SCREENSHOTS) { 146 | const path = `./${OUT_DIR}/${slugify(page.url)}.png`; 147 | let imgBuff = await newPage.screenshot({fullPage: false}); 148 | imgBuff = await sharp(imgBuff).resize(null, 150).toBuffer(); // resize image to 150 x auto. 149 | util.promisify(fs.writeFile)(path, imgBuff); // async 150 | page.img = `data:img/png;base64,${imgBuff.toString('base64')}`; 151 | } 152 | 153 | crawledPages.set(page.url, page); // cache it. 154 | 155 | await newPage.close(); 156 | } 157 | 158 | // Crawl subpages. 159 | for (const childPage of page.children) { 160 | await crawl(browser, childPage, depth + 1); 161 | } 162 | } 163 | 164 | (async() => { 165 | 166 | mkdirSync(OUT_DIR); // create output dir if it doesn't exist. 167 | await del([`${OUT_DIR}/*`]); // cleanup after last run. 168 | 169 | const browser = await puppeteer.launch(); 170 | const page = await browser.newPage(); 171 | if (VIEWPORT) { 172 | await page.setViewport(VIEWPORT); 173 | } 174 | 175 | const root = {url: URL}; 176 | await crawl(browser, root); 177 | 178 | await util.promisify(fs.writeFile)(`./${OUT_DIR}/crawl.json`, JSON.stringify(root, null, ' ')); 179 | 180 | await browser.close(); 181 | 182 | })(); 183 | 184 | -------------------------------------------------------------------------------- /detect_sound.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Trivially detect if media elements on the page are producing audio on page load. 21 | * Note: approach doesn't work in headless Chrome (which doesn't play sound). 22 | */ 23 | 24 | const puppeteer = require('puppeteer'); 25 | 26 | const URL = process.env.URL || 'https://www.youtube.com/watch?v=sK1ODp0nDbM'; 27 | 28 | (async() => { 29 | 30 | // Note: headless doesn't play audio. Launch headful chrome. 31 | const browser = await puppeteer.launch({headless: false}); 32 | 33 | const page = await browser.newPage(); 34 | await page.goto(URL, {waitUntil: 'networkidle2'}); 35 | 36 | const playingAudio = await page.evaluate(() => { 37 | const mediaEls = Array.from(document.querySelectorAll('audio,video')); 38 | return mediaEls.every(el => el.duration > 0 && !el.paused); 39 | }); 40 | 41 | console.log('Playing audio:', playingAudio); 42 | 43 | await browser.close(); 44 | 45 | })(); 46 | -------------------------------------------------------------------------------- /element-to-pdf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Takes a screenshot of the latest tweet in a user's timeline and creates a 21 | * PDF of it. Shows how to use Puppeteer to: 22 | * 23 | * 1. screenshot a DOM element 24 | * 2. craft an HTML page on-the-fly 25 | * 3. produce an image of the element and PDF of the page with the image embedded 26 | * 27 | * Usage: 28 | * node element-to-pdf.js 29 | * USERNAME=ChromiumDev node element-to-pdf.js 30 | * 31 | * --searchable makes "find in page" work: 32 | * node element-to-pdf.js --searchable 33 | * 34 | * Output: 35 | * tweet.png and tweet.pdf 36 | */ 37 | const puppeteer = require('puppeteer'); 38 | 39 | const username = process.env.USERNAME || 'ebidel'; 40 | const searchable = process.argv.includes('--searchable'); 41 | 42 | (async() => { 43 | 44 | const browser = await puppeteer.launch(); 45 | 46 | const page = await browser.newPage(); 47 | await page.setViewport({width: 1200, height: 800, deviceScaleFactor: 2}); 48 | await page.goto(`https://twitter.com/${username}`); 49 | 50 | // Can't use elementHandle.click() because it clicks the center of the element 51 | // with the mouse. On tweets like https://twitter.com/ebidel/status/915996563234631680 52 | // there is an embedded card link to another tweet that it clicks. 53 | await page.$eval(`.tweet[data-screen-name="${username}"]`, tweet => tweet.click()); 54 | await page.waitForSelector('.tweet.permalink-tweet', {visible: true}); 55 | 56 | const overlay = await page.$('.tweet.permalink-tweet'); 57 | const screenshot = await overlay.screenshot({path: 'tweet.png'}); 58 | 59 | if (searchable) { 60 | await page.evaluate(tweet => { 61 | const width = getComputedStyle(tweet).width; 62 | tweet = tweet.cloneNode(true); 63 | tweet.style.width = width; 64 | document.body.innerHTML = ` 65 |
; 66 | ${tweet.outerHTML} 67 |
68 | `; 69 | }, overlay); 70 | } else { 71 | await page.setContent(` 72 | 73 | 74 | 75 | 90 | 91 | 92 | 93 | 94 | 95 | `); 96 | } 97 | 98 | await page.pdf({path: 'tweet.pdf', printBackground: true}); 99 | 100 | await browser.close(); 101 | 102 | })(); 103 | -------------------------------------------------------------------------------- /fullscreen.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Launch a URL in full screen. 21 | */ 22 | 23 | const puppeteer = require('puppeteer'); 24 | 25 | const url = process.env.URL || 'https://news.ycombinator.com/'; 26 | 27 | (async() => { 28 | 29 | const browser = await puppeteer.launch({ 30 | headless: false, 31 | // See flags at https://peter.sh/experiments/chromium-command-line-switches/. 32 | args: [ 33 | '--disable-infobars', // Removes the butter bar. 34 | '--start-maximized', 35 | // '--start-fullscreen', 36 | // '--window-size=1920,1080', 37 | // '--kiosk', 38 | ], 39 | }); 40 | 41 | const page = await browser.newPage(); 42 | await page.setViewport({width: 1920, height: 1080}); 43 | await page.goto(url); 44 | await page.evaluate('document.documentElement.webkitRequestFullscreen()'); 45 | await page.waitFor(5000); 46 | 47 | await browser.close(); 48 | })(); -------------------------------------------------------------------------------- /google_search_features.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Prints a non-exhaustive list of HTML/JS/CSS features a page is using which 21 | * are not supported by the Google Search bot and "Render as Google" 22 | * (runs Chrome 41). See developers.google.com/search/docs/guides/rendering. 23 | */ 24 | 25 | const fs = require('fs'); 26 | const puppeteer = require('puppeteer'); 27 | const fetch = require('node-fetch'); 28 | const chalk = require('chalk'); 29 | const caniuseDB = require('caniuse-db/data.json').data; 30 | 31 | const url = process.env.URL || 'https://www.chromestatus.com/features'; 32 | 33 | const GOOGLE_SEARCH_CHROME_VERSION = process.env.CHROME_VERSION || 41; 34 | 35 | const BlinkFeatureNameToCaniuseName = { 36 | AddEventListenerPassiveTrue: 'passive-event-listener', 37 | AddEventListenerPassiveFalse: 'passive-event-listener', 38 | PromiseConstructor: 'promises', 39 | PromiseResolve: 'promises', 40 | PromiseReject: 'promises', 41 | V8PromiseChain: 'promises', 42 | DocumentRegisterElement: 'custom-elements', 43 | V0CustomElementsRegisterHTMLCustomTag: 'custom-elements', 44 | V0CustomElementsCreateCustomTagElement: 'custom-elements', 45 | V0CustomElementsRegisterHTMLTypeExtension: 'custom-elements', 46 | V0CustomElementsCreateTypeExtensionElement: 'custom-elements', 47 | CSSSelectorPseudoMatches: 'css-matches-pseudo', 48 | CustomElementRegistryDefine: 'custom-elementsv1', 49 | ElementAttachShadow: 'shadowdomv1', 50 | ElementAttachShadowOpen: 'shadowdomv1', 51 | ElementAttachShadowClosed: 'shadowdomv1', 52 | CSSSelectorPseudoSlotted: 'shadowdomv1', 53 | HTMLSlotElement: 'shadowdomv1', 54 | CSSSelectorPseudoHost: 'shadowdom', 55 | ElementCreateShadowRoot: 'shadowdom', 56 | CSSSelectorPseudoShadow: 'shadowdom', 57 | CSSSelectorPseudoContent: 'shadowdom', 58 | CSSSelectorPseudoHostContext: 'shadowdom', 59 | HTMLShadowElement: 'shadowdom', 60 | HTMLContentElement: 'shadowdom', 61 | LinkRelPreconnect: 'link-rel-preconnect', 62 | LinkRelPreload: 'link-rel-preload', 63 | HTMLImports: 'imports', 64 | HTMLImportsAsyncAttribute: 'imports', 65 | LinkRelModulePreload: 'es6-module', 66 | V8BroadcastChannel_Constructor: 'broadcastchannel', 67 | Fetch: 'fetch', 68 | GlobalCacheStorage: 'cachestorage', // missing: https://github.com/Fyrd/caniuse/issues/3122 69 | OffMainThreadFetch: 'fetch', 70 | IntersectionObserver_Constructor: 'intersectionobserver', 71 | V8Window_RequestIdleCallback_Method: 'requestidlecallback', 72 | NotificationPermission: 'notifications', 73 | UnprefixedPerformanceTimeline: 'user-timing', 74 | V8Element_GetBoundingClientRect_Method: 'getboundingclientrect', 75 | AddEventListenerThirdArgumentIsObject: 'once-event-listener', // TODO: not a perfect match. 76 | // TODO: appears to be no UMA tracking for classes, async/await, spread, and 77 | // other newer js features. Those aren't being caught here. 78 | contain: 'css-containment', 79 | 'tab-size': 'css3-tabsize', 80 | // Explicitly disabled by search https://developers.google.com/search/docs/guides/rendering 81 | UnprefixedIndexedDB: 'indexeddb', 82 | DocumentCreateEventWebGLContextEvent: 'webgl', 83 | CSSGridLayout: 'css-grid', 84 | CSSValueDisplayContents: 'css-display-contents', 85 | CSSPaintFunction: 'css-paint-api', 86 | WorkerStart: 'webworkers', 87 | ServiceWorkerControlledPage: 'serviceworkers', 88 | PrepareModuleScript: 'es6-module', 89 | // CookieGet: 90 | // CookieSet 91 | }; 92 | 93 | /** 94 | * Unique items based on obj property. 95 | * @param {!Array} items 96 | * @param {string} propName Property name to filter on. 97 | * @return {!Array} unique array of items 98 | */ 99 | function uniqueByProperty(items, propName) { 100 | const posts = Array.from(items.reduce((map, item) => { 101 | return map.set(item[propName], item); 102 | }, new Map()).values()); 103 | return posts; 104 | } 105 | 106 | /** 107 | * Sorts array of features by their name 108 | * @param {!Object} a 109 | * @param {!Object} b 110 | */ 111 | function sortByName(a, b) { 112 | if (a.name < b.name) { 113 | return -1; 114 | } 115 | if (a.name > b.name) { 116 | return 1; 117 | } 118 | return 0; 119 | } 120 | 121 | function printHeader(usage) { 122 | console.log(''); 123 | console.log(`${chalk.bold(chalk.yellow('CAREFUL'))}: using ${usage.FeatureFirstUsed.length} HTML/JS, ${usage.CSSFirstUsed.length} CSS features. Some features are ${chalk.underline('not')} supported by the Google Search crawler.`); 124 | console.log(`The bot runs ${chalk.redBright('Chrome ' + GOOGLE_SEARCH_CHROME_VERSION)}, which may not render your page correctly when it's being indexed.`); 125 | console.log(''); 126 | console.log(chalk.dim('More info at https://developers.google.com/search/docs/guides/rendering.')); 127 | console.log(''); 128 | console.log(`Features used which are not supported by Google Search:`); 129 | console.log(''); 130 | } 131 | 132 | /** 133 | * Returns true if `feature` is supported by the Google Search bot. 134 | * @param {string} feature caniuse.com feature name/id. 135 | * @return {boolean} True if the feature is (likely) supported by Google Search. 136 | */ 137 | function supportedByGoogleSearch(feature) { 138 | const data = caniuseDB[feature]; 139 | if (!data) { 140 | return null; 141 | } 142 | const support = data.stats.chrome[GOOGLE_SEARCH_CHROME_VERSION]; 143 | return support === 'y'; // TODO: consider 'p'. Partial support / polyfill. 144 | } 145 | 146 | /** 147 | * Fetches HTML/JS feature id/names from chromestatus.com. 148 | * @param {!Browser} browser 149 | * @return {!Map} key/val pairs of ids -> feature name 150 | */ 151 | async function fetchFeatureToNameMapping() { 152 | const resp = await fetch('https://www.chromestatus.com/data/blink/features'); 153 | return new Map(await resp.json()); 154 | } 155 | 156 | /** 157 | * Fetches CSS property id/names from chromestatus.com 158 | * @param {!Browser} browser 159 | * @return {!Map} key/val pairs of ids -> feature name 160 | */ 161 | async function fetchCSSFeatureToNameMapping(browser) { 162 | const resp = await fetch('https://www.chromestatus.com/data/blink/cssprops'); 163 | return new Map(await resp.json()); 164 | } 165 | 166 | /** 167 | * Start a trace during load to capture web platform features used by the page. 168 | * @param {!Browser} browser 169 | * @return {!Object} 170 | */ 171 | async function collectFeatureTraceEvents(browser) { 172 | const page = await browser.newPage(); 173 | 174 | console.log(chalk.cyan(`Trace started.`)); 175 | 176 | await page.tracing.start({ 177 | categories: [ 178 | '-*', 179 | 'disabled-by-default-devtools.timeline', // for TracingStartedInPage 180 | 'disabled-by-default-blink.feature_usage' 181 | ], 182 | }); 183 | console.log(chalk.cyan(`Navigating to ${url}`)); 184 | await page.goto(url, {waitUntil: 'networkidle2'}); 185 | console.log(chalk.cyan(`Waiting for page to be idle...`)); 186 | await page.waitFor(5000); // add a little more time in case other features are used. 187 | const trace = JSON.parse(await page.tracing.stop()); 188 | console.log(chalk.cyan(`Trace complete.`)); 189 | 190 | // Filter out all trace events that aren't 1. blink feature usage 191 | // and 2. from the same process/thread id as our test page's main thread. 192 | const traceStartEvent = findTraceStartEvent(trace.traceEvents); 193 | const events = trace.traceEvents.filter(e => { 194 | return e.cat === 'disabled-by-default-blink.feature_usage' && 195 | e.pid === traceStartEvent.pid && e.tid === traceStartEvent.tid; 196 | }); 197 | 198 | // // Gut check. 199 | // console.assert(events.every((entry, i, arr) => { 200 | // // const nextIdx = Math.min(i + 1, arr.length - 1); 201 | // // return entry.pid === arr[nextIdx].pid && entry.tid === arr[nextIdx].tid; 202 | // return entry.pid === traceStartEvent.pid && entry.tid === traceStartEvent.tid; 203 | // }), 'Trace event is not from the same process/thread id as the page being tested.'); 204 | 205 | await page.close(); 206 | 207 | return events; 208 | } 209 | 210 | /** 211 | * @param {Array} events 212 | * @return {Object} 213 | */ 214 | function findTraceStartEvent(events) { 215 | const startedInBrowserEvt = events.find(e => e.name === 'TracingStartedInBrowser'); 216 | if (startedInBrowserEvt && startedInBrowserEvt.args.data && startedInBrowserEvt.args.data.frames) { 217 | const mainFrame = startedInBrowserEvt.args.data.frames.find(frame => !frame.parent); 218 | const pid = mainFrame && mainFrame.processId; 219 | const threadNameEvt = events.find(e => e.pid === pid && e.ph === 'M' && 220 | e.cat === '__metadata' && e.name === 'thread_name' && e.args.name === 'CrRendererMain'); 221 | 222 | const tid = threadNameEvt && threadNameEvt.tid; 223 | if (pid && tid) { 224 | return { 225 | pid, 226 | tid 227 | }; 228 | } 229 | } 230 | 231 | // // Support legacy browser versions 232 | const startedInPageEvt = events.find(e => e.name === 'TracingStartedInPage'); 233 | if (startedInPageEvt && startedInPageEvt.args && startedInPageEvt.args.data) { 234 | return { 235 | pid: startedInPageEvt.pid, 236 | tid: startedInPageEvt.tid 237 | }; 238 | } 239 | } 240 | 241 | /** 242 | * @param {!Object} feature 243 | */ 244 | function printFeatureName(feature, url= null) { 245 | const suffix = url ? `: ${url}` : ''; 246 | if (feature.css) { 247 | console.log(chalk.grey('-'), `CSS \`${feature.name}\`${suffix}`); 248 | } else { 249 | console.log(chalk.grey('-'), `${feature.name}${suffix}`); 250 | } 251 | } 252 | 253 | (async() => { 254 | 255 | const browser = await puppeteer.launch({ 256 | // headless: false, 257 | }); 258 | 259 | // Parallelize the separate page loads. 260 | const [featureMap, cssFeatureMap, traceEvents] = await Promise.all([ 261 | fetchFeatureToNameMapping(), 262 | fetchCSSFeatureToNameMapping(), 263 | collectFeatureTraceEvents(browser), 264 | ]); 265 | 266 | const usage = traceEvents.reduce((usage, e) => { 267 | if (!(e.name in usage)) { 268 | usage[e.name] = []; 269 | } 270 | const isCSS = e.name === 'CSSFirstUsed'; 271 | const id = e.args.feature; 272 | const name = isCSS ? cssFeatureMap.get(id) : featureMap.get(id); 273 | usage[e.name].push({id, name, ts: e.ts, css: isCSS}); 274 | 275 | return usage; 276 | }, {}); 277 | 278 | // Unique events based on feature property id. 279 | usage.FeatureFirstUsed = uniqueByProperty(usage.FeatureFirstUsed, 'id'); 280 | usage.CSSFirstUsed = uniqueByProperty(usage.CSSFirstUsed, 'id'); 281 | 282 | printHeader(usage); 283 | 284 | const allFeaturesUsed = Object.entries([...usage.FeatureFirstUsed, ...usage.CSSFirstUsed].sort(sortByName)); 285 | for (const [id, feature] of allFeaturesUsed) { 286 | const caniuseName = BlinkFeatureNameToCaniuseName[feature.name]; 287 | const supported = supportedByGoogleSearch(caniuseName); 288 | if (caniuseName && !supported) { 289 | const url = chalk.magentaBright(`https://caniuse.com/#feat=${caniuseName}`); 290 | printFeatureName(feature, url); 291 | } 292 | } 293 | console.log(''); 294 | console.log('All features used on the page:'); 295 | console.log(''); 296 | for (const [id, feature] of allFeaturesUsed) { 297 | printFeatureName(feature); 298 | } 299 | console.log(''); 300 | 301 | await browser.close(); 302 | 303 | })(); 304 | -------------------------------------------------------------------------------- /hash_navigation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Hash (#) changes aren't considered navigations in Chrome. This makes it 21 | * tricky to test a SPA that use hashes to change views. 22 | * 23 | * This script shows how to observe the view of a SPA changing in Puppeteer 24 | * by injecting code into the page that listens for `hashchange` events. 25 | * 26 | * To run: 27 | * 1. Start a web server in this folder on port 5000. 28 | * 2. node hash_navigation.js 29 | */ 30 | 31 | const {URL} = require('url'); 32 | const puppeteer = require('puppeteer'); 33 | 34 | const url = 'http://localhost:5000/html/spa.html'; 35 | 36 | async function printVisibleView(page) { 37 | console.log('Visible panel:', await page.$eval(':target', el => el.textContent)); 38 | } 39 | 40 | async function main() { 41 | const browser = await puppeteer.launch(); 42 | 43 | const page = await browser.newPage(); 44 | 45 | // Catch + "forward" hashchange events from page to node puppeteer. 46 | await page.exposeFunction('onHashChange', url => page.emit('hashchange', url)); 47 | await page.evaluateOnNewDocument(() => { 48 | addEventListener('hashchange', e => onHashChange(location.href)); 49 | }); 50 | 51 | // Listen for hashchange events in node Puppeteer code. 52 | page.on('hashchange', url => console.log('hashchange event:', new URL(url).hash)); 53 | 54 | await page.goto(url); 55 | await page.waitForSelector('[data-page="#page1"]'); // wait for view 1 to be "loaded". 56 | await printVisibleView(page); 57 | 58 | try { 59 | // "Navigate" to view 2 in SPA. We don't want to wait for the `load` event, 60 | // so set a small timeout and catch the "navigation timeout". 61 | await page.goto(`${url}#page2`, {timeout: 1}); 62 | } catch (err) { 63 | // noop 64 | } 65 | 66 | await page.waitForSelector('[data-page="#page2"]'); // wait for view 2 to be "loaded". 67 | await printVisibleView(page); 68 | 69 | await browser.close(); 70 | } 71 | 72 | main(); -------------------------------------------------------------------------------- /html/d3tree.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 28 | 29 | 30 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /html/d3tree_images.html: -------------------------------------------------------------------------------- 1 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | SPA pages 122 | 123 | 124 | 142 | 143 | 144 | 145 | 146 |
147 |

Site pages

148 |

Click to view their identity

149 |

And link to their web page!

150 |
151 |
152 | 356 | 357 | 358 | -------------------------------------------------------------------------------- /html/lazyload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Lazy load examples 7 | 8 | 9 | 74 | 75 | 76 | 77 |
78 | 79 |

The Best Product Page Ever

80 |
81 | 82 |
83 |

84 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vulputate sit 85 | amet massa id faucibus. Nam gravida pellentesque consectetur. Nulla suscipit blandit 86 | tristique. Proin ut diam eu mi fermentum bibendum ac a lacus. Donec dui enim, rhoncus vel cursus 87 | at, feugiat et purus. Phasellus nec varius elit. In hac habitasse platea dictumst. Donec 88 | dictum placerat ante, sit amet tempus ante varius vitae.

89 | 90 |

91 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vulputate sit 92 | amet massa id faucibus. Nam gravida pellentesque consectetur. Nulla suscipit blandit 93 | tristique. Proin ut diam eu mi fermentum bibendum ac a lacus. Donec dui enim, rhoncus vel cursus 94 | at, feugiat et purus. Phasellus nec varius elit. In hac habitasse platea dictumst. Donec 95 | dictum placerat ante, sit amet tempus ante varius vitae.

96 |

97 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vulputate sit 98 | amet massa id faucibus. Nam gravida pellentesque consectetur. Nulla suscipit blandit 99 | tristique. Proin ut diam eu mi fermentum bibendum ac a lacus. Donec dui enim, rhoncus vel cursus 100 | at, feugiat et purus. Phasellus nec varius elit. In hac habitasse platea dictumst. Donec 101 | dictum placerat ante, sit amet tempus ante varius vitae. 102 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vulputate sit 103 | amet massa id faucibus. Nam gravida pellentesque consectetur. Nulla suscipit blandit 104 | tristique. Proin ut diam eu mi fermentum bibendum ac a lacus. Donec dui enim, rhoncus vel cursus 105 | at, feugiat et purus. Phasellus nec varius elit. In hac habitasse platea dictumst. Donec 106 | dictum placerat ante, sit amet tempus ante varius vitae.

107 |

108 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vulputate sit 109 | amet massa id faucibus. Nam gravida pellentesque consectetur. Nulla suscipit blandit 110 | tristique. Proin ut diam eu mi fermentum bibendum ac a lacus. Donec dui enim, rhoncus vel cursus 111 | at, feugiat et purus. Phasellus nec varius elit. In hac habitasse platea dictumst. Donec 112 | dictum placerat ante, sit amet tempus ante varius vitae.

113 | 114 | 115 | 116 |
117 | 118 | 119 | 120 |
121 | 122 |

123 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vulputate sit 124 | amet massa id faucibus. Nam gravida pellentesque consectetur. Nulla suscipit blandit 125 | tristique. Proin ut diam eu mi fermentum bibendum ac a lacus. Donec dui enim, rhoncus vel cursus 126 | at, feugiat et purus. Phasellus nec varius elit. In hac habitasse platea dictumst. Donec 127 | dictum placerat ante, sit amet tempus ante varius vitae.

128 | 129 |

130 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vulputate sit 131 | amet massa id faucibus. Nam gravida pellentesque consectetur. Nulla suscipit blandit 132 | tristique. Proin ut diam eu mi fermentum bibendum ac a lacus. Donec dui enim, rhoncus vel cursus 133 | at, feugiat et purus. Phasellus nec varius elit. In hac habitasse platea dictumst. Donec 134 | dictum placerat ante, sit amet tempus ante varius vitae.

135 | 136 |

137 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vulputate sit 138 | amet massa id faucibus. Nam gravida pellentesque consectetur. Nulla suscipit blandit 139 | tristique. Proin ut diam eu mi fermentum bibendum ac a lacus. Donec dui enim, rhoncus vel cursus 140 | at, feugiat et purus. Phasellus nec varius elit. In hac habitasse platea dictumst. Donec 141 | dictum placerat ante, sit amet tempus ante varius vitae.

142 | 143 |

Iframe content using lazyload attribute:

144 | 145 | 146 |

Lazy loaded images using lazyimages.js library:

147 | 148 |
149 | 150 | 151 | 152 |
153 | 154 |

155 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus vulputate sit 156 | amet massa id faucibus. Nam gravida pellentesque consectetur. Nulla suscipit blandit 157 | tristique. Proin ut diam eu mi fermentum bibendum ac a lacus. Donec dui enim, rhoncus vel cursus 158 | at, feugiat et purus. Phasellus nec varius elit. In hac habitasse platea dictumst. Donec 159 | dictum placerat ante, sit amet tempus ante varius vitae.

160 | 161 |

End of page

162 | 163 |
164 | 165 | 166 | 208 | 217 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /html/speech_synth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31 | -------------------------------------------------------------------------------- /lazyimages_without_scroll_events.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Verifies that images on the page which are lazy loaded do not use scroll 21 | * events to do so. This can present a problem for search crawlers discovering 22 | * the images on a page. 23 | * 24 | * Usage: 25 | * 26 | * node lazyimages_without_scroll_events.js -h 27 | * // PASSES. Uses IntersectionObserver and lazyimages.js checks image visibility on page load without needing scroll events. 28 | * node lazyimages_without_scroll_events.js --url=https://rawgit.com/GoogleChromeLabs/puppeteer-examples/master/html/lazyload.html 29 | * // FAILS. uses scroll events. 30 | * node lazyimages_without_scroll_events.js --url=https://css-tricks.com/examples/LazyLoading/ 31 | * // FAIL. Uses scroll events. 32 | * node lazyimages_without_scroll_events.js --url=http://dinbror.dk/blazy/ -o results.html --save 33 | */ 34 | 35 | const puppeteer = require('puppeteer'); 36 | const fs = require('fs'); 37 | const PixelDiff = require('pixel-diff'); 38 | const PNG = require('pngjs').PNG; 39 | const resizeImg = require('resize-img'); 40 | 41 | const DEFAULT_VIEWPORT = { 42 | width: 1000, 43 | height: 2000, 44 | deviceScaleFactor: 1, 45 | }; 46 | 47 | const PNG_NOSCROLL_FILENAME = 'page_noscroll.png'; 48 | const PNG_SCROLL_FILENAME = 'page_scroll.png'; 49 | const PNG_DIFF_FILENAME = 'page_diff.png'; 50 | 51 | const WAIT_FOR = 2000; // Additional seconds to wait after page is considered load. 52 | 53 | const argv = require('yargs') 54 | .options({ 55 | 'save': { 56 | alias: 's', 57 | describe: 'Save screenshots to disk', 58 | default: false, 59 | }, 60 | 'url': { 61 | alias: 'u', 62 | describe: 'URL to load', 63 | demandOption: true, 64 | type: 'string', 65 | }, 66 | 'output': { 67 | alias: 'o', 68 | describe: 'Output HTML file', 69 | default: 'results.html', 70 | type: 'string', 71 | }, 72 | 'scroll': { 73 | describe: 'Filename for screenshot of scrolled page. Only worked with --save option.', 74 | default: PNG_SCROLL_FILENAME, 75 | }, 76 | 'noscroll': { 77 | describe: 'Filename for screenshot of non-scrolled page. Only worked with --save option.', 78 | default: PNG_NOSCROLL_FILENAME, 79 | }, 80 | 'diff': { 81 | describe: 'Filename for diff screenshot between pages.', 82 | default: PNG_DIFF_FILENAME, 83 | }, 84 | }) 85 | .help() 86 | .argv; 87 | 88 | (async() => { 89 | 90 | const browser = await puppeteer.launch({ 91 | // headless: false, 92 | defaultViewport: DEFAULT_VIEWPORT, 93 | }); 94 | 95 | // async function waitForNetworkIdle(page, idle='networkidle0') { 96 | // return new Promise(resolve => { 97 | // page._client.on('Page.lifecycleEvent', e => { 98 | // if (e.name === 'networkIdle' && idle === 'networkidle0') { 99 | // resolve(); 100 | // } else if (e.name === 'networkAlmostIdle' && idle === 'networkidle2') { 101 | // resolve(); 102 | // } 103 | // }); 104 | // }); 105 | // } 106 | 107 | async function screenshotPageWithoutScroll(url) { 108 | const context = await browser.createIncognitoBrowserContext(); 109 | 110 | const page = await context.newPage(); 111 | 112 | // Set viewport height to same as the page when it's completely scrolled 113 | // so final screenshot is same height. 114 | // const viewport = Object.assign({}, DEFAULT_VIEWPORT); 115 | // viewport.height = maxScrollHeight; 116 | // await page.setViewport(viewport); 117 | 118 | // Prevent page from scrolling. 119 | // page.on('console', msg => console.log(msg.text())); 120 | // await page.evaluate(() => { 121 | // document.addEventListener('scroll', e => { 122 | // console.log('scroll!'); 123 | // e.stopImmediatePropagation(); 124 | // e.stopPropagation(); 125 | // }); 126 | // }); 127 | 128 | await page.goto(url, {waitUntil: 'networkidle2'}); 129 | await page.waitFor(WAIT_FOR); // Wait a bit more in case other things are loading. 130 | // await waitForNetworkIdle(page, 'networkidle0'); // wait for network to be idle. 131 | const buffer = await page.screenshot({ 132 | path: argv.save ? argv.noscroll : null, 133 | fullPage: true 134 | }); 135 | await context.close(); 136 | return buffer; 137 | } 138 | 139 | async function screenshotPageAfterScroll(url) { 140 | const context = await browser.createIncognitoBrowserContext(); 141 | 142 | const page = await context.newPage(); 143 | await page.goto(url, {waitUntil: 'networkidle2'}); 144 | 145 | await page.evaluate(() => { 146 | // const viewPortHeight = document.documentElement.clientHeight; 147 | let lastScrollTop = document.scrollingElement.scrollTop; 148 | // Scroll to bottom of page until we can't scroll anymore. 149 | const scroll = () => { 150 | document.scrollingElement.scrollTop += 100;//(viewPortHeight / 2); 151 | if (document.scrollingElement.scrollTop !== lastScrollTop) { 152 | lastScrollTop = document.scrollingElement.scrollTop; 153 | requestAnimationFrame(scroll); 154 | } 155 | }; 156 | scroll(); 157 | }); 158 | 159 | await page.waitFor(WAIT_FOR); // Wait a bit more in case other things are loading. 160 | // await waitForNetworkIdle(page, 'networkidle0'); // wait for network to be idle. 161 | 162 | // const maxScrollHeight = await page.evaluate( 163 | // 'document.scrollingElement.scrollHeight'); 164 | 165 | const buffer = await page.screenshot({ 166 | path: argv.save ? argv.scroll : null, 167 | fullPage: true 168 | }); 169 | 170 | await context.close(); 171 | return {screenshot: buffer}; 172 | } 173 | 174 | async function resizeImage(pngBuffer, scale = 0.5) { 175 | const png = PNG.sync.read(pngBuffer); 176 | pngBuffer = await resizeImg(pngBuffer, { 177 | width: Math.round(png.width * scale), 178 | height: Math.round(png.height * scale), 179 | }); 180 | return {buffer: pngBuffer, png: PNG.sync.read(pngBuffer)}; 181 | } 182 | 183 | // First take screenshot of page scrolling it. This will also allow us to 184 | // determine the total scroll height of the page and set the viewport for 185 | // the unscrolled page. 186 | let {screenshot: screenshotB} = await screenshotPageAfterScroll(argv.url); 187 | let screenshotA = await screenshotPageWithoutScroll(argv.url); 188 | 189 | let pngA = PNG.sync.read(screenshotA); 190 | let pngB = PNG.sync.read(screenshotB); 191 | // const sameDimensions = pngA.height === pngB.height && pngA.width === pngB.width; 192 | 193 | const diff = new PixelDiff({ 194 | imageA: screenshotA, 195 | imageB: screenshotB, 196 | thresholdType: PixelDiff.THRESHOLD_PERCENT, // thresholdType: PixelDiff.RESULT_DIFFERENT, 197 | threshold: 0.01, // 1% threshold 198 | imageOutputPath: argv.diff, 199 | // composeTopToBottom: true, 200 | // copyImageBToOutput: true, 201 | // copyImageAToOutput: false, 202 | cropImageB: { 203 | x: 0, 204 | y: 0, 205 | width: pngA.width, 206 | height: pngA.height, 207 | }, 208 | }); 209 | 210 | const result = await diff.runWithPromise(); 211 | 212 | const passed = diff.hasPassed(result.code);// && sameDimensions; 213 | console.log(`Lazy images loaded correctly: ${passed ? 'Passed' : 'Failed'}`); 214 | console.log(`Found ${result.differences} pixels differences.`); 215 | 216 | ({png: pngA, buffer: screenshotA} = await resizeImage(screenshotA, 0.25)); 217 | ({png: pngB, buffer: screenshotB} = await resizeImage(screenshotB, 0.25)); 218 | 219 | console.log(`Dimension image A: ${pngA.width}x${pngA.height}`); 220 | console.log(`Dimension image B: ${pngB.width}x${pngB.height}`); 221 | 222 | const {png: pngDiff, buffer: diffBuffer} = await resizeImage(fs.readFileSync(argv.diff), 0.25); 223 | 224 | const page = await browser.newPage(); 225 | await page.setContent(` 226 | 227 | 228 | 229 | 230 | 288 | 289 | 290 |
291 |

Do your lazy loaded images work in search crawlers?

292 |

The two screenshots below should look more or less the same. 293 | If there are missing images in the left screenshot, it's likely they 294 | are being lazy loaded using scroll events. This can present a problem 295 | for search engines which often do not run JavaScript, and therefore, 296 | do not run scroll handlers. Images need to be fully loaded when they're 297 | "in the viewport", without scrolling the page. Instead of scroll events, 298 | use a more modern approach like 299 | IntersectionObserver with a 300 | polyfill. 301 | If you're using library for lazy loading, find one that doesn't use scroll events.

302 |
303 |

Site result: ${passed ? 'PASSED' : 'FAILED'}

304 | 305 |
306 |
307 |

Page without being scrolled

308 |

This is how the lazy loaded images on your page appear to a search engine. 309 | Does it look right? If images are missing, they might be lazy loaded 310 | using scroll events.

311 | 312 |
313 |
314 |

 

315 |

( difference between two screenshots )

316 | 317 |
318 |
319 |

Page after scrolling

320 |

If there are more images in the screenshot below, 321 | the page is using scroll events to lazy load images. Instead, consider using another approach like 322 | IntersectionObserver.

323 | 324 |
325 |
326 | 327 | 328 | `); 329 | 330 | fs.writeFileSync(argv.output, await page.content(), {encoding: 'utf8'}); 331 | 332 | await page.close(); 333 | await browser.close(); 334 | 335 | })(); -------------------------------------------------------------------------------- /lighthouse/chromelauncher_puppeteer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * How to: 21 | * 1. Use chrome-launcher module to launch chrome 22 | * 2. Connect to that browser instance using Puppeteer. 23 | * 3. Run Lighthouse on the page. 24 | */ 25 | 26 | const chromeLauncher = require('chrome-launcher'); 27 | const puppeteer = require('puppeteer'); 28 | const lighthouse = require('lighthouse'); 29 | const request = require('request'); 30 | const util = require('util'); 31 | 32 | (async() => { 33 | 34 | const URL = 'https://www.chromestatus.com/features'; 35 | 36 | const opts = { 37 | chromeFlags: ['--headless'], 38 | logLevel: 'info', 39 | output: 'json' 40 | }; 41 | 42 | // Launch chrome using chrome-launcher. 43 | const chrome = await chromeLauncher.launch(opts); 44 | opts.port = chrome.port; 45 | 46 | // Connect to it using puppeteer.connect(). 47 | const resp = await util.promisify(request)(`http://localhost:${opts.port}/json/version`); 48 | const {webSocketDebuggerUrl} = JSON.parse(resp.body); 49 | const browser = await puppeteer.connect({browserWSEndpoint: webSocketDebuggerUrl}); 50 | 51 | // Run Lighthouse. 52 | const {lhr} = await lighthouse(URL, opts, null); 53 | console.log(`Lighthouse scores: ${Object.values(lhr.categories).map(c => `${c.title} ${c.score}`).join(', ')}`); 54 | 55 | await browser.disconnect(); 56 | await chrome.kill(); 57 | 58 | })(); -------------------------------------------------------------------------------- /lighthouse/throttling.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Custom network throttling for Lighthouse Testing using Puppeteer. 21 | * 22 | * Chrome gets launched by Puppeteer (normally done by LH) and custom network 23 | * conditions are established through Puppeteer's APIs. Lighthouse then audits 24 | * the page with those settings. 25 | * 26 | * The flow is: 27 | * 1. Disable Lighthouse's default throttling settings. 28 | * 2. Launch Chrome using Puppeteer. Tell Lighthouse to reuse that chrome 29 | * instance instead of launching it's own. 30 | * 3. Hand the url to Lighthouse for testing. 31 | * Puppeteer observes the page opening, then sets up emulation. 32 | */ 33 | 34 | const puppeteer = require('puppeteer'); 35 | const lighthouse = require('lighthouse'); 36 | // const ReportGenerator = require('lighthouse/lighthouse-core/report/v2/report-generator'); 37 | const fs = require('fs'); 38 | const {URL} = require('url'); 39 | 40 | (async() => { 41 | 42 | const url = 'https://www.chromestatus.com/features'; 43 | 44 | // Use Puppeteer to launch headless Chrome. 45 | const browser = await puppeteer.launch({headless: true}); 46 | const remoteDebugPort =(new URL(browser.wsEndpoint())).port; 47 | 48 | // Watch for Lighthouse to open url, then customize network conditions. 49 | // Note: re-establishes throttle settings every time LH reloads the page. Shooooould be ok :) 50 | browser.on('targetchanged', async target => { 51 | const page = await target.page(); 52 | 53 | if (page && page.url() === url) { 54 | const client = await page.target().createCDPSession(); 55 | // await client.send('Network.enable'); // Already enabled by pptr. 56 | await client.send('Network.emulateNetworkConditions', { 57 | offline: false, 58 | // values of 0 remove any active throttling. crbug.com/456324#c9 59 | latency: 800, 60 | downloadThroughput: Math.floor(1.6 * 1024 * 1024 / 8), // 1.6Mbps 61 | uploadThroughput: Math.floor(750 * 1024 / 8) // 750Kbps 62 | }); 63 | } 64 | }); 65 | 66 | // Lighthouse opens url and tests it. 67 | // Note: Possible race with Puppeteer observing the tab opening using `targetchanged` above. 68 | const {report} = await lighthouse(url, { 69 | port: remoteDebugPort, 70 | output: 'html', 71 | logLevel: 'info', 72 | disableNetworkThrottling: true, 73 | //disableCpuThrottling: true, 74 | //disableDeviceEmulation: true, 75 | }); 76 | 77 | // Save html report. 78 | fs.writeFileSync('results.html', report); 79 | console.log('Results written.'); 80 | 81 | await browser.close(); 82 | })(); 83 | -------------------------------------------------------------------------------- /monitor_internet_connection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Uses Puppeteer and the browser's online/offline events to monitor internet 21 | * connection status. 22 | */ 23 | 24 | const util = require('util'); 25 | const dns = require('dns'); 26 | const puppeteer = require('puppeteer'); 27 | 28 | async function isConnected() { 29 | try { 30 | const lookupService = util.promisify(dns.lookupService); 31 | const result = await lookupService('8.8.8.8', 53); 32 | return true; 33 | } catch (err) { 34 | return false; 35 | } 36 | } 37 | 38 | puppeteer.launch().then(async browser => { 39 | const page = await browser.newPage(); 40 | 41 | page.on('online', () => console.info('Online!')); 42 | page.on('offline', () => console.info('Offline!')); 43 | 44 | // Adds window.connectionChange in page. 45 | await page.exposeFunction('connectionChange', async online => { 46 | // Since online/offline events aren't 100% reliable, do an 47 | // actual dns lookup to verify connectivity. 48 | const isReallyConnected = await isConnected(); 49 | page.emit(isReallyConnected ? 'online' : 'offline'); 50 | }); 51 | 52 | // Monitor browser online/offline events in the page. 53 | await page.evaluateOnNewDocument(() => { 54 | window.addEventListener('online', e => window.connectionChange(navigator.onLine)); 55 | window.addEventListener('offline', e => window.connectionChange(navigator.onLine)); 56 | }); 57 | 58 | // Kick off a navigation so evaluateOnNewDocument runs. 59 | await page.goto('data:text/html,hi'); 60 | 61 | 62 | // ... do other stuff ... 63 | 64 | // await browser.close(); // Don't close the browser so we can monitor! 65 | }); 66 | -------------------------------------------------------------------------------- /output/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppeteer/examples/59355609ecb3c2e396a289b28f34d5116fc89b8e/output/.keep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppeteer-examples", 3 | "version": "0.0.1", 4 | "author": "Eric Bidelman ", 5 | "description": "Examples of using Puppeteer to do big things", 6 | "license": "Apache-2.0", 7 | "bugs": { 8 | "url": "https://github.com/GoogleChromeLabs/puppeteer-examples/issues" 9 | }, 10 | "engines": { 11 | "node": ">=8" 12 | }, 13 | "dependencies": { 14 | "caniuse-db": "^1.0.30000921", 15 | "chalk": "^2.3.2", 16 | "chrome-har": "^0.7.1", 17 | "chrome-launcher": "^0.10.5", 18 | "cli-table": "^0.3.1", 19 | "del": "^3.0.0", 20 | "express": "^4.16.4", 21 | "lighthouse": "^4.0.0-alpha.2-3.2.1", 22 | "mime": "^2.4.0", 23 | "node-fetch": "^2.3.0", 24 | "pixel-diff": "^1.0.1", 25 | "pngjs": "^3.3.3", 26 | "puppeteer": "^1.11.0", 27 | "request": "^2.88.0", 28 | "request-promise": "^4.2.2", 29 | "request-promise-native": "^1.0.5", 30 | "resize-img": "^1.1.2", 31 | "sharp": "^0.21.1", 32 | "ws": "^6.1.2", 33 | "yargs": "^12.0.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pacman.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Play Google Pac-man Doodle from Node! Uses Puppeteer's keyboard API to 21 | * forward key presses to the browser. 22 | */ 23 | 24 | const readline = require('readline'); 25 | const puppeteer = require('puppeteer'); 26 | 27 | (async() => { 28 | 29 | const browser = await puppeteer.launch({ 30 | headless: false, 31 | args: ['--window-size=800,500'] 32 | }); 33 | 34 | const page = await browser.newPage(); 35 | await page.setViewport({width: 800, height: 500, deviceScaleFactor: 2}); 36 | await page.goto('https://www.google.com/logos/2010/pacman10-i.html'); 37 | 38 | process.stdin.on('keypress', async (str, key) => { 39 | // In "raw" mode, so create own kill switch. 40 | if (key.sequence === '\u0003') { 41 | await browser.close(); 42 | process.exit(); 43 | } 44 | 45 | // See https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#keyboarddownkey-options 46 | if (['up', 'down', 'left', 'right'].includes(key.name)) { 47 | const capitalized = key.name[0].toUpperCase() + key.name.slice(1); 48 | const keyName = `Arrow${capitalized}`; 49 | console.log(`page.keyboard.down('${keyName}')`); 50 | await page.keyboard.down(keyName); 51 | } 52 | }); 53 | 54 | readline.emitKeypressEvents(process.stdin); 55 | process.stdin.setRawMode(true); 56 | 57 | })(); 58 | -------------------------------------------------------------------------------- /screenshot_element_on_tab.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Takes screenshots of DOM elements as you tab through the page. 21 | */ 22 | 23 | const puppeteer = require('puppeteer'); 24 | 25 | const AUTO_TAB = !false; // If true, tabbing through elements is done automatically. 26 | const padding = 25; // padding around the element screenshot. 27 | const url = process.env.URL || 'https://perf-sandbox.appspot.com/'; 28 | 29 | (async() => { 30 | 31 | let screenshotNum = 1; 32 | 33 | const browser = await puppeteer.launch({headless: AUTO_TAB}); 34 | const page = await browser.newPage(); 35 | await page.setViewport({width: 1200, height: 800, deviceScaleFactor: 2}); 36 | 37 | await page.exposeFunction('onTabToElement', async selector => { 38 | const el = await page.$(selector); 39 | console.log(`Taking screenshot of ${selector}`); 40 | const boundingBox = await el.boundingBox(); 41 | 42 | await el.screenshot({ 43 | path: `screenshot_${screenshotNum++}.png`, 44 | clip: { 45 | x: boundingBox.x - padding, 46 | y: boundingBox.y - padding, 47 | width: boundingBox.width + padding * 2, 48 | height: boundingBox.height + padding * 2, 49 | } 50 | }); 51 | }); 52 | 53 | await page.evaluateOnNewDocument(() => { 54 | window.addEventListener('keyup', e => { 55 | if (e.key === 'Tab') { 56 | const active = document.activeElement; 57 | const selector = active.getAttribute('id') ? `#${active.id}` : 58 | `${active.localName}.${active.className.replace(/\s/g, '.')}`; 59 | window.onTabToElement(selector); 60 | } 61 | }); 62 | }); 63 | 64 | await page.goto(url); 65 | 66 | if (AUTO_TAB) { 67 | await page.keyboard.press('Tab'); 68 | await page.waitFor(1000); 69 | await page.keyboard.press('Tab'); 70 | await page.waitFor(1000); 71 | await page.keyboard.press('Tab'); 72 | await page.waitFor(1000); 73 | await page.keyboard.press('Tab'); 74 | await page.waitFor(1000); 75 | 76 | await browser.close(); 77 | } 78 | 79 | // If not in AUTO_TAB mode, close the browser when you're down with ctrl+c. 80 | 81 | })(); 82 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | 4 | app.use(express.static('.')); 5 | 6 | app.listen(8080); 7 | -------------------------------------------------------------------------------- /side-by-side-pageload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Launches one or more URLs in different browser windows to visually compare 21 | * the page loads side-by-side. Options to center the windows on screen, 22 | * emulate mobile devices, and toggle CPU/Network throttling. 23 | * 24 | * Usage: 25 | * 26 | * node side-by-side-pageload.js -h 27 | */ 28 | 29 | const fs = require('fs'); 30 | const puppeteer = require('puppeteer'); 31 | const devices = require('puppeteer/DeviceDescriptors'); 32 | const nexus5X = devices['Nexus 5X']; 33 | 34 | const argv = require('yargs') 35 | .options({ 36 | 'mobile': { 37 | alias: 'm', 38 | describe: 'Emulate a mobile viewport', 39 | default: true, 40 | }, 41 | 'throttle': { 42 | describe: 'Throttles CPU by 4x and network to "Slow 3G"', 43 | default: true, 44 | }, 45 | 'center': { 46 | alias: 'c', 47 | describe: 'Centers the the windows on screen', 48 | default: true, 49 | }, 50 | 'url': { 51 | alias: 'u', 52 | describe: 'URL to load', 53 | demandOption: true, 54 | }, 55 | 'space': { 56 | alias: 's', 57 | describe: 'Spaces between windows (when emulating mobile)', 58 | default: 20, 59 | }, 60 | 'timeout': { 61 | alias: 't', 62 | describe: 'Timeout after page finish loading before closing the browers', 63 | default: 3000, 64 | }, 65 | }) 66 | .array('url') 67 | .help() 68 | .example('$0 --url https://devwebfeed.appspot.com https://devwebfeed.appspot.com/ssr') 69 | .example('$0 --no-throttle --no-mobile -u https://devwebfeed.appspot.com https://devwebfeed.appspot.com/ssr') 70 | .example('$0 -u https://www.bing.com/ https://www.google.com/ https://www.yahoo.com/') 71 | .wrap(null) 72 | .argv; 73 | 74 | const urls = argv.url.length ? argv.url : [ 75 | 'https://devwebfeed.appspot.com/', 76 | 'https://devwebfeed.appspot.com/ssr', 77 | ]; 78 | 79 | const CENTER_WINDOWS_ON_SCREEN = argv.center; 80 | const SPACE_BETWEEN_WINDOWS = argv.space; 81 | const MOBILE = argv.mobile; 82 | const THROTTLE = argv.throttle; 83 | const TIMEOUT_AFTER_LOAD = argv.timeout; 84 | const DEFAULT_VIEWPORT = {width: 1000, height: 800, deviceScaleFactor: 2}; 85 | 86 | const sleep = (timeout) => new Promise(r => setTimeout(r, timeout)); 87 | 88 | async function launch(position, screen) { 89 | const totalSpacerWidthAddition = SPACE_BETWEEN_WINDOWS * (urls.length - 1); 90 | const totalWidthOfWindows = urls.length * DEFAULT_VIEWPORT.width; 91 | const totalWidthOfWindowsWithSpacers = totalWidthOfWindows + totalSpacerWidthAddition; 92 | 93 | const centerScreenX = screen.width / 2; 94 | const centerScreenY = screen.height / 2; 95 | 96 | let dx = DEFAULT_VIEWPORT.width * position; 97 | dx += SPACE_BETWEEN_WINDOWS * position; 98 | 99 | const x = Math.floor(centerScreenX - (totalWidthOfWindowsWithSpacers / 2) + dx); 100 | const y = Math.floor(centerScreenY - (DEFAULT_VIEWPORT.height / 2)); 101 | 102 | const browser = await puppeteer.launch({ 103 | headless: false, 104 | args: [ 105 | `--window-size=${DEFAULT_VIEWPORT.width},${DEFAULT_VIEWPORT.height}`, 106 | CENTER_WINDOWS_ON_SCREEN ? `--window-position=${x},${y}` : `--window-position=${dx},0`, 107 | ], 108 | }); 109 | 110 | const page = await browser.newPage(); 111 | if (MOBILE) { 112 | await page.emulate(nexus5X); 113 | } else { 114 | await page.setViewport(DEFAULT_VIEWPORT); 115 | } 116 | 117 | if (THROTTLE) { 118 | const client = await page.target().createCDPSession(); 119 | // Emulate "Slow 3G" according to WebPageTest. 120 | await client.send('Network.emulateNetworkConditions', { 121 | offline: false, 122 | latency: 400, 123 | downloadThroughput: Math.floor(400 * 1024 / 8), // 400 Kbps 124 | uploadThroughput: Math.floor(400 * 1024 / 8) // 400 Kbps 125 | }); 126 | await client.send('Emulation.setCPUThrottlingRate', {rate: 4}); 127 | } 128 | 129 | return page; 130 | } 131 | 132 | (async () => { 133 | 134 | const browser = await puppeteer.launch(); 135 | const page = await browser.newPage(); 136 | 137 | const screen = await page.evaluate(() => { 138 | return {width: window.screen.availWidth, height: window.screen.availHeight}; 139 | }); 140 | await browser.close(); 141 | 142 | // Take up full desktop space or emulate mobile. 143 | DEFAULT_VIEWPORT.width = MOBILE ? nexus5X.viewport.width : Math.floor(screen.width / urls.length); 144 | DEFAULT_VIEWPORT.height = MOBILE ? nexus5X.viewport.height : screen.height; 145 | 146 | const pages = await Promise.all(urls.map((url, i) => launch(i, screen))); 147 | 148 | const start = Date.now(); 149 | 150 | const waitForPage = async pos => { 151 | const page = pages[pos]; 152 | const url = urls[pos]; 153 | return page.goto(url, {waitUntil: 'networkidle2'}) 154 | .then(() => Date.now()); 155 | }; 156 | 157 | const stopTimes = await Promise.all(urls.map((url, i) => waitForPage(i))); 158 | stopTimes.forEach((stopTime, i) => console.log(`Page ${i + 1} took ${stopTime - start} ms to reach network idle`)); 159 | 160 | await sleep(TIMEOUT_AFTER_LOAD); 161 | 162 | await Promise.all(pages.map(page => page.browser().close())); 163 | 164 | })(); 165 | -------------------------------------------------------------------------------- /speech.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Uses the web speech synth API to make the browser talk. 21 | * 22 | * Run it: 23 | * node speech.js -t Hello there, my name is Jarvis. 24 | * node speech.js -t Read anything good lately? 25 | * CHROME_PATH=/path/to/chrome node speech.js -t hi and bye! 26 | */ 27 | 28 | const fs = require('fs'); 29 | const puppeteer = require('puppeteer'); 30 | 31 | const DEFAULT_TXT = 'Hello there, my name is Puppeteer. I am controlling your browser.'; 32 | 33 | const executablePath = process.env.CHROME_PATH || 34 | '/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary'; 35 | 36 | (async() => { 37 | 38 | const browser = await puppeteer.launch({ 39 | executablePath, // Note: need Chrome (not Chromium) to use non-default voices. 40 | headless: false, // Speech synth API doesn't work in headless. crbug.com/815388 41 | args: [ 42 | '--window-size=0,0', // Launch baby window for fun. 43 | '--window-position=0,0', 44 | '--enable-speech-dispatcher', // Needed for Linux? 45 | ], 46 | }); 47 | 48 | const page = await browser.newPage(); 49 | 50 | // Clever way to "communicate with page". Know when speech is done. 51 | page.on('console', async msg => { 52 | if (msg.text() === 'SPEECH_DONE') { 53 | await browser.close(); 54 | } 55 | }); 56 | 57 | const flagIdx = process.argv.findIndex(item => item === '-t'); 58 | const text = flagIdx === -1 ? DEFAULT_TXT : process.argv.slice(flagIdx + 1).join(' '); 59 | 60 | await page.evaluateOnNewDocument(text => window.TEXT2SPEECH = text, text); 61 | 62 | const html = fs.readFileSync('./html/speech_synth.html', {encoding: 'utf-8'}); 63 | // Cause a navigation so the evaluateOnNewDocument kicks in. 64 | await page.goto(`data:text/html,${html}`); 65 | 66 | const button = await page.$('button'); 67 | button.click(); 68 | 69 | })(); 70 | -------------------------------------------------------------------------------- /verify_download.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Shows how to click a file download link and verify that the file gets 21 | * downloaded to the expected download location in the filesystem. Typically, 22 | * ~/Downloads. 23 | * Note: this approach only works in headful Chrome. 24 | * 25 | * Install: 26 | * npm i puppeteer 27 | * Run: 28 | * node verify_download.js 29 | */ 30 | 31 | const fs = require('fs'); 32 | const os = require('os'); 33 | const path = require('path'); 34 | const puppeteer = require('puppeteer'); 35 | 36 | const DOWNLOAD_PATH = path.resolve(__dirname, 'downloads'); 37 | 38 | /** 39 | * From @xprudhomme. 40 | * Check if file exists, watching containing directory meanwhile. 41 | * Resolve if the file exists, or if the file is created before the timeout 42 | * occurs. 43 | * @param {string} filePath 44 | * @param {integer} timeout 45 | * @returns {!Promise} Resolves when file has been created. Rejects 46 | * if timeout is reached. 47 | */ 48 | function waitForFileExists(filePath, timeout=15000) { 49 | return new Promise((resolve, reject) => { 50 | const dir = path.dirname(filePath); 51 | const basename = path.basename(filePath); 52 | 53 | const watcher = fs.watch(dir, (eventType, filename) => { 54 | if (eventType === 'rename' && filename === basename) { 55 | clearTimeout(timer); 56 | watcher.close(); 57 | resolve(); 58 | } 59 | }); 60 | 61 | const timer = setTimeout(() => { 62 | watcher.close(); 63 | reject(new Error(' [checkFileExists] File does not exist, and was not created during the timeout delay.')); 64 | }, timeout); 65 | 66 | fs.access(filePath, fs.constants.R_OK, err => { 67 | if (!err) { 68 | clearTimeout(timer); 69 | watcher.close(); 70 | resolve(); 71 | } 72 | }); 73 | }); 74 | } 75 | 76 | (async() => { 77 | 78 | const browser = await puppeteer.launch(); 79 | 80 | const page = await browser.newPage(); 81 | 82 | // Change from the default ~/Downloads folder to our own. 83 | const client = await page.target().createCDPSession(); 84 | await client.send('Page.setDownloadBehavior', { 85 | behavior: 'allow', 86 | downloadPath: DOWNLOAD_PATH, 87 | }); 88 | 89 | const url = 'https://www.nseindia.com/products/content/equities/equities/homepage_eq.htm'; 90 | await page.goto(url); 91 | // Wait for main content area to have list of links. 92 | await page.waitForSelector('.main_content', {visible: true, timeout: 5000}); 93 | 94 | const downloadUrl = await page.evaluate(() => { 95 | const link = document.evaluate(`//a[text()="Short Selling (csv)"]`, document, 96 | null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; 97 | if (link) { 98 | // Prevent link from opening up in a new tab. Puppeteer won't respect 99 | // the Page.setDownloadBehavior on the new tab and the file ends up in the 100 | // default download folder. 101 | link.target = ''; 102 | link.click(); 103 | return link.href; 104 | } 105 | return null; 106 | }); 107 | 108 | if (!downloadUrl) { 109 | console.warn('Did not find link to download!'); 110 | await browser.close(); 111 | return; 112 | } 113 | 114 | // Wait for file response to complete. 115 | await new Promise(resolve => { 116 | page.on('response', async resp => { 117 | if (resp.url() === downloadUrl) { 118 | resolve(); 119 | } 120 | }); 121 | }); 122 | 123 | console.log('Downloaded.'); 124 | 125 | // Verify it's on the file system. 126 | await waitForFileExists(`${DOWNLOAD_PATH}/ShortSelling.csv`); 127 | console.log('Exists!'); 128 | 129 | await browser.close(); 130 | 131 | })(); 132 | -------------------------------------------------------------------------------- /verify_download2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Note: this approach only works in headful Chrome. 21 | * Another approach to verifying a file gets downloaded. Shows how to click a 22 | * file download link and verify that the file gets downloaded in the 23 | * chrome:downloads page. 24 | * 25 | * Install: 26 | * npm i puppeteer 27 | * Run: 28 | * node verify_download2.js 29 | */ 30 | 31 | const puppeteer = require('puppeteer'); 32 | const fs = require('fs'); 33 | const path = require('path'); 34 | const os = require('os'); 35 | 36 | const DOWNLOADS_FOLDER = `${os.homedir()}/Downloads`; 37 | 38 | /** 39 | * From @xprudhomme. 40 | * Check if file exists, watching containing directory meanwhile. 41 | * Resolve if the file exists, or if the file is created before the timeout 42 | * occurs. 43 | * @param {string} filePath 44 | * @param {integer} timeout 45 | * @returns {!Promise} Resolves when file has been created. Rejects 46 | * if timout is reached. 47 | */ 48 | function checkFileExists(filePath, timeout=15000) { 49 | return new Promise((resolve, reject) => { 50 | const dir = path.dirname(filePath); 51 | const basename = path.basename(filePath); 52 | 53 | const watcher = fs.watch(dir, (eventType, filename) => { 54 | if (eventType === 'rename' && filename === basename) { 55 | clearTimeout(timer); 56 | watcher.close(); 57 | resolve(); 58 | } 59 | }); 60 | 61 | const timer = setTimeout(() => { 62 | watcher.close(); 63 | reject(new Error(' [checkFileExists] File does not exist, and was not created during the timeout delay.')); 64 | }, timeout); 65 | 66 | fs.access(filePath, fs.constants.R_OK, err => { 67 | if (!err) { 68 | clearTimeout(timer); 69 | watcher.close(); 70 | resolve(); 71 | } 72 | }); 73 | }); 74 | } 75 | 76 | /** 77 | * @param {!Browser} browser 78 | * @param {string} url The URL of the download file to wait for. 79 | * @returns {!Promise} Metadata about the latest file in Download Manager. 80 | */ 81 | async function waitForFileToDownload(browser, url) { 82 | const downloadPage = await browser.newPage(); 83 | // Note: navigating to this page only works in headful chrome. 84 | await downloadPage.goto('chrome://downloads/'); 85 | 86 | // Wait for our download to show up in the list by matching on its url. 87 | const jsHandle = await downloadPage.waitForFunction(downloadUrl => { 88 | const manager = document.querySelector('downloads-manager'); 89 | const downloads = manager.items_.length; 90 | const lastDownload = manager.items_[0]; 91 | if (downloads && lastDownload.url === downloadUrl && 92 | lastDownload.state === 'COMPLETE') { 93 | return manager.items_[0]; 94 | } 95 | }, {polling: 100}, url); 96 | 97 | const fileMeta = await jsHandle.jsonValue(); 98 | 99 | await downloadPage.close(); 100 | 101 | return fileMeta; 102 | } 103 | 104 | /** 105 | * @param {!Browser} browser 106 | * @param {string} url The url of the page to navigate to. 107 | * @param {string} text The link with this text to find and click on the page. 108 | * @returns {!Promise} The download resource's url. 109 | */ 110 | async function clickDownloadLink(browser, url, text) { 111 | const page = await browser.newPage(); 112 | await page.goto(url, {waitUntil: 'networkidle2'}); 113 | 114 | const downloadUrl = await page.evaluate((text) => { 115 | const link = document.evaluate(`//a[text()="${text}"]`, document, 116 | null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; 117 | if (link) { 118 | link.click(); 119 | return link.href; 120 | } 121 | return null; 122 | }, text); 123 | 124 | await page.close(); 125 | 126 | return downloadUrl; 127 | } 128 | 129 | (async() => { 130 | 131 | const browser = await puppeteer.launch({ 132 | headless: false, 133 | // dumpio: true, 134 | }); 135 | 136 | // TODO: setDownloadBehavior would be a good approach, as we could check 137 | // that the file shows up in the location specified by downloadPath. However, 138 | // that arg doesn't currently work. 139 | // const client = await page.target().createCDPSession(); 140 | // await client.send('Page.setDownloadBehavior', { 141 | // behavior: 'allow', 142 | // downloadPath: path.resolve(__dirname, 'downloads'), 143 | // }); 144 | 145 | // await client.detach(); 146 | 147 | // 1. navigate to a page with a bunch links to download. 148 | // 2. click the "Short Selling (csv)" link on the page. The browser force downloads the file. 149 | const url = 'https://www.nseindia.com/products/content/equities/equities/homepage_eq.htm'; 150 | const downloadUrl = await clickDownloadLink(browser, url, 'Short Selling (csv)'); 151 | 152 | if (!downloadUrl) { 153 | console.error('Did not find download link!'); 154 | return; 155 | } 156 | 157 | // 3. Open chrome:downloads and wait for the file to be downloaded. 158 | const fileMeta = await waitForFileToDownload(browser, downloadUrl); 159 | console.log(`"${fileMeta.file_name}" was downloaded`); 160 | 161 | // 4. Optionally check that the file really ends up in the expected location 162 | // on the filesystem. 163 | const exists = await checkFileExists(`${DOWNLOADS_FOLDER}/${fileMeta.file_name}`); 164 | console.assert(exists, `${fileMeta.file_name} was not downloaded to correct location.`); 165 | 166 | await browser.close(); 167 | 168 | })(); -------------------------------------------------------------------------------- /verify_sw_caching.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | /** 20 | * Verify that page resources are being cached by service worker and served 21 | * from the cache on repeat visits. 22 | */ 23 | 24 | const chalk = require('chalk'); 25 | const puppeteer = require('puppeteer'); 26 | 27 | const URL = process.env.URL || 'https://www.chromestatus.com/features'; 28 | 29 | (async() => { 30 | 31 | const browser = await puppeteer.launch(); 32 | const page = await browser.newPage(); 33 | 34 | // page.on('console', msg => console.log(chalk.yellow('console'), msg.text())); 35 | 36 | console.log(chalk.cyan('Page: 1st load')); 37 | await page.goto(URL); 38 | // Wait for sw ready promise to resolve before moving on. This signals the sw 39 | // has installed and cached assets in the `install` event. 40 | await page.evaluate('navigator.serviceWorker.ready'); 41 | // Alternatively, wait for UI toast to popup signalling sw caching is done. 42 | // That's specific to this page's implementation though. 43 | // await page.waitForSelector('chromedash-toast[open]'); 44 | 45 | // Capture requests during 2nd load. 46 | const allRequests = new Map(); 47 | page.on('request', req => { 48 | allRequests.set(req.url(), req); 49 | }); 50 | 51 | // Could also go offline and verify requests don't 404. 52 | // await page.setOfflineMode(true); 53 | 54 | // Reload page to pick up any runtime caching done by the service worker. 55 | console.log(chalk.cyan('Page: 2nd load')); 56 | await page.reload({waitUntil: 'networkidle0'}); 57 | 58 | // Assert the page has a SW. 59 | console.assert(await page.evaluate('navigator.serviceWorker.controller'), 60 | 'page has active service worker'); 61 | 62 | console.log(chalk.cyan(`Requests made by ${URL}`), 63 | `(${chalk.green('✔ cached by sw')}, ${chalk.red('✕ not cached')})`); 64 | Array.from(allRequests.values()).forEach(req => { 65 | const NUM_CHARS = 75; 66 | const url = req.url().length > NUM_CHARS ? req.url().slice(0, NUM_CHARS) + '...' : req.url(); 67 | console.log(url, req.response().fromServiceWorker() ? chalk.green('✔') : chalk.red('✕')); 68 | }); 69 | 70 | await browser.close(); 71 | 72 | })(); 73 | -------------------------------------------------------------------------------- /view-source.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 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 | * @author ebidel@ (Eric Bidelman) 17 | */ 18 | 19 | // Curl a page and dump its markup. 20 | 21 | const puppeteer = require('puppeteer'); 22 | 23 | const URL = process.env.URL || 'https://www.chromestatus.com/features'; 24 | 25 | puppeteer.launch().then(async browser => { 26 | const page = await browser.newPage(); 27 | const response = await page.goto(URL); 28 | console.log(await response.text()); 29 | await browser.close(); 30 | }); 31 | --------------------------------------------------------------------------------