From c3fc24b85e5dad08b01e02f49ea3e9217c876ac1 Mon Sep 17 00:00:00 2001 From: DEBRIS APRON Date: Wed, 13 Apr 2022 20:57:49 -0700 Subject: [PATCH 01/34] Add cy.origin command API page --- content/_data/sidebar.json | 3 +- content/api/commands/origin.md | 467 +++++++++++++++++++++++++++++++++ 2 files changed, 469 insertions(+), 1 deletion(-) create mode 100755 content/api/commands/origin.md diff --git a/content/_data/sidebar.json b/content/_data/sidebar.json index e2126c0e6f..09f2e394a4 100644 --- a/content/_data/sidebar.json +++ b/content/_data/sidebar.json @@ -568,6 +568,7 @@ "title": "not", "slug": "not" }, + { "title": "origin", "slug": "origin" }, { "title": "parent", "slug": "parent" @@ -1011,4 +1012,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/content/api/commands/origin.md b/content/api/commands/origin.md new file mode 100755 index 0000000000..9ecb54aae4 --- /dev/null +++ b/content/api/commands/origin.md @@ -0,0 +1,467 @@ +--- +title: origin +--- + +Visit multiple domains of different +[origin](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy#definition_of_an_origin) +in a single test. + +In normal use, a single Cypress test may only run commands in a single origin, a +limitation determined by standard web security features of the browser. The +`cy.origin()` command allows your tests to bypass this limitation. + + + + +Experimental + +TODO Maybe rework this & move to a partial + +The `origin` API is currently experimental, and can be enabled by setting +the [`experimentalSessionAndOrigin`](/guides/references/experiments) flag +to `true` in the Cypress config or by +using [`Cypress.config()`](/api/cypress-api/config) at the top of a spec file. + +Enabling this flag does the following: + +- It adds the [`cy.session()`](/api/commands/session) and `cy.origin()` + commands, and [`Cypress.session`](/api/cypress-api/session) API. +- It adds the following new behaviors (that will be the default in a future + major update of Cypress) at the beginning of each test: + - The page is cleared (by setting it to `about:blank`). + - All active session data (cookies, `localStorage` and `sessionStorage`) + across all domains are cleared. +- It overrides + the [`Cypress.Cookies.preserveOnce()`](/api/cypress-api/cookies#Preserve-Once) and [`Cypress.Cookies.defaults()`](/api/cypress-api/cookies#Defaults) methods. +- Cross-domain requests will no longer fail immediately, but instead, time out + based on [`pageLoadTimeout`](/guides/references/configuration#Timeouts). +- Tests will no longer wait on page loads before moving on to the next test. + +Because the page is cleared before each +test, [`cy.visit()`](/api/commands/visit) must be explicitly called in each test +to visit a page in your application. + + + +## Syntax + +```js +cy.origin(url, callbackFn) +cy.origin(url, options, callbackFn) +``` + +### Usage + +** Correct Usage** + +```js +const hits = getHits() // Defined elsewhere +// Run commands in a secondary origin, passing in any serializable values we need +cy.origin('https://www.acme.com', { args: { hits } }, ({ hits }) => { + // Inside callback baseUrl is https://www.acme.com + cy.visit('/history/founder') + // Commands are executed in secondary origin + cy.get('h1').contains('About our Founder, Marvin Acme') + // Passed in values are accessed via callback args + cy.get('#hitcounter').contains(hits) +}) +// Even though we are outside the secondary origin block, we are still on acme.com +// so return to baseUrl +cy.visit('/') +// Continue running commands on primary origin +cy.get('h1').contains('My cool site under test') +``` + +** Incorrect Usage** + +```js +const hits = 9000 +// This should be inside the callback, Cypress needs to be injected before visit +cy.visit('https://www.acme.com/history/founder') +cy.origin('https://www.acme.com', () => { + // Won't work because Cypress is not present on secondary origin + cy.get('h1').contains('About our Founder, Marvin Acme') + // Won't work because hits is not passed in via args + cy.get('#hitcounter').contains(hits) +}) +// Won't work because still on acme.com +cy.get('h1').contains('My cool site under test') +``` + +### Arguments + +** url** **_(String)_** + +A URL specifying the secondary origin in which the callback is to be executed. +This should at the very least contain a hostname, and may also include the +protocol, port number & path. This argument will be used in two ways. + +Firstly, it uniquely identifies a secondary origin in which the commands in the +callback will be executed. The test-runner will inject the Cypress runtime into +this origin, and then send it code to evaluate in that origin, without violating +the browser's same-origin policy. + +Secondly, it temporarily overrides the `baseUrl` configured in your +[global configuration](/guides/references/configuration#Global) whilst inside +the callback. So `cy.visit()` will navigate relative to this URL, not the +configured `baseUrl`. + +** options** **_(Object)_** + +Pass in an options object to control the behavior of `cy.origin()`. + +| option | description | +| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| args | Plain JavaScript object which will be serialized and sent from the primary origin to the secondary origin, where it will be deserialized and passed into the callback function as its first and only argument. | + + + +The `args` object is the **only** mechanism via which data may be injected into +the callback, the callback is **not** a closure and does not retain access to +the JavaScript context in which it was declared. + + + +** callbackFn** **_(Function)_** + +The function containing the commands to be executed in the secondary origin. + +This function will be stringified, sent to the Cypress instance in the secondary +origin and evaluated. If the `args` option is specified, the deserialized args +object will be passed into the function as its first and only argument. + +There are a number of limitations placed on commands run inside the callback, +please see [Callback restrictions](#Callback-restrictions) section below for a +full list. + +The function's return value is ignored. ??? + +### Yields [](/guides/core-concepts/introduction-to-cypress#Subject-Management) + +- `cy.origin()` yields `null`. +- `cy.origin()` cannot be chained further. + +## Examples + +### Using dynamic data in a secondary origin + +Callbacks are executed inside an entirely separate instance of Cypress, so +arguments must be transmitted to the other instance by means of serialization & +deserialization. The interface for this mechanism is the `args` option. + +```js +const sentArgs = { password: 'P@55w0rd!!!1!?!111one' } +cy.origin( + 'supersecurelogons.com', + // Send the args here... + { args: sentArgs }, + // ...and receive them at the other end here! + (receivedArgs) => { + const { password } = receivedArgs + cy.visit('/login') + cy.get('input#password').type(password) + cy.contains('button', 'Login').click() + } +) +``` + +Note: You can just replace `sentArgs` and `receivedArgs` with `args` if you +want, the naming in this example is purely for clarity. + +### Navigating to secondary origin with cy.visit + +When navigating to a secondary origin using `cy.visit()`, it is essential to +trigger the navigation **after** entering the origin callback, otherwise a +cross-domain error will be thrown. + +```js +// Do things in primary domain... + +cy.origin('acme.com', () => { + // Visit https://www.acme.com/history/founder + cy.visit('/history/founder') + cy.get('h1').contains('About our Founder, Marvin Acme') +}) +``` + +```js +// TODO Example with onBeforeLoad... +``` + +TODO baseUrl + +TODO default protocol + +### Navigating to secondary origin with UI + +When navigating to a secondary origin by clicking a link or button in the +primary origin, it is essential to trigger the navigation _before_ entering the +origin callback, otherwise a cross-domain error will be thrown. + +```js +// Click button in primary origin that navigates to https://acme.com +cy.contains('button', 'Go to Acme.com').click() + +cy.origin('acme.com', () => { + // No cy.visit is needed as the button brought us here + cy.get('h1').contains('ACME CORP') +}) +``` + +### Navigating to multiple secondary origins in succession + +Callbacks may **not** themselves contain `cy.origin()` calls, so when visiting +multiple origins, do so at the top level of the test. + +```js +cy.origin('foo.com', () => { + cy.visit('/') + cy.url().should('contain', 'foo.com') +}) + +cy.origin('bar.com', () => { + cy.visit('/') + cy.url().should('contain', 'bar.com') +}) + +cy.origin('baz.com', () => { + cy.visit('/') + cy.url().should('contain', 'baz.com') +}) +``` + +TODO Stabilization? + +### Waiting to return to primary origin + +Sometimes, a secondary origin returns to the primary origin as a result of user +action (for example, by clicking a "Login" button on a syndicated login +provider). In this situation, your test should wait for the navigation to +complete before making further assertions, otherwise a cross-domain error will +be thrown. + +```js +cy.visit('/home') +// This will take us to the secondary origin +cy.contains('button', 'Go to someothersite').click() + +cy.origin('someothersite.com', () => { + // Click button that takes us back to primary origin + cy.contains('button', 'Go back').click() +) +// Wait until confirmation we are back at the primary origin before continuing +cy.url().should('contain', '/home') +// Do more things in primary domain... +``` + +### SSO login custom command + +A very common requirement is logging in to a site before running a test. If +login itself is not the specific focus of the test, it's good to encapsulate +this functionality in a `login` +[custom command](/api/cypress-api/custom-commands) so you don't have to +duplicate this login code in every test. Here's an idealized example of how to +do this with `cy.origin()`. + +```js +Cypress.Commands.add('login', (username, password) => { + // Remember to pass in dependencies via `args` + const args = { username, password } + cy.origin('auth-provider.com', { args }, ({ username, password }) => { + // Go to https://auth-provider.com/login + cy.visit('/login') + cy.contains('Username').find('input').type(username) + cy.contains('Password').find('input').type(password) + cy.get('button').contains('Login').click() + }) + // Wait until confirmation we are back at the primary origin before continuing + cy.url().should('contain', '/home') +}) +``` + +However, having to go through an entire login flow before every test is not very +performant. Up until now you could get around this by putting login code in the +first test of your file, then performing subsequent tests reusing the same +session. However, once the `experimentalSessionAndOrigin` flag is activated this +is no longer possible, as all session state is now cleared between tests. So to +avoid this overhead we have added the [`cy.session()`](/api/commands/session) +command, which allows you to easily cache session information and reuse it +across not just tests, but test files too. So now let's enhance our custom login +command with `cy.session()` for a complete syndicated login flow with session +caching and validation. No mocking, no workarounds, no third-party plugins! + +```js +Cypress.Commands.add('login', (username, password) => { + const args = { username, password } + cy.session( + // The username & password combination can be used as the cache key too + args, + () => { + cy.origin('auth-provider.com', { args }, ({ username, password }) => { + cy.visit('/login') + cy.contains('Username').find('input').type(username) + cy.contains('Password').find('input').type(password) + cy.get('button').contains('Login').click() + }) + cy.url().should('contain', '/home') + }, + { + validate() { + cy.request('/api/user').its('status').should('eq', 200) + }, + } + ) +}) +``` + +## Notes + +### Migrating existing tests + +Enabling the `experimentalSessionAndOrigin` flag makes the test-runner work +slightly differently, and some test suites that rely on the existing behaviour +may have to be updated. The most important of these changes is **test +isolation**. This means that after every test, the current page is reset to +[`about:blank`](https://en.wikipedia.org/wiki/About_URI_scheme#Standardization) +and all active session data (cookies, `localStorage` and `sessionStorage`) +across all domains are cleared. This change is opt-in for now, but will be +standardized in a future major release of Cypress, so eventually all tests will +need to be isolated. + +Before this change, it was possible to write tests such that you could, for +example, login to a CMS in the first test, change some content in the second +test, verify the new version is displayed on a different URL in the third, and +logout in the fourth. Here's a simplified example of such a test strategy. + +Before Multiple small tests against different +origins + +```js +it('logs in', () => { + cy.visit('https"//supersecurelogons.com') + cy.get('input#password').type('Password123!') + cy.get('button#submit').click() +}) + +it('updates the content', () => { + cy.get('#current-user').contains('logged in') + cy.get('button#edit-1').click() + cy.get('input#title').type('Updated title') + cy.get('button#submit').click() + cy.get('.toast').type('Changes saved!') +}) + +it('validates the change', () => { + cy.visit('/items/1') + cy.get('h1').contains('Updated title') +}) +``` + +After switching on `experimentalSessionAndOrigin`, this flow would need to be +contained within a single test. While this practice has always been +[discouraged](/guides/references/best-practices#Having-tests-rely-on-the-state-of-previous-tests) +we know some users have historically written tests this way, often to get around +the same-origin restrictions. But with `cy.origin()` you no longer need these +kind of brittle hacks, as your multi-origin logic can all reside in a single +test, like the following. + +After One big test using `cy.origin()` + +```js +it('securely edits content', () => { + cy.origin('supersecurelogons.com', () => { + cy.visit('https"//supersecurelogons.com') + cy.get('input#password').type('Password123!') + cy.get('button#submit').click() + }) + + cy.origin('mycms.com', () => { + cy.url().should('contain', 'cms') + cy.get('#current-user').contains('logged in') + cy.get('button#edit-1').click() + cy.get('input#title').type('Updated title') + cy.get('button#submit').click() + cy.get('.toast').type('Changes saved!') + }) + + cy.visit('/items/1') + cy.get('h1').contains('Updated title') +}) +``` + +Always remember, +[Cypress tests are not unit tests](https://docs.cypress.io/guides/references/best-practices#Creating-tiny-tests-with-a-single-assertion). + +### Serialization + +When entering a `cy.origin()` block, the test-runner injects the Cypress +runtime, with all your configurations settings, into the requested origin, and +sets up bidirectional communication with that instance. This coordination model +requires that any data sent from one instance to another be +[serialized](https://developer.mozilla.org/en-US/docs/Glossary/Serialization) +for transmission. It is very important to understand that variables **inside** +the callback are not shared with the scope **outside** the callback. For example +this will not work: + +```js +const foo = 1 +cy.origin('somesite.com', () => { + cy.visit('/') + // This line will throw a ReferenceError because `foo` is not defined in the + // scope of the callback + cy.get('input').type(foo) +}) +``` + +Instead, the variable must be explicitly passed into the callback using the +`args` option: + +```js +const foo = 1 +cy.origin('somesite.com', { args: { foo } }, ({ foo }) => { + cy.visit('/') + // Now it will pass + cy.get('input').type(foo) +}) +``` + +Underneath the hood, Cypress uses +[`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) +and +[`JSON.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) +(or equivalent library functions, implementation details may change) to +serialize and deserialize the `args` object. This introduces a number of +restrictions on the data which may be transmitted: + +- Primitive values only: object, array, string, number, boolean, null +- Date objects will be converted to strings +- Inherited properties are not serialized +- Circular references are not allowed and will throw an error + +### Callback restrictions + +Because of the way in which the callback is transmitted and executed, there are +certain limitations on what code may be run inside it. In particular, the +following Cypress commands will throw errors if used in the callback: + +- `cy.origin()` +- [`cy.intercept()`](/api/commands/intercept) +- [`cy.session()`](/api/commands/session) +- [`cy.server()`](/api/commands/server) +- [`cy.route()`](/api/commands/route) +- [`Cypress.Server.defaults()`](/api/cypress-api/cypress-server) +- [`Cypress.Cookies.preserveOnce()`](/api/cypress-api/cookies) + +TODO require + +TODO maybe repeat the part about evaluation + +## Command log + +TODO + +## See also + +- [Custom Commands](/api/cypress-api/custom-commands) +- [`cy.session()`](/api/commands/session) +- [`cy.visit()`](/api/commands/visit) From 59a14271b93bf3072b50ae57fb00f64bb8e9fff4 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Wed, 20 Apr 2022 14:55:23 -0500 Subject: [PATCH 02/34] Initial blank changelog --- content/_changelogs/9.6.0.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 content/_changelogs/9.6.0.md diff --git a/content/_changelogs/9.6.0.md b/content/_changelogs/9.6.0.md new file mode 100644 index 0000000000..c90404ca5d --- /dev/null +++ b/content/_changelogs/9.6.0.md @@ -0,0 +1,9 @@ +## 9.6.0 + +_Released 4/25/2022_ + +**Features:** + +**Bugfixes:** + +**Dependency Updates:** From b341c051ed7c158f6a8bd6e0fdbb6bf70b9e09e7 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 21 Apr 2022 16:34:18 -0500 Subject: [PATCH 03/34] add changelog entries --- content/_changelogs/9.6.0.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/content/_changelogs/9.6.0.md b/content/_changelogs/9.6.0.md index c90404ca5d..657abf86e6 100644 --- a/content/_changelogs/9.6.0.md +++ b/content/_changelogs/9.6.0.md @@ -4,6 +4,35 @@ _Released 4/25/2022_ **Features:** +- TO DO: Add cy.origin info. + **Bugfixes:** +- Fixed an issue with Firefox 98+ where the Enter keystroke was not being sent + to an input element when using `.type('{enter})`. Fixed + [#21033](https://github.com/cypress-io/cypress/issues/21033). +- When Cypress is interrupted by the user while tests are running, Cypress was + incorrectly reporting an error message that indicate a plugin crashed. Updates + were make to verify if the interruption was signaled by the user or if it + indeed came from an error in a plugin before showing error message. Fixed + [#21010](https://github.com/cypress-io/cypress/issues/21010). +- Improved the error message observed on Windows platforms when unzipping the + Cypress binary and the max file length is exceeded. The error now include a + link to the mitigation steps a user can take to enable long paths on the + system which should allow for a successful install. Fixed in + [#21047](https://github.com/cypress-io/cypress/pull/21047). +- Updated the `Cypress.Commands.add()` TypeScript types to better reflect the + attributes of the `.add()` utility and the JQuery element, a possible previous + subject type. [#20376](https://github.com/cypress-io/cypress/issues/20376). + **Dependency Updates:** + +- Upgraded `electron` dependency from `15.3.5` to `15.5.1` to consume fixes + related to + [improve performance](https://github.com/electron/electron/pull/33406) on + macOS Big Sur and later. Addressed + [#21068](https://github.com/cypress-io/cypress/issues/21068). + +\*\* IN REVIEW: + +- https://github.com/cypress-io/cypress/pull/20704 From 334b2483d6dbd737cc531731f2975df07eedce7a Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 21 Apr 2022 17:19:04 -0500 Subject: [PATCH 04/34] add multi-origin info --- content/_changelogs/9.6.0.md | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/content/_changelogs/9.6.0.md b/content/_changelogs/9.6.0.md index 657abf86e6..e354d039e0 100644 --- a/content/_changelogs/9.6.0.md +++ b/content/_changelogs/9.6.0.md @@ -2,9 +2,38 @@ _Released 4/25/2022_ +**Experiments:** + +- The Sessions beta feature has been enhanced by the new beta feature which + enables Multi-Origin support. To reflect the intent of using these experiments + together, the `experimentalSessionSupport` configuration option has been + replaced by the `experimentalSessionAndOrigin` configuration option. The + behaviors and usage of the `cy.session()` command has not been changed. Please + update your configuration to use the new `experimentalSessionAndOrigin` option + to continue to opt-in to this beta feature. + **Features:** -- TO DO: Add cy.origin info. +- We are excited to announce a new beta feature to enable Multi-Origin support. + This feature allows a user to visit multiple domains of different origins + within a single test. This enhances the current test experience which limits a + user to only run assertions against a single origin in a test due to standard + web security features of the browser. This experiment was designed to work + side-by-wide with the Sessions beta feature. For more details, see [our blog + post] (TO DO). Addressed + [#17336](https://github.com/cypress-io/cypress/issues/17336) and + [#944](https://github.com/cypress-io/cypress/issues/944) -- TO DO. + - Added a new configuration option called `experimentalSessionAndOrigin`. This + options enabled both Session support and Multi-Origin support. + - Added a new Cypress command, called `cy.origin()`, which enables running + commands within multiple origins from a single test. + - When enabled, cross-origin requests will no longer immediately fail, but + instead use the `pageLoadTimeout` configuration as the timeout value. This + allows the request to be appropriately handled by the `cy.origin()` command + and if it is not handled, it's assumed it was an unexpected request that + should fail. + - When enabled, tests will no longer wait for the page to load before starting + the next tests. **Bugfixes:** @@ -33,6 +62,6 @@ _Released 4/25/2022_ macOS Big Sur and later. Addressed [#21068](https://github.com/cypress-io/cypress/issues/21068). -\*\* IN REVIEW: +** IN REVIEW** - https://github.com/cypress-io/cypress/pull/20704 From 884bea8b053cf71d141014d52142ff38087c162c Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 21 Apr 2022 17:19:53 -0500 Subject: [PATCH 05/34] remove notes --- content/_changelogs/9.6.0.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/content/_changelogs/9.6.0.md b/content/_changelogs/9.6.0.md index e354d039e0..4f8a2f2525 100644 --- a/content/_changelogs/9.6.0.md +++ b/content/_changelogs/9.6.0.md @@ -61,7 +61,3 @@ _Released 4/25/2022_ [improve performance](https://github.com/electron/electron/pull/33406) on macOS Big Sur and later. Addressed [#21068](https://github.com/cypress-io/cypress/issues/21068). - -** IN REVIEW** - -- https://github.com/cypress-io/cypress/pull/20704 From 65fb1b625aa21807c2a3699a5382945a0cf8fd99 Mon Sep 17 00:00:00 2001 From: DEBRIS APRON Date: Thu, 21 Apr 2022 20:03:09 -0700 Subject: [PATCH 06/34] Address feedback --- content/api/commands/origin.md | 183 ++++++++++++++++++--------------- 1 file changed, 99 insertions(+), 84 deletions(-) diff --git a/content/api/commands/origin.md b/content/api/commands/origin.md index 9ecb54aae4..1735e1a370 100755 --- a/content/api/commands/origin.md +++ b/content/api/commands/origin.md @@ -15,12 +15,9 @@ limitation determined by standard web security features of the browser. The Experimental -TODO Maybe rework this & move to a partial - The `origin` API is currently experimental, and can be enabled by setting the [`experimentalSessionAndOrigin`](/guides/references/experiments) flag -to `true` in the Cypress config or by -using [`Cypress.config()`](/api/cypress-api/config) at the top of a spec file. +to `true` in the Cypress config. Enabling this flag does the following: @@ -56,7 +53,7 @@ cy.origin(url, options, callbackFn) ```js const hits = getHits() // Defined elsewhere -// Run commands in a secondary origin, passing in any serializable values we need +// Run commands in secondary origin, passing in serializable values cy.origin('https://www.acme.com', { args: { hits } }, ({ hits }) => { // Inside callback baseUrl is https://www.acme.com cy.visit('/history/founder') @@ -65,8 +62,8 @@ cy.origin('https://www.acme.com', { args: { hits } }, ({ hits }) => { // Passed in values are accessed via callback args cy.get('#hitcounter').contains(hits) }) -// Even though we are outside the secondary origin block, we are still on acme.com -// so return to baseUrl +// Even though we're outside the secondary origin block, +// we're still on acme.com so return to baseUrl cy.visit('/') // Continue running commands on primary origin cy.get('h1').contains('My cool site under test') @@ -75,13 +72,13 @@ cy.get('h1').contains('My cool site under test') ** Incorrect Usage** ```js -const hits = 9000 -// This should be inside the callback, Cypress needs to be injected before visit +const hits = getHits() +// cy.visit() should be inside cy.origin() callback cy.visit('https://www.acme.com/history/founder') cy.origin('https://www.acme.com', () => { - // Won't work because Cypress is not present on secondary origin + // Fails because origin was visited before cy.origin() block cy.get('h1').contains('About our Founder, Marvin Acme') - // Won't work because hits is not passed in via args + // Fails because hits is not passed in via args cy.get('#hitcounter').contains(hits) }) // Won't work because still on acme.com @@ -94,17 +91,19 @@ cy.get('h1').contains('My cool site under test') A URL specifying the secondary origin in which the callback is to be executed. This should at the very least contain a hostname, and may also include the -protocol, port number & path. This argument will be used in two ways. +protocol, port number & path. + +This argument will be used in two ways: -Firstly, it uniquely identifies a secondary origin in which the commands in the -callback will be executed. The test-runner will inject the Cypress runtime into -this origin, and then send it code to evaluate in that origin, without violating -the browser's same-origin policy. +1. It uniquely identifies a secondary origin in which the commands in the + callback will be executed. Cypress will inject itself into this origin, and + then send it code to evaluate in that origin, without violating the browser's + same-origin policy. -Secondly, it temporarily overrides the `baseUrl` configured in your -[global configuration](/guides/references/configuration#Global) whilst inside -the callback. So `cy.visit()` will navigate relative to this URL, not the -configured `baseUrl`. +2. It overrides the `baseUrl` configured in your + [global configuration](/guides/references/configuration#Global) while inside + the callback. So `cy.visit()` will navigate relative to this URL, not the + configured `baseUrl`. ** options** **_(Object)_** @@ -118,7 +117,8 @@ Pass in an options object to control the behavior of `cy.origin()`. The `args` object is the **only** mechanism via which data may be injected into the callback, the callback is **not** a closure and does not retain access to -the JavaScript context in which it was declared. +the JavaScript context in which it was declared. Values passed into `args` +**must** be serializable. @@ -134,31 +134,32 @@ There are a number of limitations placed on commands run inside the callback, please see [Callback restrictions](#Callback-restrictions) section below for a full list. -The function's return value is ignored. ??? - ### Yields [](/guides/core-concepts/introduction-to-cypress#Subject-Management) -- `cy.origin()` yields `null`. -- `cy.origin()` cannot be chained further. +- `cy.origin()` yields the value yielded by the last Cypress command in the + callback function. +- If the callback contains no Cypress commands, `cy.origin()` yields the return + value of the function. ## Examples ### Using dynamic data in a secondary origin Callbacks are executed inside an entirely separate instance of Cypress, so -arguments must be transmitted to the other instance by means of serialization & -deserialization. The interface for this mechanism is the `args` option. +arguments must be transmitted to the other instance by means of +[the structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). +The interface for this mechanism is the `args` option. ```js -const sentArgs = { password: 'P@55w0rd!!!1!?!111one' } +const sentArgs = { username: 'username', password: 'P@55w0rd!' } cy.origin( 'supersecurelogons.com', // Send the args here... { args: sentArgs }, // ...and receive them at the other end here! - (receivedArgs) => { - const { password } = receivedArgs + ({ username, password }) => { cy.visit('/login') + cy.get('input#username').type(username) cy.get('input#password').type(password) cy.contains('button', 'Login').click() } @@ -177,20 +178,14 @@ cross-domain error will be thrown. ```js // Do things in primary domain... -cy.origin('acme.com', () => { +cy.origin('https://www.acme.com', () => { // Visit https://www.acme.com/history/founder cy.visit('/history/founder') cy.get('h1').contains('About our Founder, Marvin Acme') }) ``` -```js -// TODO Example with onBeforeLoad... -``` - -TODO baseUrl - -TODO default protocol +TODO baseUrl / default protocol ### Navigating to secondary origin with UI @@ -199,10 +194,10 @@ primary origin, it is essential to trigger the navigation _before_ entering the origin callback, otherwise a cross-domain error will be thrown. ```js -// Click button in primary origin that navigates to https://acme.com -cy.contains('button', 'Go to Acme.com').click() +// Button in primary origin goes to https://www.acme.com +cy.contains('button', 'Go').click() -cy.origin('acme.com', () => { +cy.origin('www.acme.com', () => { // No cy.visit is needed as the button brought us here cy.get('h1').contains('ACME CORP') }) @@ -230,29 +225,11 @@ cy.origin('baz.com', () => { }) ``` -TODO Stabilization? - -### Waiting to return to primary origin + -Sometimes, a secondary origin returns to the primary origin as a result of user -action (for example, by clicking a "Login" button on a syndicated login -provider). In this situation, your test should wait for the navigation to -complete before making further assertions, otherwise a cross-domain error will -be thrown. - -```js -cy.visit('/home') -// This will take us to the secondary origin -cy.contains('button', 'Go to someothersite').click() +A future version of Cypress will allow the use of nested `cy.origin()` calls. -cy.origin('someothersite.com', () => { - // Click button that takes us back to primary origin - cy.contains('button', 'Go back').click() -) -// Wait until confirmation we are back at the primary origin before continuing -cy.url().should('contain', '/home') -// Do more things in primary domain... -``` + ### SSO login custom command @@ -267,37 +244,39 @@ do this with `cy.origin()`. Cypress.Commands.add('login', (username, password) => { // Remember to pass in dependencies via `args` const args = { username, password } - cy.origin('auth-provider.com', { args }, ({ username, password }) => { + cy.origin('my-auth.com', { args }, ({ username, password }) => { // Go to https://auth-provider.com/login cy.visit('/login') cy.contains('Username').find('input').type(username) cy.contains('Password').find('input').type(password) cy.get('button').contains('Login').click() }) - // Wait until confirmation we are back at the primary origin before continuing + // Confirm we're back at the primary origin before continuing cy.url().should('contain', '/home') }) ``` -However, having to go through an entire login flow before every test is not very -performant. Up until now you could get around this by putting login code in the -first test of your file, then performing subsequent tests reusing the same -session. However, once the `experimentalSessionAndOrigin` flag is activated this -is no longer possible, as all session state is now cleared between tests. So to -avoid this overhead we have added the [`cy.session()`](/api/commands/session) -command, which allows you to easily cache session information and reuse it -across not just tests, but test files too. So now let's enhance our custom login -command with `cy.session()` for a complete syndicated login flow with session -caching and validation. No mocking, no workarounds, no third-party plugins! +Having to go through an entire login flow before every test is not very +performant. Up until now you could get around this problem by putting login code +in the first test of your file, then performing subsequent tests reusing the +same session. + +However, once the `experimentalSessionAndOrigin` flag is activated this is no +longer possible, as all session state is now cleared between tests. So to avoid +this overhead we have added the [`cy.session()`](/api/commands/session) command, +which allows you to easily cache session information and reuse it across tests. +So now let's enhance our custom login command with `cy.session()` for a complete +syndicated login flow with session caching and validation. No mocking, no +workarounds, no third-party plugins! ```js Cypress.Commands.add('login', (username, password) => { const args = { username, password } cy.session( - // The username & password combination can be used as the cache key too + // Username & password can be used as the cache key too args, () => { - cy.origin('auth-provider.com', { args }, ({ username, password }) => { + cy.origin('my-auth.com', { args }, ({ username, password }) => { cy.visit('/login') cy.contains('Username').find('input').type(username) cy.contains('Password').find('input').type(password) @@ -394,15 +373,17 @@ Always remember, ### Serialization -When entering a `cy.origin()` block, the test-runner injects the Cypress -runtime, with all your configurations settings, into the requested origin, and -sets up bidirectional communication with that instance. This coordination model -requires that any data sent from one instance to another be +When entering a `cy.origin()` block, Cypress injects itself at runtime, with all +your configuration settings, into the requested origin, and sets up +bidirectional communication with that instance. This coordination model requires +that any data sent from one instance to another be [serialized](https://developer.mozilla.org/en-US/docs/Glossary/Serialization) for transmission. It is very important to understand that variables **inside** the callback are not shared with the scope **outside** the callback. For example this will not work: +** Incorrect Usage** + ```js const foo = 1 cy.origin('somesite.com', () => { @@ -416,6 +397,8 @@ cy.origin('somesite.com', () => { Instead, the variable must be explicitly passed into the callback using the `args` option: +** Correct Usage** + ```js const foo = 1 cy.origin('somesite.com', { args: { foo } }, ({ foo }) => { @@ -452,13 +435,45 @@ following Cypress commands will throw errors if used in the callback: - [`Cypress.Server.defaults()`](/api/cypress-api/cypress-server) - [`Cypress.Cookies.preserveOnce()`](/api/cypress-api/cookies) -TODO require +It is also currently not possible to use +[`require()`](https://nodejs.org/en/knowledge/getting-started/what-is-require/) +or +[dynamic `import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports) +within the callback. This functionality will be provided in a future version of +Cypress, but for now a simple workaround to reuse code between `cy.origin()` +callbacks is to create a custom Cypress command within the secondary origin in a +`before` block: + +```js +before(() => { + cy.origin('somesite.com', () => { + Cypress.Commands.add('clickLink', (label) => { + cy.get('a').contains(label).click() + }) + }) +}) + +it('clicks the secondary origin link', () => { + cy.origin('somesite.com', () => { + cy.clickLink('Click Me') + }) +}) +``` + +### Other limitations -TODO maybe repeat the part about evaluation +There are other testing scenarios which are not currently covered by +`cy.origin()`: -## Command log +- It cannot run commands + [in a different browser window](/guides/references/trade-offs#Multiple-browsers-open-at-the-same-time) +- It cannot run commands + [in a different browser tab](/guides/references/trade-offs#Multiple-tabs) +- It cannot run commands + [inside an `