mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-21 22:50:49 -06:00
feat: capture protocol delivery (#26421)
Co-authored-by: Matt Schile <mschile@cypress.io> Co-authored-by: David Rowe <95636404+davidr-cy@users.noreply.github.com> Co-authored-by: Ryan Manuel <ryanm@cypress.io>
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
# Bump this version to force CI to re-create the cache from scratch.
|
||||
|
||||
04-19-22
|
||||
05-10-22
|
||||
|
||||
@@ -31,6 +31,7 @@ mainBuildFilters: &mainBuildFilters
|
||||
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
|
||||
- 'update-v8-snapshot-cache-on-develop'
|
||||
- 'feat/protocol'
|
||||
- 'tgriesser/feat/protocol-delivery'
|
||||
|
||||
# usually we don't build Mac app - it takes a long time
|
||||
# but sometimes we want to really confirm we are doing the right thing
|
||||
@@ -42,6 +43,7 @@ macWorkflowFilters: &darwin-workflow-filters
|
||||
# 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: [ 'feat/protocol', << pipeline.git.branch >> ]
|
||||
- equal: [ 'tgriesser/feat/protocol-delivery', << pipeline.git.branch >> ]
|
||||
- matches:
|
||||
pattern: /^release\/\d+\.\d+\.\d+$/
|
||||
value: << pipeline.git.branch >>
|
||||
@@ -53,6 +55,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters
|
||||
# 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: [ 'feat/protocol', << pipeline.git.branch >> ]
|
||||
- equal: [ 'tgriesser/feat/protocol-delivery', << pipeline.git.branch >> ]
|
||||
- matches:
|
||||
pattern: /^release\/\d+\.\d+\.\d+$/
|
||||
value: << pipeline.git.branch >>
|
||||
@@ -73,6 +76,7 @@ windowsWorkflowFilters: &windows-workflow-filters
|
||||
# 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: [ 'feat/protocol', << pipeline.git.branch >> ]
|
||||
- equal: [ 'tgriesser/feat/protocol-delivery', << pipeline.git.branch >> ]
|
||||
- matches:
|
||||
pattern: /^release\/\d+\.\d+\.\d+$/
|
||||
value: << pipeline.git.branch >>
|
||||
@@ -139,7 +143,7 @@ commands:
|
||||
- run:
|
||||
name: Check current branch to persist artifacts
|
||||
command: |
|
||||
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "update-v8-snapshot-cache-on-develop" && "$CIRCLE_BRANCH" != "feat/protocol" ]]; then
|
||||
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "update-v8-snapshot-cache-on-develop" && "$CIRCLE_BRANCH" != "feat/protocol" && "$CIRCLE_BRANCH" != "tgriesser/feat/protocol-delivery" ]]; then
|
||||
echo "Not uploading artifacts or posting install comment for this branch."
|
||||
circleci-agent step halt
|
||||
fi
|
||||
@@ -201,6 +205,7 @@ commands:
|
||||
command: |
|
||||
source ./scripts/ensure-node.sh
|
||||
yarn gulp buildProd
|
||||
yarn gulp syncCloudValidations
|
||||
- run:
|
||||
name: Build packages
|
||||
command: |
|
||||
|
||||
@@ -40,6 +40,8 @@ module.exports = {
|
||||
'cli/types/**',
|
||||
// these fixtures are supposed to fail linting
|
||||
'npm/eslint-plugin-dev/test/fixtures/**',
|
||||
// Cloud generated
|
||||
'system-tests/lib/validations/**',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
|
||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -4,4 +4,5 @@
|
||||
|
||||
**/.eslintrc text eol=lf
|
||||
|
||||
packages/errors/__snapshot-html__/** linguist-generated=true
|
||||
packages/errors/__snapshot-html__/** linguist-generated=true
|
||||
system-tests/lib/validations/** linguist-generated=true
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -397,3 +397,6 @@ tooling/v8-snapshot/cache/dev-win32
|
||||
tooling/v8-snapshot/cache/prod-darwin
|
||||
tooling/v8-snapshot/cache/prod-linux
|
||||
tooling/v8-snapshot/cache/prod-win32
|
||||
|
||||
# Cloud API validations
|
||||
system-tests/lib/validations
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
|
||||
## 12.13.0
|
||||
|
||||
_Released 05/23/2023 (PENDING)_
|
||||
|
||||
**Features:**
|
||||
|
||||
- Adds a new cloud api that confirms the uploads of artifacts. Addressed in [#26421](https://github.com/cypress-io/cypress/pull/26421).
|
||||
|
||||
## 12.12.0
|
||||
|
||||
_Released 05/09/2023_
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
export const addCaptureProtocolListeners = (Cypress: Cypress.Cypress) => {
|
||||
Cypress.on('log:added', (log) => {
|
||||
// TODO: UNIFY-1318 - Race condition in unified runner - we should not need this null check
|
||||
if (!Cypress.runner) {
|
||||
return
|
||||
}
|
||||
|
||||
const displayProps = Cypress.runner.getDisplayPropsForLog(log)
|
||||
|
||||
Cypress.backend('protocol:command:log:added', displayProps)
|
||||
})
|
||||
|
||||
Cypress.on('log:changed', (log) => {
|
||||
// TODO: UNIFY-1318 - Race condition in unified runner - we should not need this null check
|
||||
if (!Cypress.runner) {
|
||||
return
|
||||
}
|
||||
|
||||
const displayProps = Cypress.runner.getDisplayPropsForLog(log)
|
||||
|
||||
Cypress.backend('protocol:command:log:changed', displayProps)
|
||||
|
||||
44
packages/errors/__snapshot-html__/CLOUD_CANNOT_UPLOAD_ARTIFACTS.html
generated
Normal file
44
packages/errors/__snapshot-html__/CLOUD_CANNOT_UPLOAD_ARTIFACTS.html
generated
Normal file
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
|
||||
body {
|
||||
font-family: "Courier Prime", Courier, monospace;
|
||||
padding: 0 1em;
|
||||
line-height: 1.4;
|
||||
color: #eee;
|
||||
background-color: #111;
|
||||
}
|
||||
pre {
|
||||
padding: 0 0;
|
||||
margin: 0 0;
|
||||
font-family: "Courier Prime", Courier, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 5px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body><pre><span style="color:#e05561">Warning: We encountered an error while confirming the upload of artifacts.<span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561">These results will not display artifacts.<span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561">This error will not affect or change the exit code.<span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#de73ff">Error: fail whale<span style="color:#e05561"><span style="color:#e6e6e6"></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
|
||||
</pre></body></html>
|
||||
@@ -36,7 +36,7 @@
|
||||
</head>
|
||||
<body><pre><span style="color:#e05561">Recording this run failed. The request was invalid.<span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e5e510">request should follow postRunRequest@2.0.0 schema<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e5e510">Request Validation Error<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561">Errors:<span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
|
||||
@@ -522,6 +522,16 @@ export const AllCypressErrors = {
|
||||
|
||||
${fmt.highlightSecondary(apiErr)}`
|
||||
},
|
||||
CLOUD_CANNOT_UPLOAD_ARTIFACTS: (apiErr: Error) => {
|
||||
return errTemplate`\
|
||||
Warning: We encountered an error while confirming the upload of artifacts.
|
||||
|
||||
These results will not display artifacts.
|
||||
|
||||
This error will not affect or change the exit code.
|
||||
|
||||
${fmt.highlightSecondary(apiErr)}`
|
||||
},
|
||||
CLOUD_CANNOT_CREATE_RUN_OR_INSTANCE: (apiErr: Error) => {
|
||||
return errTemplate`\
|
||||
Warning: We encountered an error communicating with our servers.
|
||||
|
||||
@@ -431,6 +431,11 @@ describe('visual error templates', () => {
|
||||
}],
|
||||
}
|
||||
},
|
||||
CLOUD_CANNOT_UPLOAD_ARTIFACTS: () => {
|
||||
return {
|
||||
default: [makeErr()],
|
||||
}
|
||||
},
|
||||
CLOUD_STALE_RUN: () => {
|
||||
return {
|
||||
default: [{
|
||||
@@ -600,7 +605,7 @@ describe('visual error templates', () => {
|
||||
CLOUD_INVALID_RUN_REQUEST: () => {
|
||||
return {
|
||||
default: [{
|
||||
message: 'request should follow postRunRequest@2.0.0 schema',
|
||||
message: 'Request Validation Error',
|
||||
errors: [
|
||||
'data.commit has additional properties',
|
||||
'data.ci.buildNumber is required',
|
||||
|
||||
@@ -1126,6 +1126,7 @@ enum ErrorTypeEnum {
|
||||
CLOUD_CANNOT_CREATE_RUN_OR_INSTANCE
|
||||
CLOUD_CANNOT_PROCEED_IN_PARALLEL
|
||||
CLOUD_CANNOT_PROCEED_IN_SERIAL
|
||||
CLOUD_CANNOT_UPLOAD_ARTIFACTS
|
||||
CLOUD_CANNOT_UPLOAD_RESULTS
|
||||
CLOUD_GRAPHQL_ERROR
|
||||
CLOUD_INVALID_RUN_REQUEST
|
||||
|
||||
@@ -3,7 +3,7 @@ import Debug from 'debug'
|
||||
import { _connectAsync, _getDelayMsForRetry } from './protocol'
|
||||
import * as errors from '../errors'
|
||||
import { create, CriClient } from './cri-client'
|
||||
import type { ProtocolManager } from '@packages/types'
|
||||
import type { ProtocolManagerShape } from '@packages/types'
|
||||
|
||||
const debug = Debug('cypress:server:browsers:browser-cri-client')
|
||||
|
||||
@@ -92,7 +92,7 @@ const retryWithIncreasingDelay = async <T>(retryable: () => Promise<T>, browserN
|
||||
|
||||
export class BrowserCriClient {
|
||||
currentlyAttachedTarget: CriClient | undefined
|
||||
private constructor (private browserClient: CriClient, private versionInfo, public host: string, public port: number, private browserName: string, private onAsynchronousError: Function, private protocolManager?: ProtocolManager) {}
|
||||
private constructor (private browserClient: CriClient, private versionInfo, public host: string, public port: number, private browserName: string, private onAsynchronousError: Function, private protocolManager?: ProtocolManagerShape) {}
|
||||
|
||||
/**
|
||||
* Factory method for the browser cri client. Connects to the browser and then returns a chrome remote interface wrapper around the
|
||||
@@ -104,7 +104,7 @@ export class BrowserCriClient {
|
||||
* @param onAsynchronousError callback for any cdp fatal errors
|
||||
* @returns a wrapper around the chrome remote interface that is connected to the browser target
|
||||
*/
|
||||
static async create (hosts: string[], port: number, browserName: string, onAsynchronousError: Function, onReconnect?: (client: CriClient) => void, protocolManager?: ProtocolManager): Promise<BrowserCriClient> {
|
||||
static async create (hosts: string[], port: number, browserName: string, onAsynchronousError: Function, onReconnect?: (client: CriClient) => void, protocolManager?: ProtocolManagerShape): Promise<BrowserCriClient> {
|
||||
const host = await ensureLiveBrowser(hosts, port, browserName)
|
||||
|
||||
return retryWithIncreasingDelay(async () => {
|
||||
|
||||
@@ -20,7 +20,7 @@ import type { Browser, BrowserInstance } from './types'
|
||||
import { BrowserCriClient } from './browser-cri-client'
|
||||
import type { CriClient } from './cri-client'
|
||||
import type { Automation } from '../automation'
|
||||
import type { BrowserLaunchOpts, BrowserNewTabOpts, ProtocolManager, RunModeVideoApi } from '@packages/types'
|
||||
import type { BrowserLaunchOpts, BrowserNewTabOpts, ProtocolManagerShape, RunModeVideoApi } from '@packages/types'
|
||||
import memory from './memory'
|
||||
|
||||
const debug = debugModule('cypress:server:browsers:chrome')
|
||||
@@ -569,7 +569,7 @@ export = {
|
||||
/**
|
||||
* Clear instance state for the chrome instance, this is normally called in on kill or on exit.
|
||||
*/
|
||||
clearInstanceState (protocolManager?: ProtocolManager) {
|
||||
clearInstanceState (protocolManager?: ProtocolManagerShape) {
|
||||
debug('closing remote interface client')
|
||||
// Do nothing on failure here since we're shutting down anyway
|
||||
browserCriClient?.close().catch()
|
||||
|
||||
@@ -11,7 +11,7 @@ import * as errors from '../errors'
|
||||
import type { Browser, BrowserInstance } from './types'
|
||||
import type { BrowserWindow } from 'electron'
|
||||
import type { Automation } from '../automation'
|
||||
import type { BrowserLaunchOpts, Preferences, ProtocolManager, RunModeVideoApi } from '@packages/types'
|
||||
import type { BrowserLaunchOpts, Preferences, ProtocolManagerShape, RunModeVideoApi } from '@packages/types'
|
||||
import memory from './memory'
|
||||
import { BrowserCriClient } from './browser-cri-client'
|
||||
import { getRemoteDebuggingPort } from '../util/electron-app'
|
||||
@@ -232,7 +232,7 @@ export = {
|
||||
return this._launch(win, url, automation, electronOptions)
|
||||
},
|
||||
|
||||
async _launch (win: BrowserWindow, url: string, automation: Automation, options: ElectronOpts, videoApi?: RunModeVideoApi, protocolManager?: ProtocolManager) {
|
||||
async _launch (win: BrowserWindow, url: string, automation: Automation, options: ElectronOpts, videoApi?: RunModeVideoApi, protocolManager?: ProtocolManagerShape) {
|
||||
if (options.show) {
|
||||
menu.set({ withInternalDevTools: true })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const _ = require('lodash')
|
||||
const os = require('os')
|
||||
const debug = require('debug')('cypress:server:cloud:api')
|
||||
const debugProtocol = require('debug')('cypress:server:protocol')
|
||||
const request = require('@cypress/request-promise')
|
||||
const humanInterval = require('human-interval')
|
||||
|
||||
@@ -18,11 +19,15 @@ import * as enc from './encryption'
|
||||
import getEnvInformationForProjectRoot from './environment'
|
||||
|
||||
import type { OptionsWithUrl } from 'request-promise'
|
||||
import type { ProtocolManager } from '@packages/types'
|
||||
import type { ProtocolManagerShape } from '@packages/types'
|
||||
import { fs } from '../util/fs'
|
||||
|
||||
const THIRTY_SECONDS = humanInterval('30 seconds')
|
||||
const SIXTY_SECONDS = humanInterval('60 seconds')
|
||||
const TWO_MINUTES = humanInterval('2 minutes')
|
||||
|
||||
const PUBLIC_KEY_VERSION = '1'
|
||||
|
||||
const DELAYS: number[] = process.env.API_RETRY_INTERVALS
|
||||
? process.env.API_RETRY_INTERVALS.split(',').map(_.toNumber)
|
||||
: [THIRTY_SECONDS, SIXTY_SECONDS, TWO_MINUTES]
|
||||
@@ -30,6 +35,7 @@ const DELAYS: number[] = process.env.API_RETRY_INTERVALS
|
||||
const runnerCapabilities = {
|
||||
'dynamicSpecsInSerialMode': true,
|
||||
'skipSpecAction': true,
|
||||
'protocolMountVersion': 1,
|
||||
}
|
||||
|
||||
let responseCache = {}
|
||||
@@ -44,7 +50,7 @@ class DecryptionError extends Error {
|
||||
}
|
||||
|
||||
export interface CypressRequestOptions extends OptionsWithUrl {
|
||||
encrypt?: boolean | 'always'
|
||||
encrypt?: boolean | 'always' | 'signed'
|
||||
method: string
|
||||
cacheable?: boolean
|
||||
}
|
||||
@@ -130,7 +136,7 @@ const rp = request.defaults((params: CypressRequestOptions, callback) => {
|
||||
|
||||
params.body = jwe
|
||||
|
||||
headers['x-cypress-encrypted'] = '1'
|
||||
headers['x-cypress-encrypted'] = PUBLIC_KEY_VERSION
|
||||
}
|
||||
|
||||
return request[method](params, callback).promise()
|
||||
@@ -242,7 +248,45 @@ export type CreateRunOptions = {
|
||||
tags: string[]
|
||||
testingType: 'e2e' | 'component'
|
||||
timeout?: number
|
||||
protocolManager?: ProtocolManager
|
||||
protocolManager?: ProtocolManagerShape
|
||||
}
|
||||
|
||||
type CreateRunResponse = {
|
||||
groupId: string
|
||||
machineId: string
|
||||
runId: string
|
||||
tags: string[] | null
|
||||
runUrl: string
|
||||
warnings: (Record<string, unknown> & {
|
||||
code: string
|
||||
message: string
|
||||
name: string
|
||||
})[]
|
||||
captureProtocolUrl?: string | undefined
|
||||
}
|
||||
|
||||
type UpdateInstanceArtifactsOptions = {
|
||||
runId: string
|
||||
instanceId: string
|
||||
timeout: number | undefined
|
||||
protocol: {
|
||||
url: string
|
||||
success: boolean
|
||||
fileSize?: number | undefined
|
||||
error?: string | undefined
|
||||
} | undefined
|
||||
screenshots: {
|
||||
url: string
|
||||
success: boolean
|
||||
fileSize?: number | undefined
|
||||
error?: string | undefined
|
||||
}[] | undefined
|
||||
video: {
|
||||
url: string
|
||||
success: boolean
|
||||
fileSize?: number | undefined
|
||||
error?: string | undefined
|
||||
} | undefined
|
||||
}
|
||||
|
||||
let preflightResult = {
|
||||
@@ -332,9 +376,28 @@ module.exports = {
|
||||
})
|
||||
})
|
||||
})
|
||||
.then(async (result) => {
|
||||
// TODO(protocol): Get url for the protocol code and pass it down to download
|
||||
await options.protocolManager?.setupProtocol()
|
||||
.then(async (result: CreateRunResponse) => {
|
||||
try {
|
||||
if (result.captureProtocolUrl || process.env.CYPRESS_LOCAL_PROTOCOL_PATH) {
|
||||
const script = await this.getCaptureProtocolScript(result.captureProtocolUrl || process.env.CYPRESS_LOCAL_PROTOCOL_PATH)
|
||||
|
||||
if (script) {
|
||||
await options.protocolManager?.setupProtocol(script, result.runId)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
options.protocolManager?.sendErrors([
|
||||
{
|
||||
args: [result.captureProtocolUrl],
|
||||
captureMethod: 'getCaptureProtocolScript',
|
||||
error: {
|
||||
message: e.message,
|
||||
stack: e.stack,
|
||||
name: e.name,
|
||||
},
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
@@ -411,6 +474,28 @@ module.exports = {
|
||||
})
|
||||
},
|
||||
|
||||
updateInstanceArtifacts (options: UpdateInstanceArtifactsOptions) {
|
||||
return retryWithBackoff((attemptIndex) => {
|
||||
return rp.put({
|
||||
url: recordRoutes.instanceArtifacts(options.instanceId),
|
||||
json: true,
|
||||
timeout: options.timeout ?? SIXTY_SECONDS,
|
||||
body: {
|
||||
protocol: options.protocol,
|
||||
screenshots: options.screenshots,
|
||||
video: options.video,
|
||||
},
|
||||
headers: {
|
||||
'x-route-version': '1',
|
||||
'x-cypress-run-id': options.runId,
|
||||
'x-cypress-request-attempt': attemptIndex,
|
||||
},
|
||||
})
|
||||
.catch(RequestErrors.StatusCodeError, formatResponseBody)
|
||||
.catch(tagError)
|
||||
})
|
||||
},
|
||||
|
||||
postInstanceResults (options) {
|
||||
return retryWithBackoff((attemptIndex) => {
|
||||
return rp.post({
|
||||
@@ -530,5 +615,40 @@ module.exports = {
|
||||
})
|
||||
},
|
||||
|
||||
getCaptureProtocolScript (url: string) {
|
||||
// TODO(protocol): Ensure this is removed in production
|
||||
if (process.env.CYPRESS_LOCAL_PROTOCOL_PATH) {
|
||||
debugProtocol(`Loading protocol via script at local path %s`, process.env.CYPRESS_LOCAL_PROTOCOL_PATH)
|
||||
|
||||
return fs.promises.readFile(process.env.CYPRESS_LOCAL_PROTOCOL_PATH, 'utf8')
|
||||
}
|
||||
|
||||
return retryWithBackoff(async (attemptIndex) => {
|
||||
return rp.get({
|
||||
url,
|
||||
headers: {
|
||||
'x-route-version': '1',
|
||||
'x-cypress-request-attempt': attemptIndex,
|
||||
'x-cypress-signature': PUBLIC_KEY_VERSION,
|
||||
},
|
||||
agent,
|
||||
encrypt: 'signed',
|
||||
resolveWithFullResponse: true,
|
||||
})
|
||||
}).then((res) => {
|
||||
const verified = enc.verifySignature(res.body, res.headers['x-cypress-signature'])
|
||||
|
||||
if (!verified) {
|
||||
debugProtocol(`Unable to verify protocol signature %s`, url)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
debugProtocol(`Loading protocol via url %s`, url)
|
||||
|
||||
return res.body
|
||||
})
|
||||
},
|
||||
|
||||
retryWithBackoff,
|
||||
}
|
||||
|
||||
@@ -36,6 +36,14 @@ export interface EncryptRequestData {
|
||||
secretKey: crypto.KeyObject
|
||||
}
|
||||
|
||||
export function verifySignature (body: string, signature: string, publicKey?: crypto.KeyObject) {
|
||||
const verify = crypto.createVerify('SHA256')
|
||||
|
||||
verify.update(body)
|
||||
|
||||
return verify.verify(publicKey || getPublicKey(), Buffer.from(signature, 'base64'))
|
||||
}
|
||||
|
||||
// Implements the https://www.rfc-editor.org/rfc/rfc7516 spec
|
||||
// Functionally equivalent to the behavior for AES-256-GCM encryption
|
||||
// in the jose library (https://github.com/panva/jose/blob/main/src/jwe/general/encrypt.ts),
|
||||
|
||||
@@ -1,71 +1,116 @@
|
||||
import fs from 'fs-extra'
|
||||
import { NodeVM } from 'vm2'
|
||||
import Debug from 'debug'
|
||||
import type { ProtocolManager, AppCaptureProtocolInterface } from '@packages/types'
|
||||
import type { ProtocolManagerShape, AppCaptureProtocolInterface, CDPClient, ProtocolError } from '@packages/types'
|
||||
import Database from 'better-sqlite3'
|
||||
import path from 'path'
|
||||
import os from 'os'
|
||||
import { createGzip } from 'zlib'
|
||||
import fetch from 'cross-fetch'
|
||||
import { performance } from 'perf_hooks'
|
||||
|
||||
const routes = require('./routes')
|
||||
const pkg = require('@packages/root')
|
||||
const { agent } = require('@packages/network')
|
||||
const debug = Debug('cypress:server:protocol')
|
||||
const debugVerbose = Debug('cypress-verbose:server:protocol')
|
||||
|
||||
const setupProtocol = async (url?: string): Promise<AppCaptureProtocolInterface | undefined> => {
|
||||
let script: string | undefined
|
||||
const CAPTURE_ERRORS = !process.env.CYPRESS_LOCAL_PROTOCOL_PATH
|
||||
const DELETE_DB = !process.env.CYPRESS_LOCAL_PROTOCOL_PATH
|
||||
|
||||
// TODO(protocol): We will need to remove this option in production
|
||||
if (process.env.CYPRESS_LOCAL_PROTOCOL_PATH) {
|
||||
script = await fs.readFile(process.env.CYPRESS_LOCAL_PROTOCOL_PATH, 'utf8')
|
||||
} else if (url) {
|
||||
// TODO(protocol): Download the protocol script from the cloud
|
||||
export class ProtocolManager implements ProtocolManagerShape {
|
||||
private _runId?: string
|
||||
private _instanceId?: string
|
||||
private _db?: Database.Database
|
||||
private _dbPath?: string
|
||||
private _errors: ProtocolError[] = []
|
||||
private _protocol: AppCaptureProtocolInterface | undefined
|
||||
|
||||
get protocolEnabled (): boolean {
|
||||
return !!this._protocol
|
||||
}
|
||||
|
||||
if (script) {
|
||||
const cypressProtocolDirectory = path.join(os.tmpdir(), 'cypress', 'protocol')
|
||||
async setupProtocol (script: string, runId: string) {
|
||||
debug('setting up protocol via script')
|
||||
try {
|
||||
this._runId = runId
|
||||
if (script) {
|
||||
const cypressProtocolDirectory = path.join(os.tmpdir(), 'cypress', 'protocol')
|
||||
|
||||
// TODO(protocol): Handle any errors here appropriately. Likely, we will want to handle all errors in the initialization process similarly (e.g. downloading, file permissions, etc.)
|
||||
await fs.ensureDir(cypressProtocolDirectory)
|
||||
const vm = new NodeVM({
|
||||
console: 'inherit',
|
||||
sandbox: {
|
||||
Debug,
|
||||
performance: {
|
||||
now: performance.now,
|
||||
timeOrigin: performance.timeOrigin,
|
||||
},
|
||||
await fs.ensureDir(cypressProtocolDirectory)
|
||||
const vm = new NodeVM({
|
||||
console: 'inherit',
|
||||
sandbox: {
|
||||
Debug,
|
||||
performance: {
|
||||
now: performance.now,
|
||||
timeOrigin: performance.timeOrigin,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { AppCaptureProtocol } = vm.run(script)
|
||||
|
||||
this._protocol = new AppCaptureProtocol()
|
||||
}
|
||||
} catch (error) {
|
||||
if (CAPTURE_ERRORS) {
|
||||
this._errors.push({
|
||||
error,
|
||||
args: [script],
|
||||
captureMethod: 'setupProtocol',
|
||||
})
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async connectToBrowser (cdpClient: CDPClient) {
|
||||
// Wrap the cdp client listeners so that we can be notified of any errors that may occur
|
||||
const newCdpClient: CDPClient = {
|
||||
...cdpClient,
|
||||
on: (event, listener) => {
|
||||
cdpClient.on(event, async (message) => {
|
||||
try {
|
||||
await listener(message)
|
||||
} catch (error) {
|
||||
if (CAPTURE_ERRORS) {
|
||||
this._errors.push({ captureMethod: 'cdpClient.on', error, args: [event, message] })
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const { AppCaptureProtocol } = vm.run(script)
|
||||
|
||||
return new AppCaptureProtocol()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
class ProtocolManagerImpl implements ProtocolManager {
|
||||
private protocol: AppCaptureProtocolInterface | undefined
|
||||
|
||||
async setupProtocol (url?: string) {
|
||||
debug('setting up protocol via url %s', url)
|
||||
|
||||
this.protocol = await setupProtocol(url)
|
||||
}
|
||||
|
||||
async connectToBrowser (cdpClient) {
|
||||
await this.protocol?.connectToBrowser(cdpClient)
|
||||
await this.invokeAsync('connectToBrowser', newCdpClient)
|
||||
}
|
||||
|
||||
addRunnables (runnables) {
|
||||
this.protocol?.addRunnables(runnables)
|
||||
this.invokeSync('addRunnables', runnables)
|
||||
}
|
||||
|
||||
beforeSpec (spec: { instanceId: string }) {
|
||||
if (!this.protocol) {
|
||||
if (!this._protocol) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this._beforeSpec(spec)
|
||||
} catch (error) {
|
||||
if (CAPTURE_ERRORS) {
|
||||
this._errors.push({ captureMethod: 'beforeSpec', error, args: [spec] })
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _beforeSpec (spec: { instanceId: string }) {
|
||||
this._instanceId = spec.instanceId
|
||||
|
||||
const cypressProtocolDirectory = path.join(os.tmpdir(), 'cypress', 'protocol')
|
||||
const dbPath = path.join(cypressProtocolDirectory, `${spec.instanceId}.db`)
|
||||
|
||||
@@ -76,40 +121,223 @@ class ProtocolManagerImpl implements ProtocolManager {
|
||||
verbose: debugVerbose,
|
||||
})
|
||||
|
||||
this.protocol?.beforeSpec(db)
|
||||
this._db = db
|
||||
this._dbPath = dbPath
|
||||
this.invokeSync('beforeSpec', db)
|
||||
}
|
||||
|
||||
afterSpec () {
|
||||
if (!this.protocol) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return this.protocol.afterSpec()
|
||||
async afterSpec () {
|
||||
await this.invokeAsync('afterSpec')
|
||||
}
|
||||
|
||||
beforeTest (test) {
|
||||
this.protocol?.beforeTest(test)
|
||||
beforeTest (test: Record<string, any>) {
|
||||
this.invokeSync('beforeTest', test)
|
||||
}
|
||||
|
||||
afterTest (test) {
|
||||
this.protocol?.afterTest(test)
|
||||
afterTest (test: Record<string, any>) {
|
||||
this.invokeSync('afterTest', test)
|
||||
}
|
||||
|
||||
commandLogAdded (log: any) {
|
||||
this.protocol?.commandLogAdded(log)
|
||||
this.invokeSync('commandLogAdded', log)
|
||||
}
|
||||
|
||||
commandLogChanged (log: any): void {
|
||||
this.protocol?.commandLogChanged(log)
|
||||
this.invokeSync('commandLogChanged', log)
|
||||
}
|
||||
|
||||
viewportChanged (input: any): void {
|
||||
this.protocol?.viewportChanged(input)
|
||||
this.invokeSync('viewportChanged', input)
|
||||
}
|
||||
|
||||
urlChanged (input: any): void {
|
||||
this.protocol?.urlChanged(input)
|
||||
this.invokeSync('urlChanged', input)
|
||||
}
|
||||
|
||||
async uploadCaptureArtifact (uploadUrl: string) {
|
||||
const dbPath = this._dbPath
|
||||
|
||||
if (!this._protocol || !dbPath || !this._db) {
|
||||
if (this._errors.length) {
|
||||
await this.sendErrors()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
debug(`uploading %s to %s`, dbPath, uploadUrl)
|
||||
|
||||
let zippedFileSize = 0
|
||||
|
||||
try {
|
||||
const body = await new Promise((resolve, reject) => {
|
||||
const gzip = createGzip()
|
||||
const buffers: Buffer[] = []
|
||||
|
||||
gzip.on('data', (args) => {
|
||||
zippedFileSize += args.length
|
||||
buffers.push(args)
|
||||
})
|
||||
|
||||
gzip.on('end', () => {
|
||||
resolve(Buffer.concat(buffers))
|
||||
})
|
||||
|
||||
gzip.on('error', reject)
|
||||
|
||||
fs.createReadStream(dbPath).pipe(gzip, { end: true })
|
||||
})
|
||||
const res = await fetch(uploadUrl, {
|
||||
agent,
|
||||
method: 'PUT',
|
||||
// @ts-expect-error - this is supported
|
||||
body,
|
||||
headers: {
|
||||
'Content-Encoding': 'gzip',
|
||||
'Content-Type': 'binary/octet-stream',
|
||||
'Content-Length': `${zippedFileSize}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
fileSize: zippedFileSize,
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
|
||||
const err = await res.text()
|
||||
|
||||
debug(`error response text: %s`, err)
|
||||
|
||||
return {
|
||||
fileSize: zippedFileSize,
|
||||
success: false,
|
||||
error: err,
|
||||
}
|
||||
} catch (e) {
|
||||
if (CAPTURE_ERRORS) {
|
||||
this._errors.push({
|
||||
error: e,
|
||||
captureMethod: 'uploadCaptureArtifact',
|
||||
})
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
|
||||
return {
|
||||
fileSize: zippedFileSize,
|
||||
success: false,
|
||||
error: e,
|
||||
}
|
||||
} finally {
|
||||
await Promise.all([
|
||||
this.sendErrors(),
|
||||
DELETE_DB ? fs.unlink(dbPath).catch((e) => {
|
||||
debug(`Error unlinking db %o`, e)
|
||||
}) : Promise.resolve(),
|
||||
])
|
||||
|
||||
// Reset errors after they have been sent
|
||||
this._errors = []
|
||||
}
|
||||
}
|
||||
|
||||
async sendErrors (protocolErrors: ProtocolError[] = this._errors) {
|
||||
if (protocolErrors.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const body = JSON.stringify({
|
||||
runId: this._runId,
|
||||
instanceId: this._instanceId,
|
||||
errors: protocolErrors.map((e) => {
|
||||
return {
|
||||
name: e.error.name ?? `Unknown name`,
|
||||
stack: e.error.stack ?? `Unknown stack`,
|
||||
message: e.error.message ?? `Unknown message`,
|
||||
captureMethod: e.captureMethod,
|
||||
args: e.args ? this.stringify(e.args) : undefined,
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
await fetch(routes.apiRoutes.captureProtocolErrors() as string, {
|
||||
// @ts-expect-error - this is supported
|
||||
agent,
|
||||
method: 'POST',
|
||||
body,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-cypress-version': pkg.version,
|
||||
'x-os-name': os.platform(),
|
||||
'x-arch': os.arch(),
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
debug(`Error calling ProtocolManager.sendErrors: %o, original errors %o`, e, protocolErrors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstracts invoking a synchronous method on the AppCaptureProtocol instance, so we can handle
|
||||
* errors in a uniform way
|
||||
*/
|
||||
private invokeSync<K extends ProtocolSyncMethods> (method: K, ...args: Parameters<AppCaptureProtocolInterface[K]>) {
|
||||
if (!this._protocol) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// @ts-expect-error - TS not associating the method & args properly, even though we know it's correct
|
||||
this._protocol[method].apply(this._protocol, args)
|
||||
} catch (error) {
|
||||
if (CAPTURE_ERRORS) {
|
||||
this._errors.push({ captureMethod: method, error, args })
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstracts invoking a synchronous method on the AppCaptureProtocol instance, so we can handle
|
||||
* errors in a uniform way
|
||||
*/
|
||||
private async invokeAsync <K extends ProtocolAsyncMethods> (method: K, ...args: Parameters<AppCaptureProtocolInterface[K]>) {
|
||||
if (!this._protocol) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// @ts-expect-error - TS not associating the method & args properly, even though we know it's correct
|
||||
await this._protocol[method].apply(this._protocol, args)
|
||||
} catch (error) {
|
||||
if (CAPTURE_ERRORS) {
|
||||
this._errors.push({ captureMethod: method, error, args })
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private stringify (val: any) {
|
||||
try {
|
||||
return JSON.stringify(val)
|
||||
} catch (e) {
|
||||
return `Unserializable ${typeof val}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProtocolManagerImpl
|
||||
// Helper types for invokeSync / invokeAsync
|
||||
type ProtocolSyncMethods = {
|
||||
[K in keyof AppCaptureProtocolInterface]: ReturnType<AppCaptureProtocolInterface[K]> extends void ? K : never
|
||||
}[keyof AppCaptureProtocolInterface]
|
||||
|
||||
type ProtocolAsyncMethods = {
|
||||
[K in keyof AppCaptureProtocolInterface]: ReturnType<AppCaptureProtocolInterface[K]> extends Promise<any> ? K : never
|
||||
}[keyof AppCaptureProtocolInterface]
|
||||
|
||||
export default ProtocolManager
|
||||
|
||||
@@ -13,6 +13,8 @@ const CLOUD_ENDPOINTS = {
|
||||
instanceTests: 'instances/:id/tests',
|
||||
instanceResults: 'instances/:id/results',
|
||||
instanceStdout: 'instances/:id/stdout',
|
||||
instanceArtifacts: 'instances/:id/artifacts',
|
||||
captureProtocolErrors: 'capture-protocol/errors',
|
||||
exceptions: 'exceptions',
|
||||
telemetry: 'telemetry',
|
||||
} as const
|
||||
|
||||
@@ -23,6 +23,7 @@ const terminal = require('../util/terminal')
|
||||
const ciProvider = require('../util/ci_provider')
|
||||
const testsUtils = require('../util/tests_utils')
|
||||
const specWriter = require('../util/spec_writer')
|
||||
const { fs } = require('../util/fs')
|
||||
|
||||
// dont yell about any errors either
|
||||
const runningInternalTests = () => {
|
||||
@@ -108,9 +109,37 @@ const getSpecRelativePath = (spec) => {
|
||||
}
|
||||
|
||||
const uploadArtifacts = (options = {}) => {
|
||||
const { video, screenshots, videoUploadUrl, shouldUploadVideo, screenshotUploadUrls, quiet } = options
|
||||
const { protocolManager, video, screenshots, videoUploadUrl, captureUploadUrl, shouldUploadVideo, screenshotUploadUrls, quiet } = options
|
||||
|
||||
const uploads = []
|
||||
const uploadReport = {
|
||||
protocol: undefined,
|
||||
screenshots: [],
|
||||
video: undefined,
|
||||
}
|
||||
|
||||
const attachMetadataToUploadReport = async (key, pathToFile, statFile, initialUploadMetadata) => {
|
||||
const uploadMetadata = {
|
||||
...initialUploadMetadata,
|
||||
}
|
||||
|
||||
if (statFile) {
|
||||
try {
|
||||
const { size } = await fs.statAsync(pathToFile)
|
||||
|
||||
uploadMetadata.fileSize = size
|
||||
} catch (err) {
|
||||
debug('failed to get stats for upload artifact %o', {
|
||||
file: pathToFile,
|
||||
stack: err.stack,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
uploadReport[key] = Array.isArray(uploadReport[key]) ?
|
||||
[...uploadReport[key], uploadMetadata] : uploadMetadata
|
||||
}
|
||||
|
||||
let count = 0
|
||||
|
||||
const nums = () => {
|
||||
@@ -119,17 +148,36 @@ const uploadArtifacts = (options = {}) => {
|
||||
return chalk.gray(`(${count}/${uploads.length})`)
|
||||
}
|
||||
|
||||
const send = (pathToFile, url) => {
|
||||
const success = () => {
|
||||
const success = (pathToFile, url, uploadReportOptions) => {
|
||||
const { statFile, key } = uploadReportOptions
|
||||
|
||||
return async (res) => {
|
||||
await attachMetadataToUploadReport(key, pathToFile, statFile, {
|
||||
success: true,
|
||||
url,
|
||||
...res,
|
||||
})
|
||||
|
||||
if (!quiet) {
|
||||
// eslint-disable-next-line no-console
|
||||
return console.log(` - Done Uploading ${nums()}`, chalk.blue(pathToFile))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fail = (pathToFile, url, uploadReportOptions) => {
|
||||
const { statFile, key } = uploadReportOptions
|
||||
|
||||
return async (err) => {
|
||||
await attachMetadataToUploadReport(key, pathToFile, statFile, {
|
||||
success: false,
|
||||
url,
|
||||
error: err.message,
|
||||
})
|
||||
|
||||
const fail = (err) => {
|
||||
debug('failed to upload artifact %o', {
|
||||
file: pathToFile,
|
||||
url,
|
||||
stack: err.stack,
|
||||
})
|
||||
|
||||
@@ -138,26 +186,36 @@ const uploadArtifacts = (options = {}) => {
|
||||
return console.log(` - Failed Uploading ${nums()}`, chalk.red(pathToFile))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const send = (pathToFile, url, reportKey) => {
|
||||
return uploads.push(
|
||||
upload.send(pathToFile, url)
|
||||
.then(success)
|
||||
.catch(fail),
|
||||
.then(success(pathToFile, url, { key: reportKey, statFile: true }))
|
||||
.catch(fail(pathToFile, url, { key: reportKey, statFile: true })),
|
||||
)
|
||||
}
|
||||
|
||||
if (videoUploadUrl && shouldUploadVideo) {
|
||||
send(video, videoUploadUrl)
|
||||
send(video, videoUploadUrl, 'video')
|
||||
}
|
||||
|
||||
if (screenshotUploadUrls) {
|
||||
screenshotUploadUrls.forEach((obj) => {
|
||||
const screenshot = _.find(screenshots, { screenshotId: obj.screenshotId })
|
||||
|
||||
return send(screenshot.path, obj.uploadUrl)
|
||||
return send(screenshot.path, obj.uploadUrl, 'screenshots')
|
||||
})
|
||||
}
|
||||
|
||||
if (captureUploadUrl && protocolManager) {
|
||||
uploads.push(
|
||||
protocolManager.uploadCaptureArtifact(captureUploadUrl)
|
||||
.then(success('Test Replay', captureUploadUrl, { key: 'protocol', statFile: false }))
|
||||
.catch(fail('Test Replay', captureUploadUrl, { key: 'protocol', statFile: false })),
|
||||
)
|
||||
}
|
||||
|
||||
if (!uploads.length && !quiet) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(' - Nothing to Upload')
|
||||
@@ -170,6 +228,25 @@ const uploadArtifacts = (options = {}) => {
|
||||
|
||||
return exception.create(err)
|
||||
})
|
||||
.finally(() => {
|
||||
api.updateInstanceArtifacts({
|
||||
runId: options.runId,
|
||||
instanceId: options.instanceId,
|
||||
...uploadReport,
|
||||
})
|
||||
.catch((err) => {
|
||||
debug('failed updating artifact status %o', {
|
||||
stack: err.stack,
|
||||
})
|
||||
|
||||
errors.warning('CLOUD_CANNOT_UPLOAD_ARTIFACTS', err)
|
||||
|
||||
// don't log exceptions if we have a 503 status code
|
||||
if (err.statusCode !== 503) {
|
||||
return exception.create(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const updateInstanceStdout = (options = {}) => {
|
||||
@@ -720,12 +797,16 @@ const createRunAndRecordSpecs = (options = {}) => {
|
||||
}
|
||||
|
||||
const { video, shouldUploadVideo, screenshots } = results
|
||||
const { videoUploadUrl, screenshotUploadUrls } = resp
|
||||
const { videoUploadUrl, captureUploadUrl, screenshotUploadUrls } = resp
|
||||
|
||||
return uploadArtifacts({
|
||||
runId,
|
||||
instanceId,
|
||||
video,
|
||||
screenshots,
|
||||
videoUploadUrl,
|
||||
captureUploadUrl,
|
||||
protocolManager,
|
||||
shouldUploadVideo,
|
||||
screenshotUploadUrls,
|
||||
quiet,
|
||||
|
||||
@@ -25,7 +25,7 @@ import type { SpecWithRelativeRoot, SpecFile, TestingType, OpenProjectLaunchOpts
|
||||
import type { Cfg } from '../project-base'
|
||||
import type { Browser } from '../browsers/types'
|
||||
import * as printResults from '../util/print-run'
|
||||
import ProtocolManager from '../cloud/protocol'
|
||||
import { ProtocolManager } from '../cloud/protocol'
|
||||
import { telemetry } from '@packages/telemetry'
|
||||
|
||||
type SetScreenshotMetadata = (data: TakeScreenshotProps) => void
|
||||
|
||||
@@ -20,7 +20,7 @@ import { SocketE2E } from './socket-e2e'
|
||||
import { ensureProp } from './util/class-helpers'
|
||||
|
||||
import system from './util/system'
|
||||
import type { BannersState, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ReceivedCypressOptions, ResolvedConfigurationOptions, TestingType, VideoRecording, ProtocolManager } from '@packages/types'
|
||||
import type { BannersState, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ReceivedCypressOptions, ResolvedConfigurationOptions, TestingType, VideoRecording, ProtocolManagerShape } from '@packages/types'
|
||||
import { DataContext, getCtx } from '@packages/data-context'
|
||||
import { createHmac } from 'crypto'
|
||||
|
||||
@@ -143,7 +143,7 @@ export class ProjectBase<TServer extends Server> extends EE {
|
||||
: new ServerCt() as TServer
|
||||
}
|
||||
|
||||
async open (protocolManager?: ProtocolManager) {
|
||||
async open (protocolManager?: ProtocolManagerShape) {
|
||||
debug('opening project instance %s', this.projectRoot)
|
||||
debug('project open options %o', this.options)
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import type { Browser } from '@packages/server/lib/browsers/types'
|
||||
import { InitializeRoutes, createCommonRoutes } from './routes'
|
||||
import { createRoutesE2E } from './routes-e2e'
|
||||
import { createRoutesCT } from './routes-ct'
|
||||
import type { FoundSpec, ProtocolManager } from '@packages/types'
|
||||
import type { FoundSpec, ProtocolManagerShape } from '@packages/types'
|
||||
import type { Server as WebSocketServer } from 'ws'
|
||||
import { RemoteStates } from './remote_states'
|
||||
import { cookieJar, SerializableAutomationCookie } from './util/cookies'
|
||||
@@ -109,7 +109,7 @@ export interface OpenServerOptions {
|
||||
getCurrentBrowser: () => Browser
|
||||
getSpec: () => FoundSpec | null
|
||||
shouldCorrelatePreRequests: () => boolean
|
||||
protocolManager?: ProtocolManager
|
||||
protocolManager?: ProtocolManagerShape
|
||||
}
|
||||
|
||||
export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
|
||||
|
||||
@@ -28,7 +28,7 @@ import { telemetry } from '@packages/telemetry'
|
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
import type { Socket } from '@packages/socket'
|
||||
|
||||
import type { RunState, CachedTestState, ProtocolManager } from '@packages/types'
|
||||
import type { RunState, CachedTestState, ProtocolManagerShape } from '@packages/types'
|
||||
import { cors } from '@packages/network'
|
||||
import memory from './browsers/memory'
|
||||
|
||||
@@ -52,10 +52,10 @@ export class SocketBase {
|
||||
protected supportsRunEvents: boolean
|
||||
protected ended: boolean
|
||||
protected _io?: socketIo.SocketIOServer
|
||||
protected protocolManager?: ProtocolManager
|
||||
protected protocolManager?: ProtocolManagerShape
|
||||
localBus: EventEmitter
|
||||
|
||||
constructor (config: Record<string, any>, protocolManager?: ProtocolManager) {
|
||||
constructor (config: Record<string, any>, protocolManager?: ProtocolManagerShape) {
|
||||
this.inRunMode = config.isTextTerminal
|
||||
this.supportsRunEvents = config.isTextTerminal || config.experimentalInteractiveRunEvents
|
||||
this.ended = false
|
||||
|
||||
@@ -5,14 +5,14 @@ import dfd from 'p-defer'
|
||||
import type { Socket } from '@packages/socket'
|
||||
import type { DestroyableHttpServer } from '@packages/server/lib/util/server_destroy'
|
||||
import assert from 'assert'
|
||||
import type { ProtocolManager } from '@packages/types'
|
||||
import type { ProtocolManagerShape } from '@packages/types'
|
||||
|
||||
const debug = Debug('cypress:server:socket-ct')
|
||||
|
||||
export class SocketCt extends SocketBase {
|
||||
#destroyAutPromise?: dfd.DeferredPromise<void>
|
||||
|
||||
constructor (config: Record<string, any>, protocolManager?: ProtocolManager) {
|
||||
constructor (config: Record<string, any>, protocolManager?: ProtocolManagerShape) {
|
||||
super(config, protocolManager)
|
||||
|
||||
// should we use this option at all for component testing 😕?
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SocketBase } from './socket-base'
|
||||
import { fs } from './util/fs'
|
||||
import type { DestroyableHttpServer } from './util/server_destroy'
|
||||
import * as studio from './studio'
|
||||
import type { FoundSpec, ProtocolManager } from '@packages/types'
|
||||
import type { FoundSpec, ProtocolManagerShape } from '@packages/types'
|
||||
|
||||
const debug = Debug('cypress:server:socket-e2e')
|
||||
|
||||
@@ -15,7 +15,7 @@ const isSpecialSpec = (name) => {
|
||||
export class SocketE2E extends SocketBase {
|
||||
private testFilePath: string | null
|
||||
|
||||
constructor (config: Record<string, any>, protocolManager?: ProtocolManager) {
|
||||
constructor (config: Record<string, any>, protocolManager?: ProtocolManagerShape) {
|
||||
super(config, protocolManager)
|
||||
|
||||
this.testFilePath = null
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "electron-rebuild -o better-sqlite3",
|
||||
"build-prod": "tsc || echo 'built, with type errors'",
|
||||
"check-ts": "tsc --noEmit && yarn -s tslint",
|
||||
"clean-deps": "rimraf node_modules",
|
||||
@@ -13,6 +12,7 @@
|
||||
"docker": "cd ../.. && WORKING_DIR=/packages/server ./scripts/run-docker-local.sh",
|
||||
"tslint": "tslint --config ../ts/tslint.json --project .",
|
||||
"postinstall": "patch-package",
|
||||
"rebuild-better-sqlite3": "electron-rebuild -o better-sqlite3",
|
||||
"repl": "node repl.js",
|
||||
"start": "node ../../scripts/cypress open --dev --global",
|
||||
"test": "node ./test/scripts/run.js",
|
||||
@@ -138,7 +138,6 @@
|
||||
"@babel/core": "7.9.0",
|
||||
"@babel/preset-env": "7.9.0",
|
||||
"@cypress/debugging-proxy": "2.0.1",
|
||||
"@cypress/json-schemas": "5.39.0",
|
||||
"@cypress/sinon-chai": "2.9.1",
|
||||
"@cypress/webpack-dev-server": "0.0.0-development",
|
||||
"@electron/rebuild": "3.2.10",
|
||||
@@ -166,6 +165,7 @@
|
||||
"@types/http-proxy": "1.17.4",
|
||||
"@types/mime": "3.0.1",
|
||||
"@types/node": "14.14.31",
|
||||
"@types/request-promise": "^4.1.48",
|
||||
"babel-loader": "8.1.0",
|
||||
"chai": "1.10.0",
|
||||
"chai-as-promised": "7.1.1",
|
||||
@@ -175,6 +175,7 @@
|
||||
"cross-env": "6.0.3",
|
||||
"devtools-protocol": "0.0.1124027",
|
||||
"eol": "0.9.1",
|
||||
"esbuild": "^0.15.3",
|
||||
"eventsource": "2.0.2",
|
||||
"https-proxy-agent": "3.0.1",
|
||||
"mocha": "7.1.0",
|
||||
@@ -223,4 +224,4 @@
|
||||
"fsevents": "^2",
|
||||
"registry-js": "1.15.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/* global Debug */
|
||||
|
||||
const AppCaptureProtocol = class {
|
||||
constructor () {
|
||||
this.Debug = Debug
|
||||
|
||||
this.connectToBrowser = this.connectToBrowser.bind(this)
|
||||
this.addRunnables = this.addRunnables.bind(this)
|
||||
this.beforeSpec = this.beforeSpec.bind(this)
|
||||
this.afterSpec = this.afterSpec.bind(this)
|
||||
this.beforeTest = this.beforeTest.bind(this)
|
||||
this.commandLogAdded = this.commandLogAdded.bind(this)
|
||||
this.commandLogChanged = this.commandLogChanged.bind(this)
|
||||
this.viewportChanged = this.viewportChanged.bind(this)
|
||||
this.urlChanged = this.urlChanged.bind(this)
|
||||
}
|
||||
|
||||
connectToBrowser (cdpClient) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
addRunnables (runnables) {}
|
||||
beforeSpec (spec) {}
|
||||
afterSpec (spec) {}
|
||||
beforeTest (test) {}
|
||||
commandLogAdded (log) {}
|
||||
commandLogChanged (log) {}
|
||||
viewportChanged (input) {}
|
||||
urlChanged (input) {}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AppCaptureProtocol,
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { ProtocolManagerShape } from '@packages/types'
|
||||
|
||||
declare const Debug: (namespace) => import('debug').IDebugger
|
||||
declare const performance: {
|
||||
now(): number
|
||||
timeOrigin: number
|
||||
}
|
||||
|
||||
export class AppCaptureProtocol implements ProtocolManagerShape {
|
||||
private Debug: typeof Debug
|
||||
private performance: typeof performance
|
||||
|
||||
constructor () {
|
||||
this.Debug = Debug
|
||||
this.performance = performance
|
||||
}
|
||||
|
||||
setupProtocol = (script, runId) => {
|
||||
return Promise.resolve()
|
||||
}
|
||||
connectToBrowser = (cdpClient) => {
|
||||
return Promise.resolve()
|
||||
}
|
||||
addRunnables = (runnables) => {}
|
||||
beforeSpec = (spec) => {}
|
||||
afterSpec = () => {
|
||||
return Promise.resolve()
|
||||
}
|
||||
beforeTest = (test) => {}
|
||||
commandLogAdded = (log) => {}
|
||||
commandLogChanged = (log) => {}
|
||||
viewportChanged = (input) => {}
|
||||
urlChanged = (input) => {}
|
||||
sendErrors (errors) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
uploadCaptureArtifact (uploadUrl) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
afterTest (test): void {}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { expect, proxyquire, sinon } from '../../spec_helper'
|
||||
import * as protocol from '../../../lib/browsers/protocol'
|
||||
import { stripAnsi } from '@packages/errors'
|
||||
import net from 'net'
|
||||
import { ProtocolManager } from '@packages/types'
|
||||
import { ProtocolManagerShape } from '@packages/types'
|
||||
|
||||
const HOST = '127.0.0.1'
|
||||
const PORT = 50505
|
||||
@@ -23,7 +23,7 @@ describe('lib/browsers/cri-client', function () {
|
||||
Version: sinon.SinonStub
|
||||
}
|
||||
let onError: sinon.SinonStub
|
||||
let getClient: (protocolManager?: ProtocolManager) => ReturnType<typeof BrowserCriClient.create>
|
||||
let getClient: (protocolManager?: ProtocolManagerShape) => ReturnType<typeof BrowserCriClient.create>
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(protocol, '_connectAsync')
|
||||
|
||||
@@ -28,6 +28,11 @@ const AUTH_URLS = {
|
||||
'dashboardLogoutUrl': 'http://localhost:3000/logout',
|
||||
}
|
||||
|
||||
const {
|
||||
CYPRESS_LOCAL_PROTOCOL_STUB,
|
||||
CYPRESS_LOCAL_PROTOCOL_STUB_SIGN,
|
||||
} = require('@tooling/system-tests/lib/protocolStubResponse')
|
||||
|
||||
const makeError = (details = {}) => {
|
||||
return _.extend(new Error(details.message || 'Some error'), details)
|
||||
}
|
||||
@@ -527,7 +532,8 @@ describe('lib/cloud/api', () => {
|
||||
beforeEach(function () {
|
||||
this.protocolManager = {
|
||||
setupProtocol: sinon.stub(),
|
||||
},
|
||||
}
|
||||
|
||||
this.buildProps = {
|
||||
group: null,
|
||||
parallel: null,
|
||||
@@ -550,6 +556,7 @@ describe('lib/cloud/api', () => {
|
||||
},
|
||||
specs: ['foo.js', 'bar.js'],
|
||||
runnerCapabilities: {
|
||||
'protocolMountVersion': 1,
|
||||
'dynamicSpecsInSerialMode': true,
|
||||
'skipSpecAction': true,
|
||||
},
|
||||
@@ -557,6 +564,12 @@ describe('lib/cloud/api', () => {
|
||||
})
|
||||
|
||||
it('POST /runs + returns runId', function () {
|
||||
nock(API_BASEURL)
|
||||
.get('/capture-protocol/script/protocolStub.js')
|
||||
.reply(200, CYPRESS_LOCAL_PROTOCOL_STUB, {
|
||||
'x-cypress-signature': CYPRESS_LOCAL_PROTOCOL_STUB_SIGN,
|
||||
})
|
||||
|
||||
nock(API_BASEURL)
|
||||
.matchHeader('x-route-version', '4')
|
||||
.matchHeader('x-os-name', 'linux')
|
||||
@@ -564,6 +577,7 @@ describe('lib/cloud/api', () => {
|
||||
.post('/runs', this.buildProps)
|
||||
.reply(200, {
|
||||
runId: 'new-run-id-123',
|
||||
captureProtocolUrl: 'http://localhost:1234/capture-protocol/script/protocolStub.js',
|
||||
})
|
||||
|
||||
return api.createRun({
|
||||
@@ -571,7 +585,11 @@ describe('lib/cloud/api', () => {
|
||||
protocolManager: this.protocolManager,
|
||||
})
|
||||
.then((ret) => {
|
||||
expect(ret).to.deep.eq({ runId: 'new-run-id-123' })
|
||||
expect(ret).to.deep.eq({
|
||||
runId: 'new-run-id-123',
|
||||
captureProtocolUrl: 'http://localhost:1234/capture-protocol/script/protocolStub.js',
|
||||
})
|
||||
|
||||
expect(this.protocolManager.setupProtocol).to.be.called
|
||||
})
|
||||
})
|
||||
@@ -581,6 +599,12 @@ describe('lib/cloud/api', () => {
|
||||
sinon.restore()
|
||||
sinon.stub(os, 'platform').returns('linux')
|
||||
|
||||
nock(API_BASEURL)
|
||||
.get('/capture-protocol/script/protocolStub.js')
|
||||
.reply(200, CYPRESS_LOCAL_PROTOCOL_STUB, {
|
||||
'x-cypress-signature': CYPRESS_LOCAL_PROTOCOL_STUB_SIGN,
|
||||
})
|
||||
|
||||
preflightNock(API_BASEURL)
|
||||
.reply(200, decryptReqBodyAndRespond({
|
||||
resBody: {
|
||||
@@ -598,6 +622,7 @@ describe('lib/cloud/api', () => {
|
||||
reqBody: this.buildProps,
|
||||
resBody: {
|
||||
runId: 'new-run-id-123',
|
||||
captureProtocolUrl: 'http://localhost:1234/capture-protocol/script/protocolStub.js',
|
||||
},
|
||||
}))
|
||||
}))
|
||||
@@ -607,11 +632,46 @@ describe('lib/cloud/api', () => {
|
||||
protocolManager: this.protocolManager,
|
||||
})
|
||||
.then((ret) => {
|
||||
expect(ret).to.deep.eq({ runId: 'new-run-id-123' })
|
||||
expect(ret).to.deep.eq({
|
||||
runId: 'new-run-id-123',
|
||||
captureProtocolUrl: 'http://localhost:1234/capture-protocol/script/protocolStub.js',
|
||||
})
|
||||
|
||||
expect(this.protocolManager.setupProtocol).to.be.called
|
||||
})
|
||||
})
|
||||
|
||||
it('POST /runs does not call setupProtocol with invalid signature', function () {
|
||||
nock(API_BASEURL)
|
||||
.get('/capture-protocol/script/protocolStub.js')
|
||||
.reply(200, CYPRESS_LOCAL_PROTOCOL_STUB, {
|
||||
'x-cypress-signature': 'invalid',
|
||||
})
|
||||
|
||||
nock(API_BASEURL)
|
||||
.matchHeader('x-route-version', '4')
|
||||
.matchHeader('x-os-name', 'linux')
|
||||
.matchHeader('x-cypress-version', pkg.version)
|
||||
.post('/runs', this.buildProps)
|
||||
.reply(200, {
|
||||
runId: 'new-run-id-123',
|
||||
captureProtocolUrl: 'http://localhost:1234/capture-protocol/script/protocolStub.js',
|
||||
})
|
||||
|
||||
return api.createRun({
|
||||
...this.buildProps,
|
||||
protocolManager: this.protocolManager,
|
||||
})
|
||||
.then((ret) => {
|
||||
expect(ret).to.deep.eq({
|
||||
runId: 'new-run-id-123',
|
||||
captureProtocolUrl: 'http://localhost:1234/capture-protocol/script/protocolStub.js',
|
||||
})
|
||||
|
||||
expect(this.protocolManager.setupProtocol).not.to.be.called
|
||||
})
|
||||
})
|
||||
|
||||
it('POST /runs failure formatting', function () {
|
||||
nock(API_BASEURL)
|
||||
.matchHeader('x-route-version', '4')
|
||||
@@ -1400,4 +1460,43 @@ describe('lib/cloud/api', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('.updateInstanceArtifacts', () => {
|
||||
beforeEach(function () {
|
||||
this.artifactProps = {
|
||||
runId: 'run-id-123',
|
||||
instanceId: 'instance-id-123',
|
||||
screenshots: [{
|
||||
url: `http://localhost:1234/screenshots/upload/instance-id-123/a877e957-f90e-4ba4-9fa8-569812f148c4.png`,
|
||||
uploadSize: 100,
|
||||
}],
|
||||
video: {
|
||||
url: `http://localhost:1234/video/upload/instance-id-123/f17754c4-581d-4e08-a922-1fa402f9c6de.mp4`,
|
||||
uploadSize: 122,
|
||||
},
|
||||
protocol: {
|
||||
url: `http://localhost:1234/protocol/upload/instance-id-123/2ed89c81-e7eb-4b97-8a6e-185c410471df.db`,
|
||||
uploadSize: 123,
|
||||
},
|
||||
}
|
||||
// TODO: add schema validation
|
||||
})
|
||||
|
||||
it('PUTs/instances/:id/artifacts', function () {
|
||||
nock(API_BASEURL)
|
||||
.matchHeader('x-route-version', '1')
|
||||
.matchHeader('x-cypress-run-id', this.artifactProps.runId)
|
||||
.matchHeader('x-cypress-request-attempt', '0')
|
||||
.matchHeader('x-os-name', 'linux')
|
||||
.matchHeader('x-cypress-version', pkg.version)
|
||||
.put('/instances/instance-id-123/artifacts', {
|
||||
protocol: this.artifactProps.protocol,
|
||||
screenshots: this.artifactProps.screenshots,
|
||||
video: this.artifactProps.video,
|
||||
})
|
||||
.reply(200)
|
||||
|
||||
return api.updateInstanceArtifacts(this.artifactProps)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,46 +1,85 @@
|
||||
import { proxyquire } from '../../spec_helper'
|
||||
import path from 'path'
|
||||
import os from 'os'
|
||||
import type { AppCaptureProtocolInterface, ProtocolManager as ProtocolManagerInterface } from '@packages/types'
|
||||
import type { AppCaptureProtocolInterface, ProtocolManagerShape } from '@packages/types'
|
||||
import { expect } from 'chai'
|
||||
import { EventEmitter } from 'stream'
|
||||
import esbuild from 'esbuild'
|
||||
|
||||
class TestClient extends EventEmitter {
|
||||
send: sinon.SinonStub = sinon.stub()
|
||||
}
|
||||
|
||||
const mockDb = sinon.stub()
|
||||
const mockDatabase = sinon.stub().returns(mockDb)
|
||||
|
||||
const { default: ProtocolManager } = proxyquire('../lib/cloud/protocol', {
|
||||
const { ProtocolManager } = proxyquire('../lib/cloud/protocol', {
|
||||
'better-sqlite3': mockDatabase,
|
||||
}) as typeof import('@packages/server/lib/cloud/protocol')
|
||||
|
||||
const { outputFiles: [{ contents: stubProtocolRaw }] } = esbuild.buildSync({
|
||||
entryPoints: [path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'protocol', 'test-protocol.ts')],
|
||||
bundle: true,
|
||||
format: 'cjs',
|
||||
write: false,
|
||||
})
|
||||
const stubProtocol = new TextDecoder('utf-8').decode(stubProtocolRaw)
|
||||
|
||||
describe('lib/cloud/protocol', () => {
|
||||
let protocolManager: ProtocolManagerInterface
|
||||
let protocolManager: ProtocolManagerShape
|
||||
let protocol: AppCaptureProtocolInterface
|
||||
|
||||
beforeEach(async () => {
|
||||
process.env.CYPRESS_LOCAL_PROTOCOL_PATH = path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'protocol', 'test-protocol.js')
|
||||
|
||||
protocolManager = new ProtocolManager()
|
||||
|
||||
await protocolManager.setupProtocol()
|
||||
await protocolManager.setupProtocol(stubProtocol, '1')
|
||||
|
||||
protocol = (protocolManager as any).protocol
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.CYPRESS_LOCAL_PROTOCOL_PATH
|
||||
protocol = (protocolManager as any)._protocol
|
||||
})
|
||||
|
||||
it('should be able to setup the protocol', () => {
|
||||
expect(protocol).not.to.be.undefined
|
||||
expect((protocol as any).Debug).not.to.be.undefined
|
||||
expect((protocol as any).performance).not.to.be.undefined
|
||||
expect((protocol as any).performance.now).not.to.be.undefined
|
||||
expect((protocol as any).performance.timeOrigin).not.to.be.undefined
|
||||
})
|
||||
|
||||
it('should be able to connect to the browser', async () => {
|
||||
const mockCdpClient = sinon.stub()
|
||||
const mockCdpClient = new TestClient()
|
||||
|
||||
sinon.stub(protocol, 'connectToBrowser').resolves()
|
||||
const connectToBrowserStub = sinon.stub(protocol, 'connectToBrowser').resolves()
|
||||
|
||||
await protocolManager.connectToBrowser(mockCdpClient as any)
|
||||
|
||||
expect(protocol.connectToBrowser).to.be.calledWith(mockCdpClient)
|
||||
const newCdpClient = connectToBrowserStub.getCall(0).args[0]
|
||||
|
||||
newCdpClient.send('Page.enable')
|
||||
expect(mockCdpClient.send).to.be.calledWith('Page.enable')
|
||||
|
||||
const mockSuccess = sinon.stub()
|
||||
|
||||
newCdpClient.on('Page.loadEventFired', mockSuccess)
|
||||
|
||||
const mockThrows = sinon.stub().throws()
|
||||
|
||||
newCdpClient.on('Page.backForwardCacheNotUsed', mockThrows)
|
||||
|
||||
mockCdpClient.emit('Page.loadEventFired')
|
||||
|
||||
expect(mockSuccess).to.be.called
|
||||
expect((protocolManager as any)._errors).to.be.empty
|
||||
|
||||
mockCdpClient.emit('Page.backForwardCacheNotUsed', { test: 'test1' })
|
||||
|
||||
expect(mockThrows).to.be.called
|
||||
expect((protocolManager as any)._errors).to.have.length(1)
|
||||
expect((protocolManager as any)._errors[0].captureMethod).to.equal('cdpClient.on')
|
||||
expect((protocolManager as any)._errors[0].args).to.deep.equal([
|
||||
'Page.backForwardCacheNotUsed',
|
||||
{
|
||||
test: 'test1',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should be able to initialize a new spec', () => {
|
||||
|
||||
@@ -13,29 +13,31 @@ export interface CDPClient {
|
||||
|
||||
// TODO(protocol): This is basic for now but will evolve as we progress with the protocol work
|
||||
|
||||
export interface AppCaptureProtocolInterface {
|
||||
export interface AppCaptureProtocolCommon {
|
||||
addRunnables (runnables: any): void
|
||||
connectToBrowser (cdpClient: CDPClient): Promise<void>
|
||||
beforeSpec (db: Database): void
|
||||
afterSpec (): Promise<void>
|
||||
beforeTest(test: Record<string, any>): void
|
||||
afterTest(test: Record<string, any>): void
|
||||
commandLogAdded (log: any): void
|
||||
commandLogChanged (log: any): void
|
||||
viewportChanged (input: any): void
|
||||
urlChanged (input: any): void
|
||||
beforeTest(test: Record<string, any>): void
|
||||
afterTest(test: Record<string, any>): void
|
||||
afterSpec (): Promise<void>
|
||||
connectToBrowser (cdpClient: CDPClient): Promise<void>
|
||||
}
|
||||
|
||||
export interface ProtocolManager {
|
||||
setupProtocol(url?: string): Promise<void>
|
||||
addRunnables (runnables: any): void
|
||||
connectToBrowser (cdpClient: CDPClient): Promise<void>
|
||||
beforeSpec (spec: { instanceId: string}): void
|
||||
afterSpec (): Promise<void>
|
||||
beforeTest(test: Record<string, any>): void
|
||||
afterTest(test: Record<string, any>): void
|
||||
commandLogAdded (log: any): void
|
||||
commandLogChanged (log: any): void
|
||||
viewportChanged (input: any): void
|
||||
urlChanged (input: any): void
|
||||
export interface AppCaptureProtocolInterface extends AppCaptureProtocolCommon {
|
||||
beforeSpec (db: Database): void
|
||||
}
|
||||
|
||||
export interface ProtocolError {
|
||||
args?: any
|
||||
error: Error
|
||||
captureMethod: keyof AppCaptureProtocolInterface | 'setupProtocol' | 'uploadCaptureArtifact' | 'getCaptureProtocolScript' | 'cdpClient.on'
|
||||
}
|
||||
|
||||
export interface ProtocolManagerShape extends AppCaptureProtocolCommon {
|
||||
setupProtocol(script: string, runId: string): Promise<void>
|
||||
beforeSpec (spec: { instanceId: string}): void
|
||||
sendErrors (errors: ProtocolError[]): Promise<void>
|
||||
uploadCaptureArtifact(uploadUrl: string): Promise<{ fileSize: number, success: boolean, error?: string } | void>
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { FoundBrowser } from './browser'
|
||||
import type { ReceivedCypressOptions } from './config'
|
||||
import type { PlatformName } from './platform'
|
||||
import type { RunModeVideoApi } from './video'
|
||||
import type { ProtocolManager } from './protocol'
|
||||
import type { ProtocolManagerShape } from './protocol'
|
||||
|
||||
export type OpenProjectLaunchOpts = {
|
||||
projectRoot: string
|
||||
@@ -11,7 +11,7 @@ export type OpenProjectLaunchOpts = {
|
||||
videoApi?: RunModeVideoApi
|
||||
onWarning: (err: Error) => void
|
||||
onError: (err: Error) => void
|
||||
protocolManager?: ProtocolManager
|
||||
protocolManager?: ProtocolManagerShape
|
||||
}
|
||||
|
||||
export type BrowserLaunchOpts = {
|
||||
@@ -23,7 +23,7 @@ export type BrowserLaunchOpts = {
|
||||
onBrowserClose?: (...args: unknown[]) => void
|
||||
onBrowserOpen?: (...args: unknown[]) => void
|
||||
relaunchBrowser?: () => Promise<any>
|
||||
protocolManager?: ProtocolManager
|
||||
protocolManager?: ProtocolManagerShape
|
||||
} & Partial<OpenProjectLaunchOpts> // TODO: remove the `Partial` here by making it impossible for openProject.launch to be called w/o OpenProjectLaunchOpts
|
||||
& Pick<ReceivedCypressOptions, 'userAgent' | 'proxyUrl' | 'socketIoRoute' | 'chromeWebSecurity' | 'downloadsFolder' | 'experimentalModifyObstructiveThirdPartyCode' | 'experimentalWebKitSupport'>
|
||||
|
||||
@@ -94,7 +94,7 @@ export interface OpenProjectLaunchOptions {
|
||||
onError?: (err: Error) => void
|
||||
|
||||
// Manager used to communicate with the Cloud protocol
|
||||
protocolManager?: ProtocolManager
|
||||
protocolManager?: ProtocolManagerShape
|
||||
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ export async function buildCypressApp (options: BuildCypressAppOpts) {
|
||||
...packageJsonContents,
|
||||
scripts: {
|
||||
// After the `yarn --production` install, we need to patch packages and trigger a server build to rebuild native bindings for `better-sqlite3`
|
||||
postinstall: 'patch-package && yarn workspace @packages/server build',
|
||||
postinstall: 'patch-package && yarn workspace @packages/server rebuild-better-sqlite3',
|
||||
},
|
||||
}, { spaces: 2 })
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { execSync } from 'child_process'
|
||||
import { webpackReporter, webpackRunner } from './tasks/gulpWebpack'
|
||||
import { e2eTestScaffold, e2eTestScaffoldWatch } from './tasks/gulpE2ETestScaffold'
|
||||
import dedent from 'dedent'
|
||||
import { ensureCloudValidations, syncCloudValidations } from './tasks/gulpSyncValidations'
|
||||
|
||||
if (process.env.CYPRESS_INTERNAL_VITE_DEV) {
|
||||
process.env.CYPRESS_INTERNAL_VITE_APP_PORT ??= '3333'
|
||||
@@ -64,6 +65,7 @@ gulp.task(
|
||||
makePathMap,
|
||||
// Before dev, fetch the latest "remote" schema from Cypress Cloud
|
||||
syncRemoteGraphQL,
|
||||
syncCloudValidations,
|
||||
gulp.parallel(
|
||||
viteClean,
|
||||
e2eTestScaffoldWatch,
|
||||
@@ -264,6 +266,8 @@ gulp.task(makePackage)
|
||||
* here for debugging, e.g. `yarn gulp syncRemoteGraphQL`
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
gulp.task(ensureCloudValidations)
|
||||
gulp.task(syncCloudValidations)
|
||||
gulp.task(syncRemoteGraphQL)
|
||||
gulp.task(generateFrontendSchema)
|
||||
gulp.task(makePathMap)
|
||||
|
||||
40
scripts/gulp/tasks/gulpSyncValidations.ts
Normal file
40
scripts/gulp/tasks/gulpSyncValidations.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import crossFetch from 'cross-fetch'
|
||||
|
||||
const INTERNAL_CLOUD_ENV = process.env.CYPRESS_INTERNAL_ENV || 'production'
|
||||
|
||||
const CY_CLOUD_VALIDATION_BASE = {
|
||||
test: 'https://api.cypress.io',
|
||||
production: 'https://api.cypress.io',
|
||||
staging: 'https://api-staging.cypress.io',
|
||||
development: 'http://localhost:1234',
|
||||
}
|
||||
|
||||
const VALIDATION_BASE = CY_CLOUD_VALIDATION_BASE[INTERNAL_CLOUD_ENV]
|
||||
|
||||
const OUTPUT_FOLDER = path.join(__dirname, '../../../system-tests/lib/validations')
|
||||
|
||||
export async function syncCloudValidations () {
|
||||
const [validationsResponse, typesResponse] = await Promise.all([
|
||||
crossFetch(`${VALIDATION_BASE}/cypress-app/validations`),
|
||||
crossFetch(`${VALIDATION_BASE}/cypress-app/validations/types`),
|
||||
])
|
||||
const [validations, validationsTypes] = await Promise.all([
|
||||
validationsResponse.text(),
|
||||
typesResponse.text(),
|
||||
])
|
||||
|
||||
await fs.ensureDir(OUTPUT_FOLDER)
|
||||
|
||||
await Promise.all([
|
||||
fs.promises.writeFile(path.join(OUTPUT_FOLDER, 'cloudValidations.js'), validations),
|
||||
fs.promises.writeFile(path.join(OUTPUT_FOLDER, 'cloudValidations.d.ts'), validationsTypes),
|
||||
])
|
||||
}
|
||||
|
||||
export async function ensureCloudValidations () {
|
||||
if (!fs.existsSync(path.join(OUTPUT_FOLDER, 'cloudValidations.js')) || !fs.existsSync(path.join(OUTPUT_FOLDER, 'cloudValidations.d.ts'))) {
|
||||
await syncCloudValidations()
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ const { execSync } = require('child_process')
|
||||
const executionEnv = process.env.CI ? 'ci' : 'local'
|
||||
|
||||
const postInstallCommands = {
|
||||
local: 'patch-package && yarn-deduplicate --strategy=highest && yarn clean && gulp postinstall && yarn build && yarn build-v8-snapshot-dev',
|
||||
ci: 'patch-package && yarn clean && gulp postinstall',
|
||||
local: 'patch-package && yarn-deduplicate --strategy=highest && yarn clean && gulp postinstall && yarn workspace @packages/server rebuild-better-sqlite3 && yarn build && yarn build-v8-snapshot-dev',
|
||||
ci: 'patch-package && yarn clean && gulp postinstall && yarn workspace @packages/server rebuild-better-sqlite3',
|
||||
}
|
||||
|
||||
execSync(postInstallCommands[executionEnv], {
|
||||
|
||||
@@ -1073,8 +1073,8 @@ Details:
|
||||
|
||||
{
|
||||
"code": "OUT_OF_TIME",
|
||||
"name": "OutOfTime",
|
||||
"hadTime": 1000,
|
||||
"name": "OutOfTime",
|
||||
"spentTime": 999
|
||||
}
|
||||
|
||||
@@ -2258,13 +2258,14 @@ exports['e2e record quiet mode respects quiet mode 1'] = `
|
||||
exports['e2e record api interaction errors create run 412 errors and exits when request schema is invalid 1'] = `
|
||||
Recording this run failed. The request was invalid.
|
||||
|
||||
request should follow postRunRequest@2.0.0 schema
|
||||
Request Validation Error
|
||||
|
||||
Errors:
|
||||
|
||||
[
|
||||
"data has additional properties: group, parallel, ciBuildId, tags, testingType, runnerCapabilities",
|
||||
"data.platform is the wrong type"
|
||||
"ci is the wrong type, saw null, expected object",
|
||||
"commit is the wrong type, saw null, expected object",
|
||||
"platform is the wrong type, saw null, expected object"
|
||||
]
|
||||
|
||||
Request Sent:
|
||||
@@ -2280,7 +2281,7 @@ Request Sent:
|
||||
"parallel": null,
|
||||
"ciBuildId": null,
|
||||
"projectId": "pid123",
|
||||
"recordKey": "f858a2bc-b469-4e48-be67-0876339ee7e1",
|
||||
"recordKey": "f85...7e1",
|
||||
"specPattern": "cypress/e2e/record_pass*",
|
||||
"tags": [
|
||||
""
|
||||
@@ -2288,7 +2289,8 @@ Request Sent:
|
||||
"testingType": "e2e",
|
||||
"runnerCapabilities": {
|
||||
"dynamicSpecsInSerialMode": true,
|
||||
"skipSpecAction": true
|
||||
"skipSpecAction": true,
|
||||
"protocolMountVersion": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2773,3 +2775,78 @@ If you are trying to parallelize this run, then also pass the --parallel flag, e
|
||||
https://on.cypress.io/run-group-name-not-unique
|
||||
|
||||
`
|
||||
|
||||
exports['e2e record capture-protocol passing retrieves the capture protocol 1'] = `
|
||||
|
||||
====================================================================================================
|
||||
|
||||
(Run Starting)
|
||||
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Cypress: 1.2.3 │
|
||||
│ Browser: FooBrowser 88 │
|
||||
│ Specs: 1 found (record_pass.cy.js) │
|
||||
│ Searched: cypress/e2e/record_pass* │
|
||||
│ Params: Tag: false, Group: false, Parallel: false │
|
||||
│ Run URL: https://dashboard.cypress.io/projects/cjvoj7/runs/12 │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Running: record_pass.cy.js (1 of 1)
|
||||
Estimated: X second(s)
|
||||
|
||||
|
||||
record pass
|
||||
✓ passes
|
||||
- is pending
|
||||
|
||||
|
||||
1 passing
|
||||
1 pending
|
||||
|
||||
|
||||
(Results)
|
||||
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Tests: 2 │
|
||||
│ Passing: 1 │
|
||||
│ Failing: 0 │
|
||||
│ Pending: 1 │
|
||||
│ Skipped: 0 │
|
||||
│ Screenshots: 1 │
|
||||
│ Video: false │
|
||||
│ Duration: X seconds │
|
||||
│ Estimated: X second(s) │
|
||||
│ Spec Ran: record_pass.cy.js │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
(Screenshots)
|
||||
|
||||
- /XXX/XXX/XXX/cypress/screenshots/record_pass.cy.js/yay it passes.png (400x1022)
|
||||
|
||||
|
||||
(Uploading Results)
|
||||
|
||||
- Done Uploading (1/1) /foo/bar/.projects/e2e/cypress/screenshots/record_pass.cy.js/yay it passes.png
|
||||
|
||||
====================================================================================================
|
||||
|
||||
(Run Finished)
|
||||
|
||||
|
||||
Spec Tests Passing Failing Pending Skipped
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ✔ record_pass.cy.js XX:XX 2 1 - 1 - │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
✔ All specs passed! XX:XX 2 1 - 1 -
|
||||
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Recorded Run: https://dashboard.cypress.io/projects/cjvoj7/runs/12
|
||||
|
||||
|
||||
`
|
||||
|
||||
41
system-tests/lib/protocolStub.ts
Normal file
41
system-tests/lib/protocolStub.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { ProtocolManagerShape } from '@packages/types'
|
||||
|
||||
declare const Debug: (namespace) => import('debug').IDebugger
|
||||
declare const performance: {
|
||||
now(): number
|
||||
timeOrigin: number
|
||||
}
|
||||
|
||||
export class AppCaptureProtocol implements ProtocolManagerShape {
|
||||
private Debug: typeof Debug
|
||||
private performance: typeof performance
|
||||
|
||||
constructor () {
|
||||
this.Debug = Debug
|
||||
this.performance = performance
|
||||
}
|
||||
|
||||
setupProtocol = (script, runId) => {
|
||||
return Promise.resolve()
|
||||
}
|
||||
connectToBrowser = (cdpClient) => {
|
||||
return Promise.resolve()
|
||||
}
|
||||
addRunnables = (runnables) => {}
|
||||
beforeSpec = (spec) => {}
|
||||
afterSpec = () => {
|
||||
return Promise.resolve()
|
||||
}
|
||||
beforeTest = (test) => {}
|
||||
commandLogAdded = (log) => {}
|
||||
commandLogChanged = (log) => {}
|
||||
viewportChanged = (input) => {}
|
||||
urlChanged = (input) => {}
|
||||
sendErrors (errors) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
uploadCaptureArtifact (uploadUrl) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
afterTest (test): void {}
|
||||
}
|
||||
24
system-tests/lib/protocolStubResponse.ts
Normal file
24
system-tests/lib/protocolStubResponse.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import path from 'path'
|
||||
import { gzipSync } from 'zlib'
|
||||
import crypto from 'crypto'
|
||||
import base64Url from 'base64url'
|
||||
import esbuild from 'esbuild'
|
||||
|
||||
export const SYSTEM_TESTS_PRIVATE = 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ3VBa1docWZSTTB3dFUKZ0toNXE5Z2hTU1BsdG5kM1UxUWk1VHhuZm1pR3lvQVZlL25HRkFidkxXQjNMaTRoVTBkVGlSTjg4TUIwam5hMQpXbHIwK2F1YzBmeVYwMTNiaW5ONFRxWUhFNjdaUUlKYkJNWDNMOEE5K1BybDJ6WkVqZlFZNkYraklKbXNIQ29RCnl5NXU3WGxSS09VOU9rQ0ZsQmp0L3FXbnd6b3RvM0lnY3JmcmJUejkxbk9LVHdKSXBtWGFRRGd3TEhLVm84aFgKbFJWMmI0UjIvWnErWWF6K2dMbE5aUkR2MVFsdXZUMTdPYUY1cldyL2xYT2lQaWp6MGtrS0ROQnF0aWo5UDdsaQpUSnUyQ0YzZkRxdzRuUUVKeVJBVExTTlpSdFZIRkdRN3hOdVdzdGFYOURBT3ZCRDhNTStFVmtlRWVYNEgrdEExCmFWclZhMG10QWdNQkFBRUNnZ0VBWW9OWXhwakFmWW54M1NwbHQxU0pyUGFLZ3krVlhSSHBEVVI0dVNNQXJHY1MKc3BjWXBvS0tGbmk3SjE0V3NibERKVkR5bm9aeWZzcDAvR0VtSTVFQ0RtdDNzNThSZ1F4V0tTTmxyWllBSkhENApHKzJNNGsrL1o1YUEvUWJwSjFDeWhETnlpWmtZUnk4K3hYa3lWWXpPWlJ0aEJSUG9tWGRwMGJ1Y0wybEFrN3NJCnVTUWIreTJtTUFXY1Q2UmRpYnFqcnNNMkE5YW1PQWc1bHd4L3NQUHRTbEdmVkZ6eExQYklDK3o3UmR1eWcyVEwKOXhnZkV5c0Y2dkpxSzJieW1pNGprd3dVZnhFRHluTmtIbEwzR1NsQlE1TkxnVjRVaXhrWEhKZ21OY041OERGTwpwT1NHQzAxMkNOVjQ4b3Fuc3VObEVjeVZhbTVSZk1iWXlCRm5PQVF5WlFLQmdRRGUxNmdISUk3Yk1VaXVLZHBwClV0YU8vMTNjMzlqQXFIcnllVnQ5UUhaTjU5aXdGZlN1N3kzZlYzVlFZRWJYZVpIU1ZkbC9uakhYTmRaaHdtbmUKWlcvZ3UzbHo4TlVjSElhaWZuT2RVSEh0czY2bjFlYUNvZDN0T29VYkhVUEhqYUl2L0F6ZlZTNWtBNzB6RTh6RApRNW5qS2JEc1hucExKY0QrV25VYzVIUlNjd0tCZ1FESDVueGZBaGkxckppQk1TeUpJYkZjUTl0dVVMT3JiQk9mCkZSeVArQzZybi9kZndvb09vL2lvaHJvT3FPSnVZTG9oTTltN1NvaHNpU3R2bG1VVEl3YlVTd1NNR00yMFdlK1cKR0ZjT01rQlk5NFVXdHF2aDlDaGMycmV6NkNDZE1VQkNHaVlMQ1V1SGp4ZDZqZ3ZZbG5vS2xsZzVBakJ2aUJDbApNM0VNZ2tOTFh3S0JnUUNwUVNGRmNJd3duZWszSjJEVjJHNVFwRk0xZk91VHdTUEk0VFlGRng0RUpCRm9CUFVZCm5WKzVJQ05oamc2Z2dKeXFKanlSZXFVZWNheklDYk1Ca1FmOXFFY2lNWXliMG1yTUpzRkhmaDlhVEx4ZWk4K04KN3NXeDlsMjg3MmhZdkJHdzRuOGdiZ0ZUUTZmRGtNbFlraExpLy9wNlBYUWplYVJ4VEdGaE5YL0lVd0tCZ0dKeQpyTVhOcm9XcW51RGhhdUdPYWw3YVBITXo0NGlGRFpUSFBPM2FlSUdsb3ByU29GTmRoZFRacFVBYkJJai9zaXN2CjhnYy9TYmpLUlU0TGIzUGhTRGU5U2x3RXl5b0xNT2RtelZqOGZweFNLb1ZwS1hWNlhYWjljUU4xU3JxZnl0bkQKTHdFNGJxNHdWb3ZROFJ5VjN6emZsa3RkUEtWeENXR1MyQllsQVNkWkFvR0FGRjliM2QvRko4Rm0rS25qNlhTaAozT3FuZlJ6NGRLN042bkxIUGdxelBGdVdiVWVPRGY1dTkrN3NpUVlNVkZyRWlZUlNvRStqc0FWREhBb1dIZ1Q3CmZlM2lUNzZuZVlHWVd3M1VwTjdQby9udTNiT3FWUzZSUEs0L05wZ0ZuM1ZzTUdyRTVKVVY5N0Z1Q1NKNHM4Wk8KTzJnWnBRdVpHQm40Und0LzEwOXdEYTQ9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0='
|
||||
|
||||
export const TEST_PRIVATE = crypto.createPrivateKey(Buffer.from(SYSTEM_TESTS_PRIVATE, 'base64').toString('utf8'))
|
||||
|
||||
const { outputFiles: [{ contents: stubProtocolRaw }] } = esbuild.buildSync({
|
||||
entryPoints: [path.join(__dirname, 'protocolStub.ts')],
|
||||
bundle: true,
|
||||
format: 'cjs',
|
||||
write: false,
|
||||
})
|
||||
|
||||
export const CYPRESS_LOCAL_PROTOCOL_STUB = new TextDecoder('utf-8').decode(stubProtocolRaw)
|
||||
|
||||
export const CYPRESS_LOCAL_PROTOCOL_STUB_COMPRESSED = gzipSync(CYPRESS_LOCAL_PROTOCOL_STUB)
|
||||
|
||||
export const CYPRESS_LOCAL_PROTOCOL_STUB_HASH = base64Url.fromBase64(crypto.createHash('SHA256').update(CYPRESS_LOCAL_PROTOCOL_STUB).digest('base64'))
|
||||
|
||||
export const CYPRESS_LOCAL_PROTOCOL_STUB_SIGN = base64Url.fromBase64(crypto.createSign('SHA256').update(CYPRESS_LOCAL_PROTOCOL_STUB).sign(TEST_PRIVATE, 'base64'))
|
||||
@@ -2,20 +2,29 @@ import crypto from 'crypto'
|
||||
import _ from 'lodash'
|
||||
import Bluebird from 'bluebird'
|
||||
import bodyParser from 'body-parser'
|
||||
import { api as jsonSchemas } from '@cypress/json-schemas'
|
||||
import type { RequestHandler } from 'express'
|
||||
|
||||
import { getExample, assertSchema, RecordSchemaVersions } from './validations/cloudValidations'
|
||||
|
||||
import * as jose from 'jose'
|
||||
import base64Url from 'base64url'
|
||||
|
||||
import systemTests from './system-tests'
|
||||
|
||||
const SYSTEM_TESTS_PRIVATE = 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ3VBa1docWZSTTB3dFUKZ0toNXE5Z2hTU1BsdG5kM1UxUWk1VHhuZm1pR3lvQVZlL25HRkFidkxXQjNMaTRoVTBkVGlSTjg4TUIwam5hMQpXbHIwK2F1YzBmeVYwMTNiaW5ONFRxWUhFNjdaUUlKYkJNWDNMOEE5K1BybDJ6WkVqZlFZNkYraklKbXNIQ29RCnl5NXU3WGxSS09VOU9rQ0ZsQmp0L3FXbnd6b3RvM0lnY3JmcmJUejkxbk9LVHdKSXBtWGFRRGd3TEhLVm84aFgKbFJWMmI0UjIvWnErWWF6K2dMbE5aUkR2MVFsdXZUMTdPYUY1cldyL2xYT2lQaWp6MGtrS0ROQnF0aWo5UDdsaQpUSnUyQ0YzZkRxdzRuUUVKeVJBVExTTlpSdFZIRkdRN3hOdVdzdGFYOURBT3ZCRDhNTStFVmtlRWVYNEgrdEExCmFWclZhMG10QWdNQkFBRUNnZ0VBWW9OWXhwakFmWW54M1NwbHQxU0pyUGFLZ3krVlhSSHBEVVI0dVNNQXJHY1MKc3BjWXBvS0tGbmk3SjE0V3NibERKVkR5bm9aeWZzcDAvR0VtSTVFQ0RtdDNzNThSZ1F4V0tTTmxyWllBSkhENApHKzJNNGsrL1o1YUEvUWJwSjFDeWhETnlpWmtZUnk4K3hYa3lWWXpPWlJ0aEJSUG9tWGRwMGJ1Y0wybEFrN3NJCnVTUWIreTJtTUFXY1Q2UmRpYnFqcnNNMkE5YW1PQWc1bHd4L3NQUHRTbEdmVkZ6eExQYklDK3o3UmR1eWcyVEwKOXhnZkV5c0Y2dkpxSzJieW1pNGprd3dVZnhFRHluTmtIbEwzR1NsQlE1TkxnVjRVaXhrWEhKZ21OY041OERGTwpwT1NHQzAxMkNOVjQ4b3Fuc3VObEVjeVZhbTVSZk1iWXlCRm5PQVF5WlFLQmdRRGUxNmdISUk3Yk1VaXVLZHBwClV0YU8vMTNjMzlqQXFIcnllVnQ5UUhaTjU5aXdGZlN1N3kzZlYzVlFZRWJYZVpIU1ZkbC9uakhYTmRaaHdtbmUKWlcvZ3UzbHo4TlVjSElhaWZuT2RVSEh0czY2bjFlYUNvZDN0T29VYkhVUEhqYUl2L0F6ZlZTNWtBNzB6RTh6RApRNW5qS2JEc1hucExKY0QrV25VYzVIUlNjd0tCZ1FESDVueGZBaGkxckppQk1TeUpJYkZjUTl0dVVMT3JiQk9mCkZSeVArQzZybi9kZndvb09vL2lvaHJvT3FPSnVZTG9oTTltN1NvaHNpU3R2bG1VVEl3YlVTd1NNR00yMFdlK1cKR0ZjT01rQlk5NFVXdHF2aDlDaGMycmV6NkNDZE1VQkNHaVlMQ1V1SGp4ZDZqZ3ZZbG5vS2xsZzVBakJ2aUJDbApNM0VNZ2tOTFh3S0JnUUNwUVNGRmNJd3duZWszSjJEVjJHNVFwRk0xZk91VHdTUEk0VFlGRng0RUpCRm9CUFVZCm5WKzVJQ05oamc2Z2dKeXFKanlSZXFVZWNheklDYk1Ca1FmOXFFY2lNWXliMG1yTUpzRkhmaDlhVEx4ZWk4K04KN3NXeDlsMjg3MmhZdkJHdzRuOGdiZ0ZUUTZmRGtNbFlraExpLy9wNlBYUWplYVJ4VEdGaE5YL0lVd0tCZ0dKeQpyTVhOcm9XcW51RGhhdUdPYWw3YVBITXo0NGlGRFpUSFBPM2FlSUdsb3ByU29GTmRoZFRacFVBYkJJai9zaXN2CjhnYy9TYmpLUlU0TGIzUGhTRGU5U2x3RXl5b0xNT2RtelZqOGZweFNLb1ZwS1hWNlhYWjljUU4xU3JxZnl0bkQKTHdFNGJxNHdWb3ZROFJ5VjN6emZsa3RkUEtWeENXR1MyQllsQVNkWkFvR0FGRjliM2QvRko4Rm0rS25qNlhTaAozT3FuZlJ6NGRLN042bkxIUGdxelBGdVdiVWVPRGY1dTkrN3NpUVlNVkZyRWlZUlNvRStqc0FWREhBb1dIZ1Q3CmZlM2lUNzZuZVlHWVd3M1VwTjdQby9udTNiT3FWUzZSUEs0L05wZ0ZuM1ZzTUdyRTVKVVY5N0Z1Q1NKNHM4Wk8KTzJnWnBRdVpHQm40Und0LzEwOXdEYTQ9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0='
|
||||
const TEST_PRIVATE = crypto.createPrivateKey(Buffer.from(SYSTEM_TESTS_PRIVATE, 'base64').toString('utf8'))
|
||||
let CAPTURE_PROTOCOL_ENABLED = false
|
||||
|
||||
export const postRunResponseWithWarnings = jsonSchemas.getExample('postRunResponse')('2.2.0')
|
||||
import {
|
||||
TEST_PRIVATE,
|
||||
CYPRESS_LOCAL_PROTOCOL_STUB_COMPRESSED,
|
||||
CYPRESS_LOCAL_PROTOCOL_STUB_HASH,
|
||||
CYPRESS_LOCAL_PROTOCOL_STUB_SIGN,
|
||||
} from './protocolStubResponse'
|
||||
|
||||
export const postRunInstanceResponse = jsonSchemas.getExample('postRunInstanceResponse')('2.1.0')
|
||||
export const postRunResponseWithWarnings = getExample('createRun', 4, 'res')
|
||||
|
||||
export const postInstanceTestsResponse = jsonSchemas.getExample('postInstanceTestsResponse')('1.0.0')
|
||||
export const postRunInstanceResponse = getExample('createInstance', 5, 'res')
|
||||
|
||||
export const postInstanceTestsResponse = getExample('postInstanceTests', 1, 'res')
|
||||
|
||||
postInstanceTestsResponse.actions = []
|
||||
export const postRunResponse = _.assign({}, postRunResponseWithWarnings, { warnings: [] })
|
||||
@@ -69,7 +78,19 @@ export const encryptBody = async (req, res, body) => {
|
||||
return await enc.encrypt()
|
||||
}
|
||||
|
||||
export const routeHandlers = {
|
||||
type RouteHandler = {
|
||||
method: 'get' | 'post' | 'put' | 'delete'
|
||||
url: string
|
||||
reqSchema?: {
|
||||
[K in keyof RecordSchemaVersions]: [K, keyof RecordSchemaVersions[K]]
|
||||
}[keyof RecordSchemaVersions]
|
||||
resSchema?: {
|
||||
[K in keyof RecordSchemaVersions]: [K, keyof RecordSchemaVersions[K]]
|
||||
}[keyof RecordSchemaVersions]
|
||||
res?: RequestHandler | object
|
||||
}
|
||||
|
||||
export const routeHandlers: Record<string, RouteHandler> = {
|
||||
sendPreflight: {
|
||||
method: 'post',
|
||||
url: '/preflight',
|
||||
@@ -82,14 +103,22 @@ export const routeHandlers = {
|
||||
postRun: {
|
||||
method: 'post',
|
||||
url: '/runs',
|
||||
reqSchema: 'postRunRequest@2.4.0',
|
||||
resSchema: 'postRunResponse@2.2.0',
|
||||
reqSchema: ['createRun', 4],
|
||||
resSchema: ['createRun', 4],
|
||||
res: (req, res) => {
|
||||
if (!req.body.specs) {
|
||||
throw new Error('expected for Test Runner to post specs')
|
||||
}
|
||||
|
||||
mockServerState.setSpecs(req)
|
||||
if (CAPTURE_PROTOCOL_ENABLED && req.body.runnerCapabilities.protocolMountVersion === 1) {
|
||||
res.json({
|
||||
...postRunResponse,
|
||||
captureProtocolUrl: `http://localhost:1234/capture-protocol/script/${CYPRESS_LOCAL_PROTOCOL_STUB_HASH}.js`,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return res.json(postRunResponse)
|
||||
},
|
||||
@@ -97,8 +126,8 @@ export const routeHandlers = {
|
||||
postRunInstance: {
|
||||
method: 'post',
|
||||
url: '/runs/:id/instances',
|
||||
reqSchema: 'postRunInstanceRequest@2.1.0',
|
||||
resSchema: 'postRunInstanceResponse@2.1.0',
|
||||
reqSchema: ['createInstance', 5],
|
||||
resSchema: ['createInstance', 5],
|
||||
res: (req, res) => {
|
||||
const response = {
|
||||
...postRunInstanceResponse,
|
||||
@@ -113,21 +142,29 @@ export const routeHandlers = {
|
||||
postInstanceTests: {
|
||||
method: 'post',
|
||||
url: '/instances/:id/tests',
|
||||
reqSchema: 'postInstanceTestsRequest@1.0.0',
|
||||
resSchema: 'postInstanceTestsResponse@1.0.0',
|
||||
reqSchema: ['postInstanceTests', 1],
|
||||
resSchema: ['postInstanceTests', 1],
|
||||
res: postInstanceTestsResponse,
|
||||
},
|
||||
postInstanceResults: {
|
||||
method: 'post',
|
||||
url: '/instances/:id/results',
|
||||
reqSchema: 'postInstanceResultsRequest@1.1.0',
|
||||
resSchema: 'postInstanceResultsResponse@1.0.0',
|
||||
reqSchema: ['postInstanceResults', 1],
|
||||
resSchema: ['postInstanceResults', 1],
|
||||
res: sendUploadUrls,
|
||||
},
|
||||
putArtifacts: {
|
||||
method: 'put',
|
||||
url: '/instances/:id/artifacts',
|
||||
// reqSchema: TODO(protocol): export this as part of manifest from cloud
|
||||
res: async (req, res) => {
|
||||
res.status(200)
|
||||
},
|
||||
},
|
||||
putInstanceStdout: {
|
||||
method: 'put',
|
||||
url: '/instances/:id/stdout',
|
||||
reqSchema: 'putInstanceStdoutRequest@1.0.0',
|
||||
reqSchema: ['updateInstanceStdout', 1],
|
||||
res (req, res) {
|
||||
return res.sendStatus(200)
|
||||
},
|
||||
@@ -149,7 +186,14 @@ export const routeHandlers = {
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
getCaptureScript: {
|
||||
method: 'get',
|
||||
url: '/capture-protocol/script/*',
|
||||
res: async (req, res) => {
|
||||
res.header('x-cypress-signature', CYPRESS_LOCAL_PROTOCOL_STUB_SIGN)
|
||||
res.status(200).send(CYPRESS_LOCAL_PROTOCOL_STUB_COMPRESSED)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const createRoutes = (props: DeepPartial<typeof routeHandlers>) => {
|
||||
@@ -170,15 +214,6 @@ export const getRequests = () => {
|
||||
return mockServerState.requests.filter((r) => r.url !== 'POST /preflight')
|
||||
}
|
||||
|
||||
const getSchemaErr = (tag, err, schema) => {
|
||||
return {
|
||||
errors: err.errors,
|
||||
object: err.object,
|
||||
example: err.example,
|
||||
message: `${tag} should follow ${schema} schema`,
|
||||
}
|
||||
}
|
||||
|
||||
const getResponse = function (responseSchema) {
|
||||
if (!responseSchema) {
|
||||
throw new Error('No response schema supplied')
|
||||
@@ -188,9 +223,10 @@ const getResponse = function (responseSchema) {
|
||||
return responseSchema
|
||||
}
|
||||
|
||||
const [name, version] = responseSchema.split('@')
|
||||
const [name, version] = responseSchema
|
||||
|
||||
return jsonSchemas.getExample(name)(version)
|
||||
// @ts-expect-error
|
||||
return getExample(name, version, 'res')
|
||||
}
|
||||
|
||||
const sendResponse = function (req, res, responseBody) {
|
||||
@@ -216,7 +252,7 @@ const ensureSchema = function (onRequestBody, expectedRequestSchema, responseBod
|
||||
let reqName; let reqVersion
|
||||
|
||||
if (expectedRequestSchema) {
|
||||
[reqName, reqVersion] = expectedRequestSchema.split('@')
|
||||
[reqName, reqVersion] = expectedRequestSchema
|
||||
}
|
||||
|
||||
return async function (req, res) {
|
||||
@@ -228,7 +264,8 @@ const ensureSchema = function (onRequestBody, expectedRequestSchema, responseBod
|
||||
|
||||
try {
|
||||
if (expectedRequestSchema) {
|
||||
jsonSchemas.assertSchema(reqName, reqVersion)(body)
|
||||
// @ts-expect-error
|
||||
assertSchema(reqName, reqVersion, 'req')(body)
|
||||
}
|
||||
|
||||
res.expectedResponseSchema = expectedResponseSchema
|
||||
@@ -245,7 +282,7 @@ const ensureSchema = function (onRequestBody, expectedRequestSchema, responseBod
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Schema Error:', err.message)
|
||||
|
||||
return res.status(412).json(getSchemaErr('request', err, expectedRequestSchema))
|
||||
return res.status(412).json(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,15 +309,15 @@ const assertResponseBodySchema = function (req, res, next) {
|
||||
if (res.expectedResponseSchema && _.inRange(res.statusCode, 200, 299)) {
|
||||
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
||||
|
||||
const [resName, resVersion] = res.expectedResponseSchema.split('@')
|
||||
const [resName, resVersion] = res.expectedResponseSchema
|
||||
|
||||
try {
|
||||
jsonSchemas.assertSchema(resName, resVersion)(body)
|
||||
assertSchema(resName, resVersion, 'res')(body)
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Schema Error:', err.message)
|
||||
|
||||
return res.status(412).json(getSchemaErr('response', err, res.expectedResponseSchema))
|
||||
return res.status(412).json(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,6 +369,16 @@ const onServer = (routes) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const enableCaptureProtocol = () => {
|
||||
beforeEach(() => {
|
||||
CAPTURE_PROTOCOL_ENABLED = true
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
CAPTURE_PROTOCOL_ENABLED = false
|
||||
})
|
||||
}
|
||||
|
||||
export const setupStubbedServer = (routes) => {
|
||||
systemTests.setup({
|
||||
servers: [{
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
"clean-deps": "find . -depth -name node_modules -type d -exec rimraf {} \\;",
|
||||
"preprojects:yarn:install": "yarn clean-deps",
|
||||
"projects:yarn:install": "node ./scripts/projects-yarn-install.js",
|
||||
"pretest": "yarn gulp ensureCloudValidations",
|
||||
"test": "node ./scripts/run.js --glob-in-dir=\"{test,test-binary}\"",
|
||||
"pretest:ci": "yarn gulp ensureCloudValidations",
|
||||
"test:ci": "node ./scripts/run.js",
|
||||
"update:snapshots": "SNAPSHOT_UPDATE=1 npm run test"
|
||||
},
|
||||
@@ -20,7 +22,6 @@
|
||||
"@babel/preset-env": "7.9.0",
|
||||
"@cypress/commit-info": "2.2.0",
|
||||
"@cypress/debugging-proxy": "2.0.1",
|
||||
"@cypress/json-schemas": "5.39.0",
|
||||
"@cypress/request": "2.88.10",
|
||||
"@cypress/request-promise": "4.2.6",
|
||||
"@cypress/sinon-chai": "2.9.1",
|
||||
@@ -54,6 +55,7 @@
|
||||
"debug": "^4.3.4",
|
||||
"dedent": "^0.7.0",
|
||||
"dockerode": "3.3.1",
|
||||
"esbuild": "^0.15.3",
|
||||
"execa": "4",
|
||||
"express": "4.17.3",
|
||||
"express-session": "1.16.1",
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const Promise = require('bluebird')
|
||||
const jsonSchemas = require('@cypress/json-schemas').api
|
||||
const dedent = require('dedent')
|
||||
|
||||
const systemTests = require('../lib/system-tests').default
|
||||
const { fs } = require('@packages/server/lib/util/fs')
|
||||
const Fixtures = require('../lib/fixtures')
|
||||
const { assertSchema } = require('../lib/validations/cloudValidations')
|
||||
const {
|
||||
createRoutes,
|
||||
setupStubbedServer,
|
||||
enableCaptureProtocol,
|
||||
getRequestUrls,
|
||||
getRequests,
|
||||
postRunResponse,
|
||||
@@ -319,7 +320,7 @@ describe('e2e record', () => {
|
||||
resp.claimedInstances = claimed.length
|
||||
resp.totalInstances = allSpecs.length
|
||||
|
||||
jsonSchemas.assertSchema('postRunInstanceResponse', '2.1.0')(resp)
|
||||
assertSchema('createInstance', 5, 'req')(resp)
|
||||
|
||||
return res.json(resp)
|
||||
}
|
||||
@@ -669,6 +670,8 @@ describe('e2e record', () => {
|
||||
...postInstanceTestsResponse,
|
||||
actions: [{
|
||||
type: 'SPEC',
|
||||
clientId: null,
|
||||
payload: null,
|
||||
action: 'SKIP',
|
||||
}],
|
||||
})
|
||||
@@ -709,6 +712,7 @@ describe('e2e record', () => {
|
||||
console.log(requests[0].body.runnerCapabilities)
|
||||
expect(requests[0].body).property('runnerCapabilities').deep.eq({
|
||||
'dynamicSpecsInSerialMode': true,
|
||||
'protocolMountVersion': 1,
|
||||
'skipSpecAction': true,
|
||||
})
|
||||
})
|
||||
@@ -1093,7 +1097,8 @@ describe('e2e record', () => {
|
||||
describe('create run 412', () => {
|
||||
setupStubbedServer(createRoutes({
|
||||
postRun: {
|
||||
reqSchema: 'postRunRequest@2.0.0', // force this to throw a schema error
|
||||
reqSchema: ['createRun', 4],
|
||||
// force this to throw a schema error
|
||||
onReqBody (body) {
|
||||
_.extend(body, {
|
||||
ci: null,
|
||||
@@ -2263,4 +2268,21 @@ describe('e2e record', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('capture-protocol', () => {
|
||||
setupStubbedServer(createRoutes())
|
||||
enableCaptureProtocol()
|
||||
|
||||
describe('passing', () => {
|
||||
it('retrieves the capture protocol', function () {
|
||||
return systemTests.exec(this, {
|
||||
key: 'f858a2bc-b469-4e48-be67-0876339ee7e1',
|
||||
configFile: 'cypress-with-project-id.config.js',
|
||||
spec: 'record_pass*',
|
||||
record: true,
|
||||
snapshot: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
115
yarn.lock
115
yarn.lock
@@ -2177,17 +2177,6 @@
|
||||
check-more-types "2.24.0"
|
||||
lazy-ass "1.6.0"
|
||||
|
||||
"@bahmutov/is-my-json-valid@2.17.3":
|
||||
version "2.17.3"
|
||||
resolved "https://registry.yarnpkg.com/@bahmutov/is-my-json-valid/-/is-my-json-valid-2.17.3.tgz#955e8e9767db913e55c4c3f541dcfd2a30e160c4"
|
||||
integrity sha1-lV6Ol2fbkT5VxMP1Qdz9KjDhYMQ=
|
||||
dependencies:
|
||||
generate-function "^2.0.0"
|
||||
generate-object-property "^1.1.0"
|
||||
is-my-ip-valid "^1.0.0"
|
||||
jsonpointer "^4.0.0"
|
||||
xtend "^4.0.0"
|
||||
|
||||
"@bcoe/v8-coverage@^0.2.3":
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
@@ -2305,17 +2294,6 @@
|
||||
optionalDependencies:
|
||||
registry-js "1.8.0"
|
||||
|
||||
"@cypress/json-schemas@5.39.0":
|
||||
version "5.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@cypress/json-schemas/-/json-schemas-5.39.0.tgz#a650ba91d0124f56a2954edc156704da4a313780"
|
||||
integrity sha512-QpSUflVOP06Bi4lCoDlMWgbfeDN4N3UHZqH9s2qC65FRhdSek0BcUc9jd/Q4cGUlHYIfDdCLmSRG1b1/0Vh/PQ==
|
||||
dependencies:
|
||||
"@cypress/schema-tools" "4.7.7"
|
||||
lodash "^4.17.21"
|
||||
lodash.clonedeep "^4.5.0"
|
||||
lodash.merge "^4.6.2"
|
||||
lodash.omit "^4.5.0"
|
||||
|
||||
"@cypress/mocha-teamcity-reporter@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@cypress/mocha-teamcity-reporter/-/mocha-teamcity-reporter-1.0.0.tgz#efc8ab938c99f9654f438bef412bce1cd5e129d7"
|
||||
@@ -2382,24 +2360,6 @@
|
||||
tunnel-agent "^0.6.0"
|
||||
uuid "^8.3.2"
|
||||
|
||||
"@cypress/schema-tools@4.7.7":
|
||||
version "4.7.7"
|
||||
resolved "https://registry.yarnpkg.com/@cypress/schema-tools/-/schema-tools-4.7.7.tgz#251a9864caba0eded884ff5c71de16c76dbf556a"
|
||||
integrity sha512-RRzksoJIXDTeUjt7YE9xAhOynqc7R+j8Tx8ebpkSPJB6Z3WujdLP0sigVh2AV24G/CySOvJGuQQY94aBEpCZaA==
|
||||
dependencies:
|
||||
"@bahmutov/all-paths" "1.0.2"
|
||||
"@bahmutov/is-my-json-valid" "2.17.3"
|
||||
"@types/ramda" "0.25.47"
|
||||
debug "4.3.1"
|
||||
json-stable-stringify "1.0.1"
|
||||
json2md "1.6.3"
|
||||
lodash.camelcase "4.3.0"
|
||||
lodash.get "4.4.2"
|
||||
lodash.reduce "^4.6.0"
|
||||
lodash.set "4.3.2"
|
||||
quote "0.4.0"
|
||||
ramda "0.25.0"
|
||||
|
||||
"@cypress/sinon-chai@2.9.1":
|
||||
version "2.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@cypress/sinon-chai/-/sinon-chai-2.9.1.tgz#1705c0341bc286740979b1b1cac89b7f5d34d6bc"
|
||||
@@ -7011,11 +6971,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.6.tgz#df9c3c8b31a247ec315e6996566be3171df4b3b1"
|
||||
integrity sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==
|
||||
|
||||
"@types/ramda@0.25.47":
|
||||
version "0.25.47"
|
||||
resolved "https://registry.yarnpkg.com/@types/ramda/-/ramda-0.25.47.tgz#904f2ee46149af42902fe7dc01867e32798e8b37"
|
||||
integrity sha512-+ffSU83+PR4/cZtNTkUcFkg70sK4GePle7p5h05bQ37ycPumOx/TBpU52bt36GKDlds6tCqXheqPvgC52MMLug==
|
||||
|
||||
"@types/range-parser@*":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
|
||||
@@ -7051,6 +7006,14 @@
|
||||
"@types/bluebird" "*"
|
||||
"@types/request" "*"
|
||||
|
||||
"@types/request-promise@^4.1.48":
|
||||
version "4.1.48"
|
||||
resolved "https://registry.yarnpkg.com/@types/request-promise/-/request-promise-4.1.48.tgz#46f4225a58cefaa342c87fe5f2efb8ad3cb2c2e3"
|
||||
integrity sha512-sLsfxfwP5G3E3U64QXxKwA6ctsxZ7uKyl4I28pMj3JvV+ztWECRns73GL71KMOOJME5u1A5Vs5dkBqyiR1Zcnw==
|
||||
dependencies:
|
||||
"@types/bluebird" "*"
|
||||
"@types/request" "*"
|
||||
|
||||
"@types/request@*":
|
||||
version "2.48.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.5.tgz#019b8536b402069f6d11bee1b2c03e7f232937a0"
|
||||
@@ -15920,20 +15883,6 @@ gauge@~2.7.3:
|
||||
strip-ansi "^3.0.1"
|
||||
wide-align "^1.1.0"
|
||||
|
||||
generate-function@^2.0.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f"
|
||||
integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==
|
||||
dependencies:
|
||||
is-property "^1.0.2"
|
||||
|
||||
generate-object-property@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0"
|
||||
integrity sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=
|
||||
dependencies:
|
||||
is-property "^1.0.0"
|
||||
|
||||
gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2:
|
||||
version "1.0.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||
@@ -17695,11 +17644,6 @@ indent-string@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
|
||||
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
|
||||
|
||||
indento@^1.1.7:
|
||||
version "1.1.13"
|
||||
resolved "https://registry.yarnpkg.com/indento/-/indento-1.1.13.tgz#751331b327c04740eeb7be40c5606e6e255c9e36"
|
||||
integrity sha512-YZWk3mreBEM7sBPddsiQnW9Z8SGg/gNpFfscJq00HCDS7pxcQWWWMSVKJU7YkTRyDu1Zv2s8zaK8gQWKmCXHlg==
|
||||
|
||||
indexof@0.0.1, indexof@~0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
|
||||
@@ -18282,11 +18226,6 @@ is-module@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
|
||||
integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=
|
||||
|
||||
is-my-ip-valid@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824"
|
||||
integrity sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==
|
||||
|
||||
is-negated-glob@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2"
|
||||
@@ -18441,11 +18380,6 @@ is-promise@^2.0.0, is-promise@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
|
||||
integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
|
||||
|
||||
is-property@^1.0.0, is-property@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
|
||||
integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=
|
||||
|
||||
is-reference@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7"
|
||||
@@ -19126,13 +19060,6 @@ json-wire-protocol@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/json-wire-protocol/-/json-wire-protocol-1.0.0.tgz#50a6fd7e5f1406dbaf5a4d3279be2620181276f8"
|
||||
integrity sha1-UKb9fl8UBtuvWk0yeb4mIBgSdvg=
|
||||
|
||||
json2md@1.6.3:
|
||||
version "1.6.3"
|
||||
resolved "https://registry.yarnpkg.com/json2md/-/json2md-1.6.3.tgz#852e52f9fe579691eaf3c75f626992aefa33ca09"
|
||||
integrity sha512-bdza+dm2rKu9NgguimGe9Os7grpYE8CCLXIXMkIYGOfkZLxSMKN487OOT8PBgBW2xFCcItoxh6WFA7SJOEDKkw==
|
||||
dependencies:
|
||||
indento "^1.1.7"
|
||||
|
||||
json3@3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
|
||||
@@ -19221,11 +19148,6 @@ jsonparse@^1.2.0, jsonparse@^1.3.1:
|
||||
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
|
||||
integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
|
||||
|
||||
jsonpointer@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.1.0.tgz#501fb89986a2389765ba09e6053299ceb4f2c2cc"
|
||||
integrity sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg==
|
||||
|
||||
jsonwebtoken@^8.5.1:
|
||||
version "8.5.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
|
||||
@@ -20065,7 +19987,7 @@ lodash._isiterateecall@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c"
|
||||
integrity sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=
|
||||
|
||||
lodash.camelcase@4.3.0, lodash.camelcase@^4.3.0:
|
||||
lodash.camelcase@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
|
||||
integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
|
||||
@@ -20114,7 +20036,7 @@ lodash.flatten@^4.4.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
|
||||
integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
|
||||
|
||||
lodash.get@4.4.2, lodash.get@^4, lodash.get@^4.0.0, lodash.get@^4.4.2:
|
||||
lodash.get@^4, lodash.get@^4.0.0, lodash.get@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
||||
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
|
||||
@@ -20183,7 +20105,7 @@ lodash.keys@^3.0.0:
|
||||
lodash.isarguments "^3.0.0"
|
||||
lodash.isarray "^3.0.0"
|
||||
|
||||
lodash.merge@4.6.2, lodash.merge@^4.6.2:
|
||||
lodash.merge@4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||
@@ -20193,11 +20115,6 @@ lodash.mergewith@^4.6.2:
|
||||
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
|
||||
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
|
||||
|
||||
lodash.omit@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60"
|
||||
integrity sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA=
|
||||
|
||||
lodash.once@^4.0.0, lodash.once@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
||||
@@ -20208,16 +20125,6 @@ lodash.range@3.2.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.range/-/lodash.range-3.2.0.tgz#f461e588f66683f7eadeade513e38a69a565a15d"
|
||||
integrity sha1-9GHliPZmg/fq3q3lE+OKaaVloV0=
|
||||
|
||||
lodash.reduce@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b"
|
||||
integrity sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=
|
||||
|
||||
lodash.set@4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
|
||||
integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=
|
||||
|
||||
lodash.sortby@^4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||
|
||||
Reference in New Issue
Block a user