From 5d42406610295e0c4f7033c943bcbe44fa675f08 Mon Sep 17 00:00:00 2001 From: Teo Anastasiadis <31965686+TheoAnastasiadis@users.noreply.github.com> Date: Tue, 27 Feb 2024 18:51:44 +0200 Subject: [PATCH 01/11] fix: Disabled elements being clickable (#28807) * fixes #28788 -- mouse.ts logic If mouseUp element or mouseDown element or commonAncestor element is :disabled, click event should be prevented * fixes #28788 -- e2e test tests that no click events are registered when click happens on child of disabled element * fixes #28788 -- mouse.ts logic If mouseUp element or mouseDown element have an "actually disabled" parent, the click event should not be registered * refactoring: minor name changes for readability * fixes #24322 --added changelog entry * Update packages/driver/src/cy/mouse.ts Co-authored-by: Bill Glesias * Update packages/driver/cypress/e2e/commands/actions/click.cy.js Co-authored-by: Bill Glesias * Update packages/driver/src/cy/mouse.ts Co-authored-by: Bill Glesias * Update packages/driver/src/cy/mouse.ts Co-authored-by: Bill Glesias * update mouse.ts -- unexpected token * docs: moved entry to 13.6.5 * Update CHANGELOG.md * Update CHANGELOG.md * Update packages/driver/cypress/e2e/commands/actions/click.cy.js Co-authored-by: Bill Glesias * Update cli/CHANGELOG.md Co-authored-by: Bill Glesias * lint: fixed 2 linting errors 565:1 error Expected indentation of 8 spaces but found 10 indent 567:7 error Expected blank line before this statement padding-line-between-statements * style: removed trailing space 567:1 error Trailing spaces not allowed no-trailing-spaces * Update CHANGELOG.md --------- Co-authored-by: Bill Glesias Co-authored-by: Jennifer Shehane Co-authored-by: Cacie Prins --- cli/CHANGELOG.md | 1 + .../cypress/e2e/commands/actions/click.cy.js | 24 +++++++++++++++++++ packages/driver/src/cy/mouse.ts | 15 ++++++++++++ 3 files changed, 40 insertions(+) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 05491b212dd2..675e194e3ec6 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -5,6 +5,7 @@ _Released 2/27/2024 (PENDING)_ **Bugfixes:** +- Fixed an issue where `.click()` commands on children of disabled elements would still produce "click" events -- even without `{ force: true }`. Fixes [#28788](/~https://github.com/cypress-io/cypress/issues/28788). - Changed RequestBody type to allow for boolean and null literals to be passed as body values. [#28789](/~https://github.com/cypress-io/cypress/issues/28789) ## 13.6.6 diff --git a/packages/driver/cypress/e2e/commands/actions/click.cy.js b/packages/driver/cypress/e2e/commands/actions/click.cy.js index 98650cb31341..bc679e8c6b9f 100644 --- a/packages/driver/cypress/e2e/commands/actions/click.cy.js +++ b/packages/driver/cypress/e2e/commands/actions/click.cy.js @@ -546,6 +546,30 @@ describe('src/cy/commands/actions/click', () => { cy.getAll('span2', 'focus click mousedown').each(shouldNotBeCalled) }) + // /~https://github.com/cypress-io/cypress/issues/28788 + it('no click when element is disabled', () => { + const btn = cy.$$('button:first') + const span = $('foooo') + + attachFocusListeners({ btn, span }) + attachMouseClickListeners({ btn, span }) + attachMouseHoverListeners({ btn, span }) + + btn.html('') + btn.attr('disabled', true) + btn.append(span) + + cy.get('button:first span').click() + + if (Cypress.browser.name === 'chrome') { + cy.getAll('btn', 'mouseenter mousedown mouseup').each(shouldBeCalled) + } + + cy.getAll('btn', 'focus click').each(shouldNotBeCalled) + cy.getAll('span', 'mouseenter mousedown mouseup').each(shouldBeCalled) + cy.getAll('span', 'focus click').each(shouldNotBeCalled) + }) + it('no click when new element at coords is not ancestor', () => { const btn = cy.$$('button:first') const span1 = $('foooo') diff --git a/packages/driver/src/cy/mouse.ts b/packages/driver/src/cy/mouse.ts index 2a34656ac7ff..c87e4b110a4b 100644 --- a/packages/driver/src/cy/mouse.ts +++ b/packages/driver/src/cy/mouse.ts @@ -567,6 +567,21 @@ export const create = (state: StateFunc, keyboard: Keyboard, focused: IFocused, return { skipClickEventReason: 'element was detached' } } + // Only send click event if element is not disabled. + // First find an parent element that can actually be disabled + const findParentThatCanBeDisabled = (el: HTMLElement): HTMLElement | null => { + const elementsThatCanBeDisabled = ['button', 'input', 'select', 'textarea', 'optgroup', 'option', 'fieldset'] + + return elementsThatCanBeDisabled.includes($elements.getTagName(el)) ? el : null + } + + const parentThatCanBeDisabled = $elements.findParent(mouseUpPhase.targetEl, findParentThatCanBeDisabled) || $elements.findParent(mouseDownPhase.targetEl, findParentThatCanBeDisabled) + + // Then check if parent is indeed disabled + if (parentThatCanBeDisabled !== null && $elements.isDisabled($(parentThatCanBeDisabled))) { + return { skipClickEventReason: 'element was disabled' } + } + const commonAncestor = mouseUpPhase.targetEl && mouseDownPhase.targetEl && $elements.getFirstCommonAncestor(mouseUpPhase.targetEl, mouseDownPhase.targetEl) From 00092059b4f103e3a584b5d69a02d73a1fdee7b1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:03:35 -0500 Subject: [PATCH 02/11] chore(deps): update dependency cypress-example-kitchensink to v2.0.1 (#29019) * chore(deps): update dependency cypress-example-kitchensink to v2.0.1 * empty commit to trigger ci --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jennifer Shehane --- packages/example/package.json | 2 +- yarn.lock | 32 +++++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/example/package.json b/packages/example/package.json index 4879017cfecb..0370d56b58bc 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -15,7 +15,7 @@ }, "devDependencies": { "cross-env": "6.0.3", - "cypress-example-kitchensink": "2.0.0", + "cypress-example-kitchensink": "2.0.1", "gh-pages": "5.0.0", "gulp": "4.0.2", "gulp-clean": "0.4.0", diff --git a/yarn.lock b/yarn.lock index 4362c015ae34..f1ffd7c87883 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9463,7 +9463,7 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0, ansi-styles@^4.2.1, ansi-styles@^4.3.0: dependencies: color-convert "^2.0.1" -ansi-styles@^5: +ansi-styles@^5, ansi-styles@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== @@ -13057,12 +13057,12 @@ cypress-each@^1.11.0: resolved "https://registry.yarnpkg.com/cypress-each/-/cypress-each-1.11.0.tgz#013c9b43a950f157bcf082d4bd0bb424fb370441" integrity sha512-zeqeQkppPL6BKLIJdfR5IUoZRrxRudApJapnFzWCkkrmefQSqdlBma2fzhmniSJ3TRhxe5xpK3W3/l8aCrHvwQ== -cypress-example-kitchensink@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/cypress-example-kitchensink/-/cypress-example-kitchensink-2.0.0.tgz#16b0e5c226eca08d937d0057293c5d168d0498fd" - integrity sha512-Uoqz+E5UNdmraQ/s+BtKOQ/diRacfB4vGl6mXM37nHKDJFCRo6PGV9Ooy68m5IGZQs97aclL9ALPb2GpTUcTmg== +cypress-example-kitchensink@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/cypress-example-kitchensink/-/cypress-example-kitchensink-2.0.1.tgz#69196d5a22e2c404a138c9db198319185edd9c34" + integrity sha512-p0Pkx9PoqkmqH+moFXu9e9CzZSR7UdJzFEaMT30YKdjBANDaded2snlinlETQX2IpT7HWctj1tYXuB6jg1sMhw== dependencies: - npm-run-all "^4.1.2" + npm-run-all2 "^5.0.0" serve "14.2.1" cypress-expect@^2.5.3: @@ -23104,7 +23104,20 @@ npm-registry-fetch@^14.0.0, npm-registry-fetch@^14.0.3: npm-package-arg "^10.0.0" proc-log "^3.0.0" -npm-run-all@^4.1.2, npm-run-all@^4.1.5: +npm-run-all2@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/npm-run-all2/-/npm-run-all2-5.0.2.tgz#7dae8e11ba90be9edd05379414a01407416b336c" + integrity sha512-S2G6FWZ3pNWAAKm2PFSOtEAG/N+XO/kz3+9l6V91IY+Y3XFSt7Lp7DV92KCgEboEW0hRTu0vFaMe4zXDZYaOyA== + dependencies: + ansi-styles "^5.0.0" + cross-spawn "^7.0.3" + memorystream "^0.3.1" + minimatch "^3.0.4" + pidtree "^0.5.0" + read-pkg "^5.2.0" + shell-quote "^1.6.1" + +npm-run-all@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba" integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ== @@ -24524,6 +24537,11 @@ pidtree@^0.3.0: resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a" integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA== +pidtree@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.5.0.tgz#ad5fbc1de78b8a5f99d6fbdd4f6e4eee21d1aca1" + integrity sha512-9nxspIM7OpZuhBxPg73Zvyq7j1QMPMPsGKTqRc2XOaFQauDvoNz9fM1Wdkjmeo7l9GXOZiRs97sPkuayl39wjA== + pidusage@3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/pidusage/-/pidusage-3.0.2.tgz#6faa5402b2530b3af2cf93d13bcf202889724a53" From 7a270ad2abaf9f5f2d300a41b5e85d242e04b258 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 28 Feb 2024 10:00:55 -0500 Subject: [PATCH 03/11] chore: Update Chrome (stable) to 122.0.6261.94 (#29028) Co-authored-by: cypress-bot[bot] <41898282+cypress-bot[bot]@users.noreply.github.com> --- browser-versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser-versions.json b/browser-versions.json index c5b638338815..612b29f041e6 100644 --- a/browser-versions.json +++ b/browser-versions.json @@ -1,5 +1,5 @@ { "chrome:beta": "123.0.6312.4", - "chrome:stable": "122.0.6261.69", + "chrome:stable": "122.0.6261.94", "chrome:minimum": "64.0.3282.0" } From 96362b49c12190c2f85ad9d2d041088e65f864eb Mon Sep 17 00:00:00 2001 From: Andrii Chubatiuk Date: Thu, 29 Feb 2024 22:24:30 +0200 Subject: [PATCH 04/11] dependency: upgraded http-proxy-middleware (#28902) * fix: upgraded http-proxy-middleware * add changelog entry --------- Co-authored-by: Jennifer Shehane --- cli/CHANGELOG.md | 4 ++++ system-tests/projects/cra-5/yarn.lock | 6 +++--- system-tests/projects/cra-ejected/yarn.lock | 6 +++--- system-tests/projects/create-react-app-configured/yarn.lock | 6 +++--- .../projects/create-react-app-custom-index-html/yarn.lock | 6 +++--- .../projects/create-react-app-unconfigured/yarn.lock | 6 +++--- system-tests/projects/nextjs-configured/yarn.lock | 6 +++--- system-tests/projects/nextjs-unconfigured/yarn.lock | 6 +++--- .../projects/react-app-webpack-5-unconfigured/yarn.lock | 6 +++--- system-tests/projects/vuecli5-vue3-type-module/yarn.lock | 6 +++--- system-tests/projects/vuecli5-vue3/yarn.lock | 6 +++--- system-tests/projects/vuecli5vue3-configured/yarn.lock | 6 +++--- system-tests/projects/vuecli5vue3-unconfigured/yarn.lock | 6 +++--- system-tests/projects/webpack-react-nested-config/yarn.lock | 6 +++--- system-tests/projects/webpack4_wds4-react/yarn.lock | 6 +++--- system-tests/projects/webpack5_wds4-react/yarn.lock | 6 +++--- yarn.lock | 6 +++--- 17 files changed, 52 insertions(+), 48 deletions(-) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 675e194e3ec6..e1aed9661e30 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -8,6 +8,10 @@ _Released 2/27/2024 (PENDING)_ - Fixed an issue where `.click()` commands on children of disabled elements would still produce "click" events -- even without `{ force: true }`. Fixes [#28788](/~https://github.com/cypress-io/cypress/issues/28788). - Changed RequestBody type to allow for boolean and null literals to be passed as body values. [#28789](/~https://github.com/cypress-io/cypress/issues/28789) +**Dependency Updates:** + +- Updated http-proxy-middleware from `2.0.4` to `2.0.6`. Addressed in [#28902](/~https://github.com/cypress-io/cypress/pull/28902). + ## 13.6.6 _Released 2/22/2024_ diff --git a/system-tests/projects/cra-5/yarn.lock b/system-tests/projects/cra-5/yarn.lock index 5f3464a42813..c6477390183c 100644 --- a/system-tests/projects/cra-5/yarn.lock +++ b/system-tests/projects/cra-5/yarn.lock @@ -4404,9 +4404,9 @@ http-proxy-agent@^4.0.1: debug "4" http-proxy-middleware@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz#03af0f4676d172ae775cb5c33f592f40e1a4e07a" - integrity sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg== + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" diff --git a/system-tests/projects/cra-ejected/yarn.lock b/system-tests/projects/cra-ejected/yarn.lock index 1a922eecb0b3..2a003277b516 100644 --- a/system-tests/projects/cra-ejected/yarn.lock +++ b/system-tests/projects/cra-ejected/yarn.lock @@ -4228,9 +4228,9 @@ http-parser-js@>=0.5.1: integrity sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA== http-proxy-middleware@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz#03af0f4676d172ae775cb5c33f592f40e1a4e07a" - integrity sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg== + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" diff --git a/system-tests/projects/create-react-app-configured/yarn.lock b/system-tests/projects/create-react-app-configured/yarn.lock index d0f169eaf910..6bf8bcb59be9 100644 --- a/system-tests/projects/create-react-app-configured/yarn.lock +++ b/system-tests/projects/create-react-app-configured/yarn.lock @@ -5772,9 +5772,9 @@ http-proxy-middleware@0.19.1: micromatch "^3.1.10" http-proxy-middleware@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.3.tgz#5df04f69a89f530c2284cd71eeaa51ba52243289" - integrity sha512-1bloEwnrHMnCoO/Gcwbz7eSVvW50KPES01PecpagI+YLNLci4AcuKJrujW4Mc3sBLpFxMSlsLNHS5Nl/lvrTPA== + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" diff --git a/system-tests/projects/create-react-app-custom-index-html/yarn.lock b/system-tests/projects/create-react-app-custom-index-html/yarn.lock index 4963b1e27ef1..ca365f24b896 100644 --- a/system-tests/projects/create-react-app-custom-index-html/yarn.lock +++ b/system-tests/projects/create-react-app-custom-index-html/yarn.lock @@ -5770,9 +5770,9 @@ http-proxy-middleware@0.19.1: micromatch "^3.1.10" http-proxy-middleware@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.3.tgz#5df04f69a89f530c2284cd71eeaa51ba52243289" - integrity "sha1-XfBPaaifUwwihM1x7qpRulIkMok= sha512-1bloEwnrHMnCoO/Gcwbz7eSVvW50KPES01PecpagI+YLNLci4AcuKJrujW4Mc3sBLpFxMSlsLNHS5Nl/lvrTPA==" + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" diff --git a/system-tests/projects/create-react-app-unconfigured/yarn.lock b/system-tests/projects/create-react-app-unconfigured/yarn.lock index 1f58c340b807..c3ebd5141603 100644 --- a/system-tests/projects/create-react-app-unconfigured/yarn.lock +++ b/system-tests/projects/create-react-app-unconfigured/yarn.lock @@ -4595,9 +4595,9 @@ http-proxy-agent@^4.0.1: debug "4" http-proxy-middleware@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz#03af0f4676d172ae775cb5c33f592f40e1a4e07a" - integrity "sha1-A68PRnbRcq53XLXDP1kvQOGk4Ho= sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==" + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" diff --git a/system-tests/projects/nextjs-configured/yarn.lock b/system-tests/projects/nextjs-configured/yarn.lock index 7b885d6cf9db..654b3e052b4f 100644 --- a/system-tests/projects/nextjs-configured/yarn.lock +++ b/system-tests/projects/nextjs-configured/yarn.lock @@ -2725,9 +2725,9 @@ http-parser-js@>=0.5.1: integrity "sha1-18MNXTyQ2GW0ouhwGB+dbyKsesU= sha512-x+JVEkO2PoM8qqpbPbOL3cqHPwerep7OwzK7Ay+sMQjKzaKCqWvjoXm5tqMP9tXWWTnTzAjIhXg+J99XYuPhPA==" http-proxy-middleware@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.3.tgz#5df04f69a89f530c2284cd71eeaa51ba52243289" - integrity "sha1-XfBPaaifUwwihM1x7qpRulIkMok= sha512-1bloEwnrHMnCoO/Gcwbz7eSVvW50KPES01PecpagI+YLNLci4AcuKJrujW4Mc3sBLpFxMSlsLNHS5Nl/lvrTPA==" + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" diff --git a/system-tests/projects/nextjs-unconfigured/yarn.lock b/system-tests/projects/nextjs-unconfigured/yarn.lock index b90113c4ca68..ba5586f00ed7 100644 --- a/system-tests/projects/nextjs-unconfigured/yarn.lock +++ b/system-tests/projects/nextjs-unconfigured/yarn.lock @@ -2569,9 +2569,9 @@ http-parser-js@>=0.5.1: integrity "sha1-LgJAarLfivinq/umLg2gHGK5Wv0= sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA==" http-proxy-middleware@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz#03af0f4676d172ae775cb5c33f592f40e1a4e07a" - integrity "sha1-A68PRnbRcq53XLXDP1kvQOGk4Ho= sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==" + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" diff --git a/system-tests/projects/react-app-webpack-5-unconfigured/yarn.lock b/system-tests/projects/react-app-webpack-5-unconfigured/yarn.lock index 4b122a889cb4..281b788fe4be 100644 --- a/system-tests/projects/react-app-webpack-5-unconfigured/yarn.lock +++ b/system-tests/projects/react-app-webpack-5-unconfigured/yarn.lock @@ -4668,9 +4668,9 @@ http-proxy-agent@^4.0.1: debug "4" http-proxy-middleware@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz#03af0f4676d172ae775cb5c33f592f40e1a4e07a" - integrity "sha1-A68PRnbRcq53XLXDP1kvQOGk4Ho= sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==" + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" diff --git a/system-tests/projects/vuecli5-vue3-type-module/yarn.lock b/system-tests/projects/vuecli5-vue3-type-module/yarn.lock index 9ecb1745d80a..3abf63467b92 100644 --- a/system-tests/projects/vuecli5-vue3-type-module/yarn.lock +++ b/system-tests/projects/vuecli5-vue3-type-module/yarn.lock @@ -2008,9 +2008,9 @@ http-parser-js@>=0.5.1: integrity sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA== http-proxy-middleware@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz#03af0f4676d172ae775cb5c33f592f40e1a4e07a" - integrity sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg== + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" diff --git a/system-tests/projects/vuecli5-vue3/yarn.lock b/system-tests/projects/vuecli5-vue3/yarn.lock index 9ecb1745d80a..3abf63467b92 100644 --- a/system-tests/projects/vuecli5-vue3/yarn.lock +++ b/system-tests/projects/vuecli5-vue3/yarn.lock @@ -2008,9 +2008,9 @@ http-parser-js@>=0.5.1: integrity sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA== http-proxy-middleware@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz#03af0f4676d172ae775cb5c33f592f40e1a4e07a" - integrity sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg== + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" diff --git a/system-tests/projects/vuecli5vue3-configured/yarn.lock b/system-tests/projects/vuecli5vue3-configured/yarn.lock index 859710edf767..f6dc77f6fe4e 100644 --- a/system-tests/projects/vuecli5vue3-configured/yarn.lock +++ b/system-tests/projects/vuecli5vue3-configured/yarn.lock @@ -2084,9 +2084,9 @@ http-parser-js@>=0.5.1: integrity "sha1-18MNXTyQ2GW0ouhwGB+dbyKsesU= sha512-x+JVEkO2PoM8qqpbPbOL3cqHPwerep7OwzK7Ay+sMQjKzaKCqWvjoXm5tqMP9tXWWTnTzAjIhXg+J99XYuPhPA==" http-proxy-middleware@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.3.tgz#5df04f69a89f530c2284cd71eeaa51ba52243289" - integrity "sha1-XfBPaaifUwwihM1x7qpRulIkMok= sha512-1bloEwnrHMnCoO/Gcwbz7eSVvW50KPES01PecpagI+YLNLci4AcuKJrujW4Mc3sBLpFxMSlsLNHS5Nl/lvrTPA==" + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" diff --git a/system-tests/projects/vuecli5vue3-unconfigured/yarn.lock b/system-tests/projects/vuecli5vue3-unconfigured/yarn.lock index ad8ceec74840..815b07eb608d 100644 --- a/system-tests/projects/vuecli5vue3-unconfigured/yarn.lock +++ b/system-tests/projects/vuecli5vue3-unconfigured/yarn.lock @@ -2098,9 +2098,9 @@ http-parser-js@>=0.5.1: integrity "sha1-LgJAarLfivinq/umLg2gHGK5Wv0= sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA==" http-proxy-middleware@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz#03af0f4676d172ae775cb5c33f592f40e1a4e07a" - integrity "sha1-A68PRnbRcq53XLXDP1kvQOGk4Ho= sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==" + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" diff --git a/system-tests/projects/webpack-react-nested-config/yarn.lock b/system-tests/projects/webpack-react-nested-config/yarn.lock index e804d4b4c328..7356995e5441 100644 --- a/system-tests/projects/webpack-react-nested-config/yarn.lock +++ b/system-tests/projects/webpack-react-nested-config/yarn.lock @@ -1656,9 +1656,9 @@ http-parser-js@>=0.5.1: integrity sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA== http-proxy-middleware@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz#03af0f4676d172ae775cb5c33f592f40e1a4e07a" - integrity sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg== + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" diff --git a/system-tests/projects/webpack4_wds4-react/yarn.lock b/system-tests/projects/webpack4_wds4-react/yarn.lock index 63737097493f..31c5e3718ed8 100644 --- a/system-tests/projects/webpack4_wds4-react/yarn.lock +++ b/system-tests/projects/webpack4_wds4-react/yarn.lock @@ -2858,9 +2858,9 @@ http-parser-js@>=0.5.1: integrity sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA== http-proxy-middleware@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz#03af0f4676d172ae775cb5c33f592f40e1a4e07a" - integrity sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg== + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" diff --git a/system-tests/projects/webpack5_wds4-react/yarn.lock b/system-tests/projects/webpack5_wds4-react/yarn.lock index f6ff4264bf8b..a0f77c725b4d 100644 --- a/system-tests/projects/webpack5_wds4-react/yarn.lock +++ b/system-tests/projects/webpack5_wds4-react/yarn.lock @@ -2770,9 +2770,9 @@ http-parser-js@>=0.5.1: integrity sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA== http-proxy-middleware@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz#03af0f4676d172ae775cb5c33f592f40e1a4e07a" - integrity sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg== + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" diff --git a/yarn.lock b/yarn.lock index f1ffd7c87883..1d8b53a84875 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17975,9 +17975,9 @@ http-proxy-middleware@^1.0.0: micromatch "^4.0.2" http-proxy-middleware@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz#03af0f4676d172ae775cb5c33f592f40e1a4e07a" - integrity sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg== + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" From 22aacb1a4c95722df9a8c723b44799f4f59aa110 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:27:54 -0500 Subject: [PATCH 05/11] chore: Update v8 snapshot cache (#28998) * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache --------- Co-authored-by: cypress-bot[bot] <+cypress-bot[bot]@users.noreply.github.com> Co-authored-by: Jennifer Shehane --- tooling/v8-snapshot/cache/darwin/snapshot-meta.json | 4 +--- tooling/v8-snapshot/cache/linux/snapshot-meta.json | 4 +--- tooling/v8-snapshot/cache/win32/snapshot-meta.json | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/tooling/v8-snapshot/cache/darwin/snapshot-meta.json b/tooling/v8-snapshot/cache/darwin/snapshot-meta.json index faba0d13c49e..dd74226ee603 100644 --- a/tooling/v8-snapshot/cache/darwin/snapshot-meta.json +++ b/tooling/v8-snapshot/cache/darwin/snapshot-meta.json @@ -256,7 +256,6 @@ "./node_modules/cp-file/fs.js", "./node_modules/cp-file/node_modules/semver/semver.js", "./node_modules/cp-file/progress-emitter.js", - "./node_modules/cross-fetch/node_modules/node-fetch/lib/index.js", "./node_modules/cross-spawn-async/lib/parse.js", "./node_modules/cross-spawn-async/lib/resolveCommand.js", "./node_modules/debug/src/browser.js", @@ -3691,7 +3690,6 @@ "./packages/data-context/node_modules/ast-types/lib/types.js", "./packages/data-context/node_modules/ast-types/main.js", "./packages/data-context/node_modules/brace-expansion/index.js", - "./packages/data-context/node_modules/cross-fetch/dist/node-ponyfill.js", "./packages/data-context/node_modules/cross-spawn/index.js", "./packages/data-context/node_modules/cross-spawn/lib/enoent.js", "./packages/data-context/node_modules/cross-spawn/lib/parse.js", @@ -4318,5 +4316,5 @@ "./tooling/v8-snapshot/cache/darwin/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "36343347abdcd6955e6f2c0e7cf60b7f50d041d841eece41ae59773d62dae7b3" + "deferredHash": "513ac10fb30d8db8cdf3f539f1dd78a715bc69e947f244ebf133e7fbc28c14ae" } \ No newline at end of file diff --git a/tooling/v8-snapshot/cache/linux/snapshot-meta.json b/tooling/v8-snapshot/cache/linux/snapshot-meta.json index 90849fd1bd42..98c494ba09c5 100644 --- a/tooling/v8-snapshot/cache/linux/snapshot-meta.json +++ b/tooling/v8-snapshot/cache/linux/snapshot-meta.json @@ -256,7 +256,6 @@ "./node_modules/cp-file/fs.js", "./node_modules/cp-file/node_modules/semver/semver.js", "./node_modules/cp-file/progress-emitter.js", - "./node_modules/cross-fetch/node_modules/node-fetch/lib/index.js", "./node_modules/cross-spawn-async/lib/parse.js", "./node_modules/cross-spawn-async/lib/resolveCommand.js", "./node_modules/debug/src/browser.js", @@ -3690,7 +3689,6 @@ "./packages/data-context/node_modules/ast-types/lib/types.js", "./packages/data-context/node_modules/ast-types/main.js", "./packages/data-context/node_modules/brace-expansion/index.js", - "./packages/data-context/node_modules/cross-fetch/dist/node-ponyfill.js", "./packages/data-context/node_modules/cross-spawn/index.js", "./packages/data-context/node_modules/cross-spawn/lib/enoent.js", "./packages/data-context/node_modules/cross-spawn/lib/parse.js", @@ -4317,5 +4315,5 @@ "./tooling/v8-snapshot/cache/linux/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "36343347abdcd6955e6f2c0e7cf60b7f50d041d841eece41ae59773d62dae7b3" + "deferredHash": "513ac10fb30d8db8cdf3f539f1dd78a715bc69e947f244ebf133e7fbc28c14ae" } \ No newline at end of file diff --git a/tooling/v8-snapshot/cache/win32/snapshot-meta.json b/tooling/v8-snapshot/cache/win32/snapshot-meta.json index 554e7aad5700..c33fd643e540 100644 --- a/tooling/v8-snapshot/cache/win32/snapshot-meta.json +++ b/tooling/v8-snapshot/cache/win32/snapshot-meta.json @@ -258,7 +258,6 @@ "./node_modules/cp-file/fs.js", "./node_modules/cp-file/node_modules/semver/semver.js", "./node_modules/cp-file/progress-emitter.js", - "./node_modules/cross-fetch/node_modules/node-fetch/lib/index.js", "./node_modules/cross-spawn-async/lib/parse.js", "./node_modules/cross-spawn-async/lib/resolveCommand.js", "./node_modules/debug/src/browser.js", @@ -3690,7 +3689,6 @@ "./packages/data-context/node_modules/ast-types/lib/types.js", "./packages/data-context/node_modules/ast-types/main.js", "./packages/data-context/node_modules/brace-expansion/index.js", - "./packages/data-context/node_modules/cross-fetch/dist/node-ponyfill.js", "./packages/data-context/node_modules/cross-spawn/index.js", "./packages/data-context/node_modules/cross-spawn/lib/enoent.js", "./packages/data-context/node_modules/cross-spawn/lib/parse.js", @@ -4317,5 +4315,5 @@ "./tooling/v8-snapshot/cache/win32/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "f360d5af7ee4451d73a90d4fa6eaf259aba149a25a3f201272ab33e5d2b3160e" + "deferredHash": "7f4c4b31d91af9e60270b852d88fa074843b54d327f9c280058f5d4e0fe625fe" } \ No newline at end of file From 77e34b200979f461ed12564a49b8cf055503e628 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 13:01:13 -0500 Subject: [PATCH 06/11] chore: Update Chrome (beta) to 123.0.6312.22 (#29041) Co-authored-by: cypress-bot[bot] <41898282+cypress-bot[bot]@users.noreply.github.com> --- browser-versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser-versions.json b/browser-versions.json index 612b29f041e6..ebb6f0d48022 100644 --- a/browser-versions.json +++ b/browser-versions.json @@ -1,5 +1,5 @@ { - "chrome:beta": "123.0.6312.4", + "chrome:beta": "123.0.6312.22", "chrome:stable": "122.0.6261.94", "chrome:minimum": "64.0.3282.0" } From acc7055b763eb266eee9c4c7c0ef6122da98ead4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 13:01:56 -0500 Subject: [PATCH 07/11] chore: Update v8 snapshot cache (#29039) * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache --------- Co-authored-by: cypress-bot[bot] <+cypress-bot[bot]@users.noreply.github.com> Co-authored-by: Jennifer Shehane --- tooling/v8-snapshot/cache/darwin/snapshot-meta.json | 2 +- tooling/v8-snapshot/cache/linux/snapshot-meta.json | 2 +- tooling/v8-snapshot/cache/win32/snapshot-meta.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tooling/v8-snapshot/cache/darwin/snapshot-meta.json b/tooling/v8-snapshot/cache/darwin/snapshot-meta.json index dd74226ee603..6356dcb82266 100644 --- a/tooling/v8-snapshot/cache/darwin/snapshot-meta.json +++ b/tooling/v8-snapshot/cache/darwin/snapshot-meta.json @@ -4316,5 +4316,5 @@ "./tooling/v8-snapshot/cache/darwin/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "513ac10fb30d8db8cdf3f539f1dd78a715bc69e947f244ebf133e7fbc28c14ae" + "deferredHash": "4dc7adee0a8fdaac492f2776462f9522a163c41a63a1c15d43e2c43db6cd852f" } \ No newline at end of file diff --git a/tooling/v8-snapshot/cache/linux/snapshot-meta.json b/tooling/v8-snapshot/cache/linux/snapshot-meta.json index 98c494ba09c5..7594d7b795bf 100644 --- a/tooling/v8-snapshot/cache/linux/snapshot-meta.json +++ b/tooling/v8-snapshot/cache/linux/snapshot-meta.json @@ -4315,5 +4315,5 @@ "./tooling/v8-snapshot/cache/linux/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "513ac10fb30d8db8cdf3f539f1dd78a715bc69e947f244ebf133e7fbc28c14ae" + "deferredHash": "4dc7adee0a8fdaac492f2776462f9522a163c41a63a1c15d43e2c43db6cd852f" } \ No newline at end of file diff --git a/tooling/v8-snapshot/cache/win32/snapshot-meta.json b/tooling/v8-snapshot/cache/win32/snapshot-meta.json index c33fd643e540..3e1960ca2aa5 100644 --- a/tooling/v8-snapshot/cache/win32/snapshot-meta.json +++ b/tooling/v8-snapshot/cache/win32/snapshot-meta.json @@ -4315,5 +4315,5 @@ "./tooling/v8-snapshot/cache/win32/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "7f4c4b31d91af9e60270b852d88fa074843b54d327f9c280058f5d4e0fe625fe" + "deferredHash": "020919e5d8c45808a095c25e2b4b005d4afa60c5d3cdd2e7a0b33eb169533c17" } \ No newline at end of file From cdd7a7e10c9a0dcd49399e4fd9f8c0cf04f92d43 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 13:02:27 -0500 Subject: [PATCH 08/11] dependency: update dependency signal-exit to v3.0.7 (#28979) * fix(deps): update dependency signal-exit to v3.0.7 * Remove signal-exit - this is not used * empty commit * fix(deps): update dependency signal-exit to v3.0.7 * Update changelog * Update cli/CHANGELOG.md --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jennifer Shehane Co-authored-by: Emily Rohrbough Co-authored-by: Ryan Manuel --- cli/CHANGELOG.md | 1 + packages/server/package.json | 2 +- yarn.lock | 5 ----- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index e1aed9661e30..44c1fc3c57e8 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -10,6 +10,7 @@ _Released 2/27/2024 (PENDING)_ **Dependency Updates:** +- Updated signal-exit from `3.0.3` to `3.0.7`. Addressed in [#28979](/~https://github.com/cypress-io/cypress/pull/28979). - Updated http-proxy-middleware from `2.0.4` to `2.0.6`. Addressed in [#28902](/~https://github.com/cypress-io/cypress/pull/28902). ## 13.6.6 diff --git a/packages/server/package.json b/packages/server/package.json index d95eab6c5aba..f25ecdcef6af 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -115,7 +115,7 @@ "semver": "7.3.2", "send": "0.17.1", "shell-env": "3.0.1", - "signal-exit": "3.0.3", + "signal-exit": "3.0.7", "squirrelly": "7.9.2", "strip-ansi": "6.0.0", "syntax-error": "1.4.0", diff --git a/yarn.lock b/yarn.lock index 1d8b53a84875..beb344574596 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27385,11 +27385,6 @@ sigmund@^1.0.1, sigmund@~1.0.0: resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= -signal-exit@3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - signal-exit@3.0.7, signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" From 092a7eb5e55a27ac11179b4988b973f58ca82137 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 10:12:21 -0500 Subject: [PATCH 09/11] chore: Update v8 snapshot cache (#29047) * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache --------- Co-authored-by: cypress-bot[bot] <+cypress-bot[bot]@users.noreply.github.com> --- tooling/v8-snapshot/cache/darwin/snapshot-meta.json | 6 +----- tooling/v8-snapshot/cache/linux/snapshot-meta.json | 6 +----- tooling/v8-snapshot/cache/win32/snapshot-meta.json | 6 +----- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/tooling/v8-snapshot/cache/darwin/snapshot-meta.json b/tooling/v8-snapshot/cache/darwin/snapshot-meta.json index 6356dcb82266..45bd8069c987 100644 --- a/tooling/v8-snapshot/cache/darwin/snapshot-meta.json +++ b/tooling/v8-snapshot/cache/darwin/snapshot-meta.json @@ -77,7 +77,6 @@ "./packages/server/node_modules/graceful-fs/polyfills.js", "./packages/server/node_modules/is-ci/index.js", "./packages/server/node_modules/mocha/node_modules/debug/src/node.js", - "./packages/server/node_modules/signal-exit/index.js", "./process-nextick-args/index.js", "./signal-exit/index.js", "./ws/lib/websocket.js" @@ -785,7 +784,6 @@ "./packages/server/node_modules/cross-spawn/node_modules/semver/semver.js", "./packages/server/node_modules/duplexify/index.js", "./packages/server/node_modules/execa/lib/errname.js", - "./packages/server/node_modules/execa/node_modules/signal-exit/signals.js", "./packages/server/node_modules/get-stream/buffer-stream.js", "./packages/server/node_modules/glob/glob.js", "./packages/server/node_modules/glob/sync.js", @@ -829,7 +827,6 @@ "./packages/server/node_modules/send/node_modules/debug/src/index.js", "./packages/server/node_modules/send/node_modules/debug/src/node.js", "./packages/server/node_modules/send/node_modules/mime/mime.js", - "./packages/server/node_modules/signal-exit/signals.js", "./packages/server/node_modules/supports-color/index.js", "./packages/server/start-cypress.js", "./packages/server/v8-snapshot-entry.js", @@ -4194,7 +4191,6 @@ "./packages/server/node_modules/cross-spawn/lib/util/resolveCommand.js", "./packages/server/node_modules/execa/index.js", "./packages/server/node_modules/execa/lib/stdio.js", - "./packages/server/node_modules/execa/node_modules/signal-exit/index.js", "./packages/server/node_modules/get-stream/index.js", "./packages/server/node_modules/glob/common.js", "./packages/server/node_modules/graceful-fs/clone.js", @@ -4316,5 +4312,5 @@ "./tooling/v8-snapshot/cache/darwin/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "4dc7adee0a8fdaac492f2776462f9522a163c41a63a1c15d43e2c43db6cd852f" + "deferredHash": "da24f5340a66cbfd88e98624bf19e59e4eba7b48330f2fc0cff54075115b6076" } \ No newline at end of file diff --git a/tooling/v8-snapshot/cache/linux/snapshot-meta.json b/tooling/v8-snapshot/cache/linux/snapshot-meta.json index 7594d7b795bf..039aa7159377 100644 --- a/tooling/v8-snapshot/cache/linux/snapshot-meta.json +++ b/tooling/v8-snapshot/cache/linux/snapshot-meta.json @@ -77,7 +77,6 @@ "./packages/server/node_modules/graceful-fs/polyfills.js", "./packages/server/node_modules/is-ci/index.js", "./packages/server/node_modules/mocha/node_modules/debug/src/node.js", - "./packages/server/node_modules/signal-exit/index.js", "./process-nextick-args/index.js", "./signal-exit/index.js", "./ws/lib/websocket.js" @@ -784,7 +783,6 @@ "./packages/server/node_modules/cross-spawn/node_modules/semver/semver.js", "./packages/server/node_modules/duplexify/index.js", "./packages/server/node_modules/execa/lib/errname.js", - "./packages/server/node_modules/execa/node_modules/signal-exit/signals.js", "./packages/server/node_modules/get-stream/buffer-stream.js", "./packages/server/node_modules/glob/glob.js", "./packages/server/node_modules/glob/sync.js", @@ -828,7 +826,6 @@ "./packages/server/node_modules/send/node_modules/debug/src/index.js", "./packages/server/node_modules/send/node_modules/debug/src/node.js", "./packages/server/node_modules/send/node_modules/mime/mime.js", - "./packages/server/node_modules/signal-exit/signals.js", "./packages/server/node_modules/supports-color/index.js", "./packages/server/start-cypress.js", "./packages/server/v8-snapshot-entry.js", @@ -4193,7 +4190,6 @@ "./packages/server/node_modules/cross-spawn/lib/util/resolveCommand.js", "./packages/server/node_modules/execa/index.js", "./packages/server/node_modules/execa/lib/stdio.js", - "./packages/server/node_modules/execa/node_modules/signal-exit/index.js", "./packages/server/node_modules/get-stream/index.js", "./packages/server/node_modules/glob/common.js", "./packages/server/node_modules/graceful-fs/clone.js", @@ -4315,5 +4311,5 @@ "./tooling/v8-snapshot/cache/linux/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "4dc7adee0a8fdaac492f2776462f9522a163c41a63a1c15d43e2c43db6cd852f" + "deferredHash": "da24f5340a66cbfd88e98624bf19e59e4eba7b48330f2fc0cff54075115b6076" } \ No newline at end of file diff --git a/tooling/v8-snapshot/cache/win32/snapshot-meta.json b/tooling/v8-snapshot/cache/win32/snapshot-meta.json index 3e1960ca2aa5..a87b578a69fb 100644 --- a/tooling/v8-snapshot/cache/win32/snapshot-meta.json +++ b/tooling/v8-snapshot/cache/win32/snapshot-meta.json @@ -77,7 +77,6 @@ "./packages/server/node_modules/graceful-fs/polyfills.js", "./packages/server/node_modules/is-ci/index.js", "./packages/server/node_modules/mocha/node_modules/debug/src/node.js", - "./packages/server/node_modules/signal-exit/index.js", "./process-nextick-args/index.js", "./signal-exit/index.js", "./ws/lib/websocket.js" @@ -789,7 +788,6 @@ "./packages/server/node_modules/cross-spawn/node_modules/semver/semver.js", "./packages/server/node_modules/duplexify/index.js", "./packages/server/node_modules/execa/lib/errname.js", - "./packages/server/node_modules/execa/node_modules/signal-exit/signals.js", "./packages/server/node_modules/get-stream/buffer-stream.js", "./packages/server/node_modules/glob/glob.js", "./packages/server/node_modules/glob/sync.js", @@ -833,7 +831,6 @@ "./packages/server/node_modules/send/node_modules/debug/src/index.js", "./packages/server/node_modules/send/node_modules/debug/src/node.js", "./packages/server/node_modules/send/node_modules/mime/mime.js", - "./packages/server/node_modules/signal-exit/signals.js", "./packages/server/node_modules/supports-color/index.js", "./packages/server/start-cypress.js", "./packages/server/v8-snapshot-entry.js", @@ -4193,7 +4190,6 @@ "./packages/server/node_modules/cross-spawn/lib/util/resolveCommand.js", "./packages/server/node_modules/execa/index.js", "./packages/server/node_modules/execa/lib/stdio.js", - "./packages/server/node_modules/execa/node_modules/signal-exit/index.js", "./packages/server/node_modules/get-stream/index.js", "./packages/server/node_modules/glob/common.js", "./packages/server/node_modules/graceful-fs/clone.js", @@ -4315,5 +4311,5 @@ "./tooling/v8-snapshot/cache/win32/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "020919e5d8c45808a095c25e2b4b005d4afa60c5d3cdd2e7a0b33eb169533c17" + "deferredHash": "a9125c8169bd9325acd5d5d11758ac8023aa33b677b6b26b4e66698734445672" } \ No newline at end of file From 19fe34992fd70ef5b4cf916de7a8a507d65adbae Mon Sep 17 00:00:00 2001 From: Cacie Prins Date: Mon, 4 Mar 2024 10:35:09 -0500 Subject: [PATCH 10/11] chore: create direct dependency link between frontend-shared and data-context (#29045) --- packages/frontend-shared/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json index 01e203ea68d0..d61da656914c 100644 --- a/packages/frontend-shared/package.json +++ b/packages/frontend-shared/package.json @@ -17,7 +17,9 @@ "postinstall": "patch-package", "lint": "eslint --ext .js,.jsx,.ts,.tsx,.json,.vue ." }, - "dependencies": {}, + "dependencies": { + "@packages/data-context": "0.0.0-development" + }, "devDependencies": { "@antfu/utils": "^0.7.0", "@cypress-design/css": "^0.13.3", From a29eae92608b7144b0b51e824256c08b6dba35c2 Mon Sep 17 00:00:00 2001 From: Matt Schile Date: Tue, 5 Mar 2024 11:24:59 -0700 Subject: [PATCH 11/11] perf: fix performance issue for service workers that don't handle requests (#28900) --- .circleci/workflows.yml | 20 +- cli/CHANGELOG.md | 4 + .../cypress/e2e/e2e/service-worker.cy.js | 655 ++++++++++ .../cypress/fixtures/service-worker.html | 38 + packages/driver/cypress/plugins/index.js | 9 + packages/driver/cypress/plugins/server.js | 2 +- packages/proxy/lib/http/index.ts | 17 +- .../proxy/lib/http/response-middleware.ts | 36 +- .../lib/http/util/service-worker-injector.ts | 264 ++++ .../lib/http/util/service-worker-manager.ts | 166 ++- packages/proxy/lib/network-proxy.ts | 9 +- packages/proxy/test/unit/http/index.spec.ts | 19 +- .../unit/http/response-middleware.spec.ts | 80 ++ .../http/util/service-worker-injector.spec.ts | 23 + .../http/util/service-worker-manager.spec.ts | 1101 ++++++++--------- packages/server/lib/automation/automation.ts | 6 +- .../server/lib/browsers/browser-cri-client.ts | 78 +- .../server/lib/browsers/cdp_automation.ts | 6 +- packages/server/lib/browsers/chrome.ts | 11 +- packages/server/lib/browsers/electron.ts | 10 +- packages/server/lib/browsers/firefox-util.ts | 2 +- packages/server/lib/project-base.ts | 10 +- packages/server/lib/server-base.ts | 9 +- .../unit/browsers/browser-cri-client_spec.ts | 31 +- .../server/test/unit/browsers/firefox_spec.ts | 10 +- 25 files changed, 1948 insertions(+), 668 deletions(-) create mode 100644 packages/driver/cypress/e2e/e2e/service-worker.cy.js create mode 100644 packages/driver/cypress/fixtures/service-worker.html create mode 100644 packages/proxy/lib/http/util/service-worker-injector.ts create mode 100644 packages/proxy/test/unit/http/util/service-worker-injector.spec.ts diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index 2338737cb514..19956a2b04ae 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -29,10 +29,7 @@ mainBuildFilters: &mainBuildFilters - develop - /^release\/\d+\.\d+\.\d+$/ # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - - 'cacie/dep/electron-27' - - 'fix/force_colors_on_verify' - - 'publish-binary' - - 'em/circle2' + - 'update-v8-snapshot-cache-on-develop' # usually we don't build Mac app - it takes a long time # but sometimes we want to really confirm we are doing the right thing @@ -43,9 +40,7 @@ macWorkflowFilters: &darwin-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] - - equal: [ 'cacie/dep/electron-27', << pipeline.git.branch >> ] - - equal: [ 'fix/force_colors_on_verify', << pipeline.git.branch >> ] - - equal: [ 'ryanm/fix/service-worker-capture', << pipeline.git.branch >> ] + - equal: [ 'mschile/service_worker', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -56,9 +51,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] - - equal: [ 'cacie/dep/electron-27', << pipeline.git.branch >> ] - - equal: [ 'fix/force_colors_on_verify', << pipeline.git.branch >> ] - - equal: [ 'em/circle2', << pipeline.git.branch >> ] + - equal: [ 'mschile/service_worker', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -81,10 +74,7 @@ windowsWorkflowFilters: &windows-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] # use the following branch as well to ensure that v8 snapshot cache updates are fully tested - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] - - equal: [ 'cacie/dep/electron-27', << pipeline.git.branch >> ] - - equal: [ 'fix/force_colors_on_verify', << pipeline.git.branch >> ] - - equal: [ 'lerna-optimize-tasks', << pipeline.git.branch >> ] - - equal: [ 'mschile/mochaEvents_win_sep', << pipeline.git.branch >> ] + - equal: [ 'mschile/service_worker', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -154,7 +144,7 @@ commands: name: Set environment variable to determine whether or not to persist artifacts command: | echo "Setting SHOULD_PERSIST_ARTIFACTS variable" - echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "publish-binary" && "$CIRCLE_BRANCH" != "fix/force_colors_on_verify" && "$CIRCLE_BRANCH" != "cacie/dep/electron-27" ]]; then + echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "mschile/service_worker" ]]; then export SHOULD_PERSIST_ARTIFACTS=true fi' >> "$BASH_ENV" # You must run `setup_should_persist_artifacts` command and be using bash before running this command diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 44c1fc3c57e8..46477195b7c3 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -3,6 +3,10 @@ _Released 2/27/2024 (PENDING)_ +**Performance:** + +- Fixed a performance regression from [`13.6.4`](https://docs.cypress.io/guides/references/changelog#13-6-4) where unhandled service worker requests may not correlate correctly. Fixes [#28868](/~https://github.com/cypress-io/cypress/issues/28868). + **Bugfixes:** - Fixed an issue where `.click()` commands on children of disabled elements would still produce "click" events -- even without `{ force: true }`. Fixes [#28788](/~https://github.com/cypress-io/cypress/issues/28788). diff --git a/packages/driver/cypress/e2e/e2e/service-worker.cy.js b/packages/driver/cypress/e2e/e2e/service-worker.cy.js new file mode 100644 index 000000000000..8a25fafa6d07 --- /dev/null +++ b/packages/driver/cypress/e2e/e2e/service-worker.cy.js @@ -0,0 +1,655 @@ +// decrease the timeouts to ensure we don't hit the 2s correlation timeout +describe('service workers', { defaultCommandTimeout: 1000, pageLoadTimeout: 1000, retries: 0 }, () => { + let sessionId + + const getSessionId = async () => { + if (!sessionId) { + const targets = (await Cypress.automation('remote:debugger:protocol', { command: 'Target.getTargets', params: {} })).targetInfos + const serviceWorkerTarget = targets.reverse().find((target) => target.type === 'service_worker' && target.url === 'http://localhost:3500/fixtures/service-worker.js') + + ;({ sessionId } = await Cypress.automation('remote:debugger:protocol', { command: 'Target.attachToTarget', params: { targetId: serviceWorkerTarget.targetId, flatten: true } })) + } + + return sessionId + } + + const getEventListenersLength = async () => { + const sessionId = await getSessionId() + let result = await Cypress.automation('remote:debugger:protocol', { command: 'Runtime.evaluate', params: { expression: 'getEventListeners(self).fetch', includeCommandLineAPI: true }, sessionId }) + + if (result.result.type === 'undefined') return 0 + + result = await Cypress.automation('remote:debugger:protocol', { command: 'Runtime.getProperties', params: { objectId: result.result.objectId }, sessionId }) + + const length = result.result.find((prop) => prop.name === 'length').value.value + + return length + } + + const getOnFetchHandlerType = async () => { + const sessionId = await getSessionId() + + const result = await Cypress.automation('remote:debugger:protocol', { command: 'Runtime.evaluate', params: { expression: 'self.onfetch', includeCommandLineAPI: true }, sessionId }) + + return result.result.type + } + + const validateFetchHandlers = ({ listenerCount, onFetchHandlerType }) => { + if (Cypress.browser.family !== 'chromium') { + cy.log('Skipping fetch handlers validation in non-Chromium browsers') + + return + } + + cy.then(() => { + cy.wrap(getEventListenersLength()).should('equal', listenerCount) + if (onFetchHandlerType) cy.wrap(getOnFetchHandlerType()).should('equal', onFetchHandlerType) + }) + } + + beforeEach(async () => { + sessionId = null + + // unregister the service worker to ensure it does not affect other tests + const registrations = await navigator.serviceWorker.getRegistrations() + + await Promise.all(registrations.map((registration) => registration.unregister())) + }) + + describe('a service worker that handles requests', () => { + it('supports using addEventListener with function', () => { + const script = () => { + self.addEventListener('fetch', function (event) { + event.respondWith(fetch(event.request)) + }) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 1 }) + }) + + it('supports using addEventListener with object', () => { + const script = () => { + const obj = { + handleEvent (event) { + event.respondWith(fetch(event.request)) + }, + } + + self.addEventListener('fetch', obj) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 1 }) + }) + + it('supports using addEventListener with delayed handleEvent', () => { + const script = () => { + const obj = {} + + self.addEventListener('fetch', obj) + obj.handleEvent = function (event) { + event.respondWith(fetch(event.request)) + } + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 1 }) + }) + + it('adds both listeners when addEventListener and onfetch use the same listener', () => { + const script = () => { + const listener = function (event) { + event.respondWith(fetch(event.request)) + } + + self.onfetch = listener + self.addEventListener('fetch', listener) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + // onfetch will add an event listener + validateFetchHandlers({ listenerCount: 2, onFetchHandlerType: 'function' }) + }) + + it('supports using onfetch', () => { + const script = () => { + self.onfetch = function (event) { + event.respondWith(fetch(event.request)) + } + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + // onfetch will add an event listener + validateFetchHandlers({ listenerCount: 1, onFetchHandlerType: 'function' }) + }) + }) + + describe('a service worker that does not handle requests', () => { + it('supports using addEventListener', () => { + const script = () => { + self.addEventListener('fetch', function (event) { + return + }) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 1 }) + }) + + it('supports using onfetch', () => { + const script = () => { + self.onfetch = function (event) { + return + } + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + // onfetch will add an event listener + validateFetchHandlers({ listenerCount: 1, onFetchHandlerType: 'function' }) + }) + + it('does not add a null listener', () => { + const script = () => { + // does not add the listener because it is null + self.addEventListener('fetch', null) + // does not add the listener because it is undefined + self.addEventListener('fetch', undefined) + + // adds the listener because it is a function + self.addEventListener('fetch', () => { + return + }) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 1 }) + }) + }) + + describe('a service worker that removes fetch handlers', () => { + it('supports using addEventListener', () => { + const script = () => { + const handler = function (event) { + return new Response('Network error', { + status: 400, + headers: { 'Content-Type': 'text/plain' }, + }) + } + + self.addEventListener('fetch', handler) + self.removeEventListener('fetch', handler) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 0 }) + }) + + it('supports removing event listener on delay', () => { + const script = () => { + const handler = function (event) { + return new Response('Network error', { + status: 400, + headers: { 'Content-Type': 'text/plain' }, + }) + } + + self.addEventListener('fetch', handler) + // remove the listener after a delay + setTimeout(() => { + self.removeEventListener('fetch', handler) + }, 0) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 0 }) + }) + + it('supports using onfetch', () => { + const script = () => { + self.onfetch = function (event) { + return new Response('Network error', { + status: 400, + headers: { 'Content-Type': 'text/plain' }, + }) + } + + self.onfetch = undefined + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 0 }) + }) + + it('does not fail when removing a non-existent listener', () => { + const script = () => { + const listener = function (event) { + return + } + + self.addEventListener('fetch', listener) + + // this does not remove the listener because the listener is not the same function + self.removeEventListener('fetch', function (event) { + return + }) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 1 }) + }) + + it('does not fail when removing a null listener', () => { + const script = () => { + self.addEventListener('fetch', () => { + return + }) + + self.removeEventListener('fetch', null) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 1 }) + }) + }) + + describe('a service worker with multiple fetch handlers', () => { + it('supports using addEventListener and onfetch', () => { + const script = () => { + self.addEventListener('fetch', function (event) { + event.respondWith(fetch(event.request)) + }) + + self.onfetch = function (event) { + event.respondWith(fetch(event.request)) + } + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + // onfetch will add an event listener + validateFetchHandlers({ listenerCount: 2, onFetchHandlerType: 'function' }) + }) + + it('supports other options', () => { + const script = () => { + const handler = function (event) { + event.respondWith(fetch(event.request)) + } + + self.addEventListener('fetch', handler) + + // this one does not get added because capture is the same + self.addEventListener('fetch', handler, { capture: false }) + + // this one gets added because capture is different + self.addEventListener('fetch', handler, { capture: true }) + + // this one does not get added because capture is the same + self.addEventListener('fetch', handler, { once: true }) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 2 }) + }) + }) + + describe('multiple concurrent requests', () => { + it('page request sent (handled by service worker) and then service worker request', () => { + const script = () => { + self.addEventListener('fetch', function (event) { + const response = fetch(event.request) + + // send a request from the service worker after the page request + if (event.request.url.includes('timeout')) { + fetch('/timeout').catch(() => {}) + } + + event.respondWith(response) + }) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('/fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + }) + + it('service worker request sent and then page request (handled by service worker):', () => { + const script = () => { + self.addEventListener('fetch', function (event) { + // send a request from the service worker before the page request + if (event.request.url.includes('timeout')) { + fetch('/timeout').catch(() => {}) + } + + const response = fetch(event.request) + + event.respondWith(response) + }) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('/fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + }) + + it('page request sent (NOT handled by service worker) and then service worker request:', () => { + const script = () => { + self.addEventListener('fetch', function (event) { + if (event.request.url.includes('timeout')) { + fetch('/timeout').catch(() => {}) + } + + return + }) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('/fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + }) + }) + + it('supports aborted listeners', () => { + const script = () => { + const alreadyAborted = new AbortController() + + alreadyAborted.abort() + + // this one does not get added because the signal is aborted before adding the listener + self.addEventListener('fetch', () => { + return + }, { signal: alreadyAborted.signal }) + + const notAborted = new AbortController() + + // this one gets added because the signal is not aborted + self.addEventListener('fetch', (event) => { + event.respondWith(fetch(event.request)) + }, { signal: notAborted.signal }) + + const aborted = new AbortController() + + // this one gets added but then immediately removed because the signal is aborted after adding the listener + self.addEventListener('fetch', (event) => { + event.respondWith(fetch(event.request)) + }, { signal: aborted.signal }) + + aborted.abort() + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 1 }) + }) + + it('supports changing the handleEvent function', () => { + const script = () => { + const listener = { + handleEvent (event) { + event.respondWith(new Response('Network error', { + status: 400, + headers: { 'Content-Type': 'text/plain' }, + })) + }, + } + + self.addEventListener('fetch', listener) + + listener.handleEvent = function (event) { + event.respondWith(fetch(event.request)) + } + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 1 }) + }) + + it('supports changing the handleEvent function prior to adding', () => { + const script = () => { + const listener = { + handleEvent (event) { + event.respondWith(new Response('Network error', { + status: 400, + headers: { 'Content-Type': 'text/plain' }, + })) + }, + } + + listener.handleEvent = function (event) { + event.respondWith(fetch(event.request)) + } + + self.addEventListener('fetch', listener) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 1 }) + }) + + it('succeeds when there are no listeners', () => { + const script = () => {} + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 0 }) + }) + + it('supports caching', () => { + const script = () => { + self.addEventListener('install', function (event) { + event.waitUntil( + caches.open('v1').then(function (cache) { + return cache.addAll([ + '/fixtures/1mb', + ]) + }), + ) + }) + + self.addEventListener('fetch', function (event) { + event.respondWith( + caches.match(event.request).then(function (response) { + return response || fetch(event.request) + }), + ) + }) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html').then(async (win) => { + const response = await win.fetch('/1mb') + + expect(response.ok).to.be.true + }) + + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 1 }) + }) + + it('supports clients.claim', () => { + const script = () => { + self.addEventListener('activate', (event) => { + event.waitUntil(self.clients.claim()) + }) + + self.addEventListener('fetch', function (event) { + event.respondWith(fetch(event.request)) + }) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 1 }) + }) + + it('supports async fetch handler', () => { + const script = () => { + self.addEventListener('fetch', async function (event) { + await Promise.resolve() + event.respondWith(fetch(event.request)) + }) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 1 }) + }) + + it('does not fail when the listener throws an error', () => { + const script = () => { + self.addEventListener('fetch', function (event) { + throw new Error('Error in fetch listener') + }) + } + + cy.intercept('/fixtures/service-worker.js', (req) => { + req.reply(`(${script})()`, + { 'Content-Type': 'application/javascript' }) + }) + + cy.visit('fixtures/service-worker.html') + + cy.get('#output').should('have.text', 'done') + validateFetchHandlers({ listenerCount: 1 }) + }) +}) diff --git a/packages/driver/cypress/fixtures/service-worker.html b/packages/driver/cypress/fixtures/service-worker.html new file mode 100644 index 000000000000..6ca9ad9eebf5 --- /dev/null +++ b/packages/driver/cypress/fixtures/service-worker.html @@ -0,0 +1,38 @@ + + + + + + + +

hi

+
+ + diff --git a/packages/driver/cypress/plugins/index.js b/packages/driver/cypress/plugins/index.js index 805da64e8585..966a202bed51 100644 --- a/packages/driver/cypress/plugins/index.js +++ b/packages/driver/cypress/plugins/index.js @@ -42,6 +42,15 @@ module.exports = async (on, config) => { on('file:preprocessor', wp({ webpackOptions })) + on('before:browser:launch', (browser, launchOptions) => { + if (browser.family === 'firefox') { + // set testing_localhost_is_secure_when_hijacked to true so localhost will be considered a secure context + launchOptions.preferences['network.proxy.testing_localhost_is_secure_when_hijacked'] = true + } + + return launchOptions + }) + on('task', { 'return:arg' (arg) { return arg diff --git a/packages/driver/cypress/plugins/server.js b/packages/driver/cypress/plugins/server.js index 8aaad9d1a460..32407ecae4b7 100644 --- a/packages/driver/cypress/plugins/server.js +++ b/packages/driver/cypress/plugins/server.js @@ -122,7 +122,7 @@ const createApp = (port) => { return res.send(req.body) }) - app.get('/1mb', (req, res) => { + app.get('*/1mb', (req, res) => { return res.type('text').send('X'.repeat(1024 * 1024)) }) diff --git a/packages/proxy/lib/http/index.ts b/packages/proxy/lib/http/index.ts index da75e598bce6..4b3ddcf10713 100644 --- a/packages/proxy/lib/http/index.ts +++ b/packages/proxy/lib/http/index.ts @@ -10,6 +10,7 @@ import RequestMiddleware from './request-middleware' import ResponseMiddleware from './response-middleware' import { HttpBuffers } from './util/buffers' import { GetPreRequestCb, PendingRequest, PreRequests } from './util/prerequests' +import { ServiceWorkerManager } from './util/service-worker-manager' import type EventEmitter from 'events' import type CyServer from '@packages/server' @@ -27,7 +28,7 @@ import type { CookieJar, SerializableAutomationCookie } from '@packages/server/l import type { ResourceTypeAndCredentialManager } from '@packages/server/lib/util/resourceTypeAndCredentialManager' import type { ProtocolManagerShape } from '@packages/types' import type Protocol from 'devtools-protocol' -import { ServiceWorkerManager } from './util/service-worker-manager' +import type { ServiceWorkerClientEvent } from './util/service-worker-manager' function getRandomColorFn () { return chalk.hex(`#${Number( @@ -35,7 +36,7 @@ function getRandomColorFn () { ).toString(16).padStart(6, 'F').toUpperCase()}`) } -const hasServiceWorkerHeader = (headers: Record) => { +export const hasServiceWorkerHeader = (headers: Record) => { return headers?.['service-worker'] === 'script' || headers?.['Service-Worker'] === 'script' } @@ -466,8 +467,8 @@ export class Http { return this.buffers.set(buffer) } - addPendingBrowserPreRequest (browserPreRequest: BrowserPreRequest) { - if (this.shouldIgnorePendingRequest(browserPreRequest)) { + async addPendingBrowserPreRequest (browserPreRequest: BrowserPreRequest) { + if (await this.shouldIgnorePendingRequest(browserPreRequest)) { return } @@ -498,6 +499,10 @@ export class Http { this.serviceWorkerManager.addInitiatorToServiceWorker({ scriptURL: data.scriptURL, initiatorOrigin: data.initiatorOrigin }) } + handleServiceWorkerClientEvent (event: ServiceWorkerClientEvent) { + this.serviceWorkerManager.handleServiceWorkerClientEvent(event) + } + setProtocolManager (protocolManager: ProtocolManagerShape) { this.protocolManager = protocolManager this.preRequests.setProtocolManager(protocolManager) @@ -507,7 +512,7 @@ export class Http { this.preRequests.setPreRequestTimeout(timeout) } - private shouldIgnorePendingRequest (browserPreRequest: BrowserPreRequest) { + private async shouldIgnorePendingRequest (browserPreRequest: BrowserPreRequest) { // The initial request that loads the service worker does not always get sent to CDP. If it does, we want it to not clog up either the prerequests // or pending requests. Thus, we need to explicitly ignore it here and in `get`. We determine it's the service worker request via the // `service-worker` header @@ -517,7 +522,7 @@ export class Http { return true } - if (this.serviceWorkerManager.processBrowserPreRequest(browserPreRequest)) { + if (await this.serviceWorkerManager.processBrowserPreRequest(browserPreRequest)) { debugVerbose('Not correlating request since it is fully controlled by the service worker and the correlation will happen within the service worker: %o', browserPreRequest) return true diff --git a/packages/proxy/lib/http/response-middleware.ts b/packages/proxy/lib/http/response-middleware.ts index 171fcc7ffa39..1722c1d8eb56 100644 --- a/packages/proxy/lib/http/response-middleware.ts +++ b/packages/proxy/lib/http/response-middleware.ts @@ -9,7 +9,7 @@ import { InterceptResponse } from '@packages/net-stubbing' import { concatStream, cors, httpUtils } from '@packages/network' import { toughCookieToAutomationCookie } from '@packages/server/lib/util/cookies' import { telemetry } from '@packages/telemetry' -import { isVerboseTelemetry as isVerbose } from '.' +import { hasServiceWorkerHeader, isVerboseTelemetry as isVerbose } from '.' import { CookiesHelper } from './util/cookies' import * as rewriter from './util/rewriter' import { doesTopNeedToBeSimulated } from './util/top-simulation' @@ -21,6 +21,7 @@ import type { HttpMiddleware, HttpMiddlewareThis } from '.' import type { IncomingMessage, IncomingHttpHeaders } from 'http' import { cspHeaderNames, generateCspDirectives, nonceDirectives, parseCspHeaders, problematicCspDirectives, unsupportedCSPDirectives } from './util/csp-header' +import { injectIntoServiceWorker } from './util/service-worker-injector' export interface ResponseMiddlewareProps { /** @@ -832,6 +833,38 @@ const MaybeRemoveSecurity: ResponseMiddleware = function () { this.next() } +const MaybeInjectServiceWorker: ResponseMiddleware = function () { + const span = telemetry.startSpan({ name: 'maybe:inject:service:worker', parentSpan: this.resMiddlewareSpan, isVerbose }) + const hasHeader = hasServiceWorkerHeader(this.req.headers) + + span?.setAttributes({ hasServiceWorkerHeader: hasHeader }) + + if (!hasHeader) { + span?.end() + + return this.next() + } + + this.makeResStreamPlainText() + + this.incomingResStream.setEncoding('utf8') + + this.incomingResStream.pipe(concatStream(async (body) => { + const updatedBody = injectIntoServiceWorker(body) + + const pt = new PassThrough + + pt.write(updatedBody) + pt.end() + + this.incomingResStream = pt + + this.next() + })).on('error', this.onError).once('close', () => { + span?.end() + }) +} + const GzipBody: ResponseMiddleware = async function () { if (this.protocolManager && this.req.browserPreRequest?.requestId) { const preRequest = this.req.browserPreRequest @@ -909,6 +942,7 @@ export default { MaybeEndWithEmptyBody, MaybeInjectHtml, MaybeRemoveSecurity, + MaybeInjectServiceWorker, GzipBody, SendResponseBodyToClient, } diff --git a/packages/proxy/lib/http/util/service-worker-injector.ts b/packages/proxy/lib/http/util/service-worker-injector.ts new file mode 100644 index 000000000000..aea58df93c70 --- /dev/null +++ b/packages/proxy/lib/http/util/service-worker-injector.ts @@ -0,0 +1,264 @@ +/// + +import type { ServiceWorkerClientEvent } from './service-worker-manager' + +// this should be of type ServiceWorkerGlobalScope from the webworker lib, +// but we can't reference it directly because it causes errors in other packages +interface ServiceWorkerGlobalScope extends WorkerGlobalScope { + onfetch: FetchListener | null + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void + __cypressServiceWorkerClientEvent: (event: string) => void +} + +// this should be of type FetchEvent from the webworker lib, +// but we can't reference it directly because it causes errors in other packages +interface FetchEvent extends Event { + readonly request: Request + respondWith(r: Response | PromiseLike): void +} + +type FetchListener = (this: ServiceWorkerGlobalScope, ev: FetchEvent) => any + +declare let self: ServiceWorkerGlobalScope + +/** + * Injects code into the service worker to overwrite the fetch events to determine if the service worker handled the request. + * @param body the body of the service worker + * @returns the updated service worker + */ +export const injectIntoServiceWorker = (body: Buffer) => { + function __cypressInjectIntoServiceWorker () { + let listenerCount = 0 + let eventQueue: ServiceWorkerClientEvent[] = [] + const nonCaptureListenersMap = new WeakMap() + const captureListenersMap = new WeakMap() + const targetToWrappedHandleEventMap = new WeakMap() + const targetToOrigHandleEventMap = new WeakMap() + + const sendEvent = (event: ServiceWorkerClientEvent) => { + // if the binding has been created, we can call it + // otherwise, we need to queue the event + if (self.__cypressServiceWorkerClientEvent) { + self.__cypressServiceWorkerClientEvent(JSON.stringify(event)) + } else { + eventQueue.push(event) + } + } + + const sendHasFetchEventHandlers = () => { + // @ts-expect-error __cypressIsScriptEvaluated is declared below + // if the script has been evaluated, we can call the CDP binding to inform the backend whether or not the service worker has a handler + if (__cypressIsScriptEvaluated) { + sendEvent({ type: 'hasFetchHandler', payload: { hasFetchHandler: !!(listenerCount > 0 || self.onfetch) } }) + } + } + + const sendFetchRequest = (payload: { url: string, isControlled: boolean }) => { + // call the CDP binding to inform the backend whether or not the service worker handled the request + sendEvent({ type: 'fetchRequest', payload }) + } + + // A listener is considered valid if it is a function or an object (with the handleEvent function or the function could be added later) + const isValidListener = (listener: EventListenerOrEventListenerObject) => { + return listener && (typeof listener === 'function' || typeof listener === 'object') + } + + // Determine if the event listener was aborted + const isAborted = (options?: boolean | AddEventListenerOptions) => { + return typeof options === 'object' && options.signal?.aborted + } + + // Get the capture value from the options + const getCaptureValue = (options?: boolean | AddEventListenerOptions) => { + return typeof options === 'boolean' ? options : options?.capture + } + + function wrapListener (listener: FetchListener): FetchListener { + return (event) => { + // we want to override the respondWith method so we can track if it was called + // to determine if the service worker handled the request + const oldRespondWith = event.respondWith + let respondWithCalled = false + + event.respondWith = (...args) => { + respondWithCalled = true + oldRespondWith.call(event, ...args) + } + + let returnValue + + try { + // call the original listener + returnValue = listener.call(self, event) + } catch { + // if the listener throws an error, we still want to proceed with calling the binding + } + + if (returnValue instanceof Promise) { + // if the listener returns a promise, we need to wait for it to resolve + // before we can determine if the service worker handled the request + returnValue.then(() => { + sendFetchRequest({ url: event.request.url, isControlled: respondWithCalled }) + }) + } else { + sendFetchRequest({ url: event.request.url, isControlled: respondWithCalled }) + } + + return returnValue + } + } + + const oldAddEventListener = self.addEventListener + + // Overwrite the addEventListener method so we can determine if the service worker handled the request + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener + self.addEventListener = (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) => { + if (type === 'fetch' && isValidListener(listener) && !isAborted(options)) { + const capture = getCaptureValue(options) + const existingListener = capture ? captureListenersMap.get(listener) : nonCaptureListenersMap.get(listener) + + // If the listener is already in the map, we don't need to wrap it again + if (existingListener) { + return oldAddEventListener(type, existingListener, options) + } + + let newListener: EventListenerOrEventListenerObject + + // If the listener is a function, we can just wrap it + // Otherwise, we need to wrap the listener in a proxy so we can track and wrap the handleEvent function + if (typeof listener === 'function') { + newListener = wrapListener(listener) as EventListener + } else { + // since the handleEvent function could change, we need to use a proxy to wrap it + newListener = new Proxy(listener, { + get (target, key) { + if (key === 'handleEvent') { + const wrappedHandleEvent = targetToWrappedHandleEventMap.get(target) + const origHandleEvent = targetToOrigHandleEventMap.get(target) + + // If the handleEvent function has not been wrapped yet, or if it has changed, we need to wrap it + if ((!wrappedHandleEvent && target.handleEvent) || target.handleEvent !== origHandleEvent) { + targetToWrappedHandleEventMap.set(target, wrapListener(target.handleEvent) as EventListener) + targetToOrigHandleEventMap.set(target, target.handleEvent) + } + + return targetToWrappedHandleEventMap.get(target) + } + + return Reflect.get(target, key) + }, + }) + } + + // call the original addEventListener function prior to doing any additional work since it may fail + const result = oldAddEventListener(type, newListener, options) + + // get the capture value so we know which map to add the listener to + // so we can then remove the listener later if requested + getCaptureValue(options) ? captureListenersMap.set(listener, newListener) : nonCaptureListenersMap.set(listener, newListener) + listenerCount++ + + sendHasFetchEventHandlers() + + return result + } + + return oldAddEventListener(type, listener, options) + } + + const oldRemoveEventListener = self.removeEventListener + + // Overwrite the removeEventListener method so we can remove the listener from the map + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener + self.removeEventListener = (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) => { + if (type === 'fetch' && isValidListener(listener)) { + // get the capture value so we know which map to remove the listener from + const capture = getCaptureValue(options) + const newListener = capture ? captureListenersMap.get(listener) : nonCaptureListenersMap.get(listener) + + // call the original removeEventListener function prior to doing any additional work since it may fail + const result = oldRemoveEventListener(type, newListener!, options) + + capture ? captureListenersMap.delete(listener) : nonCaptureListenersMap.delete(listener) + listenerCount-- + + // If the listener is an object with a handleEvent method, we need to remove the wrapped function + if (typeof listener === 'object' && typeof listener.handleEvent === 'function') { + targetToWrappedHandleEventMap.delete(listener) + targetToOrigHandleEventMap.delete(listener) + } + + sendHasFetchEventHandlers() + + return result + } + + return oldRemoveEventListener(type, listener, options) + } + + const originalPropertyDescriptor = Object.getOwnPropertyDescriptor( + self, + 'onfetch', + ) + + if (!originalPropertyDescriptor) { + return + } + + // Overwrite the onfetch property so we can + // determine if the service worker handled the request + Object.defineProperty( + self, + 'onfetch', + { + configurable: originalPropertyDescriptor.configurable, + enumerable: originalPropertyDescriptor.enumerable, + get () { + return originalPropertyDescriptor.get?.call(this) + }, + set (value: typeof self.onfetch) { + let newHandler + + if (value) { + newHandler = wrapListener(value) + } + + originalPropertyDescriptor.set?.call(this, newHandler) + + sendHasFetchEventHandlers() + }, + }, + ) + + // listen for the activate event so we can inform the + // backend whether or not the service worker has a handler + self.addEventListener('activate', () => { + sendHasFetchEventHandlers() + + // if the binding has not been created yet, we need to wait for it + if (!self.__cypressServiceWorkerClientEvent) { + const timer = setInterval(() => { + if (self.__cypressServiceWorkerClientEvent) { + clearInterval(timer) + + // send any events that were queued + eventQueue.forEach((event) => { + self.__cypressServiceWorkerClientEvent(JSON.stringify(event)) + }) + + eventQueue = [] + } + }, 5) + } + }) + } + + const updatedBody = ` +let __cypressIsScriptEvaluated = false; +(${__cypressInjectIntoServiceWorker})(); +${body}; +__cypressIsScriptEvaluated = true;` + + return updatedBody +} diff --git a/packages/proxy/lib/http/util/service-worker-manager.ts b/packages/proxy/lib/http/util/service-worker-manager.ts index cf4f3c9db50c..606aae28adff 100644 --- a/packages/proxy/lib/http/util/service-worker-manager.ts +++ b/packages/proxy/lib/http/util/service-worker-manager.ts @@ -1,4 +1,5 @@ import Debug from 'debug' +import pDefer from 'p-defer' import type { BrowserPreRequest } from '../../types' import type Protocol from 'devtools-protocol' @@ -36,6 +37,31 @@ type AddInitiatorToServiceWorkerOptions = { initiatorOrigin: string } +export const serviceWorkerClientEventHandlerName = '__cypressServiceWorkerClientEvent' + +export declare type ServiceWorkerEventsPayload = { + 'fetchRequest': { url: string, isControlled: boolean } + 'hasFetchHandler': { hasFetchHandler: boolean } +} + +type _ServiceWorkerClientEvent = { type: T, payload: ServiceWorkerEventsPayload[T] } + +export type ServiceWorkerClientEvent = _ServiceWorkerClientEvent + +export type ServiceWorkerEventHandler = (event: ServiceWorkerClientEvent) => void + +/** + * Adds and listens to the service worker client event CDP binding. + * @param event the attached to target event + */ +export const serviceWorkerClientEventHandler = (handler: ServiceWorkerEventHandler) => { + return (event: { name: string, payload: string }) => { + if (event.name === serviceWorkerClientEventHandlerName) { + handler(JSON.parse(event.payload)) + } + } +} + /** * Manages service worker registrations and their controlled URLs. * @@ -53,10 +79,14 @@ type AddInitiatorToServiceWorkerOptions = { * * 1. The document URL for the browser pre-request matches the initiator origin for the service worker. * 2. The request URL is within the scope of the service worker or the request URL's initiator is controlled by the service worker. + * 3. The fetch handler for the service worker handles the request by calling `event.respondWith`. */ export class ServiceWorkerManager { private serviceWorkerRegistrations: Map = new Map() private pendingInitiators: Map = new Map() + private pendingPotentiallyControlledRequests: Map[]> = new Map[]>() + private pendingServiceWorkerFetches: Map = new Map() + private hasFetchHandler = false /** * Goes through the list of service worker registrations and adds or removes them from the manager. @@ -87,6 +117,7 @@ export class ServiceWorkerManager { * be added to the service worker when it is activated. */ addInitiatorToServiceWorker ({ scriptURL, initiatorOrigin }: AddInitiatorToServiceWorkerOptions) { + debug('Adding initiator origin %s to service worker with script URL %s', initiatorOrigin, scriptURL) let initiatorAdded = false for (const registration of this.serviceWorkerRegistrations.values()) { @@ -104,20 +135,42 @@ export class ServiceWorkerManager { } /** - * Processes a browser pre-request to determine if it is controlled by a service worker. If it is, the service worker's controlled URLs are updated with the given request URL. + * Handles a service worker fetch event. + * @param event the service worker fetch event to handle + */ + handleServiceWorkerClientEvent (event: ServiceWorkerClientEvent) { + debug('Handling service worker fetch event: %o', event) + + switch (event.type) { + case 'fetchRequest': + this.handleServiceWorkerFetchEvent(event.payload as ServiceWorkerEventsPayload['fetchRequest']) + break + case 'hasFetchHandler': + this.hasServiceWorkerFetchHandlers(event.payload as ServiceWorkerEventsPayload['hasFetchHandler']) + break + default: + throw new Error(`Unknown event type: ${event.type}`) + } + } + + /** + * Processes a browser pre-request to determine if it is controlled by a service worker. + * If it is, the service worker's controlled URLs are updated with the given request URL. * * @param browserPreRequest The browser pre-request to process. * @returns `true` if the request is controlled by a service worker, `false` otherwise. */ - processBrowserPreRequest (browserPreRequest: BrowserPreRequest) { + async processBrowserPreRequest (browserPreRequest: BrowserPreRequest) { if (browserPreRequest.initiator?.type === 'preload') { return false } - let requestControlledByServiceWorker = false + let requestPotentiallyControlledByServiceWorker = false + let activatedServiceWorker: ServiceWorker | undefined + const paramlessURL = browserPreRequest.url?.split('?')[0] || '' this.serviceWorkerRegistrations.forEach((registration) => { - const activatedServiceWorker = registration.activatedServiceWorker + activatedServiceWorker = registration.activatedServiceWorker const paramlessDocumentURL = browserPreRequest.documentURL?.split('?')[0] || '' // We are determining here if a request is controlled by a service worker. A request is controlled by a service worker if @@ -127,10 +180,11 @@ export class ServiceWorkerManager { activatedServiceWorker.scriptURL === paramlessDocumentURL || !activatedServiceWorker.initiatorOrigin || !paramlessDocumentURL.startsWith(activatedServiceWorker.initiatorOrigin)) { + debug('Service worker not activated or request URL is from the service worker, skipping: %o', { activatedServiceWorker, browserPreRequest }) + return } - const paramlessURL = browserPreRequest.url?.split('?')[0] || '' const paramlessInitiatorURL = browserPreRequest.initiator?.url?.split('?')[0] const paramlessCallStackURL = browserPreRequest.initiator?.stack?.callFrames[0]?.url?.split('?')[0] const urlIsControlled = paramlessURL.startsWith(registration.scopeURL) @@ -138,20 +192,112 @@ export class ServiceWorkerManager { const topStackUrlIsControlled = paramlessCallStackURL && activatedServiceWorker.controlledURLs?.has(paramlessCallStackURL) if (urlIsControlled || initiatorUrlIsControlled || topStackUrlIsControlled) { - activatedServiceWorker.controlledURLs.add(paramlessURL) - requestControlledByServiceWorker = true + requestPotentiallyControlledByServiceWorker = true } }) - return requestControlledByServiceWorker + if (activatedServiceWorker && requestPotentiallyControlledByServiceWorker && await this.isURLControlledByServiceWorker(browserPreRequest.url)) { + debug('Request is controlled by service worker: %o', browserPreRequest.url) + activatedServiceWorker.controlledURLs.add(paramlessURL) + + return true + } + + if (activatedServiceWorker) { + debug('Request is not controlled by service worker: %o', browserPreRequest.url) + } + + return false + } + + /** + * Handles a service worker has fetch handlers event. + * @param event the service worker has fetch handlers event to handle + */ + private hasServiceWorkerFetchHandlers (event: ServiceWorkerEventsPayload['hasFetchHandler']) { + debug('service worker has fetch handlers event called: %o', event) + this.hasFetchHandler = event.hasFetchHandler + } + + /** + * Handles a service worker fetch event. + * @param event the service worker fetch event to handle + */ + private handleServiceWorkerFetchEvent (event: ServiceWorkerEventsPayload['fetchRequest']) { + const promises = this.pendingPotentiallyControlledRequests.get(event.url) + + if (promises) { + debug('found pending controlled request promise: %o', event) + + const currentPromiseForUrl = promises.shift() + + if (promises.length === 0) { + this.pendingPotentiallyControlledRequests.delete(event.url) + } + + currentPromiseForUrl?.resolve(event.isControlled) + } else { + const fetches = this.pendingServiceWorkerFetches.get(event.url) + + debug('no pending controlled request promise found, adding a pending service worker fetch: %o', event) + + if (fetches) { + fetches.push(event.isControlled) + } else { + this.pendingServiceWorkerFetches.set(event.url, [event.isControlled]) + } + } + } + + /** + * Determines if the given URL is controlled by a service worker. + * @param url the URL to check + * @returns a promise that resolves to `true` if the URL is controlled by a service worker, `false` otherwise. + */ + private isURLControlledByServiceWorker (url: string) { + if (!this.hasFetchHandler) { + return false + } + + const fetches = this.pendingServiceWorkerFetches.get(url) + + if (fetches) { + const isControlled = fetches.shift() + + debug('found pending service worker fetch: %o', { url, isControlled }) + + if (fetches.length === 0) { + this.pendingServiceWorkerFetches.delete(url) + } + + return Promise.resolve(isControlled) + } + + let promises = this.pendingPotentiallyControlledRequests.get(url) + + if (!promises) { + promises = [] + this.pendingPotentiallyControlledRequests.set(url, promises) + } + + const deferred = pDefer() + + promises.push(deferred) + debug('adding pending controlled request promise: %s', url) + + return deferred.promise } /** * Registers the given service worker with the given scope. Will not overwrite an existing registration. */ private registerServiceWorker ({ registrationId, scopeURL }: RegisterServiceWorkerOptions) { + debug('Registering service worker with registration ID %s and scope URL %s', registrationId, scopeURL) + // Only register service workers if they haven't already been registered if (this.serviceWorkerRegistrations.get(registrationId)?.scopeURL === scopeURL) { + debug('Service worker with registration ID %s and scope URL %s already registered', registrationId, scopeURL) + return } @@ -165,6 +311,7 @@ export class ServiceWorkerManager { * Unregisters the service worker with the given registration ID. */ private unregisterServiceWorker ({ registrationId }: UnregisterServiceWorkerOptions) { + debug('Unregistering service worker with registration ID %s', registrationId) this.serviceWorkerRegistrations.delete(registrationId) } @@ -172,6 +319,7 @@ export class ServiceWorkerManager { * Adds an activated service worker to the manager. */ private addActivatedServiceWorker ({ registrationId, scriptURL }: AddActivatedServiceWorkerOptions) { + debug('Adding activated service worker with registration ID %s and script URL %s', registrationId, scriptURL) const registration = this.serviceWorkerRegistrations.get(registrationId) if (registration) { @@ -180,7 +328,7 @@ export class ServiceWorkerManager { registration.activatedServiceWorker = { registrationId, scriptURL, - controlledURLs: new Set(), + controlledURLs: registration.activatedServiceWorker?.controlledURLs || new Set(), initiatorOrigin: initiatorOrigin || registration.activatedServiceWorker?.initiatorOrigin, } diff --git a/packages/proxy/lib/network-proxy.ts b/packages/proxy/lib/network-proxy.ts index e54172ba55fe..227919a8aa6e 100644 --- a/packages/proxy/lib/network-proxy.ts +++ b/packages/proxy/lib/network-proxy.ts @@ -2,6 +2,7 @@ import { telemetry } from '@packages/telemetry' import { Http, ServerCtx } from './http' import type { BrowserPreRequest } from './types' import type Protocol from 'devtools-protocol' +import type { ServiceWorkerClientEvent } from './http/util/service-worker-manager' export class NetworkProxy { http: Http @@ -10,8 +11,8 @@ export class NetworkProxy { this.http = new Http(opts) } - addPendingBrowserPreRequest (preRequest: BrowserPreRequest) { - this.http.addPendingBrowserPreRequest(preRequest) + async addPendingBrowserPreRequest (preRequest: BrowserPreRequest) { + await this.http.addPendingBrowserPreRequest(preRequest) } removePendingBrowserPreRequest (requestId: string) { @@ -38,6 +39,10 @@ export class NetworkProxy { this.http.updateServiceWorkerClientSideRegistrations(data) } + handleServiceWorkerClientEvent (event: ServiceWorkerClientEvent) { + this.http.handleServiceWorkerClientEvent(event) + } + handleHttpRequest (req, res) { const span = telemetry.startSpan({ name: 'network:proxy:handleHttpRequest', diff --git a/packages/proxy/test/unit/http/index.spec.ts b/packages/proxy/test/unit/http/index.spec.ts index b0f7051f8cfd..7af1adf38f64 100644 --- a/packages/proxy/test/unit/http/index.spec.ts +++ b/packages/proxy/test/unit/http/index.spec.ts @@ -345,7 +345,7 @@ describe('http', function () { cdpRequestWillBeSentReceivedTimestamp: performance.now() + performance.timeOrigin + 10000, } - processBrowserPreRequestStub.returns(true) + processBrowserPreRequestStub.resolves(true) http.addPendingBrowserPreRequest(browserPreRequest as BrowserPreRequest) @@ -446,5 +446,22 @@ describe('http', function () { expect(processBrowserPreRequestStub).to.be.calledOnce }) + + it('handles service worker client events', () => { + const http = new Http(httpOpts) + const handleServiceWorkerClientEventStub = sinon.stub(http.serviceWorkerManager, 'handleServiceWorkerClientEvent') + + const event = { + type: 'fetchRequest' as const, + payload: { + url: 'foo', + isControlled: true, + }, + } + + http.handleServiceWorkerClientEvent(event) + + expect(handleServiceWorkerClientEventStub).to.be.calledWith(event) + }) }) }) diff --git a/packages/proxy/test/unit/http/response-middleware.spec.ts b/packages/proxy/test/unit/http/response-middleware.spec.ts index 1216a73783c7..b41de4996205 100644 --- a/packages/proxy/test/unit/http/response-middleware.spec.ts +++ b/packages/proxy/test/unit/http/response-middleware.spec.ts @@ -8,6 +8,7 @@ import { RemoteStates } from '@packages/server/lib/remote_states' import { Readable } from 'stream' import * as rewriter from '../../../lib/http/util/rewriter' import { nonceDirectives, problematicCspDirectives, unsupportedCSPDirectives } from '../../../lib/http/util/csp-header' +import * as serviceWorkerInjector from '../../../lib/http/util/service-worker-injector' describe('http/response-middleware', function () { it('exports the members in the correct order', function () { @@ -28,6 +29,7 @@ describe('http/response-middleware', function () { 'MaybeEndWithEmptyBody', 'MaybeInjectHtml', 'MaybeRemoveSecurity', + 'MaybeInjectServiceWorker', 'GzipBody', 'SendResponseBodyToClient', ]) @@ -2293,6 +2295,84 @@ describe('http/response-middleware', function () { } }) + describe('MaybeInjectServiceWorker', function () { + const { MaybeInjectServiceWorker } = ResponseMiddleware + let ctx + let injectIntoServiceWorkerStub + + beforeEach(() => { + injectIntoServiceWorkerStub = sinon.spy(serviceWorkerInjector, 'injectIntoServiceWorker') + }) + + afterEach(() => { + injectIntoServiceWorkerStub.restore() + }) + + it('does not rewrite service worker if the request does not have the service worker header', function () { + prepareContext({ + req: { + proxiedUrl: 'http://www.foobar.com:3501/not-service-worker.js', + }, + }) + + return testMiddleware([MaybeInjectServiceWorker], ctx) + .then(() => { + expect(injectIntoServiceWorkerStub).not.to.be.called + }) + }) + + it('rewrites the service worker', async function () { + prepareContext({ + req: { + proxiedUrl: 'http://www.foobar.com:3501/service-worker.js', + headers: { + 'service-worker': 'script', + }, + }, + }) + + return testMiddleware([MaybeInjectServiceWorker], ctx) + .then(() => { + expect(injectIntoServiceWorkerStub).to.be.calledOnce + expect(injectIntoServiceWorkerStub).to.be.calledWith('foo') + }) + }) + + function prepareContext (props) { + const remoteStates = new RemoteStates(() => {}) + const stream = Readable.from(['foo']) + + // set the primary remote state + remoteStates.set('http://127.0.0.1:3501') + + ctx = { + incomingRes: { + headers: {}, + ...props.incomingRes, + }, + res: { + on: (event, listener) => {}, + off: (event, listener) => {}, + ...props.res, + }, + req: { + ...props.req, + }, + makeResStreamPlainText () {}, + incomingResStream: stream, + config: {}, + remoteStates, + debug: (formatter, ...args) => { + debugVerbose(`%s %s %s ${formatter}`, ctx.req.method, ctx.req.proxiedUrl, ctx.stage, ...args) + }, + onError (error) { + throw error + }, + ..._.omit(props, 'incomingRes', 'res', 'req'), + } + } + }) + describe('GzipBody', function () { const { GzipBody } = ResponseMiddleware let ctx diff --git a/packages/proxy/test/unit/http/util/service-worker-injector.spec.ts b/packages/proxy/test/unit/http/util/service-worker-injector.spec.ts new file mode 100644 index 000000000000..d1ba8d577bae --- /dev/null +++ b/packages/proxy/test/unit/http/util/service-worker-injector.spec.ts @@ -0,0 +1,23 @@ +import { expect } from 'chai' +import { injectIntoServiceWorker } from '../../../../lib/http/util/service-worker-injector' + +describe('lib/http/util/service-worker-injector', () => { + describe('injectIntoServiceWorker', () => { + it('injects into the service worker', () => { + const actual = injectIntoServiceWorker(Buffer.from('foo')) + + // this regex is used to verify the actual output, + // it verifies the service worker has the injected __cypressInjectIntoServiceWorker + // function followed by the contents of the user's service worker (in this case 'foo'), + // it does not verify the contents of __cypressInjectIntoServiceWorker function + // as it is subject to change and is not relevant to the test + const expected = new RegExp(` + let __cypressIsScriptEvaluated = false; + \\(function __cypressInjectIntoServiceWorker\\(\\) \\{.*\\}\\)\\(\\); + foo; + __cypressIsScriptEvaluated = true;`.replace(/\s/g, '')) + + expect(actual.replace(/\s/g, '')).to.match(expected) + }) + }) +}) diff --git a/packages/proxy/test/unit/http/util/service-worker-manager.spec.ts b/packages/proxy/test/unit/http/util/service-worker-manager.spec.ts index e1167988b8ee..9103141020f5 100644 --- a/packages/proxy/test/unit/http/util/service-worker-manager.spec.ts +++ b/packages/proxy/test/unit/http/util/service-worker-manager.spec.ts @@ -1,621 +1,534 @@ import { expect } from 'chai' -import { ServiceWorkerManager } from '../../../../lib/http/util/service-worker-manager' +import sinon from 'sinon' +import { ServiceWorkerManager, serviceWorkerClientEventHandler } from '../../../../lib/http/util/service-worker-manager' + +const createBrowserPreRequest = ( + { url, initiatorUrl, callFrameUrl, documentUrl, isPreload }: + { url: string, initiatorUrl?: string, documentUrl?: string, callFrameUrl?: string, isPreload?: boolean }, +) => { + return { + requestId: 'id-1', + method: 'GET', + url, + headers: {}, + resourceType: 'fetch' as const, + originalResourceType: undefined, + ...(isPreload + ? { + initiator: { + type: 'preload' as const, + }, + } + : {}), + ...(initiatorUrl + ? { + initiator: { + type: 'script' as const, + url: initiatorUrl, + }, + } + : {}), + ...(callFrameUrl + ? { + initiator: { + type: 'script' as const, + stack: { + callFrames: [{ + url: callFrameUrl, + lineNumber: 1, + columnNumber: 1, + functionName: '', + scriptId: '1', + }], + }, + }, + } + : {}), + documentURL: documentUrl || 'http://localhost:8080/index.html', + cdpRequestWillBeSentTimestamp: 0, + cdpRequestWillBeSentReceivedTimestamp: 0, + } +} describe('lib/http/util/service-worker-manager', () => { - it('will detect when requests are controlled by a service worker', () => { - const manager = new ServiceWorkerManager() - - manager.updateServiceWorkerRegistrations({ - registrations: [{ - registrationId: '1', - scopeURL: 'http://localhost:8080', - isDeleted: false, - }], - }) - - manager.updateServiceWorkerVersions({ - versions: [{ - versionId: '1', - runningStatus: 'running', - registrationId: '1', - scriptURL: 'http://localhost:8080/sw.js', - status: 'activated', - }], - }) - - manager.addInitiatorToServiceWorker({ - scriptURL: 'http://localhost:8080/sw.js', - initiatorOrigin: 'http://localhost:8080/', - }) - - // A script request emanated from the service worker's initiator is controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-1', - method: 'GET', - url: 'http://localhost:8080/foo.js', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - documentURL: 'http://localhost:8080/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.true - - // A script request emanated from the previous script request is controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-2', - method: 'GET', - url: 'http://example.com/bar.css', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - stack: { - callFrames: [{ - url: 'http://localhost:8080/foo.js', - lineNumber: 1, - columnNumber: 1, - functionName: '', - scriptId: '1', + describe('ServiceWorkerManager', () => { + context('processBrowserPreRequest', () => { + let manager: ServiceWorkerManager + + beforeEach(() => { + manager = new ServiceWorkerManager() + + manager.updateServiceWorkerRegistrations({ + registrations: [{ + registrationId: '1', + scopeURL: 'http://localhost:8080', + isDeleted: false, }], - }, - }, - documentURL: 'http://localhost:8080/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.true - - // A script request emanated from the previous css is controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-3', - method: 'GET', - url: 'http://example.com/baz.woff2', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - url: 'http://example.com/bar.css', - }, - documentURL: 'http://localhost:8080/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.true - - // A script request emanated from a different script request is not controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-4', - method: 'GET', - url: 'http://example.com/quux.js', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - stack: { - callFrames: [{ - url: 'http://example.com/bar.js', - lineNumber: 1, - columnNumber: 1, - functionName: '', - scriptId: '1', + }) + + manager.updateServiceWorkerVersions({ + versions: [{ + versionId: '1', + runningStatus: 'running', + registrationId: '1', + scriptURL: 'http://localhost:8080/sw.js', + status: 'activated', }], - }, - }, - documentURL: 'http://localhost:8080/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.false - - // A script request emanated from a different css request is not controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-5', - method: 'GET', - url: 'http://example.com/quux.css', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - url: 'http://example.com/baz.css', - }, - documentURL: 'http://localhost:8080/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.false - - // A script request emanated from a different document is not controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-6', - method: 'GET', - url: 'http://example.com/quux.css', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - url: 'http://example.com/baz.css', - }, - documentURL: 'http://example.com/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.false - - // A preload request is not controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-7', - method: 'GET', - url: 'http://example.com/quux.css', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'preload', - }, - documentURL: 'http://localhost:8080/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.false - }) - - it('will not detect requests when not controlled by an active service worker', () => { - const manager = new ServiceWorkerManager() - - manager.updateServiceWorkerRegistrations({ - registrations: [{ - registrationId: '1', - scopeURL: 'http://localhost:8080', - isDeleted: false, - }], - }) - - manager.updateServiceWorkerVersions({ - versions: [{ - versionId: '1', - runningStatus: 'running', - registrationId: '1', - scriptURL: 'http://localhost:8080/sw.js', - status: 'activating', - }], - }) + }) + + manager.addInitiatorToServiceWorker({ + scriptURL: 'http://localhost:8080/sw.js', + initiatorOrigin: 'http://localhost:8080/', + }) + + manager.handleServiceWorkerClientEvent({ + type: 'hasFetchHandler', + payload: { + hasFetchHandler: true, + }, + }) + }) + + it('will detect when requests are controlled by a service worker', async () => { + // A script request emanated from the service worker's initiator is controlled + let result = manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://localhost:8080/foo.js', + })) + + manager.handleServiceWorkerClientEvent({ + type: 'fetchRequest', + payload: { + url: 'http://localhost:8080/foo.js', + isControlled: true, + }, + }) + + expect(await result).to.be.true + + // A script request emanated from the previous script request is controlled + result = manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/bar.css', + callFrameUrl: 'http://localhost:8080/foo.js', + })) + + manager.handleServiceWorkerClientEvent({ + type: 'fetchRequest', + payload: { + url: 'http://example.com/bar.css', + isControlled: true, + }, + }) + + expect(await result).to.be.true + + // A script request emanated from the previous css is controlled + result = manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/baz.woff2', + initiatorUrl: 'http://example.com/bar.css', + })) + + manager.handleServiceWorkerClientEvent({ + type: 'fetchRequest', + payload: { + url: 'http://example.com/baz.woff2', + isControlled: true, + }, + }) + + expect(await result).to.be.true + + // A script request emanated from a different script request is not controlled + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/quux.js', + callFrameUrl: 'http://example.com/bar.js', + }))).to.be.false + + // A script request emanated from a different css request is not controlled + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/quux.css', + initiatorUrl: 'http://example.com/baz.css', + }))).to.be.false + + // A script request emanated from a different document is not controlled + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/quux.css', + initiatorUrl: 'http://example.com/baz.css', + }))).to.be.false + + // A preload request is not controlled + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/quux.css', + isPreload: true, + }))).to.be.false + + // A request that is not handled by the service worker 'fetch' handler is not controlled (browser pre-request first) + result = manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://localhost:8080/foo.js', + })) + + manager.handleServiceWorkerClientEvent({ + type: 'fetchRequest', + payload: { + url: 'http://localhost:8080/foo.js', + isControlled: false, + }, + }) - manager.addInitiatorToServiceWorker({ - scriptURL: 'http://localhost:8080/sw.js', - initiatorOrigin: 'http://localhost:8080/', - }) + expect(await result).to.be.false - // A script request emanated from the service worker's initiator is controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-1', - method: 'GET', - url: 'http://localhost:8080/foo.js', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - documentURL: 'http://localhost:8080/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.false - - // A script request emanated from the previous script request is controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-2', - method: 'GET', - url: 'http://example.com/bar.css', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - stack: { - callFrames: [{ + // A request that is not handled by the service worker 'fetch' handler is not controlled (fetch event first) + manager.handleServiceWorkerClientEvent({ + type: 'fetchRequest', + payload: { url: 'http://localhost:8080/foo.js', - lineNumber: 1, - columnNumber: 1, - functionName: '', - scriptId: '1', - }], - }, - }, - documentURL: 'http://localhost:8080/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.false - - // A script request emanated from the previous css is controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-3', - method: 'GET', - url: 'http://example.com/baz.woff2', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - url: 'http://example.com/bar.css', - }, - documentURL: 'http://localhost:8080/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.false - - // A script request emanated from a different script request is not controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-4', - method: 'GET', - url: 'http://example.com/quux.js', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - stack: { - callFrames: [{ - url: 'http://example.com/bar.js', - lineNumber: 1, - columnNumber: 1, - functionName: '', - scriptId: '1', + isControlled: false, + }, + }) + + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://localhost:8080/foo.js', + }))).to.be.false + }) + + it('will not detect requests when not controlled by an active service worker', async () => { + // remove the current service worker + manager.updateServiceWorkerRegistrations({ + registrations: [{ + registrationId: '1', + scopeURL: 'http://localhost:8080', + isDeleted: true, }], - }, - }, - documentURL: 'http://localhost:8080/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.false - - // A script request emanated from a different css request is not controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-5', - method: 'GET', - url: 'http://example.com/quux.css', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - url: 'http://example.com/baz.css', - }, - documentURL: 'http://localhost:8080/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.false - - // A script request emanated from a different document is not controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-6', - method: 'GET', - url: 'http://example.com/quux.css', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - url: 'http://example.com/baz.css', - }, - documentURL: 'http://example.com/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.false - - // A preload request is not controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-7', - method: 'GET', - url: 'http://example.com/quux.css', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'preload', - }, - documentURL: 'http://localhost:8080/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.false - }) - - it('will detect when requests are controlled by a service worker and handles query parameters', () => { - const manager = new ServiceWorkerManager() - - manager.updateServiceWorkerRegistrations({ - registrations: [{ - registrationId: '1', - scopeURL: 'http://localhost:8080', - isDeleted: false, - }], - }) - - manager.updateServiceWorkerVersions({ - versions: [{ - versionId: '1', - runningStatus: 'running', - registrationId: '1', - scriptURL: 'http://localhost:8080/sw.js', - status: 'activated', - }], - }) - - manager.addInitiatorToServiceWorker({ - scriptURL: 'http://localhost:8080/sw.js', - initiatorOrigin: 'http://localhost:8080/', - }) - - // A script request emanated from the service worker's initiator is controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-1', - method: 'GET', - url: 'http://localhost:8080/foo.js', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - documentURL: 'http://localhost:8080/index.html?foo=bar', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.true - - // A script request emanated from the previous script request is controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-2', - method: 'GET', - url: 'http://example.com/bar.css', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - stack: { - callFrames: [{ - url: 'http://localhost:8080/foo.js?foo=bar', - lineNumber: 1, - columnNumber: 1, - functionName: '', - scriptId: '1', + }) + + // add a new service worker that is not activated + manager.updateServiceWorkerRegistrations({ + registrations: [{ + registrationId: '2', + scopeURL: 'http://localhost:8080', + isDeleted: false, }], - }, - }, - documentURL: 'http://localhost:8080/index.html?foo=bar', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.true - - // A script request emanated from the previous css is controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-3', - method: 'GET', - url: 'http://example.com/baz.woff2', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - url: 'http://example.com/bar.css?foo=bar', - }, - documentURL: 'http://localhost:8080/index.html?foo=bar', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.true - - // A script request emanated from a different script request is not controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-4', - method: 'GET', - url: 'http://example.com/quux.js', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - stack: { - callFrames: [{ - url: 'http://example.com/bar.js?foo=bar', - lineNumber: 1, - columnNumber: 1, - functionName: '', - scriptId: '1', + }) + + // update the service worker to be in a non-activated state + manager.updateServiceWorkerVersions({ + versions: [{ + versionId: '1', + runningStatus: 'running', + registrationId: '2', + scriptURL: 'http://localhost:8080/sw.js', + status: 'activating', }], - }, - }, - documentURL: 'http://localhost:8080/index.html?foo=bar', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.false - - // A script request emanated from a different css request is not controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-5', - method: 'GET', - url: 'http://example.com/quux.css', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - url: 'http://example.com/baz.css?foo=bar', - }, - documentURL: 'http://localhost:8080/index.html?foo=bar', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.false - - // A script request emanated from a different document is not controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-6', - method: 'GET', - url: 'http://example.com/quux.css', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - url: 'http://example.com/baz.css?foo=bar', - }, - documentURL: 'http://example.com/index.html?foo=bar', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.false - - // A preload request is not controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-7', - method: 'GET', - url: 'http://example.com/quux.css', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'preload', - }, - documentURL: 'http://localhost:8080/index.html?foo=bar', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.false - }) + }) + + manager.addInitiatorToServiceWorker({ + scriptURL: 'http://localhost:8080/sw.js', + initiatorOrigin: 'http://localhost:8080/', + }) + + // A script request emanated from the service worker's initiator is not controlled + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://localhost:8080/foo.js', + }))).to.be.false + + // A script request emanated from the previous script request is not controlled + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/bar.css', + callFrameUrl: 'http://localhost:8080/foo.js', + }))).to.be.false + + // A script request emanated from the previous css is not controlled + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/baz.woff2', + initiatorUrl: 'http://example.com/bar.css', + }))).to.be.false + + // A script request emanated from a different script request is not controlled + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/quux.js', + callFrameUrl: 'http://example.com/bar.js', + }))).to.be.false + + // A script request emanated from a different css request is not controlled + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/quux.css', + initiatorUrl: 'http://example.com/baz.css', + }))).to.be.false + + // A script request emanated from a different document is not controlled + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/quux.css', + initiatorUrl: 'http://example.com/baz.css', + documentUrl: 'http://example.com/index.html', + }))).to.be.false + + // A preload request is not controlled + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/quux.css', + isPreload: true, + }))).to.be.false + }) + + it('will detect when requests are controlled by a service worker and handles query parameters', async () => { + // A script request emanated from the service worker's initiator is controlled + let result = manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://localhost:8080/foo.js', + documentUrl: 'http://localhost:8080/index.html?foo=bar', + })) + + manager.handleServiceWorkerClientEvent({ + type: 'fetchRequest', + payload: { + url: 'http://localhost:8080/foo.js', + isControlled: true, + }, + }) + + expect(await result).to.be.true + + // A script request emanated from the previous script request is controlled + result = manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/bar.css', + callFrameUrl: 'http://localhost:8080/foo.js?foo=bar', + documentUrl: 'http://localhost:8080/index.html?foo=bar', + })) + + manager.handleServiceWorkerClientEvent({ + type: 'fetchRequest', + payload: { + url: 'http://example.com/bar.css', + isControlled: true, + }, + }) + + expect(await result).to.be.true + + // A script request emanated from the previous css is controlled + result = manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/baz.woff2', + initiatorUrl: 'http://example.com/bar.css?foo=bar', + documentUrl: 'http://localhost:8080/index.html?foo=bar', + })) + + manager.handleServiceWorkerClientEvent({ + type: 'fetchRequest', + payload: { + url: 'http://example.com/baz.woff2', + isControlled: true, + }, + }) + + expect(await result).to.be.true + + // A script request emanated from a different script request is not controlled + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/quux.js', + callFrameUrl: 'http://example.com/bar.js?foo=bar', + documentUrl: 'http://localhost:8080/index.html?foo=bar', + }))).to.be.false + + // A script request emanated from a different css request is not controlled + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/quux.css', + initiatorUrl: 'http://example.com/baz.css?foo=bar', + documentUrl: 'http://localhost:8080/index.html?foo=bar', + }))).to.be.false + + // A script request emanated from a different document is not controlled + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/quux.css', + initiatorUrl: 'http://example.com/baz.css?foo=bar', + documentUrl: 'http://localhost:8080/index.html?foo=bar', + }))).to.be.false + + // A preload request is not controlled + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/quux.css', + isPreload: true, + documentUrl: 'http://localhost:8080/index.html?foo=bar', + }))).to.be.false + }) + + it('will detect when requests are controlled by a service worker and handles re-registrations', async () => { + // A script request emanated from the service worker's initiator is controlled + let result = manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://localhost:8080/foo.js', + })) + + manager.handleServiceWorkerClientEvent({ + type: 'fetchRequest', + payload: { + url: 'http://localhost:8080/foo.js', + isControlled: true, + }, + }) - it('will detect when requests are controlled by a service worker and handles re-registrations', () => { - const manager = new ServiceWorkerManager() + expect(await result).to.be.true - manager.updateServiceWorkerRegistrations({ - registrations: [{ - registrationId: '1', - scopeURL: 'http://localhost:8080', - isDeleted: false, - }], - }) + manager.updateServiceWorkerRegistrations({ + registrations: [{ + registrationId: '1', + scopeURL: 'http://localhost:8080', + isDeleted: false, + }], + }) + + // A script request emanated from the previous script request is controlled + result = manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/bar.css', + callFrameUrl: 'http://localhost:8080/foo.js', + })) + + manager.handleServiceWorkerClientEvent({ + type: 'fetchRequest', + payload: { + url: 'http://example.com/bar.css', + isControlled: true, + }, + }) + + expect(await result).to.be.true + }) + + it('will detect when requests are controlled by a service worker and handles unregistrations', async () => { + // A script request emanated from the service worker's initiator is controlled + const result = manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://localhost:8080/foo.js', + })) + + manager.handleServiceWorkerClientEvent({ + type: 'fetchRequest', + payload: { + url: 'http://localhost:8080/foo.js', + isControlled: true, + }, + }) - manager.updateServiceWorkerVersions({ - versions: [{ - versionId: '1', - runningStatus: 'running', - registrationId: '1', - scriptURL: 'http://localhost:8080/sw.js', - status: 'activated', - }], - }) + expect(await result).to.be.true - manager.addInitiatorToServiceWorker({ - scriptURL: 'http://localhost:8080/sw.js', - initiatorOrigin: 'http://localhost:8080/', + manager.updateServiceWorkerRegistrations({ + registrations: [{ + registrationId: '1', + scopeURL: 'http://localhost:8080', + isDeleted: true, + }], + }) + + // A script request emanated from the previous script request is not controlled since the service worker was unregistered + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://example.com/bar.css', + callFrameUrl: 'http://localhost:8080/foo.js', + }))).to.be.false + }) + + it('supports multiple fetch handler calls first', async () => { + manager.handleServiceWorkerClientEvent({ + type: 'fetchRequest', + payload: { + url: 'http://localhost:8080/foo.js', + isControlled: true, + }, + }) + + manager.handleServiceWorkerClientEvent({ + type: 'fetchRequest', + payload: { + url: 'http://localhost:8080/bar.js', + isControlled: false, + }, + }) + + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://localhost:8080/foo.js', + }))).to.be.true + + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://localhost:8080/bar.js', + }))).to.be.false + }) + + it('supports multiple browser pre-request calls first', async () => { + const request1 = manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://localhost:8080/foo.js', + })) + + const request2 = manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://localhost:8080/bar.js', + })) + + manager.handleServiceWorkerClientEvent({ + type: 'fetchRequest', + payload: { + url: 'http://localhost:8080/foo.js', + isControlled: true, + }, + }) + + manager.handleServiceWorkerClientEvent({ + type: 'fetchRequest', + payload: { + url: 'http://localhost:8080/bar.js', + isControlled: false, + }, + }) + + expect(await request1).to.be.true + expect(await request2).to.be.false + }) + + it('supports no client fetch handlers', async () => { + manager.handleServiceWorkerClientEvent({ + type: 'hasFetchHandler', + payload: { + hasFetchHandler: false, + }, + }) + + expect(await manager.processBrowserPreRequest(createBrowserPreRequest({ + url: 'http://localhost:8080/foo.js', + }))).to.be.false + }) }) + }) - // A script request emanated from the service worker's initiator is controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-1', - method: 'GET', - url: 'http://localhost:8080/foo.js', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - documentURL: 'http://localhost:8080/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.true - - manager.updateServiceWorkerRegistrations({ - registrations: [{ - registrationId: '1', - scopeURL: 'http://localhost:8080', - isDeleted: false, - }], - }) + describe('serviceWorkerClientEventHandler', () => { + it('handles the __cypressServiceWorkerClientEvent event', () => { + const handler = sinon.stub() - // A script request emanated from the previous script request is controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-2', - method: 'GET', - url: 'http://example.com/bar.css', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - stack: { - callFrames: [{ + const event = { + name: '__cypressServiceWorkerClientEvent', + payload: JSON.stringify({ + type: 'fetchRequest', + payload: { url: 'http://localhost:8080/foo.js', - lineNumber: 1, - columnNumber: 1, - functionName: '', - scriptId: '1', - }], + respondWithCalled: true, + }, + }), + } + + serviceWorkerClientEventHandler(handler)(event) + + expect(handler).to.have.been.calledWith({ + type: 'fetchRequest', + payload: { + url: 'http://localhost:8080/foo.js', + respondWithCalled: true, }, - }, - documentURL: 'http://localhost:8080/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.true - }) - - it('will detect when requests are controlled by a service worker and handles unregistrations', () => { - const manager = new ServiceWorkerManager() - - manager.updateServiceWorkerRegistrations({ - registrations: [{ - registrationId: '1', - scopeURL: 'http://localhost:8080', - isDeleted: false, - }], + }) }) - manager.updateServiceWorkerVersions({ - versions: [{ - versionId: '1', - runningStatus: 'running', - registrationId: '1', - scriptURL: 'http://localhost:8080/sw.js', - status: 'activated', - }], - }) + it('does not handle other events', () => { + const handler = sinon.stub() - manager.addInitiatorToServiceWorker({ - scriptURL: 'http://localhost:8080/sw.js', - initiatorOrigin: 'http://localhost:8080/', - }) + const event = { + name: 'notServiceWorkerClientEvent', + payload: JSON.stringify({ + url: 'http://localhost:8080/foo.js', + }), + } - // A script request emanated from the service worker's initiator is controlled - expect(manager.processBrowserPreRequest({ - requestId: 'id-1', - method: 'GET', - url: 'http://localhost:8080/foo.js', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - documentURL: 'http://localhost:8080/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.true - - manager.updateServiceWorkerRegistrations({ - registrations: [{ - registrationId: '1', - scopeURL: 'http://localhost:8080', - isDeleted: true, - }], - }) + serviceWorkerClientEventHandler(handler)(event) - // A script request emanated from the previous script request is not controlled since the service worker was unregistered - expect(manager.processBrowserPreRequest({ - requestId: 'id-2', - method: 'GET', - url: 'http://example.com/bar.css', - headers: {}, - resourceType: 'fetch', - originalResourceType: undefined, - initiator: { - type: 'script', - stack: { - callFrames: [{ - url: 'http://localhost:8080/foo.js', - lineNumber: 1, - columnNumber: 1, - functionName: '', - scriptId: '1', - }], - }, - }, - documentURL: 'http://localhost:8080/index.html', - cdpRequestWillBeSentTimestamp: 0, - cdpRequestWillBeSentReceivedTimestamp: 0, - })).to.be.false + expect(handler).not.to.have.been.called + }) }) }) diff --git a/packages/server/lib/automation/automation.ts b/packages/server/lib/automation/automation.ts index 09b95d1fff4c..e047ad195e8e 100644 --- a/packages/server/lib/automation/automation.ts +++ b/packages/server/lib/automation/automation.ts @@ -5,8 +5,9 @@ import { Screenshot } from './screenshot' import type { BrowserPreRequest } from '@packages/proxy' import type { AutomationMiddleware, OnRequestEvent, OnServiceWorkerClientSideRegistrationUpdated, OnServiceWorkerRegistrationUpdated, OnServiceWorkerVersionUpdated } from '@packages/types' import { cookieJar } from '../util/cookies' +import type { ServiceWorkerEventHandler } from '@packages/proxy/lib/http/util/service-worker-manager' -export type OnBrowserPreRequest = (browserPreRequest: BrowserPreRequest) => void +export type OnBrowserPreRequest = (browserPreRequest: BrowserPreRequest) => Promise export type AutomationOptions = { cyNamespace?: string @@ -20,6 +21,7 @@ export type AutomationOptions = { onServiceWorkerRegistrationUpdated?: OnServiceWorkerRegistrationUpdated onServiceWorkerVersionUpdated?: OnServiceWorkerVersionUpdated onServiceWorkerClientSideRegistrationUpdated?: OnServiceWorkerClientSideRegistrationUpdated + onServiceWorkerClientEvent: ServiceWorkerEventHandler } export class Automation { @@ -35,6 +37,7 @@ export class Automation { public onServiceWorkerRegistrationUpdated: OnServiceWorkerRegistrationUpdated | undefined public onServiceWorkerVersionUpdated: OnServiceWorkerVersionUpdated | undefined public onServiceWorkerClientSideRegistrationUpdated: OnServiceWorkerClientSideRegistrationUpdated | undefined + public onServiceWorkerClientEvent: ServiceWorkerEventHandler constructor (options: AutomationOptions) { this.onBrowserPreRequest = options.onBrowserPreRequest @@ -45,6 +48,7 @@ export class Automation { this.onServiceWorkerRegistrationUpdated = options.onServiceWorkerRegistrationUpdated this.onServiceWorkerVersionUpdated = options.onServiceWorkerVersionUpdated this.onServiceWorkerClientSideRegistrationUpdated = options.onServiceWorkerClientSideRegistrationUpdated + this.onServiceWorkerClientEvent = options.onServiceWorkerClientEvent this.requests = {} diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index cfb29addb1ba..96c60234e8fa 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -6,7 +6,9 @@ import type { Protocol } from 'devtools-protocol' import { _connectAsync, _getDelayMsForRetry } from './protocol' import * as errors from '../errors' import { create, CriClient, DEFAULT_NETWORK_ENABLE_OPTIONS } from './cri-client' +import { serviceWorkerClientEventHandler, serviceWorkerClientEventHandlerName } from '@packages/proxy/lib/http/util/service-worker-manager' import type { ProtocolManagerShape } from '@packages/types' +import type { ServiceWorkerEventHandler } from '@packages/proxy/lib/http/util/service-worker-manager' const debug = Debug('cypress:server:browsers:browser-cri-client') @@ -24,6 +26,7 @@ type BrowserCriClientOptions = { onAsynchronousError: Function protocolManager?: ProtocolManagerShape fullyManageTabs?: boolean + onServiceWorkerClientEvent: ServiceWorkerEventHandler } type BrowserCriClientCreateOptions = { @@ -34,6 +37,7 @@ type BrowserCriClientCreateOptions = { onReconnect?: (client: CriClient) => void port: number protocolManager?: ProtocolManagerShape + onServiceWorkerClientEvent: ServiceWorkerEventHandler } interface ManageTabsOptions { @@ -180,6 +184,7 @@ export class BrowserCriClient { private onAsynchronousError: Function private protocolManager?: ProtocolManagerShape private fullyManageTabs?: boolean + onServiceWorkerClientEvent: ServiceWorkerEventHandler currentlyAttachedTarget: CriClient | undefined // whenever we instantiate the instance we're already connected bc // we receive an underlying CRI connection @@ -192,28 +197,31 @@ export class BrowserCriClient { extraTargetClients: Map = new Map() onClose: Function | null = null - private constructor ({ browserClient, versionInfo, host, port, browserName, onAsynchronousError, protocolManager, fullyManageTabs }: BrowserCriClientOptions) { - this.browserClient = browserClient - this.versionInfo = versionInfo - this.host = host - this.port = port - this.browserName = browserName - this.onAsynchronousError = onAsynchronousError - this.protocolManager = protocolManager - this.fullyManageTabs = fullyManageTabs + private constructor (options: BrowserCriClientOptions) { + this.browserClient = options.browserClient + this.versionInfo = options.versionInfo + this.host = options.host + this.port = options.port + this.browserName = options.browserName + this.onAsynchronousError = options.onAsynchronousError + this.protocolManager = options.protocolManager + this.fullyManageTabs = options.fullyManageTabs + this.onServiceWorkerClientEvent = options.onServiceWorkerClientEvent } /** * Factory method for the browser cri client. Connects to the browser and then returns a chrome remote interface wrapper around the * browser target * - * @param browserName the display name of the browser being launched - * @param fullyManageTabs whether or not to fully manage tabs. This is useful for firefox where some work is done with marionette and some with CDP. We don't want to handle disconnections in this class in those scenarios - * @param hosts the hosts to which to attempt to connect - * @param onAsynchronousError callback for any cdp fatal errors - * @param onReconnect callback for when the browser cri client reconnects to the browser - * @param port the port to which to connect - * @param protocolManager the protocol manager to use with the browser cri client + * @param {BrowserCriClientCreateOptions} options the options for creating the browser cri client + * @param options.browserName the display name of the browser being launched + * @param options.fullyManageTabs whether or not to fully manage tabs. This is useful for firefox where some work is done with marionette and some with CDP. We don't want to handle disconnections in this class in those scenarios + * @param options.hosts the hosts to which to attempt to connect + * @param options.onAsynchronousError callback for any cdp fatal errors + * @param options.onReconnect callback for when the browser cri client reconnects to the browser + * @param options.port the port to which to connect + * @param options.protocolManager the protocol manager to use with the browser cri client + * @param options.onServiceWorkerClientEvent callback for when a service worker fetch event is received * @returns a wrapper around the chrome remote interface that is connected to the browser target */ static async create (options: BrowserCriClientCreateOptions): Promise { @@ -225,6 +233,7 @@ export class BrowserCriClient { onReconnect, port, protocolManager, + onServiceWorkerClientEvent, } = options const host = await ensureLiveBrowser(hosts, port, browserName) @@ -240,7 +249,17 @@ export class BrowserCriClient { fullyManageTabs, }) - const browserCriClient = new BrowserCriClient({ browserClient, versionInfo, host, port, browserName, onAsynchronousError, protocolManager, fullyManageTabs }) + const browserCriClient = new BrowserCriClient({ + browserClient, + versionInfo, + host, + port, + browserName, + onAsynchronousError, + protocolManager, + fullyManageTabs, + onServiceWorkerClientEvent, + }) if (fullyManageTabs) { await this._manageTabs({ browserClient, browserCriClient, browserName, host, onAsynchronousError, port, protocolManager }) @@ -250,7 +269,8 @@ export class BrowserCriClient { }, browserName, port) } - static async _manageTabs ({ browserClient, browserCriClient, browserName, host, onAsynchronousError, port, protocolManager }: ManageTabsOptions) { + static async _manageTabs (options: ManageTabsOptions) { + const { browserClient, browserCriClient, browserName, host, onAsynchronousError, port, protocolManager } = options const promises = [ browserClient.send('Target.setDiscoverTargets', { discover: true }), browserClient.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }), @@ -270,7 +290,7 @@ export class BrowserCriClient { // We will still auto attach in this case, but we need to runIfWaitingForDebugger to get the page back to a running state await browserClient.send('Runtime.runIfWaitingForDebugger', undefined, sessionId) } catch (error) { - // it's possible that the target was closed before we can run. If so, just ignore + // it's possible that the target was closed before we can run. If so, just ignore debug('error running Runtime.runIfWaitingForDebugger:', error) } }) @@ -299,6 +319,16 @@ export class BrowserCriClient { debug('error running Network.enable:', error) } + try { + // attach a binding to the runtime so that we can listen for service worker events + if (event.targetInfo.type === 'service_worker') { + browserClient.on(`Runtime.bindingCalled.${event.sessionId}` as 'Runtime.bindingCalled', serviceWorkerClientEventHandler(browserCriClient.onServiceWorkerClientEvent)) + await browserClient.send('Runtime.addBinding', { name: serviceWorkerClientEventHandlerName }, event.sessionId) + } + } catch (error) { + debug('error adding service worker binding:', error) + } + if (!waitingForDebugger) { debug('Not waiting for debugger (id: %s)', targetId) @@ -513,7 +543,15 @@ export class BrowserCriClient { throw new Error(`Could not find url target in browser ${url}. Targets were ${JSON.stringify(targets)}`) } - this.currentlyAttachedTarget = await create({ target: target.targetId, onAsynchronousError: this.onAsynchronousError, host: this.host, port: this.port, protocolManager: this.protocolManager, fullyManageTabs: this.fullyManageTabs, browserClient: this.browserClient }) + this.currentlyAttachedTarget = await create({ + target: target.targetId, + onAsynchronousError: this.onAsynchronousError, + host: this.host, + port: this.port, + protocolManager: this.protocolManager, + fullyManageTabs: this.fullyManageTabs, + browserClient: this.browserClient, + }) await this.protocolManager?.connectToBrowser(this.currentlyAttachedTarget) diff --git a/packages/server/lib/browsers/cdp_automation.ts b/packages/server/lib/browsers/cdp_automation.ts index 9f0c6d6827e2..cc13967646cf 100644 --- a/packages/server/lib/browsers/cdp_automation.ts +++ b/packages/server/lib/browsers/cdp_automation.ts @@ -205,7 +205,7 @@ export class CdpAutomation implements CDPClient { return cdpAutomation } - private onNetworkRequestWillBeSent = (params: Protocol.Network.RequestWillBeSentEvent) => { + private onNetworkRequestWillBeSent = async (params: Protocol.Network.RequestWillBeSentEvent) => { debugVerbose('received networkRequestWillBeSent %o', params) let url = params.request.url @@ -239,7 +239,7 @@ export class CdpAutomation implements CDPClient { cdpRequestWillBeSentReceivedTimestamp: performance.now() + performance.timeOrigin, } - this.automation.onBrowserPreRequest?.(browserPreRequest) + await this.automation.onBrowserPreRequest?.(browserPreRequest) } private onRequestServedFromCache = (params: Protocol.Network.RequestServedFromCacheEvent) => { @@ -492,7 +492,7 @@ export class CdpAutomation implements CDPClient { case 'is:automation:client:connected': return true case 'remote:debugger:protocol': - return this.sendDebuggerCommandFn(data.command, data.params) + return this.sendDebuggerCommandFn(data.command, data.params, data.sessionId) case 'take:screenshot': return this.sendDebuggerCommandFn('Page.captureScreenshot', { format: 'png' }) .catch((err) => { diff --git a/packages/server/lib/browsers/chrome.ts b/packages/server/lib/browsers/chrome.ts index 5cce2e5cacec..3cc6421ba09b 100644 --- a/packages/server/lib/browsers/chrome.ts +++ b/packages/server/lib/browsers/chrome.ts @@ -484,7 +484,15 @@ export = { debug('connecting to existing chrome instance with url and debugging port', { url: options.url, port }) if (!options.onError) throw new Error('Missing onError in connectToExisting') - const browserCriClient = await BrowserCriClient.create({ hosts: ['127.0.0.1'], port, browserName: browser.displayName, onAsynchronousError: options.onError, onReconnect, fullyManageTabs: false }) + const browserCriClient = await BrowserCriClient.create({ + hosts: ['127.0.0.1'], + port, + browserName: browser.displayName, + onAsynchronousError: options.onError, + onReconnect, + fullyManageTabs: false, + onServiceWorkerClientEvent: automation.onServiceWorkerClientEvent, + }) if (!options.url) throw new Error('Missing url in connectToExisting') @@ -620,6 +628,7 @@ export = { onReconnect, protocolManager: options.protocolManager, fullyManageTabs: true, + onServiceWorkerClientEvent: automation.onServiceWorkerClientEvent, }) la(browserCriClient, 'expected Chrome remote interface reference', browserCriClient) diff --git a/packages/server/lib/browsers/electron.ts b/packages/server/lib/browsers/electron.ts index 0483c0c7e768..891683ac1408 100644 --- a/packages/server/lib/browsers/electron.ts +++ b/packages/server/lib/browsers/electron.ts @@ -54,7 +54,15 @@ const _getAutomation = async function (win, options: BrowserLaunchOpts, parent) const port = getRemoteDebuggingPort() if (!browserCriClient) { - browserCriClient = await BrowserCriClient.create({ hosts: ['127.0.0.1'], port, browserName: 'electron', onAsynchronousError: options.onError, onReconnect: () => {}, fullyManageTabs: true }) + browserCriClient = await BrowserCriClient.create({ + hosts: ['127.0.0.1'], + port, + browserName: 'electron', + onAsynchronousError: options.onError, + onReconnect: () => {}, + fullyManageTabs: true, + onServiceWorkerClientEvent: parent.onServiceWorkerClientEvent, + }) } const pageCriClient = await browserCriClient.attachToTargetUrl('about:blank') diff --git a/packages/server/lib/browsers/firefox-util.ts b/packages/server/lib/browsers/firefox-util.ts index 72b9c1e545b3..6661e81b8936 100644 --- a/packages/server/lib/browsers/firefox-util.ts +++ b/packages/server/lib/browsers/firefox-util.ts @@ -138,7 +138,7 @@ async function connectToNewSpec (options, automation: Automation, browserCriClie } async function setupRemote (remotePort, automation, onError): Promise { - const browserCriClient = await BrowserCriClient.create({ hosts: ['127.0.0.1', '::1'], port: remotePort, browserName: 'Firefox', onAsynchronousError: onError }) + const browserCriClient = await BrowserCriClient.create({ hosts: ['127.0.0.1', '::1'], port: remotePort, browserName: 'Firefox', onAsynchronousError: onError, onServiceWorkerClientEvent: automation.onServiceWorkerClientEvent }) const pageCriClient = await browserCriClient.attachToTargetUrl('about:blank') await CdpAutomation.create(pageCriClient.send, pageCriClient.on, pageCriClient.off, browserCriClient.resetBrowserTargets, automation) diff --git a/packages/server/lib/project-base.ts b/packages/server/lib/project-base.ts index f3fe8fa11189..fae0e0a1a07b 100644 --- a/packages/server/lib/project-base.ts +++ b/packages/server/lib/project-base.ts @@ -24,6 +24,7 @@ import { createHmac } from 'crypto' import type ProtocolManager from './cloud/protocol' import { ServerBase } from './server-base' import type Protocol from 'devtools-protocol' +import type { ServiceWorkerClientEvent } from '@packages/proxy/lib/http/util/service-worker-manager' export interface Cfg extends ReceivedCypressOptions { projectId?: string @@ -324,8 +325,8 @@ export class ProjectBase extends EE { projectRoot, }) - const onBrowserPreRequest = (browserPreRequest) => { - this.server.addBrowserPreRequest(browserPreRequest) + const onBrowserPreRequest = async (browserPreRequest) => { + await this.server.addBrowserPreRequest(browserPreRequest) } const onRequestEvent = (eventName, data) => { @@ -356,6 +357,10 @@ export class ProjectBase extends EE { this.server.updateServiceWorkerClientSideRegistrations(data) } + const onServiceWorkerClientEvent = (event: ServiceWorkerClientEvent) => { + this.server.handleServiceWorkerClientEvent(event) + } + this._automation = new Automation({ cyNamespace: namespace, cookieNamespace: socketIoCookie, @@ -368,6 +373,7 @@ export class ProjectBase extends EE { onServiceWorkerRegistrationUpdated, onServiceWorkerVersionUpdated, onServiceWorkerClientSideRegistrationUpdated, + onServiceWorkerClientEvent, }) const ios = this.server.startWebsockets(this.automation, this.cfg, { diff --git a/packages/server/lib/server-base.ts b/packages/server/lib/server-base.ts index 1137a8135e92..34840ee1961a 100644 --- a/packages/server/lib/server-base.ts +++ b/packages/server/lib/server-base.ts @@ -41,6 +41,7 @@ import headersUtil from './util/headers' import stream from 'stream' import isHtml from 'is-html' import type Protocol from 'devtools-protocol' +import type { ServiceWorkerClientEvent } from '@packages/proxy/lib/http/util/service-worker-manager' const debug = Debug('cypress:server:server-base') @@ -492,8 +493,8 @@ export class ServerBase { }) } - addBrowserPreRequest (browserPreRequest: BrowserPreRequest) { - this.networkProxy.addPendingBrowserPreRequest(browserPreRequest) + async addBrowserPreRequest (browserPreRequest: BrowserPreRequest) { + await this.networkProxy.addPendingBrowserPreRequest(browserPreRequest) } removeBrowserPreRequest (requestId: string) { @@ -524,6 +525,10 @@ export class ServerBase { this.networkProxy.updateServiceWorkerClientSideRegistrations(data) } + handleServiceWorkerClientEvent (event: ServiceWorkerClientEvent) { + this.networkProxy.handleServiceWorkerClientEvent(event) + } + _createHttpServer (app): DestroyableHttpServer { const svr = http.createServer(httpUtils.lenientOptions, app) diff --git a/packages/server/test/unit/browsers/browser-cri-client_spec.ts b/packages/server/test/unit/browsers/browser-cri-client_spec.ts index 8dec67da1123..854ca3c9704b 100644 --- a/packages/server/test/unit/browsers/browser-cri-client_spec.ts +++ b/packages/server/test/unit/browsers/browser-cri-client_spec.ts @@ -6,6 +6,7 @@ import { stripAnsi } from '@packages/errors' import net from 'net' import { ProtocolManagerShape } from '@packages/types' import type { Protocol } from 'devtools-protocol' +import { serviceWorkerClientEventHandlerName } from '@packages/proxy/lib/http/util/service-worker-manager' const HOST = '127.0.0.1' const PORT = 50505 @@ -16,7 +17,7 @@ type GetClientParams = { fullyManageTabs?: boolean } -describe('lib/browsers/cri-client', function () { +describe('lib/browsers/browser-cri-client', function () { let browserCriClient: { BrowserCriClient: { create: typeof BrowserCriClient.create @@ -30,6 +31,7 @@ describe('lib/browsers/cri-client', function () { Version: sinon.SinonStub } let onError: sinon.SinonStub + let onServiceWorkerClientEvent: sinon.SinonStub let getClient: (options?: GetClientParams) => ReturnType beforeEach(function () { @@ -64,7 +66,7 @@ describe('lib/browsers/cri-client', function () { close, }) - return browserCriClient.BrowserCriClient.create({ hosts: ['127.0.0.1'], port: PORT, browserName: 'Chrome', onAsynchronousError: onError, protocolManager, fullyManageTabs }) + return browserCriClient.BrowserCriClient.create({ hosts: ['127.0.0.1'], port: PORT, browserName: 'Chrome', onAsynchronousError: onError, protocolManager, fullyManageTabs, onServiceWorkerClientEvent }) } }) @@ -101,7 +103,7 @@ describe('lib/browsers/cri-client', function () { criImport.Version.withArgs({ host: '::1', port: THROWS_PORT, useHostName: true }).resolves({ webSocketDebuggerUrl: 'http://web/socket/url' }) - await browserCriClient.BrowserCriClient.create({ hosts: ['127.0.0.1', '::1'], port: THROWS_PORT, browserName: 'Chrome', onAsynchronousError: onError }) + await browserCriClient.BrowserCriClient.create({ hosts: ['127.0.0.1', '::1'], port: THROWS_PORT, browserName: 'Chrome', onAsynchronousError: onError, onServiceWorkerClientEvent }) expect(criImport.Version).to.be.calledOnce }) @@ -112,7 +114,7 @@ describe('lib/browsers/cri-client', function () { .onSecondCall().returns(100) .onThirdCall().returns(100) - const client = await browserCriClient.BrowserCriClient.create({ hosts: ['127.0.0.1'], port: THROWS_PORT, browserName: 'Chrome', onAsynchronousError: onError }) + const client = await browserCriClient.BrowserCriClient.create({ hosts: ['127.0.0.1'], port: THROWS_PORT, browserName: 'Chrome', onAsynchronousError: onError, onServiceWorkerClientEvent }) expect(client.attachToTargetUrl).to.be.instanceOf(Function) @@ -124,7 +126,7 @@ describe('lib/browsers/cri-client', function () { .onFirstCall().returns(100) .onSecondCall().returns(undefined) - await expect(browserCriClient.BrowserCriClient.create({ hosts: ['127.0.0.1'], port: THROWS_PORT, browserName: 'Chrome', onAsynchronousError: onError })).to.be.rejected + await expect(browserCriClient.BrowserCriClient.create({ hosts: ['127.0.0.1'], port: THROWS_PORT, browserName: 'Chrome', onAsynchronousError: onError, onServiceWorkerClientEvent })).to.be.rejected expect(criImport.Version).to.be.calledTwice }) @@ -137,6 +139,7 @@ describe('lib/browsers/cri-client', function () { options = { browserClient: { send: sinon.stub(), + on: sinon.stub(), }, browserCriClient: { addExtraTargetClient: sinon.stub(), @@ -275,6 +278,24 @@ describe('lib/browsers/cri-client', function () { expect(options.browserClient.send).to.be.calledWith('Runtime.runIfWaitingForDebugger', undefined, 'session-id') }) + it('adds the service worker fetch event binding', async () => { + options.event.targetInfo.type = 'service_worker' + + await BrowserCriClient._onAttachToTarget(options as any) + + expect(options.browserClient.on).to.be.calledWith('Runtime.bindingCalled.session-id', sinon.match.func) + expect(options.browserClient.send).to.be.calledWith('Runtime.addBinding', { name: serviceWorkerClientEventHandlerName }, options.event.sessionId) + }) + + it('does not add the service worker fetch event binding for non-service_worker targets', async () => { + options.event.targetInfo.type = 'other' + + await BrowserCriClient._onAttachToTarget(options as any) + + expect(options.browserClient.on).not.to.be.calledWith('Runtime.bindingCalled.session-id', sinon.match.func) + expect(options.browserClient.send).not.to.be.calledWith('Runtime.addBinding', { name: serviceWorkerClientEventHandlerName }, options.event.sessionId) + }) + it('adds X-Cypress-Is-From-Extra-Target header to requests from extra target', async () => { const criClient = { send: sinon.stub(), diff --git a/packages/server/test/unit/browsers/firefox_spec.ts b/packages/server/test/unit/browsers/firefox_spec.ts index 6827d10f0fca..1f29702ab50c 100644 --- a/packages/server/test/unit/browsers/firefox_spec.ts +++ b/packages/server/test/unit/browsers/firefox_spec.ts @@ -573,6 +573,10 @@ describe('lib/browsers/firefox', () => { connected: false, } + const automationStub = { + onServiceWorkerClientEvent: sinon.stub(), + } + const browserCriClient: BrowserCriClient = sinon.createStubInstance(BrowserCriClient) browserCriClient.attachToTargetUrl = sinon.stub().resolves(criClientStub) @@ -580,17 +584,17 @@ describe('lib/browsers/firefox', () => { sinon.stub(BrowserCriClient, 'create').resolves(browserCriClient) sinon.stub(CdpAutomation, 'create').resolves() - const actual = await firefoxUtil.setupRemote(port, null, null) + const actual = await firefoxUtil.setupRemote(port, automationStub, null) expect(actual).to.equal(browserCriClient) expect(browserCriClient.attachToTargetUrl).to.be.calledWith('about:blank') - expect(BrowserCriClient.create).to.be.calledWith({ hosts: ['127.0.0.1', '::1'], port, browserName: 'Firefox', onAsynchronousError: null }) + expect(BrowserCriClient.create).to.be.calledWith({ hosts: ['127.0.0.1', '::1'], port, browserName: 'Firefox', onAsynchronousError: null, onServiceWorkerClientEvent: automationStub.onServiceWorkerClientEvent }) expect(CdpAutomation.create).to.be.calledWith( criClientStub.send, criClientStub.on, criClientStub.off, browserCriClient.resetBrowserTargets, - null, + automationStub, ) }) })