Skip to content

chore: Share error utils with the cloud #31887

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/driver/cypress/e2e/cypress/error_utils.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,27 @@ describe('driver/src/cypress/error_utils', () => {
})
})

context('.extendErrorMessages', () => {
it('extends error messages', () => {
$errUtils.extendErrorMessages({
testErrors: {
test: 'test error message',
},
})

const fn = () => {
$errUtils.throwErrByPath('testErrors.test')
}

expect(fn).to.throw().and.satisfy((err) => {
expect(err.message).to.equal('test error message')
expect(err.name).to.eq('CypressError')

return true
})
})
})

context('.getUnsupportedPlugin', () => {
it('returns unsupported plugin if the error msg is the expected one', () => {
const unsupportedPlugin = $errUtils.getUnsupportedPlugin({
Expand Down
23 changes: 11 additions & 12 deletions packages/driver/src/cy/commands/prompt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,26 @@ const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress, cy: Cypress.Cyp
Cypress: Cypress as CypressInternal,
cy,
eventManager: window.getEventManager ? window.getEventManager() : undefined,
errorUtils: {
extendErrorMessages: $errUtils.extendErrorMessages,
throwErrByPath: $errUtils.throwErrByPath,
},
})
} catch (error) {
return error
}
}

export default (Commands, Cypress, cy) => {
export default (Commands: Cypress.Cypress['Commands'], Cypress: Cypress.Cypress, cy: Cypress.Cypress['cy']) => {
// @ts-expect-error - these types are not yet implemented until the prompt command is rolled out
if (Cypress.config('experimentalPromptCommand')) {
let initializeCloudCyPromptPromise: Promise<ReturnType<CyPromptDriverDefaultShape['createCyPrompt']> | Error> | undefined

if (Cypress.browser.family === 'chromium' || Cypress.browser.name === 'electron') {
initializeCloudCyPromptPromise = initializeCloudCyPrompt(Cypress, cy)
}

const prompt = async (message: string, options: object = {}) => {
const prompt = (message: string, options: object = {}) => {
if (Cypress.testingType === 'component') {
$errUtils.throwErrByPath('prompt.promptTestingTypeError')

Expand All @@ -108,22 +113,16 @@ export default (Commands, Cypress, cy) => {
return
}

try {
const bundleResult = await initializeCloudCyPromptPromise

// TODO: figure out how to handle timeout more generally
return cy.wrap(initializeCloudCyPromptPromise, { log: false, timeout: 45000 }).then((bundleResult: ReturnType<CyPromptDriverDefaultShape['createCyPrompt']> | Error) => {
if (bundleResult instanceof Error) {
throw bundleResult
}

const cyPrompt = bundleResult

return await cyPrompt(message, options)
} catch (error) {
// TODO: Check error that the user is logged in / record key

// TODO: handle this better
throw new Error(`CyPromptDriver not found: ${error}`)
}
return cyPrompt(message, options)
})
}

// For testing purposes, we can reset the prompt command initialization
Expand Down
4 changes: 4 additions & 0 deletions packages/driver/src/cy/commands/prompt/prompt-driver-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export interface CyPromptOptions {
// Note that the eventManager is present in same origin AUTs, but not cross origin
// so we need to check for it's presence before using it
eventManager?: CyPromptEventManager
errorUtils: {
extendErrorMessages: (errorMessages: any) => void
throwErrByPath: (err: any, path: string) => void
}
}

export interface CyPromptDriverDefaultShape {
Expand Down
14 changes: 12 additions & 2 deletions packages/driver/src/cypress/error_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const ERR_PREPARED_FOR_SERIALIZATION = Symbol('ERR_PREPARED_FOR_SERIALIZATION')

const crossOriginScriptRe = /^script error/i

let allErrorMessages = $errorMessages

if (!Error.captureStackTrace) {
Error.captureStackTrace = (err, fn) => {
const stack = (new Error()).stack
Expand Down Expand Up @@ -396,7 +398,7 @@ const docsUrlByParents = (msgPath) => {
return // reached root
}

const obj = _.get($errorMessages, msgPath)
const obj = _.get(allErrorMessages, msgPath)

if (obj.hasOwnProperty('docsUrl')) {
return obj.docsUrl
Expand All @@ -406,7 +408,7 @@ const docsUrlByParents = (msgPath) => {
}

const errByPath = (msgPath, args?) => {
let msgValue = _.get($errorMessages, msgPath)
let msgValue = _.get(allErrorMessages, msgPath)

if (!msgValue) {
return internalErr({ message: `Error message path '${msgPath}' does not exist` })
Expand Down Expand Up @@ -655,6 +657,13 @@ const getUnsupportedPlugin = (runnable) => {
return null
}

const extendErrorMessages = (errorMessages: any) => {
allErrorMessages = {
...allErrorMessages,
...errorMessages,
}
}

export default {
stackWithReplacedProps,
appendErrMsg,
Expand All @@ -679,4 +688,5 @@ export default {
throwErrByPath,
warnByPath,
wrapErr,
extendErrorMessages,
}
46 changes: 37 additions & 9 deletions packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { readFile } from 'fs-extra'
import { ensureCyPromptBundle } from './ensure_cy_prompt_bundle'
import chokidar from 'chokidar'
import { getCloudMetadata } from '../get_cloud_metadata'
import type { CyPromptAuthenticatedUserShape } from '@packages/types'

const debug = Debug('cypress:server:cy-prompt-lifecycle-manager')

Expand All @@ -33,22 +34,34 @@ export class CyPromptLifecycleManager {
* @param ctx Data context to register this instance with
*/
initializeCyPromptManager ({
projectId,
cloudDataSource,
ctx,
record,
key,
}: {
projectId?: string
cloudDataSource: CloudDataSource
ctx: DataContext
record?: boolean
key?: string
}): void {
// Register this instance in the data context
ctx.update((data) => {
data.cyPromptLifecycleManager = this
})

const getProjectOptions = async () => {
return {
user: await ctx.actions.auth.authApi.getUser(),
projectSlug: (await ctx.project.getConfig()).projectId || undefined,
record,
key,
isOpenMode: ctx.isOpenMode,
}
}

const cyPromptManagerPromise = this.createCyPromptManager({
projectId,
cloudDataSource,
getProjectOptions,
}).catch(async (error) => {
debug('Error during cy prompt manager setup: %o', error)

Expand Down Expand Up @@ -81,8 +94,8 @@ export class CyPromptLifecycleManager {
this.cyPromptManagerPromise = cyPromptManagerPromise

this.setupWatcher({
projectId,
cloudDataSource,
getProjectOptions,
})
}

Expand All @@ -97,17 +110,25 @@ export class CyPromptLifecycleManager {
}

private async createCyPromptManager ({
projectId,
cloudDataSource,
getProjectOptions,
}: {
projectId?: string
cloudDataSource: CloudDataSource
getProjectOptions: () => Promise<{
user?: CyPromptAuthenticatedUserShape
projectSlug?: string
record?: boolean
key?: string
}>
}): Promise<{ cyPromptManager?: CyPromptManager, error?: Error }> {
let cyPromptHash: string
let cyPromptPath: string

const currentProjectOptions = await getProjectOptions()
const projectId = currentProjectOptions.projectSlug
const cyPromptSession = await postCyPromptSession({
projectId,
projectId: currentProjectOptions.projectSlug,
})

if (!process.env.CYPRESS_LOCAL_CY_PROMPT_PATH) {
Expand Down Expand Up @@ -138,20 +159,19 @@ export class CyPromptLifecycleManager {
const script = await readFile(serverFilePath, 'utf8')
const cyPromptManager = new CyPromptManager()

const { cloudUrl, cloudHeaders } = await getCloudMetadata(cloudDataSource)
const { cloudUrl } = await getCloudMetadata(cloudDataSource)

await cyPromptManager.setup({
script,
cyPromptPath,
cyPromptHash,
projectSlug: projectId,
cloudApi: {
cloudUrl,
cloudHeaders,
CloudRequest,
isRetryableError,
asyncRetry,
},
getProjectOptions,
})

debug('cy prompt is ready')
Expand Down Expand Up @@ -181,9 +201,16 @@ export class CyPromptLifecycleManager {
private setupWatcher ({
projectId,
cloudDataSource,
getProjectOptions,
}: {
projectId?: string
cloudDataSource: CloudDataSource
getProjectOptions: () => Promise<{
user?: CyPromptAuthenticatedUserShape
projectSlug?: string
record?: boolean
key?: string
}>
}) {
// Don't setup a watcher if the cy prompt bundle is NOT local
if (!process.env.CYPRESS_LOCAL_CY_PROMPT_PATH) {
Expand All @@ -204,6 +231,7 @@ export class CyPromptLifecycleManager {
this.cyPromptManagerPromise = this.createCyPromptManager({
projectId,
cloudDataSource,
getProjectOptions,
}).catch((error) => {
debug('Error during reload of cy prompt manager: %o', error)

Expand Down
12 changes: 9 additions & 3 deletions packages/server/lib/cloud/cy-prompt/CyPromptManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CyPromptManagerShape, CyPromptStatus, CyPromptServerDefaultShape, CyPromptServerShape, CyPromptCloudApi, CyPromptCDPClient } from '@packages/types'
import type { CyPromptManagerShape, CyPromptStatus, CyPromptServerDefaultShape, CyPromptServerShape, CyPromptCloudApi, CyPromptCDPClient, CyPromptAuthenticatedUserShape } from '@packages/types'
import type { Router } from 'express'
import Debug from 'debug'
import { requireScript } from '../require_script'
Expand All @@ -12,6 +12,12 @@ interface SetupOptions {
cyPromptHash?: string
projectSlug?: string
cloudApi: CyPromptCloudApi
getProjectOptions: () => Promise<{
user?: CyPromptAuthenticatedUserShape
projectSlug?: string
record?: boolean
key?: string
}>
}

const debug = Debug('cypress:server:cy-prompt')
Expand All @@ -20,14 +26,14 @@ export class CyPromptManager implements CyPromptManagerShape {
status: CyPromptStatus = 'NOT_INITIALIZED'
private _cyPromptServer: CyPromptServerShape | undefined

async setup ({ script, cyPromptPath, cyPromptHash, projectSlug, cloudApi }: SetupOptions): Promise<void> {
async setup ({ script, cyPromptPath, cyPromptHash, getProjectOptions, cloudApi }: SetupOptions): Promise<void> {
const { createCyPromptServer } = requireScript<CyPromptServer>(script).default

this._cyPromptServer = await createCyPromptServer({
cyPromptHash,
cyPromptPath,
projectSlug,
cloudApi,
getProjectOptions,
})

this.status = 'INITIALIZED'
Expand Down
2 changes: 2 additions & 0 deletions packages/server/lib/modes/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ const openProjectCreate = (projectRoot, socketId, args) => {
onWarning,
spec: args.spec,
onError: args.onError,
record: args.record,
key: args.key,
}

return openProject.create(projectRoot, args, options)
Expand Down
3 changes: 2 additions & 1 deletion packages/server/lib/project-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,10 @@ export class ProjectBase extends EE {
const cyPromptLifecycleManager = new CyPromptLifecycleManager()

cyPromptLifecycleManager.initializeCyPromptManager({
projectId: cfg.projectId,
cloudDataSource: this.ctx.cloud,
ctx: this.ctx,
record: this.options.record,
key: this.options.key,
})
}

Expand Down
Loading