├── .github └── workflows │ ├── 4-deploying-ava.yaml │ ├── 4-deploying-cypress.yaml │ ├── 4-deploying-jest.yaml │ ├── 4-deploying-puppeteer.yaml │ ├── 4-deploying-testcafe.yaml │ ├── 4-deploying-trace-and-har.yaml │ ├── 4-deploying-videos-screenshots.yaml │ ├── 4-deploying-videos-simple.yaml │ └── lighthouse-ci.yaml ├── .gitignore ├── 1-testing ├── 1-taking-screenshots │ ├── ava │ │ ├── package.json │ │ ├── readme.md │ │ ├── test.js │ │ └── with-page.js │ ├── cypress │ │ ├── cypress │ │ │ ├── integration │ │ │ │ └── examples │ │ │ │ │ └── screenshot.js │ │ │ ├── plugins │ │ │ │ └── index.js │ │ │ └── support │ │ │ │ ├── commands.js │ │ │ │ └── index.js │ │ ├── package.json │ │ └── readme.md │ ├── jest │ │ ├── package.json │ │ ├── readme.md │ │ └── test.js │ ├── playwright │ │ ├── index.js │ │ ├── package.json │ │ └── readme.md │ ├── puppeteer │ │ ├── index.js │ │ ├── package.json │ │ └── readme.md │ ├── testcafe │ │ ├── index.js │ │ ├── package.json │ │ └── readme.md │ └── webdriver │ │ ├── index.js │ │ ├── package.json │ │ └── readme.md ├── 2-basic-assertions │ ├── cypress │ │ ├── cypress │ │ │ ├── integration │ │ │ │ └── examples │ │ │ │ │ └── web-search.js │ │ │ ├── plugins │ │ │ │ └── index.js │ │ │ └── support │ │ │ │ ├── commands.js │ │ │ │ └── index.js │ │ ├── package.json │ │ └── readme.md │ ├── puppeteer │ │ ├── index.js │ │ ├── package.json │ │ └── readme.md │ ├── testcafe │ │ ├── index.js │ │ ├── package.json │ │ └── readme.md │ └── webdriver │ │ ├── index.js │ │ ├── package.json │ │ └── readme.md ├── 3-quick-recipes │ ├── assets │ │ └── .gitkeep │ ├── create-pdf.js │ ├── emulate-device.js │ ├── index.js │ ├── intercept-request.js │ ├── mock-response-sainsburys.json │ ├── package.json │ ├── readme.md │ └── type-text.js ├── 4-selectors │ ├── index.js │ ├── package.json │ └── readme.md ├── 5-errors │ ├── ava │ │ ├── package.json │ │ ├── readme.md │ │ ├── test.js │ │ └── with-page.js │ ├── cypress │ │ ├── cypress │ │ │ ├── integration │ │ │ │ └── examples │ │ │ │ │ └── example-domain.js │ │ │ ├── plugins │ │ │ │ └── index.js │ │ │ ├── screenshots │ │ │ │ └── examples │ │ │ │ │ └── example-domain.js │ │ │ │ │ └── my tests -- example domain (failed).png │ │ │ └── support │ │ │ │ ├── commands.js │ │ │ │ └── index.js │ │ ├── package.json │ │ └── readme.md │ ├── jest │ │ ├── package.json │ │ ├── readme.md │ │ └── test.js │ ├── puppeteer │ │ ├── index.js │ │ ├── package.json │ │ └── readme.md │ ├── testcafe │ │ ├── index.js │ │ ├── package.json │ │ └── readme.md │ └── webdriver │ │ ├── index.js │ │ ├── package.json │ │ └── readme.md ├── 6-test-generators │ ├── .qawolf │ │ └── myWikipediaTest.test.js │ ├── package.json │ ├── qawolf.config.js │ └── readme.md ├── 7-payment-checkout │ ├── index.js │ ├── package.json │ └── readme.md └── 8-bdd │ ├── features │ ├── maths.feature │ └── support │ │ ├── steps.js │ │ └── world.js │ ├── package.json │ └── readme.md ├── 2-scraping ├── 1-no-code │ └── readme.md ├── 10-captcha │ ├── .env.sample │ ├── index.js │ ├── package.json │ └── readme.md ├── 2-page-metadata │ ├── index.js │ ├── package.json │ └── readme.md ├── 3-infinite-scrolling │ ├── index.js │ ├── package.json │ └── readme.md ├── 4-tweet-screenshot │ ├── index.js │ ├── package.json │ └── readme.md ├── 5-selective-scraping │ ├── index.js │ ├── package.json │ └── readme.md ├── 6-products-extractor │ ├── index.js │ ├── package.json │ ├── readme.md │ └── views │ │ ├── index.html │ │ ├── layout.html │ │ └── search.html ├── 7-wiki-philosophy │ ├── clean.js │ ├── index.js │ ├── package.json │ └── readme.md ├── 8-product-price-checker │ ├── .env.sample │ ├── index.js │ ├── package.json │ └── readme.md └── 9-offline-websites │ ├── index.js │ ├── package.json │ └── readme.md ├── 3-auditing ├── 1-lighthouse │ ├── README.md │ ├── index.js │ ├── package.json │ ├── pics │ │ ├── all-checks-passing.png │ │ ├── ga-fail-log.png │ │ ├── incomplete-checks.png │ │ ├── lh-dashboard-third-party-increase.png │ │ ├── lh-single-report.png │ │ ├── lhci-dashboard-changes.png │ │ ├── lhci-dashboard-overview.png │ │ └── some-checks-not-successful.png │ └── views │ │ ├── example.html │ │ ├── index.html │ │ └── layout.html ├── 2-lighthouse-custom-audit │ ├── amazon-audit.js │ ├── custom-config.js │ ├── package.json │ ├── product-price-gatherer.js │ └── readme.md ├── 3-visual-testing │ ├── amazon-cat-mug │ │ ├── .env.sample │ │ ├── index.js │ │ ├── package.json │ │ └── readme.md │ ├── modern-devtools │ │ ├── .env.sample │ │ ├── actions.js │ │ ├── index.js │ │ ├── package.json │ │ └── readme.md │ └── single-element │ │ ├── index.js │ │ ├── package.json │ │ └── readme.md ├── 4-code-coverage │ ├── index.js │ ├── package.json │ └── readme.md └── 5-devtools-trace │ ├── index.js │ ├── package.json │ ├── profile.json │ └── readme.md ├── 4-deploying ├── 1-github-actions │ ├── ava │ │ ├── package.json │ │ ├── preview.png │ │ ├── readme.md │ │ ├── test.js │ │ └── with-page.js │ ├── cypress │ │ ├── cypress-bot-comment-fail.png │ │ ├── cypress-bot-comment-pass.png │ │ ├── cypress.json │ │ ├── cypress │ │ │ ├── fixtures │ │ │ │ └── example.json │ │ │ ├── integration │ │ │ │ └── examples │ │ │ │ │ └── example-test.js │ │ │ ├── plugins │ │ │ │ └── index.js │ │ │ └── support │ │ │ │ ├── commands.js │ │ │ │ └── index.js │ │ ├── package.json │ │ ├── permission.png │ │ ├── preview.png │ │ └── readme.md │ ├── jest │ │ ├── package.json │ │ ├── preview.png │ │ ├── readme.md │ │ └── test.js │ ├── puppeteer │ │ ├── index.js │ │ ├── package.json │ │ ├── preview.png │ │ ├── readme.md │ │ ├── test-1.js │ │ ├── test-2.js │ │ └── test-3.js │ └── testcafe │ │ ├── index.js │ │ ├── package.json │ │ ├── preview.png │ │ └── readme.md ├── 2-vps │ ├── index.js │ ├── package.json │ └── readme.md ├── 3-videos-simple │ ├── index.js │ ├── package.json │ └── readme.md ├── 4-videos-screenshots │ ├── index.js │ ├── package.json │ ├── readme.md │ ├── test-1.js │ ├── test-2.js │ └── test-3.js ├── 5-trace-and-har │ ├── index.js │ ├── package.json │ └── readme.md └── 6-serverless │ ├── functions │ ├── hello.js │ └── screenshot.js │ ├── index.html │ └── readme.md ├── 5-debugging ├── 1-page-console │ ├── index.js │ ├── package.json │ └── readme.md ├── 2-node-repl │ ├── index.js │ ├── package.json │ └── readme.md ├── 3-development-environment │ ├── index.js │ ├── package.json │ ├── public │ │ ├── index.html │ │ └── styles.css │ └── readme.md ├── 4-devtools │ ├── index.js │ ├── package.json │ └── readme.md └── 5-live-reload │ ├── index.js │ ├── open.js │ ├── package.json │ └── readme.md ├── docs ├── CNAME ├── amazon │ └── cat-mug │ │ ├── index.html │ │ ├── index_files │ │ ├── 01j2xsQ1yML.css │ │ ├── 01mqQVb87-L.css │ │ ├── 01r8lpNJhRL.css │ │ ├── 11hlEWdpPvL._RC_41CyUWqi4BL.css_.css │ │ ├── 31I0VVdQ1VL._AC_SR320,320_.jpg │ │ ├── 31SNqepeVzL._AC_SR320,320_.jpg │ │ ├── 31d6CWrLLNL._AC_US40_.jpg │ │ ├── 31dGvnzpm9L._AC_SR320,320_.jpg │ │ ├── 31dOoxaK6mL.css │ │ ├── 31lOd+WyJML._AC_SR320,320_.jpg │ │ ├── 31lp66Pk0rL._AC_SR320,320_.jpg │ │ ├── 31nGbYwCw1L._AC_SR320,320_.jpg │ │ ├── 31oIYaNaqlL._AC_US40_.jpg │ │ ├── 31qOgP7eV7L._AC_SR320,320_.jpg │ │ ├── 31zbpRzA6qL._AC_SR320,320_.jpg │ │ ├── 360_icon_73x73v2._CB485971279_SS40_FMpng_RI_.png │ │ ├── 411zcSwEStL._AC_SR320,320_.jpg │ │ ├── 4127V-q6txL._AC_US40_.jpg │ │ ├── 4179Re3+b7L._AC_US40_.jpg │ │ ├── 418lRtVfxuL._AC_SR320,320_.jpg │ │ ├── 41HWZW3GRML._AC_SR320,320_.jpg │ │ ├── 41JJyKcWHaL._AC_SR320,320_.jpg │ │ ├── 41N-vhjKtcL.css │ │ ├── 41PaSVEJ-jL._AC_SR320,320_.jpg │ │ ├── 41R8U8rQ3GL._AC_SR320,320_.jpg │ │ ├── 41VccVZJOLL._AC_SR320,320_.jpg │ │ ├── 41aclEB6i1L._AC_SR320,320_.jpg │ │ ├── 41fwczmGpCL._AC_SR320,320_.jpg │ │ ├── 41mOR1Fh85L._AC_SR320,320_.jpg │ │ ├── 41usTtryBgL._AC_US40_.jpg │ │ ├── 51B2bnyDy3L._AC_SR320,320_.jpg │ │ ├── 51MYD1rWJoL._AC_SR320,320_.jpg │ │ ├── 51NWD6WHFDL._AC_US40_.jpg │ │ ├── 51NfLtDS9mL._RC_01oxUylwjHL.css_.css │ │ ├── 51TwTYMESaL._AC_US40_.jpg │ │ ├── 51YPCqzNCyL._AC_SS350_.jpg │ │ ├── 51f+yB2tzoL._AC_UL320_SR320,320_.jpg │ │ ├── 51vmGJoCi-L._AC_SR320,320_.jpg │ │ ├── 61O0M6uTnSL._CR500,0,999,999_UX175.jpg │ │ ├── 61O5DXTLf1L._AC_SS350_.jpg │ │ ├── 61O5DXTLf1L._AC_UL320_SR320,320_.jpg │ │ ├── 61bQZDJewOL._CR0,204,1224,1224_UX175.jpg │ │ ├── 61hRdmo7xCL._AC_SS350_.jpg │ │ ├── 61hRdmo7xCL._AC_SX679_.jpg │ │ ├── 61hRdmo7xCL._AC_UL115_.jpg │ │ ├── 61uVRSyOMFL._AC_SX679_.jpg │ │ ├── 61vBkxFFq-L._AC_UL320_SR320,320_.jpg │ │ ├── 61xFVZdiNhL._AC_SL1500_.jpg │ │ ├── 61xFVZdiNhL._AC_SX679_.jpg │ │ ├── 61zkIV+5lXL._AC_UL320_SR320,320_.jpg │ │ ├── 71-C+L-rmtL._AC_UL320_SR320,320_.jpg │ │ ├── 710BRvY2H7L._AC_UL115_.jpg │ │ ├── 719z0s1QRvL._AC_SX679_.jpg │ │ ├── 71REHWzcBuL._AC_SX679_.jpg │ │ ├── 71T-jtV1+4L._AC_SS350_.jpg │ │ ├── 71T-jtV1+4L._AC_UL115_.jpg │ │ ├── 71UXTFQCv4L._AC_SS350_.jpg │ │ ├── 71UXTFQCv4L._AC_UL320_SR320,320_.jpg │ │ ├── 71pxJBtmV6L._AC_SS350_.jpg │ │ ├── 71pxJBtmV6L._AC_UL320_SR320,320_.jpg │ │ ├── 71rVaCGCXEL._CR0,204,1224,1224_UX175.jpg │ │ ├── 71ySysGeNxL._AC_SX679_.jpg │ │ ├── 723a9837-75e6-4e95-bd8b-a083c040aee2._CR83,0,333,333_SX48_.jpg │ │ ├── 81p0H9wZZyL._AC_SX679_.jpg │ │ ├── beacon._CB485971591_(1).css │ │ ├── beacon._CB485971591_.css │ │ ├── default._CR0,0,1024,1024_SX48_.png │ │ ├── ec4311a4-c34b-4dec-b29d-61dc44eee005._CR0,0,375,375_SX48_.jpg │ │ ├── grey-pixel.gif │ │ ├── loadIndicator-large._CB485943906_.gif │ │ ├── loading-4x-gray._CB485916920_.gif │ │ ├── nav-sprite-global_bluebeacon-1x_optimized_layout1._CB468670774_.png │ │ ├── renamed3.css │ │ ├── renamed4.css │ │ ├── renamed5.css │ │ ├── renamed6.css │ │ ├── review-lightbox-combined._CB485923683_.css │ │ ├── secured-ssl._CB485936932_.png │ │ └── vse_play_icon_2x.png │ │ └── main.js ├── index.html └── readme.md ├── netlify.toml ├── package-lock.json ├── package.json └── readme.md /.github/workflows/4-deploying-ava.yaml: -------------------------------------------------------------------------------- 1 | name: Runs ava tests (module 4) 2 | on: 3 | push: 4 | branches: [ master ] 5 | paths: 6 | - '4-deploying/1-github-actions/ava/**' 7 | - '.github/workflows/4-deploying-ava.yaml' 8 | pull_request: 9 | branches: [ master ] 10 | paths: 11 | - '4-deploying/1-github-actions/ava/**' 12 | - '.github/workflows/4-deploying-ava.yaml' 13 | jobs: 14 | ava: 15 | name: Execute ava 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use latest Node.js 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: '*' 25 | 26 | - name: Cache node modules 27 | uses: actions/cache@v2 28 | with: 29 | path: ~/.npm 30 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 31 | restore-keys: ${{ runner.os }}-node- 32 | 33 | - name: npm ci 34 | run: npm ci 35 | 36 | - name: run tests with ava 37 | run: | 38 | npm run --silent --prefix 4-deploying/1-github-actions/ava start -------------------------------------------------------------------------------- /.github/workflows/4-deploying-cypress.yaml: -------------------------------------------------------------------------------- 1 | name: Runs cypress tests (module 4) 2 | on: 3 | push: 4 | branches: [ master ] 5 | paths: 6 | - '4-deploying/1-github-actions/cypress/**' 7 | - '.github/workflows/4-deploying-cypress.yaml' 8 | pull_request: 9 | branches: [ master ] 10 | paths: 11 | - '4-deploying/1-github-actions/cypress/**' 12 | - '.github/workflows/4-deploying-cypress.yaml' 13 | jobs: 14 | cypress: 15 | name: Execute cypress 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use latest Node.js 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: '*' 25 | 26 | - name: Cache node modules 27 | uses: actions/cache@v2 28 | with: 29 | path: ~/.npm 30 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 31 | restore-keys: ${{ runner.os }}-node- 32 | 33 | - name: npm ci 34 | run: npm ci 35 | 36 | - name: run cypress tests 37 | run: npm run --silent --prefix 4-deploying/1-github-actions/cypress start 38 | env: 39 | CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} -------------------------------------------------------------------------------- /.github/workflows/4-deploying-jest.yaml: -------------------------------------------------------------------------------- 1 | name: Runs jest tests (module 4) 2 | on: 3 | push: 4 | branches: [ master ] 5 | paths: 6 | - '4-deploying/1-github-actions/jest/**' 7 | - '.github/workflows/4-deploying-jest.yaml' 8 | pull_request: 9 | branches: [ master ] 10 | paths: 11 | - '4-deploying/1-github-actions/jest/**' 12 | - '.github/workflows/4-deploying-jest.yaml' 13 | jobs: 14 | jest: 15 | name: Execute jest 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use latest Node.js 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: '*' 25 | 26 | - name: Cache node modules 27 | uses: actions/cache@v2 28 | with: 29 | path: ~/.npm 30 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 31 | restore-keys: ${{ runner.os }}-node- 32 | 33 | - name: npm ci 34 | run: npm ci 35 | 36 | - name: runs tests with jest 37 | run: npm run --silent --prefix 4-deploying/1-github-actions/jest start 38 | env: 39 | CI: true -------------------------------------------------------------------------------- /.github/workflows/4-deploying-puppeteer.yaml: -------------------------------------------------------------------------------- 1 | name: Runs puppeteer tests (module 4) 2 | on: 3 | push: 4 | branches: [ master ] 5 | paths: 6 | - '4-deploying/1-github-actions/puppeteer/**' 7 | - '.github/workflows/4-deploying-puppeteer.yaml' 8 | pull_request: 9 | branches: [ master ] 10 | paths: 11 | - '4-deploying/1-github-actions/puppeteer/**' 12 | - '.github/workflows/4-deploying-puppeteer.yaml' 13 | jobs: 14 | pptr: 15 | name: Execute puppeteer 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use latest Node.js 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: '*' 25 | 26 | - name: Cache node modules 27 | uses: actions/cache@v2 28 | with: 29 | path: ~/.npm 30 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 31 | restore-keys: ${{ runner.os }}-node- 32 | 33 | - name: npm ci 34 | run: npm ci 35 | 36 | - name: run puppeteer tests 37 | run: | 38 | npm run --silent --prefix 4-deploying/1-github-actions/puppeteer start -------------------------------------------------------------------------------- /.github/workflows/4-deploying-testcafe.yaml: -------------------------------------------------------------------------------- 1 | name: Runs testcafe tests (module 4) 2 | on: 3 | push: 4 | branches: [ master ] 5 | paths: 6 | - '4-deploying/1-github-actions/testcafe/**' 7 | - '.github/workflows/4-deploying-testcafe.yaml' 8 | pull_request: 9 | branches: [ master ] 10 | paths: 11 | - '4-deploying/1-github-actions/testcafe/**' 12 | - '.github/workflows/4-deploying-testcafe.yaml' 13 | jobs: 14 | testcafe: 15 | name: Execute testcafe 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use latest Node.js 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: '*' 25 | 26 | - name: Cache node modules 27 | uses: actions/cache@v2 28 | with: 29 | path: ~/.npm 30 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 31 | restore-keys: ${{ runner.os }}-node- 32 | 33 | - name: npm ci 34 | run: npm ci 35 | 36 | - name: runs testcafe tests 37 | run: | 38 | npm run --silent --prefix 4-deploying/1-github-actions/testcafe start -------------------------------------------------------------------------------- /.github/workflows/4-deploying-trace-and-har.yaml: -------------------------------------------------------------------------------- 1 | name: Runs the 5-trace-and-har module 2 | on: 3 | push: 4 | branches: [ master ] 5 | paths: 6 | - '4-deploying/5-trace-and-har/**' 7 | - '.github/workflows/4-deploying-trace-and-har.yaml' 8 | pull_request: 9 | branches: [ master ] 10 | paths: 11 | - '4-deploying/5-trace-and-har/**' 12 | - '.github/workflows/4-deploying-trace-and-har.yaml' 13 | jobs: 14 | pptr: 15 | name: Execute puppeteer 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use latest Node.js 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: '*' 25 | 26 | - name: Cache node modules 27 | uses: actions/cache@v2 28 | with: 29 | path: ~/.npm 30 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 31 | restore-keys: ${{ runner.os }}-node- 32 | 33 | - name: npm ci 34 | run: npm ci 35 | 36 | - name: run tests 37 | run: | 38 | npm run --prefix 4-deploying/5-trace-and-har start 39 | 40 | - name: Upload HAR file and DevTools trace 41 | if: ${{ success() }} 42 | uses: actions/upload-artifact@v1 43 | with: 44 | name: test-output 45 | path: 4-deploying/5-trace-and-har/test-output -------------------------------------------------------------------------------- /.github/workflows/4-deploying-videos-screenshots.yaml: -------------------------------------------------------------------------------- 1 | name: Runs the 4-videos-screenshots module 2 | on: 3 | push: 4 | branches: [ master ] 5 | paths: 6 | - '4-deploying/4-videos-screenshots/**' 7 | - '.github/workflows/4-deploying-videos-screenshots.yaml' 8 | pull_request: 9 | branches: [ master ] 10 | paths: 11 | - '4-deploying/4-videos-screenshots/**' 12 | - '.github/workflows/4-deploying-videos-screenshots.yaml' 13 | jobs: 14 | pptr: 15 | name: Execute playwright with videos and screenshots 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use latest Node.js 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: '*' 25 | 26 | - name: Cache node modules 27 | uses: actions/cache@v2 28 | with: 29 | path: ~/.npm 30 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 31 | restore-keys: ${{ runner.os }}-node- 32 | 33 | - name: npm ci 34 | run: npm ci 35 | 36 | - name: run tests 37 | run: | 38 | npm run --silent --prefix 4-deploying/4-videos-screenshots start 39 | 40 | - name: Upload videos and screenshots 41 | if: ${{ always() }} 42 | uses: actions/upload-artifact@v1 43 | with: 44 | name: test-output 45 | path: 4-deploying/4-videos-screenshots/test-output -------------------------------------------------------------------------------- /.github/workflows/4-deploying-videos-simple.yaml: -------------------------------------------------------------------------------- 1 | name: Runs the 3-videos-simple module 2 | on: 3 | push: 4 | branches: [ master ] 5 | paths: 6 | - '4-deploying/3-videos-simple/**' 7 | - '.github/workflows/4-deploying-videos-simple.yaml' 8 | pull_request: 9 | branches: [ master ] 10 | paths: 11 | - '4-deploying/3-videos-simple/**' 12 | - '.github/workflows/4-deploying-videos-simple.yaml' 13 | jobs: 14 | pptr: 15 | name: Execute playwright 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use latest Node.js 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: '*' 25 | 26 | - name: Cache node modules 27 | uses: actions/cache@v2 28 | with: 29 | path: ~/.npm 30 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 31 | restore-keys: ${{ runner.os }}-node- 32 | 33 | - name: npm ci 34 | run: npm ci 35 | 36 | - name: run tests 37 | run: | 38 | npm run --silent --prefix 4-deploying/3-videos-simple start 39 | 40 | - name: Upload videos 41 | if: ${{ always() }} 42 | uses: actions/upload-artifact@v1 43 | with: 44 | name: test-output 45 | path: 4-deploying/3-videos-simple/test-output -------------------------------------------------------------------------------- /.github/workflows/lighthouse-ci.yaml: -------------------------------------------------------------------------------- 1 | name: Run Lighthouse CI 2 | on: 3 | push: 4 | branches: [ master ] 5 | paths: 6 | - '3-auditing/1-lighthouse/**' 7 | - '.github/workflows/lighthouse-ci.yaml' 8 | pull_request: 9 | branches: [ master ] 10 | paths: 11 | - '3-auditing/1-lighthouse/**' 12 | - '.github/workflows/lighthouse-ci.yaml' 13 | jobs: 14 | lhci: 15 | name: Lighthouse CI 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use latest Node.js 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: '*' 25 | 26 | - name: Cache node modules 27 | uses: actions/cache@v2 28 | with: 29 | path: ~/.npm 30 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 31 | restore-keys: ${{ runner.os }}-node- 32 | 33 | - name: npm ci 34 | run: npm ci 35 | 36 | - name: run Lighthouse CI 37 | env: 38 | LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} 39 | run: | 40 | npm run --prefix 3-auditing/1-lighthouse lighthouse-private-with-error || echo "LHCI failed!" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | .nyc_output 3 | 4 | .DS_Store 5 | 6 | # Compiled source # 7 | ################### 8 | *.com 9 | *.class 10 | *.dll 11 | *.exe 12 | *.o 13 | *.so 14 | 15 | # Packages # 16 | ############ 17 | # it's better to unpack these files and commit the raw source 18 | # git has its own built in compression methods 19 | *.7z 20 | *.dmg 21 | *.gz 22 | *.iso 23 | *.jar 24 | *.rar 25 | *.tar 26 | *.zip 27 | 28 | # Logs and databases # 29 | ###################### 30 | *.log 31 | *.sql 32 | *.sqlite 33 | 34 | # OS generated files # 35 | ###################### 36 | .DS_Store 37 | .DS_Store? 38 | ._* 39 | .Spotlight-V100 40 | .Trashes 41 | Icon? 42 | ehthumbs.db 43 | Thumbs.db 44 | 45 | 46 | # Node specific # 47 | ################# 48 | lib-cov 49 | *.seed 50 | *.log 51 | *.dat 52 | *.out 53 | *.pid 54 | *.gz 55 | pids 56 | logs 57 | #results 58 | npm-debug.log 59 | node_modules/ 60 | 61 | 62 | differencify_reports 63 | .netlify/ 64 | sftp-config* 65 | *.sublime-* 66 | *.screenflow 67 | *.mp4 68 | *.key 69 | .env 70 | .lighthouseci 71 | 1-testing/3-quick-recipes/assets/* 72 | !1-testing/3-quick-recipes/assets/.gitkeep 73 | /wip 74 | 1-testing/**/*.png 75 | 2-scraping/**/*.png 76 | 3-auditing/2-lighthouse-custom-audit/*.report.html 77 | 4-deploying/4-videos-screenshots/test-output/*.png 78 | 4-deploying/5-trace-and-har/test-output/*.har 79 | 4-deploying/5-trace-and-har/test-output/*.json -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/ava/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "../../../node_modules/.bin/ava" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/ava/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | 11 | # or 12 | 13 | npx ava 14 | ``` -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/ava/test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import withPage from './with-page.js'; 3 | 4 | const url = 'https://example.com/'; 5 | 6 | test('page title should be correct', withPage, async (t, page) => { 7 | await page.goto(url); 8 | await page.setViewport({width: 1280, height: 720}); 9 | await page.screenshot({path: 'ava-screenshot.png'}); 10 | 11 | t.true((await page.title()).includes('Example Domain'), 'Page title is correct'); 12 | }); 13 | -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/ava/with-page.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | async function withPage(t, run) { 4 | const browser = await puppeteer.launch(); 5 | const page = await browser.newPage(); 6 | try { 7 | await run(t, page); 8 | } finally { 9 | await page.close(); 10 | await browser.close(); 11 | } 12 | } 13 | 14 | export default withPage; -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/cypress/cypress/integration/examples/screenshot.js: -------------------------------------------------------------------------------- 1 | describe('my tests', () => { 2 | it('takes a screenshot', () => { 3 | cy.visit('https://example.com'); 4 | cy.screenshot(); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/cypress/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | }; 22 | -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/cypress/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/cypress/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/cypress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "scripts": { 4 | "start": "../../../node_modules/.bin/cypress run --config-file false" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/cypress/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | 11 | # or 12 | 13 | npx cypress run --config-file false 14 | ``` -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/jest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "../../../node_modules/.bin/jest" 6 | }, 7 | "jest": { 8 | "preset": "jest-puppeteer" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/jest/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | 11 | # or 12 | 13 | npx jest 14 | ``` -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/jest/test.js: -------------------------------------------------------------------------------- 1 | describe('Example.com', () => { 2 | beforeAll(async () => { 3 | await page.goto('https://example.com'); 4 | }); 5 | 6 | it('Should have the correct title', async () => { 7 | await expect(page.title()).resolves.toMatch('Example Domain'); 8 | await page.screenshot({path: 'jest-screenshot.png'}); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/playwright/index.js: -------------------------------------------------------------------------------- 1 | import playwright from 'playwright'; 2 | 3 | function sleep(ms = 1000) {return new Promise((resolve) => setTimeout(resolve, ms))}; 4 | 5 | for (const browserType of ['chromium', 'firefox', 'webkit']) { 6 | console.log(`Loading ${browserType}`); 7 | const browser = await playwright[browserType].launch({ 8 | headless: false 9 | }); 10 | const context = await browser.newContext(); 11 | const page = await context.newPage(); 12 | await page.goto('https://example.com'); 13 | await page.screenshot({ path: `playwright-${browserType}-screenshot.png` }); 14 | await sleep(2000); 15 | await browser.close(); 16 | } -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/playwright/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/playwright/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/puppeteer/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | const browser = await puppeteer.launch({ 4 | headless: false 5 | }); 6 | 7 | const page = await browser.newPage(); 8 | 9 | await page.setViewport({ width: 1280, height: 720 }); 10 | await page.goto('https://example.com'); 11 | 12 | await page.screenshot({ path: 'screenshot.png' }); 13 | 14 | await browser.close(); 15 | -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/puppeteer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/puppeteer/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/testcafe/index.js: -------------------------------------------------------------------------------- 1 | fixture`Getting Started`.page`https://example.com`; 2 | 3 | test('My first test', async t => { 4 | await t.takeScreenshot({ 5 | fullPage: true, 6 | path: './testcafe-screenshot.png' 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/testcafe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "../../../node_modules/.bin/testcafe 'firefox:headless' index.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/testcafe/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | 11 | # or 12 | 13 | npx testcafe 'firefox:headless' index.js 14 | ``` -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/webdriver/index.js: -------------------------------------------------------------------------------- 1 | import 'chromedriver'; 2 | import webdriver from 'selenium-webdriver'; 3 | import {promises as fsPromise} from 'fs'; 4 | 5 | const {writeFile} = fsPromise; 6 | 7 | const driver = await new webdriver.Builder().forBrowser('chrome').build(); 8 | 9 | try { 10 | await driver.get('https://example.com'); 11 | const screenshot = await driver.takeScreenshot(); 12 | await writeFile('./webdriver-screenshot.png', screenshot, 'base64'); 13 | } finally { 14 | await driver.quit(); 15 | } -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/webdriver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /1-testing/1-taking-screenshots/webdriver/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` -------------------------------------------------------------------------------- /1-testing/2-basic-assertions/cypress/cypress/integration/examples/web-search.js: -------------------------------------------------------------------------------- 1 | describe('my tests', () => { 2 | it('takes a screenshot', () => { 3 | cy.visit('https://duckduckgo.com/'); 4 | 5 | cy.get('#search_form_input_homepage') 6 | .type('umar hansa'); 7 | 8 | cy.get('input[type="submit"]') 9 | .click(); 10 | 11 | // Cypress doesn't allow cross origins https://on.cypress.io/cross-origin-violation 12 | // cy.get(`a[href='https://umaar.com/']`).first().click(); 13 | 14 | cy.title().should('eq', 'umar hansa at DuckDuckGo'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /1-testing/2-basic-assertions/cypress/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | }; 22 | -------------------------------------------------------------------------------- /1-testing/2-basic-assertions/cypress/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /1-testing/2-basic-assertions/cypress/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /1-testing/2-basic-assertions/cypress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "scripts": { 4 | "start": "../../../node_modules/.bin/cypress run --config-file false" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /1-testing/2-basic-assertions/cypress/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | 11 | # or 12 | 13 | npx cypress run --config-file false 14 | ``` -------------------------------------------------------------------------------- /1-testing/2-basic-assertions/puppeteer/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | import assert from 'assert'; 3 | 4 | const selector = `a[href='https://umaar.com/']`; 5 | 6 | const browser = await puppeteer.launch({ 7 | headless: false 8 | }); 9 | const page = await browser.newPage(); 10 | await page.setViewport({ width: 1280, height: 720 }); 11 | await page.goto('https://duckduckgo.com/', { waitUntil: 'networkidle2' }) 12 | 13 | await page.type('#search_form_input_homepage', 'umar hansa', { delay: 100 }) 14 | 15 | await page.click('input[type="submit"]'); 16 | await page.waitForSelector(selector); 17 | 18 | await Promise.all([ 19 | page.waitForNavigation(), 20 | await page.click(selector) 21 | ]); 22 | 23 | const pageTitle = await page.title(); 24 | await browser.close(); 25 | 26 | console.log('Title: ', pageTitle); 27 | assert.strictEqual(pageTitle, 'Umar Hansa', 'Title is correct'); -------------------------------------------------------------------------------- /1-testing/2-basic-assertions/puppeteer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /1-testing/2-basic-assertions/puppeteer/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` -------------------------------------------------------------------------------- /1-testing/2-basic-assertions/testcafe/index.js: -------------------------------------------------------------------------------- 1 | fixture`Getting Started`.page`https://duckduckgo.com`; 2 | 3 | test('My first test', async t => { 4 | await t.typeText('#search_form_input_homepage', 'Umar Hansa'); 5 | await t.click('input[type="submit"]'); 6 | await t.click('a[href=\'https://umaar.com/\']'); 7 | const title = await t.eval(() => document.title); 8 | await t.expect(title).eql('Umar Hansa', 'Landed on the correct page'); 9 | }); 10 | -------------------------------------------------------------------------------- /1-testing/2-basic-assertions/testcafe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "../../../node_modules/.bin/testcafe 'firefox:headless' index.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /1-testing/2-basic-assertions/testcafe/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | 11 | # or 12 | 13 | npx testcafe 'firefox:headless' index.js 14 | ``` -------------------------------------------------------------------------------- /1-testing/2-basic-assertions/webdriver/index.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import 'chromedriver'; 4 | import webdriver from 'selenium-webdriver'; 5 | 6 | const {Builder, By, until} = webdriver; 7 | 8 | let driver = await new Builder().forBrowser('chrome').build(); 9 | 10 | try { 11 | await driver.get('https://duckduckgo.com/'); 12 | await driver.findElement(By.css('#search_form_input_homepage')).sendKeys('umar hansa'); 13 | await driver.findElement(By.css('input[type="submit"]')).click(); 14 | await driver.findElement(By.css(`a[href='https://umaar.com/']`)).click(); 15 | await driver.wait(until.titleIs('Umar Hansa')); 16 | 17 | const pageTitle = await driver.getTitle(); 18 | 19 | console.log('Title: ', pageTitle); 20 | assert.strictEqual(pageTitle, 'Umar Hansa', 'Title is correct'); 21 | } finally { 22 | await driver.quit(); 23 | } -------------------------------------------------------------------------------- /1-testing/2-basic-assertions/webdriver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /1-testing/2-basic-assertions/webdriver/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` -------------------------------------------------------------------------------- /1-testing/3-quick-recipes/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/1-testing/3-quick-recipes/assets/.gitkeep -------------------------------------------------------------------------------- /1-testing/3-quick-recipes/create-pdf.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import puppeteer from 'puppeteer'; 3 | 4 | const filePath = path.join(process.cwd(), 'assets', 'facebook.pdf'); 5 | 6 | const browser = await puppeteer.launch({headless: true}); 7 | const page = await browser.newPage(); 8 | 9 | await page.goto('https://facebook.com'); 10 | await page.pdf({ path: filePath, format: 'A4' }); 11 | 12 | await browser.close(); -------------------------------------------------------------------------------- /1-testing/3-quick-recipes/emulate-device.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import puppeteer from 'puppeteer'; 3 | 4 | const filePath = path.join(process.cwd(), 'assets', 'emulate-device.png'); 5 | 6 | const pixel2 = puppeteer.devices['Pixel 2'] 7 | const browser = await puppeteer.launch(); 8 | const page = await browser.newPage(); 9 | 10 | await page.emulate(pixel2); 11 | await page.goto('https://www.whatismybrowser.com/'); 12 | await page.screenshot({ path: filePath }); 13 | 14 | await browser.close(); -------------------------------------------------------------------------------- /1-testing/3-quick-recipes/index.js: -------------------------------------------------------------------------------- 1 | import './type-text.js'; 2 | import './create-pdf.js'; 3 | import './emulate-device.js'; 4 | import './intercept-request.js'; 5 | -------------------------------------------------------------------------------- /1-testing/3-quick-recipes/intercept-request.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import puppeteer from 'puppeteer'; 4 | 5 | const screenshotPath = path.join(process.cwd(), 'assets', 'sainsburys-screenshot.png'); 6 | 7 | const browser = await puppeteer.launch(); 8 | const page = await browser.newPage(); 9 | 10 | await page.setRequestInterception(true) 11 | 12 | page.on('request', async request => { 13 | if (request.url().includes('/product/v1/product')) { 14 | await request.respond({ 15 | status: 200, 16 | contentType: 'application/json', 17 | body: fs.readFileSync('mock-response-sainsburys.json') 18 | }); 19 | } else { 20 | await request.continue(); 21 | } 22 | }); 23 | 24 | await page.setViewport({ width: 1280, height: 800 }) 25 | await page.goto('https://www.sainsburys.co.uk/gol-ui/product/vita-coco-coconut-oil-500ml') 26 | await page.evaluate(() => { 27 | let cookieOverlay = document.querySelector('.onetrust-pc-dark-filter'); 28 | cookieOverlay.parentNode.removeChild(cookieOverlay); 29 | }); 30 | await page.screenshot({ path: screenshotPath, fullPage: true }) 31 | 32 | await browser.close(); 33 | -------------------------------------------------------------------------------- /1-testing/3-quick-recipes/mock-response-sainsburys.json: -------------------------------------------------------------------------------- 1 | {"products":[{"product_uid":"7847880","favourite_uid":null,"product_type":"BASIC","name":" 🔥️ 🔥️ Vita Coco Coconut Oil 500ml 🔥️ 🔥️","image":"https://www.sainsburys.co.uk/wcsstore/ExtendedSitesCatalogAssetStore/images/catalog/productImages/52/5060232810452/5060232810452_L.jpeg","image_zoom":null,"image_thumbnail":"https://www.sainsburys.co.uk/wcsstore/ExtendedSitesCatalogAssetStore/images/catalog/productImages/52/5060232810452/5060232810452_M.jpeg","image_thumbnail_small":"https://www.sainsburys.co.uk/wcsstore/ExtendedSitesCatalogAssetStore/images/catalog/productImages/52/5060232810452/5060232810452_S.jpeg","full_url":"https://www.sainsburys.co.uk/shop/gb/groceries/product/details/vita-coco-coconut-oil-500ml","unit_price":{"price":1.8,"measure":"ml","measure_amount":100},"retail_price":{"price":9,"measure":"unit"},"is_available":true,"promotions":[],"associations":[],"is_alcoholic":false,"is_spotlight":false,"is_intolerant":false,"is_mhra":false,"badges":[],"labels":[],"zone":null,"department":null,"reviews":{"is_enabled":true,"product_uid":"7847880","total":4,"average_rating":5},"breadcrumbs":[{"label":"Food cupboard","url":"gb/groceries/food-cupboard"},{"label":"Cooking ingredients \u0026 oils","url":"gb/groceries/cooking-ingredients-oils"},{"label":"Oils","url":"gb/groceries/oils-"},{"label":"All oils","url":"gb/groceries/all-oils"}],"details_html":"PGRpdiBpZD0ibWFpblBhcnQiPgo8ZGl2IGNsYXNzPSJpdGVtVHlwZUdyb3VwQ29udGFpbmVyIHByb2R1Y3RUZXh0IiBzdHlsZT0icGFnZS1icmVhay1pbnNpZGUgOiBhdm9pZCI+CjxoMz5EZXNjcmlwdGlvbjwvaDM+CjxkaXYgY2xhc3M9Im1lbW8iPgo8cD5Db2NvbnV0IE9pbDwvcD4KPC9kaXY+CjxkaXYgY2xhc3M9ImxvbmdUZXh0SXRlbXMiPgo8cD5PcmdhbmljPC9wPgo8cD5FeHRyYSB2aXJnaW48L3A+CjxwPkNvbGQgcHJlc3NlZDwvcD4KPHA+MTAwJSByYXc8L3A+CjxwPkVhdCBpdCwgd2VhciBpdCwgc3dlYXIgYnkgaXQ8L3A+CjxwPkdsdXRlbiBmcmVlPC9wPgo8L2Rpdj4KPGRpdiBjbGFzcz0iaXRlbVR5cGVHcm91cCI+CjxwIGNsYXNzPSJzdGF0ZW1lbnRzIj4KPHA+RVUgT3JnYW5pYzwvcD4KPHA+T3JnYW5pYyBGYXJtZXJzICYgR3Jvd2VycyAoT3JnYW5pYyBDZXJ0aWZpY2F0aW9uIFVLMik8L3A+CjwvcD4KPC9kaXY+CjwvZGl2Pgo8ZGl2IGNsYXNzPSJpdGVtVHlwZUdyb3VwQ29udGFpbmVyIHByb2R1Y3RUZXh0IiBzdHlsZT0icGFnZS1icmVhay1pbnNpZGUgOiBhdm9pZCI+CjxkaXYgY2xhc3M9Iml0ZW1UeXBlR3JvdXAiPgo8ZGl2IGNsYXNzPSJ0ZXh0dWFsTnV0cml0aW9uIj4KPGgzPk51dHJpdGlvbjwvaDM+CjxwPgo8c3Ryb25nPlRhYmxlIG9mIE51dHJpdGlvbmFsIEluZm9ybWF0aW9uPC9zdHJvbmc+CjwvcD4KPGRpdiB4bWxuczphc3A9InJlbW92ZSIgY2xhc3M9InRhYmxlV3JhcHBlciI+Cjx0YWJsZSBjbGFzcz0ibnV0cml0aW9uVGFibGUiPgo8dGhlYWQ+Cjx0cj4KPHRoIHNjb3BlPSJjb2wiPjwvdGg+PHRoIHNjb3BlPSJjb2wiPnBlciAxMDBnPC90aD4KPC90cj4KPC90aGVhZD4KPHRib2R5Pgo8dHI+Cjx0aCBzY29wZT0icm93IiBjbGFzcz0icm93SGVhZGVyIj5FbmVyZ3kgPC90aD48dGQ+MzI4MGtKICg3ODRrY2FsKTwvdGQ+CjwvdHI+Cjx0cj4KPHRoIHNjb3BlPSJyb3ciIGNsYXNzPSJyb3dIZWFkZXIiPkZhdCA8L3RoPjx0ZD45MWc8L3RkPgo8L3RyPgo8dHI+Cjx0aCBzY29wZT0icm93Ij4ob2Ygd2hpY2ggc2F0dXJhdGVzKTwvdGg+PHRkPjc4LjdnPC90ZD4KPC90cj4KPHRyPgo8dGggc2NvcGU9InJvdyI+KG9mIHdoaWNoIHBvbHl1bnNhdHVyYXRlcyk8L3RoPjx0ZD4xLjZnPC90ZD4KPC90cj4KPHRyPgo8dGggc2NvcGU9InJvdyI+KG9mIHdoaWNoIG1vbm91bnNhdHVyYXRlcyk8L3RoPjx0ZD41ZzwvdGQ+CjwvdHI+Cjx0cj4KPHRoIHNjb3BlPSJyb3ciIGNsYXNzPSJyb3dIZWFkZXIiPkNhcmJvaHlkcmF0ZSA8L3RoPjx0ZD4wZzwvdGQ+CjwvdHI+Cjx0cj4KPHRoIHNjb3BlPSJyb3ciPihvZiB3aGljaCBzdWdhcnMpPC90aD48dGQ+MGc8L3RkPgo8L3RyPgo8dHI+Cjx0aCBzY29wZT0icm93IiBjbGFzcz0icm93SGVhZGVyIj5Qcm90ZWluIDwvdGg+PHRkPjBnPC90ZD4KPC90cj4KPHRyPgo8dGggc2NvcGU9InJvdyIgY2xhc3M9InJvd0hlYWRlciI+U2FsdCA8L3RoPjx0ZD4wbWc8L3RkPgo8L3RyPgo8L3Rib2R5Pgo8L3RhYmxlPgo8L2Rpdj4KPGRpdiBjbGFzcz0ibmFtZVRleHRMb29rdXBzIj48L2Rpdj4KPC9kaXY+CjwvZGl2Pgo8L2Rpdj4KPGRpdiBjbGFzcz0iaXRlbVR5cGVHcm91cENvbnRhaW5lciBwcm9kdWN0VGV4dCIgc3R5bGU9InBhZ2UtYnJlYWstaW5zaWRlIDogYXZvaWQiPgo8ZGl2IGNsYXNzPSJpdGVtVHlwZUdyb3VwIj4KPGRpdiBjbGFzcz0ibG9uZ1RleHRJdGVtcyI+CjxoMyBjbGFzcz0iaXRlbUhlYWRlciI+SW5ncmVkaWVudHM8L2gzPgo8dWwgY2xhc3M9InByb2R1Y3RJbmdyZWRpZW50cyI+CjxsaT5PcmdhbmljIEV4dHJhIFZpcmdpbiBDb2NvbnV0IE9pbDwvbGk+CjwvdWw+CjwvZGl2Pgo8L2Rpdj4KPC9kaXY+CjxkaXYgY2xhc3M9Iml0ZW1UeXBlR3JvdXBDb250YWluZXIgcHJvZHVjdFRleHQiIHN0eWxlPSJwYWdlLWJyZWFrLWluc2lkZSA6IGF2b2lkIj4KPGgzPkRpZXRhcnkgSW5mb3JtYXRpb248L2gzPgo8ZGl2IGNsYXNzPSJpdGVtVHlwZUdyb3VwIj4KPHAgY2xhc3M9InN0YXRlbWVudHMiPgo8cD5PcmdhbmljPC9wPgo8L3A+CjxkaXYgY2xhc3M9Im1lbW8iPgo8cD48L3A+CjwvZGl2Pgo8ZGl2IGNsYXNzPSJuYW1lTG9va3VwcyI+CjxwPkZyZWUgRnJvbSBHbHV0ZW48L3A+CjwvZGl2Pgo8L2Rpdj4KPC9kaXY+CjwvZGl2PjxkaXYgaWQ9Im1haW5QYXJ0Ij4KPGRpdiBjbGFzcz0iaXRlbVR5cGVHcm91cENvbnRhaW5lciBwcm9kdWN0VGV4dCIgc3R5bGU9InBhZ2UtYnJlYWstaW5zaWRlIDogYXZvaWQiPgo8aDMgY2xhc3M9InByaW50T25seUJsb2NrIHBhcnRIZWFkIj5NYW51ZmFjdHVyZXI8L2gzPgo8ZGl2IGNsYXNzPSJpdGVtVHlwZUdyb3VwIj4KPGRpdiBjbGFzcz0ibWVtbyI+CjxwPlByb2R1Y2VkIGZvcjo8YnI+QWxsIE1hcmtldCBFdXJvcGUgTHRkLDxicj5QTyBCb3ggNzIwNjksPGJyPkxvbmRvbiw8YnI+RUMxUCAxSEosPGJyPlVLLjwvcD4KPC9kaXY+CjwvZGl2Pgo8L2Rpdj4KPGRpdiBjbGFzcz0iaXRlbVR5cGVHcm91cENvbnRhaW5lciBwcm9kdWN0VGV4dCIgc3R5bGU9InBhZ2UtYnJlYWstaW5zaWRlIDogYXZvaWQiPgo8aDMgY2xhc3M9InByaW50T25seUJsb2NrIHBhcnRIZWFkIj5QcmVwYXJhdGlvbjwvaDM+CjxkaXYgY2xhc3M9Iml0ZW1UeXBlR3JvdXAiPgo8ZGl2IGNsYXNzPSJtZW1vIj4KPHA+VGhlIG5ldyAiaW4tZ3JlZGllbnQiIGZvciBjb29raW5nLCBiYWtpbmcgYW5kIGZyeWluZzxicj5Hb2VzIGJleW9uZCB0aGUgY2FsbCBvZiBiZWF1dHkgYXMgYSBtb2lzdHVyaXNlciBhbmQgaGFpciBjb25kaXRpb25lcjxicj5BIGhlYWx0aGllciBzcHJlYWQ/IFdlJ2xsIHRvYXN0IHRvIHRoYXQhPC9wPgo8L2Rpdj4KPC9kaXY+CjwvZGl2Pgo8ZGl2IGNsYXNzPSJpdGVtVHlwZUdyb3VwQ29udGFpbmVyIHByb2R1Y3RUZXh0IiBzdHlsZT0icGFnZS1icmVhay1pbnNpZGUgOiBhdm9pZCI+CjxoMyBjbGFzcz0icHJpbnRPbmx5QmxvY2sgcGFydEhlYWQiPkNvdW50cnkgb2YgT3JpZ2luPC9oMz4KPGRpdiBjbGFzcz0iaXRlbVR5cGVHcm91cCI+CjxkaXYgY2xhc3M9Im5hbWVMb29rdXBzIj4KPHA+Q291bnRyeSBvZiBvcmlnaW46IFBoaWxpcHBpbmVzPC9wPgo8cD5QYWNrZWQgaW46IFVuaXRlZCBLaW5nZG9tPC9wPgo8L2Rpdj4KPGRpdiBjbGFzcz0ibmFtZVRleHRJdGVtcyI+CjxwPlByb2R1Y3Qgb2YgdGhlIFBoaWxpcHBpbmVzLiBQYWNrYWdlZCBpbiB0aGUgVUs8L3A+CjwvZGl2Pgo8L2Rpdj4KPC9kaXY+CjxkaXYgY2xhc3M9Iml0ZW1UeXBlR3JvdXBDb250YWluZXIgcHJvZHVjdFRleHQiIHN0eWxlPSJwYWdlLWJyZWFrLWluc2lkZSA6IGF2b2lkIj4KPGgzIGNsYXNzPSJwcmludE9ubHlCbG9jayBwYXJ0SGVhZCI+UGFja2FnaW5nPC9oMz4KPGRpdiBjbGFzcz0iaXRlbVR5cGVHcm91cCI+CjxkaXYgY2xhc3M9Im5hbWVMb29rdXBzIj4KPHA+SmFyPC9wPgo8L2Rpdj4KPC9kaXY+CjwvZGl2Pgo8ZGl2IGNsYXNzPSJpdGVtVHlwZUdyb3VwQ29udGFpbmVyIHByb2R1Y3RUZXh0IiBzdHlsZT0icGFnZS1icmVhay1pbnNpZGUgOiBhdm9pZCI+CjxoMyBjbGFzcz0icHJpbnRPbmx5QmxvY2sgcGFydEhlYWQiPlN0b3JhZ2U8L2gzPgo8ZGl2IGNsYXNzPSJpdGVtVHlwZUdyb3VwIj4KPGRpdiBjbGFzcz0ibWVtbyI+CjxwPktlZXAgYXdheSBmcm9tIGRpcmVjdCBzdW5saWdodDxicj5SZWZyaWdlcmF0aW9uIG5vdCByZXF1aXJlZDxicj5Tb2xpZGlmaWVzIGluIHRlbXBlcmF0dXJlcyBiZWxvdyAyM8K6QyAoNzfCukYpPC9wPgo8L2Rpdj4KPC9kaXY+CjwvZGl2Pgo8L2Rpdj48ZGl2IHN0eWxlPSJwYWRkaW5nOjEwcHg7Ij4gCjwvZGl2Pgo=","assets":{"plp_image":"https://assets.sainsburys-groceries.co.uk/gol/7847880/image.jpg","images":[{"id":"1","sizes":[{"width":100,"height":100,"url":"https://assets.sainsburys-groceries.co.uk/gol/7847880/1/100x100.jpg"},{"width":140,"height":140,"url":"https://assets.sainsburys-groceries.co.uk/gol/7847880/1/140x140.jpg"},{"width":300,"height":300,"url":"https://assets.sainsburys-groceries.co.uk/gol/7847880/1/300x300.jpg"},{"width":640,"height":640,"url":"https://assets.sainsburys-groceries.co.uk/gol/7847880/1/640x640.jpg"},{"width":1500,"height":1500,"url":"https://assets.sainsburys-groceries.co.uk/gol/7847880/1/1500x1500.jpg"},{"width":2365,"height":2365,"url":"https://assets.sainsburys-groceries.co.uk/gol/7847880/1/2365x2365.jpg"}]}],"video":[]},"description":["Coconut Oil"],"important_information":["The above details have been prepared to help you select suitable products. Products and their ingredients are liable to change.","\u003cstrong\u003eYou should always read the label before consuming or using the product and never rely solely on the information presented here.","If you require specific advice on any Sainsbury's branded product, please contact our Customer Careline on 0800 636262. For all other products, please contact the manufacturer, whose details will appear on the packaging or label. Sainsbury’s is therefore unable to accept liability for any incorrect information.","You should also note that the picture / images show only our serving suggestions of how to serve or prepare your food – all accessories and additional items and/or ingredients pictured with the product you are purchasing are not included.","This information is supplied for your personal use only. It may not be reproduced in any way without the prior consent of Sainsbury's Supermarkets Ltd and due acknowledgement"],"attachments":[],"categories":[{"id":"267545","name":"All oils"},{"id":"267554","name":"Coconut oil"}],"display_icons":[],"pdp_deep_link":"/shop/ProductDisplay?storeId=10151\u0026langId=44\u0026productId=1109052"}]} -------------------------------------------------------------------------------- /1-testing/3-quick-recipes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /1-testing/3-quick-recipes/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` 11 | 12 | ## Run an individual script 13 | 14 | ```sh 15 | node --experimental-top-level-await create-pdf.js 16 | ``` -------------------------------------------------------------------------------- /1-testing/3-quick-recipes/type-text.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import puppeteer from 'puppeteer'; 3 | 4 | const screenshotPath = path.join(process.cwd(), 'assets', 'type-text.png'); 5 | 6 | const browser = await puppeteer.launch({headless: true}); 7 | const page = await browser.newPage(); 8 | 9 | await page.goto('https://trix-editor.org/'); 10 | await page.focus('trix-editor'); 11 | await page.keyboard.type('Typing some text'); 12 | await page.screenshot({ path: screenshotPath }); 13 | 14 | await browser.close(); -------------------------------------------------------------------------------- /1-testing/4-selectors/index.js: -------------------------------------------------------------------------------- 1 | // Cheat sheet for CSS selectors https://umaar.com/dev-tips/229-css-attribute-selectors/ 2 | 3 | import puppeteer from 'puppeteer'; 4 | import assert from 'assert'; 5 | 6 | const browser = await puppeteer.launch({headless: true}); 7 | const page = await browser.newPage(); 8 | 9 | await page.goto('https://en.wikipedia.org/wiki/Space'); 10 | 11 | const expectedText = 'universe'; 12 | 13 | // All these selectors are valid and will match against Wikipedia 14 | // Which one do you think is best? And why? 15 | // Are any of these brittle? 16 | const selectors = [ 17 | 'a[href="/wiki/Universe"]', 18 | 'a[href="/wiki/universe" i]', 19 | 'a[title="Universe"]', 20 | '[title="Universe"]', 21 | '#content [title="Universe"]', 22 | 'html body #content .mw-body-content [title="Universe"]', 23 | // '#mw-content-text > div > p:nth-child(6) > a:nth-child(12)' 24 | ]; 25 | 26 | for (const selector of selectors) { 27 | const text = await page.$eval(selector, el => el.textContent); 28 | assert.equal(text, expectedText); 29 | } 30 | 31 | // Searching by text on the page 32 | // https://pptr.dev/#?product=Puppeteer&version=v5.2.1&show=api-pageevaluatepagefunction-args 33 | // ^ see the part that says "Passing arguments to pageFunction:" 34 | const text = await page.evaluate((expectedText) => { 35 | return [...document.querySelectorAll('a')] 36 | .find(element => element.textContent === expectedText) 37 | .textContent 38 | }, expectedText); 39 | 40 | assert.equal(text, expectedText) 41 | 42 | await browser.close(); -------------------------------------------------------------------------------- /1-testing/4-selectors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /1-testing/4-selectors/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` -------------------------------------------------------------------------------- /1-testing/5-errors/ava/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "../../../node_modules/.bin/ava test.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /1-testing/5-errors/ava/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | 11 | # or 12 | 13 | npx ava 14 | ``` -------------------------------------------------------------------------------- /1-testing/5-errors/ava/test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import withPage from './with-page.js'; 3 | 4 | const url = 'example.com/'; 5 | 6 | test('Should have the correct page title', withPage, async (t, page) => { 7 | t.plan(2); 8 | await page.goto(url); 9 | await page.setViewport({width: 1280, height: 720}); 10 | t.is(await page.title(), 'Example domain', 'page title is correct'); 11 | }); 12 | -------------------------------------------------------------------------------- /1-testing/5-errors/ava/with-page.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | async function withPage(t, run) { 4 | const browser = await puppeteer.launch(); 5 | const page = await browser.newPage(); 6 | try { 7 | await run(t, page); 8 | } finally { 9 | await page.close(); 10 | await browser.close(); 11 | } 12 | } 13 | 14 | export default withPage; -------------------------------------------------------------------------------- /1-testing/5-errors/cypress/cypress/integration/examples/example-domain.js: -------------------------------------------------------------------------------- 1 | describe('my tests', () => { 2 | it('example domain', () => { 3 | nonexistant.method(); // Delete this line 4 | cy.visit('https://example.com'); 5 | cy.contains('This domain is for illustrative examples'); 6 | cy.url().should('include', 'examples'); 7 | cy.get('h1').should('have.text', 'An Example Domain'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /1-testing/5-errors/cypress/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | }; 22 | -------------------------------------------------------------------------------- /1-testing/5-errors/cypress/cypress/screenshots/examples/example-domain.js/my tests -- example domain (failed).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/1-testing/5-errors/cypress/cypress/screenshots/examples/example-domain.js/my tests -- example domain (failed).png -------------------------------------------------------------------------------- /1-testing/5-errors/cypress/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /1-testing/5-errors/cypress/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /1-testing/5-errors/cypress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "scripts": { 4 | "start": "../../../node_modules/.bin/cypress run --config-file false" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /1-testing/5-errors/cypress/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | 11 | # or 12 | 13 | npx cypress run --config-file false 14 | ``` -------------------------------------------------------------------------------- /1-testing/5-errors/jest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "../../../node_modules/.bin/jest" 6 | }, 7 | "jest": { 8 | "preset": "jest-puppeteer" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /1-testing/5-errors/jest/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | 11 | # or 12 | 13 | npx jest 14 | ``` -------------------------------------------------------------------------------- /1-testing/5-errors/jest/test.js: -------------------------------------------------------------------------------- 1 | describe('Example.com', () => { 2 | beforeAll(async () => { 3 | await page.goto('https://example.com'); 4 | }); 5 | 6 | it('Should have the correct title', async () => { 7 | await expect(page.title).resolves.toMatch('Example Domain'); // Tip: should it be `.title` or `.title()`? 8 | await expect(page).toMatch('without prior co-ordination'); 9 | await expect(page).toClick('a', {text: 'More information....'}); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /1-testing/5-errors/puppeteer/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | import assert from 'assert'; 3 | 4 | const browser = await puppeteer.launch(); 5 | const page = await browser.newPage(); 6 | 7 | await page.goto('https://example.com'); 8 | 9 | await page.waitForSelector('h2', { 10 | timeout: 1000 // tip: the problem isn't this timeout 11 | }); 12 | 13 | const pageTitle = await page.title(); 14 | 15 | await browser.close(); 16 | 17 | assert.strictEqual(pageTitle, 'Example'); -------------------------------------------------------------------------------- /1-testing/5-errors/puppeteer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /1-testing/5-errors/puppeteer/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` -------------------------------------------------------------------------------- /1-testing/5-errors/testcafe/index.js: -------------------------------------------------------------------------------- 1 | import {Selector} from 'testcafe'; 2 | 3 | fixture`Getting Started`.page`https://example.com`; 4 | 5 | test('My first test', async t => { 6 | const h1 = await Selector('h1'); 7 | const headerText = await h1.textContent; 8 | t.expect(headerText).eql('example.com'); 9 | }); 10 | -------------------------------------------------------------------------------- /1-testing/5-errors/testcafe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "../../../node_modules/.bin/testcafe 'firefox:headless' index.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /1-testing/5-errors/testcafe/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | 11 | # or 12 | 13 | npx testcafe 'firefox:headless' index.js 14 | ``` -------------------------------------------------------------------------------- /1-testing/5-errors/webdriver/index.js: -------------------------------------------------------------------------------- 1 | import 'chromedriver'; 2 | import webdriver from 'selenium-webdriver'; 3 | import assert from 'assert'; 4 | 5 | const {By, until} = webdriver; 6 | 7 | const driver = await new webdriver.Builder().forBrowser('chrome').build(); 8 | 9 | try { 10 | await driver.get('https://example.com'); 11 | await driver.findElement(By.css(`body > div > p:nth-child(3)`)); 12 | 13 | /* 14 | If you want to exlpore what methods are on the `until` object 15 | Try this: console.log(until); 16 | */ 17 | await driver.wait(until.title('Example Domain')); 18 | const script = `return document.queryselector('h1').innerText`; 19 | const h1 = await driver.executeScript(script); 20 | assert.strictEqual(h1, 'Example Domain'); 21 | } finally { 22 | await driver.quit(); 23 | } -------------------------------------------------------------------------------- /1-testing/5-errors/webdriver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /1-testing/5-errors/webdriver/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` -------------------------------------------------------------------------------- /1-testing/6-test-generators/.qawolf/myWikipediaTest.test.js: -------------------------------------------------------------------------------- 1 | const qawolf = require("qawolf"); 2 | const assert = require("assert"); 3 | function sleep(ms = 1000) {return new Promise((resolve) => setTimeout(resolve, ms))}; 4 | let browser; 5 | let page; 6 | 7 | beforeAll(async () => { 8 | browser = await qawolf.launch(); 9 | const context = await browser.newContext(); 10 | await qawolf.register(context); 11 | page = await context.newPage(); 12 | }); 13 | 14 | afterAll(async () => { 15 | await qawolf.stopVideos(); 16 | await browser.close(); 17 | }); 18 | 19 | test("myWikipediaTest", async () => { 20 | await page.goto("https://www.wikipedia.org/"); 21 | await page.click("#searchInput"); 22 | await page.fill("#searchInput", "space"); 23 | await page.press("#searchInput", "Enter"); 24 | await page.fill("#searchInput", "web browser"); 25 | await page.click('a[title="Web browser"]'); 26 | assert.strictEqual(await page.title(), 'Web browser - Wikipedia'); 27 | }); -------------------------------------------------------------------------------- /1-testing/6-test-generators/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "8-test-generators", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "qawolf.config.js", 6 | "scripts": { 7 | "start": "node ../../node_modules/qawolf/build/index.js test myWikipediaTest", 8 | "howl": "node ../../node_modules/qawolf/build/index.js howl" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC" 13 | } 14 | -------------------------------------------------------------------------------- /1-testing/6-test-generators/qawolf.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | config: '../../node_modules/qawolf/js-jest.config.json', 3 | rootDir: '.qawolf', 4 | testTimeout: 10000, 5 | useTypeScript: false 6 | }; 7 | -------------------------------------------------------------------------------- /1-testing/6-test-generators/readme.md: -------------------------------------------------------------------------------- 1 | 2 | __Update__: Playwright can now do this [natively](https://github.com/microsoft/playwright-cli)! 3 | 4 | ### How to use this 5 | 6 | 1. Firstly, make sure the existing test in this folder runs successfully. Run this command in your terminal: 7 | 8 | ```sh 9 | npm run howl # checks that qawolf is working 10 | npm start # runs a sample wikipedia test 11 | ``` 12 | 13 | 2. First, run this command to CREATE your test: 14 | 15 | ```sh 16 | node ../../node_modules/qawolf/build/index.js create https://www.wikipedia.org/ yourTestNameHere 17 | ``` 18 | 19 | 3. Then, run this command to RUN your test: 20 | 21 | ```sh 22 | node ../../node_modules/qawolf/build/index.js test yourTestNameHere 23 | ``` 24 | 25 | 4. Create new tests on websites other than Wikipedia 26 | 27 | ### Alternative commands 28 | 29 | If those commands above do not work on for you, for example on Windows, try these: 30 | 31 | ```sh 32 | npx qawolf howl 33 | 34 | # Create a test 35 | npx qawolf create https://www.wikipedia.org/ yourTestNameHere 36 | 37 | # Run your test 38 | npx qawolf test yourTestNameHere 39 | ``` 40 | 41 | ### See also 42 | 43 | [Puppeteer Recorder](https://chrome.google.com/webstore/detail/puppeteer-recorder/djeegiggegleadkkbgopoonhjimgehda?hl=en) 44 | -------------------------------------------------------------------------------- /1-testing/7-payment-checkout/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | import assert from 'assert'; 3 | import {promisify} from 'util' 4 | 5 | const sleep = promisify(setTimeout); 6 | 7 | function random() { 8 | return Math.random().toString(36).substr(2, 9); 9 | } 10 | 11 | const browser = await puppeteer.launch({ 12 | headless: false, 13 | slowMo: 50, 14 | // https://github.com/GoogleChrome/puppeteer/issues/2548#issuecomment-390077713 15 | args: [ 16 | '--disable-features=site-per-process', 17 | '--no-sandbox', 18 | '--window-size=1000,1000', 19 | ] 20 | }); 21 | 22 | const page = await browser.newPage(); 23 | await page.setViewport({ width: 1000, height: 1000 }); 24 | 25 | await page.goto('http://localhost:3000') 26 | 27 | await Promise.all([ 28 | page.waitForNavigation({ 29 | waitUntil: ['load', 'domcontentloaded', 'networkidle0'] 30 | }), 31 | page.click('[href="/pay"]') 32 | ]); 33 | 34 | const title = await page.title(); 35 | const expectedTitle = 'Purchase the Modern DevTools Course - Modern DevTools'; 36 | 37 | assert.equal(title, expectedTitle, 'Page title is correct'); 38 | const frames = page.frames(); 39 | 40 | const email = `test+${random()}@blackhole.postmarkapp.com` 41 | await page.type('#user-email', email); 42 | 43 | const cc = frames.find(frame => frame.name() === '__privateStripeFrame5'); 44 | const exp = frames.find(frame => frame.name() === '__privateStripeFrame6'); 45 | const cvc = frames.find(frame => frame.name() === '__privateStripeFrame7'); 46 | 47 | /* 48 | Tip: Don't forget these to work with iframes: 49 | --disable-features=site-per-process 50 | --no-sandbox 51 | */ 52 | 53 | await cc.type('[name="cardnumber"]', '4242 4242 4242 4242'); 54 | 55 | await exp.type('[name="exp-date"]', '0125'); 56 | 57 | await cvc.type('[name="cvc"]', '123'); 58 | 59 | await Promise.all([ 60 | page.waitForNavigation({ 61 | waitUntil: ['load', 'domcontentloaded', 'networkidle0'] 62 | }), 63 | page.click('.buy-button') 64 | ]); 65 | 66 | assert.equal(await page.title(), 'Enter a new password - Modern DevTools', 'Page title is correct'); 67 | 68 | 69 | await sleep(4000); 70 | await browser.close(); -------------------------------------------------------------------------------- /1-testing/7-payment-checkout/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /1-testing/7-payment-checkout/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | Note: Running this test relies on a local development web server which is not part of this repo. 5 | 6 | ```sh 7 | npm start 8 | 9 | # or 10 | 11 | yarn start 12 | ``` 13 | 14 | ## Operations 15 | 16 | 1. Navigate to the payment page 17 | What does navigation involve exactly? 18 | When is the next page ready? - Anyone want to give this a shot? 19 | 2. Fill in the payment fields 20 | Maybe assert the page title at this point 21 | What type of fields are they - iframe inputs? 22 | 3. Submit the payment form 23 | 4. Assert payment was successful 24 | Let's just use the page title for now 25 | Optional: Assert the email 26 | -------------------------------------------------------------------------------- /1-testing/8-bdd/features/maths.feature: -------------------------------------------------------------------------------- 1 | Feature: Simple maths 2 | In order to do maths 3 | As a developer 4 | I want to increment variables 5 | 6 | Scenario: easy maths 7 | Given a variable set to 1 8 | When I increment the variable by 1 9 | Then the variable should contain 2 10 | 11 | Scenario Outline: much more complex stuff 12 | Given a variable set to 13 | When I increment the variable by 14 | Then the variable should contain 15 | 16 | Examples: 17 | | var | increment | result | 18 | | 100 | 5 | 105 | 19 | | 99 | 1234 | 1333 | 20 | | 12 | 5 | 17 | -------------------------------------------------------------------------------- /1-testing/8-bdd/features/support/steps.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert').strict 2 | const { Given, When, Then } = require('cucumber'); 3 | 4 | Given('a variable set to {int}', number => { 5 | this.setTo(number); 6 | }); 7 | 8 | When('I increment the variable by {int}', number => { 9 | this.incrementBy(number); 10 | }); 11 | 12 | Then('the variable should contain {int}', number => { 13 | assert.equal(this.variable, number); 14 | }); -------------------------------------------------------------------------------- /1-testing/8-bdd/features/support/world.js: -------------------------------------------------------------------------------- 1 | const {setWorldConstructor} = require('cucumber'); 2 | 3 | class CustomWorld { 4 | constructor() { 5 | this.variable = 0; 6 | } 7 | 8 | setTo(number) { 9 | this.variable = number; 10 | } 11 | 12 | incrementBy(number) { 13 | this.variable += number; 14 | } 15 | } 16 | 17 | setWorldConstructor(CustomWorld); 18 | -------------------------------------------------------------------------------- /1-testing/8-bdd/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "commonjs", 4 | "scripts": { 5 | "start": "../../node_modules/.bin/cucumber-js --format usage" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /1-testing/8-bdd/readme.md: -------------------------------------------------------------------------------- 1 | ## To run 2 | 3 | ```sh 4 | npm start 5 | 6 | # or 7 | 8 | yarn start 9 | ``` 10 | 11 | Example taken from: https://github.com/cucumber/cucumber-js -------------------------------------------------------------------------------- /2-scraping/1-no-code/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## No code 3 | 4 | - https://docs.google.com/spreadsheets/d/1hep5rIaSpolaD2W09riTJ4eaCfLOX9tCUC5HdSB0wJs 5 | - https://simplescraper.io/ 6 | 7 | ## Cheat sheet 8 | 9 | 10 | ``` 11 | =IMPORTXML("https://umaar.com/dev-tips/235-smooth-scroll-into-view/","//span[@class='dt-tip-date']") 12 | 13 | =IMPORTXML("https://umaar.com/blog/", "//ol/li//a/@href") 14 | 15 | =IMPORTHTML("https://en.wikipedia.org/wiki/List_of_web_browsers","table",1) 16 | ``` -------------------------------------------------------------------------------- /2-scraping/10-captcha/.env.sample: -------------------------------------------------------------------------------- 1 | CAPTCHA_TOKEN= -------------------------------------------------------------------------------- /2-scraping/10-captcha/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer-extra'; 2 | import RecaptchaPlugin from 'puppeteer-extra-plugin-recaptcha'; 3 | import dotenv from 'dotenv'; 4 | 5 | dotenv.config(); 6 | 7 | const CAPTCHA_TOKEN = process.env.CAPTCHA_TOKEN; 8 | 9 | if (!CAPTCHA_TOKEN) { 10 | throw new Error(`This won't work without a CAPTCHA_TOKEN (check the readme.md file)`) 11 | } 12 | 13 | function sleep(ms = 1000) {return new Promise((resolve) => setTimeout(resolve, ms))}; 14 | 15 | puppeteer.use(RecaptchaPlugin({ 16 | provider: { 17 | id: '2captcha', 18 | token: process.env.CAPTCHA_TOKEN 19 | }, 20 | visualFeedback: true 21 | })); 22 | 23 | const browser = await puppeteer.launch({ headless: false }); 24 | const page = await browser.newPage(); 25 | 26 | await page.goto('https://www.google.com/recaptcha/api2/demo'); 27 | await sleep(2000); 28 | 29 | await page.solveRecaptchas(); 30 | 31 | await Promise.all([ 32 | page.waitForNavigation(), 33 | page.click(`#recaptcha-demo-submit`) 34 | ]); 35 | 36 | await page.screenshot({ path: 'response.png', fullPage: true }); 37 | await sleep(4000); 38 | await browser.close() -------------------------------------------------------------------------------- /2-scraping/10-captcha/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /2-scraping/10-captcha/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## Note 3 | 4 | This is currently configured to use [`2captcha`](https://2captcha.com/enterpage), a paid service. You'll need to get a token from them, and put it in an `.env` file to get this working. 5 | 6 | ## To run 7 | 8 | ```sh 9 | npm start 10 | 11 | # or 12 | 13 | yarn start 14 | ``` -------------------------------------------------------------------------------- /2-scraping/2-page-metadata/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | const browser = await puppeteer.launch({headless: true}); 4 | const page = await browser.newPage(); 5 | 6 | await page.goto('https://en.wikipedia.org/wiki/Space'); 7 | 8 | // await page.evaluate('document.title') 9 | const title = await page.title(); 10 | 11 | const image = await page.$eval('meta[property="og:image" ]', el => el.content); 12 | 13 | const text = await page.$eval('h1', el => el.textContent); 14 | 15 | const links = await page.$$eval('#mw-content-text a', els => { 16 | return els.slice(0, 3).map(el => el.textContent) 17 | }); 18 | 19 | console.log({ 20 | title, 21 | image, 22 | text, 23 | links 24 | }); 25 | 26 | await browser.close(); 27 | -------------------------------------------------------------------------------- /2-scraping/2-page-metadata/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /2-scraping/2-page-metadata/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` -------------------------------------------------------------------------------- /2-scraping/3-infinite-scrolling/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | function sleep(ms = 1000) {return new Promise((resolve) => setTimeout(resolve, ms))}; 4 | 5 | const browser = await puppeteer.launch({headless: false}); 6 | const page = await browser.newPage(); 7 | 8 | await page.goto('https://www.reddit.com/r/webdev/', { 9 | waitUntil: ['networkidle0'] 10 | }); 11 | 12 | const maxPages = 4; 13 | 14 | for (let currentPage = 0; currentPage < maxPages; currentPage++) { 15 | await sleep(4000); 16 | const tweets = await page.$$('[data-click-id="timestamp"]'); 17 | console.log(`${tweets.length} posts. Scrolling the page...`); 18 | await page.evaluate( 19 | 'window.scrollTo(0, document.body.scrollHeight)' 20 | ); 21 | } 22 | 23 | await browser.close(); -------------------------------------------------------------------------------- /2-scraping/3-infinite-scrolling/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /2-scraping/3-infinite-scrolling/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` -------------------------------------------------------------------------------- /2-scraping/4-tweet-screenshot/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | const browser = await puppeteer.launch({headless: true}); 4 | const page = await browser.newPage(); 5 | 6 | await page.goto('https://twitter.com/umaar/status/1288781312816549888', { 7 | waitUntil: ['networkidle0'] 8 | }); 9 | 10 | const tweet = await page.$('[role="article"'); 11 | await tweet.screenshot({ path: 'screenshot.png' }); 12 | await browser.close(); 13 | -------------------------------------------------------------------------------- /2-scraping/4-tweet-screenshot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /2-scraping/4-tweet-screenshot/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` -------------------------------------------------------------------------------- /2-scraping/5-selective-scraping/index.js: -------------------------------------------------------------------------------- 1 | import URL from 'URL'; 2 | import puppeteer from 'puppeteer'; 3 | 4 | function sleep(ms = 1000) {return new Promise((resolve) => setTimeout(resolve, ms))}; 5 | const browser = await puppeteer.launch({ 6 | headless: false 7 | }); 8 | 9 | const page = await browser.newPage(); 10 | 11 | await page.setRequestInterception(true) 12 | 13 | page.on('request', async request => { 14 | const pathname = URL.parse(request.url()).pathname || ''; 15 | 16 | if (pathname.endsWith('.css') || pathname.endsWith('.js')) { 17 | await request.abort(); 18 | } else { 19 | await request.continue(); 20 | } 21 | }); 22 | 23 | await page.goto('https://www.walmart.com/ip/Mainstays-1-7-Liter-Plastic-Electric-Kettle-White/471494848'); 24 | 25 | await sleep(5000); 26 | 27 | await browser.close(); 28 | -------------------------------------------------------------------------------- /2-scraping/5-selective-scraping/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /2-scraping/5-selective-scraping/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` -------------------------------------------------------------------------------- /2-scraping/6-products-extractor/index.js: -------------------------------------------------------------------------------- 1 | import URL from 'URL'; 2 | import express from 'express'; 3 | import nunjucks from 'nunjucks'; 4 | import puppeteer from 'puppeteer'; 5 | 6 | async function getProducts(query) { 7 | const browser = await puppeteer.launch({ 8 | headless: false 9 | }); 10 | 11 | const page = await browser.newPage(); 12 | 13 | await page.goto(`https://www.walmart.com/search/?query=${query}`); 14 | 15 | const products = await page.$$eval('.search-result-gridview-item', els => { 16 | return els.slice(0, 4).map(el => { 17 | return { 18 | title: el.querySelector('.product-title-link').textContent, 19 | price: el.querySelector('.price-main .visuallyhidden').textContent, 20 | } 21 | }) 22 | }); 23 | 24 | await browser.close(); 25 | console.log(products); 26 | 27 | return products; 28 | } 29 | 30 | var app = express(); 31 | 32 | nunjucks.configure('views', { 33 | autoescape: true, 34 | express: app 35 | }); 36 | 37 | app.set('port', 3000); 38 | 39 | // Home page 40 | app.get('/', (req, res) => { 41 | res.render('index.html', { 42 | page: 'home', 43 | port: app.get('port') 44 | }); 45 | }); 46 | 47 | app.get('/search/:query', async (req, res) => { 48 | const {query} = req.params; 49 | 50 | if (!query) { 51 | return res.status(404).send('No products found'); 52 | } 53 | 54 | const products = await getProducts(query); 55 | 56 | res.render('search.html', { 57 | page: 'search', 58 | port: app.get('port'), 59 | products 60 | }); 61 | }); 62 | 63 | // Kick start our server 64 | app.listen(app.get('port'), () => { 65 | console.log('Server started on port', app.get('port')); 66 | }); -------------------------------------------------------------------------------- /2-scraping/6-products-extractor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /2-scraping/6-products-extractor/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` 11 | 12 | ## Your challenge 13 | 14 | Your challenge is to enhance this project. 15 | 16 | Currently, scraping walmart is a bit slow. Puppeteer needs to: 17 | 18 | - To block the requests of CSS & JS resources (and png/jpg?) 19 | 20 | Also, the search results page doesn't link back to the Walmart product, so this code needs: 21 | 22 | - A hyperlink to the Walmart product on the search results page 23 | 24 | --- 25 | 26 | ## Guidance (partial solution) 27 | 28 | ```js 29 | await page.setRequestInterception(true) 30 | 31 | page.on('request', async request => { 32 | const pathname = URL.parse(request.url()).pathname || ''; 33 | 34 | if (pathname.endsWith('.css') || pathname.endsWith('.js')) { 35 | await request.abort(); 36 | } else { 37 | await request.continue(); 38 | } 39 | }); 40 | ``` 41 | 42 | ## Credits 43 | 44 | Express boilerplate [from here](https://github.com/iamstuartwilson/express-nunjucks-boilerplate). 45 | -------------------------------------------------------------------------------- /2-scraping/6-products-extractor/views/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block body %} 4 |
5 |
6 |
7 | {% block content %} 8 |

Page: {{ page }}

9 | {% endblock %} 10 |
11 |
12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /2-scraping/6-products-extractor/views/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Product Extractor 5 | 6 | 7 | 8 |
9 |
10 |
11 |

Product Extractor

12 |
13 |
14 |
15 | 22 |
23 |
24 | 25 | {% block body %} 26 | {% endblock %} 27 | 28 |
29 |
30 |

App running on port: {{ port }}

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /2-scraping/6-products-extractor/views/search.html: -------------------------------------------------------------------------------- 1 | {% extends 'index.html' %} 2 | 3 | {% block content %} 4 | {{ super() }} 5 | 6 |

Found {{products.length}} items

7 | 8 |
    9 | {% for product in products %} 10 |
  • {{product.title}} / {{product.price}}
  • 11 | {% endfor %} 12 |
13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /2-scraping/7-wiki-philosophy/clean.js: -------------------------------------------------------------------------------- 1 | function clean(string) { 2 | let d = 0; 3 | let k = 0; 4 | let c; 5 | let out = ''; 6 | for (const element of string) { 7 | c = element; 8 | 9 | if (d < 1) { 10 | if (c === '>') { 11 | k -= 1; 12 | } 13 | 14 | if (c === '<') { 15 | k += 1; 16 | } 17 | } 18 | 19 | if (k < 1) { 20 | if (c === '(') { 21 | d += 1; 22 | } 23 | 24 | if (d > 0) { 25 | out += ' '; 26 | } else { 27 | out += c; 28 | } 29 | 30 | if (c === ')') { 31 | d -= 1; 32 | } 33 | } else { 34 | out += c; 35 | } 36 | } 37 | 38 | return out.replace(/'/g, '').trim(); 39 | } 40 | 41 | export default clean; 42 | -------------------------------------------------------------------------------- /2-scraping/7-wiki-philosophy/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | import clean from './clean.js'; 3 | 4 | const browser = await puppeteer.launch({ 5 | headless: false 6 | }); 7 | 8 | const page = await browser.newPage(); 9 | 10 | await page.setViewport({ width: 1280, height: 720 }); 11 | 12 | await page.goto('http://en.wikipedia.org/wiki/Special:Random'); 13 | // await page.goto('http://en.wikipedia.org/wiki/Aircraft'); 14 | 15 | await page.exposeFunction('clean', clean); 16 | 17 | async function handleLink() { 18 | const firstLinkSelector = '.mw-parser-output > p > a[title]'; 19 | const h1 = await page.$eval('h1', el => el.innerText); 20 | 21 | console.log('> ', h1); 22 | 23 | if (h1 === 'Philosophy') { 24 | return; 25 | } 26 | 27 | await page.evaluate(async () => { 28 | const para = document.querySelector('.mw-parser-output > p:not(.mw-empty-elt)'); 29 | para.innerHTML = await clean(para.innerHTML); 30 | }); 31 | 32 | await Promise.all([ 33 | page.waitForNavigation({ 34 | waitUntil: ['domcontentloaded'] 35 | }), 36 | page.click(firstLinkSelector) 37 | ]); 38 | 39 | return handleLink(); 40 | } 41 | 42 | await handleLink(); 43 | await browser.close(); 44 | -------------------------------------------------------------------------------- /2-scraping/7-wiki-philosophy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /2-scraping/7-wiki-philosophy/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` -------------------------------------------------------------------------------- /2-scraping/8-product-price-checker/.env.sample: -------------------------------------------------------------------------------- 1 | AIRTABLE_API_KEY= 2 | AIRTABLE_BASE= -------------------------------------------------------------------------------- /2-scraping/8-product-price-checker/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | import notifier from 'node-notifier'; 3 | import Airtable from 'airtable'; 4 | import dotenv from 'dotenv'; 5 | 6 | dotenv.config(); 7 | 8 | let base; 9 | 10 | const AIRTABLE_API_KEY = process.env.AIRTABLE_API_KEY; 11 | const AIRTABLE_BASE = process.env.AIRTABLE_BASE; 12 | 13 | const shouldSaveToCloud = AIRTABLE_API_KEY && AIRTABLE_BASE; 14 | 15 | const airtableConfig = { 16 | apiKey: AIRTABLE_API_KEY 17 | }; 18 | 19 | if (shouldSaveToCloud) { 20 | base = new Airtable(airtableConfig).base(AIRTABLE_BASE); 21 | } 22 | 23 | const browser = await puppeteer.launch({headless: true}); 24 | const page = await browser.newPage(); 25 | 26 | function sleep(ms = 1000) {return new Promise((resolve) => setTimeout(resolve, ms))}; 27 | 28 | let price = 0; 29 | let maxChecks = 5; 30 | let checksMade = 0; 31 | 32 | async function getLatestPrice() { 33 | await page.goto('https://automatebrowsers.com/amazon/cat-mug/'); 34 | await page.waitForSelector('#price_inside_buybox'); 35 | const latestPrice = await page.$eval('#price_inside_buybox', el => { 36 | const priceAsFloat = parseFloat(el.textContent.substr(1)); 37 | return Math.round(priceAsFloat); 38 | }); 39 | 40 | return latestPrice; 41 | } 42 | 43 | async function logPriceChange(latestPrice) { 44 | if (price === latestPrice) { 45 | return; 46 | } 47 | 48 | const priceDifference = latestPrice - price; 49 | 50 | if (priceDifference > 0) { 51 | console.log(`Price increase $${latestPrice} (+${priceDifference})`); 52 | } else { 53 | const message = `Price decrease $${latestPrice} (${priceDifference})`; 54 | console.log(message); 55 | notifier.notify(message); 56 | } 57 | 58 | if (shouldSaveToCloud) { 59 | await base('Amazon Price Updates').create([{ 60 | "fields": { 61 | "Price": latestPrice, 62 | "Time": new Date().toString() 63 | } 64 | }]); 65 | } 66 | } 67 | 68 | async function check() { 69 | if (checksMade <= maxChecks ) { 70 | checksMade++; 71 | const latestPrice = await getLatestPrice(); 72 | if (checksMade > 1) await logPriceChange(latestPrice); 73 | price = latestPrice; 74 | await sleep(2000); 75 | return check(); 76 | } else { 77 | console.log('Closing browser'); 78 | await browser.close(); 79 | } 80 | } 81 | 82 | await check(); 83 | -------------------------------------------------------------------------------- /2-scraping/8-product-price-checker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /2-scraping/8-product-price-checker/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## Amazon Price Checker 3 | 4 | ### Goals 5 | 6 | We want to build a price checker for an Amazon product. The Amazon product is hosted on this standalone page, which is suitable for frequent scraping: [Amazon: Cat mug](https://automatebrowsers.com/amazon/cat-mug/). 7 | 8 | 1. Write a node.js script which automates a browser to scrape the Amazon price of the cat mug 9 | 2. The script should not close, instead, it can check for price changes at a specified interval (the price changes on page reload for testing purposes) 10 | 3. The script could optionally use something like `node-notifier` to notify the user of changes to the price 11 | 4. The script could optionally save prices to the cloud, using a service like [Airtable](https://airtable.com) - they have a generous free plan 12 | 13 | ### Instructions 14 | 15 | ```sh 16 | npm start # or yarn start 17 | ``` 18 | 19 | ### Want to save results to the cloud? 20 | 21 | [Airtable Table Example](https://airtable.com/shrHejfReBwZPavxA) 22 | 23 | 1. Make a free account at airtable.com 24 | 2. Copy `.env.sample` to `.env` 25 | 3. Fill it with values from: https://airtable.com/api (you'll need your 'base' and api key) 26 | 27 | -------------------------------------------------------------------------------- /2-scraping/9-offline-websites/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | const browser = await puppeteer.connect({ 4 | browserURL: 'http://localhost:9222/', 5 | headless: false 6 | }); 7 | 8 | const page = await browser.newPage(); 9 | await page.setCacheEnabled(false); 10 | 11 | await page.goto('https://umaar.com/dev-tips/'); 12 | 13 | /* Interact with the offline page below */ -------------------------------------------------------------------------------- /2-scraping/9-offline-websites/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node ../../node_modules/archivist1/index.js", 6 | "interact": "node --experimental-top-level-await index" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /2-scraping/9-offline-websites/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | 1. Start 22120 5 | 6 | ```sh 7 | npm start 8 | 9 | # or 10 | 11 | yarn start 12 | ``` 13 | 14 | 2. Open [http://localhost:22120/](http://localhost:22120/) and ensure it's in __Save mode__ 15 | 16 | 3. Navigate to any page(s) you wish to scrape/interact with through code. Visiting such websites automatically saves them 17 | 18 | 4. Set the mode to __Serve mode__ 19 | 20 | 5. In a new terminal tab, within the `2-scraping/9-offline-websites` folder, run: 21 | 22 | ```sh 23 | npm run interact 24 | 25 | # or 26 | 27 | yarn interact 28 | ``` -------------------------------------------------------------------------------- /3-auditing/1-lighthouse/README.md: -------------------------------------------------------------------------------- 1 | - [Notes](#notes) 2 | - [1. Upload lighthouse results](#1-upload-lighthouse-results) 3 | - [2. Upload lighthouse results locally](#2-upload-lighthouse-results-locally) 4 | - [2.1. Start lighthouse CI](#21-start-lighthouse-ci) 5 | - [2.2. Configure lighthouse CI](#22-configure-lighthouse-ci) 6 | - [2.3. Upload lighthouse results locally](#23-upload-lighthouse-results-locally) 7 | - [Windows alternative](#windows-alternative) 8 | - [3. Run lighthouse CI in the cloud](#3-run-lighthouse-ci-in-the-cloud) 9 | - [3.1. Setup heroku](#31-setup-heroku) 10 | - [3.2. Deploy lighthouse CI to heroku (free)](#32-deploy-lighthouse-ci-to-heroku-free) 11 | - [3.3. Run the lighthouse wizard](#33-run-the-lighthouse-wizard) 12 | - [3.4. Upload lighthouse results to heroku](#34-upload-lighthouse-results-to-heroku) 13 | - [Windows alternative](#windows-alternative-1) 14 | - [4. Connect lighthouse with github](#4-connect-lighthouse-with-github) 15 | - [4.1. Add an action file](#41-add-an-action-file) 16 | - [4.2. Enable the status check](#42-enable-the-status-check) 17 | - [4.3. Make lighthouse mandatory](#43-make-lighthouse-mandatory) 18 | - [5. Finishing up](#5-finishing-up) 19 | - [Example dashboard](#example-dashboard) 20 | 21 | 22 | # Lighthouse CI 23 | 24 | 🔥️ This uses completely free services. 25 | 26 | The definitive guide to automated performance testing using Lighthouse, GitHub Actions and Heroku. 27 | 28 | | ![pics/all-checks-passing.png](pics/all-checks-passing.png) | 29 | |:--:| 30 | | *Preview of what we'll achieve* | 31 | 32 | ## Notes 33 | 34 | - Please take time to understand how the commands work, inspect the `package.json` file, and approach with a willingness to debug. This is not just copy-and-paste everything and it magically works. 35 | - You will need to read external documentation, such as that for lighthouse, to get this working fully. 36 | - You'll need to adapt some of the commands/instructions to run this in your own fresh repository. It will help to read through this whole guide, taking notes of the commands might need changing. 37 | - Windows users should ensure they have git installed. 38 | 39 | 40 | __The end goal__: At the end of this, you'll be able to run standard Lighthouse audits, but also __custom audits created by you__. 41 | 42 | You can run performance checks, accessibility checks, security checks, on each pull request to your GitHub repo. 43 | 44 | ### Which website should you test? 45 | 46 | _Some_ of the commands can be run within this folder (`3-auditing/1-lighthouse`) - there's a small `express` web server which has some random HTML and images, and is currently what Lighthouse is auditing. 47 | 48 | It would be helpful to audit __your own website__. Consider these options: 49 | 50 | - Use your own website, whether it's dynamic or a set of static pages. Ideally, you want to be able to add lots of large images to your page, and then immediately see how the Lighthouse scores are affected. Pay attention to the `--collect.startServerCommand` flag throughout these instructions. 51 | - If you don't have your own website to test, you can just use a boilerplate, e.g. here's [express + nunjucks](https://github.com/iamstuartwilson/express-nunjucks-boilerplate), but using any sort of boilerplate is fine, ideally something simple which can be started via `npm start`. 52 | - If you do not have your own website, and you don't wan't to copy from a boilerplate, you could in theory test a publicly facing URL. Just pay attention to the `--collect.url` flag you'll see throughout these instructions. 53 | 54 | For now, you can run commands in this `3-auditing/1-lighthouse` folder, it will become clear when you need to add in your own repo. 55 | 56 | --- 57 | 58 | ## 1. Upload lighthouse results 59 | 60 | | ![pics/lh-single-report.png](pics/lh-single-report.png) | 61 | |:--:| 62 | | *Preview of a single Lighthouse report* | 63 | 64 | Run lighthouse and upload results to their public server. 65 | 66 | While the Lighthouse CI tool does support extra config, let's just run this as one command: 67 | 68 | ```sh 69 | # from 3-auditing/1-lighthouse 70 | npm run lighthouse # or yarn lighthouse 71 | ``` 72 | 73 | 💡️ Go and check what the `lighthouse` script does, in the `package.json`, it's important to understand the following: 74 | 75 | - [startServerCommand](https://github.com/GoogleChrome/lighthouse-ci/blob/master/docs/configuration.md#startservercommand) 76 | - [target](https://github.com/GoogleChrome/lighthouse-ci/blob/master/docs/configuration.md#target) 77 | - [url](https://github.com/GoogleChrome/lighthouse-ci/blob/master/docs/configuration.md#url) 78 | - [numberOfRuns](https://github.com/GoogleChrome/lighthouse-ci/blob/master/docs/configuration.md#numberofruns) 79 | 80 | Before you continue, please understand each part of the `npm run lighthouse` command. 81 | 82 | --- 83 | 84 | ## 2. Upload lighthouse results locally 85 | 86 | Upload results to your local lighthouse server. 87 | 88 | ### 2.1. Start lighthouse CI 89 | 90 | | ![pics/lh-dashboard-third-party-increase.png](pics/lh-dashboard-third-party-increase.png) | 91 | |:--:| 92 | | *Lighthouse CI can show how metrics are improving or worsening over time* | 93 | 94 | Start the lighthouse CI server on your local machine. 95 | 96 | ```sh 97 | # from 3-auditing/1-lighthouse 98 | npm run lighthouse-local-server # or yarn lighthouse-local-server 99 | ``` 100 | 101 | Now: check the app is running @ http://localhost:9001 102 | 103 | 💡️ As usual, go and check what the `lighthouse-local-server` command does (in `package.json`). 104 | 105 | ### 2.2. Configure lighthouse CI 106 | 107 | Configure the lighthouse CI server. The lighthouse wizard tool can configure your lighthouse CI instance, both locally and remotely. 108 | 109 | In a new terminal tab: 110 | 111 | ```sh 112 | # from 3-auditing/1-lighthouse 113 | npm run lighthouse-wizard # or yarn lighthouse-wizard 114 | ``` 115 | 116 | I used the following answers, you can substitute the appropriate values for your own: 117 | 118 | 119 | ``` 120 | ? Which wizard do you want to run? new-project 121 | ? What is the URL of your LHCI server? http://localhost:9001 122 | ? What would you like to name the project? learn-browser-testing 123 | ? Where is the project's code hosted? https://github.com/umaar/learn-browser-testing 124 | ? What branch is considered the repo's trunk or main branch? master 125 | ``` 126 | 127 | 💡️ You might want to use your own repository when answering those questions. 128 | 129 | After executing that, take note of the `build token`. 130 | 131 | ### 2.3. Upload lighthouse results locally 132 | 133 | | ![pics/lhci-dashboard-changes.png](pics/lhci-dashboard-changes.png) | 134 | |:--:| 135 | | *The Lighthouse CI dashboard gives a handy overview of your page scores* | 136 | 137 | Run lighthouse and upload the results to your __local__ lighthouse CI server: 138 | 139 | Run the following command, and be sure to substitute `[YOUR_TOKEN]` for your actual `build token`. 140 | 141 | 💡️ __Important__ - Take time to understand what this command is doing, and how it works: 142 | 143 | - [serverBaseUrl](https://github.com/GoogleChrome/lighthouse-ci/blob/master/docs/configuration.md#serverbaseurl) 144 | - [token](https://github.com/GoogleChrome/lighthouse-ci/blob/master/docs/configuration.md#token) 145 | - What is the difference between `--upload.target=lhci` and `--upload.target=temporary-public-storage`? 146 | 147 | ```sh 148 | # from 3-auditing/1-lighthouse 149 | ../../node_modules/.bin/lhci autorun \ 150 | --collect.numberOfRuns=1 \ 151 | --collect.startServerCommand="npm start" \ 152 | --collect.url="http://localhost:3000" \ 153 | --upload.target=lhci \ 154 | --upload.serverBaseUrl="http://127.0.0.1:9001" \ 155 | --upload.token="[YOUR_TOKEN]" 156 | ``` 157 | 158 | #### Windows alternative 159 | 160 | ```sh 161 | ..\\..\\node_modules\\.bin\\lhci autorun ^ 162 | --collect.numberOfRuns=1 ^ 163 | --collect.startServerCommand="npm start" ^ 164 | --collect.url="http://localhost:3000" ^ 165 | --upload.target=lhci ^ 166 | --upload.serverBaseUrl="http://127.0.0.1:9001" ^ 167 | --upload.token="[YOUR_TOKEN]" 168 | ``` 169 | 170 | Now, you can verify the results on your local lighthouse CI server, e.g. at http://localhost:9001 171 | 172 | --- 173 | 174 | ## 3. Run lighthouse CI in the cloud 175 | 176 | This runs the tool which powers the lighthouse dashboard, to the cloud. Earlier, we ran `npm run lighthouse-local-server` - we're going to run that server online through a hosting platform. 177 | 178 | On this occasion, we'll use [heroku](https://www.heroku.com/) since it has a free tier. 179 | 180 | ### 3.1. Setup heroku 181 | 182 | - Make a [heroku.com](heroku.com) account 183 | - Install their [CLI tool](https://devcenter.heroku.com/articles/heroku-cli) 184 | 185 | ### 3.2. Deploy lighthouse CI to heroku (free) 186 | 187 | The lighthouse CI dashboard is completely independent of this `learn-browser-testing` repo, therefore, clone the lhci-heroku starter kit __outside__ of this current project. Instructions below: 188 | 189 | ```sh 190 | # For example, in ~/code or wherever your code projects live 191 | git clone https://github.com/umaar/lhci-heroku.git 192 | cd lhci-heroku 193 | 194 | # run this command just once 195 | heroku login 196 | 197 | # Create your new project in heroku 198 | heroku create 199 | 200 | # Create a new database (https://devcenter.heroku.com/articles/heroku-postgresql#provisioning-heroku-postgres) 201 | heroku addons:create heroku-postgresql:hobby-dev 202 | 203 | # The "git remote" named "heroku" is automatically configured, you just need to run `git push heroku master` to push to the heroku servers 204 | git push heroku master 205 | 206 | # Finally, start the app 207 | heroku ps:scale web=1 208 | ``` 209 | 210 | ### 3.3. Run the lighthouse wizard 211 | 212 | Previously, we ran the lighthouse wizard to configure a __local instance__ of the lighthouse CI tool. Now, we'll use that exact same wizard to configure the __remote heroku instance__ of the lighthouse CI tool. 213 | 214 | ```sh 215 | # While in the `lhci-heroku` folder, run: 216 | npm install 217 | npx lhci wizard 218 | ``` 219 | 220 | I gave these answers. In the answers below, configure the URL so it points to the platform you've deployed to Heroku. 221 | 222 | ``` 223 | ? Which wizard do you want to run? new-project 224 | ? What is the URL of your LHCI server? https://salty-headland-92476.herokuapp.com/ 225 | ? What would you like to name the project? lhci-heroku 226 | ? Where is the project's code hosted? https://github.com/umaar/lhci-heroku 227 | ? What branch is considered the repo's trunk or main branch? master 228 | ``` 229 | 230 | Take note of the tokens which are presented to you. 231 | 232 | ### 3.4. Upload lighthouse results to heroku 233 | 234 | At this point, you have: 235 | 236 | - Run lighthouse and uploaded results to their (the lighthouse team) public temporary storage 237 | - Run lighthouse and uploaded results to your local instance of lighthouse ci 238 | 239 | Now, you will run lighthouse and upload the results to your __heroku lighthouse CI server__: 240 | 241 | - Substitute `[YOUR_TOKEN]` for your actual `build token`. 242 | - Substitute `[YOUR BASE URL]` for your heroku URL. 243 | 244 | ```sh 245 | # Back in 3-auditing/1-lighthouse 246 | ../../node_modules/.bin/lhci autorun \ 247 | --collect.numberOfRuns=1 \ 248 | --collect.startServerCommand="npm start" \ 249 | --collect.url="http://localhost:3000" \ 250 | --upload.target=lhci \ 251 | --upload.serverBaseUrl="[YOUR BASE URL]" \ 252 | --upload.token="[YOUR_TOKEN]" 253 | ``` 254 | 255 | #### Windows alternative 256 | 257 | ```sh 258 | # Back in 3-auditing/1-lighthouse 259 | ..\\..\\node_modules\\.bin\\lhci autorun ^ 260 | --collect.numberOfRuns=1 ^ 261 | --collect.startServerCommand="npm start" ^ 262 | --collect.url="http://localhost:3000" ^ 263 | --upload.target=lhci ^ 264 | --upload.serverBaseUrl="[YOUR BASE URL]" ^ 265 | --upload.token="[YOUR_TOKEN]" 266 | ``` 267 | 268 | Be sure to verify the results on your heroku lighthouse CI server. 269 | 270 | --- 271 | 272 | ## 4. Connect lighthouse with github 273 | 274 | | ![pics/incomplete-checks.png](pics/incomplete-checks.png) | 275 | |:--:| 276 | | *We'll configure GitHub to require successful checks for merging* | 277 | 278 | You can do this for your own repository, or just follow along by observing. 279 | 280 | Starting with GitHub actions, you need to add an actions file. 281 | 282 | ### 4.1. Add an action file 283 | 284 | This [action file](https://github.com/umaar/learn-browser-testing/blob/master/.github/workflows/lighthouse-ci.yaml) is a sensible starting point. Just add it in your repo, under `.github/workflows/lighthouse-ci.yaml`. 285 | 286 | 💡️ If you've never used GitHub actions before, spend a bit of time [reading about them](https://docs.github.com/en/actions/reference). 287 | 288 | Note the final run command which I've used: 289 | 290 | ```sh 291 | npm run --prefix 3-auditing/1-lighthouse lighthouse-private-with-error 292 | ``` 293 | 294 | I've resorted to this command because of the directory structure of this particular repository, and because that command above is run from the `root` of the repo. 295 | 296 | If you're doing this in your own repo, you'll want to simplify that command to something like `npm test` or `npm run lighthouse`, and make sure the relevant script definition is in your `package.json`. 297 | 298 | ### 4.2. Enable the status check 299 | 300 | | ![pics/some-checks-not-successful.png](pics/some-checks-not-successful.png) | 301 | |:--:| 302 | | *We'll configure GitHub to require that all checks were successful before merging* | 303 | 304 | Now, configure a Lighthouse 'status' message to appear under pull requests. This can inform you whether or not the pull request passes the lighthouse audit. 305 | 306 | 1. Open https://github.com/apps/lighthouse-ci 307 | 2. Click `Configure` 308 | 3. Enable for the repo you are interested in 309 | 4. Click authorise 310 | 311 | Observe the message like: 312 | 313 | ``` 314 | Authorized 315 | Save the token below in a safe place. 316 | This is the only time it will be visible to you! 317 | Store the token as LHCI_GITHUB_APP_TOKEN in your build environment. 318 | 319 | abc:123 320 | ``` 321 | 322 | 5. Add the token as a GitHub secret, e.g. https://github.com/umaar/learn-browser-testing/settings/secrets/new 323 | 324 | + Token name = `LHCI_GITHUB_APP_TOKEN` 325 | + Value = `[value from the message you saw earlier]` 326 | 327 | 328 | 💡️ Understand why we are adding this as a secret. Hint, we added `LHCI_GITHUB_APP_TOKEN` in our actions file which exposes this as an [environment variable](https://docs.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables). 329 | 330 | ### 4.3. Make lighthouse mandatory 331 | 332 | Make the status check mandatory for merging a PR 333 | 334 | If the status check reports a failure, by default, this will not block pull requests from being merged. We can change this behaviour: 335 | 336 | 1. Add a new [protection rule](https://github.com/umaar/learn-browser-testing/settings/branch_protection_rules/new). 337 | + GitHub Repo > Settings > Branches > Branch protection rules > Add rule 338 | 2. Enter the following: 339 | + Branch name pattern = * 340 | + Require status checks to pass before merging = enabled 341 | + Require branches to be up to date before merging = enabled 342 | + Enable the status checks = Lighthouse CI and lhci/url/ 343 | 344 | --- 345 | 346 | ## 5. Finishing up 347 | 348 | | ![pics/ga-fail-log.png](pics/ga-fail-log.png) | 349 | |:--:| 350 | | *GitHub provides full console output logs so you can debug* | 351 | 352 | That was quite a few steps, but it should all be working now. 353 | 354 | Test this by making a PR to your repo, do you see the Lighthouse status checks? You can use their assertions feature (e.g. fail when this performance metric is too low) to block pull requests from merging. 355 | 356 | Here's a good way to check, by failing on the `heading-order` error. It's simple enough that you can add in some HTML like this to make the build pass/fail: 357 | 358 | ```html 359 |

heading 2

360 |

heading 1

361 |

heading 3

362 | ``` 363 | 364 | Here's the command to run: 365 | 366 | ```sh 367 | # from 3-auditing/1-lighthouse, or run this in your own repo 368 | # this is using my personal token and personal dashboard, feel free to swap with your own 369 | ../../node_modules/.bin/lhci autorun \ 370 | --collect.numberOfRuns=1 \ 371 | --collect.startServerCommand='npm start' \ 372 | --collect.url='http://localhost:3000' \ 373 | --upload.target=lhci \ 374 | --upload.serverBaseUrl='[YOUR_HEROKU_URL]' \ 375 | --upload.token='[YOUR_TOKEN]' \ 376 | --assert.assertions.heading-order=error 377 | ``` 378 | 379 | 💡️ Read up on the [`assertions`](https://github.com/GoogleChrome/lighthouse-ci/blob/master/docs/configuration.md#assertions) feature of the lighthouse ci tool. The `--assert.assertions` flag is what can instruct the PR to fail if the audit does not pass, so be sure to understand it. 380 | 381 | Finally, send me your repository! I can submit a PR and try to get things to fail/pass. 382 | 383 | --- 384 | 385 | ## Example dashboard 386 | 387 | Here's [my dashboard](https://salty-headland-92476.herokuapp.com/). -------------------------------------------------------------------------------- /3-auditing/1-lighthouse/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import nunjucks from 'nunjucks'; 3 | 4 | const app = express(); 5 | 6 | // Setup nunjucks templating engine 7 | nunjucks.configure('views', { 8 | autoescape: true, 9 | express: app 10 | }); 11 | 12 | app.set('port', process.env.PORT || 3000); 13 | 14 | // Home page 15 | app.get('/', function(req, res) { 16 | res.render('index.html', { 17 | page: 'home', 18 | port: app.get('port') 19 | }); 20 | }); 21 | 22 | // Other example 23 | app.get('/example', function(req, res) { 24 | res.render('example.html', { 25 | page: 'example', 26 | port: app.get('port') 27 | }); 28 | }); 29 | 30 | // Kick start our server 31 | app.listen(app.get('port'), function() { 32 | console.log('Server ready on port', app.get('port')); 33 | }); 34 | -------------------------------------------------------------------------------- /3-auditing/1-lighthouse/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index", 6 | "lighthouse": "../../node_modules/.bin/lhci autorun --collect.numberOfRuns=1 --collect.startServerCommand=\"npm start\" --collect.url=\"http://localhost:3000\" --upload.target=temporary-public-storage", 7 | "lighthouse-with-error": "../../node_modules/.bin/lhci autorun --collect.numberOfRuns=1 --collect.startServerCommand=\"npm start\" --collect.url=\"http://localhost:3000\" --upload.target=temporary-public-storage --assert.assertions.heading-order=error", 8 | "lighthouse-private": "../../node_modules/.bin/lhci autorun --collect.numberOfRuns=1 --collect.startServerCommand=\"npm start\" --collect.url=\"http://localhost:3000\" --upload.target=lhci --upload.serverBaseUrl=\"https://salty-headland-92476.herokuapp.com\" --upload.token=\"02fd25fc-e007-4ef9-9d88-eec9fa59f966\"", 9 | "lighthouse-private-with-error": "../../node_modules/.bin/lhci autorun --collect.numberOfRuns=1 --collect.startServerCommand=\"npm start\" --collect.url=\"http://localhost:3000\" --upload.target=lhci --upload.serverBaseUrl=\"https://salty-headland-92476.herokuapp.com\" --upload.token=\"02fd25fc-e007-4ef9-9d88-eec9fa59f966\" --assert.assertions.heading-order=error", 10 | "lighthouse-local": "../../node_modules/.bin/lhci autorun --collect.numberOfRuns=1 --collect.startServerCommand=\"npm start\" --collect.url=\"http://localhost:3000\" --upload.target=lhci --upload.serverBaseUrl=\"http://127.0.0.1:9001\" --upload.token=\"5ae28497-95ed-4aa1-93c4-525a1893b643\"", 11 | "lighthouse-local-server": "node ../../node_modules/@lhci/cli/src/cli.js server --storage.storageMethod=sql --storage.sqlDialect=sqlite --storage.sqlDatabasePath=./db.sql", 12 | "lighthouse-wizard": "node ../../node_modules/@lhci/cli/src/cli.js wizard" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /3-auditing/1-lighthouse/pics/all-checks-passing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/3-auditing/1-lighthouse/pics/all-checks-passing.png -------------------------------------------------------------------------------- /3-auditing/1-lighthouse/pics/ga-fail-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/3-auditing/1-lighthouse/pics/ga-fail-log.png -------------------------------------------------------------------------------- /3-auditing/1-lighthouse/pics/incomplete-checks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/3-auditing/1-lighthouse/pics/incomplete-checks.png -------------------------------------------------------------------------------- /3-auditing/1-lighthouse/pics/lh-dashboard-third-party-increase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/3-auditing/1-lighthouse/pics/lh-dashboard-third-party-increase.png -------------------------------------------------------------------------------- /3-auditing/1-lighthouse/pics/lh-single-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/3-auditing/1-lighthouse/pics/lh-single-report.png -------------------------------------------------------------------------------- /3-auditing/1-lighthouse/pics/lhci-dashboard-changes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/3-auditing/1-lighthouse/pics/lhci-dashboard-changes.png -------------------------------------------------------------------------------- /3-auditing/1-lighthouse/pics/lhci-dashboard-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/3-auditing/1-lighthouse/pics/lhci-dashboard-overview.png -------------------------------------------------------------------------------- /3-auditing/1-lighthouse/pics/some-checks-not-successful.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/3-auditing/1-lighthouse/pics/some-checks-not-successful.png -------------------------------------------------------------------------------- /3-auditing/1-lighthouse/views/example.html: -------------------------------------------------------------------------------- 1 | {% extends 'index.html' %} 2 | 3 | {% block content %} 4 | {{ super() }} 5 |

This is the example.html page

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /3-auditing/1-lighthouse/views/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Express ∓ Nunjucks 5 | 6 | 7 | 8 | 12 | 16 | 17 | 22 | 23 | 24 |
25 |
26 |
27 |

Express & Nunjucks

28 |
29 |
30 |
31 | 38 |
39 |
40 | 41 | {% block body %} 42 | {% endblock %} 43 | 44 |
45 |
46 |

App running on port: {{ port }}

47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /3-auditing/2-lighthouse-custom-audit/amazon-audit.js: -------------------------------------------------------------------------------- 1 | const Audit = require('lighthouse').Audit; 2 | 3 | class AmazonAudit extends Audit { 4 | static get meta() { 5 | return { 6 | id: 'amazon-audit', 7 | title: 'Price is reasonable', 8 | failureTitle: 'Price is too high', 9 | description: 'Used to ensure the price of an amazon product is not too high', 10 | requiredArtifacts: ['AmazonPrice'], 11 | }; 12 | } 13 | 14 | static audit(artifacts) { 15 | const rawPrice = artifacts.AmazonPrice; 16 | 17 | const price = Math.round(rawPrice.substr(1)); 18 | const passed = price < 150; 19 | 20 | return { 21 | displayValue: `Price of ${rawPrice} is ${passed ? 'ok' : 'too high!'}`, 22 | score: passed ? 1 : 0, 23 | }; 24 | } 25 | } 26 | 27 | module.exports = AmazonAudit; 28 | -------------------------------------------------------------------------------- /3-auditing/2-lighthouse-custom-audit/custom-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'lighthouse:default', 3 | 4 | passes: [{ 5 | passName: 'defaultPass', 6 | gatherers: [ 7 | 'product-price-gatherer' 8 | ] 9 | }], 10 | 11 | audits: [ 12 | 'amazon-audit' 13 | ], 14 | 15 | categories: { 16 | mysite: { 17 | title: 'Amazon Site Metrics', 18 | description: 'Metrics for Amazon', 19 | auditRefs: [ 20 | {id: 'amazon-audit', weight: 1} 21 | ] 22 | }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /3-auditing/2-lighthouse-custom-audit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "private": true, 4 | "scripts": { 5 | "start" : "../../node_modules/.bin/lighthouse --view --quiet --chrome-flags=\"--headless\" --config-path=custom-config.js https://automatebrowsers.com/amazon/cat-mug/", 6 | "single-audit" : "../../node_modules/.bin/lighthouse --view --quiet --chrome-flags=\"--headless\" --only-audits=amazon-audit --config-path=custom-config.js https://automatebrowsers.com/amazon/cat-mug/" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /3-auditing/2-lighthouse-custom-audit/product-price-gatherer.js: -------------------------------------------------------------------------------- 1 | const Gatherer = require('lighthouse').Gatherer; 2 | 3 | class AmazonPrice extends Gatherer { 4 | afterPass({driver}) { 5 | const querySelector = `document.querySelector('#priceblock_ourprice').innerText`; 6 | return driver.evaluateAsync(querySelector) 7 | } 8 | } 9 | 10 | module.exports = AmazonPrice; 11 | -------------------------------------------------------------------------------- /3-auditing/2-lighthouse-custom-audit/readme.md: -------------------------------------------------------------------------------- 1 | ## To run 2 | 3 | ```sh 4 | npm start 5 | 6 | # or 7 | 8 | yarn start 9 | 10 | # or to run a single audit (a bit faster) 11 | 12 | npm run single-audit 13 | 14 | # or 15 | 16 | yarn single-audit 17 | ``` 18 | 19 | ## Note 20 | 21 | 💡️ This is __not__ a a demonstration of _good_ lighthouse architecture. It's just to show how you can technically wire up code to extract something off of a page, and make an assertion on it. 22 | 23 | -------------------------------------------------------------------------------- /3-auditing/3-visual-testing/amazon-cat-mug/.env.sample: -------------------------------------------------------------------------------- 1 | PERCY_TOKEN= -------------------------------------------------------------------------------- /3-auditing/3-visual-testing/amazon-cat-mug/index.js: -------------------------------------------------------------------------------- 1 | import percy from '@percy/puppeteer'; 2 | import puppeteer from 'puppeteer'; 3 | 4 | const {percySnapshot} = percy; 5 | 6 | const browser = await puppeteer.launch({ 7 | headless: false, 8 | args: [ 9 | '--window-size=1000,1000' 10 | ] 11 | }); 12 | 13 | const page = await browser.newPage(); 14 | await page.setViewport({ width: 1000, height: 1000 }); 15 | 16 | await page.goto('https://automatebrowsers.com/amazon/cat-mug/', { 17 | waitUntil: ['networkidle0'] 18 | }); 19 | 20 | await percySnapshot(page, 'Cat Mug Page'); 21 | 22 | await browser.close(); -------------------------------------------------------------------------------- /3-auditing/3-visual-testing/amazon-cat-mug/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "../../../node_modules/.bin/percy exec -- node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /3-auditing/3-visual-testing/amazon-cat-mug/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To start 3 | 4 | 1. Make a free [percy.io](https://percy.io/) account. 5 | 2. In the Percy dashboard, make a new project and copy the token from project settings 6 | 3. Copy `.env.sample` over to `.env` and fill in your token 7 | 8 | ## To run 9 | 10 | ```sh 11 | npm start 12 | 13 | # or 14 | 15 | yarn start 16 | ``` 17 | 18 | ## Example 19 | 20 | https://percy.io/559ccd2f -------------------------------------------------------------------------------- /3-auditing/3-visual-testing/modern-devtools/.env.sample: -------------------------------------------------------------------------------- 1 | PERCY_TOKEN= -------------------------------------------------------------------------------- /3-auditing/3-visual-testing/modern-devtools/actions.js: -------------------------------------------------------------------------------- 1 | import {promisify} from 'util' 2 | import puppeteer from 'puppeteer'; 3 | 4 | const sleep = promisify(setTimeout); 5 | 6 | function random() { 7 | return Math.random().toString(36).substr(2, 9); 8 | } 9 | 10 | async function closeBrowser(browser) { 11 | await sleep(2000); 12 | await browser.close(); 13 | } 14 | 15 | async function openModernDevTools() { 16 | const browser = await puppeteer.launch({ 17 | headless: false, 18 | slowMo: 3, 19 | // https://github.com/GoogleChrome/puppeteer/issues/2548#issuecomment-390077713 20 | args: [ 21 | '--disable-features=site-per-process', 22 | '--no-sandbox', 23 | '--window-size=1000,1000', 24 | ] 25 | }); 26 | 27 | const page = await browser.newPage(); 28 | await page.setViewport({ width: 1000, height: 1000 }); 29 | 30 | await page.goto('http://localhost:3000', { 31 | waitUntil: ['load', 'domcontentloaded', 'networkidle0'] 32 | }); 33 | 34 | return {page, browser}; 35 | } 36 | 37 | async function navigateToPaymentPage(page) { 38 | await Promise.all([ 39 | page.waitForNavigation({ 40 | waitUntil: ['load', 'domcontentloaded', 'networkidle0'] 41 | }), 42 | page.click('[href="/pay"]') 43 | ]); 44 | } 45 | 46 | async function fillAndSubmitPaymentForm(page) { 47 | await page.waitForSelector(`[name="__privateStripeFrame5"]`); 48 | 49 | const frames = page.frames(); 50 | 51 | const email = `test+${random()}@blackhole.postmarkapp.com` 52 | await page.type('#user-email', email); 53 | 54 | 55 | const cc = frames.find(frame => frame.name() === '__privateStripeFrame5'); 56 | const exp = frames.find(frame => frame.name() === '__privateStripeFrame6'); 57 | const cvc = frames.find(frame => frame.name() === '__privateStripeFrame7'); 58 | 59 | await cc.type('[name="cardnumber"]', '4242 4242 4242 4242'); 60 | await exp.type('[name="exp-date"]', '0125'); 61 | await cvc.type('[name="cvc"]', '123'); 62 | 63 | await Promise.all([ 64 | page.waitForNavigation({ 65 | waitUntil: ['load', 'domcontentloaded', 'networkidle0'] 66 | }), 67 | page.click('.buy-button') 68 | ]); 69 | } 70 | 71 | export { 72 | openModernDevTools, 73 | navigateToPaymentPage, 74 | fillAndSubmitPaymentForm, 75 | closeBrowser 76 | }; -------------------------------------------------------------------------------- /3-auditing/3-visual-testing/modern-devtools/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | closeBrowser, 3 | navigateToPaymentPage, 4 | openModernDevTools, 5 | fillAndSubmitPaymentForm 6 | } from './actions.js'; 7 | 8 | import percy from '@percy/puppeteer'; 9 | 10 | const {percySnapshot} = percy; 11 | 12 | const {page, browser} = await openModernDevTools(); 13 | await percySnapshot(page, 'Home Page'); 14 | await navigateToPaymentPage(page); 15 | await percySnapshot(page, 'Payment Page'); 16 | await fillAndSubmitPaymentForm(page); 17 | await percySnapshot(page, 'Payment Success Page'); 18 | 19 | await closeBrowser(browser); -------------------------------------------------------------------------------- /3-auditing/3-visual-testing/modern-devtools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "../../../node_modules/.bin/percy exec -- node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /3-auditing/3-visual-testing/modern-devtools/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ⚠️ This tests a website which you don't have access to, and is only for demonstrations purposes. Please use the `amazon-cat-mug` folder instead. 3 | 4 | ## To start 5 | 6 | 1. Make a free [percy.io](https://percy.io/) account. 7 | 2. In the Percy dashboard, make a new project and copy the token from project settings 8 | 3. Copy `.env.sample` over to `.env` and fill in your token 9 | 10 | ## To run 11 | 12 | ```sh 13 | npm start 14 | 15 | # or 16 | 17 | yarn start 18 | ``` 19 | 20 | ## Example 21 | 22 | https://percy.io/559ccd2f -------------------------------------------------------------------------------- /3-auditing/3-visual-testing/single-element/index.js: -------------------------------------------------------------------------------- 1 | import Differencify from 'differencify'; 2 | const differencify = new Differencify(); 3 | 4 | await differencify.launchBrowser(); 5 | const target = differencify.init({ 6 | testName: 'Cat mug', chain: false 7 | }); 8 | 9 | const page = await target.newPage(); 10 | await page.goto('https://automatebrowsers.com/amazon/cat-mug/', { 11 | waitUntil: ['networkidle0'] 12 | }); 13 | await page.setViewport({ width: 1600, height: 1200 }); 14 | const priceBox = await page.$('.a-box-group'); 15 | const image = await priceBox.screenshot(); 16 | // const image = await page.screenshot(); 17 | const identical = await target.toMatchSnapshot(image); 18 | await page.close(); 19 | 20 | if (identical) { 21 | console.log('✅️ No change detected'); 22 | } else { 23 | console.log('⚠️ A change has been detected'); 24 | } 25 | 26 | await differencify.cleanup(); 27 | -------------------------------------------------------------------------------- /3-auditing/3-visual-testing/single-element/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /3-auditing/3-visual-testing/single-element/readme.md: -------------------------------------------------------------------------------- 1 | ## To run 2 | 3 | ```sh 4 | npm start 5 | 6 | # or 7 | 8 | yarn start 9 | ``` 10 | 11 | Then check the `differencify_reports` folder. -------------------------------------------------------------------------------- /3-auditing/4-code-coverage/index.js: -------------------------------------------------------------------------------- 1 | // https://github.com/addyosmani/puppeteer-webperf/blob/master/code-coverage.js 2 | 3 | import puppeteer from 'puppeteer'; 4 | 5 | const browser = await puppeteer.launch(); 6 | const page = await browser.newPage(); 7 | 8 | // Gather coverage for JS and CSS files 9 | await Promise.all([page.coverage.startJSCoverage(), page.coverage.startCSSCoverage()]); 10 | 11 | await page.goto('https://twitter.com/'); 12 | 13 | // Stops the coverage gathering 14 | const [jsCoverage, cssCoverage] = await Promise.all([ 15 | page.coverage.stopJSCoverage(), 16 | page.coverage.stopCSSCoverage(), 17 | ]); 18 | 19 | // Calculates # bytes being used based on the coverage 20 | const calculateUsedBytes = (type, coverage) => 21 | coverage.map(({url, ranges, text}) => { 22 | let usedBytes = 0; 23 | 24 | ranges.forEach((range) => (usedBytes += range.end - range.start - 1)); 25 | 26 | return { 27 | url, 28 | type, 29 | usedBytes, 30 | totalBytes: text.length, 31 | percentUsed: `${(usedBytes / text.length * 100).toFixed(2)}%` 32 | }; 33 | }); 34 | 35 | console.info([ 36 | ...calculateUsedBytes('js', jsCoverage), 37 | ...calculateUsedBytes('css', cssCoverage), 38 | ]); 39 | 40 | await browser.close(); 41 | -------------------------------------------------------------------------------- /3-auditing/4-code-coverage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /3-auditing/4-code-coverage/readme.md: -------------------------------------------------------------------------------- 1 | ## To run 2 | 3 | ```sh 4 | npm start 5 | 6 | # or 7 | 8 | yarn start 9 | ``` 10 | 11 | ## Credits 12 | 13 | Full credits: [here](https://github.com/addyosmani/puppeteer-webperf) 14 | -------------------------------------------------------------------------------- /3-auditing/5-devtools-trace/index.js: -------------------------------------------------------------------------------- 1 | // https://github.com/addyosmani/puppeteer-webperf 2 | import puppeteer from 'puppeteer'; 3 | 4 | const browser = await puppeteer.launch(); 5 | const page = await browser.newPage(); 6 | // Drag and drop this JSON file to the DevTools Performance panel! 7 | await page.tracing.start({path: 'profile.json'}); 8 | await page.goto('https://www.facebook.com/'); 9 | await page.tracing.stop(); 10 | await browser.close(); -------------------------------------------------------------------------------- /3-auditing/5-devtools-trace/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /3-auditing/5-devtools-trace/readme.md: -------------------------------------------------------------------------------- 1 | ## To run 2 | 3 | ```sh 4 | npm start 5 | 6 | # or 7 | 8 | yarn start 9 | ``` 10 | 11 | ## Credits 12 | 13 | Full credits: [here](https://github.com/addyosmani/puppeteer-webperf) 14 | -------------------------------------------------------------------------------- /4-deploying/1-github-actions/ava/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "scripts": { 4 | "start": "../../../node_modules/.bin/ava" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /4-deploying/1-github-actions/ava/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/4-deploying/1-github-actions/ava/preview.png -------------------------------------------------------------------------------- /4-deploying/1-github-actions/ava/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` 11 | 12 | ## Preview 13 | 14 | Here's what it looks like when run via GitHub actions 15 | 16 | | ![preview.png](preview.png) | 17 | |:--:| 18 | | *Preview of GitHub actions output* | -------------------------------------------------------------------------------- /4-deploying/1-github-actions/ava/test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const withPage = require('./with-page.js'); 3 | 4 | test('This is test 1', withPage, async (t, page) => { 5 | await page.goto('about:blank'); 6 | const result = await page.evaluate('1 + 1'); 7 | t.is(result, 2, 'Value is 2'); 8 | }); 9 | 10 | test('This is test 2', withPage, async (t, page) => { 11 | await page.goto('about:blank'); 12 | const result = await page.evaluate('1 + 1'); 13 | t.is(result, 2, 'Value is 2'); 14 | }); 15 | 16 | test('This is test 3', withPage, async (t, page) => { 17 | await page.goto('about:blank'); 18 | const result = await page.evaluate('1 + 1'); 19 | t.is(result, 3, 'Value is 2'); 20 | }); 21 | 22 | test('This is test 4', withPage, async (t, page) => { 23 | await page.goto('about:blank'); 24 | const result = await page.evaluate('1 + 1'); 25 | t.is(result, 2, 'Value is 2'); 26 | }); 27 | 28 | -------------------------------------------------------------------------------- /4-deploying/1-github-actions/ava/with-page.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | 3 | async function withPage(t, run) { 4 | const browser = await puppeteer.launch(); 5 | const page = await browser.newPage(); 6 | try { 7 | await run(t, page); 8 | } finally { 9 | await page.close(); 10 | await browser.close(); 11 | } 12 | } 13 | 14 | module.exports = withPage; -------------------------------------------------------------------------------- /4-deploying/1-github-actions/cypress/cypress-bot-comment-fail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/4-deploying/1-github-actions/cypress/cypress-bot-comment-fail.png -------------------------------------------------------------------------------- /4-deploying/1-github-actions/cypress/cypress-bot-comment-pass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/4-deploying/1-github-actions/cypress/cypress-bot-comment-pass.png -------------------------------------------------------------------------------- /4-deploying/1-github-actions/cypress/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectId": "skdry1" 3 | } 4 | -------------------------------------------------------------------------------- /4-deploying/1-github-actions/cypress/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /4-deploying/1-github-actions/cypress/cypress/integration/examples/example-test.js: -------------------------------------------------------------------------------- 1 | describe('my tests', () => { 2 | it('Has the correct title test 1', () => { 3 | cy.visit('https://example.com'); 4 | cy.title().should('eq', 'Example Domain'); 5 | }); 6 | 7 | it('Has the correct title test 2', () => { 8 | cy.visit('https://example.com'); 9 | cy.title().should('eq', 'Example Domain'); 10 | }); 11 | 12 | it('Has the correct title test 3', () => { 13 | cy.visit('https://example.com'); 14 | cy.title().should('eq', 'hey hey'); 15 | }); 16 | 17 | it('Has the correct title test 4', () => { 18 | cy.visit('https://example.com'); 19 | cy.title().should('eq', 'Example Domain'); 20 | }); 21 | }); -------------------------------------------------------------------------------- /4-deploying/1-github-actions/cypress/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | }; 22 | -------------------------------------------------------------------------------- /4-deploying/1-github-actions/cypress/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /4-deploying/1-github-actions/cypress/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /4-deploying/1-github-actions/cypress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "scripts": { 4 | "start": "CYPRESS_PROJECT_ID=skdry1 ../../../node_modules/.bin/cypress run --record" 5 | } 6 | } -------------------------------------------------------------------------------- /4-deploying/1-github-actions/cypress/permission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/4-deploying/1-github-actions/cypress/permission.png -------------------------------------------------------------------------------- /4-deploying/1-github-actions/cypress/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/4-deploying/1-github-actions/cypress/preview.png -------------------------------------------------------------------------------- /4-deploying/1-github-actions/cypress/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | CYPRESS_PROJECT_ID=YOUR_ID CYPRESS_RECORD_KEY=YOUR_KEY ../../../node_modules/.bin/cypress run --record 6 | ``` 7 | 8 | ## Example 9 | 10 | https://github.com/umaar/learn-browser-testing/pull/10 11 | 12 | ## Preview 13 | 14 | Here's what it looks like when run via GitHub actions 15 | 16 | | ![preview.png](preview.png) | 17 | |:--:| 18 | | *Preview of GitHub actions output* | 19 | 20 | ### Permissions 21 | 22 | You need to authorise Cypress with GitHub, from their Cypress Dashboard service. 23 | 24 | ![permission.png](permission.png) 25 | 26 | Example [link](https://github.com/umaar/learn-browser-testing/runs/1066585437) -------------------------------------------------------------------------------- /4-deploying/1-github-actions/jest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "../../../node_modules/.bin/jest" 6 | }, 7 | "jest": { 8 | "preset": "jest-puppeteer", 9 | "reporters": [ 10 | "default", 11 | "jest-github-actions-reporter" 12 | ], 13 | "testLocationInResults": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /4-deploying/1-github-actions/jest/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/4-deploying/1-github-actions/jest/preview.png -------------------------------------------------------------------------------- /4-deploying/1-github-actions/jest/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` 11 | 12 | ## Preview 13 | 14 | Here's what it looks like when run via GitHub actions 15 | 16 | | ![preview.png](preview.png) | 17 | |:--:| 18 | | *Preview of GitHub actions output* | 19 | 20 | Example [link](https://github.com/umaar/learn-browser-testing/actions/runs/237672367) -------------------------------------------------------------------------------- /4-deploying/1-github-actions/jest/test.js: -------------------------------------------------------------------------------- 1 | describe('Showcasing GitHub Actions', () => { 2 | beforeAll(async () => { 3 | await page.goto('about:blank'); 4 | }); 5 | 6 | it('Test 1', async () => { 7 | const result = await page.evaluate('1 + 1'); 8 | expect(result).toBe(2); 9 | }); 10 | 11 | it('Test 2', async () => { 12 | const result = await page.evaluate('1 + 1'); 13 | expect(result).toBe(2); 14 | }); 15 | 16 | it('Test 3', async () => { 17 | const result = await page.evaluate('1 + 1'); 18 | expect(result).toBe(3); 19 | }); 20 | 21 | it('Test 4', async () => { 22 | const result = await page.evaluate('1 + 1'); 23 | expect(result).toBe(2); 24 | }); 25 | }); -------------------------------------------------------------------------------- /4-deploying/1-github-actions/puppeteer/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | import tests1 from './test-1.js'; 4 | import tests2 from './test-2.js'; 5 | import tests3 from './test-3.js'; 6 | 7 | const allTests = {...tests1, ...tests2, ...tests3}; 8 | 9 | const browser = await puppeteer.launch(); 10 | const page = await browser.newPage(); 11 | await page.setViewport({ width: 1280, height: 720 }); 12 | 13 | for (const [testName, testFunction] of Object.entries(allTests)) { 14 | console.log(`\n> Running: ${testName}`); 15 | try { 16 | await testFunction(page); 17 | console.log(`\tPass`); 18 | } catch (error) { 19 | await browser.close(); 20 | console.log('\tFail\n'); 21 | console.log(error); 22 | process.exit(1); 23 | } 24 | } 25 | 26 | await browser.close(); 27 | console.log('\nAll tests passed! ✅️'); 28 | -------------------------------------------------------------------------------- /4-deploying/1-github-actions/puppeteer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /4-deploying/1-github-actions/puppeteer/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/4-deploying/1-github-actions/puppeteer/preview.png -------------------------------------------------------------------------------- /4-deploying/1-github-actions/puppeteer/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` 11 | 12 | ## Preview 13 | 14 | Here's what it looks like when run via GitHub actions 15 | 16 | | ![preview.png](preview.png) | 17 | |:--:| 18 | | *Preview of GitHub actions output* | -------------------------------------------------------------------------------- /4-deploying/1-github-actions/puppeteer/test-1.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | const tests = { 4 | async 'This is a test in the test-1 file'(page) { 5 | await page.goto('about:blank'); 6 | const result = await page.evaluate('1 + 1'); 7 | assert.equal(result, '2', 'The result is 2'); 8 | }, 9 | 10 | async 'This is another test in the test-1 file'(page) { 11 | await page.goto('about:blank'); 12 | const result = await page.evaluate('1 + 1'); 13 | assert.equal(result, '2', 'The result is 2'); 14 | } 15 | }; 16 | 17 | export default tests; -------------------------------------------------------------------------------- /4-deploying/1-github-actions/puppeteer/test-2.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | const tests = { 4 | async 'This is a test in the test-2 file'(page) { 5 | await page.goto('about:blank'); 6 | const result = await page.evaluate('1 + 1'); 7 | assert.equal(result, '2', 'The result is 2'); 8 | }, 9 | 10 | async 'This is another test in the test-2 file'(page) { 11 | await page.goto('about:blank'); 12 | const result = await page.evaluate('1 + 1'); 13 | assert.equal(result, '2', 'The result is 2'); 14 | } 15 | }; 16 | 17 | export default tests; -------------------------------------------------------------------------------- /4-deploying/1-github-actions/puppeteer/test-3.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | const tests = { 4 | async 'This is a test in the test-3 file'(page) { 5 | await page.goto('about:blank'); 6 | const result = await page.evaluate('1 + 1'); 7 | assert.equal(result, '2', 'The result is 2'); 8 | }, 9 | 10 | async 'This is another test in the test-3 file'(page) { 11 | await page.goto('about:blank'); 12 | const result = await page.evaluate('1 + 1'); 13 | assert.equal(result, '2', 'The result is 2'); 14 | } 15 | }; 16 | 17 | export default tests; -------------------------------------------------------------------------------- /4-deploying/1-github-actions/testcafe/index.js: -------------------------------------------------------------------------------- 1 | fixture`Testcafe demo`.page`https://example.com`; 2 | 3 | test('Testcafe example test 1', async t => { 4 | const title = await t.eval(() => document.title); 5 | await t.expect(title).eql('Example Domain', 'Has the correct title'); 6 | }); 7 | 8 | test('Testcafe example test 2', async t => { 9 | const title = await t.eval(() => document.title); 10 | await t.expect(title).eql('Example Domain', 'Has the correct title'); 11 | }); 12 | 13 | test('Testcafe example test 3', async t => { 14 | const title = await t.eval(() => document.title); 15 | await t.expect(title).eql('Example.com', 'Has the correct title'); 16 | }); -------------------------------------------------------------------------------- /4-deploying/1-github-actions/testcafe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "../../../node_modules/.bin/testcafe 'chrome:headless' index.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /4-deploying/1-github-actions/testcafe/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/4-deploying/1-github-actions/testcafe/preview.png -------------------------------------------------------------------------------- /4-deploying/1-github-actions/testcafe/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` 11 | 12 | ## Preview 13 | 14 | Here's what it looks like when run via GitHub actions 15 | 16 | | ![preview.png](preview.png) | 17 | |:--:| 18 | | *Preview of GitHub actions output* | 19 | 20 | Example [link](https://github.com/umaar/learn-browser-testing/runs/1066543162) -------------------------------------------------------------------------------- /4-deploying/2-vps/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | console.log('\nOpening puppeteer\n'); 4 | 5 | const browser = await puppeteer.launch({ 6 | headless: true, 7 | args: ['--no-sandbox', '--disable-setuid-sandbox'] 8 | }); 9 | const page = await browser.newPage(); 10 | await page.goto('https://umaar.com'); 11 | 12 | console.log('Page title = ', await page.title()); 13 | 14 | await browser.close(); 15 | console.log('\nPuppeteer finished\n'); 16 | -------------------------------------------------------------------------------- /4-deploying/2-vps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /4-deploying/2-vps/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` 11 | 12 | ## VPS instructions 13 | 14 | ```sh 15 | curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - 16 | sudo apt-get install -y nodejs && node -v 17 | 18 | git init --bare learn-browser-testing.git 19 | ``` 20 | 21 | ```sh 22 | git remote add vps root@IP_ADDRESS:/root/learn-browser-testing.git 23 | ``` 24 | 25 | ```sh 26 | git clone learn-browser-testing.git 27 | ``` 28 | 29 | ```sh 30 | cd learn-browser-testing 31 | npm install --no-package-lock puppeteer 32 | ``` 33 | 34 | ```sh 35 | #!/bin/sh 36 | echo 'Post receive starting' 37 | unset GIT_DIR 38 | cd /root/learn-browser-testing 39 | git fetch origin master 40 | git reset --hard origin/master 41 | cd 4-deploying/2-vps/ 42 | npm start 43 | echo 'Post Receive Complete!' 44 | ``` 45 | 46 | ```sh 47 | apt-get instsall -y ca-certificates fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils 48 | 26 apt-get install -y ca-certificates fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils 49 | ``` 50 | -------------------------------------------------------------------------------- /4-deploying/3-videos-simple/index.js: -------------------------------------------------------------------------------- 1 | import {mkdirSync} from 'fs'; 2 | import path from 'path'; 3 | import {chromium} from 'playwright'; 4 | import playwrightVideo from 'playwright-video'; 5 | 6 | const artifactsFolder = path.join(process.cwd(), `test-output`); 7 | mkdirSync(artifactsFolder, {recursive: true}); 8 | 9 | const fullVideoPath = path.join(artifactsFolder, 'video.mp4'); 10 | 11 | console.log('Launching Chrome'); 12 | 13 | const browser = await chromium.launch({ 14 | slowMo: 100, 15 | args: ['--no-sandbox', '--disable-setuid-sandbox'] 16 | }); 17 | 18 | const context = await browser.newContext(); 19 | const page = await context.newPage(); 20 | page.setDefaultTimeout(10000); 21 | 22 | const capture = await playwrightVideo.saveVideo(page, fullVideoPath); 23 | 24 | try { 25 | await page.goto('https://umaar.com/'); 26 | await page.click(`a[href$="/dev-tips/"]`); 27 | await page.click(`a[href$="/blog/"]`); 28 | await page.click(`a[href$="/videos/"]`); 29 | await page.click(`a[href$="/code/"]`); 30 | } finally { 31 | console.log('Closing browser'); 32 | await capture.stop(); 33 | await browser.close(); 34 | } 35 | -------------------------------------------------------------------------------- /4-deploying/3-videos-simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /4-deploying/3-videos-simple/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` 11 | 12 | ## Artifact 13 | 14 | - Local: Check the `test-output` folder 15 | - GitHub actions: [View it here](https://github.com/umaar/learn-browser-testing/actions/runs/234871351) -------------------------------------------------------------------------------- /4-deploying/4-videos-screenshots/index.js: -------------------------------------------------------------------------------- 1 | import {mkdirSync} from 'fs'; 2 | import path from 'path'; 3 | import {chromium} from 'playwright'; 4 | import playwrightVideo from 'playwright-video'; 5 | 6 | import tests1 from './test-1.js'; 7 | import tests2 from './test-2.js'; 8 | import tests3 from './test-3.js'; 9 | 10 | const allTests = {...tests1, ...tests2, ...tests3}; 11 | 12 | const artifactsFolder = path.join(process.cwd(), `test-output`); 13 | mkdirSync(artifactsFolder, {recursive: true}); 14 | 15 | const fullVideoPath = path.join(artifactsFolder, 'video.mp4'); 16 | 17 | console.log('Launching Chrome'); 18 | 19 | const browser = await chromium.launch({ 20 | slowMo: 50, 21 | args: ['--no-sandbox', '--disable-setuid-sandbox'] 22 | }); 23 | 24 | const context = await browser.newContext(); 25 | const page = await context.newPage(); 26 | page.setDefaultTimeout(10000); 27 | 28 | const capture = await playwrightVideo.saveVideo(page, fullVideoPath); 29 | 30 | async function takeScreenshot(page, testName) { 31 | const fileName = `${testName}.png` 32 | const fullScreenshotPath = path.join(artifactsFolder, fileName) 33 | 34 | await page.screenshot({ 35 | path: fullScreenshotPath 36 | }); 37 | } 38 | 39 | for (const [testName, testFunction] of Object.entries(allTests)) { 40 | console.log(`\n> Running: ${testName}`); 41 | try { 42 | await testFunction(page); 43 | await takeScreenshot(page, `PASS - ${testName}`); 44 | console.log(`\tPass`); 45 | } catch (error) { 46 | await takeScreenshot(page, `FAIL - ${testName}`); 47 | await capture.stop(); 48 | await browser.close(); 49 | console.log('\tFail\n', error); 50 | process.exit(1); 51 | } 52 | } 53 | 54 | await capture.stop(); 55 | await browser.close(); 56 | console.log('\nAll tests passed! ✅️'); -------------------------------------------------------------------------------- /4-deploying/4-videos-screenshots/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /4-deploying/4-videos-screenshots/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` 11 | 12 | ## Artifact 13 | 14 | - Local: Check the `test-output` folder 15 | - GitHub actions: [View it here](https://github.com/umaar/learn-browser-testing/runs/1058540887) -------------------------------------------------------------------------------- /4-deploying/4-videos-screenshots/test-1.js: -------------------------------------------------------------------------------- 1 | const tests = { 2 | async 'Menu Navigation'(page) { 3 | await page.goto('https://umaar.com/'); 4 | await page.click(`a[href$="/dev-tips/"]`); 5 | await page.click(`a[href$="/blog/"]`); 6 | await page.click(`a[href$="/videos/"]`); 7 | await page.click(`a[href$="/code/"]`); 8 | } 9 | }; 10 | 11 | export default tests; -------------------------------------------------------------------------------- /4-deploying/4-videos-screenshots/test-2.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | const tests = { 4 | async 'Single Dev Tips title'(page) { 5 | await page.goto('https://umaar.com/dev-tips/'); 6 | await page.click(`a[href="/dev-tips/197-clear-site-data/"]`); 7 | const h1 = await page.$eval('h1', el => el.textContent); 8 | assert.equal(h1, 'Chrome DevTools: Quickly clear the data from a website'); 9 | } 10 | }; 11 | 12 | export default tests; -------------------------------------------------------------------------------- /4-deploying/4-videos-screenshots/test-3.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | const tests = { 4 | async 'Single blog title'(page) { 5 | await page.goto('https://umaar.com/blog/'); 6 | await page.click(`a[href="/blog/spreadsheet-to-compare-job-offers-in-tech/"]`); 7 | const h1 = await page.$eval('h1', el => el.textContent); 8 | assert.equal(h1, 'A spreadsheet to compare job offers'); 9 | } 10 | }; 11 | 12 | export default tests; -------------------------------------------------------------------------------- /4-deploying/5-trace-and-har/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | import HAR from 'puppeteer-har'; 3 | 4 | import {mkdirSync} from 'fs'; 5 | import path from 'path'; 6 | 7 | const artifactsFolder = path.join(process.cwd(), `test-output`); 8 | mkdirSync(artifactsFolder, {recursive: true}); 9 | 10 | const browser = await puppeteer.launch(); 11 | 12 | const page = await browser.newPage(); 13 | 14 | const har = new HAR(page); 15 | 16 | console.log('Stating HAR and devtools trace recordings'); 17 | 18 | await har.start({ 19 | path: path.join(artifactsFolder, 'http-archive.har') 20 | }); 21 | 22 | await page.tracing.start({ 23 | path: path.join(artifactsFolder, 'devtools.json') 24 | }); 25 | 26 | await page.goto('https://moderndevtools.com/', { 27 | waitUntil: ['networkidle0'] 28 | }); 29 | 30 | await page.tracing.stop(); 31 | 32 | await har.stop(); 33 | 34 | console.log('Finished HAR and devtools trace recordings'); 35 | 36 | await browser.close(); 37 | -------------------------------------------------------------------------------- /4-deploying/5-trace-and-har/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /4-deploying/5-trace-and-har/readme.md: -------------------------------------------------------------------------------- 1 | ## To run 2 | 3 | ```sh 4 | npm start 5 | 6 | # or 7 | 8 | yarn start 9 | ``` 10 | 11 | ## Artifact 12 | 13 | - Local: Check the `test-output` folder 14 | - GitHub actions: [View it here](https://github.com/umaar/learn-browser-testing/runs/1058758073) -------------------------------------------------------------------------------- /4-deploying/6-serverless/functions/hello.js: -------------------------------------------------------------------------------- 1 | exports.handler = function(event, context, callback) { 2 | callback(null, { 3 | statusCode: 200, 4 | body: "Hello world!" 5 | }); 6 | }; -------------------------------------------------------------------------------- /4-deploying/6-serverless/functions/screenshot.js: -------------------------------------------------------------------------------- 1 | const chromium = require('chrome-aws-lambda') 2 | const puppeteer = require('puppeteer-core'); 3 | const { launch } = require('puppeteer-core'); 4 | 5 | async function launchBrowser() { 6 | const executablePath = await chromium.executablePath; 7 | 8 | return puppeteer.launch({ 9 | args: chromium.args, 10 | executablePath: executablePath, 11 | headless: chromium.headless 12 | }); 13 | } 14 | 15 | exports.handler = async ({queryStringParameters}) => { 16 | const {url = "https://umaar.com"} = queryStringParameters; 17 | let screenshot = null; 18 | let browser = null; 19 | 20 | try { 21 | console.log('Launching browser...'); 22 | browser = await launchBrowser(); 23 | const page = await browser.newPage() 24 | 25 | await page.goto(url, { 26 | waitUntil: ["domcontentloaded", "networkidle0"] 27 | }); 28 | 29 | screenshot = await page.screenshot({ 30 | encoding: 'base64' 31 | }); 32 | } catch (error) { 33 | console.log(error); 34 | 35 | return { 36 | statusCode: 500, 37 | body: JSON.stringify({ 38 | error 39 | }) 40 | }; 41 | } finally { 42 | if (browser !== null) { 43 | await browser.close(); 44 | } 45 | } 46 | 47 | return { 48 | statusCode: 200, 49 | headers: { 50 | 'Content-type': 'image/jpeg' 51 | }, 52 | body: screenshot, 53 | isBase64Encoded: true 54 | } 55 | } -------------------------------------------------------------------------------- /4-deploying/6-serverless/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 8 | 9 |

This is a demo!

10 | 11 | -------------------------------------------------------------------------------- /4-deploying/6-serverless/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## About 3 | 4 | There's nothing to `npm start` here, this is just a demonstration of a serverless function executing a puppeteer script 5 | 6 | ## Demo 7 | 8 | This serverless function returns a screenshot of the webpage. 9 | 10 | - https://serverless.automatebrowsers.com/.netlify/functions/screenshot?url=https://example.com 11 | 12 | ## Deploy 13 | 14 | Deploying can be done at the root of this repo: 15 | 16 | ``` 17 | netlify deploy 18 | ``` 19 | 20 | ## Example function 21 | 22 | ```js 23 | exports.handler = async () => { 24 | return { 25 | statusCode: 200, 26 | body: 'Hey hey' 27 | } 28 | } 29 | ``` 30 | 31 | ## Example netlify.toml 32 | 33 | ```toml 34 | [build] 35 | command = "#" 36 | publish = "page" 37 | functions = "functions" 38 | ``` -------------------------------------------------------------------------------- /5-debugging/1-page-console/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | const browser = await puppeteer.launch(); 4 | 5 | const page = await browser.newPage(); 6 | 7 | page.on('console', msg => { 8 | console.log('\t\tLog: ', msg.text(), msg.location().url); 9 | }); 10 | 11 | await page.goto('https://reddit.com', { 12 | waitUntil: ['networkidle0'] 13 | }); 14 | 15 | await browser.close(); 16 | -------------------------------------------------------------------------------- /5-debugging/1-page-console/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /5-debugging/1-page-console/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | ```sh 5 | npm start 6 | 7 | # or 8 | 9 | yarn start 10 | ``` -------------------------------------------------------------------------------- /5-debugging/2-node-repl/index.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | const browser = await puppeteer.launch({headless: false}); 3 | const page = await browser.newPage(); 4 | await page.setViewport({ width: 600, height: 600 }); 5 | await page.goto('https://example.com'); -------------------------------------------------------------------------------- /5-debugging/2-node-repl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "scripts": { 4 | "start": "node --experimental-repl-await" 5 | } 6 | } 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /5-debugging/2-node-repl/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | 1. Start the repl 5 | 6 | ```sh 7 | npm start 8 | 9 | # or 10 | 11 | yarn start 12 | ``` 13 | 14 | 2. Paste the contents of `index.js` into the repl 15 | 16 | 3. Try typing (and executing) some of the following: 17 | 18 | ```js 19 | page 20 | page. // then press the tab key 21 | page.constructor.prototype. // then press the tab key 22 | page.constructor.prototype.pdf.toString() 23 | ``` -------------------------------------------------------------------------------- /5-debugging/3-development-environment/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import express from 'express'; 3 | import playwright from 'playwright'; 4 | 5 | 6 | async function startExpress() { 7 | const app = express(); 8 | app.use(express.static('public')); 9 | 10 | return new Promise(resolve => { 11 | app.listen(3000, () => { 12 | console.log('App listening on 3000!'); 13 | resolve(); 14 | }); 15 | }) 16 | } 17 | 18 | await startExpress(); 19 | 20 | const settings = { 21 | url: 'http://localhost:3000', 22 | windowSize: 800, 23 | windowPositionOffset: 200 24 | }; 25 | 26 | const pages = []; 27 | 28 | for (const [index, browserType] of ['chromium', 'webkit'].entries()) { 29 | console.log(`${index}. Loading ${browserType}`); 30 | const browser = await playwright[browserType].launch({ 31 | headless: false, 32 | args: [ 33 | `--window-size=${settings.windowSize},${settings.windowSize}`, 34 | `--window-position=${settings.windowPositionOffset},${settings.windowPositionOffset * (index + 1)}` 35 | ] 36 | }); 37 | 38 | const context = await browser.newContext({ 39 | viewport: { 40 | width: settings.windowSize, 41 | height: settings.windowSize 42 | } 43 | }); 44 | 45 | const page = await context.newPage(); 46 | await page.goto(settings.url); 47 | 48 | pages.push(page); 49 | } 50 | 51 | fs.watch('./public', async (_, filename) => { 52 | console.log(`${filename} changed, reloading`); 53 | for (const page of pages) { 54 | await page.reload(); 55 | } 56 | }); -------------------------------------------------------------------------------- /5-debugging/3-development-environment/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await index" 6 | } 7 | } -------------------------------------------------------------------------------- /5-debugging/3-development-environment/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dev Env 7 | 8 | 9 | 10 |
    11 |
  • Lorem ipsum dolor sit amet consectetur adipisicing elit.
  • 12 |
  • Lorem ipsum dolor sit amet consectetur adipisicing elit.
  • 13 |
  • Lorem ipsum dolor sit amet consectetur adipisicing elit.
  • 14 |
  • Lorem ipsum dolor sit amet consectetur adipisicing elit.
  • 15 |
  • Lorem ipsum dolor sit amet consectetur adipisicing elit.
  • 16 |
17 | 18 | -------------------------------------------------------------------------------- /5-debugging/3-development-environment/public/styles.css: -------------------------------------------------------------------------------- 1 | ul { 2 | display: grid; 3 | grid-gap: 4rem; 4 | grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); 5 | } -------------------------------------------------------------------------------- /5-debugging/3-development-environment/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | 1. Start the express server/playwright: 5 | 6 | ```sh 7 | npm start 8 | 9 | # or 10 | 11 | yarn start 12 | ``` 13 | 14 | 2. Make a change to the file, or files, in the `public` folder. 15 | + For example increase the font-size in the file `public/styles.css` 16 | 17 | ## Alternative 18 | 19 | As an alternative approach to live reloading, what about injecting the live-reload script via playwright's `addInitScript()` method? -------------------------------------------------------------------------------- /5-debugging/4-devtools/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | const browser = await puppeteer.launch({ 4 | headless: false 5 | }); 6 | 7 | const page = await browser.newPage(); 8 | 9 | global.page = page; 10 | 11 | await page.setViewport({ width: 1280, height: 720 }); 12 | await page.goto('https://example.com'); 13 | 14 | console.log('Ready to debug!'); 15 | 16 | debugger; 17 | 18 | await browser.close(); 19 | -------------------------------------------------------------------------------- /5-debugging/4-devtools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node --experimental-top-level-await --inspect-brk index" 6 | } 7 | } 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /5-debugging/4-devtools/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To start 3 | 4 | 1. 5 | 6 | ```sh 7 | npm start 8 | 9 | # or 10 | 11 | yarn start 12 | ``` 13 | 14 | 2. Then open up a remote DevTools instance via `chrome://inspect/` 15 | 16 | 3. Resume script execution in DevTools 17 | 18 | 4. Open up the Console Panel, starting typing commands! E.g. `page.goto('https://google.com')` -------------------------------------------------------------------------------- /5-debugging/5-live-reload/index.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | let browser = await puppeteer.connect({ 4 | browserURL: 'http://localhost:9222/', 5 | headless: false 6 | }); 7 | 8 | console.log('Connected to existing browser'); 9 | 10 | const [page] = await browser.pages(); 11 | await page.goto('https://example.com') -------------------------------------------------------------------------------- /5-debugging/5-live-reload/open.js: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | 3 | console.log('Launching a new browser'); 4 | 5 | const browser = await puppeteer.launch({ 6 | headless: false, 7 | args: [ 8 | '--remote-debugging-port=9222' 9 | ] 10 | }); -------------------------------------------------------------------------------- /5-debugging/5-live-reload/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "scripts": { 5 | "start": "../../node_modules/.bin/nodemon --experimental-top-level-await index", 6 | "open": "node --experimental-top-level-await open" 7 | } 8 | } -------------------------------------------------------------------------------- /5-debugging/5-live-reload/readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## To run 3 | 4 | 1. In one terminal tab, run this: 5 | 6 | ```sh 7 | npm run open 8 | 9 | # or 10 | 11 | yarn open 12 | ``` 13 | 14 | 2. In _another_ terminal tab, run this: 15 | 16 | ```sh 17 | npm start 18 | 19 | # or 20 | 21 | yarn start 22 | ``` -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | automatebrowsers.com 2 | www.automatebrowsers.com -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/01j2xsQ1yML.css: -------------------------------------------------------------------------------- 1 | #dp{margin:0 auto;min-width:1000px;max-width:1500px;background-color:#fff} -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/01mqQVb87-L.css: -------------------------------------------------------------------------------- 1 | #preRegistration-container .a-icon{display:none}#preRegistration-container.inline-popup-link .a-checkbox{padding-right:4px}#preRegistration-container.inline-popup-link .a-checkbox-label{display:inline;padding-left:0}#preRegistration-container .preRegistration-popup-link{position:relative}#preRegistration-alert-container #preregistrationAlert{padding-left:4px}#preRegistration-alert-container #preregistrationQuantityAlert{padding-left:4px}#gifting-option-container{margin-bottom:0} -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/01r8lpNJhRL.css: -------------------------------------------------------------------------------- 1 | .js-feature-refresh-overlay{opacity:.5;pointer-events:none} -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/31I0VVdQ1VL._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/31I0VVdQ1VL._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/31SNqepeVzL._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/31SNqepeVzL._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/31d6CWrLLNL._AC_US40_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/31d6CWrLLNL._AC_US40_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/31dGvnzpm9L._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/31dGvnzpm9L._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/31dOoxaK6mL.css: -------------------------------------------------------------------------------- 1 | #all-offers-display{overflow:visible!important;position:fixed;right:-620px;width:602px;bottom:0;z-index:290;margin:0;background-color:#f9fbfb;border-width:0;top:0;box-shadow:-4px 0 5px rgba(0,0,0,.25);-webkit-overflow-scrolling:touch;text-align:initial;font-size:13px;color:#111}#all-offers-display #aod-offer-list{overflow:hidden;background-color:#fff}#all-offers-display .aod-delivery-promise-column{margin-right:0!important}#all-offers-display .aod-delivery-promise-column .aod-scheduled-delivery #sd_buybox_root{margin:0!important;padding:0!important}#all-offers-display .aod-delivery-promise-column .aod-scheduled-delivery #afn_content,#all-offers-display .aod-delivery-promise-column .aod-scheduled-delivery .sd_bb_centered{display:none}#all-offers-display .aod-delivery-promise-column .aod-scheduled-delivery .a-row{margin-bottom:0!important;margin-top:0!important}#all-offers-display .aod-delivery-promise .a-section{padding:0!important;padding-top:0!important;margin:0!important}#all-offers-display #fast-track .a-section{padding:0!important;margin:0!important}#all-offers-display #amazon-day-message,#all-offers-display #delivery-message,#all-offers-display #upsell-message{padding-top:0!important;margin:0!important}#all-offers-display .aod-delivery-promise-truncate{overflow:hidden;position:relative;display:block;white-space:nowrap}#all-offers-display .aod-delivery-promise-truncate #amazon-day-message,#all-offers-display .aod-delivery-promise-truncate #delivery-message,#all-offers-display .aod-delivery-promise-truncate #upsell-message{display:inline}#all-offers-display .aod-delivery-promise-truncate br~*{display:none}#all-offers-display .aod-delivery-promise-truncate h5{display:inline}#all-offers-display .aod-delivery-promise-truncate #upsell-message~*{display:none}#all-offers-display .aod-delivery-promise-truncate #amazon-day-message~*{display:none}#all-offers-display .aod-delivery-promise-truncate #delivery-message~*{display:none}#all-offers-display #aod-pinned-offer .aod-delivery-morelink{display:none}#all-offers-display .aod-delivery-column{padding-right:20px!important}#all-offers-display .aod-container{max-width:602px!important}#all-offers-display .aod-information-block{padding-right:20px!important;padding-left:20px!important}#all-offers-display .aod-asin-block-margin{padding-top:20px;padding-left:20px;padding-right:20px;margin-bottom:0!important;background-color:#fff}#all-offers-display .sticky-pinned-offer{position:sticky;position:-webkit-sticky;top:0;z-index:100000000;background:#fff;margin-bottom:0!important}#all-offers-display .aod-asin-title-text-class{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#all-offers-display .aod-asin-reviews-block-class{vertical-align:bottom}#all-offers-display .aod-seller-rating-count-class{vertical-align:top}#all-offers-display .aod-condition-divider{width:100%}#all-offers-display .aod-offer-block-divider{border-top:4px solid #e6edf0!important;width:100%!important}#all-offers-display .aod-offer-divider{height:0}#all-offers-display .aod-truncation-2-line{overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}#all-offers-display .aod-atc-preorder-btn i{display:none}#all-offers-display .aod-atc-preorder-btn .a-button-text{white-space:inherit;line-height:20px;padding:4px 8px}#all-offers-display .aod-atc-preorder-btn .a-button-inner{height:auto}#all-offers-display .aod-condition-image-thumbnail{position:relative;display:inline-block;vertical-align:middle;width:48px;height:48px;margin-bottom:8px;margin-right:10px;margin-top:3px;border:1px solid;border-color:#CCC;border-radius:2px;padding:2px}#all-offers-display .aod-filter-block-container{width:100%!important;background:#F4F6F7;margin:0!important;position:relative;padding:18px 20px!important;border-bottom:1px solid #e6edf0!important}#all-offers-display .aod-filter-block-container .a-row:after{clear:both!important}#all-offers-display #aod-filter-hide{transform:rotateX(180deg);display:none}#all-offers-display .aod-filter-component-container{position:absolute;top:18px;right:20px}#all-offers-display .aod-filter-list-container{width:210px;position:absolute;right:0;display:none;box-shadow:0 4px 4px -4px #000;-moz-box-shadow:0 4px 4px -4px #000;-webkit-box-shadow:0 4px 4px -4px #000;border-radius:5px;border:solid 1px #a9a9a9;background:#fff;padding:14px 18px 18px 18px!important}#all-offers-display .aod-filter-parent-filter{text-indent:10px}#all-offers-display .aod-filter-subfilter{text-indent:20px}#all-offers-display .aod-clear-all-div{text-align:right}#all-offers-display .aod-checkbox-col{padding-left:0!important;position:relative!important}#all-offers-display .aod-checkbox-col>label{top:0!important;position:absolute!important;right:0!important}#all-offers-display .aod-checkbox-col>label>.a-icon-checkbox{position:relative!important;margin-top:0!important}#all-offers-display .aod-padding-right-10{padding-right:10px!important}#all-offers-display .aod-atc-column{text-align:right}#all-offers-display .aod-atc-generic-btn-desktop i{display:none}#all-offers-display .aod-atc-generic-btn-desktop .a-button-text{white-space:inherit;line-height:20px;padding:4px 8px;font-size:13px!important}#all-offers-display .aod-atc-generic-btn-desktop .a-button-inner{height:auto}#all-offers-display #all-offers-display-scroller{overflow-x:hidden!important;overflow-y:auto!important}#all-offers-display .aod-div-for-focus{outline:0}#all-offers-display .aod-close-button{opacity:1;width:20px;height:30px;background-position:-350px -100px}#all-offers-display .aod-filter-list-container{z-index:10000!important}#all-offers-display .aod-clear-float{clear:both}#all-offers-display .aod-arrow-up{margin-top:8px;margin-right:2px}#all-offers-display .aod-arrow-low{margin-top:8px;margin-right:2px;transform:rotate(180deg)}#all-offers-display .aod-pinned-offer{top:0;z-index:1000000;background:#fff}#all-offers-display .aod-sticky-pinned-container{position:relative;width:602px;z-index:1000000}#all-offers-display .aod-sticky-pinned-offer{position:fixed;width:602px;top:-200px;z-index:100000000;background:#fff;transition-property:top;transition-duration:.7s;transition-timing-function:ease-in-out}#all-offers-display .aod-display-block{display:block}#all-offers-display .aod-sticky-pinned-offer-show-position{top:0!important}#all-offers-display .aod-no-offer-normal-font{font-weight:400!important}#all-offers-display #aod-close{position:absolute;left:-30px;cursor:pointer}#all-offers-display .expandable-expand-action{margin-left:-2px}#all-offers-display .aod-hide{display:none!important}#all-offers-display #aod-footer{background:#f9fbfb;height:51px;padding-right:20px;padding-left:20px;padding-top:14px;padding-bottom:18px;width:100%}#all-offers-display #aod-footer-spinner-container{position:relative}#all-offers-display #aod-offer-load-spinner{position:absolute;left:50%}#all-offers-display .aod-no-select{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}#all-offers-display .aod-pinned-offer-block{padding:14px 20px!important}#all-offers-display .aod-pinned-offer-price-div{margin-top:0!important}#all-offers-display .aod-zero-offer-class{background:#F4F6F7!important;width:100vw;height:100vh}#all-offers-display .aod-popover{z-index:6001!important}#all-offers-display #fast-track-message{font-size:inherit;line-height:inherit;text-align:inherit}.aod-darken-background{opacity:.4;position:fixed;top:0;left:0;height:100%;width:100%;background:#000;z-index:280;cursor:pointer}.aod-condition-image-full-image{width:500px;height:490px;max-width:100%;max-height:100%;object-fit:fill;overflow:hidden;padding:3px 5px 5px}.aod-pinned-image{padding-right:20px;vertical-align:middle;text-align:center}.aod-pinned-asin-details{max-width:400px}#aod-asin-image-id{max-height:135px;max-width:135px}#pinned-image-id{text-align:center}.asin-container-padding{padding:18px 20px}#aod-sticky-pinned-offer .aod-delivery-morelink{display:none}.aod-ags-import-badge-learn-more-align{vertical-align:top}.aod-ships-from-country>p{margin:0}#all-offers-display .aod-filter-swatch-box-text-div{height:22px;display:inline-flex;align-items:center}#all-offers-display .aod-filter-swatch{padding-right:6px!important}#all-offers-display .aod-filter-swatch-content{padding-left:8px!important;padding-right:6px!important;height:22px;display:flex;align-items:center}#all-offers-display .aod-filter-swatch-content-text{padding-right:5px!important;display:flex}#all-offers-display .aod-filter-swatch-container{display:flex;flex-flow:wrap;width:100%}#all-offers-display #aod-filter-swatch-container-bottom>.aod-filter-swatch{margin-top:6px!important;padding-right:6px!important}#all-offers-display #aod-filter-swatch-box-clear-all-div{padding-left:14px!important;margin-left:auto} -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/31lOd+WyJML._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/31lOd+WyJML._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/31lp66Pk0rL._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/31lp66Pk0rL._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/31nGbYwCw1L._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/31nGbYwCw1L._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/31oIYaNaqlL._AC_US40_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/31oIYaNaqlL._AC_US40_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/31qOgP7eV7L._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/31qOgP7eV7L._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/31zbpRzA6qL._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/31zbpRzA6qL._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/360_icon_73x73v2._CB485971279_SS40_FMpng_RI_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/360_icon_73x73v2._CB485971279_SS40_FMpng_RI_.png -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/411zcSwEStL._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/411zcSwEStL._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/4127V-q6txL._AC_US40_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/4127V-q6txL._AC_US40_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/4179Re3+b7L._AC_US40_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/4179Re3+b7L._AC_US40_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/418lRtVfxuL._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/418lRtVfxuL._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/41HWZW3GRML._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/41HWZW3GRML._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/41JJyKcWHaL._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/41JJyKcWHaL._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/41PaSVEJ-jL._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/41PaSVEJ-jL._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/41R8U8rQ3GL._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/41R8U8rQ3GL._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/41VccVZJOLL._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/41VccVZJOLL._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/41aclEB6i1L._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/41aclEB6i1L._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/41fwczmGpCL._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/41fwczmGpCL._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/41mOR1Fh85L._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/41mOR1Fh85L._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/41usTtryBgL._AC_US40_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/41usTtryBgL._AC_US40_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/51B2bnyDy3L._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/51B2bnyDy3L._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/51MYD1rWJoL._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/51MYD1rWJoL._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/51NWD6WHFDL._AC_US40_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/51NWD6WHFDL._AC_US40_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/51TwTYMESaL._AC_US40_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/51TwTYMESaL._AC_US40_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/51YPCqzNCyL._AC_SS350_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/51YPCqzNCyL._AC_SS350_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/51f+yB2tzoL._AC_UL320_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/51f+yB2tzoL._AC_UL320_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/51vmGJoCi-L._AC_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/51vmGJoCi-L._AC_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/61O0M6uTnSL._CR500,0,999,999_UX175.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/61O0M6uTnSL._CR500,0,999,999_UX175.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/61O5DXTLf1L._AC_SS350_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/61O5DXTLf1L._AC_SS350_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/61O5DXTLf1L._AC_UL320_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/61O5DXTLf1L._AC_UL320_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/61bQZDJewOL._CR0,204,1224,1224_UX175.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/61bQZDJewOL._CR0,204,1224,1224_UX175.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/61hRdmo7xCL._AC_SS350_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/61hRdmo7xCL._AC_SS350_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/61hRdmo7xCL._AC_SX679_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/61hRdmo7xCL._AC_SX679_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/61hRdmo7xCL._AC_UL115_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/61hRdmo7xCL._AC_UL115_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/61uVRSyOMFL._AC_SX679_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/61uVRSyOMFL._AC_SX679_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/61vBkxFFq-L._AC_UL320_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/61vBkxFFq-L._AC_UL320_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/61xFVZdiNhL._AC_SL1500_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/61xFVZdiNhL._AC_SL1500_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/61xFVZdiNhL._AC_SX679_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/61xFVZdiNhL._AC_SX679_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/61zkIV+5lXL._AC_UL320_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/61zkIV+5lXL._AC_UL320_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/71-C+L-rmtL._AC_UL320_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/71-C+L-rmtL._AC_UL320_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/710BRvY2H7L._AC_UL115_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/710BRvY2H7L._AC_UL115_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/719z0s1QRvL._AC_SX679_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/719z0s1QRvL._AC_SX679_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/71REHWzcBuL._AC_SX679_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/71REHWzcBuL._AC_SX679_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/71T-jtV1+4L._AC_SS350_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/71T-jtV1+4L._AC_SS350_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/71T-jtV1+4L._AC_UL115_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/71T-jtV1+4L._AC_UL115_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/71UXTFQCv4L._AC_SS350_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/71UXTFQCv4L._AC_SS350_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/71UXTFQCv4L._AC_UL320_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/71UXTFQCv4L._AC_UL320_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/71pxJBtmV6L._AC_SS350_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/71pxJBtmV6L._AC_SS350_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/71pxJBtmV6L._AC_UL320_SR320,320_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/71pxJBtmV6L._AC_UL320_SR320,320_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/71rVaCGCXEL._CR0,204,1224,1224_UX175.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/71rVaCGCXEL._CR0,204,1224,1224_UX175.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/71ySysGeNxL._AC_SX679_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/71ySysGeNxL._AC_SX679_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/723a9837-75e6-4e95-bd8b-a083c040aee2._CR83,0,333,333_SX48_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/723a9837-75e6-4e95-bd8b-a083c040aee2._CR83,0,333,333_SX48_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/81p0H9wZZyL._AC_SX679_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/81p0H9wZZyL._AC_SX679_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/default._CR0,0,1024,1024_SX48_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/default._CR0,0,1024,1024_SX48_.png -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/ec4311a4-c34b-4dec-b29d-61dc44eee005._CR0,0,375,375_SX48_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/ec4311a4-c34b-4dec-b29d-61dc44eee005._CR0,0,375,375_SX48_.jpg -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/grey-pixel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/grey-pixel.gif -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/loadIndicator-large._CB485943906_.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/loadIndicator-large._CB485943906_.gif -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/loading-4x-gray._CB485916920_.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/loading-4x-gray._CB485916920_.gif -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/nav-sprite-global_bluebeacon-1x_optimized_layout1._CB468670774_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/nav-sprite-global_bluebeacon-1x_optimized_layout1._CB468670774_.png -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/review-lightbox-combined._CB485923683_.css: -------------------------------------------------------------------------------- 1 | 2 | /* US-secure-core-reviewsLightboxCSS-3127921709.css start */ 3 | .reviewsLightbox .thumbnailPreviewTile{width:128px;height:128px;margin:5px;float:left;background-position:center center;background-repeat:no-repeat;overflow:hidden;background-size:cover;cursor:pointer}@media only screen and (max-width : 650px){.reviewsLightbox .thumbnailPreviewTile{width:30%;height:auto;padding:15%;margin:1.5%;float:left}}@media only screen and (max-width : 750px) and (min-width : 651px){.reviewsLightbox .thumbnailPreviewTile{width:20%;height:auto;padding:10%;margin:2.5%;float:left}}.reviewsLightbox{width:100%;height:100%;font-family:arial}.reviewsLightbox .loadingIcon{background:url("https://images-na.ssl-images-amazon.com/images/G/01/x-locale/common/loading/loading-small._V192239831_.gif") no-repeat 0 0;width:14px;height:14px}.reviewsLightbox .large-loadingIcon{background:url("https://images-na.ssl-images-amazon.com/images/G/01/x-locale/common/loading/loading-2x._V387541822_.gif") no-repeat 0 0;width:50px;height:50px}.reviewsLightbox .hidden{display:none}.reviewsLightbox .fl{float:left}.reviewsLightbox .fr{float:right}.reviewsLightbox .clearBoth{clear:both}.reviewsLightbox .spriteSheet,.reviewsLightbox .immersiveView .back-button,.reviewsLightbox .immersiveView .back-button-mobile,.reviewsLightbox .immersiveView .next-button,.reviewsLightbox .immersiveView .next-button-mobile,.reviewsLightbox .immersiveView .mobile-information-panel-expand-button,.reviewsLightbox .immersiveView .gallery-icon{background:url("https://images-na.ssl-images-amazon.com/images/G/01/x-locale/common/customer-reviews/reviewsImageGallerySprite-v2._V324675860_.png") no-repeat}.reviewsLightbox .spriteSheetBackgroundSize,.reviewsLightbox .immersiveView .back-button,.reviewsLightbox .immersiveView .back-button-mobile,.reviewsLightbox .immersiveView .next-button,.reviewsLightbox .immersiveView .next-button-mobile,.reviewsLightbox .immersiveView .mobile-information-panel-expand-button,.reviewsLightbox .immersiveView .gallery-icon{background-size:306px 396px}.reviewsLightbox .ellipsis{display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.reviewsLightbox .immersiveView .imageElement{position:absolute;top:0;bottom:0;left:0;right:0;margin:auto;max-width:100%;max-height:100%}.reviewsLightbox .immersiveView .hideButton-mobile{background-color:white;width:90px;height:40px;line-height:40px;text-align:center;border-radius:4px;background:rgba(255, 255, 255, 0.5);position:absolute;top:10px;left:10px;z-index:999;cursor:pointer}.reviewsLightbox .immersiveView .backButtonContainer{width:33%;height:100%;position:relative;float:left;cursor:pointer;-webkit-tap-highlight-color:rgba(255, 255, 255, 0)}.reviewsLightbox .immersiveView .nextButtonContainer{width:67%;height:100%;position:relative;float:right;cursor:pointer;-webkit-tap-highlight-color:rgba(255, 255, 255, 0)}.reviewsLightbox .immersiveView .back-button,.reviewsLightbox .immersiveView .back-button-mobile{width:41px;height:60px;background-position:-6px -8px;position:absolute;margin-top:-30px;top:50%;margin-left:10px;opacity:0.25;background-position:-156px -283px \9;width:29px\9;height:49px\9}.reviewsLightbox .immersiveView .next-button,.reviewsLightbox .immersiveView .next-button-mobile{width:41px;height:60px;background-position:-80px -8px;position:absolute;margin-top:-30px;top:50%;margin-right:10px;opacity:0.25;right:0;background-position:-206px -283px \9;width:29px\9;height:49px\9}.reviewsLightbox .immersiveView .back-button-mobile{transform:scale(0.7, 0.7)}.reviewsLightbox .immersiveView .next-button-mobile{transform:scale(0.7, 0.7)}.reviewsLightbox .immersiveView .mobile-information-panel-expand-button{width:35px;height:25px;background-position:-255px -21px;float:right;zoom:0.75;opacity:0.2;margin-top:12px;transform:scale(0.8, 0.8)}.reviewsLightbox .immersiveView .imageHighlight{border:orange;border-width:2px;border-radius:4px;border-style:solid}.reviewsLightbox .immersiveView .gallery-icon{background-position:-23px -82px;width:30px;height:30px;float:left;background-position:-34px -357px \9;width:16px\9;height:16px\9;margin-right:10px\9}.reviewLightboxPopoverContainer{width:570px;height:450px;position:relative;overflow:hidden;width:710px\9;height:450px\9}@media only screen and (min-width : 768px){.reviewLightboxPopoverContainer{width:570px;height:450px;position:relative;overflow:hidden}}@media only screen and (min-width : 1024px){.reviewLightboxPopoverContainer{width:710px;height:450px;position:relative;overflow:hidden}}@media only screen and (min-width : 1280px){.reviewLightboxPopoverContainer{width:845px;height:450px;position:relative;overflow:hidden}}@media only screen and (min-width : 1400px){.reviewLightboxPopoverContainer{width:985px;height:600px;position:relative;overflow:hidden}} 4 | 5 | /* US-secure-core-reviewsLightboxCSS-3127921709.css end */ 6 | -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/secured-ssl._CB485936932_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/secured-ssl._CB485936932_.png -------------------------------------------------------------------------------- /docs/amazon/cat-mug/index_files/vse_play_icon_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umaar/learn-browser-testing/4d67c76a80d9bcb5fb13e0b60db581ed25822403/docs/amazon/cat-mug/index_files/vse_play_icon_2x.png -------------------------------------------------------------------------------- /docs/amazon/cat-mug/main.js: -------------------------------------------------------------------------------- 1 | function updatePrices(price) { 2 | [...document.querySelectorAll("#priceblock_ourprice,#price_inside_buybox,.twisterSwatchPrice")].forEach(el => { 3 | el.textContent = price; 4 | el.removeAttribute('hidden'); 5 | }) 6 | 7 | } 8 | 9 | function random(min, max) { 10 | return Math.floor(Math.random() * (max - min + 1)) + min; 11 | } 12 | 13 | window.addEventListener('DOMContentLoaded', (event) => { 14 | const newPrice = random(8, 150); 15 | const decimal = random(11, 99); 16 | updatePrices(`$${newPrice}.${decimal}`); 17 | }); -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Automation Sandbox 7 | 8 | 9 |

10 | This is the demo site for LearnBrowserTesting.com - you can use it to scrape and test with ✅️ 11 |

12 | 13 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | ### Info 2 | 3 | This is purely for GitHub pages - nothing to see here. -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "#" 3 | publish = "4-deploying/6-serverless" 4 | functions = "4-deploying/6-serverless/functions" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "type": "module", 4 | "dependencies": { 5 | "@ffmpeg-installer/ffmpeg": "^1.0.20", 6 | "@lhci/cli": "^0.6.1", 7 | "@lhci/server": "^0.6.1", 8 | "@percy/puppeteer": "^1.1.0", 9 | "airtable": "^0.10.1", 10 | "archivist1": "^1.3.13", 11 | "ava": "^3.13.0", 12 | "chrome-aws-lambda": "^5.3.1", 13 | "chromedriver": "^87.0.0", 14 | "cucumber": "^6.0.5", 15 | "cypress": "^5.6.0", 16 | "differencify": "^1.5.5", 17 | "dotenv": "^8.2.0", 18 | "express": "^4.17.1", 19 | "jest": "^26.6.3", 20 | "jest-github-actions-reporter": "^1.0.2", 21 | "jest-puppeteer": "^4.4.0", 22 | "node-notifier": "^8.0.0", 23 | "nodemon": "^2.0.6", 24 | "nunjucks": "^3.2.2", 25 | "playwright": "^1.6.2", 26 | "playwright-video": "^2.4.0", 27 | "puppeteer": "^5.5.0", 28 | "puppeteer-core": "^5.5.0", 29 | "puppeteer-extra": "^3.1.15", 30 | "puppeteer-extra-plugin-recaptcha": "^3.2.0", 31 | "puppeteer-har": "^1.1.2", 32 | "qawolf": "^1.4.0", 33 | "selenium-webdriver": "^4.0.0-alpha.7", 34 | "sqlite3": "^5.0.0", 35 | "testcafe": "^1.9.4" 36 | }, 37 | "jest": { 38 | "preset": "jest-puppeteer" 39 | }, 40 | "devDependencies": { 41 | "@umaar/personal-eslint-config": "^1.0.3", 42 | "xo": "^0.34.2" 43 | }, 44 | "xo": { 45 | "extends": "./node_modules/@umaar/personal-eslint-config/rules.json" 46 | }, 47 | "scripts": { 48 | "lint": "xo" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## Learn Browser Testing 3 | 4 | End to end testing and browser automation. 5 | 6 | ### Quick start 7 | 8 | Please use Node v14.4.0 or above. 9 | 10 | ```sh 11 | npm install 12 | ``` 13 | 14 | Then, `cd` into any directory and run: 15 | 16 | ```sh 17 | npm start 18 | ``` 19 | 20 | ### Node modules 21 | 22 | The dependencies (`node_modules`) for this project can come to many hundreds of megabytes. Rather than you having to run `npm install` each time you `cd` into a new folder (and download gigabytes of `node_modules` scattered across the various folders in this repo), you just need to do it once at the root level. 23 | 24 | ### Windows users 25 | 26 | If `npm start` works for you (try one of the cypress, or just examples), then continue with that - you don't need to change anything. Otherwise, run this to fix the error: 27 | 28 | ```sh 29 | npm install -g yarn 30 | ``` 31 | 32 | Now, instead of running npm start, run: 33 | 34 | ```sh 35 | yarn start 36 | ``` 37 | 38 | --------------------------------------------------------------------------------