Merge branch 'develop' into md-10.0-merge

This commit is contained in:
Bill Glesias
2022-04-27 10:22:08 -04:00
278 changed files with 18060 additions and 2715 deletions
+2 -1
View File
@@ -5,7 +5,8 @@
"type": "node",
"request": "attach",
"name": "Attach by Process ID",
"processId": "${command:PickProcess}"
"processId": "${command:PickProcess}",
"continueOnAttach": true
},
{
"type": "node",
+1 -1
View File
@@ -1,4 +1,4 @@
{
"chrome:beta": "101.0.4951.34",
"chrome:beta": "101.0.4951.41",
"chrome:stable": "100.0.4896.127"
}
+81 -4
View File
@@ -38,6 +38,8 @@ linuxWorkflowExcludeFilters: &linux-workflow-exclude-filters
or:
- false
# - equal: [ 'tgriesser/chore/fix-windows-build', << pipeline.git.branch >> ]
- feature-multidomain
- unify-1449-beta-slug-length
# usually we don't build Mac app - it takes a long time
# but sometimes we want to really confirm we are doing the right thing
@@ -48,6 +50,8 @@ macWorkflowFilters: &mac-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
- equal: [ '10.0-release', << pipeline.git.branch >> ]
- equal: [ chore/cutover-to-bundled-react-mount, << pipeline.git.branch >> ]
- equal: [ feature-multidomain, << pipeline.git.branch >> ]
- equal: [ unify-1449-beta-slug-length, << pipeline.git.branch >> ]
- matches:
pattern: "-release$"
value: << pipeline.git.branch >>
@@ -59,6 +63,8 @@ windowsWorkflowFilters: &windows-workflow-filters
- equal: [ '10.0-release', << pipeline.git.branch >> ]
- equal: [ chore/cutover-to-bundled-react-mount, << pipeline.git.branch >> ]
- equal: [ 'unify-1036-windows-test-projects', << pipeline.git.branch >> ]
- equal: [ feature-multidomain, << pipeline.git.branch >> ]
- equal: [ unify-1449-beta-slug-length, << pipeline.git.branch >> ]
- matches:
pattern: "-release$"
value: << pipeline.git.branch >>
@@ -416,6 +422,10 @@ commands:
description: chrome channel to install
type: string
default: ''
experimentalSessionAndOrigin:
description: experimental flag to apply
type: boolean
default: false
steps:
- restore_cached_workspace
- when:
@@ -433,8 +443,13 @@ commands:
if [[ -v MAIN_RECORD_KEY ]]; then
# internal PR
CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \
yarn cypress:run --record --parallel --group 5x-driver-<<parameters.browser>> --browser <<parameters.browser>>
if <<parameters.experimentalSessionAndOrigin>>; then
CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \
yarn cypress:run-experimentalSessionAndOrigin --record --parallel --group 5x-driver-<<parameters.browser>>-experimentalSessionAndOrigin --browser <<parameters.browser>>
else
CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \
yarn cypress:run --record --parallel --group 5x-driver-<<parameters.browser>> --browser <<parameters.browser>>
fi
else
# external PR
TESTFILES=$(circleci tests glob "cypress/e2e/**/*.cy.*" | circleci tests split --total=$CIRCLE_NODE_TOTAL)
@@ -443,7 +458,11 @@ commands:
if [[ -z "$TESTFILES" ]]; then
echo "Empty list of test files"
fi
yarn cypress:run --browser <<parameters.browser>> --spec $TESTFILES
if <<parameters.experimentalSessionAndOrigin>>; then
yarn cypress:run-experimentalSessionAndOrigin --browser <<parameters.browser>> --spec $TESTFILES
else
yarn cypress:run --browser <<parameters.browser>> --spec $TESTFILES
fi
fi
working_directory: packages/driver
- verify-mocha-results
@@ -1415,6 +1434,44 @@ jobs:
- run-driver-integration-tests:
browser: electron
driver-integration-tests-chrome-experimentalSessionAndOrigin:
<<: *defaults
resource_class: medium
parallelism: 5
steps:
- run-driver-integration-tests:
browser: chrome
install-chrome-channel: stable
experimentalSessionAndOrigin: true
driver-integration-tests-chrome-beta-experimentalSessionAndOrigin:
<<: *defaults
resource_class: medium
parallelism: 5
steps:
- run-driver-integration-tests:
browser: chrome:beta
install-chrome-channel: beta
experimentalSessionAndOrigin: true
driver-integration-tests-firefox-experimentalSessionAndOrigin:
<<: *defaults
resource_class: medium
parallelism: 5
steps:
- run-driver-integration-tests:
browser: firefox
experimentalSessionAndOrigin: true
driver-integration-tests-electron-experimentalSessionAndOrigin:
<<: *defaults
resource_class: medium
parallelism: 5
steps:
- run-driver-integration-tests:
browser: electron
experimentalSessionAndOrigin: true
reporter-integration-tests:
<<: *defaults
parallelism: 3
@@ -1719,7 +1776,7 @@ jobs:
- run:
name: Check current branch to persist artifacts
command: |
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "chore/cutover-to-bundled-react-mount" && "$CIRCLE_BRANCH" != "unify-1036-windows-test-projects" && "$CIRCLE_BRANCH" != "10.0-release" ]]; then
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "chore/cutover-to-bundled-react-mount" && "$CIRCLE_BRANCH" != "unify-1036-windows-test-projects" && "$CIRCLE_BRANCH" != "10.0-release" && "$CIRCLE_BRANCH" != "unify-1449-beta-slug-length" && "$CIRCLE_BRANCH" != "feature-multidomain" ]]; then
echo "Not uploading artifacts or posting install comment for this branch."
circleci-agent step halt
fi
@@ -2242,6 +2299,22 @@ linux-workflow: &linux-workflow
context: test-runner:cypress-record-key
requires:
- build
- driver-integration-tests-chrome-experimentalSessionAndOrigin:
context: test-runner:cypress-record-key
requires:
- build
- driver-integration-tests-chrome-beta-experimentalSessionAndOrigin:
context: test-runner:cypress-record-key
requires:
- build
- driver-integration-tests-firefox-experimentalSessionAndOrigin:
context: test-runner:cypress-record-key
requires:
- build
- driver-integration-tests-electron-experimentalSessionAndOrigin:
context: test-runner:cypress-record-key
requires:
- build
- run-frontend-shared-component-tests-chrome:
context: [test-runner:launchpad-tests, test-runner:percy]
percy: true
@@ -2346,6 +2419,10 @@ linux-workflow: &linux-workflow
- driver-integration-tests-chrome
- driver-integration-tests-chrome-beta
- driver-integration-tests-electron
- driver-integration-tests-firefox-experimentalSessionAndOrigin
- driver-integration-tests-chrome-experimentalSessionAndOrigin
- driver-integration-tests-chrome-beta-experimentalSessionAndOrigin
- driver-integration-tests-electron-experimentalSessionAndOrigin
- system-tests-non-root
- system-tests-firefox
- system-tests-electron
+68 -26
View File
@@ -10,10 +10,13 @@ declare namespace Cypress {
type PrevSubject = keyof PrevSubjectMap
type TestingType = 'e2e' | 'component'
type PluginConfig = (on: PluginEvents, config: PluginConfigOptions) => void | ConfigOptions | Promise<ConfigOptions>
interface JQueryWithSelector<TElement = HTMLElement> extends JQuery<TElement> {
selector?: string | null
}
interface PrevSubjectMap<O = unknown> {
optional: O
element: JQuery
element: JQueryWithSelector
document: Document
window: Window
}
@@ -53,19 +56,6 @@ declare namespace Cypress {
password: string
}
interface RemoteState {
auth?: {
username: string
password: string
}
domainName: string
strategy: 'file' | 'http'
origin: string
fileServer: string
props: Record<string, any>
visiting: string
}
interface Backend {
/**
* Firefox only: Force Cypress to run garbage collection routines.
@@ -467,16 +457,18 @@ declare namespace Cypress {
Commands: {
add<T extends keyof Chainable>(name: T, fn: CommandFn<T>): void
add<T extends keyof Chainable>(name: T, options: CommandOptions & {prevSubject: false}, fn: CommandFn<T>): void
add<T extends keyof Chainable, S = any>(name: T, options: CommandOptions & {prevSubject: true}, fn: CommandFnWithSubject<T, S>): void
add<T extends keyof Chainable, S extends PrevSubject>(
name: T, options: CommandOptions & { prevSubject: true | S | ['optional'] }, fn: CommandFnWithSubject<T, PrevSubjectMap[S]>,
name: T, options: CommandOptions & { prevSubject: S | ['optional'] }, fn: CommandFnWithSubject<T, PrevSubjectMap[S]>,
): void
add<T extends keyof Chainable, S extends PrevSubject>(
name: T, options: CommandOptions & { prevSubject: S[] }, fn: CommandFnWithSubject<T, PrevSubjectMap<void>[S]>,
): void
addAll<T extends keyof Chainable>(fns: CommandFns): void
addAll<T extends keyof Chainable>(options: CommandOptions & {prevSubject: false}, fns: CommandFns): void
addAll<T extends keyof Chainable, S = any>(options: CommandOptions & { prevSubject: true }, fns: CommandFnsWithSubject<S>): void
addAll<T extends keyof Chainable, S extends PrevSubject>(
options: CommandOptions & { prevSubject: true | S | ['optional'] }, fns: CommandFnsWithSubject<PrevSubjectMap[S]>,
options: CommandOptions & { prevSubject: S | ['optional'] }, fns: CommandFnsWithSubject<PrevSubjectMap[S]>,
): void
addAll<T extends keyof Chainable, S extends PrevSubject>(
options: CommandOptions & { prevSubject: S[] }, fns: CommandFnsWithSubject<PrevSubjectMap<void>[S]>,
@@ -634,7 +626,7 @@ declare namespace Cypress {
* Trigger action
* @private
*/
action: (action: string, ...args: any[]) => void
action: (action: string, ...args: any[]) => any[] | void
/**
* Load files
@@ -1059,7 +1051,7 @@ declare namespace Cypress {
/**
* Save/Restore browser Cookies, LocalStorage, and SessionStorage data resulting from the supplied `setup` function.
*
* Only available if the `experimentalSessionSupport` config option is enabled.
* Only available if the `experimentalSessionAndOrigin` config option is enabled.
*
* @see https://on.cypress.io/session
*/
@@ -1421,6 +1413,29 @@ declare namespace Cypress {
*/
off: Actions
/**
* Enables running Cypress commands in a secondary origin.
* @see https://on.cypress.io/origin
* @example
* cy.origin('example.com', () => {
* cy.get('h1').should('equal', 'Example Domain')
* })
*/
origin<T extends any>(urlOrDomain: string, fn: () => void): Chainable<T>
/**
* Enables running Cypress commands in a secondary origin.
* @see https://on.cypress.io/origin
* @example
* cy.origin('example.com', { args: { key: 'value', foo: 'foo' } }, ({ key, foo }) => {
* expect(key).to.equal('value')
* expect(foo).to.equal('foo')
* })
*/
origin<T, S extends any>(urlOrDomain: string, options: {
args: T
}, fn: (args: T) => void): Chainable<S>
/**
* Get the parent DOM element of a set of DOM elements.
*
@@ -2514,7 +2529,7 @@ declare namespace Cypress {
action: 'select' | 'drag-drop'
}
interface BlurOptions extends Loggable, Forceable { }
interface BlurOptions extends Loggable, Timeoutable, Forceable { }
interface CheckOptions extends Loggable, Timeoutable, ActionableOptions {
interval: number
@@ -2806,16 +2821,16 @@ declare namespace Cypress {
* @default 'top'
*/
scrollBehavior: scrollBehaviorOptions
/**
* Enable experimental session support. See https://on.cypress.io/session
* @default false
*/
experimentalSessionSupport: boolean
/**
* Allows listening to the `before:run`, `after:run`, `before:spec`, and `after:spec` events in the plugins file during interactive mode.
* @default false
*/
experimentalInteractiveRunEvents: boolean
/**
* Enables cross-origin and improved session support, including the `cy.origin` and `cy.session` commands. See https://on.cypress.io/origin and https://on.cypress.io/session.
* @default false
*/
experimentalSessionAndOrigin: boolean
/**
* Generate and save commands directly to your test suite by interacting with your app as an end user would.
* @default false
@@ -2956,7 +2971,6 @@ declare namespace Cypress {
parentTestsFolderDisplay: string
projectName: string
proxyUrl: string
remote: RemoteState
report: boolean
reporterRoute: string
reporterUrl: string
@@ -5487,6 +5501,21 @@ declare namespace Cypress {
(action: 'task', tasks: Tasks): void
}
interface CodeFrame {
frame: string
language: string
line: number
column: number
absoluteFile: string
originalFile: string
relativeFile: string
}
interface CypressError extends Error {
docsUrl?: string
codeFrame?: CodeFrame
}
// for just a few events like "window:alert" it makes sense to allow passing cy.stub() or
// a user callback function. Others probably only need a callback function.
@@ -5592,7 +5621,7 @@ declare namespace Cypress {
* This event exists because it's extremely useful for debugging purposes.
* @see https://on.cypress.io/catalog-of-events#App-Events
*/
(action: 'fail', fn: (error: Error, mocha: Mocha.Runnable) => void): Cypress
(action: 'fail', fn: (error: CypressError, mocha: Mocha.Runnable) => void): Cypress
/**
* Fires whenever the viewport changes via a `cy.viewport()` or naturally when
* Cypress resets the viewport to the default between tests. Useful for debugging purposes.
@@ -5626,6 +5655,12 @@ declare namespace Cypress {
* @see https://on.cypress.io/catalog-of-events#App-Events
*/
(action: 'command:end', fn: (command: CommandQueue) => void): Cypress
/**
* Fires when a command is skipped, namely the `should` command.
* Useful for debugging and understanding how commands are handled.
* @see https://on.cypress.io/catalog-of-events#App-Events
*/
(action: 'skipped:command:end', fn: (command: CommandQueue) => void): Cypress
/**
* Fires whenever a command begins its retrying routines.
* This is called on the trailing edge after Cypress has internally
@@ -5716,10 +5751,13 @@ declare namespace Cypress {
}
interface EnqueuedCommand {
id: string
name: string
args: any[]
type: string
chainerId: string
injected: boolean
userInvocationStack?: string
fn(...args: any[]): any
}
@@ -5758,6 +5796,8 @@ declare namespace Cypress {
}
interface LogConfig extends Timeoutable {
/** Unique id for the log, in the form of '<origin>-<number>' */
id: string
/** The JQuery element for the command. This will highlight the command in the main window when debugging */
$el: JQuery
/** The scope of the log entry. If child, will appear nested below parents, prefixed with '-' */
@@ -5770,6 +5810,8 @@ declare namespace Cypress {
message: any
/** Set to false if you want to control the finishing of the command in the log yourself */
autoEnd: boolean
/** Set to true to immediately finish the log */
end: boolean
/** Return an object that will be printed in the dev tools console */
consoleProps(): ObjectLike
}
+1 -1
View File
@@ -33,7 +33,7 @@ Cypress.on('url:changed', (url) => {
})
Cypress.on('fail', (error, mocha) => {
error // $ExpectType Error
error // $ExpectType CypressError
mocha // $ExpectType Runnable
})
+29 -12
View File
@@ -85,7 +85,7 @@ namespace CypressCommandsTests {
arg // $ExpectType string
})
Cypress.Commands.add('newCommand', { prevSubject: true }, (subject, arg) => {
subject // $ExpectType unknown
subject // $ExpectType any
arg // $ExpectType string
return
})
@@ -115,11 +115,11 @@ namespace CypressCommandsTests {
arg // $ExpectType string
})
Cypress.Commands.add('newCommand', { prevSubject: 'element' }, (subject, arg) => {
subject // $ExpectType JQuery<HTMLElement>
subject // $ExpectType JQueryWithSelector<HTMLElement>
arg // $ExpectType string
})
Cypress.Commands.add('newCommand', { prevSubject: ['element'] }, (subject, arg) => {
subject // $ExpectType JQuery<HTMLElement>
subject // $ExpectType JQueryWithSelector<HTMLElement>
arg // $ExpectType string
})
Cypress.Commands.add('newCommand', { prevSubject: ['element', 'document', 'window'] }, (subject, arg) => {
@@ -128,7 +128,7 @@ namespace CypressCommandsTests {
} else if (subject instanceof Document) {
subject // $ExpectType Document
} else {
subject // $ExpectType JQuery<HTMLElement>
subject // $ExpectType JQueryWithSelector<HTMLElement>
}
arg // $ExpectType string
})
@@ -138,7 +138,7 @@ namespace CypressCommandsTests {
} else if (subject instanceof Document) {
subject // $ExpectType Document
} else if (subject) {
subject // $ExpectType JQuery<HTMLElement>
subject // $ExpectType JQueryWithSelector<HTMLElement>
} else {
subject // $ExpectType void
}
@@ -175,7 +175,7 @@ namespace CypressCommandsTests {
})
Cypress.Commands.addAll({ prevSubject: true }, {
newCommand: (subject, arg) => {
subject // $ExpectType unknown
subject // $ExpectType any
arg // $ExpectType any
return
},
@@ -217,13 +217,13 @@ namespace CypressCommandsTests {
})
Cypress.Commands.addAll({ prevSubject: 'element' }, {
newCommand: (subject, arg) => {
subject // $ExpectType JQuery<HTMLElement>
subject // $ExpectType JQueryWithSelector<HTMLElement>
arg // $ExpectType any
}
})
Cypress.Commands.addAll({ prevSubject: ['element'] }, {
newCommand: (subject, arg) => {
subject // $ExpectType JQuery<HTMLElement>
subject // $ExpectType JQueryWithSelector<HTMLElement>
arg // $ExpectType any
}
})
@@ -234,7 +234,7 @@ namespace CypressCommandsTests {
} else if (subject instanceof Document) {
subject // $ExpectType Document
} else {
subject // $ExpectType JQuery<HTMLElement>
subject // $ExpectType JQueryWithSelector<HTMLElement>
}
arg // $ExpectType any
}
@@ -246,7 +246,7 @@ namespace CypressCommandsTests {
} else if (subject instanceof Document) {
subject // $ExpectType Document
} else if (subject) {
subject // $ExpectType JQuery<HTMLElement>
subject // $ExpectType JQueryWithSelector<HTMLElement>
} else {
subject // $ExpectType void
}
@@ -273,7 +273,7 @@ namespace CypressCommandsTests {
originalFn.apply(this, [arg]) // $ExpectType Chainable<number>
})
Cypress.Commands.overwrite<'type', 'element'>('type', (originalFn, element, text, options?: Partial<Cypress.TypeOptions & {sensitive: boolean}>) => {
element // $ExpectType JQuery<HTMLElement>
element // $ExpectType JQueryWithSelector<HTMLElement>
text // $ExpectType string
if (options && options.sensitive) {
@@ -903,7 +903,6 @@ namespace CypressTaskTests {
}
namespace CypressSessionsTests {
Cypress.config('experimentalSessionSupport') // $ExpectType boolean
cy.session('user')
cy.session('user', () => {})
cy.session({ name: 'bob' }, () => {})
@@ -938,3 +937,21 @@ namespace CypressKeyboardTests {
delay: 500 // $ExpectError
})
}
namespace CypressOriginTests {
cy.origin('example.com', () => {})
cy.origin('example.com', { args: {}}, (value: object) => {})
cy.origin('example.com', { args: { one: 1, key: 'value', bool: true } }, (value: { one: number, key: string, bool: boolean}) => {})
cy.origin('example.com', { args: [1, 'value', true ] }, (value: Array<(number | string | boolean)>) => {})
cy.origin('example.com', { args : 'value'}, (value: string) => {})
cy.origin('example.com', { args: 1 }, (value: number) => {})
cy.origin('example.com', { args: true }, (value: boolean) => {})
cy.origin() // $ExpectError
cy.origin('example.com') // $ExpectError
cy.origin(true) // $ExpectError
cy.origin('example.com', {}) // $ExpectError
cy.origin('example.com', {}, {}) // $ExpectError
cy.origin('example.com', { args: ['value'] }, (value: boolean[]) => {}) // $ExpectError
cy.origin('example.com', {}, (value: undefined) => {}) // $ExpectError
}
+1 -5
View File
@@ -1,6 +1,6 @@
{
"name": "cypress",
"version": "9.5.4",
"version": "9.6.0",
"description": "Cypress.io end to end testing tool",
"private": true,
"scripts": {
@@ -77,7 +77,6 @@
"devDependencies": {
"@aws-sdk/credential-providers": "3.53.0",
"@cypress/commit-message-install": "3.1.3",
"@cypress/env-or-json-file": "2.0.0",
"@cypress/github-commit-status-check": "1.5.0",
"@cypress/questions-remain": "1.0.1",
"@cypress/request": "2.88.10",
@@ -198,11 +197,8 @@
"mock-fs": "5.1.1",
"p-defer": "^3.0.0",
"patch-package": "6.4.7",
"plist": "3.0.5",
"pluralize": "8.0.0",
"postinstall-postinstall": "2.0.0",
"prefixed-list": "1.0.1",
"pretty-ms": "7.0.0",
"print-arch": "1.0.0",
"proxyquire": "2.1.3",
"rimraf": "3.0.2",
+1 -1
View File
@@ -173,7 +173,7 @@ export const sessionLifecycle = () => {
<div class='container'>
${svgCy}
<br/>
<p class="warn">Because <code><b>experimentalSessionSupport</b></code> is enabled, Cypress navigates to the default blank page <span class="em">before each test</span> to ensure test reliability.</p>
<p class="warn">Because <code><b>experimentalSessionAndOrigin</b></code> is enabled, Cypress navigates to the default blank page <span class="em">before each test</span> to ensure test reliability.</p>
<p>This is the default blank page.</p>
<p>To test your web application:</p>
<ul>
+51 -34
View File
@@ -82,6 +82,40 @@ export class AutIframe {
return Cypress.cy.detachDom(this._contents())
}
/**
* If the AUT is cross origin relative to top, a security error is thrown and the method returns false
* If the AUT is cross origin relative to top and chromeWebSecurity is false, origins of the AUT and top need to be compared and returns false
* Otherwise, if top and the AUT match origins, the method returns true.
* If the AUT origin is "about://blank", that means the src attribute has been stripped off the iframe and is adhering to same origin policy
*/
doesAUTMatchTopOriginPolicy = () => {
const Cypress = this.eventManager.getCypress()
if (!Cypress) return true
try {
const { href: currentHref } = (this.$iframe as any)[0].contentWindow.document.location
const locationTop = Cypress.Location.create(window.location.href)
const locationAUT = Cypress.Location.create(currentHref)
return locationTop.originPolicy === locationAUT.originPolicy || locationAUT.originPolicy === 'about://blank'
} catch (err) {
if (err.name === 'SecurityError') {
return false
}
throw err
}
}
/**
* Removes the src attribute from the AUT iframe, resulting in 'about:blank' being loaded into the iframe
* See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-src for more details
*/
removeSrcAttribute = () => {
this.$iframe?.removeAttr('src')
}
visitBlank = ({ type }: { type?: 'session' | 'session-lifecycle' }) => {
return new Promise<void>((resolve) => {
if (!this.$iframe) {
@@ -108,6 +142,23 @@ export class AutIframe {
}
restoreDom = (snapshot) => {
if (!this.doesAUTMatchTopOriginPolicy()) {
/**
* A load event fires here when the src is removed (as does an unload event).
* This is equivalent to loading about:blank (see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-src).
* This doesn't resort in a log message being generated for a new page.
* In the event-manager code, we stop adding logs from other domains once the spec is finished.
*/
this.$iframe?.one('load', () => {
this.restoreDom(snapshot)
})
// The iframe is in a cross origin state. Remove the src attribute to adhere to same origin policy. NOTE: This should only be done ONCE.
this.removeSrcAttribute()
return
}
const Cypress = this.eventManager.getCypress()
const { headStyles = undefined, bodyStyles = undefined } = Cypress ? Cypress.cy.getStyles(snapshot) : {}
const { body, htmlAttrs } = snapshot
@@ -425,40 +476,6 @@ export class AutIframe {
})
}
beforeScreenshot = (config) => {
// could fail if iframe is cross-origin, so fail gracefully
try {
if (config.disableTimersAndAnimations) {
this.dom.addCssAnimationDisabler(this._body())
}
_.each(config.blackout, (selector) => {
this.dom.addBlackout(this._body(), selector)
})
} catch (err) {
/* eslint-disable no-console */
console.error('Failed to modify app this.dom:')
console.error(err)
/* eslint-disable no-console */
}
}
afterScreenshot = (config) => {
// could fail if iframe is cross-origin, so fail gracefully
try {
if (config.disableTimersAndAnimations) {
this.dom.removeCssAnimationDisabler(this._body())
}
this.dom.removeBlackouts(this._body())
} catch (err) {
/* eslint-disable no-console */
console.error('Failed to modify app this.dom:')
console.error(err)
/* eslint-disable no-console */
}
}
startStudio = () => {
if (this.studioRecorder.isLoading) {
this.studioRecorder.start(this._body()?.[0])
+120 -4
View File
@@ -8,7 +8,9 @@ import type { AutomationElementId, FileDetails } from '@packages/types'
import { logger } from './logger'
import type { Socket } from '@packages/socket/lib/browser'
import * as cors from '@packages/network/lib/cors'
import { automation, useRunnerUiStore } from '../store'
import { useScreenshotStore } from '../store/screenshot-store'
export type CypressInCypressMochaEvent = Array<Array<string | Record<string, any>>>
@@ -159,6 +161,10 @@ export class EventManager {
})
})
this.ws.on('cross:origin:delaying:html', (request) => {
Cypress.primaryOriginCommunicator.emit('delaying:html', request)
})
localToReporterEvents.forEach((event) => {
this.localBus.on(event, (...args) => {
this.reporterBus.emit(event, ...args)
@@ -350,6 +356,14 @@ export class EventManager {
this._clearAllCookies()
this._setUnload()
})
// The window.top should not change between test reloads, and we only need to bind the message event once
// Forward all message events to the current instance of the multi-origin communicator
if (!window.top) throw new Error('missing window.top in event-manager')
window.top.addEventListener('message', ({ data, source }) => {
Cypress?.primaryOriginCommunicator.onMessage({ data, source })
}, false)
}
start (config) {
@@ -482,7 +496,14 @@ export class EventManager {
this.reporterBus.emit('reporter:log:state:changed', displayProps)
})
Cypress.on('before:screenshot', (config, cb) => {
// TODO: MOVE BACK INTO useEventManager. Verify this works
const screenshotStore = useScreenshotStore()
const handleBeforeScreenshot = (config, cb) => {
if (config.appOnly) {
screenshotStore.setScreenshotting(true)
}
const beforeThenCb = () => {
this.localBus.emit('before:screenshot', config)
cb()
@@ -501,11 +522,16 @@ export class EventManager {
}
if (!wait) beforeThenCb()
})
}
Cypress.on('after:screenshot', (config) => {
Cypress.on('before:screenshot', handleBeforeScreenshot)
const handleAfterScreenshot = (config) => {
screenshotStore.setScreenshotting(false)
this.localBus.emit('after:screenshot', config)
})
}
Cypress.on('after:screenshot', handleAfterScreenshot)
driverToReporterEvents.forEach((event) => {
Cypress.on(event, (...args) => {
@@ -564,6 +590,90 @@ export class EventManager {
this.studioRecorder.testFailed()
}
})
Cypress.on('test:before:run', (...args) => {
Cypress.primaryOriginCommunicator.toAllSpecBridges('test:before:run', ...args)
})
Cypress.on('test:before:run:async', (...args) => {
Cypress.primaryOriginCommunicator.toAllSpecBridges('test:before:run:async', ...args)
})
// Inform all spec bridges that the primary origin has begun to unload.
Cypress.on('window:before:unload', () => {
Cypress.primaryOriginCommunicator.toAllSpecBridges('before:unload')
})
Cypress.primaryOriginCommunicator.on('window:load', ({ url }, originPolicy) => {
// Sync stable if the expected origin has loaded.
// Only listen to window load events from the most recent secondary origin, This prevents nondeterminism in the case where we redirect to an already
// established spec bridge, but one that is not the current or next cy.origin command.
if (cy.state('latestActiveOriginPolicy') === originPolicy) {
// We remain in an anticipating state until either a load even happens or a timeout.
cy.state('autOrigin', cy.state('autOrigin', cors.getOriginPolicy(url)))
cy.isAnticipatingCrossOriginResponseFor(undefined)
cy.isStable(true, 'load')
// Prints out the newly loaded URL
Cypress.emit('internal:window:load', { type: 'cross:origin', url })
// Re-broadcast to any other specBridges.
Cypress.primaryOriginCommunicator.toAllSpecBridges('window:load', { url })
}
})
Cypress.primaryOriginCommunicator.on('before:unload', () => {
// We specifically don't call 'cy.isStable' here because we don't want to inject another load event.
// Unstable is unstable regardless of where it initiated from.
cy.state('isStable', false)
// Re-broadcast to any other specBridges.
Cypress.primaryOriginCommunicator.toAllSpecBridges('before:unload')
})
Cypress.primaryOriginCommunicator.on('expect:origin', (originPolicy) => {
this.localBus.emit('expect:origin', originPolicy)
})
Cypress.primaryOriginCommunicator.on('viewport:changed', (viewport, originPolicy) => {
const callback = () => {
Cypress.primaryOriginCommunicator.toSpecBridge(originPolicy, 'viewport:changed:end')
}
Cypress.primaryOriginCommunicator.emit('sync:viewport', viewport)
this.localBus.emit('viewport:changed', viewport, callback)
})
Cypress.primaryOriginCommunicator.on('before:screenshot', (config, originPolicy) => {
const callback = () => {
Cypress.primaryOriginCommunicator.toSpecBridge(originPolicy, 'before:screenshot:end')
}
handleBeforeScreenshot(config, callback)
})
Cypress.primaryOriginCommunicator.on('url:changed', ({ url }) => {
this.localBus.emit('url:changed', url)
})
Cypress.primaryOriginCommunicator.on('after:screenshot', handleAfterScreenshot)
const crossOriginLogs = {}
Cypress.primaryOriginCommunicator.on('log:added', (attrs) => {
// If the test is over and the user enters interactive snapshot mode, do not add cross origin logs to the test runner.
if (Cypress.state('test')?.final) return
// Create a new local log representation of the cross origin log.
// It will be attached to the current command.
// We also keep a reference to it to update it in the future.
crossOriginLogs[attrs.id] = Cypress.log(attrs)
})
Cypress.primaryOriginCommunicator.on('log:changed', (attrs) => {
// Retrieve the referenced log and update it.
const log = crossOriginLogs[attrs.id]
// this will trigger a log changed event for the log itself.
log?.set(attrs)
})
}
_runDriver (state) {
@@ -619,6 +729,7 @@ export class EventManager {
// but we want to be aggressive here
// and force GC early and often
Cypress.removeAllListeners()
Cypress.primaryOriginCommunicator.removeAllListeners()
this.localBus.emit('restart')
}
@@ -676,6 +787,11 @@ export class EventManager {
this.ws.emit('spec:changed', specFile)
}
notifyCrossOriginBridgeReady (originPolicy) {
// Any multi-origin event appends the origin as the third parameter and we do the same here for this short circuit
Cypress.primaryOriginCommunicator.emit('bridge:ready', undefined, originPolicy)
}
snapshotUnpinned () {
this._unpinSnapshot()
this._hideSnapshot()
+30
View File
@@ -34,6 +34,7 @@ export class IframeModel {
private detachDom: () => AutSnapshot,
private restoreDom: (snapshot: any) => void,
private highlightEl: ({ body }: any, opts: any) => void,
private isAUTSameOrigin: () => boolean,
private eventManager: EventManager,
private studio: {
selectorPlaygroundModel: any
@@ -247,6 +248,35 @@ export class IframeModel {
_storeOriginalState () {
const autStore = useAutStore()
if (!this.isAUTSameOrigin()) {
const Cypress = this.eventManager.getCypress()
/**
* This only happens if the AUT ends in a cross origin state that the primary doesn't have access to.
* In this case, the final snapshot request from the primary is sent out to the cross-origin spec bridges.
* The spec bridge that matches the origin policy will take a snapshot and send it back to the primary for the runner to store in originalState.
*/
Cypress.primaryOriginCommunicator.toAllSpecBridges('generate:final:snapshot', autStore.url || '')
Cypress.primaryOriginCommunicator.once('final:snapshot:generated', (finalSnapshot) => {
// todo(lachlan): UNIFY-1318 - find correct default, if they are even needed, for required fields ($el, coords...)
// @ts-ignore
this.originalState = {
body: finalSnapshot.body,
htmlAttrs: finalSnapshot.htmlAttrs,
snapshot: finalSnapshot,
snapshots: [],
url: autStore.url || '',
// TODO: use same attr for both runner and runner-ct states.
// these refer to the same thing - the viewport dimensions.
viewportWidth: autStore.viewportWidth,
viewportHeight: autStore.viewportHeight,
}
})
return
}
const finalSnapshot = this.detachDom()
if (!finalSnapshot) return
+50 -10
View File
@@ -100,6 +100,7 @@ function createIframeModel () {
autIframe.detachDom,
autIframe.restoreDom,
autIframe.highlightEl,
autIframe.doesAUTMatchTopOriginPolicy,
getEventManager(),
{
recorder: getEventManager().studioRecorder,
@@ -185,6 +186,30 @@ export async function teardown () {
isTorndown = true
}
/**
* Add a cross origin iframe for multi-domain
*/
export function addCrossOriginIframe (location) {
const id = `Spec Bridge: ${location.originPolicy}`
// if it already exists, don't add another one
if (document.getElementById(id)) {
getEventManager().notifyCrossOriginBridgeReady(location.originPolicy)
return
}
addIframe({
id,
// the cross origin iframe is added to the document body instead of the
// container since it needs to match the size of the top window for screenshots
$container: document.body,
className: 'spec-bridge-iframe',
// TODO: verify window.UnifiedRunner.config.namespace
src: `${location.originPolicy}/${window.UnifiedRunner.config.namespace}/multi-domain-iframes`,
})
}
/**
* Set up a spec by creating a fresh AUT and initializing
* Cypress on it.
@@ -228,15 +253,17 @@ function runSpecCT (spec: SpecFile) {
}
/**
* Create a Spec IFrame. Used for loading the spec to execute in E2E
* Create an IFrame. If the Iframe is the spec iframe,
* this function is used for loading the spec to execute in E2E
*/
function createSpecIFrame (specSrc: string) {
const el = document.createElement('iframe')
function addIframe ({ $container, id, src, className }) {
const $addedIframe = document.createElement('iframe')
el.id = `Your Spec: '${specSrc}'`,
el.className = 'spec-iframe'
$addedIframe.id = id,
$addedIframe.className = className
return el
$container.appendChild($addedIframe)
$addedIframe.setAttribute('src', src)
}
// this is how the Cypress driver knows which spec to run.
@@ -282,17 +309,30 @@ function runSpecE2E (spec: SpecFile) {
const $autIframe: JQuery<HTMLIFrameElement> = autIframe.create().appendTo($container)
// Remove the spec bridge iframe
document.querySelectorAll('iframe.spec-bridge-iframe').forEach((el) => {
el.remove()
})
autIframe.showInitialBlankContentsE2E()
// create Spec IFrame
const specSrc = getSpecUrl(config.namespace, encodeURIComponent(spec.relative))
const $specIframe = createSpecIFrame(specSrc)
// FIXME: BILL Determine where to call client with to force browser repaint
/**
* call the clientWidth to force the browser to repaint for viewport changes
* otherwise firefox may fail when changing the viewport in between origins
* this.refs.container.clientWidth
*/
// append to document, so the iframe will execute the spec
$container.appendChild($specIframe)
$specIframe.src = specSrc
addIframe({
$container,
src: specSrc,
id: `Your Spec: '${specSrc}'`,
className: 'spec-iframe',
})
// initialize Cypress (driver) with the AUT!
getEventManager().initialize($autIframe, config)
+3 -17
View File
@@ -1,7 +1,6 @@
import { watch } from 'vue'
import { getAutIframeModel, getEventManager, UnifiedRunnerAPI } from '.'
import { addCrossOriginIframe, getAutIframeModel, getEventManager, UnifiedRunnerAPI } from '.'
import { useAutStore, useSpecStore } from '../store'
import { useScreenshotStore } from '../store/screenshot-store'
import { empty, getReporterElement, getRunnerElement } from './utils'
export function useEventManager () {
@@ -20,26 +19,11 @@ export function useEventManager () {
}
function initializeRunnerLifecycleEvents () {
const screenshotStore = useScreenshotStore()
// these events do not use GraphQL
eventManager.on('restart', () => {
runSpec()
})
eventManager.on('before:screenshot', (payload) => {
if (payload.appOnly) {
screenshotStore.setScreenshotting(true)
}
getAutIframeModel().beforeScreenshot(payload)
})
eventManager.on('after:screenshot', (config) => {
screenshotStore.setScreenshotting(false)
getAutIframeModel().afterScreenshot(config)
})
eventManager.on('script:error', (err) => {
autStore.setScriptError(err)
})
@@ -51,6 +35,8 @@ export function useEventManager () {
eventManager.on('visit:blank', ({ type }) => {
getAutIframeModel().visitBlank({ type })
})
eventManager.on('expect:origin', addCrossOriginIframe)
}
const startSpecWatcher = () => {
+82 -82
View File
@@ -1,4 +1,4 @@
exports['src/index .getBreakingKeys returns list of breaking config keys 1'] = [
exports['config/lib/index .getBreakingKeys returns list of breaking config keys 1'] = [
"componentFolder",
"integrationFolder",
"testFiles",
@@ -16,7 +16,7 @@ exports['src/index .getBreakingKeys returns list of breaking config keys 1'] = [
"nodeVersion"
]
exports['src/index .getDefaultValues returns list of public config keys 1'] = {
exports['config/lib/index .getDefaultValues returns list of public config keys 1'] = {
"animationDistanceThreshold": 5,
"baseUrl": null,
"blockHosts": null,
@@ -79,7 +79,7 @@ exports['src/index .getDefaultValues returns list of public config keys 1'] = {
"browsers": [],
"clientRoute": "/__/",
"configFile": "cypress.config.js",
"cypressBinaryRoot": "/root/cypress",
"cypressBinaryRoot": "/Users/bill/Repositories/cypress",
"devServerPublicPathRoute": "/__cypress/src",
"hosts": null,
"isInteractive": true,
@@ -93,7 +93,85 @@ exports['src/index .getDefaultValues returns list of public config keys 1'] = {
"xhrRoute": "/xhrs/"
}
exports['src/index .getPublicConfigKeys returns list of public config keys 1'] = [
exports['config/lib/index .getDefaultValues returns list of public config keys for selected testing type 1'] = {
"animationDistanceThreshold": 5,
"baseUrl": null,
"blockHosts": null,
"chromeWebSecurity": true,
"clientCertificates": [],
"component": {
"specPattern": "**/*.cy.{js,jsx,ts,tsx}",
"indexHtmlFile": "cypress/support/component-index.html"
},
"defaultCommandTimeout": 4000,
"downloadsFolder": "cypress/downloads",
"e2e": {
"specPattern": "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}"
},
"env": {},
"execTimeout": 60000,
"experimentalFetchPolyfill": false,
"experimentalInteractiveRunEvents": false,
"experimentalSessionSupport": false,
"experimentalSourceRewriting": false,
"fileServerFolder": "",
"fixturesFolder": "cypress/fixtures",
"excludeSpecPattern": "*.hot-update.js",
"includeShadowDom": false,
"keystrokeDelay": 0,
"modifyObstructiveCode": true,
"numTestsKeptInMemory": 50,
"pageLoadTimeout": 60000,
"port": null,
"projectId": null,
"redirectionLimit": 20,
"reporter": "spec",
"reporterOptions": null,
"requestTimeout": 5000,
"resolvedNodePath": null,
"resolvedNodeVersion": null,
"responseTimeout": 30000,
"retries": {
"runMode": 0,
"openMode": 0
},
"screenshotOnRunFailure": true,
"screenshotsFolder": "cypress/screenshots",
"slowTestThreshold": 10000,
"scrollBehavior": "top",
"supportFile": "cypress/support/e2e.{js,jsx,ts,tsx}",
"supportFolder": false,
"taskTimeout": 60000,
"trashAssetsBeforeRuns": true,
"userAgent": null,
"video": true,
"videoCompression": 32,
"videosFolder": "cypress/videos",
"videoUploadOnPasses": true,
"viewportHeight": 660,
"viewportWidth": 1000,
"waitForAnimations": true,
"watchForFileChanges": true,
"autoOpen": false,
"browsers": [],
"clientRoute": "/__/",
"configFile": "cypress.config.js",
"cypressBinaryRoot": "/Users/bill/Repositories/cypress",
"devServerPublicPathRoute": "/__cypress/src",
"hosts": null,
"isInteractive": true,
"isTextTerminal": false,
"morgan": true,
"namespace": "__cypress",
"reporterRoute": "/__cypress/reporter",
"socketId": null,
"socketIoCookie": "__socket",
"socketIoRoute": "/__socket",
"xhrRoute": "/xhrs/",
"specPattern": "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}"
}
exports['config/lib/index .getPublicConfigKeys returns list of public config keys 1'] = [
"animationDistanceThreshold",
"arch",
"baseUrl",
@@ -153,81 +231,3 @@ exports['src/index .getPublicConfigKeys returns list of public config keys 1'] =
"modifyObstructiveCode",
"specPattern"
]
exports['src/index .getDefaultValues returns list of public config keys for selected testing type 1'] = {
"animationDistanceThreshold": 5,
"baseUrl": null,
"blockHosts": null,
"chromeWebSecurity": true,
"clientCertificates": [],
"component": {
"specPattern": "**/*.cy.{js,jsx,ts,tsx}",
"indexHtmlFile": "cypress/support/component-index.html"
},
"defaultCommandTimeout": 4000,
"downloadsFolder": "cypress/downloads",
"e2e": {
"specPattern": "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}"
},
"env": {},
"execTimeout": 60000,
"experimentalFetchPolyfill": false,
"experimentalInteractiveRunEvents": false,
"experimentalSessionSupport": false,
"experimentalSourceRewriting": false,
"fileServerFolder": "",
"fixturesFolder": "cypress/fixtures",
"excludeSpecPattern": "*.hot-update.js",
"includeShadowDom": false,
"keystrokeDelay": 0,
"modifyObstructiveCode": true,
"numTestsKeptInMemory": 50,
"pageLoadTimeout": 60000,
"port": null,
"projectId": null,
"redirectionLimit": 20,
"reporter": "spec",
"reporterOptions": null,
"requestTimeout": 5000,
"resolvedNodePath": null,
"resolvedNodeVersion": null,
"responseTimeout": 30000,
"retries": {
"runMode": 0,
"openMode": 0
},
"screenshotOnRunFailure": true,
"screenshotsFolder": "cypress/screenshots",
"slowTestThreshold": 10000,
"scrollBehavior": "top",
"supportFile": "cypress/support/e2e.{js,jsx,ts,tsx}",
"supportFolder": false,
"taskTimeout": 60000,
"trashAssetsBeforeRuns": true,
"userAgent": null,
"video": true,
"videoCompression": 32,
"videosFolder": "cypress/videos",
"videoUploadOnPasses": true,
"viewportHeight": 660,
"viewportWidth": 1000,
"waitForAnimations": true,
"watchForFileChanges": true,
"autoOpen": false,
"browsers": [],
"clientRoute": "/__/",
"configFile": "cypress.config.js",
"cypressBinaryRoot": "/root/cypress",
"devServerPublicPathRoute": "/__cypress/src",
"hosts": null,
"isInteractive": true,
"isTextTerminal": false,
"morgan": true,
"namespace": "__cypress",
"reporterRoute": "/__cypress/reporter",
"socketId": null,
"socketIoCookie": "__socket",
"socketIoRoute": "/__socket",
"xhrRoute": "/xhrs/",
"specPattern": "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}"
}
@@ -1,14 +1,3 @@
exports['src/validation .isValidClientCertificatesSet returns error message for certs not passed as an array array 1'] = {
"key": "mockConfigKey",
"value": "1",
"type": "a positive number or null or an object with keys \"openMode\" and \"runMode\" with values of numbers or nulls"
}
exports['src/validation .isValidClientCertificatesSet returns error message for certs object without url 1'] = {
"key": "clientCertificates[0].url",
"type": "a URL matcher"
}
exports['missing https protocol'] = {
"key": "clientCertificates[0].url",
"value": "http://url.com",
@@ -21,7 +10,111 @@ exports['invalid url'] = {
"type": "a valid URL"
}
exports['src/validation .isValidBrowser passes valid browsers and forms error messages for invalid ones isValidBrowser 1'] = {
exports['undefined browsers'] = `
Missing browsers list
`
exports['empty list of browsers'] = `
Expected at least one browser
`
exports['browsers list with a string'] = {
"key": "name",
"value": "foo",
"type": "a non-empty string",
"list": "browsers"
}
exports['invalid retry value'] = {
"key": "mockConfigKey",
"value": "1",
"type": "a positive number or null or an object with keys \"openMode\" and \"runMode\" with values of numbers or nulls"
}
exports['invalid retry object'] = {
"key": "mockConfigKey",
"value": {
"fakeMode": 1
},
"type": "a positive number or null or an object with keys \"openMode\" and \"runMode\" with values of numbers or nulls"
}
exports['not qualified url'] = {
"key": "mockConfigKey",
"value": "url.com",
"type": "a fully qualified URL (starting with `http://` or `https://`)"
}
exports['empty string'] = {
"key": "mockConfigKey",
"value": "",
"type": "a fully qualified URL (starting with `http://` or `https://`)"
}
exports['not string or array'] = {
"key": "mockConfigKey",
"value": null,
"type": "a string or an array of strings"
}
exports['array of non-strings'] = {
"key": "mockConfigKey",
"value": [
1,
2,
3
],
"type": "a string or an array of strings"
}
exports['not one of the strings error message'] = {
"key": "test",
"value": "nope",
"type": "one of these values: \"foo\", \"bar\""
}
exports['number instead of string'] = {
"key": "test",
"value": 42,
"type": "one of these values: \"foo\", \"bar\""
}
exports['null instead of string'] = {
"key": "test",
"value": null,
"type": "one of these values: \"foo\", \"bar\""
}
exports['not one of the numbers error message'] = {
"key": "test",
"value": 4,
"type": "one of these values: 1, 2, 3"
}
exports['string instead of a number'] = {
"key": "test",
"value": "foo",
"type": "one of these values: 1, 2, 3"
}
exports['null instead of a number'] = {
"key": "test",
"value": null,
"type": "one of these values: 1, 2, 3"
}
exports['config/lib/validation .isValidClientCertificatesSet returns error message for certs not passed as an array array 1'] = {
"key": "mockConfigKey",
"value": "1",
"type": "a positive number or null or an object with keys \"openMode\" and \"runMode\" with values of numbers or nulls"
}
exports['config/lib/validation .isValidClientCertificatesSet returns error message for certs object without url 1'] = {
"key": "clientCertificates[0].url",
"type": "a URL matcher"
}
exports['config/lib/validation .isValidBrowser passes valid browsers and forms error messages for invalid ones isValidBrowser 1'] = {
"name": "isValidBrowser",
"behavior": [
{
@@ -90,137 +183,44 @@ exports['src/validation .isValidBrowser passes valid browsers and forms error me
]
}
exports['undefined browsers'] = `
Missing browsers list
`
exports['empty list of browsers'] = `
Expected at least one browser
`
exports['browsers list with a string'] = {
"key": "name",
"value": "foo",
"type": "a non-empty string",
"list": "browsers"
}
exports['invalid retry value'] = {
"key": "mockConfigKey",
"value": "1",
"type": "a positive number or null or an object with keys \"openMode\" and \"runMode\" with values of numbers or nulls"
}
exports['invalid retry object'] = {
"key": "mockConfigKey",
"value": {
"fakeMode": 1
},
"type": "a positive number or null or an object with keys \"openMode\" and \"runMode\" with values of numbers or nulls"
}
exports['src/validation .isPlainObject returns error message when value is a not an object 1'] = {
exports['config/lib/validation .isPlainObject returns error message when value is a not an object 1'] = {
"key": "mockConfigKey",
"value": 1,
"type": "a plain object"
}
exports['src/validation .isNumber returns error message when value is a not a number 1'] = {
exports['config/lib/validation .isNumber returns error message when value is a not a number 1'] = {
"key": "mockConfigKey",
"value": "string",
"type": "a number"
}
exports['src/validation .isNumberOrFalse returns error message when value is a not number or false 1'] = {
exports['config/lib/validation .isNumberOrFalse returns error message when value is a not number or false 1'] = {
"key": "mockConfigKey",
"value": null,
"type": "a number or false"
}
exports['not qualified url'] = {
"key": "mockConfigKey",
"value": "url.com",
"type": "a fully qualified URL (starting with `http://` or `https://`)"
}
exports['empty string'] = {
"key": "mockConfigKey",
"value": "",
"type": "a fully qualified URL (starting with `http://` or `https://`)"
}
exports['src/validation .isBoolean returns error message when value is a not a string 1'] = {
exports['config/lib/validation .isBoolean returns error message when value is a not a string 1'] = {
"key": "mockConfigKey",
"value": 1,
"type": "a string"
}
exports['src/validation .isString returns error message when value is a not a string 1'] = {
exports['config/lib/validation .isString returns error message when value is a not a string 1'] = {
"key": "mockConfigKey",
"value": 1,
"type": "a string"
}
exports['src/validation .isArray returns error message when value is a non-array 1'] = {
exports['config/lib/validation .isArray returns error message when value is a non-array 1'] = {
"key": "mockConfigKey",
"value": 1,
"type": "an array"
}
exports['src/validation .isStringOrFalse returns error message when value is neither string nor false 1'] = {
exports['config/lib/validation .isStringOrFalse returns error message when value is neither string nor false 1'] = {
"key": "mockConfigKey",
"value": null,
"type": "a string or false"
}
exports['not string or array'] = {
"key": "mockConfigKey",
"value": null,
"type": "a string or an array of strings"
}
exports['array of non-strings'] = {
"key": "mockConfigKey",
"value": [
1,
2,
3
],
"type": "a string or an array of strings"
}
exports['not one of the strings error message'] = {
"key": "test",
"value": "nope",
"type": "one of these values: \"foo\", \"bar\""
}
exports['number instead of string'] = {
"key": "test",
"value": 42,
"type": "one of these values: \"foo\", \"bar\""
}
exports['null instead of string'] = {
"key": "test",
"value": null,
"type": "one of these values: \"foo\", \"bar\""
}
exports['not one of the numbers error message'] = {
"key": "test",
"value": 4,
"type": "one of these values: 1, 2, 3"
}
exports['string instead of a number'] = {
"key": "test",
"value": "foo",
"type": "one of these values: 1, 2, 3"
}
exports['null instead of a number'] = {
"key": "test",
"value": null,
"type": "one of these values: 1, 2, 3"
}
+528
View File
@@ -0,0 +1,528 @@
const validate = require('./validation')
interface ResolvedConfigOption {
name: string
defaultValue?: any
validation: Function
isFolder?: boolean
isExperimental?: boolean
/**
* Can be mutated with Cypress.config() or test-specific configuration overrides
*/
canUpdateDuringTestTime?: boolean
}
interface RuntimeConfigOption {
name: string
defaultValue: any
validation: Function
isInternal?: boolean
/**
* Can be mutated with Cypress.config() or test-specific configuration overrides
*/
canUpdateDuringTestTime?: boolean
}
interface BreakingOption {
/**
* The non-passive configuration option.
*/
name: string
/**
* String to summarize the error messaging that is logged.
*/
errorKey: string
/**
* Configuration value of the configuration option to check against.
*/
value?: string
/**
* The new configuration key that is replacing the existing configuration key.
*/
newName?: string
/**
* Whether to log the error message as a warning instead of throwing an error.
*/
isWarning?: boolean
}
const isValidConfig = (key, config) => {
const status = validate.isPlainObject(key, config)
if (status !== true) {
return status
}
for (const rule of options) {
if (rule.name in config && rule.validation) {
const status = rule.validation(`${key}.${rule.name}`, config[rule.name])
if (status !== true) {
return status
}
}
}
return true
}
// NOTE:
// If you add/remove/change a config value, make sure to update the following
// - cli/types/index.d.ts (including allowed config options on TestOptions)
// - cypress.schema.json
//
// Add options in alphabetical order for better readability
// TODO - add boolean attribute to indicate read-only / static vs mutable options
// that can be updated during test executions
const resolvedOptions: Array<ResolvedConfigOption> = [
{
name: 'animationDistanceThreshold',
defaultValue: 5,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'baseUrl',
defaultValue: null,
validation: validate.isFullyQualifiedUrl,
canUpdateDuringTestTime: true,
}, {
name: 'blockHosts',
defaultValue: null,
validation: validate.isStringOrArrayOfStrings,
canUpdateDuringTestTime: true,
}, {
name: 'chromeWebSecurity',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
}, {
name: 'clientCertificates',
defaultValue: [],
validation: validate.isValidClientCertificatesSet,
canUpdateDuringTestTime: false,
}, {
name: 'component',
// runner-ct overrides
defaultValue: {},
validation: isValidConfig,
canUpdateDuringTestTime: false,
}, {
name: 'componentFolder',
defaultValue: 'cypress/component',
validation: validate.isStringOrFalse,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'defaultCommandTimeout',
defaultValue: 4000,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'downloadsFolder',
defaultValue: 'cypress/downloads',
validation: validate.isString,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'e2e',
// e2e runner overrides
defaultValue: {},
validation: isValidConfig,
canUpdateDuringTestTime: false,
}, {
name: 'env',
defaultValue: {},
validation: validate.isPlainObject,
canUpdateDuringTestTime: true,
}, {
name: 'execTimeout',
defaultValue: 60000,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'experimentalFetchPolyfill',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
canUpdateDuringTestTime: false,
}, {
name: 'experimentalInteractiveRunEvents',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
canUpdateDuringTestTime: false,
}, {
name: 'experimentalSessionAndOrigin',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
canUpdateDuringTestTime: false,
}, {
name: 'experimentalSourceRewriting',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
canUpdateDuringTestTime: false,
}, {
name: 'experimentalStudio',
defaultValue: false,
validation: validate.isBoolean,
isExperimental: true,
canUpdateDuringTestTime: false,
}, {
name: 'fileServerFolder',
defaultValue: '',
validation: validate.isString,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'fixturesFolder',
defaultValue: 'cypress/fixtures',
validation: validate.isStringOrFalse,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'ignoreTestFiles',
defaultValue: '*.hot-update.js',
validation: validate.isStringOrArrayOfStrings,
canUpdateDuringTestTime: true,
}, {
name: 'includeShadowDom',
defaultValue: false,
validation: validate.isBoolean,
canUpdateDuringTestTime: true,
}, {
name: 'integrationFolder',
defaultValue: 'cypress/integration',
validation: validate.isString,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'keystrokeDelay',
defaultValue: 0,
validation: validate.isNumberOrFalse,
canUpdateDuringTestTime: true,
}, {
name: 'modifyObstructiveCode',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
}, {
name: 'nodeVersion',
validation: validate.isOneOf('bundled', 'system'),
canUpdateDuringTestTime: false,
}, {
name: 'numTestsKeptInMemory',
defaultValue: 50,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'pageLoadTimeout',
defaultValue: 60000,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'pluginsFile',
defaultValue: 'cypress/plugins',
validation: validate.isStringOrFalse,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'port',
defaultValue: null,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'projectId',
defaultValue: null,
validation: validate.isString,
canUpdateDuringTestTime: true,
}, {
name: 'redirectionLimit',
defaultValue: 20,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'reporter',
defaultValue: 'spec',
validation: validate.isString,
canUpdateDuringTestTime: true,
}, {
name: 'reporterOptions',
defaultValue: null,
validation: validate.isPlainObject,
canUpdateDuringTestTime: true,
}, {
name: 'requestTimeout',
defaultValue: 5000,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'resolvedNodePath',
defaultValue: null,
validation: validate.isString,
canUpdateDuringTestTime: false,
}, {
name: 'resolvedNodeVersion',
defaultValue: null,
validation: validate.isString,
canUpdateDuringTestTime: false,
}, {
name: 'responseTimeout',
defaultValue: 30000,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'retries',
defaultValue: {
runMode: 0,
openMode: 0,
},
validation: validate.isValidRetriesConfig,
canUpdateDuringTestTime: true,
}, {
name: 'screenshotOnRunFailure',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: true,
}, {
name: 'screenshotsFolder',
defaultValue: 'cypress/screenshots',
validation: validate.isStringOrFalse,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'slowTestThreshold',
defaultValue: (options: Record<string, any> = {}) => options.testingType === 'component' ? 250 : 10000,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'scrollBehavior',
defaultValue: 'top',
validation: validate.isOneOf('center', 'top', 'bottom', 'nearest', false),
canUpdateDuringTestTime: true,
}, {
name: 'supportFile',
defaultValue: 'cypress/support',
validation: validate.isStringOrFalse,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'supportFolder',
defaultValue: false,
validation: validate.isStringOrFalse,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'taskTimeout',
defaultValue: 60000,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'testFiles',
defaultValue: '**/*.*',
validation: validate.isStringOrArrayOfStrings,
canUpdateDuringTestTime: false,
}, {
name: 'trashAssetsBeforeRuns',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
}, {
name: 'userAgent',
defaultValue: null,
validation: validate.isString,
canUpdateDuringTestTime: false,
}, {
name: 'video',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
}, {
name: 'videoCompression',
defaultValue: 32,
validation: validate.isNumberOrFalse,
canUpdateDuringTestTime: false,
}, {
name: 'videosFolder',
defaultValue: 'cypress/videos',
validation: validate.isString,
isFolder: true,
canUpdateDuringTestTime: false,
}, {
name: 'videoUploadOnPasses',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
}, {
name: 'viewportHeight',
defaultValue: 660,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'viewportWidth',
defaultValue: 1000,
validation: validate.isNumber,
canUpdateDuringTestTime: true,
}, {
name: 'waitForAnimations',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: true,
}, {
name: 'watchForFileChanges',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
},
]
const runtimeOptions: Array<RuntimeConfigOption> = [
{
name: 'autoOpen',
defaultValue: false,
validation: validate.isBoolean,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'browsers',
defaultValue: [],
validation: validate.isValidBrowserList,
canUpdateDuringTestTime: false,
}, {
name: 'clientRoute',
defaultValue: '/__/',
validation: validate.isString,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'configFile',
defaultValue: 'cypress.json',
validation: validate.isStringOrFalse,
// not truly internal, but can only be set via cli,
// so we don't consider it a "public" option
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'devServerPublicPathRoute',
defaultValue: '/__cypress/src',
validation: validate.isString,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'hosts',
defaultValue: null,
validation: validate.isPlainObject,
canUpdateDuringTestTime: false,
}, {
name: 'isInteractive',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
}, {
name: 'isTextTerminal',
defaultValue: false,
validation: validate.isBoolean,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'morgan',
defaultValue: true,
validation: validate.isBoolean,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'modifyObstructiveCode',
defaultValue: true,
validation: validate.isBoolean,
canUpdateDuringTestTime: false,
}, {
name: 'namespace',
defaultValue: '__cypress',
validation: validate.isString,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'reporterRoute',
defaultValue: '/__cypress/reporter',
validation: validate.isString,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'socketId',
defaultValue: null,
validation: validate.isString,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'socketIoCookie',
defaultValue: '__socket.io',
validation: validate.isString,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'socketIoRoute',
defaultValue: '/__socket.io',
validation: validate.isString,
isInternal: true,
canUpdateDuringTestTime: false,
}, {
name: 'xhrRoute',
defaultValue: '/xhrs/',
validation: validate.isString,
isInternal: true,
canUpdateDuringTestTime: false,
},
]
export const options: Array<ResolvedConfigOption|RuntimeConfigOption> = [
...resolvedOptions,
...runtimeOptions,
]
export const breakingOptions: Array<BreakingOption> = [
{
name: 'blacklistHosts',
errorKey: 'RENAMED_CONFIG_OPTION',
newName: 'blockHosts',
}, {
name: 'experimentalComponentTesting',
errorKey: 'EXPERIMENTAL_COMPONENT_TESTING_REMOVED',
isWarning: false,
}, {
name: 'experimentalGetCookiesSameSite',
errorKey: 'EXPERIMENTAL_SAMESITE_REMOVED',
isWarning: true,
}, {
name: 'experimentalNetworkStubbing',
errorKey: 'EXPERIMENTAL_NETWORK_STUBBING_REMOVED',
isWarning: true,
}, {
name: 'experimentalRunEvents',
errorKey: 'EXPERIMENTAL_RUN_EVENTS_REMOVED',
isWarning: true,
}, {
name: 'experimentalSessionSupport',
errorKey: 'EXPERIMENTAL_SESSION_SUPPORT_REMOVED',
isWarning: true,
}, {
name: 'experimentalShadowDomSupport',
errorKey: 'EXPERIMENTAL_SHADOW_DOM_REMOVED',
isWarning: true,
}, {
name: 'firefoxGcInterval',
errorKey: 'FIREFOX_GC_INTERVAL_REMOVED',
isWarning: true,
}, {
name: 'nodeVersion',
value: 'system',
errorKey: 'NODE_VERSION_DEPRECATION_SYSTEM',
isWarning: true,
}, {
name: 'nodeVersion',
value: 'bundled',
errorKey: 'NODE_VERSION_DEPRECATION_BUNDLED',
isWarning: true,
},
]
+1 -1
View File
@@ -8,7 +8,7 @@ import * as configUtil from '../../src/index'
chai.use(sinonChai)
const { expect } = chai
describe('src/index', () => {
describe('config/lib/index', () => {
describe('.allowed', () => {
it('returns filter config only containing allowed keys', () => {
const keys = configUtil.allowed({
+1 -1
View File
@@ -3,7 +3,7 @@ import { expect } from 'chai'
import * as validation from '../../src/validation'
describe('src/validation', () => {
describe('config/lib/validation', () => {
const mockKey = 'mockConfigKey'
describe('.isValidClientCertificatesSet', () => {
+1
View File
@@ -4,6 +4,7 @@ export default defineConfig({
'projectId': 'ypt4pf',
'hosts': {
'*.foobar.com': '127.0.0.1',
'*.idp.com': '127.0.0.1',
},
'reporter': 'cypress-multi-reporters',
'reporterOptions': {
@@ -2,18 +2,8 @@ const { assertLogLength } = require('../../../support/utils')
const { _, Promise, $ } = Cypress
describe('src/cy/commands/actions/check', () => {
before(() => {
cy
.visit('/fixtures/dom.html')
.then(function (win) {
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.body).empty().html(this.body)
cy.visit('/fixtures/dom.html')
})
context('#check', () => {
@@ -6,18 +6,8 @@ const getActiveElement = () => {
}
describe('src/cy/commands/actions/focus', () => {
before(() => {
cy
.visit('/fixtures/dom.html')
.then(function (win) {
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.body).empty().html(this.body)
beforeEach(() => {
cy.visit('/fixtures/dom.html')
})
context('#focus', () => {
@@ -1,18 +1,6 @@
const { $ } = Cypress
describe('src/cy/commands/actions/hover', () => {
before(() => {
cy
.visit('/fixtures/dom.html')
.then(function (win) {
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.body).empty().html(this.body)
beforeEach(() => {
cy.visit('/fixtures/dom.html')
})
context('#hover', () => {
@@ -1,19 +1,8 @@
const { $ } = window.Cypress.$Cypress
const { _ } = window.Cypress
const { _, $ } = Cypress
describe('src/cy/commands/actions/scroll', () => {
before(() => {
cy
.visit('/fixtures/scrolling.html')
.then(function (win) {
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.body).empty().html(this.body)
beforeEach(() => {
cy.visit('/fixtures/scrolling.html')
cy.viewport(600, 200)
})
@@ -2,18 +2,8 @@ const { assertLogLength } = require('../../../support/utils')
const { _, $ } = Cypress
describe('src/cy/commands/actions/select', () => {
before(() => {
cy
.visit('/fixtures/dom.html')
.then(function (win) {
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.body).empty().html(this.body)
beforeEach(() => {
cy.visit('/fixtures/dom.html')
})
context('#select', () => {
@@ -563,7 +553,7 @@ describe('src/cy/commands/actions/select', () => {
done()
})
cy.get('select[name=fielset-disabled]').select('foo')
cy.get('select[name=fieldset-disabled]').select('foo')
})
it('throws when optgroup is disabled', (done) => {
@@ -2,18 +2,8 @@ const { assertLogLength } = require('../../../support/utils')
const { _, $, Promise } = Cypress
describe('src/cy/commands/actions/submit', () => {
before(() => {
cy
.visit('/fixtures/dom.html')
.then(function (win) {
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.body).empty().html(this.body)
cy.visit('/fixtures/dom.html')
})
context('#submit', () => {
@@ -2,18 +2,8 @@ const { assertLogLength } = require('../../../support/utils')
const { _, $ } = Cypress
describe('src/cy/commands/actions/trigger', () => {
before(() => {
cy
.visit('/fixtures/dom.html')
.then(function (win) {
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.body).empty().html(this.body)
cy.visit('/fixtures/dom.html')
})
context('#trigger', () => {
@@ -2,18 +2,8 @@ const { assertLogLength } = require('../../support/utils')
const { _, $ } = Cypress
describe('src/cy/commands/aliasing', () => {
before(() => {
cy
.visit('/fixtures/dom.html')
.then(function (win) {
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.body).empty().html(this.body)
beforeEach(() => {
cy.visit('/fixtures/dom.html')
})
context('#as', () => {
@@ -2,7 +2,7 @@ const { assertLogLength } = require('../../support/utils')
const { _, $ } = Cypress
describe('src/cy/commands/angular', () => {
before(() => {
beforeEach(() => {
cy.visit('/fixtures/angular.html')
})
@@ -35,20 +35,10 @@ const captureCommands = () => {
}
describe('src/cy/commands/assertions', () => {
before(() => {
cy
.visit('/fixtures/jquery.html')
.then(function (win) {
this.body = win.document.body.outerHTML
})
})
let testCommands
beforeEach(function () {
const doc = cy.state('document')
$(doc.body).empty().html(this.body)
cy.visit('/fixtures/jquery.html')
testCommands = captureCommands()
})
@@ -70,6 +60,7 @@ describe('src/cy/commands/assertions', () => {
.noop({ foo: 'bar' }).should('deep.eq', { foo: 'bar' })
.then((obj) => {
expect(testCommands()).to.eql([
{ name: 'visit', snapshots: 1, retries: 0 },
{ name: 'noop', snapshots: 0, retries: 0 },
{ name: 'should', snapshots: 1, retries: 0 },
{ name: 'then', snapshots: 0, retries: 0 },
@@ -204,6 +195,7 @@ describe('src/cy/commands/assertions', () => {
})
.then(() => {
expect(testCommands()).to.eql([
{ name: 'visit', snapshots: 1, retries: 0 },
// cy.get() has 2 snapshots, 1 for itself, and 1
// for the .should(...) assertion.
@@ -2997,11 +2989,10 @@ describe('src/cy/commands/assertions', () => {
// should be taken.
it('only snapshots once when failing to find DOM elements and not retrying', (done) => {
cy.on('fail', (err) => {
expect(testCommands()).to.eql([{
name: 'get',
snapshots: 1,
retries: 0,
}])
expect(testCommands()).to.eql([
{ name: 'visit', snapshots: 1, retries: 0 },
{ name: 'get', snapshots: 1, retries: 0 },
])
done()
})
@@ -115,6 +115,7 @@ describe('src/cy/commands/clock', () => {
// this test was written to catch a bug in lolex (dep, now @sinonjs/fake-timers) 3 and was fixed by lolex 4 upgrade,
it(`doesn't override window.performance members`, () => {
cy.visit('/fixtures/empty.html')
cy.clock()
.then((clock) => {
cy.window().then((win) => {
@@ -1,18 +1,8 @@
const { _, $ } = Cypress
const { _ } = Cypress
describe('src/cy/commands/commands', () => {
before(() => {
cy
.visit('/fixtures/dom.html')
.then(function (win) {
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.body).empty().html(this.body)
beforeEach(() => {
cy.visit('/fixtures/dom.html')
})
it('can invoke commands by name', () => {
@@ -43,7 +33,7 @@ describe('src/cy/commands/commands', () => {
cy.command('get', 'body').then(() => {
const names = cy.queue.names()
expect(names).to.deep.eq(['get', 'then'])
expect(names).to.deep.eq(['visit', 'get', 'then'])
})
})
@@ -3,18 +3,8 @@ const { _, Promise, $ } = Cypress
describe('src/cy/commands/connectors', () => {
describe('with jquery', () => {
before(() => {
cy
.visit('/fixtures/jquery.html')
.then(function (win) {
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.body).empty().html(this.body)
cy.visit('/fixtures/jquery.html')
})
context('#spread', () => {
@@ -90,7 +80,8 @@ describe('src/cy/commands/connectors', () => {
it('does not insert a mocha callback', () => {
cy.noop().then(() => {
expect(cy.queue.length).to.eq(2)
// queue: visit -> noop -> then
expect(cy.queue.length).to.eq(3)
})
})
@@ -1803,18 +1794,8 @@ describe('src/cy/commands/connectors', () => {
})
describe('without jquery', () => {
before(() => {
cy
.visit('/fixtures/dom.html')
.then(function (win) {
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.body).empty().html(this.body)
beforeEach(() => {
cy.visit('/fixtures/dom.html')
})
context('#each', () => {
@@ -161,6 +161,8 @@ describe('src/cy/commands/files', () => {
defaultCommandTimeout: 50,
}, () => {
beforeEach(function () {
cy.visit('/fixtures/empty.html')
this.logs = []
cy.on('log:added', (attrs, log) => {
@@ -2,18 +2,8 @@ const { assertLogLength } = require('../../support/utils')
const { _, $, dom } = Cypress
describe('src/cy/commands/misc', () => {
before(() => {
cy
.visit('/fixtures/jquery.html')
.then(function (win) {
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.body).empty().html(this.body)
cy.visit('/fixtures/jquery.html')
})
context('#end', () => {
@@ -6,24 +6,8 @@ const { _, Promise, $ } = Cypress
describe('src/cy/commands/navigation', () => {
context('#reload', () => {
before(() => {
cy
.visit('/fixtures/generic.html')
.then(function (win) {
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
this.win = cy.state('window')
$(doc.body).empty().html(this.body)
})
afterEach(function () {
cy.state('window', this.win)
cy.visit('/fixtures/generic.html')
})
it('calls into window.location.reload', () => {
@@ -288,24 +272,11 @@ describe('src/cy/commands/navigation', () => {
})
context('#go', () => {
before(() => {
cy
.visit('/fixtures/generic.html')
.then(function (win) {
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.body).empty().html(this.body)
})
// TODO: fix this
it('sets timeout to Cypress.config(pageLoadTimeout)', {
pageLoadTimeout: 4567,
}, () => {
cy.visit('/fixtures/generic.html')
const timeout = cy.spy(Promise.prototype, 'timeout')
cy
@@ -508,7 +479,7 @@ describe('src/cy/commands/navigation', () => {
})
it('only logs once on error', function (done) {
cy.on('fail', (err) => {
cy.once('fail', (err) => {
assertLogLength(this.logs, 1)
expect(this.logs[0].get('error')).to.eq(err)
@@ -814,7 +785,7 @@ describe('src/cy/commands/navigation', () => {
})
// https://github.com/cypress-io/cypress/issues/1311
it(`window immediately resolves and doesn't reload when visiting the same URL with hashes`, () => {
it('window immediately resolves and doesn\'t reload when visiting the same URL with hashes', () => {
const onLoad = cy.stub()
cy
@@ -825,7 +796,7 @@ describe('src/cy/commands/navigation', () => {
onLoad,
})
.then((win) => {
expect(win.bar).to.not.exist
expect(win.foo).to.equal('bar')
expect(onLoad).not.to.have.been.called
})
})
@@ -971,7 +942,7 @@ describe('src/cy/commands/navigation', () => {
})
describe('location getter overrides', () => {
before(() => {
beforeEach(function () {
cy
.visit('/fixtures/jquery.html?foo=bar#dashboard?baz=quux')
.window().as('win').then((win) => {
@@ -980,9 +951,7 @@ describe('src/cy/commands/navigation', () => {
// overriding the location getters
expect(win.location.href).to.include('/fixtures/jquery.html?foo=bar#dashboard?baz=quux')
})
})
beforeEach(function () {
this.win = cy.state('window')
this.eq = (attr, str) => {
@@ -1440,10 +1409,25 @@ describe('src/cy/commands/navigation', () => {
it('throws when attempting to visit a 2nd domain on different port', function (done) {
cy.on('fail', (err) => {
const { lastLog } = this
const experimentalMessage = Cypress.config('experimentalSessionAndOrigin') ? `You likely forgot to use \`cy.origin()\`:\n` : `In order to visit a different origin, you can enable the \`experimentalSessionAndOrigin\` flag and use \`cy.origin()\`:\n`
expect(err.message).to.equal(stripIndent`\
\`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n
${experimentalMessage}
\`cy.visit('http://localhost:3500/fixtures/generic.html')\`
\`<commands targeting http://localhost:3500 go here>\`\n
\`cy.origin('http://localhost:3501', () => {\`
\` cy.visit('http://localhost:3501/fixtures/generic.html')\`
\` <commands targeting http://localhost:3501 go here>\`
\`})\`\n
The new URL is considered a different origin because the following parts of the URL are different:\n
> port\n
You may only \`cy.visit()\` same-origin URLs within a single test.\n
The previous URL you visited was:\n
> 'http://localhost:3500'\n
You're attempting to visit this URL:\n
> 'http://localhost:3501'`)
expect(err.message).to.include('`cy.visit()` failed because you are attempting to visit a URL that is of a different origin.')
expect(err.message).to.include('The new URL is considered a different origin because the following parts of the URL are different:')
expect(err.message).to.include('> port')
expect(err.docsUrl).to.eq('https://on.cypress.io/cannot-visit-different-origin-domain')
assertLogLength(this.logs, 2)
expect(lastLog.get('error')).to.eq(err)
@@ -1452,17 +1436,31 @@ describe('src/cy/commands/navigation', () => {
})
cy.visit('http://localhost:3500/fixtures/generic.html')
cy.visit('http://localhost:3501/fixtures/generic.html')
})
it('throws when attempting to visit a 2nd domain on different protocol', function (done) {
cy.on('fail', (err) => {
const { lastLog } = this
const experimentalMessage = Cypress.config('experimentalSessionAndOrigin') ? `You likely forgot to use \`cy.origin()\`:\n` : `In order to visit a different origin, you can enable the \`experimentalSessionAndOrigin\` flag and use \`cy.origin()\`:\n`
expect(err.message).to.equal(stripIndent`\
\`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n
${experimentalMessage}
\`cy.visit('http://localhost:3500/fixtures/generic.html')\`
\`<commands targeting http://localhost:3500 go here>\`\n
\`cy.origin('https://localhost:3502', () => {\`
\` cy.visit('https://localhost:3502/fixtures/generic.html')\`
\` <commands targeting https://localhost:3502 go here>\`
\`})\`\n
The new URL is considered a different origin because the following parts of the URL are different:\n
> protocol, port\n
You may only \`cy.visit()\` same-origin URLs within a single test.\n
The previous URL you visited was:\n
> 'http://localhost:3500'\n
You're attempting to visit this URL:\n
> 'https://localhost:3502'`)
expect(err.message).to.include('`cy.visit()` failed because you are attempting to visit a URL that is of a different origin.')
expect(err.message).to.include('The new URL is considered a different origin because the following parts of the URL are different:')
expect(err.message).to.include('> protocol')
expect(err.docsUrl).to.eq('https://on.cypress.io/cannot-visit-different-origin-domain')
assertLogLength(this.logs, 2)
expect(lastLog.get('error')).to.eq(err)
@@ -1471,16 +1469,31 @@ describe('src/cy/commands/navigation', () => {
})
cy.visit('http://localhost:3500/fixtures/generic.html')
cy.visit('https://localhost:3500/fixtures/generic.html')
cy.visit('https://localhost:3502/fixtures/generic.html')
})
it('throws when attempting to visit a 2nd domain on different superdomain', function (done) {
cy.on('fail', (err) => {
const { lastLog } = this
const experimentalMessage = Cypress.config('experimentalSessionAndOrigin') ? `You likely forgot to use \`cy.origin()\`:\n` : `In order to visit a different origin, you can enable the \`experimentalSessionAndOrigin\` flag and use \`cy.origin()\`:\n`
expect(err.message).to.equal(stripIndent`\
\`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n
${experimentalMessage}
\`cy.visit('http://localhost:3500/fixtures/generic.html')\`
\`<commands targeting http://localhost:3500 go here>\`\n
\`cy.origin('http://foobar.com:3500', () => {\`
\` cy.visit('http://www.foobar.com:3500/fixtures/generic.html')\`
\` <commands targeting http://www.foobar.com:3500 go here>\`
\`})\`\n
The new URL is considered a different origin because the following parts of the URL are different:\n
> superdomain\n
You may only \`cy.visit()\` same-origin URLs within a single test.\n
The previous URL you visited was:\n
> 'http://localhost:3500'\n
You're attempting to visit this URL:\n
> 'http://www.foobar.com:3500'`)
expect(err.message).to.include('`cy.visit()` failed because you are attempting to visit a URL that is of a different origin.')
expect(err.message).to.include('The new URL is considered a different origin because the following parts of the URL are different:')
expect(err.message).to.include('> superdomain')
expect(err.docsUrl).to.eq('https://on.cypress.io/cannot-visit-different-origin-domain')
assertLogLength(this.logs, 2)
expect(lastLog.get('error')).to.eq(err)
@@ -1489,14 +1502,31 @@ describe('src/cy/commands/navigation', () => {
})
cy.visit('http://localhost:3500/fixtures/generic.html')
cy.visit('http://google.com:3500/fixtures/generic.html')
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')
})
it('throws attempting to visit 2 unique ip addresses', function (done) {
cy.on('fail', (err) => {
const { lastLog } = this
const experimentalMessage = Cypress.config('experimentalSessionAndOrigin') ? `You likely forgot to use \`cy.origin()\`:\n` : `In order to visit a different origin, you can enable the \`experimentalSessionAndOrigin\` flag and use \`cy.origin()\`:\n`
expect(err.message).to.equal(stripIndent`\
\`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n
${experimentalMessage}
\`cy.visit('http://127.0.0.1:3500/fixtures/generic.html')\`
\`<commands targeting http://127.0.0.1:3500 go here>\`\n
\`cy.origin('http://0.0.0.0:3500', () => {\`
\` cy.visit('http://0.0.0.0:3500/fixtures/generic.html')\`
\` <commands targeting http://0.0.0.0:3500 go here>\`
\`})\`\n
The new URL is considered a different origin because the following parts of the URL are different:\n
> superdomain\n
You may only \`cy.visit()\` same-origin URLs within a single test.\n
The previous URL you visited was:\n
> 'http://127.0.0.1:3500'\n
You're attempting to visit this URL:\n
> 'http://0.0.0.0:3500'`)
expect(err.message).to.include('`cy.visit()` failed because you are attempting to visit a URL that is of a different origin.')
expect(err.docsUrl).to.eq('https://on.cypress.io/cannot-visit-different-origin-domain')
assertLogLength(this.logs, 2)
expect(lastLog.get('error')).to.eq(err)
@@ -1506,22 +1536,7 @@ describe('src/cy/commands/navigation', () => {
cy
.visit('http://127.0.0.1:3500/fixtures/generic.html')
.visit('http://126.0.0.1:3500/fixtures/generic.html')
})
it('does not call resolve:url when throws attempting to visit a 2nd domain', (done) => {
const backend = cy.spy(Cypress, 'backend')
cy.on('fail', (err) => {
expect(backend).to.be.calledWithMatch('resolve:url', 'http://localhost:3500/fixtures/generic.html')
expect(backend).not.to.be.calledWithMatch('resolve:url', 'http://google.com:3500/fixtures/generic.html')
done()
})
cy
.visit('http://localhost:3500/fixtures/generic.html')
.visit('http://google.com:3500/fixtures/generic.html')
.visit('http://0.0.0.0:3500/fixtures/generic.html')
})
it('displays loading_network_failed when _resolveUrl throws', function (done) {
@@ -2134,31 +2149,51 @@ describe('src/cy/commands/navigation', () => {
.get('#does-not-exist', { timeout: 200 }).should('have.class', 'foo')
})
it('captures cross origin failures', function (done) {
cy.once('fail', (err) => {
it('displays cross origin failures when navigating to a cross origin', { pageLoadTimeout: 3000 }, function (done) {
cy.on('fail', (err) => {
const { lastLog } = this
assertLogLength(this.logs, 2)
expect(err.message).to.include('Cypress detected a cross origin error happened on page load')
expect(err.docsUrl).to.eq('https://on.cypress.io/cross-origin-violation')
expect(lastLog.get('name')).to.eq('page load')
expect(lastLog.get('state')).to.eq('failed')
if (Cypress.config('experimentalSessionAndOrigin')) {
// When the experimentalSessionAndOrigin feature is enabled, we will timeout and display this message.
expect(err.message).to.equal(stripIndent`\
Timed out after waiting \`3000ms\` for your remote page to load on origin(s):\n
- \`http://localhost:3500\`\n
A cross-origin request for \`http://www.foobar.com:3500/fixtures/multi-domain-secondary.html\` was detected.\n
A command that triggers cross-origin navigation must be immediately followed by a \`cy.origin()\` command:\n
\`cy.origin(\'http://foobar.com:3500\', () => {\`
\` <commands targeting http://www.foobar.com:3500 go here>\`
\`})\`\n
If the cross-origin request was an intermediary state, you can try increasing the \`pageLoadTimeout\` value in \`cypress.json\` to wait longer.\n
Browsers will not fire the \`load\` event until all stylesheets and scripts are done downloading.\n
When this \`load\` event occurs, Cypress will continue running commands.`)
expect(err.docsUrl).to.eq('https://on.cypress.io/origin')
} else {
const error = Cypress.isBrowser('firefox') ? 'Permission denied to access property "document" on cross-origin object' : 'Blocked a frame with origin "http://localhost:3500" from accessing a cross-origin frame.'
// When the experimentalSessionAndOrigin feature is disabled, we will immediately and display this message.
expect(err.message).to.equal(stripIndent`\
Cypress detected a cross origin error happened on page load:\n
> ${error}\n
Before the page load, you were bound to the origin policy:\n
> http://localhost:3500\n
A cross origin error happens when your application navigates to a new URL which does not match the origin policy above.\n
A new URL does not match the origin policy if the 'protocol', 'port' (if specified), and/or 'host' (unless of the same superdomain) are different.\n
Cypress does not allow you to navigate to a different origin URL within a single test.\n
You may need to restructure some of your test code to avoid this problem.\n
Alternatively you can also disable Chrome Web Security in Chromium-based browsers which will turn off this restriction by setting { chromeWebSecurity: false } in \`cypress.json\`.`)
expect(err.docsUrl).to.eq('https://on.cypress.io/cross-origin-violation')
}
assertLogLength(this.logs, 6)
expect(lastLog.get('error')).to.eq(err)
expect(cy.state('onPageLoadErr')).to.be.null
done()
})
cy
.visit('/fixtures/jquery.html')
.window({ log: false }).then((win) => {
const url = 'http://localhost:3501/fixtures/generic.html'
const $a = win.$(`<a href='${url}'>jquery</a>`)
.appendTo(win.document.body)
causeSynchronousBeforeUnload($a)
})
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
return null
@@ -2285,20 +2320,33 @@ describe('src/cy/commands/navigation', () => {
})
})
it('waits for stability at the end of the command queue when not stable', (done) => {
it('tests waiting on stability at the end of the command queue', (done) => {
cy
.visit('/fixtures/generic.html')
.then((win) => {
cy.on('window:load', () => {
// We do not wait if the experimentalSessionAndOrigin feature is enabled
if (Cypress.config('experimentalSessionAndOrigin')) {
const onLoad = cy.spy()
cy.on('window:load', onLoad)
cy.on('command:queue:end', () => {
expect(onLoad).not.have.been.called
done()
})
})
} else {
// We do wait if the experimentalSessionAndOrigin feature is not enabled
cy.on('window:load', () => {
cy.on('command:queue:end', () => {
done()
})
})
}
cy.on('command:queue:before:end', () => {
// force us to become unstable immediately
// else the beforeunload event fires at the end
// of the tick which is too late
// force us to become unstable immediately
// else the beforeunload event fires at the end
// of the tick which is too late
cy.isStable(false, 'testing')
win.location.href = '/timeout?ms=100'
@@ -1011,7 +1011,7 @@ describe('network stubbing', function () {
// using "hosts" setting in the "cypress.json" file
const corsUrl = 'http://diff.foobar.com:3501/no-cors'
before(() => {
beforeEach(() => {
cy.visit('http://127.0.0.1:3500/fixtures/dom.html')
})
@@ -1070,7 +1070,7 @@ describe('network stubbing', function () {
// a different domain from the page own domain
const corsUrl = 'http://diff.foobar.com:3501/cors'
before(() => {
beforeEach(() => {
cy.visit('http://127.0.0.1:3500/fixtures/dom.html')
})
@@ -1278,6 +1278,8 @@ describe('network stubbing', function () {
// @see https://github.com/cypress-io/cypress/issues/15841
it('prevents requests from reaching destination server', function () {
cy.visit('/fixtures/empty.html')
const v = String(Date.now())
// this test creates server-side state via /set-var to test if requests are being sent or not
@@ -1991,6 +1993,7 @@ describe('network stubbing', function () {
.intercept(`${url}*`, (req) => {
// @ts-ignore
req.on(eventName, (res) => {
res.headers['content-type'] = 'application/json'
res.send({
statusCode: 200,
fixture: 'valid.json',
@@ -2616,6 +2619,8 @@ describe('network stubbing', function () {
it('intercepts cached responses as expected', {
browser: '!firefox', // TODO: why does firefox behave differently? transparently returns cached response
}, function () {
cy.visit('/fixtures/empty.html')
// use a queryparam to bust cache from previous runs of this test
const url = `/fixtures/generic.html?t=${Date.now()}`
let hits = 0
@@ -2688,6 +2693,7 @@ describe('network stubbing', function () {
return Promise.delay(delay)
.then(() => {
res.headers['content-type'] = 'text/plain'
res.send('Promise.delay worked')
})
})
@@ -2731,6 +2737,7 @@ describe('network stubbing', function () {
req.reply((res) => {
this.start = Date.now()
res.headers['content-type'] = 'text/plain'
res.setThrottle(kbps).send(payload)
})
}).then(() => {
@@ -2755,6 +2762,7 @@ describe('network stubbing', function () {
req.reply((res) => {
this.start = Date.now()
res.headers['content-type'] = 'text/plain'
res.setThrottle(kbps).setDelay(delay).send({
statusCode: 200,
body: payload,
@@ -2929,6 +2937,7 @@ describe('network stubbing', function () {
it('res.send(body)', function () {
cy.intercept('/custom-headers*', function (req) {
req.reply((res) => {
res.headers['content-type'] = 'text/plain'
res.send('baz')
})
})
@@ -2985,6 +2994,7 @@ describe('network stubbing', function () {
it('res.send(status, body)', function (done) {
cy.intercept('/custom-headers*', function (req) {
req.reply((res) => {
res.headers['content-type'] = 'text/plain'
res.send(777, 'bar')
})
})
@@ -3047,6 +3057,7 @@ describe('network stubbing', function () {
cy.intercept(`${url}*`, function (req) {
req.reply((res) => {
res.headers['content-type'] = 'application/json'
res.send({
statusCode: 200,
fixture: 'valid.json',
@@ -3090,6 +3101,7 @@ describe('network stubbing', function () {
req.reply((res) => {
this.start = Date.now()
res.headers['content-type'] = 'text/plain'
// ensure .throttle and .delay are overridden
res.setThrottle(1e6).setDelay(1).send({
statusCode: 200,
@@ -761,9 +761,9 @@ describe('src/cy/commands/screenshot', () => {
cy.get('.short-element').within(() => {
cy.screenshot({ capture: 'runner' })
}).then(() => {
// the runner was captured
expect(Cypress.action.withArgs('cy:before:screenshot').args[0][1].appOnly).to.be.true
expect(Cypress.automation.withArgs('take:screenshot').args[0][1].capture).to.equal('viewport')
// the runner was captured ("appOnly === true" means to hide the runner UI)
expect(Cypress.action.withArgs('cy:before:screenshot').args[0][1].appOnly).to.be.false
expect(Cypress.automation.withArgs('take:screenshot').args[0][1].capture).to.equal('runner')
})
})
@@ -0,0 +1,244 @@
const {
getSessionDetails,
getConsoleProps,
navigateAboutBlank,
} = require('@packages/driver/src/cy/commands/sessions/utils')
describe('src/cy/commands/sessions/utils.ts', () => {
describe('.getSessionDetails', () => {
it('for one domain with neither cookies or local storage set', () => {
const sessionState = {
id: 'session1',
}
const details = getSessionDetails(sessionState)
expect(details.id).to.eq('session1')
expect(Object.keys(details.data)).to.have.length(0)
})
it('for one domain with only cookies set', () => {
const sessionState = {
id: 'session1',
cookies: [
{ name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expiry: 123 },
],
}
const details = getSessionDetails(sessionState)
expect(details.id).to.eq('session1')
expect(Object.keys(details.data)).to.have.length(1)
expect(details.data).to.have.property('localhost')
expect(details.data.localhost).to.deep.eq({
cookies: 1,
})
})
it('for one domain with only local storage set', () => {
const sessionState = {
id: 'session1',
localStorage: [
{ origin: 'localhost', value: { 'stor-foo': 's-f' } },
],
}
const details = getSessionDetails(sessionState)
expect(details.id).to.eq('session1')
expect(Object.keys(details.data)).to.have.length(1)
expect(details.data).to.have.property('localhost')
expect(details.data.localhost).to.deep.eq({
localStorage: 1,
})
})
it('for one domain with both cookies and localStorage', () => {
const sessionState = {
id: 'session1',
cookies: [
{ name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expiry: 123 },
],
localStorage: [
{ origin: 'localhost', value: { 'stor-foo': 's-f' } },
],
}
const details = getSessionDetails(sessionState)
expect(details.id).to.eq('session1')
expect(Object.keys(details.data)).to.have.length(1)
expect(details.data).to.have.property('localhost')
expect(details.data.localhost).to.deep.eq({
cookies: 1,
localStorage: 1,
})
})
it('for multiple domains', () => {
const sessionState = {
id: 'session1',
cookies: [
{ name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expiry: 123 },
{ name: 'bar', value: 'b', path: '/', domain: 'localhost', secure: false, httpOnly: false, expiry: 456 },
],
localStorage: [
{ origin: 'localhost', value: { 'stor-foo': 's-f' } },
{ origin: 'http://example.com', value: { 'random': 'hi' } },
],
}
const details = getSessionDetails(sessionState)
expect(details.id).to.eq('session1')
expect(Object.keys(details.data)).to.have.length(2)
expect(details.data).to.have.property('localhost')
expect(details.data.localhost).to.deep.eq({
cookies: 2,
localStorage: 1,
})
expect(details.data).to.have.property('example.com')
expect(details.data['example.com']).to.deep.eq({
localStorage: 1,
})
})
})
describe('.getConsoleProps', () => {
it('for one domain with neither cookies or localStorage set', () => {
const sessionState = {
id: 'session1',
}
const consoleProps = getConsoleProps(sessionState)
expect(consoleProps.id).to.eq('session1')
expect(consoleProps.table).to.have.length(0)
})
it('for one domain with only cookies set', () => {
const sessionState = {
id: 'session1',
cookies: [
{ name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expiry: 123 },
],
}
const consoleProps = getConsoleProps(sessionState)
expect(consoleProps.id).to.eq('session1')
expect(consoleProps.table).to.have.length(1)
const cookiesTable = consoleProps.table[0]()
expect(cookiesTable.name).to.contain('Cookies - localhost (1)')
expect(cookiesTable.data).to.deep.eq(sessionState.cookies)
})
it('for one domain with only localStorage set', () => {
const sessionState = {
id: 'session1',
localStorage: [
{ origin: 'localhost', value: { 'stor-foo': 's-f' } },
],
}
const consoleProps = getConsoleProps(sessionState)
expect(consoleProps.id).to.eq('session1')
expect(consoleProps.table).to.have.length(1)
const localStorageTable = consoleProps.table[0]()
expect(localStorageTable.name).to.contain('Storage - localhost (1)')
expect(localStorageTable.data).to.have.length(1)
expect(localStorageTable.data).to.deep.eq([{ key: 'stor-foo', value: 's-f' }])
})
it('for one domain with both cookies and localStorage set', () => {
const sessionState = {
id: 'session1',
cookies: [
{ name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expiry: 123 },
],
localStorage: [
{ origin: 'localhost', value: { 'stor-foo': 's-f' } },
],
}
const consoleProps = getConsoleProps(sessionState)
expect(consoleProps.id).to.eq('session1')
expect(consoleProps.table).to.have.length(2)
let table = consoleProps.table[0]()
expect(table.name).to.contain('Cookies - localhost (1)')
expect(table.data).to.have.length(1)
expect(table.data).to.deep.eq(sessionState.cookies)
table = consoleProps.table[1]()
expect(table.name).to.contain('Storage - localhost (1)')
expect(table.data).to.have.length(1)
expect(table.data).to.deep.eq([{ key: 'stor-foo', value: 's-f' }])
})
it('for multiple domains', () => {
const sessionState = {
id: 'session1',
cookies: [
{ name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expiry: 123 },
{ name: 'bar', value: 'b', path: '/', domain: 'localhost', secure: false, httpOnly: false, expiry: 456 },
],
localStorage: [
{ origin: 'localhost', value: { 'stor-foo': 's-f' } },
{ origin: 'http://example.com', value: { 'random': 'hi' } },
],
}
const consoleProps = getConsoleProps(sessionState)
expect(consoleProps.id).to.eq('session1')
expect(consoleProps.table).to.have.length(3)
let table = consoleProps.table[0]()
expect(table.name).to.contain('Cookies - localhost (2)')
expect(table.data).to.have.length(2)
expect(table.data).to.deep.eq(sessionState.cookies)
table = consoleProps.table[1]()
expect(table.name).to.contain('Storage - localhost (1)')
expect(table.data).to.have.length(1)
expect(table.data).to.deep.eq([{ key: 'stor-foo', value: 's-f' }])
table = consoleProps.table[2]()
expect(table.name).to.contain('Storage - example.com (1)')
expect(table.data).to.have.length(1)
expect(table.data).to.deep.eq([{ key: 'random', value: 'hi' }])
})
})
describe('.navigateAboutBlank', () => {
it('triggers session blank page visit', () => {
const stub = cy.stub(Cypress, 'action').log(false)
.callThrough()
.withArgs('cy:visit:blank')
cy.then(() => {
navigateAboutBlank()
navigateAboutBlank(true)
expect(stub).to.have.been.calledTwice
expect(stub.args[0]).to.deep.eq(['cy:visit:blank', { type: 'session' }])
expect(stub.args[1]).to.deep.eq(['cy:visit:blank', { type: 'session' }])
})
})
it('triggers session-lifecycle blank page visit', () => {
const stub = cy.stub(Cypress, 'action').log(false)
.callThrough()
.withArgs('cy:visit:blank')
cy.then(() => {
navigateAboutBlank(false)
expect(stub).to.have.been.calledWith('cy:visit:blank', { type: 'session-lifecycle' })
})
})
})
})
@@ -53,7 +53,7 @@ describe('src/cy/commands/waiting', () => {
})
describe('alias argument', () => {
before(() => {
beforeEach(() => {
cy.visit('/fixtures/jquery.html')
})
@@ -750,7 +750,7 @@ describe('src/cy/commands/waiting', () => {
})
describe('multiple alias arguments', () => {
before(() => {
beforeEach(() => {
cy.visit('/fixtures/jquery.html')
})
@@ -776,7 +776,7 @@ describe('src/cy/commands/waiting', () => {
})
describe('multiple separate alias waits', () => {
before(() => {
beforeEach(() => {
cy.visit('/fixtures/jquery.html')
})
@@ -1022,6 +1022,8 @@ describe('src/cy/commands/waiting', () => {
done()
})
cy.visit('/fixtures/empty.html')
cy
.server()
.route(/foo/, {}).as('getFoo')
@@ -1042,6 +1044,10 @@ describe('src/cy/commands/waiting', () => {
})
describe('alias argument', () => {
beforeEach(() => {
cy.visit('/fixtures/empty.html')
})
it('is a parent command', () => {
cy
.server()
@@ -1133,6 +1139,10 @@ describe('src/cy/commands/waiting', () => {
})
describe('timeouts', function () {
beforeEach(() => {
cy.visit('/fixtures/empty.html')
})
it('sets default requestTimeout', {
requestTimeout: 199,
}, function (done) {
@@ -364,26 +364,16 @@ describe('src/cy/commands/window', () => {
})
context('#title', () => {
before(() => {
beforeEach(() => {
cy
.visit('/fixtures/generic.html')
.then(function (win) {
const h = $(win.document.head)
h.find('script').remove()
this.head = h.prop('outerHTML')
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.head).empty().html(this.head)
$(doc.body).empty().html(this.body)
})
it('returns the pages title as a string', () => {
const title = cy.$$('title').text()
+1 -11
View File
@@ -3,26 +3,16 @@ const { assertLogLength } = require('../../support/utils')
const { _, $, Promise } = Cypress
describe('src/cy/commands/xhr', () => {
before(() => {
beforeEach(function () {
cy
.visit('/fixtures/jquery.html')
.then(function (win) {
const h = $(win.document.head)
h.find('script').remove()
this.head = h.prop('outerHTML')
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.head).empty().html(this.head)
$(doc.body).empty().html(this.body)
})
context('#startXhrServer', () => {
it('continues to be a defined properties', () => {
cy
+8 -17
View File
@@ -32,28 +32,16 @@ describe('driver/src/cy/snapshots', () => {
})
context('snapshot el', () => {
before(() => {
beforeEach(() => {
cy
.visit('/fixtures/generic.html')
.then(function (win) {
const h = $(win.document.head)
h.find('script').remove()
this.head = h.prop('outerHTML')
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.head).empty().html(this.head)
$(doc.body).empty().html(this.body)
this.$el = $('<span id=\'snapshot\'>snapshot</span>').appendTo(cy.$$('body'))
})
it('does not clone scripts', function () {
$('<script type=\'text/javascript\' />').appendTo(cy.$$('body'))
@@ -95,17 +83,20 @@ describe('driver/src/cy/snapshots', () => {
})
it('sets data-cypress-el attr', function () {
const attr = cy.spy(this.$el, 'attr')
const $el = $('<span id=\'snapshot\'>snapshot</span>').appendTo(cy.$$('body'))
const attr = cy.spy($el, 'attr')
cy.createSnapshot(null, this.$el)
cy.createSnapshot(null, $el)
expect(attr).to.be.calledWith('data-cypress-el', 'true')
})
it('removes data-cypress-el attr', function () {
cy.createSnapshot(null, this.$el)
const $el = $('<span id=\'snapshot\'>snapshot</span>').appendTo(cy.$$('body'))
expect(this.$el.attr('data-cypress-el')).to.be.undefined
cy.createSnapshot(null, $el)
expect($el.attr('data-cypress-el')).to.be.undefined
})
// https://github.com/cypress-io/cypress/issues/8679
+1 -13
View File
@@ -1,20 +1,8 @@
const { $ } = Cypress
describe('driver/src/cypress/cy', () => {
let body
before(() => {
cy
.visit('/fixtures/dom.html')
.then((win) => {
body = win.document.body.outerHTML
})
})
beforeEach(() => {
const doc = cy.state('document')
$(doc.body).empty().html(body)
cy.visit('/fixtures/dom.html')
})
// https://github.com/cypress-io/cypress/issues/7731
+48 -1
View File
@@ -1,4 +1,4 @@
const { create } = require('@packages/driver/src/cypress/log')
const { create, LogUtils } = require('@packages/driver/src/cypress/log')
describe('src/cypress/log', function () {
context('#snapshot', function () {
@@ -54,4 +54,51 @@ describe('src/cypress/log', function () {
expect(result).to.equal(log)
})
})
context('countLogsByTests', () => {
it('returns zero if tests is empty', () => {
const tests = {}
expect(LogUtils.countLogsByTests(tests)).to.equal(0)
})
it('finds the highest id amongst the different types', () => {
const tests = {
a: {
agents: [{
id: 'log-idp.com-1',
}],
routes: [{
id: 'log-idp.com-2',
}],
commands: [{
id: 'log-idp.com-3',
}],
prevAttempts: [{
agents: [{
id: 'log-idp.com-4',
}],
routes: [{
id: 'log-idp.com-5',
}],
commands: [{
id: 'log-idp.com-6',
}],
}],
},
}
expect(LogUtils.countLogsByTests(tests)).to.equal(6)
})
it('returns zero if there are no agents, routes, or commands', () => {
const tests = {
a: {
notAThing: true,
},
}
expect(LogUtils.countLogsByTests(tests)).to.equal(0)
})
})
})
@@ -104,6 +104,8 @@ describe('Proxy Logging', () => {
// @see https://github.com/cypress-io/cypress/issues/18757 and https://github.com/cypress-io/cypress/issues/17656
it('xhr log has response body/status code when xhr response is logged first', (done) => {
cy.visit('/fixtures/empty.html')
cy.window()
.then({ timeout: 10000 }, (win) => {
cy.on('log:changed', (log) => {
@@ -149,6 +151,8 @@ describe('Proxy Logging', () => {
// @see https://github.com/cypress-io/cypress/issues/18757 and https://github.com/cypress-io/cypress/issues/17656
it('xhr log has response body/status code when xhr response is logged second', (done) => {
cy.visit('/fixtures/empty.html')
cy.window()
.then({ timeout: 10000 }, (win) => {
cy.on('log:changed', (log) => {
@@ -460,6 +464,7 @@ describe('Proxy Logging', () => {
}
beforeEach(() => {
cy.visit('/fixtures/empty.html')
cy.window()
.then(({ XMLHttpRequest }) => {
$XMLHttpRequest = XMLHttpRequest
@@ -1,25 +1,16 @@
import $coordinates from '../../../src/dom/coordinates'
import $elements from '../../../src/dom/elements'
const { $ } = Cypress
export {}
describe('src/dom/coordinates', () => {
let doc: Document
before(() => {
return cy
.visit('/fixtures/generic.html')
.then(function (win) {
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
doc = cy.state('document')
$(doc.body).empty().html(this.body)
this.$button = $('<button style=\'position: absolute; top: 25px; left: 50px; width: 100px; line-height: 50px; padding: 10px; margin: 10px; border: 10px solid black\'>foo</button>')
.appendTo(cy.$$('body'))
cy.visit('/fixtures/generic.html').then(() => {
this.$button = $('<button style=\'position: absolute; top: 25px; left: 50px; width: 100px; line-height: 50px; padding: 10px; margin: 10px; border: 10px solid black\'>foo</button>')
.appendTo(cy.$$('body'))
})
})
// this is necessary so that document.elementFromPoint
@@ -107,17 +98,17 @@ describe('src/dom/coordinates', () => {
context('.getElementAtPointFromViewport', () => {
it('returns same element based on x/y coords', function () {
expect(Cypress.dom.getElementAtPointFromViewport(doc, 100, 60)).to.eq(this.$button.get(0))
expect(Cypress.dom.getElementAtPointFromViewport(cy.state('document'), 100, 60)).to.eq(this.$button.get(0))
})
it('does not return if element is hidden', function () {
this.$button.hide()
expect(Cypress.dom.getElementAtPointFromViewport(doc, 100, 60)).not.to.eq(this.$button.get(0))
expect(Cypress.dom.getElementAtPointFromViewport(cy.state('document'), 100, 60)).not.to.eq(this.$button.get(0))
})
it('returns null if no element was found', function () {
expect(Cypress.dom.getElementAtPointFromViewport(doc, 1e9, 1e9)).to.be.null
expect(Cypress.dom.getElementAtPointFromViewport(cy.state('document'), 1e9, 1e9)).to.be.null
})
})
@@ -233,6 +224,51 @@ describe('src/dom/coordinates', () => {
})
})
context('isAUTFrame', () => {
const { isAUTFrame } = $coordinates
// our test for a window is that it has a `window` that refers
// to itself
const getWindowLikeObject = () => {
const win = { parent: {} as any }
win.parent.window = win.parent
return win
}
it('returns true if parent is a window and not an iframe', () => {
const win = cy.state('window')
expect(isAUTFrame(win)).to.be.true
})
it('returns true if parent is a window and getting its frameElement property throws a cross-origin error', () => {
const win = getWindowLikeObject()
const err = new Error('cross-origin error')
err.name = 'SecurityError'
cy.stub($elements, 'getNativeProp').throws(err)
expect(isAUTFrame(win)).to.be.true
})
it('returns false if parent is not a window', () => {
const win = { parent: {} }
expect(isAUTFrame(win)).to.be.false
})
it('returns false if parent is an iframe', () => {
const win = getWindowLikeObject()
cy.stub($elements, 'getNativeProp').returns(true)
expect(isAUTFrame(win)).to.be.false
})
})
context('span spanning multiple lines', () => {
it('gets first dom rect in multiline text', () => {
$(`\
+20 -15
View File
@@ -135,38 +135,43 @@ describe('src/dom/elements', () => {
})
it('DOM element does not exist', () => {
cy.visit('/fixtures/dom.html')
const $el = $('foo-bar-baz')
cy.visit('/fixtures/dom.html').then(() => {
const $el = $('foo-bar-baz')
expect(Cypress.dom.isUndefinedOrHTMLBodyDoc($el)).to.be.true
expect(Cypress.dom.isUndefinedOrHTMLBodyDoc($el)).to.be.true
})
})
it('when html', () => {
cy.visit('/fixtures/dom.html')
const $el = $('html')
cy.visit('/fixtures/dom.html').then(() => {
const $el = $('html')
expect(Cypress.dom.isUndefinedOrHTMLBodyDoc($el)).to.be.true
expect(Cypress.dom.isUndefinedOrHTMLBodyDoc($el)).to.be.true
})
})
it('when body', () => {
cy.visit('/fixtures/dom.html')
const $el = $('body')
cy.visit('/fixtures/dom.html').then(() => {
const $el = $('body')
expect(Cypress.dom.isUndefinedOrHTMLBodyDoc($el)).to.be.true
expect(Cypress.dom.isUndefinedOrHTMLBodyDoc($el)).to.be.true
})
})
it('when document', () => {
cy.visit('/fixtures/dom.html')
const $el = $('document')
cy.visit('/fixtures/dom.html').then(() => {
const $el = $('document')
expect(Cypress.dom.isUndefinedOrHTMLBodyDoc($el)).to.be.true
expect(Cypress.dom.isUndefinedOrHTMLBodyDoc($el)).to.be.true
})
})
it('when existing DOM element', () => {
cy.visit('/fixtures/dom.html')
const $el = $('input')
cy.visit('/fixtures/dom.html').then(() => {
const $el = $('input')
expect(Cypress.dom.isUndefinedOrHTMLBodyDoc($el)).to.be.false
expect(Cypress.dom.isUndefinedOrHTMLBodyDoc($el)).to.be.false
})
})
})
@@ -0,0 +1,247 @@
describe('basic login', () => {
// Scenario, Token based auth. Visit site, redirect to IDP hosted on secondary origin, login and redirect back to site.
describe('visit primary first', () => {
it('logs in with idp redirect', () => {
cy.visit('/fixtures/auth/index.html') // Establishes primary origin
cy.get('[data-cy="login-idp"]').click() // Takes you to idp.com
cy.origin('http://idp.com:3500', () => {
cy.get('[data-cy="username"]').type('BJohnson')
cy.get('[data-cy="login"]').click()
})
// Verify that the user has logged in on localhost
cy.get('[data-cy="welcome"]') // Stability is false, this command is prevented from running until stability is achieved.
.invoke('text')
.should('equal', 'Welcome BJohnson')
})
// Scenario, Token based auth. Visit site, manually redirect to IDP hosted on secondary origin, login and redirect back to site.
it('does not redirect', () => {
cy.visit('/fixtures/auth/index.html') // Establishes primary origin
// Missing the call to go to idp.com
cy.window().then((win) => {
win.location.href = 'http://www.idp.com:3500/fixtures/auth/idp.html'
})
cy.origin('http://idp.com:3500', () => {
cy.get('[data-cy="username"]').type('FJohnson')
cy.get('[data-cy="login"]').click()
})
// Verify that the user has logged in on localhost
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome FJohnson')
})
it('visits foobar first', () => {
cy.visit('http://www.foobar.com:3500/fixtures/auth/index.html') // Establishes primary origin
cy.get('[data-cy="login-idp"]').click() // Takes you to idp.com
cy.origin('http://idp.com:3500', () => {
cy.get('[data-cy="username"]').type('BJohnson')
cy.get('[data-cy="login"]').click()
})
// Verify that the user has logged in on localhost
cy.get('[data-cy="welcome"]') // Stability is false, this command is prevented from running until stability is achieved.
.invoke('text')
.should('equal', 'Welcome BJohnson')
})
})
// Scenario, Token based auth. Visit IDP hosted on secondary origin, login and redirect back to site.
describe('visit secondary first', () => {
describe('How to determine primary origin', () => {
// NOTE: Enable to set the top origin to foobar before running the next test.
it.skip('reset top', () => {
cy.visit('http://www.foobar.com:3500/fixtures/auth/index.html')
})
// Primary established via base url
// TODO: baseUrl does not establish primary without a visit
it.skip('logs in with primary set via baseurl', { baseUrl: 'http://localhost:3500' }, () => {
cy.origin('http://idp.com:3500', () => { // primary origin is localhost
cy.visit('http://www.idp.com:3500/fixtures/auth/idp.html')
cy.get('[data-cy="username"]').type('FJohnson')
cy.get('[data-cy="login"]').click()
})
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome FJohnson')
})
it('reset top', () => {
cy.visit('http://www.foobar.com:3500/fixtures/auth/index.html')
})
it('logs in with primary set via visit', () => {
cy.visit('/fixtures/auth/index.html')
cy.origin('http://idp.com:3500', () => { // primary origin is localhost
cy.visit('http://www.idp.com:3500/fixtures/auth/idp.html')
cy.get('[data-cy="username"]').type('FJohnson')
cy.get('[data-cy="login"]').click()
})
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome FJohnson')
})
})
describe('session', () => {
// Custom login command that establishes a session
const login = (name) => {
cy.session(name, () => {
// Note, this assumes localhost is the primary origin, ideally we'd be able to specify this directly.
cy.origin('http://idp.com:3500', { args: name }, (name) => {
cy.visit('http://www.idp.com:3500/fixtures/auth/idp.html')
cy.get('[data-cy="username"]').type(name)
cy.get('[data-cy="login"]').click()
})
cy.url().should('contain', '/index.html')
}, {
validate: () => {
cy.window().then((win) => {
const cypressAuthToken = win.sessionStorage.getItem('cypressAuthToken')
return !!cypressAuthToken
})
},
})
}
// Scenario, Token based auth. Establish session using custom login command (login through IDP hosted on secondary origin), and verify to site.
it('establishes a session', () => {
login('BJohnson')
cy.visit('/fixtures/auth/index.html')
// Verify that the user has logged in on localhost
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome BJohnson')
})
// Scenario, Token based auth. use previously established session, and verify to site.
it('uses established session', () => {
login('BJohnson')
cy.visit('/fixtures/auth/index.html')
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome BJohnson')
})
})
// What we don't want them to do, but should still work
// Visit IDP first
it('logs in and runs the test in cy.origin', () => { // Setting the base url
cy.visit('http://www.idp.com:3500/fixtures/auth/idp.html') // Visit idp.com
cy.get('[data-cy="username"]').type('FJohnson')
cy.get('[data-cy="login"]').click()
cy.origin('http://localhost:3500', () => {
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome FJohnson')
})
})
})
})
describe('Multi-step Auth', () => {
// TODO: cy.origin does not work in cy.origin yet.
it.skip('final auth redirects back to localhost - nested', () => {
cy.visit('/fixtures/auth/index.html')
cy.get('[data-cy="login-with-approval"]').click() // takes you to foobar.com.../approval
cy.url() //fail
cy.origin('http://foobar.com:3500', () => { // Parent origin is localhost
cy.get('[data-cy="approve-orig"]').click() // takes you to idp.com
cy.origin('http://idp.com:3500', () => { // Parent origin is foobar.com
cy.get('[data-cy="username"]').type('MarkyMark')
cy.get('[data-cy="login"]').click() // Takes you back to localhost
}) // Does not wait on foobar.com because there are no subsequent commands (would wait forever)
}) // Waits on localhost because there are subsequent commands
// Verify that the user has logged in
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome MarkyMark')
})
it('final-auth redirects back to localhost - flat', () => {
cy.visit('/fixtures/auth/index.html')
cy.get('[data-cy="login-with-approval"]').click() // takes you to foobar.com.../approval
cy.origin('http://foobar.com:3500', () => { // Parent origin is localhost
cy.get('[data-cy="approve-orig"]').click() // takes you to idp.com
}) // Exits and moves on to the next command
cy.origin('http://idp.com:3500', () => { // Parent origin is localhost
cy.get('[data-cy="username"]').type('MarkyMark')
cy.get('[data-cy="login"]').click() // Takes you back to localhost
}) // Exits and moves on to the next command
// Verify that the user has logged in
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome MarkyMark')
})
// TODO: cy.origin does not work in cy.origin yet.
it.skip('final auth redirects back to localhost - nested - approval first', () => {
cy.origin('http://foobar.com:3500', () => { // parent origin is localhost
cy.visit('http://www.foobar.com:3500/fixtures/auth/approval.html')
cy.get('[data-cy="approve-orig"]').click() // takes you to idp.com
cy.origin('http://idp.com:3500', () => { // parent origin is foobar.com
cy.get('[data-cy="username"]').type('MarkyMark')
cy.get('[data-cy="login"]').click() // Takes you back to localhost
}) // Does not wait on foobar.com because there are no subsequent commands (would wait forever)
}) // Exits and moves on to the next command
// Verify that the user has logged in
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome MarkyMark')
})
// TODO: Switch to origin does not work in switch to origin yet.
it.skip('final auth redirects back to approval page - nested', () => {
cy.visit('/fixtures/auth/index.html')
cy.get('[data-cy="login-with-approval"]').click() // takes you to foobar.com.../approval
cy.origin('http://foobar.com:3500', () => { // parent origin is localhost
cy.get('[data-cy="approve-me"]').click() // takes you to idp.com
cy.origin('http://idp.com:3500', () => { // parent origin is foobar.com
cy.get('[data-cy="username"]').type('MarkyMark')
cy.get('[data-cy="login"]').click() // Takes you back to foobar.com.../approval
}) // Exits and moves on to the next command
cy.get('[data-cy="login-success"]').click() // Takes you back to localhost
}) // Exits and moves on to the next command
// Verify that the user has logged in
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome MarkyMark')
})
it('final auth redirects back to approval page - flat', () => {
cy.visit('/fixtures/auth/index.html')
cy.get('[data-cy="login-with-approval"]').click() // takes you to foobar.com.../approval
cy.origin('http://foobar.com:3500', () => { // parent origin is localhost
cy.get('[data-cy="approve-me"]').click() // takes you to idp.com
}) // waits on localhost forever, this breaks
cy.origin('http://idp.com:3500', () => { // parent origin is localhost
cy.get('[data-cy="username"]').type('MarkyMark')
cy.get('[data-cy="login"]').click() // Takes you back to foobar.com.../approval
}) // Exits and moves on to the next command
cy.origin('http://foobar.com:3500', () => { // parent origin is localhost
cy.get('[data-cy="login-success"]').click() // Takes you back to localhost
}) // Exits and moves on to the next command
// Verify that the user has logged in
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome MarkyMark')
})
})
@@ -0,0 +1,809 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin actions', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
})
it('.type()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#input').type('foo')
.should('have.value', 'foo')
})
})
it('.focus()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#input').focus()
.should('be.focused')
})
})
it('.blur()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#input').type('foo').blur()
.should('not.be.focused')
})
})
it('.clear()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#input')
.type('foo').should('have.value', 'foo')
.clear().should('have.value', '')
})
})
it('.submit()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
const afterFormSubmitted = new Promise<void>((resolve) => {
cy.once('form:submitted', resolve)
})
cy.get('#input-type-submit').submit()
cy.wrap(afterFormSubmitted)
})
})
it('.click()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#button').then(($btn) => {
const onClick = new Promise<void>((resolve) => {
$btn.on('click', () => resolve())
})
cy.wrap($btn).click()
cy.wrap(onClick)
})
})
})
it('.dblclick()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#button').then(($btn) => {
const afterDblClick = new Promise<void>((resolve) => {
$btn.on('dblclick', () => resolve())
})
cy.wrap($btn).dblclick()
cy.wrap(afterDblClick)
})
})
})
it('.rightclick()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#button').then(($btn) => {
const afterContextmenu = new Promise<void>((resolve) => {
$btn.on('contextmenu', () => resolve())
})
cy.wrap($btn).rightclick()
cy.wrap(afterContextmenu)
})
})
})
it('.check()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get(':checkbox[name="colors"][value="blue"]')
.check().should('be.checked')
})
})
it('.uncheck()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get(':checkbox[name="colors"][value="blue"]')
.check().should('be.checked')
.uncheck().should('not.be.checked')
})
})
it('.select()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('select[name="foods"]')
.select('Japanese').should('have.value', 'Japanese')
})
})
it('.scrollIntoView()', () => {
cy.get('a[data-cy="scrolling-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#scroll-into-view-vertical h5')
.should('not.be.visible')
.scrollIntoView().should('be.visible')
})
})
it('.scrollTo()', () => {
cy.get('a[data-cy="scrolling-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#scroll-into-view-vertical h5').should('not.be.visible')
cy.get('#scroll-into-view-vertical').scrollTo(0, 300)
cy.get('#scroll-into-view-vertical h5').should('be.visible')
})
})
it('.trigger()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#button').then(($btn) => {
const afterClick = new Promise<void>((resolve) => {
$btn.on('click', () => resolve())
})
cy.wrap($btn).trigger('click')
cy.wrap(afterClick)
})
})
})
it('.selectFile()', () => {
cy.get('a[data-cy="files-form-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.wrap(Cypress.Buffer.from('foo')).as('foo')
cy.get('#basic')
.selectFile({ contents: '@foo', fileName: 'foo.txt' })
.should(($input) => {
const input = $input[0] as HTMLInputElement
const file = input!.files![0]
expect(file.name).to.equal('foo.txt')
return file.arrayBuffer()
.then((arrayBuffer) => {
const decoder = new TextDecoder('utf8')
const contents = decoder.decode(arrayBuffer)
expect(contents).to.equal('foo')
})
})
})
})
context('#consoleProps', () => {
const { _ } = Cypress
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.get()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#button')
})
cy.shouldWithTimeout(() => {
const { consoleProps, $el } = findCrossOriginLogs('get', logs, 'foobar.com')
// make sure $el is in fact a jquery instance to keep the logs happy
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('get')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Selector).to.equal('#button')
// The Yielded value here SHOULD be correct as it will be reified from its props as it should not be found in the current DOM state
expect(consoleProps.Yielded.tagName).to.equal('BUTTON')
expect(consoleProps.Yielded.getAttribute('id')).to.equal('button')
})
})
it('.alias()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#button').as('buttonAlias')
})
cy.shouldWithTimeout(() => {
const { alias, aliasType, consoleProps, $el } = findCrossOriginLogs('get', logs, 'foobar.com')
// make sure $el is in fact a jquery instance to keep the logs happy
expect($el.jquery).to.be.ok
expect(alias).to.equal('buttonAlias')
expect(aliasType).to.equal('dom')
expect(consoleProps.Command).to.equal('get')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Selector).to.equal('#button')
// The Yielded value here SHOULD be correct as it will be reified from its props as it should not be found in the current DOM state
expect(consoleProps.Yielded.tagName).to.equal('BUTTON')
expect(consoleProps.Yielded.getAttribute('id')).to.equal('button')
})
})
it('.click()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#button-inside-a').click()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { alias, aliasType, consoleProps, $el } = findCrossOriginLogs('click', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(alias).to.equal(undefined)
expect(aliasType).to.equal(undefined)
expect(consoleProps.Command).to.equal('click')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Coords).to.have.property('x').that.is.a('number')
expect(consoleProps.Coords).to.have.property('y').that.is.a('number')
expect(consoleProps.Selector).to.be.undefined
expect(consoleProps.Yielded).to.be.undefined
expect(consoleProps.Options).to.be.undefined
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('FORM')
// TODO: test class list serialization
// expect(consoleProps['Applied To']).to.have.property('classList').that.contains(['my-custom-button-css', 'class2', '@class3'])
expect(consoleProps['Applied To'].getAttribute('id')).to.contain('button-inside-a')
expect(consoleProps['Applied To'].innerHTML).to.contain('<span>click button</span>')
expect(consoleProps.table[1]).to.be.a('function')
const tableContents = consoleProps.table[1]()
expect(tableContents.name).to.equal('Mouse Events')
// can't exactly assert that is an Array because it is a Proxy object to an array
expect(tableContents.data).to.be.a('object')
tableContents.data.forEach((datum) => {
expect(datum).to.have.property('Active Modifiers').that.equals(null)
expect(datum).to.have.property('Event Type').that.is.oneOf(['pointerover', 'mouseover', 'pointermove', 'mousemove', 'pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'])
expect(datum).to.have.property('Prevented Default').that.equals(null)
expect(datum).to.have.property('Stopped Propagation').that.equals(null)
expect(datum).to.have.property('Target Element').that.deep.equals(consoleProps['Applied To'])
})
})
})
it('.dblclick()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#button').dblclick()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps, $el } = findCrossOriginLogs('dblclick', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('dblclick')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Coords).to.have.property('x').that.is.a('number')
expect(consoleProps.Coords).to.have.property('y').that.is.a('number')
expect(consoleProps.Selector).to.be.undefined
expect(consoleProps.Yielded).to.be.undefined
expect(consoleProps.Options).to.be.undefined
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('BUTTON')
expect(consoleProps['Applied To'].getAttribute('id')).to.contain('button')
expect(consoleProps['Applied To'].innerHTML).to.contain('button')
expect(consoleProps.table[1]).to.be.a('function')
const tableContents = consoleProps.table[1]()
expect(tableContents.name).to.equal('Mouse Events')
// can't exactly assert that is an Array because it is a Proxy object to an array
expect(tableContents.data).to.be.a('object')
tableContents.data.forEach((datum) => {
expect(datum).to.have.property('Active Modifiers').that.equals(null)
expect(datum).to.have.property('Event Type').that.is.oneOf(['pointerover', 'mouseover', 'pointermove', 'mousemove', 'pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click', 'dblclick'])
expect(datum).to.have.property('Prevented Default').that.equals(null)
expect(datum).to.have.property('Stopped Propagation').that.equals(null)
expect(datum).to.have.property('Target Element').that.deep.equals(consoleProps['Applied To'])
})
})
})
it('.rightclick()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#button').rightclick()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps, $el } = findCrossOriginLogs('rightclick', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('rightclick')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Coords).to.have.property('x').that.is.a('number')
expect(consoleProps.Coords).to.have.property('y').that.is.a('number')
expect(consoleProps.Selector).to.be.undefined
expect(consoleProps.Yielded).to.be.undefined
expect(consoleProps.Options).to.be.undefined
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('BUTTON')
expect(consoleProps['Applied To'].getAttribute('id')).to.contain('button')
expect(consoleProps['Applied To'].innerHTML).to.contain('button')
expect(consoleProps.table[1]).to.be.a('function')
const tableContents = consoleProps.table[1]()
expect(tableContents.name).to.equal('Mouse Events')
// can't exactly assert that is an Array because it is a Proxy object to an array
expect(tableContents.data).to.be.a('object')
tableContents.data.forEach((datum) => {
expect(datum).to.have.property('Active Modifiers').that.equals(null)
expect(datum).to.have.property('Event Type').that.is.oneOf(['pointerover', 'mouseover', 'pointermove', 'mousemove', 'pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click', 'contextmenu'])
expect(datum).to.have.property('Prevented Default').that.equals(null)
expect(datum).to.have.property('Stopped Propagation').that.equals(null)
expect(datum).to.have.property('Target Element').that.deep.equals(consoleProps['Applied To'])
})
})
})
it('.type()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('input#input').type('foo')
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { alias, aliasType, consoleProps, $el } = findCrossOriginLogs('type', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(alias).to.equal(undefined)
expect(aliasType).to.equal(undefined)
expect(consoleProps.Command).to.equal('type')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Coords).to.have.property('x').that.is.a('number')
expect(consoleProps.Coords).to.have.property('y').that.is.a('number')
expect(consoleProps.Typed).to.equal('foo')
expect(consoleProps.Options).to.be.undefined
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To'].getAttribute('id')).to.equal('input')
expect(consoleProps['Applied To'].innerHTML).to.equals('')
expect(consoleProps['Applied To'].type).to.equal('text')
expect(consoleProps.table[1]).to.be.a('function')
expect(consoleProps.table[2]).to.be.a('function')
const mouseEventsTable = consoleProps.table[1]()
const KeyboardEventsTable = consoleProps.table[2]()
expect(mouseEventsTable.name).to.equal('Mouse Events')
expect(KeyboardEventsTable.name).to.equal('Keyboard Events')
// can't exactly assert that is an Array because it is a Proxy object to an array
expect(mouseEventsTable.data).to.be.a('object')
expect(KeyboardEventsTable.data).to.be.a('object')
_.forEach(mouseEventsTable.data, (datum) => {
expect(datum).to.have.property('Active Modifiers').that.equals(null)
expect(datum).to.have.property('Event Type').that.is.oneOf(['pointerover', 'mouseover', 'pointermove', 'mousemove', 'pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'])
expect(datum).to.have.property('Prevented Default').that.equals(null)
expect(datum).to.have.property('Stopped Propagation').that.equals(null)
expect(datum).to.have.property('Target Element').that.deep.equals(consoleProps['Applied To'])
})
_.forEach(KeyboardEventsTable.data, (datum) => {
expect(datum).to.have.property('Active Modifiers').that.equals(null)
expect(datum).to.have.property('Events Fired').that.equals('keydown, keypress, beforeinput, textInput, input, keyup')
expect(datum).to.have.property('Prevented Default').that.equals(null)
expect(datum).to.have.property('Target Element').that.deep.equals(consoleProps['Applied To'])
})
expect(KeyboardEventsTable.data[1]).to.have.property('Details').that.equals('{ code: KeyF, which: 70 }')
expect(KeyboardEventsTable.data[1]).to.have.property('Typed').that.equals('f')
expect(KeyboardEventsTable.data[2]).to.have.property('Details').that.equals('{ code: KeyO, which: 79 }')
expect(KeyboardEventsTable.data[2]).to.have.property('Typed').that.equals('o')
expect(KeyboardEventsTable.data[3]).to.have.property('Details').that.equals('{ code: KeyO, which: 79 }')
expect(KeyboardEventsTable.data[3]).to.have.property('Typed').that.equals('o')
})
})
it('.submit()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('form#multiple-inputs-and-input-submit input[name="fname"]').type('foo')
cy.get('form#multiple-inputs-and-input-submit input[name="lname"]').type('bar')
cy.get('form#multiple-inputs-and-input-submit').submit()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps, $el } = findCrossOriginLogs('submit', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('submit')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('FORM')
expect(consoleProps['Applied To'].getAttribute('id')).to.equal('multiple-inputs-and-input-submit')
expect(consoleProps['Applied To'].querySelector('input[name="fname"]')).to.be.ok
expect(consoleProps['Applied To'].querySelector('input[name="lname"]')).to.be.ok
expect(consoleProps['Applied To'].querySelector('input[type="submit"]')).to.be.ok
// make sure input values are passed along into the serialized snapshot/element
expect(consoleProps['Applied To'].querySelector('input[name="fname"]').value).to.equal('foo')
expect(consoleProps['Applied To'].querySelector('input[name="lname"]').value).to.equal('bar')
expect(consoleProps['Applied To'].querySelector('input[type="submit"]').value).to.equal('submit me')
})
})
it('.focus()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#input').focus()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps, $el } = findCrossOriginLogs('focus', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('focus')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To'].getAttribute('id')).to.equal('input')
})
})
it('.blur()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
// FIXME: snapshot shows the primary domain (before type). Should be secondary
cy.get('#input').type('foo').blur()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps, $el } = findCrossOriginLogs('blur', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('blur')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To'].getAttribute('id')).to.equal('input')
})
})
it('.clear()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
// FIXME: snapshot shows the primary domain. Should be secondary
cy.get('#input').type('foo').clear()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps, $el } = findCrossOriginLogs('clear', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('clear')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Options).to.be.undefined
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To'].getAttribute('id')).to.equal('input')
})
})
it('.check()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get(':checkbox[name="colors"][value="blue"]').check()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps, $el } = findCrossOriginLogs('check', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('check')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Coords).to.have.property('x').that.is.a('number')
expect(consoleProps.Coords).to.have.property('y').that.is.a('number')
expect(consoleProps.Options).to.be.undefined
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.table[1]).to.be.a('function')
const tableContents = consoleProps.table[1]()
expect(tableContents.name).to.equal('Mouse Events')
// can't exactly assert that is an Array because it is a Proxy object to an array
expect(tableContents.data).to.be.a('object')
tableContents.data.forEach((datum) => {
expect(datum).to.have.property('Active Modifiers').that.equals(null)
expect(datum).to.have.property('Event Type').that.is.oneOf(['pointerover', 'mouseover', 'pointermove', 'mousemove', 'pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click', 'contextmenu'])
expect(datum).to.have.property('Prevented Default').that.equals(null)
expect(datum).to.have.property('Stopped Propagation').that.equals(null)
expect(datum).to.have.property('Target Element').that.deep.equals(consoleProps['Applied To'])
})
})
})
it('.uncheck()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get(':checkbox[name="colors"][value="blue"]')
.check().uncheck()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps, $el } = findCrossOriginLogs('uncheck', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('uncheck')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Coords).to.have.property('x').that.is.a('number')
expect(consoleProps.Coords).to.have.property('y').that.is.a('number')
expect(consoleProps.Options).to.be.undefined
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.table[1]).to.be.a('function')
const tableContents = consoleProps.table[1]()
expect(tableContents.name).to.equal('Mouse Events')
// can't exactly assert that is an Array because it is a Proxy object to an array
expect(tableContents.data).to.be.a('object')
tableContents.data.forEach((datum) => {
expect(datum).to.have.property('Active Modifiers').that.equals(null)
expect(datum).to.have.property('Event Type').that.is.oneOf(['pointerover', 'mouseover', 'pointermove', 'mousemove', 'pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click', 'contextmenu'])
expect(datum).to.have.property('Prevented Default').that.equals(null)
expect(datum).to.have.property('Stopped Propagation').that.equals(null)
expect(datum).to.have.property('Target Element').that.deep.equals(consoleProps['Applied To'])
})
})
})
it('.select()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
// TODO: wrong selected value is displayed in the snapshot after
cy.get('select[name="foods"]').select('Japanese')
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps, $el } = findCrossOriginLogs('select', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('select')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Coords).to.have.property('x').that.is.a('number')
expect(consoleProps.Coords).to.have.property('y').that.is.a('number')
expect(consoleProps.Options).to.be.undefined
expect(consoleProps.Selected[0]).to.equal('Japanese')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('SELECT')
expect(consoleProps.table[1]).to.be.a('function')
const tableContents = consoleProps.table[1]()
expect(tableContents.name).to.equal('Mouse Events')
// can't exactly assert that is an Array because it is a Proxy object to an array
expect(tableContents.data).to.be.a('object')
tableContents.data.forEach((datum) => {
expect(datum).to.have.property('Active Modifiers').that.equals(null)
expect(datum).to.have.property('Event Type').that.is.oneOf(['pointerover', 'mouseover', 'pointermove', 'mousemove', 'pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click', 'contextmenu'])
expect(datum).to.have.property('Prevented Default').that.equals(null)
expect(datum).to.have.property('Stopped Propagation').that.equals(null)
expect(datum).to.have.property('Target Element').that.deep.equals(consoleProps['Applied To'])
})
})
})
it('.scrollIntoView()', () => {
cy.get('a[data-cy="scrolling-link"]').click()
cy.origin('http://foobar.com:3500', () => {
// FIXME: snapshot of primary is showing for scrollIntoView
cy.get('#scroll-into-view-vertical h5')
.should('not.be.visible')
.scrollIntoView()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps, $el } = findCrossOriginLogs('scrollIntoView', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('scrollIntoView')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('H5')
expect(consoleProps['Scrolled Element']).to.have.property('tagName').that.equals('H5')
})
})
it('.scrollTo()', () => {
cy.get('a[data-cy="scrolling-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#scroll-into-view-vertical h5').should('not.be.visible')
cy.get('#scroll-into-view-vertical').scrollTo(0, 300)
})
cy.shouldWithTimeout(() => {
const { consoleProps, $el } = findCrossOriginLogs('scrollTo', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('scrollTo')
expect(consoleProps.X).to.equal(0)
expect(consoleProps.Y).to.equal(300)
expect(consoleProps['Scrolled Element']).to.have.property('tagName').that.equals('DIV')
})
})
it('.trigger()', () => {
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#button').trigger('click')
})
cy.shouldWithTimeout(() => {
const { consoleProps, $el } = findCrossOriginLogs('trigger', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('trigger')
expect(consoleProps['Event options']).to.have.property('bubbles').that.is.a('boolean')
expect(consoleProps['Event options']).to.have.property('cancelable').that.is.a('boolean')
expect(consoleProps['Event options']).to.have.property('clientX').that.is.a('number')
expect(consoleProps['Event options']).to.have.property('clientY').that.is.a('number')
expect(consoleProps['Event options']).to.have.property('pageX').that.is.a('number')
expect(consoleProps['Event options']).to.have.property('pageY').that.is.a('number')
expect(consoleProps['Event options']).to.have.property('screenX').that.is.a('number')
expect(consoleProps['Event options']).to.have.property('screenY').that.is.a('number')
expect(consoleProps.Yielded[0]).to.have.property('tagName').that.equals('BUTTON')
})
})
it('.selectFile()', () => {
cy.get('a[data-cy="files-form-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('#basic').selectFile({ contents: Cypress.Buffer.from('foo'), fileName: 'foo.txt' })
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps, $el } = findCrossOriginLogs('selectFile', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('selectFile')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Target).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Target).to.have.property('id').that.equals('basic')
})
})
})
})
@@ -0,0 +1,50 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin aliasing', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="dom-link"]').click()
})
it('.as()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get(':checkbox[name="colors"][value="blue"]').as('checkbox')
cy.get('@checkbox').click().should('be.checked')
})
})
context('#consoleProps', () => {
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.as()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#button').as('buttonAlias')
})
cy.shouldWithTimeout(() => {
const { alias, aliasType, consoleProps, $el } = findCrossOriginLogs('get', logs, 'foobar.com')
// make sure $el is in fact a jquery instance to keep the logs happy
expect($el.jquery).to.be.ok
expect(alias).to.equal('buttonAlias')
expect(aliasType).to.equal('dom')
expect(consoleProps.Command).to.equal('get')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Selector).to.equal('#button')
// The Yielded value here SHOULD be correct as it will be reified from its props as it should not be found in the current DOM state
expect(consoleProps.Yielded.tagName).to.equal('BUTTON')
expect(consoleProps.Yielded.getAttribute('id')).to.equal('button')
})
})
})
})
@@ -0,0 +1,56 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin assertions', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="dom-link"]').click()
})
it('.should() and .and()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get(':checkbox[name="colors"][value="blue"]')
.should('not.be.checked').and('not.be.disabled')
})
})
context('#consoleProps', () => {
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.should() and .and()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get(':checkbox[name="colors"][value="blue"]')
.should('not.be.checked').and('not.be.disabled')
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const assertionLogs = findCrossOriginLogs('assert', logs, 'foobar.com')
expect(assertionLogs[0].consoleProps.Message).to.equal('expected <input> not to be checked')
expect(assertionLogs[1].consoleProps.Message).to.equal('expected <input> not to be disabled')
assertionLogs.forEach(({ $el, consoleProps }) => {
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('assert')
expect(consoleProps.subject[0]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.subject[0]).to.have.property('value').that.equals('blue')
expect(consoleProps.subject[0].getAttribute('name')).to.equal('colors')
})
})
})
})
})
@@ -0,0 +1,119 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin connectors', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="dom-link"]').click()
})
it('.each()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-name>[name="colors"]').each(($element, index) => {
expect($element.prop('type')).to.equal('checkbox')
})
})
})
it('.its()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id>input').its('length').should('eq', 3)
})
})
it('.invoke()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#button').invoke('text').should('eq', 'button')
})
})
it('.spread()', () => {
cy.origin('http://foobar.com:3500', () => {
const arr = ['foo', 'bar', 'baz']
cy.wrap(arr).spread((foo, bar, baz) => {
expect(foo).to.equal('foo')
expect(bar).to.equal('bar')
expect(baz).to.equal('baz')
})
})
})
it('.then()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id>input').then(($list) => {
expect($list).to.have.length(3)
})
})
})
context('#consoleProps', () => {
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.its()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id>input').its('length')
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps, $el } = findCrossOriginLogs('its', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('its')
expect(consoleProps.Property).to.equal('.length')
expect(consoleProps.Yielded).to.equal(3)
expect(consoleProps.Subject.length).to.equal(3)
// make sure subject elements are indexed in the correct order
expect(consoleProps.Subject[0]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Subject[0]).to.have.property('id').that.equals('input')
expect(consoleProps.Subject[1]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Subject[1]).to.have.property('id').that.equals('name')
expect(consoleProps.Subject[2]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Subject[2]).to.have.property('id').that.equals('age')
})
})
it('.invoke()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#button').invoke('text')
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps, $el } = findCrossOriginLogs('invoke', logs, 'foobar.com')
expect($el.jquery).to.be.ok
expect(consoleProps.Command).to.equal('invoke')
expect(consoleProps.Function).to.equal('.text()')
expect(consoleProps.Yielded).to.equal('button')
expect(consoleProps.Subject).to.have.property('tagName').that.equals('BUTTON')
expect(consoleProps.Subject).to.have.property('id').that.equals('button')
})
})
})
})
@@ -0,0 +1,177 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin cookies', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
it('.getCookie(), .getCookies(), and .setCookie()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.getCookies().should('be.empty')
cy.setCookie('foo', 'bar')
cy.getCookie('foo').should('have.property', 'value', 'bar')
cy.getCookies().should('have.length', 1)
})
})
it('.clearCookie()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.setCookie('foo', 'bar')
cy.getCookie('foo').should('not.be.null')
cy.clearCookie('foo')
cy.getCookie('foo').should('be.null')
})
})
it('.clearCookies()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.setCookie('foo', 'bar')
cy.setCookie('faz', 'baz')
cy.getCookies().should('have.length', 2)
cy.clearCookies()
cy.getCookies().should('be.empty')
})
})
context('#consoleProps', () => {
const { _ } = Cypress
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.getCookie()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.getCookies().should('be.empty')
cy.setCookie('foo', 'bar')
cy.getCookie('foo')
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('getCookie', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('getCookie')
expect(consoleProps.Yielded).to.have.property('domain').that.includes('foobar.com')
expect(consoleProps.Yielded).to.have.property('expiry').that.is.a('number')
expect(consoleProps.Yielded).to.have.property('httpOnly').that.equals(false)
expect(consoleProps.Yielded).to.have.property('secure').that.equals(false)
expect(consoleProps.Yielded).to.have.property('name').that.equals('foo')
expect(consoleProps.Yielded).to.have.property('value').that.equals('bar')
expect(consoleProps.Yielded).to.have.property('path').that.is.a('string')
})
})
it('.getCookies()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.getCookies().should('be.empty')
cy.setCookie('foo', 'bar')
cy.getCookies()
})
cy.shouldWithTimeout(() => {
// get the last 'getCookies' command, which is the one we care about for this test
const allGetCookieLogs = findCrossOriginLogs('getCookies', logs, 'foobar.com')
const { consoleProps } = allGetCookieLogs.pop() as any
expect(consoleProps.Command).to.equal('getCookies')
expect(consoleProps['Num Cookies']).to.equal(1)
// can't exactly assert on length() as this is a array proxy object
expect(consoleProps.Yielded.length).to.equal(1)
expect(consoleProps.Yielded[0]).to.have.property('expiry').that.is.a('number')
expect(consoleProps.Yielded[0]).to.have.property('httpOnly').that.equals(false)
expect(consoleProps.Yielded[0]).to.have.property('secure').that.equals(false)
expect(consoleProps.Yielded[0]).to.have.property('name').that.equals('foo')
expect(consoleProps.Yielded[0]).to.have.property('value').that.equals('bar')
expect(consoleProps.Yielded[0]).to.have.property('path').that.is.a('string')
})
})
it('.setCookie()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.getCookies().should('be.empty')
cy.setCookie('foo', 'bar')
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('setCookie', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('setCookie')
expect(consoleProps.Yielded).to.have.property('domain').that.includes('foobar.com')
expect(consoleProps.Yielded).to.have.property('expiry').that.is.a('number')
expect(consoleProps.Yielded).to.have.property('httpOnly').that.equals(false)
expect(consoleProps.Yielded).to.have.property('secure').that.equals(false)
expect(consoleProps.Yielded).to.have.property('name').that.equals('foo')
expect(consoleProps.Yielded).to.have.property('value').that.equals('bar')
expect(consoleProps.Yielded).to.have.property('path').that.is.a('string')
})
})
it('.clearCookie()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.setCookie('foo', 'bar')
cy.getCookie('foo').should('not.be.null')
cy.clearCookie('foo')
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('clearCookie', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('clearCookie')
expect(consoleProps.Yielded).to.equal('null')
expect(consoleProps['Cleared Cookie']).to.have.property('domain').that.includes('foobar.com')
expect(consoleProps['Cleared Cookie']).to.have.property('expiry').that.is.a('number')
expect(consoleProps['Cleared Cookie']).to.have.property('httpOnly').that.equals(false)
expect(consoleProps['Cleared Cookie']).to.have.property('secure').that.equals(false)
expect(consoleProps['Cleared Cookie']).to.have.property('name').that.equals('foo')
expect(consoleProps['Cleared Cookie']).to.have.property('value').that.equals('bar')
expect(consoleProps['Cleared Cookie']).to.have.property('path').that.is.a('string')
})
})
it('.clearCookies()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.setCookie('foo', 'bar')
cy.setCookie('faz', 'baz')
cy.getCookies().should('have.length', 2)
cy.clearCookies()
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('clearCookies', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('clearCookies')
expect(consoleProps['Num Cookies']).to.equal(2)
expect(consoleProps.Yielded).to.equal('null')
expect(consoleProps['Cleared Cookies'].length).to.equal(2)
expect(consoleProps['Cleared Cookies'][0]).to.have.property('name').that.equals('foo')
expect(consoleProps['Cleared Cookies'][0]).to.have.property('value').that.equals('bar')
expect(consoleProps['Cleared Cookies'][1]).to.have.property('name').that.equals('faz')
expect(consoleProps['Cleared Cookies'][1]).to.have.property('value').that.equals('baz')
_.forEach(consoleProps['Cleared Cookies'], (clearedCookie) => {
expect(clearedCookie).to.have.property('httpOnly').that.equals(false)
expect(clearedCookie).to.have.property('secure').that.equals(false)
expect(clearedCookie).to.have.property('path').that.is.a('string')
})
})
})
})
})
@@ -0,0 +1,96 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin files', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
it('.fixture()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.fixture('example.json').then((json) => {
expect(json).to.be.an('object')
expect(json.example).to.be.true
})
})
})
it('.readFile()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.readFile('cypress/fixtures/example.json').then((json) => {
expect(json).to.be.an('object')
expect(json.example).to.be.true
})
})
})
it('.writeFile()', () => {
cy.origin('http://foobar.com:3500', () => {
const contents = JSON.stringify({ foo: 'bar' })
cy.stub(Cypress, 'backend').resolves({
contents,
filePath: 'foo.json',
})
cy.writeFile('foo.json', contents).then(() => {
expect(Cypress.backend).to.be.calledWith(
'write:file',
'foo.json',
contents,
{
encoding: 'utf8',
flag: 'w',
},
)
})
})
})
context('#consoleProps', () => {
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.readFile()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.readFile('cypress/fixtures/example.json')
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('readFile', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('readFile')
expect(consoleProps['File Path']).to.include('cypress/fixtures/example.json')
expect(consoleProps.Contents).to.deep.equal({ example: true })
})
})
it('.writeFile()', () => {
cy.origin('http://foobar.com:3500', () => {
const contents = JSON.stringify({ foo: 'bar' })
cy.stub(Cypress, 'backend').resolves({
contents,
filePath: 'foo.json',
})
cy.writeFile('foo.json', contents)
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('writeFile', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('writeFile')
expect(consoleProps['File Path']).to.equal('foo.json')
expect(consoleProps.Contents).to.equal('{"foo":"bar"}')
})
})
})
})
@@ -0,0 +1,52 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin local storage', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
it('.clearLocalStorage()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.window().then((win) => {
win.localStorage.setItem('foo', 'bar')
expect(win.localStorage.getItem('foo')).to.equal('bar')
})
cy.clearLocalStorage().should((localStorage) => {
expect(localStorage.length).to.equal(0)
expect(localStorage.getItem('foo')).to.be.null
})
})
})
context('#consoleProps', () => {
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.clearLocalStorage()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.window().then((win) => {
win.localStorage.setItem('foo', 'bar')
expect(win.localStorage.getItem('foo')).to.equal('bar')
})
cy.clearLocalStorage()
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('clearLocalStorage', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('clearLocalStorage')
expect(consoleProps.Yielded).to.be.null
})
})
})
})
@@ -0,0 +1,93 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin location', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
it('.hash()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.hash().should('be.empty')
})
})
it('.location()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.location().should((location) => {
expect(location.href).to.equal('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
expect(location.origin).to.equal('http://www.foobar.com:3500')
})
})
})
it('.url()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.url().should('equal', 'http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
})
})
context('#consoleProps', () => {
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.hash()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.hash()
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('hash', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('hash')
})
})
it('.location()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.location()
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('location', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('location')
expect(consoleProps.Yielded).to.have.property('auth').that.is.a('string')
expect(consoleProps.Yielded).to.have.property('authObj').that.is.undefined
expect(consoleProps.Yielded).to.have.property('hash').that.is.a('string')
expect(consoleProps.Yielded).to.have.property('host').that.is.a('string')
expect(consoleProps.Yielded).to.have.property('hostname').that.is.a('string')
expect(consoleProps.Yielded).to.have.property('href').that.is.a('string')
expect(consoleProps.Yielded).to.have.property('origin').that.is.a('string')
expect(consoleProps.Yielded).to.have.property('originPolicy').that.is.a('string')
expect(consoleProps.Yielded).to.have.property('pathname').that.is.a('string')
expect(consoleProps.Yielded).to.have.property('port').that.is.a('string')
expect(consoleProps.Yielded).to.have.property('protocol').that.is.a('string')
expect(consoleProps.Yielded).to.have.property('search').that.is.a('string')
expect(consoleProps.Yielded).to.have.property('superDomain').that.is.a('string')
})
})
it('.url()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.url()
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('url', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('url')
expect(consoleProps.Yielded).to.equal('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
})
})
})
})
@@ -0,0 +1,69 @@
import { assertLogLength } from '../../../../support/utils'
context('cy.origin log', () => {
let logs: any = []
let lastTestLogId = ''
beforeEach(() => {
logs = []
cy.on('log:added', (attrs, log) => {
logs.push(log)
})
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="dom-link"]').click()
})
it('logs in primary and secondary origins', () => {
cy.origin<string>('http://foobar.com:3500', () => {
const afterLogAdded = new Promise<void>((resolve) => {
const listener = (attrs) => {
if (attrs.message === 'test log in cy.origin') {
expect(attrs.message).to.eq('test log in cy.origin')
cy.removeListener('log:added', listener)
resolve(attrs.id)
}
}
cy.on('log:added', listener)
})
cy.log('test log in cy.origin')
cy.wrap(afterLogAdded)
}).then((id) => {
lastTestLogId = id
// Verify the log is also fired in the primary origin.
expect(logs[6].get('message')).to.eq('test log in cy.origin')
// Verify the log has the same ID as was generated in the cross-origin
expect(logs[6].get('id')).to.equal(id)
assertLogLength(logs, 11)
})
})
it('has a different id in a second test', () => {
cy.origin('http://foobar.com:3500', () => {
const afterLogAdded = new Promise<void>((resolve) => {
const listener = (attrs) => {
if (attrs.message === 'test log in cy.origin') {
expect(attrs.message).to.eq('test log in cy.origin')
cy.removeListener('log:added', listener)
resolve(attrs.id)
}
}
cy.on('log:added', listener)
})
cy.log('test log in cy.origin')
cy.wrap(afterLogAdded)
}).then((id) => {
// Verify the log is also fired in the primary origin.
expect(logs[6].get('message')).to.eq('test log in cy.origin')
// Verify the log has the same ID as was generated in the cross-origin
expect(logs[6].get('id')).to.equal(id)
expect(logs[6].get('id')).to.not.equal(lastTestLogId)
assertLogLength(logs, 12)
})
})
})
@@ -0,0 +1,229 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin misc', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="dom-link"]').click()
})
it('.end()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#button').end().should('be.null')
})
})
it('.exec()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.exec('echo foobar').its('stdout').should('contain', 'foobar')
})
})
it('.focused()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#button').click().focused().should('have.id', 'button')
})
})
it('.wrap()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.wrap({ foo: 'bar' }).should('deep.equal', { foo: 'bar' })
})
})
it('.debug()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#button').debug().should('have.id', 'button')
})
})
it('.pause()', () => {
cy.origin('http://foobar.com:3500', () => {
const afterPaused = new Promise<void>((resolve) => {
cy.once('paused', () => {
Cypress.emit('resume:all')
resolve()
})
})
cy.pause().wrap({}).should('deep.eq', {})
// pause is a noop in run mode, so only wait for it if in open mode
if (Cypress.config('isInteractive')) {
cy.wrap(afterPaused)
}
})
})
it('.task()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.task('return:arg', 'works').should('eq', 'works')
})
})
context('#consoleProps', () => {
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.exec()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.exec('echo foobar')
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('exec', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('exec')
expect(consoleProps['Shell Used']).to.be.undefined
expect(consoleProps.Yielded).to.have.property('code').that.equals(0)
expect(consoleProps.Yielded).to.have.property('stderr').that.equals('')
expect(consoleProps.Yielded).to.have.property('stdout').that.equals('foobar')
})
})
it('.focused()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#button').click().focused()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('focused', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('focused')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Yielded).to.have.property('tagName').that.equals('BUTTON')
expect(consoleProps.Yielded).to.have.property('id').that.equals('button')
})
})
it('.wrap()', () => {
cy.origin('http://foobar.com:3500', () => {
const arr = ['foo', 'bar', 'baz']
cy.wrap(arr).spread((foo, bar, baz) => {
expect(foo).to.equal('foo')
expect(bar).to.equal('bar')
expect(baz).to.equal('baz')
})
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('wrap', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('wrap')
expect(consoleProps.Yielded[0]).to.equal('foo')
expect(consoleProps.Yielded[1]).to.equal('bar')
expect(consoleProps.Yielded[2]).to.equal('baz')
})
})
it('.debug()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#button').debug()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('debug', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('debug')
expect(consoleProps.Yielded).to.have.property('tagName').that.equals('BUTTON')
expect(consoleProps.Yielded).to.have.property('id').that.equals('button')
})
})
it('.pause()', () => {
cy.origin('http://foobar.com:3500', () => {
const afterPaused = new Promise<void>((resolve) => {
cy.once('paused', () => {
Cypress.emit('resume:all')
resolve()
})
})
cy.pause().wrap({}).should('deep.eq', {})
// pause is a noop in run mode, so only wait for it if in open mode
if (Cypress.config('isInteractive')) {
cy.wrap(afterPaused)
}
})
cy.shouldWithTimeout(() => {
if (Cypress.config('isInteractive')) {
// if `isInteractive`, the .pause() will NOT show up in the command log in this case. Essentially a no-op.
return
}
const { consoleProps } = findCrossOriginLogs('pause', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('pause')
expect(consoleProps.Yielded).to.be.undefined
})
})
it('.task()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.task('return:arg', 'works')
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('task', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('task')
expect(consoleProps.Yielded).to.equal('works')
expect(consoleProps.arg).to.equal('works')
expect(consoleProps.task).to.equal('return:arg')
})
})
})
})
it('verifies number of cy commands', () => {
// @ts-ignore
// remove 'getAll' and 'shouldWithTimeout' commands since they are custom commands we added for our own testing and are not actual cy commands
const actualCommands = Cypress._.reject(Object.keys(cy.commandFns), (command) => command === 'getAll' || command === 'shouldWithTimeout')
const expectedCommands = [
'check', 'uncheck', 'click', 'dblclick', 'rightclick', 'focus', 'blur', 'hover', 'scrollIntoView', 'scrollTo', 'select',
'selectFile', 'submit', 'type', 'clear', 'trigger', 'as', 'ng', 'should', 'and', 'clock', 'tick', 'spread', 'each', 'then',
'invoke', 'its', 'getCookie', 'getCookies', 'setCookie', 'clearCookie', 'clearCookies', 'pause', 'debug', 'exec', 'readFile',
'writeFile', 'fixture', 'clearLocalStorage', 'url', 'hash', 'location', 'end', 'noop', 'log', 'wrap', 'reload', 'go', 'visit',
'focused', 'get', 'contains', 'root', 'shadow', 'within', 'request', 'session', 'screenshot', 'task', 'find', 'filter', 'not',
'children', 'eq', 'closest', 'first', 'last', 'next', 'nextAll', 'nextUntil', 'parent', 'parents', 'parentsUntil', 'prev',
'prevAll', 'prevUntil', 'siblings', 'wait', 'title', 'window', 'document', 'viewport', 'server', 'route', 'intercept', 'origin',
]
const addedCommands = Cypress._.difference(actualCommands, expectedCommands)
const removedCommands = Cypress._.difference(expectedCommands, actualCommands)
if (addedCommands.length && removedCommands.length) {
throw new Error(`Commands have been added to and removed from Cypress.
The following command(s) were added: ${addedCommands.join(', ')}
The following command(s) were removed: ${removedCommands.join(', ')}
Update this test accordingly.`)
}
if (addedCommands.length) {
throw new Error(`The following command(s) have been added to Cypress: ${addedCommands.join(', ')}. Please add tests for the command(s) in cy.origin and add the command(s) to this test.`)
}
if (removedCommands.length) {
throw new Error(`The following command(s) have been removed from Cypress: ${removedCommands.join(', ')}. Please remove the command(s) from this test.`)
}
})
@@ -0,0 +1,574 @@
const { stripIndent } = require('common-tags')
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin navigation', () => {
it('.go()', () => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/dom.html')
cy.go('back')
cy.location('pathname').should('include', 'multi-domain-secondary.html')
cy.go('forward')
cy.location('pathname').should('include', 'dom.html')
})
})
it('.reload()', () => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get(':checkbox[name="colors"][value="blue"]').check().should('be.checked')
cy.reload()
cy.get(':checkbox[name="colors"][value="blue"]').should('not.be.checked')
})
})
context('.visit()', () => {
it('calls the correct load handlers', () => {
const primaryCyBeforeLoadSpy = cy.spy()
const primaryCyLoadSpy = cy.spy()
const primaryVisitBeforeLoadSpy = cy.spy()
const primaryVisitLoadSpy = cy.spy()
cy.on('window:before:load', primaryCyBeforeLoadSpy)
cy.on('window:load', primaryCyLoadSpy)
cy.visit('/fixtures/multi-domain.html', {
onBeforeLoad: primaryVisitBeforeLoadSpy,
onLoad: primaryVisitLoadSpy,
}).then(() => {
expect(primaryCyBeforeLoadSpy).to.be.calledOnce
expect(primaryCyLoadSpy).to.be.calledTwice // twice because it's also called for 'about:blank'
expect(primaryVisitBeforeLoadSpy).to.be.calledOnce
expect(primaryVisitLoadSpy).to.be.calledOnce
})
cy.origin('http://foobar.com:3500', () => {
const secondaryCyBeforeLoadSpy = cy.spy()
const secondaryCyLoadSpy = cy.spy()
const secondaryVisitBeforeLoadSpy = cy.spy()
const secondaryVisitLoadSpy = cy.spy()
cy.on('window:before:load', secondaryCyBeforeLoadSpy)
cy.on('window:load', secondaryCyLoadSpy)
cy.visit('http://www.foobar.com:3500/fixtures/dom.html', {
onBeforeLoad: secondaryVisitBeforeLoadSpy,
onLoad: secondaryVisitLoadSpy,
}).then(() => {
expect(secondaryCyBeforeLoadSpy).to.be.calledOnce
expect(secondaryCyLoadSpy).to.be.calledOnce
expect(secondaryVisitBeforeLoadSpy).to.be.calledOnce
expect(secondaryVisitLoadSpy).to.be.calledOnce
})
}).then(() => {
expect(primaryCyBeforeLoadSpy).to.be.calledOnce
expect(primaryCyLoadSpy).to.be.calledTwice
expect(primaryVisitBeforeLoadSpy).to.be.calledOnce
expect(primaryVisitLoadSpy).to.be.calledOnce
})
})
it('supports visiting primary first', () => {
cy.visit('/fixtures/multi-domain.html')
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary origin')
})
})
it('supports skipping visiting primary first', () => {
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary origin')
})
})
// TODO: we don't support nested cy.origin yet...
it.skip('supports nesting a third origin', () => {
cy.visit('/fixtures/multi-domain.html')
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary origin')
cy.origin('http://idp.com:3500', () => {
cy.visit('http://www.idp.com:3500/fixtures/dom.html')
})
})
})
it('supports navigating to secondary through button and then visiting', () => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary origin')
cy.visit('http://www.foobar.com:3500/fixtures/dom.html')
cy.location('href').should('equal', 'http://www.foobar.com:3500/fixtures/dom.html')
})
})
it('supports relative urls within secondary', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('/fixtures/dom.html')
cy.location('href').should('equal', 'http://www.foobar.com:3500/fixtures/dom.html')
})
})
it('supports relative urls with path within secondary', () => {
cy.origin('http://www.foobar.com:3500/fixtures', () => {
cy.visit('/dom.html')
cy.location('href').should('equal', 'http://www.foobar.com:3500/fixtures/dom.html')
})
})
it('supports relative urls with hash within secondary', () => {
cy.origin('http://www.foobar.com:3500/#hash', () => {
cy.visit('/more-hash')
cy.location('href').should('equal', 'http://www.foobar.com:3500/#hash/more-hash')
})
})
it('supports relative urls with path and hash within secondary', () => {
cy.origin('http://www.foobar.com:3500/welcome/#hash', () => {
cy.visit('/more-hash')
cy.location('href').should('equal', 'http://www.foobar.com:3500/welcome/#hash/more-hash')
})
})
it('supports hash change within secondary', () => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html#hashchange')
cy.location('hash').should('equal', '#hashchange')
})
})
it('navigates back to primary', () => {
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary origin')
})
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').should('have.text', 'http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
cy.location('href').should('equal', 'http://localhost:3500/fixtures/multi-domain.html')
})
it('errors when visiting a new origin within origin', (done) => {
cy.on('fail', (e) => {
expect(e.message).to.equal(stripIndent`\
\`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n
You likely forgot to use \`cy.origin()\`:\n
\`cy.origin('http://foobar.com:3500', () => {\`
\` cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')\`
\` <commands targeting http://www.foobar.com:3500 go here>\`
\`})\`\n
\`cy.origin('http://idp.com:3500', () => {\`
\` cy.visit('http://www.idp.com:3500/fixtures/dom.html')\`
\` <commands targeting http://www.idp.com:3500 go here>\`
\`})\`\n
The new URL is considered a different origin because the following parts of the URL are different:\n
> superdomain\n
You may only \`cy.visit()\` same-origin URLs within \`cy.origin()\`.\n
The previous URL you visited was:\n
> 'http://www.foobar.com:3500'\n
You're attempting to visit this URL:\n
> 'http://www.idp.com:3500'`)
done()
})
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
cy.origin('http://foobar.com:3500', () => {
// this call should error since we can't visit a cross-origin
cy.visit('http://www.idp.com:3500/fixtures/dom.html')
})
})
// @ts-ignore
it('informs user to use cy.origin with experimental flag off', { experimentalSessionAndOrigin: false }, (done) => {
cy.on('fail', (e) => {
expect(e.message).to.equal(stripIndent`\
\`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n
In order to visit a different origin, you can enable the \`experimentalSessionAndOrigin\` flag and use \`cy.origin()\`:\n
\`cy.visit('http://localhost:3500/fixtures/multi-domain.html')\`
\`<commands targeting http://localhost:3500 go here>\`\n
\`cy.origin('http://foobar.com:3500', () => {\`
\` cy.visit('http://www.foobar.com:3500/fixtures/dom.html')\`
\` <commands targeting http://www.foobar.com:3500 go here>\`
\`})\`\n
The new URL is considered a different origin because the following parts of the URL are different:\n
> superdomain\n
You may only \`cy.visit()\` same-origin URLs within a single test.\n
The previous URL you visited was:\n
> 'http://localhost:3500'\n
You're attempting to visit this URL:\n
> 'http://www.foobar.com:3500'`)
done()
})
cy.visit('/fixtures/multi-domain.html')
// this call should error since we can't visit a cross-origin
cy.visit('http://www.foobar.com:3500/fixtures/dom.html')
})
it('supports the query string option', () => {
cy.visit('/fixtures/multi-domain.html')
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html', { qs: { foo: 'bar' } })
cy.location('search').should('equal', '?foo=bar')
})
})
it('can send a POST request', () => {
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/post-only', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
bar: 'baz',
}),
})
cy.contains('it worked!').contains('{"bar":"baz"}')
})
})
it('succeeds when the AUT window in the secondary is undefined', () => {
// manually remove the spec bridge iframe to ensure Cypress.state('window') is not already set
window.top?.document.getElementById('Spec\ Bridge:\ foobar.com')?.remove()
cy.visit('/fixtures/multi-domain.html')
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary origin')
})
})
it('succeeds when the secondary is already defined but the AUT is still on the primary', () => {
// setup the secondary to be on the secondary origin
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary origin')
})
// update the AUT to be on the primary origin
cy.visit('/fixtures/multi-domain.html')
// verify there aren't any issues when the AUT is on primary but the spec bridge is on secondary (cross-origin)
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary origin')
})
})
it('does not navigate to about:blank in secondary', () => {
const primaryCyLoadSpy = cy.spy()
cy.on('window:load', primaryCyLoadSpy)
cy.origin('http://foobar.com:3500', () => {
const secondaryCyLoadSpy = cy.spy()
cy.on('window:load', secondaryCyLoadSpy)
cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html').then(() => {
expect(secondaryCyLoadSpy).to.have.been.calledOnce
expect(secondaryCyLoadSpy.args[0][0].location.href).to.equal('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
})
})
cy.then(() => {
expect(primaryCyLoadSpy).to.not.have.been.called
})
})
it('supports redirecting from primary to secondary in cy.origin', () => {
cy.visit('/fixtures/multi-domain.html')
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://localhost:3500/redirect?href=http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary origin')
})
})
it('supports redirecting from secondary to primary outside of cy.origin', () => {
cy.visit('/fixtures/multi-domain.html')
cy.visit('http://www.foobar.com:3500/redirect?href=http://localhost:3500/fixtures/generic.html')
})
it('errors when trying to redirect from secondary to primary in cy.origin', (done) => {
cy.on('fail', (e) => {
expect(e.message).to.equal(stripIndent`
\`cy.visit()\` failed because you are attempting to visit a URL from a previous origin inside of \`cy.origin()\`.\n
Instead of placing the \`cy.visit()\` inside of \`cy.origin()\`, the \`cy.visit()\` should be placed outside of the \`cy.origin()\` block.\n
\`<commands targeting http://localhost:3500 go here>\`\n
\`cy.origin('http://foobar.com:3500', () => {\`
\` <commands targeting http://foobar.com:3500 go here>\`
\`})\`\n
\`cy.visit('http://www.foobar.com:3500/redirect?href=http://localhost:3500/fixtures/generic.html')\``)
done()
})
cy.visit('http://localhost:3500/fixtures/multi-domain.html')
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('/redirect?href=http://localhost:3500/fixtures/generic.html')
})
})
it('errors when trying to visit primary in cy.origin', (done) => {
cy.on('fail', (e) => {
expect(e.message).to.equal(stripIndent`
\`cy.visit()\` failed because you are attempting to visit a URL from a previous origin inside of \`cy.origin()\`.\n
Instead of placing the \`cy.visit()\` inside of \`cy.origin()\`, the \`cy.visit()\` should be placed outside of the \`cy.origin()\` block.\n
\`<commands targeting http://localhost:3500 go here>\`\n
\`cy.origin('http://foobar.com:3500', () => {\`
\` <commands targeting http://foobar.com:3500 go here>\`
\`})\`\n
\`cy.visit('http://localhost:3500/fixtures/generic.html')\``)
done()
})
cy.visit('http://localhost:3500/fixtures/multi-domain.html')
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://localhost:3500/fixtures/generic.html')
})
})
it('errors when trying to redirect from primary to secondary outside of cy.origin', (done) => {
cy.on('fail', (e) => {
expect(e.message).to.equal(stripIndent`\
\`cy.visit()\` failed because you are attempting to visit a URL that is of a different origin.\n
You likely forgot to use \`cy.origin()\`:\n
\`cy.visit('http://localhost:3500/fixtures/multi-domain.html')\`
\`<commands targeting http://localhost:3500 go here>\`\n
\`cy.origin('http://foobar.com:3500', () => {\`
\` cy.visit('http://localhost:3500/redirect?href=http://www.foobar.com:3500/fixtures/generic.html')\`
\` <commands targeting http://www.foobar.com:3500 go here>\`
\`})\`\n
The new URL is considered a different origin because the following parts of the URL are different:\n
> superdomain\n
You may only \`cy.visit()\` same-origin URLs within a single test.\n
The previous URL you visited was:\n
> 'http://localhost:3500'\n
You're attempting to visit this URL:\n
> 'http://www.foobar.com:3500'`)
done()
})
cy.visit('/fixtures/multi-domain.html')
cy.visit('http://localhost:3500/redirect?href=http://www.foobar.com:3500/fixtures/generic.html')
})
it('supports auth options and adding auth to subsequent requests', () => {
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/basic_auth', {
auth: {
username: 'cypress',
password: 'password123',
},
})
cy.get('body').should('have.text', 'basic auth worked')
cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/basic_auth'
})
cy.get('body').should('have.text', 'basic auth worked')
})
// attaches the auth options for the foobar origin even from another origin
cy.origin('http://www.idp.com:3500', () => {
cy.visit('/fixtures/multi-domain.html')
cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/basic_auth'
})
})
cy.origin('http://foobar.com:3500', () => {
cy.get('body').should('have.text', 'basic auth worked')
})
cy.visit('/fixtures/multi-domain.html')
// attaches the auth options for the foobar origin from the top-level
cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/basic_auth'
})
cy.origin('http://foobar.com:3500', () => {
cy.get('body').should('have.text', 'basic auth worked')
})
})
it('does not propagate the auth options across tests', (done) => {
cy.intercept('/basic_auth', (req) => {
req.on('response', (res) => {
// clear the www-authenticate header so the browser doesn't prompt for username/password
res.headers['www-authenticate'] = ''
expect(res.statusCode).to.equal(401)
done()
})
})
cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/fixtures/multi-domain.html'
})
cy.origin('http://foobar.com:3500', () => {
cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/basic_auth'
})
})
})
it('succeeds when visiting local file server first', { baseUrl: undefined }, () => {
cy.visit('cypress/fixtures/multi-domain.html')
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('/fixtures/multi-domain-secondary.html')
cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary origin')
})
})
it('handles visit failures', { baseUrl: undefined }, (done) => {
cy.on('fail', (e) => {
expect(e.message).to.include('failed trying to load:\n\nhttp://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
expect(e.message).to.include('500: Internal Server Error')
done()
})
cy.intercept('*/multi-domain-secondary.html', { statusCode: 500 })
cy.visit('cypress/fixtures/multi-domain.html')
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('fixtures/multi-domain-secondary.html')
})
})
})
it('supports navigating through changing the window.location.href', () => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/fixtures/dom.html'
})
cy.location('pathname').should('equal', '/fixtures/dom.html')
})
})
context('#consoleProps', () => {
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.go()', () => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/dom.html')
cy.go('back')
})
cy.shouldWithTimeout(() => {
const { consoleProps, ...attrs } = findCrossOriginLogs('go', logs, 'foobar.com')
expect(attrs.name).to.equal('go')
expect(attrs.message).to.equal('back')
expect(consoleProps.Command).to.equal('go')
expect(consoleProps.Yielded).to.be.null
})
})
it('.reload()', () => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="dom-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.reload()
})
cy.shouldWithTimeout(() => {
const { consoleProps, ...attrs } = findCrossOriginLogs('reload', logs, 'foobar.com')
expect(attrs.name).to.equal('reload')
expect(attrs.message).to.equal('')
expect(consoleProps.Command).to.equal('reload')
expect(consoleProps.Yielded).to.be.null
})
})
it('visit()', () => {
cy.visit('/fixtures/multi-domain.html')
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary origin')
})
cy.shouldWithTimeout(() => {
const { consoleProps, ...attrs } = findCrossOriginLogs('visit', logs, 'foobar.com')
expect(attrs.name).to.equal('visit')
expect(attrs.message).to.equal('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
expect(consoleProps.Command).to.equal('visit')
expect(consoleProps).to.have.property('Cookies Set').that.is.an('object')
expect(consoleProps).to.have.property('Redirects').that.is.an('object')
expect(consoleProps).to.have.property('Resolved Url').that.equals('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
})
})
})
})
@@ -0,0 +1,74 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin network requests', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="request-link"]').click()
})
it('.request() to secondary origin', () => {
cy.origin('http://foobar.com:3500', () => {
cy.request('http://www.foobar.com:3500/fixtures/example.json').should((response) => {
expect(response.status).to.equal(200)
expect(response.allRequestResponses[0]['Request URL']).to.equal('http://www.foobar.com:3500/fixtures/example.json')
})
})
})
it('.request() to secondary origin with relative path', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.request('/fixtures/example.json').should((response) => {
expect(response.status).to.equal(200)
expect(response.allRequestResponses[0]['Request URL']).to.equal('http://www.foobar.com:3500/fixtures/example.json')
})
})
})
it('.request() to primary origin', () => {
cy.origin('http://foobar.com:3500', () => {
cy.request('http://localhost:3500/fixtures/example.json').should((response) => {
expect(response.status).to.equal(200)
expect(response.allRequestResponses[0]['Request URL']).to.equal('http://localhost:3500/fixtures/example.json')
})
})
})
context('#consoleProps', () => {
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.request()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.request('http://www.foobar.com:3500/fixtures/example.json')
})
cy.shouldWithTimeout(() => {
const { consoleProps, renderProps } = findCrossOriginLogs('request', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('request')
expect(consoleProps.Request).to.have.property('Request Body').that.equals(null)
expect(consoleProps.Request).to.have.property('Request Headers').that.is.a('object')
expect(consoleProps.Request).to.have.property('Request URL').that.equals('http://www.foobar.com:3500/fixtures/example.json')
expect(consoleProps.Request).to.have.property('Response Body').that.is.a('string')
expect(consoleProps.Request).to.have.property('Response Headers').that.is.a('object')
expect(consoleProps.Request).to.have.property('Response Status').that.equals(200)
expect(consoleProps.Yielded).to.have.property('body').that.deep.equals({ example: true })
expect(consoleProps.Yielded).to.have.property('duration').that.is.a('number')
expect(consoleProps.Yielded).to.have.property('headers').that.is.a('object')
expect(consoleProps.Yielded).to.have.property('status').that.equals(200)
expect(renderProps).to.have.property('indicator').that.equals('successful')
expect(renderProps).to.have.property('message').that.equals('GET 200 http://www.foobar.com:3500/fixtures/example.json')
})
})
})
})
@@ -0,0 +1,104 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin querying', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="dom-link"]').click()
})
it('.get()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#input')
})
})
it('.contains()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.contains('Nested Find')
})
})
it('.within()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id').within(() => {
cy.get('#input')
})
})
})
it('.root()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.root().should('match', 'html')
})
})
context('#consoleProps', () => {
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.contains()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.contains('Nested Find')
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('contains', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('contains')
expect(consoleProps['Applied To']).to.be.undefined
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Content).to.equal('Nested Find')
expect(consoleProps.Yielded).to.have.property('tagName').that.equals('DIV')
expect(consoleProps.Yielded).to.have.property('id').that.equals('nested-find')
})
})
it('.within()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id').within(() => {
cy.get('#input')
})
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('within', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('within')
expect(consoleProps.Yielded).to.have.property('tagName').that.equals('FORM')
expect(consoleProps.Yielded).to.have.property('id').that.equals('by-id')
})
})
it('.root()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.root()
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('root', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('root')
expect(consoleProps.Yielded).to.have.property('tagName').that.equals('HTML')
})
})
})
})
@@ -0,0 +1,51 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin shadow dom', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="shadow-dom-link"]').click()
})
it('.shadow()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#shadow-element-1').shadow().find('p.shadow-1')
.should('have.text', 'Shadow Content 1')
})
})
context('#consoleProps', () => {
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.shadow()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#shadow-element-1').shadow()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('shadow', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('shadow')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('CY-TEST-ELEMENT')
expect(consoleProps['Applied To']).to.have.property('id').that.equals('shadow-element-1')
expect(consoleProps.Yielded).to.be.null
})
})
})
})
@@ -0,0 +1,321 @@
context('cy.origin screenshot', () => {
context('set viewport', () => {
beforeEach(() => {
this.serverResult = {
path: '/path/to/screenshot',
size: 12,
dimensions: { width: 20, height: 20 },
multipart: false,
pixelRatio: 1,
takenAt: new Date().toISOString(),
name: 'name',
blackout: [],
testAttemptIndex: 0,
duration: 100,
}
cy.viewport(600, 200)
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="screenshots-link"]').click()
})
it('captures the fullPage', () => {
cy.origin('http://foobar.com:3500', { args: this.serverResult }, (serverResult) => {
const automationStub = cy.stub(Cypress, 'automation').withArgs('take:screenshot').resolves(serverResult)
cy.screenshot({ capture: 'fullPage' })
.then(() => {
expect(automationStub).to.be.calledThrice
expect(automationStub.args[0][1].capture).to.equal('fullPage')
expect(automationStub.args[1][1].capture).to.equal('fullPage')
expect(automationStub.args[2][1].capture).to.equal('fullPage')
})
})
})
it('captures the runner', () => {
cy.origin('http://foobar.com:3500', { args: this.serverResult }, (serverResult) => {
const automationStub = cy.stub(Cypress, 'automation').withArgs('take:screenshot').resolves(serverResult)
cy.screenshot({ capture: 'runner' })
.then(() => {
expect(automationStub).to.be.calledOnce
expect(automationStub.args[0][1].capture).to.equal('runner')
})
})
})
it('captures the viewport', () => {
cy.origin('http://foobar.com:3500', { args: this.serverResult }, (serverResult) => {
const automationStub = cy.stub(Cypress, 'automation').withArgs('take:screenshot').resolves(serverResult)
cy.screenshot({ capture: 'viewport' })
.then(() => {
expect(automationStub).to.be.calledOnce
expect(automationStub.args[0][1].capture).to.equal('viewport')
})
})
})
})
context('without setting viewport', () => {
beforeEach(() => {
this.serverResult = {
path: '/path/to/screenshot',
size: 12,
dimensions: { width: 20, height: 20 },
multipart: false,
pixelRatio: 1,
takenAt: new Date().toISOString(),
name: 'name',
blackout: [],
testAttemptIndex: 0,
duration: 100,
}
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="screenshots-link"]').click()
})
it('supports multiple titles', () => {
cy.origin('http://foobar.com:3500', { args: this.serverResult }, (serverResult) => {
const automationStub = cy.stub(Cypress, 'automation').withArgs('take:screenshot').resolves(serverResult)
cy.screenshot()
.then(() => {
expect(automationStub.args[0][1].titles).to.deep.equal(['cy.origin screenshot', 'without setting viewport', 'supports multiple titles'])
})
})
})
it('supports the blackout option', () => {
cy.origin('http://foobar.com:3500', { args: this.serverResult }, (serverResult) => {
cy.stub(Cypress, 'automation').withArgs('take:screenshot').resolves(serverResult)
cy.screenshot({
blackout: ['.short-element'],
onBeforeScreenshot: ($el) => {
const $blackoutElement = $el.find('.__cypress-blackout')
const $shortElement = $el.find('.short-element')
expect($blackoutElement.outerHeight()).to.equal($shortElement.outerHeight())
expect($blackoutElement.outerWidth()).to.equal($shortElement.outerWidth())
expect($blackoutElement.offset()).to.deep.equal($shortElement.offset())
},
})
})
})
it('supports element screenshots', () => {
cy.origin('http://foobar.com:3500', { args: this.serverResult }, (serverResult) => {
const automationStub = cy.stub(Cypress, 'automation').withArgs('take:screenshot').resolves(serverResult)
cy.get('.tall-element').screenshot()
.then(() => {
expect(automationStub.args[0][1].clip.x).to.be.greaterThan(0)
expect(automationStub.args[0][1].clip.y).to.be.greaterThan(0)
})
})
})
it('supports screenshot retrying with appropriate naming', () => {
cy.origin('http://foobar.com:3500', { args: this.serverResult }, (serverResult) => {
const automationStub = cy.stub(Cypress, 'automation').withArgs('take:screenshot').resolves(serverResult)
cy.state('runnable')._currentRetry = 2
cy.screenshot()
.then(() => {
expect(automationStub.args[0][1].testAttemptIndex).to.equal(2)
})
})
})
it('calls the onBeforeScreenshot callback', () => {
cy.origin('http://foobar.com:3500', { args: this.serverResult }, (serverResult) => {
cy.stub(Cypress, 'automation').withArgs('take:screenshot').resolves(serverResult)
const onBeforeScreenshot = cy.stub()
cy.screenshot({ onBeforeScreenshot })
cy.wrap(onBeforeScreenshot).should('be.called')
})
})
it('calls the onAfterScreenshot callback', () => {
cy.origin('http://foobar.com:3500', { args: this.serverResult }, (serverResult) => {
cy.stub(Cypress, 'automation').withArgs('take:screenshot').resolves(serverResult)
const onAfterScreenshot = cy.stub()
cy.screenshot({ onAfterScreenshot })
cy.wrap(onAfterScreenshot).should('be.called')
})
})
it('supports the Cypress.screenshot callbacks', () => {
cy.origin('http://foobar.com:3500', { args: this.serverResult }, (serverResult) => {
cy.stub(Cypress, 'automation').withArgs('take:screenshot').resolves(serverResult)
const onAfterScreenshot = cy.stub()
const onBeforeScreenshot = cy.stub()
Cypress.Screenshot.defaults({
onBeforeScreenshot,
onAfterScreenshot,
})
cy.screenshot()
cy.wrap(onBeforeScreenshot).should('be.called')
cy.wrap(onAfterScreenshot).should('be.called')
})
})
it('supports pausing timers', () => {
cy.origin('http://foobar.com:3500', { args: this.serverResult }, (serverResult) => {
cy.stub(Cypress, 'automation').withArgs('take:screenshot').returns(Cypress.Promise.delay(500, serverResult))
cy.window().then((win) => {
// Hide the element using setTimeout
win.setTimeout(() => {
(win.document.getElementsByClassName('tall-element')[0] as HTMLElement).style.display = 'none'
}, 50)
})
cy.screenshot({
onBeforeScreenshot: ($el) => {
// Set the timeout to be longer than the element hiding timeout so if the timer was
// not paused, it would've hidden the element but since we are pausing the
// timers, the style is still 'block'
setTimeout(() => {
expect($el.find('.tall-element').css('display')).to.equal('block')
}, 100)
},
onAfterScreenshot: ($el) => {
// Set the timeout to be longer than the element hiding timeout so when the timers
// are unpaused, this will run after the timeout to hide the element
setTimeout(() => {
expect($el.find('.tall-element').css('display')).to.equal('none')
}, 100)
},
})
cy.wait(200)
})
})
it('does not pause timers when disableTimersAndAnimations is false', () => {
cy.origin('http://foobar.com:3500', { args: this.serverResult }, (serverResult) => {
cy.stub(Cypress, 'automation').withArgs('take:screenshot').returns(Cypress.Promise.delay(500, serverResult))
cy.window().then((win) => {
// Hide the element using setTimeout
win.setTimeout(() => {
(win.document.getElementsByClassName('tall-element')[0] as HTMLElement).style.display = 'none'
}, 50)
})
cy.screenshot({
disableTimersAndAnimations: false,
onBeforeScreenshot: ($el) => {
// Set the timeout to be longer than the element hiding timeout so it has time to run and hide the element
setTimeout(() => {
expect($el.find('.tall-element').css('display')).to.equal('none')
}, 100)
},
})
cy.wait(200)
})
})
it('handles errors thrown from setTimeout after the timer is paused', () => {
cy.on('fail', (err) => {
expect(err.name).to.eq('Error')
expect(err.message).to.include('setTimeout error after screenshot')
})
cy.origin('http://foobar.com:3500', { args: this.serverResult }, (serverResult) => {
cy.stub(Cypress, 'automation').withArgs('take:screenshot').returns(Cypress.Promise.delay(100, serverResult))
cy.window().then((win) => {
// Add a timeout error
win.setTimeout(() => {
throw new Error('setTimeout error after screenshot')
}, 50)
})
cy.screenshot()
// wait to ensure the timeout error has time to process
cy.wait(100)
})
})
it('handles errors thrown from setTimeout when the timer is NOT paused', () => {
cy.on('fail', (err) => {
expect(err.name).to.eq('Error')
expect(err.message).to.include('setTimeout error during screenshot')
expect(err.message).to.include('The following error originated from your application code, not from Cypress.')
expect(err.docsUrl).to.deep.eq(['https://on.cypress.io/uncaught-exception-from-application'])
})
cy.origin('http://foobar.com:3500', { args: this.serverResult }, (serverResult) => {
cy.stub(Cypress, 'automation').withArgs('take:screenshot').returns(Cypress.Promise.delay(100, serverResult))
cy.window().then((win) => {
// Add a timeout error
win.setTimeout(() => {
throw new Error('setTimeout error during screenshot')
}, 50)
})
cy.screenshot({ disableTimersAndAnimations: false })
// wait to ensure the timeout error has time to process
cy.wait(100)
})
})
})
context('#consoleProps', () => {
const { findCrossOriginLogs } = require('../../../../support/utils')
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="screenshots-link"]').click()
})
it('.screenshot()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.screenshot({ capture: 'fullPage' })
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('screenshot', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('screenshot')
expect(consoleProps).to.have.property('blackout')
expect(consoleProps).to.have.property('capture').that.equals('fullPage')
expect(consoleProps).to.have.property('dimensions').that.is.a('string')
expect(consoleProps).to.have.property('disableTimersAndAnimations').that.is.a('boolean')
expect(consoleProps).to.have.property('duration').that.is.a('string')
expect(consoleProps).to.have.property('multipart').that.is.a('boolean')
expect(consoleProps).to.have.property('name').to.be.null
expect(consoleProps).to.have.property('path').that.is.a('string')
expect(consoleProps).to.have.property('pixelRatio').that.is.a('number')
expect(consoleProps).to.have.property('scaled').that.is.a('boolean')
expect(consoleProps).to.have.property('size').that.is.a('string')
expect(consoleProps).to.have.property('specName').that.is.a('string')
expect(consoleProps).to.have.property('takenAt').that.is.a('string')
expect(consoleProps).to.have.property('testAttemptIndex').that.is.a('number')
})
})
})
})
@@ -0,0 +1,182 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin spies, stubs, and clock', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
it('spy()', () => {
cy.origin('http://foobar.com:3500', () => {
const foo = { bar () { } }
cy.spy(foo, 'bar')
foo.bar()
expect(foo.bar).to.be.called
})
})
it('stub()', () => {
cy.origin('http://foobar.com:3500', () => {
const foo = { bar () { } }
cy.stub(foo, 'bar')
foo.bar()
expect(foo.bar).to.be.called
})
})
context('resets stubs', () => {
it('creates the stub', () => {
cy.origin('http://foobar.com:3500', () => {
const stubEnv = cy.stub(Cypress, 'env').withArgs('foo').returns('bar')
expect(Cypress.env('foo')).to.equal('bar')
expect(stubEnv).to.be.calledOnce
// @ts-ignore
expect(Cypress.env.isSinonProxy).to.be.true
})
})
it('verifies the stub got restored', () => {
cy.origin('http://foobar.com:3500', () => {
expect(Cypress.env('foo')).to.be.undefined
// @ts-ignore
expect(Cypress.env.isSinonProxy).to.be.undefined
})
})
})
context('resets spies', () => {
it('creates the spy', () => {
cy.origin('http://foobar.com:3500', () => {
const stubEnv = cy.spy(Cypress, 'env')
Cypress.env()
expect(stubEnv).to.be.calledOnce
// @ts-ignore
expect(Cypress.env.isSinonProxy).to.be.true
})
})
it('verifies the spy got restored', () => {
cy.origin('http://foobar.com:3500', () => {
// @ts-ignore
expect(Cypress.env.isSinonProxy).to.be.undefined
})
})
})
it('clock() and tick()', () => {
cy.origin('http://foobar.com:3500', () => {
const now = Date.UTC(2022, 0, 12)
cy.clock(now)
cy.window().then((win) => {
expect(win.Date.now()).to.equal(now)
})
cy.tick(10000) // 10 seconds passed
cy.window().then((win) => {
expect(win.Date.now()).to.equal(now + 10000)
})
})
})
context('#consoleProps', () => {
const { _ } = Cypress
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
// cy.clock only adds a log and does NOT update
cy.on('log:added', (attrs, log) => {
logs.set(attrs.id, log)
})
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('spy()', () => {
cy.origin('http://foobar.com:3500', () => {
const foo = { bar () { } }
cy.spy(foo, 'bar')
foo.bar()
expect(foo.bar).to.be.called
})
cy.shouldWithTimeout(() => {
const spyLog = findCrossOriginLogs('spy-1', logs, 'foobar.com')
expect(spyLog.consoleProps.Command).to.equal('spy-1')
expect(spyLog.callCount).to.be.a('number')
expect(spyLog.functionName).to.equal('bar')
})
})
it('.stub()', () => {
cy.origin('http://foobar.com:3500', () => {
const foo = { bar () { } }
cy.stub(foo, 'bar')
foo.bar()
expect(foo.bar).to.be.called
})
cy.shouldWithTimeout(() => {
const stubLog = findCrossOriginLogs('stub-1', logs, 'foobar.com')
expect(stubLog.consoleProps.Command).to.equal('stub-1')
expect(stubLog.callCount).to.be.a('number')
expect(stubLog.functionName).to.equal('bar')
})
})
it('.clock()', () => {
cy.origin('http://foobar.com:3500', () => {
const now = Date.UTC(2022, 0, 12)
cy.clock(now)
})
cy.shouldWithTimeout(() => {
const clockLog = findCrossOriginLogs('clock', logs, 'foobar.com')
expect(clockLog.name).to.equal('clock')
const consoleProps = clockLog.consoleProps()
expect(consoleProps.Command).to.equal('clock')
expect(consoleProps).to.have.property('Methods replaced').that.is.a('object')
expect(consoleProps).to.have.property('Now').that.is.a('number')
})
})
it('.tick()', () => {
cy.origin('http://foobar.com:3500', () => {
const now = Date.UTC(2022, 0, 12)
cy.clock(now)
cy.tick(10000)
})
cy.shouldWithTimeout(() => {
const tickLog = findCrossOriginLogs('tick', logs, 'foobar.com')
expect(tickLog.name).to.equal('tick')
const consoleProps = _.isFunction(tickLog.consoleProps) ? tickLog.consoleProps() : tickLog.consoleProps
expect(consoleProps.Command).to.equal('tick')
expect(consoleProps).to.have.property('Methods replaced').that.is.a('object')
expect(consoleProps).to.have.property('Now').that.is.a('number')
expect(consoleProps).to.have.property('Ticked').that.is.a('string')
})
})
})
})
@@ -0,0 +1,644 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin traversal', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="dom-link"]').click()
})
it('.children()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id').children().should('have.length', 3)
})
})
it('.closest()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id').closest('form')
})
})
it('.eq()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id>input').eq(1).should('have.id', 'name')
})
})
it('.filter()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-name>input')
.filter('[name="dogs"]').should('have.length', 4)
})
})
it('.find()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id').find('input').should('have.length', 3)
})
})
it('.first()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id>input').first().should('have.id', 'input')
})
})
it('.last()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id>input').last().should('have.id', 'age')
})
})
it('.next()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#input').next().should('have.id', 'name')
})
})
it('.nextAll()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#input').nextAll().should('have.length', 2)
})
})
it('.nextUntil()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#input').nextUntil('#age').should('have.length', 1)
})
})
it('.not()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id>input').not('#age').should('have.length', 2)
})
})
it('.parent()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id').parent().should('have.id', 'dom')
})
})
it('.parents()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id').parents().should('have.length', 3)
})
})
it('.parentsUntil()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id').parentsUntil('body').should('have.length', 1)
})
})
it('.prev()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#age').prev().should('have.id', 'name')
})
})
it('.prevAll()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#age').prevAll().should('have.length', 2)
})
})
it('.prevUntil()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#age').prevUntil('#input').should('have.length', 1)
})
})
it('.siblings()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#input').siblings().should('have.length', 2)
})
})
context('#consoleProps', () => {
const { _ } = Cypress
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.children()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id').children()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('children', logs, 'foobar.com')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('FORM')
expect(consoleProps['Applied To']).to.have.property('id').that.equals('by-id')
expect(consoleProps.Command).to.equal('children')
expect(consoleProps.Elements).to.equal(3)
expect(consoleProps.Selector).to.equal('')
expect(consoleProps.Yielded.length).to.equal(3)
expect(consoleProps.Yielded[0]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded[0]).to.have.property('id').that.equals('input')
expect(consoleProps.Yielded[1]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded[1]).to.have.property('id').that.equals('name')
expect(consoleProps.Yielded[2]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded[2]).to.have.property('id').that.equals('age')
})
})
it('.closest()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id').closest('form')
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('closest', logs, 'foobar.com')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('FORM')
expect(consoleProps['Applied To']).to.have.property('id').that.equals('by-id')
expect(consoleProps.Command).to.equal('closest')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Selector).to.equal('form')
expect(consoleProps.Yielded).to.have.property('tagName').that.equals('FORM')
expect(consoleProps.Yielded).to.have.property('id').that.equals('by-id')
expect(consoleProps.Yielded.querySelector('input#input')).to.be.ok
expect(consoleProps.Yielded.querySelector('input#name')).to.be.ok
expect(consoleProps.Yielded.querySelector('input#age')).to.be.ok
})
})
it('.eq()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id>input').eq(1)
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('eq', logs, 'foobar.com')
expect(consoleProps['Applied To'].length).to.equal(3)
expect(consoleProps['Applied To'][0]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To'][0]).to.have.property('id').that.equals('input')
expect(consoleProps['Applied To'][1]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To'][1]).to.have.property('id').that.equals('name')
expect(consoleProps['Applied To'][2]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To'][2]).to.have.property('id').that.equals('age')
expect(consoleProps.Command).to.equal('eq')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Selector).to.equal('1')
expect(consoleProps.Yielded).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded).to.have.property('id').that.equals('name')
})
})
it('.filter()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-name>input')
.filter('[name="dogs"]')
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('filter', logs, 'foobar.com')
expect(consoleProps['Applied To'].length).to.equal(12)
expect(consoleProps.Command).to.equal('filter')
expect(consoleProps.Elements).to.equal(4)
expect(consoleProps.Selector).to.equal('[name="dogs"]')
expect(consoleProps.Yielded.length).to.equal(4)
_.forEach(consoleProps.Yielded, (yielded) => {
expect(yielded).to.have.property('tagName').that.equals('INPUT')
expect(yielded).to.have.property('name').that.equals('dogs')
})
})
})
it('.find()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id').find('input')
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('find', logs, 'foobar.com')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('FORM')
expect(consoleProps['Applied To']).to.have.property('id').that.equals('by-id')
expect(consoleProps.Command).to.equal('find')
expect(consoleProps.Elements).to.equal(3)
expect(consoleProps.Selector).to.equal('input')
expect(consoleProps.Yielded.length).to.equal(3)
expect(consoleProps.Yielded[0]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded[0]).to.have.property('id').that.equals('input')
expect(consoleProps.Yielded[1]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded[1]).to.have.property('id').that.equals('name')
expect(consoleProps.Yielded[2]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded[2]).to.have.property('id').that.equals('age')
})
})
it('.first()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id>input').first()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('first', logs, 'foobar.com')
expect(consoleProps['Applied To'].length).to.equal(3)
expect(consoleProps['Applied To'][0]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To'][0]).to.have.property('id').that.equals('input')
expect(consoleProps['Applied To'][1]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To'][1]).to.have.property('id').that.equals('name')
expect(consoleProps['Applied To'][2]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To'][2]).to.have.property('id').that.equals('age')
expect(consoleProps.Command).to.equal('first')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Selector).to.equal('')
expect(consoleProps.Yielded).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded).to.have.property('id').that.equals('input')
})
})
it('.last()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id>input').last()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('last', logs, 'foobar.com')
expect(consoleProps['Applied To'].length).to.equal(3)
expect(consoleProps['Applied To'][0]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To'][0]).to.have.property('id').that.equals('input')
expect(consoleProps['Applied To'][1]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To'][1]).to.have.property('id').that.equals('name')
expect(consoleProps['Applied To'][2]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To'][2]).to.have.property('id').that.equals('age')
expect(consoleProps.Command).to.equal('last')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Selector).to.equal('')
expect(consoleProps.Yielded).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded).to.have.property('id').that.equals('age')
})
})
it('.next()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#input').next()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('next', logs, 'foobar.com')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To']).to.have.property('id').that.equals('input')
expect(consoleProps.Command).to.equal('next')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Selector).to.equal('')
expect(consoleProps.Yielded).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded).to.have.property('id').that.equals('name')
})
})
it('.nextAll()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#input').nextAll()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('nextAll', logs, 'foobar.com')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To']).to.have.property('id').that.equals('input')
expect(consoleProps.Command).to.equal('nextAll')
expect(consoleProps.Elements).to.equal(2)
expect(consoleProps.Selector).to.equal('')
expect(consoleProps.Yielded.length).to.equal(2)
expect(consoleProps.Yielded[0]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded[0]).to.have.property('id').that.equals('name')
expect(consoleProps.Yielded[1]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded[1]).to.have.property('id').that.equals('age')
})
})
it('.nextUntil()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#input').nextUntil('#age')
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('nextUntil', logs, 'foobar.com')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To']).to.have.property('id').that.equals('input')
expect(consoleProps.Command).to.equal('nextUntil')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Selector).to.equal('#age')
expect(consoleProps.Yielded).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded).to.have.property('id').that.equals('name')
})
})
it('.not()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id>input').not('#age')
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('not', logs, 'foobar.com')
expect(consoleProps['Applied To'].length).to.equal(3)
expect(consoleProps['Applied To'][0]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To'][0]).to.have.property('id').that.equals('input')
expect(consoleProps['Applied To'][1]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To'][1]).to.have.property('id').that.equals('name')
expect(consoleProps['Applied To'][2]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To'][2]).to.have.property('id').that.equals('age')
expect(consoleProps.Command).to.equal('not')
expect(consoleProps.Elements).to.equal(2)
expect(consoleProps.Selector).to.equal('#age')
expect(consoleProps.Yielded.length).to.equal(2)
expect(consoleProps.Yielded[0]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded[0]).to.have.property('id').that.equals('input')
expect(consoleProps.Yielded[1]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded[1]).to.have.property('id').that.equals('name')
})
})
it('.parent()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id').parent()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('parent', logs, 'foobar.com')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('FORM')
expect(consoleProps['Applied To']).to.have.property('id').that.equals('by-id')
expect(consoleProps.Command).to.equal('parent')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Selector).to.equal('')
expect(consoleProps.Yielded).to.have.property('tagName').that.equals('DIV')
expect(consoleProps.Yielded).to.have.property('id').that.equals('dom')
})
})
it('.parents()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id').parents()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('parents', logs, 'foobar.com')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('FORM')
expect(consoleProps['Applied To']).to.have.property('id').that.equals('by-id')
expect(consoleProps.Command).to.equal('parents')
expect(consoleProps.Elements).to.equal(3)
expect(consoleProps.Selector).to.equal('')
expect(consoleProps.Yielded.length).to.equal(3)
expect(consoleProps.Yielded[0]).to.have.property('tagName').that.equals('DIV')
expect(consoleProps.Yielded[0]).to.have.property('id').that.equals('dom')
expect(consoleProps.Yielded[1]).to.have.property('tagName').that.equals('BODY')
expect(consoleProps.Yielded[2]).to.have.property('tagName').that.equals('HTML')
})
})
it('.parentsUntil()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#by-id').parentsUntil('body')
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('parentsUntil', logs, 'foobar.com')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('FORM')
expect(consoleProps['Applied To']).to.have.property('id').that.equals('by-id')
expect(consoleProps.Command).to.equal('parentsUntil')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Selector).to.equal('body')
expect(consoleProps.Yielded).to.have.property('tagName').that.equals('DIV')
expect(consoleProps.Yielded).to.have.property('id').that.equals('dom')
})
})
it('.prev()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#age').prev()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('prev', logs, 'foobar.com')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To']).to.have.property('id').that.equals('age')
expect(consoleProps.Command).to.equal('prev')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Selector).to.equal('')
expect(consoleProps.Yielded).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded).to.have.property('id').that.equals('name')
})
})
it('.prevAll()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#age').prevAll()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('prevAll', logs, 'foobar.com')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To']).to.have.property('id').that.equals('age')
expect(consoleProps.Command).to.equal('prevAll')
expect(consoleProps.Elements).to.equal(2)
expect(consoleProps.Selector).to.equal('')
expect(consoleProps.Yielded.length).to.equal(2)
expect(consoleProps.Yielded[0]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded[0]).to.have.property('id').that.equals('name')
expect(consoleProps.Yielded[1]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded[1]).to.have.property('id').that.equals('input')
})
})
it('.prevUntil()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#age').prevUntil('#input')
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('prevUntil', logs, 'foobar.com')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To']).to.have.property('id').that.equals('age')
expect(consoleProps.Command).to.equal('prevUntil')
expect(consoleProps.Elements).to.equal(1)
expect(consoleProps.Selector).to.equal('#input')
expect(consoleProps.Yielded).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded).to.have.property('id').that.equals('name')
})
})
it('.siblings()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('#input').siblings()
})
cy.shouldWithTimeout(() => {
// in the case of some firefox browsers, the document state is left in a cross origin context when running these assertions
// set to context to undefined to run the assertions
if (Cypress.isBrowser('firefox')) {
cy.state('document', undefined)
}
const { consoleProps } = findCrossOriginLogs('siblings', logs, 'foobar.com')
expect(consoleProps['Applied To']).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps['Applied To']).to.have.property('id').that.equals('input')
expect(consoleProps.Command).to.equal('siblings')
expect(consoleProps.Elements).to.equal(2)
expect(consoleProps.Selector).to.equal('')
expect(consoleProps.Yielded.length).to.equal(2)
expect(consoleProps.Yielded[0]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded[0]).to.have.property('id').that.equals('name')
expect(consoleProps.Yielded[1]).to.have.property('tagName').that.equals('INPUT')
expect(consoleProps.Yielded[1]).to.have.property('id').that.equals('age')
})
})
})
})
@@ -0,0 +1,66 @@
context('cy.origin unsupported commands', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
it('cy.route() method is deprecated', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.equal('`cy.route()` has been deprecated and its use is not supported in the `cy.origin()` callback. Consider using `cy.intercept()` (outside of the callback) instead.')
expect(err.docsUrl).to.equal('https://on.cypress.io/intercept')
done()
})
cy.origin('http://foobar.com:3500', () => {
cy.route('api')
})
})
it('cy.server() method is deprecated', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.equal('`cy.server()` has been deprecated and its use is not supported in the `cy.origin()` callback. Consider using `cy.intercept()` (outside of the callback) instead.')
expect(err.docsUrl).to.equal('https://on.cypress.io/intercept')
done()
})
cy.origin('http://foobar.com:3500', () => {
cy.server()
})
})
it('cy.origin() is not yet supported', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.equal('`cy.origin()` use is not currently supported in the `cy.origin()` callback, but is planned for a future release. Please 👍 the following issue and leave a comment with your use-case:')
expect(err.docsUrl).to.equal('https://on.cypress.io/github-issue/20718')
done()
})
cy.origin('http://foobar.com:3500', () => {
cy.origin('barbaz.com', () => {})
})
})
it('cy.intercept() is not supported', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.equal('`cy.intercept()` use is not supported in the `cy.origin()` callback. Consider using it outside of the callback instead. Otherwise, please 👍 the following issue and leave a comment with your use-case:')
expect(err.docsUrl).to.equal('https://on.cypress.io/github-issue/20720')
done()
})
cy.origin('http://foobar.com:3500', () => {
cy.intercept('/foo')
})
})
it('cy.session() is not supported', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.equal('`cy.session()` use is not supported in the `cy.origin()` callback. Consider using it outside of the callback instead. Otherwise, please 👍 the following issue and leave a comment with your use-case:')
expect(err.docsUrl).to.equal('https://on.cypress.io/github-issue/20721')
done()
})
cy.origin('http://foobar.com:3500', () => {
cy.session('/foo')
})
})
})
@@ -0,0 +1,210 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin viewport', () => {
it('syncs the viewport from the primary to secondary', () => {
// change the viewport in the primary first
cy.viewport(320, 480)
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
cy.origin('http://foobar.com:3500', () => {
const viewportChangedSpy = cy.spy()
cy.on('viewport:changed', viewportChangedSpy)
// changing the viewport to the same size shouldn't do anything
cy.viewport(320, 480).then(() => {
expect(viewportChangedSpy).not.to.be.called
})
cy.window().then((win) => {
expect(win.innerWidth).to.equal(320)
expect(win.innerHeight).to.equal(480)
})
})
})
context('with out pre-set viewport', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
context('.viewport()', () => {
it('changes the viewport', () => {
cy.origin('http://foobar.com:3500', () => {
cy.window().then((win) => {
expect(win.innerHeight).to.equal(660)
expect(win.innerWidth).to.equal(1000)
})
cy.viewport(320, 480)
cy.window().then((win) => {
expect(win.innerHeight).to.equal(480)
expect(win.innerWidth).to.equal(320)
})
})
})
it('resets the viewport between tests', () => {
cy.origin('http://foobar.com:3500', () => {
cy.window().then((win) => {
expect(win.innerHeight).to.equal(660)
expect(win.innerWidth).to.equal(1000)
})
})
})
context('cy.on(\'viewport:changed\')', () => {
it('calls viewport:changed handler in cy.origin', () => {
cy.origin('http://foobar.com:3500', () => {
const viewportChangedSpy = cy.spy()
cy.on('viewport:changed', viewportChangedSpy)
cy.viewport(320, 480).then(() => {
expect(viewportChangedSpy).to.be.calledOnce
})
})
})
it('does NOT call viewport:changed handler of primary', () => {
const viewportChangedSpy = cy.spy()
cy.on('viewport:changed', viewportChangedSpy)
cy.origin('http://foobar.com:3500', () => {
cy.viewport(320, 480)
}).then(() => {
expect(viewportChangedSpy).not.to.be.called
})
})
})
context('Cypress.on(\'viewport:changed\')', () => {
let viewportChangedSpyPrimary
before(() => {
viewportChangedSpyPrimary = cy.spy()
cy.origin('http://foobar.com:3500', () => {
// using global since a function can't be passed to cy.origin
// and we need to be able to remove the listener in the 'after' hook
globalThis.viewportChangedSpySecondary = cy.spy()
})
})
after(() => {
Cypress.off('viewport:changed', viewportChangedSpyPrimary)
cy.origin('http://foobar.com:3500', () => {
Cypress.off('viewport:changed', globalThis.viewportChangedSpySecondary)
})
delete globalThis.viewportChangedSpySecondary
})
it('calls viewport:changed handler in cy.origin', () => {
cy.origin('http://foobar.com:3500', () => {
Cypress.on('viewport:changed', globalThis.viewportChangedSpySecondary)
cy.viewport(320, 480).then(() => {
expect(globalThis.viewportChangedSpySecondary).to.be.calledOnce
})
})
})
it('does NOT call viewport:changed handler of primary', () => {
Cypress.on('viewport:changed', viewportChangedSpyPrimary)
cy.origin('http://foobar.com:3500', () => {
cy.viewport(320, 480)
}).then(() => {
expect(viewportChangedSpyPrimary).not.to.be.called
})
})
})
it('syncs the viewport from the secondary to primary', () => {
const viewportChangedSpy = cy.spy()
cy.on('viewport:changed', viewportChangedSpy)
cy.origin('http://foobar.com:3500', () => {
// change the viewport in the secondary first
cy.viewport(320, 480)
cy.window().then((win) => {
win.location.href = 'http://localhost:3500/fixtures/multi-domain.html'
})
})
// changing the viewport to the same size shouldn't do anything
cy.viewport(320, 480).then(() => {
expect(viewportChangedSpy).not.to.be.called
})
cy.window().then((win) => {
expect(win.innerWidth).to.equal(320)
expect(win.innerHeight).to.equal(480)
})
})
it('syncs the viewport across multiple origins', () => {
cy.origin('http://foobar.com:3500', () => {
cy.viewport(320, 480)
cy.window().then((win) => {
expect(win.innerWidth).to.equal(320)
expect(win.innerHeight).to.equal(480)
})
})
cy.window().then((win) => {
win.location.href = 'http://www.idp.com:3500/fixtures/multi-domain.html'
})
cy.origin('http://idp.com:3500', () => {
const viewportChangedSpy = cy.spy()
cy.on('viewport:changed', viewportChangedSpy)
// changing the viewport to the same size shouldn't do anything
cy.viewport(320, 480).then(() => {
expect(viewportChangedSpy).not.to.be.called
})
cy.window().then((win) => {
expect(win.innerWidth).to.equal(320)
expect(win.innerHeight).to.equal(480)
})
})
})
})
context('#consoleProps', () => {
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.viewport()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.viewport(320, 480)
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('viewport', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('viewport')
expect(consoleProps.Width).to.equal(320)
expect(consoleProps.Height).to.equal(480)
})
})
})
})
})
@@ -0,0 +1,43 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin waiting', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
it('.wait()', () => {
cy.origin('http://foobar.com:3500', () => {
const delay = cy.spy(Cypress.Promise, 'delay')
cy.wait(50).then(() => {
expect(delay).to.be.calledWith(50, 'wait')
})
})
})
context('#consoleProps', () => {
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.wait()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.wait(200)
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('wait', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('wait')
expect(consoleProps).to.have.property('Waited For').to.equal('200ms before continuing')
})
})
})
})
@@ -0,0 +1,77 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin window', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="dom-link"]').click()
})
it('.window()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.window().should('have.property', 'top')
})
})
it('.document()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.document().should('have.property', 'charset').and('eq', 'UTF-8')
})
})
it('.title()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.title().should('include', 'DOM Fixture')
})
})
context('#consoleProps', () => {
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.window()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.window()
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('window', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('window')
expect(consoleProps.Yielded).to.be.null
})
})
it('.document()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.document()
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('document', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('document')
expect(consoleProps.Yielded).to.be.null
})
})
it('.title()', () => {
cy.origin('http://foobar.com:3500', () => {
cy.title()
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('title', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('title')
expect(consoleProps.Yielded).to.equal('DOM Fixture')
})
})
})
})
@@ -0,0 +1,95 @@
describe('cy.origin - cookie login', () => {
const verifyLoggedIn = (username) => {
cy.get('h1')
.invoke('text')
.should('equal', `Welcome, ${username}!`)
}
it('works in a session', () => {
cy.session('ZJohnson', () => {
cy.visit('/fixtures/multi-domain.html')
cy.get('[data-cy="cookie-login"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="username"]').type('ZJohnson')
cy.get('[data-cy="login"]').click()
})
}, {
validate () {
cy.getCookie('user').its('value').should('equal', 'ZJohnson')
},
})
cy.visit('/welcome')
verifyLoggedIn('ZJohnson')
})
describe('SameSite handling', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('[data-cy="cookie-login"]').click()
})
it('works with no SameSite, no Secure', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="username"]').type('AJohnson')
cy.get('[data-cy="login"]').click()
})
verifyLoggedIn('AJohnson')
})
it('works with SameSite=None, Secure', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="sameSite"]').select('None')
cy.get('[data-cy="secure"]').check()
cy.get('[data-cy="username"]').type('BJohnson')
cy.get('[data-cy="login"]').click()
})
verifyLoggedIn('BJohnson')
})
it('works with SameSite=None, no Secure', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="sameSite"]').select('None')
cy.get('[data-cy="username"]').type('CJohnson')
cy.get('[data-cy="login"]').click()
})
verifyLoggedIn('CJohnson')
})
it('works with SameSite=Lax, Secure', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="sameSite"]').select('Lax')
cy.get('[data-cy="secure"]').check()
cy.get('[data-cy="username"]').type('DJohnson')
cy.get('[data-cy="login"]').click()
})
verifyLoggedIn('DJohnson')
})
it('works with SameSite=Strict, Secure', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="sameSite"]').select('Strict')
cy.get('[data-cy="secure"]').check()
cy.get('[data-cy="username"]').type('EJohnson')
cy.get('[data-cy="login"]').click()
})
verifyLoggedIn('EJohnson')
})
it('works with invalid SameSite, Secure', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="sameSite"]').select('Invalid')
cy.get('[data-cy="secure"]').check()
cy.get('[data-cy="username"]').type('FJohnson')
cy.get('[data-cy="login"]').click()
})
verifyLoggedIn('FJohnson')
})
})
})
@@ -0,0 +1,148 @@
describe('cy.origin logging', () => {
const { _ } = Cypress
it('groups callback commands on a passing test', () => {
const logs: any[] = []
cy.on('log:added', (attrs) => {
logs.push(attrs)
})
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="dom-check"]').invoke('text')
})
cy.log('after').should(() => {
const originLog = _.find(logs, { name: 'origin' })
const getLog = _.find(logs, { name: 'get', message: '[data-cy="dom-check"]' })
const invokeLog = _.find(logs, { name: 'invoke', message: '.text()' })
const newUrlLog = _.find(logs, { name: 'new url' })
const logLog = _.find(logs, { name: 'log' })
expect(originLog.groupStart).to.be.true
expect(getLog.group).to.equal(originLog.id)
expect(invokeLog.group).to.equal(originLog.id)
expect(newUrlLog.group).to.equal(originLog.id)
expect(logLog.group).to.be.undefined // ensure the group has ended
})
})
it('logs cy.origin as group when failing with validation failure', (done) => {
const logs: any[] = []
cy.on('log:added', (attrs) => {
logs.push(attrs)
})
cy.on('fail', () => {
const originLog = _.find(logs, { name: 'origin' })
expect(originLog.groupStart).to.be.true
done()
})
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
// @ts-ignore
cy.origin(false, () => {})
})
it('logs cy.origin as group when failing with serialization failure', (done) => {
const logs: any[] = []
cy.on('log:added', (attrs) => {
logs.push(attrs)
})
cy.on('fail', () => {
const originLog = _.find(logs, { name: 'origin' })
expect(originLog.groupStart).to.be.true
done()
})
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
const options = { args: { div: Cypress.$('div') } }
cy.origin('http://foobar.com:3500', options, () => {})
})
it('groups callback commands when failing with inner command failure', (done) => {
const logs: any[] = []
cy.on('log:added', (attrs) => {
logs.push(attrs)
})
cy.on('fail', () => {
const originLog = _.find(logs, { name: 'origin' })
const getLog = _.find(logs, { name: 'get', message: '[data-cy="dom-check"]' })
const invokeLog = _.find(logs, { name: 'invoke', message: '.text()' })
const newUrlLog = _.find(logs, { name: 'new url' })
const failingGetLog = _.find(logs, { name: 'get', message: '#does-not-exist' })
expect(originLog.groupStart).to.be.true
expect(getLog.group).to.equal(originLog.id)
expect(invokeLog.group).to.equal(originLog.id)
expect(newUrlLog.group).to.equal(originLog.id)
expect(failingGetLog.group).to.equal(originLog.id)
done()
})
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="dom-check"]').invoke('text')
cy.get('#does-not-exist', { timeout: 1 })
})
cy.log('after')
})
it('groups callback commands when failing with async failure', (done) => {
const logs: any[] = []
cy.on('log:added', (attrs) => {
logs.push(attrs)
})
cy.on('fail', () => {
const originLog = _.find(logs, { name: 'origin' })
const getLog = _.find(logs, { name: 'get', message: '[data-cy="dom-check"]' })
const invokeLog = _.find(logs, { name: 'invoke', message: '.text()' })
const newUrlLog = _.find(logs, { name: 'new url' })
expect(originLog.groupStart).to.be.true
expect(getLog.group).to.equal(originLog.id)
expect(invokeLog.group).to.equal(originLog.id)
expect(newUrlLog.group).to.equal(originLog.id)
done()
})
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="dom-check"]').invoke('text').then(() => {
setTimeout(() => {
throw new Error('async error')
})
})
cy.wait(300)
})
cy.log('after')
})
})
@@ -0,0 +1,251 @@
describe('cy.origin', () => {
it('passes viewportWidth/Height state to the secondary origin', () => {
const expectedViewport = [320, 480]
cy.viewport(320, 480).then(() => {
const primaryViewport = [cy.state('viewportWidth'), cy.state('viewportHeight')]
expect(primaryViewport).to.deep.equal(expectedViewport)
})
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
cy.origin('http://foobar.com:3500', { args: expectedViewport }, (expectedViewport) => {
const secondaryViewport = [cy.state('viewportWidth'), cy.state('viewportHeight')]
expect(secondaryViewport).to.deep.equal(expectedViewport)
})
})
context('withBeforeEach', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
it('runs commands in secondary origin', () => {
cy.origin('http://foobar.com:3500', () => {
cy
.get('[data-cy="dom-check"]')
.invoke('text')
.should('equal', 'From a secondary origin')
})
cy.log('after cy.origin')
})
it('passes runnable state to the secondary origin', () => {
const runnable = cy.state('runnable')
const expectedRunnable = {
clearTimeout: null,
isPending: null,
resetTimeout: null,
timeout: null,
id: runnable.id,
_currentRetry: runnable._currentRetry,
_timeout: 4000,
type: 'test',
title: 'passes runnable state to the secondary origin',
titlePath: [
'cy.origin',
'withBeforeEach',
'passes runnable state to the secondary origin',
],
parent: {
id: runnable.parent.id,
type: 'suite',
title: 'withBeforeEach',
titlePath: [
'withBeforeEach',
],
parent: {
id: runnable.parent.parent.id,
type: 'suite',
title: '',
titlePath: undefined,
ctx: {},
},
ctx: {},
},
ctx: {},
}
cy.origin('http://foobar.com:3500', { args: expectedRunnable }, (expectedRunnable) => {
const actualRunnable = cy.state('runnable')
expect(actualRunnable.titlePath()).to.deep.equal(expectedRunnable.titlePath)
expectedRunnable.titlePath = actualRunnable.titlePath
expect(actualRunnable.title).to.equal(expectedRunnable.title)
expect(actualRunnable.id).to.equal(expectedRunnable.id)
expect(actualRunnable.ctx).to.deep.equal(expectedRunnable.ctx)
expect(actualRunnable._timeout).to.equal(expectedRunnable._timeout)
expect(actualRunnable.type).to.equal(expectedRunnable.type)
expect(actualRunnable.callback).to.exist
expect(actualRunnable.timeout).to.exist
expect(actualRunnable.parent.title).to.equal(expectedRunnable.parent.title)
expect(actualRunnable.parent.type).to.equal(expectedRunnable.parent.type)
})
})
it('handles querying nested elements', () => {
cy.origin('http://foobar.com:3500', () => {
cy
.get('form button')
.invoke('text')
.should('equal', 'Submit')
})
cy.log('after cy.origin')
})
it('sets up window.Cypress in secondary origin', () => {
cy.origin('http://foobar.com:3500', () => {
cy
.get('[data-cy="cypress-check"]')
.invoke('text')
.should('equal', 'Has window.Cypress')
})
})
describe('data argument', () => {
it('passes object to callback function', () => {
cy.origin('http://foobar.com:3500', { args: { foo: 'foo', bar: 'bar' } }, ({ foo, bar }) => {
expect(foo).to.equal('foo')
expect(bar).to.equal('bar')
})
})
it('passes array to callback function', () => {
cy.origin('http://foobar.com:3500', { args: ['foo', 'bar'] }, ([foo, bar]) => {
expect(foo).to.equal('foo')
expect(bar).to.equal('bar')
})
})
it('passes string to callback function', () => {
cy.origin('http://foobar.com:3500', { args: 'foo' }, (foo) => {
expect(foo).to.equal('foo')
})
})
it('passes number to callback function', () => {
cy.origin('http://foobar.com:3500', { args: 1 }, (num) => {
expect(num).to.equal(1)
})
})
it('passes boolean to callback function', () => {
cy.origin('http://foobar.com:3500', { args: true }, (bool) => {
expect(bool).to.be.true
})
})
it('passes mixed types to callback function', () => {
cy.origin('http://foobar.com:3500', { args: { foo: 'foo', num: 1, bool: true } }, ({ foo, num, bool }) => {
expect(foo).to.equal('foo')
expect(num).to.equal(1)
expect(bool).to.be.true
})
})
})
describe('errors', () => {
it('propagates secondary origin errors to the primary that occur within the test', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include('variable is not defined')
expect(err.message).to.include(`Variables must either be defined within the \`cy.origin()\` command or passed in using the args option.`)
expect(err.stack).to.include(`Variables must either be defined within the \`cy.origin()\` command or passed in using the args option.`)
// make sure that the secondary origin failures do NOT show up as spec failures or AUT failures
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
expect(err.codeFrame).to.exist
expect(err.codeFrame!.frame).to.include('cy.origin')
done()
})
const variable = 'string'
cy.origin('http://foobar.com:3500', () => {
cy.log(variable)
})
})
it('propagates thrown errors in the secondary origin back to the primary w/ done', (done) => {
cy.on('fail', (e) => {
expect(e.message).to.equal('oops')
done()
})
cy.origin('http://foobar.com:3500', () => {
throw 'oops'
})
})
it('propagates thrown errors in the secondary origin back to the primary w/o done', () => {
return new Promise((resolve) => {
cy.on('fail', (e) => {
expect(e.message).to.equal('oops')
resolve(undefined)
})
cy.origin('http://foobar.com:3500', () => {
throw 'oops'
})
})
})
it('receives command failures from the secondary origin', (done) => {
const timeout = 50
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out retrying after ${timeout}ms: Expected to find element: \`#doesnt-exist\`, but never found it`)
// make sure that the secondary origin failures do NOT show up as spec failures or AUT failures
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
done()
})
cy.origin('http://foobar.com:3500', { args: timeout }, (timeout) => {
cy.get('#doesnt-exist', {
timeout,
})
})
})
it('receives command failures from the secondary origin with the default timeout', { defaultCommandTimeout: 50 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out retrying after 50ms: Expected to find element: \`#doesnt-exist\`, but never found it`)
// make sure that the secondary origin failures do NOT show up as spec failures or AUT failures
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
done()
})
cy.origin('http://foobar.com:3500', () => {
cy.get('#doesnt-exist')
})
})
it('has non serializable arguments', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`This is likely because the arguments specified are not serializable. Note that functions and DOM objects cannot be serialized.`)
expect(err.stack).to.include(`This is likely because the arguments specified are not serializable. Note that functions and DOM objects cannot be serialized.`)
// make sure that the secondary origin failures do NOT show up as spec failures or AUT failures
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
expect(err.codeFrame).to.exist
expect(err.codeFrame!.frame).to.include('cy.origin')
done()
})
const variable = () => {}
cy.origin('http://idp.com:3500', { args: variable }, (variable) => {
variable()
})
})
})
})
})
@@ -0,0 +1,271 @@
['config', 'env'].forEach((fnName) => {
describe(`cy.origin- Cypress.${fnName}()`, () => {
const USED_KEYS = {
foo: 'cy-origin-foo',
bar: 'cy-origin-bar',
baz: 'cy-origin-baz',
qux: 'cy-origin-qux',
quux: 'cy-origin-quux',
unserializable: 'cy-origin-unserializable',
error: 'cy-origin-error',
}
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
afterEach(() => {
// @ts-ignore
window.top.__cySkipValidateConfig = true
})
if (fnName === 'config') {
it(`throws if mutating read-only config values with Cypress.config()`, () => {
return new Promise<void>((resolve) => {
// @ts-ignore
window.top.__cySkipValidateConfig = false
cy.on('fail', (err) => {
expect(err.message).to.include('`Cypress.config()` cannot mutate option `chromeWebSecurity` because it is a read-only property.')
resolve()
})
cy.origin('http://foobar.com:3500', () => {
// @ts-ignore
Cypress.config('chromeWebSecurity', false)
})
})
})
}
context('serializable', () => {
it(`syncs initial Cypress.${fnName}() from the primary origin to the secondary (synchronously)`, () => {
Cypress[fnName](USED_KEYS.foo, 'bar')
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
const bar = Cypress[fnName](USED_KEYS.foo)
expect(bar).to.equal('bar')
})
})
it(`syncs serializable values in the Cypress.${fnName}() again to the secondary even after spec bridge is created`, () => {
Cypress[fnName](USED_KEYS.foo, 'baz')
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
const baz = Cypress[fnName](USED_KEYS.foo)
expect(baz).to.equal('baz')
})
})
it(`syncs serializable Cypress.${fnName}() values outwards from secondary (synchronously)`, () => {
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
Cypress[fnName](USED_KEYS.bar, 'baz')
}).then(() => {
const baz = Cypress[fnName](USED_KEYS.bar)
expect(baz).to.equal('baz')
})
})
it(`syncs serializable Cypress.${fnName}() values outwards from secondary even if the value is undefined`, () => {
Cypress[fnName](USED_KEYS.foo, 'bar')
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
Cypress[fnName](USED_KEYS.foo, undefined)
}).then(() => {
expect(Cypress[fnName]('foo')).to.be.undefined
})
})
it(`syncs serializable Cypress.${fnName}() values outwards from secondary (commands/async)`, () => {
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
cy.then(() => {
Cypress[fnName](USED_KEYS.bar, 'quux')
})
}).then(() => {
const quux = Cypress[fnName](USED_KEYS.bar)
expect(quux).to.equal('quux')
})
})
it(`persists Cypress.${fnName}() changes made in the secondary, assuming primary has not overwritten with a serializable value`, () => {
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
const quux = Cypress[fnName](USED_KEYS.bar)
expect(quux).to.equal('quux')
})
})
it(`does NOT sync Cypress.${fnName}() changes made in the secondary after the command queue is finished and the callback window is closed`, {
[USED_KEYS.baz]: undefined,
env: {
[USED_KEYS.baz]: undefined,
},
}, () => {
return new Promise<void>((resolve) => {
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
setTimeout(() => {
// this value STILL gets set, but will be blown away on the next origin with whatever exists in the primary
Cypress[fnName](USED_KEYS.baz, 'qux')
}, 100)
Cypress[fnName](USED_KEYS.baz, 'quux')
}).then(() => {
const quux = Cypress[fnName](USED_KEYS.baz)
expect(quux).to.equal('quux')
resolve()
})
})
})
it('overwrites different values in secondary if one exists in the primary', {
[USED_KEYS.baz]: 'quux',
env: {
[USED_KEYS.baz]: 'quux',
},
}, () => {
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
// in previous test, 'baz' was set to 'qux' after the callback window was closed.
// this should be overwritten by 'quux' that exists in the primary
const quux = Cypress[fnName](USED_KEYS.baz)
expect(quux).to.equal('quux')
})
})
it(`overwrites different values in secondary, even if the Cypress.${fnName}() value does not exist in the primary`, {
[USED_KEYS.baz]: undefined,
env: {
[USED_KEYS.baz]: undefined,
},
}, () => {
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
const isNowUndefined = Cypress[fnName](USED_KEYS.baz)
expect(isNowUndefined).to.be.undefined
})
})
})
context('unserializable', () => {
const unserializableFunc = () => undefined
it('does not sync unserializable values from the primary to the secondary', () => {
Cypress[fnName](USED_KEYS.unserializable, unserializableFunc)
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
const isUndefined = Cypress[fnName](USED_KEYS.unserializable)
expect(isUndefined).to.be.undefined
})
})
it('does not overwrite unserializable values in the primary when none exist in the secondary', () => {
Cypress[fnName](USED_KEYS.unserializable, unserializableFunc)
cy.origin('http://foobar.com:3500', () => undefined)
const isFunc = Cypress[fnName](USED_KEYS.unserializable)
expect(isFunc).to.equal(unserializableFunc)
})
it('overwrites unserializable values in the primary when serializable values of same key exist in secondary', () => {
Cypress[fnName](USED_KEYS.unserializable, unserializableFunc)
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
Cypress[fnName](USED_KEYS.unserializable, undefined)
}).then(() => {
const isNowUndefined = Cypress[fnName](USED_KEYS.unserializable)
expect(isNowUndefined).to.be.undefined
})
})
it('overwrites unserializable values in the secondary when serializable values of same key exist in primary', () => {
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
const unserializableFuncSecondary = () => undefined
Cypress[fnName](USED_KEYS.unserializable, unserializableFuncSecondary)
}).then(() => {
Cypress[fnName](USED_KEYS.unserializable, undefined)
})
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
const isUndefined = Cypress[fnName](USED_KEYS.unserializable)
expect(isUndefined).to.be.undefined
}).then(() => {
const isUndefined = Cypress[fnName](USED_KEYS.unserializable)
expect(isUndefined).to.be.undefined
})
})
it('does not overwrite unserializable values in the primary when unserializable values of same key exist in secondary', () => {
Cypress[fnName](USED_KEYS.unserializable, unserializableFunc)
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
const unserializableFuncSecondary = () => undefined
Cypress[fnName](USED_KEYS.unserializable, unserializableFuncSecondary)
}).then(() => {
const isFunc = Cypress[fnName](USED_KEYS.unserializable)
expect(isFunc).to.equal(unserializableFunc)
})
})
it('does not try to merge objects that contain unserializable values', () => {
const partiallyUnserializableObject = {
a: 1,
b: document.createElement('h1'),
}
Cypress[fnName](USED_KEYS.unserializable, partiallyUnserializableObject)
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
const doesNotContainPartialAProperty = Cypress[fnName](USED_KEYS.unserializable)
expect(doesNotContainPartialAProperty?.a).to.be.undefined
Cypress[fnName](USED_KEYS.unserializable, {
a: 3,
c: document.createElement('h1'),
})
}).then(() => {
const isPartiallyUnserializableObject = Cypress[fnName](USED_KEYS.unserializable)
expect(isPartiallyUnserializableObject.a).to.equal(1)
})
})
})
context('structuredClone()', () => {
it('(firefox) uses native structuredClone in firefox and does NOT serialize Error objects in config', {
browser: 'firefox',
}, function () {
Cypress[fnName](USED_KEYS.error, new Error('error'))
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
const isUndefined = Cypress[fnName](USED_KEYS.error)
expect(isUndefined).to.be.undefined
})
})
// NOTE: chrome 98 and above uses a native structuredClone() method, but that method CAN clone Error objects
it('(chromium) uses ponyfilled or native structuredClone that can serialize Error objects in config', {
browser: { family: 'chromium' },
}, () => {
Cypress[fnName](USED_KEYS.error, new Error('error'))
cy.origin('http://foobar.com:3500', { args: { fnName, USED_KEYS } }, ({ fnName, USED_KEYS }) => {
const isErrorObj = Cypress[fnName](USED_KEYS.error)
// We preserve the error structure, but on preprocessing to the spec bridge, the error is converted to a flat object
expect(isErrorObj).to.be.an.instanceof(Object)
expect(isErrorObj.message).to.eq('error')
})
})
})
})
})
@@ -0,0 +1,244 @@
const { assertLogLength } = require('../../../support/utils')
describe('cy.origin Cypress API', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
context('Commands', () => {
it('adds a custom command', () => {
cy.origin('http://foobar.com:3500', () => {
// @ts-ignore
Cypress.Commands.add('foo', () => 'bar')
// @ts-ignore
cy.foo().should('equal', 'bar')
})
// persists added command through spec bridge
cy.origin('http://foobar.com:3500', () => {
// @ts-ignore
cy.foo().should('equal', 'bar')
})
})
it('overwrites an existing command in the spec bridge', () => {
cy.origin('http://foobar.com:3500', () => {
// @ts-ignore
Cypress.Commands.overwrite('foo', () => 'baz')
// @ts-ignore
cy.foo().should('equal', 'baz')
})
// persists overwritten command through spec bridge
cy.origin('http://foobar.com:3500', () => {
// @ts-ignore
cy.foo().should('equal', 'baz')
})
})
})
context('Keyboard', () => {
it('does NOT sync defaults', () => {
const defaults = Cypress.Keyboard.defaults({
keystrokeDelay: 30,
})
cy.origin('http://foobar.com:3500', { args: defaults }, (primaryKeyboardDefaults) => {
const crossOriginKeyboardDefaults = Cypress.Keyboard.defaults({})
expect(crossOriginKeyboardDefaults).to.not.deep.equal(primaryKeyboardDefaults)
})
})
it('allows a user to configure defaults', () => {
cy.origin('http://foobar.com:3500', () => {
const crossOriginKeyboardDefaults = Cypress.Keyboard.defaults({
keystrokeDelay: 60,
})
expect(crossOriginKeyboardDefaults).to.deep.include({
keystrokeDelay: 60,
})
})
// persists default configuration changes through spec bridge
cy.origin('http://foobar.com:3500', () => {
const crossOriginKeyboardDefaults = Cypress.Keyboard.defaults({})
expect(crossOriginKeyboardDefaults).to.deep.include({
keystrokeDelay: 60,
})
})
})
})
context('Screenshot', () => {
it('does NOT sync defaults', () => {
Cypress.Screenshot.defaults({
blackout: ['foo'],
overwrite: true,
onBeforeScreenshot: () => undefined,
onAfterScreenshot: () => undefined,
})
cy.origin('http://foobar.com:3500', () => {
const crossOriginScreenshotDefaults = Cypress.Screenshot.defaults({})
expect(crossOriginScreenshotDefaults).to.not.deep.include({
blackout: ['foo'],
overwrite: true,
onBeforeScreenshot: () => undefined,
onAfterScreenshot: () => undefined,
})
})
})
it('allows a user to configure defaults', () => {
cy.origin('http://foobar.com:3500', () => {
const crossOriginScreenshotDefaults = Cypress.Screenshot.defaults({
blackout: ['foo'],
overwrite: true,
})
expect(crossOriginScreenshotDefaults).to.deep.include({
blackout: ['foo'],
overwrite: true,
})
})
// persists default configuration changes through spec bridge
cy.origin('http://foobar.com:3500', () => {
const crossOriginScreenshotDefaults = Cypress.Screenshot.defaults({})
expect(crossOriginScreenshotDefaults).to.deep.include({
blackout: ['foo'],
overwrite: true,
})
})
})
})
context('dom', () => {
it('provides a sanity check that the dom API exists on Cypress.*', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="dom-check"]').then(($el) => {
expect(Cypress.dom.isAttached($el)).to.be.true
})
})
})
})
context('properties', () => {
it('has arch property synced from primary', () => {
cy.origin('http://foobar.com:3500', { args: Cypress.arch }, (theArch) => {
expect(Cypress.arch).to.equal(theArch)
})
})
it('has browser property synced from primary', () => {
cy.origin('http://foobar.com:3500', { args: Cypress.browser }, (theBrowser) => {
expect(Cypress.browser).to.deep.equal(theBrowser)
})
})
it('has currentTest property synced from primary', () => {
cy.origin('http://foobar.com:3500', { args: Cypress.currentTest }, (theCurrentTest) => {
expect(Cypress.currentTest).to.deep.equal(theCurrentTest)
})
})
it('has platform property synced from primary', () => {
cy.origin('http://foobar.com:3500', { args: Cypress.platform }, (thePlatform) => {
expect(Cypress.platform).to.equal(thePlatform)
})
})
it('has testingType property synced from primary', () => {
cy.origin('http://foobar.com:3500', { args: Cypress.testingType }, (theTestingType) => {
expect(Cypress.testingType).to.deep.equal(theTestingType)
})
})
it('has spec property synced from primary', () => {
cy.origin('http://foobar.com:3500', { args: Cypress.spec }, (theSpec) => {
expect(Cypress.spec).to.deep.equal(theSpec)
})
})
})
context('methods', () => {
it('isCy()', () => {
cy.origin('http://foobar.com:3500', () => {
expect(Cypress.isCy(cy)).to.be.true
})
})
it('isBrowser()', () => {
cy.origin('http://foobar.com:3500', { args: Cypress.browser }, (theBrowser) => {
expect(Cypress.isBrowser(theBrowser.name)).to.equal(true)
})
})
it('log()', () => {
const logs: Cypress.Log[] = []
cy.on('log:added', (attrs, log: Cypress.Log) => {
logs.push(log)
})
cy.origin('http://foobar.com:3500', () => {
Cypress.log({
name: 'log',
message: 'test log',
})
})
.then(() => {
assertLogLength(logs, 3)
expect(logs[1].get('name')).to.equal('log')
expect(logs[1].get('message')).to.equal('test log')
})
})
})
context('not supported', () => {
it('throws an error when a user attempts to configure Cypress.Server.defaults() inside of cy.origin', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.equal('`Cypress.Server.*` has been deprecated and its use is not supported in the `cy.origin()` callback. Consider using `cy.intercept()` (outside of the callback) instead.')
expect(err.docsUrl).to.equal('https://on.cypress.io/intercept')
done()
})
cy.origin('http://foobar.com:3500', () => {
Cypress.Server.defaults({})
})
})
it('throws an error when a user attempts to configure Cypress.Cookies.preserveOnce() inside of cy.origin', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.equal('`Cypress.Cookies.preserveOnce` use is not supported in the `cy.origin()` callback. Consider using `cy.session()` (outside of the callback) instead.')
expect(err.docsUrl).to.equal('https://on.cypress.io/session')
done()
})
cy.origin('http://foobar.com:3500', () => {
// @ts-ignore
Cypress.Cookies.preserveOnce({})
})
})
it('throws an error when a user attempts to call Cypress.session.clearAllSavedSessions() inside of multi-domain', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.equal('`Cypress.session.*` methods are not supported in the `cy.switchToDomain()` callback. Consider using them outside of the callback instead.')
expect(err.docsUrl).to.equal('https://on.cypress.io/session-api')
done()
})
cy.origin('http://foobar.com:3500', () => {
Cypress.session.clearAllSavedSessions()
})
})
})
})
@@ -0,0 +1,152 @@
describe('cy.origin', () => {
it('window:before:load event', () => {
cy.visit('/fixtures/multi-domain.html')
cy.on('window:before:load', (win: {testPrimaryOriginBeforeLoad: boolean}) => {
win.testPrimaryOriginBeforeLoad = true
})
cy.window().its('testPrimaryOriginBeforeLoad').should('be.true')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.on('window:before:load', (win: {testSecondaryWindowBeforeLoad: boolean}) => {
win.testSecondaryWindowBeforeLoad = true
})
cy.window().its('testSecondaryWindowBeforeLoad').should('be.true')
cy.window().its('testPrimaryOriginBeforeLoad').should('be.undefined')
cy
.get('[data-cy="window-before-load"]')
.invoke('text')
.should('equal', 'Window Before Load Called')
})
cy.visit('/fixtures/multi-domain.html')
cy.window().its('testPrimaryOriginBeforeLoad').should('be.true')
cy.window().its('testSecondaryWindowBeforeLoad').should('be.undefined')
})
describe('post window load events', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
it('form:submitted', () => {
cy.origin('http://foobar.com:3500', () => {
const afterFormSubmitted = new Promise<void>((resolve) => {
Cypress.once('form:submitted', (e) => {
const $form = cy.$$('form')
expect(e.target).to.eq($form.get(0))
resolve()
})
})
cy.get('form').submit()
cy.wrap(afterFormSubmitted)
})
})
it('window:before:unload', () => {
cy.origin('http://www.foobar.com:3500', () => {
const afterWindowBeforeUnload = new Promise<void>((resolve) => {
Cypress.once('window:before:unload', () => {
expect(location.host).to.equal('foobar.com:3500')
resolve()
})
})
cy.visit('/fixtures/multi-domain.html')
cy.wrap(afterWindowBeforeUnload)
})
})
it('window:unload', () => {
cy.origin('http://www.foobar.com:3500', () => {
const afterWindowUnload = new Promise<void>((resolve) => {
Cypress.once('window:unload', () => {
expect(location.host).to.equal('foobar.com:3500')
resolve()
})
})
cy.visit('/fixtures/multi-domain.html')
cy.wrap(afterWindowUnload)
})
})
it('window:alert', () => {
cy.origin('http://foobar.com:3500', () => {
const afterWindowAlert = new Promise<void>((resolve) => {
Cypress.once('window:alert', (text) => {
expect(location.host).to.equal('foobar.com:3500')
expect(`window:alert ${text}`).to.equal('window:alert the alert text')
resolve()
})
})
cy.get('[data-cy="alert"]').click()
cy.wrap(afterWindowAlert)
})
})
it('window:confirm', () => {
cy.origin('http://foobar.com:3500', () => {
const afterWindowConfirm = new Promise<void>((resolve) => {
Cypress.once('window:confirm', (text) => {
expect(location.host).to.equal('foobar.com:3500')
expect(`window:confirm ${text}`).to.equal('window:confirm the confirm text')
resolve()
})
})
cy.get('[data-cy="confirm"]').click()
cy.wrap(afterWindowConfirm)
})
})
it('window:confirmed - true when no window:confirm listeners return false', () => {
cy.origin('http://foobar.com:3500', () => {
const afterWindowConfirmed = new Promise<void>((resolve) => {
Cypress.once('window:confirmed', (text, returnedFalse) => {
expect(location.host).to.equal('foobar.com:3500')
expect(`window:confirmed ${text} - ${returnedFalse}`).to.equal('window:confirmed the confirm text - true')
resolve()
})
})
Cypress.on('window:confirm', () => {})
Cypress.on('window:confirm', () => {
return true
})
cy.get('[data-cy="confirm"]').click()
cy.wrap(afterWindowConfirmed)
})
})
it('window:confirmed - false when any window:confirm listeners return false', () => {
cy.origin('http://foobar.com:3500', () => {
const afterWindowConfirmed = new Promise<void>((resolve) => {
Cypress.once('window:confirmed', (text, returnedFalse) => {
expect(location.host).to.equal('foobar.com:3500')
expect(`window:confirmed ${text} - ${returnedFalse}`).to.equal('window:confirmed the confirm text - false')
resolve()
})
})
Cypress.on('window:confirm', () => {
return false
})
Cypress.on('window:confirm', () => {})
cy.get('[data-cy="confirm"]').click()
cy.wrap(afterWindowConfirmed)
})
})
})
})
@@ -0,0 +1,31 @@
// @ts-ignore
describe('cy.origin - rerun', { }, () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
it('successfully reruns tests', () => {
// @ts-ignore
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="dom-check"]')
})
.then(() => {
const top = window.top!
// @ts-ignore
if (!top.hasRunOnce) {
// @ts-ignore
top.hasRunOnce = true
// cause a rerun event to occur by triggering a hash change
top.dispatchEvent(new Event('hashchange'))
return
}
// this only executes after the test has been rerun
expect(true).to.be.true
})
})
})
@@ -0,0 +1,464 @@
describe('cy.origin - uncaught errors', () => {
beforeEach(() => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="errors-link"]').click()
})
describe('sync errors', () => {
it('appropriately reports negative assertions', (done) => {
cy.on('fail', (err) => {
expect(err.name).to.eq('AssertionError')
expect(err.message).to.include('expected true to be false')
done()
})
cy.origin('http://foobar.com:3500', () => {
cy.then(() => {
expect(true).to.be.false
})
})
})
it('fails the current test/command if sync errors are thrown from the cy.origin callback', (done) => {
const uncaughtExceptionSpy = cy.spy()
const r = cy.state('runnable')
cy.on('uncaught:exception', uncaughtExceptionSpy)
cy.on('fail', (err, runnable) => {
// TODO: we likely need to change the messaging around this error to make it clear to cy.origin users that
// this behavior is configurable with 'uncaught:exception', but it MUST be declared inside the cy.origin callback
// and that 'uncaught:exception' will NOT be triggered outside that callback (inside the primary origin)
// https://github.com/cypress-io/cypress/issues/20969
expect(err.name).to.eq('Error')
expect(err.message).to.include('sync error')
expect(err.message).to.include('The following error originated from your application code, not from Cypress.')
expect(err.message).to.not.include('https://on.cypress.io/uncaught-exception-from-application')
expect(err.docsUrl).to.deep.eq(['https://on.cypress.io/uncaught-exception-from-application'])
// lastly, make sure the `uncaught:exception' handler is NOT called in the primary
expect(uncaughtExceptionSpy).not.to.be.called
expect(runnable).to.be.equal(r)
done()
})
cy.origin('http://foobar.com:3500', () => {
cy.get('.trigger-sync-error').click()
})
})
it('returns false from cy.on(uncaught:exception), resulting in cy:fail to not be called in the primary and passes the test', () => {
const uncaughtExceptionSpy = cy.spy()
const failureSpy = cy.spy()
cy.on('uncaught:exception', uncaughtExceptionSpy)
cy.on('fail', failureSpy)
cy.origin('http://foobar.com:3500', () => {
cy.on('uncaught:exception', () => false)
cy.get('.trigger-sync-error').click()
}).then(() => {
expect(uncaughtExceptionSpy).not.to.be.called
expect(failureSpy).not.to.be.called
})
})
it('returns true from cy.on(uncaught:exception), resulting in cy:fail to be called in the primary', (done) => {
cy.on('fail', (err) => {
expect(err.name).to.eq('Error')
expect(err.message).to.include('sync error')
expect(err.message).to.include('The following error originated from your application code, not from Cypress.')
expect(err.message).to.not.include('https://on.cypress.io/uncaught-exception-from-application')
expect(err.docsUrl).to.deep.eq(['https://on.cypress.io/uncaught-exception-from-application'])
done()
})
cy.origin('http://foobar.com:3500', () => {
cy.once('uncaught:exception', () => true)
cy.get('.trigger-sync-error').click()
})
})
// if we mutate the error, the app's listeners for 'error' or
// 'unhandledrejection' will have our wrapped error instead of the original
it('original error is not mutated for "error" in the origin', () => {
cy.origin('http://foobar.com:3500', () => {
cy.once('uncaught:exception', () => false)
cy.get('.trigger-sync-error').click()
cy.get('.error-one').invoke('text').should('equal', 'sync error')
cy.get('.error-two').invoke('text').should('equal', 'sync error')
})
})
})
describe('async errors', () => {
it('fails the current test/command if async errors are thrown from the cy.origin callback', (done) => {
cy.on('fail', (err) => {
expect(err.name).to.eq('Error')
expect(err.message).to.include('setTimeout error')
expect(err.message).to.include('The following error originated from your test code, not from Cypress.')
// ensure error doesn't get wrapped twice
expect(err.message).not.to.include('> The following error originated')
done()
})
cy.origin('http://foobar.com:3500', () => {
setTimeout(() => {
throw new Error('setTimeout error')
}, 50)
// add the cy.wait here to keep commands streaming in, forcing the
// cy.origin callback window to be open long enough for the error to occur
cy.wait(250)
})
})
it('fails the current test/command if async errors are thrown from the secondary origin AUT', (done) => {
const uncaughtExceptionSpy = cy.spy()
const r = cy.state('runnable')
cy.on('uncaught:exception', uncaughtExceptionSpy)
cy.on('fail', (err, runnable) => {
expect(err.name).to.eq('Error')
expect(err.message).to.include('async error')
expect(err.message).to.include('The following error originated from your application code, not from Cypress.')
// ensure error doesn't get wrapped twice
expect(err.message).not.to.include('> The following error originated')
expect(err.message).to.not.include('https://on.cypress.io/uncaught-exception-from-application')
expect(err.docsUrl).to.deep.eq(['https://on.cypress.io/uncaught-exception-from-application'])
expect(uncaughtExceptionSpy).not.to.be.called
expect(runnable).to.be.equal(r)
done()
})
cy.origin('http://foobar.com:3500', () => {
cy.get('.trigger-async-error').click()
// add the cy.wait here to keep commands streaming in,
// forcing the cy.origin callback window to be open long enough for an async error to occur
cy.wait(1000)
})
})
it('passes the current test/command if async errors are thrown from the secondary origin AUT, but the cy.origin callback is finished running', () => {
const uncaughtExceptionSpy = cy.spy()
const failureSpy = cy.spy()
cy.on('uncaught:exception', uncaughtExceptionSpy)
cy.on('fail', failureSpy)
cy.origin('http://foobar.com:3500', () => {
// the async error here should be thrown AFTER the current command and test has finished, resulting in a passed test with no fail being triggered in the primary
cy.get('.trigger-async-error').click()
}).then(() => {
expect(uncaughtExceptionSpy).not.to.be.called
expect(failureSpy).not.to.be.called
})
})
it('fails the current test/command if async errors are thrown from the cy.origin callback after it is finished running', (done) => {
cy.once('fail', (err) => {
expect(err.name).to.eq('Error')
expect(err.message).to.include('setTimeout error')
expect(err.message).to.include('The following error originated from your test code, not from Cypress.')
// ensure error doesn't get wrapped twice
expect(err.message).not.to.include('> The following error originated')
done()
})
cy.origin('http://foobar.com:3500', () => {
setTimeout(() => {
throw new Error('setTimeout error')
}, 50)
})
cy.wait(250)
})
})
describe('unhandled rejections', () => {
it('unhandled rejection triggers uncaught:exception and has promise as third argument', () => {
cy.origin('http://foobar.com:3500', () => {
const r = cy.state('runnable')
const afterUncaughtException = new Promise<void>((resolve) => {
cy.once('uncaught:exception', (err, runnable, promise) => {
expect(err.stack).to.include('promise rejection')
expect(err.stack).to.include('one')
expect(err.stack).to.include('two')
expect(err.stack).to.include('three')
expect(runnable).to.be.equal(r)
expect(promise).to.be.a('promise')
resolve()
return false
})
})
cy.get('.trigger-unhandled-rejection').click()
cy.wrap(afterUncaughtException)
})
})
it('original error is not mutated for "unhandledrejection"', () => {
cy.origin('http://foobar.com:3500', () => {
cy.once('uncaught:exception', () => false)
cy.get('.trigger-unhandled-rejection').click()
cy.get('.error-one').invoke('text').should('equal', 'promise rejection')
cy.get('.error-two').invoke('text').should('equal', 'promise rejection')
})
})
it('fails the current test/command if a promise is rejected from the test code in cy.origin', (done) => {
cy.on('fail', (err) => {
expect(err.name).to.eq('Error')
expect(err.message).to.include('rejected promise')
expect(err.message).to.include('The following error originated from your test code, not from Cypress. It was caused by an unhandled promise rejection.')
expect(err.message).to.not.include('https://on.cypress.io/uncaught-exception-from-application')
done()
})
cy.origin('http://foobar.com:3500', () => {
Promise.reject(new Error('rejected promise'))
// add the cy.wait here to keep commands streaming in, forcing the
// cy.origin callback window to be open long enough for the error to occur
cy.wait(250)
})
})
it('fails the current test/command if a promise is rejected from the cy.origin callback after it is finished running', (done) => {
cy.on('fail', (err) => {
expect(err.name).to.eq('Error')
expect(err.message).to.include('rejected promise')
expect(err.message).to.include('The following error originated from your test code, not from Cypress. It was caused by an unhandled promise rejection.')
expect(err.message).to.not.include('https://on.cypress.io/uncaught-exception-from-application')
done()
})
cy.origin('http://foobar.com:3500', () => {
Promise.reject(new Error('rejected promise'))
})
cy.wait(250)
})
})
describe('unserializable errors', () => {
it('handles users throwing dom elements', (done) => {
cy.on('fail', (err) => {
expect(err.name).to.equal('CypressError')
expect(err.message).to.equal('`cy.origin()` could not serialize the thrown value. Please make sure the value being thrown is supported by the structured clone algorithm.')
done()
})
cy.origin('http://foobar.com:3500', () => {
throw document.createElement('h1')
})
})
it('handles users throwing functions', (done) => {
cy.on('fail', (err) => {
expect(err.name).to.equal('CypressError')
expect(err.message).to.equal('`cy.origin()` could not serialize the thrown value. Please make sure the value being thrown is supported by the structured clone algorithm.')
done()
})
cy.origin('http://foobar.com:3500', () => {
throw () => undefined
})
})
it('handles users throwing symbols', (done) => {
cy.on('fail', (err) => {
expect(err.name).to.equal('CypressError')
expect(err.message).to.equal('`cy.origin()` could not serialize the thrown value. Please make sure the value being thrown is supported by the structured clone algorithm.')
done()
})
cy.origin('http://foobar.com:3500', () => {
throw Symbol('foo')
})
})
it('handles users throwing promises', (done) => {
cy.on('fail', (err) => {
expect(err.name).to.equal('CypressError')
expect(err.message).to.equal('`cy.origin()` could not serialize the thrown value. Please make sure the value being thrown is supported by the structured clone algorithm.')
done()
})
cy.origin('http://foobar.com:3500', () => {
throw new Promise(() => {})
})
})
})
describe('serializable errors', () => {
it('handles users throwing complex errors/classes', (done) => {
cy.on('fail', (err: any) => {
expect(err.name).to.equal('CustomError')
expect(err.message).to.equal('custom error')
expect(err._name).to.equal('CustomError')
expect(err.foo).to.equal('bar')
const { writable } = Object.getOwnPropertyDescriptor(err, 'name') as PropertyDescriptor
// After serialization, read-only properties are now writable
expect(writable).to.be.true
done()
})
cy.origin('http://foobar.com:3500', () => {
class CustomError extends Error {
private _name = 'CustomError'
get name () {
return this._name
}
set name (name: string) {
this._name = name
}
}
const customErrorInstance = new CustomError('custom error')
// @ts-ignore
customErrorInstance.foo = 'bar'
const { writable } = Object.getOwnPropertyDescriptor(CustomError, 'name') as PropertyDescriptor
// make sure the name property is read-only before serializing it through postMessage
expect(writable).to.be.false
cy.then(() => {
throw customErrorInstance
})
})
})
it('handles users throwing complex objects/classes', (done) => {
cy.on('fail', (err: any) => {
expect(err.customMethod).to.be.undefined
expect(err.customProp).to.equal('foobar')
expect(err._metasyntaticList).to.deep.equal(['foo', 'bar'])
expect(err.metasyntaticList).to.deep.equal(['foo', 'bar'])
done()
})
cy.origin('http://foobar.com:3500', () => {
class FooBar {
private _metasyntaticList = ['foo']
get metasyntaticList (): string[] {
return this._metasyntaticList
}
set metasyntaticList (itemsToAdd: string[]) {
this._metasyntaticList = this._metasyntaticList.concat(itemsToAdd)
}
}
const foobarInstance: any = new FooBar
foobarInstance.customProp = 'foobar'
foobarInstance.metasyntaticList = ['bar']
foobarInstance.customMethod = () => undefined
throw foobarInstance
})
})
it('handles users throwing strings', (done) => {
cy.on('fail', (err) => {
expect(err.name).to.equal('Error')
expect(err.message).to.equal(`oops`)
done()
})
cy.origin('http://foobar.com:3500', () => {
throw 'oops'
})
})
it('handles users throwing arrays', (done) => {
cy.on('fail', (err) => {
expect(err.name).to.equal('Error')
expect(err.message).to.equal('why would anyone do this?,this is odd')
done()
})
cy.origin('http://foobar.com:3500', () => {
throw ['why would anyone do this?', 'this is odd']
})
})
it('handles users throwing numbers', (done) => {
cy.on('fail', (err) => {
expect(err.name).to.equal('Error')
expect(err.message).to.equal('2')
done()
})
cy.origin('http://foobar.com:3500', () => {
throw 2
})
})
it('handles users throwing booleans', (done) => {
cy.on('fail', (err) => {
expect(err.name).to.equal('Error')
expect(err.message).to.equal('true')
done()
})
cy.origin('http://foobar.com:3500', () => {
throw true
})
})
it('handles users throwing null', (done) => {
cy.on('fail', (err) => {
expect(err.name).to.equal('Error')
expect(err.message).to.equal('null')
done()
})
cy.origin('http://foobar.com:3500', () => {
throw null
})
})
it('handles users throwing undefined', (done) => {
cy.on('fail', (err) => {
expect(err.name).to.equal('Error')
expect(err.message).to.equal('undefined')
done()
})
cy.origin('http://foobar.com:3500', () => {
throw undefined
})
})
it('handles throwing of arbitrary data types that are serializable but cannot be mapped to an error', (done) => {
cy.on('fail', (err) => {
expect(err.name).to.equal('CypressError')
expect(err.message).to.equal('`cy.origin()` could not serialize the thrown value. Please make sure the value being thrown is supported by the structured clone algorithm.')
done()
})
cy.origin('http://foobar.com:3500', () => {
throw new Date()
})
})
})
})
@@ -0,0 +1,358 @@
describe('cy.origin', () => {
describe('successes', () => {
it('succeeds on a localhost domain name', () => {
cy.origin('localhost', () => undefined)
cy.then(() => {
const expectedSrc = `https://localhost/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://localhost') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('succeeds on an ip address', () => {
cy.origin('127.0.0.1', () => undefined)
cy.then(() => {
const expectedSrc = `https://127.0.0.1/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://127.0.0.1') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
// TODO: $Location does not support ipv6
// https://github.com/cypress-io/cypress/issues/20970
it.skip('succeeds on an ipv6 address', () => {
cy.origin('0000:0000:0000:0000:0000:0000:0000:0001', () => undefined)
cy.then(() => {
const expectedSrc = `https://[::1]/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://[::1]') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('succeeds on a unicode domain', () => {
cy.origin('はじめよう.みんな', () => undefined)
cy.then(() => {
const expectedSrc = `https://xn--p8j9a0d9c9a.xn--q9jyb4c/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://xn--p8j9a0d9c9a.xn--q9jyb4c') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('succeeds on a complete origin', () => {
cy.origin('http://foobar1.com:3500', () => undefined)
cy.then(() => {
const expectedSrc = `http://foobar1.com:3500/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ http://foobar1.com:3500') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('succeeds on a complete origin using https', () => {
cy.origin('https://foobar2.com:3500', () => undefined)
cy.then(() => {
const expectedSrc = `https://foobar2.com:3500/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar2.com:3500') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('succeeds on a hostname and port', () => {
cy.origin('foobar3.com:3500', () => undefined)
cy.then(() => {
const expectedSrc = `https://foobar3.com:3500/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar3.com:3500') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('succeeds on a protocol and hostname', () => {
cy.origin('http://foobar4.com', () => undefined)
cy.then(() => {
const expectedSrc = `http://foobar4.com/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ http://foobar4.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('succeeds on a subdomain', () => {
cy.origin('app.foobar5.com', () => undefined)
cy.then(() => {
const expectedSrc = `https://foobar5.com/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar5.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('succeeds when only domain is passed', () => {
cy.origin('foobar6.com', () => undefined)
cy.then(() => {
const expectedSrc = `https://foobar6.com/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar6.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('succeeds on a url with path', () => {
cy.origin('http://www.foobar7.com/login', () => undefined)
cy.then(() => {
const expectedSrc = `http://foobar7.com/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ http://foobar7.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('succeeds on a url with a hash', () => {
cy.origin('http://www.foobar8.com/#hash', () => undefined)
cy.then(() => {
const expectedSrc = `http://foobar8.com/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ http://foobar8.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('succeeds on a url with a path and hash', () => {
cy.origin('http://www.foobar9.com/login/#hash', () => undefined)
cy.then(() => {
const expectedSrc = `http://foobar9.com/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ http://foobar9.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('succeeds on a domain with path', () => {
cy.origin('foobar10.com/login', () => undefined)
cy.then(() => {
const expectedSrc = `https://foobar10.com/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar10.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('succeeds on a domain with a hash', () => {
cy.origin('foobar11.com/#hash', () => undefined)
cy.then(() => {
const expectedSrc = `https://foobar11.com/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar11.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('succeeds on a domain with a path and hash', () => {
cy.origin('foobar12.com/login/#hash', () => undefined)
cy.then(() => {
const expectedSrc = `https://foobar12.com/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar12.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('succeeds on a public suffix with a subdomain', () => {
cy.origin('app.foobar.herokuapp.com', () => undefined)
cy.then(() => {
const expectedSrc = `https://foobar.herokuapp.com/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar.herokuapp.com') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('succeeds on a machine name', () => {
cy.origin('machine-name', () => undefined)
cy.then(() => {
const expectedSrc = `https://machine-name/__cypress/multi-domain-iframes`
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://machine-name') as HTMLIFrameElement
expect(iframe.src).to.equal(expectedSrc)
})
})
it('finds the right spec bridge with a subdomain', () => {
cy.visit('/fixtures/auth/index.html')
cy.window().then((win) => {
win.location.href = 'http://baz.foobar.com:3500/fixtures/auth/idp.html'
})
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="username"]').type('TJohnson')
cy.get('[data-cy="login"]').click()
})
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome TJohnson')
})
it('finds the correct spec bridge even if a previous spec bridge host is a subset of the current host', () => {
// Establish a spec bridge with a 'bar.com' host prior to loading 'foobar.com'
cy.origin('http://www.bar.com:3500', () => undefined)
cy.origin('http://www.app.foobar.com:3500', () => {
cy.visit('/fixtures/multi-domain.html')
})
})
it('uses cy.origin twice', () => {
cy.visit('/fixtures/auth/index.html')
cy.get('[data-cy="login-idp"]').click()
cy.origin('http://idp.com:3500', () => {
cy.get('[data-cy="username"]').type('BJohnson')
cy.get('[data-cy="login"]').click()
})
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome BJohnson')
cy.get('[data-cy="logout"]').click()
cy.window().then((win) => {
win.location.href = 'http://baz.foobar.com:3500/fixtures/auth/idp.html'
})
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="username"]').type('TJohnson')
cy.get('[data-cy="login"]').click()
})
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome TJohnson')
})
it('creates a spec bridge for https://idp.com:3502', () => {
cy.visit('/fixtures/auth/index.html')
cy.origin('idp.com:3502', () => {
cy.visit('https://www.idp.com:3502/fixtures/auth/index.html')
cy.get('[data-cy="login-idp"]').invoke('text').should('equal', 'Login IDP')
})
})
it('creates a spec bridge for http://idp.com:3500', () => {
cy.visit('/fixtures/auth/index.html')
cy.origin('http://idp.com:3500', () => {
cy.visit('http://www.idp.com:3500/fixtures/auth/index.html')
cy.get('[data-cy="login-idp"]').invoke('text').should('equal', 'Login IDP')
})
})
})
describe('errors', () => {
// @ts-ignore
it('errors if experimental flag is not enabled', { experimentalSessionAndOrigin: false }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.equal('`cy.origin()` requires enabling the experimentalSessionAndOrigin flag')
done()
})
// @ts-ignore
cy.origin()
})
it('errors if passed a non-string for the origin argument', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.equal('`cy.origin()` requires the first argument to be either a url (`https://www.example.com/path`) or a domain name (`example.com`). Query parameters are not allowed. You passed: ``')
done()
})
// @ts-ignore
cy.origin()
})
it('errors if query params are provided', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.equal('`cy.origin()` requires the first argument to be either a url (`https://www.example.com/path`) or a domain name (`example.com`). Query parameters are not allowed. You passed: `http://www.foobar.com?foo=bar`')
done()
})
cy.origin('http://www.foobar.com?foo=bar', () => undefined)
})
it('errors passing non-array to callback function', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.equal('`cy.origin()` requires the \'options\' argument to be an object. You passed: `foo`')
done()
})
// @ts-ignore
cy.origin('foobar.com', 'foo', () => {})
})
it('errors passing in invalid config object to callback function', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.origin()` detected extraneous keys in your options configuration.')
expect(err.message).to.include('The extraneous keys detected were:')
expect(err.message).to.include('> `foo, bar`')
expect(err.message).to.include('Valid keys include the following:')
expect(err.message).to.include('> `args`')
done()
})
cy.origin('foobar.com', {
// @ts-ignore
foo: 'foo',
bar: 'bar',
}, () => {})
})
it('errors if passed a non-serializable args value', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include('arguments specified are not serializable')
if (Cypress.browser.family === 'chromium') {
expect(err.message).to.include('HTMLDivElement object could not be cloned')
} else if (Cypress.browser.family === 'firefox') {
expect(err.message).to.include('The object could not be cloned')
}
done()
})
const el = document.createElement('div')
cy.origin('foobar.com', { args: ['foo', '1', el] }, () => {})
})
it('errors if last argument is absent', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.equal('`cy.origin()` requires the last argument to be a function. You passed: ``')
done()
})
// @ts-ignore
cy.origin('foobar.com')
})
it('errors if last argument is not a function', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.equal('`cy.origin()` requires the last argument to be a function. You passed: `{}`')
done()
})
// @ts-ignore
cy.origin('foobar.com', {})
})
})
})
@@ -0,0 +1,257 @@
import { assertLogLength } from '../../../support/utils'
describe('cy.origin yields', () => {
let logs: any = []
beforeEach(() => {
logs = []
cy.on('log:added', (attrs, log) => {
logs.push(log)
})
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
it('yields a value', () => {
cy.origin('http://foobar.com:3500', () => {
cy
.get('[data-cy="dom-check"]')
.invoke('text')
}).should('equal', 'From a secondary origin')
})
it('yields the cy value even if a return is present', () => {
cy.origin('http://foobar.com:3500', () => {
cy
.get('[data-cy="dom-check"]')
.invoke('text')
return new Promise((resolve) => {
setTimeout(() => {
resolve('text')
}, 50)
})
}).should('equal', 'From a secondary origin')
})
it('errors if a cy command is present and it returns a sync value', (done) => {
cy.on('fail', (err) => {
assertLogLength(logs, 5)
expect(logs[4].get('error')).to.eq(err)
expect(err.message).to.include('`cy.origin()` failed because you are mixing up async and sync code.')
done()
})
cy.origin('http://foobar.com:3500', () => {
cy
.get('[data-cy="dom-check"]')
.invoke('text')
return 'text'
})
})
it('yields synchronously', () => {
cy.origin('http://foobar.com:3500', () => {
return 'From a secondary origin'
}).should('equal', 'From a secondary origin')
})
it('yields asynchronously', () => {
cy.origin('http://foobar.com:3500', () => {
return new Promise((resolve: (val: string) => any, reject) => {
setTimeout(() => {
resolve('From a secondary origin')
}, 50)
})
}).should('equal', 'From a secondary origin')
})
it('succeeds if subject cannot be serialized and is not accessed synchronously', () => {
cy.origin('http://foobar.com:3500', () => {
return {
symbol: Symbol(''),
}
}).then((obj) => {
return 'object not accessed'
}).should('equal', 'object not accessed')
})
it('throws if subject cannot be serialized and is accessed synchronously', (done) => {
cy.on('fail', (err) => {
assertLogLength(logs, 7)
expect(logs[6].get('error')).to.eq(err)
expect(err.message).to.include('`cy.origin()` could not serialize the subject due to one of its properties not being supported by the structured clone algorithm.')
done()
})
cy.origin('http://foobar.com:3500', () => {
return {
symbol: Symbol(''),
}
}).then((obj) => {
// @ts-ignore
return obj.symbol // This will fail accessing the symbol
})
})
it('succeeds if subject cannot be serialized and is not accessed', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="dom-check"]')
})
.then(() => {
return 'object not accessed'
})
.should('equal', 'object not accessed')
})
it('throws if subject cannot be serialized and is accessed', (done) => {
cy.on('fail', (err) => {
assertLogLength(logs, 8)
expect(logs[7].get('error')).to.eq(err)
expect(err.message).to.include('`cy.origin()` could not serialize the subject due to one of its properties not being supported by the structured clone algorithm.')
done()
})
cy.origin<JQuery>('http://foobar.com:3500', () => {
cy.get('[data-cy="dom-check"]')
})
.then((subject) => subject.text())
.should('equal', 'From a secondary origin')
})
it('throws if an object contains a function', (done) => {
cy.on('fail', (err) => {
assertLogLength(logs, 8)
expect(logs[7].get('error')).to.eq(err)
expect(err.message).to.include('`cy.origin()` could not serialize the subject due to one of its properties not being supported by the structured clone algorithm.')
done()
})
cy.origin<{ key: Function }>('http://foobar.com:3500', () => {
cy.wrap({
key: () => {
return 'whoops'
},
})
})
.then((subject) => subject.key())
.should('equal', 'whoops')
})
it('throws if an object contains a symbol', (done) => {
cy.on('fail', (err) => {
assertLogLength(logs, 8)
expect(logs[7].get('error')).to.eq(err)
expect(err.message).to.include('`cy.origin()` could not serialize the subject due to one of its properties not being supported by the structured clone algorithm.')
done()
})
cy.origin('http://foobar.com:3500', () => {
cy.wrap({
key: Symbol('whoops'),
})
})
.should('equal', undefined)
})
it('throws if an object is a function', (done) => {
cy.on('fail', (err) => {
assertLogLength(logs, 8)
expect(logs[7].get('error')).to.eq(err)
expect(err.message).to.include('`cy.origin()` could not serialize the subject due to functions not being supported by the structured clone algorithm.')
done()
})
cy.origin('http://foobar.com:3500', () => {
cy.wrap(() => {
return 'text'
})
})
.then((obj) => {
// @ts-ignore
obj()
})
})
it('throws if an object is a symbol', (done) => {
cy.on('fail', (err) => {
assertLogLength(logs, 8)
expect(logs[7].get('error')).to.eq(err)
expect(err.message).to.include('`cy.origin()` could not serialize the subject due to symbols not being supported by the structured clone algorithm.')
done()
})
cy.origin('http://foobar.com:3500', () => {
cy.wrap(Symbol('symbol'))
})
.should('equal', 'symbol')
})
// NOTE: Errors can only be serialized on chromium browsers.
it('yields an error if an object contains an error', (done) => {
const isChromium = Cypress.isBrowser({ family: 'chromium' })
cy.on('fail', (err) => {
if (!isChromium) {
assertLogLength(logs, 8)
expect(logs[7].get('error')).to.eq(err)
expect(err.message).to.include('`cy.origin()` could not serialize the subject due to one of its properties not being supported by the structured clone algorithm.')
}
done()
})
cy.origin('http://foobar.com:3500', () => {
cy.wrap({
key: new Error('Boom goes the dynamite'),
})
})
.its('key.message')
.should('equal', 'Boom goes the dynamite').then(() => {
done()
})
})
it('yields an object containing valid types', () => {
cy.origin('http://foobar.com:3500', () => {
cy.wrap({
array: [
1,
2,
],
undefined,
bool: true,
null: null,
number: 12,
object: {
key: 'key',
},
string: 'string',
})
})
.should('deep.equal', {
array: [
1,
2,
],
undefined,
bool: true,
null: null,
number: 12,
object: {
key: 'key',
},
string: 'string',
})
})
})
@@ -0,0 +1,539 @@
import { assertLogLength } from '../../../support/utils'
// makes logs coming from secondary origin work with `assertLogLength`
const reifyLogs = (logs) => {
return logs.map((attrs) => {
return {
get (name) {
return attrs[name]
},
}
})
}
describe('navigation events', () => {
let logs: any = []
beforeEach(() => {
logs = []
cy.on('log:added', (attrs, log) => {
logs.push(log)
})
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
describe('navigation:changed', () => {
it('navigation:changed via hashChange', () => {
cy.origin('http://foobar.com:3500', () => {
const afterNavigationChanged = new Promise<void>((resolve) => {
const listener = () => {
cy.location().should((loc) => {
expect(loc.host).to.equal('www.foobar.com:3500')
expect(loc.pathname).to.equal('/fixtures/multi-domain-secondary.html')
expect(loc.hash).to.equal('#hashChange')
})
resolve()
}
cy.once('navigation:changed', listener)
})
cy.get('a[data-cy="hashChange"]').click()
cy.wrap(afterNavigationChanged)
})
})
it('navigates forward and back using history', () => {
cy.origin('http://foobar.com:3500', () => {
const onLoad = (cb) => {
const onNavChanged = (event) => {
if (event === 'page navigation event (\'load\')') {
cy.off('navigation:changed', onNavChanged)
cb()
}
}
cy.on('navigation:changed', onNavChanged)
}
cy.get('a[data-cy="cross-origin-page"]').click()
.window().then((win) => {
return new Promise<void>((resolve) => {
onLoad(resolve)
win.history.back()
}).then(() => {
return new Promise<void>((resolve) => {
onLoad(resolve)
win.history.forward()
})
})
})
})
})
})
describe('window:load', () => {
it('reloads', () => {
cy.origin('http://foobar.com:3500', () => {
const logs: any[] = []
cy.on('log:added', (attrs, log) => {
logs.push(log)
})
const afterWindowLoad = new Promise<void>((resolve) => {
let times = 0
const listener = (win) => {
times++
expect(win.location.host).to.equal('www.foobar.com:3500')
expect(win.location.pathname).to.equal('/fixtures/multi-domain-secondary.html')
if (times === 2) {
cy.removeListener('window:load', listener)
resolve()
}
}
cy.on('window:load', listener)
})
cy.get('button[data-cy="reload"]').click()
cy.wrap(afterWindowLoad).then(() => {
return logs.map((log) => ({ name: log.get('name') }))
})
})
.then(reifyLogs)
.then((secondaryLogs) => {
assertLogLength(secondaryLogs, 9)
expect(secondaryLogs[5].get('name')).to.eq('page load')
})
})
it('navigates to a new page', () => {
cy.origin('http://foobar.com:3500', () => {
const logs: any[] = []
cy.on('log:added', (attrs, log) => {
logs.push(log)
})
const afterWindowLoad = new Promise<void>((resolve) => {
let times = 0
const listener = (win) => {
times++
if (times === 1) {
expect(win.location.host).to.equal('www.foobar.com:3500')
expect(win.location.pathname).to.equal('/fixtures/multi-domain-secondary.html')
}
if (times === 2) {
cy.removeListener('window:load', listener)
expect(win.location.host).to.equal('www.foobar.com:3500')
expect(win.location.pathname).to.equal('/fixtures/multi-domain.html')
resolve()
}
}
cy.on('window:load', listener)
})
cy.get('a[data-cy="cross-origin-page"]').click()
cy.get('a[data-cy="cross-origin-secondary-link').invoke('text').should('equal', 'http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
cy.wrap(afterWindowLoad).then(() => {
return logs.map((log) => ({ name: log.get('name'), message: log.get('message') }))
})
})
.then(reifyLogs)
.then((secondaryLogs) => {
assertLogLength(secondaryLogs, 13)
expect(secondaryLogs[5].get('name')).to.eq('page load')
expect(secondaryLogs[6].get('name')).to.eq('new url')
expect(secondaryLogs[6].get('message')).to.eq('http://www.foobar.com:3500/fixtures/multi-domain.html')
})
})
})
describe('url:changed', () => {
it('reloads', () => {
cy.origin('http://foobar.com:3500', () => {
const afterUrlChanged = new Promise<void>((resolve) => {
cy.once('url:changed', (url) => {
expect(url).to.equal('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
resolve()
})
})
cy.get('button[data-cy="reload"]').click()
cy.wrap(afterUrlChanged)
})
})
it('navigates to a new page', () => {
cy.origin('http://foobar.com:3500', () => {
const afterUrlChanged = new Promise<void>((resolve) => {
let times = 0
const listener = (url) => {
times++
if (times === 1) {
expect(url).to.equal('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
}
if (times === 2) {
cy.removeListener('url:changed', listener)
expect(url).to.equal('http://www.foobar.com:3500/fixtures/multi-domain.html')
resolve()
}
}
cy.on('url:changed', listener)
})
cy.get('a[data-cy="cross-origin-page"]').click()
cy.wrap(afterUrlChanged)
})
})
// TODO: this test should re revisited with the cypress in cypress tests available in 10.0
// https://github.com/cypress-io/cypress/issues/20973
it.skip('the runner url updates appropriately', () => {
cy.origin('http://foobar.com:3500', () => {
cy.get('a[data-cy="cross-origin-page"]').click()
})
})
})
})
// @ts-ignore / session support is needed for visiting about:blank between tests
describe('event timing', () => {
it('does not timeout when receiving a delaying:html event after cy.origin has started, but before the spec bridge is ready', () => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.log('inside cy.origin foobar')
})
// This command is run from localhost against the cross-origin aut. Updating href is one of the few allowed commands. See https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy#location
cy.window().then((win) => {
win.location.href = 'http://www.idp.com:3500/fixtures/multi-domain.html'
})
cy.origin('http://idp.com:3500', () => {
cy.log('inside cy.origin idp')
})
})
})
// @ts-ignore / session support is needed for visiting about:blank between tests
describe('delayed navigation', { defaultCommandTimeout: 2000 }, () => {
it('localhost -> localhost', () => {
cy.visit('/fixtures/auth/delayedNavigate.html')
cy.get('[data-cy="to-localhost"]').click()
cy.get('[data-cy="login-idp"]')
})
it('localhost -> foobar, delay in', () => {
cy.visit('/fixtures/auth/delayedNavigate.html')
cy.get('[data-cy="to-foobar"]').click()
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="login-idp"]')
})
})
it('foobar -> localhost, delay out', () => {
cy.visit('/fixtures/auth/index.html')
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/auth/delayedNavigate.html')
cy.get('[data-cy="to-localhost"]').click()
})
cy.get('[data-cy="login-idp"]')
})
it('foobar -> idp, delay out', () => {
cy.visit('/fixtures/auth/index.html')
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/auth/delayedNavigate.html')
cy.get('[data-cy="to-idp"]').click()
})
cy.origin('http://idp.com:3500', () => {
cy.get('[data-cy="login-idp"]')
})
})
})
// @ts-ignore / session support is needed for visiting about:blank between tests
describe('errors', () => {
it('never calls cy.origin', { pageLoadTimeout: 5000 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out after waiting \`5000ms\` for your remote page to load on origin(s):`)
expect(err.message).to.include(`\n- \`http://localhost:3500\`\n`)
expect(err.message).to.include(`A cross-origin request for \`http://www.foobar.com:3500/fixtures/auth/idp.html?redirect=http%3A%2F%2Flocalhost%3A3500%2Ffixtures%2Fauth%2Findex.html\` was detected.`)
expect(err.message).to.include(`A command that triggers cross-origin navigation must be immediately followed by a \`cy.origin()\` command:`)
expect(err.message).to.include(`\`cy.origin(\'http://foobar.com:3500\', () => {\`\n\` <commands targeting http://www.foobar.com:3500 go here>\`\n\`})\`\n`)
expect(err.message).to.include(`If the cross-origin request was an intermediary state, you can try increasing the \`pageLoadTimeout\` value in \`cypress.json\` to wait longer`)
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
done()
})
cy.visit('/fixtures/auth/index.html')
cy.get('[data-cy="login-foobar"]').click() // Timeout on page load here, we never reach the expected origin
cy.get('[data-cy="login-foobar"]')
})
it('never redirects to the cross-origin', { defaultCommandTimeout: 50 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out retrying after 50ms: Expected to find element: \`[data-cy="username"]\`, but never found it`)
expect(err.message).to.include(`The command was expected to run against origin \`http://idp.com:3500\` but the application is at origin \`http://localhost:3500\`.`)
expect(err.message).to.include(`This commonly happens when you have either not navigated to the expected origin or have navigated away unexpectedly.`)
// make sure that the secondary origin failures do NOT show up as spec failures or AUT failures
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
done()
})
cy.visit('/fixtures/auth/index.html')
cy.get('[data-cy="login-idp"]')
cy.origin('http://idp.com:3500', () => {
cy.get('[data-cy="username"]').type('BJohnson') // Timeout here on command, cannot find element
cy.get('[data-cy="login"]').click()
})
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome BJohnson')
})
it('redirects to the wrong cross-origin', { pageLoadTimeout: 5000 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out after waiting \`5000ms\` for your remote page to load on origin(s):`)
expect(err.message).to.include(`\n- \`http://idp.com:3500\`\n`)
expect(err.message).to.include(`A cross-origin request for \`http://www.foobar.com:3500/fixtures/auth/idp.html?redirect=http%3A%2F%2Flocalhost%3A3500%2Ffixtures%2Fauth%2Findex.html\` was detected.`)
expect(err.message).to.include(`\`cy.origin(\'http://foobar.com:3500\', () => {\`\n\` <commands targeting http://www.foobar.com:3500 go here>\`\n\`})\`\n`)
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
done()
})
cy.visit('/fixtures/auth/index.html')
cy.get('[data-cy="login-foobar"]').click() // Timeout on page load here, we never reach the expected origin
cy.origin('http://idp.com:3500', () => {
cy.get('[data-cy="username"]').type('BJohnson')
cy.get('[data-cy="login"]').click()
})
// Verify that the user has logged in on localhost
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome BJohnson')
})
it('never returns to the primary origin', { defaultCommandTimeout: 50 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out retrying after 50ms: Expected to find element: \`[data-cy="welcome"]\`, but never found it`)
expect(err.message).to.include(`The command was expected to run against origin \`http://localhost:3500\` but the application is at origin \`http://idp.com:3500\`.`)
// make sure that the secondary origin failures do NOT show up as spec failures or AUT failures
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
done()
})
cy.visit('/fixtures/auth/index.html')
cy.get('[data-cy="login-idp"]').click()
cy.origin('http://idp.com:3500', () => {
cy.get('[data-cy="username"]').type('BJohnson')
}) // cy.origin is stable so the command exits
// Verify that the user has logged in on localhost
cy.get('[data-cy="welcome"]') // Timeout here on command, cannot find element
.invoke('text')
.should('equal', 'Welcome BJohnson')
})
it('redirects to an unexpected cross-origin', { pageLoadTimeout: 5000 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out after waiting \`5000ms\` for your remote page to load on origin(s):`)
expect(err.message).to.include(`\n- \`http://localhost:3500\`\n`)
expect(err.message).to.include(`A cross-origin request for \`http://www.foobar.com:3500/fixtures/auth/index.html\` was detected.`)
expect(err.message).to.include(`\`cy.origin(\'http://foobar.com:3500\', () => {\`\n\` <commands targeting http://www.foobar.com:3500 go here>\`\n\`})\`\n`)
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
done()
})
cy.visit('/fixtures/auth/index.html')
cy.get('[data-cy="login-idp"]').click()
cy.origin('http://idp.com:3500', () => {
cy.get('[data-cy="username"]').type('BJohnson')
cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/fixtures/auth/index.html'
})
})
// Verify that the user has logged in on localhost
cy.get('[data-cy="welcome"]') // Stability is false, this command is prevented from running until stability is achieved.
.invoke('text')
.should('equal', 'Welcome BJohnson')
})
it('redirects to an unexpected cross-origin and calls another command in the cy.origin command', { pageLoadTimeout: 5000 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out after waiting \`5000ms\` for your remote page to load on origin(s):`)
expect(err.message).to.include(`\n- \`http://idp.com:3500\`\n`)
expect(err.message).to.include(`A cross-origin request for \`http://www.foobar.com:3500/fixtures/auth/index.html\` was detected.`)
expect(err.message).to.include(`\`cy.origin(\'http://foobar.com:3500\', () => {\`\n\` <commands targeting http://www.foobar.com:3500 go here>\`\n\`})\`\n`)
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
done()
})
cy.visit('/fixtures/auth/index.html')
cy.get('[data-cy="login-idp"]').click()
cy.origin('http://idp.com:3500', () => {
cy.get('[data-cy="username"]').type('BJohnson')
cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/fixtures/auth/index.html'
})
cy.get('[data-cy="welcome"]') // Timeout here on command, cannot find element
})
// Verify that the user has logged in on localhost
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome BJohnson')
})
it('fails in cy.origin when a command is run after we return to localhost', { defaultCommandTimeout: 50 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out retrying after 50ms: Expected to find element: \`[data-cy="cannot_find"]\`, but never found it`)
expect(err.message).to.include(`The command was expected to run against origin \`http://idp.com:3500\` but the application is at origin \`http://localhost:3500\`.`)
// make sure that the secondary origin failures do NOT show up as spec failures or AUT failures
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
done()
})
cy.visit('/fixtures/auth/index.html') // Establishes primary origin
cy.get('[data-cy="login-idp"]').click() // Takes you to idp.com
cy.origin('http://idp.com:3500', () => {
cy.get('[data-cy="username"]').type('BJohnson')
cy.get('[data-cy="login"]').click()
cy.get('[data-cy="cannot_find"]') // Timeout here on command stability achieved by primary origin, this command times out.
})
// Verify that the user has logged in on localhost
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome BJohnson')
})
it('fails with a normal timeout', { defaultCommandTimeout: 50 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out retrying after 50ms: Expected to find element: \`[data-cy="cannot_find"]\`, but never found it`)
// make sure that the secondary origin failures do NOT show up as spec failures or AUT failures
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
expect(err.message).not.to.include(`The command was expected to run against origin:`)
done()
})
cy.visit('/fixtures/auth/index.html') // Establishes primary origin
cy.get('[data-cy="login-idp"]').click() // Takes you to idp.com
cy.origin('http://idp.com:3500', () => {
cy.get('[data-cy="cannot_find"]') // Timeout here on command stability achieved by primary origin, this command times out.
})
})
describe('Pre established spec bridge', () => {
// These next three tests test and edge case where we want to prevent a load event from an established spec bridge that is not part of the test.
// This test removes the foobar spec bridge, navigates to idp, then navigates to foobar and attempts to access selectors on localhost.
it('times out in cy.origin with foobar spec bridge undefined', { pageLoadTimeout: 5000 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out after waiting \`5000ms\` for your remote page to load on origin(s):`)
expect(err.message).to.include(`\n- \`http://localhost:3500\`\n`)
expect(err.message).to.include(`A cross-origin request for \`http://www.foobar.com:3500/fixtures/auth/index.html\` was detected.`)
expect(err.message).to.include(`\`cy.origin(\'http://foobar.com:3500\', () => {\`\n\` <commands targeting http://www.foobar.com:3500 go here>\`\n\`})\`\n`)
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
done()
})
cy.visit('/fixtures/auth/index.html') // Establishes primary origin
cy.window().then(() => {
// Force remove the spec bridge
window?.top?.document.getElementById('Spec Bridge: foobar.com')?.remove()
})
cy.get('[data-cy="login-idp"]').click() // Takes you to idp.com
cy.origin('http://idp.com:3500', () => {
cy.get('[data-cy="username"]').type('BJohnson')
cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/fixtures/auth/index.html'
})//
})
// Verify that the user has logged in on localhost
cy.get('[data-cy="welcome"]') // Stability is false, this command is prevented from running until stability is achieved.
.invoke('text')
.should('equal', 'Welcome BJohnson')
})
// this test just needs to establish the foobar spec bridge.
it('establishes foobar spec bridge', () => {
cy.visit('/fixtures/auth/index.html') // Establishes primary origin
cy.get('[data-cy="login-foobar"]').click() // Takes you to idp.com
cy.origin('http://foobar.com:3500', () => {
cy.get('[data-cy="username"]').type('BJohnson')
cy.get('[data-cy="login"]').click()
})
// Verify that the user has logged in on localhost
cy.get('[data-cy="welcome"]')
.invoke('text')
.should('equal', 'Welcome BJohnson')
})
// This test is the same as the first test but the foobar spec bridge has been established and we expect it to behave the same as the first test.
// The primary origin should ignore the load event from the foobar spec bridge and load should timeout in the idp cy.origin command..
it('times out in cy.origin with foobar spec bridge defined', { pageLoadTimeout: 5000 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out after waiting \`5000ms\` for your remote page to load on origin(s):`)
expect(err.message).to.include(`\n- \`http://localhost:3500\`\n`)
expect(err.message).to.include(`A cross-origin request for \`http://www.foobar.com:3500/fixtures/auth/index.html\` was detected.`)
expect(err.message).to.include(`\`cy.origin(\'http://foobar.com:3500\', () => {\`\n\` <commands targeting http://www.foobar.com:3500 go here>\`\n\`})\`\n`)
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
done()
})
cy.visit('/fixtures/auth/index.html') // Establishes primary origin
cy.get('[data-cy="login-idp"]').click() // Takes you to idp.com
cy.origin('http://idp.com:3500', () => {
cy.get('[data-cy="username"]').type('BJohnson')
cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/fixtures/auth/index.html'
})
})
// Verify that the user has logged in on localhost
cy.get('[data-cy="welcome"]') // Stability is false, this command is prevented from running until stability is achieved.
.invoke('text')
.should('equal', 'Welcome BJohnson')
})
})
})
@@ -0,0 +1,34 @@
describe('stability', () => {
describe('before each transitions', () => {
describe('transitioning from a before block to an it block while unstable', () => {
beforeEach(() => {
cy.visit('/fixtures/auth/index.html')
cy.window().then((win) => {
win.location.href = 'http://localhost:3500/timeout?ms=1000'
})
})
it('fails if the page does not load within the page load timeout', { defaultCommandTimeout: 50, pageLoadTimeout: 500 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out after waiting \`500ms\` for your remote page to load.`)
done()
})
cy.get('[data-cy="login-idp"]').click() // Takes you to idp.com
})
it('waits for the page to load before running the command', { defaultCommandTimeout: 50 }, () => {
cy.get('body').invoke('text').should('equal', 'timeout')
})
it('will retry and fail the command after the page loads', { defaultCommandTimeout: 50 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out retrying after 50ms: expected 'timeout' to equal 'not timeout'`)
done()
})
cy.get('body').invoke('text').should('equal', 'not timeout')
})
})
})
})
@@ -1,9 +1,12 @@
let changed = 0
describe('src/cy/commands/actions/type text_mask_spec', () => {
before(() => {
beforeEach(() => {
cy.visit('/fixtures/text-mask.html')
// reset number of change events before test
changed = 0
// count the number of change events
return cy.get('input').then(($els) => {
return $els.change(($el) => {
@@ -12,11 +15,6 @@ describe('src/cy/commands/actions/type text_mask_spec', () => {
})
})
beforeEach(() => {
// reset number of change events before test
changed = 0
})
context('#type', () => {
it('can type into phone', () => {
cy.get('#phone')
+1 -11
View File
@@ -2,26 +2,16 @@
const { $ } = Cypress
describe('issue 3890 overwriting cy.route command', () => {
before(() => {
beforeEach(function () {
cy
.visit('/fixtures/jquery.html')
.then(function (win) {
const h = $(win.document.head)
h.find('script').remove()
this.head = h.prop('outerHTML')
this.body = win.document.body.outerHTML
})
})
beforeEach(function () {
const doc = cy.state('document')
$(doc.head).empty().html(this.head)
$(doc.body).empty().html(this.body)
})
it('stores route as an alias', () => {
// sanity test before overwriting cy.route in the next test
cy
@@ -201,4 +201,16 @@ describe('src/util/queue', () => {
expect(queue.stopped).to.false
})
})
context('.last', () => {
it('returns the last item', () => {
expect(queue.last()).to.deep.equal({ id: '3' })
queue.add({ id: '4' })
expect(queue.last()).to.deep.equal({ id: '4' })
queue.clear()
expect(queue.last()).to.equal(undefined)
})
})
})
@@ -0,0 +1,688 @@
import { reifyDomElement, preprocessDomElement, preprocessLogLikeForSerialization, preprocessLogForSerialization, reifyLogFromSerialization } from '../../../src/util/serialization/log'
describe('Log Serialization', () => {
const buildSnapshot = (innerSnapshotElement) => {
const mockSnapshot = document.createElement('body')
// populate some items into the mockSnapshot that would mimic what the DOM might actually look like, along with our inner snapshot element
const mockContainer = document.createElement('div')
const mockInnerHeader = document.createElement('h1')
const mockTextNode = document.createTextNode('Mock Snapshot Header')
mockInnerHeader.appendChild(mockTextNode)
mockContainer.appendChild(mockInnerHeader)
mockContainer.appendChild(innerSnapshotElement)
mockSnapshot.appendChild(mockContainer)
return mockSnapshot
}
it('preprocesses complex log-like data structures by preprocessing log DOM elements and table functions', () => {
const mockSpan = document.createElement('span')
mockSpan.innerHTML = 'click button'
const mockButton = document.createElement('button')
mockButton.appendChild(mockSpan)
const mockClickedElement = document.createElement('form')
mockClickedElement.appendChild(mockButton)
mockClickedElement.id = 'button-inside-a'
const mockSnapshot = buildSnapshot(mockClickedElement)
const mockSnapshots = ['before', 'after'].map((snapshotName) => {
return {
name: snapshotName,
htmlAttrs: {},
body: {
get: () => Cypress.$(mockSnapshot),
},
}
})
// mockLogAttrs should look just like log attributes that are emitted from log:changed/log:added events. This example is what a 'click' log may look like
const mockLogAttrs = {
$el: Cypress.$(mockClickedElement),
alias: undefined,
chainerId: 'mock-chainer-id',
consoleProps: {
['Applied To']: mockClickedElement,
Command: 'click',
Coords: {
x: 100,
y: 50,
},
Options: undefined,
Yielded: undefined,
table: {
1: () => {
return {
name: 'Mouse Events',
// NOTE: click data length is truncated for test readability
data: [
{
'Active Modifiers': null,
'Event Type': 'pointerover',
'Prevented Default': null,
'Stopped Propagation': null,
'Target Element': mockClickedElement,
},
{
'Active Modifiers': null,
'Event Type': 'mouseover',
'Prevented Default': null,
'Stopped Propagation': null,
'Target Element': mockClickedElement,
},
],
}
},
},
},
coords: { top: 50, left: 50, topCenter: 100, leftCenter: 1000, x: 100, y: 50 },
ended: true,
err: undefined,
event: false,
highlightAttr: 'data-cypress-el',
hookId: 'r4',
id: 'mock-log-id',
instrument: 'command',
message: '',
name: 'click',
numElements: 1,
referencesAlias: undefined,
renderProps: {},
snapshots: mockSnapshots,
state: 'passed',
testCurrentRetry: 0,
testId: 'r4',
timeout: 4000,
type: 'child',
url: 'http://www.foobar.com',
viewportHeight: 660,
viewportWidth: 1000,
visible: true,
wallClockStartedAt: '2022-04-18T21:52:37.833Z',
}
const { consoleProps, snapshots, $el, ...logAttrs } = preprocessLogForSerialization(mockLogAttrs)
expect(logAttrs).to.deep.equal({
alias: undefined,
chainerId: 'mock-chainer-id',
coords: { top: 50, left: 50, topCenter: 100, leftCenter: 1000, x: 100, y: 50 },
ended: true,
err: undefined,
event: false,
highlightAttr: 'data-cypress-el',
hookId: 'r4',
id: 'mock-log-id',
instrument: 'command',
message: '',
name: 'click',
numElements: 1,
referencesAlias: undefined,
renderProps: {},
state: 'passed',
testCurrentRetry: 0,
testId: 'r4',
timeout: 4000,
type: 'child',
url: 'http://www.foobar.com',
viewportHeight: 660,
viewportWidth: 1000,
visible: true,
wallClockStartedAt: '2022-04-18T21:52:37.833Z',
})
expect($el).to.deep.equal([
{
attributes: {
id: 'button-inside-a',
},
innerHTML: '<button><span>click button</span></button>',
serializationKey: 'dom',
tagName: 'FORM',
},
])
expect(consoleProps).to.deep.equal({
['Applied To']: {
attributes: {
id: 'button-inside-a',
},
innerHTML: '<button><span>click button</span></button>',
serializationKey: 'dom',
tagName: 'FORM',
},
Command: 'click',
Coords: {
x: 100,
y: 50,
},
Options: undefined,
Yielded: undefined,
table: {
1: {
serializationKey: 'function',
value: {
name: 'Mouse Events',
data: [
{
'Active Modifiers': null,
'Event Type': 'pointerover',
'Prevented Default': null,
'Stopped Propagation': null,
'Target Element': {
attributes: {
id: 'button-inside-a',
},
innerHTML: '<button><span>click button</span></button>',
serializationKey: 'dom',
tagName: 'FORM',
},
},
{
'Active Modifiers': null,
'Event Type': 'mouseover',
'Prevented Default': null,
'Stopped Propagation': null,
'Target Element': {
attributes: {
id: 'button-inside-a',
},
innerHTML: '<button><span>click button</span></button>',
serializationKey: 'dom',
tagName: 'FORM',
},
},
],
},
},
},
})
expect(snapshots).to.deep.equal([
{
name: 'before',
htmlAttrs: {},
styles: {},
body: {
get: {
serializationKey: 'function',
value: [{
attributes: {},
innerHTML: `<div><h1>Mock Snapshot Header</h1><form id="button-inside-a"><button><span>click button</span></button></form></div>`,
serializationKey: 'dom',
tagName: 'BODY',
}],
},
},
},
{
name: 'after',
htmlAttrs: {},
styles: {},
body: {
get: {
serializationKey: 'function',
value: [{
attributes: {},
innerHTML: `<div><h1>Mock Snapshot Header</h1><form id="button-inside-a"><button><span>click button</span></button></form></div>`,
serializationKey: 'dom',
tagName: 'BODY',
}],
},
},
},
])
})
it('reifies complex log-like data structures by reifying serialized DOM elements and table functions back into native data types, respectively', () => {
// this should log identical to the test output above from what a preprocessed click log looks like after postMessage()
const mockPreprocessedLogAttrs = {
$el: [
{
attributes: {
id: 'button-inside-a',
},
innerHTML: '<button><span>click button</span></button>',
serializationKey: 'dom',
tagName: 'FORM',
},
],
alias: undefined,
chainerId: 'mock-chainer-id',
consoleProps: {
['Applied To']: {
attributes: {
id: 'button-inside-a',
},
innerHTML: '<button><span>click button</span></button>',
serializationKey: 'dom',
tagName: 'FORM',
},
Command: 'click',
Coords: {
x: 100,
y: 50,
},
Options: undefined,
Yielded: undefined,
table: {
1: {
serializationKey: 'function',
value: {
name: 'Mouse Events',
// NOTE: click data length is truncated for test readability
data: [
{
'Active Modifiers': null,
'Event Type': 'pointerover',
'Prevented Default': null,
'Stopped Propagation': null,
'Target Element': {
attributes: {
id: 'button-inside-a',
},
innerHTML: '<button><span>click button</span></button>',
serializationKey: 'dom',
tagName: 'FORM',
},
},
{
'Active Modifiers': null,
'Event Type': 'mouseover',
'Prevented Default': null,
'Stopped Propagation': null,
'Target Element': {
attributes: {
id: 'button-inside-a',
},
innerHTML: '<button><span>click button</span></button>',
serializationKey: 'dom',
tagName: 'FORM',
},
},
],
},
},
},
},
coords: { top: 50, left: 50, topCenter: 100, leftCenter: 1000, x: 100, y: 50 },
ended: true,
err: undefined,
event: false,
highlightAttr: 'data-cypress-el',
hookId: 'r4',
id: 'mock-log-id',
instrument: 'command',
message: '',
name: 'click',
numElements: 1,
referencesAlias: undefined,
renderProps: {},
snapshots: [
{
name: 'before',
htmlAttrs: {},
styles: {},
body: {
get: {
serializationKey: 'function',
value: [{
attributes: {},
innerHTML: `<div><h1>Mock Snapshot Header</h1><form id="button-inside-a"><button><span>click button</span></button></form></div>`,
serializationKey: 'dom',
tagName: 'BODY',
}],
},
},
},
{
name: 'after',
htmlAttrs: {},
styles: {},
body: {
get: {
serializationKey: 'function',
value: [{
attributes: {},
innerHTML: `<div><h1>Mock Snapshot Header</h1><form id="button-inside-a"><button><span>click button</span></button></form></div>`,
serializationKey: 'dom',
tagName: 'BODY',
}],
},
},
},
],
state: 'passed',
testCurrentRetry: 0,
testId: 'r4',
timeout: 4000,
type: 'child',
url: 'http://www.foobar.com',
viewportHeight: 660,
viewportWidth: 1000,
visible: true,
wallClockStartedAt: '2022-04-18T21:52:37.833Z',
}
const { consoleProps, snapshots, $el, ...logAttrs } = reifyLogFromSerialization(mockPreprocessedLogAttrs)
expect(logAttrs).to.deep.equal({
alias: undefined,
chainerId: 'mock-chainer-id',
coords: { top: 50, left: 50, topCenter: 100, leftCenter: 1000, x: 100, y: 50 },
ended: true,
err: undefined,
event: false,
highlightAttr: 'data-cypress-el',
hookId: 'r4',
id: 'mock-log-id',
instrument: 'command',
message: '',
name: 'click',
numElements: 1,
referencesAlias: undefined,
renderProps: {},
state: 'passed',
testCurrentRetry: 0,
testId: 'r4',
timeout: 4000,
type: 'child',
url: 'http://www.foobar.com',
viewportHeight: 660,
viewportWidth: 1000,
visible: true,
wallClockStartedAt: '2022-04-18T21:52:37.833Z',
})
expect($el.jquery).to.be.ok
expect($el.length).to.equal(1)
expect($el[0]).to.be.instanceOf(HTMLFormElement)
expect($el[0].id).to.equal('button-inside-a')
expect($el[0].textContent).to.equal('click button')
// most of the consoleProps logic is tested in the e2e/multi-domain folder. focus in this test will be mostly snapshot serialization
expect(consoleProps['Applied To']).to.be.instanceOf(HTMLFormElement)
expect(consoleProps['Applied To']).to.have.property('id').that.equals('button-inside-a')
expect(consoleProps['Applied To']).to.have.property('textContent').that.equals('click button')
expect(consoleProps.table).to.have.property('1')
expect(consoleProps.table[1]).to.be.a('function')
expect(snapshots).to.have.lengthOf(2)
expect(snapshots[0]).to.have.property('name').that.equals('before')
expect(snapshots[0]).to.have.property('htmlAttrs').that.deep.equals({})
// styles should now live in the CSS map after a snapshot is processed through createSnapshot and snapshots should exist in document map
expect(snapshots[0]).to.not.have.property('styles')
expect(snapshots[0]).to.have.property('body').that.has.property('get').that.is.a('function')
const snapshotBodyBefore = snapshots[0].body.get()
expect(snapshotBodyBefore.length).to.equal(1)
expect(snapshotBodyBefore[0]).to.be.instanceOf(HTMLBodyElement)
// verify to some degree that the reified elements above can be matched into the snapshot
expect(snapshotBodyBefore[0].querySelector('form#button-inside-a')).to.be.instanceOf(HTMLFormElement)
expect(snapshots[1]).to.have.property('name').that.equals('after')
expect(snapshots[1]).to.have.property('htmlAttrs').that.deep.equals({})
expect(snapshots[1]).to.not.have.property('styles')
expect(snapshots[1]).to.have.property('body').that.has.property('get').that.is.a('function')
const snapshotBodyAfter = snapshots[1].body.get()
expect(snapshotBodyAfter.length).to.equal(1)
expect(snapshotBodyAfter[0]).to.be.instanceOf(HTMLBodyElement)
// verify to some degree that the reified elements above can be matched into the snapshot
expect(snapshotBodyAfter[0].querySelector('form#button-inside-a')).to.be.instanceOf(HTMLFormElement)
})
// purpose of these 'DOM Elements' tests is to give a very basic understanding of how DOM element serialization works in the log serializer
context('DOM Elements- preprocesses/reifies a given DOM element with stateful', () => {
context('input', () => {
it('preprocess', () => {
const inputElement = document.createElement('input')
inputElement.type = 'text'
inputElement.value = 'foo'
inputElement.setAttribute('data-cy', 'bar')
const snapshot = buildSnapshot(inputElement)
snapshot.setAttribute('foo', 'bar')
const preprocessedSnapshot = preprocessDomElement(snapshot)
expect(preprocessedSnapshot).to.have.property('tagName').that.equals('BODY')
expect(preprocessedSnapshot).to.have.property('serializationKey').that.equals('dom')
expect(preprocessedSnapshot).to.have.property('attributes').that.deep.equals({
foo: 'bar',
})
expect(preprocessedSnapshot).to.have.property('innerHTML').that.equals(`<div><h1>Mock Snapshot Header</h1><input type="text" data-cy="bar" value="foo"></div>`)
})
it('reifies', () => {
const preprocessedSnapshot = {
tagName: 'BODY',
serializationKey: 'dom',
attributes: {
foo: 'bar',
},
innerHTML: `<div><h1>Mock Snapshot Header</h1><input type="text" data-cy="bar" value="foo"></div>`,
}
const reifiedSnapshot = reifyDomElement(preprocessedSnapshot)
expect(reifiedSnapshot).to.be.instanceOf(HTMLBodyElement)
expect(reifiedSnapshot.getAttribute('foo')).to.equal('bar')
expect(reifiedSnapshot.querySelector('input[type="text"][value="foo"][data-cy="bar"]')).to.be.instanceOf(HTMLInputElement)
})
})
context('select', () => {
it('preprocess', () => {
const selectElement = document.createElement('select')
selectElement.id = 'metasyntactic-variables'
selectElement.name = 'Metasyntactic Variables'
const options = ['Hank Hill', 'Buck Strickland', 'Donna', 'Old Donna'].map((val) => {
const option = document.createElement('option')
option.value = val
return option
})
options.forEach((option) => selectElement.appendChild(option))
selectElement.selectedIndex = 1
const snapshot = buildSnapshot(selectElement)
const preprocessedSnapshot = preprocessDomElement(snapshot)
expect(preprocessedSnapshot).to.have.property('tagName').that.equals('BODY')
expect(preprocessedSnapshot).to.have.property('serializationKey').that.equals('dom')
expect(preprocessedSnapshot).to.have.property('innerHTML').that.equals(`<div><h1>Mock Snapshot Header</h1><select id="metasyntactic-variables" name="Metasyntactic Variables"><option value="Hank Hill"></option><option value="Buck Strickland" selected="true"></option><option value="Donna"></option><option value="Old Donna"></option></select></div>`)
})
it('reifies', () => {
const preprocessedSnapshot = {
tagName: 'BODY',
serializationKey: 'dom',
attributes: {},
innerHTML: `<div><h1>Mock Snapshot Header</h1><select id="metasyntactic-variables" name="Metasyntactic Variables"><option value="Hank Hill"></option><option value="Buck Strickland" selected="true"></option><option value="Donna"></option><option value="Old Donna"></option></select></div>`,
}
const reifiedSnapshot = reifyDomElement(preprocessedSnapshot)
expect(reifiedSnapshot).to.be.instanceOf(HTMLBodyElement)
expect(reifiedSnapshot.querySelector('select#metasyntactic-variables option[selected]')).to.have.property('value').that.equals('Buck Strickland')
})
})
context('textarea', () => {
it('preprocess', () => {
const textAreaElement = document.createElement('textarea')
textAreaElement.rows = 4
textAreaElement.cols = 20
textAreaElement.value = 'Generic variable names that function as placeholders'
const snapshot = buildSnapshot(textAreaElement)
const preprocessedSnapshot = preprocessDomElement(snapshot)
expect(preprocessedSnapshot).to.have.property('tagName').that.equals('BODY')
expect(preprocessedSnapshot).to.have.property('serializationKey').that.equals('dom')
expect(preprocessedSnapshot).to.have.property('innerHTML').that.equals(`<div><h1>Mock Snapshot Header</h1><textarea rows="4" cols="20">Generic variable names that function as placeholders</textarea></div>`)
})
it('reifies', () => {
const preprocessedSnapshot = {
tagName: 'BODY',
serializationKey: 'dom',
attributes: {},
innerHTML: `<div><h1>Mock Snapshot Header</h1><textarea rows="4" cols="20">Generic variable names that function as placeholders</textarea></div>`,
}
const reifiedSnapshot = reifyDomElement(preprocessedSnapshot)
expect(reifiedSnapshot).to.be.instanceOf(HTMLBodyElement)
expect(reifiedSnapshot.querySelector('textarea[rows="4"]')).to.have.property('textContent').that.equals('Generic variable names that function as placeholders')
})
})
context('radio', () => {
it('preprocess', () => {
const formElement = document.createElement('form')
const radioInputs = ['foo', 'bar', 'baz'].map((val) => {
const radioInput = document.createElement('input')
radioInput.type = 'radio'
radioInput.value = val
return radioInput
})
radioInputs[1].checked = true
radioInputs.forEach((radioInput) => formElement.appendChild(radioInput))
const snapshot = buildSnapshot(formElement)
const preprocessedSnapshot = preprocessDomElement(snapshot)
expect(preprocessedSnapshot).to.have.property('tagName').that.equals('BODY')
expect(preprocessedSnapshot).to.have.property('serializationKey').that.equals('dom')
expect(preprocessedSnapshot).to.have.property('innerHTML').that.equals(`<div><h1>Mock Snapshot Header</h1><form><input type="radio" value="foo"><input type="radio" value="bar" checked=""><input type="radio" value="baz"></form></div>`)
})
it('reifies', () => {
const preprocessedSnapshot = {
tagName: 'BODY',
serializationKey: 'dom',
attributes: {},
innerHTML: `<div><h1>Mock Snapshot Header</h1><form><input type="radio" value="foo"><input type="radio" value="bar" checked=""><input type="radio" value="baz"></form></div>`,
}
const reifiedSnapshot = reifyDomElement(preprocessedSnapshot)
expect(reifiedSnapshot).to.be.instanceOf(HTMLBodyElement)
expect(reifiedSnapshot.querySelector('form input[value="bar"]')).to.have.property('checked').that.equals(true)
})
})
context('checkbox', () => {
it('preprocess', () => {
const formElement = document.createElement('form')
const checkboxInputs = ['foo', 'bar', 'bar'].map((val) => {
const checkboxInput = document.createElement('input')
checkboxInput.type = 'checkbox'
checkboxInput.value = val
return checkboxInput
})
checkboxInputs[1].checked = true
checkboxInputs.forEach((checkboxInput) => formElement.appendChild(checkboxInput))
const snapshot = buildSnapshot(formElement)
const preprocessedSnapshot = preprocessDomElement(snapshot)
expect(preprocessedSnapshot).to.have.property('tagName').that.equals('BODY')
expect(preprocessedSnapshot).to.have.property('serializationKey').that.equals('dom')
expect(preprocessedSnapshot).to.have.property('innerHTML').that.equals(`<div><h1>Mock Snapshot Header</h1><form><input type="checkbox" value="foo"><input type="checkbox" value="bar" checked=""><input type="checkbox" value="bar"></form></div>`)
})
it('reifies', () => {
const preprocessedSnapshot = {
tagName: 'BODY',
serializationKey: 'dom',
attributes: {},
innerHTML: `"<div><h1>Mock Snapshot Header</h1><form><input type="checkbox" value="foo"><input type="checkbox" value="bar" checked=""><input type="checkbox" value="bar"></form></div>"`,
}
const reifiedSnapshot = reifyDomElement(preprocessedSnapshot)
expect(reifiedSnapshot).to.be.instanceOf(HTMLBodyElement)
expect(reifiedSnapshot.querySelector('form input[value="bar"]')).to.have.property('checked').that.equals(true)
})
})
})
// purpose of these 'DOM Elements' tests is to give a very basic understanding of how DOM element serialization works in the log serializer
context('Functions', () => {
it('does NOT try to serialize a function unless `attemptToSerializeFunctions` is set to true', () => {
const serializedFunction = preprocessLogLikeForSerialization(() => 'foo')
expect(serializedFunction).to.be.null
})
it('Tries to serialize EXPLICIT/KNOWN serializable functions by setting `attemptToSerializeFunctions` to true', () => {
const functionContents = [
'foo',
{
bar: 'baz',
},
document.createElement('html'),
]
const myKnownSerializableFunction = () => functionContents
const serializedFunction = preprocessLogLikeForSerialization(myKnownSerializableFunction, true)
expect(serializedFunction).to.deep.equal({
serializationKey: 'function',
value: [
'foo',
{
bar: 'baz',
},
{
tagName: 'HTML',
serializationKey: 'dom',
attributes: {},
innerHTML: '',
},
],
})
})
})
})
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<button data-cy="approve-orig">Approve Redirect to originator after auth</button>
<button data-cy="approve-me">Approve Redirect to me after auth</button>
<script>
const queryString = location.search;
const urlParams = new URLSearchParams(queryString);
const token = urlParams.get('token')
const addTokenToRedirect = (href, jwt) => {
const redirectUrl = new URL(href)
const redirectUrlParams = new URLSearchParams(redirectUrl.search);
redirectUrlParams.append('token', jwt)
redirectUrl.search = redirectUrlParams.toString()
return redirectUrl.href
}
const getRedirectHref = () => {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
let redirect = urlParams.get('redirect')
if (redirect){
return decodeURIComponent(redirect)
}
return 'http://localhost:3500/fixtures/auth/index.html'
}
if (token) {
// Add Login button that redirects to the idp
const loginBtn = document.createElement("button");
loginBtn.innerHTML = "Login Successful - return to main site"
loginBtn.dataset.cy = "login-success"
loginBtn.onclick = function () {
const redirectHref = getRedirectHref()
window.location.href = addTokenToRedirect(redirectHref, token)
};
document.body.appendChild(loginBtn);
}
document.querySelector('[data-cy="approve-orig"]').addEventListener('click', () => {
const redirectHref = getRedirectHref()
window.location.href = `http://www.idp.com:3500/fixtures/auth/idp.html?redirect=${encodeURIComponent(redirectHref)}`
})
document.querySelector('[data-cy="approve-me"]').addEventListener('click', () => {
window.location.href = `http://www.idp.com:3500/fixtures/auth/idp.html?redirect=${encodeURIComponent(window.location.href)}`
})
</script>
</body>
</html>
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<select data-cy="sameSite">
<option value="">Empty</option>
<option value="None">None</option>
<option value="Lax">Lax</option>
<option value="Strict">Strict</option>
<option value="Invalid">Invalid</option>
</select>
<label>
<input type="checkbox" data-cy="secure" /> Secure
</label>
<label for="name">Username: </label>
<input data-cy="username" type="text" required>
<button data-cy="login">Login</button>
<script>
document.querySelector('[data-cy="login"]').addEventListener('click', () => {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const redirect = urlParams.get('redirect')
const username = document.querySelector('[data-cy=username]').value
const sameSite = document.querySelector('[data-cy=sameSite]').value
const secure = document.querySelector('[data-cy=secure]').checked
let url = `https://www.foobar.com:3502/cookie-login?redirect=${redirect}&username=${username}`
if (sameSite) {
url += `&sameSite=${sameSite}`
}
if (secure) {
url += `&secure=true`
}
window.location.href = url
})
</script>
</body>
</html>
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<button data-cy="to-localhost" onclick="setTimeout(()=>{window.location.href = 'http://localhost:3500/fixtures/auth/index.html'}, 1500)">navigate to localhost</button>
<button data-cy="to-foobar" onclick="setTimeout(()=>{window.location.href = 'http://www.foobar.com:3500/fixtures/auth/index.html'}, 1500)">navigate to foobar</button>
<button data-cy="to-idp" onclick="setTimeout(()=>{window.location.href = 'http://www.idp.com:3500/fixtures/auth/index.html'}, 1500)">navigate to idp</button>
</body>
</html>
@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<label for="name">Username: </label>
<input data-cy="username" type="text" name="name" id="name" required>
<button data-cy="login">Login</button>
<button data-cy="slow-login">Slow Login</button>
<script>
const createToken = (username) => {
const expiry = new Date()
// Expire in 10 minutes
expiry.setMinutes(expiry.getMinutes() + 10)
// create faux jwt
return {
header: {
alg: 'string',
type: 'JWT'
},
body: {
username,
expiry: expiry.toISOString()
}
}
}
const getRedirectHref = () => {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
let redirect = urlParams.get('redirect')
if (redirect){
return decodeURIComponent(redirect)
}
return 'http://localhost:3500/fixtures/auth/index.html'
}
const addTokenToRedirect = (href, token) => {
const redirectUrl = new URL(href)
const redirectUrlParams = new URLSearchParams(redirectUrl.search);
redirectUrlParams.append('token', encodeURIComponent(JSON.stringify(token)))
redirectUrl.search = redirectUrlParams.toString()
return redirectUrl.href
}
document.querySelector('[data-cy="login"]').addEventListener('click', () => {
const token = createToken(document.querySelector('[data-cy="username"]').value)
const redirectHref = getRedirectHref()
window.location.href = addTokenToRedirect(redirectHref, token)
})
document.querySelector('[data-cy="slow-login"]').addEventListener('click', () => {
const token = createToken(document.querySelector('[data-cy="username"]').value)
const redirectHref = getRedirectHref()
setTimeout(() => {
window.location.href = addTokenToRedirect(redirectHref, token)
}, 5000);
})
</script>
</body>
</html>
@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<script>
const sessionStorage = window.sessionStorage;
// If there is a new token, set it. This is clearly not secure and no one should setup real auth this way
const queryString = location.search;
const urlParams = new URLSearchParams(queryString);
const newToken = urlParams.get('token')
if (newToken) {
sessionStorage.setItem('cypressAuthToken', decodeURIComponent(newToken));
urlParams.delete('token')
const newSearchParams = urlParams.toString()
let newLocation = `${location.origin}${location.pathname}`
if(newSearchParams){
newLocation = `${newLocation}?${newSearchParams}`
}
if(location.hash){
newLocation = `${newLocation}${location.hash}`
}
location.replace(newLocation)
}
const cypressAuthToken = sessionStorage.getItem('cypressAuthToken');
// If a token doesn't exist we aren't logged in
if (!cypressAuthToken){
// Add Message
const tag = document.createElement("p");
const text = document.createTextNode("You are not logged in");
tag.appendChild(text)
const body = document.body
body.appendChild(tag)
// Add Login button that redirects to the idp
const loginBtn = document.createElement("button");
loginBtn.innerHTML = "Login IDP"
loginBtn.dataset.cy = "login-idp"
loginBtn.onclick = function () {
window.location.href = `http://www.idp.com:3500/fixtures/auth/idp.html?redirect=${encodeURIComponent(window.location.href)}`
};
document.body.appendChild(loginBtn);
// Add Login button that redirects to the idp
const loginFoobarBtn = document.createElement("button");
loginFoobarBtn.innerHTML = "Login Foobar"
loginFoobarBtn.dataset.cy = "login-foobar"
loginFoobarBtn.onclick = function () {
window.location.href = `http://www.foobar.com:3500/fixtures/auth/idp.html?redirect=${encodeURIComponent(window.location.href)}`
};
document.body.appendChild(loginFoobarBtn);
// Add Login button that redirects to the idp
const loginWithApprovalBtn = document.createElement("button");
loginWithApprovalBtn.innerHTML = "Login With Approval"
loginWithApprovalBtn.dataset.cy = "login-with-approval"
loginWithApprovalBtn.onclick = function () {
window.location.href = `http://wwww.foobar.com:3500/fixtures/auth/approval.html?redirect=${encodeURIComponent(window.location.href)}`
};
document.body.appendChild(loginWithApprovalBtn);
} else {
const token = JSON.parse(cypressAuthToken)
// If the token exists, hooray, give them a logout button to destroy the token and refresh.
const tag = document.createElement("p");
const text = document.createTextNode(`Welcome ${token.body.username}`);
tag.dataset.cy = "welcome"
tag.appendChild(text)
const body = document.body
body.appendChild(tag)
// Add log out button
const btn = document.createElement("button");
btn.innerHTML = "Logout";
btn.dataset.cy = "logout"
btn.onclick = function () {
sessionStorage.removeItem('cypressAuthToken');
location.reload()
};
document.body.appendChild(btn);
}
</script>
</body>
</html>
+3 -3
View File
@@ -437,8 +437,8 @@
</select>
<select name="foods">
<option value="Japanese">Ramen</option>
<option value="Ramen">Japanese</option>
<option value="Ramen">Ramen</option>
<option value="Japanese">Japanese</option>
</select>
<select name="names">
@@ -463,7 +463,7 @@
</select>
<fieldset disabled>
<select name="fielset-disabled">
<select name="fieldset-disabled">
<option value="foo">foo</option>
<option value="bar">bar</option>
</select>
@@ -1,44 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>DOM Fixture</title>
<script type="text/javascript" src="/node_modules/sinon/dist/sinon.js"></script>
<script type="text/javascript" src="/node_modules/jquery/dist/jquery.js"></script>
</head>
<body>
<div id="background">
<link rel="stylesheet" href="/node_modules/@cypress/bower-kendo-ui/styles/kendo.common.min.css">
<link rel="stylesheet" href="/node_modules/@cypress/bower-kendo-ui/styles/kendo.default.min.css">
<style>
#background {
position: fixed;
background-color: gray;
width: 100%;
height: 100%;
}
</style>
<input id="dropdown" />
<script type="text/javascript" src="/node_modules/@cypress/bower-kendo-ui/js/kendo.ui.core.min.js"></script>
<script type="text/javascript">
$(function(){
$("#dropdown").kendoDropDownList({
optionLabel: "--Select--",
dataTextField: "t",
dataValueField: "v",
dataSource: [
{v: 1, t: "Apples"},
{v: 2, t: "Bananas"},
{v: 3, t: "Pears"},
{v: 4, t: "Watermelon"},
{v: 5, t: "Grapes"},
{v: 6, t: "Strawberries"}
]
})
})
</script>
</div>
</body>
</html>
@@ -1,32 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>DOM Fixture</title>
<script type="text/javascript" src="/node_modules/sinon/dist/sinon.js"></script>
<script type="text/javascript" src="/node_modules/jquery/dist/jquery.js"></script>
<script type="text/javascript" src="/node_modules/bootstrap/dist/js/bootstrap.js"></script>
</head>
<body>
<div id="clicks">
<link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.css">
<style>
body {
padding-top: 70px;
}
#clicks {
min-height: 2000px;
}
</style>
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">Brand</a>
</div>
</div>
</nav>
<div class="container">
<button class="btn btn-primary">click me</button>
</div>
</div>
</body>
</html>
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<p data-cy="dom-check">From a secondary origin</p>
<p data-cy="cypress-check"></p>
<p data-cy="window-before-load"></p>
<form>
<button type="submit">Submit</button>
</form>
<button data-cy="alert">Alert</button>
<button data-cy="confirm">Confirm</button>
<button data-cy="reload">reload</button>
<a data-cy="hashChange" href='#hashChange'>hashChange</a>
<a data-cy="cross-origin-page"
href="/fixtures/multi-domain.html">/fixtures/multi-domain.html</a>
<script>
if (window.Cypress) {
document.querySelector('[data-cy="cypress-check"]').innerText = 'Has window.Cypress'
}
if (window.testSecondaryWindowBeforeLoad){
document.querySelector('[data-cy="window-before-load"]').innerText = 'Window Before Load Called'
}
document.querySelector('[data-cy="alert"]').addEventListener('click', () => {
window.alert('the alert text')
})
document.querySelector('[data-cy="confirm"]').addEventListener('click', () => {
window.confirm('the confirm text')
})
document.querySelector('[data-cy="reload"]').addEventListener('click', () => {
window.location.reload()
})
</script>
</body>
</html>
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<style>
a {
display: block;
}
</style>
<div>
Go to different origin:
<a data-cy="cross-origin-secondary-link"
href="http://www.foobar.com:3500/fixtures/multi-domain-secondary.html">http://www.foobar.com:3500/fixtures/multi-domain-secondary.html</a>
<a data-cy="dom-link" href="http://www.foobar.com:3500/fixtures/dom.html">http://www.foobar.com:3500/fixtures/dom.html</a>
<a data-cy="scrolling-link" href="http://www.foobar.com:3500/fixtures/scrolling.html">http://www.foobar.com:3500/fixtures/scrolling.html</a>
<a data-cy="request-link" href="http://www.foobar.com:3500/fixtures/request.html">http://www.foobar.com:3500/fixtures/request.html</a>
<a data-cy="shadow-dom-link" href="http://www.foobar.com:3500/fixtures/shadow-dom.html">http://www.foobar.com:3500/fixtures/shadow-dom.html</a>
<a data-cy="files-form-link" href="http://www.foobar.com:3500/fixtures/files-form.html">http://www.foobar.com:3500/fixtures/files-form.html</a>
<a data-cy="errors-link" href="http://www.foobar.com:3500/fixtures/errors.html">http://www.foobar.com:3500/fixtures/errors.html</a>
<a data-cy="screenshots-link" href="http://www.foobar.com:3500/fixtures/screenshots.html">http://www.foobar.com:3500/fixtures/screenshots.html</a>
<a data-cy="cookie-login" href="http://www.foobar.com:3500/fixtures/auth/cookie-login.html?redirect=http://localhost:3500/login">Login with Social</a>
<a data-cy="welcome" href="http://localhost:3500/welcome">Go to Welcome</a>
</div>
</body>
</html>
+70
View File
@@ -10,6 +10,7 @@ const multer = require('multer')
const upload = multer({ dest: 'cypress/_test-output/' })
const PATH_TO_SERVER_PKG = path.dirname(require.resolve('@packages/server'))
const httpPorts = [3500, 3501]
const httpsPort = 3502
@@ -25,6 +26,7 @@ const createApp = (port) => {
})
app.use(require('cors')())
app.use(require('cookie-parser')())
app.use(require('compression')())
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
@@ -35,6 +37,10 @@ const createApp = (port) => {
return res.sendStatus(200)
})
app.get('/', (req, res) => {
return res.send('<html><body>root page</body></html>')
})
app.get('/timeout', (req, res) => {
return Promise
.delay(req.query.ms || 0)
@@ -127,6 +133,7 @@ const createApp = (port) => {
return res
.set('WWW-Authenticate', 'Basic')
.type('html')
.sendStatus(401)
})
@@ -188,6 +195,69 @@ const createApp = (port) => {
.send('<html><body>server error</body></html>')
})
const getCookieAdditions = ({ sameSite, secure }) => {
let additions = ''
if (sameSite) additions += `; SameSite=${sameSite}`
if (secure) additions += '; Secure'
return additions
}
app.get('/cookie-login', (req, res) => {
const { username, redirect } = req.query
res
.header('Set-Cookie', `user=${username}${getCookieAdditions(req.query)}`)
.redirect(302, `/verify-cookie-login?username=${username}&redirect=${redirect}`)
})
app.get('/verify-cookie-login', (req, res) => {
if (!req.cookies.user) {
return res
.status(403)
.send('<html><body><h1>Not logged in</h1></body></html>')
}
const { username, redirect } = req.query
res.send(`
<html>
<body>
<h1>Redirecting ${username}...</h1>
<script>
setTimeout(() => {
window.location.href = '${redirect}?username=${username}'
}, 1000)
</script>
</body>
</html>
`)
})
app.get('/login', (req, res) => {
const { username } = req.query
if (!username) {
return res.send('<html><body><h1>Must specify username to log in</h1></body></html>')
}
// can't use res.cookie() because it won't allow setting an invalid
// SameSite value, which we want to test
res
.header('Set-Cookie', `user=${username}${getCookieAdditions(req.query)}`)
.redirect(302, '/welcome')
})
app.get('/welcome', (req, res) => {
if (!req.cookies.user) {
return res.send('<html><body><h1>No user found</h1></body></html>')
}
res.send(`<html><body><h1>Welcome, ${req.cookies.user}!</h1></body></html>`)
})
let _var = ''
app.get('/set-var', (req, res) => {
+20
View File
@@ -72,6 +72,20 @@ export const assertLogLength = (logs, expectedLength) => {
expect(logs.length).to.eq(expectedLength, `received ${logs.length} logs when we expected ${expectedLength}: [${receivedLogs}]`)
}
export const findCrossOriginLogs = (consolePropCommand, logMap, matchingOrigin) => {
const matchedLogs = Array.from(logMap.values()).filter((log) => {
const props = log.get()
let consoleProps = _.isFunction(props?.consoleProps) ? props.consoleProps() : props?.consoleProps
return consoleProps.Command === consolePropCommand && props.id.includes(matchingOrigin)
})
const logAttrs = matchedLogs.map((log) => log.get())
return logAttrs.length === 1 ? logAttrs[0] : logAttrs
}
export const attachListeners = (listenerArr) => {
return (els) => {
_.each(els, (el, elName) => {
@@ -94,6 +108,10 @@ const getAllFn = (...aliases) => {
)
}
const shouldWithTimeout = (cb, timeout = 250) => {
cy.wrap({}, { timeout }).should(cb)
}
export const keyEvents = [
'keydown',
'keyup',
@@ -120,6 +138,8 @@ export const expectCaret = (start, end) => {
Cypress.Commands.add('getAll', getAllFn)
Cypress.Commands.add('shouldWithTimeout', shouldWithTimeout)
const chaiSubset = require('chai-subset')
chai.use(chaiSubset)
+7 -10
View File
@@ -5,25 +5,26 @@
"scripts": {
"clean-deps": "rimraf node_modules",
"cypress:open": "node ../../scripts/cypress open",
"cypress:run": "node ../../scripts/cypress run",
"cypress:run": "node ../../scripts/cypress run --spec \"cypress/integration/*/*\",\"cypress/integration/*/!(multi-domain)/**/*\"",
"cypress:open-experimentalSessionAndOrigin": "node ../../scripts/cypress open --config experimentalSessionAndOrigin=true",
"cypress:run-experimentalSessionAndOrigin": "node ../../scripts/cypress run --config experimentalSessionAndOrigin=true",
"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 e2e.setupNodeEvents config.\n\tChanges to the server will be watched and reloaded automatically.`))'"
},
"dependencies": {},
"devDependencies": {
"@babel/code-frame": "7.8.3",
"@cypress/bower-kendo-ui": "0.0.2",
"@cypress/sinon-chai": "2.9.1",
"@cypress/unique-selector": "0.4.4",
"@cypress/webpack-preprocessor": "0.0.0-development",
"@cypress/what-is-circular": "1.0.1",
"@packages/config": "0.0.0-development",
"@packages/network": "0.0.0-development",
"@packages/resolve-dist": "0.0.0-development",
"@packages/runner": "0.0.0-development",
"@packages/runner-shared": "0.0.0-development",
"@packages/server": "0.0.0-development",
"@packages/socket": "0.0.0-development",
"@packages/ts": "0.0.0-development",
"@rollup/plugin-node-resolve": "^13.0.4",
"@sinonjs/fake-timers": "8.1.0",
"@types/chalk": "^2.2.0",
"@types/common-tags": "^1.8.0",
@@ -36,14 +37,13 @@
"blob-util": "2.0.2",
"bluebird": "3.5.3",
"body-parser": "1.19.0",
"bootstrap": "4.4.1",
"bytes": "3.1.0",
"chai": "4.2.0",
"chai-as-promised": "7.1.1",
"chai-subset": "1.6.0",
"chokidar-cli": "2.1.0",
"clone": "2.1.2",
"compression": "1.7.4",
"cookie-parser": "1.4.5",
"core-js-pure": "3.21.0",
"cors": "2.8.5",
"cypress-multi-reporters": "1.4.0",
"dayjs": "^1.10.3",
@@ -56,7 +56,6 @@
"is-valid-hostname": "1.0.1",
"jquery": "3.1.1",
"js-cookie": "2.2.1",
"jsdom": "14.1.0",
"json-stable-stringify": "1.0.1",
"lodash": "^4.17.21",
"md5": "2.3.0",
@@ -64,9 +63,7 @@
"methods": "1.1.2",
"mime-types": "2.1.27",
"minimatch": "3.0.4",
"minimist": "1.2.6",
"mocha": "7.0.1",
"morgan": "1.9.1",
"multer": "1.4.2",
"ordinal": "1.0.3",
"react-15.6.1": "npm:react@15.6.1",
@@ -4,16 +4,29 @@ import $dom from '../../../dom'
import $utils from '../../../cypress/utils'
import $errUtils from '../../../cypress/error_utils'
import $elements from '../../../dom/elements'
import type { Log } from '../../../cypress/log'
interface InternalFocusOptions extends Partial<Cypress.Loggable & Cypress.Timeoutable> {
_log?: Log
$el: JQuery
error: boolean
verify: boolean
}
interface InternalBlurOptions extends Partial<Cypress.BlurOptions> {
_log?: Log
$el: JQuery
$focused: JQuery
error: boolean
verify: boolean
}
export default (Commands, Cypress, cy) => {
return Commands.addAll({ prevSubject: ['element', 'window'] }, {
// TODO: any -> Partial<Cypress.Loggable & Cypress.Timeoutable>
focus (subject, options: any = {}) {
const userOptions = options
focus (subject, userOptions: Partial<Cypress.Loggable & Cypress.Timeoutable> = {}) {
// we should throw errors by default!
// but allow them to be silenced
options = _.defaults({}, userOptions, {
const options: InternalFocusOptions = _.defaults({}, userOptions, {
$el: subject,
error: true,
log: true,
@@ -85,13 +98,10 @@ export default (Commands, Cypress, cy) => {
return verifyAssertions()
},
// TODO: any -> Partial<Cypress.BlurOptions>
blur (subject, options: any = {}) {
const userOptions = options
blur (subject, userOptions: Partial<Cypress.BlurOptions> = {}) {
// we should throw errors by default!
// but allow them to be silenced
options = _.defaults({}, userOptions, {
const options: InternalBlurOptions = _.defaults({}, userOptions, {
$el: subject,
$focused: cy.getFocused(),
error: true,
@@ -5,6 +5,7 @@ import Promise from 'bluebird'
import $dom from '../../../dom'
import $utils from '../../../cypress/utils'
import $errUtils from '../../../cypress/error_utils'
import type { Log } from '../../../cypress/log'
const findScrollableParent = ($el, win) => {
const $parent = $dom.getParent($el)
@@ -28,12 +29,26 @@ const isNaNOrInfinity = (item) => {
return _.isNaN(num) || !_.isFinite(num)
}
interface InternalScrollIntoViewOptions extends Partial<Cypress.ScrollToOptions> {
_log?: Log
$el: JQuery
$parent: any
axis: string
offset?: object
}
interface InternalScrollToOptions extends Partial<Cypress.ScrollToOptions> {
_log?: Log
$el: any
x: number
y: number
error?: any
axis: string
}
export default (Commands, Cypress, cy, state) => {
Commands.addAll({ prevSubject: 'element' }, {
// TODO: any -> Partial<Cypress.ScrollToOptions>
scrollIntoView (subject, options: any = {}) {
const userOptions = options
scrollIntoView (subject, userOptions: Partial<Cypress.ScrollToOptions> = {}) {
if (!_.isObject(userOptions)) {
$errUtils.throwErrByPath('scrollIntoView.invalid_argument', { args: { arg: userOptions } })
}
@@ -49,7 +64,7 @@ export default (Commands, Cypress, cy, state) => {
$errUtils.throwErrByPath('scrollIntoView.multiple_elements', { args: { num: subject.length } })
}
options = _.defaults({}, userOptions, {
const options: InternalScrollIntoViewOptions = _.defaults({}, userOptions, {
$el: subject,
$parent: state('window'),
log: true,
@@ -115,9 +130,6 @@ export default (Commands, Cypress, cy, state) => {
const scrollIntoView = () => {
return new Promise((resolve, reject) => {
// scroll our axes
// TODO: done() came from jQuery animate(), specifically, EffectsOptions at misc.d.ts
// The type definition should be fixed at @types/jquery.scrollto.
// @ts-ignore
return $(options.$parent).scrollTo(options.$el, {
axis: options.axis,
easing: options.easing,
@@ -157,10 +169,8 @@ export default (Commands, Cypress, cy, state) => {
})
Commands.addAll({ prevSubject: ['optional', 'element', 'window'] }, {
// TODO: any -> Partial<Cypress.ScrollToOptions>
scrollTo (subject, xOrPosition, yOrOptions, options: any = {}) {
scrollTo (subject, xOrPosition, yOrOptions, userOptions: Partial<Cypress.ScrollToOptions> = {}) {
let x; let y
let userOptions = options
// check for undefined or null values
if (xOrPosition === undefined || xOrPosition === null) {
@@ -261,7 +271,7 @@ export default (Commands, Cypress, cy, state) => {
$errUtils.throwErrByPath('scrollTo.multiple_containers', { args: { num: $container.length } })
}
options = _.defaults({}, userOptions, {
const options: InternalScrollToOptions = _.defaults({}, userOptions, {
$el: $container,
log: true,
duration: 0,
@@ -361,10 +371,7 @@ export default (Commands, Cypress, cy, state) => {
const scrollTo = () => {
return new Promise((resolve, reject) => {
// scroll our axis'
// TODO: done() came from jQuery animate(), specifically, EffectsOptions at misc.d.ts
// The type definition should be fixed at @types/jquery.scrollto.
// @ts-ignore
// scroll our axis
$(options.$el).scrollTo({ left: x, top: y }, {
axis: options.axis,
easing: options.easing,

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