feat(webkit): fix multidomain driver tests in WebKit (#23442)

* Initial async changes

* Small fixes and test updates.

* updating tests

* Fixes for cookie login tests

* remove the onlys

* Most tests passing

* Fix driver tests?

* fix firefox test?

* fix unit tests

* fix tests??

* a better check

* fix integration tests

* minor cleanup

* Comment out tyler fix for 10.0 origin issue

* also fix integration tests

* remove fixmes

* Adding Retries for cookie actions. May break other error tests.

* Address (some) PR comments

* factor console logging out of run.ts

* fix print-run

* minimize diff

* chore(server): convert browsers/index to typescript

* fix tests

* update stubbed tests

* convert electron.js to .ts

* Suggestions from code review

* Clean up new type errors

* electron.connectToExisting can be sync

* more type errors for the type god

* Suggestions from code review

* refactor: move more of video capture into browser automations

* unit tests

* refactor: move videoCapture to browsers

* fix snapshots

* update to warn about cross origin command AUT in assertions

* Fix type errors

* fix multi-spec videos?

* webkit video recording works!

* webkit system tests

* skip system-tests that won't be fixed in this PR

~60 tests skipped out of ~99:
* ~6 skipped due to needing multidomain support
* ~8 skipped due to missing before:browser:launch support
* ~22 skipped due to broken stack traces

* fix single-tab mode

* cleanup/api renames

* fix more tests

* minimize diff, fix ff

* fix unit tests

* fix tests

* cleanup

* Move document.cookie patch to injection

* enable ci job

* fix up/add request events to webkit automation

* update undefined message

* doesn't need an underscore

* Adding iframe patching.

* forward errors prior to attaching

* Add error message when using visit to visit a cross origin site with the onLoad or onBeforeLoad options.

* Attempt to fix test errors.

* more fixes, but not all

* use the origin policy

* Fix types

* more fixes

* consider chromeWebSecurity when checking if you can communicate with the AUT

* firefox

* prevent hangs if before unload happens after on load.

* Fix some ToDos

* code cleanup

* remove quotes

* Code review changes

* more cr changes

* fix tests possibly

* Updating cy.origin websocket for webkit connection error

* for realz this time

* temp fix for before:unload/load issue

* roll back change

* Fix some flake

* temporarily comment out autWindow.Error patch

* updating cookies to match develop

* update circle.yml

* commenting out driver-integration-tests-webkit-experimentalSessionAndOrigin

* revert cookie test change

* revert cross origin change

* Fix clear cookie problem

* Try it again

* test cy.origin in webkit

* Skip origin tests when running in webkit

* missed one

* attempt to revert web_security changes

* enable sessions on webkit

* maybe this fixes system tests??

* Update web_security_spec.js

* Update web_security_spec.js

* file cleanup

* Unify socket creation logic

* Address PR Comments

Co-authored-by: mjhenkes <mjhenkes@gmail.com>
Co-authored-by: Matt Schile <mschile@cypress.io>
This commit is contained in:
Zach Bloomquist
2022-10-12 17:21:58 -04:00
committed by GitHub
parent b7213c9bf0
commit 688b7ea33e
53 changed files with 267 additions and 202 deletions

View File

@@ -47,6 +47,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters
when:
or:
- equal: [ develop, << pipeline.git.branch >> ]
- equal: [ 'webkit-multidomain', << pipeline.git.branch >> ]
- equal: [ 'issue-23843_electron_21_upgrade', << pipeline.git.branch >> ]
- matches:
pattern: "-release$"
@@ -1856,7 +1857,7 @@ jobs:
name: Build
command: yarn workspace @cypress/mount-utils build
- store-npm-logs
npm-xpath:
<<: *defaults
resource_class: small
@@ -2437,11 +2438,10 @@ linux-x64-workflow: &linux-x64-workflow
context: test-runner:cypress-record-key
requires:
- build
# TODO: Implement WebKit network automation to fix the majority of these tests before re-enabling
# - driver-integration-tests-webkit-experimentalSessionAndOrigin:
# context: test-runner:cypress-record-key
# requires:
# - build
- driver-integration-tests-webkit-experimentalSessionAndOrigin:
context: test-runner:cypress-record-key
requires:
- build
- run-frontend-shared-component-tests-chrome:
context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy]
percy: true

View File

@@ -22,7 +22,7 @@ import { getRunnerElement, empty } from './utils'
import { IframeModel } from './iframe-model'
import { AutIframe } from './aut-iframe'
import { EventManager } from './event-manager'
import { client } from '@packages/socket/lib/browser'
import { createWebsocket as createWebsocketIo } from '@packages/socket/lib/browser'
import { decodeBase64Unicode } from '@packages/frontend-shared/src/utils/base64'
import type { AutomationElementId } from '@packages/types/src'
import { useSnapshotStore } from './snapshot-store'
@@ -31,11 +31,7 @@ import { useStudioStore } from '../store/studio-store'
let _eventManager: EventManager | undefined
export function createWebsocket (config: Cypress.Config) {
const ws = client({
path: config.socketIoRoute,
// TODO(webkit): the websocket socket.io transport is busted in WebKit, need polling
transports: config.browser.family === 'webkit' ? ['polling'] : ['websocket'],
})
const ws = createWebsocketIo({ path: config.socketIoRoute, browserFamily: config.browser.family })
ws.on('connect', () => {
ws.emit('runner:connected')

View File

@@ -2,6 +2,25 @@ const { assertLogLength } = require('../../support/utils')
const { stripIndent } = require('common-tags')
const { Promise } = Cypress
describe('src/cy/commands/cookies - no stub', () => {
it('clears all cookies', () => {
cy.setCookie('foo', 'bar')
cy.getCookies().should('have.length', 1)
cy.clearCookies()
cy.getCookies().should('have.length', 0)
})
it('clears a single cookie', () => {
cy.setCookie('foo', 'bar')
cy.setCookie('key', 'val')
cy.getCookies().should('have.length', 2)
cy.clearCookie('foo')
cy.getCookies().should('have.length', 1).then((cookies) => {
expect(cookies[0].name).to.eq('key')
})
})
})
describe('src/cy/commands/cookies', () => {
beforeEach(() => {
// call through normally on everything

View File

@@ -24,6 +24,27 @@ const clearAllSavedSessions = () => {
})
}
// In webkit, the clear page and clear cookies, etc log messages may be reversed. This isn't an issue, but we just want to test we have both messages.
const validateClearLogs = (logs, sessionGroupId) => {
let clearPageLogIndex = 0
let clearCookiesIndex = 1
if (logs[1].get('name') === 'Clear page') {
clearPageLogIndex = 1
clearCookiesIndex = 0
}
expect(logs[clearPageLogIndex].get()).to.contain({
name: 'Clear page',
group: sessionGroupId,
})
expect(logs[clearCookiesIndex].get()).to.contain({
displayName: 'Clear cookies, localStorage and sessionStorage',
group: sessionGroupId,
})
}
describe('cy.session', { retries: 0 }, () => {
describe('args', () => {
it('accepts string as id', () => {
@@ -200,15 +221,7 @@ describe('cy.session', { retries: 0 }, () => {
},
})
expect(logs[1].get()).to.contain({
name: 'Clear page',
group: sessionGroupId,
})
expect(logs[2].get()).to.contain({
displayName: 'Clear cookies, localStorage and sessionStorage',
group: sessionGroupId,
})
validateClearLogs([logs[1], logs[2]], sessionGroupId)
const createNewSessionGroup = logs[3].get()
@@ -282,15 +295,7 @@ describe('cy.session', { retries: 0 }, () => {
},
})
expect(logs[1].get()).to.contain({
name: 'Clear page',
group: sessionGroupId,
})
expect(logs[2].get()).to.contain({
displayName: 'Clear cookies, localStorage and sessionStorage',
group: sessionGroupId,
})
validateClearLogs([logs[1], logs[2]], sessionGroupId)
const createNewSessionGroup = logs[3].get()
@@ -344,15 +349,7 @@ describe('cy.session', { retries: 0 }, () => {
},
})
expect(logs[1].get()).to.contain({
name: 'Clear page',
group: sessionGroupId,
})
expect(logs[2].get()).to.contain({
displayName: 'Clear cookies, localStorage and sessionStorage',
group: sessionGroupId,
})
validateClearLogs([logs[1], logs[2]], sessionGroupId)
const createNewSessionGroup = logs[3].get()
@@ -437,15 +434,7 @@ describe('cy.session', { retries: 0 }, () => {
},
})
expect(logs[1].get()).to.contain({
name: 'Clear page',
group: sessionGroupId,
})
expect(logs[2].get()).to.contain({
displayName: 'Clear cookies, localStorage and sessionStorage',
group: sessionGroupId,
})
validateClearLogs([logs[1], logs[2]], sessionGroupId)
const restoreSavedSessionGroup = logs[3].get()
@@ -500,15 +489,7 @@ describe('cy.session', { retries: 0 }, () => {
},
})
expect(logs[1].get()).to.contain({
name: 'Clear page',
group: sessionGroupId,
})
expect(logs[2].get()).to.contain({
displayName: 'Clear cookies, localStorage and sessionStorage',
group: sessionGroupId,
})
validateClearLogs([logs[1], logs[2]], sessionGroupId)
const restoreSavedSessionGroup = logs[3].get()
@@ -582,15 +563,7 @@ describe('cy.session', { retries: 0 }, () => {
},
})
expect(logs[1].get()).to.contain({
name: 'Clear page',
group: sessionGroupId,
})
expect(logs[2].get()).to.contain({
displayName: 'Clear cookies, localStorage and sessionStorage',
group: sessionGroupId,
})
validateClearLogs([logs[1], logs[2]], sessionGroupId)
const restoreSavedSessionGroup = logs[3].get()
@@ -618,15 +591,7 @@ describe('cy.session', { retries: 0 }, () => {
expect(logs[6].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.')
expect(logs[7].get()).to.contain({
name: 'Clear page',
group: sessionGroupId,
})
expect(logs[8].get()).to.contain({
displayName: 'Clear cookies, localStorage and sessionStorage',
group: sessionGroupId,
})
validateClearLogs([logs[7], logs[8]], sessionGroupId)
const createNewSessionGroup = logs[9].get()
@@ -692,15 +657,7 @@ describe('cy.session', { retries: 0 }, () => {
},
})
expect(logs[1].get()).to.contain({
name: 'Clear page',
group: sessionGroupId,
})
expect(logs[2].get()).to.contain({
displayName: 'Clear cookies, localStorage and sessionStorage',
group: sessionGroupId,
})
validateClearLogs([logs[1], logs[2]], sessionGroupId)
const restoreSavedSessionGroup = logs[3].get()
@@ -728,15 +685,7 @@ describe('cy.session', { retries: 0 }, () => {
expect(logs[6].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.')
expect(logs[7].get()).to.contain({
name: 'Clear page',
group: sessionGroupId,
})
expect(logs[8].get()).to.contain({
displayName: 'Clear cookies, localStorage and sessionStorage',
group: sessionGroupId,
})
validateClearLogs([logs[7], logs[8]], sessionGroupId)
const createNewSessionGroup = logs[9].get()
@@ -1046,7 +995,10 @@ describe('cy.session', { retries: 0 }, () => {
cy.once('fail', (err) => {
expect(err.message).contain('Expected to find element: `#does_not_exist`')
expect(err.message).contain(errorHookMessage)
expect(err.codeFrame).exist
// TODO: Webkit does not have correct stack traces on errors currently
if (Cypress.isBrowser('!webkit')) {
expect(err.codeFrame).exist
}
done()
})
@@ -1064,7 +1016,11 @@ describe('cy.session', { retries: 0 }, () => {
cy.once('fail', (err) => {
expect(err.message).contain('validate error')
expect(err.message).contain(errorHookMessage)
expect(err.codeFrame).exist
// TODO: Webkit does not have correct stack traces on errors currently
if (Cypress.isBrowser('!webkit')) {
expect(err.codeFrame).exist
}
done()
})
@@ -1081,7 +1037,10 @@ describe('cy.session', { retries: 0 }, () => {
cy.once('fail', (err) => {
expect(err.message).contain('validate error')
expect(err.message).contain(errorHookMessage)
expect(err.codeFrame).exist
// TODO: Webkit does not have correct stack traces on errors currently
if (Cypress.isBrowser('!webkit')) {
expect(err.codeFrame).exist
}
done()
})
@@ -1099,7 +1058,10 @@ describe('cy.session', { retries: 0 }, () => {
cy.once('fail', (err) => {
expect(err.message).to.contain('Your `cy.session` **validate** callback returned false.')
expect(err.message).contain(errorHookMessage)
expect(err.codeFrame).exist
// TODO: Webkit does not have correct stack traces on errors currently
if (Cypress.isBrowser('!webkit')) {
expect(err.codeFrame).exist
}
done()
})
@@ -1117,7 +1079,11 @@ describe('cy.session', { retries: 0 }, () => {
cy.once('fail', (err) => {
expect(err.message).to.contain('Your `cy.session` **validate** callback resolved false.')
expect(err.message).contain(errorHookMessage)
expect(err.codeFrame).exist
// TODO: Webkit does not have correct stack traces on errors currently
if (Cypress.isBrowser('!webkit')) {
expect(err.codeFrame).exist
}
done()
})

View File

@@ -1,4 +1,4 @@
describe('basic login', () => {
describe('basic login', { browser: '!webkit' }, () => {
// 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', () => {
@@ -148,7 +148,7 @@ describe('basic login', () => {
})
})
describe('Multi-step Auth', () => {
describe('Multi-step Auth', { browser: '!webkit' }, () => {
// 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')

View File

@@ -1,6 +1,6 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin actions', () => {
context('cy.origin actions', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
})

View File

@@ -1,6 +1,6 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin aliasing', () => {
context('cy.origin aliasing', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
})

View File

@@ -1,6 +1,6 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin assertions', () => {
context('cy.origin assertions', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="dom-link"]').click()

View File

@@ -1,6 +1,6 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin connectors', () => {
context('cy.origin connectors', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="dom-link"]').click()

View File

@@ -1,6 +1,6 @@
import { findCrossOriginLogs, assertLogLength } from '../../../../support/utils'
describe('cy.origin cookies', () => {
describe('cy.origin cookies', { browser: '!webkit' }, () => {
context('client side', () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')

View File

@@ -1,6 +1,6 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin files', () => {
context('cy.origin files', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()

View File

@@ -1,6 +1,6 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin local storage', () => {
context('cy.origin local storage', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()

View File

@@ -1,6 +1,6 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin location', () => {
context('cy.origin location', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()

View File

@@ -1,6 +1,6 @@
import { assertLogLength } from '../../../../support/utils'
context('cy.origin log', () => {
context('cy.origin log', { browser: '!webkit' }, () => {
let logs: any = []
let lastTestLogId = ''

View File

@@ -1,6 +1,6 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin misc', () => {
context('cy.origin misc', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="dom-link"]').click()

View File

@@ -1,7 +1,7 @@
const { stripIndent } = require('common-tags')
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin navigation', () => {
context('cy.origin navigation', { browser: '!webkit' }, () => {
it('.go()', () => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()

View File

@@ -1,6 +1,6 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin network requests', () => {
context('cy.origin network requests', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="request-link"]').click()

View File

@@ -1,6 +1,6 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin querying', () => {
context('cy.origin querying', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="dom-link"]').click()

View File

@@ -1,6 +1,6 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin shadow dom', () => {
context('cy.origin shadow dom', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="shadow-dom-link"]').click()

View File

@@ -1,4 +1,4 @@
context('cy.origin screenshot', () => {
context('cy.origin screenshot', { browser: '!webkit' }, () => {
const { devicePixelRatio } = window
context('set viewport', () => {

View File

@@ -1,6 +1,6 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin spies, stubs, and clock', () => {
context('cy.origin spies, stubs, and clock', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()

View File

@@ -1,6 +1,6 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin traversal', () => {
context('cy.origin traversal', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="dom-link"]').click()

View File

@@ -1,4 +1,4 @@
context('cy.origin unsupported commands', () => {
context('cy.origin unsupported commands', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()

View File

@@ -1,6 +1,6 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin viewport', () => {
context('cy.origin viewport', { browser: '!webkit' }, () => {
it('syncs the viewport from the primary to secondary', () => {
// change the viewport in the primary first
cy.viewport(320, 480)

View File

@@ -22,7 +22,7 @@ const abortRequests = () => {
reqQueue = []
}
context('cy.origin waiting', () => {
context('cy.origin waiting', { browser: '!webkit' }, () => {
before(() => {
cy.origin('http://www.foobar.com:3500', () => {
let reqQueue: XMLHttpRequest[] = []

View File

@@ -1,6 +1,6 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin window', () => {
context('cy.origin window', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="dom-link"]').click()

View File

@@ -1,5 +1,5 @@
['config', 'env'].forEach((fnName) => {
describe(`cy.origin- Cypress.${fnName}()`, () => {
describe(`cy.origin- Cypress.${fnName}()`, { browser: '!webkit' }, () => {
const USED_KEYS = {
foo: 'cy-origin-foo',
bar: 'cy-origin-bar',

View File

@@ -1,4 +1,4 @@
describe('Cookie Behavior with experimentalSessionAndOrigin=true', () => {
describe('Cookie Behavior with experimentalSessionAndOrigin=true', { browser: '!webkit' }, () => {
const makeRequest = (
win: Cypress.AUTWindow,
url: string,

View File

@@ -1,6 +1,6 @@
import dayjs from 'dayjs'
describe('cy.origin - cookie login', () => {
describe('cy.origin - cookie login', { browser: '!webkit' }, () => {
const { _ } = Cypress
// ensures unique username so there's no risk of false positives from
// test pollution
@@ -71,7 +71,7 @@ describe('cy.origin - cookie login', () => {
• displays "Welcome, <username>"
****************************************************************************/
describe('general behavior', () => {
describe('general behavior', { browser: '!webkit' }, () => {
let username
beforeEach(() => {

View File

@@ -1,6 +1,6 @@
const { assertLogLength } = require('../../../support/utils')
describe('cy.origin Cypress API', () => {
describe('cy.origin Cypress API', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()

View File

@@ -1,4 +1,4 @@
describe('cy.origin dependencies - jsx', () => {
describe('cy.origin dependencies - jsx', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()

View File

@@ -1,4 +1,4 @@
describe('cy.origin dependencies', () => {
describe('cy.origin dependencies', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()

View File

@@ -1,4 +1,4 @@
describe('cy.origin', () => {
describe('cy.origin', { browser: '!webkit' }, () => {
it('window:before:load event', () => {
cy.visit('/fixtures/primary-origin.html')
cy.on('window:before:load', (win: {testPrimaryOriginBeforeLoad: boolean}) => {

View File

@@ -4,7 +4,7 @@ import type { TemplateExecutor } from 'lodash'
// NOTE: in order to run these tests, the following config flags need to be set
// experimentalSessionAndOrigin=true
// experimentalModifyObstructiveThirdPartyCode=true
describe('Integrity Preservation', () => {
describe('Integrity Preservation', { browser: '!webkit' }, () => {
// Add common SRI hashes used when setting script/link integrity.
// These are the ones supported by SRI (see https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity#using_subresource_integrity)
// For our tests, we will use CryptoJS to calculate these hashes as they can regenerate the integrity without us having to do it manually every

View File

@@ -1,4 +1,4 @@
describe('cy.origin logging', () => {
describe('cy.origin logging', { browser: '!webkit' }, () => {
const { _ } = Cypress
it('groups callback commands on a passing test', () => {

View File

@@ -11,7 +11,7 @@ const reifyLogs = (logs) => {
})
}
describe('navigation events', () => {
describe('navigation events', { browser: '!webkit' }, () => {
let logs: any = []
beforeEach(() => {
@@ -185,7 +185,7 @@ describe('navigation events', () => {
})
// @ts-ignore / session support is needed for visiting about:blank between tests
describe('event timing', () => {
describe('event timing', { browser: '!webkit' }, () => {
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/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
@@ -206,7 +206,7 @@ describe('event timing', () => {
})
// @ts-ignore / session support is needed for visiting about:blank between tests
describe('delayed navigation', { defaultCommandTimeout: 2000 }, () => {
describe('delayed navigation', { browser: '!webkit' }, { defaultCommandTimeout: 2000 }, () => {
it('localhost -> localhost', () => {
cy.visit('/fixtures/auth/delayedNavigate.html')
cy.get('[data-cy="to-localhost"]').click()
@@ -245,7 +245,7 @@ describe('delayed navigation', { defaultCommandTimeout: 2000 }, () => {
})
// @ts-ignore / session support is needed for visiting about:blank between tests
describe('errors', () => {
describe('errors', { browser: '!webkit' }, () => {
it('never calls cy.origin', { defaultCommandTimeout: 50 }, (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include(`Timed out retrying after 50ms:`)

View File

@@ -1,4 +1,4 @@
describe('cy.origin', () => {
describe('cy.origin', { browser: '!webkit' }, () => {
it('passes viewportWidth/Height state to the secondary origin', () => {
const expectedViewport = [320, 480]
@@ -224,7 +224,9 @@ describe('cy.origin', () => {
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')
const undefinedMessage = Cypress.isBrowser('webkit') ? 'Can\'t find variable: variable' : 'variable is not defined'
expect(err.message).to.include(undefinedMessage)
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

View File

@@ -1,4 +1,4 @@
describe('src/cross-origin/patches', () => {
describe('src/cross-origin/patches', { browser: '!webkit' }, () => {
context('submit', () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')

View File

@@ -1,5 +1,5 @@
// @ts-ignore
describe('cy.origin - rerun', { }, () => {
describe('cy.origin - rerun', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()

View File

@@ -1,7 +1,7 @@
// import to bind shouldWithTimeout into global cy commands
import '../../../support/utils'
describe('cy.origin - snapshots', () => {
describe('cy.origin - snapshots', { browser: '!webkit' }, () => {
const findLog = (logMap: Map<string, any>, displayName: string, url: string) => {
return Array.from(logMap.values()).find((log: any) => {
const props = log.get()

View File

@@ -1,4 +1,4 @@
it('visits foobar.com and types foobar inside an input', () => {
it('visits foobar.com and types foobar inside an input', { browser: '!webkit' }, () => {
cy.visit('/fixtures/primary-origin.html')
cy.get('[data-cy="cross-origin-secondary-link"]').click()

View File

@@ -1,4 +1,4 @@
describe('cy.origin - uncaught errors', () => {
describe('cy.origin - uncaught errors', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="errors-link"]').click()

View File

@@ -1,4 +1,4 @@
describe('cy.origin', () => {
describe('cy.origin', { browser: '!webkit' }, () => {
describe('successes', () => {
beforeEach(() => {
// TODO: There seems to be a limit of 15 active spec bridges during a given test.

View File

@@ -1,6 +1,6 @@
import { assertLogLength } from '../../../support/utils'
describe('cy.origin yields', () => {
describe('cy.origin yields', { browser: '!webkit' }, () => {
let logs: any = []
beforeEach(() => {

View File

@@ -9,17 +9,7 @@ describe('WebKit-specific behavior', { browser: 'webkit' }, () => {
cy.origin('foo', () => {})
})
it('cy.session() is disabled', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.equal('`cy.session()` is not currently supported in experimental WebKit.')
expect(err.docsUrl).to.equal('https://on.cypress.io/webkit-experiment')
done()
})
cy.session('foo', () => {})
})
it('cy.session() is disabled', (done) => {
it('forceNetworkError intercept option is disabled', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include('`forceNetworkError` was passed, but it is not currently supported in experimental WebKit.')
expect(err.docsUrl).to.equal('https://on.cypress.io/intercept')

View File

@@ -1,10 +1,9 @@
import { client } from '@packages/socket/lib/browser'
import { createWebsocket } from '@packages/socket/lib/browser'
export const handleSocketEvents = (Cypress) => {
const webSocket = client({
path: Cypress.config('socketIoRoute'),
transports: ['websocket'],
}).connect()
const webSocket = createWebsocket({ path: Cypress.config('socketIoRoute'), browserFamily: Cypress.config('browser').family })
webSocket.connect()
const onBackendRequest = (...args) => {
webSocket.emit('backend:request', ...args)

View File

@@ -25,10 +25,6 @@ export default function (Commands, Cypress, cy) {
// @ts-ignore
function throwIfNoSessionSupport () {
if (Cypress.isBrowser('webkit')) {
$errUtils.throwErrByPath('webkit.session')
}
if (!Cypress.config('experimentalSessionAndOrigin')) {
$errUtils.throwErrByPath('sessions.experimentNotEnabled', {
args: {

View File

@@ -551,22 +551,6 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
let isRunnerAbleToCommunicateWithAUT: boolean
if (this.Cypress.isBrowser('webkit')) {
// WebKit's unhandledrejection event will sometimes not fire within the AUT
// due to a documented bug: https://bugs.webkit.org/show_bug.cgi?id=187822
// To ensure that the event will always fire (and always report these
// unhandled rejections to the user), we patch the AUT's Error constructor
// to enqueue a no-op microtask when executed, which ensures that the unhandledrejection
// event handler will be executed if this Error is uncaught.
const originalError = autWindow.Error
autWindow.Error = function __CyWebKitError (...args) {
autWindow.queueMicrotask(() => {})
return originalError.apply(this, args)
}
}
try {
// Test to see if we can communicate with the AUT.
autWindow.location.href
@@ -581,6 +565,22 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
// If the runner can communicate, we should setup all events, otherwise just setup the window and fire the load event.
if (isRunnerAbleToCommunicateWithAUT) {
if (this.Cypress.isBrowser('webkit')) {
// WebKit's unhandledrejection event will sometimes not fire within the AUT
// due to a documented bug: https://bugs.webkit.org/show_bug.cgi?id=187822
// To ensure that the event will always fire (and always report these
// unhandled rejections to the user), we patch the AUT's Error constructor
// to enqueue a no-op microtask when executed, which ensures that the unhandledrejection
// event handler will be executed if this Error is uncaught.
const originalError = autWindow.Error
autWindow.Error = function __CyWebKitError (...args) {
autWindow.queueMicrotask(() => {})
return originalError.apply(this, args)
}
}
setWindowDocumentProps(autWindow, this.state)
// we may need to update the url now

View File

@@ -2352,7 +2352,6 @@ export default {
webkit: {
docsUrl: 'https://on.cypress.io/webkit-experiment',
origin: '`cy.origin()` is not currently supported in experimental WebKit.',
session: '`cy.session()` is not currently supported in experimental WebKit.',
},
window: {

View File

@@ -30,22 +30,26 @@ function convertSameSiteExtensionToCypress (str: CyCookie['sameSite']): 'None' |
return str ? extensionMap[str] : undefined
}
const normalizeGetCookieProps = (cookie: any): CyCookie => {
if (cookie.expires === -1) {
delete cookie.expires
const normalizeGetCookieProps = ({ name, value, domain, path, secure, httpOnly, sameSite, expires }: playwright.Cookie): CyCookie => {
const cyCookie: CyCookie = {
name,
value,
domain,
path,
secure,
httpOnly,
hostOnly: false,
// Use expirationDate instead of expires
...expires !== -1 ? { expirationDate: expires } : {},
}
// Use expirationDate instead of expires 🤷‍♀️
cookie.expirationDate = cookie.expires
delete cookie.expires
if (cookie.sameSite === 'None') {
cookie.sameSite = 'no_restriction'
} else if (cookie.sameSite) {
cookie.sameSite = cookie.sameSite.toLowerCase()
if (sameSite === 'None') {
cyCookie.sameSite = 'no_restriction'
} else if (sameSite) {
cyCookie.sameSite = sameSite.toLowerCase() as CyCookie['sameSite']
}
return cookie as CyCookie
return cyCookie
}
const normalizeSetCookieProps = (cookie: CyCookie): playwright.Cookie => {
@@ -88,17 +92,33 @@ let requestIdCounter = 1
const requestIdMap = new WeakMap<playwright.Request, string>()
let downloadIdCounter = 1
type WebKitAutomationOpts = {
automation: Automation
browser: playwright.Browser
shouldMarkAutIframeRequests: boolean
initialUrl: string
downloadsFolder: string
videoApi?: RunModeVideoApi
}
export class WebKitAutomation {
automation: Automation
private browser: playwright.Browser
private context!: playwright.BrowserContext
private page!: playwright.Page
private shouldMarkAutIframeRequests: boolean
private constructor (public automation: Automation, private browser: playwright.Browser) {}
private constructor (opts: WebKitAutomationOpts) {
this.automation = opts.automation
this.browser = opts.browser
this.shouldMarkAutIframeRequests = opts.shouldMarkAutIframeRequests
}
// static initializer to avoid "not definitively declared"
static async create (automation: Automation, browser: playwright.Browser, initialUrl: string, downloadsFolder: string, videoApi?: RunModeVideoApi) {
const wkAutomation = new WebKitAutomation(automation, browser)
static async create (opts: WebKitAutomationOpts) {
const wkAutomation = new WebKitAutomation(opts)
await wkAutomation.reset({ downloadsFolder, newUrl: initialUrl, videoApi })
await wkAutomation.reset({ downloadsFolder: opts.downloadsFolder, newUrl: opts.initialUrl, videoApi: opts.videoApi })
return wkAutomation
}
@@ -127,6 +147,9 @@ export class WebKitAutomation {
let promises: Promise<any>[] = []
// TODO: remove with experimentalSessionAndOrigin
if (this.shouldMarkAutIframeRequests) promises.push(this.markAutIframeRequests())
if (oldPwPage) promises.push(oldPwPage.context().close())
if (options.newUrl) promises.push(this.page.goto(options.newUrl))
@@ -168,6 +191,28 @@ export class WebKitAutomation {
})
}
private async markAutIframeRequests () {
function isAutIframeRequest (request: playwright.Request) {
// is an iframe
return (request.resourceType() === 'document')
// is a top-level iframe (only 1 parent in chain)
&& request.frame().parentFrame() && !request.frame().parentFrame()?.parentFrame()
// is not the runner itself
&& !request.url().includes('__cypress')
}
await this.context.route('**', (route, request) => {
if (!isAutIframeRequest(request)) return route.continue()
return route.continue({
headers: {
...request.headers(),
'X-Cypress-Is-AUT-Frame': 'true',
},
})
})
}
private handleDownloadEvents (downloadsFolder: string) {
this.page.on('download', async (download) => {
const id = downloadIdCounter++
@@ -234,7 +279,7 @@ export class WebKitAutomation {
})
}
private async getCookies () {
private async getCookies (): Promise<CyCookie[]> {
const cookies = await this.context.cookies()
return cookies.map(normalizeGetCookieProps)
@@ -256,8 +301,12 @@ export class WebKitAutomation {
return normalizeGetCookieProps(cookie)
}
private async clearCookie (filter: CookieFilter) {
/**
* Clears one specific cookie
* @param filter the cookie to be cleared
* @returns the cleared cookie
*/
private async clearCookie (filter: CookieFilter): Promise<CookieFilter> {
const allCookies = await this.context.cookies()
const persistCookies = allCookies.filter((cookie) => {
return !_cookieMatches(cookie, filter)
@@ -265,6 +314,20 @@ export class WebKitAutomation {
await this.context.clearCookies()
if (persistCookies.length) await this.context.addCookies(persistCookies)
return filter
}
/**
* Clear all cookies
* @returns cookies cleared
*/
private async clearCookies (): Promise<CyCookie[]> {
const allCookies = await this.getCookies()
await this.context.clearCookies()
return allCookies
}
private async takeScreenshot (data) {
@@ -293,7 +356,7 @@ export class WebKitAutomation {
case 'set:cookies':
return await this.context.addCookies(data.map(normalizeSetCookieProps))
case 'clear:cookies':
return await this.context.clearCookies()
return await this.clearCookies()
case 'clear:cookie':
return await this.clearCookie(data)
case 'take:screenshot':

View File

@@ -92,7 +92,15 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
const pwBrowser = await pw.webkit.connect(pwServer.wsEndpoint())
wkAutomation = await WebKitAutomation.create(automation, pwBrowser, url, options.downloadsFolder, options.videoApi)
wkAutomation = await WebKitAutomation.create({
automation,
browser: pwBrowser,
initialUrl: url,
downloadsFolder: options.downloadsFolder,
shouldMarkAutIframeRequests: !!options.experimentalSessionAndOrigin,
videoApi: options.videoApi,
})
automation.use(wkAutomation)
class WkInstance extends EventEmitter implements BrowserInstance {

View File

@@ -5,3 +5,12 @@ export type { Socket } from 'socket.io-client'
export {
io as client,
}
export function createWebsocket ({ path, browserFamily }: { path: string, browserFamily: string}) {
return io({
path,
// TODO(webkit): the websocket socket.io transport is busted in WebKit, need polling
// https://github.com/cypress-io/cypress/issues/23807
transports: browserFamily === 'webkit' ? ['polling'] : ['websocket'],
})
}

View File

@@ -21,6 +21,24 @@ describe('Socket', function () {
expect(browserLib.client).to.eq(client)
})
it('exports createWebSocket from lib/browser', function () {
expect(browserLib.createWebsocket).to.be.defined
})
it('creates a websocket for non webkit browsers', function () {
const socket = browserLib.createWebsocket({ path: '/path', browserFamily: 'chromium' })
expect(socket.io.opts.path).to.eq('/path')
expect(socket.io.opts.transports[0]).to.eq('websocket')
})
it('creates a websocket for non webkit browsers', function () {
const socket = browserLib.createWebsocket({ path: '/path', browserFamily: 'webkit' })
expect(socket.io.opts.path).to.eq('/path')
expect(socket.io.opts.transports[0]).to.eq('polling')
})
context('.getPathToClientSource', function () {
it('returns path to socket.io.js', function () {
const clientPath = path.join(resolvePkg('socket.io-client'), 'dist', 'socket.io.js')