Skip to content

Commit

Permalink
chore(webkit): add before:browser:launch and download support (#23662)
Browse files Browse the repository at this point in the history
  • Loading branch information
flotwig authored Sep 6, 2022
1 parent dc9e9dc commit b5ba6d7
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 31 deletions.
6 changes: 3 additions & 3 deletions packages/server/lib/browsers/chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ const _navigateUsingCRI = async function (client, url) {
await client.send('Page.navigate', { url })
}

const _handleDownloads = async function (client, dir, automation) {
const _handleDownloads = async function (client, downloadsFolder: string, automation) {
client.on('Page.downloadWillBegin', (data) => {
const downloadItem = {
id: data.guid,
Expand All @@ -282,7 +282,7 @@ const _handleDownloads = async function (client, dir, automation) {

if (filename) {
// @ts-ignore
downloadItem.filePath = path.join(dir, data.suggestedFilename)
downloadItem.filePath = path.join(downloadsFolder, data.suggestedFilename)
// @ts-ignore
downloadItem.mime = mime.getType(data.suggestedFilename)
}
Expand All @@ -300,7 +300,7 @@ const _handleDownloads = async function (client, dir, automation) {

await client.send('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: dir,
downloadPath: downloadsFolder,
})
}

Expand Down
53 changes: 41 additions & 12 deletions packages/server/lib/browsers/webkit-automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { Automation } from '../automation'
import { normalizeResourceType } from './cdp_automation'
import os from 'os'
import type { RunModeVideoApi } from '@packages/types'
import path from 'path'
import mime from 'mime'

const debug = Debug('cypress:server:browsers:webkit-automation')

Expand Down Expand Up @@ -84,6 +86,7 @@ const _cookieMatches = (cookie: any, filter: Record<string, any>) => {

let requestIdCounter = 1
const requestIdMap = new WeakMap<playwright.Request, string>()
let downloadIdCounter = 1

export class WebKitAutomation {
private context!: playwright.BrowserContext
Expand All @@ -92,20 +95,20 @@ export class WebKitAutomation {
private constructor (public automation: Automation, private browser: playwright.Browser) {}

// static initializer to avoid "not definitively declared"
static async create (automation: Automation, browser: playwright.Browser, initialUrl: string, videoApi?: RunModeVideoApi) {
static async create (automation: Automation, browser: playwright.Browser, initialUrl: string, downloadsFolder: string, videoApi?: RunModeVideoApi) {
const wkAutomation = new WebKitAutomation(automation, browser)

await wkAutomation.reset(initialUrl, videoApi)
await wkAutomation.reset({ downloadsFolder, newUrl: initialUrl, videoApi })

return wkAutomation
}

public async reset (newUrl?: string, videoApi?: RunModeVideoApi) {
debug('resetting playwright page + context %o', { newUrl })
public async reset (options: { downloadsFolder?: string, newUrl?: string, videoApi?: RunModeVideoApi }) {
debug('resetting playwright page + context %o', options)
// new context comes with new cache + storage
const newContext = await this.browser.newContext({
ignoreHTTPSErrors: true,
recordVideo: videoApi && {
recordVideo: options.videoApi && {
dir: os.tmpdir(),
size: { width: 1280, height: 720 },
},
Expand All @@ -116,14 +119,17 @@ export class WebKitAutomation {
this.page = await newContext.newPage()
this.context = this.page.context()

this.attachListeners(this.page)
if (videoApi) this.recordVideo(videoApi, contextStarted)
this.handleRequestEvents()

if (options.downloadsFolder) this.handleDownloadEvents(options.downloadsFolder)

if (options.videoApi) this.recordVideo(options.videoApi, contextStarted)

let promises: Promise<any>[] = []

if (oldPwPage) promises.push(oldPwPage.context().close())

if (newUrl) promises.push(this.page.goto(newUrl))
if (options.newUrl) promises.push(this.page.goto(options.newUrl))

if (promises.length) await Promise.all(promises)
}
Expand Down Expand Up @@ -162,9 +168,30 @@ export class WebKitAutomation {
})
}

private attachListeners (page: playwright.Page) {
private handleDownloadEvents (downloadsFolder: string) {
this.page.on('download', async (download) => {
const id = downloadIdCounter++
const suggestedFilename = download.suggestedFilename()
const filePath = path.join(downloadsFolder, suggestedFilename)

this.automation.push('create:download', {
id,
url: download.url(),
filePath,
mime: mime.getType(suggestedFilename),
})

// NOTE: WebKit does have a `downloadsPath` option, but it is trashed after each run
// Cypress trashes before runs - so we have to use `.saveAs` to move it
await download.saveAs(filePath)

this.automation.push('complete:download', { id })
})
}

private handleRequestEvents () {
// emit preRequest to proxy
page.on('request', (request) => {
this.page.on('request', (request) => {
// ignore socket.io events
// TODO: use config.socketIoRoute here instead
if (request.url().includes('/__socket') || request.url().includes('/__cypress')) return
Expand All @@ -188,7 +215,7 @@ export class WebKitAutomation {
this.automation.onBrowserPreRequest?.(browserPreRequest)
})

page.on('requestfinished', async (request) => {
this.page.on('requestfinished', async (request) => {
const requestId = requestIdMap.get(request)

if (!requestId) return
Expand Down Expand Up @@ -274,9 +301,11 @@ export class WebKitAutomation {
case 'focus:browser:window':
return await this.context.pages[0]?.bringToFront()
case 'reset:browser:state':
debug('stubbed reset:browser:state')

return
case 'reset:browser:tabs:for:next:test':
if (data.shouldKeepTabOpen) return await this.reset()
if (data.shouldKeepTabOpen) return await this.reset({})

return await this.context.browser()?.close()
default:
Expand Down
52 changes: 41 additions & 11 deletions packages/server/lib/browsers/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Browser, BrowserInstance } from './types'
import type { Automation } from '../automation'
import { WebKitAutomation } from './webkit-automation'
import type { BrowserLaunchOpts, BrowserNewTabOpts } from '@packages/types'
import utils from './utils'

const debug = Debug('cypress:server:browsers:webkit')

Expand All @@ -16,7 +17,11 @@ export async function connectToNewSpec (browser: Browser, options: BrowserNewTab
automation.use(wkAutomation)
wkAutomation.automation = automation
await options.onInitializeNewBrowserTab()
await wkAutomation.reset(options.url, options.videoApi)
await wkAutomation.reset({
newUrl: options.url,
downloadsFolder: options.downloadsFolder,
videoApi: options.videoApi,
})
}

export function connectToExisting () {
Expand All @@ -26,22 +31,47 @@ export function connectToExisting () {
export async function open (browser: Browser, url: string, options: BrowserLaunchOpts, automation: Automation): Promise<BrowserInstance> {
// resolve pw from user's project path
const pwModulePath = require.resolve('playwright-webkit', { paths: [process.cwd()] })
const pw = require(pwModulePath) as typeof playwright
const pw = await import(pwModulePath) as typeof playwright

const pwBrowser = await pw.webkit.launch({
proxy: {
server: options.proxyServer,
const defaultLaunchOptions = {
preferences: {
proxy: {
server: options.proxyServer,
},
headless: browser.isHeadless,
},
downloadsPath: options.downloadsFolder,
headless: browser.isHeadless,
})
extensions: [],
args: [],
}

const launchOptions = await utils.executeBeforeBrowserLaunch(browser, defaultLaunchOptions, options)

if (launchOptions.extensions.length) options.onWarning?.(new Error('WebExtensions not supported in WebKit, but extensions were passed in before:browser:launch.'))

launchOptions.preferences.args = [...launchOptions.args, ...(launchOptions.preferences.args || [])]

const pwServer = await pw.webkit.launchServer(launchOptions.preferences)

/**
* Playwright adds an `exit` event listener to run a cleanup process. It tries to use the current binary to run a Node script by passing it as argv[1].
* However, the Electron binary does not support an entrypoint, leading Cypress to think it's being opened in global mode (no args) when this fn is called.
* Solution is to filter out the problematic function.
* TODO(webkit): do we want to run this cleanup script another way?
* @see https://github.com/microsoft/playwright/blob/7e2aec7454f596af452b51a2866e86370291ac8b/packages/playwright-core/src/utils/processLauncher.ts#L191-L203
*/
const killProcessAndCleanup = process.rawListeners('exit').find((fn) => fn.name === 'killProcessAndCleanup')

// @ts-expect-error Electron's Process types override those of @types/node, leading to `exit` not being recognized as an event
if (killProcessAndCleanup) process.removeListener('exit', killProcessAndCleanup)
else debug('did not find killProcessAndCleanup, which may cause interactive mode to unexpectedly open')

const pwBrowser = await pw.webkit.connect(pwServer.wsEndpoint())

wkAutomation = await WebKitAutomation.create(automation, pwBrowser, url, options.videoApi)
wkAutomation = await WebKitAutomation.create(automation, pwBrowser, url, options.downloadsFolder, options.videoApi)
automation.use(wkAutomation)

class WkInstance extends EventEmitter implements BrowserInstance {
// TODO: how to obtain launched process PID from PW? this is used for process_profiler
pid = NaN
pid = pwServer.process().pid

constructor () {
super()
Expand Down
3 changes: 0 additions & 3 deletions packages/server/lib/cypress.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,6 @@ module.exports = {
case 'interactive':
return this.runElectron(mode, options)

case 'openProject':
throw new Error('Unused')

default:
throw new Error(`Cannot start. Invalid mode: '${mode}'`)
}
Expand Down
1 change: 1 addition & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
"@types/chrome": "0.0.101",
"@types/chrome-remote-interface": "0.31.4",
"@types/http-proxy": "1.17.4",
"@types/mime": "3.0.1",
"@types/node": "14.14.31",
"babel-loader": "8.1.0",
"chai-as-promised": "7.1.1",
Expand Down
4 changes: 2 additions & 2 deletions system-tests/test/deprecated_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ describe('deprecated before:browser:launch args', () => {
})

systemTests.it('using non-deprecated API - no warning', {
browser: '!webkit', // TODO(webkit): fix+unskip (add executeBeforeBrowserLaunch to WebKit)
// TODO: implement webPreferences.additionalArgs here
// once we decide if/what we're going to make the implemenation
// SUGGESTION: add this to Cypress.browser.args which will capture
// whatever args we use to launch the browser
browser: '!webkit', // throws in WebKit since it rejects unsupported arguments
config: {
video: false,
env: {
Expand All @@ -64,11 +64,11 @@ describe('deprecated before:browser:launch args', () => {
})

systemTests.it('concat return returns once', {
browser: '!webkit', // TODO(webkit): fix+unskip (add executeBeforeBrowserLaunch to WebKit)
// TODO: implement webPreferences.additionalArgs here
// once we decide if/what we're going to make the implemenation
// SUGGESTION: add this to Cypress.browser.args which will capture
// whatever args we use to launch the browser
browser: '!webkit', // throws in WebKit since it rejects unsupported arguments
config: {
video: false,
env: {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6648,6 +6648,11 @@
resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.0.tgz#9ca52cda363f699c69466c2a6ccdaad913ea7a73"
integrity sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM=

"@types/mime@3.0.1":
version "3.0.1"
resolved "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==

"@types/mime@^1":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
Expand Down

5 comments on commit b5ba6d7

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on b5ba6d7 Sep 6, 2022

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 platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.7.1/linux-x64/develop-b5ba6d7b87deaa8b5f275b20e38127b740490bfd/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on b5ba6d7 Sep 6, 2022

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 platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.7.1/darwin-arm64/develop-b5ba6d7b87deaa8b5f275b20e38127b740490bfd/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on b5ba6d7 Sep 6, 2022

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 platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.7.1/linux-arm64/develop-b5ba6d7b87deaa8b5f275b20e38127b740490bfd/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on b5ba6d7 Sep 6, 2022

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 platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.7.1/darwin-x64/develop-b5ba6d7b87deaa8b5f275b20e38127b740490bfd/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on b5ba6d7 Sep 6, 2022

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 platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.7.1/win32-x64/develop-b5ba6d7b87deaa8b5f275b20e38127b740490bfd/cypress.tgz

Please sign in to comment.