Merge branch 'develop' into feature-multidomain

This commit is contained in:
Chris Breiding
2021-06-22 09:58:10 -04:00
148 changed files with 1887 additions and 3341 deletions

View File

@@ -61,6 +61,11 @@ You can build the Cypress binary locally by running `yarn binary-build`. You can
ZENHUB_API_TOKEN="..."
```
- The `cypress-bot` GitHub app credentials are also needed. Ask another team member who has done a deploy for those.
- For purging the Cloudflare cache (part of the `move-binaries` step), you'll need `CF_ZONEID` and `CF_TOKEN` set. These can be found in 1Password. If you don't have access, ask a team member who has done a deploy.
```text
CF_ZONEID="..."
CF_TOKEN="..."
```
- Tip: Use [as-a](https://github.com/bahmutov/as-a) to manage environment variables for different situations.
### Before Publishing a New Version

View File

@@ -2,9 +2,8 @@ branches:
only:
- master
- develop
- 7.0-release
- windows-code-signing
- /win*/
- feature/cross-platform-wizard
# https://www.appveyor.com/docs/lang/nodejs-iojs/
environment:
@@ -32,6 +31,10 @@ environment:
secure: tAoqu4zIgZUxOfW0u9YQgw==
GH_PRIVATE_KEY:
secure: msLmlIBnkNovqrqTeCqa7ZPjETyS8Xn4JLuiRMWYK7gZBTO66pNnFaoeqwPFwH+ooO0cDFhAOPTToLisgTLXCo4hnw38zuBuKq+ywCh5mtk5uZn4x4F8G2XyRLD/ViZm+VuD2yZzaTWF11upDqC4xbXDe32yD6OSLKhA5ms5F5ke83zEuWSLTqVVCIpVH12rVTJHl3QHaWPwZbBBE3SFN8D6uiclvI06y3pEg2bVShU8YqlwearYTRuErsYXNCUmT0SrDd2kHznlYf08edQDHpydnQvvTViZMgomvYp5wDCXFD+/FxtTMuTptJFpspirXL8w/xjYy1/JaTd/K01oUUD2Xwl/v0cS28OpdcraETyrQxQhEgTCXfg9ONbZ5mRvQlkaRROaTqDSGMmEPs4N91zarpA7RLxu7PPvxXQcbDW4GiJvH5BhVWu8lY/QBZsr8It1dhLYSzTPNIh9ey8xNaUbZ3oQhPBoreRi36B+FSPBsrZpB8Q8aa97gd+lCa8br2RfaEpzx8gA0pSK44odqcGuJe7T8MHOqYo0cUEUb2UypPPG7mWyjGip+x3Z9P/vSrZzDV+YFFvEzQAMoyRMp/456V+YL8iduryMRIadkJcB4ZVZz2hsxY5Gv6Eeh9NhwzyM64Rz5NP5fJ9Kw8E5Vm+ddEmft8Ec6dajcURoVN0i+s8t7h/e3Hzrr62UjWr0FpUx5fPBC/Tldn3+h4Rr9/HFI2RCZAI5wHOrx/aQ/HknA9UCEdqdod8ix5yAdSpTxp3aCGEoS97STXU43CjLEiQFyLaReoHOOwFp5EqaAiAqiORJaKuShWoir+OqSk7rucU7kFvIlU9GDfLuKUpxcQoDq/8fKT3lcG3Pr4MVV79BJ6EcjcsEf4ukQ3IfwMY+2RbwYWEowsQP18k4HztZpMEOuYPlSCiAPL7Cz4dcE5oybSURr9QQbSqVMoiCKZBn344KxpvH59KW90wt8CYyoeLSlPpM9s73g9My4fwbB3W9lcbw/AteRGer01VYEHY+1MyQwhqgHoXQ//op4gztFbpSLcli88v1IOopcr0Dw5NrylcjCTKuVWmQs0uIAfOr7zxqCZ8DCXG6spdipjF1jx+bxp318ZgH56pmmTOTMbj5Cmdpr3KlCFbYB4JI7lexnZmti1NcHtOglDSq+XT4092myAiarSzQLA6smB+gk68M50W492+QNuc+6LAOfev+Da4geLiErqMpuIqfA3jw4h5+9Ns6mf3JnOLZd1c/X/xvnV3JjBzSJ6f9xGMLBcMTQm/wVfkHM9tO1oZrHswDiBlE1AkQrj6kqT9Kznu/rbAUGRnWL65FoCwdMbYVEhQQvLbLvVCRGBJfB01oD2xs80jyZ2YYZFRZCl/d0lGrVVVZsq6XM7CsxR5WlpJy5JLxCQ4kliG8cjexh0GkVYJoRYneJifw8yThMlyAnMQ88iNS2p2MnYk0WZgTJOIHliIhPRFY4z6BtrxmL8SR1no1vhaQCdbE5RI/rYbk8NpOmQunkjcDwp7nTKn1d8bMTfKGUH+DzhvmqwxA5PW37P84FFSK+3ePY9+oKXcInkAaxiXUpzcZJ4KzUGEZaZCB6irU+sxs6QLDzsq05PprwVz2DGtEn1TcY8qQ6ezeMGxJMRgDvEGq2J0nEgOEZ98CJ7XiPJRlnvUjGUzBlcjnbfFH8zzl/0p189YtENhE6Fyr5bD9MAI6NpVHjLLlg3yjmQ6X95fUtiNCmSpCUveEqIQCRtHCY2E/RrulGqTWE+vCvbM6IJV3WnatPOtWZfXEntWHmS08j6aUkUDM9TodBuzG8TRhW2Kgv8b4pfoejuMa4WkvwRAUU7V+clTWG26dT9UHdk+QuOIQDUiCewWk3PmpIJI4WdcxpBWwDvIgojob7uaGzhkabFKi77RJRc5/Ulxm6yM2MX79jgJxrQprWxxkjlsQnJk186nQZQqpuwziH/ZxV82n1bmI9zCqMXgE1Yr86gvyZpk2UbWhlFdtXEPapge9Cfo/fWUBCIbVcd77Bk98E88Y5Y372YWW+D8oHZed8l+0tCeyZmoHQNCYykcf6w77C+8C+bVdJplPns96vyLgbWIr0cpqZBK4qmkAxHuKZoG0AKRw4U379lnXOsI+02TaTzGOMlFTg4ME5miCbxo/2pUnjrydyTE5evdImLzKAK50Fhy1XASaPxgLrkjhGZebwf1UD2kYg6A1NCHchQId25vSEwGRkMPWvY3a5KOmgsMmRoOUJ17uo/r57p7nLgZV9c1+YEdZxu+GmgwQDLNGpgW1cpEN6GSVpx8xhaGKeYSuqd4lh6H9U5/P8masNckrsz+EHv+w5plzx8nJ/Fx/H50OdOm1KUjo66m26aITX7EjJB/U1qtqNfiK6dt8EttJ5iRXlCbfOkj2biRYeKbXQ2Ezr+61/Mu/W/nhLqmLFDtM6K3xf2bSJnEXQFZOOXTRkKXnRDP7Y47ZgG3563fJQjSfoU4Hsw5xnegTOKlJsoEm95Rnq0esdMTA450Ki2wBOeIsOycljoApACBYLAlSe+ewxEaOjrLtnIR0LfzcKXlCRYbM31YWOCtMhMRehJbX9qWGNPTQHmjabYz7/IhLKtJuaMIpj3pfYgS/oQQ36g6ItCo7vLQAq+rgU99IUyQROOGXMUgK/8umL71oijA9dht0LmH9E7EGwih0WuLO2SndovTJODDfK9YrRTEocbo3B9S05O4fpGoQ32TK99mXjoQdlyxd/dn9Q9uDD27u/fGgUoYdt9VzAIigbRIQuRx430n33V0ZyXv90QuD4ESOLxVI1vnLj6JKAS4PGRz66rouYG6U+1syDWpf5Y6DzC/2KOfdLPwmuwjMQxuhf+6+tGeJbeotNX/eJF0LkRfyieRwEGKxIo0PaxdmVwsF7vKR6ZnOpr5BuLm/+44Rg3bQdJ4bcRW6i6dIhOyHWniLvsAPLu1NZDVN6jA13KTChhcrNnSGddjRFLekawl80E3KhG1p+KvItIZX3kzG4QjJ
CSC_KEY_PASSWORD:
secure: GiXelhGGKXKUNW6T7ptKUw==
CSC_LINK:
secure: 9uSZwUYwcdZejLTpGpySd6t9JSL1Hw3iTvb4T2HZrx6iKd5DSR7AN6A7lS5ThTZ6g1JNSypSHRwDeC1Z5xkP8QEIjDqKjyNrqC19gCiSMrpdjjIR8Y8upIISrDBWjOiI
platform:
- x64

View File

@@ -1,4 +1,4 @@
{
"chrome:beta": "92.0.4515.51",
"chrome:stable": "91.0.4472.101"
"chrome:beta": "92.0.4515.59",
"chrome:stable": "91.0.4472.114"
}

View File

@@ -36,7 +36,8 @@
"commander": "^5.1.0",
"common-tags": "^1.8.0",
"dayjs": "^1.10.4",
"debug": "4.3.2",
"debug": "^4.3.2",
"enquirer": "^2.3.6",
"eventemitter2": "^6.4.3",
"execa": "4.1.0",
"executable": "^4.1.1",

View File

@@ -490,6 +490,13 @@ declare namespace Cypress {
getElementCoordinatesByPositionRelativeToXY(element: JQuery | HTMLElement, x: number, y: number): ElementPositioning
}
/**
* @see https://on.cypress.io/keyboard-api
*/
Keyboard: {
defaults(options: Partial<KeyboardDefaultsOptions>): void
}
/**
* @see https://on.cypress.io/api/api-server
*/
@@ -2786,6 +2793,7 @@ declare namespace Cypress {
interface TestConfigOverrides extends Partial<Pick<ConfigOptions, 'animationDistanceThreshold' | 'baseUrl' | 'defaultCommandTimeout' | 'env' | 'execTimeout' | 'includeShadowDom' | 'requestTimeout' | 'responseTimeout' | 'retries' | 'scrollBehavior' | 'taskTimeout' | 'viewportHeight' | 'viewportWidth' | 'waitForAnimations'>> {
browser?: IsBrowserMatcher | IsBrowserMatcher[]
keystrokeDelay?: number
}
/**
@@ -2836,6 +2844,18 @@ declare namespace Cypress {
env: object
}
/**
* Options for Cypress.Keyboard.defaults()
*/
interface KeyboardDefaultsOptions {
/**
* Time, in milliseconds, between each keystroke when typing. (Pass 0 to disable)
*
* @default 10
*/
keystrokeDelay: number
}
/**
* Full set of possible options for cy.request call
*/

View File

@@ -592,17 +592,19 @@ namespace CypressTestConfigOverridesTests {
browser: [{name: 'firefox'}, {name: 'chrome'}]
}, () => {})
it('test', {
browser: 'firefox'
browser: 'firefox',
keystrokeDelay: 0
}, () => {})
it('test', {
browser: {foo: 'bar'} // $ExpectError
browser: {foo: 'bar'}, // $ExpectError
}, () => {})
it('test', {
retries: null
retries: null,
keystrokeDelay: 0
}, () => { })
it('test', {
retries: 3
retries: 3,
keystrokeDelay: false, // $ExpectError
}, () => { })
it('test', {
retries: {
@@ -631,14 +633,16 @@ namespace CypressTestConfigOverridesTests {
// set config on a per-suite basis
describe('suite', {
browser: {family: 'firefox'},
baseUrl: 'www.example.com'
baseUrl: 'www.example.com',
keystrokeDelay: 0
}, () => {})
context('suite', {}, () => {})
describe('suite', {
browser: {family: 'firefox'},
baseUrl: 'www.example.com'
baseUrl: 'www.example.com',
keystrokeDelay: false // $ExpectError
foo: 'foo' // $ExpectError
}, () => {})
@@ -672,3 +676,18 @@ namespace CypressTaskTests {
val // $ExpectType unknown
})
}
namespace CypressKeyboardTests {
Cypress.Keyboard.defaults({
keystrokeDelay: 0
})
Cypress.Keyboard.defaults({
keystrokeDelay: 500
})
Cypress.Keyboard.defaults({
keystrokeDelay: false // $ExpectError
})
Cypress.Keyboard.defaults({
delay: 500 // $ExpectError
})
}

View File

@@ -1,3 +1,10 @@
# [create-cypress-tests-v1.1.2](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.1.1...create-cypress-tests-v1.1.2) (2021-06-17)
### Bug Fixes
* case issue create cypress tests with `react/plugins/load-webpack` ([#16961](https://github.com/cypress-io/cypress/issues/16961)) ([c37ecea](https://github.com/cypress-io/cypress/commit/c37ecea3ca462015637515b331d1c9828ac1ed29)), closes [#16960](https://github.com/cypress-io/cypress/issues/16960)
# [create-cypress-tests-v1.1.1](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.1.0...create-cypress-tests-v1.1.1) (2021-05-10)

View File

@@ -7,7 +7,7 @@ module.exports = (on, config) => {
if (config.testingType === "component") {
injectDevServer(on, config, {
// TODO replace with valid webpack config path
webpackFileName: './webpack.config.js'
webpackFilename: './webpack.config.js'
});
}
@@ -23,7 +23,7 @@ const something = require("something");
module.exports = (on, config) => {
if (config.testingType === "component") {
injectDevServer(on, config, {
webpackFileName: 'config/webpack.config.js'
webpackFilename: 'config/webpack.config.js'
});
}

View File

@@ -25,7 +25,7 @@ export const WebpackTemplate: Template<{ webpackConfigPath: string }> = {
includeWarnComment
? ' // TODO replace with valid webpack config path'
: '',
` webpackFileName: '${webpackConfigPath}'`,
` webpackFilename: '${webpackConfigPath}'`,
'})',
].join('\n'), { preserveComments: true }),
}

View File

@@ -1,3 +1,11 @@
# [@cypress/schematic-v1.3.1](https://github.com/cypress-io/cypress/compare/@cypress/schematic-v1.3.0...@cypress/schematic-v1.3.1) (2021-06-17)
### Bug Fixes
* ensure schematic is installed as devDependency ([#16965](https://github.com/cypress-io/cypress/issues/16965)) ([b49fcaf](https://github.com/cypress-io/cypress/commit/b49fcaf9cfc929313ed681248f6ca9c0a0bdf8c5))
* **angular:** set rxjs versions > 6.6.0 as dependency ([#16676](https://github.com/cypress-io/cypress/issues/16676)) ([46de81e](https://github.com/cypress-io/cypress/commit/46de81e75fd18bc37cb884e9a751106fff4d08ad))
# [@cypress/schematic-v1.3.0](https://github.com/cypress-io/cypress/compare/@cypress/schematic-v1.2.0...@cypress/schematic-v1.3.0) (2021-05-26)

View File

@@ -56,5 +56,8 @@
"registry": "http://registry.npmjs.org/"
},
"builders": "./src/builders/builders.json",
"ng-add": {
"save": "devDependencies"
},
"schematics": "./src/schematics/collection.json"
}

View File

@@ -1,3 +1,10 @@
# [@cypress/vite-dev-server-v2.0.1](https://github.com/cypress-io/cypress/compare/@cypress/vite-dev-server-v2.0.0...@cypress/vite-dev-server-v2.0.1) (2021-06-18)
### Bug Fixes
* vite startDevServer needs to return close() ([#16950](https://github.com/cypress-io/cypress/issues/16950)) ([67b2b3b](https://github.com/cypress-io/cypress/commit/67b2b3b9be13437e56384e377c7d32c6e433e064))
# [@cypress/vite-dev-server-v2.0.0](https://github.com/cypress-io/cypress/compare/@cypress/vite-dev-server-v1.2.7...@cypress/vite-dev-server-v2.0.0) (2021-05-31)

View File

@@ -1,13 +1,14 @@
import { debug as debugFn } from 'debug'
import { Server } from 'http'
import { start as createDevServer, StartDevServer } from './startServer'
const debug = debugFn('cypress:vite-dev-server:vite')
export { StartDevServer }
type DoneCallback = () => unknown
export interface ResolvedDevServerConfig {
port: number
server: Server
close: (done?: DoneCallback) => void
}
export async function startDevServer (startDevServerArgs: StartDevServer): Promise<ResolvedDevServerConfig> {
@@ -18,5 +19,5 @@ export async function startDevServer (startDevServerArgs: StartDevServer): Promi
debug('Component testing vite server started on port', port)
return { port, server: app.httpServer }
return { port, close: app.httpServer.close }
}

View File

@@ -1,7 +1,12 @@
module.exports = {
...require('../../.releaserc.base'),
branches: [
{ name: 'npm/vue/v2', range: '2.x' },
// we need to keep this branch in here even if no used because semantic-release demands
// that we have at least one branch that has no config
'next/npm/vue',
// this line forces releasing 2.X releases on the latest channel
{ name: 'npm/vue/v2', range: '2.X.X' },
// this one releases v3 on master as beta on the next channel
{ name: 'master', channel: 'next', prerelease: 'beta' },
],
}

View File

@@ -1,3 +1,10 @@
# [@cypress/vue-v3.0.0-beta.2](https://github.com/cypress-io/cypress/compare/@cypress/vue-v3.0.0-beta.1...@cypress/vue-v3.0.0-beta.2) (2021-06-17)
### Bug Fixes
* add latest channel to properly release npm packages ([#16994](https://github.com/cypress-io/cypress/issues/16994)) ([ac16efc](https://github.com/cypress-io/cypress/commit/ac16efca80f33e12153b0c2bd0fc3f04983ed305))
# [@cypress/vue-v3.0.0-beta.1](https://github.com/cypress-io/cypress/compare/@cypress/vue-v2.2.3...@cypress/vue-v3.0.0-beta.1) (2021-05-31)

View File

@@ -0,0 +1,6 @@
{
"__description__": "This is here to prove that babel.config.json does not get used by babel-loader. It's not used for the compilation of this project.",
"plugins": [
"doesnt-exist"
]
}

View File

@@ -97,6 +97,8 @@ const getDefaultWebpackOptions = () => {
[require.resolve('@babel/preset-env'), { modules: 'commonjs', targets: { 'chrome': '64' } }],
require.resolve('@babel/preset-react'),
],
configFile: false,
babelrc: false,
},
}],
}, {

View File

@@ -0,0 +1,6 @@
{
"__description__": "This is here to prove that .babelrc does not get used by babel-loader. It's not used for the compilation of this project.",
"plugins": [
"doesnt-exist"
]
}

View File

@@ -1,3 +1,10 @@
# [@cypress/webpack-dev-server-v1.4.0](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v1.3.1...@cypress/webpack-dev-server-v1.4.0) (2021-06-17)
### Features
* **npm/webpack-dev-server,runner-ct:** Normalize webpack errors + general React/TS improvements ([#16613](https://github.com/cypress-io/cypress/issues/16613)) ([c0fc23a](https://github.com/cypress-io/cypress/commit/c0fc23a052e53354a8300dd3f783cb161ae161e1))
# [@cypress/webpack-dev-server-v1.3.1](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v1.3.0...@cypress/webpack-dev-server-v1.3.1) (2021-05-26)

File diff suppressed because one or more lines are too long

View File

@@ -13,6 +13,7 @@ const debugStats = require('debug')('cypress:webpack:stats')
type FilePath = string
interface BundleObject {
promise: Promise<FilePath>
deferreds: Array<{ resolve: (filePath: string) => void, reject: (error: Error) => void, promise: Promise<string> }>
initial: boolean
}
@@ -60,6 +61,11 @@ const cleanModuleNotFoundError = (err: Error) => {
if (!message.includes('Module not found')) return err
// Webpack 5 error messages are much less verbose. No need to clean.
if ('NormalModule' in webpack) {
return err
}
const startIndex = message.lastIndexOf('resolve ')
const endIndex = message.lastIndexOf(`doesn't exist`) + `doesn't exist`.length
const partToReplace = message.substring(startIndex, endIndex)
@@ -226,16 +232,14 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
const compiler = webpack(webpackOptions)
// we keep a reference to the latest bundle in this scope
// it's a deferred object that will be resolved or rejected in
// the `handle` function below and its promise is what is ultimately
// returned from this function
let latestBundle = createDeferred<string>()
let firstBundle = createDeferred<string>()
// cache the bundle promise, so it can be returned if this function
// is invoked again with the same filePath
bundles[filePath] = {
promise: latestBundle.promise,
promise: firstBundle.promise,
// we will resolve all reject everything in this array when a compile completes in the `handle` function
deferreds: [firstBundle],
initial: true,
}
@@ -247,7 +251,10 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
debug(`errored bundling ${outputPath}`, err.message)
latestBundle.reject(err)
const lastBundle = bundles[filePath].deferreds[bundles[filePath].deferreds.length - 1]
lastBundle.reject(err)
bundles[filePath].deferreds.length = 0
}
// this function is called when bundling is finished, once at the start
@@ -294,7 +301,15 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
// Seems to be a race condition where changing file before next tick
// does not cause build to rerun
Promise.delay(0).then(() => {
latestBundle.resolve(outputPath)
if (!bundles[filePath]) {
return
}
bundles[filePath].deferreds.forEach((deferred) => {
deferred.resolve(outputPath)
})
bundles[filePath].deferreds.length = 0
})
}
@@ -303,11 +318,10 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
const onCompile = () => {
debug('compile', filePath)
// we overwrite the latest bundle, so that a new call to this function
// returns a promise that resolves when the bundling is finished
latestBundle = createDeferred<string>()
bundles[filePath].promise = latestBundle.promise
const nextBundle = createDeferred<string>()
bundles[filePath].promise = nextBundle.promise
bundles[filePath].deferreds.push(nextBundle)
bundles[filePath].promise.finally(() => {
debug('- compile finished for %s, initial? %s', filePath, bundles[filePath].initial)
// when the bundling is finished, emit 'rerun' to let Cypress
@@ -335,7 +349,8 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
// so seems we just need to pass plugin.name
// @ts-ignore
compiler.hooks.compile.tap(plugin, onCompile)
} else {
} else if ('plugin' in compiler) {
// @ts-ignore
compiler.plugin('compile', onCompile)
}
}
@@ -380,7 +395,15 @@ preprocessor.__reset = () => {
bundles = {}
}
function cleanseError (err: string | Error) {
// for testing purposes, but do not add this to the typescript interface
// @ts-ignore
preprocessor.__bundles = () => {
return bundles
}
// @ts-ignore - webpack.StatsError is unique to webpack 5
// TODO: Remove this when we update to webpack 5.
function cleanseError (err: string | webpack.StatsError) {
let msg = typeof err === 'string' ? err : err.message
return msg.replace(/\n\s*at.*/g, '').replace(/From previous event:\n?/g, '')

View File

@@ -11,7 +11,7 @@
"secure": "nsp check",
"semantic-release": "semantic-release",
"size": "npm pack --dry",
"test": "yarn test-unit && yarn test-e2e",
"test": "node ./test-webpack-4-5.js",
"test-debug": "node --inspect --debug-brk ./node_modules/.bin/_mocha",
"test-e2e": "mocha test/e2e/*.spec.*",
"test-unit": "mocha test/unit/*.spec.*",
@@ -29,7 +29,7 @@
"@babel/plugin-proposal-nullish-coalescing-operator": "7.8.3",
"@babel/preset-env": "^7.0.0",
"@fellow/eslint-plugin-coffee": "0.4.13",
"@types/webpack": "4.41.12",
"@types/webpack": "^4.41.12",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"babel-loader": "^8.0.2",
@@ -58,13 +58,13 @@
"sinon-chai": "^3.5.0",
"snap-shot-it": "7.9.2",
"ts-node": "8.10.1",
"webpack": "^4.18.1"
"webpack": "^4.41.12"
},
"peerDependencies": {
"@babel/core": "^7.0.1",
"@babel/preset-env": "^7.0.0",
"babel-loader": "^8.0.2",
"webpack": "^4.18.1"
"webpack": "^4 || ^5"
},
"files": [
"dist"
@@ -83,4 +83,4 @@
"cypress-preprocessor",
"webpack"
]
}
}

View File

@@ -0,0 +1,57 @@
const execa = require('execa')
const pkg = require('./package.json')
const fs = require('fs')
/**
* This file installs Webpack 5 and runs the unit and e2e tests for the preprocessor.
* We read package.json, update the webpack version, then re-run yarn install.
* After it finishes, pass or fail,
* we revert the package.json back to the original state.
*
* The tests for the example projects (inside of examples) run with Webpack 4.
* This ensures we have some coverage for both versions.
*/
const main = async () => {
const originalPkg = JSON.stringify(pkg, null, 2)
const resetPkg = async () => {
fs.writeFileSync('package.json', originalPkg, 'utf8')
await execa('yarn', ['install'], { stdio: 'inherit' })
}
const checkExit = async ({ exitCode, step }) => {
if (typeof exitCode !== 'number') {
// eslint-disable-next-line no-console
console.error(`${step} finished with missing exit code from execa (received ${exitCode})`)
}
if (step === 'e2e' || (step === 'unit' && exitCode !== 0)) {
await resetPkg()
process.exit(exitCode)
}
}
pkg.dependencies['webpack'] = '^5.39.0'
delete pkg.devDependencies['@types/webpack']
delete pkg.devDependencies['webpack']
// eslint-disable-next-line no-console
console.log('[@cypress/webpack-preprocessor]: updating package.json...')
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2))
// eslint-disable-next-line no-console
console.log('[@cypress/webpack-preprocessor]: install dependencies...')
await execa('yarn', ['install'], { stdio: 'inherit' })
const unit = await execa('yarn', ['test-unit'], { stdio: 'inherit' })
await checkExit({ exitCode: unit.exitCode, step: 'unit' })
const e2e = await execa('yarn', ['test-e2e'], { stdio: 'inherit' })
await checkExit({ exitCode: e2e.exitCode, step: 'e2e' })
}
// execute main function if called from command line
if (require.main === module) {
main()
}

View File

@@ -91,7 +91,11 @@ describe('webpack preprocessor', function () {
})
it('runs webpack', function () {
expect(preprocessor.__bundles()[this.file.filePath]).to.be.undefined
return this.run().then(() => {
expect(preprocessor.__bundles()[this.file.filePath].deferreds).to.be.empty
expect(preprocessor.__bundles()[this.file.filePath].promise).to.be.instanceOf(Promise)
expect(webpack).to.be.called
})
})
@@ -264,7 +268,7 @@ describe('webpack preprocessor', function () {
this.compilerApi.plugin.withArgs('compile').yield()
this.compilerApi.watch.yield(null, this.statsApi)
return Promise.delay(10) // give assertion time till next tick
return Promise.delay(11) // give assertion time till next tick
})
.then(() => {
expect(this.file.emit).to.be.calledWith('rerun')
@@ -331,8 +335,11 @@ describe('webpack preprocessor', function () {
it('it rejects with error when an err', function () {
this.compilerApi.run.yields(this.err)
expect(preprocessor.__bundles()[this.file.filePath]).to.be.undefined
return this.run().catch((err) => {
expect(preprocessor.__bundles()[this.file.filePath].deferreds).to.be.empty
expect(preprocessor.__bundles()[this.file.filePath].promise).to.be.instanceOf(Promise)
expect(err.stack).to.equal(this.err.stack)
})
})

View File

@@ -46,7 +46,7 @@
"stop-only": "npx stop-only --skip .cy,.publish,.projects,node_modules,dist,dist-test,fixtures,lib,bower_components,src,__snapshots__ --exclude e2e.ts,cypress-tests.ts,unwritten.spec.ts",
"stop-only-all": "yarn stop-only --folder packages",
"pretest": "yarn ensure-deps",
"test": "yarn lerna exec yarn test --scope cypress --scope \"'@packages/{electron,extension,https-proxy,launcher,net-stubbing,network,proxy,rewriter,runner,socket}'\"",
"test": "yarn lerna exec yarn test --scope cypress --scope \"'@packages/{electron,extension,https-proxy,launcher,net-stubbing,network,proxy,rewriter,runner,runner-shared,socket}'\"",
"test-debug": "lerna exec yarn test-debug --ignore \"'@packages/{desktop-gui,driver,root,static,web-config}'\"",
"pretest-e2e": "yarn ensure-deps",
"test-e2e": "lerna exec yarn test-e2e --ignore \"'@packages/{desktop-gui,driver,root,static,web-config}'\"",

View File

@@ -7,9 +7,5 @@
"reporter": "cypress-multi-reporters",
"reporterOptions": {
"configFile": "../../mocha-reporter-config.json"
},
"retries": {
"runMode": 2,
"openMode": 0
}
}

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<title>Display binary data</title>
</head>
<body>
<script type='text/javascript'>
var xhr = new XMLHttpRequest()
var url = 'http://localhost:3500/binary'
xhr.onload = function() {
var arrayBuffer = xhr.response
var uint8 = new Uint8Array(arrayBuffer)
var divNode = document.getElementById('result')
divNode.innerText = uint8.join(', ')
}
xhr.open('GET', url + '?_=' + Date.now(), true)
xhr.responseType = 'arraybuffer'
xhr.send()
</script>
<div id='result'></div>
</body>
</html>

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<title>Post and dump binary data</title>
</head>
<body>
<script type='text/javascript'>
var xhr = new XMLHttpRequest()
var url = 'http://localhost:3500/binary'
xhr.onload = function() {
var arrayBuffer = xhr.response
var uint8 = new Uint8Array(arrayBuffer)
var divNode = document.getElementById('result')
divNode.innerText = uint8.join(', ')
}
xhr.open('POST', url + '?_=' + Date.now(), true)
xhr.setRequestHeader('content-type', 'application/octet-stream')
xhr.responseType = 'arraybuffer'
const data = new Uint8Array(2)
data[0] = 41
data[1] = 155
xhr.send(data)
</script>
<div id='result'></div>
</body>
</html>

View File

@@ -639,6 +639,16 @@ describe('src/cy/commands/actions/type - #type', () => {
})
describe('delay', () => {
it('adds default delay to delta for each key sequence', () => {
cy.spy(cy, 'timeout')
cy.get(':text:first')
.type('foo{enter}bar{leftarrow}')
.then(() => {
expect(cy.timeout).to.be.calledWith(10 * 8, true, 'type')
})
})
it('adds delay to delta for each key sequence', () => {
cy.spy(cy, 'timeout')
@@ -667,6 +677,72 @@ describe('src/cy/commands/actions/type - #type', () => {
cy.get(':text:first').type('foo{enter}bar{leftarrow}')
})
it('test config keystrokeDelay overrides global value', { keystrokeDelay: 5 }, () => {
cy.spy(cy, 'timeout')
cy.get(':text:first')
.type('foo{enter}bar{leftarrow}')
.then(() => {
expect(cy.timeout).to.be.calledWith(5 * 8, true, 'type')
})
})
it('delay will override default keystrokeDelay', () => {
Cypress.Keyboard.defaults({
keystrokeDelay: 20,
})
cy.spy(cy, 'timeout')
cy.get(':text:first')
.type('foo{enter}bar{leftarrow}', { delay: 5 })
.then(() => {
expect(cy.timeout).to.be.calledWith(5 * 8, true, 'type')
Cypress.Keyboard.reset()
})
})
it('delay will override test config keystrokeDelay', { keystrokeDelay: 1000 }, () => {
cy.spy(cy, 'timeout')
cy.get(':text:first')
.type('foo{enter}bar{leftarrow}', { delay: 5 })
.then(() => {
expect(cy.timeout).to.be.calledWith(5 * 8, true, 'type')
})
})
it('does not increase the timeout delta when delay is 0', () => {
cy.spy(cy, 'timeout')
cy.get(':text:first').type('foo{enter}', { delay: 0 }).then(() => {
expect(cy.timeout).not.to.be.calledWith(0, true, 'type')
})
})
describe('errors', () => {
it('throws when delay is invalid', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.eq('`cy.type()` `delay` option must be 0 (zero) or a positive number. You passed: `false`')
expect(err.docsUrl).to.equal('https://on.cypress.io/type')
done()
})
cy.get(':text:first').type('foo', { delay: false })
})
it('throws when test config keystrokeDelay is invalid', { keystrokeDelay: false }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.eq('The test configuration `keystrokeDelay` option must be 0 (zero) or a positive number. You passed: `false`')
expect(err.docsUrl).to.equal('https://on.cypress.io/test-configuration')
done()
})
cy.get(':text:first').type('foo')
})
})
})
describe('events', () => {

View File

@@ -1395,6 +1395,31 @@ describe('network stubbing', { retries: { runMode: 2, openMode: 0 } }, function
cy.wait('@upload')
})
it('can stub a response with an ArrayBuffer', function () {
const stub = new Uint8Array(2)
stub[0] = 35
stub[1] = 2
const assertBody = (body: ArrayBuffer) => {
const uint8 = new Uint8Array(body)
stub.forEach((value, index) => {
expect(uint8[index]).to.eq(value)
})
}
cy.intercept('/binary*', {
body: stub.buffer,
headers: {
'content-type': 'application/octet-stream',
},
statusCode: 200,
}).as('get')
.visit('/fixtures/display-binary.html')
.wait('@get').its('response.body').should(assertBody)
.get('#result').should('have.text', stub.join(', '))
})
})
})
@@ -1428,6 +1453,27 @@ describe('network stubbing', { retries: { runMode: 2, openMode: 0 } }, function
})
})
it('can modify an ArrayBuffer request body', function () {
const modifiedUint8 = new Uint8Array(2)
modifiedUint8[0] = 35
modifiedUint8[1] = 2
const assertBody = (body: ArrayBuffer) => {
const uint8 = new Uint8Array(body)
modifiedUint8.forEach((value, index) => {
expect(uint8[index]).to.eq(value)
})
}
cy.intercept('/binary*', function (req) {
req.body = modifiedUint8.buffer
}).as('post')
.visit('/fixtures/dump-binary.html')
.wait('@post').its('response.body').should(assertBody)
.get('#result').should('have.text', modifiedUint8.join(', '))
})
it('can modify original request body and have it passed to next handler', function (done) {
cy.intercept('/post-only', function (req) {
expect(req.body).to.eq('quuz')
@@ -2417,6 +2463,28 @@ describe('network stubbing', { retries: { runMode: 2, openMode: 0 } }, function
.wait('@get').its('response.body').should('deep.eq', '{ "foo": "bar" }')
})
// @see https://github.com/cypress-io/cypress/issues/16722
it('doesn\'t automatically parse response bodies if content is binary', function () {
const expectedBody = [120, 42, 7]
const assertBody = (body: ArrayBuffer) => {
const uint8 = new Uint8Array(body)
expectedBody.forEach((value, index) => {
expect(uint8[index]).to.eq(value)
})
}
cy.intercept('/binary*', (req) => {
req.on('response', (res) => {
expect(_.isArrayBuffer(res.body)).to.eq(true)
assertBody(res.body)
})
}).as('get')
.visit('/fixtures/display-binary.html')
.wait('@get').its('response.body').should(assertBody)
.get('#result').should('have.text', expectedBody.join(', '))
})
it('sets body to string if JSON is malformed', function () {
const p = Promise.defer()

View File

@@ -0,0 +1,108 @@
const { Keyboard } = Cypress
const DEFAULTS = {
keystrokeDelay: 10,
}
describe('src/cypress/keyboard', () => {
beforeEach(() => {
Keyboard.reset()
})
it('has defaults', () => {
expect(Keyboard.getConfig()).to.deep.eq(DEFAULTS)
})
context('.getConfig', () => {
it('returns config', () => {
expect(Keyboard.getConfig()).to.deep.eq(DEFAULTS)
})
it('does not allow mutation of config', () => {
const config = Keyboard.getConfig()
config.keystrokeDelay = 0
expect(Keyboard.getConfig().keystrokeDelay).to.eq(DEFAULTS.keystrokeDelay)
})
})
context('.defaults', () => {
it('is noop if not called with any valid properties', () => {
Keyboard.defaults({})
expect(Keyboard.getConfig()).to.deep.eq(DEFAULTS)
})
it('sets keystrokeDelay if specified', () => {
Keyboard.defaults({
keystrokeDelay: 5,
})
expect(Keyboard.getConfig().keystrokeDelay).to.eql(5)
})
it('returns new config', () => {
const result = Keyboard.defaults({
keystrokeDelay: 5,
})
expect(result).to.deep.eql({
keystrokeDelay: 5,
})
})
it('does not allow mutation via returned config', () => {
const result = Keyboard.defaults({
keystrokeDelay: 5,
})
result.keystrokeDelay = 0
expect(Keyboard.getConfig().keystrokeDelay).to.eq(5)
})
describe('errors', () => {
it('throws if not passed an object', () => {
const fn = () => {
Keyboard.defaults()
}
expect(fn).to.throw()
.with.property('message')
.and.eq('`Cypress.Keyboard.defaults()` must be called with an object. You passed: ``')
expect(fn).to.throw()
.with.property('docsUrl')
.and.eq('https://on.cypress.io/keyboard-api')
})
it('throws if keystrokeDelay is not a number', () => {
const fn = () => {
Keyboard.defaults({ keystrokeDelay: false })
}
expect(fn).to.throw()
.with.property('message')
.and.eq('`Cypress.Keyboard.defaults()` `keystrokeDelay` option must be 0 (zero) or a positive number. You passed: `false`')
expect(fn).to.throw()
.with.property('docsUrl')
.and.eq('https://on.cypress.io/keyboard-api')
})
it('throws if keystrokeDelay is a negative number', () => {
const fn = () => {
Keyboard.defaults({ keystrokeDelay: -10 })
}
expect(fn).to.throw()
.with.property('message')
.and.eq('`Cypress.Keyboard.defaults()` `keystrokeDelay` option must be 0 (zero) or a positive number. You passed: `-10`')
expect(fn).to.throw()
.with.property('docsUrl')
.and.eq('https://on.cypress.io/keyboard-api')
})
})
})
})

View File

@@ -89,6 +89,24 @@ const createApp = (port) => {
})
})
app.get('/binary', (req, res) => {
const uint8 = new Uint8Array(3)
uint8[0] = 120
uint8[1] = 42
uint8[2] = 7
res.setHeader('Content-Type', 'application/octet-stream')
return res.send(Buffer.from(uint8))
})
app.post('/binary', (req, res) => {
res.setHeader('Content-Type', 'application/octet-stream')
return res.send(req.body)
})
app.get('/1mb', (req, res) => {
return res.type('text').send('X'.repeat(1024 * 1024))
})

View File

@@ -14,6 +14,11 @@ beforeEach(() => {
// necessary or else snapshots will not be taken
// and we can't test them
Cypress.config('numTestsKeptInMemory', 1)
// we want to only enable retries in runMode
// and because we set `isInteractive` above
// we have to set retries here
Cypress.config('retries', 2)
}
// remove all event listeners

View File

@@ -2,4 +2,5 @@
/// <reference path="../ts/index.d.ts" />
export const $Cypress: Cypress.Cypress
export default $Cypress
export const $: typeof JQuery
export default $Cypress

View File

@@ -24,7 +24,7 @@ module.exports = function (Commands, Cypress, cy, state, config) {
log: true,
verify: true,
force: false,
delay: 10,
delay: config('keystrokeDelay') || $Keyboard.getConfig().keystrokeDelay,
release: true,
parseSpecialCharSequences: true,
waitForAnimations: config('waitForAnimations'),
@@ -130,6 +130,30 @@ module.exports = function (Commands, Cypress, cy, state, config) {
})
}
const isInvalidDelay = (delay) => {
return delay !== undefined && (!_.isNumber(delay) || delay < 0)
}
if (isInvalidDelay(userOptions.delay)) {
$errUtils.throwErrByPath('keyboard.invalid_delay', {
onFail: options._log,
args: {
cmd: 'type',
docsPath: 'type',
option: 'delay',
delay: userOptions.delay,
},
})
}
// specific error if test config keystrokeDelay is invalid
if (isInvalidDelay(config('keystrokeDelay'))) {
$errUtils.throwErrByPath('keyboard.invalid_per_test_delay', {
onFail: options._log,
args: { delay: config('keystrokeDelay') },
})
}
chars = `${chars}`
const win = state('window')
@@ -282,7 +306,9 @@ module.exports = function (Commands, Cypress, cy, state, config) {
// for the total number of keys we're about to
// type, ensure we raise the timeout to account
// for the delay being added to each keystroke
return cy.timeout(totalKeys * options.delay, true, 'type')
if (options.delay) {
return cy.timeout(totalKeys * options.delay, true, 'type')
}
},
onEvent: updateTable || _.noop,

View File

@@ -10,6 +10,7 @@ import * as $elements from '../dom/elements'
// eslint-disable-next-line no-duplicate-imports
import { HTMLTextLikeElement } from '../dom/elements'
import * as $selection from '../dom/selection'
import $utils from '../cypress/utils'
import $window from '../dom/window'
const debug = Debug('cypress:driver:keyboard')
@@ -840,9 +841,14 @@ export class Keyboard {
return Promise
.each(typeKeyFns, (fn) => {
if (options.delay) {
return Promise
.try(fn)
.delay(options.delay)
}
return Promise
.try(fn)
.delay(options.delay)
})
.then(() => {
if (options.release !== false) {
@@ -1315,10 +1321,56 @@ const create = (Cypress, state) => {
return new Keyboard(Cypress, state)
}
let _defaults
const reset = () => {
_defaults = {
keystrokeDelay: 10,
}
}
reset()
const getConfig = () => {
return _.clone(_defaults)
}
const defaults = (props: Partial<Cypress.KeyboardDefaultsOptions>) => {
if (!_.isPlainObject(props)) {
$errUtils.throwErrByPath('keyboard.invalid_arg', {
args: { arg: $utils.stringify(props) },
})
}
if (!('keystrokeDelay' in props)) {
return getConfig()
}
if (!_.isNumber(props.keystrokeDelay) || props.keystrokeDelay! < 0) {
$errUtils.throwErrByPath('keyboard.invalid_delay', {
args: {
cmd: 'Cypress.Keyboard.defaults',
docsPath: 'keyboard-api',
option: 'keystrokeDelay',
delay: $utils.stringify(props.keystrokeDelay),
},
})
}
_.extend(_defaults, {
keystrokeDelay: props.keystrokeDelay,
})
return getConfig()
}
export {
create,
defaults,
getConfig,
getKeymap,
modifiersToString,
reset,
toModifiersEventOptions,
fromModifierEventOptions,
}

View File

@@ -14,6 +14,7 @@ import {
NetEvent,
StringMatcher,
NumberMatcher,
BackendStaticResponseWithArrayBuffer,
} from '@packages/net-stubbing/lib/types'
import {
validateStaticResponse,
@@ -212,7 +213,7 @@ export function addCommand (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy,
return $errUtils.throwErrByPath('net_stubbing.intercept.invalid_middleware_handler', { args: { handler } })
}
const frame: NetEvent.ToServer.AddRoute = {
const frame: NetEvent.ToServer.AddRoute<BackendStaticResponseWithArrayBuffer> = {
routeId,
hasInterceptor,
routeMatcher,

View File

@@ -7,7 +7,7 @@ import {
SERIALIZABLE_REQ_PROPS,
Subscription,
} from '../types'
import { parseJsonBody } from './utils'
import { parseJsonBody, stringifyJsonBody } from './utils'
import {
validateStaticResponse,
parseStaticResponseShorthand,
@@ -74,7 +74,7 @@ export const onBeforeRequest: HandlerFn<CyHttpMessages.IncomingRequest> = (Cypre
const { routeId } = subscription
const route = getRoute(routeId)
parseJsonBody(req)
const bodyParsed = parseJsonBody(req)
const subscribe = (eventName, handler) => {
const subscription: Subscription = {
@@ -239,8 +239,8 @@ export const onBeforeRequest: HandlerFn<CyHttpMessages.IncomingRequest> = (Cypre
updateRequest(req)
if (_.isObject(req.body)) {
req.body = JSON.stringify(req.body)
if (bodyParsed) {
stringifyJsonBody(req)
}
resolve({

View File

@@ -12,7 +12,7 @@ import {
import $errUtils from '../../../cypress/error_utils'
import { HandlerFn, HandlerResult } from '.'
import Bluebird from 'bluebird'
import { parseJsonBody } from './utils'
import { parseJsonBody, stringifyJsonBody } from './utils'
type Result = HandlerResult<CyHttpMessages.IncomingResponse>
@@ -21,7 +21,7 @@ export const onResponse: HandlerFn<CyHttpMessages.IncomingResponse> = async (Cyp
const { routeId } = subscription
const request = getRequest(routeId, frame.requestId)
parseJsonBody(res)
const bodyParsed = parseJsonBody(res)
let responseSent = false
let resolved = false
@@ -104,8 +104,8 @@ export const onResponse: HandlerFn<CyHttpMessages.IncomingResponse> = async (Cyp
finishResponseStage(res)
if (_.isObject(res.body)) {
res.body = JSON.stringify(res.body)
if (bodyParsed) {
stringifyJsonBody(res)
}
resolve({

View File

@@ -7,14 +7,22 @@ export function hasJsonContentType (headers: { [k: string]: string }) {
return contentType && /^application\/.*json/i.test(contentType)
}
export function parseJsonBody (message: CyHttpMessages.BaseMessage) {
export function parseJsonBody (message: CyHttpMessages.BaseMessage): boolean {
if (!hasJsonContentType(message.headers)) {
return
return false
}
try {
message.body = JSON.parse(message.body)
return true
} catch (e) {
// invalid JSON
}
return false
}
export function stringifyJsonBody (message: CyHttpMessages.BaseMessage) {
message.body = JSON.stringify(message.body)
}

View File

@@ -2,7 +2,7 @@ import _ from 'lodash'
import {
StaticResponse,
BackendStaticResponse,
BackendStaticResponseWithArrayBuffer,
FixtureOpts,
} from '@packages/net-stubbing/lib/types'
import $errUtils from '../../cypress/error_utils'
@@ -95,8 +95,8 @@ function getFixtureOpts (fixture: string): FixtureOpts {
return { filePath, encoding }
}
export function getBackendStaticResponse (staticResponse: Readonly<StaticResponse>): BackendStaticResponse {
const backendStaticResponse: BackendStaticResponse = _.omit(staticResponse, 'body', 'fixture', 'delayMs')
export function getBackendStaticResponse (staticResponse: Readonly<StaticResponse>): BackendStaticResponseWithArrayBuffer {
const backendStaticResponse: BackendStaticResponseWithArrayBuffer = _.omit(staticResponse, 'body', 'fixture', 'delayMs')
if (staticResponse.delayMs) {
// support deprecated `delayMs` usage
@@ -108,7 +108,7 @@ export function getBackendStaticResponse (staticResponse: Readonly<StaticRespons
}
if (!_.isUndefined(staticResponse.body)) {
if (_.isString(staticResponse.body)) {
if (_.isString(staticResponse.body) || _.isArrayBuffer(staticResponse.body)) {
backendStaticResponse.body = staticResponse.body
} else {
backendStaticResponse.body = JSON.stringify(staticResponse.body)

View File

@@ -668,6 +668,24 @@ module.exports = {
docsUrl: 'https://on.cypress.io/{{cmd}}',
},
},
keyboard: {
invalid_arg: {
message: `${cmd('Cypress.Keyboard.defaults')} must be called with an object. You passed: \`{{arg}}\``,
docsUrl: 'https://on.cypress.io/keyboard-api',
},
invalid_delay ({ cmd: command, option, delay, docsPath }) {
return {
message: `${cmd(command)} \`${option}\` option must be 0 (zero) or a positive number. You passed: \`${delay}\``,
docsUrl: `https://on.cypress.io/${docsPath}`,
}
},
invalid_per_test_delay: {
message: `The test configuration \`keystrokeDelay\` option must be 0 (zero) or a positive number. You passed: \`{{delay}}\``,
docsUrl: 'https://on.cypress.io/test-configuration',
},
},
location: {
invalid_key: {
message: 'Location object does not have key: `{{key}}`',
@@ -917,7 +935,7 @@ module.exports = {
reached_redirection_limit ({ href, limit }) {
return stripIndent`\
The application redirected to \`${href}\` more than ${limit} times. Please check if it's an intended behavior.
If so, increase \`redirectionLimit\` value in configuration.`
},
},
@@ -934,7 +952,7 @@ module.exports = {
extra_arguments: ({ argsLength, overload }) => {
return cyStripIndent(`\
The ${cmd('intercept', overload.join(', '))} signature accepts a maximum of ${overload.length} arguments, but ${argsLength} arguments were passed.
Please refer to the docs for all accepted signatures for ${cmd('intercept')}.`, 10)
},
invalid_handler: ({ handler }) => {
@@ -977,9 +995,9 @@ module.exports = {
unknown_event: ({ validEvents, eventName }) => {
return cyStripIndent(`\
An invalid event name was passed as the first parameter to \`req.on()\`.
Valid event names are: ${format(validEvents)}
You passed: ${format(eventName)}`, 10)
},
event_needs_handler: `\`req.on()\` requires the second parameter to be a function.`,
@@ -1910,7 +1928,7 @@ module.exports = {
${cmd('visit')} failed because the 'file://...' protocol is not supported by Cypress.
To visit a local file, you can pass in the relative path to the file from the \`projectRoot\` (Note: if the configuration value \`baseUrl\` is set, the supplied path will be resolved from the \`baseUrl\` instead of \`projectRoot\`)`,
docsUrl: ['https://docs.cypress.io/api/commands/visit.html', '/https://docs.cypress.io/api/cypress-api/config.html'],
docsUrl: 'https://on.cypress.io/visit',
},
},

View File

@@ -383,7 +383,7 @@ export type RouteHandler = string | StaticResponse | RouteHandlerController | ob
/**
* Describes a response that will be sent back to the browser to fulfill the request.
*/
export type StaticResponse = GenericStaticResponse<string, string | object | boolean | null> & {
export type StaticResponse = GenericStaticResponse<string, string | object | boolean | ArrayBuffer | null> & {
/**
* Milliseconds to delay before the response is sent.
* @deprecated Use `delay` instead of `delayMs`.

View File

@@ -13,6 +13,8 @@ export type FixtureOpts = {
export type BackendStaticResponse = GenericStaticResponse<FixtureOpts, string>
export type BackendStaticResponseWithArrayBuffer = GenericStaticResponse<FixtureOpts, string | ArrayBuffer>
export const SERIALIZABLE_REQ_PROPS = [
'headers',
'body', // doesn't exist on the OG message, but will be attached by the backend
@@ -68,9 +70,9 @@ export declare namespace NetEvent {
}
export namespace ToServer {
export interface AddRoute {
export interface AddRoute<StaticResponse> {
routeMatcher: AnnotatedRouteMatcherOptions
staticResponse?: BackendStaticResponse
staticResponse?: StaticResponse
hasInterceptor: boolean
routeId: string
}

View File

@@ -18,10 +18,11 @@ import {
} from './util'
import { InterceptedRequest } from './intercepted-request'
import CyServer from '@packages/server'
import { BackendStaticResponse } from '../internal-types'
const debug = Debug('cypress:net-stubbing:server:driver-events')
async function onRouteAdded (state: NetStubbingState, getFixture: GetFixtureFn, options: NetEvent.ToServer.AddRoute) {
async function onRouteAdded (state: NetStubbingState, getFixture: GetFixtureFn, options: NetEvent.ToServer.AddRoute<BackendStaticResponse>) {
const routeMatcher = _restoreMatcherOptionsTypes(options.routeMatcher)
const { staticResponse } = options
@@ -111,7 +112,7 @@ type OnNetEventOpts = {
socket: CyServer.Socket
getFixture: GetFixtureFn
args: any[]
frame: NetEvent.ToServer.AddRoute | NetEvent.ToServer.EventHandlerResolved | NetEvent.ToServer.Subscribe | NetEvent.ToServer.SendStaticResponse
frame: NetEvent.ToServer.AddRoute<BackendStaticResponse> | NetEvent.ToServer.EventHandlerResolved | NetEvent.ToServer.Subscribe | NetEvent.ToServer.SendStaticResponse
}
export async function onNetEvent (opts: OnNetEventOpts): Promise<any> {
@@ -121,7 +122,7 @@ export async function onNetEvent (opts: OnNetEventOpts): Promise<any> {
switch (eventName) {
case 'route:added':
return onRouteAdded(state, getFixture, <NetEvent.ToServer.AddRoute>frame)
return onRouteAdded(state, getFixture, <NetEvent.ToServer.AddRoute<BackendStaticResponse>>frame)
case 'subscribe':
return subscribe(state, <NetEvent.ToServer.Subscribe>frame)
case 'event:handler:resolved':

View File

@@ -15,6 +15,7 @@ import {
sendStaticResponse,
setDefaultHeaders,
mergeDeletedHeaders,
mergeWithPreservedBuffers,
getBodyEncoding,
} from '../util'
import { InterceptedRequest } from '../intercepted-request'
@@ -142,18 +143,7 @@ export const InterceptRequest: RequestMiddleware = async function () {
// resolve and propagate any changes to the URL
request.req.proxiedUrl = after.url = url.resolve(request.req.proxiedUrl, after.url)
// if the body is binary, don't recursively merge it or it will get
// incorrectly converted from a Buffer into an array
// @see https://github.com/cypress-io/cypress/issues/15898
const serializableProps = _.without(SERIALIZABLE_REQ_PROPS, 'body')
_.merge(before, _.pick(after, serializableProps))
if (bodyIsBinary) {
before.body = after.body
} else {
_.merge(before, { body: after.body })
}
mergeWithPreservedBuffers(before, _.pick(after, SERIALIZABLE_REQ_PROPS))
mergeDeletedHeaders(before, after)
}

View File

@@ -14,6 +14,7 @@ import {
import {
getBodyStream,
mergeDeletedHeaders,
mergeWithPreservedBuffers,
} from '../util'
const debug = Debug('cypress:net-stubbing:server:intercept-response')
@@ -65,7 +66,7 @@ export const InterceptResponse: ResponseMiddleware = async function () {
}
const mergeChanges = (before: CyHttpMessages.IncomingResponse, after: CyHttpMessages.IncomingResponse) => {
_.merge(before, _.pick(after, SERIALIZABLE_RES_PROPS))
mergeWithPreservedBuffers(before, _.pick(after, SERIALIZABLE_RES_PROPS))
mergeDeletedHeaders(before, after)
}

View File

@@ -232,6 +232,19 @@ export function mergeDeletedHeaders (before: CyHttpMessages.BaseMessage, after:
}
}
export function mergeWithPreservedBuffers (before: CyHttpMessages.BaseMessage, after: Partial<CyHttpMessages.BaseMessage>) {
// lodash merge converts Buffer into Array (by design)
// https://github.com/lodash/lodash/issues/2964
// @see https://github.com/cypress-io/cypress/issues/15898
_.mergeWith(before, after, (_a, b) => {
if (b instanceof Buffer) {
return b
}
return undefined
})
}
type BodyEncoding = 'utf8' | 'binary' | null
export function getBodyEncoding (req: CyHttpMessages.IncomingRequest): BodyEncoding {

View File

@@ -3,7 +3,7 @@ import React from 'react'
import { mount } from '@cypress/react'
import RunnerCt from '../../src/app/RunnerCt'
import '@packages/runner/src/main.scss'
import eventManager from '../../src/lib/event-manager'
import { eventManager } from '@packages/runner-shared'
import { testSpecFile } from '../fixtures/testSpecFile'
import { makeState, fakeConfig, getPort } from './utils'

View File

@@ -3,7 +3,7 @@ import React from 'react'
import { mount } from '@cypress/react'
import RunnerCt from '../../src/app/RunnerCt'
import '@packages/runner/src/main.scss'
import eventManager from '../../src/lib/event-manager'
import { eventManager } from '@packages/runner-shared'
import { testSpecFile } from '../fixtures/testSpecFile'
import { makeState, fakeConfig, getPort } from './utils'

View File

@@ -2,7 +2,7 @@
import React from 'react'
import { mount } from '@cypress/react'
import ScriptError from '../../src/errors/script-error'
import { ScriptError } from '@packages/runner-shared'
describe('ScriptError', () => {
it('renders an error', () => {

View File

@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import State from '../lib/state'
import { Hidden } from '../lib/Hidden'
import { namedObserver } from '../lib/mobx'
import { namedObserver } from '@packages/runner-shared'
import { PLUGIN_BAR_HEIGHT } from './RunnerCt'
import styles from './RunnerCt.module.scss'

View File

@@ -3,10 +3,8 @@ import cs from 'classnames'
import { ReporterHeaderProps } from '@packages/reporter/src/header/header'
import { Reporter } from '@packages/reporter/src/main'
import errorMessages from '../errors/error-messages'
import EventManager from '../lib/event-manager'
import { errorMessages, namedObserver, eventManager as EventManager } from '@packages/runner-shared'
import State from '../lib/state'
import { namedObserver } from '../lib/mobx'
import { ReporterHeader } from './ReporterHeader'
import { NoSpec } from './NoSpec'
@@ -15,7 +13,10 @@ import styles from './RunnerCt.module.scss'
interface ReporterContainerProps {
state: State
eventManager: typeof EventManager
config: Cypress.RuntimeConfigOptions
config: {
configFile: string
[key: string]: unknown
}
}
export const ReporterContainer = namedObserver('ReporterContainer',

View File

@@ -3,7 +3,7 @@ import { ReporterHeaderProps } from '@packages/reporter/src/header/header'
import Stats from '@packages/reporter/src/header/stats'
import Controls from '@packages/reporter/src/header/controls'
import { StatsStore } from '@packages/reporter/src/header/stats-store'
import { namedObserver } from '../lib/mobx'
import { namedObserver } from '@packages/runner-shared'
import styles from './ReporterHeader.module.scss'
export const EmptyReporterHeader: React.FC = () => {

View File

@@ -14,13 +14,12 @@ library.add(fas)
library.add(fab)
import State from '../lib/state'
import EventManager from '../lib/event-manager'
import { eventManager as EventManager, namedObserver } from '@packages/runner-shared'
import { useGlobalHotKey } from '../lib/useHotKey'
import { animationFrameDebounce } from '../lib/debounce'
import { LeftNavMenu } from './LeftNavMenu'
import { SpecContent } from './SpecContent'
import { hideIfScreenshotting, hideSpecsListIfNecessary } from '../lib/hideGuard'
import { namedObserver } from '../lib/mobx'
import { SpecList } from './SpecList/SpecList'
import { NoSpec } from './NoSpec'

View File

@@ -1,27 +1,27 @@
import cs from 'classnames'
import * as React from 'react'
import SplitPane from 'react-split-pane'
import { Message, namedObserver, eventManager as EventManager, Header } from '@packages/runner-shared'
import Header from '../header/header'
import { Iframes } from '../iframe/iframes'
import { animationFrameDebounce } from '../lib/debounce'
import { Message } from '../message/message'
import { KeyboardHelper } from './KeyboardHelper'
import { NoSpec } from './NoSpec'
import { Plugins } from './Plugins'
import { ReporterContainer } from './ReporterContainer'
import { PLUGIN_BAR_HEIGHT } from './RunnerCt'
import State from '../lib/state'
import EventManager from '../lib/event-manager'
import { hideIfScreenshotting, hideReporterIfNecessary } from '../lib/hideGuard'
import styles from './RunnerCt.module.scss'
import { namedObserver } from '../lib/mobx'
interface SpecContentProps {
state: State
eventManager: typeof EventManager
config: Cypress.RuntimeConfigOptions
config: {
configFile: string
[key: string]: unknown
}
}
interface SpecContentWrapperProps {
@@ -62,7 +62,7 @@ export const SpecContent = namedObserver('SpecContent', (props: SpecContentProps
},
)}
>
<Header {...props} />
<Header {...props} runner='ct' />
{props.state.spec
? <Iframes {...props} />
: (
@@ -70,7 +70,19 @@ export const SpecContent = namedObserver('SpecContent', (props: SpecContentProps
<KeyboardHelper />
</NoSpec>
)}
<Message state={props.state} />
<Message
state={{
messageTitle: props.state.messageTitle,
messageControls: props.state.messageControls,
messageDescription: props.state.messageDescription,
messageType: props.state.messageType,
messageStyles: {
state: props.state.messageStyles.state,
styles: props.state.messageStyles.styles,
messageType: props.state.messageType,
},
}}
/>
</div>
<Plugins
key="plugins"

View File

@@ -1,6 +1,6 @@
import * as React from 'react'
import { runInAction } from 'mobx'
import EventManager from '../lib/event-manager'
import { eventManager as EventManager } from '@packages/runner-shared'
import State from '../lib/state'
/**

View File

@@ -1,24 +0,0 @@
export default {
reporterError (err, specPath) {
if (!err) return null
switch (err.type) {
case 'BUNDLE_ERROR':
return {
title: 'Oops...we found an error preparing this test file:',
link: 'https://on.cypress.io/we-found-an-error-preparing-your-test-file',
callout: specPath,
message: `
This occurred while Cypress was compiling and bundling your test code. This is usually caused by:
* A missing file or dependency
* A syntax error in the file or one of its dependencies
Fix the error in your code and re-run your tests.
`,
}
default:
return null
}
},
}

View File

@@ -1,111 +0,0 @@
import cs from 'classnames'
import { action, observable } from 'mobx'
import { observer } from 'mobx-react'
import React, { Component } from 'react'
import Tooltip from '@cypress/react-tooltip'
import State from '../lib/state'
import { configFileFormatted } from '../lib/config-file-formatted'
import SelectorPlayground from '../selector-playground/selector-playground'
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
interface HeaderProps {
state: State
config: Cypress.RuntimeConfigOptions
}
@observer
export default class Header extends Component<HeaderProps> {
headerRef = React.createRef()
@observable showingViewportMenu = false
render () {
const { state, config } = this.props
return (
<header
ref={this.headerRef}
className={cs({
'showing-selector-playground': selectorPlaygroundModel.isOpen,
'display-none': state.screenshotting,
})}
>
<div className='sel-url-wrap'>
<Tooltip
title='Open Selector Playground'
visible={selectorPlaygroundModel.isOpen ? false : null}
wrapperClassName='selector-playground-toggle-tooltip-wrapper'
className='cy-tooltip'
>
<button
aria-label='Open Selector Playground'
className='header-button selector-playground-toggle'
disabled={state.isLoading || state.isRunning}
onClick={this._togglePlaygroundOpen}
>
<i aria-hidden="true" className='fas fa-crosshairs' />
</button>
</Tooltip>
</div>
<ul className='menu'>
<li className={cs('viewport-info', { 'menu-open': this.showingViewportMenu })}>
<button onClick={this._toggleViewportMenu}>
{`${state.viewportWidth} `}
<span className='the-x'>x</span>
{` ${state.viewportHeight}`}
<i className='fas fa-fw fa-info-circle'></i>
</button>
<div className='popup-menu viewport-menu'>
{/* eslint-disable react/jsx-one-expression-per-line */}
<p>The <strong>viewport</strong> determines the width and height of your application. By default the viewport will be
<strong>{state.defaults.viewportWidth}px</strong> by
<strong>{state.defaults.viewportHeight}px</strong> unless specified by a
<code>cy.viewport</code> command.
</p>
<p>Additionally you can override the default viewport dimensions by specifying these values in your {configFileFormatted(config.configFile)}.</p>
<pre>{/* eslint-disable indent */}
{`{
"viewportWidth": ${state.defaults.viewportWidth},
"viewportHeight": ${state.defaults.viewportHeight}
}`}
</pre>
{/* eslint-enable indent */}
<p>
<a href='https://on.cypress.io/viewport' target='_blank' rel='noreferrer'>
<i className='fas fa-info-circle'></i>
Read more about viewport here.
</a>
</p>
{/* eslint-enable react/jsx-one-expression-per-line */}
</div>
</li>
</ul>
<SelectorPlayground model={selectorPlaygroundModel} />
</header>
)
}
componentDidMount () {
this.previousSelectorPlaygroundOpen = selectorPlaygroundModel.isOpen
}
componentDidUpdate () {
if (selectorPlaygroundModel.isOpen !== this.previousSelectorPlaygroundOpen) {
this.props.state.updateWindowDimensions({
headerHeight: this.headerRef.current.offsetHeight,
})
this.previousSelectorPlaygroundOpen = selectorPlaygroundModel.isOpen
}
}
_togglePlaygroundOpen = () => {
selectorPlaygroundModel.toggleOpen()
}
@action _toggleViewportMenu = () => {
this.showingViewportMenu = !this.showingViewportMenu
}
}

View File

@@ -1,399 +0,0 @@
import _ from 'lodash'
import { $ } from '@packages/driver'
import dom from '../lib/dom'
import logger from '../lib/logger'
import eventManager from '../lib/event-manager'
import visitFailure from './visit-failure'
import blankContents from './blank-contents'
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
export default class AutIframe {
constructor (config) {
this.config = config
this.debouncedToggleSelectorPlayground = _.debounce(this.toggleSelectorPlayground, 300)
}
create () {
this.$iframe = $('<iframe>', {
id: `Your App: '${this.config.projectName}'`,
class: 'aut-iframe',
})
return this.$iframe
}
showBlankContents () {
this._showContents(blankContents())
}
showVisitFailure = (props) => {
this._showContents(visitFailure(props))
}
_showContents (contents) {
this._body().html(contents)
}
_contents () {
return this.$iframe && this.$iframe.contents()
}
_window () {
return this.$iframe.prop('contentWindow')
}
_document () {
return this.$iframe.prop('contentDocument')
}
_body () {
return this._contents() && this._contents().find('body')
}
detachDom = () => {
const Cypress = eventManager.getCypress()
if (!Cypress) return
return Cypress.cy.detachDom(this._contents())
}
restoreDom = (snapshot) => {
const Cypress = eventManager.getCypress()
const { headStyles, bodyStyles } = Cypress ? Cypress.cy.getStyles(snapshot) : {}
const { body, htmlAttrs } = snapshot
const contents = this._contents()
const $html = contents.find('html')
this._replaceHtmlAttrs($html, htmlAttrs)
this._replaceHeadStyles(headStyles)
// remove the old body and replace with restored one
this._body().remove()
this._insertBodyStyles(body.get(), bodyStyles)
$html.append(body.get())
this.debouncedToggleSelectorPlayground(selectorPlaygroundModel.isEnabled)
}
_replaceHtmlAttrs ($html, htmlAttrs) {
let oldAttrs = {}
// remove all attributes
if ($html[0]) {
oldAttrs = _.map($html[0].attributes, (attr) => {
return attr.name
})
}
_.each(oldAttrs, (attr) => {
$html.removeAttr(attr)
})
// set the ones specified
_.each(htmlAttrs, (value, key) => {
$html.attr(key, value)
})
}
_replaceHeadStyles (styles = []) {
const $head = this._contents().find('head')
const existingStyles = $head.find('link[rel="stylesheet"],style')
_.each(styles, (style, index) => {
if (style.href) {
// make a best effort at not disturbing <link> stylesheets
// if possible by checking to see if the existing head has a
// stylesheet with the same href in the same position
this._replaceLink($head, existingStyles[index], style)
} else {
// for <style> tags, just replace them completely since the contents
// could be different and it shouldn't cause a FOUC since
// there's no http request involved
this._replaceStyle($head, existingStyles[index], style)
}
})
// remove any extra stylesheets
if (existingStyles.length > styles.length) {
existingStyles.slice(styles.length).remove()
}
}
_replaceLink ($head, existingStyle, style) {
const linkTag = this._linkTag(style)
if (!existingStyle) {
// no existing style at this index, so no more styles at all in
// the head, so just append it
$head.append(linkTag)
return
}
if (existingStyle.href !== style.href) {
$(existingStyle).replaceWith(linkTag)
}
}
_replaceStyle ($head, existingStyle, style) {
const styleTag = this._styleTag(style)
if (existingStyle) {
$(existingStyle).replaceWith(styleTag)
} else {
// no existing style at this index, so no more styles at all in
// the head, so just append it
$head.append(styleTag)
}
}
_insertBodyStyles ($body, styles = []) {
_.each(styles, (style) => {
$body.append(style.href ? this._linkTag(style) : this._styleTag(style))
})
}
_linkTag (style) {
return `<link rel="stylesheet" href="${style.href}" />`
}
_styleTag (style) {
return `<style>${style}</style>`
}
highlightEl = ({ body }, { $el, coords, highlightAttr, scrollBy }) => {
this.removeHighlights()
if (body) {
$el = body.get().find(`[${highlightAttr}]`)
} else {
body = { get: () => this._body() }
}
// normalize
const el = $el.get(0)
const $body = body.get()
body = $body.get(0)
// scroll the top of the element into view
if (el) {
el.scrollIntoView()
// if we have a scrollBy on our command
// then we need to additional scroll the window
// by these offsets
if (scrollBy) {
this.$iframe.prop('contentWindow').scrollBy(scrollBy.x, scrollBy.y)
}
}
$el.each((__, element) => {
const $_el = $(element)
// bail if our el no longer exists in the parent body
if (!$.contains(body, element)) return
// switch to using outerWidth + outerHeight
// because we want to highlight our element even
// if it only has margin and zero content height / width
const dimensions = dom.getOuterSize($_el)
// dont show anything if our element displaces nothing
if (dimensions.width === 0 || dimensions.height === 0 || $_el.css('display') === 'none') {
return
}
dom.addElementBoxModelLayers($_el, $body).attr('data-highlight-el', true)
})
if (coords) {
requestAnimationFrame(() => {
dom.addHitBoxLayer(coords, $body).attr('data-highlight-hitbox', true)
})
}
}
removeHighlights = () => {
this._contents() && this._contents().find('.__cypress-highlight').remove()
}
toggleSelectorPlayground = (isEnabled) => {
const $body = this._body()
if (!$body) return
if (isEnabled) {
$body.on('mouseenter', this._resetShowHighlight)
$body.on('mousemove', this._onSelectorMouseMove)
$body.on('mouseleave', this._clearHighlight)
} else {
$body.off('mouseenter', this._resetShowHighlight)
$body.off('mousemove', this._onSelectorMouseMove)
$body.off('mouseleave', this._clearHighlight)
if (this._highlightedEl) {
this._clearHighlight()
}
}
}
_resetShowHighlight = () => {
selectorPlaygroundModel.setShowingHighlight(false)
}
_onSelectorMouseMove = (e) => {
const $body = this._body()
if (!$body) return
let el = e.target
let $el = $(el)
const $ancestorHighlight = $el.closest('.__cypress-selector-playground')
if ($ancestorHighlight.length) {
$el = $ancestorHighlight
}
if ($ancestorHighlight.length || $el.hasClass('__cypress-selector-playground')) {
const $highlight = $el
$highlight.css('display', 'none')
el = this._document().elementFromPoint(e.clientX, e.clientY)
$el = $(el)
$highlight.css('display', 'block')
}
if (this._highlightedEl === el) return
this._highlightedEl = el
const Cypress = eventManager.getCypress()
const selector = Cypress.SelectorPlayground.getSelector($el)
dom.addOrUpdateSelectorPlaygroundHighlight({
$el,
selector,
$body,
showTooltip: true,
onClick: () => {
selectorPlaygroundModel.setNumElements(1)
selectorPlaygroundModel.resetMethod()
selectorPlaygroundModel.setSelector(selector)
},
})
}
_clearHighlight = () => {
const $body = this._body()
if (!$body) return
dom.addOrUpdateSelectorPlaygroundHighlight({ $el: null, $body })
if (this._highlightedEl) {
this._highlightedEl = null
}
}
toggleSelectorHighlight (isShowingHighlight) {
if (!isShowingHighlight) {
this._clearHighlight()
return
}
const Cypress = eventManager.getCypress()
const $el = this.getElements(Cypress.dom)
selectorPlaygroundModel.setValidity(!!$el)
if ($el) {
selectorPlaygroundModel.setNumElements($el.length)
if ($el.length) {
dom.scrollIntoView(this._window(), $el[0])
}
}
dom.addOrUpdateSelectorPlaygroundHighlight({
$el: $el && $el.length ? $el : null,
selector: selectorPlaygroundModel.selector,
$body: this._body(),
showTooltip: false,
})
}
getElements (cypressDom) {
const { selector, method } = selectorPlaygroundModel
const $contents = this._contents()
if (!$contents || !selector) return
return dom.getElementsForSelector({
method,
selector,
cypressDom,
$root: $contents,
})
}
printSelectorElementsToConsole () {
logger.clearLog()
const Cypress = eventManager.getCypress()
const $el = this.getElements(Cypress.dom)
const command = `cy.${selectorPlaygroundModel.method}('${selectorPlaygroundModel.selector}')`
if (!$el) {
return logger.logFormatted({
Command: command,
Yielded: 'Nothing',
})
}
logger.logFormatted({
Command: command,
Elements: $el.length,
Yielded: Cypress.dom.getElements($el),
})
}
beforeScreenshot = (config) => {
// could fail if iframe is cross-origin, so fail gracefully
try {
if (config.disableTimersAndAnimations) {
dom.addCssAnimationDisabler(this._body())
}
_.each(config.blackout, (selector) => {
dom.addBlackout(this._body(), selector)
})
} catch (err) {
/* eslint-disable no-console */
console.error('Failed to modify app dom:')
console.error(err)
/* eslint-disable no-console */
}
}
afterScreenshot = (config) => {
// could fail if iframe is cross-origin, so fail gracefully
try {
if (config.disableTimersAndAnimations) {
dom.removeCssAnimationDisabler(this._body())
}
dom.removeBlackouts(this._body())
} catch (err) {
/* eslint-disable no-console */
console.error('Failed to modify app dom:')
console.error(err)
/* eslint-disable no-console */
}
}
}

View File

@@ -1,232 +0,0 @@
import _ from 'lodash'
import { action } from 'mobx'
import eventManager from '../lib/event-manager'
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
export default class IframeModel {
constructor ({ state, detachDom, restoreDom, highlightEl, snapshotControls }) {
this.state = state
this.detachDom = detachDom
this.restoreDom = restoreDom
this.highlightEl = highlightEl
this.snapshotControls = snapshotControls
this._reset()
}
listen () {
eventManager.on('run:start', action('run:start', this._beforeRun))
eventManager.on('run:end', action('run:end', this._afterRun))
eventManager.on('viewport:changed', action('viewport:changed', this._updateViewport))
eventManager.on('config', action('config', (config) => {
this._updateViewport(_.map(config, 'viewportHeight', 'viewportWidth'))
}))
eventManager.on('url:changed', action('url:changed', this._updateUrl))
eventManager.on('page:loading', action('page:loading', this._updateLoadingUrl))
eventManager.on('show:snapshot', action('show:snapshot', this._setSnapshots))
eventManager.on('hide:snapshot', action('hide:snapshot', this._clearSnapshots))
eventManager.on('pin:snapshot', action('pin:snapshot', this._pinSnapshot))
eventManager.on('unpin:snapshot', action('unpin:snapshot', this._unpinSnapshot))
}
_beforeRun = () => {
this.state.isLoading = false
this.state.isRunning = true
this.state.resetUrl()
selectorPlaygroundModel.setEnabled(false)
this._reset()
this._clearMessage()
}
_afterRun = () => {
this.state.isRunning = false
}
_updateViewport = ({ viewportWidth, viewportHeight }, cb) => {
this.state.updateAutViewportDimensions({ viewportWidth, viewportHeight })
if (cb) {
this.state.setCallbackAfterUpdate(cb)
}
}
_updateUrl = (url) => {
this.state.url = url
}
_updateLoadingUrl = (isLoadingUrl) => {
this.state.isLoadingUrl = isLoadingUrl
}
_clearMessage = () => {
this.state.clearMessage()
}
_setSnapshots = (snapshotProps) => {
if (this.isSnapshotPinned) return
if (this.state.isRunning) {
return this._testsRunningError()
}
const { snapshots } = snapshotProps
if (!snapshots || !snapshots.length) {
this._clearSnapshots()
this._setMissingSnapshotMessage()
return
}
this.state.highlightUrl = true
if (!this.originalState) {
this._storeOriginalState()
}
this.detachedId = snapshotProps.id
this._updateViewport(snapshotProps)
this._updateUrl(snapshotProps.url)
clearInterval(this.intervalId)
const revert = action('revert:snapshot', this._showSnapshot)
if (snapshots.length > 1) {
let i = 0
this.intervalId = setInterval(() => {
if (this.isSnapshotPinned) return
i += 1
if (!snapshots[i]) {
i = 0
}
revert(snapshots[i], snapshotProps)
}, 800)
}
revert(snapshots[0], snapshotProps)
}
_showSnapshot = (snapshot, snapshotProps) => {
this.state.messageTitle = 'DOM Snapshot'
this.state.messageDescription = snapshot.name
this.state.messageType = ''
this._restoreDom(snapshot, snapshotProps)
}
_restoreDom (snapshot, snapshotProps) {
this.restoreDom(snapshot)
if (snapshotProps.$el) {
this.highlightEl(snapshot, snapshotProps)
}
}
_clearSnapshots = () => {
if (this.isSnapshotPinned) return
clearInterval(this.intervalId)
this.state.highlightUrl = false
if (!this.originalState || !this.originalState.body) {
return this._clearMessage()
}
const previousDetachedId = this.detachedId
// process on next tick so we don't restore the dom if we're
// about to receive another 'show:snapshot' event, else that would
// be a huge waste
setTimeout(action('clear:snapshots:next:tick', () => {
// we want to only restore the dom if we haven't received
// another snapshot by the time this function runs
if (previousDetachedId !== this.detachedId) return
this._updateViewport(this.originalState)
this._updateUrl(this.originalState.url)
this.restoreDom(this.originalState.snapshot)
this._clearMessage()
this.originalState = null
this.detachedId = null
}))
}
_pinSnapshot = (snapshotProps) => {
const { snapshots } = snapshotProps
if (!snapshots || !snapshots.length) {
eventManager.snapshotUnpinned()
this._setMissingSnapshotMessage()
return
}
clearInterval(this.intervalId)
this.isSnapshotPinned = true
this.state.snapshot.showingHighlights = true
this.state.snapshot.stateIndex = 0
this.state.messageTitle = 'DOM Snapshot'
this.state.messageDescription = 'pinned'
this.state.messageType = 'info'
this.state.messageControls = this.snapshotControls(snapshotProps)
this._restoreDom(snapshots[0], snapshotProps)
}
_setMissingSnapshotMessage () {
this.state.messageTitle = 'The snapshot is missing. Displaying current state of the DOM.'
this.state.messageDescription = ''
this.state.messageType = 'warning'
}
_unpinSnapshot = () => {
this.isSnapshotPinned = false
this.state.messageTitle = 'DOM Snapshot'
this.state.messageDescription = ''
this.state.messageControls = null
}
_testsRunningError () {
this.state.messageTitle = 'Cannot show Snapshot while tests are running'
this.state.messageType = 'warning'
}
_storeOriginalState () {
const finalSnapshot = this.detachDom()
if (!finalSnapshot) return
const { body, htmlAttrs } = finalSnapshot
this.originalState = {
body,
htmlAttrs,
snapshot: finalSnapshot,
url: this.state.url,
viewportWidth: this.state.viewportWidth,
viewportHeight: this.state.viewportHeight,
}
}
_reset () {
this.detachedId = null
this.intervalId = null
this.originalState = null
this.isSnapshotPinned = false
}
}

View File

@@ -2,16 +2,18 @@ import cs from 'classnames'
import { action, when, autorun } from 'mobx'
import React, { useRef, useEffect } from 'react'
import { default as $Cypress } from '@packages/driver'
import {
SnapshotControls,
ScriptError,
namedObserver,
IframeModel,
selectorPlaygroundModel,
AutIframe,
eventManager as EventManager,
} from '@packages/runner-shared'
import State from '../../src/lib/state'
import AutIframe from './aut-iframe'
import { ScriptError } from '../errors/script-error'
import SnapshotControls from './snapshot-controls'
import IframeModel from './iframe-model'
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
import styles from '../app/RunnerCt.module.scss'
import eventManager from '../lib/event-manager'
import { namedObserver } from '../lib/mobx'
import './iframes.scss'
export function getSpecUrl ({ namespace, spec }, prefix = '') {
@@ -20,7 +22,7 @@ export function getSpecUrl ({ namespace, spec }, prefix = '') {
interface IFramesProps {
state: State
eventManager: typeof eventManager
eventManager: typeof EventManager
config: Cypress.RuntimeConfigOptions
}
@@ -168,7 +170,7 @@ export const Iframes = namedObserver('Iframes', ({
state.callbackAfterUpdate?.()
})
const { viewportHeight, viewportWidth, scriptError, scale, screenshotting } = state
const { height, width, scriptError, scale, screenshotting } = state
return (
<div
@@ -188,8 +190,8 @@ export const Iframes = namedObserver('Iframes', ({
})
}
style={{
height: viewportHeight,
width: viewportWidth,
height,
width,
transform: `scale(${screenshotting ? 1 : scale})`,
}}
/>

View File

@@ -1,78 +0,0 @@
import cs from 'classnames'
import _ from 'lodash'
import { action } from 'mobx'
import { observer } from 'mobx-react'
import React, { Component } from 'react'
import Tooltip from '@cypress/react-tooltip'
@observer
class SnapshotControls extends Component {
render () {
return (
<span
className={cs('snapshot-controls', {
'showing-selection': this.props.state.snapshot.showingHighlights,
})}
>
{this._selectionToggle()}
{this._states()}
<Tooltip title='Unpin' className='cy-tooltip'>
<button className='unpin' onClick={this._unpin}>
<i className='fas fa-times' />
</button>
</Tooltip>
</span>
)
}
_selectionToggle () {
if (!this.props.snapshotProps.$el) return null
const showingHighlights = this.props.state.snapshot.showingHighlights
return (
<Tooltip title={`${showingHighlights ? 'Hide' : 'Show'} Highlights`} className='cy-tooltip'>
<button className='toggle-selection' onClick={this._toggleHighlights}>
<i className='far fa-object-group' />
</button>
</Tooltip>
)
}
_states () {
const { snapshots } = this.props.snapshotProps
if (snapshots.length < 2) return null
return (
<span className='snapshot-state-picker'>
{_.map(snapshots, (snapshot, index) => (
<button
key={snapshot.name ?? index}
className={cs({
'state-is-selected': this.props.state.snapshot.stateIndex === index,
})}
href="#"
onClick={this._changeState(index)}
>
{snapshot.name ?? index + 1}
</button>
))}
</span>
)
}
_unpin = () => {
this.props.eventManager.snapshotUnpinned()
}
@action _toggleHighlights = () => {
this.props.onToggleHighlights(this.props.snapshotProps)
}
_changeState = (index) => action('change:snapshot:state', () => {
this.props.onStateChange(this.props.snapshotProps, index)
})
}
export default SnapshotControls

View File

@@ -1,33 +0,0 @@
import React from 'react'
import { isUndefined } from 'lodash'
const configFileFormatted = (configFile) => {
if (configFile === false) {
return (
<>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<code>cypress.json</code> file (currently disabled by <code>--config-file false</code>)
</>
)
}
if (isUndefined(configFile) || configFile === 'cypress.json') {
return (
<>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
<code>cypress.json</code> file
</>
)
}
return (
<>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
custom config file <code>{configFile}</code>
</>
)
}
export {
configFileFormatted,
}

View File

@@ -1,457 +0,0 @@
import _ from 'lodash'
import { $ } from '@packages/driver'
import selectorPlaygroundHighlight from '@packages/runner/src/selector-playground/highlight'
// The '!' tells webpack to disable normal loaders, and keep loaders with `enforce: 'pre'` and `enforce: 'post'`
// This disables the CSSExtractWebpackPlugin and allows us to get the CSS as a raw string instead of saving it to a separate file.
import selectorPlaygroundCSS from '!@packages/runner/src/selector-playground/selector-playground.scss'
const styles = (styleString) => {
return styleString.replace(/\s*\n\s*/g, '')
}
const resetStyles = `
border: none !important;
margin: 0 !important;
padding: 0 !important;
`
function addHitBoxLayer (coords, $body) {
$body = $body || $('body')
const height = 10
const width = 10
const dotHeight = 4
const dotWidth = 4
const top = coords.y - height / 2
const left = coords.x - width / 2
const dotTop = height / 2 - dotHeight / 2
const dotLeft = width / 2 - dotWidth / 2
const boxStyles = styles(`
${resetStyles}
position: absolute;
top: ${top}px;
left: ${left}px;
width: ${width}px;
height: ${height}px;
background-color: red;
border-radius: 5px;
box-shadow: 0 0 5px #333;
z-index: 2147483647;
`)
const $box = $(`<div class="__cypress-highlight" style="${boxStyles}" />`)
const wrapper = $(`<div style="${styles(resetStyles)} position: relative" />`).appendTo($box)
const dotStyles = styles(`
${resetStyles}
position: absolute;
top: ${dotTop}px;
left: ${dotLeft}px;
height: ${dotHeight}px;
width: ${dotWidth}px;
height: ${dotHeight}px;
background-color: pink;
border-radius: 5px;
`)
$(`<div style="${dotStyles}">`).appendTo(wrapper)
return $box.appendTo($body)
}
function addElementBoxModelLayers ($el, $body) {
$body = $body || $('body')
const dimensions = getElementDimensions($el)
const $container = $('<div class="__cypress-highlight">')
.css({
opacity: 0.7,
position: 'absolute',
zIndex: 2147483647,
})
const layers = {
Content: '#9FC4E7',
Padding: '#C1CD89',
Border: '#FCDB9A',
Margin: '#F9CC9D',
}
// create the margin / bottom / padding layers
_.each(layers, (color, attr) => {
let obj
switch (attr) {
case 'Content':
// rearrange the contents offset so
// its inside of our border + padding
obj = {
width: dimensions.width,
height: dimensions.height,
top: dimensions.offset.top + dimensions.borderTop + dimensions.paddingTop,
left: dimensions.offset.left + dimensions.borderLeft + dimensions.paddingLeft,
}
break
default:
obj = {
width: getDimensionsFor(dimensions, attr, 'width'),
height: getDimensionsFor(dimensions, attr, 'height'),
top: dimensions.offset.top,
left: dimensions.offset.left,
}
}
// if attr is margin then we need to additional
// subtract what the actual marginTop + marginLeft
// values are, since offset disregards margin completely
if (attr === 'Margin') {
obj.top -= dimensions.marginTop
obj.left -= dimensions.marginLeft
}
if (attr === 'Padding') {
obj.top += dimensions.borderTop
obj.left += dimensions.borderLeft
}
// bail if the dimensions of this layer match the previous one
// so we dont create unnecessary layers
if (dimensionsMatchPreviousLayer(obj, $container)) return
return createLayer($el, attr, color, $container, obj)
})
$container.appendTo($body)
$container.children().each((index, el) => {
const $el = $(el)
const top = $el.data('top')
const left = $el.data('left')
// dont ask... for some reason we
// have to run offset twice!
_.times(2, () => {
return $el.offset({ top, left })
})
})
return $container
}
function getOrCreateSelectorHelperDom ($body) {
let $container = $body.find('.__cypress-selector-playground')
if ($container.length) {
const shadowRoot = $container[0].shadowRoot
return {
$container,
shadowRoot,
$reactContainer: $(shadowRoot).find('.react-container'),
}
}
$container = $('<div />')
.addClass('__cypress-selector-playground')
.css({ position: 'static' })
.appendTo($body)
const shadowRoot = $container[0].attachShadow({ mode: 'open' })
const $reactContainer = $('<div />')
.addClass('react-container')
.appendTo(shadowRoot)
$('<style />', { html: selectorPlaygroundCSS.toString() }).prependTo(shadowRoot)
return { $container, shadowRoot, $reactContainer }
}
function addOrUpdateSelectorPlaygroundHighlight ({ $el, $body, selector, showTooltip, onClick }) {
const { $container, shadowRoot, $reactContainer } = getOrCreateSelectorHelperDom($body)
if (!$el) {
selectorPlaygroundHighlight.unmount($reactContainer[0])
$reactContainer.off('click')
$container.remove()
return
}
const borderSize = 2
const styles = $el.map((__, el) => {
const $el = $(el)
const offset = $el.offset()
return {
position: 'absolute',
margin: 0,
padding: 0,
width: $el.outerWidth(),
height: $el.outerHeight(),
top: offset.top - borderSize,
left: offset.left - borderSize,
transform: $el.css('transform'),
zIndex: getZIndex($el),
}
}).get()
if ($el.length === 1) {
$reactContainer
.off('click')
.on('click', onClick)
}
selectorPlaygroundHighlight.render($reactContainer[0], {
selector,
appendTo: shadowRoot,
showTooltip,
styles,
})
}
function createLayer ($el, attr, color, container, dimensions) {
const transform = $el.css('transform')
const css = {
transform,
width: dimensions.width,
height: dimensions.height,
position: 'absolute',
zIndex: getZIndex($el),
backgroundColor: color,
}
return $('<div>')
.css(css)
.attr('data-top', dimensions.top)
.attr('data-left', dimensions.left)
.attr('data-layer', attr)
.prependTo(container)
}
function dimensionsMatchPreviousLayer (obj, container) {
// since we're prepending to the container that
// means the previous layer is actually the first child element
const previousLayer = container.children().first().get(0)
// bail if there is no previous layer
if (!previousLayer) {
return
}
return obj.width === previousLayer.offsetWidth &&
obj.height === previousLayer.offsetHeight
}
function getDimensionsFor (dimensions, attr, dimension) {
return dimensions[`${dimension}With${attr}`]
}
function getZIndex (el) {
if (/^(auto|0)$/.test(el.css('zIndex'))) {
return 2147483647
}
return _.toNumber(el.css('zIndex'))
}
function getElementDimensions ($el) {
const el = $el.get(0)
const { offsetHeight, offsetWidth } = el
const box = {
offset: $el.offset(), // offset disregards margin but takes into account border + padding
// dont use jquery here for width/height because it uses getBoundingClientRect() which returns scaled values.
// TODO: switch back to using jquery when upgrading to jquery 3.4+
paddingTop: getPadding($el, 'top'),
paddingRight: getPadding($el, 'right'),
paddingBottom: getPadding($el, 'bottom'),
paddingLeft: getPadding($el, 'left'),
borderTop: getBorder($el, 'top'),
borderRight: getBorder($el, 'right'),
borderBottom: getBorder($el, 'bottom'),
borderLeft: getBorder($el, 'left'),
marginTop: getMargin($el, 'top'),
marginRight: getMargin($el, 'right'),
marginBottom: getMargin($el, 'bottom'),
marginLeft: getMargin($el, 'left'),
}
// NOTE: offsetWidth/height always give us content + padding + border, so subtract them
// to get the true "clientHeight" and "clientWidth".
// we CANNOT just use "clientHeight" and "clientWidth" because those always return 0
// for inline elements >_<
//
box.width = offsetWidth - (box.paddingLeft + box.paddingRight + box.borderLeft + box.borderRight)
box.height = offsetHeight - (box.paddingTop + box.paddingBottom + box.borderTop + box.borderBottom)
// innerHeight: Get the current computed height for the first
// element in the set of matched elements, including padding but not border.
// outerHeight: Get the current computed height for the first
// element in the set of matched elements, including padding, border,
// and optionally margin. Returns a number (without 'px') representation
// of the value or null if called on an empty set of elements.
box.heightWithPadding = box.height + box.paddingTop + box.paddingBottom
box.heightWithBorder = box.heightWithPadding + box.borderTop + box.borderBottom
box.heightWithMargin = box.heightWithBorder + box.marginTop + box.marginBottom
box.widthWithPadding = box.width + box.paddingLeft + box.paddingRight
box.widthWithBorder = box.widthWithPadding + box.borderLeft + box.borderRight
box.widthWithMargin = box.widthWithBorder + box.marginLeft + box.marginRight
return box
}
function getNumAttrValue ($el, attr) {
// nuke anything thats not a number or a negative symbol
const num = _.toNumber($el.css(attr).replace(/[^0-9\.-]+/, ''))
if (!_.isFinite(num)) {
throw new Error('Element attr did not return a valid number')
}
return num
}
function getPadding ($el, dir) {
return getNumAttrValue($el, `padding-${dir}`)
}
function getBorder ($el, dir) {
return getNumAttrValue($el, `border-${dir}-width`)
}
function getMargin ($el, dir) {
return getNumAttrValue($el, `margin-${dir}`)
}
function getOuterSize ($el) {
return {
width: $el.outerWidth(true),
height: $el.outerHeight(true),
}
}
function isInViewport (win, el) {
let rect = el.getBoundingClientRect()
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= win.innerHeight &&
rect.right <= win.innerWidth
)
}
function scrollIntoView (win, el) {
if (!el || isInViewport(win, el)) return
el.scrollIntoView()
}
const sizzleRe = /sizzle/i
function getElementsForSelector ({ $root, selector, method, cypressDom }) {
let $el = null
try {
if (method === 'contains') {
$el = $root.find(cypressDom.getContainsSelector(selector))
if ($el.length) {
$el = cypressDom.getFirstDeepestElement($el)
}
} else {
$el = $root.find(selector)
}
} catch (err) {
// if not a sizzle error, ignore it and let $el be null
if (!sizzleRe.test(err.stack)) throw err
}
return $el
}
function addCssAnimationDisabler ($body) {
$(`
<style id="__cypress-animation-disabler">
*, *:before, *:after {
transition-property: none !important;
animation: none !important;
}
</style>
`).appendTo($body)
}
function removeCssAnimationDisabler ($body) {
$body.find('#__cypress-animation-disabler').remove()
}
function addBlackoutForElement ($body, $el) {
const dimensions = getElementDimensions($el)
const width = dimensions.widthWithBorder
const height = dimensions.heightWithBorder
const top = dimensions.offset.top
const left = dimensions.offset.left
const style = styles(`
${resetStyles}
position: absolute;
top: ${top}px;
left: ${left}px;
width: ${width}px;
height: ${height}px;
background-color: black;
z-index: 2147483647;
`)
$(`<div class="__cypress-blackout" style="${style}">`).appendTo($body)
}
function addBlackout ($body, selector) {
let $el
try {
$el = $body.find(selector)
if (!$el.length) return
} catch (err) {
// if it's an invalid selector, just ignore it
return
}
$el.each(function () {
addBlackoutForElement($body, $(this))
})
}
function removeBlackouts ($body) {
$body.find('.__cypress-blackout').remove()
}
export default {
addBlackout,
removeBlackouts,
addElementBoxModelLayers,
addHitBoxLayer,
addOrUpdateSelectorPlaygroundHighlight,
addCssAnimationDisabler,
removeCssAnimationDisabler,
getElementsForSelector,
getOuterSize,
scrollIntoView,
}

View File

@@ -1,491 +0,0 @@
import _ from 'lodash'
import { EventEmitter } from 'events'
import Promise from 'bluebird'
import { action } from 'mobx'
import { client } from '@packages/socket'
import automation from './automation'
import logger from './logger'
import $Cypress, { $ } from '@packages/driver'
const ws = client.connect({
path: '/__socket.io',
transports: ['websocket'],
})
ws.on('connect', () => {
ws.emit('runner:connected')
})
const driverToReporterEvents = 'paused before:firefox:force:gc after:firefox:force:gc'.split(' ')
const driverToLocalAndReporterEvents = 'run:start run:end'.split(' ')
const driverToSocketEvents = 'backend:request automation:request mocha recorder:frame'.split(' ')
const driverTestEvents = 'test:before:run:async test:after:run'.split(' ')
const driverToLocalEvents = 'viewport:changed config stop url:changed page:loading visit:failed'.split(' ')
const socketRerunEvents = 'runner:restart watched:file:changed'.split(' ')
const socketToDriverEvents = 'net:event script:error'.split(' ')
const localBus = new EventEmitter()
const reporterBus = new EventEmitter()
// NOTE: this is exposed for testing, ideally we should only expose this if a test flag is set
window.runnerWs = ws
// NOTE: this is for testing Cypress-in-Cypress, window.Cypress is undefined here
// unless Cypress has been loaded into the AUT frame
if (window.Cypress) {
window.eventManager = { reporterBus, localBus }
}
/**
* @type {Cypress.Cypress}
*/
let Cypress
const eventManager = {
reporterBus,
getCypress () {
return Cypress
},
addGlobalListeners (state, connectionInfo) {
const rerun = () => {
if (!this) {
// if the tests have been reloaded
// then nothing to rerun
return
}
return this._reRun(state)
}
ws.emit('is:automation:client:connected', connectionInfo, action('automationEnsured', (isConnected) => {
state.automation = isConnected ? automation.CONNECTED : automation.MISSING
ws.on('automation:disconnected', action('automationDisconnected', () => {
state.automation = automation.DISCONNECTED
}))
}))
ws.on('change:to:url', (url) => {
window.location.href = url
})
ws.on('automation:push:message', (msg, data = {}) => {
if (!Cypress) return
switch (msg) {
case 'change:cookie':
Cypress.Cookies.log(data.message, data.cookie, data.removed)
break
default:
break
}
})
ws.on('component:specs:changed', (specs) => {
state.setSpecs(specs)
})
ws.on('dev-server:hmr:error', (error) => {
Cypress.stop()
localBus.emit('script:error', error)
})
_.each(socketRerunEvents, (event) => {
ws.on(event, rerun)
})
_.each(socketToDriverEvents, (event) => {
ws.on(event, (...args) => {
Cypress.emit(event, ...args)
})
})
const logCommand = (logId) => {
const consoleProps = Cypress.runner.getConsolePropsForLogById(logId)
logger.logFormatted(consoleProps)
}
reporterBus.on('runner:console:error', ({ err, commandId }) => {
if (!Cypress) return
if (commandId || err) logger.clearLog()
if (commandId) logCommand(commandId)
if (err) logger.logError(err.stack)
})
reporterBus.on('runner:console:log', (logId) => {
if (!Cypress) return
logger.clearLog()
logCommand(logId)
})
reporterBus.on('focus:tests', this.focusTests)
reporterBus.on('get:user:editor', (cb) => {
ws.emit('get:user:editor', cb)
})
reporterBus.on('set:user:editor', (editor) => {
ws.emit('set:user:editor', editor)
})
reporterBus.on('runner:restart', rerun)
function sendEventIfSnapshotProps (logId, event) {
if (!Cypress) return
const snapshotProps = Cypress.runner.getSnapshotPropsForLogById(logId)
if (snapshotProps) {
localBus.emit(event, snapshotProps)
}
}
reporterBus.on('runner:show:snapshot', (logId) => {
sendEventIfSnapshotProps(logId, 'show:snapshot')
})
reporterBus.on('runner:hide:snapshot', this._hideSnapshot.bind(this))
reporterBus.on('runner:pin:snapshot', (logId) => {
sendEventIfSnapshotProps(logId, 'pin:snapshot')
})
reporterBus.on('runner:unpin:snapshot', this._unpinSnapshot.bind(this))
reporterBus.on('runner:resume', () => {
if (!Cypress) return
Cypress.emit('resume:all')
})
reporterBus.on('runner:next', () => {
if (!Cypress) return
Cypress.emit('resume:next')
})
reporterBus.on('runner:stop', () => {
if (!Cypress) return
Cypress.stop()
})
reporterBus.on('save:state', (state) => {
this.saveState(state)
})
reporterBus.on('external:open', (url) => {
ws.emit('external:open', url)
})
reporterBus.on('open:file', (url) => {
ws.emit('open:file', url)
})
const $window = $(window)
// when we actually unload then
// nuke all of the cookies again
// so we clear out unload
$window.on('unload', () => {
this._clearAllCookies()
})
// when our window triggers beforeunload
// we know we've change the URL and we need
// to clear our cookies
// additionally we set unload to true so
// that Cypress knows not to set any more
// cookies
$window.on('beforeunload', () => {
reporterBus.emit('reporter:restart:test:run')
this._clearAllCookies()
this._setUnload()
})
},
start (config) {
if (config.socketId) {
ws.emit('app:connect', config.socketId)
}
},
setup (config) {
Cypress = this.Cypress = $Cypress.create(config)
// expose Cypress globally
// since CT AUT shares the window with the spec, we don't want to overwrite
// our spec Cypress instance with the component's Cypress instance
if (window.top === window) {
window.Cypress = Cypress
}
this._addCypressListeners(Cypress)
ws.emit('watch:test:file', config.spec)
},
isBrowser (browserName) {
if (!this.Cypress) return false
return this.Cypress.isBrowser(browserName)
},
initialize ($autIframe, config) {
performance.mark('initialize-start')
return Cypress.initialize({
$autIframe,
onSpecReady: () => {
// get the current runnable in case we reran mid-test due to a visit
// to a new domain
ws.emit('get:existing:run:state', (state = {}) => {
if (!Cypress.runner) {
// the tests have been reloaded
return
}
const runnables = Cypress.runner.normalizeAll(state.tests)
const run = () => {
performance.mark('initialize-end')
performance.measure('initialize', 'initialize-start', 'initialize-end')
this._runDriver(state)
}
reporterBus.emit('runnables:ready', runnables)
if (state.numLogs) {
Cypress.runner.setNumLogs(state.numLogs)
}
if (state.startTime) {
Cypress.runner.setStartTime(state.startTime)
}
if (config.isTextTerminal && !state.currentId) {
// we are in run mode and it's the first load
// store runnables in backend and maybe send to dashboard
return ws.emit('set:runnables:and:maybe:record:tests', runnables, run)
}
if (state.currentId) {
// if we have a currentId it means
// we need to tell the Cypress to skip
// ahead to that test
Cypress.runner.resumeAtTest(state.currentId, state.emissions)
}
run()
})
},
})
},
_addCypressListeners (Cypress) {
Cypress.on('message', (msg, data, cb) => {
ws.emit('client:request', msg, data, cb)
})
_.each(driverToSocketEvents, (event) => {
Cypress.on(event, (...args) => {
return ws.emit(event, ...args)
})
})
Cypress.on('collect:run:state', () => {
if (Cypress.env('NO_COMMAND_LOG')) {
return Promise.resolve()
}
return new Promise((resolve) => {
reporterBus.emit('reporter:collect:run:state', resolve)
})
})
Cypress.on('log:added', (log) => {
const displayProps = Cypress.runner.getDisplayPropsForLog(log)
reporterBus.emit('reporter:log:add', displayProps)
})
Cypress.on('log:changed', (log) => {
const displayProps = Cypress.runner.getDisplayPropsForLog(log)
reporterBus.emit('reporter:log:state:changed', displayProps)
})
Cypress.on('before:screenshot', (config, cb) => {
const beforeThenCb = () => {
localBus.emit('before:screenshot', config)
cb()
}
if (Cypress.env('NO_COMMAND_LOG')) {
return beforeThenCb()
}
const wait = !config.appOnly && config.waitForCommandSynchronization
if (!config.appOnly) {
reporterBus.emit('test:set:state', _.pick(config, 'id', 'isOpen'), wait ? beforeThenCb : undefined)
}
if (!wait) beforeThenCb()
})
Cypress.on('after:screenshot', (config) => {
localBus.emit('after:screenshot', config)
})
_.each(driverToReporterEvents, (event) => {
Cypress.on(event, (...args) => {
reporterBus.emit(event, ...args)
})
})
_.each(driverTestEvents, (event) => {
Cypress.on(event, (test, cb) => {
reporterBus.emit(event, test, cb)
})
})
_.each(driverToLocalAndReporterEvents, (event) => {
Cypress.on(event, (...args) => {
localBus.emit(event, ...args)
reporterBus.emit(event, ...args)
})
})
_.each(driverToLocalEvents, (event) => {
Cypress.on(event, (...args) => {
return localBus.emit(event, ...args)
})
})
Cypress.on('script:error', (err) => {
Cypress.stop()
localBus.emit('script:error', err)
})
},
_runDriver (state) {
performance.mark('run-s')
Cypress.run(() => {
performance.mark('run-e')
performance.measure('run', 'run-s', 'run-e')
})
reporterBus.emit('reporter:start', {
firefoxGcInterval: Cypress.getFirefoxGcInterval(),
startTime: Cypress.runner.getStartTime(),
numPassed: state.passed,
numFailed: state.failed,
numPending: state.pending,
autoScrollingEnabled: state.autoScrollingEnabled,
scrollTop: state.scrollTop,
})
},
stop () {
localBus.removeAllListeners()
ws.off()
},
_reRun (state) {
if (!Cypress) return
state.setIsLoading(true)
// when we are re-running we first
// need to stop cypress always
Cypress.stop()
return this._restart()
.then(() => {
// this probably isn't 100% necessary
// since Cypress will fall out of scope
// but we want to be aggressive here
// and force GC early and often
Cypress.removeAllListeners()
localBus.emit('restart')
})
},
_restart () {
return new Promise((resolve) => {
reporterBus.once('reporter:restarted', resolve)
reporterBus.emit('reporter:restart:test:run')
})
},
emit (event, ...args) {
localBus.emit(event, ...args)
},
on (event, ...args) {
localBus.on(event, ...args)
},
off (event, ...args) {
localBus.off(event, ...args)
},
notifyRunningSpec (specFile) {
ws.emit('spec:changed', specFile)
},
focusTests () {
ws.emit('focus:tests')
},
snapshotUnpinned () {
this._unpinSnapshot()
this._hideSnapshot()
reporterBus.emit('reporter:snapshot:unpinned')
},
_unpinSnapshot () {
localBus.emit('unpin:snapshot')
},
_hideSnapshot () {
localBus.emit('hide:snapshot')
},
launchBrowser (browser) {
ws.emit('reload:browser', window.location.toString(), browser && browser.name)
},
// clear all the cypress specific cookies
// whenever our app starts
// and additional when we stop running our tests
_clearAllCookies () {
if (!Cypress) return
Cypress.Cookies.clearCypressCookies()
},
_setUnload () {
if (!Cypress) return
Cypress.Cookies.setCy('unload', true)
},
saveState (state) {
ws.emit('save:app:state', state)
},
}
export default eventManager

View File

@@ -1,8 +1,8 @@
import { action, computed, observable } from 'mobx'
import _ from 'lodash'
import automation from './automation'
import { UIPlugin } from '../plugins/UIPlugin'
import { nanoid } from 'nanoid'
import { automation } from '@packages/runner-shared'
import {
DEFAULT_REPORTER_WIDTH,
LEFT_NAV_WIDTH,
@@ -21,16 +21,13 @@ interface Defaults {
messageType: string
messageControls: unknown
width: number
height: number
reporterWidth: number | null
pluginsHeight: number | null
specListWidth: number | null
isSpecsListOpen: boolean
viewportHeight: number
viewportWidth: number
height: number
width: number
url: string
highlightUrl: boolean
@@ -48,11 +45,8 @@ const _defaults: Defaults = {
messageType: '',
messageControls: null,
width: 500,
height: 500,
viewportHeight: 500,
viewportWidth: 500,
width: 500,
pluginsHeight: PLUGIN_BAR_HEIGHT,
@@ -111,9 +105,6 @@ export default class State {
@observable windowWidth = 0
@observable windowHeight = 0
@observable viewportWidth = _defaults.viewportWidth
@observable viewportHeight = _defaults.viewportHeight
@observable automation = automation.CONNECTING
@observable.ref scriptError: string | undefined
@@ -175,10 +166,10 @@ export default class State {
return 1
}
if (autAreaWidth < this.viewportWidth || autAreaHeight < this.viewportHeight) {
if (autAreaWidth < this.width || autAreaHeight < this.height) {
return Math.min(
autAreaWidth / this.viewportWidth,
autAreaHeight / this.viewportHeight,
autAreaWidth / this.width,
autAreaHeight / this.height,
)
}
@@ -218,9 +209,9 @@ export default class State {
this.screenshotting = screenshotting
}
@action updateAutViewportDimensions (dimensions: { viewportWidth: number, viewportHeight: number }) {
this.viewportHeight = dimensions.viewportHeight
this.viewportWidth = dimensions.viewportWidth
@action updateDimensions (width: number, height: number) {
this.height = height
this.width = width
}
@action toggleIsSpecsListOpen () {
@@ -249,7 +240,11 @@ export default class State {
this.specListWidth = width
}
@action updateWindowDimensions ({ windowWidth, windowHeight }: { windowWidth?: number, windowHeight?: number }) {
@action updateWindowDimensions ({
windowWidth,
windowHeight,
headerHeight,
}: { windowWidth?: number, windowHeight?: number, headerHeight?: number }) {
if (windowWidth) {
this.windowWidth = windowWidth
}
@@ -257,6 +252,10 @@ export default class State {
if (windowHeight) {
this.windowHeight = windowHeight
}
if (headerHeight) {
this.headerHeight = headerHeight
}
}
@action clearMessage () {
@@ -330,7 +329,7 @@ export default class State {
}
runMultiMode = async () => {
const eventManager = require('./event-manager').default
const eventManager = require('@packages/runner-shared').eventManager
const waitForRunEnd = () => new Promise((res) => eventManager.on('run:end', res))
this.setSpec(null)

View File

@@ -4,8 +4,9 @@ import { render } from 'react-dom'
import { utils as driverUtils } from '@packages/driver'
import defaultEvents from '@packages/reporter/src/lib/events'
import App from './app/RunnerCt'
import State from './lib/state'
import Container from './app/container'
import { Container, eventManager } from '@packages/runner-shared'
import util from './lib/util'
// to support async/await
@@ -62,9 +63,20 @@ const Runner = {
Runner.state = state
Runner.configureMobx = configure
state.updateAutViewportDimensions({ viewportWidth: config.viewportWidth, viewportHeight: config.viewportHeight })
state.updateDimensions(config.viewportWidth, config.viewportHeight)
render(<Container config={config} state={state} />, el)
const container = (
<Container
config={config}
runner='ct'
state={state}
App={App}
hasSpecFile={util.hasSpecFile}
eventManager={eventManager}
/>
)
render(container, el)
})()
},
}

View File

@@ -27,7 +27,7 @@
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
// "traceResolution": true,
"strict": false,
"strict": true,
"forceConsistentCasingInFileNames": true,
/**
* Skip type checking of all declaration files (*.d.ts).
@@ -56,6 +56,8 @@
},
"include": [
"./lib/*.ts",
"./src*.ts",
"./src*.tsx",
"./index.ts",
"./index.d.ts",
"./../ts/index.d.ts"

View File

@@ -21,20 +21,31 @@ babelLoader.use.options.plugins.push([require.resolve('babel-plugin-prismjs'), {
css: false,
}])
let pngRule
// @ts-ignore
const nonPngRules = _.filter(commonConfig.module.rules, (rule) => {
// @ts-ignore
if (rule.test.toString().includes('png')) {
pngRule = rule
return false
const { pngRule, nonPngRules } = commonConfig!.module!.rules!.reduce<{
nonPngRules: webpack.RuleSetRule[]
pngRule: webpack.RuleSetRule | undefined
}>((acc, rule) => {
if (rule?.test?.toString().includes('png')) {
return {
...acc,
pngRule: rule,
}
}
return true
return {
...acc,
nonPngRules: [...acc.nonPngRules, rule],
}
}, {
nonPngRules: [],
pngRule: undefined,
})
pngRule.use[0].options = {
if (!pngRule || !pngRule.use) {
throw Error('Could not find png loader')
}
(pngRule.use as webpack.RuleSetLoader[])[0].options = {
name: '[name].[ext]',
outputPath: 'img',
publicPath: '/__cypress/runner/img/',

View File

@@ -0,0 +1,137 @@
{
"plugins": [
"cypress",
"@cypress/dev"
],
"extends": [
"plugin:@cypress/dev/general",
"plugin:@cypress/dev/tests",
"plugin:@cypress/dev/react",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"../reporter/src/.eslintrc.json"
],
"parser": "@typescript-eslint/parser",
"env": {
"cypress/globals": true
},
"rules": {
"react/display-name": "off",
"react/function-component-definition": [
"error",
{
"namedComponents": "arrow-function",
"unnamedComponents": "arrow-function"
}
],
"react/jsx-boolean-value": [
"error",
"always"
],
"react/jsx-closing-bracket-location": [
"error",
"line-aligned"
],
"react/jsx-closing-tag-location": "error",
"react/jsx-curly-brace-presence": [
"error",
{
"props": "never",
"children": "never"
}
],
"react/jsx-curly-newline": "error",
"react/jsx-filename-extension": [
"warn",
{
"extensions": [
".js",
".jsx",
".tsx"
]
}
],
"react/jsx-first-prop-new-line": "error",
"react/jsx-max-props-per-line": [
"error",
{
"maximum": 1,
"when": "multiline"
}
],
"react/jsx-no-bind": [
"error",
{
"ignoreDOMComponents": true
}
],
"react/jsx-no-useless-fragment": "error",
"react/jsx-one-expression-per-line": [
"error",
{
"allow": "literal"
}
],
"react/jsx-sort-props": [
"error",
{
"callbacksLast": true,
"ignoreCase": true,
"noSortAlphabetically": true,
"reservedFirst": true
}
],
"react/jsx-tag-spacing": [
"error",
{
"closingSlash": "never",
"beforeSelfClosing": "always"
}
],
"react/jsx-wrap-multilines": [
"error",
{
"declaration": "parens-new-line",
"assignment": "parens-new-line",
"return": "parens-new-line",
"arrow": "parens-new-line",
"condition": "parens-new-line",
"logical": "parens-new-line",
"prop": "parens-new-line"
}
],
"react/no-array-index-key": "error",
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
"quote-props": [
"error",
"as-needed"
]
},
"overrides": [
{
"files": [
"lib/*"
],
"rules": {
"no-console": 1
}
},
{
"files": [
"**/*.json"
],
"rules": {
"quotes": "off",
"comma-dangle": "off"
}
},
{
"files": "*.tsx",
"rules": {
"no-unused-vars": "off",
"react/jsx-no-bind": "off"
}
}
]
}

View File

@@ -0,0 +1,32 @@
{
"name": "@packages/runner-shared",
"version": "0.0.0-development",
"private": true,
"main": "src/index.ts",
"scripts": {
"test": "yarn test-unit",
"test-unit": "mocha --config test/.mocharc.json src/**/*.spec.* --exit"
},
"dependencies": {
"@cypress/react-tooltip": "0.5.3",
"ansi-to-html": "0.6.14",
"classnames": "2.3.1",
"lodash": "4.17.21",
"mobx": "5.15.4",
"mobx-react": "6.1.8",
"react": "16.8.6",
"react-dom": "16.8.6"
},
"devDependencies": {
"@packages/driver": "0.0.0-development",
"@packages/socket": "0.0.0-development",
"@packages/web-config": "0.0.0-development",
"chai": "4.2.0",
"chai-enzyme": "1.0.0-beta.1",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.2",
"mocha": "7.0.1",
"sinon": "7.5.0",
"sinon-chai": "3.3.0"
}
}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { shallow } from 'enzyme'
import sinon from 'sinon'
import AutomationDisconnected from './automation-disconnected'
import { AutomationDisconnected } from '.'
describe('<AutomationDisconnected />', () => {
it('renders the message', () => {

View File

@@ -1,6 +1,6 @@
import React from 'react'
export default ({ onReload }) => (
export const AutomationDisconnected = ({ onReload }) => (
<div className='runner automation-failure'>
<div className='automation-message automation-disconnected'>
<p>Whoops, the Cypress extension has disconnected.</p>

View File

@@ -0,0 +1,17 @@
import React from 'react'
export const automationElementId = '__cypress-string'
interface AutomationElementProps {
randomString: string
}
export const AutomationElement: React.FC<AutomationElementProps> = ({
randomString,
}) => {
return (
<div id={automationElementId} style={{ display: 'none' }}>
{randomString}
</div>
)
}

View File

@@ -1,4 +1,4 @@
export default {
export const automation = {
CONNECTING: 'CONNECTING',
MISSING: 'MISSING',
CONNECTED: 'CONNECTED',

View File

@@ -1,4 +1,4 @@
export default () => {
export const blankContents = () => {
return `
<style>
body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img,a img{border:none;}address,caption,cite,code,dfn,em,strong,th,var,optgroup{font-style:inherit;font-weight:inherit;}del,ins{text-decoration:none;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:baseline;}sub{vertical-align:baseline;}legend{color:#000;}

View File

@@ -0,0 +1,40 @@
import React from 'react'
import { isUndefined } from 'lodash'
const configFileFormatted = (configFile) => {
if (configFile === false) {
return (
<>
<code>cypress.json</code>
{' '}
file (currently disabled by
{' '}
<code>--config-file false</code>
)
</>
)
}
if (isUndefined(configFile) || configFile === 'cypress.json') {
return (
<>
<code>cypress.json</code>
{' '}
file
</>
)
}
return (
<>
custom config file
<code>
{configFile}
</code>
</>
)
}
export {
configFileFormatted,
}

View File

@@ -2,16 +2,18 @@ import React from 'react'
import { mount, shallow } from 'enzyme'
import sinon from 'sinon'
import App from './app'
import automation from '../lib/automation'
import AutomationDisconnected from '../errors/automation-disconnected'
import NoAutomation from '../errors/no-automation'
import NoSpec from '../errors/no-spec'
import State from '../lib/state'
import App from '@packages/runner/src/app/app'
import { AutomationDisconnected } from '../automation-disconnected'
import { automation } from '../automation'
import { NoAutomation } from '../no-automation'
import { automationElementId } from '../automation-element'
import NoSpec from '@packages/runner/src/errors/no-spec'
import Container, { automationElementId } from './container'
import { Container } from '.'
const createProps = () => ({
runner: 'e2e',
hasSpecFile: sinon.stub(),
config: {
browsers: [],
integrationFolder: '',
@@ -19,6 +21,15 @@ const createProps = () => ({
projectName: '',
viewportHeight: 0,
viewportWidth: 0,
spec: {
name: 'test/spec.js',
relative: './this/is/a/test/spec.js',
absolute: '/Users/me/code/this/is/a/test/spec.js',
},
state: {
autoScrollingEnabled: true,
reporterWidth: 300,
},
},
eventManager: {
addGlobalListeners: sinon.spy(),
@@ -28,11 +39,19 @@ const createProps = () => ({
emit: () => {},
on: () => {},
},
on: () => {},
start: () => {},
setup: () => {},
},
state: new State(),
util: {
hasSpecFile: sinon.stub(),
state: {
automation: undefined,
defaults: {
width: 500,
height: 500,
},
},
App,
NoSpec,
})
describe('<Container />', () => {
@@ -53,12 +72,11 @@ describe('<Container />', () => {
const props = createProps()
props.state.automation = automation.CONNECTING
component = shallow(<Container {...props} />)
component = mount(<Container {...props} />)
})
it('renders the automation element alone', () => {
expect(component.find(`#${automationElementId}`)).to.exist
expect(component.find(`#${automationElementId}`).parent()).not.to.exist
})
})
@@ -117,7 +135,7 @@ describe('<Container />', () => {
beforeEach(() => {
props = createProps()
props.state.automation = automation.CONNECTED
props.util.hasSpecFile.returns(false)
props.hasSpecFile.returns(false)
component = shallow(<Container {...props} />)
})
@@ -125,35 +143,11 @@ describe('<Container />', () => {
expect(component.find(NoSpec)).to.have.prop('config', props.config)
})
it('renders the automation element', () => {
expect(component.find(`#${automationElementId}`)).to.exist
})
it('renders the app when hash changes with and has a spec file', () => {
props.util.hasSpecFile.returns(true)
props.hasSpecFile.returns(true)
component.find(NoSpec).prop('onHashChange')()
component.update()
expect(component.find(App)).to.exist
})
})
describe('when automation is connected and there is a spec file', () => {
let props
let component
beforeEach(() => {
props = createProps()
props.state.automation = automation.CONNECTED
props.util.hasSpecFile.returns(true)
component = shallow(<Container {...props} />)
})
it('renders <App />', () => {
expect(component.find(App)).to.exist
})
it('renders the automation element', () => {
expect(component.find(`#${automationElementId}`)).to.exist
})
})
})

View File

@@ -1,20 +1,13 @@
import { observer } from 'mobx-react'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import automation from '../lib/automation'
import eventManager from '../lib/event-manager'
import State from '../lib/state'
import util from '../lib/util'
import RunnerCt from './RunnerCt'
import AutomationDisconnected from '../errors/automation-disconnected'
import NoAutomation from '../errors/no-automation'
const automationElementId = '__cypress-string'
import { AutomationDisconnected } from '../automation-disconnected'
import { automation } from '../automation'
import { NoAutomation } from '../no-automation'
import { automationElementId, AutomationElement } from '../automation-element'
@observer
class Container extends Component {
export class Container extends Component {
constructor (...args) {
super(...args)
@@ -40,30 +33,34 @@ class Container extends Component {
return this._automationDisconnected()
case automation.CONNECTED:
default:
return this._app()
if (this.props.runner === 'e2e') {
return this.props.hasSpecFile()
? this._app()
: this._noSpec()
}
if (this.props.runner === 'ct') {
return this._app()
}
throw Error(`runner prop is required and must be 'e2e' or 'ct'. You passed: ${this.props.runner}.`)
}
}
_automationElement () {
return (
<div id={automationElementId} style={{ display: 'none' }}>
{this.randomString}
</div>
<AutomationElement randomString={this.randomString} />
)
}
_app () {
return (
<RunnerCt {...this.props}>
{this._automationElement()}
</RunnerCt>
)
}
const { App, ...rest } = this.props
_checkSpecFile = () => {
if (this.props.util.hasSpecFile()) {
this.forceUpdate()
}
return (
<App {...rest}>
{this._automationElement()}
</App>
)
}
_noAutomation () {
@@ -82,18 +79,23 @@ class Container extends Component {
_automationDisconnected () {
return <AutomationDisconnected onReload={this.props.eventManager.launchBrowser} />
}
// This two functions, _noSpec anad _checkSpecFile, are only used by the E2E runner.
// TODO: remove any runner specific code from this file.
_noSpec () {
const { NoSpec } = this.props
return (
<NoSpec config={this.props.config} onHashChange={this._checkSpecFile}>
{this._automationElement()}
</NoSpec>
)
}
// This is only used by the E2E runner.
_checkSpecFile = () => {
if (this.props.hasSpecFile()) {
this.forceUpdate()
}
}
}
Container.defaultProps = {
eventManager,
util,
}
Container.propTypes = {
config: PropTypes.object.isRequired,
state: PropTypes.instanceOf(State),
}
export { automationElementId }
export default Container

View File

@@ -1,10 +1,10 @@
import _ from 'lodash'
import { $ } from '@packages/driver'
import selectorPlaygroundHighlight from '../selector-playground/highlight'
import { selectorPlaygroundHighlight } from './selector-playground/highlight'
// The '!' tells webpack to disable normal loaders, and keep loaders with `enforce: 'pre'` and `enforce: 'post'`
// This disables the CSSExtractWebpackPlugin and allows us to get the CSS as a raw string instead of saving it to a separate file.
import selectorPlaygroundCSS from '!../selector-playground/selector-playground.scss'
import selectorPlaygroundCSS from '!./selector-playground/selector-playground.scss'
const styles = (styleString) => {
return styleString.replace(/\s*\n\s*/g, '')
@@ -443,7 +443,7 @@ function removeBlackouts ($body) {
$body.find('.__cypress-blackout').remove()
}
export default {
export const dom = {
addBlackout,
removeBlackouts,
addElementBoxModelLayers,

View File

@@ -1,4 +1,4 @@
export default {
export const errorMessages = {
reporterError (err, specPath) {
if (!err) return null

View File

@@ -5,10 +5,10 @@ import { action } from 'mobx'
import { client } from '@packages/socket'
import automation from './automation'
import logger from './logger'
import studioRecorder from '../studio/studio-recorder'
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
import { studioRecorder } from './studio'
import { automation } from './automation'
import { logger } from './logger'
import { selectorPlaygroundModel } from './selector-playground'
import $Cypress, { $ } from '@packages/driver'
@@ -26,7 +26,7 @@ const driverToLocalAndReporterEvents = 'run:start run:end'.split(' ')
const driverToSocketEvents = 'backend:request automation:request mocha recorder:frame'.split(' ')
const driverTestEvents = 'test:before:run:async test:after:run'.split(' ')
const driverToLocalEvents = 'viewport:changed config stop url:changed page:loading visit:failed switch:domain'.split(' ')
const socketRerunEvents = 'runner:restart'.split(' ')
const socketRerunEvents = 'runner:restart watched:file:changed'.split(' ')
const socketToDriverEvents = 'net:event script:error'.split(' ')
const localToReporterEvents = 'reporter:log:add reporter:log:state:changed reporter:log:remove'.split(' ')
@@ -47,7 +47,7 @@ if (window.Cypress) {
*/
let Cypress
const eventManager = {
export const eventManager = {
reporterBus,
getCypress () {
@@ -99,6 +99,15 @@ const eventManager = {
rerun()
})
ws.on('component:specs:changed', (specs) => {
state.setSpecs(specs)
})
ws.on('dev-server:hmr:error', (error) => {
Cypress.stop()
localBus.emit('script:error', error)
})
_.each(socketRerunEvents, (event) => {
ws.on(event, rerun)
})
@@ -304,9 +313,13 @@ const eventManager = {
Cypress = this.Cypress = $Cypress.create(config)
// expose Cypress globally
window.Cypress = Cypress
// since CT AUT shares the window with the spec, we don't want to overwrite
// our spec Cypress instance with the component's Cypress instance
if (window.top === window) {
window.Cypress = Cypress
}
this._addListeners()
this._addListeners(Cypress)
ws.emit('watch:test:file', config.spec)
},
@@ -603,6 +616,10 @@ const eventManager = {
localBus.on(event, ...args)
},
off (event, ...args) {
localBus.off(event, ...args)
},
notifyRunningSpec (specFile) {
ws.emit('spec:changed', specFile)
},
@@ -648,5 +665,3 @@ const eventManager = {
ws.emit('save:app:state', state)
},
}
export default eventManager

View File

@@ -5,12 +5,10 @@ import sinon from 'sinon'
import driver from '@packages/driver'
import Tooltip from '@cypress/react-tooltip'
import eventManager from '../lib/event-manager'
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
import studioRecorder from '../studio/studio-recorder'
import Header from './header'
import Studio from '../studio/studio'
import { eventManager } from '../event-manager'
import { Studio, studioRecorder } from '../studio'
import { selectorPlaygroundModel } from '../selector-playground'
import { Header } from '.'
const getState = (props) => _.extend({
defaults: {},
@@ -21,6 +19,7 @@ const propsWithState = (stateProps, configProps = {}) =>
({
state: getState(stateProps),
config: configProps,
runner: 'e2e',
})
describe('<Header />', () => {
@@ -297,7 +296,7 @@ describe('<Header />', () => {
describe('viewport info', () => {
it('has menu-open class on button click', () => {
const component = shallow(<Header {...propsWithState()} />)
const component = mount(<Header {...propsWithState()} />)
component.find('.viewport-info button').simulate('click')
expect(component.find('.viewport-info')).to.have.className('menu-open')
@@ -305,14 +304,14 @@ describe('<Header />', () => {
it('displays width, height, and display scale', () => {
const state = { width: 1, height: 2, displayScale: 3 }
const component = shallow(<Header {...propsWithState(state)} />)
const component = mount(<Header {...propsWithState(state)} />)
expect(component.find('.viewport-info button').text()).to.contain('1 x 2 (3%)')
})
it('displays default width and height in menu', () => {
const state = { defaults: { width: 4, height: 5 } }
const component = shallow(<Header {...propsWithState(state)} />)
const component = mount(<Header {...propsWithState(state)} />)
expect(component.find('.viewport-menu pre').text()).to.contain('"viewportWidth": 4')
expect(component.find('.viewport-menu pre').text()).to.contain('"viewportHeight": 5')

View File

@@ -0,0 +1,242 @@
import cs from 'classnames'
import { action, computed, observable } from 'mobx'
import { observer } from 'mobx-react'
import React, { Component, createRef } from 'react'
import Tooltip from '@cypress/react-tooltip'
import { $ } from '@packages/driver'
import { ViewportInfo } from '../viewport-info'
import { SelectorPlayground } from '../selector-playground/SelectorPlayground'
import { selectorPlaygroundModel } from '../selector-playground'
import { Studio, studioRecorder } from '../studio'
import { eventManager } from '../event-manager'
interface BaseState {
isLoading: boolean
isRunning: boolean
width: number
height: number
displayScale: number | undefined
defaults: {
width: number
height: number
}
updateWindowDimensions: (payload: { headerHeight: number }) => void
}
interface StateCT {
runner: 'ct'
state: {
screenshotting: boolean
} & BaseState
}
interface StateE2E {
runner: 'e2e'
state: {
url: string
isLoadingUrl: boolean
highlightUrl: boolean
} & BaseState
}
interface HeaderBaseProps {
config: {
configFile: string
[k: string]: unknown
}
}
type CtHeaderProps = StateCT & HeaderBaseProps
type E2EHeaderProps = StateE2E & HeaderBaseProps
type HeaderProps = CtHeaderProps | E2EHeaderProps
@observer
export class Header extends Component<HeaderProps> {
@observable showingViewportMenu = false
@observable urlInput = ''
@observable previousSelectorPlaygroundOpen: boolean = false
@observable previousRecorderIsOpen: boolean = false
urlInputRef = createRef<HTMLInputElement>()
headerRef = createRef<HTMLHeadElement>()
get studioForm () {
if (this.props.runner !== 'e2e') {
return
}
return (
<form
className={cs('url-container', {
loading: this.props.runner === 'e2e' && this.props.state.isLoadingUrl,
highlighted: this.props.runner === 'e2e' && this.props.state.highlightUrl,
'menu-open': this._studioNeedsUrl,
})}
onSubmit={this._visitUrlInput}
>
<input
ref={this.urlInputRef}
type='text'
className={cs('url', { 'input-active': this._studioNeedsUrl })}
value={this._studioNeedsUrl ? this.urlInput : this.props.state.url}
readOnly={!this._studioNeedsUrl}
onChange={this._onUrlInput}
onClick={this._openUrl}
/>
<div className='popup-menu url-menu'>
<p>
<strong>Please enter a valid URL to visit.</strong>
</p>
<div className='menu-buttons'>
<button type='button' className='btn-cancel' onClick={this._cancelStudio}>Cancel</button>
<button type='submit' className='btn-submit' disabled={!this.urlInput}>
{`Go `}
<i className='fas fa-arrow-right' />
</button>
</div>
</div>
<span className='loading-container'>
...loading
{' '}
<i className='fas fa-spinner fa-pulse' />
</span>
</form>
)
}
render () {
const { config, state } = this.props
return (
<header
ref={this.headerRef}
className={cs({
'showing-selector-playground': selectorPlaygroundModel.isOpen,
'showing-studio': studioRecorder.isOpen,
'display-none': this.props.runner === 'ct' && this.props.state.screenshotting,
})}
>
<div className='sel-url-wrap'>
<Tooltip
title='Open Selector Playground'
visible={selectorPlaygroundModel.isOpen || studioRecorder.isOpen ? false : null}
wrapperClassName='selector-playground-toggle-tooltip-wrapper'
className='cy-tooltip'
>
<button
aria-label='Open Selector Playground'
className='header-button selector-playground-toggle'
disabled={this.props.state.isLoading || state.isRunning || studioRecorder.isOpen}
onClick={this._togglePlaygroundOpen}
>
<i aria-hidden="true" className='fas fa-crosshairs' />
</button>
</Tooltip>
<div className={cs('menu-cover', { 'menu-cover-display': this._studioNeedsUrl })} />
{this.studioForm}
</div>
<ViewportInfo
showingViewportMenu={this.showingViewportMenu}
width={state.width}
height={state.height}
config={config}
displayScale={this.props.runner === 'e2e' ? state.displayScale : undefined}
defaults={{
width: state.defaults.width,
height: state.defaults.height,
}}
toggleViewportMenu={this._toggleViewportMenu}
/>
<SelectorPlayground
model={selectorPlaygroundModel}
eventManager={eventManager}
/>
{this.props.runner === 'e2e' &&
<Studio model={studioRecorder} hasUrl={!!this.props.state.url} />}
</header>
)
}
@action componentDidMount () {
this.previousSelectorPlaygroundOpen = selectorPlaygroundModel.isOpen
this.previousRecorderIsOpen = studioRecorder.isOpen
this.urlInput = this.props.config.baseUrl ? `${this.props.config.baseUrl}/` : ''
}
@action componentDidUpdate () {
if (selectorPlaygroundModel.isOpen !== this.previousSelectorPlaygroundOpen) {
this._updateWindowDimensions()
this.previousSelectorPlaygroundOpen = selectorPlaygroundModel.isOpen
}
if (studioRecorder.isOpen !== this.previousRecorderIsOpen) {
this._updateWindowDimensions()
this.previousRecorderIsOpen = studioRecorder.isOpen
}
if (this._studioNeedsUrl && this.urlInputRef.current) {
this.urlInputRef.current.focus()
}
}
_togglePlaygroundOpen = () => {
selectorPlaygroundModel.toggleOpen()
}
@action _toggleViewportMenu = () => {
this.showingViewportMenu = !this.showingViewportMenu
}
_updateWindowDimensions = () => {
if (!this.headerRef.current) {
return
}
this.props.state.updateWindowDimensions({
headerHeight: $(this.headerRef.current).outerHeight(),
})
}
_openUrl = () => {
if (this._studioNeedsUrl || this.props.runner !== 'e2e') {
return
}
window.open(this.props.state.url)
}
@computed get _studioNeedsUrl () {
if (this.props.runner !== 'e2e') {
return
}
return studioRecorder.needsUrl && !this.props.state.url
}
@action _onUrlInput = (e) => { // : React.FormEvent<HTMLInputElement>) => {
if (!this._studioNeedsUrl) return
this.urlInput = e.target.value
}
@action _visitUrlInput = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!this._studioNeedsUrl) return
studioRecorder.visitUrl(this.urlInput)
this.urlInput = ''
}
_cancelStudio = () => {
eventManager.emit('studio:cancel')
}
}

View File

@@ -1,15 +1,14 @@
import _ from 'lodash'
import { $ } from '@packages/driver'
import { blankContents } from '../blank-contents'
import { visitFailure } from '../visit-failure'
import { selectorPlaygroundModel } from '../selector-playground'
import { eventManager } from '../event-manager'
import { dom } from '../dom'
import { logger } from '../logger'
import { studioRecorder } from '../studio'
import dom from '../lib/dom'
import logger from '../lib/logger'
import eventManager from '../lib/event-manager'
import visitFailure from './visit-failure'
import blankContents from './blank-contents'
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
import studioRecorder from '../studio/studio-recorder'
export default class AutIframe {
export class AutIframe {
constructor (config) {
this.config = config
this.debouncedToggleSelectorPlayground = _.debounce(this.toggleSelectorPlayground, 300)

View File

@@ -1,15 +1,14 @@
import _ from 'lodash'
import { action } from 'mobx'
import eventManager from '../lib/event-manager'
import selectorPlaygroundModel from '../selector-playground/selector-playground-model'
import studioRecorder from '../studio/studio-recorder'
import { selectorPlaygroundModel } from '../selector-playground'
import { studioRecorder } from '../studio'
import { eventManager } from '../event-manager'
export default class IframeModel {
constructor ({ state, detachDom, removeHeadStyles, restoreDom, highlightEl, snapshotControls }) {
export class IframeModel {
constructor ({ state, detachDom, restoreDom, highlightEl, snapshotControls }) {
this.state = state
this.detachDom = detachDom
this.removeHeadStyles = removeHeadStyles
this.restoreDom = restoreDom
this.highlightEl = highlightEl
this.snapshotControls = snapshotControls
@@ -229,6 +228,8 @@ export default class IframeModel {
htmlAttrs,
snapshot: finalSnapshot,
url: this.state.url,
// TODO: use same attr for both runner and runner-ct states.
// these refer to the same thing - the viewport dimensions.
viewportWidth: this.state.width,
viewportHeight: this.state.height,
}

View File

@@ -0,0 +1,3 @@
export * from './iframe-model'
export * from './aut-iframe'

View File

@@ -0,0 +1,35 @@
export * from './snapshot-controls'
export * from './visit-failure'
export * from './blank-contents'
export * from './message'
export * from './selector-playground'
export * from './script-error'
export * from './mobx'
export * from './error-messages'
export * from './iframe'
export * from './dom'
export * from './logger'
export * from './event-manager'
export * from './automation'
export * from './studio'
export * from './viewport-info'
export * from './config-file-formatted'
export * from './header'
export * from './container'

View File

@@ -2,7 +2,7 @@
import _ from 'lodash'
export default {
export const logger = {
log (...args) {
console.log(...args)
},

View File

@@ -1,10 +1,20 @@
import cs from 'classnames'
import { observer } from 'mobx-react'
import React, { forwardRef } from 'react'
import State from '../lib/state'
import './message.scss'
interface MessageProps {
state: State
state: {
messageTitle?: string
messageControls?: unknown
messageDescription: string
messageType?: string
messageStyles: {
state: string
styles: React.CSSProperties
messageType: string
}
}
}
export const Message = observer(forwardRef<HTMLDivElement, MessageProps>(({ state }, ref) => {

View File

@@ -1,3 +1,5 @@
@import '../variables.scss';
.runner {
.message-container {
display: flex;

View File

@@ -0,0 +1,4 @@
@mixin button-active {
background-color: #e9e9e9;
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}

View File

@@ -48,7 +48,7 @@ const browserPicker = (browsers, onLaunchBrowser) => {
)
}
export default ({ browsers, onLaunchBrowser }) => (
export const NoAutomation = ({ browsers, onLaunchBrowser }) => (
<div className='runner automation-failure'>
<div className='automation-message'>
<p>Whoops, we can't run your tests.</p>

View File

@@ -3,7 +3,7 @@ import React from 'react'
import { shallow } from 'enzyme'
import sinon from 'sinon'
import NoAutomation from './no-automation'
import { NoAutomation } from '.'
const noBrowsers = []
const browsersWithChosen = [

View File

@@ -1,5 +1,5 @@
import React from 'react'
import { namedObserver } from '../lib/mobx'
import { namedObserver } from '../mobx'
const ansiToHtml = require('ansi-to-html')
const convert = new ansiToHtml({
@@ -24,5 +24,3 @@ export const ScriptError: React.FC<{ error: string }> = namedObserver('ScriptErr
/>
)
})
export default ScriptError

View File

@@ -1,7 +1,7 @@
import React from 'react'
import { shallow } from 'enzyme'
import ScriptError from './script-error'
import { ScriptError } from '@packages/runner-shared'
describe('<ScriptError />', () => {
it('renders nothing when there is no script error', () => {
@@ -12,9 +12,9 @@ describe('<ScriptError />', () => {
})
it('renders ansi as colors', () => {
const state = { error: { error: `Webpack Compilation Error
const state = { error: `Webpack Compilation Error
  11 |  it('is true for actual jquery instances', () => 
@ multi ./cypress/integration/dom/jquery_spec.js main[0]` } }
@ multi ./cypress/integration/dom/jquery_spec.js main[0]` }
const component = shallow(<ScriptError {...state} />)
const { dangerouslySetInnerHTML } = component.props()

View File

@@ -4,8 +4,7 @@ import { action, observable } from 'mobx'
import { observer } from 'mobx-react'
import React, { Component } from 'react'
import Tooltip from '@cypress/react-tooltip'
import eventManager from '../lib/event-manager'
import './selector-playground.scss'
const defaultCopyText = 'Copy to clipboard'
const defaultPrintText = 'Print to console'
@@ -113,7 +112,7 @@ class SelectorPlayground extends Component {
{' Learn more'}
</a>
<button className='close' onClick={this._togglePlaygroundOpen}>
x
x
</button>
</div>
)
@@ -155,7 +154,7 @@ x
>
<button onClick={this._toggleMethodPicker}>
<i className='fas fa-caret-down'></i>
{` cy. ${model.method}`}
{` cy.${model.method}`}
</button>
<div className='method-picker'>
{_.map(methods, (method) => (
@@ -216,7 +215,7 @@ x
}
_printToConsole = () => {
eventManager.emit('print:selector:elements:to:console')
this.props.eventManager.emit('print:selector:elements:to:console')
this._setPrintText('Printed!')
}
@@ -244,4 +243,4 @@ x
}
}
export default SelectorPlayground
export { SelectorPlayground }

View File

@@ -31,7 +31,7 @@ function renderHighlight (container, props) {
render(<Highlight {...props} />, container)
}
export default {
export const selectorPlaygroundHighlight = {
render: renderHighlight,
unmount: unmountComponentAtNode,
}

Some files were not shown because too many files have changed in this diff Show More