Merge pull request #32748 from cypress-io/chore/refactor-socket-and-extension

chore: (feature-branch), convert `@packages/socket` and `@packages/extension` into bundle packages and `@packages/extension` to `vitest`
This commit is contained in:
Bill Glesias
2025-10-23 15:33:33 -04:00
committed by GitHub
74 changed files with 1121 additions and 925 deletions

View File

@@ -1785,7 +1785,7 @@ jobs:
source ./scripts/ensure-node.sh
yarn lerna run types
- sanitize-verify-and-store-mocha-results:
expectedResultCount: 4
expectedResultCount: 3
verify-release-readiness:
<<: *defaults

View File

@@ -12,7 +12,11 @@
#### Notes
When migrating some of these projects away from the `ts-node` entry [see `@packages/scaffold-config` example](https://github.com/cypress-io/cypress/blob/v15.2.0/packages/scaffold-config/index.js), it is somewhat difficult to make separate browser/node entries as the v8-snapshot [tsconfig.json](https://github.com/cypress-io/cypress/blob/v15.2.0/tooling/v8-snapshot/tsconfig.json) is using an older style of module resolution where the `exports` key inside a package's `package.json` is not well supported. Because of this, we need to find ways to bundle code that is needed internally in the browser vs in node without them being a part of the same bundle. This is a temporary work around until we are able to get every package being able to build as an ES Module, which as that point we can re assess how the Cypress binary is being built as well as v8-snapshots, and will allow us to reconfigure this packages to export content in a more proper fashion.
When migrating some of these projects away from the `ts-node` entry [see `@packages/scaffold-config` example](https://github.com/cypress-io/cypress/blob/v15.2.0/packages/scaffold-config/index.js), it is somewhat difficult to make separate browser/node entries as the v8-snapshot [tsconfig.json](https://github.com/cypress-io/cypress/blob/v15.2.0/tooling/v8-snapshot/tsconfig.json) is using an older style of module resolution where the `exports` key inside a package's `package.json` is not well supported. Because of this, we need to find ways to bundle code that is needed internally in the browser vs in node without them being a part of the same bundle. This is a temporary work around until we are able to get every package being able to build as an ES Module, which as that point we can re assess how the Cypress binary is being built as well as v8-snapshots, and will allow us to reconfigure this packages to export content in a more proper fashion. We are currently doing something similar in the following packages:
* `@packages/scaffold-config`
* `@packages/socket`
* `@packages/telemetry`
#### Status
@@ -44,7 +48,7 @@ When migrating some of these projects away from the `ts-node` entry [see `@packa
- [x] packages/error ✅ **COMPLETED**
- [x] packages/eslint-config ✅ **COMPLETED**
- [ ] packages/example
- [ ] packages/extension
- [x] packages/extension**COMPLETED**
- [ ] packages/frontend-shared **PARTIAL** - entry point is JS
- [x] packages/electron ✅ **COMPLETED**
- [x] packages/https-proxy - ✅ **COMPLETED**
@@ -63,7 +67,7 @@ When migrating some of these projects away from the `ts-node` entry [see `@packa
- [x] packages/runner ✅ **COMPLETED**
- [x] packages/scaffold-config ✅ **COMPLETED**
- [ ] packages/server **PARTIAL** - many source/test files in JS. highest priority
- [ ] packages/socket **PARTIAL** - entry point is JS. Tests are JS
- [x] packages/socket **COMPLETED**
- [x] packages/stderr-filtering ✅ **COMPLETED**
- [x] packages/telemetry ✅ **COMPLETED**
- [ ] packages/ts **PARTIAL** - ultimate goal is removal and likely not worth the effort to convert
@@ -95,7 +99,7 @@ When migrating some of these projects away from the `ts-node` entry [see `@packa
- [x] packages/driver ✅ **COMPLETED**
- [x] packages/electron ✅ **COMPLETED**
- [x] packages/error ✅ **COMPLETED**
- [ ] packages/extension
- [x] packages/extension**COMPLETED**
- [x] packages/https-proxy ✅ **COMPLETED**
- [x] packages/electron ✅ **COMPLETED**
- [x] packages/icons ✅ **COMPLETED**

View File

@@ -1,6 +1,6 @@
import { AutIframe } from '../../../src/runner/aut-iframe'
import { EventManager } from '../../../src/runner/event-manager'
import type { Socket } from '@packages/socket/lib/browser'
import type { Socket } from '@packages/socket/browser/client'
export const StubWebsocket = new Proxy<Socket>(Object.create(null), {
get: (obj, prop) => {

View File

@@ -1,6 +1,6 @@
/// <reference path="../driver/types/internal-types-lite.d.ts" />
import type { SocketShape } from '@packages/socket/lib/types'
import type { SocketShape } from '@packages/socket/browser/client'
import type MobX from 'mobx'
import type { EventManager } from './src/runner/event-manager'

View File

@@ -7,7 +7,7 @@ import type { LocalBusEmitsMap, LocalBusEventMap, DriverToLocalBus, SocketToDriv
import type { RunState, CachedTestState, AutomationElementId, FileDetails, ReporterStartInfo, ReporterRunState } from '@packages/types'
import { logger } from './logger'
import type { SocketShape } from '@packages/socket/lib/types'
import type { SocketShape } from '@packages/socket/browser/client'
import { automation, useRunnerUiStore, useSpecStore } from '../store'
import { useScreenshotStore } from '../store/screenshot-store'
import { EntrySource, useStudioStore } from '../store/studio-store'

View File

@@ -22,7 +22,7 @@ import { getRunnerElement, empty } from './utils'
import { IframeModel } from './iframe-model'
import { AutIframe } from './aut-iframe'
import { EventManager } from './event-manager'
import { createWebsocket as createWebsocketIo } from '@packages/socket/lib/browser'
import { createWebsocket as createWebsocketIo } from '@packages/socket/browser/client'
import type { AutomationElementId } from '@packages/types'
import { useSnapshotStore } from './snapshot-store'
import { useStudioStore } from '../store/studio-store'

View File

@@ -2,9 +2,8 @@ import util from 'util'
import type { AddressInfo } from 'net'
import type { Server } from 'http'
import type { SocketIONamespace, SocketIOServer } from '@packages/socket'
import type { SocketIONamespace, SocketIOServer, CDPSocketServer } from '@packages/socket'
import type { DataContext } from '..'
import type { CDPSocketServer } from '@packages/socket/lib/cdp-socket'
export class ServersActions {
constructor (private ctx: DataContext) {}

View File

@@ -4,12 +4,11 @@ import type { NexusGenObjects } from '../gen/nxs.gen'
// tslint:disable-next-line no-implicit-dependencies - electron dep needs to be defined
import type { App, BrowserWindow } from 'electron'
import type { ChildProcess } from 'child_process'
import type { SocketIONamespace, SocketIOServer } from '@packages/socket'
import type { SocketIONamespace, SocketIOServer, CDPSocketServer } from '@packages/socket'
import type { Server } from 'http'
import type { ErrorWrapperSource } from '@packages/errors'
import type { EventCollectorSource, GitDataSource } from '../sources'
import { machineId as getMachineId } from 'node-machine-id'
import type { CDPSocketServer } from '@packages/socket/lib/cdp-socket'
export type Maybe<T> = T | null | undefined

2
packages/extension/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
app-dist/
lib-dist/

View File

@@ -6,12 +6,20 @@ This is the WebExtension responsible for automating the browser
### Watching
Kicks off the gulp watcher that rebuilds the app/lib directories on change.
```bash
yarn workspace @packages/extension watch
```
## Building
`@packages/extension` has a few different build processes occurring that are all driven by the [`gulpfile`](./gulpfile.ts).
* `app` - The web extension piece of the code, has two separate bundles:
* `v2`: Version 2 of the web extension which uses [webpack](./webpack.config.mjs) to bundle the `app/v2` directory and output it as `background.js`
* `v3`: Version 3 of the web extension, which doesn't have any external dependencies so we are able to compile down to `ESM to run in the browser natively.
* `lib` - the `@packages/extension` `main` entry that has utility methods on how to find/load the extension. This is transpiled to CommonJS as it is consumed in the Node context.
```bash
yarn workspace @packages/extension build
```

View File

@@ -1,9 +1,9 @@
const get = require('lodash/get')
const once = require('lodash/once')
const Promise = require('bluebird')
const browser = require('webextension-polyfill')
import get from 'lodash/get'
import once from 'lodash/once'
import Bluebird from 'bluebird'
import browser from 'webextension-polyfill'
const client = require('./client')
import { connect as clientConnect } from './client'
const checkIfFirefox = async () => {
if (!browser || !get(browser, 'runtime.getBrowserInfo')) {
@@ -15,9 +15,9 @@ const checkIfFirefox = async () => {
return name === 'Firefox'
}
const connect = function (host, path, extraOpts) {
const connect = function (host: string, path: string, extraOpts?: any) {
const listenToCookieChanges = once(() => {
return browser.cookies.onChanged.addListener((info) => {
return browser.cookies.onChanged.addListener((info: any) => {
if (info.cause !== 'overwrite') {
return ws.emit('automation:push:request', 'change:cookie', info)
}
@@ -25,7 +25,7 @@ const connect = function (host, path, extraOpts) {
})
const listenToDownloads = once(() => {
browser.downloads.onCreated.addListener((downloadItem) => {
browser.downloads.onCreated.addListener((downloadItem: any) => {
ws.emit('automation:push:request', 'create:download', {
id: `${downloadItem.id}`,
filePath: downloadItem.filename,
@@ -34,7 +34,7 @@ const connect = function (host, path, extraOpts) {
})
})
browser.downloads.onChanged.addListener((downloadDelta) => {
browser.downloads.onChanged.addListener((downloadDelta: any) => {
const state = (downloadDelta.state || {}).current
if (state === 'complete') {
@@ -51,7 +51,7 @@ const connect = function (host, path, extraOpts) {
})
})
const fail = (id, err) => {
const fail = (id: number, err: any) => {
return ws.emit('automation:response', id, {
__error: err.message,
__stack: err.stack,
@@ -59,21 +59,22 @@ const connect = function (host, path, extraOpts) {
})
}
const invoke = function (method, id, ...args) {
const respond = (data) => {
const invoke = function (method: string, id: number, ...args: any[]) {
const respond = (data: any) => {
return ws.emit('automation:response', id, { response: data })
}
return Promise.try(() => {
return Bluebird.try(() => {
// @ts-expect-error
return automation[method].apply(automation, args.concat(respond))
}).catch((err) => {
return fail(id, err)
})
}
const ws = client.connect(host, path, extraOpts)
const ws = clientConnect(host, path, extraOpts)
ws.on('automation:request', (id, msg, data) => {
ws.on('automation:request', (id: number, msg: string, data: any) => {
switch (msg) {
case 'reset:browser:state':
return invoke('resetBrowserState', id)
@@ -82,7 +83,7 @@ const connect = function (host, path, extraOpts) {
}
})
ws.on('automation:config', async (config) => {
ws.on('automation:config', async (config: any) => {
const isFirefox = await checkIfFirefox()
listenToCookieChanges()
@@ -99,15 +100,13 @@ const connect = function (host, path, extraOpts) {
return ws
}
const automation = {
export const automation = {
connect,
resetBrowserState (fn) {
resetBrowserState (fn: any) {
// We remove browser data. Firefox goes through this path, while chrome goes through cdp automation
// Note that firefox does not support fileSystems or serverBoundCertificates
// (https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browsingData/DataTypeSet).
return browser.browsingData.remove({}, { cache: true, cookies: true, downloads: true, formData: true, history: true, indexedDB: true, localStorage: true, passwords: true, pluginData: true, serviceWorkers: true }).then(fn)
},
}
module.exports = automation

View File

@@ -1,6 +1,6 @@
import { client } from '@packages/socket/lib/browser'
import { client } from '@packages/socket/browser/client'
export const connect = (host, path, extraOpts = {}) => {
export const connect = (host: string, path: string, extraOpts: any = {}) => {
return client(host, {
path,
transports: ['websocket'],

View File

@@ -1,7 +1,7 @@
const background = require('./background')
import { automation } from './background'
const HOST = 'CHANGE_ME_HOST'
const PATH = 'CHANGE_ME_PATH'
// immediately connect
background.connect(HOST, PATH)
automation.connect(HOST, PATH)

View File

@@ -26,7 +26,7 @@ window.addEventListener('message', ({ data, source }) => {
})
// this listens for messages from the background service worker script
port.onMessage.addListener(({ message }) => {
port.onMessage.addListener(({ message }: { message: string }) => {
// this lets us know the message we sent to the background script to activate
// the main tab was successful, so we in turn send it on to Cypress
// via postMessage

View File

@@ -1,4 +1,4 @@
/* global chrome */
declare let chrome: any
// this background script runs in a service worker. it has access to the
// extension API, but not direct access the web page or anything else
@@ -9,10 +9,9 @@
// go to `chrome://extensions` and hit the reload button under the Cypress
// extension. sometimes that doesn't work and requires re-launching Chrome
// and then reloading the extension via `chrome://extensions`
async function getFromStorage (key) {
async function getFromStorage (key: string) {
return new Promise((resolve) => {
chrome.storage.local.get(key, (storage) => {
chrome.storage.local.get(key, (storage: any) => {
resolve(storage[key])
})
})
@@ -23,7 +22,7 @@ async function activateMainTab () {
const url = await getFromStorage('mostRecentUrl')
const tabs = await chrome.tabs.query({})
const cypressTab = tabs.find((tab) => tab.url.includes(url))
const cypressTab = tabs.find((tab: any) => tab.url.includes(url))
if (!cypressTab) return
@@ -41,8 +40,8 @@ async function activateMainTab () {
// here we connect to the content script, which has access to the web page
// running Cypress, but not the extension API
chrome.runtime.onConnect.addListener((port) => {
port.onMessage.addListener(async ({ message, url }) => {
chrome.runtime.onConnect.addListener((port: any) => {
port.onMessage.addListener(async ({ message, url }: { message: string, url: string }) => {
if (message === 'activate:main:tab') {
await activateMainTab()

View File

@@ -1,87 +1,82 @@
import { promisify } from 'util'
import { exec } from 'child_process'
import gulp from 'gulp'
import { rimraf } from 'rimraf'
import { waitUntilIconsBuilt } from '../../scripts/ensure-icons'
import cp from 'child_process'
import * as path from 'path'
import { getPathToIcon, getPathToLogo } from '@packages/icons'
const nodeWebpack = path.join(__dirname, '..', '..', 'scripts', 'run-webpack.js')
const execAsync = promisify(exec)
async function cypressIcons () {
await waitUntilIconsBuilt()
export async function clean (): Promise<boolean> {
const removedAppDist = await rimraf('app-dist')
const removedLibDist = await rimraf('lib-dist')
return require('@packages/icons')
}
function clean (): Promise<boolean> {
return rimraf('dist')
return removedAppDist && removedLibDist
}
const manifest = (v: 'v2' | 'v3') => {
return () => {
return gulp.src(`app/${v}/manifest.json`)
.pipe(gulp.dest(`dist/${v}`))
.pipe(gulp.dest(`app-dist/${v}`))
}
}
const background = (cb) => {
cp.fork(nodeWebpack, { stdio: 'inherit' }).on('exit', (code) => {
cb(code === 0 ? null : new Error(`Webpack process exited with code ${code}`))
})
const buildAppV2 = async () => {
await execAsync('yarn build:v2')
}
const copyScriptsForV3 = () => {
return gulp.src('app/v3/*.js')
.pipe(gulp.dest('dist/v3'))
const buildAppV3 = async () => {
await execAsync('yarn build:v3')
}
const buildLib = async () => {
await execAsync('yarn build:lib')
}
const html = () => {
return gulp.src('app/**/*.html')
.pipe(gulp.dest('dist/v2'))
.pipe(gulp.dest('dist/v3'))
.pipe(gulp.dest('app-dist/v2'))
.pipe(gulp.dest('app-dist/v3'))
}
const css = () => {
return gulp.src('app/**/*.css')
.pipe(gulp.dest('dist/v2'))
.pipe(gulp.dest('dist/v3'))
.pipe(gulp.dest('app-dist/v2'))
.pipe(gulp.dest('app-dist/v3'))
}
const icons = async () => {
const cyIcons = await cypressIcons()
return gulp.src([
cyIcons.getPathToIcon('icon_16x16.png'),
cyIcons.getPathToIcon('icon_19x19.png'),
cyIcons.getPathToIcon('icon_38x38.png'),
cyIcons.getPathToIcon('icon_48x48.png'),
cyIcons.getPathToIcon('icon_128x128.png'),
getPathToIcon('icon_16x16.png'),
getPathToIcon('icon_19x19.png'),
getPathToIcon('icon_38x38.png'),
getPathToIcon('icon_48x48.png'),
getPathToIcon('icon_128x128.png'),
])
.pipe(gulp.dest('dist/v2/icons'))
.pipe(gulp.dest('dist/v3/icons'))
.pipe(gulp.dest('app-dist/v2/icons'))
.pipe(gulp.dest('app-dist/v3/icons'))
}
const logos = async () => {
const cyIcons = await cypressIcons()
// appease TS
return gulp.src([
cyIcons.getPathToLogo('cypress-bw.png'),
getPathToLogo('cypress-bw.png'),
])
.pipe(gulp.dest('dist/v2/logos'))
.pipe(gulp.dest('dist/v3/logos'))
.pipe(gulp.dest('app-dist/v2/logos'))
.pipe(gulp.dest('app-dist/v3/logos'))
}
const build = gulp.series(
export const build = gulp.series(
clean,
buildAppV2,
buildAppV3,
gulp.parallel(
icons,
logos,
manifest('v2'),
manifest('v3'),
background,
copyScriptsForV3,
html,
css,
buildLib,
),
)
@@ -89,10 +84,4 @@ const watchBuild = () => {
return gulp.watch('app/**/*', build)
}
const watch = gulp.series(build, watchBuild)
module.exports = {
build,
clean,
watch,
}
export const watch = gulp.series(build, watchBuild)

View File

@@ -1,5 +0,0 @@
/// <reference path="../../cli/types/jquery/index.d.ts" />
declare const lib: typeof import('./lib/extension')
export default lib

View File

@@ -1 +0,0 @@
module.exports = require('./lib/extension')

View File

@@ -1,38 +0,0 @@
const path = require('path')
const Promise = require('bluebird')
const { getCookieUrl } = require('./util')
const fs = Promise.promisifyAll(require('fs'))
module.exports = {
getPathToExtension (...args) {
args = [__dirname, '..', 'dist', 'v2'].concat(args)
return path.join.apply(path, args)
},
getPathToV3Extension (...args) {
return path.join(...[__dirname, '..', 'dist', 'v3', ...args])
},
getPathToTheme () {
return path.join(__dirname, '..', 'theme')
},
getPathToRoot () {
return path.join(__dirname, '..')
},
setHostAndPath (host, path) {
const src = this.getPathToExtension('background.js')
return fs.readFileAsync(src, 'utf8')
.then((str) => {
return str
.replace('CHANGE_ME_HOST', host)
.replace('CHANGE_ME_PATH', path)
})
},
getCookieUrl,
}

View File

@@ -0,0 +1,30 @@
import path from 'path'
import { readFile } from 'fs/promises'
export const getPathToExtension = (...args: string[]) => {
args = [__dirname, '..', 'app-dist', 'v2'].concat(args)
return path.join.apply(path, args)
}
export const getPathToV3Extension = (...args: string[]) => {
return path.join(...[__dirname, '..', 'app-dist', 'v3', ...args])
}
export const getPathToTheme = () => {
return path.join(__dirname, '..', 'theme')
}
export const getPathToRoot = () => {
return path.join(__dirname, '..')
}
export const setHostAndPath = async (host: string, path: string) => {
const src = getPathToExtension('background.js')
const str = await readFile(src, 'utf8')
return str
.replace('CHANGE_ME_HOST', host)
.replace('CHANGE_ME_PATH', path)
}

View File

@@ -1,10 +0,0 @@
module.exports = {
getCookieUrl: (cookie = {}) => {
const prefix = cookie.secure ? 'https://' : 'http://'
// https://github.com/cypress-io/cypress/issues/6375
const host = cookie.domain.startsWith('.') ? cookie.domain.slice(1) : cookie.domain
return prefix + host + (cookie.path || '')
},
}

View File

@@ -2,17 +2,21 @@
"name": "@packages/extension",
"version": "0.0.0-development",
"private": true,
"main": "index.js",
"main": "lib-dist/index.js",
"scripts": {
"build": "gulp build",
"check-ts": "tsc --noEmit && yarn -s tslint",
"build-prod": "yarn build",
"build:lib": "tsc -p tsconfig.lib.json",
"build:v2": "webpack-cli",
"build:v3": "tsc -p tsconfig.app.v3.json",
"check-ts": "tsc -p tsconfig.json --noEmit && yarn -s tslint -p tsconfig.json",
"clean": "gulp clean",
"clean-deps": "rimraf node_modules",
"postinstall": "echo '@packages/extension needs: yarn build'",
"lint": "eslint --ext .js,.jsx,.ts,.tsx,.json, .",
"test": "yarn test-unit",
"test-debug": "yarn test-unit --inspect-brk=5566",
"test-unit": "cross-env NODE_ENV=test mocha -r @packages/ts/register --reporter mocha-multi-reporters --reporter-options configFile=../../mocha-reporter-config.json",
"test-debug": "vitest --inspect-brk --no-file-parallelism --test-timeout=0 --hook-timeout=0",
"test-unit": "vitest run",
"test-watch": "yarn test-unit --watch",
"tslint": "tslint --config ../ts/tslint.json --project . --exclude ./dist/v2/background.js",
"watch": "yarn build && chokidar 'app/**/*.*' 'app/*.*' -c 'yarn build'"
@@ -24,25 +28,21 @@
"devDependencies": {
"@packages/icons": "0.0.0-development",
"@packages/socket": "0.0.0-development",
"chai": "3.5.0",
"@types/webextension-polyfill": "0.12.4",
"chokidar-cli": "2.1.0",
"cross-env": "7.0.3",
"eol": "0.10.0",
"fs-extra": "9.1.0",
"gulp": "4.0.2",
"mocha": "3.5.3",
"mock-require": "3.0.3",
"rimraf": "6.0.1",
"sinon": "7.3.2",
"sinon-chai": "3.7.0",
"ts-loader": "9.5.2",
"vitest": "^3.2.4",
"webextension-polyfill": "0.4.0",
"webpack": "^5.88.2"
"webpack": "^5.88.2",
"webpack-cli": "^6.0.1"
},
"files": [
"app",
"dist",
"lib",
"app-dist",
"lib-dist",
"theme"
],
"nx": {

View File

@@ -0,0 +1,275 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import _ from 'lodash'
import http from 'http'
import { SocketIOServer } from '@packages/socket'
import { connect } from '../../../app/v2/client'
import EventEmitter from 'events'
import type { SocketShape } from '@packages/socket/browser/client'
import browser from 'webextension-polyfill'
import { automation } from '../../../app/v2/background'
vi.mock('../../../app/v2/client', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
connect: vi.fn(),
}
})
vi.mock('webextension-polyfill', () => {
return {
default: {
cookies: {
onChanged: {
addListener: vi.fn(),
},
},
downloads: {
onCreated: {
addListener: vi.fn(),
},
onChanged: {
addListener: vi.fn(),
},
},
runtime: {},
browsingData: {
remove: vi.fn(),
},
},
}
})
const PORT = 12345
describe('app/background', () => {
let httpSrv: http.Server
let server: http.Server
let connectWrapper: (options?: Record<string, unknown>) => Promise<SocketShape>
beforeEach(async () => {
vi.resetAllMocks()
vi.stubGlobal('window', {})
httpSrv = http.createServer()
// @ts-expect-error
server = new SocketIOServer(httpSrv, { path: '/__socket' })
// use an event emitter and wrap in in a vitest mock to assert on calls
const webSocketEventBus = new EventEmitter()
const ws = {
on: vi.fn().mockImplementation(webSocketEventBus.on),
emit: vi.fn(webSocketEventBus.emit),
} as unknown as SocketShape
vi.mocked(connect).mockReturnValue(ws)
browser.runtime.getBrowserInfo = vi.fn().mockResolvedValue({ name: 'Firefox' })
connectWrapper = async (options = {}) => {
const ws = automation.connect(`http://localhost:${PORT}`, '/__socket.io')
// skip 'connect' and 'automation:client:connected' and trigger
// the handler that kicks everything off
ws.emit('automation:config', options)
await new Promise<void>((resolve) => setTimeout(resolve, 1))
return ws
}
return new Promise<void>((resolve) => {
httpSrv.listen(PORT, resolve)
})
})
afterEach(function () {
server.close()
return new Promise<void>((resolve) => {
httpSrv.close(() => {
resolve()
})
})
})
describe('.connect', () => {
it('emits \'automation:client:connected\'', async function () {
const ws = automation.connect(`http://localhost:${PORT}`, '/__socket.io')
ws.emit('connect')
expect(ws.emit).toHaveBeenCalledWith('automation:client:connected')
})
it('listens to cookie changes', async function () {
await connectWrapper()
expect(browser.cookies.onChanged.addListener).toHaveBeenCalledOnce()
})
})
describe('cookies', () => {
it('onChanged does not emit when cause is overwrite', async function () {
const ws = await connectWrapper()
// @ts-expect-error
const fn = browser.cookies.onChanged.addListener.mock.calls[0][0]
fn({ cause: 'overwrite' })
expect(ws.emit).not.toHaveBeenCalledWith('automation:push:request')
})
it('onChanged emits automation:push:request change:cookie', async function () {
const info = { cause: 'explicit', cookie: { name: 'foo', value: 'bar' } }
vi.mocked(browser.cookies.onChanged.addListener).mockImplementation((fn) => fn(info as any))
const ws = await connectWrapper()
expect(ws.emit).toHaveBeenCalledWith('automation:push:request', 'change:cookie', info)
})
})
describe('downloads', () => {
it('onCreated emits automation:push:request create:download', async function () {
const downloadItem = {
id: '1',
filename: '/path/to/download.csv',
mime: 'text/csv',
url: 'http://localhost:1234/download.csv',
}
vi.mocked(browser.downloads.onCreated.addListener).mockImplementation((fn) => fn(downloadItem as any))
const ws = await connectWrapper()
expect(ws.emit).toHaveBeenCalledWith('automation:push:request', 'create:download', {
id: `${downloadItem.id}`,
filePath: downloadItem.filename,
mime: downloadItem.mime,
url: downloadItem.url,
})
})
it('onChanged emits automation:push:request complete:download', async function () {
const downloadDelta = {
id: '1',
state: {
current: 'complete',
},
}
vi.mocked(browser.downloads.onChanged.addListener).mockImplementation((fn) => fn(downloadDelta as any))
const ws = await connectWrapper()
expect(ws.emit).toHaveBeenCalledWith('automation:push:request', 'complete:download', {
id: `${downloadDelta.id}`,
})
})
it('onChanged emits automation:push:request canceled:download', async function () {
const downloadDelta = {
id: '1',
state: {
current: 'canceled',
},
}
vi.mocked(browser.downloads.onChanged.addListener).mockImplementation((fn) => fn(downloadDelta as any))
const ws = await connectWrapper()
expect(ws.emit).toHaveBeenCalledWith('automation:push:request', 'canceled:download', {
id: `${downloadDelta.id}`,
})
})
it('onChanged does not emit if state does not exist', async function () {
const downloadDelta = {
id: '1',
}
vi.mocked(browser.downloads.onChanged.addListener).mockImplementation((fn) => fn(downloadDelta as any))
const ws = await connectWrapper()
expect(ws.emit).not.toHaveBeenCalledWith('automation:push:request')
})
it('onChanged does not emit if state.current is not "complete"', async function () {
const downloadDelta = {
id: '1',
state: {
current: 'inprogress',
},
}
vi.mocked(browser.downloads.onChanged.addListener).mockImplementation((fn) => fn(downloadDelta as any))
const ws = await connectWrapper()
expect(ws.emit).not.toHaveBeenCalledWith('automation:push:request')
})
it('does not add downloads listener if in non-Firefox browser', async function () {
vi.mocked(browser.runtime.getBrowserInfo).mockResolvedValue({ name: 'Chrome' } as any)
await connectWrapper()
expect(browser.downloads.onCreated.addListener).not.toHaveBeenCalled()
expect(browser.downloads.onChanged.addListener).not.toHaveBeenCalled()
})
})
describe('integration', () => {
let socket: SocketShape
beforeEach(async function () {
const { connect: connectActual } = await vi.importActual<typeof import('../../../app/v2/client')>('../../../app/v2/client')
vi.mocked(connect).mockImplementation(connectActual)
await new Promise<void>((resolve) => {
server.on('connection', (socket1) => {
socket = socket1 as unknown as SocketShape
resolve()
})
automation.connect(`http://localhost:${PORT}`, '/__socket')
})
})
describe('reset:browser:state', () => {
beforeEach(() => {
vi.mocked(browser.browsingData.remove).mockImplementation((args: any, options: any) => {
if (_.isEqual(args, {}) && _.isEqual(options, { cache: true, cookies: true, downloads: true, formData: true, history: true, indexedDB: true, localStorage: true, passwords: true, pluginData: true, serviceWorkers: true })) {
return Promise.resolve()
}
return Promise.reject(new Error('Unexpected arguments'))
})
})
it('resets the browser state', function () {
return new Promise<void>((resolve) => {
socket.on('automation:response', (id: number, obj: { response: unknown }) => {
expect(id).toEqual(123)
expect(obj.response).toBeUndefined()
expect(browser.browsingData.remove).toHaveBeenCalled()
resolve()
})
server.emit('automation:request', 123, 'reset:browser:state')
})
})
})
})
})

View File

@@ -1,244 +0,0 @@
/* tslint:disable:no-empty */
require('../../spec_helper')
const _ = require('lodash')
const http = require('http')
const socket = require('@packages/socket')
const mockRequire = require('mock-require')
const client = require('../../../app/v2/client')
const browser = {
cookies: {
onChanged: {
addListener () {},
},
},
downloads: {
onCreated: {
addListener () {},
},
onChanged: {
addListener () {},
},
},
runtime: {},
browsingData: {
remove () {},
},
}
mockRequire('webextension-polyfill', browser)
const background = require('../../../app/v2/background')
const { expect } = require('chai')
const PORT = 12345
describe('app/background', () => {
beforeEach(function (done) {
global.window = {}
this.httpSrv = http.createServer()
this.server = socket.server(this.httpSrv, { path: '/__socket' })
const ws = {
on: sinon.stub(),
emit: sinon.stub(),
}
sinon.stub(client, 'connect').returns(ws)
browser.runtime.getBrowserInfo = sinon.stub().resolves({ name: 'Firefox' }),
this.connect = async (options = {}) => {
const ws = background.connect(`http://localhost:${PORT}`, '/__socket.io')
// skip 'connect' and 'automation:client:connected' and trigger
// the handler that kicks everything off
await ws.on.withArgs('automation:config').args[0][1](options)
return ws
}
this.httpSrv.listen(PORT, done)
})
afterEach(function (done) {
this.server.close()
this.httpSrv.close(() => {
done()
})
})
context('.connect', () => {
it('emits \'automation:client:connected\'', async function () {
const ws = background.connect(`http://localhost:${PORT}`, '/__socket.io')
await ws.on.withArgs('connect').args[0][1]()
expect(ws.emit).to.be.calledWith('automation:client:connected')
})
it('listens to cookie changes', async function () {
const addListener = sinon.stub(browser.cookies.onChanged, 'addListener')
await this.connect()
expect(addListener).to.be.calledOnce
})
})
context('cookies', () => {
it('onChanged does not emit when cause is overwrite', async function () {
const addListener = sinon.stub(browser.cookies.onChanged, 'addListener')
const ws = await this.connect()
const fn = addListener.getCall(0).args[0]
fn({ cause: 'overwrite' })
expect(ws.emit).not.to.be.calledWith('automation:push:request')
})
it('onChanged emits automation:push:request change:cookie', async function () {
const info = { cause: 'explicit', cookie: { name: 'foo', value: 'bar' } }
sinon.stub(browser.cookies.onChanged, 'addListener').yields(info)
const ws = await this.connect()
expect(ws.emit).to.be.calledWith('automation:push:request', 'change:cookie', info)
})
})
context('downloads', () => {
it('onCreated emits automation:push:request create:download', async function () {
const downloadItem = {
id: '1',
filename: '/path/to/download.csv',
mime: 'text/csv',
url: 'http://localhost:1234/download.csv',
}
sinon.stub(browser.downloads.onCreated, 'addListener').yields(downloadItem)
const ws = await this.connect()
expect(ws.emit).to.be.calledWith('automation:push:request', 'create:download', {
id: `${downloadItem.id}`,
filePath: downloadItem.filename,
mime: downloadItem.mime,
url: downloadItem.url,
})
})
it('onChanged emits automation:push:request complete:download', async function () {
const downloadDelta = {
id: '1',
state: {
current: 'complete',
},
}
sinon.stub(browser.downloads.onChanged, 'addListener').yields(downloadDelta)
const ws = await this.connect()
expect(ws.emit).to.be.calledWith('automation:push:request', 'complete:download', {
id: `${downloadDelta.id}`,
})
})
it('onChanged emits automation:push:request canceled:download', async function () {
const downloadDelta = {
id: '1',
state: {
current: 'canceled',
},
}
sinon.stub(browser.downloads.onChanged, 'addListener').yields(downloadDelta)
const ws = await this.connect()
expect(ws.emit).to.be.calledWith('automation:push:request', 'canceled:download', {
id: `${downloadDelta.id}`,
})
})
it('onChanged does not emit if state does not exist', async function () {
const downloadDelta = {
id: '1',
}
const addListener = sinon.stub(browser.downloads.onChanged, 'addListener')
const ws = await this.connect()
addListener.getCall(0).args[0](downloadDelta)
expect(ws.emit).not.to.be.calledWith('automation:push:request')
})
it('onChanged does not emit if state.current is not "complete"', async function () {
const downloadDelta = {
id: '1',
state: {
current: 'inprogress',
},
}
const addListener = sinon.stub(browser.downloads.onChanged, 'addListener')
const ws = await this.connect()
addListener.getCall(0).args[0](downloadDelta)
expect(ws.emit).not.to.be.calledWith('automation:push:request')
})
it('does not add downloads listener if in non-Firefox browser', async function () {
browser.runtime.getBrowserInfo = undefined
const onCreated = sinon.stub(browser.downloads.onCreated, 'addListener')
const onChanged = sinon.stub(browser.downloads.onChanged, 'addListener')
await this.connect()
expect(onCreated).not.to.be.called
expect(onChanged).not.to.be.called
})
})
context('integration', () => {
beforeEach(function (done) {
done = _.once(done)
client.connect.restore()
this.server.on('connection', (socket1) => {
this.socket = socket1
done()
})
this.client = background.connect(`http://localhost:${PORT}`, '/__socket')
})
describe('reset:browser:state', () => {
beforeEach(() => {
sinon.stub(browser.browsingData, 'remove').withArgs({}, { cache: true, cookies: true, downloads: true, formData: true, history: true, indexedDB: true, localStorage: true, passwords: true, pluginData: true, serviceWorkers: true }).resolves()
})
it('resets the browser state', function (done) {
this.socket.on('automation:response', (id, obj) => {
expect(id).to.eq(123)
expect(obj.response).to.be.undefined
expect(browser.browsingData.remove).to.be.called
done()
})
this.server.emit('automation:request', 123, 'reset:browser:state')
})
})
})
})

View File

@@ -0,0 +1,124 @@
import { describe, expect, beforeAll, beforeEach, it, vi } from 'vitest'
describe('app/v3/content', () => {
let port: { onMessage: { addListener: () => void }, postMessage: () => void }
let chrome: { runtime: { connect: () => { onMessage: { addListener: () => void } } } }
let window: { addEventListener: () => void, postMessage: () => void }
beforeAll(async () => {
port = {
onMessage: {
addListener: vi.fn(),
},
postMessage: vi.fn(),
}
chrome = {
runtime: {
connect: vi.fn().mockReturnValue(port),
},
}
// @ts-expect-error
global.chrome = chrome
window = {
addEventListener: vi.fn(),
postMessage: vi.fn(),
},
// @ts-expect-error
global.window = window
})
beforeEach(() => {
vi.resetModules()
vi.clearAllMocks()
})
it('adds window message listener and port onMessage listener', async () => {
await vi.importActual('../../../app/v3/content')
expect(window.addEventListener).toHaveBeenCalledWith('message', expect.any(Function))
expect(port.onMessage.addListener).toHaveBeenCalledWith(expect.any(Function))
})
describe('messages from window (i.e Cypress)', () => {
describe('on cypress:extension:activate:main:tab', () => {
const data = { message: 'cypress:extension:activate:main:tab' }
it('posts message to port', async () => {
// @ts-expect-error
vi.mocked(window.addEventListener).mockImplementation((event: MessageEvent, callback: (event: MessageEvent) => void) => callback({ data, source: window } as any))
await vi.importActual('../../../app/v3/content')
expect(port.postMessage).toHaveBeenCalledWith({
message: 'activate:main:tab',
})
})
it('is a noop if source is not the same window', async () => {
// @ts-expect-error
vi.mocked(window.addEventListener).mockImplementation((event: MessageEvent, callback: (event: MessageEvent) => void) => callback({ data, source: {} } as any))
await vi.importActual('../../../app/v3/content')
expect(port.postMessage).not.toHaveBeenCalled()
})
})
describe('on cypress:extension:url:changed', () => {
const data = { message: 'cypress:extension:url:changed', url: 'the://url' }
it('posts message to port', async () => {
// @ts-expect-error
vi.mocked(window.addEventListener).mockImplementation((event: MessageEvent, callback: (event: MessageEvent) => void) => callback({ data, source: window } as any))
await vi.importActual('../../../app/v3/content')
expect(port.postMessage).toHaveBeenCalledWith({
message: 'url:changed',
url: data.url,
})
})
it('is a noop if source is not the same window', async () => {
// @ts-expect-error
vi.mocked(window.addEventListener).mockImplementation((event: MessageEvent, callback: (event: MessageEvent) => void) => callback({ data, source: {} } as any))
await vi.importActual('../../../app/v3/content')
expect(port.postMessage).not.toHaveBeenCalled()
})
})
it('is a noop if message is not supported', async () => {
const data = { message: 'unsupported' }
// @ts-expect-error
vi.mocked(window.addEventListener).mockImplementation((event: MessageEvent, callback: (event: MessageEvent) => void) => callback({ data, source: window } as any))
await vi.importActual('../../../app/v3/content')
expect(port.postMessage).not.toHaveBeenCalled()
})
})
describe('messages from port (i.e. service worker)', () => {
describe('on main:tab:activated', () => {
it('posts message to window', async () => {
// @ts-expect-error
vi.mocked(port.onMessage.addListener).mockImplementation((callback: (event: MessageEvent) => void) => callback({ message: 'main:tab:activated' } as any))
await vi.importActual('../../../app/v3/content')
expect(window.postMessage).toHaveBeenCalledWith({ message: 'cypress:extension:main:tab:activated' }, '*')
})
})
it('is a noop if message is not main:tab:activated', async () => {
const data = { message: 'unsupported' }
// @ts-expect-error
vi.mocked(port.onMessage.addListener).mockImplementation((callback: (event: MessageEvent) => void) => callback({ data, source: window } as any))
await vi.importActual('../../../app/v3/content')
expect(window.postMessage).not.toHaveBeenCalled()
})
})
})

View File

@@ -1,108 +0,0 @@
require('../../spec_helper')
describe('app/v3/content', () => {
let port
let chrome
let window
before(() => {
port = {
onMessage: {
addListener: sinon.stub(),
},
postMessage: sinon.stub(),
}
chrome = {
runtime: {
connect: sinon.stub().returns(port),
},
}
global.chrome = chrome
window = {
addEventListener: sinon.stub(),
postMessage: sinon.stub(),
},
global.window = window
require('../../../app/v3/content')
})
beforeEach(() => {
port.postMessage.reset()
window.postMessage.reset()
})
it('adds window message listener and port onMessage listener', () => {
expect(window.addEventListener).to.be.calledWith('message', sinon.match.func)
expect(port.onMessage.addListener).to.be.calledWith(sinon.match.func)
})
describe('messages from window (i.e Cypress)', () => {
describe('on cypress:extension:activate:main:tab', () => {
const data = { message: 'cypress:extension:activate:main:tab' }
it('posts message to port', () => {
window.addEventListener.yield({ data, source: window })
expect(port.postMessage).to.be.calledWith({
message: 'activate:main:tab',
})
})
it('is a noop if source is not the same window', () => {
window.addEventListener.yield({ data, source: {} })
expect(port.postMessage).not.to.be.called
})
})
describe('on cypress:extension:url:changed', () => {
const data = { message: 'cypress:extension:url:changed', url: 'the://url' }
it('posts message to port', () => {
window.addEventListener.yield({ data, source: window })
expect(port.postMessage).to.be.calledWith({
message: 'url:changed',
url: data.url,
})
})
it('is a noop if source is not the same window', () => {
window.addEventListener.yield({ data, source: {} })
expect(port.postMessage).not.to.be.called
})
})
it('is a noop if message is not supported', () => {
const data = { message: 'unsupported' }
window.addEventListener.yield({ data, source: window })
expect(port.postMessage).not.to.be.called
})
})
describe('messages from port (i.e. service worker)', () => {
describe('on main:tab:activated', () => {
it('posts message to window', () => {
port.onMessage.addListener.yield({ message: 'main:tab:activated' })
expect(window.postMessage).to.be.calledWith({ message: 'cypress:extension:main:tab:activated' }, '*')
})
})
it('is a noop if message is not main:tab:activated', () => {
const data = { message: 'unsupported' }
port.onMessage.addListener.yield({ data, source: window })
expect(window.postMessage).not.to.be.called
})
})
})

View File

@@ -0,0 +1,157 @@
import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest'
describe('app/v3/service-worker', () => {
let chrome: { runtime: { onConnect: { addListener: () => void } }, tabs: { query: () => void, update: () => void }, storage: { local: { set: () => void, get: () => void } } }
let port: { onMessage: { addListener: () => void }, postMessage: () => void }
beforeAll(() => {
chrome = {
runtime: {
onConnect: {
addListener: vi.fn(),
},
},
tabs: {
query: vi.fn(),
update: vi.fn(),
},
storage: {
local: {
set: vi.fn(),
get: vi.fn(),
},
},
}
// @ts-expect-error
global.chrome = chrome
})
beforeEach(() => {
vi.resetModules()
vi.clearAllMocks()
port = {
onMessage: {
addListener: vi.fn(),
},
postMessage: vi.fn(),
}
})
it('adds onConnect listener', async () => {
await vi.importActual('../../../app/v3/service-worker')
expect(chrome.runtime.onConnect.addListener).toHaveBeenCalledWith(expect.any(Function))
})
it('adds port onMessage listener', async () => {
// @ts-expect-error
vi.mocked(chrome.runtime.onConnect.addListener).mockImplementation((fn: (port: { onMessage: { addListener: () => void } }) => void) => fn(port))
await vi.importActual('../../../app/v3/service-worker')
expect(port.onMessage.addListener).toHaveBeenCalledWith(expect.any(Function))
})
describe('on message', () => {
beforeEach(() => {
// @ts-expect-error
vi.mocked(chrome.runtime.onConnect.addListener).mockImplementation((fn: (port: { onMessage: { addListener: () => void } }) => void) => fn(port))
})
describe('activate:main:tab', () => {
const tab1 = { id: '1', url: 'the://url' }
const tab2 = { id: '2', url: 'some://other.url' }
beforeEach(() => {
// @ts-expect-error
vi.mocked(chrome.tabs.query).mockResolvedValue([tab1, tab2])
})
describe('when there is a most recent url', () => {
beforeEach(() => {
// @ts-expect-error
vi.mocked(chrome.storage.local.get).mockImplementation((key: string, callback: (result: { mostRecentUrl: string }) => void) => callback({ mostRecentUrl: tab1.url }))
})
it('activates the tab matching the url', async () => {
// @ts-expect-error
vi.mocked(port.onMessage.addListener).mockImplementation((callback: (event: MessageEvent) => void) => callback({ message: 'activate:main:tab' } as any))
await vi.importActual('../../../app/v3/service-worker')
expect(chrome.tabs.update).toHaveBeenCalledWith(tab1.id, { active: true })
})
describe('but no tab matches the most recent url', () => {
beforeEach(() => {
// @ts-expect-error
vi.mocked(chrome.tabs.query).mockResolvedValue([tab2])
})
it('does not try to activate any tabs', async () => {
// @ts-expect-error
vi.mocked(port.onMessage.addListener).mockImplementation((callback: (event: MessageEvent) => void) => callback({ message: 'activate:main:tab' } as any))
await vi.importActual('../../../app/v3/service-worker')
expect(chrome.tabs.update).not.toHaveBeenCalled()
})
})
describe('and chrome throws an error while activating the tab', () => {
let err: Error
beforeEach(() => {
vi.spyOn(console, 'log').mockImplementation(() => undefined)
err = new Error('uh oh')
vi.mocked(chrome.tabs.update).mockRejectedValue(err)
})
it('is a noop, logging the error', async () => {
// @ts-expect-error
vi.mocked(port.onMessage.addListener).mockImplementation((callback: (event: MessageEvent) => void) => callback({ message: 'activate:main:tab' } as any))
await vi.importActual('../../../app/v3/service-worker')
// eslint-disable-next-line no-console
expect(console.log).toHaveBeenCalledWith('Activating main Cypress tab errored:', err)
})
})
})
describe('when there is not a most recent url', () => {
beforeEach(() => {
// @ts-expect-error
vi.mocked(chrome.storage.local.get).mockImplementation((key: string, callback: (result: { mostRecentUrl: string }) => void) => callback({}))
})
it('does not try to activate any tabs', async () => {
// @ts-expect-error
vi.mocked(port.onMessage.addListener).mockImplementation((callback: (event: MessageEvent) => void) => callback({ message: 'activate:main:tab' } as any))
await vi.importActual('../../../app/v3/service-worker')
expect(chrome.tabs.update).not.toHaveBeenCalled()
})
})
})
describe('url:changed', () => {
it('sets the mostRecentUrl', async () => {
const url = 'some://url'
// @ts-expect-error
vi.mocked(port.onMessage.addListener).mockImplementation((callback: (event: MessageEvent) => void) => callback({ message: 'url:changed', url } as any))
await vi.importActual('../../../app/v3/service-worker')
expect(chrome.storage.local.set).toHaveBeenCalledWith({ mostRecentUrl: url })
})
})
it('is a noop if message is not a supported message', async () => {
// @ts-expect-error
vi.mocked(port.onMessage.addListener).mockImplementation((callback: (event: MessageEvent) => void) => callback({ message: 'unsupported' } as any))
await vi.importActual('../../../app/v3/service-worker')
expect(chrome.tabs.update).not.toHaveBeenCalled()
expect(chrome.storage.local.set).not.toHaveBeenCalled()
})
})
})

View File

@@ -1,137 +0,0 @@
require('../../spec_helper')
describe('app/v3/service-worker', () => {
let chrome
let port
before(() => {
chrome = {
runtime: {
onConnect: {
addListener: sinon.stub(),
},
},
tabs: {
query: sinon.stub(),
update: sinon.stub(),
},
storage: {
local: {
set: sinon.stub(),
get: sinon.stub(),
},
},
}
global.chrome = chrome
require('../../../app/v3/service-worker')
})
beforeEach(() => {
chrome.tabs.query.reset()
chrome.tabs.update.reset()
chrome.storage.local.set.reset()
chrome.storage.local.get.reset()
port = {
onMessage: {
addListener: sinon.stub(),
},
postMessage: sinon.stub(),
}
})
it('adds onConnect listener', () => {
expect(chrome.runtime.onConnect.addListener).to.be.calledWith(sinon.match.func)
})
it('adds port onMessage listener', () => {
chrome.runtime.onConnect.addListener.yield(port)
expect(port.onMessage.addListener).to.be.calledWith(sinon.match.func)
})
describe('on message', () => {
beforeEach(() => {
chrome.runtime.onConnect.addListener.yield(port)
})
describe('activate:main:tab', () => {
const tab1 = { id: '1', url: 'the://url' }
const tab2 = { id: '2', url: 'some://other.url' }
beforeEach(() => {
chrome.tabs.query.resolves([tab1, tab2])
})
describe('when there is a most recent url', () => {
beforeEach(() => {
chrome.storage.local.get.callsArgWith(1, { mostRecentUrl: tab1.url })
})
it('activates the tab matching the url', async () => {
await port.onMessage.addListener.yield({ message: 'activate:main:tab' })[0]
expect(chrome.tabs.update).to.be.calledWith(tab1.id, { active: true })
})
describe('but no tab matches the most recent url', () => {
beforeEach(() => {
chrome.tabs.query.reset()
chrome.tabs.query.resolves([tab2])
})
it('does not try to activate any tabs', async () => {
await port.onMessage.addListener.yield({ message: 'activate:main:tab' })[0]
expect(chrome.tabs.update).not.to.be.called
})
})
describe('and chrome throws an error while activating the tab', () => {
let err
beforeEach(() => {
sinon.stub(console, 'log')
err = new Error('uh oh')
chrome.tabs.update.rejects(err)
})
it('is a noop, logging the error', async () => {
await port.onMessage.addListener.yield({ message: 'activate:main:tab' })[0]
// eslint-disable-next-line no-console
expect(console.log).to.be.calledWith('Activating main Cypress tab errored:', err)
})
})
})
describe('when there is not a most recent url', () => {
beforeEach(() => {
chrome.storage.local.get.callsArgWith(1, {})
})
it('does not try to activate any tabs', async () => {
await port.onMessage.addListener.yield({ message: 'activate:main:tab' })[0]
expect(chrome.tabs.update).not.to.be.called
})
})
})
describe('url:changed', () => {
it('sets the mostRecentUrl', async () => {
const url = 'some://url'
await port.onMessage.addListener.yield({ message: 'url:changed', url })[0]
expect(chrome.storage.local.set).to.be.calledWith({ mostRecentUrl: url })
})
})
it('is a noop if message is not a supported message', async () => {
await port.onMessage.addListener.yield({ message: 'unsupported' })[0]
expect(chrome.tabs.update).not.to.be.called
expect(chrome.storage.local.set).not.to.be.called
})
})
})

View File

@@ -1,4 +0,0 @@
test/unit
test/integration
--reporter spec
--recursive

View File

@@ -1,12 +0,0 @@
const chai = require('chai')
const sinon = require('sinon')
const sinonChai = require('sinon-chai')
chai.use(sinonChai)
global.sinon = sinon
global.expect = chai.expect
afterEach(() => {
return sinon.restore()
})

View File

@@ -0,0 +1,111 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { exec } from 'child_process'
import fs from 'fs-extra'
import path from 'path'
import * as extension from '../../lib/index'
vi.mock('../../lib/index', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
getPathToExtension: vi.fn(),
}
})
const cwd = process.cwd()
describe('Extension', () => {
describe('.getPathToExtension', () => {
beforeEach(async () => {
const { getPathToExtension } = await vi.importActual<typeof import('../../lib/index')>('../../lib/index')
// use the actual implementation for these tests
vi.mocked(extension.getPathToExtension).mockImplementation(getPathToExtension)
})
it('returns path to app-dist/v2', () => {
const result = extension.getPathToExtension()
const expected = path.join(cwd, 'app-dist', 'v2')
expect(path.normalize(result)).toEqual(path.normalize(expected))
})
it('returns path to files in app-dist/v2', () => {
const result = extension.getPathToExtension('background.js')
const expected = path.join(cwd, '/app-dist/v2/background.js')
expect(path.normalize(result)).toEqual(path.normalize(expected))
})
})
describe('.getPathToV3Extension', () => {
it('returns path to app-dist/v3', () => {
const result = extension.getPathToV3Extension()
const expected = path.join(cwd, 'app-dist', 'v3')
expect(path.normalize(result)).toEqual(path.normalize(expected))
})
})
describe('.getPathToTheme', () => {
it('returns path to theme', () => {
const result = extension.getPathToTheme()
const expected = path.join(cwd, 'theme')
expect(path.normalize(result)).toEqual(path.normalize(expected))
})
})
describe('.getPathToRoot', () => {
it('returns path to root', () => {
expect(extension.getPathToRoot()).toEqual(cwd)
})
})
describe('.setHostAndPath', () => {
let src: string
beforeEach(function () {
src = path.join(cwd, 'test', 'helpers', 'background.js')
vi.mocked(extension.getPathToExtension).mockImplementation((file) => {
if (file === 'background.js') {
return src
}
throw new Error(`Unexpected file: ${file}`)
})
})
it('does not mutate background.js', async () => {
const str = await fs.readFile(src, 'utf8')
await extension.setHostAndPath('http://dev.local:8080', '/__foo')
const str2 = await fs.readFile(src, 'utf8')
expect(str).toEqual(str2)
})
})
describe('manifest', () => {
it('has a key that resolves to the static extension ID', async () => {
const manifest = await fs.readJson(path.join(cwd, 'app/v2/manifest.json'))
const cmd = `echo \"${manifest.key}\" | openssl base64 -d -A | shasum -a 256 | head -c32 | tr 0-9a-f a-p`
const stdout = await new Promise((resolve, reject) => {
exec(cmd, (error, stdout) => {
if (error) {
reject(error)
}
resolve(stdout)
})
})
expect(stdout).toEqual('caljajdfkjjjdehjdoimjkkakekklcck')
})
})
})

View File

@@ -1,136 +0,0 @@
require('../spec_helper')
let { exec } = require('child_process')
let fs = require('fs-extra')
const eol = require('eol')
const path = require('path')
const Promise = require('bluebird')
const extension = require('../../index')
const cwd = process.cwd()
fs = Promise.promisifyAll(fs)
exec = Promise.promisify(exec)
describe('Extension', () => {
context('.getCookieUrl', () => {
it('returns cookie url', () => {
expect(extension.getCookieUrl({
name: 'foo',
value: 'bar',
path: '/foo/bar',
domain: 'www.google.com',
secure: true,
})).to.eq('https://www.google.com/foo/bar')
})
})
context('.getPathToExtension', () => {
it('returns path to dist/v2', () => {
const result = extension.getPathToExtension()
const expected = path.join(cwd, 'dist', 'v2')
expect(path.normalize(result)).to.eq(path.normalize(expected))
})
it('returns path to files in dist/v2', () => {
const result = extension.getPathToExtension('background.js')
const expected = path.join(cwd, '/dist/v2/background.js')
expect(path.normalize(result)).to.eq(path.normalize(expected))
})
})
context('.getPathToV3Extension', () => {
it('returns path to dist/v3', () => {
const result = extension.getPathToV3Extension()
const expected = path.join(cwd, 'dist', 'v3')
expect(path.normalize(result)).to.eq(path.normalize(expected))
})
})
context('.getPathToTheme', () => {
it('returns path to theme', () => {
const result = extension.getPathToTheme()
const expected = path.join(cwd, 'theme')
expect(path.normalize(result)).to.eq(path.normalize(expected))
})
})
context('.getPathToRoot', () => {
it('returns path to root', () => {
expect(extension.getPathToRoot()).to.eq(cwd)
})
})
context('.setHostAndPath', () => {
beforeEach(function () {
this.src = path.join(cwd, 'test', 'helpers', 'background.js')
return sinon.stub(extension, 'getPathToExtension')
.withArgs('background.js').returns(this.src)
})
it('rewrites the background.js source', () => {
return extension.setHostAndPath('http://dev.local:8080', '/__foo')
.then((str) => {
const result = eol.auto(str)
const expected = eol.auto(`\
(function() {
var HOST, PATH, automation, client, fail, invoke,
slice = [].slice;
HOST = "http://dev.local:8080";
PATH = "/__foo";
client = io.connect(HOST, {
path: PATH
});
automation = {
getAllCookies: function(filter, fn) {
if (filter == null) {
filter = {};
}
return chrome.cookies.getAll(filter, fn);
}
};
}).call(this);
\
`)
expect(result).to.eq(expected)
})
})
it('does not mutate background.js', function () {
return fs.readFileAsync(this.src, 'utf8')
.then((str) => {
return extension.setHostAndPath('http://dev.local:8080', '/__foo')
.then(() => {
return fs.readFileAsync(this.src, 'utf8')
}).then((str2) => {
expect(str).to.eq(str2)
})
})
})
})
context('manifest', () => {
it('has a key that resolves to the static extension ID', () => {
return fs.readJsonAsync(path.join(cwd, 'app/v2/manifest.json'))
.then((manifest) => {
const cmd = `echo \"${manifest.key}\" | openssl base64 -d -A | shasum -a 256 | head -c32 | tr 0-9a-f a-p`
return exec(cmd)
.then((stdout) => {
expect(stdout).to.eq('caljajdfkjjjdehjdoimjkkakekklcck')
})
})
})
})
})

View File

@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"include": [
"app/v2/**/*.ts"
],
"compilerOptions": {
"rootDir": "./app/v2",
"outDir": "./app-dist/v2",
"target": "ES2022",
"module": "Es2022"
}
}

View File

@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"include": [
"app/v3**/*.ts"
],
"compilerOptions": {
"rootDir": "./app/v3",
"outDir": "./app-dist/v3",
"target": "ES2022",
"module": "ES2022"
}
}

View File

@@ -1,8 +1,14 @@
{
"extends": "../ts/tsconfig.json",
"compilerOptions": {
"target": "es2020",
"allowJs": true,
"strict": false
"moduleResolution": "node",
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"types": [
"node"
]
}
}
}

View File

@@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"include": [
"lib/**/*.ts"
],
"compilerOptions": {
"declaration": true,
"rootDir": "./lib",
"outDir": "./lib-dist",
"target": "ES2022",
"module": "CommonJS"
}
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['test/**/*.spec.ts'],
globals: true,
environment: 'node',
},
})

View File

@@ -1,9 +1,9 @@
const path = require('path')
const webpack = require('webpack')
import path from 'path'
import webpack from 'webpack'
module.exports = {
export default {
mode: process.env.NODE_ENV || 'development',
entry: './app/v2/init.js',
entry: './app/v2/init.ts',
// https://github.com/cypress-io/cypress/issues/15032
// Default webpack output setting is "eval".
// Chrome doesn't allow "eval" inside extensions.
@@ -12,7 +12,14 @@ module.exports = {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
use: [
{
loader: 'ts-loader',
options: {
configFile: path.resolve(import.meta.dirname, 'tsconfig.app.v2.json'),
},
},
],
exclude: /node_modules/,
},
],
@@ -22,7 +29,7 @@ module.exports = {
},
output: {
filename: 'background.js',
path: path.resolve(__dirname, 'dist', 'v2'),
path: path.resolve(import.meta.dirname, 'app-dist', 'v2'),
},
plugins: [
new webpack.DefinePlugin({

View File

@@ -10,8 +10,8 @@ import {
} from '@urql/core'
import { devtoolsExchange } from '@urql/devtools'
import { useToast } from 'vue-toastification'
import type { SocketShape } from '@packages/socket/lib/types'
import { client } from '@packages/socket/lib/browser'
import type { SocketShape } from '@packages/socket/browser/client'
import { client } from '@packages/socket/browser/client'
import { createClient as createWsClient } from 'graphql-ws'
import { cacheExchange as graphcacheExchange } from '@urql/exchange-graphcache'

View File

@@ -1,6 +1,6 @@
import { pipe, tap } from 'wonka'
import type { Exchange, Operation, OperationResult } from '@urql/core'
import type { SocketShape } from '@packages/socket/lib/types'
import type { SocketShape } from '@packages/socket/browser/client'
import type { DefinitionNode, DocumentNode, OperationDefinitionNode } from 'graphql'
export const pubSubExchange = (io: SocketShape): Exchange => {

View File

@@ -1,5 +1,5 @@
import _ from 'lodash'
import type { SocketShape } from '@packages/socket/lib/types'
import type { SocketShape } from '@packages/socket/browser/client'
import type { ClientOptions } from '@urql/core'
export const urqlFetchSocketAdapter = (io: SocketShape): ClientOptions['fetch'] => {

View File

@@ -9,7 +9,7 @@ import url from 'url'
import DebuggingProxy from '@cypress/debugging-proxy'
import Request from '@cypress/request-promise'
import * as socketIo from '@packages/socket/lib/browser'
import * as socketIo from '@packages/socket/browser/client'
import {
buildConnectReqHead,
createProxySock,

View File

@@ -1,4 +1,4 @@
import * as errors from '@packages/server/lib/errors'
import errors from '@packages/errors'
import type { HttpMiddleware } from '.'
import type { Readable } from 'stream'

View File

@@ -1,6 +1,5 @@
import _ from 'lodash'
import Debug from 'debug'
import extension from '@packages/extension'
import { isHostOnlyCookie } from '../browsers/cdp_automation'
import type { SerializableAutomationCookie } from '../util/cookies'
@@ -32,6 +31,19 @@ const normalizeCookies = (cookies: (SerializableAutomationCookie | AutomationCoo
return _.map(cookies, normalizeCookieProps) as AutomationCookie[]
}
const getCookieUrl = (cookie: {
secure?: boolean | null
domain?: string | null
path?: string | null
} = {}) => {
const prefix = cookie.secure ? 'https://' : 'http://'
// https://github.com/cypress-io/cypress/issues/6375
const host = cookie.domain?.startsWith('.') ? cookie.domain.slice(1) : cookie.domain
return prefix + host + (cookie.path || '')
}
const normalizeCookieProps = function (automationCookie: SerializableAutomationCookie | AutomationCookie | null) {
if (!automationCookie) {
return automationCookie
@@ -151,7 +163,7 @@ export class Cookies {
// lets construct the url ourselves right now
// unless we already have a URL
cookie.url = data.url != null ? data.url : extension.getCookieUrl(data)
cookie.url = data.url != null ? data.url : getCookieUrl(data)
debug('set:cookie %o', cookie)
@@ -175,7 +187,7 @@ export class Cookies {
// lets construct the url ourselves right now
// unless we already have a URL
cookie.url = data.url != null ? data.url : extension.getCookieUrl(data)
cookie.url = data.url != null ? data.url : getCookieUrl(data)
return cookie
})

View File

@@ -4,7 +4,7 @@ import la from 'lazy-ass'
import _ from 'lodash'
import os from 'os'
import path from 'path'
import extension from '@packages/extension'
import * as extension from '@packages/extension'
import mime from 'mime'
import { launch } from '@packages/launcher'
@@ -21,7 +21,7 @@ import type { Automation } from '../automation'
import memory from './memory'
import type { BrowserLaunchOpts, BrowserNewTabOpts, ProtocolManagerShape, CyPromptManagerShape, RunModeVideoApi } from '@packages/types'
import type { CDPSocketServer } from '@packages/socket/lib/cdp-socket'
import type { CDPSocketServer } from '@packages/socket'
import { DEFAULT_CHROME_FLAGS } from '../util/chromium_flags'
const debug = debugModule('cypress:server:browsers:chrome')

View File

@@ -13,7 +13,7 @@ import type { Browser, BrowserInstance, GracefulShutdownOptions } from './types'
import type { BrowserWindow } from 'electron'
import type { Automation } from '../automation'
import type { BrowserLaunchOpts, Preferences, ProtocolManagerShape, CyPromptManagerShape, RunModeVideoApi } from '@packages/types'
import type { CDPSocketServer } from '@packages/socket/lib/cdp-socket'
import type { CDPSocketServer } from '@packages/socket'
import memory from './memory'
import { BrowserCriClient } from './browser-cri-client'
import { getRemoteDebuggingPort } from '../util/electron-app'

View File

@@ -10,7 +10,7 @@ import { BROWSER_FAMILY, BrowserLaunchOpts, BrowserNewTabOpts, FoundBrowser, Pro
import type { Browser, BrowserInstance, BrowserLauncher } from './types'
import type { Automation } from '../automation'
import type { DataContext } from '@packages/data-context'
import type { CDPSocketServer } from '@packages/socket/lib/cdp-socket'
import type { CDPSocketServer } from '@packages/socket'
const debug = Debug('cypress:server:browsers')
const isBrowserFamily = (browser: string) => BROWSER_FAMILY.includes(browser)

View File

@@ -1,7 +1,7 @@
import type { FoundBrowser, BrowserLaunchOpts, BrowserNewTabOpts, ProtocolManagerShape, CyPromptManagerShape } from '@packages/types'
import type { EventEmitter } from 'events'
import type { Automation } from '../automation'
import type { CDPSocketServer } from '@packages/socket/lib/cdp-socket'
import type { CDPSocketServer } from '@packages/socket'
export type Browser = FoundBrowser & {
majorVersion: number | string

View File

@@ -6,7 +6,7 @@ import { getCtx } from '@packages/data-context'
import { handleGraphQLSocketRequest } from '@packages/data-context/graphql/makeGraphQLServer'
import { onNetStubbingEvent } from '@packages/net-stubbing'
import * as socketIo from '@packages/socket'
import { CDPSocketServer } from '@packages/socket/lib/cdp-socket'
import { CDPSocketServer } from '@packages/socket'
import * as errors from './errors'
import fixture from './fixture'

View File

@@ -5,7 +5,9 @@ const ws = require('ws')
const httpsProxyAgent = require('https-proxy-agent')
const evilDns = require('evil-dns')
const Promise = require('bluebird')
const socketIo = require(`@packages/socket/lib/browser`)
// NOTE: we need to import the client from the lib directory because the browser/client directory is compiled to ESM.
// we are unable to import ESM into a CommonJS test context, even if we await import() the module.
const socketIo = require('@packages/socket/lib/client')
const httpsServer = require(`@packages/https-proxy/test/helpers/https_server`)
const config = require(`../../lib/config`)
const { ServerBase } = require(`../../lib/server-base`)

View File

@@ -337,7 +337,7 @@ describe('lib/browsers/firefox', () => {
it('writes extension and ensure write access', async function () {
mockfs({
[path.resolve(`${__dirname }../../../../../extension/dist/v2`)]: {
[path.resolve(`${__dirname }../../../../../extension/app-dist/v2`)]: {
'background.js': mockfs.file({
mode: 0o444,
}),

View File

@@ -3,7 +3,10 @@ require('../spec_helper')
const _ = require('lodash')
const path = require('path')
const httpsAgent = require('https-proxy-agent')
const socketIo = require('@packages/socket/lib/browser')
// NOTE: we need to import the client from the lib directory because the browser/client directory is compiled to ESM.
// we are unable to import ESM into a CommonJS test context, even if we await import() the module.
const socketIo = require('@packages/socket/lib/client')
const Fixtures = require('@tooling/system-tests')
const errors = require('../../lib/errors')
@@ -28,6 +31,19 @@ describe('lib/socket', () => {
path: 'path-to-browser-one',
}
// needed to run these tests locally
// sinon.stub(ctx.browser, 'machineBrowsers').resolves([
// {
// channel: 'stable',
// displayName: 'Electron',
// family: 'chromium',
// majorVersion: '123',
// name: 'electron',
// path: 'path-to-browser-one',
// version: '123.45.67',
// },
// ])
// Don't bother initializing the child process, etc for this
sinon.stub(ctx.actions.project, 'initializeActiveProject')
sinon.stub(preprocessor.emitter, 'on')

View File

@@ -1 +1,5 @@
**/tsconfig.json
cjs/
esm/
browser/

3
packages/socket/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
cjs/
esm/
browser/

View File

@@ -1,32 +1,19 @@
# Socket
This is a shared lib for holding both the `socket.io` server and client.
This is a shared lib for holding both the `socket.io` Cypress implements of the server and client.
## Using
```javascript
const socket = require("@packages/socket")
// returns
{
server: require("socket.io"),
getPathToClientSource: function () {
// returns path to the client 'socket.io.js' file
// for use in the browser
}
}
```
```javascript
const socket = require("@packages/socket")
// server usage
const srv = require("http").createServer()
const io = socket.server(srv)
const io = new SocketIOServer(srv)
io.on("connection", function(){})
// client usage
const { client } = require("@packages/socket/lib/client")
const { client } = require("@packages/socket/browser/client")
const client = socket.client("http://localhost:2020")
client.on("connect", function(){})
client.on("event", function(){})

View File

@@ -1,5 +0,0 @@
if (process.env.CYPRESS_INTERNAL_ENV !== 'production') {
require('@packages/ts/register')
}
module.exports = require('./lib/socket')

View File

@@ -1,8 +1,6 @@
import io, { ManagerOptions, SocketOptions } from 'socket.io-client'
import { CDPBrowserSocket } from './cdp-browser'
import type { SocketShape } from './types'
export type { Socket } from 'socket.io-client'
import type { SocketShape } from './cdp-browser'
declare global {
interface Window {
@@ -26,8 +24,7 @@ export function client (uri: string, opts?: Partial<ManagerOptions & SocketOptio
// Connect the socket regardless of whether or not we have newly created it
window.cypressSockets[fullNamespace].connect()
// @ts-expect-error TODO: fix type
return window.cypressSockets[fullNamespace]
return window.cypressSockets[fullNamespace] as unknown as SocketShape
}
return io(uri, opts)
@@ -49,8 +46,7 @@ export function createWebsocket ({ path, browserFamily }: { path: string, browse
// Connect the socket regardless of whether or not we have newly created it
window.cypressSockets[fullNamespace].connect()
// @ts-expect-error TODO: fix type
return window.cypressSockets[fullNamespace]
return window.cypressSockets[fullNamespace] as unknown as SocketShape
}
return io({

View File

@@ -1,8 +1,9 @@
/// <reference lib="dom" />
import Emitter from 'component-emitter'
import { v4 as uuidv4 } from 'uuid'
import { decode, encode } from './utils'
import type { SocketShape } from './types'
import { decode, encode } from '../utils'
import Emitter from 'component-emitter'
export type SocketShape = Emitter
type CDPSocketNamespaceKey = `cypressSocket-${string}`
type CDPSendToServerNamespaceKey = `cypressSendToServer-${string}`

View File

@@ -0,0 +1,7 @@
export { client, createWebsocket } from './browser'
export { CDPBrowserSocket } from './cdp-browser'
export type { Socket } from 'socket.io-client'
export type { SocketShape } from './cdp-browser'

View File

@@ -2,7 +2,7 @@ import type { CDPClient } from '@packages/types/src/protocol'
import type Protocol from 'devtools-protocol/types/protocol.d'
import { EventEmitter } from 'stream'
import { randomUUID } from 'crypto'
import { decode, encode } from './utils'
import { decode, encode } from '../utils'
import Debug from 'debug'
const debugVerbose = Debug('cypress-verbose:server:socket:cdp-socket')
@@ -36,6 +36,7 @@ export class CDPSocketServer extends EventEmitter {
// @ts-expect-error TODO: fix emit type
emit = async (event: string, ...args: any[]) => {
// tslint:disable-next-line:no-floating-promises
this._cdpSocket?.emit(event, ...args)
return true

View File

@@ -0,0 +1,5 @@
export { SocketIOServer, getPathToClientSource, getClientVersion } from './socket'
export type { ServerOptions, Socket, Namespace as SocketIONamespace } from 'socket.io'
export { CDPSocketServer } from './cdp-socket'

View File

@@ -1,18 +1,15 @@
import buffer from 'buffer'
import type http from 'http'
import server, { Server as SocketIOBaseServer, ServerOptions, Socket, Namespace } from 'socket.io'
export type { Socket, Namespace as SocketIONamespace }
import { Server as SocketIOBaseServer, ServerOptions } from 'socket.io'
// TODO: this will need to be updated to use an ESM version of the package
const { version } = require('socket.io-client/package.json')
const clientSource = require.resolve('socket.io-client/dist/socket.io.js')
export { ServerOptions }
// socket.io types are incorrect
type PatchedServerOptions = ServerOptions & { cookie: { name: string | boolean } }
class SocketIOServer extends SocketIOBaseServer {
export class SocketIOServer extends SocketIOBaseServer {
constructor (srv: http.Server, opts?: Partial<PatchedServerOptions>) {
opts = opts ?? {}
@@ -27,12 +24,6 @@ class SocketIOServer extends SocketIOBaseServer {
}
}
export {
server,
SocketIOServer,
}
// TODO: I don't know that this is used anywhere?
export const getPathToClientSource = () => {
return clientSource
}

View File

@@ -2,18 +2,23 @@
"name": "@packages/socket",
"version": "0.0.0-development",
"private": true,
"browser": "lib/browser.ts",
"main": "cjs/node/index.js",
"browser": "browser/client/index.js",
"scripts": {
"build-prod": "tsc || echo 'built, with type errors' && rm lib/browser.js",
"check-ts": "tsc --noEmit && yarn -s tslint",
"clean": "rimraf --glob 'lib/*.js'",
"build": "yarn build:browser && yarn build:node && yarn build:node:esm",
"build-prod": "yarn build",
"build:browser": "rimraf browser && tsc -p tsconfig.browser.json",
"build:node": "rimraf cjs && tsc -p tsconfig.cjs.json",
"build:node:esm": "rimraf esm && tsc -p tsconfig.esm.json",
"check-ts": "tsc -p tsconfig.browser.json --noEmit && tsc -p tsconfig.cjs.json --noEmit && yarn -s tslint",
"clean-deps": "rimraf node_modules",
"postinstall": "patch-package",
"lint": "eslint --ext .js,.jsx,.ts,.tsx,.json, .",
"test": "yarn test-unit",
"test-debug": "vitest --inspect-brk --no-file-parallelism --test-timeout=0 --hook-timeout=0",
"test-unit": "vitest",
"tslint": "tslint --config ../ts/tslint.json --project ."
"tslint": "tslint --config ../ts/tslint.json --project .",
"watch": "yarn build:browser -- -w & yarn build:node -- -w"
},
"dependencies": {
"component-emitter": "1.3.0",
@@ -32,14 +37,16 @@
"cross-env": "7.0.3",
"devtools-protocol": "0.0.1528500",
"resolve-pkg": "2.0.0",
"rimraf": "6.0.1",
"vitest": "^3.2.4"
},
"files": [
"index.js",
"lib",
"browser/",
"cjs/",
"esm/",
"patches"
],
"types": "lib/socket.ts",
"module": "esm/node/index.js",
"workspaces": {
"nohoist": [
"devtools-protocol",

View File

@@ -3,19 +3,14 @@ import fs from 'fs'
import path from 'path'
import parser from 'socket.io-parser'
import { hasBinary } from 'socket.io-parser/dist/is-binary'
// @ts-expect-error
import pkg from '../package.json'
import lib from '../index'
import * as browserLib from '../lib/browser'
import * as lib from '../lib/node'
import * as browserLib from '../lib/client'
import resolvePkg from 'resolve-pkg'
const { PacketType } = parser
describe('Socket', function () {
it('exports server', function () {
expect(lib.server).toBeDefined()
})
it('exports client from lib/browser', function () {
expect(browserLib.client).toBeDefined()
})

View File

@@ -0,0 +1,18 @@
{
"include": [
"lib/client/**/*.ts",
"lib/utils.ts"
],
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDirs": [
"./lib/client",
"./lib/utils"
],
"outDir": "./browser",
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "node",
"declaration": true
}
}

View File

@@ -0,0 +1,18 @@
{
"include": [
"lib/node/**/*.ts",
"lib/utils.ts"
],
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDirs": [
"./lib/node",
"./lib/utils"
],
"outDir": "./cjs",
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "node",
"declaration": true
}
}

View File

@@ -0,0 +1,19 @@
{
"include": [
"lib/node/**/*.ts",
"lib/utils.ts"
],
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDirs": [
"./lib/node",
"./lib/utils"
],
"outDir": "./esm",
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "node",
"declaration": true,
"noEmit": true
}
}

View File

@@ -1,13 +1,14 @@
{
"extends": "./../ts/tsconfig.json",
"compilerOptions": {
"noImplicitAny": true,
"skipLibCheck": false
},
"include": [
"lib/*.ts",
"lib/**/*.ts"
],
"files": [
"./../ts/index.d.ts"
]
}
"compilerOptions": {
"allowJs": false,
"noImplicitAny": true,
"noImplicitReturns": false,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"esModuleInterop": true
}
}

View File

@@ -1,7 +1,7 @@
const { expect } = require('chai')
const HttpsProxyAgent = require('https-proxy-agent')
const os = require('os')
const socketIo = require('@packages/socket/lib/browser')
const socketIo = require('@packages/socket/browser/client')
module.exports = {
'e2e': {

View File

@@ -821,6 +821,7 @@
"./packages/server/lib/cloud/studio/StudioLifecycleManager.ts",
"./packages/server/lib/cloud/user.ts",
"./packages/server/lib/config.ts",
"./packages/server/lib/controllers/client.ts",
"./packages/server/lib/cypress.ts",
"./packages/server/lib/environment.js",
"./packages/server/lib/modes/record.ts",
@@ -899,9 +900,8 @@
"./packages/server/node_modules/tough-cookie/lib/cookie.js",
"./packages/server/start-cypress.js",
"./packages/server/v8-snapshot-entry.js",
"./packages/socket/index.js",
"./packages/socket/lib/cdp-socket.ts",
"./packages/socket/lib/socket.ts",
"./packages/socket/cjs/node/cdp-socket.js",
"./packages/socket/cjs/node/socket.js",
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/browser.js",
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/index.js",
"./packages/socket/node_modules/socket.io/dist/broadcast-operator.js",
@@ -4091,7 +4091,6 @@
"./packages/server/lib/cloud/upload/stream_activity_monitor.ts",
"./packages/server/lib/cloud/upload/stream_stalled_error.ts",
"./packages/server/lib/cohorts.ts",
"./packages/server/lib/controllers/client.ts",
"./packages/server/lib/controllers/files.ts",
"./packages/server/lib/controllers/iframes.ts",
"./packages/server/lib/controllers/runner.ts",
@@ -4289,7 +4288,8 @@
"./packages/server/node_modules/whatwg-url/lib/public-api.js",
"./packages/server/node_modules/whatwg-url/lib/url-state-machine.js",
"./packages/server/node_modules/whatwg-url/lib/utils.js",
"./packages/socket/lib/utils.ts",
"./packages/socket/cjs/node/index.js",
"./packages/socket/cjs/utils.js",
"./packages/socket/node_modules/engine.io-parser/lib/commons.js",
"./packages/socket/node_modules/engine.io-parser/lib/decodePacket.js",
"./packages/socket/node_modules/engine.io-parser/lib/encodePacket.js",

View File

@@ -820,6 +820,7 @@
"./packages/server/lib/cloud/studio/StudioLifecycleManager.ts",
"./packages/server/lib/cloud/user.ts",
"./packages/server/lib/config.ts",
"./packages/server/lib/controllers/client.ts",
"./packages/server/lib/cypress.ts",
"./packages/server/lib/environment.js",
"./packages/server/lib/modes/record.ts",
@@ -898,9 +899,8 @@
"./packages/server/node_modules/tough-cookie/lib/cookie.js",
"./packages/server/start-cypress.js",
"./packages/server/v8-snapshot-entry.js",
"./packages/socket/index.js",
"./packages/socket/lib/cdp-socket.ts",
"./packages/socket/lib/socket.ts",
"./packages/socket/cjs/node/cdp-socket.js",
"./packages/socket/cjs/node/socket.js",
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/browser.js",
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/index.js",
"./packages/socket/node_modules/socket.io/dist/broadcast-operator.js",
@@ -4090,7 +4090,6 @@
"./packages/server/lib/cloud/upload/stream_activity_monitor.ts",
"./packages/server/lib/cloud/upload/stream_stalled_error.ts",
"./packages/server/lib/cohorts.ts",
"./packages/server/lib/controllers/client.ts",
"./packages/server/lib/controllers/files.ts",
"./packages/server/lib/controllers/iframes.ts",
"./packages/server/lib/controllers/runner.ts",
@@ -4288,7 +4287,8 @@
"./packages/server/node_modules/whatwg-url/lib/public-api.js",
"./packages/server/node_modules/whatwg-url/lib/url-state-machine.js",
"./packages/server/node_modules/whatwg-url/lib/utils.js",
"./packages/socket/lib/utils.ts",
"./packages/socket/cjs/node/index.js",
"./packages/socket/cjs/utils.js",
"./packages/socket/node_modules/engine.io-parser/lib/commons.js",
"./packages/socket/node_modules/engine.io-parser/lib/decodePacket.js",
"./packages/socket/node_modules/engine.io-parser/lib/encodePacket.js",

View File

@@ -827,6 +827,7 @@
"./packages/server/lib/cloud/studio/StudioLifecycleManager.ts",
"./packages/server/lib/cloud/user.ts",
"./packages/server/lib/config.ts",
"./packages/server/lib/controllers/client.ts",
"./packages/server/lib/cypress.ts",
"./packages/server/lib/environment.js",
"./packages/server/lib/modes/record.ts",
@@ -905,9 +906,8 @@
"./packages/server/node_modules/tough-cookie/lib/cookie.js",
"./packages/server/start-cypress.js",
"./packages/server/v8-snapshot-entry.js",
"./packages/socket/index.js",
"./packages/socket/lib/cdp-socket.ts",
"./packages/socket/lib/socket.ts",
"./packages/socket/cjs/node/cdp-socket.js",
"./packages/socket/cjs/node/socket.js",
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/browser.js",
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/index.js",
"./packages/socket/node_modules/socket.io/dist/broadcast-operator.js",
@@ -4090,7 +4090,6 @@
"./packages/server/lib/cloud/upload/stream_activity_monitor.ts",
"./packages/server/lib/cloud/upload/stream_stalled_error.ts",
"./packages/server/lib/cohorts.ts",
"./packages/server/lib/controllers/client.ts",
"./packages/server/lib/controllers/files.ts",
"./packages/server/lib/controllers/iframes.ts",
"./packages/server/lib/controllers/runner.ts",
@@ -4288,7 +4287,8 @@
"./packages/server/node_modules/whatwg-url/lib/public-api.js",
"./packages/server/node_modules/whatwg-url/lib/url-state-machine.js",
"./packages/server/node_modules/whatwg-url/lib/utils.js",
"./packages/socket/lib/utils.ts",
"./packages/socket/cjs/node/index.js",
"./packages/socket/cjs/utils.js",
"./packages/socket/node_modules/engine.io-parser/lib/commons.js",
"./packages/socket/node_modules/engine.io-parser/lib/decodePacket.js",
"./packages/socket/node_modules/engine.io-parser/lib/encodePacket.js",

View File

@@ -3047,6 +3047,11 @@
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
"@discoveryjs/json-ext@^0.6.1":
version "0.6.3"
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz#f13c7c205915eb91ae54c557f5e92bddd8be0e83"
integrity sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==
"@electron/asar@^3.2.13", "@electron/asar@^3.2.7":
version "3.4.1"
resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.4.1.tgz#4e9196a4b54fba18c56cd8d5cac67c5bdc588065"
@@ -9017,6 +9022,11 @@
resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52"
integrity sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=
"@types/webextension-polyfill@0.12.4":
version "0.12.4"
resolved "https://registry.yarnpkg.com/@types/webextension-polyfill/-/webextension-polyfill-0.12.4.tgz#d111b76e1ebf421fb64244598453bf44763a0266"
integrity sha512-wK8YdSI0pDiaehSLDIvtvonYmLwUUivg4Z6JCJO8rkyssMAG82cFJgwPK/V7NO61mJBLg/tXeoXQL8AFzpXZmQ==
"@types/webpack-bundle-analyzer@4.7.0":
version "4.7.0"
resolved "https://registry.npmjs.org/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz#fe199e724ce3d38705f6f1ba4d62429b7c360541"
@@ -10166,16 +10176,31 @@
resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646"
integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==
"@webpack-cli/configtest@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-3.0.1.tgz#76ac285b9658fa642ce238c276264589aa2b6b57"
integrity sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==
"@webpack-cli/info@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd"
integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==
"@webpack-cli/info@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-3.0.1.tgz#3cff37fabb7d4ecaab6a8a4757d3826cf5888c63"
integrity sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==
"@webpack-cli/serve@^2.0.5":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e"
integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==
"@webpack-cli/serve@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-3.0.1.tgz#bd8b1f824d57e30faa19eb78e4c0951056f72f00"
integrity sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==
"@xmldom/xmldom@^0.8.8":
version "0.8.10"
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99"
@@ -13400,6 +13425,11 @@ commander@^10.0.0, commander@^10.0.1:
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
commander@^12.1.0:
version "12.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3"
integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==
commander@^14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-14.0.0.tgz#f244fc74a92343514e56229f16ef5c5e22ced5e9"
@@ -15691,11 +15721,16 @@ env-paths@^3.0.0:
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-3.0.0.tgz#2f1e89c2f6dbd3408e1b1711dd82d62e317f58da"
integrity sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==
envinfo@7.13.0, envinfo@^7.7.3:
envinfo@7.13.0:
version "7.13.0"
resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.13.0.tgz#81fbb81e5da35d74e814941aeab7c325a606fb31"
integrity sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==
envinfo@^7.14.0, envinfo@^7.7.3:
version "7.18.0"
resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.18.0.tgz#38793d9dab9a5dec7b2a3146ed094cda8e754ed8"
integrity sha512-02QGCLRW+Jb8PC270ic02lat+N57iBaWsvHjcJViqp6UVupRB+Vsg7brYPTqEFXvsdTql3KnSczv5ModZFpl8Q==
environment@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz#8e86c66b180f363c7ab311787e0259665f45a9f1"
@@ -15727,11 +15762,6 @@ enzyme-adapter-utils@^1.11.0:
prop-types "^15.7.2"
semver "^5.7.1"
eol@0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/eol/-/eol-0.10.0.tgz#51b35c6b9aa0329a26d102b6ddc454be8654739b"
integrity sha512-+w3ktYrOphcIqC1XKmhQYvM+o2uxgQFiimL7B6JPZJlWVxf7Lno9e/JWLPIgbHo7DoZ+b7jsf/NzrUcNe6ZTZQ==
err-code@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
@@ -33303,6 +33333,25 @@ webpack-cli@^5.1.4:
rechoir "^0.8.0"
webpack-merge "^5.7.3"
webpack-cli@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-6.0.1.tgz#a1ce25da5ba077151afd73adfa12e208e5089207"
integrity sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==
dependencies:
"@discoveryjs/json-ext" "^0.6.1"
"@webpack-cli/configtest" "^3.0.1"
"@webpack-cli/info" "^3.0.1"
"@webpack-cli/serve" "^3.0.1"
colorette "^2.0.14"
commander "^12.1.0"
cross-spawn "^7.0.3"
envinfo "^7.14.0"
fastest-levenshtein "^1.0.12"
import-local "^3.0.2"
interpret "^3.1.1"
rechoir "^0.8.0"
webpack-merge "^6.0.1"
webpack-dev-middleware@^7.4.2:
version "7.4.2"
resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz#40e265a3d3d26795585cff8207630d3a8ff05877"
@@ -33367,6 +33416,15 @@ webpack-merge@^5.4.0, webpack-merge@^5.7.3:
clone-deep "^4.0.1"
wildcard "^2.0.0"
webpack-merge@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-6.0.1.tgz#50c776868e080574725abc5869bd6e4ef0a16c6a"
integrity sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==
dependencies:
clone-deep "^4.0.1"
flat "^5.0.2"
wildcard "^2.0.1"
webpack-sources@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
@@ -33629,10 +33687,10 @@ widest-line@^4.0.1:
dependencies:
string-width "^5.0.1"
wildcard@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
wildcard@^2.0.0, wildcard@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67"
integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==
win-version-info@^6.0.1:
version "6.0.1"