mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-03 05:20:38 -05:00
Merge remote-tracking branch 'origin/10.0-release' into UNIFY-1302-slowTestThreshold-by-testing-type
This commit is contained in:
@@ -9,6 +9,9 @@ _test-output
|
||||
cypress.zip
|
||||
.babel-cache
|
||||
|
||||
# from config, compiled .js files
|
||||
packages/config/src/*.js
|
||||
|
||||
# from extension
|
||||
Cached Theme.pak
|
||||
Cached Theme Material Design.pak
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"chrome:beta": "100.0.4896.20",
|
||||
"chrome:beta": "100.0.4896.30",
|
||||
"chrome:stable": "99.0.4844.51"
|
||||
}
|
||||
|
||||
@@ -116,14 +116,6 @@
|
||||
"default": false,
|
||||
"description": "Path to folder containing component test files (Pass false to disable)"
|
||||
},
|
||||
"pluginsFile": {
|
||||
"type": [
|
||||
"string",
|
||||
"boolean"
|
||||
],
|
||||
"default": "cypress/plugins/index.js",
|
||||
"description": "Path to plugins file. (Pass false to disable)"
|
||||
},
|
||||
"screenshotOnRunFailure": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@@ -260,11 +252,6 @@
|
||||
"default": false,
|
||||
"description": "Polyfills `window.fetch` to enable Network spying and stubbing"
|
||||
},
|
||||
"experimentalStudio": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Generate and save commands directly to your test suite by interacting with your app as an end user would."
|
||||
},
|
||||
"retries": {
|
||||
"type": [
|
||||
"object",
|
||||
|
||||
Vendored
-10
@@ -2713,11 +2713,6 @@ declare namespace Cypress {
|
||||
* @default "bundled"
|
||||
*/
|
||||
nodeVersion: 'system' | 'bundled'
|
||||
/**
|
||||
* Path to plugins file. (Pass false to disable)
|
||||
* @default "cypress/plugins/index.js"
|
||||
*/
|
||||
pluginsFile: string | false
|
||||
/**
|
||||
* The application under test cannot redirect more than this limit.
|
||||
* @default 20
|
||||
@@ -2818,11 +2813,6 @@ declare namespace Cypress {
|
||||
* @default false
|
||||
*/
|
||||
experimentalSourceRewriting: boolean
|
||||
/**
|
||||
* Generate and save commands directly to your test suite by interacting with your app as an end user would.
|
||||
* @default false
|
||||
*/
|
||||
experimentalStudio: boolean
|
||||
/**
|
||||
* Number of times to retry a failed test.
|
||||
* If a number is set, tests will retry in both runMode and openMode.
|
||||
|
||||
@@ -9,7 +9,6 @@ const pluginConfig2: Cypress.PluginConfig = (on, config) => {
|
||||
config.baseUrl // $ExpectType string | null
|
||||
config.configFile // $ExpectType string | false
|
||||
config.fixturesFolder // $ExpectType string | false
|
||||
config.pluginsFile // $ExpectType string | false
|
||||
config.screenshotsFolder // $ExpectType string | false
|
||||
config.videoCompression // $ExpectType number | false
|
||||
config.projectRoot // $ExpectType string
|
||||
|
||||
+12
-6
@@ -156,15 +156,21 @@ module.exports = {
|
||||
|
||||
`npm install -D @cypress/code-coverage`
|
||||
|
||||
- Then add the code below to your supportFile and pluginsFile
|
||||
- Then add the code below to your component support file
|
||||
|
||||
```javascript
|
||||
// cypress/support/component.js
|
||||
import '@cypress/code-coverage/support';
|
||||
// cypress/plugins/index.js
|
||||
module.exports = (on, config) => {
|
||||
require('@cypress/code-coverage/task')(on, config);
|
||||
return config;
|
||||
```
|
||||
- Then add the code below to your cypress configuration
|
||||
```js
|
||||
{
|
||||
...
|
||||
component: {
|
||||
setupNodeEvents(on, config) {
|
||||
require('@cypress/code-coverage/task')(on, config);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
# [@cypress/react-v5.12.4](https://github.com/cypress-io/cypress/compare/@cypress/react-v5.12.3...@cypress/react-v5.12.4) (2022-03-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* avoid nextjs unsafeCache and watchOptions ([#20440](https://github.com/cypress-io/cypress/issues/20440)) ([9f60901](https://github.com/cypress-io/cypress/commit/9f6090170b0675d25b26b98cd0f987a5e395ab78))
|
||||
|
||||
# [@cypress/react-v5.12.3](https://github.com/cypress-io/cypress/compare/@cypress/react-v5.12.2...@cypress/react-v5.12.3) (2022-02-10)
|
||||
|
||||
|
||||
|
||||
@@ -41,6 +41,15 @@ async function getNextWebpackConfig (config) {
|
||||
|
||||
checkSWC(nextWebpackConfig, config)
|
||||
|
||||
if (nextWebpackConfig.watchOptions && Array.isArray(nextWebpackConfig.watchOptions.ignored)) {
|
||||
nextWebpackConfig.watchOptions = {
|
||||
...nextWebpackConfig.watchOptions,
|
||||
ignored: [...nextWebpackConfig.watchOptions.ignored.filter((pattern) => !/node_modules/.test(pattern)), '**/node_modules/!(@cypress/webpack-dev-server/dist/browser.js)**'],
|
||||
}
|
||||
|
||||
debug('found options next.js watchOptions.ignored %O', nextWebpackConfig.watchOptions.ignored)
|
||||
}
|
||||
|
||||
return nextWebpackConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'cypress'
|
||||
import { startDevServer } from './dist'
|
||||
|
||||
export default defineConfig({
|
||||
'video': false,
|
||||
'fixturesFolder': false,
|
||||
'component': {
|
||||
'supportFile': './cypress/support.js',
|
||||
specPattern: '**/smoke.cy.ts',
|
||||
// startDevServer is the legacy distribution that was renamed
|
||||
// to devServer to align with Cypress 10.0 configuration pitons.
|
||||
// This configuration verifying backwards compatibility.
|
||||
devServer (options) {
|
||||
return startDevServer({ options })
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -2,17 +2,10 @@ import { defineConfig } from 'cypress'
|
||||
import { devServer } from './dist'
|
||||
|
||||
export default defineConfig({
|
||||
'pluginsFile': 'cypress/plugins.js',
|
||||
'video': false,
|
||||
'fixturesFolder': false,
|
||||
'component': {
|
||||
'supportFile': 'cypress/support.js',
|
||||
devServer (cypressDevServerConfig) {
|
||||
const path = require('path')
|
||||
|
||||
return devServer(cypressDevServerConfig, {
|
||||
configFile: path.resolve(__dirname, 'vite.config.ts'),
|
||||
})
|
||||
},
|
||||
'supportFile': './cypress/support.js',
|
||||
devServer,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* This file is intended to test the new normalized signature
|
||||
* of devServers. To make the test shorter we only test
|
||||
* the smkoke test here
|
||||
*/
|
||||
|
||||
const path = require('path')
|
||||
const { devServer } = require('../../dist')
|
||||
|
||||
module.exports = (on, config) => {
|
||||
on('dev-server:start', async (options) => {
|
||||
return devServer(
|
||||
options,
|
||||
{
|
||||
configFile: path.resolve(__dirname, '..', '..', 'vite.config.ts'),
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
config.component.specPattern = '**/smoke.spec.ts'
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -9,8 +9,8 @@
|
||||
"check-ts": "tsc --noEmit",
|
||||
"cy:open": "node ../../scripts/cypress.js open --component --project ${PWD}",
|
||||
"cy:run": "node ../../scripts/cypress.js run --component --project ${PWD}",
|
||||
"cy:run-signature": "yarn cy:run --config=\"{\\\"pluginsFile\\\":\\\"cypress/new-signature/plugins.js\\\"}\"",
|
||||
"test": "yarn cy:run && yarn cy:run-signature",
|
||||
"cy:run-legacy": "yarn cy:run --config-file ./cypress.config.legacy.ts",
|
||||
"test": "yarn cy:run && yarn cy:run-legacy",
|
||||
"watch": "tsc -w"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
# [@cypress/webpack-dev-server-v1.8.2](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v1.8.1...@cypress/webpack-dev-server-v1.8.2) (2022-03-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* avoid nextjs unsafeCache and watchOptions ([#20440](https://github.com/cypress-io/cypress/issues/20440)) ([9f60901](https://github.com/cypress-io/cypress/commit/9f6090170b0675d25b26b98cd0f987a5e395ab78))
|
||||
* error regression - strip ansi colors out of cy.fixture() error message ([#20335](https://github.com/cypress-io/cypress/issues/20335)) ([e0bd6ac](https://github.com/cypress-io/cypress/commit/e0bd6ac2aaf8d00b9233fffefed8f6ed2484bf45))
|
||||
|
||||
# [@cypress/webpack-dev-server-v1.8.1](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v1.8.0...@cypress/webpack-dev-server-v1.8.1) (2022-02-08)
|
||||
|
||||
|
||||
|
||||
@@ -96,6 +96,14 @@ export async function makeWebpackConfig (userWebpackConfig: webpack.Configuratio
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof userWebpackConfig?.module?.unsafeCache === 'function') {
|
||||
const originalCachePredicate = userWebpackConfig.module.unsafeCache
|
||||
|
||||
userWebpackConfig.module.unsafeCache = (module: any) => {
|
||||
return originalCachePredicate(module) && !/[\\/]webpack-dev-server[\\/]dist[\\/]browser\.js/.test(module.resource)
|
||||
}
|
||||
}
|
||||
|
||||
const mergedConfig = merge<webpack.Configuration>(
|
||||
userWebpackConfig,
|
||||
makeDefaultWebpackConfig(indexHtmlFile),
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cypress",
|
||||
"version": "9.5.1",
|
||||
"version": "9.5.2",
|
||||
"description": "Cypress.io end to end testing tool",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
+71
-9
@@ -10,15 +10,80 @@ This is the front-end for the Cypress App.
|
||||
3. Open chrome (or another browser)
|
||||
4. It will show the new Vite powered app
|
||||
|
||||
## How it works
|
||||
## How the App works
|
||||
|
||||
Cypress has two modes: `run` and `open`. We want run mode to be as light and fast as possible, since this is the mode used to run on CI machines, etc. Open mode is the interactive experience, with the command log, snapshots, selector playground, etc.
|
||||
Cypress has two modes: `run` and `open`. We want run mode to be as light and fast as possible, since this is the mode used to run on CI machines, etc. Run mode has minimal UI showing only what is necessary. Open mode is the interactive experience.
|
||||
|
||||
- **`open`** mode is driven using GraphQL and urql. It shows the full Cypress app, include the top nav, side nav, spec list, etc. You can change between testing types, check your latest runs on the Cypress dashboard, updating settings, etc.
|
||||
- **`run`** mode is does not rely on GraphQL. This is so we can be as performant as possible. It only renders the "runner" part of the UI, which is comprised of the command log and AUT iframe.
|
||||
- **`open`** mode is driven using GraphQL and urql. It shows the full Cypress app, include the top nav, side nav, spec list, etc. You can change between testing types, check your latest runs on the Cypress dashboard, update settings, etc.
|
||||
- **`run`** mode is does not rely on GraphQL. This is so we can be as performant as possible. It only renders the "runner" part of the UI, which is comprised of the command log, Spec Runner header, and AUT iframe.
|
||||
|
||||
The two modes are composed using the same logic, but have slightly different components. You can see where the differences are in `Runner.vue`(src/pages/Specs/Runner.vue). Notice that `<SpecRunnerOpenMode>` receives a `gql` prop, since it uses GraphQL, and `<SpecRunnerRunMode>` does not.
|
||||
|
||||
## Router
|
||||
|
||||
The App's routing doesn't need to be touched often, because it is almost all auto-generated based on the file structure. There's so little code that it helps to describe the approach here so that when we do want to modify a route or do some other route-specific behavior, we can get our bearings.
|
||||
|
||||
[`vite-plugin-pages`](https://github.com/hannoeru/vite-plugin-pages) is used to generate routes based on the page-level Vue components contained in `src/pages`. These generated routes are pulled into a standard [Vue Router](https://router.vuejs.org/) setup using `createRouter()` in [`router.ts`](src/router/router.ts).
|
||||
|
||||
Route configuration that might typically appear in `router.ts` can be set in a `<route>` block in these page components, for example `name` and `meta` properties (documented in Vue Router docs):
|
||||
|
||||
```ts
|
||||
<route>
|
||||
{
|
||||
name: 'SpecsRunner',
|
||||
meta: {
|
||||
header: false,
|
||||
navBarExpandedAllowed: false
|
||||
}
|
||||
}
|
||||
</route>
|
||||
```
|
||||
|
||||
The advantage here is that the route definition is co-located with the component. The route block will be parsed as JSON5 by default, which means that certain Vue Router properties aren't valid in the `<route>` block, such as if a function is needed to be the value.
|
||||
|
||||
So far, we haven't needed to use any such properties, but if we do, [`extendRoute`](https://github.com/hannoeru/vite-plugin-pages#extendroute) can be used where the `Pages` plugin is added in `vite.config.ts` to extend the generated route objects, using regular TypeScript.
|
||||
|
||||
### Some gotchas if you are new to Vue Router
|
||||
|
||||
#### `name` vs `path`
|
||||
|
||||
With the implicit setup of the router, routes are named based on the file name. This is not always ideal as the route's name is used in the UI as the title. The `name` property can be overridden in the `<route>` block of a single file component, so that we can have a more appropriate user-facing name, or something that's more explicit and stable when we intend to refer to it in code.
|
||||
|
||||
In addition to sometimes showing a route's name in the UI, we also use the `name` value to push a new route object to the router (either in code directly, or using `RouterLink`s), if that object includes `params`. Otherwise we usually use `path` when pushing to the router.
|
||||
|
||||
Example of `name` + `params` in a route object:
|
||||
|
||||
```ts
|
||||
router.push({
|
||||
name: 'Specs',
|
||||
params: {
|
||||
unrunnable: router.currentRoute.value.query.file,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Example of pushing with `path` + `query`:
|
||||
|
||||
```ts
|
||||
router.push({
|
||||
path: '/specs/runner',
|
||||
query: { file: row.data.relative }
|
||||
})
|
||||
```
|
||||
|
||||
These objects in this format can be used either with `push` to the router directly or in a `router-link`'s `to` prop.
|
||||
|
||||
#### `params` vs `query`
|
||||
|
||||
More details on all of this are at https://router.vuejs.org/ - these are just some examples of things we are using here:
|
||||
|
||||
We use `params` when there is temporary state that should be present when a user gets to a route from somewhere else in the App, but shouldn't be present after page refresh or when using the `back` button. An example of this in use is the notice that appears over the AUT if you visit a spec from the "View this Spec" link right after creating it. This banner is useful only in that narrow situation, so a route param is a nice way to have it open when needed, and then ignored at other times, without needing to put it in a store and clean it up.
|
||||
|
||||
For values that should survive a page refresh, we use `query` in the route object to put the values into the URL's query parameters. This is the most common way to attach state to a particular route, we usually want that state to be present if we refresh the page or copy and paste a link.
|
||||
|
||||
The terminology can get a bit confusing as Vue Router's `params` are not the query parameters in the url. Vue Router's `params` can be accessed and used to form dynamic parts of a URL's path, but we are not currently using that feature, so for us `params` are used as described above. The option to interpolate values from `params` into a `path` is why the `to` property needs to be the route's `name` in order for `params` to be passed. Vue will warn, not error, if we try to do specify both `path` and `params` in a `to` or a router push, and our `params` will be ignored.
|
||||
|
||||
|
||||
## Using existing, Vite-incompatible modules
|
||||
|
||||
Some of our modules, like `@packages/reporter`, `@packages/driver` and `@packages/runner-shared` cannot be easily
|
||||
@@ -59,10 +124,10 @@ Cy has a very custom icon library.
|
||||
*/
|
||||
|
||||
<i-cy-book_x16 class="
|
||||
hover:icon-dark-pink-500
|
||||
hover:icon-light-purple-300
|
||||
icon-dark-pink-300
|
||||
icon-dark-purple-50
|
||||
hover:icon-dark-pink-500
|
||||
hover:icon-light-purple-300
|
||||
" />
|
||||
```
|
||||
|
||||
@@ -79,7 +144,4 @@ If an icon path doesn't define a class, nothing bad will happen, it just won't g
|
||||
|
||||
2. Finally, you don't need to expose anything. `./src/assets/icons` is automatically watched and loaded 😮
|
||||
|
||||
## Diagram
|
||||
|
||||
]
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ export default defineConfig({
|
||||
},
|
||||
'e2e': {
|
||||
baseUrl: 'http://localhost:5555',
|
||||
pluginsFile: 'cypress/e2e/plugins/index.ts',
|
||||
supportFile: 'cypress/e2e/support/e2eSupport.ts',
|
||||
async setupNodeEvents (on, config) {
|
||||
if (!process.env.HTTP_PROXY_TARGET_FOR_ORIGIN_REQUESTS) {
|
||||
|
||||
@@ -16,7 +16,7 @@ function getRunnerHref (specPath: string) {
|
||||
|
||||
describe('App: Index', () => {
|
||||
describe('Testing Type: E2E', () => {
|
||||
context('project with default spec pattern', () => {
|
||||
context('js project with default spec pattern', () => {
|
||||
beforeEach(() => {
|
||||
cy.scaffoldProject('no-specs-no-storybook')
|
||||
cy.openProject('no-specs-no-storybook')
|
||||
@@ -219,6 +219,110 @@ describe('App: Index', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('ts project with default spec pattern', () => {
|
||||
beforeEach(() => {
|
||||
cy.scaffoldProject('no-specs-no-storybook')
|
||||
cy.openProject('no-specs-no-storybook')
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('tsconfig.json', '{}')
|
||||
})
|
||||
|
||||
cy.openProject('no-specs-no-storybook')
|
||||
|
||||
cy.startAppServer('e2e')
|
||||
cy.__incorrectlyVisitAppWithIntercept()
|
||||
|
||||
// With no specs present, the page renders two cards, one for scaffolding example specs,
|
||||
// another for creating a new blank spec.
|
||||
cy.findAllByTestId('card').eq(0).as('ScaffoldCard')
|
||||
.within(() => {
|
||||
cy.findByRole('button', {
|
||||
name: defaultMessages.createSpec.e2e.importFromScaffold.header,
|
||||
}).should('be.visible')
|
||||
.and('not.be.disabled')
|
||||
|
||||
cy.contains(defaultMessages.createSpec.e2e.importFromScaffold.description)
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
cy.findAllByTestId('card').eq(1).as('EmptySpecCard')
|
||||
.within(() => {
|
||||
cy.findByRole('button', {
|
||||
name: defaultMessages.createSpec.e2e.importEmptySpec.header,
|
||||
}).should('be.visible')
|
||||
.and('not.be.disabled')
|
||||
|
||||
cy.contains(defaultMessages.createSpec.e2e.importEmptySpec.description)
|
||||
.should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
context('scaffold empty spec', () => {
|
||||
it('should generate empty spec for a TS project', () => {
|
||||
// Verify the modal can be closed
|
||||
cy.get('@EmptySpecCard').click()
|
||||
cy.get('body').click(0, 0)
|
||||
cy.get('[data-cy="create-spec-modal"]').should('not.exist')
|
||||
cy.get('@EmptySpecCard').click()
|
||||
cy.get('[aria-label="Close"]').click()
|
||||
cy.get('[data-cy="create-spec-modal"]').should('not.exist')
|
||||
cy.get('@EmptySpecCard').click()
|
||||
cy.contains('button', defaultMessages.components.button.back).click()
|
||||
cy.get('[data-cy="create-spec-modal"]').within(() => {
|
||||
cy.get('[data-cy="card"]').contains(defaultMessages.createSpec.e2e.importEmptySpec.header).click()
|
||||
})
|
||||
|
||||
cy.percySnapshot('Default')
|
||||
|
||||
cy.findAllByLabelText(defaultMessages.createSpec.e2e.importEmptySpec.inputPlaceholder)
|
||||
.as('enterSpecInput')
|
||||
|
||||
cy.get('@enterSpecInput').invoke('val').should('eq', getPathForPlatform('cypress/e2e/filename.cy.ts'))
|
||||
cy.contains(defaultMessages.createSpec.e2e.importEmptySpec.invalidSpecWarning).should('not.exist')
|
||||
cy.get('@enterSpecInput').clear()
|
||||
cy.contains(defaultMessages.createSpec.e2e.importEmptySpec.invalidSpecWarning).should('not.exist')
|
||||
|
||||
// Shows entered file does not match spec pattern
|
||||
cy.get('@enterSpecInput').type(getPathForPlatform('cypress/e2e/no-match'))
|
||||
cy.contains(defaultMessages.createSpec.e2e.importEmptySpec.invalidSpecWarning)
|
||||
cy.contains('button', defaultMessages.createSpec.createSpec).should('be.disabled')
|
||||
|
||||
cy.percySnapshot('Invalid spec error')
|
||||
|
||||
//Shows extension warning
|
||||
cy.get('@enterSpecInput').clear().type(getPathForPlatform('cypress/e2e/MyTest.spec.t'))
|
||||
cy.intercept('mutation-EmptyGenerator_MatchSpecFile', (req) => {
|
||||
if (req.body.variables.specFile === getPathForPlatform('cypress/e2e/MyTest.spec.tx')) {
|
||||
req.on('before:response', (res) => {
|
||||
res.body.data.matchesSpecPattern = true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
cy.get('@enterSpecInput').type('x')
|
||||
cy.contains(defaultMessages.createSpec.e2e.importEmptySpec.specExtensionWarning)
|
||||
cy.percySnapshot('Non-recommended spec pattern warning')
|
||||
cy.contains('span', '{filename}.cy.tx')
|
||||
|
||||
// Create spec
|
||||
cy.get('@enterSpecInput').clear().type(getPathForPlatform('cypress/e2e/MyTest.cy.ts'))
|
||||
cy.contains('button', defaultMessages.createSpec.createSpec).should('not.be.disabled').click()
|
||||
cy.contains('h2', defaultMessages.createSpec.successPage.header)
|
||||
|
||||
cy.get('[data-cy="file-row"]').contains(getPathForPlatform('cypress/e2e/MyTest.cy.ts')).click()
|
||||
|
||||
cy.percySnapshot('Generator success')
|
||||
|
||||
cy.get('pre').should('contain', 'describe(\'MyTest.cy.ts\'')
|
||||
|
||||
cy.get('[aria-label="Close"]').click()
|
||||
|
||||
cy.visitApp().get('[data-cy="specs-list-row"]').contains('MyTest.cy.ts')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('project with custom spec pattern', () => {
|
||||
beforeEach(() => {
|
||||
cy.scaffoldProject('no-specs-custom-pattern')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SinonStub } from 'sinon'
|
||||
import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json'
|
||||
import type { AuthStateShape } from '@packages/data-context/src/data'
|
||||
|
||||
const pkg = require('@packages/root')
|
||||
|
||||
@@ -366,12 +366,14 @@ describe('App Top Nav Workflows', () => {
|
||||
const mockLogInActionsForUser = (user) => {
|
||||
cy.withCtx((ctx, options) => {
|
||||
options.sinon.stub(ctx._apis.authApi, 'logIn').callsFake(async (onMessage) => {
|
||||
onMessage({ browserOpened: true } as AuthStateShape)
|
||||
setTimeout(() => {
|
||||
onMessage({ browserOpened: true })
|
||||
}, 500)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(options.user)
|
||||
}, 2000) // timeout ensures full auth browser lifecycle is testable
|
||||
}, 1000)
|
||||
})
|
||||
})
|
||||
}, { user })
|
||||
@@ -445,12 +447,16 @@ describe('App Top Nav Workflows', () => {
|
||||
})
|
||||
|
||||
it('shows correct error when browser cannot launch', () => {
|
||||
cy.withCtx((ctx) => {
|
||||
ctx.coreData.authState = {
|
||||
name: 'AUTH_COULD_NOT_LAUNCH_BROWSER',
|
||||
message: 'http://127.0.0.1:0000/redirect-to-auth',
|
||||
browserOpened: false,
|
||||
}
|
||||
cy.withCtx((ctx, o) => {
|
||||
o.sinon.stub(ctx._apis.authApi, 'logIn').callsFake(async (onMessage) => {
|
||||
onMessage({
|
||||
name: 'AUTH_COULD_NOT_LAUNCH_BROWSER',
|
||||
message: 'http://127.0.0.1:0000/redirect-to-auth',
|
||||
browserOpened: false,
|
||||
})
|
||||
|
||||
throw new Error()
|
||||
})
|
||||
})
|
||||
|
||||
cy.findByTestId('app-header-bar').within(() => {
|
||||
@@ -458,23 +464,31 @@ describe('App Top Nav Workflows', () => {
|
||||
cy.findByRole('button', { name: 'Log In' }).click()
|
||||
})
|
||||
|
||||
cy.contains('http://127.0.0.1:0000/redirect-to-auth').should('be.visible')
|
||||
cy.contains(loginText.titleBrowserError).should('be.visible')
|
||||
cy.contains(loginText.bodyBrowserError).should('be.visible')
|
||||
cy.contains(loginText.bodyBrowserErrorDetails).should('be.visible')
|
||||
cy.findByRole('dialog', { name: 'Log in to Cypress' }).within(() => {
|
||||
cy.findByRole('button', { name: 'Log In' }).click()
|
||||
|
||||
// in this state, there is no retry UI, we ask the user to visit the auth url on their own
|
||||
cy.contains('button', loginText.actionTryAgain).should('not.exist')
|
||||
cy.contains('button', loginText.actionCancel).should('not.exist')
|
||||
cy.contains('http://127.0.0.1:0000/redirect-to-auth').should('be.visible')
|
||||
cy.contains(loginText.titleBrowserError).should('be.visible')
|
||||
cy.contains(loginText.bodyBrowserError).should('be.visible')
|
||||
cy.contains(loginText.bodyBrowserErrorDetails).should('be.visible')
|
||||
|
||||
// in this state, there is no retry UI, we ask the user to visit the auth url on their own
|
||||
cy.contains('button', loginText.actionTryAgain).should('not.be.visible')
|
||||
cy.contains('button', loginText.actionCancel).should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows correct error when error other than browser-launch happens', () => {
|
||||
cy.withCtx((ctx) => {
|
||||
ctx.coreData.authState = {
|
||||
name: 'AUTH_ERROR_DURING_LOGIN',
|
||||
message: 'An unexpected error occurred',
|
||||
browserOpened: false,
|
||||
}
|
||||
cy.withCtx((ctx, o) => {
|
||||
o.sinon.stub(ctx._apis.authApi, 'logIn').callsFake(async (onMessage) => {
|
||||
onMessage({
|
||||
name: 'AUTH_ERROR_DURING_LOGIN',
|
||||
message: 'An unexpected error occurred',
|
||||
browserOpened: false,
|
||||
})
|
||||
|
||||
throw new Error()
|
||||
})
|
||||
})
|
||||
|
||||
cy.findByTestId('app-header-bar').within(() => {
|
||||
@@ -482,34 +496,49 @@ describe('App Top Nav Workflows', () => {
|
||||
cy.findByRole('button', { name: 'Log In' }).click()
|
||||
})
|
||||
|
||||
cy.contains(loginText.titleFailed).should('be.visible')
|
||||
cy.contains(loginText.bodyError).should('be.visible')
|
||||
cy.contains('An unexpected error occurred').should('be.visible')
|
||||
cy.findByRole('dialog', { name: 'Log in to Cypress' }).within(() => {
|
||||
cy.findByRole('button', { name: 'Log In' }).click()
|
||||
|
||||
cy.contains('button', loginText.actionTryAgain).should('be.visible').as('tryAgain')
|
||||
cy.contains('button', loginText.actionCancel).should('be.visible')
|
||||
cy.contains(loginText.titleFailed).should('be.visible')
|
||||
cy.contains(loginText.bodyError).should('be.visible')
|
||||
cy.contains('An unexpected error occurred').should('be.visible')
|
||||
|
||||
cy.contains('button', loginText.actionTryAgain).should('be.visible').as('tryAgain')
|
||||
cy.contains('button', loginText.actionCancel).should('be.visible')
|
||||
})
|
||||
|
||||
cy.percySnapshot()
|
||||
|
||||
cy.withCtx((ctx) => {
|
||||
ctx.coreData.authState = {
|
||||
name: 'AUTH_BROWSER_LAUNCHED',
|
||||
message: '',
|
||||
browserOpened: true,
|
||||
}
|
||||
(ctx._apis.authApi.logIn as SinonStub).callsFake(async (onMessage) => {
|
||||
onMessage({
|
||||
name: 'AUTH_BROWSER_LAUNCHED',
|
||||
message: '',
|
||||
browserOpened: true,
|
||||
})
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('@tryAgain').click()
|
||||
cy.contains(loginText.titleInitial).should('be.visible')
|
||||
|
||||
cy.findByRole('dialog', { name: loginText.titleInitial }).within(() => {
|
||||
cy.contains(loginText.actionWaiting).should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
it('cancel button correctly clears error state', () => {
|
||||
cy.withCtx((ctx) => {
|
||||
ctx.coreData.authState = {
|
||||
name: 'AUTH_ERROR_DURING_LOGIN',
|
||||
message: 'An unexpected error occurred',
|
||||
browserOpened: false,
|
||||
}
|
||||
cy.withCtx((ctx, o) => {
|
||||
o.sinon.stub(ctx._apis.authApi, 'logIn').callsFake(async (onMessage) => {
|
||||
onMessage({
|
||||
name: 'AUTH_ERROR_DURING_LOGIN',
|
||||
message: 'An unexpected error occurred',
|
||||
browserOpened: false,
|
||||
})
|
||||
|
||||
throw new Error()
|
||||
})
|
||||
})
|
||||
|
||||
cy.findByTestId('app-header-bar').within(() => {
|
||||
@@ -517,26 +546,36 @@ describe('App Top Nav Workflows', () => {
|
||||
cy.findByRole('button', { name: 'Log In' }).as('loginButton').click()
|
||||
})
|
||||
|
||||
cy.contains(loginText.titleFailed).should('be.visible')
|
||||
cy.contains(loginText.bodyError).should('be.visible')
|
||||
cy.contains('An unexpected error occurred').should('be.visible')
|
||||
cy.findByRole('dialog', { name: 'Log in to Cypress' }).within(() => {
|
||||
cy.findByRole('button', { name: 'Log In' }).click()
|
||||
|
||||
cy.contains(loginText.titleFailed).should('be.visible')
|
||||
cy.contains(loginText.bodyError).should('be.visible')
|
||||
cy.contains('An unexpected error occurred').should('be.visible')
|
||||
})
|
||||
|
||||
cy.percySnapshot()
|
||||
|
||||
cy.contains('button', loginText.actionTryAgain).should('be.visible')
|
||||
cy.contains('button', loginText.actionCancel).click()
|
||||
cy.findByRole('dialog', { name: loginText.titleFailed }).within(() => {
|
||||
cy.contains('button', loginText.actionTryAgain).should('be.visible')
|
||||
cy.contains('button', loginText.actionCancel).click()
|
||||
})
|
||||
|
||||
cy.get('@loginButton').click()
|
||||
cy.contains(loginText.titleInitial).should('be.visible')
|
||||
})
|
||||
|
||||
it('closing modal correctly clears error state', () => {
|
||||
cy.withCtx((ctx) => {
|
||||
ctx.coreData.authState = {
|
||||
name: 'AUTH_ERROR_DURING_LOGIN',
|
||||
message: 'An unexpected error occurred',
|
||||
browserOpened: false,
|
||||
}
|
||||
cy.withCtx((ctx, o) => {
|
||||
o.sinon.stub(ctx._apis.authApi, 'logIn').callsFake(async (onMessage) => {
|
||||
onMessage({
|
||||
name: 'AUTH_ERROR_DURING_LOGIN',
|
||||
message: 'An unexpected error occurred',
|
||||
browserOpened: false,
|
||||
})
|
||||
|
||||
throw new Error()
|
||||
})
|
||||
})
|
||||
|
||||
cy.findByTestId('app-header-bar').within(() => {
|
||||
@@ -544,11 +583,14 @@ describe('App Top Nav Workflows', () => {
|
||||
cy.findByRole('button', { name: 'Log In' }).as('loginButton').click()
|
||||
})
|
||||
|
||||
cy.contains(loginText.titleFailed).should('be.visible')
|
||||
cy.contains(loginText.bodyError).should('be.visible')
|
||||
cy.contains('An unexpected error occurred').should('be.visible')
|
||||
cy.findByRole('dialog', { name: 'Log in to Cypress' }).within(() => {
|
||||
cy.findByRole('button', { name: 'Log In' }).click()
|
||||
cy.contains(loginText.titleFailed).should('be.visible')
|
||||
cy.contains(loginText.bodyError).should('be.visible')
|
||||
cy.contains('An unexpected error occurred').should('be.visible')
|
||||
|
||||
cy.findByLabelText(defaultMessages.actions.close).click()
|
||||
cy.findByLabelText(defaultMessages.actions.close).click()
|
||||
})
|
||||
|
||||
cy.get('@loginButton').click()
|
||||
cy.contains(loginText.titleInitial).should('be.visible')
|
||||
|
||||
@@ -2,6 +2,12 @@ import { createRouter as _createRouter, createWebHashHistory } from 'vue-router'
|
||||
import generatedRoutes from 'virtual:generated-pages'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
|
||||
/**
|
||||
* Generated Routes are created via https://github.com/hannoeru/vite-plugin-pages
|
||||
* The generates are based on the files contained in src/pages.
|
||||
* See README.md this package for more details
|
||||
*/
|
||||
|
||||
export const createRouter = () => {
|
||||
const routes = setupLayouts(generatedRoutes)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div
|
||||
v-show="showPanel1"
|
||||
data-cy="specs-list-panel"
|
||||
class="h-full flex-shrink-0 relative"
|
||||
class="h-full flex-shrink-0 z-10 relative"
|
||||
:style="{width: `${panel1Width}px`}"
|
||||
>
|
||||
<slot
|
||||
@@ -26,7 +26,7 @@
|
||||
<div
|
||||
v-show="showPanel2"
|
||||
data-cy="reporter-panel"
|
||||
class="h-full flex-shrink-0 relative"
|
||||
class="h-full flex-shrink-0 z-10 relative"
|
||||
:style="{width: `${panel2Width}px`}"
|
||||
>
|
||||
<slot name="panel2" />
|
||||
|
||||
@@ -36,6 +36,7 @@ describe('<CreateSpecModal />', () => {
|
||||
},
|
||||
},
|
||||
specs: [],
|
||||
fileExtensionToUse: 'js',
|
||||
},
|
||||
}}
|
||||
show={show.value}
|
||||
@@ -104,6 +105,7 @@ describe('playground', () => {
|
||||
},
|
||||
},
|
||||
specs: [],
|
||||
fileExtensionToUse: 'js',
|
||||
},
|
||||
}}
|
||||
show={show.value}
|
||||
|
||||
@@ -69,6 +69,7 @@ fragment CreateSpecModal on Query {
|
||||
...CreateSpecCards
|
||||
currentProject {
|
||||
id
|
||||
fileExtensionToUse
|
||||
...EmptyGenerator
|
||||
...ComponentGeneratorStepOne_codeGenGlob
|
||||
...StoryGeneratorStepOne_codeGenGlob
|
||||
@@ -95,7 +96,11 @@ const helpLink = computed(() => {
|
||||
return ''
|
||||
})
|
||||
|
||||
const specFileName = computed(() => getPathForPlatform('cypress/e2e/filename.cy.js'))
|
||||
const specFileName = computed(() => {
|
||||
const extension = props.gql.currentProject?.fileExtensionToUse ?? 'js'
|
||||
|
||||
return getPathForPlatform(`cypress/e2e/filename.cy.${extension}`)
|
||||
})
|
||||
|
||||
const codeGenGlob = computed(() => {
|
||||
if (!generator.value) {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 101 KiB |
+2
-2
@@ -5,9 +5,11 @@ exports['src/index .getBreakingKeys returns list of breaking config keys 1'] = [
|
||||
"experimentalNetworkStubbing",
|
||||
"experimentalRunEvents",
|
||||
"experimentalShadowDomSupport",
|
||||
"experimentalStudio",
|
||||
"firefoxGcInterval",
|
||||
"nodeVersion",
|
||||
"nodeVersion",
|
||||
"pluginsFile",
|
||||
"testFiles"
|
||||
]
|
||||
|
||||
@@ -40,7 +42,6 @@ exports['src/index .getDefaultValues returns list of public config keys 1'] = {
|
||||
"modifyObstructiveCode": true,
|
||||
"numTestsKeptInMemory": 50,
|
||||
"pageLoadTimeout": 60000,
|
||||
"pluginsFile": "cypress/plugins",
|
||||
"port": null,
|
||||
"projectId": null,
|
||||
"redirectionLimit": 20,
|
||||
@@ -116,7 +117,6 @@ exports['src/index .getPublicConfigKeys returns list of public config keys 1'] =
|
||||
"numTestsKeptInMemory",
|
||||
"platform",
|
||||
"pageLoadTimeout",
|
||||
"pluginsFile",
|
||||
"port",
|
||||
"projectId",
|
||||
"redirectionLimit",
|
||||
@@ -0,0 +1,5 @@
|
||||
if (process.env.CYPRESS_INTERNAL_ENV !== 'production') {
|
||||
require('@packages/ts/register')
|
||||
}
|
||||
|
||||
module.exports = require('./src')
|
||||
@@ -1,144 +0,0 @@
|
||||
const _ = require('lodash')
|
||||
const debug = require('debug')('cypress:config:validator')
|
||||
|
||||
const { options, breakingOptions, breakingRootOptions, testingTypeBreakingOptions } = require('./options')
|
||||
|
||||
const dashesOrUnderscoresRe = /^(_-)+/
|
||||
|
||||
// takes an array and creates an index object of [keyKey]: [valueKey]
|
||||
const createIndex = (arr, keyKey, valueKey) => {
|
||||
return _.reduce(arr, (memo, item) => {
|
||||
if (item[valueKey] !== undefined) {
|
||||
memo[item[keyKey]] = item[valueKey]
|
||||
}
|
||||
|
||||
return memo
|
||||
}, {})
|
||||
}
|
||||
|
||||
const breakingKeys = _.map(breakingOptions, 'name')
|
||||
const defaultValues = createIndex(options, 'name', 'defaultValue')
|
||||
const publicConfigKeys = _(options).reject({ isInternal: true }).map('name').value()
|
||||
const validationRules = createIndex(options, 'name', 'validation')
|
||||
const testConfigOverrideOptions = createIndex(options, 'name', 'canUpdateDuringTestTime')
|
||||
|
||||
const issuedWarnings = new Set()
|
||||
|
||||
const validateNoBreakingOptions = (breakingCfgOptions, cfg, onWarning, onErr) => {
|
||||
breakingCfgOptions.forEach(({ name, errorKey, newName, isWarning, value }) => {
|
||||
if (_.has(cfg, name)) {
|
||||
if (value && cfg[name] !== value) {
|
||||
// Bail if a value is specified but the config does not have that value.
|
||||
return
|
||||
}
|
||||
|
||||
if (isWarning) {
|
||||
if (issuedWarnings.has(errorKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
// avoid re-issuing the same warning more than once
|
||||
issuedWarnings.add(errorKey)
|
||||
|
||||
return onWarning(errorKey, {
|
||||
name,
|
||||
newName,
|
||||
value,
|
||||
configFile: cfg.configFile,
|
||||
})
|
||||
}
|
||||
|
||||
return onErr(errorKey, {
|
||||
name,
|
||||
newName,
|
||||
value,
|
||||
configFile: cfg.configFile,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
allowed: (obj = {}) => {
|
||||
const propertyNames = publicConfigKeys.concat(breakingKeys)
|
||||
|
||||
return _.pick(obj, propertyNames)
|
||||
},
|
||||
|
||||
getBreakingKeys: () => {
|
||||
return breakingKeys
|
||||
},
|
||||
|
||||
getBreakingRootKeys: () => {
|
||||
return breakingRootOptions
|
||||
},
|
||||
|
||||
getDefaultValues: (runtimeOptions = {}) => {
|
||||
// Default values can be functions, in which case they are evaluated
|
||||
// at runtime - for example, slowTestThreshold where the default value
|
||||
// varies between e2e and component testing.
|
||||
return _.mapValues(defaultValues, (value) => (typeof value === 'function' ? value(runtimeOptions) : value))
|
||||
},
|
||||
|
||||
getPublicConfigKeys: () => {
|
||||
return publicConfigKeys
|
||||
},
|
||||
|
||||
matchesConfigKey: (key) => {
|
||||
if (_.has(defaultValues, key)) {
|
||||
return key
|
||||
}
|
||||
|
||||
key = key.toLowerCase().replace(dashesOrUnderscoresRe, '')
|
||||
key = _.camelCase(key)
|
||||
|
||||
if (_.has(defaultValues, key)) {
|
||||
return key
|
||||
}
|
||||
},
|
||||
|
||||
options,
|
||||
|
||||
validate: (cfg, onErr) => {
|
||||
debug('validating configuration', cfg)
|
||||
|
||||
return _.each(cfg, (value, key) => {
|
||||
const validationFn = validationRules[key]
|
||||
|
||||
// key has a validation rule & value different from the default
|
||||
if (validationFn && value !== defaultValues[key]) {
|
||||
const result = validationFn(key, value)
|
||||
|
||||
if (result !== true) {
|
||||
return onErr(result)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
validateNoBreakingConfigRoot: (cfg, onWarning, onErr) => {
|
||||
return validateNoBreakingOptions(breakingRootOptions, cfg, onWarning, onErr)
|
||||
},
|
||||
|
||||
validateNoBreakingConfig: (cfg, onWarning, onErr) => {
|
||||
return validateNoBreakingOptions(breakingOptions, cfg, onWarning, onErr)
|
||||
},
|
||||
|
||||
validateNoBreakingTestingTypeConfig: (cfg, testingType, onWarning, onErr) => {
|
||||
const options = testingTypeBreakingOptions[testingType]
|
||||
|
||||
return validateNoBreakingOptions(options, cfg, onWarning, onErr)
|
||||
},
|
||||
|
||||
validateNoReadOnlyConfig: (config, onErr) => {
|
||||
let errProperty
|
||||
|
||||
Object.keys(config).some((option) => {
|
||||
return errProperty = testConfigOverrideOptions[option] === false ? option : undefined
|
||||
})
|
||||
|
||||
if (errProperty) {
|
||||
return onErr(errProperty)
|
||||
}
|
||||
},
|
||||
}
|
||||
Vendored
-36
@@ -1,36 +0,0 @@
|
||||
// TODO: Remove this file when we land type-safe @packages/config
|
||||
type ErrResult = {
|
||||
key: string
|
||||
value: any
|
||||
type: string
|
||||
}
|
||||
|
||||
export default {
|
||||
isValidClientCertificatesSet(_key: string, certsForUrls: any[]): ErrResult | true {},
|
||||
|
||||
isValidBrowser(browser: any): ErrResult | true {},
|
||||
|
||||
isValidBrowserList(key: string, browsers: any[]): ErrResult | true {},
|
||||
|
||||
isValidRetriesConfig(key: string, value: any): ErrResult | true {},
|
||||
|
||||
isPlainObject(key: string, value: any): ErrResult | true {},
|
||||
|
||||
isNumber(key: string, value: any): ErrResult | true {},
|
||||
|
||||
isNumberOrFalse(key: string, value: any): ErrResult | true {},
|
||||
|
||||
isFullyQualifiedUrl(key: string, value: string): ErrResult | true {},
|
||||
|
||||
isBoolean(key: string, value: any): ErrResult | true {},
|
||||
|
||||
isString(key: string, value: any): ErrResult | true {},
|
||||
|
||||
isArray(key: string, value: any): ErrResult | true {},
|
||||
|
||||
isStringOrFalse(key: string, value: any): ErrResult | true {},
|
||||
|
||||
isStringOrArrayOfStrings(key: string, value: any): ErrResult | true {},
|
||||
|
||||
isOneOf(...any: any[]): (key: any, value: any) => boolean {},
|
||||
}
|
||||
@@ -3,13 +3,16 @@
|
||||
"version": "0.0.0-development",
|
||||
"description": "Config contains the configuration types and validation function used in the cypress electron application.",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"main": "index.js",
|
||||
"browser": "src/index.ts",
|
||||
"scripts": {
|
||||
"build-prod": "tsc --project .",
|
||||
"clean": "rm lib/options.js || echo 'ok'",
|
||||
"build-prod": "tsc || echo 'built, with errors'",
|
||||
"check-ts": "tsc --noEmit",
|
||||
"clean-deps": "rm -rf node_modules",
|
||||
"clean": "rm -f ./src/*.js ./src/**/*.js ./src/**/**/*.js ./test/**/*.js || echo 'cleaned'",
|
||||
"test": "yarn test-unit",
|
||||
"test-debug": "yarn test-unit --inspect-brk=5566",
|
||||
"test-unit": "mocha --configFile=../../mocha-reporter-config.json -r @packages/ts/register -extension=.js,.ts test/unit/*spec.* --exit"
|
||||
"test-unit": "mocha --configFile=../../mocha-reporter-config.json -r @packages/ts/register test/unit/**/*.spec.ts --exit"
|
||||
},
|
||||
"dependencies": {
|
||||
"check-more-types": "2.24.0",
|
||||
@@ -20,13 +23,12 @@
|
||||
"devDependencies": {
|
||||
"@packages/root": "0.0.0-development",
|
||||
"@packages/ts": "0.0.0-development",
|
||||
"chai": "1.10.0",
|
||||
"mocha": "7.0.1",
|
||||
"sinon": "7.3.1",
|
||||
"sinon-chai": "3.3.0",
|
||||
"snap-shot-it": "7.9.3"
|
||||
"@packages/types": "0.0.0-development",
|
||||
"chai": "4.2.0",
|
||||
"mocha": "7.0.1"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
]
|
||||
"src"
|
||||
],
|
||||
"types": "src/index.ts"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import _ from 'lodash'
|
||||
import Debug from 'debug'
|
||||
import { options, breakingOptions, breakingRootOptions, testingTypeBreakingOptions } from './options'
|
||||
import type { BreakingOption, BreakingOptionErrorKey } from './options'
|
||||
|
||||
// this export has to be done in 2 lines because of a bug in babel typescript
|
||||
import * as validation from './validation'
|
||||
|
||||
export {
|
||||
validation,
|
||||
options,
|
||||
breakingOptions,
|
||||
BreakingOption,
|
||||
BreakingOptionErrorKey,
|
||||
}
|
||||
|
||||
const debug = Debug('cypress:config:validator')
|
||||
|
||||
const dashesOrUnderscoresRe = /^(_-)+/
|
||||
|
||||
// takes an array and creates an index object of [keyKey]: [valueKey]
|
||||
function createIndex<T extends Record<string, any>> (arr: Array<T>, keyKey: keyof T, valueKey: keyof T) {
|
||||
return _.reduce(arr, (memo: Record<string, any>, item) => {
|
||||
if (item[valueKey] !== undefined) {
|
||||
memo[item[keyKey] as string] = item[valueKey]
|
||||
}
|
||||
|
||||
return memo
|
||||
}, {})
|
||||
}
|
||||
|
||||
const breakingKeys = _.map(breakingOptions, 'name')
|
||||
const defaultValues = createIndex(options, 'name', 'defaultValue')
|
||||
const publicConfigKeys = _(options).reject({ isInternal: true }).map('name').value()
|
||||
const validationRules = createIndex(options, 'name', 'validation')
|
||||
const testConfigOverrideOptions = createIndex(options, 'name', 'canUpdateDuringTestTime')
|
||||
|
||||
const issuedWarnings = new Set()
|
||||
|
||||
export type BreakingErrResult = {
|
||||
name: string
|
||||
newName?: string
|
||||
value?: any
|
||||
configFile: string
|
||||
}
|
||||
|
||||
type ErrorHandler = (
|
||||
key: BreakingOptionErrorKey,
|
||||
options: BreakingErrResult
|
||||
) => void
|
||||
|
||||
const validateNoBreakingOptions = (breakingCfgOptions: BreakingOption[], cfg: any, onWarning: ErrorHandler, onErr: ErrorHandler) => {
|
||||
breakingCfgOptions.forEach(({ name, errorKey, newName, isWarning, value }) => {
|
||||
if (_.has(cfg, name)) {
|
||||
if (value && cfg[name] !== value) {
|
||||
// Bail if a value is specified but the config does not have that value.
|
||||
return
|
||||
}
|
||||
|
||||
if (isWarning) {
|
||||
if (issuedWarnings.has(errorKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
// avoid re-issuing the same warning more than once
|
||||
issuedWarnings.add(errorKey)
|
||||
|
||||
return onWarning(errorKey, {
|
||||
name,
|
||||
newName,
|
||||
value,
|
||||
configFile: cfg.configFile,
|
||||
})
|
||||
}
|
||||
|
||||
return onErr(errorKey, {
|
||||
name,
|
||||
newName,
|
||||
value,
|
||||
configFile: cfg.configFile,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const allowed = (obj = {}) => {
|
||||
const propertyNames = publicConfigKeys.concat(breakingKeys)
|
||||
|
||||
return _.pick(obj, propertyNames)
|
||||
}
|
||||
|
||||
export const getBreakingKeys = () => {
|
||||
return breakingKeys
|
||||
}
|
||||
|
||||
export const getBreakingRootKeys = () => {
|
||||
return breakingRootOptions
|
||||
}
|
||||
|
||||
export const getDefaultValues = (runtimeOptions = {}) => {
|
||||
// Default values can be functions, in which case they are evaluated
|
||||
// at runtime - for example, slowTestThreshold where the default value
|
||||
// varies between e2e and component testing.
|
||||
return _.mapValues(defaultValues, (value) => (typeof value === 'function' ? value(runtimeOptions) : value))
|
||||
}
|
||||
|
||||
export const getPublicConfigKeys = () => {
|
||||
return publicConfigKeys
|
||||
}
|
||||
|
||||
export const matchesConfigKey = (key: string) => {
|
||||
if (_.has(defaultValues, key)) {
|
||||
return key
|
||||
}
|
||||
|
||||
key = key.toLowerCase().replace(dashesOrUnderscoresRe, '')
|
||||
key = _.camelCase(key)
|
||||
|
||||
if (_.has(defaultValues, key)) {
|
||||
return key
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
export const validate = (cfg: any, onErr: (property: string) => void) => {
|
||||
debug('validating configuration', cfg)
|
||||
|
||||
return _.each(cfg, (value, key) => {
|
||||
const validationFn = validationRules[key]
|
||||
|
||||
// key has a validation rule & value different from the default
|
||||
if (validationFn && value !== defaultValues[key]) {
|
||||
const result = validationFn(key, value)
|
||||
|
||||
if (result !== true) {
|
||||
return onErr(result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const validateNoBreakingConfigRoot = (cfg: any, onWarning: ErrorHandler, onErr: ErrorHandler) => {
|
||||
return validateNoBreakingOptions(breakingRootOptions, cfg, onWarning, onErr)
|
||||
}
|
||||
|
||||
export const validateNoBreakingConfig = (cfg: any, onWarning: ErrorHandler, onErr: ErrorHandler) => {
|
||||
return validateNoBreakingOptions(breakingOptions, cfg, onWarning, onErr)
|
||||
}
|
||||
|
||||
export const validateNoBreakingConfigLaunchpad = (cfg: any, onWarning: ErrorHandler, onErr: ErrorHandler) => {
|
||||
return validateNoBreakingOptions(breakingOptions.filter((option) => option.showInLaunchpad), cfg, onWarning, onErr)
|
||||
}
|
||||
|
||||
export const validateNoBreakingTestingTypeConfig = (cfg: any, testingType: keyof typeof testingTypeBreakingOptions, onWarning: ErrorHandler, onErr: ErrorHandler) => {
|
||||
const options = testingTypeBreakingOptions[testingType]
|
||||
|
||||
return validateNoBreakingOptions(options, cfg, onWarning, onErr)
|
||||
}
|
||||
|
||||
export const validateNoReadOnlyConfig = (config: any, onErr: (property: string) => void) => {
|
||||
let errProperty
|
||||
|
||||
Object.keys(config).some((option) => {
|
||||
return errProperty = testConfigOverrideOptions[option] === false ? option : undefined
|
||||
})
|
||||
|
||||
if (errProperty) {
|
||||
return onErr(errProperty)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,25 @@
|
||||
import os from 'os'
|
||||
import validate from './validation'
|
||||
import * as validate from './validation'
|
||||
// @ts-ignore
|
||||
import pkg from '@packages/root'
|
||||
|
||||
export type BreakingOptionErrorKey =
|
||||
| 'CONFIG_FILE_INVALID_ROOT_CONFIG'
|
||||
| 'CONFIG_FILE_INVALID_ROOT_CONFIG_E2E'
|
||||
| 'CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_COMPONENT'
|
||||
| 'EXPERIMENTAL_COMPONENT_TESTING_REMOVED'
|
||||
| 'EXPERIMENTAL_SAMESITE_REMOVED'
|
||||
| 'EXPERIMENTAL_NETWORK_STUBBING_REMOVED'
|
||||
| 'EXPERIMENTAL_RUN_EVENTS_REMOVED'
|
||||
| 'EXPERIMENTAL_SHADOW_DOM_REMOVED'
|
||||
| 'EXPERIMENTAL_STUDIO_REMOVED'
|
||||
| 'FIREFOX_GC_INTERVAL_REMOVED'
|
||||
| 'NODE_VERSION_DEPRECATION_SYSTEM'
|
||||
| 'NODE_VERSION_DEPRECATION_BUNDLED'
|
||||
| 'PLUGINS_FILE_CONFIG_OPTION_REMOVED'
|
||||
| 'RENAMED_CONFIG_OPTION'
|
||||
| 'TEST_FILES_DEPRECATION'
|
||||
|
||||
type TestingType = 'e2e' | 'component'
|
||||
|
||||
interface ResolvedConfigOption {
|
||||
@@ -29,7 +46,7 @@ interface RuntimeConfigOption {
|
||||
canUpdateDuringTestTime?: boolean
|
||||
}
|
||||
|
||||
interface BreakingOption {
|
||||
export interface BreakingOption {
|
||||
/**
|
||||
* The non-passive configuration option.
|
||||
*/
|
||||
@@ -37,7 +54,7 @@ interface BreakingOption {
|
||||
/**
|
||||
* String to summarize the error messaging that is logged.
|
||||
*/
|
||||
errorKey: string
|
||||
errorKey: BreakingOptionErrorKey
|
||||
/**
|
||||
* Array of testing types this config option is valid for
|
||||
*/
|
||||
@@ -54,6 +71,10 @@ interface BreakingOption {
|
||||
* Whether to log the error message as a warning instead of throwing an error.
|
||||
*/
|
||||
isWarning?: boolean
|
||||
/**
|
||||
* Whether to show the error message in the launchpad
|
||||
*/
|
||||
showInLaunchpad?: boolean
|
||||
}
|
||||
|
||||
const isValidConfig = (key: string, config: any) => {
|
||||
@@ -233,12 +254,6 @@ const resolvedOptions: Array<ResolvedConfigOption> = [
|
||||
defaultValue: 60000,
|
||||
validation: validate.isNumber,
|
||||
canUpdateDuringTestTime: true,
|
||||
}, {
|
||||
name: 'pluginsFile',
|
||||
defaultValue: 'cypress/plugins',
|
||||
validation: validate.isStringOrFalse,
|
||||
isFolder: true,
|
||||
canUpdateDuringTestTime: false,
|
||||
}, {
|
||||
name: 'port',
|
||||
defaultValue: null,
|
||||
@@ -493,6 +508,9 @@ export const options: Array<ResolvedConfigOption | RuntimeConfigOption> = [
|
||||
...runtimeOptions,
|
||||
]
|
||||
|
||||
/**
|
||||
* Values not allowed in 10.X+ in the root, e2e and component config
|
||||
*/
|
||||
export const breakingOptions: Array<BreakingOption> = [
|
||||
{
|
||||
name: 'blacklistHosts',
|
||||
@@ -518,6 +536,11 @@ export const breakingOptions: Array<BreakingOption> = [
|
||||
name: 'experimentalShadowDomSupport',
|
||||
errorKey: 'EXPERIMENTAL_SHADOW_DOM_REMOVED',
|
||||
isWarning: true,
|
||||
}, {
|
||||
name: 'experimentalStudio',
|
||||
errorKey: 'EXPERIMENTAL_STUDIO_REMOVED',
|
||||
isWarning: true,
|
||||
showInLaunchpad: true,
|
||||
}, {
|
||||
name: 'firefoxGcInterval',
|
||||
errorKey: 'FIREFOX_GC_INTERVAL_REMOVED',
|
||||
@@ -532,8 +555,10 @@ export const breakingOptions: Array<BreakingOption> = [
|
||||
value: 'bundled',
|
||||
errorKey: 'NODE_VERSION_DEPRECATION_BUNDLED',
|
||||
isWarning: true,
|
||||
},
|
||||
{
|
||||
}, {
|
||||
name: 'pluginsFile',
|
||||
errorKey: 'PLUGINS_FILE_CONFIG_OPTION_REMOVED',
|
||||
}, {
|
||||
name: 'testFiles',
|
||||
errorKey: 'TEST_FILES_DEPRECATION',
|
||||
isWarning: false,
|
||||
@@ -559,6 +584,12 @@ export const breakingRootOptions: Array<BreakingOption> = [
|
||||
isWarning: false,
|
||||
testingTypes: ['component', 'e2e'],
|
||||
},
|
||||
{
|
||||
name: 'experimentalStudio',
|
||||
errorKey: 'EXPERIMENTAL_STUDIO_REMOVED',
|
||||
isWarning: true,
|
||||
testingTypes: ['component', 'e2e'],
|
||||
},
|
||||
{
|
||||
name: 'baseUrl',
|
||||
errorKey: 'CONFIG_FILE_INVALID_ROOT_CONFIG_E2E',
|
||||
@@ -1,17 +1,26 @@
|
||||
const _ = require('lodash')
|
||||
const debug = require('debug')('cypress:server:validation')
|
||||
const is = require('check-more-types')
|
||||
const { commaListsOr } = require('common-tags')
|
||||
const path = require('path')
|
||||
import { URL } from 'url'
|
||||
import path from 'path'
|
||||
import * as _ from 'lodash'
|
||||
import * as is from 'check-more-types'
|
||||
import { commaListsOr } from 'common-tags'
|
||||
import Debug from 'debug'
|
||||
|
||||
const debug = Debug('cypress:server:validation')
|
||||
|
||||
// validation functions take a key and a value and should:
|
||||
// - return true if it passes validation
|
||||
// - return a error message if it fails validation
|
||||
|
||||
const str = JSON.stringify
|
||||
const { isArray, isString, isFinite: isNumber } = _
|
||||
|
||||
const errMsg = (key, value, type) => {
|
||||
type ErrResult = {
|
||||
key: string
|
||||
value: any
|
||||
type: string
|
||||
list?: string
|
||||
}
|
||||
|
||||
const errMsg = (key: string, value: any, type: string): ErrResult => {
|
||||
return {
|
||||
key,
|
||||
value,
|
||||
@@ -19,15 +28,15 @@ const errMsg = (key, value, type) => {
|
||||
}
|
||||
}
|
||||
|
||||
const isFullyQualifiedUrl = (value) => {
|
||||
return isString(value) && /^https?\:\/\//.test(value)
|
||||
const _isFullyQualifiedUrl = (value: any): ErrResult | boolean => {
|
||||
return _.isString(value) && /^https?\:\/\//.test(value)
|
||||
}
|
||||
|
||||
const isArrayOfStrings = (value) => {
|
||||
return isArray(value) && _.every(value, isString)
|
||||
const isArrayOfStrings = (value: any): ErrResult | boolean => {
|
||||
return _.isArray(value) && _.every(value, _.isString)
|
||||
}
|
||||
|
||||
const isFalse = (value) => {
|
||||
const isFalse = (value: any): boolean => {
|
||||
return value === false
|
||||
}
|
||||
|
||||
@@ -35,7 +44,7 @@ const isFalse = (value) => {
|
||||
* Validates a single browser object.
|
||||
* @returns {string|true} Returns `true` if the object is matching browser object schema. Returns an error message if it does not.
|
||||
*/
|
||||
const isValidBrowser = (browser) => {
|
||||
export const isValidBrowser = (browser: any): ErrResult | true => {
|
||||
if (!is.unemptyString(browser.name)) {
|
||||
return errMsg('name', browser, 'a non-empty string')
|
||||
}
|
||||
@@ -55,11 +64,11 @@ const isValidBrowser = (browser) => {
|
||||
return errMsg('version', browser, 'a non-empty string')
|
||||
}
|
||||
|
||||
if (!is.string(browser.path)) {
|
||||
if (typeof browser.path !== 'string') {
|
||||
return errMsg('path', browser, 'a string')
|
||||
}
|
||||
|
||||
if (!is.string(browser.majorVersion) && !is.positive(browser.majorVersion)) {
|
||||
if (typeof browser.majorVersion !== 'string' && !(is.number(browser.majorVersion) && browser.majorVersion > 0)) {
|
||||
return errMsg('majorVersion', browser, 'a string or a positive number')
|
||||
}
|
||||
|
||||
@@ -69,7 +78,7 @@ const isValidBrowser = (browser) => {
|
||||
/**
|
||||
* Validates the list of browsers.
|
||||
*/
|
||||
const isValidBrowserList = (key, browsers) => {
|
||||
export const isValidBrowserList = (key: string, browsers: any): ErrResult | true | string => {
|
||||
debug('browsers %o', browsers)
|
||||
if (!browsers) {
|
||||
return 'Missing browsers list'
|
||||
@@ -86,7 +95,7 @@ const isValidBrowserList = (key, browsers) => {
|
||||
}
|
||||
|
||||
for (let k = 0; k < browsers.length; k += 1) {
|
||||
const validationResult = isValidBrowser(browsers[k])
|
||||
const validationResult: boolean | {key: string, value: string, type: string, list?: string} = isValidBrowser(browsers[k])
|
||||
|
||||
if (validationResult !== true) {
|
||||
validationResult.list = 'browsers'
|
||||
@@ -98,10 +107,10 @@ const isValidBrowserList = (key, browsers) => {
|
||||
return true
|
||||
}
|
||||
|
||||
const isValidRetriesConfig = (key, value) => {
|
||||
export const isValidRetriesConfig = (key: string, value: any): ErrResult | true => {
|
||||
const optionalKeys = ['runMode', 'openMode']
|
||||
const isValidRetryValue = (val) => _.isNull(val) || (Number.isInteger(val) && val >= 0)
|
||||
const optionalKeysAreValid = (val, k) => optionalKeys.includes(k) && isValidRetryValue(val)
|
||||
const isValidRetryValue = (val: any) => _.isNull(val) || (Number.isInteger(val) && val >= 0)
|
||||
const optionalKeysAreValid = (val: any, k: string) => optionalKeys.includes(k) && isValidRetryValue(val)
|
||||
|
||||
if (isValidRetryValue(value)) {
|
||||
return true
|
||||
@@ -114,15 +123,16 @@ const isValidRetriesConfig = (key, value) => {
|
||||
return errMsg(key, value, 'a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls')
|
||||
}
|
||||
|
||||
const isPlainObject = (key, value) => {
|
||||
if (value == null || _.isPlainObject(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(key, value, 'a plain object')
|
||||
}
|
||||
|
||||
const isOneOf = (...values) => {
|
||||
/**
|
||||
* Checks if given value for a key is equal to one of the provided values.
|
||||
* @example
|
||||
```
|
||||
validate = v.isOneOf("foo", "bar")
|
||||
validate("example", "foo") // true
|
||||
validate("example", "else") // error message string
|
||||
```
|
||||
*/
|
||||
export const isOneOf = (...values: any[]): ((key: string, value: any) => ErrResult | true) => {
|
||||
return (key, value) => {
|
||||
if (values.some((v) => {
|
||||
if (typeof value === 'function') {
|
||||
@@ -134,7 +144,7 @@ const isOneOf = (...values) => {
|
||||
return true
|
||||
}
|
||||
|
||||
const strings = values.map(str).join(', ')
|
||||
const strings = values.map((a) => str(a)).join(', ')
|
||||
|
||||
return errMsg(key, value, `one of these values: ${strings}`)
|
||||
}
|
||||
@@ -144,19 +154,32 @@ const isOneOf = (...values) => {
|
||||
* Validates whether the supplied set of cert information is valid
|
||||
* @returns {string|true} Returns `true` if the information set is valid. Returns an error message if it is not.
|
||||
*/
|
||||
const isValidClientCertificatesSet = (_key, certsForUrls) => {
|
||||
// _key: string, certsForUrls: any[]): ErrResult | true {}
|
||||
export const isValidClientCertificatesSet = (_key: string, certsForUrls: Array<{
|
||||
name?: string
|
||||
url?: string
|
||||
ca?: string[]
|
||||
certs?: Array<{
|
||||
key: string
|
||||
cert: string
|
||||
pfx: string
|
||||
}>}>): ErrResult | true | string => {
|
||||
debug('clientCerts: %o', certsForUrls)
|
||||
|
||||
if (!Array.isArray(certsForUrls)) {
|
||||
return errMsg(`clientCertificates.certs`, certsForUrls, 'an array of certs for URLs')
|
||||
}
|
||||
|
||||
let urls = []
|
||||
let urls: string[] = []
|
||||
|
||||
for (let i = 0; i < certsForUrls.length; i++) {
|
||||
debug(`Processing clientCertificates: ${i}`)
|
||||
let certsForUrl = certsForUrls[i]
|
||||
|
||||
if (!certsForUrl) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!certsForUrl.url) {
|
||||
return errMsg(`clientCertificates[${i}].url`, certsForUrl.url, 'a URL matcher')
|
||||
}
|
||||
@@ -188,7 +211,7 @@ const isValidClientCertificatesSet = (_key, certsForUrls) => {
|
||||
}
|
||||
|
||||
for (let j = 0; j < certsForUrl.certs.length; j++) {
|
||||
let certInfo = certsForUrl.certs[j]
|
||||
let certInfo = certsForUrl.certs[j]!
|
||||
|
||||
// Only one of PEM or PFX cert allowed
|
||||
if (certInfo.cert && certInfo.pfx) {
|
||||
@@ -220,9 +243,11 @@ const isValidClientCertificatesSet = (_key, certsForUrls) => {
|
||||
}
|
||||
}
|
||||
|
||||
for (let k = 0; k < certsForUrl.ca.length; k++) {
|
||||
if (path.isAbsolute(certsForUrl.ca[k])) {
|
||||
return errMsg(`clientCertificates[${k}].ca[${k}]`, certsForUrl.ca[k], 'a relative filepath')
|
||||
if (certsForUrl.ca) {
|
||||
for (let k = 0; k < certsForUrl.ca.length; k++) {
|
||||
if (path.isAbsolute(certsForUrl.ca[k] || '')) {
|
||||
return errMsg(`clientCertificates[${k}].ca[${k}]`, certsForUrl.ca[k], 'a relative filepath')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,93 +255,78 @@ const isValidClientCertificatesSet = (_key, certsForUrls) => {
|
||||
return true
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isValidClientCertificatesSet,
|
||||
export const isPlainObject = (key: string, value: any) => {
|
||||
if (value == null || _.isPlainObject(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
isValidBrowser,
|
||||
|
||||
isValidBrowserList,
|
||||
|
||||
isValidRetriesConfig,
|
||||
|
||||
isPlainObject,
|
||||
|
||||
isNumber (key, value) {
|
||||
if (value == null || isNumber(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(key, value, 'a number')
|
||||
},
|
||||
|
||||
isNumberOrFalse (key, value) {
|
||||
if (isNumber(value) || isFalse(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(key, value, 'a number or false')
|
||||
},
|
||||
|
||||
isFullyQualifiedUrl (key, value) {
|
||||
if (value == null || isFullyQualifiedUrl(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(
|
||||
key,
|
||||
value,
|
||||
'a fully qualified URL (starting with `http://` or `https://`)',
|
||||
)
|
||||
},
|
||||
|
||||
isBoolean (key, value) {
|
||||
if (value == null || _.isBoolean(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(key, value, 'a boolean')
|
||||
},
|
||||
|
||||
isString (key, value) {
|
||||
if (value == null || isString(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(key, value, 'a string')
|
||||
},
|
||||
|
||||
isArray (key, value) {
|
||||
if (value == null || isArray(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(key, value, 'an array')
|
||||
},
|
||||
|
||||
isStringOrFalse (key, value) {
|
||||
if (isString(value) || isFalse(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(key, value, 'a string or false')
|
||||
},
|
||||
|
||||
isStringOrArrayOfStrings (key, value) {
|
||||
if (isString(value) || isArrayOfStrings(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(key, value, 'a string or an array of strings')
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if given value for a key is equal to one of the provided values.
|
||||
* @example
|
||||
```
|
||||
validate = v.isOneOf("foo", "bar")
|
||||
validate("example", "foo") // true
|
||||
validate("example", "else") // error message string
|
||||
```
|
||||
*/
|
||||
isOneOf,
|
||||
return errMsg(key, value, 'a plain object')
|
||||
}
|
||||
|
||||
export function isBoolean (key: string, value: any): ErrResult | true {
|
||||
if (value == null || _.isBoolean(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(key, value, 'a boolean')
|
||||
}
|
||||
|
||||
export function isNumber (key: string, value: any): ErrResult | true {
|
||||
if (value == null || _.isNumber(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(key, value, 'a number')
|
||||
}
|
||||
|
||||
export function isString (key: string, value: any): ErrResult | true {
|
||||
if (value == null || _.isString(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(key, value, 'a string')
|
||||
}
|
||||
|
||||
export function isArray (key: string, value: any) {
|
||||
if (value == null || _.isArray(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(key, value, 'an array')
|
||||
}
|
||||
|
||||
export function isNumberOrFalse (key: string, value: any): ErrResult | true {
|
||||
if (_.isNumber(value) || isFalse(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(key, value, 'a number or false')
|
||||
}
|
||||
|
||||
export function isStringOrFalse (key: string, value: any): ErrResult | true {
|
||||
if (_.isString(value) || isFalse(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(key, value, 'a string or false')
|
||||
}
|
||||
|
||||
export function isFullyQualifiedUrl (key: string, value: any): ErrResult | true {
|
||||
if (value == null || _isFullyQualifiedUrl(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(
|
||||
key,
|
||||
value,
|
||||
'a fully qualified URL (starting with `http://` or `https://`)',
|
||||
)
|
||||
}
|
||||
|
||||
export function isStringOrArrayOfStrings (key: string, value: any): ErrResult | true {
|
||||
if (_.isString(value) || isArrayOfStrings(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return errMsg(key, value, 'a string or an array of strings')
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
const chai = require('chai')
|
||||
const snapshot = require('snap-shot-it')
|
||||
const sinon = require('sinon')
|
||||
const sinonChai = require('sinon-chai')
|
||||
import chai from 'chai'
|
||||
import snapshot from 'snap-shot-it'
|
||||
import sinon from 'sinon'
|
||||
import sinonChai from 'sinon-chai'
|
||||
|
||||
const configUtil = require('../../lib/index')
|
||||
import * as configUtil from '../../src/index'
|
||||
|
||||
chai.use(sinonChai)
|
||||
const { expect } = chai
|
||||
@@ -145,6 +145,27 @@ describe('src/index', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('.validateNoBreakingConfigLaunchpad', () => {
|
||||
it('calls warning callback if config contains breaking option that should be shown in launchpad', () => {
|
||||
const warningFn = sinon.spy()
|
||||
const errorFn = sinon.spy()
|
||||
|
||||
configUtil.validateNoBreakingConfigLaunchpad({
|
||||
'experimentalStudio': 'should break',
|
||||
configFile: 'config.js',
|
||||
}, warningFn, errorFn)
|
||||
|
||||
expect(warningFn).to.have.been.calledOnceWith('EXPERIMENTAL_STUDIO_REMOVED', {
|
||||
name: 'experimentalStudio',
|
||||
newName: undefined,
|
||||
value: undefined,
|
||||
configFile: 'config.js',
|
||||
})
|
||||
|
||||
expect(errorFn).to.have.callCount(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.validateNoReadOnlyConfig', () => {
|
||||
it('returns an error if validation fails', () => {
|
||||
const errorFn = sinon.spy()
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
const snapshot = require('snap-shot-it')
|
||||
const { expect } = require('chai')
|
||||
import snapshot from 'snap-shot-it'
|
||||
import { expect } from 'chai'
|
||||
|
||||
const validation = require('../../lib/validation')
|
||||
import * as validation from '../../src/validation'
|
||||
|
||||
describe('src/validation', () => {
|
||||
const mockKey = 'mockConfigKey'
|
||||
@@ -84,13 +84,13 @@ describe('src/validation', () => {
|
||||
|
||||
// data-driven testing - computers snapshot value for each item in the list passed through the function
|
||||
// https://github.com/bahmutov/snap-shot-it#data-driven-testing
|
||||
return snapshot.apply(null, [validation.isValidBrowser].concat(browsers))
|
||||
return snapshot.apply(null, [validation.isValidBrowser].concat(browsers as any))
|
||||
})
|
||||
})
|
||||
|
||||
describe('.isValidBrowserList', () => {
|
||||
it('does not allow empty or not browsers', () => {
|
||||
snapshot('undefined browsers', validation.isValidBrowserList('browsers'))
|
||||
snapshot('undefined browsers', validation.isValidBrowserList('browsers', undefined))
|
||||
snapshot('empty list of browsers', validation.isValidBrowserList('browsers', []))
|
||||
|
||||
return snapshot('browsers list with a string', validation.isValidBrowserList('browsers', ['foo']))
|
||||
@@ -1,6 +1,24 @@
|
||||
{
|
||||
"extends": "../ts/tsconfig.json",
|
||||
"include": [
|
||||
"lib/*",
|
||||
"src/*.ts"
|
||||
],
|
||||
}
|
||||
"exclude": [
|
||||
"test",
|
||||
"script"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"allowJs": false,
|
||||
"rootDir": "src",
|
||||
"noImplicitAny": true,
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"importsNotUsedAsValues": "error",
|
||||
"types": ["node"],
|
||||
"typeRoots": [
|
||||
"../../node_modules/@types"
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ export interface AuthApiShape {
|
||||
getUser(): Promise<Partial<AuthenticatedUserShape>>
|
||||
logIn(onMessage: (message: AuthStateShape) => void): Promise<AuthenticatedUserShape>
|
||||
logOut(): Promise<void>
|
||||
resetAuthState(): Promise<void>
|
||||
resetAuthState(): void
|
||||
}
|
||||
|
||||
export class AuthActions {
|
||||
@@ -44,19 +44,62 @@ export class AuthActions {
|
||||
}
|
||||
|
||||
async login () {
|
||||
this.setAuthenticatedUser(await this.authApi.logIn((authState) => {
|
||||
const loginPromise = new Promise<AuthenticatedUserShape | null>((resolve, reject) => {
|
||||
// A resolver is exposed to the instance so that we can
|
||||
// resolve this promise and the original mutation promise
|
||||
// if a reset occurs
|
||||
this.ctx.update((coreData) => {
|
||||
coreData.authState = authState
|
||||
coreData.cancelActiveLogin = () => resolve(null)
|
||||
})
|
||||
}))
|
||||
|
||||
this.ctx.emitter.authChange()
|
||||
this.authApi.logIn((authState) => {
|
||||
this.ctx.update((coreData) => {
|
||||
coreData.authState = authState
|
||||
})
|
||||
|
||||
// Ensure auth state changes during the login lifecycle
|
||||
// are propagated to the clients
|
||||
this.ctx.emitter.authChange()
|
||||
}).then(resolve, reject)
|
||||
})
|
||||
|
||||
const user = await loginPromise
|
||||
|
||||
if (!user) {
|
||||
// if the user is null, this promise is resolving due to a
|
||||
// login mutation cancellation. the state should already
|
||||
// be reset, so abort early.
|
||||
return
|
||||
}
|
||||
|
||||
this.setAuthenticatedUser(user as AuthenticatedUserShape)
|
||||
|
||||
this.ctx.update((coreData) => {
|
||||
coreData.cancelActiveLogin = null
|
||||
})
|
||||
|
||||
this.resetAuthState()
|
||||
}
|
||||
|
||||
resetAuthState () {
|
||||
// closes the express server opened during login, if it's still open
|
||||
this.authApi.resetAuthState()
|
||||
|
||||
// if a login mutation is still in progress, we
|
||||
// forcefully resolve it so that the mutation does not persist
|
||||
if (this.ctx.coreData.cancelActiveLogin) {
|
||||
this.ctx.coreData.cancelActiveLogin()
|
||||
|
||||
this.ctx.update((coreData) => {
|
||||
coreData.cancelActiveLogin = null
|
||||
})
|
||||
}
|
||||
|
||||
this.ctx.update((coreData) => {
|
||||
coreData.authState = { browserOpened: false }
|
||||
})
|
||||
|
||||
this.ctx.emitter.authChange()
|
||||
}
|
||||
|
||||
async logout () {
|
||||
@@ -76,7 +119,9 @@ export class AuthActions {
|
||||
}
|
||||
|
||||
private setAuthenticatedUser (authUser: AuthenticatedUserShape | null) {
|
||||
this.ctx.coreData.user = authUser
|
||||
this.ctx.update((coreData) => {
|
||||
coreData.user = authUser
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ export class MigrationActions {
|
||||
}
|
||||
|
||||
get configFileNameAfterMigration () {
|
||||
return this.ctx.lifecycleManager.legacyConfigFile.replace('.json', `.config.${this.ctx.lifecycleManager.metaState.hasTypescript ? 'ts' : 'js'}`)
|
||||
return this.ctx.lifecycleManager.legacyConfigFile.replace('.json', `.config.${this.ctx.lifecycleManager.fileExtensionToUse}`)
|
||||
}
|
||||
|
||||
async createConfigFile () {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { isBinaryFile } from 'isbinaryfile'
|
||||
import * as path from 'path'
|
||||
import * as ejs from 'ejs'
|
||||
import fm from 'front-matter'
|
||||
import _ from 'lodash'
|
||||
|
||||
export interface Action {
|
||||
templateDir: string
|
||||
@@ -36,7 +37,7 @@ export async function codeGenerator (
|
||||
const templateFiles = await allFilesInDir(action.templateDir)
|
||||
const codeGenResults: CodeGenResults = { files: [], failed: [] }
|
||||
|
||||
for (const file of templateFiles) {
|
||||
const scaffoldResults = await Promise.all(templateFiles.map(async (file) => {
|
||||
const isBinary = await isBinaryFile(file)
|
||||
const parsedFile = path.parse(file)
|
||||
|
||||
@@ -85,18 +86,26 @@ export async function codeGenerator (
|
||||
await fs.outputFile(computedPath, content)
|
||||
}
|
||||
|
||||
codeGenResults.files.push({
|
||||
return {
|
||||
file: computedPath,
|
||||
type,
|
||||
status,
|
||||
content: content.toString(),
|
||||
})
|
||||
} as const
|
||||
} catch (e) {
|
||||
codeGenResults.failed.push(e as Error)
|
||||
return e instanceof Error ? e : new Error(String(e))
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
return codeGenResults
|
||||
return scaffoldResults.reduce((accum, result) => {
|
||||
if (result instanceof Error) {
|
||||
accum.failed.push(result)
|
||||
} else {
|
||||
accum.files.push(result)
|
||||
}
|
||||
|
||||
return accum
|
||||
}, codeGenResults)
|
||||
}
|
||||
|
||||
function computePath (
|
||||
@@ -116,20 +125,16 @@ function computePath (
|
||||
}
|
||||
|
||||
async function allFilesInDir (parent: string): Promise<string[]> {
|
||||
let res: string[] = []
|
||||
const dirs = await fs.readdir(parent)
|
||||
|
||||
for (const dir of await fs.readdir(parent)) {
|
||||
const result = await Promise.all(dirs.map(async (dir) => {
|
||||
const child = path.join(parent, dir)
|
||||
const isDir = (await fs.stat(child)).isDirectory()
|
||||
|
||||
if (!isDir) {
|
||||
res.push(child)
|
||||
} else {
|
||||
res = [...res, ...(await allFilesInDir(child))]
|
||||
}
|
||||
}
|
||||
return isDir ? await allFilesInDir(child) : child
|
||||
}))
|
||||
|
||||
return res
|
||||
return _.flatten(result)
|
||||
}
|
||||
|
||||
function frontMatter (content: string, args: { [key: string]: any }) {
|
||||
|
||||
@@ -20,7 +20,8 @@ import { getError, CypressError, ConfigValidationFailureInfo } from '@packages/e
|
||||
import type { DataContext } from '..'
|
||||
import { LoadConfigReply, SetupNodeEventsReply, ProjectConfigIpc, IpcHandler } from './ProjectConfigIpc'
|
||||
import assert from 'assert'
|
||||
import type { AllModeOptions, BreakingErrResult, BreakingOption, FoundBrowser, FullConfig, TestingType } from '@packages/types'
|
||||
import type { AllModeOptions, FoundBrowser, FullConfig, TestingType } from '@packages/types'
|
||||
import type { BreakingErrResult, BreakingOptionErrorKey } from '@packages/config'
|
||||
import { autoBindDebug } from '../util/autoBindDebug'
|
||||
import type { LegacyCypressConfigJson } from '../sources'
|
||||
|
||||
@@ -46,7 +47,7 @@ export interface SetupFullConfigOptions {
|
||||
options: Partial<AllModeOptions>
|
||||
}
|
||||
|
||||
type BreakingValidationFn<T> = (type: BreakingOption, val: BreakingErrResult) => T
|
||||
type BreakingValidationFn<T> = (type: BreakingOptionErrorKey, val: BreakingErrResult) => T
|
||||
|
||||
/**
|
||||
* All of the APIs injected from @packages/server & @packages/config
|
||||
@@ -60,6 +61,7 @@ export interface InjectedConfigApi {
|
||||
updateWithPluginValues(config: FullConfig, modifiedConfig: Partial<Cypress.ConfigOptions>): FullConfig
|
||||
setupFullConfigWithDefaults(config: SetupFullConfigOptions): Promise<FullConfig>
|
||||
validateRootConfigBreakingChanges<T extends Cypress.ConfigOptions>(config: Partial<T>, onWarning: BreakingValidationFn<CypressError>, onErr: BreakingValidationFn<never>): void
|
||||
validateLaunchpadConfigBreakingChanges<T extends Cypress.ConfigOptions>(config: Partial<T>, onWarning: BreakingValidationFn<CypressError>, onErr: BreakingValidationFn<never>): void
|
||||
validateTestingTypeConfigBreakingChanges<T extends Cypress.ConfigOptions>(config: Partial<T>, testingType: Cypress.TestingType, onWarning: BreakingValidationFn<CypressError>, onErr: BreakingValidationFn<never>): void
|
||||
}
|
||||
|
||||
@@ -179,7 +181,7 @@ export class ProjectLifecycleManager {
|
||||
}
|
||||
|
||||
get configFile () {
|
||||
return this.ctx.modeOptions.configFile ?? 'cypress.config.js'
|
||||
return this.ctx.modeOptions.configFile ?? path.basename(this.configFilePath) ?? 'cypress.config.js'
|
||||
}
|
||||
|
||||
get configFilePath () {
|
||||
@@ -226,6 +228,10 @@ export class ProjectLifecycleManager {
|
||||
return path.basename(this.projectRoot)
|
||||
}
|
||||
|
||||
get fileExtensionToUse () {
|
||||
return this.metaState.hasTypescript ? 'ts' : 'js'
|
||||
}
|
||||
|
||||
async checkIfLegacyConfigFileExist () {
|
||||
const legacyConfigFileExist = await this.ctx.deref.actions.file.checkIfFileExists(this.legacyConfigFile)
|
||||
|
||||
@@ -622,6 +628,24 @@ export class ProjectLifecycleManager {
|
||||
|
||||
throw getError('CONFIG_VALIDATION_ERROR', 'configFile', file || null, errMsg)
|
||||
})
|
||||
|
||||
return this.ctx._apis.configApi.validateLaunchpadConfigBreakingChanges(
|
||||
config,
|
||||
(type, obj) => {
|
||||
const error = getError(type, obj)
|
||||
|
||||
this.ctx.onWarning(error)
|
||||
|
||||
return error
|
||||
},
|
||||
(type, obj) => {
|
||||
const error = getError(type, obj)
|
||||
|
||||
this.ctx.onError(error)
|
||||
|
||||
throw error
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -138,6 +138,7 @@ export interface CoreDataShape {
|
||||
warnings: ErrorWrapperSource[]
|
||||
packageManager: typeof PACKAGE_MANAGERS[number]
|
||||
forceReconfigureProject: ForceReconfigureProjectDataShape | null
|
||||
cancelActiveLogin: (() => void) | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,5 +210,6 @@ export function makeCoreData (modeOptions: Partial<AllModeOptions> = {}): CoreDa
|
||||
scaffoldedFiles: null,
|
||||
packageManager: 'npm',
|
||||
forceReconfigureProject: null,
|
||||
cancelActiveLogin: null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,6 @@ export class MigrationDataSource {
|
||||
}
|
||||
|
||||
get configFileNameAfterMigration () {
|
||||
return this.ctx.lifecycleManager.legacyConfigFile.replace('.json', `.config.${this.ctx.lifecycleManager.metaState.hasTypescript ? 'ts' : 'js'}`)
|
||||
return this.ctx.lifecycleManager.legacyConfigFile.replace('.json', `.config.${this.ctx.lifecycleManager.fileExtensionToUse}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { options } from '@packages/config/lib/options'
|
||||
import { options } from '@packages/config'
|
||||
|
||||
export const getDefaultSpecPatterns = () => {
|
||||
return {
|
||||
|
||||
@@ -585,16 +585,16 @@ describe('src/cy/commands/actions/type - #type events', () => {
|
||||
|
||||
describe('triggers', () => {
|
||||
const targets = [
|
||||
'#target-button-tag',
|
||||
'#target-input-button',
|
||||
'#target-input-image',
|
||||
'#target-input-reset',
|
||||
'#target-input-submit',
|
||||
'button-tag',
|
||||
'input-button',
|
||||
'input-image',
|
||||
'input-reset',
|
||||
'input-submit',
|
||||
]
|
||||
|
||||
targets.forEach((target) => {
|
||||
it(target, () => {
|
||||
cy.get(target).focus().type('{enter}')
|
||||
cy.get(`#target-${target}`).focus().type('{enter}')
|
||||
|
||||
cy.get('li').should('have.length', 4)
|
||||
cy.get('li').eq(0).should('have.text', 'keydown')
|
||||
@@ -603,17 +603,31 @@ describe('src/cy/commands/actions/type - #type events', () => {
|
||||
cy.get('li').eq(3).should('have.text', 'keyup')
|
||||
})
|
||||
})
|
||||
|
||||
describe('keydown triggered on another element', () => {
|
||||
targets.forEach((target) => {
|
||||
it(target, () => {
|
||||
cy.get('#focus-options').select(target)
|
||||
cy.get('#input-text').focus().type('{enter}')
|
||||
|
||||
cy.get('li').should('have.length', 3)
|
||||
cy.get('li').eq(0).should('have.text', 'keypress')
|
||||
cy.get('li').eq(1).should('have.text', 'click')
|
||||
cy.get('li').eq(2).should('have.text', 'keyup')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('does not trigger', () => {
|
||||
const targets = [
|
||||
'#target-input-checkbox',
|
||||
'#target-input-radio',
|
||||
'input-checkbox',
|
||||
'input-radio',
|
||||
]
|
||||
|
||||
targets.forEach((target) => {
|
||||
it(target, () => {
|
||||
cy.get(target).focus().type('{enter}')
|
||||
cy.get(`#target-${target}`).focus().type('{enter}')
|
||||
|
||||
cy.get('li').should('have.length', 3)
|
||||
cy.get('li').eq(0).should('have.text', 'keydown')
|
||||
@@ -621,6 +635,19 @@ describe('src/cy/commands/actions/type - #type events', () => {
|
||||
cy.get('li').eq(2).should('have.text', 'keyup')
|
||||
})
|
||||
})
|
||||
|
||||
describe('keydown triggered on another element', () => {
|
||||
targets.forEach((target) => {
|
||||
it(target, () => {
|
||||
cy.get('#focus-options').select(target)
|
||||
cy.get('#input-text').focus().type('{enter}')
|
||||
|
||||
cy.get('li').should('have.length', 2)
|
||||
cy.get('li').eq(0).should('have.text', 'keypress')
|
||||
cy.get('li').eq(1).should('have.text', 'keyup')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -770,5 +797,28 @@ describe('src/cy/commands/actions/type - #type events', () => {
|
||||
cy.get('#target-input-radio').should('be.checked')
|
||||
})
|
||||
})
|
||||
|
||||
describe('keydown on another element does not trigger click', () => {
|
||||
const targets = [
|
||||
'button-tag',
|
||||
'input-button',
|
||||
'input-image',
|
||||
'input-reset',
|
||||
'input-submit',
|
||||
'input-checkbox',
|
||||
'input-radio',
|
||||
]
|
||||
|
||||
targets.forEach((target) => {
|
||||
it(target, () => {
|
||||
cy.get('#focus-options').select('button-tag')
|
||||
cy.get('#input-text').focus().type(' ')
|
||||
|
||||
cy.get('li').should('have.length', 2)
|
||||
cy.get('li').eq(0).should('have.text', 'keypress')
|
||||
cy.get('li').eq(1).should('have.text', 'keyup')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,14 +6,33 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<button id="reset">clear log</button>
|
||||
<button id="target-button-tag">button tag</button>
|
||||
<input id="target-input-button" type="button" value="input button" />
|
||||
<input id="target-input-image" type="image" value="input image" />
|
||||
<input id="target-input-reset" type="reset" value="input reset" />
|
||||
<input id="target-input-submit" type="submit" value="input submit" />
|
||||
<input id="target-input-checkbox" type="checkbox" value="input checkbox" />
|
||||
<input id="target-input-radio" type="radio" value="input radio" />
|
||||
<div>
|
||||
<button id="reset">clear log</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input id="input-text" type="text" />
|
||||
<select id="focus-options">
|
||||
<option value="clear">clear</option>
|
||||
<option value="button-tag">button tag</option>
|
||||
<option value="input-button">input button</option>
|
||||
<option value="input-image">input image</option>
|
||||
<option value="input-reset">input reset</option>
|
||||
<option value="input-submit">input submit</option>
|
||||
<option value="input-checkbox">input checkbox</option>
|
||||
<option value="input-radio">input radio</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="target-button-tag">button tag</button>
|
||||
<input id="target-input-button" type="button" value="input button" />
|
||||
<input id="target-input-image" type="image" value="input image" />
|
||||
<input id="target-input-reset" type="reset" value="input reset" />
|
||||
<input id="target-input-submit" type="submit" value="input submit" />
|
||||
<input id="target-input-checkbox" type="checkbox" value="input checkbox" />
|
||||
<input id="target-input-radio" type="radio" value="input radio" />
|
||||
</div>
|
||||
|
||||
<div id="log"></div>
|
||||
|
||||
@@ -70,6 +89,32 @@
|
||||
updateLog("keyup");
|
||||
});
|
||||
});
|
||||
|
||||
let handler = null
|
||||
const focusOptions = document.getElementById("focus-options");
|
||||
|
||||
focusOptions.addEventListener('change', (event) => {
|
||||
const val = event.target.value;
|
||||
const target = document.getElementById('input-text');
|
||||
|
||||
if (handler) {
|
||||
target.removeEventListener('keydown', handler);
|
||||
}
|
||||
|
||||
if (val === 'clear') {
|
||||
handler = null
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
handler = (e) => {
|
||||
const focusEl = document.getElementById(`target-${val}`);
|
||||
|
||||
focusEl.focus()
|
||||
}
|
||||
|
||||
target.addEventListener('keydown', handler);
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"cypress:open": "node ../../scripts/cypress open",
|
||||
"cypress:run": "node ../../scripts/cypress run",
|
||||
"postinstall": "patch-package",
|
||||
"start": "node -e 'console.log(require(`chalk`).red(`\nError:\n\tRunning \\`yarn start\\` is no longer needed for driver/cypress tests.\n\tWe now automatically spawn the server in the pluginsFile.\n\tChanges to the server will be watched and reloaded automatically.`))'"
|
||||
"start": "node -e 'console.log(require(`chalk`).red(`\nError:\n\tRunning \\`yarn start\\` is no longer needed for driver/cypress tests.\n\tWe now automatically spawn the server in e2e.setupNodeEvents config.\n\tChanges to the server will be watched and reloaded automatically.`))'"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -275,11 +275,6 @@ export default function (Commands, Cypress, cy, state, config) {
|
||||
const isContentEditable = $elements.isContentEditable(options.$el.get(0))
|
||||
const isTextarea = $elements.isTextarea(options.$el.get(0))
|
||||
|
||||
// click event is only fired on button, image, submit, reset elements.
|
||||
// That's why we cannot use $elements.isButtonLike() here.
|
||||
const type = (type) => $elements.isInputType(options.$el.get(0), type)
|
||||
const sendClickEvent = type('button') || type('image') || type('submit') || type('reset')
|
||||
|
||||
const fireClickEvent = (el) => {
|
||||
const ctor = $dom.getDocumentFromElement(el).defaultView!.PointerEvent
|
||||
const event = new ctor('click')
|
||||
@@ -287,6 +282,8 @@ export default function (Commands, Cypress, cy, state, config) {
|
||||
el.dispatchEvent(event)
|
||||
}
|
||||
|
||||
let keydownEvents: any[] = []
|
||||
|
||||
return keyboard.type({
|
||||
$el: options.$el,
|
||||
chars,
|
||||
@@ -332,21 +329,29 @@ export default function (Commands, Cypress, cy, state, config) {
|
||||
updateTable(id, key, event, value)
|
||||
}
|
||||
|
||||
if (event.type === 'keydown') {
|
||||
keydownEvents.push(event.target)
|
||||
}
|
||||
|
||||
if (
|
||||
// Firefox sends a click event when the Space key is pressed.
|
||||
// We don't want send it twice.
|
||||
// We don't want to send it twice.
|
||||
!Cypress.isBrowser('firefox') &&
|
||||
// Click event is sent after keyup event with space key.
|
||||
event.type === 'keyup' && event.code === 'Space' &&
|
||||
// When event is prevented, the click event should not be emitted
|
||||
!event.defaultPrevented &&
|
||||
// Click events should be only sent to button-like elements.
|
||||
// event.target is null when used with shadow DOM.
|
||||
(event.target && $elements.isButtonLike(event.target)) &&
|
||||
// When a space key is pressed for input radio elements, the click event is only fired when it's not checked.
|
||||
!(event.target.tagName === 'INPUT' && event.target.type === 'radio' && event.target.checked === true) &&
|
||||
// When event is prevented, the click event should not be emitted
|
||||
!event.defaultPrevented
|
||||
// When a space key is pressed on another element, the click event should not be fired.
|
||||
keydownEvents.includes(event.target)
|
||||
) {
|
||||
fireClickEvent(event.target)
|
||||
|
||||
keydownEvents = []
|
||||
}
|
||||
},
|
||||
|
||||
@@ -391,6 +396,11 @@ export default function (Commands, Cypress, cy, state, config) {
|
||||
return
|
||||
}
|
||||
|
||||
// click event is only fired on button, image, submit, reset elements.
|
||||
// That's why we cannot use $elements.isButtonLike() here.
|
||||
const type = (type) => $elements.isInputType(el, type)
|
||||
const sendClickEvent = type('button') || type('image') || type('submit') || type('reset')
|
||||
|
||||
// https://github.com/cypress-io/cypress/issues/19541
|
||||
// Send click event on type('{enter}')
|
||||
if (sendClickEvent) {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
|
||||
body {
|
||||
font-family: "Courier Prime", Courier, monospace;
|
||||
padding: 0 1em;
|
||||
line-height: 1.4;
|
||||
color: #eee;
|
||||
background-color: #111;
|
||||
}
|
||||
pre {
|
||||
padding: 0 0;
|
||||
margin: 0 0;
|
||||
font-family: "Courier Prime", Courier, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 5px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body><pre><span style="color:#e05561">The <span style="color:#e5e510">pluginsFile<span style="color:#e05561"> configuration option you have supplied has been replaced with <span style="color:#de73ff">setupNodeEvents<span style="color:#e05561">.<span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561">This new option is not a one-to-one correlation and it must be configured separately as a testing type property: <span style="color:#de73ff">e2e.setupNodeEvents<span style="color:#e05561"> and <span style="color:#de73ff">component.setupNodeEvents<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff">{<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff"> e2e: {<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff"> setupNodeEvents()<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff"> },<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff"> component: {<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff"> setupNodeEvents()<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff"> },<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff">}<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561">https://on.cypress.io/migration-guide<span style="color:#e6e6e6"></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
|
||||
</pre></body></html>
|
||||
+3
-1
@@ -45,8 +45,10 @@
|
||||
<span style="color:#e05561"><span style="color:#4f5666"> - <span style="color:#e05561"><span style="color:#4ec4ff">foo<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4f5666"> - <span style="color:#e05561"><span style="color:#4ec4ff">bar<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4f5666"> - <span style="color:#e05561"><span style="color:#4ec4ff">baz<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561">Learn more at https://docs.cypress.io/api/plugins/writing-a-plugin#config<span style="color:#e6e6e6">
|
||||
<span style="color:#c062de"><span style="color:#e6e6e6">
|
||||
<span style="color:#c062de">Error: fail whale<span style="color:#e6e6e6">
|
||||
<span style="color:#c062de"> at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)<span style="color:#e6e6e6">
|
||||
<span style="color:#c062de"> at PLUGINS_INVALID_EVENT_NAME_ERROR (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)<span style="color:#e6e6e6"></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
|
||||
<span style="color:#c062de"> at SETUP_NODE_EVENTS_INVALID_EVENT_NAME_ERROR (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)<span style="color:#e6e6e6"></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
|
||||
</pre></body></html>
|
||||
@@ -21,7 +21,9 @@
|
||||
"strip-ansi": "6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@packages/config": "0.0.0-development",
|
||||
"@packages/ts": "0.0.0-development",
|
||||
"@packages/types": "0.0.0-development",
|
||||
"@types/chai": "4.2.15",
|
||||
"@types/mocha": "8.2.2",
|
||||
"@types/node": "14.14.31",
|
||||
|
||||
@@ -4,7 +4,8 @@ import chalk from 'chalk'
|
||||
import _ from 'lodash'
|
||||
import path from 'path'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
import type { BreakingErrResult, TestingType } from '@packages/types'
|
||||
import type { TestingType } from '@packages/types'
|
||||
import type { BreakingErrResult } from '@packages/config'
|
||||
|
||||
import { humanTime, logError, parseResolvedPattern, pluralize } from './errorUtils'
|
||||
import { errPartial, errTemplate, fmt, theme, PartialErr } from './errTemplate'
|
||||
@@ -645,9 +646,9 @@ export const AllCypressErrors = {
|
||||
`
|
||||
},
|
||||
// TODO: make this relative path, not absolute
|
||||
PLUGINS_INVALID_EVENT_NAME_ERROR: (pluginsFilePath: string, invalidEventName: string, validEventNames: string[], err: Error) => {
|
||||
SETUP_NODE_EVENTS_INVALID_EVENT_NAME_ERROR: (configFilePath: string, invalidEventName: string, validEventNames: string[], err: Error) => {
|
||||
return errTemplate`
|
||||
Your ${fmt.highlightSecondary(`configFile`)} threw a validation error from: ${fmt.path(pluginsFilePath)}
|
||||
Your ${fmt.highlightSecondary(`configFile`)} threw a validation error from: ${fmt.path(configFilePath)}
|
||||
|
||||
You must pass a valid event name when registering a plugin.
|
||||
|
||||
@@ -657,6 +658,8 @@ export const AllCypressErrors = {
|
||||
|
||||
${fmt.listItems(validEventNames)}
|
||||
|
||||
Learn more at https://docs.cypress.io/api/plugins/writing-a-plugin#config
|
||||
|
||||
${fmt.stackTrace(err)}
|
||||
`
|
||||
},
|
||||
@@ -681,7 +684,7 @@ export const AllCypressErrors = {
|
||||
},
|
||||
// happens when there is an error in configuration file like "cypress.json"
|
||||
// TODO: make this relative path, not absolute
|
||||
CONFIG_VALIDATION_MSG_ERROR: (fileType: 'configFile' | 'pluginsFile' | null, fileName: string | null, validationMsg: string) => {
|
||||
CONFIG_VALIDATION_MSG_ERROR: (fileType: 'configFile' | null, fileName: string | null, validationMsg: string) => {
|
||||
if (!fileType) {
|
||||
return errTemplate`
|
||||
An invalid configuration value was set:
|
||||
@@ -695,7 +698,7 @@ export const AllCypressErrors = {
|
||||
${fmt.highlight(validationMsg)}`
|
||||
},
|
||||
// TODO: make this relative path, not absolute
|
||||
CONFIG_VALIDATION_ERROR: (fileType: 'configFile' | 'pluginsFile' | null, filePath: string | null, validationResult: ConfigValidationFailureInfo) => {
|
||||
CONFIG_VALIDATION_ERROR: (fileType: 'configFile' | null, filePath: string | null, validationResult: ConfigValidationFailureInfo) => {
|
||||
const { key, type, value, list } = validationResult
|
||||
|
||||
if (!fileType) {
|
||||
@@ -1060,6 +1063,12 @@ export const AllCypressErrors = {
|
||||
|
||||
You can safely remove this option from your config.`
|
||||
},
|
||||
EXPERIMENTAL_STUDIO_REMOVED: () => {
|
||||
return errTemplate`\
|
||||
We're ending the experimental phase of Cypress Studio in ${fmt.cypressVersion(`10.0.0`)} and have learned a lot. Stay tuned for updates on Studio's official release in the future. You can leave feedback here: http://on.cypress.io/studio-beta.
|
||||
|
||||
You can safely remove the ${fmt.highlight(`experimentalStudio`)} configuration option from your config.`
|
||||
},
|
||||
FIREFOX_GC_INTERVAL_REMOVED: () => {
|
||||
return errTemplate`\
|
||||
The ${fmt.highlight(`firefoxGcInterval`)} configuration option was removed in ${fmt.cypressVersion(`8.0.0`)}. It was introduced to work around a bug in Firefox 79 and below.
|
||||
@@ -1210,6 +1219,27 @@ export const AllCypressErrors = {
|
||||
`
|
||||
},
|
||||
|
||||
PLUGINS_FILE_CONFIG_OPTION_REMOVED: (_errShape: BreakingErrResult) => {
|
||||
const code = errPartial`
|
||||
{
|
||||
e2e: {
|
||||
setupNodeEvents()
|
||||
},
|
||||
component: {
|
||||
setupNodeEvents()
|
||||
},
|
||||
}`
|
||||
|
||||
return errTemplate`\
|
||||
The ${fmt.highlight('pluginsFile')} configuration option you have supplied has been replaced with ${fmt.highlightSecondary('setupNodeEvents')}.
|
||||
|
||||
This new option is not a one-to-one correlation and it must be configured separately as a testing type property: ${fmt.highlightSecondary('e2e.setupNodeEvents')} and ${fmt.highlightSecondary('component.setupNodeEvents')}
|
||||
|
||||
${fmt.code(code)}
|
||||
|
||||
https://on.cypress.io/migration-guide`
|
||||
},
|
||||
|
||||
CONFIG_FILE_INVALID_ROOT_CONFIG: (errShape: BreakingErrResult) => {
|
||||
const code = errPartial`
|
||||
{
|
||||
|
||||
@@ -683,7 +683,7 @@ describe('visual error templates', () => {
|
||||
default: ['/path/to/cypress.config.js', err],
|
||||
}
|
||||
},
|
||||
PLUGINS_INVALID_EVENT_NAME_ERROR: () => {
|
||||
SETUP_NODE_EVENTS_INVALID_EVENT_NAME_ERROR: () => {
|
||||
const err = makeErr()
|
||||
|
||||
return {
|
||||
@@ -730,11 +730,6 @@ describe('visual error templates', () => {
|
||||
type: 'a number',
|
||||
value: [1, 2, 3],
|
||||
}],
|
||||
pluginsFile: ['pluginsFile', 'cypress/plugins/index.js', {
|
||||
key: 'defaultCommandTimeout',
|
||||
type: 'a number',
|
||||
value: false,
|
||||
}],
|
||||
noFileType: [null, null, {
|
||||
key: 'defaultCommandTimeout',
|
||||
type: 'a number',
|
||||
@@ -1006,6 +1001,11 @@ describe('visual error templates', () => {
|
||||
default: [],
|
||||
}
|
||||
},
|
||||
EXPERIMENTAL_STUDIO_REMOVED: () => {
|
||||
return {
|
||||
default: [],
|
||||
}
|
||||
},
|
||||
FIREFOX_GC_INTERVAL_REMOVED: () => {
|
||||
return {
|
||||
default: [],
|
||||
@@ -1067,6 +1067,11 @@ describe('visual error templates', () => {
|
||||
default: ['spec.{ts,js}', ['support.ts', 'support.js']],
|
||||
}
|
||||
},
|
||||
PLUGINS_FILE_CONFIG_OPTION_REMOVED: () => {
|
||||
return {
|
||||
default: [{ name: 'pluginsFile', configFile: '/path/to/cypress.config.js.ts' }],
|
||||
}
|
||||
},
|
||||
CONFIG_FILE_INVALID_ROOT_CONFIG: () => {
|
||||
return {
|
||||
default: [{ name: 'specPattern', configFile: '/path/to/cypress.config.js.ts' }],
|
||||
|
||||
@@ -26,6 +26,7 @@ export const e2eProjectDirs = [
|
||||
'downloads',
|
||||
'e2e',
|
||||
'empty-folders',
|
||||
'experimental-studio',
|
||||
'failures',
|
||||
'firefox-memory',
|
||||
'fixture-subfolder-of-integration',
|
||||
|
||||
@@ -158,11 +158,6 @@
|
||||
"from": "default",
|
||||
"field": "pageLoadTimeout"
|
||||
},
|
||||
{
|
||||
"value": "cypress/e2e/plugins/index.ts",
|
||||
"from": "config",
|
||||
"field": "pluginsFile"
|
||||
},
|
||||
{
|
||||
"value": null,
|
||||
"from": "default",
|
||||
@@ -286,8 +281,7 @@
|
||||
{
|
||||
"value": {
|
||||
"specPattern": "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}",
|
||||
"supportFile": "cypress/e2e/support/e2eSupport.ts",
|
||||
"pluginsFile": "cypress/e2e/plugins/index.ts"
|
||||
"supportFile": "cypress/e2e/support/e2eSupport.ts"
|
||||
},
|
||||
"from": "config",
|
||||
"field": "e2e"
|
||||
@@ -296,7 +290,6 @@
|
||||
"value": {
|
||||
"specPattern": "**/*.cy.{js,jsx,ts,tsx}",
|
||||
"supportFile": "cypress/component/support/index.ts",
|
||||
"pluginsFile": "cypress/component/plugins/index.js",
|
||||
"devServerConfig": {
|
||||
"viteConfig": {
|
||||
"optimizeDeps": {
|
||||
|
||||
@@ -34,37 +34,45 @@
|
||||
</div>
|
||||
<div v-else>
|
||||
<Button
|
||||
ref="loginButtonRef"
|
||||
v-if="loginMutationIsPending"
|
||||
size="lg"
|
||||
:variant="buttonVariant"
|
||||
:disabled="isLoggingIn || !isOnline"
|
||||
variant="pending"
|
||||
aria-live="polite"
|
||||
@click="handleAuth"
|
||||
:disabled="true"
|
||||
>
|
||||
<template
|
||||
v-if="isLoggingIn"
|
||||
#prefix
|
||||
>
|
||||
<i-cy-loading_x16
|
||||
class="animate-spin icon-dark-white icon-light-gray-400"
|
||||
/>
|
||||
</template>
|
||||
{{ buttonMessage }}
|
||||
{{ browserOpened ? t('topNav.login.actionWaiting') : t('topNav.login.actionOpening') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
ref="loginButtonRef"
|
||||
size="lg"
|
||||
variant="primary"
|
||||
aria-live="polite"
|
||||
:disabled="!cloudViewer && !isOnline"
|
||||
@click="handleLoginOrContinue"
|
||||
>
|
||||
{{ cloudViewer ? t('topNav.login.actionContinue') : t('topNav.login.actionLogin') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { gql } from '@urql/core'
|
||||
import { useMutation, useQuery } from '@urql/vue'
|
||||
import { useMutation } from '@urql/vue'
|
||||
import { useOnline } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
Auth_LoginDocument,
|
||||
Auth_LogoutDocument,
|
||||
Auth_ResetAuthStateDocument,
|
||||
Auth_BrowserOpenedDocument,
|
||||
} from '../generated/graphql'
|
||||
import type {
|
||||
AuthFragment,
|
||||
@@ -119,99 +127,76 @@ mutation Auth_ResetAuthState {
|
||||
}
|
||||
`
|
||||
|
||||
gql`
|
||||
query Auth_BrowserOpened {
|
||||
authState {
|
||||
browserOpened
|
||||
name
|
||||
message
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const login = useMutation(Auth_LoginDocument)
|
||||
const logout = useMutation(Auth_LogoutDocument)
|
||||
const reset = useMutation(Auth_ResetAuthStateDocument)
|
||||
|
||||
const loginButtonRef = ref(Button)
|
||||
const loginInitiated = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
loginButtonRef?.value?.$el?.focus()
|
||||
})
|
||||
|
||||
const clickedOnce = ref(false)
|
||||
onBeforeUnmount(() => {
|
||||
// If a log in was initiated from this component instance, then the auth state
|
||||
// may be polluted, due to the mutation still being fetched or due to
|
||||
// errors returned during the login process. So a reset occurs when
|
||||
// this instance unmounts to cover all scenarios where the LoginModal may be dismissed.
|
||||
//
|
||||
// We only perform the reset for the component that triggered a login
|
||||
// to prevent state conflicts when LoginModals are presented within the launchpad
|
||||
// and app simultaneously.
|
||||
if (loginInitiated.value) {
|
||||
reset.executeMutation({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'continue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const viewer = computed(() => props.gql.cloudViewer)
|
||||
const isBrowserOpened = computed(() => props.gql.authState.browserOpened)
|
||||
const isLoggingIn = computed(() => clickedOnce.value && !viewer.value)
|
||||
|
||||
const query = useQuery({
|
||||
query: Auth_BrowserOpenedDocument,
|
||||
requestPolicy: 'cache-and-network',
|
||||
const cloudViewer = computed(() => {
|
||||
return props.gql.cloudViewer
|
||||
})
|
||||
|
||||
const handleAuth = async () => {
|
||||
if (viewer.value) {
|
||||
const browserOpened = computed(() => {
|
||||
return props.gql.authState.browserOpened
|
||||
})
|
||||
|
||||
// We determine that a login is pending if there is no current cloudViewer and
|
||||
// either a login has been initiated from this component, or the browser has been
|
||||
// successfully opened.
|
||||
//
|
||||
// It is possible for the browser to be open but not due to actions by this component,
|
||||
// particularly when LoginModals are presented in both the launchpad and app simultaneously.
|
||||
const loginMutationIsPending = computed(() => {
|
||||
return !cloudViewer.value && (loginInitiated.value || browserOpened.value)
|
||||
})
|
||||
|
||||
const handleLoginOrContinue = async () => {
|
||||
if (cloudViewer.value) {
|
||||
emit('continue', true)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
clickedOnce.value = true
|
||||
loginInitiated.value = true
|
||||
|
||||
const browserCheckInterval = setInterval(async () => {
|
||||
await query.executeQuery({})
|
||||
if (isBrowserOpened.value) {
|
||||
clearInterval(browserCheckInterval)
|
||||
}
|
||||
}, 1500)
|
||||
|
||||
await login.executeMutation({})
|
||||
login.executeMutation({})
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout.executeMutation({})
|
||||
const handleLogout = () => {
|
||||
logout.executeMutation({})
|
||||
}
|
||||
|
||||
const buttonMessage = computed(() => {
|
||||
if (!isBrowserOpened.value && isLoggingIn.value) {
|
||||
return t('topNav.login.actionOpening')
|
||||
}
|
||||
const handleTryAgain = async () => {
|
||||
await reset.executeMutation({})
|
||||
|
||||
if (!clickedOnce.value && !viewer.value) {
|
||||
return t('topNav.login.actionLogin')
|
||||
}
|
||||
|
||||
if (isLoggingIn.value) {
|
||||
return t('topNav.login.actionWaiting')
|
||||
}
|
||||
|
||||
if (viewer.value) {
|
||||
return t('topNav.login.actionContinue')
|
||||
}
|
||||
|
||||
// default
|
||||
return t('topNav.login.actionLogin')
|
||||
})
|
||||
|
||||
const buttonVariant = computed(() => {
|
||||
if (clickedOnce.value && !viewer.value) {
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
return 'primary'
|
||||
})
|
||||
|
||||
const handleTryAgain = () => {
|
||||
reset.executeMutation({})
|
||||
login.executeMutation({})
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
reset.executeMutation({})
|
||||
emit('continue', true)
|
||||
}
|
||||
|
||||
|
||||
@@ -62,8 +62,8 @@ describe('<LoginModal />', { viewportWidth: 1000, viewportHeight: 750 }, () => {
|
||||
},
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: text.login.actionLogin }).click()
|
||||
|
||||
// The LoginModal immediately shows the "Waiting..." button
|
||||
// if the browser already opened
|
||||
cy.findByRole('button', { name: text.login.actionWaiting })
|
||||
.should('be.visible')
|
||||
.and('be.disabled')
|
||||
@@ -93,8 +93,8 @@ describe('<LoginModal />', { viewportWidth: 1000, viewportHeight: 750 }, () => {
|
||||
</div>),
|
||||
})
|
||||
|
||||
cy.contains('button', text.login.actionTryAgain).should('not.exist')
|
||||
cy.contains('button', text.login.actionCancel).should('not.exist')
|
||||
cy.contains('button', text.login.actionTryAgain).should('not.be.visible')
|
||||
cy.contains('button', text.login.actionCancel).should('not.be.visible')
|
||||
cy.contains(text.login.titleBrowserError).should('be.visible')
|
||||
cy.contains(text.login.bodyBrowserError).should('be.visible')
|
||||
cy.contains(text.login.bodyBrowserErrorDetails).should('be.visible')
|
||||
|
||||
@@ -72,10 +72,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
|
||||
<div
|
||||
v-if="showFooter"
|
||||
class="bg-gray-50 border-t-1px py-16px px-24px"
|
||||
:class="{ 'hidden': !showFooter }"
|
||||
>
|
||||
<Auth
|
||||
:gql="props.gql"
|
||||
@@ -97,8 +96,6 @@ import { useOnline } from '@vueuse/core'
|
||||
import NoInternetConnection from '../../components/NoInternetConnection.vue'
|
||||
import CopyText from '@cy/components/CopyText.vue'
|
||||
import StandardModalHeader from '@cy/components/StandardModalHeader.vue'
|
||||
import { useMutation } from '@urql/vue'
|
||||
import { LoginModal_ResetAuthStateDocument } from '../../generated/graphql'
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
@@ -125,21 +122,8 @@ fragment LoginModal on Query {
|
||||
}
|
||||
`
|
||||
|
||||
gql`
|
||||
mutation LoginModal_ResetAuthState {
|
||||
resetAuthState {
|
||||
...Auth
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const resetAuth = useMutation(LoginModal_ResetAuthStateDocument)
|
||||
|
||||
const setIsOpen = (value: boolean) => {
|
||||
emit('update:modelValue', value)
|
||||
if (!value) {
|
||||
resetAuth.executeMutation({})
|
||||
}
|
||||
}
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -388,6 +388,9 @@ type CurrentProject implements Node & ProjectLike {
|
||||
"""The mode the interactive runner was launched in"""
|
||||
currentTestingType: TestingTypeEnum
|
||||
|
||||
"""File extension to use based on if the project has typescript or not"""
|
||||
fileExtensionToUse: FileExtensionEnum
|
||||
|
||||
"""Whether the project has Typescript"""
|
||||
hasTypescript: Boolean
|
||||
|
||||
@@ -532,6 +535,7 @@ enum ErrorTypeEnum {
|
||||
EXPERIMENTAL_RUN_EVENTS_REMOVED
|
||||
EXPERIMENTAL_SAMESITE_REMOVED
|
||||
EXPERIMENTAL_SHADOW_DOM_REMOVED
|
||||
EXPERIMENTAL_STUDIO_REMOVED
|
||||
EXTENSION_NOT_LOADED
|
||||
FIREFOX_COULD_NOT_CONNECT
|
||||
FIREFOX_GC_INTERVAL_REMOVED
|
||||
@@ -565,7 +569,7 @@ enum ErrorTypeEnum {
|
||||
PARALLEL_FEATURE_NOT_AVAILABLE_IN_PLAN
|
||||
PLAN_EXCEEDS_MONTHLY_TESTS
|
||||
PLAN_IN_GRACE_PERIOD_RUN_GROUPING_FEATURE_USED
|
||||
PLUGINS_INVALID_EVENT_NAME_ERROR
|
||||
PLUGINS_FILE_CONFIG_OPTION_REMOVED
|
||||
PLUGINS_RUN_EVENT_ERROR
|
||||
PORT_IN_USE_LONG
|
||||
PORT_IN_USE_SHORT
|
||||
@@ -577,6 +581,7 @@ enum ErrorTypeEnum {
|
||||
RENDERER_CRASHED
|
||||
RUN_GROUPING_FEATURE_NOT_AVAILABLE_IN_PLAN
|
||||
SETUP_NODE_EVENTS_DO_NOT_SUPPORT_DEV_SERVER
|
||||
SETUP_NODE_EVENTS_INVALID_EVENT_NAME_ERROR
|
||||
SETUP_NODE_EVENTS_IS_NOT_FUNCTION
|
||||
SUPPORT_FILE_NOT_FOUND
|
||||
TESTS_DID_NOT_START_FAILED
|
||||
@@ -628,6 +633,11 @@ input FileDetailsInput {
|
||||
line: Int
|
||||
}
|
||||
|
||||
enum FileExtensionEnum {
|
||||
js
|
||||
ts
|
||||
}
|
||||
|
||||
"""Represents a file on the file system"""
|
||||
type FileParts implements Node {
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { enumType } from 'nexus'
|
||||
|
||||
export const FileExtensionEnum = enumType({
|
||||
name: 'FileExtensionEnum',
|
||||
members: ['js', 'ts'],
|
||||
})
|
||||
@@ -5,6 +5,7 @@ export * from './gql-BrowserFamilyEnum'
|
||||
export * from './gql-BrowserStatus'
|
||||
export * from './gql-CodeGenTypeEnum'
|
||||
export * from './gql-ErrorTypeEnum'
|
||||
export * from './gql-FileExtensionEnum'
|
||||
export * from './gql-ProjectEnums'
|
||||
export * from './gql-SpecEnum'
|
||||
export * from './gql-WizardEnums'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PACKAGE_MANAGERS } from '@packages/types'
|
||||
import { enumType, nonNull, objectType, stringArg } from 'nexus'
|
||||
import path from 'path'
|
||||
import { BrowserStatusEnum } from '..'
|
||||
import { BrowserStatusEnum, FileExtensionEnum } from '..'
|
||||
import { cloudProjectBySlug } from '../../stitching/remoteGraphQLCalls'
|
||||
import { TestingTypeEnum } from '../enumTypes/gql-WizardEnums'
|
||||
import { Browser } from './gql-Browser'
|
||||
@@ -121,6 +121,14 @@ export const CurrentProject = objectType({
|
||||
},
|
||||
})
|
||||
|
||||
t.field('fileExtensionToUse', {
|
||||
type: FileExtensionEnum,
|
||||
description: 'File extension to use based on if the project has typescript or not',
|
||||
resolve: (source, args, ctx) => {
|
||||
return ctx.lifecycleManager.fileExtensionToUse
|
||||
},
|
||||
})
|
||||
|
||||
t.nonNull.list.nonNull.field('specs', {
|
||||
description: 'A list of specs for the currently open testing type of a project',
|
||||
type: Spec,
|
||||
|
||||
@@ -346,7 +346,7 @@ export const mutation = mutationType({
|
||||
resolve (_, args, ctx) {
|
||||
ctx.actions.auth.resetAuthState()
|
||||
|
||||
return ctx.appData
|
||||
return {}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
+12
@@ -15,3 +15,15 @@ describe('baseUrl', () => {
|
||||
cy.get('[data-cy="alert"]').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('experimentalStudio', () => {
|
||||
it('should show experimentalStudio warning if Cypress detects experimentalStudio config has been set', () => {
|
||||
cy.scaffoldProject('experimental-studio')
|
||||
cy.openProject('experimental-studio')
|
||||
cy.visitLaunchpad()
|
||||
|
||||
cy.get('[data-cy="warning-alert"]').contains('Warning: Experimental Studio Removed')
|
||||
cy.get('[data-cy-testingtype="e2e"]').click()
|
||||
cy.get('[data-cy="warning-alert"]').contains('Warning: Experimental Studio Removed')
|
||||
})
|
||||
})
|
||||
@@ -30,6 +30,7 @@
|
||||
<Spinner />
|
||||
</template>
|
||||
<template v-else-if="!currentProject?.currentTestingType">
|
||||
<WarningList :gql="query.data.value" />
|
||||
<LaunchpadHeader
|
||||
:title="t('welcomePage.title')"
|
||||
description=""
|
||||
@@ -79,6 +80,7 @@ import Wizard from './setup/Wizard.vue'
|
||||
import ScaffoldLanguageSelect from './setup/ScaffoldLanguageSelect.vue'
|
||||
import GlobalPage from './global/GlobalPage.vue'
|
||||
import BaseError from './error/BaseError.vue'
|
||||
import WarningList from './warning/WarningList.vue'
|
||||
import StandardModal from '@cy/components/StandardModal.vue'
|
||||
import HeaderBar from '@cy/gql-components/HeaderBar.vue'
|
||||
import Spinner from '@cy/components/Spinner.vue'
|
||||
@@ -114,6 +116,7 @@ fragment MainLaunchpadQueryData on Query {
|
||||
isInGlobalMode
|
||||
...GlobalPage
|
||||
...ScaffoldedFiles
|
||||
...WarningList
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import _ from 'lodash'
|
||||
import path from 'path'
|
||||
import deepDiff from 'return-deep-diff'
|
||||
import type { ResolvedFromConfig, ResolvedConfigurationOptionSource, AllModeOptions, FullConfig } from '@packages/types'
|
||||
import configUtils from '@packages/config'
|
||||
import * as configUtils from '@packages/config'
|
||||
import * as errors from './errors'
|
||||
import { getProcessEnvVars, CYPRESS_SPECIAL_ENV_VARS } from './util/config'
|
||||
import { fs } from './util/fs'
|
||||
@@ -128,7 +128,7 @@ export function mergeDefaults (
|
||||
.chain(configUtils.allowed({ ...cliConfig, ...options }))
|
||||
.omit('env')
|
||||
.omit('browsers')
|
||||
.each((val, key) => {
|
||||
.each((val: any, key) => {
|
||||
// If users pass in testing-type specific keys (eg, specPattern),
|
||||
// we want to merge this with what we've read from the config file,
|
||||
// rather than override it entirely.
|
||||
@@ -511,9 +511,7 @@ export function parseEnv (cfg: Record<string, any>, envCLI: Record<string, any>,
|
||||
envCLI = envCLI != null ? envCLI : {}
|
||||
|
||||
const configFromEnv = _.reduce(envProc, (memo: string[], val, key) => {
|
||||
let cfgKey: string
|
||||
|
||||
cfgKey = configUtils.matchesConfigKey(key)
|
||||
const cfgKey = configUtils.matchesConfigKey(key)
|
||||
|
||||
if (cfgKey) {
|
||||
// only change the value if it hasn't been
|
||||
|
||||
@@ -228,5 +228,6 @@ const start = (onMessage, utmCode, onLoginFlowComplete) => {
|
||||
|
||||
export = {
|
||||
start,
|
||||
stopServer,
|
||||
_internal,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DataContext, getCtx, clearCtx, setCtx } from '@packages/data-context'
|
||||
import electron, { OpenDialogOptions, SaveDialogOptions, BrowserWindow } from 'electron'
|
||||
import pkg from '@packages/root'
|
||||
import configUtils from '@packages/config'
|
||||
import * as configUtils from '@packages/config'
|
||||
import { isListening } from './util/ensure-url'
|
||||
|
||||
import type {
|
||||
@@ -63,6 +63,7 @@ export function makeDataContext (options: MakeDataContextOptions): DataContext {
|
||||
updateWithPluginValues: config.updateWithPluginValues,
|
||||
setupFullConfigWithDefaults: config.setupFullConfigWithDefaults,
|
||||
validateRootConfigBreakingChanges: configUtils.validateNoBreakingConfigRoot,
|
||||
validateLaunchpadConfigBreakingChanges: configUtils.validateNoBreakingConfigLaunchpad,
|
||||
validateTestingTypeConfigBreakingChanges: configUtils.validateNoBreakingTestingTypeConfig,
|
||||
},
|
||||
appApi: {
|
||||
@@ -92,7 +93,7 @@ export function makeDataContext (options: MakeDataContextOptions): DataContext {
|
||||
return user.logOut()
|
||||
},
|
||||
resetAuthState () {
|
||||
return ctx.actions.auth.resetAuthState()
|
||||
auth.stopServer()
|
||||
},
|
||||
},
|
||||
projectApi: {
|
||||
|
||||
@@ -66,7 +66,7 @@ class RunPlugins {
|
||||
|
||||
if (!isValid) {
|
||||
const err = userEvents
|
||||
? require('@packages/errors').getError('PLUGINS_INVALID_EVENT_NAME_ERROR', this.requiredFile, event, userEvents, error)
|
||||
? require('@packages/errors').getError('SETUP_NODE_EVENTS_INVALID_EVENT_NAME_ERROR', this.requiredFile, event, userEvents, error)
|
||||
: require('@packages/errors').getError('CONFIG_FILE_SETUP_NODE_EVENTS_ERROR', this.requiredFile, initialConfig.testingType, error)
|
||||
|
||||
this.ipc.send('setupTestingType:error', util.serializeError(err))
|
||||
|
||||
@@ -11,10 +11,10 @@ const throwKnownError = function (message, props = {}) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
run (pluginsFilePath, options) {
|
||||
run (configFilePath, options) {
|
||||
debug('run task', options.task, 'with arg', options.arg)
|
||||
|
||||
const fileText = pluginsFilePath ? `\n\nFix this in your setupNodeEvents method here:\n${pluginsFilePath}` : ''
|
||||
const fileText = configFilePath ? `\n\nFix this in your setupNodeEvents method here:\n${configFilePath}` : ''
|
||||
|
||||
return Promise
|
||||
.try(() => {
|
||||
|
||||
@@ -14,7 +14,7 @@ const pkg = require('@packages/root')
|
||||
const detect = require('@packages/launcher/lib/detect')
|
||||
const launch = require('@packages/launcher/lib/browsers')
|
||||
const extension = require('@packages/extension')
|
||||
const v = require('@packages/config/lib/validation')
|
||||
const { validation: v } = require('@packages/config')
|
||||
|
||||
const argsUtil = require(`../../lib/util/args`)
|
||||
const { fs } = require(`../../lib/util/fs`)
|
||||
|
||||
@@ -517,26 +517,6 @@ describe('lib/config', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('pluginsFile', () => {
|
||||
it('passes if a string', function () {
|
||||
this.setup({ pluginsFile: 'cypress/plugins' })
|
||||
|
||||
return this.expectValidationPasses()
|
||||
})
|
||||
|
||||
it('passes if false', function () {
|
||||
this.setup({ pluginsFile: false })
|
||||
|
||||
return this.expectValidationPasses()
|
||||
})
|
||||
|
||||
it('fails if not a string or false', function () {
|
||||
this.setup({ pluginsFile: 42 })
|
||||
|
||||
return this.expectValidationFails('be a string')
|
||||
})
|
||||
})
|
||||
|
||||
context('port', () => {
|
||||
it('passes if a number', function () {
|
||||
this.setup({ port: 10 })
|
||||
@@ -1441,6 +1421,16 @@ describe('lib/config', () => {
|
||||
expect(warning).to.be.calledWith('EXPERIMENTAL_RUN_EVENTS_REMOVED')
|
||||
})
|
||||
|
||||
it('warns if experimentalStudio is passed', async function () {
|
||||
const warning = sinon.spy(errors, 'warning')
|
||||
|
||||
await this.defaults('experimentalStudio', true, {
|
||||
experimentalStudio: true,
|
||||
})
|
||||
|
||||
expect(warning).to.be.calledWith('EXPERIMENTAL_STUDIO_REMOVED')
|
||||
})
|
||||
|
||||
// @see https://github.com/cypress-io/cypress/pull/9185
|
||||
it('warns if experimentalNetworkStubbing is passed', async function () {
|
||||
const warning = sinon.spy(errors, 'warning')
|
||||
@@ -1506,7 +1496,6 @@ describe('lib/config', () => {
|
||||
modifyObstructiveCode: { value: true, from: 'default' },
|
||||
numTestsKeptInMemory: { value: 50, from: 'default' },
|
||||
pageLoadTimeout: { value: 60000, from: 'default' },
|
||||
pluginsFile: { value: 'cypress/plugins', from: 'default' },
|
||||
port: { value: 1234, from: 'cli' },
|
||||
projectId: { value: null, from: 'default' },
|
||||
redirectionLimit: { value: 20, from: 'default' },
|
||||
@@ -1617,7 +1606,6 @@ describe('lib/config', () => {
|
||||
modifyObstructiveCode: { value: true, from: 'default' },
|
||||
numTestsKeptInMemory: { value: 50, from: 'default' },
|
||||
pageLoadTimeout: { value: 60000, from: 'default' },
|
||||
pluginsFile: { value: 'cypress/plugins', from: 'default' },
|
||||
port: { value: 2020, from: 'config' },
|
||||
projectId: { value: 'projectId123', from: 'env' },
|
||||
redirectionLimit: { value: 20, from: 'default' },
|
||||
@@ -1875,7 +1863,6 @@ describe('lib/config', () => {
|
||||
|
||||
const cfg = {
|
||||
projectRoot: '/foo/bar',
|
||||
pluginsFile: '/foo/bar/cypress/plugins/index.js',
|
||||
browsers: [browser],
|
||||
resolved: {
|
||||
browsers: {
|
||||
@@ -2218,7 +2205,7 @@ describe('lib/config', () => {
|
||||
expect(config.setAbsolutePaths(obj)).to.deep.eq(obj)
|
||||
})
|
||||
|
||||
return ['fileServerFolder', 'fixturesFolder', 'supportFile', 'pluginsFile'].forEach((folder) => {
|
||||
return ['fileServerFolder', 'fixturesFolder', 'supportFile'].forEach((folder) => {
|
||||
it(`converts relative ${folder} to absolute path`, () => {
|
||||
const obj = {
|
||||
projectRoot: '/_test-output/path/to/project',
|
||||
|
||||
@@ -216,7 +216,7 @@ describe.skip('lib/plugins/child/run_plugins', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('sends error if pluginsFile function rejects the promise', function (done) {
|
||||
it('sends error if setupNodeEvents function rejects the promise', function (done) {
|
||||
const err = new Error('foo')
|
||||
const setupNodeEventsFn = sinon.stub().rejects(err)
|
||||
|
||||
|
||||
@@ -6,20 +6,20 @@ const task = require(`../../lib/task`)
|
||||
|
||||
describe('lib/task', () => {
|
||||
beforeEach(function () {
|
||||
this.pluginsFile = 'cypress/plugins'
|
||||
this.configFilePath = 'cypress.config.js'
|
||||
sinon.stub(plugins, 'execute').resolves('result')
|
||||
|
||||
return sinon.stub(plugins, 'has').returns(true)
|
||||
})
|
||||
|
||||
it('executes the \'task\' plugin', function () {
|
||||
return task.run(this.pluginsFile, { task: 'some:task', arg: 'some:arg', timeout: 1000 }).then(() => {
|
||||
return task.run(this.configFilePath, { task: 'some:task', arg: 'some:arg', timeout: 1000 }).then(() => {
|
||||
expect(plugins.execute).to.be.calledWith('task', 'some:task', 'some:arg')
|
||||
})
|
||||
})
|
||||
|
||||
it('resolves the result of the \'task\' plugin', function () {
|
||||
return task.run(this.pluginsFile, { task: 'some:task', arg: 'some:arg', timeout: 1000 }).then((result) => {
|
||||
return task.run(this.configFilePath, { task: 'some:task', arg: 'some:arg', timeout: 1000 }).then((result) => {
|
||||
expect(result).to.equal('result')
|
||||
})
|
||||
})
|
||||
@@ -27,8 +27,8 @@ describe('lib/task', () => {
|
||||
it('throws if \'task\' event is not registered', function () {
|
||||
plugins.has.returns(false)
|
||||
|
||||
return task.run(this.pluginsFile, { timeout: 1000 }).catch((err) => {
|
||||
expect(err.message).to.equal(`The 'task' event has not been registered in the setupNodeEvents method. You must register it before using cy.task()\n\nFix this in your setupNodeEvents method here:\n${this.pluginsFile}`)
|
||||
return task.run(this.configFilePath, { timeout: 1000 }).catch((err) => {
|
||||
expect(err.message).to.equal(`The 'task' event has not been registered in the setupNodeEvents method. You must register it before using cy.task()\n\nFix this in your setupNodeEvents method here:\n${this.configFilePath}`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,8 +36,8 @@ describe('lib/task', () => {
|
||||
plugins.execute.withArgs('task').resolves('__cypress_unhandled__')
|
||||
plugins.execute.withArgs('_get:task:keys').resolves(['foo', 'bar'])
|
||||
|
||||
return task.run(this.pluginsFile, { task: 'some:task', arg: 'some:arg', timeout: 1000 }).catch((err) => {
|
||||
expect(err.message).to.equal(`The task 'some:task' was not handled in the setupNodeEvents method. The following tasks are registered: foo, bar\n\nFix this in your setupNodeEvents method here:\n${this.pluginsFile}`)
|
||||
return task.run(this.configFilePath, { task: 'some:task', arg: 'some:arg', timeout: 1000 }).catch((err) => {
|
||||
expect(err.message).to.equal(`The task 'some:task' was not handled in the setupNodeEvents method. The following tasks are registered: foo, bar\n\nFix this in your setupNodeEvents method here:\n${this.configFilePath}`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -45,8 +45,8 @@ describe('lib/task', () => {
|
||||
plugins.execute.withArgs('task').resolves(undefined)
|
||||
plugins.execute.withArgs('_get:task:body').resolves('function () {}')
|
||||
|
||||
return task.run(this.pluginsFile, { task: 'some:task', arg: 'some:arg', timeout: 1000 }).catch((err) => {
|
||||
expect(err.message).to.equal(`The task 'some:task' returned undefined. You must return a value, null, or a promise that resolves to a value or null to indicate that the task was handled.\n\nThe task handler was:\n\nfunction () {}\n\nFix this in your setupNodeEvents method here:\n${this.pluginsFile}`)
|
||||
return task.run(this.configFilePath, { task: 'some:task', arg: 'some:arg', timeout: 1000 }).catch((err) => {
|
||||
expect(err.message).to.equal(`The task 'some:task' returned undefined. You must return a value, null, or a promise that resolves to a value or null to indicate that the task was handled.\n\nThe task handler was:\n\nfunction () {}\n\nFix this in your setupNodeEvents method here:\n${this.configFilePath}`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -54,8 +54,8 @@ describe('lib/task', () => {
|
||||
plugins.execute.withArgs('task').resolves(undefined)
|
||||
plugins.execute.withArgs('_get:task:body').resolves('')
|
||||
|
||||
return task.run(this.pluginsFile, { task: 'some:task', arg: 'some:arg', timeout: 1000 }).catch((err) => {
|
||||
expect(err.message).to.equal(`The task 'some:task' returned undefined. You must return a value, null, or a promise that resolves to a value or null to indicate that the task was handled.\n\nFix this in your setupNodeEvents method here:\n${this.pluginsFile}`)
|
||||
return task.run(this.configFilePath, { task: 'some:task', arg: 'some:arg', timeout: 1000 }).catch((err) => {
|
||||
expect(err.message).to.equal(`The task 'some:task' returned undefined. You must return a value, null, or a promise that resolves to a value or null to indicate that the task was handled.\n\nFix this in your setupNodeEvents method here:\n${this.configFilePath}`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -63,8 +63,8 @@ describe('lib/task', () => {
|
||||
plugins.execute.withArgs('task').resolves(Promise.delay(250))
|
||||
plugins.execute.withArgs('_get:task:body').resolves('function () {}')
|
||||
|
||||
return task.run(this.pluginsFile, { task: 'some:task', arg: 'some:arg', timeout: 10 }).catch((err) => {
|
||||
expect(err.message).to.equal(`The task handler was:\n\nfunction () {}\n\nFix this in your setupNodeEvents method here:\n${this.pluginsFile}`)
|
||||
return task.run(this.configFilePath, { task: 'some:task', arg: 'some:arg', timeout: 10 }).catch((err) => {
|
||||
expect(err.message).to.equal(`The task handler was:\n\nfunction () {}\n\nFix this in your setupNodeEvents method here:\n${this.configFilePath}`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface FullConfig extends Partial<Cypress.RuntimeConfigOptions & Cypre
|
||||
// and are required when creating a project.
|
||||
export type ReceivedCypressOptions =
|
||||
Pick<Cypress.RuntimeConfigOptions, 'hosts' | 'projectName' | 'clientRoute' | 'devServerPublicPathRoute' | 'namespace' | 'report' | 'socketIoCookie' | 'configFile' | 'isTextTerminal' | 'isNewProject' | 'proxyUrl' | 'browsers' | 'browserUrl' | 'socketIoRoute' | 'arch' | 'platform' | 'spec' | 'specs' | 'browser' | 'version' | 'remote'>
|
||||
& Pick<Cypress.ResolvedConfigOptions, 'chromeWebSecurity' | 'supportFolder' | 'experimentalSourceRewriting' | 'fixturesFolder' | 'reporter' | 'reporterOptions' | 'screenshotsFolder' | 'pluginsFile' | 'supportFile' | 'baseUrl' | 'viewportHeight' | 'viewportWidth' | 'port' | 'experimentalInteractiveRunEvents' | 'userAgent' | 'downloadsFolder' | 'env' | 'excludeSpecPattern' | 'specPattern'> // TODO: Figure out how to type this better.
|
||||
& Pick<Cypress.ResolvedConfigOptions, 'chromeWebSecurity' | 'supportFolder' | 'experimentalSourceRewriting' | 'fixturesFolder' | 'reporter' | 'reporterOptions' | 'screenshotsFolder' | 'supportFile' | 'baseUrl' | 'viewportHeight' | 'viewportWidth' | 'port' | 'experimentalInteractiveRunEvents' | 'userAgent' | 'downloadsFolder' | 'env' | 'excludeSpecPattern' | 'specPattern'> // TODO: Figure out how to type this better.
|
||||
|
||||
export interface SampleConfigFile{
|
||||
status: 'changes' | 'valid' | 'skipped' | 'error'
|
||||
@@ -54,6 +54,7 @@ export type BreakingOption =
|
||||
| 'EXPERIMENTAL_NETWORK_STUBBING_REMOVED'
|
||||
| 'EXPERIMENTAL_RUN_EVENTS_REMOVED'
|
||||
| 'EXPERIMENTAL_SHADOW_DOM_REMOVED'
|
||||
| 'EXPERIMENTAL_STUDIO_REMOVED'
|
||||
| 'FIREFOX_GC_INTERVAL_REMOVED'
|
||||
| 'NODE_VERSION_DEPRECATION_SYSTEM'
|
||||
| 'NODE_VERSION_DEPRECATION_BUNDLED'
|
||||
|
||||
@@ -218,65 +218,6 @@ exports['e2e plugins / works with user extensions'] = `
|
||||
✔ All specs passed! XX:XX 1 1 - - -
|
||||
|
||||
|
||||
`
|
||||
|
||||
exports['e2e plugins handles absolute path to pluginsFile 1'] = `
|
||||
|
||||
====================================================================================================
|
||||
|
||||
(Run Starting)
|
||||
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Cypress: 1.2.3 │
|
||||
│ Browser: FooBrowser 88 │
|
||||
│ Specs: 1 found (absolute.cy.js) │
|
||||
│ Searched: cypress/e2e/absolute.cy.js │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Running: absolute.cy.js (1 of 1)
|
||||
|
||||
|
||||
✓ uses the plugins file
|
||||
|
||||
1 passing
|
||||
|
||||
|
||||
(Results)
|
||||
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Tests: 1 │
|
||||
│ Passing: 1 │
|
||||
│ Failing: 0 │
|
||||
│ Pending: 0 │
|
||||
│ Skipped: 0 │
|
||||
│ Screenshots: 0 │
|
||||
│ Video: true │
|
||||
│ Duration: X seconds │
|
||||
│ Spec Ran: absolute.cy.js │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
(Video)
|
||||
|
||||
- Started processing: Compressing to 32 CRF
|
||||
- Finished processing: /XXX/XXX/XXX/cypress/videos/absolute.cy.js.mp4 (X second)
|
||||
|
||||
|
||||
====================================================================================================
|
||||
|
||||
(Run Finished)
|
||||
|
||||
|
||||
Spec Tests Passing Failing Pending Skipped
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ✔ absolute.cy.js XX:XX 1 1 - - - │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
✔ All specs passed! XX:XX 1 1 - - -
|
||||
|
||||
|
||||
`
|
||||
|
||||
exports['e2e plugins calls after:screenshot for cy.screenshot() and failure screenshots 1'] = `
|
||||
@@ -437,6 +378,8 @@ The following are valid events:
|
||||
- file:preprocessor
|
||||
- task
|
||||
|
||||
Learn more at https://docs.cypress.io/api/plugins/writing-a-plugin#config
|
||||
|
||||
InvalidEventNameError: invalid event name registered: invalid:event
|
||||
[stack trace lines]
|
||||
`
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
module.exports = {
|
||||
'defaultCommandTimeout': 1000,
|
||||
'pluginsFile': false,
|
||||
'e2e': {
|
||||
'supportFile': false,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
module.exports = {
|
||||
'fixturesFolder': false,
|
||||
'pluginsFile': false,
|
||||
'e2e': {
|
||||
'supportFile': false,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
experimentalStudio: true,
|
||||
e2e: {
|
||||
supportFile: false,
|
||||
},
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
module.exports = {
|
||||
'pluginsFile': false,
|
||||
'e2e': {
|
||||
'fixturesFolder': 'cypress/fixtures',
|
||||
'specPattern': 'cypress/**/*.cy.js',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
module.exports = {
|
||||
pluginsFile: false,
|
||||
e2e: {
|
||||
setupNodeEvents: (on, config) => config,
|
||||
supportFile: false,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
module.exports = {
|
||||
'pluginsFile': false,
|
||||
'fixturesFolder': 'cypress/e2e',
|
||||
'e2e': {
|
||||
'supportFile': false,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
module.exports = {
|
||||
'includeShadowDom': true,
|
||||
'pluginsFile': false,
|
||||
'e2e': {
|
||||
'supportFile': false,
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ module.exports = {
|
||||
'fixturesFolder': 'tests/_fixtures',
|
||||
'port': 8888,
|
||||
'projectId': 'abc123',
|
||||
'pluginsFile': false,
|
||||
'component': {
|
||||
'specFilePattern': 'src/**/*.spec.cy.js',
|
||||
'supportFile': 'tests/_support/spec_helper.js',
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('e2e downloads', () => {
|
||||
|
||||
systemTests.it('allows changing the downloads folder', {
|
||||
project: 'downloads',
|
||||
spec: '*',
|
||||
spec: 'downloads.cy.ts',
|
||||
config: {
|
||||
downloadsFolder: 'cypress/dls',
|
||||
video: false,
|
||||
|
||||
@@ -106,23 +106,6 @@ describe('e2e plugins', function () {
|
||||
snapshot: true,
|
||||
})
|
||||
|
||||
it('handles absolute path to pluginsFile', function () {
|
||||
const pluginsAbsolutePath = Fixtures.projectPath('plugins-absolute-path')
|
||||
|
||||
return systemTests.exec(this, {
|
||||
spec: 'absolute.cy.js',
|
||||
config: {
|
||||
pluginsFile: path.join(
|
||||
pluginsAbsolutePath,
|
||||
'cypress/plugins/index.js',
|
||||
),
|
||||
},
|
||||
project: 'plugins-absolute-path',
|
||||
sanitizeScreenshotDimensions: true,
|
||||
snapshot: true,
|
||||
})
|
||||
})
|
||||
|
||||
const pluginAfterScreenshot = 'plugin-after-screenshot'
|
||||
|
||||
it('calls after:screenshot for cy.screenshot() and failure screenshots', function () {
|
||||
|
||||
Reference in New Issue
Block a user