Skip to content

Commit

Permalink
fix: move main tab activation to puppeteer plugin (#28898)
Browse files Browse the repository at this point in the history
* fix: move main tab activation to puppeteer plugin

* tests for new url functionality in v3 extension

* tests for activateMainTab

* tests

* cleanup

* add troubleshooting to puppeteer plugin readme re: chrome extension

* changelog

* no longer attempts to activate main tab in run mode

* Update npm/puppeteer/README.md

Co-authored-by: Jennifer Shehane <jennifer@cypress.io>

* Update cli/CHANGELOG.md

Co-authored-by: Jennifer Shehane <jennifer@cypress.io>

---------

Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
  • Loading branch information
cacieprins and jennifer-shehane committed Feb 20, 2024
1 parent 1f0a9d5 commit ed2fc13
Show file tree
Hide file tree
Showing 19 changed files with 639 additions and 359 deletions.
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ _Released 2/13/2024 (PENDING)_

**Bugfixes:**

- Fixed tests hanging when the Chrome browser extension is disabled. Fixes [#28392](https://github.com/cypress-io/cypress/issues/28392)
- Fixed an issue which caused the browser to relaunch after closing the browser from the Launchpad. Fixes [#28852](https://github.com/cypress-io/cypress/issues/28852).
- Fixed an issue with the unzip promise never being rejected when an empty error happens. Fixed in [#28850](https://github.com/cypress-io/cypress/pull/28850).
- Fixed a regression introduced in [`13.6.3`](https://docs.cypress.io/guides/references/changelog#13-6-3) where Cypress could crash when processing service worker requests through our proxy. Fixes [#28950](https://github.com/cypress-io/cypress/issues/28950).
Expand Down
9 changes: 9 additions & 0 deletions npm/puppeteer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,15 @@ export default defineConfig({
})
```
## Troubleshooting
### Error: Cannot communicate with the Cypress Chrome extension. Ensure the extension is enabled when using the Puppeteer plugin.
If you receive this error in your command log, the Puppeteer plugin was unable to communicate with the Cypress extension. This extension is necessary in order to re-activate the main Cypress tab after a Puppeteer command, when running in open mode.
* Ensure this extension is enabled in the instance of Chrome that Cypress launches by visiting chrome://extensions/
* Ensure the Cypress extension is allowed by your company's security policy by its extension id, `caljajdfkjjjdehjdoimjkkakekklcck`
## Contributing
Build the TypeScript files:
Expand Down
1 change: 1 addition & 0 deletions npm/puppeteer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"puppeteer-core": "^21.2.1"
},
"devDependencies": {
"@types/node": "^18.17.5",
"chai-as-promised": "^7.1.1",
"chokidar": "^3.5.3",
"express": "4.17.3",
Expand Down
46 changes: 46 additions & 0 deletions npm/puppeteer/src/plugin/activateMainTab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/// <reference lib="browser">
import type { Browser } from 'puppeteer-core'

export const ACTIVATION_TIMEOUT = 2000

const sendActivationMessage = (activationTimeout: number) => {
// don't need to worry about tabs for Cy in Cy tests
if (document.defaultView !== top) {
return
}

let timeout: NodeJS.Timeout
let onMessage: (ev: MessageEvent) => void

// promise must resolve with a value for chai as promised to test resolution
return new Promise<void>((resolve, reject) => {
onMessage = (ev) => {
if (ev.data.message === 'cypress:extension:main:tab:activated') {
window.removeEventListener('message', onMessage)
clearTimeout(timeout)
resolve()
}
}

window.addEventListener('message', onMessage)
window.postMessage({ message: 'cypress:extension:activate:main:tab' })

timeout = setTimeout(() => {
window.removeEventListener('message', onMessage)
reject()
}, activationTimeout)
})
}

export const activateMainTab = async (browser: Browser) => {
// - Only implemented for Chromium right now. Support for Firefox/webkit
// could be added later
// - Electron doesn't have tabs
// - Focus doesn't matter for headless browsers and old headless Chrome
// doesn't run the extension
const [page] = await browser.pages()

if (page) {
return page.evaluate(sendActivationMessage, ACTIVATION_TIMEOUT)
}
}
20 changes: 18 additions & 2 deletions npm/puppeteer/src/plugin/setup.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import isPlainObject from 'lodash/isPlainObject'
import defaultPuppeteer, { Browser, PuppeteerNode } from 'puppeteer-core'
import { pluginError } from './util'
import { activateMainTab } from './activateMainTab'

type MessageHandler = (browser: Browser, ...args: any[]) => any | Promise<any>
export type MessageHandler = (browser: Browser, ...args: any[]) => any | Promise<any>

interface SetupOptions {
onMessage: Record<string, MessageHandler>
Expand Down Expand Up @@ -61,7 +62,7 @@ export function setup (options: SetupOptions) {
let debuggerUrl: string

try {
options.on('after:browser:launch', async (browser, options) => {
options.on('after:browser:launch', (browser: Cypress.Browser, options: Cypress.AfterBrowserLaunchDetails) => {
cypressBrowser = browser
debuggerUrl = options.webSocketDebuggerUrl
})
Expand Down Expand Up @@ -110,6 +111,21 @@ export function setup (options: SetupOptions) {
} catch (err: any) {
error = err
} finally {
// - Only implemented for Chromium right now. Support for Firefox/webkit
// could be added later
// - Electron doesn't have tabs
// - Focus doesn't matter for headless browsers and old headless Chrome
// doesn't run the extension
const isHeadedChromium = cypressBrowser.isHeaded && cypressBrowser.family === 'chromium' && cypressBrowser.name !== 'electron'

if (isHeadedChromium) {
try {
await activateMainTab(browser)
} catch (e) {
return messageHandlerError(pluginError('Cannot communicate with the Cypress Chrome extension. Ensure the extension is enabled when using the Puppeteer plugin.'))
}
}

await browser.disconnect()
}

Expand Down
118 changes: 118 additions & 0 deletions npm/puppeteer/test/unit/activateMainTab.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { expect, use } from 'chai'
import chaiAsPromised from 'chai-as-promised'
import sinon from 'sinon'
import sinonChai from 'sinon-chai'
import type { Browser, Page } from 'puppeteer-core'
import { activateMainTab, ACTIVATION_TIMEOUT } from '../../src/plugin/activateMainTab'

use(chaiAsPromised)
use(sinonChai)

describe('activateMainTab', () => {
let clock: sinon.SinonFakeTimers
let prevWin: Window
let prevDoc: Document
let prevTop: Window & typeof globalThis
let window: Partial<Window>
let mockDocument: Partial<Document> & {
defaultView: Window & typeof globalThis
}
let mockTop: Partial<Window & typeof globalThis>
let mockBrowser: Partial<Browser>
let mockPage: Partial<Page>

beforeEach(() => {
clock = sinon.useFakeTimers()

window = {
addEventListener: sinon.stub(),
removeEventListener: sinon.stub(),

// @ts-ignore sinon gets confused about postMessage type declaration
postMessage: sinon.stub(),
}

mockDocument = {
defaultView: window as Window & typeof globalThis,
}

mockTop = mockDocument.defaultView

// activateMainTab is eval'd in browser context, but the tests exec in a
// node context. We don't necessarily need to do this swap, but it makes the
// tests more portable.
// @ts-ignore
prevWin = global.window
prevDoc = global.document
// @ts-ignore
prevTop = global.top
//@ts-ignore
global.window = window
global.document = mockDocument as Document
//@ts-ignore
global.top = mockTop

mockPage = {
evaluate: sinon.stub().callsFake((fn, ...args) => fn(...args)),
}

mockBrowser = {
pages: sinon.stub(),
}
})

afterEach(() => {
clock.restore()
// @ts-ignore
global.window = prevWin
// @ts-ignore
global.top = prevTop
global.document = prevDoc
})

it('sends a tab activation request to the plugin, and resolves when the ack event is received', async () => {
const pagePromise = Promise.resolve([mockPage])

;(mockBrowser.pages as sinon.SinonStub).returns(pagePromise)
const p = activateMainTab(mockBrowser as Browser)

await pagePromise
// @ts-ignore
window.addEventListener.withArgs('message').yield({ data: { message: 'cypress:extension:main:tab:activated' } })
expect(window.postMessage).to.be.calledWith({ message: 'cypress:extension:activate:main:tab' })

expect(p).to.eventually.be.true
})

it('sends a tab activation request to the plugin, and rejects if it times out', async () => {
const pagePromise = Promise.resolve([mockPage])

;(mockBrowser.pages as sinon.SinonStub).returns(pagePromise)
await pagePromise

const p = activateMainTab(mockBrowser as Browser)

clock.tick(ACTIVATION_TIMEOUT + 1)

expect(p).to.be.rejected
})

describe('when cy in cy', () => {
beforeEach(() => {
mockDocument.defaultView = {} as Window & typeof globalThis
})

it('does not try to send tab activation message', async () => {
const pagePromise = Promise.resolve([mockPage])

;(mockBrowser.pages as sinon.SinonStub).returns(pagePromise)

const p = activateMainTab(mockBrowser as Browser)

await pagePromise
expect(window.postMessage).not.to.be.called
expect(window.addEventListener).not.to.be.called
await p
})
})
})

5 comments on commit ed2fc13

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on ed2fc13 Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.6.5/linux-arm64/develop-ed2fc1394623f08097d180747712c557d867ee86/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on ed2fc13 Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.6.5/linux-x64/develop-ed2fc1394623f08097d180747712c557d867ee86/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on ed2fc13 Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.6.5/darwin-x64/develop-ed2fc1394623f08097d180747712c557d867ee86/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on ed2fc13 Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.6.5/darwin-arm64/develop-ed2fc1394623f08097d180747712c557d867ee86/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on ed2fc13 Feb 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.6.5/win32-x64/develop-ed2fc1394623f08097d180747712c557d867ee86/cypress.tgz

Please sign in to comment.