mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-10 00:59:47 -06:00
Merge branch 'develop' into feature-multidomain
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
20
cli/types/cypress.d.ts
vendored
20
cli/types/cypress.d.ts
vendored
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }),
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -56,5 +56,8 @@
|
||||
"registry": "http://registry.npmjs.org/"
|
||||
},
|
||||
"builders": "./src/builders/builders.json",
|
||||
"ng-add": {
|
||||
"save": "devDependencies"
|
||||
},
|
||||
"schematics": "./src/schematics/collection.json"
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}],
|
||||
}, {
|
||||
|
||||
6
npm/webpack-batteries-included-preprocessor/test/fixtures/.babelrc
vendored
Normal file
6
npm/webpack-batteries-included-preprocessor/test/fixtures/.babelrc
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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, '')
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
57
npm/webpack-preprocessor/test-webpack-4-5.js
Normal file
57
npm/webpack-preprocessor/test-webpack-4-5.js
Normal 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()
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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}'\"",
|
||||
|
||||
@@ -7,9 +7,5 @@
|
||||
"reporter": "cypress-multi-reporters",
|
||||
"reporterOptions": {
|
||||
"configFile": "../../mocha-reporter-config.json"
|
||||
},
|
||||
"retries": {
|
||||
"runMode": 2,
|
||||
"openMode": 0
|
||||
}
|
||||
}
|
||||
|
||||
22
packages/driver/cypress/fixtures/display-binary.html
Normal file
22
packages/driver/cypress/fixtures/display-binary.html
Normal 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>
|
||||
26
packages/driver/cypress/fixtures/dump-binary.html
Normal file
26
packages/driver/cypress/fixtures/dump-binary.html
Normal 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>
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
108
packages/driver/cypress/integration/cypress/keyboard_spec.js
Normal file
108
packages/driver/cypress/integration/cypress/keyboard_spec.js
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
3
packages/driver/index.d.ts
vendored
3
packages/driver/index.d.ts
vendored
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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})`,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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/',
|
||||
|
||||
137
packages/runner-shared/.eslintrc.json
Normal file
137
packages/runner-shared/.eslintrc.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
32
packages/runner-shared/package.json
Normal file
32
packages/runner-shared/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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', () => {
|
||||
@@ -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>
|
||||
17
packages/runner-shared/src/automation-element/index.tsx
Normal file
17
packages/runner-shared/src/automation-element/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export default {
|
||||
export const automation = {
|
||||
CONNECTING: 'CONNECTING',
|
||||
MISSING: 'MISSING',
|
||||
CONNECTED: 'CONNECTED',
|
||||
@@ -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;}
|
||||
40
packages/runner-shared/src/config-file-formatted.tsx
Normal file
40
packages/runner-shared/src/config-file-formatted.tsx
Normal 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,
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
@@ -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,
|
||||
@@ -1,4 +1,4 @@
|
||||
export default {
|
||||
export const errorMessages = {
|
||||
reporterError (err, specPath) {
|
||||
if (!err) return null
|
||||
|
||||
@@ -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
|
||||
@@ -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')
|
||||
242
packages/runner-shared/src/header/index.tsx
Normal file
242
packages/runner-shared/src/header/index.tsx
Normal 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')
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
}
|
||||
3
packages/runner-shared/src/iframe/index.ts
Normal file
3
packages/runner-shared/src/iframe/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './iframe-model'
|
||||
|
||||
export * from './aut-iframe'
|
||||
35
packages/runner-shared/src/index.ts
Normal file
35
packages/runner-shared/src/index.ts
Normal 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'
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import _ from 'lodash'
|
||||
|
||||
export default {
|
||||
export const logger = {
|
||||
log (...args) {
|
||||
console.log(...args)
|
||||
},
|
||||
@@ -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) => {
|
||||
@@ -1,3 +1,5 @@
|
||||
@import '../variables.scss';
|
||||
|
||||
.runner {
|
||||
.message-container {
|
||||
display: flex;
|
||||
4
packages/runner-shared/src/mixins.scss
Normal file
4
packages/runner-shared/src/mixins.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
@mixin button-active {
|
||||
background-color: #e9e9e9;
|
||||
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 = [
|
||||
@@ -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
|
||||
@@ -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
|
||||
[0m [90m 11 | [39m it([32m'is true for actual jquery instances'[39m[33m,[39m () [33m=>[39m [0m
|
||||
@ 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()
|
||||
|
||||
@@ -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 }
|
||||
@@ -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
Reference in New Issue
Block a user