diff --git a/.circleci/src/pipeline/@pipeline.yml b/.circleci/src/pipeline/@pipeline.yml index 6f01dfd298c..8a8125567c8 100644 --- a/.circleci/src/pipeline/@pipeline.yml +++ b/.circleci/src/pipeline/@pipeline.yml @@ -1785,7 +1785,7 @@ jobs: source ./scripts/ensure-node.sh yarn lerna run types - sanitize-verify-and-store-mocha-results: - expectedResultCount: 5 + expectedResultCount: 4 verify-release-readiness: <<: *defaults diff --git a/guides/esm-migration.md b/guides/esm-migration.md index ca3212ad820..0617d4c683e 100644 --- a/guides/esm-migration.md +++ b/guides/esm-migration.md @@ -48,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** @@ -99,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** diff --git a/packages/extension/.gitignore b/packages/extension/.gitignore new file mode 100644 index 00000000000..b7364c5b387 --- /dev/null +++ b/packages/extension/.gitignore @@ -0,0 +1,2 @@ +app-dist/ +lib-dist/ \ No newline at end of file diff --git a/packages/extension/README.md b/packages/extension/README.md index ad1409fa7b1..3e63dd13552 100644 --- a/packages/extension/README.md +++ b/packages/extension/README.md @@ -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 ``` diff --git a/packages/extension/app/v2/background.js b/packages/extension/app/v2/background.ts similarity index 72% rename from packages/extension/app/v2/background.js rename to packages/extension/app/v2/background.ts index c7d91c5acc1..ac33e2533cf 100644 --- a/packages/extension/app/v2/background.js +++ b/packages/extension/app/v2/background.ts @@ -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 diff --git a/packages/extension/app/v2/client.ts b/packages/extension/app/v2/client.ts index fadcab51ce9..d87e59e5baf 100644 --- a/packages/extension/app/v2/client.ts +++ b/packages/extension/app/v2/client.ts @@ -1,6 +1,6 @@ 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'], diff --git a/packages/extension/app/v2/init.js b/packages/extension/app/v2/init.ts similarity index 53% rename from packages/extension/app/v2/init.js rename to packages/extension/app/v2/init.ts index 8652449260a..04446352438 100644 --- a/packages/extension/app/v2/init.js +++ b/packages/extension/app/v2/init.ts @@ -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) diff --git a/packages/extension/app/v3/content.js b/packages/extension/app/v3/content.ts similarity index 95% rename from packages/extension/app/v3/content.js rename to packages/extension/app/v3/content.ts index 8c34f18b27d..19f1e21698e 100644 --- a/packages/extension/app/v3/content.js +++ b/packages/extension/app/v3/content.ts @@ -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 diff --git a/packages/extension/app/v3/service-worker.js b/packages/extension/app/v3/service-worker.ts similarity index 83% rename from packages/extension/app/v3/service-worker.js rename to packages/extension/app/v3/service-worker.ts index cbc47dc5361..26205daaf38 100644 --- a/packages/extension/app/v3/service-worker.js +++ b/packages/extension/app/v3/service-worker.ts @@ -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() diff --git a/packages/extension/gulpfile.ts b/packages/extension/gulpfile.ts index ae0a2578930..e485bee648a 100644 --- a/packages/extension/gulpfile.ts +++ b/packages/extension/gulpfile.ts @@ -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 { + const removedAppDist = await rimraf('app-dist') + const removedLibDist = await rimraf('lib-dist') - return require('@packages/icons') -} - -function clean (): Promise { - 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) diff --git a/packages/extension/index.d.ts b/packages/extension/index.d.ts deleted file mode 100644 index 902c9e19955..00000000000 --- a/packages/extension/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// - -declare const lib: typeof import('./lib/extension') - -export default lib \ No newline at end of file diff --git a/packages/extension/index.js b/packages/extension/index.js deleted file mode 100644 index 35ee4527ed0..00000000000 --- a/packages/extension/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./lib/extension') diff --git a/packages/extension/lib/extension.js b/packages/extension/lib/extension.js deleted file mode 100644 index 75612fe7908..00000000000 --- a/packages/extension/lib/extension.js +++ /dev/null @@ -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, - -} diff --git a/packages/extension/lib/index.ts b/packages/extension/lib/index.ts new file mode 100644 index 00000000000..df2007342dd --- /dev/null +++ b/packages/extension/lib/index.ts @@ -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) +} diff --git a/packages/extension/lib/util.js b/packages/extension/lib/util.js deleted file mode 100644 index 20f6a0b9fe6..00000000000 --- a/packages/extension/lib/util.js +++ /dev/null @@ -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 || '') - }, -} diff --git a/packages/extension/package.json b/packages/extension/package.json index 350ed4dc5f3..5ce66af79b9 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -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": { diff --git a/packages/extension/test/integration/v2/background.spec.ts b/packages/extension/test/integration/v2/background.spec.ts new file mode 100644 index 00000000000..8a92347605d --- /dev/null +++ b/packages/extension/test/integration/v2/background.spec.ts @@ -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) => Promise + + 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((resolve) => setTimeout(resolve, 1)) + + return ws + } + + return new Promise((resolve) => { + httpSrv.listen(PORT, resolve) + }) + }) + + afterEach(function () { + server.close() + + return new Promise((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('../../../app/v2/client') + + vi.mocked(connect).mockImplementation(connectActual) + + await new Promise((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((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') + }) + }) + }) + }) +}) diff --git a/packages/extension/test/integration/v2/background_spec.js b/packages/extension/test/integration/v2/background_spec.js deleted file mode 100644 index 6eb2ce06b9e..00000000000 --- a/packages/extension/test/integration/v2/background_spec.js +++ /dev/null @@ -1,244 +0,0 @@ -/* tslint:disable:no-empty */ -require('../../spec_helper') -const _ = require('lodash') -const http = require('http') -const { SocketIOServer } = 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 = new SocketIOServer(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') - }) - }) - }) -}) diff --git a/packages/extension/test/integration/v3/content.spec.ts b/packages/extension/test/integration/v3/content.spec.ts new file mode 100644 index 00000000000..2e758f59fa6 --- /dev/null +++ b/packages/extension/test/integration/v3/content.spec.ts @@ -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() + }) + }) +}) diff --git a/packages/extension/test/integration/v3/content_spec.js b/packages/extension/test/integration/v3/content_spec.js deleted file mode 100644 index 8816a6c652d..00000000000 --- a/packages/extension/test/integration/v3/content_spec.js +++ /dev/null @@ -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 - }) - }) -}) diff --git a/packages/extension/test/integration/v3/service-worker.spec.ts b/packages/extension/test/integration/v3/service-worker.spec.ts new file mode 100644 index 00000000000..e3a75afc4d0 --- /dev/null +++ b/packages/extension/test/integration/v3/service-worker.spec.ts @@ -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() + }) + }) +}) diff --git a/packages/extension/test/integration/v3/service-worker_spec.js b/packages/extension/test/integration/v3/service-worker_spec.js deleted file mode 100644 index 3591f4c0e89..00000000000 --- a/packages/extension/test/integration/v3/service-worker_spec.js +++ /dev/null @@ -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 - }) - }) -}) diff --git a/packages/extension/test/mocha.opts b/packages/extension/test/mocha.opts deleted file mode 100644 index 831ae85fda9..00000000000 --- a/packages/extension/test/mocha.opts +++ /dev/null @@ -1,4 +0,0 @@ -test/unit -test/integration ---reporter spec ---recursive diff --git a/packages/extension/test/spec_helper.js b/packages/extension/test/spec_helper.js deleted file mode 100644 index 020b86c5133..00000000000 --- a/packages/extension/test/spec_helper.js +++ /dev/null @@ -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() -}) diff --git a/packages/extension/test/unit/extension.spec.ts b/packages/extension/test/unit/extension.spec.ts new file mode 100644 index 00000000000..8142428924d --- /dev/null +++ b/packages/extension/test/unit/extension.spec.ts @@ -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('../../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') + }) + }) +}) diff --git a/packages/extension/test/unit/extension_spec.js b/packages/extension/test/unit/extension_spec.js deleted file mode 100644 index 3f3d518f1ce..00000000000 --- a/packages/extension/test/unit/extension_spec.js +++ /dev/null @@ -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') - }) - }) - }) - }) -}) diff --git a/packages/extension/tsconfig.app.v2.json b/packages/extension/tsconfig.app.v2.json new file mode 100644 index 00000000000..3fb598fc186 --- /dev/null +++ b/packages/extension/tsconfig.app.v2.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "app/v2/**/*.ts" + ], + "compilerOptions": { + "rootDir": "./app/v2", + "outDir": "./app-dist/v2", + "target": "ES2022", + "module": "Es2022" + } +} \ No newline at end of file diff --git a/packages/extension/tsconfig.app.v3.json b/packages/extension/tsconfig.app.v3.json new file mode 100644 index 00000000000..f1f908d5b2d --- /dev/null +++ b/packages/extension/tsconfig.app.v3.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "app/v3**/*.ts" + ], + "compilerOptions": { + "rootDir": "./app/v3", + "outDir": "./app-dist/v3", + "target": "ES2022", + "module": "ES2022" + } +} \ No newline at end of file diff --git a/packages/extension/tsconfig.json b/packages/extension/tsconfig.json index 37184bd1e8a..ea5dab675ce 100644 --- a/packages/extension/tsconfig.json +++ b/packages/extension/tsconfig.json @@ -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" + ] } -} +} \ No newline at end of file diff --git a/packages/extension/tsconfig.lib.json b/packages/extension/tsconfig.lib.json new file mode 100644 index 00000000000..bb8a49c0c21 --- /dev/null +++ b/packages/extension/tsconfig.lib.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "lib/**/*.ts" + ], + "compilerOptions": { + "declaration": true, + "rootDir": "./lib", + "outDir": "./lib-dist", + "target": "ES2022", + "module": "CommonJS" + } +} \ No newline at end of file diff --git a/packages/extension/vitest.config.ts b/packages/extension/vitest.config.ts new file mode 100644 index 00000000000..1a9a321880f --- /dev/null +++ b/packages/extension/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/**/*.spec.ts'], + globals: true, + environment: 'node', + }, +}) diff --git a/packages/extension/webpack.config.js b/packages/extension/webpack.config.mjs similarity index 69% rename from packages/extension/webpack.config.js rename to packages/extension/webpack.config.mjs index 7dc314f78ed..9d9002832a5 100644 --- a/packages/extension/webpack.config.js +++ b/packages/extension/webpack.config.mjs @@ -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({ diff --git a/packages/proxy/lib/http/error-middleware.ts b/packages/proxy/lib/http/error-middleware.ts index 5331bd0f830..2610efb8d69 100644 --- a/packages/proxy/lib/http/error-middleware.ts +++ b/packages/proxy/lib/http/error-middleware.ts @@ -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' diff --git a/packages/server/lib/automation/cookies.ts b/packages/server/lib/automation/cookies.ts index ffd07b21442..3814323aeb4 100644 --- a/packages/server/lib/automation/cookies.ts +++ b/packages/server/lib/automation/cookies.ts @@ -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 }) diff --git a/packages/server/lib/browsers/chrome.ts b/packages/server/lib/browsers/chrome.ts index eaeb5069b54..a0d3e1ed372 100644 --- a/packages/server/lib/browsers/chrome.ts +++ b/packages/server/lib/browsers/chrome.ts @@ -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' diff --git a/packages/server/test/unit/browsers/firefox_spec.ts b/packages/server/test/unit/browsers/firefox_spec.ts index 49f8ca5faa9..23f30f742a0 100644 --- a/packages/server/test/unit/browsers/firefox_spec.ts +++ b/packages/server/test/unit/browsers/firefox_spec.ts @@ -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, }), diff --git a/yarn.lock b/yarn.lock index c8218e81387..44a87b0419c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -33298,6 +33328,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" @@ -33362,6 +33411,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" @@ -33624,10 +33682,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"