mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-06 15:00:50 -05:00
chore: internal refactor of privileged commands (#27060)
This commit is contained in:
@@ -74,8 +74,7 @@ windowsWorkflowFilters: &windows-workflow-filters
|
||||
- equal: [ develop, << pipeline.git.branch >> ]
|
||||
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
|
||||
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
|
||||
- equal: [ 'matth/feat/chrome-headless', << pipeline.git.branch >> ]
|
||||
- equal: [ 'lmiller/fix-windows-regressions', << pipeline.git.branch >> ]
|
||||
- equal: [ 'privileged-commands-refactor', << pipeline.git.branch >> ]
|
||||
- matches:
|
||||
pattern: /^release\/\d+\.\d+\.\d+$/
|
||||
value: << pipeline.git.branch >>
|
||||
|
||||
Vendored
+2
-2
@@ -3,8 +3,8 @@ type EventEmitter2 = import("eventemitter2").EventEmitter2
|
||||
|
||||
interface CyEventEmitter extends Omit<EventEmitter2, 'waitFor'> {
|
||||
proxyTo: (cy: Cypress.cy) => null
|
||||
emitMap: (eventName: string, args: any[]) => Array<(...args: any[]) => any>
|
||||
emitThen: (eventName: string, args: any[]) => Bluebird.BluebirdStatic
|
||||
emitMap: (eventName: string, ...args: any[]) => Array<(...args: any[]) => any>
|
||||
emitThen: (eventName: string, ...args: any[]) => Bluebird.BluebirdStatic
|
||||
}
|
||||
|
||||
// Copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/events.d.ts
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
cypress/videos/*
|
||||
cypress/screenshots/*
|
||||
cypress/downloads/*
|
||||
|
||||
components.d.ts
|
||||
components.d.ts
|
||||
|
||||
@@ -343,18 +343,6 @@ export const generateCtErrorTests = (server: 'Webpack' | 'Vite', configFile: str
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.readFile', () => {
|
||||
const verify = loadErrorSpec({
|
||||
filePath: 'errors/readfile.cy.js',
|
||||
failCount: 1,
|
||||
}, configFile)
|
||||
|
||||
verify('existence failure', {
|
||||
column: [8, 9],
|
||||
message: 'failed because the file does not exist',
|
||||
})
|
||||
})
|
||||
|
||||
it('validation errors', () => {
|
||||
const verify = loadErrorSpec({
|
||||
filePath: 'errors/validation.cy.js',
|
||||
|
||||
@@ -321,18 +321,6 @@ describe('errors ui', {
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.readFile', () => {
|
||||
const verify = loadErrorSpec({
|
||||
filePath: 'errors/readfile.cy.js',
|
||||
failCount: 1,
|
||||
})
|
||||
|
||||
verify('existence failure', {
|
||||
column: 8,
|
||||
message: 'failed because the file does not exist',
|
||||
})
|
||||
})
|
||||
|
||||
it('validation errors', () => {
|
||||
const verify = loadErrorSpec({
|
||||
filePath: 'errors/validation.cy.js',
|
||||
|
||||
@@ -750,7 +750,13 @@ export class EventManager {
|
||||
* Return it's response.
|
||||
*/
|
||||
Cypress.primaryOriginCommunicator.on('backend:request', async ({ args }, { source, responseEvent }) => {
|
||||
const response = await Cypress.backend(...args)
|
||||
let response
|
||||
|
||||
try {
|
||||
response = await Cypress.backend(...args)
|
||||
} catch (error) {
|
||||
response = { error }
|
||||
}
|
||||
|
||||
Cypress.primaryOriginCommunicator.toSource(source, responseEvent, response)
|
||||
})
|
||||
|
||||
@@ -152,13 +152,25 @@ function setupRunner () {
|
||||
createIframeModel()
|
||||
}
|
||||
|
||||
interface GetSpecUrlOptions {
|
||||
browserFamily?: string
|
||||
namespace: string
|
||||
specSrc: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for the spec. This is the URL of the AUT IFrame.
|
||||
* CT uses absolute URLs, and serves from the dev server.
|
||||
* E2E uses relative, serving from our internal server's spec controller.
|
||||
*/
|
||||
function getSpecUrl (namespace: string, specSrc: string) {
|
||||
return `/${namespace}/iframes/${specSrc}`
|
||||
function getSpecUrl ({ browserFamily, namespace, specSrc }: GetSpecUrlOptions) {
|
||||
let url = `/${namespace}/iframes/${specSrc}`
|
||||
|
||||
if (browserFamily) {
|
||||
url += `?browserFamily=${browserFamily}`
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,13 +214,15 @@ export function addCrossOriginIframe (location) {
|
||||
return
|
||||
}
|
||||
|
||||
const config = getRunnerConfigFromWindow()
|
||||
|
||||
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',
|
||||
src: `${location.origin}/${getRunnerConfigFromWindow().namespace}/spec-bridge-iframes`,
|
||||
src: `${location.origin}/${config.namespace}/spec-bridge-iframes?browserFamily=${config.browser.family}`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -234,7 +248,10 @@ function runSpecCT (config, spec: SpecFile) {
|
||||
const autIframe = getAutIframeModel()
|
||||
const $autIframe: JQuery<HTMLIFrameElement> = autIframe.create().appendTo($container)
|
||||
|
||||
const specSrc = getSpecUrl(config.namespace, spec.absolute)
|
||||
const specSrc = getSpecUrl({
|
||||
namespace: config.namespace,
|
||||
specSrc: spec.absolute,
|
||||
})
|
||||
|
||||
autIframe._showInitialBlankPage()
|
||||
$autIframe.prop('src', specSrc)
|
||||
@@ -297,7 +314,11 @@ function runSpecE2E (config, spec: SpecFile) {
|
||||
autIframe.visitBlankPage()
|
||||
|
||||
// create Spec IFrame
|
||||
const specSrc = getSpecUrl(config.namespace, encodeURIComponent(spec.relative))
|
||||
const specSrc = getSpecUrl({
|
||||
browserFamily: config.browser.family,
|
||||
namespace: config.namespace,
|
||||
specSrc: encodeURIComponent(spec.relative),
|
||||
})
|
||||
|
||||
// FIXME: BILL Determine where to call client with to force browser repaint
|
||||
/**
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
cypress/downloads
|
||||
|
||||
@@ -12,14 +12,18 @@ describe('src/cy/commands/exec', () => {
|
||||
cy.stub(Cypress, 'backend').callThrough()
|
||||
})
|
||||
|
||||
it('triggers \'exec\' with the right options', () => {
|
||||
it('sends privileged exec to backend with the right options', () => {
|
||||
Cypress.backend.resolves(okResponse)
|
||||
|
||||
cy.exec('ls').then(() => {
|
||||
expect(Cypress.backend).to.be.calledWith('exec', {
|
||||
cmd: 'ls',
|
||||
timeout: 2500,
|
||||
env: {},
|
||||
expect(Cypress.backend).to.be.calledWith('run:privileged', {
|
||||
commandName: 'exec',
|
||||
userArgs: ['ls'],
|
||||
options: {
|
||||
cmd: 'ls',
|
||||
timeout: 2500,
|
||||
env: {},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -28,17 +32,19 @@ describe('src/cy/commands/exec', () => {
|
||||
Cypress.backend.resolves(okResponse)
|
||||
|
||||
cy.exec('ls', { env: { FOO: 'foo' } }).then(() => {
|
||||
expect(Cypress.backend).to.be.calledWith('exec', {
|
||||
cmd: 'ls',
|
||||
timeout: 2500,
|
||||
env: {
|
||||
FOO: 'foo',
|
||||
expect(Cypress.backend).to.be.calledWith('run:privileged', {
|
||||
commandName: 'exec',
|
||||
userArgs: ['ls', { env: { FOO: 'foo' } }],
|
||||
options: {
|
||||
cmd: 'ls',
|
||||
timeout: 2500,
|
||||
env: { FOO: 'foo' },
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('really works', () => {
|
||||
it('works e2e', () => {
|
||||
// output is trimmed
|
||||
cy.exec('echo foo', { timeout: 20000 }).its('stdout').should('eq', 'foo')
|
||||
})
|
||||
|
||||
@@ -14,14 +14,20 @@ describe('src/cy/commands/files', () => {
|
||||
})
|
||||
|
||||
describe('#readFile', () => {
|
||||
it('triggers \'read:file\' with the right options', () => {
|
||||
it('sends privileged readFile to backend with the right options', () => {
|
||||
Cypress.backend.resolves(okResponse)
|
||||
|
||||
cy.readFile('foo.json').then(() => {
|
||||
expect(Cypress.backend).to.be.calledWith(
|
||||
'read:file',
|
||||
'foo.json',
|
||||
{ encoding: 'utf8' },
|
||||
'run:privileged',
|
||||
{
|
||||
commandName: 'readFile',
|
||||
userArgs: ['foo.json'],
|
||||
options: {
|
||||
file: 'foo.json',
|
||||
encoding: 'utf8',
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -31,9 +37,15 @@ describe('src/cy/commands/files', () => {
|
||||
|
||||
cy.readFile('foo.json', 'ascii').then(() => {
|
||||
expect(Cypress.backend).to.be.calledWith(
|
||||
'read:file',
|
||||
'foo.json',
|
||||
{ encoding: 'ascii' },
|
||||
'run:privileged',
|
||||
{
|
||||
commandName: 'readFile',
|
||||
userArgs: ['foo.json', 'ascii'],
|
||||
options: {
|
||||
file: 'foo.json',
|
||||
encoding: 'ascii',
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -47,9 +59,15 @@ describe('src/cy/commands/files', () => {
|
||||
|
||||
cy.readFile('foo.json', null).then(() => {
|
||||
expect(Cypress.backend).to.be.calledWith(
|
||||
'read:file',
|
||||
'foo.json',
|
||||
{ encoding: null },
|
||||
'run:privileged',
|
||||
{
|
||||
commandName: 'readFile',
|
||||
userArgs: ['foo.json', null],
|
||||
options: {
|
||||
file: 'foo.json',
|
||||
encoding: null,
|
||||
},
|
||||
},
|
||||
)
|
||||
}).should('eql', Buffer.from('\n'))
|
||||
})
|
||||
@@ -426,17 +444,21 @@ describe('src/cy/commands/files', () => {
|
||||
})
|
||||
|
||||
describe('#writeFile', () => {
|
||||
it('triggers \'write:file\' with the right options', () => {
|
||||
it('sends privileged writeFile to backend with the right options', () => {
|
||||
Cypress.backend.resolves(okResponse)
|
||||
|
||||
cy.writeFile('foo.txt', 'contents').then(() => {
|
||||
expect(Cypress.backend).to.be.calledWith(
|
||||
'write:file',
|
||||
'foo.txt',
|
||||
'contents',
|
||||
'run:privileged',
|
||||
{
|
||||
encoding: 'utf8',
|
||||
flag: 'w',
|
||||
commandName: 'writeFile',
|
||||
userArgs: ['foo.txt', 'contents'],
|
||||
options: {
|
||||
fileName: 'foo.txt',
|
||||
contents: 'contents',
|
||||
encoding: 'utf8',
|
||||
flag: 'w',
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -447,12 +469,16 @@ describe('src/cy/commands/files', () => {
|
||||
|
||||
cy.writeFile('foo.txt', 'contents', 'ascii').then(() => {
|
||||
expect(Cypress.backend).to.be.calledWith(
|
||||
'write:file',
|
||||
'foo.txt',
|
||||
'contents',
|
||||
'run:privileged',
|
||||
{
|
||||
encoding: 'ascii',
|
||||
flag: 'w',
|
||||
commandName: 'writeFile',
|
||||
userArgs: ['foo.txt', 'contents', 'ascii'],
|
||||
options: {
|
||||
fileName: 'foo.txt',
|
||||
contents: 'contents',
|
||||
encoding: 'ascii',
|
||||
flag: 'w',
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -462,14 +488,20 @@ describe('src/cy/commands/files', () => {
|
||||
it('explicit null encoding is sent to server as Buffer', () => {
|
||||
Cypress.backend.resolves(okResponse)
|
||||
|
||||
cy.writeFile('foo.txt', Buffer.from([0, 0, 54, 255]), null).then(() => {
|
||||
const buffer = Buffer.from([0, 0, 54, 255])
|
||||
|
||||
cy.writeFile('foo.txt', buffer, null).then(() => {
|
||||
expect(Cypress.backend).to.be.calledWith(
|
||||
'write:file',
|
||||
'foo.txt',
|
||||
Buffer.from([0, 0, 54, 255]),
|
||||
'run:privileged',
|
||||
{
|
||||
encoding: null,
|
||||
flag: 'w',
|
||||
commandName: 'writeFile',
|
||||
userArgs: ['foo.txt', buffer, null],
|
||||
options: {
|
||||
fileName: 'foo.txt',
|
||||
contents: buffer,
|
||||
encoding: null,
|
||||
flag: 'w',
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -480,12 +512,16 @@ describe('src/cy/commands/files', () => {
|
||||
|
||||
cy.writeFile('foo.txt', 'contents', { encoding: 'ascii' }).then(() => {
|
||||
expect(Cypress.backend).to.be.calledWith(
|
||||
'write:file',
|
||||
'foo.txt',
|
||||
'contents',
|
||||
'run:privileged',
|
||||
{
|
||||
encoding: 'ascii',
|
||||
flag: 'w',
|
||||
commandName: 'writeFile',
|
||||
userArgs: ['foo.txt', 'contents', { encoding: 'ascii' }],
|
||||
options: {
|
||||
fileName: 'foo.txt',
|
||||
contents: 'contents',
|
||||
encoding: 'ascii',
|
||||
flag: 'w',
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -531,12 +567,16 @@ describe('src/cy/commands/files', () => {
|
||||
|
||||
cy.writeFile('foo.txt', 'contents', { flag: 'a+' }).then(() => {
|
||||
expect(Cypress.backend).to.be.calledWith(
|
||||
'write:file',
|
||||
'foo.txt',
|
||||
'contents',
|
||||
'run:privileged',
|
||||
{
|
||||
encoding: 'utf8',
|
||||
flag: 'a+',
|
||||
commandName: 'writeFile',
|
||||
userArgs: ['foo.txt', 'contents', { flag: 'a+' }],
|
||||
options: {
|
||||
fileName: 'foo.txt',
|
||||
contents: 'contents',
|
||||
encoding: 'utf8',
|
||||
flag: 'a+',
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -9,14 +9,18 @@ describe('src/cy/commands/task', () => {
|
||||
cy.stub(Cypress, 'backend').callThrough()
|
||||
})
|
||||
|
||||
it('calls Cypress.backend(\'task\') with the right options', () => {
|
||||
it('sends privileged task to backend with the right options', () => {
|
||||
Cypress.backend.resolves(null)
|
||||
|
||||
cy.task('foo').then(() => {
|
||||
expect(Cypress.backend).to.be.calledWith('task', {
|
||||
task: 'foo',
|
||||
timeout: 2500,
|
||||
arg: undefined,
|
||||
expect(Cypress.backend).to.be.calledWith('run:privileged', {
|
||||
commandName: 'task',
|
||||
userArgs: ['foo'],
|
||||
options: {
|
||||
task: 'foo',
|
||||
timeout: 2500,
|
||||
arg: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -25,11 +29,13 @@ describe('src/cy/commands/task', () => {
|
||||
Cypress.backend.resolves(null)
|
||||
|
||||
cy.task('foo', { foo: 'foo' }).then(() => {
|
||||
expect(Cypress.backend).to.be.calledWith('task', {
|
||||
task: 'foo',
|
||||
timeout: 2500,
|
||||
arg: {
|
||||
foo: 'foo',
|
||||
expect(Cypress.backend).to.be.calledWith('run:privileged', {
|
||||
commandName: 'task',
|
||||
userArgs: ['foo', { foo: 'foo' }],
|
||||
options: {
|
||||
task: 'foo',
|
||||
timeout: 2500,
|
||||
arg: { foo: 'foo' },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,8 +22,13 @@ describe('src/cypress/script_utils', () => {
|
||||
cy.stub($sourceMapUtils, 'initializeSourceMapConsumer').resolves()
|
||||
})
|
||||
|
||||
it('fetches each script', () => {
|
||||
return $scriptUtils.runScripts(scriptWindow, scripts)
|
||||
it('fetches each script in non-webkit browsers', () => {
|
||||
return $scriptUtils.runScripts({
|
||||
browser: { family: 'chromium' },
|
||||
scripts,
|
||||
specWindow: scriptWindow,
|
||||
testingType: 'e2e',
|
||||
})
|
||||
.then(() => {
|
||||
expect($networkUtils.fetch).to.be.calledTwice
|
||||
expect($networkUtils.fetch).to.be.calledWith(scripts[0].relativeUrl)
|
||||
@@ -31,8 +36,62 @@ describe('src/cypress/script_utils', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('appends each script in e2e webkit', async () => {
|
||||
const foundScript = {
|
||||
after: cy.stub(),
|
||||
}
|
||||
const createdScript1 = {
|
||||
addEventListener: cy.stub(),
|
||||
}
|
||||
const createdScript2 = {
|
||||
addEventListener: cy.stub(),
|
||||
}
|
||||
const doc = {
|
||||
querySelector: cy.stub().returns(foundScript),
|
||||
createElement: cy.stub(),
|
||||
}
|
||||
|
||||
doc.createElement.onCall(0).returns(createdScript1)
|
||||
doc.createElement.onCall(1).returns(createdScript2)
|
||||
|
||||
scriptWindow.document = doc
|
||||
|
||||
const runScripts = $scriptUtils.runScripts({
|
||||
scripts,
|
||||
specWindow: scriptWindow,
|
||||
browser: { family: 'webkit' },
|
||||
testingType: 'e2e',
|
||||
})
|
||||
|
||||
// each script is appended and run before the next
|
||||
|
||||
await Promise.delay(1) // wait a tick due to promise
|
||||
expect(createdScript1.addEventListener).to.be.calledWith('load')
|
||||
createdScript1.addEventListener.lastCall.args[1]()
|
||||
|
||||
await Promise.delay(1) // wait a tick due to promise
|
||||
expect(createdScript2.addEventListener).to.be.calledWith('load')
|
||||
createdScript2.addEventListener.lastCall.args[1]()
|
||||
|
||||
await runScripts
|
||||
|
||||
// sets script src
|
||||
expect(createdScript1.src).to.equal(scripts[0].relativeUrl)
|
||||
expect(createdScript2.src).to.equal(scripts[1].relativeUrl)
|
||||
|
||||
// appends scripts
|
||||
expect(foundScript.after).to.be.calledTwice
|
||||
expect(foundScript.after).to.be.calledWith(createdScript1)
|
||||
expect(foundScript.after).to.be.calledWith(createdScript2)
|
||||
})
|
||||
|
||||
it('extracts the source map from each script', () => {
|
||||
return $scriptUtils.runScripts(scriptWindow, scripts)
|
||||
return $scriptUtils.runScripts({
|
||||
browser: { family: 'chromium' },
|
||||
scripts,
|
||||
specWindow: scriptWindow,
|
||||
testingType: 'e2e',
|
||||
})
|
||||
.then(() => {
|
||||
expect($sourceMapUtils.extractSourceMap).to.be.calledTwice
|
||||
expect($sourceMapUtils.extractSourceMap).to.be.calledWith('the script contents')
|
||||
@@ -41,7 +100,12 @@ describe('src/cypress/script_utils', () => {
|
||||
})
|
||||
|
||||
it('evals each script', () => {
|
||||
return $scriptUtils.runScripts(scriptWindow, scripts)
|
||||
return $scriptUtils.runScripts({
|
||||
browser: { family: 'chromium' },
|
||||
scripts,
|
||||
specWindow: scriptWindow,
|
||||
testingType: 'e2e',
|
||||
})
|
||||
.then(() => {
|
||||
expect(scriptWindow.eval).to.be.calledTwice
|
||||
expect(scriptWindow.eval).to.be.calledWith('the script contents\n//# sourceURL=http://localhost:3500cypress/integration/script1.js')
|
||||
@@ -53,7 +117,12 @@ describe('src/cypress/script_utils', () => {
|
||||
context('#runPromises', () => {
|
||||
it('handles promises and doesnt try to fetch + eval manually', async () => {
|
||||
const scriptsAsPromises = [() => Promise.resolve(), () => Promise.resolve()]
|
||||
const result = await $scriptUtils.runScripts({}, scriptsAsPromises)
|
||||
const result = await $scriptUtils.runScripts({
|
||||
browser: { family: 'chromium' },
|
||||
scripts: scriptsAsPromises,
|
||||
specWindow: {},
|
||||
testingType: 'e2e',
|
||||
})
|
||||
|
||||
expect(result).to.have.length(scriptsAsPromises.length)
|
||||
})
|
||||
|
||||
@@ -35,12 +35,16 @@ context('cy.origin files', { browser: '!webkit' }, () => {
|
||||
|
||||
cy.writeFile('foo.json', contents).then(() => {
|
||||
expect(Cypress.backend).to.be.calledWith(
|
||||
'write:file',
|
||||
'foo.json',
|
||||
contents,
|
||||
'run:privileged',
|
||||
{
|
||||
encoding: 'utf8',
|
||||
flag: 'w',
|
||||
commandName: 'writeFile',
|
||||
userArgs: ['foo.json', contents],
|
||||
options: {
|
||||
fileName: 'foo.json',
|
||||
contents,
|
||||
encoding: 'utf8',
|
||||
flag: 'w',
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -208,7 +208,7 @@ context('cy.origin misc', { browser: '!webkit' }, () => {
|
||||
|
||||
it('verifies number of cy commands', () => {
|
||||
// remove custom commands we added for our own testing
|
||||
const customCommands = ['getAll', 'shouldWithTimeout', 'originLoadUtils']
|
||||
const customCommands = ['getAll', 'shouldWithTimeout', 'originLoadUtils', 'runSupportFileCustomPrivilegedCommands']
|
||||
// @ts-ignore
|
||||
const actualCommands = Cypress._.pullAll([...Object.keys(cy.commandFns), ...Object.keys(cy.queryFns)], customCommands)
|
||||
const expectedCommands = [
|
||||
|
||||
@@ -7,7 +7,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
it('succeeds on a localhost domain name', () => {
|
||||
cy.origin('localhost', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `https://localhost/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `https://localhost/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://localhost') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -17,7 +17,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
it('succeeds on an ip address', () => {
|
||||
cy.origin('127.0.0.1', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `https://127.0.0.1/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `https://127.0.0.1/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://127.0.0.1') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -29,7 +29,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
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/spec-bridge-iframes`
|
||||
const expectedSrc = `https://[::1]/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://[::1]') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -39,7 +39,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
it('succeeds on a unicode domain', () => {
|
||||
cy.origin('はじめよう.みんな', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `https://xn--p8j9a0d9c9a.xn--q9jyb4c/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `https://xn--p8j9a0d9c9a.xn--q9jyb4c/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://xn--p8j9a0d9c9a.xn--q9jyb4c') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -49,7 +49,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
it('succeeds on a complete origin', () => {
|
||||
cy.origin('http://foobar1.com:3500', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `http://foobar1.com:3500/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `http://foobar1.com:3500/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ http://foobar1.com:3500') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -59,7 +59,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
it('succeeds on a complete origin using https', () => {
|
||||
cy.origin('https://www.foobar2.com:3500', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `https://www.foobar2.com:3500/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `https://www.foobar2.com:3500/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://www.foobar2.com:3500') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -69,7 +69,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
it('succeeds on a hostname and port', () => {
|
||||
cy.origin('foobar3.com:3500', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `https://foobar3.com:3500/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `https://foobar3.com:3500/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar3.com:3500') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -79,7 +79,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
it('succeeds on a protocol and hostname', () => {
|
||||
cy.origin('http://foobar4.com', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `http://foobar4.com/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `http://foobar4.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ http://foobar4.com') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -89,7 +89,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
it('succeeds on a subdomain', () => {
|
||||
cy.origin('app.foobar5.com', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `https://app.foobar5.com/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `https://app.foobar5.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://app.foobar5.com') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -99,7 +99,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
it('succeeds when only domain is passed', () => {
|
||||
cy.origin('foobar6.com', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `https://foobar6.com/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `https://foobar6.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar6.com') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -109,7 +109,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
it('succeeds on a url with path', () => {
|
||||
cy.origin('http://www.foobar7.com/login', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `http://www.foobar7.com/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `http://www.foobar7.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ http://www.foobar7.com') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -119,7 +119,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
it('succeeds on a url with a hash', () => {
|
||||
cy.origin('http://www.foobar8.com/#hash', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `http://www.foobar8.com/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `http://www.foobar8.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ http://www.foobar8.com') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -129,7 +129,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
it('succeeds on a url with a path and hash', () => {
|
||||
cy.origin('http://www.foobar9.com/login/#hash', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `http://www.foobar9.com/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `http://www.foobar9.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ http://www.foobar9.com') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -139,7 +139,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
it('succeeds on a domain with path', () => {
|
||||
cy.origin('foobar10.com/login', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `https://foobar10.com/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `https://foobar10.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar10.com') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -149,7 +149,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
it('succeeds on a domain with a hash', () => {
|
||||
cy.origin('foobar11.com/#hash', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `https://foobar11.com/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `https://foobar11.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar11.com') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -159,7 +159,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
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/spec-bridge-iframes`
|
||||
const expectedSrc = `https://foobar12.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://foobar12.com') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -169,7 +169,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
it('succeeds on a public suffix with a subdomain', () => {
|
||||
cy.origin('app.foobar.herokuapp.com', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `https://app.foobar.herokuapp.com/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `https://app.foobar.herokuapp.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://app.foobar.herokuapp.com') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -179,7 +179,7 @@ describe('cy.origin', { browser: '!webkit' }, () => {
|
||||
it('succeeds on a machine name', () => {
|
||||
cy.origin('machine-name', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `https://machine-name/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `https://machine-name/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://machine-name') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -356,7 +356,7 @@ describe('cy.origin - external hosts', { browser: '!webkit' }, () => {
|
||||
cy.visit('https://www.foobar.com:3502/fixtures/primary-origin.html')
|
||||
cy.origin('https://www.idp.com:3502', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `https://www.idp.com:3502/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `https://www.idp.com:3502/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://www.idp.com:3502') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
@@ -372,7 +372,7 @@ describe('cy.origin - external hosts', { browser: '!webkit' }, () => {
|
||||
cy.visit('https://www.google.com')
|
||||
cy.origin('accounts.google.com', () => undefined)
|
||||
cy.then(() => {
|
||||
const expectedSrc = `https://accounts.google.com/__cypress/spec-bridge-iframes`
|
||||
const expectedSrc = `https://accounts.google.com/__cypress/spec-bridge-iframes?browserFamily=${Cypress.browser.family}`
|
||||
const iframe = window.top?.document.getElementById('Spec\ Bridge:\ https://accounts.google.com') as HTMLIFrameElement
|
||||
|
||||
expect(iframe.src).to.equal(expectedSrc)
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import { runImportedPrivilegedCommands } from '../../support/utils'
|
||||
|
||||
const isWebkit = Cypress.isBrowser({ family: 'webkit' })
|
||||
|
||||
function runSpecFunctionCommands () {
|
||||
cy.exec('echo "hello"')
|
||||
cy.readFile('cypress/fixtures/app.json')
|
||||
cy.writeFile('cypress/_test-output/written.json', 'contents')
|
||||
cy.task('return:arg', 'arg')
|
||||
cy.get('#basic').selectFile('cypress/fixtures/valid.json')
|
||||
if (!isWebkit) {
|
||||
cy.origin('http://foobar.com:3500', () => {})
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('runSpecFileCustomPrivilegedCommands', runSpecFunctionCommands)
|
||||
|
||||
describe('privileged commands', () => {
|
||||
describe('in spec file or support file', () => {
|
||||
let ranInBeforeEach = false
|
||||
|
||||
beforeEach(() => {
|
||||
if (ranInBeforeEach) return
|
||||
|
||||
ranInBeforeEach = true
|
||||
|
||||
// ensures these run properly in hooks, but only run it once per spec run
|
||||
cy.exec('echo "hello"')
|
||||
cy.readFile('cypress/fixtures/app.json')
|
||||
cy.writeFile('cypress/_test-output/written.json', 'contents')
|
||||
cy.task('return:arg', 'arg')
|
||||
cy.get('#basic').selectFile('cypress/fixtures/valid.json')
|
||||
if (!isWebkit) {
|
||||
cy.origin('http://foobar.com:3500', () => {})
|
||||
}
|
||||
})
|
||||
|
||||
it('passes in test body', () => {
|
||||
cy.exec('echo "hello"')
|
||||
cy.readFile('cypress/fixtures/app.json')
|
||||
cy.writeFile('cypress/_test-output/written.json', 'contents')
|
||||
cy.task('return:arg', 'arg')
|
||||
cy.get('#basic').selectFile('cypress/fixtures/valid.json')
|
||||
if (!isWebkit) {
|
||||
cy.origin('http://foobar.com:3500', () => {})
|
||||
}
|
||||
})
|
||||
|
||||
it('passes two or more exact commands in a row', () => {
|
||||
cy.task('return:arg', 'arg')
|
||||
cy.task('return:arg', 'arg')
|
||||
})
|
||||
|
||||
it('passes in test body .then() callback', () => {
|
||||
cy.then(() => {
|
||||
cy.exec('echo "hello"')
|
||||
cy.readFile('cypress/fixtures/app.json')
|
||||
cy.writeFile('cypress/_test-output/written.json', 'contents')
|
||||
cy.task('return:arg', 'arg')
|
||||
cy.get('#basic').selectFile('cypress/fixtures/valid.json')
|
||||
if (!isWebkit) {
|
||||
cy.origin('http://foobar.com:3500', () => {})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('passes in spec function', () => {
|
||||
runSpecFunctionCommands()
|
||||
})
|
||||
|
||||
it('passes in imported function', () => {
|
||||
runImportedPrivilegedCommands()
|
||||
})
|
||||
|
||||
it('passes in support file global function', () => {
|
||||
window.runGlobalPrivilegedCommands()
|
||||
})
|
||||
|
||||
it('passes in spec file custom command', () => {
|
||||
cy.runSpecFileCustomPrivilegedCommands()
|
||||
})
|
||||
|
||||
it('passes in support file custom command', () => {
|
||||
cy.runSupportFileCustomPrivilegedCommands()
|
||||
})
|
||||
|
||||
// cy.origin() doesn't currently have webkit support
|
||||
it('passes in .origin() callback', { browser: '!webkit' }, () => {
|
||||
cy.origin('http://foobar.com:3500', () => {
|
||||
cy.exec('echo "hello"')
|
||||
cy.readFile('cypress/fixtures/app.json')
|
||||
cy.writeFile('cypress/_test-output/written.json', 'contents')
|
||||
cy.task('return:arg', 'arg')
|
||||
|
||||
// there's a bug using cy.selectFile() with a path inside of
|
||||
// cy.origin(): https://github.com/cypress-io/cypress/issues/25261
|
||||
// cy.visit('/fixtures/files-form.html')
|
||||
// cy.get('#basic').selectFile('cypress/fixtures/valid.json')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('in AUT', () => {
|
||||
const strategies = ['inline', 'then', 'eval', 'function']
|
||||
const commands = ['exec', 'readFile', 'writeFile', 'selectFile', 'task']
|
||||
|
||||
// cy.origin() doesn't currently have webkit support
|
||||
if (!Cypress.isBrowser({ family: 'webkit' })) {
|
||||
commands.push('origin')
|
||||
}
|
||||
|
||||
const errorForCommand = (commandName) => {
|
||||
return `\`cy.${commandName}()\` must only be invoked from the spec file or support file.`
|
||||
}
|
||||
|
||||
strategies.forEach((strategy) => {
|
||||
commands.forEach((command) => {
|
||||
describe(`strategy: ${strategy}`, () => {
|
||||
describe(`command: ${command}`, () => {
|
||||
it('fails in html script', (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include(errorForCommand(command))
|
||||
done()
|
||||
})
|
||||
|
||||
cy.visit(`/aut-commands?strategy=${strategy}&command=${command}`)
|
||||
})
|
||||
|
||||
it('fails in separate script', (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include(errorForCommand(command))
|
||||
done()
|
||||
})
|
||||
|
||||
cy.visit(`/fixtures/aut-commands.html?strategy=${strategy}&command=${command}`)
|
||||
})
|
||||
|
||||
it('does not run command in separate script appended to spec frame', () => {
|
||||
let ranCommand = false
|
||||
|
||||
cy.on('log:added', (attrs) => {
|
||||
if (attrs.name === command) {
|
||||
ranCommand = true
|
||||
}
|
||||
})
|
||||
|
||||
// this attempts to run the command by appending a <script> to the
|
||||
// spec frame, but the Content-Security-Policy we set will prevent
|
||||
// that script from running
|
||||
cy.visit(`/aut-commands?appendToSpecFrame=true&strategy=${strategy}&command=${command}`)
|
||||
// wait 500ms then ensure the command did not run
|
||||
cy.wait(500).then(() => {
|
||||
expect(ranCommand, `expected cy.${command}() not to run, but it did`).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
// while not immediately obvious, this basically triggers using
|
||||
// cy.origin() within itself. that doesn't work anyways and
|
||||
// hits a different error, so it can't be used outside of the spec
|
||||
// in this manner
|
||||
if (command !== 'origin') {
|
||||
// cy.origin() doesn't currently have webkit support
|
||||
it('fails in cross-origin html script', { browser: '!webkit' }, (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include(errorForCommand(command))
|
||||
done()
|
||||
})
|
||||
|
||||
cy.origin('http://foobar.com:3500', { args: { strategy, command } }, ({ strategy, command }) => {
|
||||
cy.visit(`/aut-commands?strategy=${strategy}&command=${command}`)
|
||||
})
|
||||
})
|
||||
|
||||
// cy.origin() doesn't currently have webkit support
|
||||
it('fails in cross-origin separate script', { browser: '!webkit' }, (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include(errorForCommand(command))
|
||||
done()
|
||||
})
|
||||
|
||||
cy.origin('http://foobar.com:3500', { args: { strategy, command } }, ({ strategy, command }) => {
|
||||
cy.visit(`/fixtures/aut-commands.html?strategy=${strategy}&command=${command}`)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,2 @@
|
||||
<input type="file" />
|
||||
<script src="aut-commands.js"></script>
|
||||
@@ -0,0 +1,97 @@
|
||||
(() => {
|
||||
const urlParams = new URLSearchParams(window.__search || window.location.search)
|
||||
const appendToSpecFrame = !!urlParams.get('appendToSpecFrame')
|
||||
const strategy = urlParams.get('strategy')
|
||||
const command = urlParams.get('command')
|
||||
const cy = window.Cypress.cy
|
||||
|
||||
if (cy.state('current')) {
|
||||
cy.state('current').attributes.args = [() => {}]
|
||||
}
|
||||
|
||||
const TOP = 'top' // prevents frame-busting
|
||||
// recursively tries sibling frames until finding the spec frame, which
|
||||
// should be the first same-origin one we come across
|
||||
const specFrame = window.__isSpecFrame ? window : (() => {
|
||||
const tryFrame = (index) => {
|
||||
try {
|
||||
// will throw if cross-origin
|
||||
window[TOP].frames[index].location.href
|
||||
|
||||
return window[TOP].frames[index]
|
||||
} catch (err) {
|
||||
return tryFrame(index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
return tryFrame(1)
|
||||
})()
|
||||
|
||||
const run = () => {
|
||||
switch (command) {
|
||||
case 'exec':
|
||||
cy.exec('echo "Goodbye"')
|
||||
break
|
||||
case 'readFile':
|
||||
cy.readFile('cypress/fixtures/example.json')
|
||||
break
|
||||
case 'writeFile':
|
||||
cy.writeFile('cypress/_test-output/written.json', 'other contents')
|
||||
break
|
||||
case 'task':
|
||||
cy.task('return:arg', 'other arg')
|
||||
break
|
||||
case 'selectFile':
|
||||
cy.get('input').selectFile('cypress/fixtures/example.json')
|
||||
break
|
||||
case 'origin':
|
||||
cy.origin('http://barbaz.com:3500', () => {})
|
||||
break
|
||||
default:
|
||||
throw new Error(`Command not supported: ${command}`)
|
||||
}
|
||||
}
|
||||
const runString = run.toString()
|
||||
|
||||
// instead of running this script in the AUT, this appends it to the
|
||||
// spec frame to run it there
|
||||
if (appendToSpecFrame) {
|
||||
cy.wait(500) // gives the script time to run without the queue ending
|
||||
|
||||
const beforeScript = specFrame.document.createElement('script')
|
||||
|
||||
beforeScript.textContent = `
|
||||
window.__search = '${window.location.search.replace('appendToSpecFrame=true&', '')}'
|
||||
window.__isSpecFrame = true
|
||||
`
|
||||
|
||||
specFrame.document.body.appendChild(beforeScript)
|
||||
|
||||
const scriptEl = specFrame.document.createElement('script')
|
||||
|
||||
scriptEl.src = '/fixtures/aut-commands.js'
|
||||
specFrame.document.body.appendChild(scriptEl)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch (strategy) {
|
||||
case 'inline':
|
||||
run()
|
||||
break
|
||||
case 'then':
|
||||
cy.then(run)
|
||||
break
|
||||
case 'eval':
|
||||
specFrame.eval(`(command) => { (${runString})() }`)(command)
|
||||
break
|
||||
case 'function': {
|
||||
const fn = new specFrame.Function('command', `(${runString})()`)
|
||||
|
||||
fn.call(specFrame, command)
|
||||
break
|
||||
}
|
||||
default:
|
||||
throw new Error(`Strategy not supported: ${strategy}`)
|
||||
}
|
||||
})()
|
||||
@@ -1,4 +1,4 @@
|
||||
const fs = require('fs')
|
||||
const fs = require('fs-extra')
|
||||
const auth = require('basic-auth')
|
||||
const bodyParser = require('body-parser')
|
||||
const express = require('express')
|
||||
@@ -355,11 +355,24 @@ const createApp = (port) => {
|
||||
const el = document.createElement('p')
|
||||
el.id = 'p' + i
|
||||
el.innerHTML = 'x'.repeat(100000)
|
||||
|
||||
|
||||
document.body.appendChild(el)
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
</html>
|
||||
`)
|
||||
})
|
||||
|
||||
app.get('/aut-commands', async (req, res) => {
|
||||
const script = (await fs.readFileAsync(path.join(__dirname, '..', 'fixtures', 'aut-commands.js'))).toString()
|
||||
|
||||
res.send(`
|
||||
<html>
|
||||
<body>
|
||||
<input type="file" />
|
||||
<script>${script}</script>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
})
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ if (!isActuallyInteractive) {
|
||||
Cypress.config('retries', 2)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
let ranPrivilegedCommandsInBeforeEach = false
|
||||
|
||||
beforeEach(function () {
|
||||
// always set that we're interactive so we
|
||||
// get consistent passes and failures when running
|
||||
// from CI and when running in GUI mode
|
||||
@@ -30,6 +32,25 @@ beforeEach(() => {
|
||||
try {
|
||||
$(cy.state('window')).off()
|
||||
} catch (error) {} // eslint-disable-line no-empty
|
||||
|
||||
// only want to run this as part of the privileged commands spec
|
||||
if (cy.config('spec').baseName === 'privileged_commands.cy.ts') {
|
||||
cy.visit('/fixtures/files-form.html')
|
||||
|
||||
// it only needs to run once per spec run
|
||||
if (ranPrivilegedCommandsInBeforeEach) return
|
||||
|
||||
ranPrivilegedCommandsInBeforeEach = true
|
||||
|
||||
cy.exec('echo "hello"')
|
||||
cy.readFile('cypress/fixtures/app.json')
|
||||
cy.writeFile('cypress/_test-output/written.json', 'contents')
|
||||
cy.task('return:arg', 'arg')
|
||||
cy.get('#basic').selectFile('cypress/fixtures/valid.json')
|
||||
if (!Cypress.isBrowser({ family: 'webkit' })) {
|
||||
cy.origin('http://foobar.com:3500', () => {})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// this is here to test that cy.origin() dependencies used directly in the
|
||||
|
||||
@@ -172,6 +172,29 @@ export const makeRequestForCookieBehaviorTests = (
|
||||
})
|
||||
}
|
||||
|
||||
function runCommands () {
|
||||
cy.exec('echo "hello"')
|
||||
cy.readFile('cypress/fixtures/app.json')
|
||||
cy.writeFile('cypress/_test-output/written.json', 'contents')
|
||||
cy.task('return:arg', 'arg')
|
||||
cy.get('#basic').selectFile('cypress/fixtures/valid.json')
|
||||
if (!Cypress.isBrowser({ family: 'webkit' })) {
|
||||
cy.origin('http://foobar.com:3500', () => {})
|
||||
}
|
||||
}
|
||||
|
||||
export const runImportedPrivilegedCommands = runCommands
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
runGlobalPrivilegedCommands: () => void
|
||||
}
|
||||
}
|
||||
|
||||
window.runGlobalPrivilegedCommands = runCommands
|
||||
|
||||
Cypress.Commands.add('runSupportFileCustomPrivilegedCommands', runCommands)
|
||||
|
||||
Cypress.Commands.addQuery('getAll', getAllFn)
|
||||
|
||||
Cypress.Commands.add('shouldWithTimeout', shouldWithTimeout)
|
||||
|
||||
@@ -167,6 +167,16 @@ export class PrimaryOriginCommunicator extends EventEmitter {
|
||||
preprocessedData.args = data.args
|
||||
}
|
||||
|
||||
// if the data has an error/err, it needs special handling for Firefox or
|
||||
// else it will end up ignored because it's not structured-cloneable
|
||||
if (data?.error) {
|
||||
preprocessedData.error = preprocessForSerialization(data.error)
|
||||
}
|
||||
|
||||
if (data?.err) {
|
||||
preprocessedData.err = preprocessForSerialization(data.err)
|
||||
}
|
||||
|
||||
// If there is no crossOriginDriverWindows, there is no need to send the message.
|
||||
source.postMessage({
|
||||
event,
|
||||
|
||||
@@ -8,6 +8,10 @@ export const handleSocketEvents = (Cypress) => {
|
||||
timeout: Cypress.config().defaultCommandTimeout,
|
||||
})
|
||||
|
||||
if (response && response.error) {
|
||||
return callback({ error: response.error })
|
||||
}
|
||||
|
||||
callback({ response })
|
||||
}
|
||||
|
||||
|
||||
@@ -181,9 +181,16 @@ export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => {
|
||||
Cypress.specBridgeCommunicator.toPrimary('queue:finished', { err }, { syncGlobals: true })
|
||||
})
|
||||
|
||||
// the name of this function is used to verify if privileged commands are
|
||||
// properly called. it shouldn't be removed and if the name is changed, it
|
||||
// needs to also be changed in server/lib/browsers/privileged-channel.js
|
||||
function invokeOriginFn (callback) {
|
||||
return window.eval(`(${callback})`)(args)
|
||||
}
|
||||
|
||||
try {
|
||||
const callback = await getCallbackFn(fn, file)
|
||||
const value = window.eval(`(${callback})`)(args)
|
||||
const value = invokeOriginFn(callback)
|
||||
|
||||
// If we detect a non promise value with commands in queue, throw an error
|
||||
if (value && cy.queue.length > 0 && !value.then) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import $dom from '../../../dom'
|
||||
import $errUtils from '../../../cypress/error_utils'
|
||||
import $actionability from '../../actionability'
|
||||
import { addEventCoords, dispatch } from './trigger'
|
||||
import { runPrivilegedCommand, trimUserArgs } from '../../../util/privileged_channel'
|
||||
|
||||
/* dropzone.js relies on an experimental, nonstandard API, webkitGetAsEntry().
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
|
||||
@@ -82,6 +83,15 @@ interface InternalSelectFileOptions extends Cypress.SelectFileOptions {
|
||||
eventTarget: JQuery
|
||||
}
|
||||
|
||||
interface FilePathObject {
|
||||
fileName?: string
|
||||
index: number
|
||||
isFilePath: boolean
|
||||
lastModified?: number
|
||||
mimeType?: string
|
||||
path: string
|
||||
}
|
||||
|
||||
const ACTIONS = {
|
||||
select: (element, dataTransfer, coords, state) => {
|
||||
(element as HTMLInputElement).files = dataTransfer.files
|
||||
@@ -153,32 +163,64 @@ export default (Commands, Cypress, cy, state, config) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Uses backend read:file rather than cy.readFile because we don't want to retry
|
||||
// loading a specific file until timeout, but rather retry the selectFile command as a whole
|
||||
const handlePath = async (file, options) => {
|
||||
return Cypress.backend('read:file', file.contents, { encoding: null })
|
||||
.then(({ contents }) => {
|
||||
return {
|
||||
// We default to the filename on the path, but allow them to override
|
||||
fileName: basename(file.contents),
|
||||
...file,
|
||||
contents: Cypress.Buffer.from(contents),
|
||||
}
|
||||
const readFiles = async (filePaths, options, userArgs) => {
|
||||
if (!filePaths.length) return []
|
||||
|
||||
// This reads the file with privileged access in the same manner as
|
||||
// cy.readFile(). We call directly into the backend rather than calling
|
||||
// cy.readFile() directly because we don't want to retry loading a specific
|
||||
// file until timeout, but rather retry the selectFile command as a whole
|
||||
return runPrivilegedCommand({
|
||||
commandName: 'selectFile',
|
||||
cy,
|
||||
Cypress: (Cypress as unknown) as InternalCypress.Cypress,
|
||||
options: {
|
||||
files: filePaths,
|
||||
},
|
||||
userArgs,
|
||||
})
|
||||
.then((results) => {
|
||||
return results.map((result) => {
|
||||
return {
|
||||
// We default to the filename on the path, but allow them to override
|
||||
fileName: basename(result.path),
|
||||
...result,
|
||||
contents: Cypress.Buffer.from(result.contents),
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isNonSpec) {
|
||||
$errUtils.throwErrByPath('miscellaneous.non_spec_invocation', {
|
||||
args: { cmd: 'selectFile' },
|
||||
})
|
||||
}
|
||||
|
||||
if (err.code === 'ENOENT') {
|
||||
$errUtils.throwErrByPath('files.nonexistent', {
|
||||
args: { cmd: 'selectFile', file: file.contents, filePath: err.filePath },
|
||||
args: { cmd: 'selectFile', file: err.originalFilePath, filePath: err.filePath },
|
||||
})
|
||||
}
|
||||
|
||||
$errUtils.throwErrByPath('files.unexpected_error', {
|
||||
onFail: options._log,
|
||||
args: { cmd: 'selectFile', action: 'read', file, filePath: err.filePath, error: err.message },
|
||||
args: { cmd: 'selectFile', action: 'read', file: err.originalFilePath, filePath: err.filePath, error: err.message },
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const getFilePathObject = (file, index) => {
|
||||
return {
|
||||
encoding: null,
|
||||
fileName: file.fileName,
|
||||
index,
|
||||
isFilePath: true,
|
||||
lastModified: file.lastModified,
|
||||
mimeType: file.mimeType,
|
||||
path: file.contents,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Turns a user-provided file - a string shorthand, ArrayBuffer, or object
|
||||
* into an object of form {
|
||||
@@ -191,7 +233,7 @@ export default (Commands, Cypress, cy, state, config) => {
|
||||
* we warn them and suggest how to fix it.
|
||||
*/
|
||||
const parseFile = (options) => {
|
||||
return async (file: any, index: number, filesArray: any[]): Promise<Cypress.FileReferenceObject> => {
|
||||
return (file: any, index: number, filesArray: any[]): Cypress.FileReferenceObject | FilePathObject => {
|
||||
if (typeof file === 'string' || ArrayBuffer.isView(file)) {
|
||||
file = { contents: file }
|
||||
}
|
||||
@@ -212,10 +254,13 @@ export default (Commands, Cypress, cy, state, config) => {
|
||||
}
|
||||
|
||||
if (typeof file.contents === 'string') {
|
||||
file = handleAlias(file, options) ?? await handlePath(file, options)
|
||||
// if not an alias, an object representing that the file is a path that
|
||||
// needs to be read from disk. contents are an empty string to they
|
||||
// it skips the next check
|
||||
file = handleAlias(file, options) ?? getFilePathObject(file, index)
|
||||
}
|
||||
|
||||
if (!_.isString(file.contents) && !ArrayBuffer.isView(file.contents)) {
|
||||
if (!file.isFilePath && !_.isString(file.contents) && !ArrayBuffer.isView(file.contents)) {
|
||||
file.contents = JSON.stringify(file.contents)
|
||||
}
|
||||
|
||||
@@ -223,8 +268,24 @@ export default (Commands, Cypress, cy, state, config) => {
|
||||
}
|
||||
}
|
||||
|
||||
async function collectFiles (files, options, userArgs) {
|
||||
const filesCollection = ([] as (Cypress.FileReference | FilePathObject)[]).concat(files).map(parseFile(options))
|
||||
// if there are any file paths, read them from the server in one go
|
||||
const filePaths = filesCollection.filter((file) => (file as FilePathObject).isFilePath)
|
||||
const filePathResults = await readFiles(filePaths, options, userArgs)
|
||||
|
||||
// stitch them back into the collection
|
||||
filePathResults.forEach((filePathResult) => {
|
||||
filesCollection[filePathResult.index] = _.pick(filePathResult, 'contents', 'fileName', 'mimeType', 'lastModified')
|
||||
})
|
||||
|
||||
return filesCollection as Cypress.FileReferenceObject[]
|
||||
}
|
||||
|
||||
Commands.addAll({ prevSubject: 'element' }, {
|
||||
async selectFile (subject: JQuery<any>, files: Cypress.FileReference | Cypress.FileReference[], options: Partial<InternalSelectFileOptions>): Promise<JQuery> {
|
||||
const userArgs = trimUserArgs([files, _.isObject(options) ? { ...options } : undefined])
|
||||
|
||||
options = _.defaults({}, options, {
|
||||
action: 'select',
|
||||
log: true,
|
||||
@@ -287,8 +348,7 @@ export default (Commands, Cypress, cy, state, config) => {
|
||||
}
|
||||
|
||||
// Make sure files is an array even if the user only passed in one
|
||||
const filesArray = await Promise.all(([] as Cypress.FileReference[]).concat(files).map(parseFile(options)))
|
||||
|
||||
const filesArray = await collectFiles(files, options, userArgs)
|
||||
const subjectChain = cy.subjectChain()
|
||||
|
||||
// We verify actionability on the subject, rather than the eventTarget,
|
||||
|
||||
@@ -3,18 +3,24 @@ import Promise from 'bluebird'
|
||||
|
||||
import $errUtils from '../../cypress/error_utils'
|
||||
import type { Log } from '../../cypress/log'
|
||||
import { runPrivilegedCommand, trimUserArgs } from '../../util/privileged_channel'
|
||||
|
||||
interface InternalExecOptions extends Partial<Cypress.ExecOptions> {
|
||||
_log?: Log
|
||||
cmd?: string
|
||||
timeout: number
|
||||
}
|
||||
|
||||
export default (Commands, Cypress, cy) => {
|
||||
Commands.addAll({
|
||||
exec (cmd: string, userOptions: Partial<Cypress.ExecOptions> = {}) {
|
||||
exec (cmd: string, userOptions: Partial<Cypress.ExecOptions>) {
|
||||
const userArgs = trimUserArgs([cmd, userOptions])
|
||||
|
||||
userOptions = userOptions || {}
|
||||
|
||||
const options: InternalExecOptions = _.defaults({}, userOptions, {
|
||||
log: true,
|
||||
timeout: Cypress.config('execTimeout'),
|
||||
timeout: Cypress.config('execTimeout') as number,
|
||||
failOnNonZeroExit: true,
|
||||
env: {},
|
||||
})
|
||||
@@ -46,7 +52,13 @@ export default (Commands, Cypress, cy) => {
|
||||
// because we're handling timeouts ourselves
|
||||
cy.clearTimeout()
|
||||
|
||||
return Cypress.backend('exec', _.pick(options, 'cmd', 'timeout', 'env'))
|
||||
return runPrivilegedCommand({
|
||||
commandName: 'exec',
|
||||
cy,
|
||||
Cypress: (Cypress as unknown) as InternalCypress.Cypress,
|
||||
options: _.pick(options, 'cmd', 'timeout', 'env'),
|
||||
userArgs,
|
||||
})
|
||||
.timeout(options.timeout)
|
||||
.then((result) => {
|
||||
if (options._log) {
|
||||
@@ -75,20 +87,26 @@ export default (Commands, Cypress, cy) => {
|
||||
})
|
||||
})
|
||||
.catch(Promise.TimeoutError, { timedOut: true }, () => {
|
||||
return $errUtils.throwErrByPath('exec.timed_out', {
|
||||
$errUtils.throwErrByPath('exec.timed_out', {
|
||||
onFail: options._log,
|
||||
args: { cmd, timeout: options.timeout },
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch((err) => {
|
||||
// re-throw if timedOut error from above
|
||||
if (error.name === 'CypressError') {
|
||||
throw error
|
||||
if (err.name === 'CypressError') {
|
||||
throw err
|
||||
}
|
||||
|
||||
return $errUtils.throwErrByPath('exec.failed', {
|
||||
if (err.isNonSpec) {
|
||||
$errUtils.throwErrByPath('miscellaneous.non_spec_invocation', {
|
||||
args: { cmd: 'exec' },
|
||||
})
|
||||
}
|
||||
|
||||
$errUtils.throwErrByPath('exec.failed', {
|
||||
onFail: options._log,
|
||||
args: { cmd, error },
|
||||
args: { cmd, error: err },
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
@@ -3,24 +3,34 @@ import { basename } from 'path'
|
||||
|
||||
import $errUtils from '../../cypress/error_utils'
|
||||
import type { Log } from '../../cypress/log'
|
||||
import { runPrivilegedCommand, trimUserArgs } from '../../util/privileged_channel'
|
||||
|
||||
interface InternalReadFileOptions extends Partial<Cypress.Loggable & Cypress.Timeoutable> {
|
||||
_log?: Log
|
||||
encoding: Cypress.Encodings
|
||||
timeout: number
|
||||
}
|
||||
|
||||
interface InternalWriteFileOptions extends Partial<Cypress.WriteFileOptions & Cypress.Timeoutable> {
|
||||
_log?: Log
|
||||
timeout: number
|
||||
}
|
||||
|
||||
type ReadFileOptions = Partial<Cypress.Loggable & Cypress.Timeoutable>
|
||||
type WriteFileOptions = Partial<Cypress.WriteFileOptions & Cypress.Timeoutable>
|
||||
|
||||
export default (Commands, Cypress, cy, state) => {
|
||||
Commands.addAll({
|
||||
readFile (file, encoding, userOptions: Partial<Cypress.Loggable & Cypress.Timeoutable> = {}) {
|
||||
readFile (file: string, encoding: Cypress.Encodings | ReadFileOptions | undefined, userOptions?: ReadFileOptions) {
|
||||
const userArgs = trimUserArgs([file, encoding, _.isObject(userOptions) ? { ...userOptions } : undefined])
|
||||
|
||||
if (_.isObject(encoding)) {
|
||||
userOptions = encoding
|
||||
encoding = undefined
|
||||
}
|
||||
|
||||
userOptions = userOptions || {}
|
||||
|
||||
const options: InternalReadFileOptions = _.defaults({}, userOptions, {
|
||||
// https://github.com/cypress-io/cypress/issues/1558
|
||||
// If no encoding is specified, then Cypress has historically defaulted
|
||||
@@ -29,7 +39,7 @@ export default (Commands, Cypress, cy, state) => {
|
||||
// to restore the default node behavior.
|
||||
encoding: encoding === undefined ? 'utf8' : encoding,
|
||||
log: true,
|
||||
timeout: Cypress.config('defaultCommandTimeout'),
|
||||
timeout: Cypress.config('defaultCommandTimeout') as number,
|
||||
})
|
||||
|
||||
const consoleProps = {}
|
||||
@@ -56,18 +66,34 @@ export default (Commands, Cypress, cy, state) => {
|
||||
cy.clearTimeout()
|
||||
|
||||
const verifyAssertions = () => {
|
||||
return Cypress.backend('read:file', file, _.pick(options, 'encoding')).timeout(options.timeout)
|
||||
return runPrivilegedCommand({
|
||||
commandName: 'readFile',
|
||||
cy,
|
||||
Cypress: (Cypress as unknown) as InternalCypress.Cypress,
|
||||
options: {
|
||||
file,
|
||||
encoding: options.encoding,
|
||||
},
|
||||
userArgs,
|
||||
})
|
||||
.timeout(options.timeout)
|
||||
.catch((err) => {
|
||||
if (err.name === 'TimeoutError') {
|
||||
return $errUtils.throwErrByPath('files.timed_out', {
|
||||
$errUtils.throwErrByPath('files.timed_out', {
|
||||
onFail: options._log,
|
||||
args: { cmd: 'readFile', file, timeout: options.timeout },
|
||||
})
|
||||
}
|
||||
|
||||
if (err.isNonSpec) {
|
||||
$errUtils.throwErrByPath('miscellaneous.non_spec_invocation', {
|
||||
args: { cmd: 'readFile' },
|
||||
})
|
||||
}
|
||||
|
||||
// Non-ENOENT errors are not retried
|
||||
if (err.code !== 'ENOENT') {
|
||||
return $errUtils.throwErrByPath('files.unexpected_error', {
|
||||
$errUtils.throwErrByPath('files.unexpected_error', {
|
||||
onFail: options._log,
|
||||
args: { cmd: 'readFile', action: 'read', file, filePath: err.filePath, error: err.message },
|
||||
})
|
||||
@@ -116,12 +142,16 @@ export default (Commands, Cypress, cy, state) => {
|
||||
return verifyAssertions()
|
||||
},
|
||||
|
||||
writeFile (fileName, contents, encoding, userOptions: Partial<Cypress.WriteFileOptions & Cypress.Timeoutable> = {}) {
|
||||
writeFile (fileName: string, contents: string, encoding: Cypress.Encodings | WriteFileOptions | undefined, userOptions: WriteFileOptions) {
|
||||
const userArgs = trimUserArgs([fileName, contents, encoding, _.isObject(userOptions) ? { ...userOptions } : undefined])
|
||||
|
||||
if (_.isObject(encoding)) {
|
||||
userOptions = encoding
|
||||
encoding = undefined
|
||||
}
|
||||
|
||||
userOptions = userOptions || {}
|
||||
|
||||
const options: InternalWriteFileOptions = _.defaults({}, userOptions, {
|
||||
// https://github.com/cypress-io/cypress/issues/1558
|
||||
// If no encoding is specified, then Cypress has historically defaulted
|
||||
@@ -168,7 +198,19 @@ export default (Commands, Cypress, cy, state) => {
|
||||
// the timeout ourselves
|
||||
cy.clearTimeout()
|
||||
|
||||
return Cypress.backend('write:file', fileName, contents, _.pick(options, 'encoding', 'flag')).timeout(options.timeout)
|
||||
return runPrivilegedCommand({
|
||||
commandName: 'writeFile',
|
||||
cy,
|
||||
Cypress: (Cypress as unknown) as InternalCypress.Cypress,
|
||||
options: {
|
||||
fileName,
|
||||
contents,
|
||||
encoding: options.encoding,
|
||||
flag: options.flag,
|
||||
},
|
||||
userArgs,
|
||||
})
|
||||
.timeout(options.timeout)
|
||||
.then(({ filePath, contents }) => {
|
||||
consoleProps['File Path'] = filePath
|
||||
consoleProps['Contents'] = contents
|
||||
@@ -183,6 +225,12 @@ export default (Commands, Cypress, cy, state) => {
|
||||
})
|
||||
}
|
||||
|
||||
if (err.isNonSpec) {
|
||||
return $errUtils.throwErrByPath('miscellaneous.non_spec_invocation', {
|
||||
args: { cmd: 'writeFile' },
|
||||
})
|
||||
}
|
||||
|
||||
return $errUtils.throwErrByPath('files.unexpected_error', {
|
||||
onFail: options._log,
|
||||
args: { cmd: 'writeFile', action: 'write', file: fileName, filePath: err.filePath, error: err.message },
|
||||
|
||||
@@ -9,6 +9,7 @@ import { $Location } from '../../../cypress/location'
|
||||
import { LogUtils } from '../../../cypress/log'
|
||||
import logGroup from '../../logGroup'
|
||||
import type { StateFunc } from '../../../cypress/state'
|
||||
import { runPrivilegedCommand, trimUserArgs } from '../../../util/privileged_channel'
|
||||
|
||||
const reHttp = /^https?:\/\//
|
||||
|
||||
@@ -23,15 +24,32 @@ const normalizeOrigin = (urlOrDomain) => {
|
||||
return $Location.normalize(origin)
|
||||
}
|
||||
|
||||
type OptionsOrFn<T> = { args: T } | (() => {})
|
||||
type Fn<T> = (args?: T) => {}
|
||||
|
||||
function stringifyFn (fn?: any) {
|
||||
return _.isFunction(fn) ? fn.toString() : undefined
|
||||
}
|
||||
|
||||
function getUserArgs<T> (urlOrDomain: string, optionsOrFn: OptionsOrFn<T>, fn?: Fn<T>) {
|
||||
return trimUserArgs([
|
||||
urlOrDomain,
|
||||
fn && _.isObject(optionsOrFn) ? { ...optionsOrFn } : stringifyFn(optionsOrFn),
|
||||
fn ? stringifyFn(fn) : undefined,
|
||||
])
|
||||
}
|
||||
|
||||
export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: StateFunc, config: Cypress.InternalConfig) => {
|
||||
const communicator = Cypress.primaryOriginCommunicator
|
||||
|
||||
Commands.addAll({
|
||||
origin<T> (urlOrDomain: string, optionsOrFn: { args: T } | (() => {}), fn?: (args?: T) => {}) {
|
||||
origin<T> (urlOrDomain: string, optionsOrFn: OptionsOrFn<T>, fn?: Fn<T>) {
|
||||
if (Cypress.isBrowser('webkit')) {
|
||||
return $errUtils.throwErrByPath('webkit.origin')
|
||||
}
|
||||
|
||||
const userArgs = getUserArgs<T>(urlOrDomain, optionsOrFn, fn)
|
||||
|
||||
const userInvocationStack = state('current').get('userInvocationStack')
|
||||
|
||||
// store the invocation stack in the case that `cy.origin` errors
|
||||
@@ -185,9 +203,21 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State
|
||||
const fn = _.isFunction(callbackFn) ? callbackFn.toString() : callbackFn
|
||||
const file = $stackUtils.getSourceDetailsForFirstLine(userInvocationStack, config('projectRoot'))?.absoluteFile
|
||||
|
||||
// once the secondary origin page loads, send along the
|
||||
// user-specified callback to run in that origin
|
||||
try {
|
||||
// origin is a privileged command, meaning it has to be invoked
|
||||
// from the spec or support file
|
||||
await runPrivilegedCommand({
|
||||
commandName: 'origin',
|
||||
cy,
|
||||
Cypress: (Cypress as unknown) as InternalCypress.Cypress,
|
||||
options: {
|
||||
specBridgeOrigin,
|
||||
},
|
||||
userArgs,
|
||||
})
|
||||
|
||||
// once the secondary origin page loads, send along the
|
||||
// user-specified callback to run in that origin
|
||||
communicator.toSpecBridge(origin, 'run:origin:fn', {
|
||||
args: options?.args || undefined,
|
||||
fn,
|
||||
@@ -212,6 +242,12 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State
|
||||
logCounter: LogUtils.getCounter(),
|
||||
})
|
||||
} catch (err: any) {
|
||||
if (err.isNonSpec) {
|
||||
return _reject($errUtils.errByPath('miscellaneous.non_spec_invocation', {
|
||||
cmd: 'origin',
|
||||
}))
|
||||
}
|
||||
|
||||
const wrappedErr = $errUtils.errByPath('origin.run_origin_fn_errored', {
|
||||
error: err.message,
|
||||
})
|
||||
|
||||
@@ -5,17 +5,23 @@ import $utils from '../../cypress/utils'
|
||||
import $errUtils from '../../cypress/error_utils'
|
||||
import $stackUtils from '../../cypress/stack_utils'
|
||||
import type { Log } from '../../cypress/log'
|
||||
import { runPrivilegedCommand, trimUserArgs } from '../../util/privileged_channel'
|
||||
|
||||
interface InternalTaskOptions extends Partial<Cypress.Loggable & Cypress.Timeoutable> {
|
||||
_log?: Log
|
||||
timeout: number
|
||||
}
|
||||
|
||||
export default (Commands, Cypress, cy) => {
|
||||
Commands.addAll({
|
||||
task (task, arg, userOptions: Partial<Cypress.Loggable & Cypress.Timeoutable> = {}) {
|
||||
task (task, arg, userOptions: Partial<Cypress.Loggable & Cypress.Timeoutable>) {
|
||||
const userArgs = trimUserArgs([task, arg, _.isObject(userOptions) ? { ...userOptions } : undefined])
|
||||
|
||||
userOptions = userOptions || {}
|
||||
|
||||
const options: InternalTaskOptions = _.defaults({}, userOptions, {
|
||||
log: true,
|
||||
timeout: Cypress.config('taskTimeout'),
|
||||
timeout: Cypress.config('taskTimeout') as number,
|
||||
})
|
||||
|
||||
let consoleOutput
|
||||
@@ -52,10 +58,16 @@ export default (Commands, Cypress, cy) => {
|
||||
// because we're handling timeouts ourselves
|
||||
cy.clearTimeout()
|
||||
|
||||
return Cypress.backend('task', {
|
||||
task,
|
||||
arg,
|
||||
timeout: options.timeout,
|
||||
return runPrivilegedCommand({
|
||||
commandName: 'task',
|
||||
cy,
|
||||
Cypress: (Cypress as unknown) as InternalCypress.Cypress,
|
||||
userArgs,
|
||||
options: {
|
||||
task,
|
||||
arg,
|
||||
timeout: options.timeout,
|
||||
},
|
||||
})
|
||||
.timeout(options.timeout)
|
||||
.then((result) => {
|
||||
@@ -71,7 +83,7 @@ export default (Commands, Cypress, cy) => {
|
||||
args: { task, timeout: options.timeout },
|
||||
})
|
||||
})
|
||||
.catch({ timedOut: true }, (error) => {
|
||||
.catch({ timedOut: true }, (error: any) => {
|
||||
$errUtils.throwErrByPath('task.server_timed_out', {
|
||||
onFail: options._log,
|
||||
args: { task, timeout: options.timeout, error: error.message },
|
||||
@@ -83,6 +95,12 @@ export default (Commands, Cypress, cy) => {
|
||||
throw err
|
||||
}
|
||||
|
||||
if (err.isNonSpec) {
|
||||
$errUtils.throwErrByPath('miscellaneous.non_spec_invocation', {
|
||||
args: { cmd: 'task' },
|
||||
})
|
||||
}
|
||||
|
||||
err.stack = $stackUtils.normalizedStack(err)
|
||||
|
||||
if (err?.isKnownError) {
|
||||
|
||||
@@ -45,6 +45,7 @@ import { setupAutEventHandlers } from './cypress/aut_event_handlers'
|
||||
|
||||
import type { CachedTestState } from '@packages/types'
|
||||
import * as cors from '@packages/network/lib/cors'
|
||||
import { setSpecContentSecurityPolicy } from './util/privileged_channel'
|
||||
|
||||
import { telemetry } from '@packages/telemetry/src/browser'
|
||||
|
||||
@@ -56,6 +57,8 @@ declare global {
|
||||
Cypress: Cypress.Cypress
|
||||
Runner: any
|
||||
cy: Cypress.cy
|
||||
// eval doesn't exist on the built-in Window type for some reason
|
||||
eval (expression: string): any
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,7 +347,17 @@ class $Cypress {
|
||||
|
||||
this.events.proxyTo(this.cy)
|
||||
|
||||
$scriptUtils.runScripts(specWindow, scripts)
|
||||
$scriptUtils.runScripts({
|
||||
browser: this.config('browser'),
|
||||
scripts,
|
||||
specWindow,
|
||||
testingType: this.testingType,
|
||||
})
|
||||
.then(() => {
|
||||
if (this.testingType === 'e2e') {
|
||||
return setSpecContentSecurityPolicy(specWindow)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.runner.onSpecError('error')({ error })
|
||||
})
|
||||
|
||||
@@ -20,13 +20,15 @@ export class $Chainer {
|
||||
|
||||
static add (key, fn) {
|
||||
$Chainer.prototype[key] = function (...args) {
|
||||
const verificationPromise = Cypress.emitMap('command:invocation', { name: key, args })
|
||||
|
||||
const userInvocationStack = $stackUtils.normalizedUserInvocationStack(
|
||||
(new this.specWindow.Error('command invocation stack')).stack,
|
||||
)
|
||||
|
||||
// call back the original function with our new args
|
||||
// pass args an as array and not a destructured invocation
|
||||
fn(this, userInvocationStack, args)
|
||||
fn(this, userInvocationStack, args, verificationPromise)
|
||||
|
||||
// return the chainer so additional calls
|
||||
// are slurped up by the chainer instead of cy
|
||||
|
||||
@@ -683,7 +683,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
|
||||
const cyFn = wrap(true)
|
||||
const chainerFn = wrap(false)
|
||||
|
||||
const callback = (chainer, userInvocationStack, args, firstCall = false) => {
|
||||
const callback = (chainer, userInvocationStack, args, verificationPromise, firstCall = false) => {
|
||||
// dont enqueue / inject any new commands if
|
||||
// onInjectCommand returns false
|
||||
const onInjectCommand = cy.state('onInjectCommand')
|
||||
@@ -699,6 +699,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
|
||||
chainerId: chainer.chainerId,
|
||||
userInvocationStack,
|
||||
fn: firstCall ? cyFn : chainerFn,
|
||||
verificationPromise,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -707,6 +708,15 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
|
||||
cy[name] = function (...args) {
|
||||
ensureRunnable(cy, name)
|
||||
|
||||
// for privileged commands, we send a message to the server that verifies
|
||||
// them as coming from the spec. the fulfillment of this promise means
|
||||
// the message was received. the implementation for those commands
|
||||
// checks to make sure this promise is fulfilled before sending its
|
||||
// websocket message for running the command to ensure prevent a race
|
||||
// condition where running the command happens before the command is
|
||||
// verified
|
||||
const verificationPromise = Cypress.emitMap('command:invocation', { name, args })
|
||||
|
||||
// this is the first call on cypress
|
||||
// so create a new chainer instance
|
||||
const chainer = new $Chainer(cy.specWindow)
|
||||
@@ -717,7 +727,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
|
||||
|
||||
const userInvocationStack = $stackUtils.captureUserInvocationStack(cy.specWindow.Error)
|
||||
|
||||
callback(chainer, userInvocationStack, args, true)
|
||||
callback(chainer, userInvocationStack, args, verificationPromise, true)
|
||||
|
||||
// if we are in the middle of a command
|
||||
// and its return value is a promise
|
||||
|
||||
@@ -747,6 +747,7 @@ export default {
|
||||
},
|
||||
|
||||
miscellaneous: {
|
||||
non_spec_invocation: `${cmd('{{cmd}}')} must only be invoked from the spec file or support file.`,
|
||||
returned_value_and_commands_from_custom_command (obj) {
|
||||
return {
|
||||
message: stripIndent`\
|
||||
|
||||
@@ -42,9 +42,38 @@ const runScriptsFromUrls = (specWindow, scripts) => {
|
||||
.then((scripts) => evalScripts(specWindow, scripts))
|
||||
}
|
||||
|
||||
const appendScripts = (specWindow, scripts) => {
|
||||
return Bluebird.each(scripts, (script: any) => {
|
||||
const firstScript = specWindow.document.querySelector('script')
|
||||
const specScript = specWindow.document.createElement('script')
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
specScript.addEventListener('load', () => {
|
||||
resolve()
|
||||
})
|
||||
|
||||
specScript.src = script.relativeUrl
|
||||
firstScript.after(specScript)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
interface Script {
|
||||
absolute: string
|
||||
relative: string
|
||||
relativeUrl: string
|
||||
}
|
||||
|
||||
interface RunScriptsOptions {
|
||||
browser: Cypress.Browser
|
||||
scripts: Script[]
|
||||
specWindow: Window
|
||||
testingType: Cypress.TestingType
|
||||
}
|
||||
|
||||
// Supports either scripts as objects or as async import functions
|
||||
export default {
|
||||
runScripts: (specWindow, scripts) => {
|
||||
runScripts: ({ browser, scripts, specWindow, testingType }: RunScriptsOptions) => {
|
||||
// if scripts contains at least one promise
|
||||
if (scripts.length && typeof scripts[0] === 'function') {
|
||||
// chain the loading promises
|
||||
@@ -54,6 +83,15 @@ export default {
|
||||
return Bluebird.each(scripts, (script: any) => script())
|
||||
}
|
||||
|
||||
// in webkit, stack traces for e2e are made pretty much useless if these
|
||||
// scripts are eval'd, so we append them as script tags instead
|
||||
if (browser.family === 'webkit' && testingType === 'e2e') {
|
||||
return appendScripts(specWindow, scripts)
|
||||
}
|
||||
|
||||
// for other browsers, we get the contents of the scripts so that we can
|
||||
// extract and utilize the source maps for better errors and code frames.
|
||||
// we then eval the script contents to run them
|
||||
return runScriptsFromUrls(specWindow, scripts)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import _ from 'lodash'
|
||||
import Bluebird from 'bluebird'
|
||||
|
||||
/**
|
||||
* prevents further scripts outside of our own and the spec itself from being
|
||||
* run in the spec frame
|
||||
* @param specWindow: Window
|
||||
*/
|
||||
export function setSpecContentSecurityPolicy (specWindow) {
|
||||
const metaEl = specWindow.document.createElement('meta')
|
||||
|
||||
metaEl.setAttribute('http-equiv', 'Content-Security-Policy')
|
||||
metaEl.setAttribute('content', `script-src 'unsafe-eval'`)
|
||||
specWindow.document.querySelector('head')!.appendChild(metaEl)
|
||||
}
|
||||
|
||||
interface RunPrivilegedCommandOptions {
|
||||
commandName: string
|
||||
cy: Cypress.cy
|
||||
Cypress: InternalCypress.Cypress
|
||||
options: any
|
||||
userArgs: any[]
|
||||
}
|
||||
|
||||
export function runPrivilegedCommand ({ commandName, cy, Cypress, options, userArgs }: RunPrivilegedCommandOptions): Bluebird<any> {
|
||||
return Bluebird.try(() => {
|
||||
return cy.state('current').get('verificationPromise')[0]
|
||||
})
|
||||
.then(() => {
|
||||
return Cypress.backend('run:privileged', {
|
||||
commandName,
|
||||
options,
|
||||
userArgs,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function trimUserArgs (args: any[]) {
|
||||
return _.dropRightWhile(args, _.isUndefined)
|
||||
}
|
||||
+3
-1
@@ -68,7 +68,9 @@ declare namespace Cypress {
|
||||
}
|
||||
|
||||
declare namespace InternalCypress {
|
||||
interface Cypress extends Cypress.Cypress, NodeEventEmitter {}
|
||||
interface Cypress extends Cypress.Cypress, NodeEventEmitter {
|
||||
backend: (eventName: string, ...args: any[]) => Promise<any>
|
||||
}
|
||||
|
||||
interface LocalStorage extends Cypress.LocalStorage {
|
||||
setStorages: (local, remote) => LocalStorage
|
||||
|
||||
+2
@@ -5,5 +5,7 @@ declare namespace Cypress {
|
||||
originLoadUtils(origin: string): Chainable
|
||||
getAll(...aliases: string[]): Chainable
|
||||
shouldWithTimeout(cb: (subj: {}) => void, timeout?: number): Chainable
|
||||
runSpecFileCustomPrivilegedCommands(): Chainable
|
||||
runSupportFileCustomPrivilegedCommands(): Chainable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,46 +5,54 @@ const debug = require('debug')('cypress:server:controllers')
|
||||
const { escapeFilenameInUrl } = require('../util/escape_filename')
|
||||
const { getCtx } = require('@packages/data-context')
|
||||
const { cors } = require('@packages/network')
|
||||
const { privilegedCommandsManager } = require('../privileged-commands/privileged-commands-manager')
|
||||
|
||||
module.exports = {
|
||||
|
||||
handleIframe (req, res, config, remoteStates, extraOptions) {
|
||||
async handleIframe (req, res, config, remoteStates, extraOptions) {
|
||||
const test = req.params[0]
|
||||
const iframePath = cwd('lib', 'html', 'iframe.html')
|
||||
const specFilter = _.get(extraOptions, 'specFilter')
|
||||
|
||||
debug('handle iframe %o', { test, specFilter })
|
||||
|
||||
return this.getSpecs(test, config, extraOptions)
|
||||
.then((specs) => {
|
||||
const supportFileJs = this.getSupportFile(config)
|
||||
const allFilesToSend = specs
|
||||
const specs = await this.getSpecs(test, config, extraOptions)
|
||||
const supportFileJs = this.getSupportFile(config)
|
||||
const allFilesToSend = specs
|
||||
|
||||
if (supportFileJs) {
|
||||
allFilesToSend.unshift(supportFileJs)
|
||||
}
|
||||
if (supportFileJs) {
|
||||
allFilesToSend.unshift(supportFileJs)
|
||||
}
|
||||
|
||||
debug('all files to send %o', _.map(allFilesToSend, 'relative'))
|
||||
debug('all files to send %o', _.map(allFilesToSend, 'relative'))
|
||||
|
||||
const superDomain = cors.shouldInjectDocumentDomain(req.proxiedUrl, {
|
||||
skipDomainInjectionForDomains: config.experimentalSkipDomainInjection,
|
||||
}) ?
|
||||
remoteStates.getPrimary().domainName :
|
||||
undefined
|
||||
const superDomain = cors.shouldInjectDocumentDomain(req.proxiedUrl, {
|
||||
skipDomainInjectionForDomains: config.experimentalSkipDomainInjection,
|
||||
}) ?
|
||||
remoteStates.getPrimary().domainName :
|
||||
undefined
|
||||
|
||||
const iframeOptions = {
|
||||
superDomain,
|
||||
title: this.getTitle(test),
|
||||
scripts: JSON.stringify(allFilesToSend),
|
||||
}
|
||||
|
||||
debug('iframe %s options %o', test, iframeOptions)
|
||||
|
||||
return res.render(iframePath, iframeOptions)
|
||||
const privilegedChannel = await privilegedCommandsManager.getPrivilegedChannel({
|
||||
browserFamily: req.query.browserFamily,
|
||||
isSpecBridge: false,
|
||||
namespace: config.namespace,
|
||||
scripts: allFilesToSend,
|
||||
url: req.proxiedUrl,
|
||||
})
|
||||
|
||||
const iframeOptions = {
|
||||
superDomain,
|
||||
title: this.getTitle(test),
|
||||
scripts: JSON.stringify(allFilesToSend),
|
||||
privilegedChannel,
|
||||
}
|
||||
|
||||
debug('iframe %s options %o', test, iframeOptions)
|
||||
|
||||
res.render(iframePath, iframeOptions)
|
||||
},
|
||||
|
||||
handleCrossOriginIframe (req, res, config) {
|
||||
async handleCrossOriginIframe (req, res, config) {
|
||||
const iframePath = cwd('lib', 'html', 'spec-bridge-iframe.html')
|
||||
const superDomain = cors.shouldInjectDocumentDomain(req.proxiedUrl, {
|
||||
skipDomainInjectionForDomains: config.experimentalSkipDomainInjection,
|
||||
@@ -54,10 +62,19 @@ module.exports = {
|
||||
|
||||
const origin = cors.getOrigin(req.proxiedUrl)
|
||||
|
||||
const privilegedChannel = await privilegedCommandsManager.getPrivilegedChannel({
|
||||
browserFamily: req.query.browserFamily,
|
||||
isSpecBridge: true,
|
||||
namespace: config.namespace,
|
||||
scripts: [],
|
||||
url: req.proxiedUrl,
|
||||
})
|
||||
|
||||
const iframeOptions = {
|
||||
superDomain,
|
||||
title: `Cypress for ${origin}`,
|
||||
namespace: config.namespace,
|
||||
privilegedChannel,
|
||||
}
|
||||
|
||||
debug('cross origin iframe with options %o', iframeOptions)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
const Bluebird = require('bluebird')
|
||||
const path = require('path')
|
||||
const { fs } = require('./util/fs')
|
||||
|
||||
module.exports = {
|
||||
readFile (projectRoot, file, options = {}) {
|
||||
const filePath = path.resolve(projectRoot, file)
|
||||
readFile (projectRoot, options = {}) {
|
||||
const filePath = path.resolve(projectRoot, options.file)
|
||||
const readFn = (path.extname(filePath) === '.json' && options.encoding !== null) ? fs.readJsonAsync : fs.readFileAsync
|
||||
|
||||
// https://github.com/cypress-io/cypress/issues/1558
|
||||
@@ -19,22 +20,39 @@ module.exports = {
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
err.originalFilePath = options.file
|
||||
err.filePath = filePath
|
||||
throw err
|
||||
})
|
||||
},
|
||||
|
||||
writeFile (projectRoot, file, contents, options = {}) {
|
||||
const filePath = path.resolve(projectRoot, file)
|
||||
readFiles (projectRoot, options = {}) {
|
||||
return Bluebird.map(options.files, (file) => {
|
||||
return this.readFile(projectRoot, {
|
||||
file: file.path,
|
||||
encoding: file.encoding,
|
||||
})
|
||||
.then(({ contents, filePath }) => {
|
||||
return {
|
||||
...file,
|
||||
filePath,
|
||||
contents,
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
writeFile (projectRoot, options = {}) {
|
||||
const filePath = path.resolve(projectRoot, options.fileName)
|
||||
const writeOptions = {
|
||||
encoding: options.encoding === undefined ? 'utf8' : options.encoding,
|
||||
flag: options.flag || 'w',
|
||||
}
|
||||
|
||||
return fs.outputFile(filePath, contents, writeOptions)
|
||||
return fs.outputFile(filePath, options.contents, writeOptions)
|
||||
.then(() => {
|
||||
return {
|
||||
contents,
|
||||
contents: options.contents,
|
||||
filePath,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,18 +5,21 @@
|
||||
<title>{{title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
{{if(options.superDomain)}}
|
||||
document.domain = '{{superDomain}}';
|
||||
{{/if}}
|
||||
<script>
|
||||
{{if(options.superDomain)}}
|
||||
document.domain = '{{superDomain}}';
|
||||
{{/if}}
|
||||
|
||||
(function(parent) {
|
||||
var Cypress = window.Cypress = parent.Cypress;
|
||||
if (!Cypress) {
|
||||
throw new Error("Tests cannot run without a reference to Cypress!");
|
||||
}
|
||||
return Cypress.onSpecWindow(window, {{scripts | safe}});
|
||||
})(window.opener || window.parent);
|
||||
</script>
|
||||
<script>{{privilegedChannel | safe}}</script>
|
||||
<script>
|
||||
window.Cypress.onSpecWindow(window, {{scripts | safe}});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
<title>{{title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
{{if(options.superDomain)}}
|
||||
document.domain = '{{superDomain}}';
|
||||
{{/if}}
|
||||
<script>
|
||||
{{if(options.superDomain)}}
|
||||
document.domain = '{{superDomain}}';
|
||||
{{/if}}
|
||||
</script>
|
||||
<script src="/{{namespace}}/runner/cypress_cross_origin_runner.js"></script>
|
||||
<script>{{privilegedChannel | safe}}</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/* global window */
|
||||
(({ browserFamily, isSpecBridge, key, namespace, scripts, url, win = window }) => {
|
||||
/**
|
||||
* This file is read as a string in the server and injected into the spec
|
||||
* frame in order to create a privileged channel between the server and
|
||||
* the spec frame. The values above are provided by the server, with the
|
||||
* `key` being particularly important since it is used to validate
|
||||
* any messages sent from this channel back to the server.
|
||||
*
|
||||
* This file does not get preprocessed, so it should not contain syntax that
|
||||
* our minimum supported browsers do not support.
|
||||
*/
|
||||
|
||||
const Err = win.Error
|
||||
const captureStackTrace = win.Error.captureStackTrace
|
||||
const filter = win.Array.prototype.filter
|
||||
const arrayIncludes = win.Array.prototype.includes
|
||||
const map = win.Array.prototype.map
|
||||
const stringIncludes = win.String.prototype.includes
|
||||
const replace = win.String.prototype.replace
|
||||
const split = win.String.prototype.split
|
||||
const functionToString = win.Function.prototype.toString
|
||||
const fetch = win.fetch
|
||||
const parse = win.JSON.parse
|
||||
const stringify = win.JSON.stringify
|
||||
|
||||
const queryStringRegex = /\?.*$/
|
||||
|
||||
// since this function is eval'd, the scripts are included as stringified JSON
|
||||
if (scripts) {
|
||||
scripts = parse(scripts)
|
||||
}
|
||||
|
||||
// when privileged commands are called within the cy.origin() callback,
|
||||
// since the callback is eval'd in the spec bridge instead of being run
|
||||
// directly in the spec frame, we need to use different criteria, namely
|
||||
// that the stack includes the function where we eval the callback
|
||||
const hasSpecBridgeInvocation = (err) => {
|
||||
switch (browserFamily) {
|
||||
case 'chromium':
|
||||
return stringIncludes.call(err.stack, 'at invokeOriginFn')
|
||||
case 'firefox':
|
||||
return stringIncludes.call(err.stack, 'invokeOriginFn@')
|
||||
// currently, this won't run in webkit since it doesn't
|
||||
// support cy.origin()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// in chromium, stacks only include lines from the frame where the error is
|
||||
// created, so to validate a function call was from the spec frame, we strip
|
||||
// message lines and any eval calls (since they could be invoked from outside
|
||||
// the spec frame) and if there are lines left, they must have been from
|
||||
// the spec frame itself
|
||||
const hasSpecFrameStackLines = (err) => {
|
||||
const stackLines = split.call(err.stack, '\n')
|
||||
const filteredLines = filter.call(stackLines, (line) => {
|
||||
return (
|
||||
!stringIncludes.call(line, err.message)
|
||||
&& !stringIncludes.call(line, 'eval at <anonymous>')
|
||||
)
|
||||
})
|
||||
|
||||
return filteredLines.length > 0
|
||||
}
|
||||
|
||||
// in non-chromium browsers, the stack will include either the spec file url
|
||||
// or the support file
|
||||
const hasStackLinesFromSpecOrSupportFile = (err) => {
|
||||
return filter.call(scripts, (script) => {
|
||||
// in webkit, stack line might not include the query string
|
||||
if (browserFamily === 'webkit') {
|
||||
script = replace.call(script, queryStringRegex, '')
|
||||
}
|
||||
|
||||
return stringIncludes.call(err.stack, script)
|
||||
}).length > 0
|
||||
}
|
||||
|
||||
// privileged commands are commands that should only be called from the spec
|
||||
// because they escape the browser sandbox and (generally) have access to node
|
||||
const privilegedCommands = [
|
||||
'exec',
|
||||
// cy.origin() doesn't directly access node, but is a pathway for other
|
||||
// commands to do so
|
||||
'origin',
|
||||
'readFile',
|
||||
// cy.selectFile() accesses node when using the path argument to read a file
|
||||
'selectFile',
|
||||
'writeFile',
|
||||
'task',
|
||||
]
|
||||
|
||||
function stackIsFromSpecFrame (err) {
|
||||
if (isSpecBridge) {
|
||||
return hasSpecBridgeInvocation(err)
|
||||
}
|
||||
|
||||
if (browserFamily === 'chromium') {
|
||||
return hasSpecFrameStackLines(err)
|
||||
}
|
||||
|
||||
return hasStackLinesFromSpecOrSupportFile(err)
|
||||
}
|
||||
|
||||
async function onCommandInvocation (command) {
|
||||
if (!arrayIncludes.call(privilegedCommands, command.name)) return
|
||||
|
||||
// message doesn't really matter since we're only interested in the stack
|
||||
const err = new Err('command stack error')
|
||||
|
||||
// strips the stack for this function itself, so we get a more accurate
|
||||
// look at where the command was called from
|
||||
if (captureStackTrace) {
|
||||
captureStackTrace.call(Err, err, onCommandInvocation)
|
||||
}
|
||||
|
||||
// if stack is not validated as being from the spec frame, don't add
|
||||
// it as a verified command
|
||||
if (!stackIsFromSpecFrame(err)) return
|
||||
|
||||
const args = map.call([...command.args], (arg) => {
|
||||
if (typeof arg === 'function') {
|
||||
return functionToString.call(arg)
|
||||
}
|
||||
|
||||
return arg
|
||||
})
|
||||
|
||||
// if we verify a privileged command was invoked from the spec frame, we
|
||||
// send it to the server, where it's stored in state. when the command is
|
||||
// run and it sends its message to the server via websocket, we check
|
||||
// that verified status before allowing the command to continue running
|
||||
await fetch(`/${namespace}/add-verified-command`, {
|
||||
body: stringify({
|
||||
args,
|
||||
name: command.name,
|
||||
key,
|
||||
url,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
}).catch(() => {
|
||||
// this erroring is unlikely, but it's fine to ignore. if adding the
|
||||
// verified command failed, the default behavior is NOT to allow
|
||||
// the privileged command to run
|
||||
})
|
||||
}
|
||||
|
||||
win.Cypress.on('command:invocation', onCommandInvocation)
|
||||
|
||||
// returned for testing purposes only
|
||||
return {
|
||||
onCommandInvocation,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,126 @@
|
||||
import _ from 'lodash'
|
||||
import os from 'os'
|
||||
import path from 'path'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import exec from '../exec'
|
||||
import files from '../files'
|
||||
import { fs } from '../util/fs'
|
||||
import task from '../task'
|
||||
|
||||
export interface SpecChannelOptions {
|
||||
isSpecBridge: boolean
|
||||
url: string
|
||||
key: string
|
||||
}
|
||||
|
||||
interface SpecOriginatedCommand {
|
||||
name: string
|
||||
args: any[]
|
||||
}
|
||||
|
||||
type NonSpecError = Error & { isNonSpec: boolean | undefined }
|
||||
type ChannelUrl = string
|
||||
type ChannelKey = string
|
||||
|
||||
class PrivilegedCommandsManager {
|
||||
channelKeys: Record<ChannelUrl, ChannelKey> = {}
|
||||
verifiedCommands: SpecOriginatedCommand[] = []
|
||||
|
||||
async getPrivilegedChannel (options) {
|
||||
// setting up a non-spec bridge channel means the beginning of running
|
||||
// a spec and is a signal that we should reset state
|
||||
if (!options.isSpecBridge) {
|
||||
this.reset()
|
||||
}
|
||||
|
||||
// no-op if already set up for url
|
||||
if (this.channelKeys[options.url]) return
|
||||
|
||||
const key = uuidv4()
|
||||
|
||||
this.channelKeys[options.url] = key
|
||||
|
||||
const script = (await fs.readFileAsync(path.join(__dirname, 'privileged-channel.js'))).toString()
|
||||
const specScripts = JSON.stringify(options.scripts.map(({ relativeUrl }) => {
|
||||
if (os.platform() === 'win32') {
|
||||
return relativeUrl.replaceAll('\\', '\\\\')
|
||||
}
|
||||
|
||||
return relativeUrl
|
||||
}))
|
||||
|
||||
return `${script}({
|
||||
browserFamily: '${options.browserFamily}',
|
||||
isSpecBridge: ${options.isSpecBridge || 'false'},
|
||||
key: '${key}',
|
||||
namespace: '${options.namespace}',
|
||||
scripts: '${specScripts}',
|
||||
url: '${options.url}'
|
||||
})`
|
||||
}
|
||||
|
||||
addVerifiedCommand ({ args, name, key, url }) {
|
||||
// if the key isn't valid, don't add it as a verified command. once the
|
||||
// command attempts to run, it will fail at that point
|
||||
if (key !== this.channelKeys[url]) return
|
||||
|
||||
this.verifiedCommands.push({ name, args })
|
||||
}
|
||||
|
||||
// finds and returns matching command from the verified commands array. it
|
||||
// also removes that command from the verified commands array
|
||||
hasVerifiedCommand (command) {
|
||||
const matchingCommand = _.find(this.verifiedCommands, ({ name, args }) => {
|
||||
return command.name === name && _.isEqual(command.args, _.dropRightWhile(args, _.isUndefined))
|
||||
})
|
||||
|
||||
return !!matchingCommand
|
||||
}
|
||||
|
||||
runPrivilegedCommand (config, { commandName, options, userArgs }) {
|
||||
// the presence of the command within the verifiedCommands array indicates
|
||||
// the command being run is verified
|
||||
const hasCommand = this.hasVerifiedCommand({ name: commandName, args: userArgs })
|
||||
|
||||
if (config.testingType === 'e2e' && !hasCommand) {
|
||||
// this error message doesn't really matter as each command will catch it
|
||||
// in the driver based on err.isNonSpec and throw a different error
|
||||
const err = new Error(`cy.${commandName}() must be invoked from the spec file or support file`) as NonSpecError
|
||||
|
||||
err.isNonSpec = true
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
switch (commandName) {
|
||||
case 'exec':
|
||||
return exec.run(config.projectRoot, options)
|
||||
case 'origin':
|
||||
// only need to verify that it's spec-originated above
|
||||
return
|
||||
case 'readFile':
|
||||
return files.readFile(config.projectRoot, options)
|
||||
case 'selectFile':
|
||||
return files.readFiles(config.projectRoot, options)
|
||||
case 'writeFile':
|
||||
return files.writeFile(config.projectRoot, options)
|
||||
case 'task': {
|
||||
const configFile = config.configFile && config.configFile.includes(config.projectRoot)
|
||||
? config.configFile
|
||||
: path.join(config.projectRoot, config.configFile)
|
||||
|
||||
return task.run(configFile ?? null, options)
|
||||
}
|
||||
default:
|
||||
throw new Error(`You requested a secure backend event for a command we cannot handle: ${commandName}`)
|
||||
}
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.channelKeys = {}
|
||||
this.verifiedCommands = []
|
||||
}
|
||||
}
|
||||
|
||||
export const privilegedCommandsManager = new PrivilegedCommandsManager()
|
||||
@@ -1,7 +1,6 @@
|
||||
import bodyParser from 'body-parser'
|
||||
import Debug from 'debug'
|
||||
import { Router } from 'express'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
|
||||
import AppData from './util/app_data'
|
||||
@@ -12,6 +11,7 @@ import client from './controllers/client'
|
||||
import files from './controllers/files'
|
||||
import type { InitializeRoutes } from './routes'
|
||||
import * as plugins from './plugins'
|
||||
import { privilegedCommandsManager } from './privileged-commands/privileged-commands-manager'
|
||||
|
||||
const debug = Debug('cypress:server:routes-e2e')
|
||||
|
||||
@@ -19,7 +19,6 @@ export const createRoutesE2E = ({
|
||||
config,
|
||||
networkProxy,
|
||||
onError,
|
||||
getSpec,
|
||||
}: InitializeRoutes) => {
|
||||
const routesE2E = Router()
|
||||
|
||||
@@ -32,26 +31,6 @@ export const createRoutesE2E = ({
|
||||
specController.handle(test, req, res, config, next, onError)
|
||||
})
|
||||
|
||||
routesE2E.get(`/${config.namespace}/get-file/:filePath`, async (req, res) => {
|
||||
const { filePath } = req.params
|
||||
|
||||
debug('get file: %s', filePath)
|
||||
|
||||
try {
|
||||
const contents = await fs.readFile(filePath)
|
||||
|
||||
res.json({ contents: contents.toString() })
|
||||
} catch (err) {
|
||||
const errorMessage = `Getting the file at the following path errored:\nPath: ${filePath}\nError: ${err.stack}`
|
||||
|
||||
debug(errorMessage)
|
||||
|
||||
res.json({
|
||||
error: errorMessage,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
routesE2E.post(`/${config.namespace}/process-origin-callback`, bodyParser.json(), async (req, res) => {
|
||||
try {
|
||||
const { file, fn, projectRoot } = req.body
|
||||
@@ -96,13 +75,6 @@ export const createRoutesE2E = ({
|
||||
networkProxy.handleSourceMapRequest(req, res)
|
||||
})
|
||||
|
||||
// special fallback - serve local files from the project's root folder
|
||||
routesE2E.get('/__root/*', (req, res) => {
|
||||
const file = path.join(config.projectRoot, req.params[0])
|
||||
|
||||
res.sendFile(file, { etag: false })
|
||||
})
|
||||
|
||||
// special fallback - serve dist'd (bundled/static) files from the project path folder
|
||||
routesE2E.get(`/${config.namespace}/bundled/*`, (req, res) => {
|
||||
const file = AppData.getBundledFilePath(config.projectRoot, path.join('src', req.params[0]))
|
||||
@@ -131,5 +103,11 @@ export const createRoutesE2E = ({
|
||||
files.handleCrossOriginIframe(req, res, config)
|
||||
})
|
||||
|
||||
routesE2E.post(`/${config.namespace}/add-verified-command`, bodyParser.json(), (req, res) => {
|
||||
privilegedCommandsManager.addVerifiedCommand(req.body)
|
||||
|
||||
res.sendStatus(204)
|
||||
})
|
||||
|
||||
return routesE2E
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import Bluebird from 'bluebird'
|
||||
import Debug from 'debug'
|
||||
import EventEmitter from 'events'
|
||||
import _ from 'lodash'
|
||||
import path from 'path'
|
||||
import { getCtx } from '@packages/data-context'
|
||||
import { handleGraphQLSocketRequest } from '@packages/graphql/src/makeGraphQLServer'
|
||||
import { onNetStubbingEvent } from '@packages/net-stubbing'
|
||||
@@ -10,10 +9,7 @@ import * as socketIo from '@packages/socket'
|
||||
|
||||
import firefoxUtil from './browsers/firefox-util'
|
||||
import * as errors from './errors'
|
||||
import exec from './exec'
|
||||
import files from './files'
|
||||
import fixture from './fixture'
|
||||
import task from './task'
|
||||
import { ensureProp } from './util/class-helpers'
|
||||
import { getUserEditor, setUserEditor } from './util/editors'
|
||||
import { openFile, OpenFileDetails } from './util/file-opener'
|
||||
@@ -31,6 +27,7 @@ import type { Socket } from '@packages/socket'
|
||||
import type { RunState, CachedTestState } from '@packages/types'
|
||||
import { cors } from '@packages/network'
|
||||
import memory from './browsers/memory'
|
||||
import { privilegedCommandsManager } from './privileged-commands/privileged-commands-manager'
|
||||
|
||||
type StartListeningCallbacks = {
|
||||
onSocketConnection: (socket: any) => void
|
||||
@@ -386,11 +383,6 @@ export class SocketBase {
|
||||
debug('backend:request %o', { eventName, args })
|
||||
|
||||
const backendRequest = () => {
|
||||
// TODO: standardize `configFile`; should it be absolute or relative to projectRoot?
|
||||
const cfgFile = config.configFile && config.configFile.includes(config.projectRoot)
|
||||
? config.configFile
|
||||
: path.join(config.projectRoot, config.configFile)
|
||||
|
||||
switch (eventName) {
|
||||
case 'preserve:run:state':
|
||||
runState = args[0]
|
||||
@@ -411,10 +403,6 @@ export class SocketBase {
|
||||
return firefoxUtil.collectGarbage()
|
||||
case 'get:fixture':
|
||||
return getFixture(args[0], args[1])
|
||||
case 'read:file':
|
||||
return files.readFile(config.projectRoot, args[0], args[1])
|
||||
case 'write:file':
|
||||
return files.writeFile(config.projectRoot, args[0], args[1], args[2])
|
||||
case 'net':
|
||||
return onNetStubbingEvent({
|
||||
eventName: args[0],
|
||||
@@ -424,10 +412,6 @@ export class SocketBase {
|
||||
getFixture,
|
||||
args,
|
||||
})
|
||||
case 'exec':
|
||||
return exec.run(config.projectRoot, args[0])
|
||||
case 'task':
|
||||
return task.run(cfgFile ?? null, args[0])
|
||||
case 'save:session':
|
||||
return session.saveSession(args[0])
|
||||
case 'clear:sessions':
|
||||
@@ -456,6 +440,8 @@ export class SocketBase {
|
||||
return memory.endProfiling()
|
||||
case 'check:memory:pressure':
|
||||
return memory.checkMemoryPressure({ ...args[0], automation })
|
||||
case 'run:privileged':
|
||||
return privilegedCommandsManager.runPrivilegedCommand(config, args[0])
|
||||
case 'telemetry':
|
||||
return (telemetry.exporter() as OTLPTraceExporterCloud)?.send(args[0], () => {}, (err) => {
|
||||
debug('error exporting telemetry data from browser %s', err)
|
||||
|
||||
@@ -9,6 +9,7 @@ type Promisified<T extends (...args: any) => any>
|
||||
interface PromisifiedFsExtra {
|
||||
statAsync: (path: string | Buffer) => Bluebird<ReturnType<typeof fsExtra.statSync>>
|
||||
removeAsync: Promisified<typeof fsExtra.removeSync>
|
||||
readFileAsync: Promisified<typeof fsExtra.readFileSync>
|
||||
writeFileAsync: Promisified<typeof fsExtra.writeFileSync>
|
||||
pathExistsAsync: Promisified<typeof fsExtra.pathExistsSync>
|
||||
}
|
||||
|
||||
@@ -46,10 +46,7 @@ describe('lib/browsers/cri-client', function () {
|
||||
})
|
||||
|
||||
getClient = () => {
|
||||
return criClient.create({
|
||||
target: DEBUGGER_URL,
|
||||
onError,
|
||||
})
|
||||
return criClient.create(DEBUGGER_URL, onError)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -109,9 +106,12 @@ describe('lib/browsers/cri-client', function () {
|
||||
const client = await getClient()
|
||||
|
||||
client.send('Page.enable')
|
||||
// @ts-ignore
|
||||
client.send('Page.foo')
|
||||
// @ts-ignore
|
||||
client.send('Page.bar')
|
||||
client.send('Network.enable')
|
||||
// @ts-ignore
|
||||
client.send('Network.baz')
|
||||
|
||||
// clear out previous calls before reconnect
|
||||
@@ -124,5 +124,21 @@ describe('lib/browsers/cri-client', function () {
|
||||
expect(criStub.send).to.be.calledWith('Page.enable')
|
||||
expect(criStub.send).to.be.calledWith('Network.enable')
|
||||
})
|
||||
|
||||
it('errors if reconnecting fails', async () => {
|
||||
criStub._notifier.on = sinon.stub()
|
||||
criStub.close.throws(new Error('could not reconnect'))
|
||||
|
||||
await getClient()
|
||||
// @ts-ignore
|
||||
await criStub._notifier.on.withArgs('disconnect').args[0][1]()
|
||||
|
||||
expect(onError).to.be.called
|
||||
|
||||
const error = onError.lastCall.args[0]
|
||||
|
||||
expect(error.messageMarkdown).to.equal('There was an error reconnecting to the Chrome DevTools protocol. Please restart the browser.')
|
||||
expect(error.isFatalApiErr).to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
require('../../spec_helper')
|
||||
|
||||
const path = require('path')
|
||||
const { fs } = require('../../../lib/util/fs')
|
||||
|
||||
describe('privileged channel', () => {
|
||||
let runPrivilegedChannel
|
||||
let win
|
||||
|
||||
beforeEach(async () => {
|
||||
const secureChannelScript = (await fs.readFileAsync(path.join(__dirname, '..', '..', '..', 'lib', 'privileged-commands', 'privileged-channel.js'))).toString()
|
||||
const ErrorStub = function (message) {
|
||||
return new Error(message)
|
||||
}
|
||||
|
||||
ErrorStub.captureStackTrace = sinon.stub()
|
||||
|
||||
// need to pull out the methods like this so when they're overwritten
|
||||
// in the tests, they don't mess up the actual globals since the test
|
||||
// runner itself relies on them
|
||||
win = {
|
||||
Array: { prototype: {
|
||||
filter: Array.prototype.filter,
|
||||
includes: Array.prototype.includes,
|
||||
map: Array.prototype.map,
|
||||
} },
|
||||
Error: ErrorStub,
|
||||
Cypress: {
|
||||
on () {},
|
||||
},
|
||||
fetch: sinon.stub().resolves(),
|
||||
Function: { prototype: {
|
||||
toString: Function.prototype.toString,
|
||||
} },
|
||||
JSON: {
|
||||
parse: JSON.parse,
|
||||
stringify: JSON.stringify,
|
||||
},
|
||||
String: { prototype: {
|
||||
includes: String.prototype.includes,
|
||||
replace: String.prototype.replace,
|
||||
split: String.prototype.split,
|
||||
} },
|
||||
}
|
||||
|
||||
runPrivilegedChannel = () => {
|
||||
return eval(`${secureChannelScript}`)({
|
||||
browserFamily: 'chromium',
|
||||
isSpecBridge: false,
|
||||
key: '1234',
|
||||
namespace: '__cypress',
|
||||
scripts: JSON.stringify(['cypress/e2e/spec.cy.js']),
|
||||
url: 'http://localhost:12345/__cypress/tests?p=cypress/integration/some-spec.js',
|
||||
win,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('overwriting native objects and methods has no effect', () => {
|
||||
it('Error', () => {
|
||||
const { onCommandInvocation } = runPrivilegedChannel()
|
||||
|
||||
win.Error = sinon.stub()
|
||||
|
||||
onCommandInvocation({ name: 'task', args: [] })
|
||||
|
||||
expect(win.Error).not.to.be.called
|
||||
})
|
||||
|
||||
it('Error.captureStackTrace', () => {
|
||||
const { onCommandInvocation } = runPrivilegedChannel()
|
||||
|
||||
win.Error.captureStackTrace = sinon.stub()
|
||||
|
||||
onCommandInvocation({ name: 'task', args: [] })
|
||||
|
||||
expect(win.Error.captureStackTrace).not.to.be.called
|
||||
})
|
||||
|
||||
it('Array.prototype.filter', () => {
|
||||
const { onCommandInvocation } = runPrivilegedChannel()
|
||||
|
||||
win.Array.prototype.filter = sinon.stub()
|
||||
|
||||
onCommandInvocation({ name: 'task', args: [] })
|
||||
|
||||
expect(win.Array.prototype.filter).not.to.be.called
|
||||
})
|
||||
|
||||
it('Array.prototype.includes', () => {
|
||||
const { onCommandInvocation } = runPrivilegedChannel()
|
||||
|
||||
win.Array.prototype.includes = sinon.stub()
|
||||
|
||||
onCommandInvocation({ name: 'task', args: [] })
|
||||
|
||||
expect(win.Array.prototype.includes).not.to.be.called
|
||||
})
|
||||
|
||||
it('Array.prototype.map', () => {
|
||||
const { onCommandInvocation } = runPrivilegedChannel()
|
||||
|
||||
win.Array.prototype.map = sinon.stub()
|
||||
|
||||
onCommandInvocation({ name: 'task', args: [] })
|
||||
|
||||
expect(win.Array.prototype.map).not.to.be.called
|
||||
})
|
||||
|
||||
it('String.prototype.split', () => {
|
||||
const { onCommandInvocation } = runPrivilegedChannel()
|
||||
|
||||
win.String.prototype.split = sinon.stub()
|
||||
|
||||
onCommandInvocation({ name: 'task', args: [] })
|
||||
|
||||
expect(win.String.prototype.split).not.to.be.called
|
||||
})
|
||||
|
||||
it('String.prototype.replace', () => {
|
||||
const { onCommandInvocation } = runPrivilegedChannel()
|
||||
|
||||
win.String.prototype.replace = sinon.stub()
|
||||
|
||||
onCommandInvocation({ name: 'task', args: [] })
|
||||
|
||||
expect(win.String.prototype.replace).not.to.be.called
|
||||
})
|
||||
|
||||
it('String.prototype.includes', () => {
|
||||
const { onCommandInvocation } = runPrivilegedChannel()
|
||||
|
||||
win.String.prototype.includes = sinon.stub()
|
||||
|
||||
onCommandInvocation({ name: 'task', args: [] })
|
||||
|
||||
expect(win.String.prototype.includes).not.to.be.called
|
||||
})
|
||||
|
||||
it('Function.prototype.toString', () => {
|
||||
const { onCommandInvocation } = runPrivilegedChannel()
|
||||
|
||||
win.Function.prototype.toString = sinon.stub()
|
||||
|
||||
onCommandInvocation({ name: 'task', args: [] })
|
||||
|
||||
expect(win.Function.prototype.toString).not.to.be.called
|
||||
})
|
||||
|
||||
it('fetch', () => {
|
||||
const { onCommandInvocation } = runPrivilegedChannel()
|
||||
|
||||
win.fetch = sinon.stub().resolves()
|
||||
|
||||
onCommandInvocation({ name: 'task', args: [] })
|
||||
|
||||
expect(win.fetch).not.to.be.called
|
||||
})
|
||||
|
||||
it('JSON.stringify', () => {
|
||||
const { onCommandInvocation } = runPrivilegedChannel()
|
||||
|
||||
win.JSON.stringify = sinon.stub()
|
||||
|
||||
onCommandInvocation({ name: 'task', args: [] })
|
||||
|
||||
expect(win.JSON.stringify).not.to.be.called
|
||||
})
|
||||
|
||||
it('JSON.parse', () => {
|
||||
const { onCommandInvocation } = runPrivilegedChannel()
|
||||
|
||||
win.JSON.parse = sinon.stub()
|
||||
|
||||
onCommandInvocation({ name: 'task', args: [] })
|
||||
|
||||
expect(win.JSON.parse).not.to.be.called
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -28,7 +28,7 @@ describe('lib/files', () => {
|
||||
|
||||
context('#readFile', () => {
|
||||
it('returns contents and full file path', function () {
|
||||
return files.readFile(this.projectRoot, 'tests/_fixtures/message.txt').then(({ contents, filePath }) => {
|
||||
return files.readFile(this.projectRoot, { file: 'tests/_fixtures/message.txt' }).then(({ contents, filePath }) => {
|
||||
expect(contents).to.eq('foobarbaz')
|
||||
|
||||
expect(filePath).to.include('/cy-projects/todos/tests/_fixtures/message.txt')
|
||||
@@ -36,26 +36,26 @@ describe('lib/files', () => {
|
||||
})
|
||||
|
||||
it('returns uses utf8 by default', function () {
|
||||
return files.readFile(this.projectRoot, 'tests/_fixtures/ascii.foo').then(({ contents }) => {
|
||||
return files.readFile(this.projectRoot, { file: 'tests/_fixtures/ascii.foo' }).then(({ contents }) => {
|
||||
expect(contents).to.eq('\n')
|
||||
})
|
||||
})
|
||||
|
||||
it('uses encoding specified in options', function () {
|
||||
return files.readFile(this.projectRoot, 'tests/_fixtures/ascii.foo', { encoding: 'ascii' }).then(({ contents }) => {
|
||||
return files.readFile(this.projectRoot, { file: 'tests/_fixtures/ascii.foo', encoding: 'ascii' }).then(({ contents }) => {
|
||||
expect(contents).to.eq('o#?\n')
|
||||
})
|
||||
})
|
||||
|
||||
// https://github.com/cypress-io/cypress/issues/1558
|
||||
it('explicit null encoding is sent to driver as a Buffer', function () {
|
||||
return files.readFile(this.projectRoot, 'tests/_fixtures/ascii.foo', { encoding: null }).then(({ contents }) => {
|
||||
return files.readFile(this.projectRoot, { file: 'tests/_fixtures/ascii.foo', encoding: null }).then(({ contents }) => {
|
||||
expect(contents).to.eql(Buffer.from('\n'))
|
||||
})
|
||||
})
|
||||
|
||||
it('parses json to valid JS object', function () {
|
||||
return files.readFile(this.projectRoot, 'tests/_fixtures/users.json').then(({ contents }) => {
|
||||
return files.readFile(this.projectRoot, { file: 'tests/_fixtures/users.json' }).then(({ contents }) => {
|
||||
expect(contents).to.eql([
|
||||
{
|
||||
id: 1,
|
||||
@@ -71,8 +71,8 @@ describe('lib/files', () => {
|
||||
|
||||
context('#writeFile', () => {
|
||||
it('writes the file\'s contents and returns contents and full file path', function () {
|
||||
return files.writeFile(this.projectRoot, '.projects/write_file.txt', 'foo').then(() => {
|
||||
return files.readFile(this.projectRoot, '.projects/write_file.txt').then(({ contents, filePath }) => {
|
||||
return files.writeFile(this.projectRoot, { fileName: '.projects/write_file.txt', contents: 'foo' }).then(() => {
|
||||
return files.readFile(this.projectRoot, { file: '.projects/write_file.txt' }).then(({ contents, filePath }) => {
|
||||
expect(contents).to.equal('foo')
|
||||
|
||||
expect(filePath).to.include('/cy-projects/todos/.projects/write_file.txt')
|
||||
@@ -81,8 +81,8 @@ describe('lib/files', () => {
|
||||
})
|
||||
|
||||
it('uses encoding specified in options', function () {
|
||||
return files.writeFile(this.projectRoot, '.projects/write_file.txt', '', { encoding: 'ascii' }).then(() => {
|
||||
return files.readFile(this.projectRoot, '.projects/write_file.txt').then(({ contents }) => {
|
||||
return files.writeFile(this.projectRoot, { fileName: '.projects/write_file.txt', contents: '', encoding: 'ascii' }).then(() => {
|
||||
return files.readFile(this.projectRoot, { file: '.projects/write_file.txt' }).then(({ contents }) => {
|
||||
expect(contents).to.equal('�')
|
||||
})
|
||||
})
|
||||
@@ -90,20 +90,20 @@ describe('lib/files', () => {
|
||||
|
||||
// https://github.com/cypress-io/cypress/issues/1558
|
||||
it('explicit null encoding is written exactly as received', function () {
|
||||
return files.writeFile(this.projectRoot, '.projects/write_file.txt', Buffer.from(''), { encoding: null }).then(() => {
|
||||
return files.readFile(this.projectRoot, '.projects/write_file.txt', { encoding: null }).then(({ contents }) => {
|
||||
return files.writeFile(this.projectRoot, { fileName: '.projects/write_file.txt', contents: Buffer.from(''), encoding: null }).then(() => {
|
||||
return files.readFile(this.projectRoot, { file: '.projects/write_file.txt', encoding: null }).then(({ contents }) => {
|
||||
expect(contents).to.eql(Buffer.from(''))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('overwrites existing file by default', function () {
|
||||
return files.writeFile(this.projectRoot, '.projects/write_file.txt', 'foo').then(() => {
|
||||
return files.readFile(this.projectRoot, '.projects/write_file.txt').then(({ contents }) => {
|
||||
return files.writeFile(this.projectRoot, { fileName: '.projects/write_file.txt', contents: 'foo' }).then(() => {
|
||||
return files.readFile(this.projectRoot, { file: '.projects/write_file.txt' }).then(({ contents }) => {
|
||||
expect(contents).to.equal('foo')
|
||||
|
||||
return files.writeFile(this.projectRoot, '.projects/write_file.txt', 'bar').then(() => {
|
||||
return files.readFile(this.projectRoot, '.projects/write_file.txt').then(({ contents }) => {
|
||||
return files.writeFile(this.projectRoot, { fileName: '.projects/write_file.txt', contents: 'bar' }).then(() => {
|
||||
return files.readFile(this.projectRoot, { file: '.projects/write_file.txt' }).then(({ contents }) => {
|
||||
expect(contents).to.equal('bar')
|
||||
})
|
||||
})
|
||||
@@ -112,12 +112,12 @@ describe('lib/files', () => {
|
||||
})
|
||||
|
||||
it('appends content to file when specified', function () {
|
||||
return files.writeFile(this.projectRoot, '.projects/write_file.txt', 'foo').then(() => {
|
||||
return files.readFile(this.projectRoot, '.projects/write_file.txt').then(({ contents }) => {
|
||||
return files.writeFile(this.projectRoot, { fileName: '.projects/write_file.txt', contents: 'foo' }).then(() => {
|
||||
return files.readFile(this.projectRoot, { file: '.projects/write_file.txt' }).then(({ contents }) => {
|
||||
expect(contents).to.equal('foo')
|
||||
|
||||
return files.writeFile(this.projectRoot, '.projects/write_file.txt', 'bar', { flag: 'a+' }).then(() => {
|
||||
return files.readFile(this.projectRoot, '.projects/write_file.txt').then(({ contents }) => {
|
||||
return files.writeFile(this.projectRoot, { fileName: '.projects/write_file.txt', contents: 'bar', flag: 'a+' }).then(() => {
|
||||
return files.readFile(this.projectRoot, { file: '.projects/write_file.txt' }).then(({ contents }) => {
|
||||
expect(contents).to.equal('foobar')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,7 +11,6 @@ const errors = require('../../lib/errors')
|
||||
const { SocketE2E } = require('../../lib/socket-e2e')
|
||||
const { ServerE2E } = require('../../lib/server-e2e')
|
||||
const { Automation } = require('../../lib/automation')
|
||||
const exec = require('../../lib/exec')
|
||||
const preprocessor = require('../../lib/plugins/preprocessor')
|
||||
const { fs } = require('../../lib/util/fs')
|
||||
const session = require('../../lib/session')
|
||||
@@ -108,11 +107,11 @@ describe('lib/socket', () => {
|
||||
|
||||
foo.bar.baz = foo
|
||||
|
||||
// going to stub exec here just so we have something that we can
|
||||
// stubbing session#getSession here just so we have something that we can
|
||||
// control the resolved value of
|
||||
sinon.stub(exec, 'run').resolves(foo)
|
||||
sinon.stub(session, 'getSession').resolves(foo)
|
||||
|
||||
return this.client.emit('backend:request', 'exec', 'quuz', (res) => {
|
||||
return this.client.emit('backend:request', 'get:session', 'quuz', (res) => {
|
||||
expect(res.response).to.deep.eq(foo)
|
||||
|
||||
return done()
|
||||
@@ -512,33 +511,6 @@ describe('lib/socket', () => {
|
||||
})
|
||||
})
|
||||
|
||||
context('on(backend:request, exec)', () => {
|
||||
it('calls exec#run with project root and options', function (done) {
|
||||
const run = sinon.stub(exec, 'run').returns(Promise.resolve('Desktop Music Pictures'))
|
||||
|
||||
return this.client.emit('backend:request', 'exec', { cmd: 'ls' }, (resp) => {
|
||||
expect(run).to.be.calledWith(this.cfg.projectRoot, { cmd: 'ls' })
|
||||
expect(resp.response).to.eq('Desktop Music Pictures')
|
||||
|
||||
return done()
|
||||
})
|
||||
})
|
||||
|
||||
it('errors when execution fails, passing through timedOut', function (done) {
|
||||
const error = new Error('command not found: lsd')
|
||||
|
||||
error.timedOut = true
|
||||
sinon.stub(exec, 'run').rejects(error)
|
||||
|
||||
return this.client.emit('backend:request', 'exec', { cmd: 'lsd' }, (resp) => {
|
||||
expect(resp.error.message).to.equal('command not found: lsd')
|
||||
expect(resp.error.timedOut).to.be.true
|
||||
|
||||
return done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('on(backend:request, firefox:force:gc)', () => {
|
||||
it('calls firefoxUtil#collectGarbage', function (done) {
|
||||
sinon.stub(firefoxUtil, 'collectGarbage').resolves()
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
exports['e2e cdp / handles disconnections as expected'] = `
|
||||
|
||||
====================================================================================================
|
||||
|
||||
(Run Starting)
|
||||
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Cypress: 1.2.3 │
|
||||
│ Browser: FooBrowser 88 │
|
||||
│ Specs: 1 found (spec.cy.ts) │
|
||||
│ Searched: cypress/e2e/spec.cy.ts │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Running: spec.cy.ts (1 of 1)
|
||||
|
||||
|
||||
e2e remote debugging disconnect
|
||||
✓ reconnects as expected
|
||||
There was an error reconnecting to the Chrome DevTools protocol. Please restart the browser.
|
||||
|
||||
Error: connect ECONNREFUSED 127.0.0.1:7777
|
||||
[stack trace lines]
|
||||
`
|
||||
@@ -1,5 +0,0 @@
|
||||
describe('cy.readFile', () => {
|
||||
it('existence failure', () => {
|
||||
cy.readFile('does-not-exist', { timeout: 100 })
|
||||
})
|
||||
})
|
||||
@@ -31,14 +31,4 @@ describe('e2e remote debugging disconnect', () => {
|
||||
currentConnectionCount: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('errors if CDP connection cannot be reestablished', () => {
|
||||
cy.task('destroy:server')
|
||||
cy.task('kill:active:connections')
|
||||
|
||||
cy.then(() => {
|
||||
// this will cause a project-level error once we realize we can't talk to CDP anymore
|
||||
return callAutomation()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -81,12 +81,6 @@ module.exports = (on, config) => {
|
||||
|
||||
return null
|
||||
},
|
||||
'destroy:server' () {
|
||||
console.error('closing server')
|
||||
server.close()
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
})
|
||||
|
||||
|
||||
@@ -15,24 +15,10 @@ describe('e2e cdp', function () {
|
||||
restoreEnv()
|
||||
})
|
||||
|
||||
// NOTE: this test takes almost a minute and is largely redundant with protocol_spec
|
||||
systemTests.it.skip('fails when remote debugging port cannot be connected to', {
|
||||
project: 'remote-debugging-port-removed',
|
||||
spec: 'spec.cy.ts',
|
||||
browser: 'chrome',
|
||||
expectedExitCode: 1,
|
||||
})
|
||||
|
||||
// https://github.com/cypress-io/cypress/issues/5685
|
||||
systemTests.it('handles disconnections as expected', {
|
||||
project: 'remote-debugging-disconnect',
|
||||
spec: 'spec.cy.ts',
|
||||
browser: 'chrome',
|
||||
expectedExitCode: 1,
|
||||
snapshot: true,
|
||||
onStdout: (stdout) => {
|
||||
// the location of this warning is non-deterministic
|
||||
return stdout.replace('The automation client disconnected. Cannot continue running tests.\n', '')
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -144,16 +144,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-0.5.2.tgz#8c2d931ff927be0ebe740169874a3d4004ab414b"
|
||||
integrity sha512-CQkeV+oJxUazwjlHD0/3ZD08QWKuGQkhnrKo3e6ly5pd48VUpXbb77q0xMU4+vc2CkJnDS02Eq/M9ugyX20XZA==
|
||||
|
||||
"@antfu/utils@^0.7.0":
|
||||
"@antfu/utils@^0.7.0", "@antfu/utils@^0.7.2":
|
||||
version "0.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-0.7.4.tgz#b1c11b95f89f13842204d3d83de01e10bb9257db"
|
||||
integrity sha512-qe8Nmh9rYI/HIspLSTwtbMFPj6dISG6+dJnOguTlPNXtCvS2uezdxscVBb7/3DrmNbQK49TDqpkSQ1chbRGdpQ==
|
||||
|
||||
"@antfu/utils@^0.7.2":
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-0.7.2.tgz#3bb6f37a6b188056fe9e2f363b6aa735ed65d7ca"
|
||||
integrity sha512-vy9fM3pIxZmX07dL+VX1aZe7ynZ+YyB0jY+jE6r3hOK6GNY2t6W8rzpFC4tgpbXUYABkFQwgJq2XYXlxbXAI0g==
|
||||
|
||||
"@ardatan/aggregate-error@0.0.6":
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@ardatan/aggregate-error/-/aggregate-error-0.0.6.tgz#fe6924771ea40fc98dc7a7045c2e872dc8527609"
|
||||
|
||||
Reference in New Issue
Block a user