mirror of
https://github.com/cypress-io/cypress.git
synced 2026-01-05 22:19:46 -06:00
feat: Error standardization (#20323)
* refactor: reworking client-side error shape * feat: add the CodeFrame to baseerror * consolidate baseError handling, type fixes * Fix UNIFY-1164 w/ test cleanup to avoid intercepting * fix types, cleanup based on review / Brian * fix: imports / types / tests * cleanup tests, fix TSError location, add reinitializeCypress mutation * fix: show correct stack trace file name (#20410) * Improve comments for regexes / TSError capture * feat: Add codeframe to error, address PR comments * update snapshot * change codeframe impl, per Brian's request * Attempt to fix test flake Co-authored-by: ElevateBart <ledouxb@gmail.com> Co-authored-by: Alejandro Estrada <estrada9166@hotmail.com>
This commit is contained in:
@@ -28,6 +28,7 @@ system-tests/lib/scaffold/support/commands.js
|
||||
system-tests/test/support/projects/e2e/cypress/
|
||||
system-tests/projects/e2e/cypress/e2e/stdout_exit_early_failing.cy.js
|
||||
system-tests/projects/e2e/cypress/e2e/typescript_syntax_error.cy.ts
|
||||
system-tests/projects/config-with-ts-syntax-error/**
|
||||
|
||||
|
||||
**/test/fixtures
|
||||
|
||||
@@ -261,7 +261,7 @@ describe('App: Index', () => {
|
||||
})
|
||||
|
||||
it('opens config file in ide from SpecPattern', () => {
|
||||
cy.intercept('mutation-OpenConfigFile', { data: { 'openFileInIDE': true } }).as('OpenIDE')
|
||||
cy.intercept('mutation-OpenFileInIDE_Mutation', { data: { openFileInIDE: true } }).as('OpenIDE')
|
||||
|
||||
cy.findByRole('button', { name: 'cypress.config.js' }).click()
|
||||
|
||||
@@ -269,7 +269,7 @@ describe('App: Index', () => {
|
||||
})
|
||||
|
||||
it('opens config file in ide from footer button', () => {
|
||||
cy.intercept('mutation-OpenConfigFile', { data: { 'openFileInIDE': true } }).as('OpenIDE')
|
||||
cy.intercept('mutation-OpenFileInIDE_Mutation', { data: { openFileInIDE: true } }).as('OpenIDE')
|
||||
|
||||
cy.contains('button', defaultMessages.createSpec.updateSpecPattern).click()
|
||||
|
||||
@@ -697,7 +697,7 @@ describe('App: Index', () => {
|
||||
})
|
||||
|
||||
it('opens config file in ide from SpecPattern', () => {
|
||||
cy.intercept('mutation-OpenConfigFile', { data: { 'openFileInIDE': true } }).as('OpenIDE')
|
||||
cy.intercept('mutation-OpenFileInIDE_Mutation', { data: { openFileInIDE: true } }).as('OpenIDE')
|
||||
|
||||
cy.findByRole('button', { name: 'cypress.config.js' }).click()
|
||||
|
||||
@@ -705,7 +705,7 @@ describe('App: Index', () => {
|
||||
})
|
||||
|
||||
it('opens config file in ide from footer button', () => {
|
||||
cy.intercept('mutation-OpenConfigFile', { data: { 'openFileInIDE': true } }).as('OpenIDE')
|
||||
cy.intercept('mutation-OpenFileInIDE_Mutation', { data: { openFileInIDE: true } }).as('OpenIDE')
|
||||
|
||||
cy.contains('button', defaultMessages.createSpec.updateSpecPattern).click()
|
||||
|
||||
|
||||
@@ -49,12 +49,12 @@ describe('hooks', {
|
||||
|
||||
cy.get('.hook-open-in-ide').should('have.length', 4)
|
||||
|
||||
cy.intercept('mutation-OpenFileInIDE', { data: { 'openFileInIDE': true } }).as('OpenIDE')
|
||||
cy.intercept('mutation-SpecRunnerOpenMode_OpenFileInIDE', { data: { 'openFileInIDE': true } }).as('OpenIDE')
|
||||
|
||||
cy.contains('Open in IDE').invoke('show').click({ force: true })
|
||||
|
||||
cy.wait('@OpenIDE').then(({ request }) => {
|
||||
expect(request.body.variables.input.absolute).to.include('hooks/basic.cy.js')
|
||||
expect(request.body.variables.input.filePath).to.include('hooks/basic.cy.js')
|
||||
expect(request.body.variables.input.column).to.eq(Cypress.browser.family === 'firefox' ? 6 : 3)
|
||||
expect(request.body.variables.input.line).to.eq(2)
|
||||
})
|
||||
|
||||
@@ -102,13 +102,13 @@ describe('src/cypress/runner', () => {
|
||||
hasPreferredIde: true,
|
||||
})
|
||||
|
||||
cy.intercept('mutation-OpenFileInIDE', { data: { 'openFileInIDE': true } }).as('OpenIDE')
|
||||
cy.intercept('mutation-SpecRunnerOpenMode_OpenFileInIDE', { data: { 'openFileInIDE': true } }).as('OpenIDE')
|
||||
|
||||
cy.contains('a', 'simple-cy-assert.runner')
|
||||
.click()
|
||||
|
||||
cy.wait('@OpenIDE').then(({ request }) => {
|
||||
expect(request.body.variables.input.absolute).to.include('simple-cy-assert.runner.cy.js')
|
||||
expect(request.body.variables.input.filePath).to.include('simple-cy-assert.runner.cy.js')
|
||||
})
|
||||
|
||||
cy.get('[data-cy="runnable-header"] [data-cy="spec-duration"]').should('exist')
|
||||
|
||||
@@ -5,12 +5,12 @@ import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json'
|
||||
// whether the test has a preferred IDE defined.
|
||||
const verifyIdeOpen = ({ fileName, action, hasPreferredIde }) => {
|
||||
if (hasPreferredIde) {
|
||||
cy.intercept('mutation-OpenFileInIDE', { data: { 'openFileInIDE': true } }).as('OpenIDE')
|
||||
cy.intercept('mutation-SpecRunnerOpenMode_OpenFileInIDE', { data: { 'openFileInIDE': true } }).as('OpenIDE')
|
||||
|
||||
action()
|
||||
|
||||
cy.wait('@OpenIDE').then(({ request }) => {
|
||||
expect(request.body.variables.input.absolute).to.include(fileName)
|
||||
expect(request.body.variables.input.filePath).to.include(fileName)
|
||||
})
|
||||
} else {
|
||||
action()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { SinonStub } from 'sinon'
|
||||
|
||||
describe('App: Settings', () => {
|
||||
before(() => {
|
||||
cy.scaffoldProject('todos', { timeout: 50 * 1000 })
|
||||
@@ -27,11 +29,15 @@ describe('App: Settings', () => {
|
||||
|
||||
it('can reconfigure a project', () => {
|
||||
cy.startAppServer('e2e')
|
||||
cy.__incorrectlyVisitAppWithIntercept('settings')
|
||||
cy.visitApp('settings')
|
||||
cy.withCtx((ctx, o) => {
|
||||
o.sinon.stub(ctx.actions.project, 'reconfigureProject')
|
||||
})
|
||||
|
||||
cy.intercept('mutation-SettingsContainer_ReconfigureProject', { 'data': { 'reconfigureProject': true } }).as('ReconfigureProject')
|
||||
cy.findByText('Reconfigure Project').click()
|
||||
cy.wait('@ReconfigureProject')
|
||||
cy.withRetryableCtx((ctx) => {
|
||||
expect(ctx.actions.project.reconfigureProject).to.have.been.called
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cloud Settings', () => {
|
||||
@@ -68,13 +74,12 @@ describe('App: Settings', () => {
|
||||
it('opens cloud settings when clicking on "Manage Keys"', () => {
|
||||
cy.startAppServer('e2e')
|
||||
cy.loginUser()
|
||||
cy.intercept('mutation-ExternalLink_OpenExternal', { 'data': { 'openExternal': true } }).as('OpenExternal')
|
||||
cy.__incorrectlyVisitAppWithIntercept('settings')
|
||||
cy.visitApp('settings')
|
||||
cy.findByText('Dashboard Settings').click()
|
||||
cy.findByText('Manage Keys').click()
|
||||
cy.wait('@OpenExternal')
|
||||
.its('request.body.variables.url')
|
||||
.should('equal', 'http:/test.cloud/cloud-project/settings')
|
||||
cy.withRetryableCtx((ctx) => {
|
||||
expect((ctx.actions.electron.openExternal as SinonStub).lastCall.lastArg).to.eq('http:/test.cloud/cloud-project/settings')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -155,15 +160,28 @@ describe('App: Settings', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: The Edit button isn't hooked up to do anything when it should trigger the openFileInIDE mutation (https://cypress-io.atlassian.net/browse/UNIFY-1164)
|
||||
it.skip('opens cypress.config.js file after clicking "Edit" button', () => {
|
||||
it('opens cypress.config.js file after clicking "Edit" button', () => {
|
||||
cy.startAppServer('e2e')
|
||||
cy.withCtx((ctx, o) => {
|
||||
ctx.coreData.localSettings.preferences.preferredEditorBinary = 'computer'
|
||||
o.sinon.stub(ctx.actions.file, 'openFile')
|
||||
})
|
||||
|
||||
cy.visitApp('/settings')
|
||||
cy.findByText('Project Settings').click()
|
||||
cy.findByRole('button', { name: 'Edit' }).click()
|
||||
cy.withRetryableCtx((ctx) => {
|
||||
expect((ctx.actions.file.openFile as SinonStub).lastCall.args[0]).to.eq(ctx.lifecycleManager.configFilePath)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('external editor', () => {
|
||||
beforeEach(() => {
|
||||
cy.startAppServer('e2e')
|
||||
cy.withCtx(async (ctx) => {
|
||||
cy.withCtx((ctx, o) => {
|
||||
o.sinon.stub(ctx.actions.localSettings, 'setPreferences')
|
||||
o.sinon.stub(ctx.actions.file, 'openFile')
|
||||
ctx.coreData.localSettings.availableEditors = [
|
||||
...ctx.coreData.localSettings.availableEditors,
|
||||
// don't rely on CI machines to have specific editors installed
|
||||
@@ -183,15 +201,15 @@ describe('App: Settings', () => {
|
||||
})
|
||||
|
||||
it('selects well known editor', () => {
|
||||
cy.intercept('POST', 'mutation-ExternalEditorSettings_SetPreferredEditorBinary').as('SetPreferred')
|
||||
|
||||
cy.contains('Choose your editor...').click()
|
||||
cy.contains('Well known editor').click()
|
||||
cy.wait('@SetPreferred').its('request.body.variables.value').should('include', '/usr/bin/well-known')
|
||||
cy.withRetryableCtx((ctx) => {
|
||||
expect((ctx.actions.localSettings.setPreferences as SinonStub).lastCall.lastArg).to.include('/usr/bin/well-known')
|
||||
})
|
||||
|
||||
// navigate away and come back
|
||||
// preferred editor selected from dropdown should have been persisted
|
||||
cy.__incorrectlyVisitAppWithIntercept()
|
||||
cy.visitApp()
|
||||
cy.get('[href="#/settings"]').click()
|
||||
cy.wait(200)
|
||||
cy.get('[data-cy="Device Settings"]').click()
|
||||
@@ -200,8 +218,6 @@ describe('App: Settings', () => {
|
||||
})
|
||||
|
||||
it('allows custom editor', () => {
|
||||
cy.intercept('POST', 'mutation-ExternalEditorSettings_SetPreferredEditorBinary').as('SetPreferred')
|
||||
|
||||
cy.contains('Choose your editor...').click()
|
||||
cy.contains('Custom').click()
|
||||
|
||||
@@ -209,7 +225,9 @@ describe('App: Settings', () => {
|
||||
// for each keystroke, making it hard to intercept **only** the final request, which I want to
|
||||
// assert contains `/usr/local/bin/vim'
|
||||
cy.findByPlaceholderText('/path/to/editor').clear().invoke('val', '/usr/local/bin/vim').trigger('input').trigger('change')
|
||||
cy.wait('@SetPreferred').its('request.body.variables.value').should('include', '/usr/local/bin/vim')
|
||||
cy.withRetryableCtx((ctx) => {
|
||||
expect((ctx.actions.localSettings.setPreferences as SinonStub).lastCall.lastArg).to.include('/usr/local/bin/vim')
|
||||
})
|
||||
|
||||
// navigate away and come back
|
||||
// preferred editor entered from input should have been persisted
|
||||
@@ -221,12 +239,12 @@ describe('App: Settings', () => {
|
||||
})
|
||||
|
||||
it('lists file browser as available editor', () => {
|
||||
cy.intercept('POST', 'mutation-ExternalEditorSettings_SetPreferredEditorBinary').as('SetPreferred')
|
||||
|
||||
cy.contains('Choose your editor...').click()
|
||||
cy.get('[data-cy="computer"]').click()
|
||||
|
||||
cy.wait('@SetPreferred').its('request.body.variables.value').should('include', 'computer')
|
||||
cy.withRetryableCtx((ctx) => {
|
||||
expect((ctx.actions.localSettings.setPreferences as SinonStub).lastCall.lastArg).to.include('computer')
|
||||
})
|
||||
|
||||
cy.get('[data-cy="custom-editor"]').should('not.exist')
|
||||
})
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('<SpecPatternModal />', () => {
|
||||
|
||||
cy.percySnapshot()
|
||||
|
||||
cy.get('[data-cy="open-config-file"').should('be.visible')
|
||||
cy.get('[data-cy="open-config-file"]').should('be.visible')
|
||||
cy.contains('button', defaultMessages.createSpec.updateSpecPattern)
|
||||
|
||||
cy.contains('button', defaultMessages.components.modal.dismiss).click()
|
||||
|
||||
@@ -19,8 +19,15 @@
|
||||
<StandardModalFooter
|
||||
class="flex gap-16px items-center"
|
||||
>
|
||||
<OpenConfigFileInIDE>
|
||||
<Button size="lg">
|
||||
<OpenConfigFileInIDE
|
||||
v-slot="{onClick}"
|
||||
:gql="props.gql"
|
||||
>
|
||||
<Button
|
||||
size="lg"
|
||||
data-cy="open-config-file"
|
||||
@click="onClick"
|
||||
>
|
||||
<template #prefix>
|
||||
<i-cy-code-editor_x16 class="icon-dark-white" />
|
||||
</template>
|
||||
@@ -52,6 +59,7 @@ gql`
|
||||
fragment SpecPatternModal on CurrentProject {
|
||||
id
|
||||
...SpecPatterns
|
||||
...OpenConfigFileInIDE
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -9,11 +9,17 @@
|
||||
{{ props.gql.specs.length }}
|
||||
</i18n-t>
|
||||
</FileMatchIndicator>
|
||||
<OpenConfigFileInIDE>
|
||||
<span class="flex items-center text-indigo-500 outline-transparent gap-8px group">
|
||||
<i-cy-document-text_x16 class="icon-light-gray-50 icon-dark-gray-300" />
|
||||
<span class="group-hocus:underline">cypress.config.js</span>
|
||||
</span>
|
||||
<OpenConfigFileInIDE
|
||||
v-slot="{onClick}"
|
||||
:gql="props.gql"
|
||||
>
|
||||
<button
|
||||
class="flex items-center text-indigo-500 outline-transparent gap-8px group"
|
||||
@click="onClick"
|
||||
>
|
||||
<i-cy-document-text_x16 class="icon-light-gray-100 icon-dark-gray-500" />
|
||||
<span class="group-hocus:underline">{{ props.gql.configFile }}</span>
|
||||
</button>
|
||||
</OpenConfigFileInIDE>
|
||||
</div>
|
||||
|
||||
@@ -42,6 +48,8 @@ fragment SpecPatterns on CurrentProject {
|
||||
id
|
||||
config
|
||||
currentTestingType
|
||||
...OpenConfigFileInIDE
|
||||
configFile
|
||||
specs {
|
||||
id
|
||||
}
|
||||
|
||||
@@ -104,7 +104,8 @@ import ScreenshotHelperPixels from './screenshot/ScreenshotHelperPixels.vue'
|
||||
import { useScreenshotStore } from '../store/screenshot-store'
|
||||
import ChooseExternalEditorModal from '@packages/frontend-shared/src/gql-components/ChooseExternalEditorModal.vue'
|
||||
import { useMutation, gql } from '@urql/vue'
|
||||
import { OpenFileInIdeDocument, SpecRunnerFragment } from '../generated/graphql'
|
||||
import { SpecRunnerOpenMode_OpenFileInIdeDocument } from '../generated/graphql'
|
||||
import type { SpecRunnerFragment } from '../generated/graphql'
|
||||
import { usePreferences } from '../composables/usePreferences'
|
||||
import ScriptError from './ScriptError.vue'
|
||||
import ResizablePanels from './ResizablePanels.vue'
|
||||
@@ -140,7 +141,7 @@ fragment SpecRunner on Query {
|
||||
`
|
||||
|
||||
gql`
|
||||
mutation OpenFileInIDE ($input: FileDetailsInput!) {
|
||||
mutation SpecRunnerOpenMode_OpenFileInIDE ($input: FileDetailsInput!) {
|
||||
openFileInIDE (input: $input)
|
||||
}
|
||||
`
|
||||
@@ -188,7 +189,7 @@ preferences.update('specListWidth', specListWidth.value)
|
||||
|
||||
let fileToOpen: FileDetails
|
||||
|
||||
const openFileInIDE = useMutation(OpenFileInIdeDocument)
|
||||
const openFileInIDE = useMutation(SpecRunnerOpenMode_OpenFileInIdeDocument)
|
||||
|
||||
function openFile () {
|
||||
runnerUiStore.setShowChooseExternalEditorModal(false)
|
||||
@@ -200,7 +201,7 @@ function openFile () {
|
||||
|
||||
openFileInIDE.executeMutation({
|
||||
input: {
|
||||
absolute: fileToOpen.absoluteFile,
|
||||
filePath: fileToOpen.absoluteFile,
|
||||
line: fileToOpen.line,
|
||||
column: fileToOpen.column,
|
||||
},
|
||||
|
||||
@@ -54,7 +54,8 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { gql, useMutation } from '@urql/vue'
|
||||
import RunsError from './RunsError.vue'
|
||||
import { RunsErrorRendererFragment, RunsErrorRenderer_RequestAccessDocument } from '../generated/graphql'
|
||||
import { RunsErrorRenderer_RequestAccessDocument } from '../generated/graphql'
|
||||
import type { RunsErrorRendererFragment } from '../generated/graphql'
|
||||
import ConnectIcon from '~icons/cy/chain-link_x16.svg'
|
||||
import SendIcon from '~icons/cy/paper-airplane_x16.svg'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
|
||||
@@ -157,7 +157,8 @@ import ConnectIcon from '~icons/cy/chain-link_x16.svg'
|
||||
import CreateIcon from '~icons/cy/add-large_x16.svg'
|
||||
import FolderIcon from '~icons/cy/folder-outline_x16.svg'
|
||||
import OrganizationIcon from '~icons/cy/office-building_x16.svg'
|
||||
import { SelectCloudProjectModalFragment, SelectCloudProjectModal_CreateCloudProjectDocument, SelectCloudProjectModal_SetProjectIdDocument } from '../../generated/graphql'
|
||||
import { SelectCloudProjectModal_CreateCloudProjectDocument, SelectCloudProjectModal_SetProjectIdDocument } from '../../generated/graphql'
|
||||
import type { SelectCloudProjectModalFragment } from '../../generated/graphql'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
max-height="10000px"
|
||||
>
|
||||
<ProjectSettings
|
||||
v-if="props.gql"
|
||||
:gql="props.gql"
|
||||
v-if="props.gql.currentProject"
|
||||
:gql="props.gql.currentProject"
|
||||
/>
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
@@ -57,8 +57,8 @@ import Button from '@cy/components/Button.vue'
|
||||
import ExternalEditorSettings from './device/ExternalEditorSettings.vue'
|
||||
import ProxySettings from './device/ProxySettings.vue'
|
||||
import SettingsCard from './SettingsCard.vue'
|
||||
import CloudSettings from './project/CloudSettings.vue'
|
||||
import ProjectSettings from './project/ProjectSettings.vue'
|
||||
import CloudSettings from '../settings/project/CloudSettings.vue'
|
||||
import TestingPreferences from './device/TestingPreferences.vue'
|
||||
import type { SettingsContainerFragment } from '../generated/graphql'
|
||||
import { SettingsContainer_ReconfigureProjectDocument } from '../generated/graphql'
|
||||
@@ -78,7 +78,10 @@ mutation SettingsContainer_ReconfigureProject {
|
||||
gql`
|
||||
fragment SettingsContainer on Query {
|
||||
...TestingPreferences
|
||||
...ProjectSettings
|
||||
currentProject {
|
||||
id
|
||||
...ProjectSettings
|
||||
}
|
||||
...CloudSettings
|
||||
...ExternalEditorSettings
|
||||
...ProxySettings
|
||||
|
||||
@@ -20,7 +20,8 @@ import SettingsSection from '../SettingsSection.vue'
|
||||
import ChooseExternalEditor from '@packages/frontend-shared/src/gql-components/ChooseExternalEditor.vue'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import { gql } from '@urql/core'
|
||||
import { ExternalEditorSettingsFragment, ExternalEditorSettings_SetPreferredEditorBinaryDocument } from '../../generated/graphql'
|
||||
import { ExternalEditorSettings_SetPreferredEditorBinaryDocument } from '../../generated/graphql'
|
||||
import type { ExternalEditorSettingsFragment } from '../../generated/graphql'
|
||||
import { useMutation } from '@urql/vue'
|
||||
|
||||
gql`
|
||||
|
||||
@@ -32,7 +32,8 @@ import SettingsSection from '../SettingsSection.vue'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import Switch from '@packages/frontend-shared/src/components/Switch.vue'
|
||||
import { gql, useMutation } from '@urql/vue'
|
||||
import { SetTestingPreferencesDocument, TestingPreferencesFragment } from '../../generated/graphql'
|
||||
import { SetTestingPreferencesDocument } from '../../generated/graphql'
|
||||
import type { TestingPreferencesFragment } from '../../generated/graphql'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('<Config/>', { viewportWidth: 1200, viewportHeight: 1600 }, () => {
|
||||
const sourceTypes = ['default', 'config', 'env', 'cli', 'plugin']
|
||||
|
||||
sourceTypes.forEach((sourceType) => {
|
||||
result.currentProject?.config?.unshift({
|
||||
result.config?.unshift({
|
||||
field: sourceType,
|
||||
value: `testValue-${sourceType}`,
|
||||
from: sourceType,
|
||||
|
||||
@@ -10,15 +10,16 @@
|
||||
scope="global"
|
||||
keypath="settingsPage.config.description"
|
||||
>
|
||||
<OpenConfigFileInIDE />
|
||||
<OpenConfigFileInIDE :gql="props.gql" />
|
||||
</i18n-t>
|
||||
</template>
|
||||
<div class="flex w-full">
|
||||
<ConfigCode
|
||||
data-cy="config-code"
|
||||
:config="configObject"
|
||||
:gql="props.gql"
|
||||
/>
|
||||
<ConfigLegend
|
||||
:gql="props.gql"
|
||||
data-cy="config-legend"
|
||||
class="rounded-tr-md rounded-br-md border-1 border-l-0 min-w-280px py-28px px-22px"
|
||||
/>
|
||||
@@ -27,7 +28,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { gql } from '@urql/vue'
|
||||
import SettingsSection from '../SettingsSection.vue'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
@@ -39,17 +39,14 @@ import OpenConfigFileInIDE from '@packages/frontend-shared/src/gql-components/Op
|
||||
const { t } = useI18n()
|
||||
|
||||
gql`
|
||||
fragment Config on Query {
|
||||
currentProject {
|
||||
id
|
||||
config
|
||||
}
|
||||
fragment Config on CurrentProject {
|
||||
id
|
||||
...OpenConfigFileInIDE
|
||||
...ConfigCode
|
||||
}
|
||||
`
|
||||
|
||||
const props = defineProps<{
|
||||
gql: ConfigFragment
|
||||
}>()
|
||||
|
||||
const configObject = computed(() => props.gql.currentProject?.config)
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ConfigCode from './ConfigCode.vue'
|
||||
import config from '../../../../frontend-shared/cypress/fixtures/config.json'
|
||||
import config from '@packages/frontend-shared/cypress/fixtures/config.json'
|
||||
import { defaultMessages } from '@cy/i18n'
|
||||
|
||||
const selector = '[data-cy=code]'
|
||||
@@ -16,11 +16,16 @@ describe('<ConfigCode />', () => {
|
||||
context('with mock values', () => {
|
||||
it('shows the arrayTest nicely', () => {
|
||||
cy.mount(() => (<div class="p-12 overflow-auto">
|
||||
<ConfigCode data-cy="code" config={[{
|
||||
field: 'arrayTest',
|
||||
value: arrayTest,
|
||||
from: 'plugin',
|
||||
}]} />
|
||||
<ConfigCode data-cy="code" gql={{
|
||||
id: 'project-id',
|
||||
configFile: 'cypress.config.js',
|
||||
configFileAbsolutePath: '/path/to/cypress.config.js',
|
||||
config: [{
|
||||
field: 'arrayTest',
|
||||
value: arrayTest,
|
||||
from: 'plugin',
|
||||
}],
|
||||
}} />
|
||||
</div>))
|
||||
|
||||
cy.contains(`arrayTest:`).should('contain.text', `['${arrayTest.join('\', \'')}', ]`)
|
||||
@@ -28,11 +33,16 @@ describe('<ConfigCode />', () => {
|
||||
|
||||
it('shows the objectTest nicely', () => {
|
||||
cy.mount(() => (<div class="p-12 overflow-auto">
|
||||
<ConfigCode data-cy="code" config={[{
|
||||
field: 'objectTest',
|
||||
value: objectTest,
|
||||
from: 'env',
|
||||
}]} />
|
||||
<ConfigCode data-cy="code" gql={{
|
||||
id: 'project-id',
|
||||
configFile: 'cypress.config.js',
|
||||
configFileAbsolutePath: '/path/to/cypress.config.js',
|
||||
config: [{
|
||||
field: 'objectTest',
|
||||
value: objectTest,
|
||||
from: 'env',
|
||||
}],
|
||||
}} />
|
||||
</div>))
|
||||
|
||||
const expectedText = `{${Object.entries(objectTest).map(([key, value]) => `'${key}': '${value}'`).join(',')},}`
|
||||
@@ -44,7 +54,12 @@ describe('<ConfigCode />', () => {
|
||||
context('with real config file', () => {
|
||||
beforeEach(() => {
|
||||
cy.mount(() => (<div class="p-12 overflow-auto">
|
||||
<ConfigCode data-cy="code" config={config as any} />
|
||||
<ConfigCode data-cy="code" gql={{
|
||||
id: 'project-id',
|
||||
configFile: 'cypress.config.js',
|
||||
configFileAbsolutePath: '/path/to/cypress.config.js',
|
||||
config,
|
||||
}} />
|
||||
</div>))
|
||||
})
|
||||
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
<template>
|
||||
<div class="rounded-bl-md rounded-tl-md mx-auto border-1 w-full min-w-100px relative hide-scrollbar overflow-auto grow-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="top-4 right-4 absolute"
|
||||
:prefix-icon="IconCode"
|
||||
prefix-icon-class="text-gray-500"
|
||||
<OpenConfigFileInIDE
|
||||
v-slot="{onClick}"
|
||||
:gql="props.gql"
|
||||
>
|
||||
{{ t('file.edit') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="top-4 right-4 absolute"
|
||||
:prefix-icon="IconCode"
|
||||
prefix-icon-class="text-gray-500"
|
||||
@click="onClick"
|
||||
>
|
||||
{{ t('file.edit') }}
|
||||
</Button>
|
||||
</OpenConfigFileInIDE>
|
||||
<code
|
||||
class="font-thin p-16px text-gray-600 text-size-14px leading-24px block"
|
||||
>
|
||||
{<br>
|
||||
<div class="pl-24px">
|
||||
<span
|
||||
v-for="{ field, value, from } in config"
|
||||
v-for="{ field, value, from } in props.gql.config"
|
||||
:key="field"
|
||||
>
|
||||
{{ field }}:
|
||||
@@ -47,12 +53,22 @@ import IconCode from '~icons/mdi/code'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import { CONFIG_LEGEND_COLOR_MAP } from './ConfigSourceColors'
|
||||
import Browsers from './renderers/Browsers.vue'
|
||||
import type { CypressResolvedConfig } from './projectSettings'
|
||||
import RenderObject from './renderers/RenderObject.vue'
|
||||
import { renderPrimitive } from './renderers/renderPrimitive'
|
||||
import { gql } from '@urql/core'
|
||||
import OpenConfigFileInIDE from '@packages/frontend-shared/src/gql-components/OpenConfigFileInIDE.vue'
|
||||
import type { ConfigCodeFragment } from '../../generated/graphql'
|
||||
|
||||
defineProps<{
|
||||
config: CypressResolvedConfig
|
||||
gql`
|
||||
fragment ConfigCode on CurrentProject {
|
||||
id
|
||||
config
|
||||
...OpenConfigFileInIDE
|
||||
}
|
||||
`
|
||||
|
||||
const props = defineProps<{
|
||||
gql: ConfigCodeFragment
|
||||
}>()
|
||||
|
||||
// a bug in vite demands that we do this passthrough
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { defaultMessages } from '@cy/i18n'
|
||||
import ConfigLegend from './ConfigLegend.vue'
|
||||
import { each } from 'lodash'
|
||||
import { OpenConfigFileInIdeFragmentDoc } from '../../generated/graphql-test'
|
||||
|
||||
const legend = defaultMessages.settingsPage.config.legend
|
||||
|
||||
describe('<ConfigLegend/>', () => {
|
||||
it('renders', () => {
|
||||
cy.mount(ConfigLegend)
|
||||
cy.mountFragment(OpenConfigFileInIdeFragmentDoc, {
|
||||
render: (gql) => <ConfigLegend gql={gql} />,
|
||||
})
|
||||
|
||||
cy.get('[data-cy="external"]').should('have.attr', 'href').and('eq', 'https://on.cypress.io/setup-node-events')
|
||||
|
||||
each(legend, ({ label, description }) => {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
scope="global"
|
||||
:keypath="legendText.config.descriptionKey"
|
||||
>
|
||||
<OpenConfigFileInIDE />
|
||||
<OpenConfigFileInIDE :gql="props.gql" />
|
||||
</i18n-t>
|
||||
</ConfigBadge>
|
||||
|
||||
@@ -59,6 +59,11 @@ import { computed } from 'vue'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import { CONFIG_LEGEND_COLOR_MAP } from './ConfigSourceColors'
|
||||
import OpenConfigFileInIDE from '@packages/frontend-shared/src/gql-components/OpenConfigFileInIDE.vue'
|
||||
import type { OpenConfigFileInIdeFragment } from '../../generated/graphql'
|
||||
|
||||
const props = defineProps<{
|
||||
gql: OpenConfigFileInIdeFragment
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const legendText = computed(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<SpecPatterns :gql="props.gql.currentProject" />
|
||||
<Experiments :gql="props.gql.currentProject" />
|
||||
<SpecPatterns :gql="props.gql" />
|
||||
<Experiments :gql="props.gql" />
|
||||
<Config :gql="props.gql" />
|
||||
</template>
|
||||
|
||||
@@ -12,12 +12,10 @@ import SpecPatterns from './SpecPatterns.vue'
|
||||
import type { ProjectSettingsFragment } from '../../generated/graphql'
|
||||
|
||||
gql`
|
||||
fragment ProjectSettings on Query {
|
||||
currentProject {
|
||||
id
|
||||
...Experiments
|
||||
...SpecPatterns_Settings
|
||||
}
|
||||
fragment ProjectSettings on CurrentProject {
|
||||
id
|
||||
...Experiments
|
||||
...SpecPatterns_Settings
|
||||
...Config
|
||||
}
|
||||
`
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { generatorList } from './generators'
|
||||
|
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
import type { GeneratorId } from './generators'
|
||||
import { computed } from 'vue'
|
||||
import type { CreateSpecCardsFragment } from '../generated/graphql'
|
||||
|
||||
@@ -25,6 +25,8 @@ describe('<CreateSpecModal />', () => {
|
||||
},
|
||||
storybook: null,
|
||||
currentTestingType: 'component',
|
||||
configFile: 'cypress.config.js',
|
||||
configFileAbsolutePath: '/path/to/cypress.config.js',
|
||||
config: {
|
||||
e2e: {
|
||||
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
|
||||
@@ -91,6 +93,8 @@ describe('playground', () => {
|
||||
},
|
||||
storybook: null,
|
||||
currentTestingType: 'component',
|
||||
configFile: 'cypress.config.js',
|
||||
configFileAbsolutePath: '/path/to/cypress.config.js',
|
||||
config: {
|
||||
e2e: {
|
||||
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
|
||||
|
||||
@@ -41,11 +41,13 @@
|
||||
</template>
|
||||
|
||||
<script lang ="ts" setup>
|
||||
import { generators, GeneratorId } from './generators'
|
||||
import { generators } from './generators'
|
||||
import type { GeneratorId } from './generators'
|
||||
import { DialogOverlay } from '@headlessui/vue'
|
||||
import StandardModal from '@cy/components/StandardModal.vue'
|
||||
import CreateSpecCards from './CreateSpecCards.vue'
|
||||
import { ref, computed, Ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import type { CreateSpecModalFragment } from '../generated/graphql'
|
||||
import { gql } from '@urql/vue'
|
||||
import { not, whenever } from '@vueuse/core'
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<!-- TODO: spread on props.gql is needed due to bug in mountFragment. Fix -->
|
||||
<SpecPatterns
|
||||
v-if="props.gql.currentProject"
|
||||
:gql="{...props.gql.currentProject}"
|
||||
/>
|
||||
<SpecPatterns :gql="{...props.gql}" />
|
||||
|
||||
<div class="flex mt-32px gap-16px justify-center">
|
||||
<OpenConfigFileInIDE>
|
||||
<OpenConfigFileInIDE
|
||||
v-slot="{onClick}"
|
||||
:gql="props.gql"
|
||||
>
|
||||
<Button
|
||||
size="lg"
|
||||
@click="onClick"
|
||||
>
|
||||
<template #prefix>
|
||||
<i-cy-code-editor_x16 class="icon-dark-white" />
|
||||
@@ -41,11 +42,11 @@ import OpenConfigFileInIDE from '@packages/frontend-shared/src/gql-components/Op
|
||||
const { t } = useI18n()
|
||||
|
||||
gql`
|
||||
fragment CustomPatternNoSpecContent on Query {
|
||||
currentProject {
|
||||
id
|
||||
...SpecPatterns
|
||||
}
|
||||
fragment CustomPatternNoSpecContent on CurrentProject {
|
||||
id
|
||||
...SpecPatterns
|
||||
...OpenConfigFileInIDE
|
||||
configFileAbsolutePath
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ import type { Specs_InlineSpecListFragment } from '../generated/graphql'
|
||||
import InlineSpecListHeader from './InlineSpecListHeader.vue'
|
||||
import InlineSpecListTree from './InlineSpecListTree.vue'
|
||||
import CreateSpecModal from './CreateSpecModal.vue'
|
||||
import { FuzzyFoundSpec, fuzzySortSpecs, makeFuzzyFoundSpec, useCachedSpecs } from '@packages/frontend-shared/src/utils/spec-utils'
|
||||
import { fuzzySortSpecs, makeFuzzyFoundSpec, useCachedSpecs } from '@packages/frontend-shared/src/utils/spec-utils'
|
||||
import type { FuzzyFoundSpec } from '@packages/frontend-shared/src/utils/spec-utils'
|
||||
import { useDebounce } from '@vueuse/core'
|
||||
|
||||
gql`
|
||||
|
||||
@@ -59,8 +59,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCollapsibleTree, UseCollapsibleTreeNode } from '@packages/frontend-shared/src/composables/useCollapsibleTree'
|
||||
import { buildSpecTree, FuzzyFoundSpec, SpecTreeNode, getDirIndexes } from '@packages/frontend-shared/src/utils/spec-utils'
|
||||
import { useCollapsibleTree } from '@packages/frontend-shared/src/composables/useCollapsibleTree'
|
||||
import type { UseCollapsibleTreeNode } from '@packages/frontend-shared/src/composables/useCollapsibleTree'
|
||||
import { buildSpecTree, getDirIndexes } from '@packages/frontend-shared/src/utils/spec-utils'
|
||||
import type { SpecTreeNode, FuzzyFoundSpec } from '@packages/frontend-shared/src/utils/spec-utils'
|
||||
import SpecFileItem from './SpecFileItem.vue'
|
||||
import { computed, watch, onMounted } from 'vue'
|
||||
import DirectoryItem from './DirectoryItem.vue'
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
/>
|
||||
<CustomPatternNoSpecContent
|
||||
v-else
|
||||
:gql="props.gql"
|
||||
:gql="props.gql.currentProject"
|
||||
@showCreateSpecModal="showCreateSpecModal"
|
||||
/>
|
||||
</div>
|
||||
@@ -51,11 +51,11 @@ gql`
|
||||
fragment NoSpecsPage on Query {
|
||||
...CreateSpecCards
|
||||
...ChooseExternalEditor
|
||||
...CustomPatternNoSpecContent
|
||||
currentProject {
|
||||
id
|
||||
currentTestingType
|
||||
configFileAbsolutePath
|
||||
...CustomPatternNoSpecContent
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -108,7 +108,8 @@ import { gql } from '@urql/vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { Specs_SpecsListFragment, SpecListRowFragment } from '../generated/graphql'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import { buildSpecTree, FuzzyFoundSpec, fuzzySortSpecs, getDirIndexes, makeFuzzyFoundSpec, useCachedSpecs } from '@packages/frontend-shared/src/utils/spec-utils'
|
||||
import { buildSpecTree, fuzzySortSpecs, getDirIndexes, makeFuzzyFoundSpec, useCachedSpecs } from '@packages/frontend-shared/src/utils/spec-utils'
|
||||
import type { FuzzyFoundSpec } from '@packages/frontend-shared/src/utils/spec-utils'
|
||||
import { useCollapsibleTree } from '@packages/frontend-shared/src/composables/useCollapsibleTree'
|
||||
import RowDirectory from './RowDirectory.vue'
|
||||
import SpecItem from './SpecItem.vue'
|
||||
|
||||
8
packages/app/vue-shims.d.ts
vendored
8
packages/app/vue-shims.d.ts
vendored
@@ -1,18 +1,16 @@
|
||||
declare module 'virtual:*' {
|
||||
import { Component } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
const src: Component
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module 'virtual:icons/*' {
|
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
import { FunctionalComponent, SVGAttributes } from 'vue'
|
||||
import type { FunctionalComponent, SVGAttributes } from 'vue'
|
||||
const component: FunctionalComponent<SVGAttributes>
|
||||
export default component
|
||||
}
|
||||
declare module '~icons/*' {
|
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
import { FunctionalComponent, SVGAttributes } from 'vue'
|
||||
import type { FunctionalComponent, SVGAttributes } from 'vue'
|
||||
const component: FunctionalComponent<SVGAttributes>
|
||||
export default component
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"test-unit": "mocha -r @packages/ts/register --config ./test/.mocharc.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "7.8.3",
|
||||
"@babel/parser": "7.13.0",
|
||||
"@storybook/csf-tools": "^6.4.0-alpha.38",
|
||||
"@urql/core": "2.3.1",
|
||||
@@ -57,6 +58,7 @@
|
||||
"@packages/ts": "0.0.0-development",
|
||||
"@packages/types": "0.0.0-development",
|
||||
"@tooling/system-tests": "0.0.0-development",
|
||||
"@types/babel__code-frame": "^7.0.3",
|
||||
"@types/dedent": "^0.7.0",
|
||||
"@types/ejs": "^3.1.0",
|
||||
"@types/fs-extra": "^8.0.1",
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from 'path'
|
||||
import util from 'util'
|
||||
import chalk from 'chalk'
|
||||
import assert from 'assert'
|
||||
import s from 'underscore.string'
|
||||
import str from 'underscore.string'
|
||||
|
||||
import 'server-destroy'
|
||||
|
||||
@@ -40,6 +40,7 @@ import type { Socket, SocketIOServer } from '@packages/socket'
|
||||
import { globalPubSub } from '.'
|
||||
import { InjectedConfigApi, ProjectLifecycleManager } from './data/ProjectLifecycleManager'
|
||||
import type { CypressError } from '@packages/errors'
|
||||
import { ErrorDataSource } from './sources/ErrorDataSource'
|
||||
|
||||
const IS_DEV_ENV = process.env.CYPRESS_INTERNAL_ENV !== 'production'
|
||||
|
||||
@@ -125,16 +126,7 @@ export class DataContext {
|
||||
}
|
||||
|
||||
get baseError () {
|
||||
if (!this.coreData.baseError) {
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO: Standardize approach to serializing errors
|
||||
return {
|
||||
title: this.coreData.baseError.title,
|
||||
message: this.coreData.baseError.message,
|
||||
stack: this.coreData.baseError.stack,
|
||||
}
|
||||
return this.coreData.baseError
|
||||
}
|
||||
|
||||
@cached
|
||||
@@ -216,6 +208,11 @@ export class DataContext {
|
||||
return new HtmlDataSource(this)
|
||||
}
|
||||
|
||||
@cached
|
||||
get error () {
|
||||
return new ErrorDataSource(this)
|
||||
}
|
||||
|
||||
@cached
|
||||
get util () {
|
||||
return new UtilDataSource(this)
|
||||
@@ -347,15 +344,19 @@ export class DataContext {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
onError = (err: Error) => {
|
||||
onError = (cypressError: CypressError, title?: string) => {
|
||||
if (this.isRunMode) {
|
||||
if (this.lifecycleManager?.runModeExitEarly) {
|
||||
this.lifecycleManager.runModeExitEarly(err)
|
||||
this.lifecycleManager.runModeExitEarly(cypressError)
|
||||
} else {
|
||||
throw err
|
||||
throw cypressError
|
||||
}
|
||||
} else {
|
||||
this.coreData.baseError = err
|
||||
this.update((coreData) => {
|
||||
coreData.baseError = { title, cypressError }
|
||||
})
|
||||
|
||||
this.emitter.toLaunchpad()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,9 +366,8 @@ export class DataContext {
|
||||
console.log(chalk.yellow(err.message))
|
||||
} else {
|
||||
this.coreData.warnings.push({
|
||||
title: `Warning: ${s.titleize(s.humanize(err.type ?? ''))}`,
|
||||
message: err.messageMarkdown || err.message,
|
||||
details: err.details,
|
||||
title: `Warning: ${str.titleize(str.humanize(err.type ?? ''))}`,
|
||||
cypressError: err,
|
||||
})
|
||||
|
||||
this.emitter.toLaunchpad()
|
||||
@@ -401,12 +401,7 @@ export class DataContext {
|
||||
* Resets all of the state for the data context,
|
||||
* so we can initialize fresh for each E2E test
|
||||
*/
|
||||
async resetForTest (modeOptions: Partial<AllModeOptions> = {}) {
|
||||
this.debug('DataContext resetForTest')
|
||||
if (!process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
|
||||
throw new Error(`DataContext.reset is only meant to be called in E2E testing mode, there's no good use for it outside of that`)
|
||||
}
|
||||
|
||||
async reinitializeCypress (modeOptions: Partial<AllModeOptions> = {}) {
|
||||
await this._reset()
|
||||
|
||||
this._modeOptions = modeOptions
|
||||
@@ -418,12 +413,6 @@ export class DataContext {
|
||||
}
|
||||
|
||||
private _reset () {
|
||||
// this._gqlServer?.close()
|
||||
// this.emitter.destroy()
|
||||
// this._loadingManager.destroy()
|
||||
// this._loadingManager = new LoadingManager(this)
|
||||
// this.coreData.currentProject?.watcher
|
||||
// this._coreData = makeCoreData({}, this._loadingManager)
|
||||
this.setAppSocketServer(undefined)
|
||||
this.setGqlSocketServer(undefined)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from 'path'
|
||||
// @ts-ignore - no types available
|
||||
import launchEditor from 'launch-editor'
|
||||
import type { DataContext } from '..'
|
||||
import assert from 'assert'
|
||||
|
||||
export class FileActions {
|
||||
constructor (private ctx: DataContext) {}
|
||||
@@ -66,9 +67,12 @@ export class FileActions {
|
||||
}
|
||||
}
|
||||
|
||||
openFile (absolute: string, line: number = 1, column: number = 1) {
|
||||
openFile (filePath: string, line: number = 1, column: number = 1) {
|
||||
assert(this.ctx.currentProject)
|
||||
const binary = this.ctx.coreData.localSettings.preferences.preferredEditorBinary
|
||||
|
||||
const absolute = path.resolve(this.ctx.currentProject, filePath)
|
||||
|
||||
if (!binary || !absolute) {
|
||||
this.ctx.debug('cannot open file without binary')
|
||||
|
||||
|
||||
@@ -61,12 +61,8 @@ export class MigrationActions {
|
||||
|
||||
const projectRoot = this.ctx.path.join(this.ctx.currentProject)
|
||||
|
||||
try {
|
||||
await moveSpecFiles(projectRoot, specsToMove)
|
||||
await cleanUpIntegrationFolder(this.ctx.currentProject)
|
||||
} catch (err: any) {
|
||||
this.ctx.coreData.baseError = err
|
||||
}
|
||||
await moveSpecFiles(projectRoot, specsToMove)
|
||||
await cleanUpIntegrationFolder(this.ctx.currentProject)
|
||||
}
|
||||
|
||||
async renameSupportFile () {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-dupe-class-members */
|
||||
import type { CypressError, SerializedError } from '@packages/errors'
|
||||
import type { CypressError } from '@packages/errors'
|
||||
import type { TestingType } from '@packages/types'
|
||||
import type { ChildProcess } from 'child_process'
|
||||
import EventEmitter from 'events'
|
||||
@@ -74,13 +74,13 @@ export class ProjectConfigIpc extends EventEmitter {
|
||||
*/
|
||||
once(evt: 'ready', listener: () => void): this
|
||||
once(evt: 'loadConfig:reply', listener: (payload: SerializedLoadConfigReply) => void): this
|
||||
once(evt: 'loadConfig:error', listener: (err: SerializedError) => void): this
|
||||
once(evt: 'loadConfig:error', listener: (err: CypressError) => void): this
|
||||
|
||||
/**
|
||||
* When
|
||||
*/
|
||||
once(evt: 'setupTestingType:reply', listener: (payload: SetupNodeEventsReply) => void): this
|
||||
once(evt: 'setupTestingType:error', listener: (error: SerializedError) => void): this
|
||||
once(evt: 'setupTestingType:error', listener: (error: CypressError) => void): this
|
||||
once (evt: string, listener: (...args: any[]) => void) {
|
||||
return super.once(evt, listener)
|
||||
}
|
||||
|
||||
@@ -16,12 +16,11 @@ import debugLib from 'debug'
|
||||
import pDefer from 'p-defer'
|
||||
import fs from 'fs'
|
||||
|
||||
import { getError, CypressError, ConfigValidationError } from '@packages/errors'
|
||||
import { getError, CypressError, ConfigValidationFailureInfo } from '@packages/errors'
|
||||
import type { DataContext } from '..'
|
||||
import { LoadConfigReply, SetupNodeEventsReply, ProjectConfigIpc, IpcHandler } from './ProjectConfigIpc'
|
||||
import assert from 'assert'
|
||||
import type { AllModeOptions, BreakingErrResult, BreakingOption, FoundBrowser, FullConfig, TestingType } from '@packages/types'
|
||||
import type { BaseErrorDataShape } from '.'
|
||||
import { autoBindDebug } from '../util/autoBindDebug'
|
||||
|
||||
const debug = debugLib(`cypress:lifecycle:ProjectLifecycleManager`)
|
||||
@@ -48,7 +47,7 @@ type BreakingValidationFn<T> = (type: BreakingOption, val: BreakingErrResult) =>
|
||||
export interface InjectedConfigApi {
|
||||
cypressVersion: string
|
||||
getServerPluginHandlers: () => IpcHandler[]
|
||||
validateConfig<T extends Cypress.ConfigOptions>(config: Partial<T>, onErr: (errMsg: ConfigValidationError | string) => never): T
|
||||
validateConfig<T extends Cypress.ConfigOptions>(config: Partial<T>, onErr: (errMsg: ConfigValidationFailureInfo | string) => never): T
|
||||
allowedConfig(config: Cypress.ConfigOptions): Cypress.ConfigOptions
|
||||
updateWithPluginValues(config: FullConfig, modifiedConfig: Partial<Cypress.ConfigOptions>): FullConfig
|
||||
setupFullConfigWithDefaults(config: SetupFullConfigOptions): Promise<FullConfig>
|
||||
@@ -184,30 +183,6 @@ export class ProjectLifecycleManager {
|
||||
return null
|
||||
}
|
||||
|
||||
get errorLoadingConfigFile (): BaseErrorDataShape | null {
|
||||
if (this._configResult.state === 'errored') {
|
||||
return {
|
||||
title: 'Error Loading Config',
|
||||
message: this._configResult.value?.messageMarkdown || '',
|
||||
stack: this._configResult.value?.stack,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
get errorLoadingNodeEvents (): BaseErrorDataShape | null {
|
||||
if (this._eventsIpcResult.state === 'errored') {
|
||||
return {
|
||||
title: 'Error Loading Config',
|
||||
message: this._eventsIpcResult.value?.messageMarkdown || '',
|
||||
stack: this._eventsIpcResult.value?.stack,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
get isLoadingConfigFile () {
|
||||
return this._configResult.state === 'loading'
|
||||
}
|
||||
@@ -636,18 +611,26 @@ export class ProjectLifecycleManager {
|
||||
}
|
||||
|
||||
if (shouldReloadConfig) {
|
||||
this.ctx.coreData.baseError = null
|
||||
this.ctx.update((coreData) => {
|
||||
coreData.baseError = null
|
||||
})
|
||||
|
||||
this.reloadConfig().catch(this.onLoadError)
|
||||
}
|
||||
}).on('error', (err) => {
|
||||
})
|
||||
|
||||
legacyFileWatcher.on('error', (err) => {
|
||||
debug('error watching config files %O', err)
|
||||
this.ctx.coreData.baseError = err
|
||||
this.ctx.onWarning(getError('UNEXPECTED_INTERNAL_ERROR', err))
|
||||
})
|
||||
|
||||
const cypressEnvFileWatcher = this.addWatcher(this.envFilePath)
|
||||
|
||||
cypressEnvFileWatcher.on('all', () => {
|
||||
this.ctx.coreData.baseError = null
|
||||
this.ctx.update((coreData) => {
|
||||
coreData.baseError = null
|
||||
})
|
||||
|
||||
this.reloadCypressEnvFile().catch(this.onLoadError)
|
||||
})
|
||||
}
|
||||
@@ -939,7 +922,7 @@ export class ProjectLifecycleManager {
|
||||
this._registeredEvents[event] = callback
|
||||
}
|
||||
|
||||
resetForTest () {
|
||||
reinitializeCypress () {
|
||||
this.resetInternalState()
|
||||
this._registeredEvents = {}
|
||||
this._handlers = []
|
||||
@@ -1328,19 +1311,19 @@ export class ProjectLifecycleManager {
|
||||
private configFileWarningCheck () {
|
||||
// Only if they've explicitly specified a config file path do we error, otherwise they'll go through onboarding
|
||||
if (!this.metaState.hasValidConfigFile && this.metaState.hasSpecifiedConfigViaCLI !== false && this.ctx.isRunMode) {
|
||||
this.ctx.onError(getError('CONFIG_FILE_NOT_FOUND', path.basename(this.metaState.hasSpecifiedConfigViaCLI), path.dirname(this.metaState.hasSpecifiedConfigViaCLI)))
|
||||
this.onLoadError(getError('CONFIG_FILE_NOT_FOUND', path.basename(this.metaState.hasSpecifiedConfigViaCLI), path.dirname(this.metaState.hasSpecifiedConfigViaCLI)))
|
||||
}
|
||||
|
||||
if (this.metaState.hasLegacyCypressJson && !this.metaState.hasValidConfigFile && this.ctx.isRunMode) {
|
||||
this.ctx.onError(getError('CONFIG_FILE_MIGRATION_NEEDED', this.projectRoot))
|
||||
this.onLoadError(getError('CONFIG_FILE_MIGRATION_NEEDED', this.projectRoot))
|
||||
}
|
||||
|
||||
if (this.metaState.hasMultipleConfigPaths) {
|
||||
this.ctx.onError(getError('CONFIG_FILES_LANGUAGE_CONFLICT', this.projectRoot, 'cypress.config.js', 'cypress.config.ts'))
|
||||
this.onLoadError(getError('CONFIG_FILES_LANGUAGE_CONFLICT', this.projectRoot, 'cypress.config.js', 'cypress.config.ts'))
|
||||
}
|
||||
|
||||
if (this.metaState.hasValidConfigFile && this.metaState.hasLegacyCypressJson) {
|
||||
this.ctx.onError(getError('LEGACY_CONFIG_FILE', path.basename(this.configFilePath), this.projectRoot))
|
||||
this.onLoadError(getError('LEGACY_CONFIG_FILE', path.basename(this.configFilePath), this.projectRoot))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1351,7 +1334,11 @@ export class ProjectLifecycleManager {
|
||||
* for run mode
|
||||
*/
|
||||
private onLoadError = (err: any) => {
|
||||
this._pendingInitialize?.reject(err)
|
||||
if (this.ctx.isRunMode && this._pendingInitialize) {
|
||||
this._pendingInitialize.reject(err)
|
||||
} else {
|
||||
this.ctx.onError(err, 'Error Loading Config')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { FoundBrowser, Editor, Warning, AllowedState, AllModeOptions, TestingType, BrowserStatus, PACKAGE_MANAGERS, AuthStateName } from '@packages/types'
|
||||
import type { FoundBrowser, Editor, AllowedState, AllModeOptions, TestingType, BrowserStatus, PACKAGE_MANAGERS, AuthStateName } from '@packages/types'
|
||||
import type { Bundler, FRONTEND_FRAMEWORKS } from '@packages/scaffold-config'
|
||||
import type { NexusGenEnums, NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen'
|
||||
import type { App, BrowserWindow } from 'electron'
|
||||
import type { ChildProcess } from 'child_process'
|
||||
import type { SocketIOServer } from '@packages/socket'
|
||||
import type { Server } from 'http'
|
||||
import type { ErrorWrapperSource } from '@packages/errors'
|
||||
|
||||
export type Maybe<T> = T | null | undefined
|
||||
|
||||
@@ -82,12 +83,6 @@ export interface ElectronShape {
|
||||
browserWindow: BrowserWindow | null
|
||||
}
|
||||
|
||||
export interface BaseErrorDataShape {
|
||||
title?: string
|
||||
message: string
|
||||
stack?: string
|
||||
}
|
||||
|
||||
export interface AuthStateShape {
|
||||
name?: AuthStateName
|
||||
message?: string
|
||||
@@ -113,7 +108,8 @@ export interface CoreDataShape {
|
||||
gqlSocketServer?: Maybe<SocketIOServer>
|
||||
}
|
||||
hasInitializedMode: 'run' | 'open' | null
|
||||
baseError: BaseErrorDataShape | null
|
||||
baseError: ErrorWrapperSource | null
|
||||
dashboardGraphQLError: ErrorWrapperSource | null
|
||||
dev: DevStateShape
|
||||
localSettings: LocalSettingsDataShape
|
||||
app: AppDataShape
|
||||
@@ -125,7 +121,7 @@ export interface CoreDataShape {
|
||||
electron: ElectronShape
|
||||
authState: AuthStateShape
|
||||
scaffoldedFiles: NexusGenObjects['ScaffoldedFile'][] | null
|
||||
warnings: Warning[]
|
||||
warnings: ErrorWrapperSource[]
|
||||
packageManager: typeof PACKAGE_MANAGERS[number]
|
||||
forceReconfigureProject: ForceReconfigureProjectDataShape | null
|
||||
}
|
||||
@@ -141,6 +137,7 @@ export function makeCoreData (modeOptions: Partial<AllModeOptions> = {}): CoreDa
|
||||
machineBrowsers: null,
|
||||
hasInitializedMode: null,
|
||||
baseError: null,
|
||||
dashboardGraphQLError: null,
|
||||
dev: {
|
||||
refreshState: null,
|
||||
},
|
||||
|
||||
@@ -42,8 +42,11 @@ export class BrowserDataSource {
|
||||
this.ctx.coreData.machineBrowsers = browsers
|
||||
}
|
||||
}).catch((e) => {
|
||||
this.ctx.coreData.machineBrowsers = null
|
||||
this.ctx.coreData.baseError = e
|
||||
this.ctx.update((coreData) => {
|
||||
coreData.machineBrowsers = null
|
||||
coreData.baseError = e
|
||||
})
|
||||
|
||||
throw e
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
RequestPolicy,
|
||||
} from '@urql/core'
|
||||
import _ from 'lodash'
|
||||
import { getError } from '@packages/errors'
|
||||
|
||||
const cloudEnv = getenv('CYPRESS_INTERNAL_CLOUD_ENV', process.env.CYPRESS_INTERNAL_ENV || 'development') as keyof typeof REMOTE_SCHEMA_URLS
|
||||
|
||||
@@ -104,13 +105,11 @@ export class CloudDataSource {
|
||||
}
|
||||
} else if ((!_.isEqual(resolvedData.data, res.data) || !_.isEqual(resolvedData.error, res.error)) && !res.error?.networkError) {
|
||||
if (res.error) {
|
||||
this.ctx.coreData.baseError = {
|
||||
title: res.error.graphQLErrors?.[0]?.originalError?.name,
|
||||
message: res.error.message,
|
||||
stack: res.error.stack,
|
||||
this.ctx.coreData.dashboardGraphQLError = {
|
||||
cypressError: getError('DASHBOARD_GRAPHQL_ERROR', res.error),
|
||||
}
|
||||
} else {
|
||||
this.ctx.coreData.baseError = null
|
||||
this.ctx.coreData.dashboardGraphQLError = null
|
||||
}
|
||||
|
||||
// TODO(tim): send a signal to the frontend so when it refetches it does 'cache-only' request,
|
||||
|
||||
@@ -7,6 +7,10 @@ import type { DataContext } from '../DataContext'
|
||||
export class EnvDataSource {
|
||||
constructor (private ctx: DataContext) {}
|
||||
|
||||
get isProduction () {
|
||||
return process.env.CYPRESS_INTERNAL_ENV === 'production'
|
||||
}
|
||||
|
||||
get HTTP_PROXY () {
|
||||
return process.env.HTTPS_PROXY || process.env.HTTP_PROXY
|
||||
}
|
||||
|
||||
69
packages/data-context/src/sources/ErrorDataSource.ts
Normal file
69
packages/data-context/src/sources/ErrorDataSource.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { ErrorWrapperSource, stackUtils } from '@packages/errors'
|
||||
import path from 'path'
|
||||
import _ from 'lodash'
|
||||
import { codeFrameColumns } from '@babel/code-frame'
|
||||
|
||||
import type { DataContext } from '..'
|
||||
|
||||
export interface CodeFrameShape {
|
||||
line: number
|
||||
column: number
|
||||
absolute: string
|
||||
codeBlock: string
|
||||
}
|
||||
|
||||
export class ErrorDataSource {
|
||||
constructor (private ctx: DataContext) {}
|
||||
|
||||
isUserCodeError (source: ErrorWrapperSource) {
|
||||
return Boolean(source.cypressError.originalError && !source.cypressError.originalError?.isCypressErr)
|
||||
}
|
||||
|
||||
async codeFrame (source: ErrorWrapperSource): Promise<CodeFrameShape | null> {
|
||||
if (!this.ctx.currentProject || !this.isUserCodeError(source)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// If we saw a TSError, we will extract the error location from the message
|
||||
const tsErrorLocation = source.cypressError.originalError?.tsErrorLocation
|
||||
|
||||
let line: number | null | undefined
|
||||
let column: number | null | undefined
|
||||
let absolute: string | null | undefined
|
||||
|
||||
if (tsErrorLocation) {
|
||||
line = tsErrorLocation.line
|
||||
column = tsErrorLocation.column
|
||||
absolute = path.join(this.ctx.currentProject, tsErrorLocation.filePath)
|
||||
} else {
|
||||
// Skip any stack trace lines which come from node:internal code
|
||||
const stackLines = stackUtils.getStackLines(source.cypressError.stack ?? '')
|
||||
const filteredStackLines = stackLines.filter((stackLine) => !stackLine.includes('node:internal'))
|
||||
const parsedLine = stackUtils.parseStackLine(filteredStackLines[0] ?? '')
|
||||
|
||||
if (parsedLine) {
|
||||
absolute = parsedLine.absolute
|
||||
line = parsedLine.line
|
||||
column = parsedLine.column
|
||||
}
|
||||
}
|
||||
|
||||
if (!absolute || !_.isNumber(line) || !_.isNumber(column)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const codeBlock = codeFrameColumns(await this.ctx.file.readFile(absolute), {
|
||||
start: { line, column },
|
||||
}, {
|
||||
linesAbove: 2,
|
||||
linesBelow: 4,
|
||||
})
|
||||
|
||||
return {
|
||||
absolute,
|
||||
line,
|
||||
column,
|
||||
codeBlock,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
export * from './BrowserDataSource'
|
||||
export * from './CloudDataSource'
|
||||
export * from './EnvDataSource'
|
||||
export * from './ErrorDataSource'
|
||||
export * from './FileDataSource'
|
||||
export * from './GitDataSource'
|
||||
export * from './GraphQLDataSource'
|
||||
|
||||
@@ -329,7 +329,7 @@ export async function cleanUpIntegrationFolder (projectRoot: string) {
|
||||
} catch (e: any) {
|
||||
// only throw if the folder exists
|
||||
if (e.code !== 'ENOENT') {
|
||||
throw Error(`Failed to remove ${integrationPath}`)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
export * from './autoRename'
|
||||
export * from './codegen'
|
||||
export * from './format'
|
||||
export * from './parserUtils'
|
||||
export * from './regexps'
|
||||
export * from './shouldShowSteps'
|
||||
|
||||
@@ -21,12 +21,12 @@ export const urqlCacheKeys: Partial<UrqlCacheKeys> = {
|
||||
DevState: (data) => data.__typename,
|
||||
Wizard: (data) => data.__typename,
|
||||
Migration: (data) => data.__typename,
|
||||
Warning: () => null,
|
||||
CloudRunCommitInfo: () => null,
|
||||
GitInfo: () => null,
|
||||
MigrationFile: () => null,
|
||||
MigrationFilePart: () => null,
|
||||
BaseError: () => null,
|
||||
ErrorWrapper: () => null,
|
||||
CodeFrame: () => null,
|
||||
ProjectPreferences: (data) => data.__typename,
|
||||
VersionData: () => null,
|
||||
ScaffoldedFile: () => null,
|
||||
|
||||
41
packages/errors/__snapshot-html__/DASHBOARD_GRAPHQL_ERROR.html
generated
Normal file
41
packages/errors/__snapshot-html__/DASHBOARD_GRAPHQL_ERROR.html
generated
Normal file
@@ -0,0 +1,41 @@
|
||||
<!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">We received an unexpected error response from the request to the Cypress dashboard:<span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#de73ff">"fail whale"<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6"></span></span></span></span></span></span></span></span></span></span>
|
||||
</pre></body></html>
|
||||
43
packages/errors/__snapshot-html__/UNEXPECTED_INTERNAL_ERROR.html
generated
Normal file
43
packages/errors/__snapshot-html__/UNEXPECTED_INTERNAL_ERROR.html
generated
Normal file
@@ -0,0 +1,43 @@
|
||||
<!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">We encountered an unexpected internal error. Please check GitHub or open a new issue <span style="color:#e6e6e6">
|
||||
<span style="color:#e05561">if you don't see one already with the details below:<span style="color:#e6e6e6">
|
||||
<span style="color:#c062de"><span style="color:#e6e6e6">
|
||||
<span style="color:#c062de">Error: fail whale<span style="color:#e6e6e6">
|
||||
<span style="color:#c062de"> at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)<span style="color:#e6e6e6">
|
||||
<span style="color:#c062de"> at UNEXPECTED_INTERNAL_ERROR (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)<span style="color:#e6e6e6"></span></span></span></span></span></span></span></span></span></span></span></span>
|
||||
</pre></body></html>
|
||||
44
packages/errors/__snapshot-html__/UNEXPECTED_MUTATION_ERROR.html
generated
Normal file
44
packages/errors/__snapshot-html__/UNEXPECTED_MUTATION_ERROR.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">An unexpected internal error occurred while executing the <span style="color:#e5e510">wizardUpdate<span style="color:#e05561"> operation with payload:<span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#de73ff">{}<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#c062de"><span style="color:#e6e6e6">
|
||||
<span style="color:#c062de">Error: fail whale<span style="color:#e6e6e6">
|
||||
<span style="color:#c062de"> at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)<span style="color:#e6e6e6">
|
||||
<span style="color:#c062de"> at UNEXPECTED_MUTATION_ERROR (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)<span style="color:#e6e6e6"></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
|
||||
</pre></body></html>
|
||||
@@ -1 +1,12 @@
|
||||
// The errors package is always compiled in a production build, but when we're developing locally,
|
||||
// there'a chance we can run into a situation where we're requriring the
|
||||
// @packages/errors from the child process in a non-ts project, and we need to build this JIT.
|
||||
// Otherwise the error will incorrectly be shown as "cannot find module ./src" instead of
|
||||
// the actual error. Double check that we can require './src', and if not install ts-node
|
||||
try {
|
||||
require.resolve('./src')
|
||||
} catch (e) {
|
||||
require('@packages/ts/register')
|
||||
}
|
||||
|
||||
module.exports = require('./src')
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { AllCypressErrors } from './errors'
|
||||
/**
|
||||
* A config validation result
|
||||
*/
|
||||
export interface ConfigValidationError {
|
||||
export interface ConfigValidationFailureInfo {
|
||||
key: string
|
||||
type: string
|
||||
value: any
|
||||
@@ -25,13 +25,13 @@ export interface ErrorLike {
|
||||
/**
|
||||
* An error originating from the @cypress/errors package,
|
||||
* includes the `type` of the error, the `originalError`
|
||||
* if one exists, and an isCypressError for duck-type checking
|
||||
* if one exists, and an isCypressErr for duck-type checking
|
||||
*/
|
||||
export interface CypressError extends ErrorLike {
|
||||
messageMarkdown: string
|
||||
type: keyof typeof AllCypressErrors
|
||||
isCypressErr: boolean
|
||||
originalError?: CypressError | ErrorLike
|
||||
originalError?: SerializedError
|
||||
details?: string
|
||||
code?: string | number
|
||||
errno?: string | number
|
||||
@@ -56,7 +56,7 @@ export interface ClonedError {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface SerializedError {
|
||||
export interface SerializedError extends Omit<CypressError, 'messageMarkdown' | 'type' | 'isCypressErr'> {
|
||||
code?: string | number
|
||||
type?: string | number
|
||||
errorType?: string
|
||||
@@ -65,4 +65,19 @@ export interface SerializedError {
|
||||
message: string
|
||||
name: string
|
||||
isCypressErr?: boolean
|
||||
// If there's a parse error from TSNode, we strip out the first error separately from
|
||||
// the message body and provide here, since this is is the error we actually want to fix
|
||||
tsErrorLocation?: {
|
||||
line: number
|
||||
column: number
|
||||
filePath: string
|
||||
} | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in the GraphQL Error / Warning objects
|
||||
*/
|
||||
export interface ErrorWrapperSource {
|
||||
title?: string | null
|
||||
cypressError: CypressError
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { humanTime, logError, parseResolvedPattern, pluralize } from './errorUti
|
||||
import { errPartial, errTemplate, fmt, theme, PartialErr } from './errTemplate'
|
||||
import { stackWithoutMessage } from './stackUtils'
|
||||
|
||||
import type { ClonedError, ConfigValidationError, CypressError, ErrTemplateResult, ErrorLike } from './errorTypes'
|
||||
import type { ClonedError, ConfigValidationFailureInfo, CypressError, ErrTemplateResult, ErrorLike } from './errorTypes'
|
||||
|
||||
const ansi_up = new AU()
|
||||
|
||||
@@ -695,7 +695,7 @@ export const AllCypressErrors = {
|
||||
${fmt.highlight(validationMsg)}`
|
||||
},
|
||||
// TODO: make this relative path, not absolute
|
||||
CONFIG_VALIDATION_ERROR: (fileType: 'configFile' | 'pluginsFile' | null, filePath: string | null, validationResult: ConfigValidationError) => {
|
||||
CONFIG_VALIDATION_ERROR: (fileType: 'configFile' | 'pluginsFile' | null, filePath: string | null, validationResult: ConfigValidationFailureInfo) => {
|
||||
const { key, type, value, list } = validationResult
|
||||
|
||||
if (!fileType) {
|
||||
@@ -1282,6 +1282,33 @@ export const AllCypressErrors = {
|
||||
`
|
||||
},
|
||||
|
||||
UNEXPECTED_MUTATION_ERROR: (mutationField: string, args: any, err: Error) => {
|
||||
return errTemplate`
|
||||
An unexpected internal error occurred while executing the ${fmt.highlight(mutationField)} operation with payload:
|
||||
|
||||
${fmt.stringify(args)}
|
||||
|
||||
${fmt.stackTrace(err)}
|
||||
`
|
||||
},
|
||||
|
||||
DASHBOARD_GRAPHQL_ERROR: (err: Error) => {
|
||||
return errTemplate`
|
||||
We received an unexpected error response from the request to the Cypress dashboard:
|
||||
|
||||
${fmt.stringify(err.message)}
|
||||
`
|
||||
},
|
||||
|
||||
UNEXPECTED_INTERNAL_ERROR: (err: Error) => {
|
||||
return errTemplate`
|
||||
We encountered an unexpected internal error. Please check GitHub or open a new issue
|
||||
if you don't see one already with the details below:
|
||||
|
||||
${fmt.stackTrace(err)}
|
||||
`
|
||||
},
|
||||
|
||||
} as const
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
@@ -31,6 +31,20 @@ export const getStackLines = (stack: string) => {
|
||||
return stackLines
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures & returns the absolute path, line, and column from a stack trace line
|
||||
*/
|
||||
export const parseStackLine = (line: string): null | { absolute: string, line: number, column: number } => {
|
||||
const stackLineCapture = /^\s*(?:at )?.*@?\((.*?)\:(\d+)\:(\d+)\)?$/
|
||||
const result = stackLineCapture.exec(line)
|
||||
|
||||
if (!result?.[1]) {
|
||||
return null
|
||||
}
|
||||
|
||||
return { absolute: result[1], line: Number(result[2]), column: Number(result[3]) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the stack and returns only the lines that contain stack-frame like entries,
|
||||
* matching the `stackLineRegex` above
|
||||
|
||||
@@ -1081,5 +1081,20 @@ describe('visual error templates', () => {
|
||||
default: ['/path/to/config.ts', {}],
|
||||
}
|
||||
},
|
||||
UNEXPECTED_INTERNAL_ERROR: () => {
|
||||
return {
|
||||
default: [makeErr()],
|
||||
}
|
||||
},
|
||||
UNEXPECTED_MUTATION_ERROR: () => {
|
||||
return {
|
||||
default: ['wizardUpdate', {}, makeErr()],
|
||||
}
|
||||
},
|
||||
DASHBOARD_GRAPHQL_ERROR: () => {
|
||||
return {
|
||||
default: [makeErr()],
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -143,7 +143,7 @@ async function makeE2ETasks () {
|
||||
await globalPubSub.emitThen('test:cleanup')
|
||||
await ctx.actions.app.removeAppDataDir()
|
||||
await ctx.actions.app.ensureAppDataDirExists()
|
||||
await ctx.resetForTest()
|
||||
await ctx.reinitializeCypress()
|
||||
sinon.reset()
|
||||
sinon.restore()
|
||||
remoteGraphQLIntercept = undefined
|
||||
@@ -239,7 +239,7 @@ async function makeE2ETasks () {
|
||||
const modeOptions = argUtils.toObject(processedArgv)
|
||||
|
||||
// Reset the state of the context
|
||||
await ctx.resetForTest(modeOptions)
|
||||
await ctx.reinitializeCypress(modeOptions)
|
||||
|
||||
// Handle any pre-loading that should occur based on the launch arg settings
|
||||
await ctx.initializeMode()
|
||||
@@ -263,7 +263,7 @@ async function makeE2ETasks () {
|
||||
const modeOptions = argUtils.toObject(processedArgv)
|
||||
|
||||
// Reset the state of the context
|
||||
await ctx.resetForTest(modeOptions)
|
||||
await ctx.reinitializeCypress(modeOptions)
|
||||
|
||||
// Handle any pre-loading that should occur based on the launch arg settings
|
||||
await ctx.initializeMode()
|
||||
|
||||
@@ -7,11 +7,13 @@ export const e2eProjectDirs = [
|
||||
'component-tests',
|
||||
'config-with-custom-file-js',
|
||||
'config-with-custom-file-ts',
|
||||
'config-with-import-error',
|
||||
'config-with-invalid-browser',
|
||||
'config-with-invalid-viewport',
|
||||
'config-with-js',
|
||||
'config-with-short-timeout',
|
||||
'config-with-ts',
|
||||
'config-with-ts-syntax-error',
|
||||
'cookies',
|
||||
'create-react-app-configured',
|
||||
'create-react-app-unconfigured',
|
||||
|
||||
@@ -345,7 +345,7 @@ function withCtx<T extends Partial<WithCtxOptions>, R> (fn: (ctx: DataContext, o
|
||||
const { log, timeout, ...rest } = opts
|
||||
|
||||
const _log = log === false ? { end () {}, set (key: string, val: any) {} } : Cypress.log({
|
||||
name: opts.retry ? 'withCtx' : 'withRetryableCtx',
|
||||
name: opts.retry ? 'withRetryableCtx' : 'withCtx',
|
||||
message: '(view in console)',
|
||||
consoleProps () {
|
||||
return {
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import { defaultMessages } from '@cy/i18n'
|
||||
import { registerMountFn, addVueCommand } from './common'
|
||||
import '../../src/styles/shared.scss'
|
||||
import 'virtual:windi.css'
|
||||
import 'cypress-real-events/support'
|
||||
import { installCustomPercyCommand } from '@packages/ui-components/cypress/support/customPercyCommand'
|
||||
import { addNetworkCommands } from './onlineNetwork'
|
||||
import { GQLStubRegistry } from './mock-graphql/stubgql-Registry'
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
gqlStub: typeof GQLStubRegistry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cy.i18n = defaultMessages
|
||||
cy.gqlStub = GQLStubRegistry
|
||||
|
||||
Cypress.on('uncaught:exception', (err) => !err.message.includes('ResizeObserver loop limit exceeded'))
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ import { executeExchange } from '@urql/exchange-execute'
|
||||
import { makeCacheExchange } from '@packages/frontend-shared/src/graphql/urqlClient'
|
||||
import { clientTestSchema } from './clientTestSchema'
|
||||
import type { ClientTestContext } from './clientTestContext'
|
||||
import type { FieldNode, GraphQLFieldResolver, GraphQLResolveInfo, GraphQLTypeResolver } from 'graphql'
|
||||
import type { GraphQLFieldResolver, GraphQLResolveInfo, GraphQLTypeResolver, FieldNode } from 'graphql'
|
||||
import { defaultTypeResolver, introspectionFromSchema, isNonNullType } from 'graphql'
|
||||
import type { CodegenTypeMap } from '../generated/test-graphql-types.gen'
|
||||
import { GQLStubRegistry } from './stubgql-Registry'
|
||||
import { pathToArray } from 'graphql/jsutils/Path'
|
||||
import dedent from 'dedent'
|
||||
import { GQLStubRegistry } from './stubgql-Registry'
|
||||
|
||||
export function testUrqlClient (context: ClientTestContext, onResult?: (result: any, context: ClientTestContext) => any): Client {
|
||||
return createClient({
|
||||
|
||||
@@ -143,8 +143,16 @@ type ResultType<T> = T extends TypedDocumentNode<infer U, any> ? U : never
|
||||
|
||||
type MountFragmentConfig<T extends TypedDocumentNode> = {
|
||||
variables?: T['__variablesType']
|
||||
render: (frag: Exclude<T['__resultType'], undefined>) => JSX.Element
|
||||
/**
|
||||
* When we are mounting a GraphQL Fragment, we can use `onResult`
|
||||
* to intercept the result and modify the contents on the fragment
|
||||
* before rendering the component
|
||||
*/
|
||||
onResult?: (result: ResultType<T>, ctx: ClientTestContext) => ResultType<T> | void
|
||||
/**
|
||||
* Render is passed the result of the "frag" and mounts the component under test
|
||||
*/
|
||||
render: (frag: Exclude<T['__resultType'], undefined>) => JSX.Element
|
||||
expectError?: boolean
|
||||
} & CyMountOptions<unknown>
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import dedent from 'dedent'
|
||||
import type { ErrorWrapper } from '../generated/test-graphql-types.gen'
|
||||
import type { MaybeResolver } from './clientTestUtils'
|
||||
|
||||
export const StubErrorWrapper = {
|
||||
__typename: 'ErrorWrapper',
|
||||
title: 'Error Loading Config',
|
||||
errorMessage: dedent`
|
||||
Your \`supportFile\` is set to \`foo.bar.js\`, but either the file is missing or it's invalid. The \`supportFile\` must be a \`.js\`, \`.ts\`, \`.coffee\` file or be supported by your preprocessor plugin (if configured).
|
||||
|
||||
Correct your \`foo.bar.js\`, create the appropriate file, or set \`supportFile\` to \`false\` if a support file is not necessary for your project.
|
||||
|
||||
Or you might have renamed the extension of your \`supportFile\` to \`.ts\`. If that's the case, restart the test runner.
|
||||
|
||||
Learn more at https://on.cypress.io/support-file-missing-or-invalid
|
||||
`,
|
||||
isUserCodeError: true,
|
||||
errorType: 'SUPPORT_FILE_NOT_FOUND',
|
||||
errorStack: dedent`
|
||||
OriginalError: foobar
|
||||
at module.exports (/Users/bmann/Dev/cypress-playground/v9.0.0/cypress/plugins/index.js:22:9)
|
||||
at /Users/bmann/Library/Caches/Cypress/9.1.1/Cypress.app/Contents/Resources/app/packages/server/lib/plugins/child/run_plugins.js:90:12
|
||||
at tryCatcher (/Users/bmann/Library/Caches/Cypress/9.1.1/Cypress.app/Contents/Resources/app/packages/server/node_modules/bluebird/js/release/util.js:16:23)
|
||||
at Function.Promise.attempt.Promise.try (/Users/bmann/Library/Caches/Cypress/9.1.1/Cypress.app/Contents/Resources/app/packages/server/node_modules/bluebird/js/release/method.js:39:29)
|
||||
at load (/Users/bmann/Library/Caches/Cypress/9.1.1/Cypress.app/Contents/Resources/app/packages/server/lib/plugins/child/run_plugins.js:87:7)
|
||||
at EventEmitter.<anonymous> (/Users/bmann/Library/Caches/Cypress/9.1.1/Cypress.app/Contents/Resources/app/packages/server/lib/plugins/child/run_plugins.js:198:5)
|
||||
at EventEmitter.emit (events.js:314:20)
|
||||
at process.<anonymous> (/Users/bmann/Library/Caches/Cypress/9.1.1/Cypress.app/Contents/Resources/app/packages/server/lib/plugins/util.js:19:22)
|
||||
at process.emit (events.js:314:20)
|
||||
at emit (internal/child_process.js:877:12)
|
||||
at processTicksAndRejections (internal/process/task_queues.js:85:21)
|
||||
`,
|
||||
errorName: 'OriginalError',
|
||||
fileToOpen: {
|
||||
__typename: 'FileParts',
|
||||
id: `FilePath:pathtoFoobarjs`,
|
||||
absolute: '/path/to/foo.bar.js',
|
||||
relative: './foo.bar.js',
|
||||
fileName: 'foo',
|
||||
contents: 'const x = 1',
|
||||
name: 'foo.bar.js',
|
||||
fileExtension: '.js',
|
||||
baseName: 'foo.bar.js',
|
||||
},
|
||||
} as const
|
||||
|
||||
// For type checking
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const _typeCheck: MaybeResolver<ErrorWrapper> = StubErrorWrapper
|
||||
@@ -53,6 +53,7 @@ export const createTestCurrentProject = (title: string, currentProject: Partial<
|
||||
isDefaultSpecPattern: true,
|
||||
browserStatus: 'closed',
|
||||
packageManager: 'yarn',
|
||||
configFileAbsolutePath: '/path/to/cypress.config.js',
|
||||
...currentProject,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { stubGlobalProject, stubProject } from './stubgql-Project'
|
||||
import { CloudOrganizationStubs, CloudProjectStubs, CloudRecordKeyStubs, CloudRunStubs, CloudUserStubs } from './stubgql-CloudTypes'
|
||||
import { stubMigration } from './stubgql-Migration'
|
||||
import type { CodegenTypeMap } from '../generated/test-graphql-types.gen'
|
||||
import { StubErrorWrapper } from './stubgql-ErrorWrapper'
|
||||
|
||||
type MaybeResolveMap = {[K in keyof CodegenTypeMap]: MaybeResolver<CodegenTypeMap[K]>}
|
||||
|
||||
@@ -22,8 +23,9 @@ export const GQLStubRegistry = {
|
||||
CloudRun: CloudRunStubs.allPassing,
|
||||
CloudRecordKey: CloudRecordKeyStubs.componentProject,
|
||||
CloudUser: CloudUserStubs.me,
|
||||
ErrorWrapper: StubErrorWrapper,
|
||||
} as const
|
||||
|
||||
// Line below added so we can refer to the above as a const value, but ensure it fits the type contract
|
||||
// For Type checking
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const _typeCheck: Partial<MaybeResolveMap> = GQLStubRegistry
|
||||
const _x: Partial<MaybeResolveMap> = GQLStubRegistry
|
||||
|
||||
@@ -79,4 +79,10 @@ describe('<ShikiHighlight/>', { viewportWidth: 800, viewportHeight: 500 }, () =>
|
||||
cy.get('.shiki').should('be.visible')
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('show line numbers with initial line when the prop is passed', () => {
|
||||
cy.mount(() => <div class="p-12"><ShikiHighlight code={code} lang="ts" lineNumbers initialLine={10} /></div>)
|
||||
cy.get('.shiki').should('be.visible')
|
||||
cy.percySnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,7 +46,8 @@ shikiWrapperClasses computed property.
|
||||
'inline': props.inline,
|
||||
'wrap': props.wrap,
|
||||
'line-numbers': props.lineNumbers,
|
||||
'p-8px': !props.lineNumbers && !props.inline,
|
||||
'p-8px': !props.lineNumbers && !props.inline && !props.codeframe,
|
||||
'p-2px': props.codeframe,
|
||||
},
|
||||
|
||||
props.class,
|
||||
@@ -118,12 +119,14 @@ onBeforeMount(async () => {
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
code: string
|
||||
initialLine?: number
|
||||
lang: CyLangType | undefined
|
||||
lineNumbers?: boolean
|
||||
inline?: boolean
|
||||
wrap?: boolean
|
||||
copyOnClick?: boolean
|
||||
copyButton?: boolean
|
||||
codeframe?: boolean
|
||||
skipTrim?: boolean
|
||||
class?: string | string[] | Record<string, any>
|
||||
}>(), {
|
||||
@@ -131,6 +134,8 @@ const props = withDefaults(defineProps<{
|
||||
inline: false,
|
||||
wrap: false,
|
||||
copyOnClick: false,
|
||||
codeframe: false,
|
||||
initialLine: 1,
|
||||
copyButton: false,
|
||||
skipTrim: false,
|
||||
class: undefined,
|
||||
@@ -196,7 +201,7 @@ $offset: 1.1em;
|
||||
@apply py-8px;
|
||||
code {
|
||||
counter-reset: step;
|
||||
counter-increment: step 0;
|
||||
counter-increment: step calc(v-bind('props.initialLine') - 1);
|
||||
|
||||
// Keep bg-gray-50 synced with the box-shadows.
|
||||
.line::before, .line:first-child::before {
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
* We could eventually use Shiki as a Markdown plugin, but I don't want to get into it right now.
|
||||
*/
|
||||
import type { Ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { computed, unref } from 'vue'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import MarkdownItClass from '@toycode/markdown-it-class'
|
||||
import { useEventListener, whenever } from '@vueuse/core'
|
||||
import type { MaybeRef } from '@vueuse/core'
|
||||
import { useExternalLink } from '../gql-components/useExternalLink'
|
||||
import { mapValues, isArray, flatten } from 'lodash'
|
||||
|
||||
@@ -38,9 +39,9 @@ const defaultClasses = {
|
||||
h4: ['font-medium', 'text-1xl', 'mb-3'],
|
||||
h5: ['font-medium', 'text-sm', 'mb-3'],
|
||||
h6: ['font-medium', 'text-xs', 'mb-3'],
|
||||
p: ['my-3 first:mt-0 text-sm'],
|
||||
pre: ['rounded p-3'],
|
||||
code: [`font-medium rounded text-sm px-4px py-2px`],
|
||||
p: ['my-3 first:mt-0 text-sm mb-4'],
|
||||
pre: ['rounded p-3 bg-white mb-2'],
|
||||
code: [`font-medium rounded text-sm px-4px py-2px bg-red-100`],
|
||||
a: ['text-blue-500', 'hover:underline text-sm'],
|
||||
ul: ['list-disc pl-6 my-3 text-sm'],
|
||||
ol: ['list-decimal pl-6 my-3 text-sm'],
|
||||
@@ -81,7 +82,7 @@ const buildClasses = (options) => {
|
||||
return _classes
|
||||
}
|
||||
|
||||
export const useMarkdown = (target: Ref<HTMLElement>, text: string, options: UseMarkdownOptions = {}) => {
|
||||
export const useMarkdown = (target: Ref<HTMLElement>, text: MaybeRef<string>, options: UseMarkdownOptions = {}) => {
|
||||
options.openExternal = options.openExternal || true
|
||||
|
||||
const classes = buildClasses(options)
|
||||
@@ -112,6 +113,6 @@ export const useMarkdown = (target: Ref<HTMLElement>, text: string, options: Use
|
||||
}
|
||||
|
||||
return {
|
||||
markdown: computed(() => md.render(text, { sanitize: true })),
|
||||
markdown: computed(() => md.render(unref(text), { sanitize: true })),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +64,11 @@ import {
|
||||
Auth_LoginDocument,
|
||||
Auth_LogoutDocument,
|
||||
Auth_ResetAuthStateDocument,
|
||||
AuthFragment,
|
||||
Auth_BrowserOpenedDocument,
|
||||
} from '../generated/graphql'
|
||||
import type {
|
||||
AuthFragment,
|
||||
} from '../generated/graphql'
|
||||
import Button from '@cy/components/Button.vue'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
|
||||
|
||||
@@ -1,90 +1,39 @@
|
||||
<template>
|
||||
<template v-if="query?.data?.value">
|
||||
<button
|
||||
data-cy="open-config-file"
|
||||
class="hocus-link-default underline-purple-500"
|
||||
@click="showCypressConfigInIDE()"
|
||||
>
|
||||
<slot>
|
||||
<OpenFileInIDE
|
||||
v-if="props.gql.configFileAbsolutePath"
|
||||
v-slot="{onClick}"
|
||||
:file-path="props.gql.configFileAbsolutePath"
|
||||
>
|
||||
<slot :on-click="onClick">
|
||||
<button
|
||||
data-testid="open-config-file"
|
||||
class="hocus-link-default underline-purple-500"
|
||||
@click="onClick"
|
||||
>
|
||||
<span
|
||||
class="cursor-pointer text-purple-500"
|
||||
class="text-purple-500 cursor-pointer"
|
||||
>
|
||||
{{ configFile }}
|
||||
{{ props.gql.configFile ?? 'cypress.config.js' }}
|
||||
</span>
|
||||
</slot>
|
||||
</button>
|
||||
|
||||
<ChooseExternalEditorModal
|
||||
v-if="isChooseEditorOpen"
|
||||
:open="isChooseEditorOpen"
|
||||
:gql="query.data?.value"
|
||||
@close="isChooseEditorOpen = false"
|
||||
@selected="openFile"
|
||||
/>
|
||||
</template>
|
||||
<div v-else />
|
||||
</button>
|
||||
</slot>
|
||||
</OpenFileInIDE>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { gql, useMutation, useQuery } from '@urql/vue'
|
||||
import { OpenConfigFileDocument, OpenConfigFileInIdeDocument } from '../generated/graphql'
|
||||
import ChooseExternalEditorModal from './ChooseExternalEditorModal.vue'
|
||||
import { gql } from '@urql/core'
|
||||
import OpenFileInIDE from './OpenFileInIDE.vue'
|
||||
import type { OpenConfigFileInIdeFragment } from '../generated/graphql'
|
||||
|
||||
gql`
|
||||
query OpenConfigFileInIDE {
|
||||
currentProject {
|
||||
id
|
||||
configFile
|
||||
configFileAbsolutePath
|
||||
}
|
||||
localSettings {
|
||||
preferences {
|
||||
preferredEditorBinary
|
||||
}
|
||||
}
|
||||
...ChooseExternalEditorModal
|
||||
fragment OpenConfigFileInIDE on CurrentProject {
|
||||
id
|
||||
configFile
|
||||
configFileAbsolutePath
|
||||
}
|
||||
`
|
||||
|
||||
gql`
|
||||
mutation OpenConfigFile ($input: FileDetailsInput!) {
|
||||
openFileInIDE (input: $input)
|
||||
}
|
||||
`
|
||||
|
||||
const query = useQuery({ query: OpenConfigFileInIdeDocument, requestPolicy: 'network-only' })
|
||||
|
||||
const configFile = computed(() => query.data?.value?.currentProject?.configFile ?? 'cypress.config.js')
|
||||
|
||||
const OpenConfigFileInIDE = useMutation(OpenConfigFileDocument)
|
||||
|
||||
const openConfigFileInIDE = (absolute: string) => {
|
||||
OpenConfigFileInIDE.executeMutation({
|
||||
input: {
|
||||
absolute,
|
||||
line: 1,
|
||||
column: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const openFile = () => {
|
||||
isChooseEditorOpen.value = false
|
||||
|
||||
if (query.data?.value?.currentProject?.configFileAbsolutePath) {
|
||||
openConfigFileInIDE(query.data?.value?.currentProject.configFileAbsolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
const showCypressConfigInIDE = () => {
|
||||
if (query.data?.value?.localSettings.preferences.preferredEditorBinary && query.data?.value?.currentProject?.configFileAbsolutePath) {
|
||||
openConfigFileInIDE(query.data?.value?.currentProject.configFileAbsolutePath)
|
||||
} else {
|
||||
isChooseEditorOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const isChooseEditorOpen = ref(false)
|
||||
|
||||
const props = defineProps<{
|
||||
gql: OpenConfigFileInIdeFragment
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<slot :on-click="maybeShowFileInIDE" />
|
||||
<ChooseExternalEditorModal
|
||||
v-if="isChooseEditorOpen && query.data.value"
|
||||
:open="isChooseEditorOpen"
|
||||
:gql="query.data.value"
|
||||
@close="isChooseEditorOpen = false"
|
||||
@selected="openFile"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { gql, useMutation, useQuery } from '@urql/vue'
|
||||
import { OpenFileInIdeDocument, OpenFileInIde_MutationDocument } from '../generated/graphql'
|
||||
import ChooseExternalEditorModal from './ChooseExternalEditorModal.vue'
|
||||
|
||||
gql`
|
||||
query OpenFileInIDE {
|
||||
localSettings {
|
||||
preferences {
|
||||
preferredEditorBinary
|
||||
}
|
||||
}
|
||||
...ChooseExternalEditorModal
|
||||
}
|
||||
`
|
||||
|
||||
gql`
|
||||
mutation OpenFileInIDE_Mutation ($input: FileDetailsInput!) {
|
||||
openFileInIDE (input: $input)
|
||||
}
|
||||
`
|
||||
|
||||
const props = defineProps<{
|
||||
line?: number
|
||||
column?: number
|
||||
filePath: string
|
||||
}>()
|
||||
|
||||
const query = useQuery({ query: OpenFileInIdeDocument, requestPolicy: 'network-only' })
|
||||
|
||||
const OpenFileInIDE = useMutation(OpenFileInIde_MutationDocument)
|
||||
|
||||
const openFileInIDE = () => {
|
||||
OpenFileInIDE.executeMutation({
|
||||
input: {
|
||||
filePath: props.filePath,
|
||||
line: props.line ?? 1,
|
||||
column: props.column ?? 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const openFile = () => {
|
||||
isChooseEditorOpen.value = false
|
||||
|
||||
openFileInIDE()
|
||||
}
|
||||
|
||||
const maybeShowFileInIDE = () => {
|
||||
// If we haven't fetched the data yet checking for the local binary,
|
||||
// wait until we have it before possibly prompting
|
||||
if (query.fetching.value) {
|
||||
query.then(() => {
|
||||
maybeShowFileInIDE()
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (query.data?.value?.localSettings.preferences.preferredEditorBinary) {
|
||||
openFileInIDE()
|
||||
} else {
|
||||
isChooseEditorOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const isChooseEditorOpen = ref(false)
|
||||
|
||||
</script>
|
||||
@@ -54,7 +54,8 @@
|
||||
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import { gql, useMutation } from '@urql/vue'
|
||||
import { TestingTypeSelectionAndReconfigureDocument, TestingTypeEnum } from '../generated/graphql'
|
||||
import { TestingTypeSelectionAndReconfigureDocument } from '../generated/graphql'
|
||||
import type { TestingTypeEnum } from '../generated/graphql'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -196,9 +196,11 @@ import TopNavList from './TopNavList.vue'
|
||||
import PromptContent from './PromptContent.vue'
|
||||
import { allBrowsersIcons } from '@packages/frontend-shared/src/assets/browserLogos'
|
||||
import { gql, useMutation } from '@urql/vue'
|
||||
import { TopNavFragment, TopNav_SetPromptShownDocument } from '../../generated/graphql'
|
||||
import { TopNav_SetPromptShownDocument } from '../../generated/graphql'
|
||||
import type { TopNavFragment } from '../../generated/graphql'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import { computed, ref, ComponentPublicInstance, watch, watchEffect } from 'vue'
|
||||
import { computed, ref, watch, watchEffect } from 'vue'
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import { onClickOutside, onKeyStroke, useTimeAgo } from '@vueuse/core'
|
||||
import type { DocsMenuVariant } from './DocsMenuContent.vue'
|
||||
import DocsMenuContent from './DocsMenuContent.vue'
|
||||
|
||||
@@ -74,7 +74,8 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import { VerticalBrowserListItemsFragment, VerticalBrowserListItems_SetBrowserDocument } from '../../generated/graphql'
|
||||
import { VerticalBrowserListItems_SetBrowserDocument } from '../../generated/graphql'
|
||||
import type { VerticalBrowserListItemsFragment } from '../../generated/graphql'
|
||||
import { computed } from 'vue'
|
||||
import { gql, useMutation } from '@urql/vue'
|
||||
import { allBrowsersIcons } from '@packages/frontend-shared/src/assets/browserLogos'
|
||||
|
||||
@@ -20,13 +20,6 @@ enum AuthStateNameEnum {
|
||||
AUTH_ERROR_DURING_LOGIN
|
||||
}
|
||||
|
||||
"""Base error"""
|
||||
type BaseError {
|
||||
message: String
|
||||
stack: String
|
||||
title: String
|
||||
}
|
||||
|
||||
"""Container representing a browser"""
|
||||
type Browser implements Node {
|
||||
channel: String!
|
||||
@@ -324,6 +317,21 @@ type CloudUser implements Node {
|
||||
userIsViewer: Boolean!
|
||||
}
|
||||
|
||||
"""
|
||||
A code frame to display for a file, used when displaying code related to errors
|
||||
"""
|
||||
type CodeFrame {
|
||||
"""Source of the code frame to display"""
|
||||
codeBlock: String
|
||||
|
||||
"""The column of the error to display"""
|
||||
column: Int
|
||||
file: FileParts!
|
||||
|
||||
"""The line number of the code snippet to display"""
|
||||
line: Int
|
||||
}
|
||||
|
||||
"""Glob patterns for detecting files for code gen."""
|
||||
type CodeGenGlobs implements Node {
|
||||
component: String!
|
||||
@@ -380,14 +388,6 @@ type CurrentProject implements Node & ProjectLike {
|
||||
"""The mode the interactive runner was launched in"""
|
||||
currentTestingType: TestingTypeEnum
|
||||
|
||||
"""If there is an error loading the config file, it is represented here"""
|
||||
errorLoadingConfigFile: BaseError
|
||||
|
||||
"""
|
||||
If there is an error related to the node events, it is represented here
|
||||
"""
|
||||
errorLoadingNodeEvents: BaseError
|
||||
|
||||
"""Whether the project has Typescript"""
|
||||
hasTypescript: Boolean
|
||||
|
||||
@@ -470,9 +470,158 @@ type Editor {
|
||||
name: String!
|
||||
}
|
||||
|
||||
enum ErrorTypeEnum {
|
||||
AUTOMATION_SERVER_DISCONNECTED
|
||||
BAD_POLICY_WARNING
|
||||
BAD_POLICY_WARNING_TOOLTIP
|
||||
BROWSER_NOT_FOUND_BY_NAME
|
||||
BROWSER_NOT_FOUND_BY_PATH
|
||||
BUNDLE_ERROR
|
||||
CANNOT_CONNECT_BASE_URL
|
||||
CANNOT_CONNECT_BASE_URL_RETRYING
|
||||
CANNOT_CONNECT_BASE_URL_WARNING
|
||||
CANNOT_CREATE_PROJECT_TOKEN
|
||||
CANNOT_FETCH_PROJECT_TOKEN
|
||||
CANNOT_RECORD_NO_PROJECT_ID
|
||||
CANNOT_REMOVE_OLD_BROWSER_PROFILES
|
||||
CANNOT_TRASH_ASSETS
|
||||
CDP_COULD_NOT_CONNECT
|
||||
CDP_COULD_NOT_RECONNECT
|
||||
CDP_RETRYING_CONNECTION
|
||||
CDP_VERSION_TOO_OLD
|
||||
CHROME_WEB_SECURITY_NOT_SUPPORTED
|
||||
CONFIG_FILES_LANGUAGE_CONFLICT
|
||||
CONFIG_FILE_DEV_SERVER_IS_NOT_A_FUNCTION
|
||||
CONFIG_FILE_INVALID_DEV_START_EVENT
|
||||
CONFIG_FILE_INVALID_ROOT_CONFIG
|
||||
CONFIG_FILE_INVALID_ROOT_CONFIG_E2E
|
||||
CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_COMPONENT
|
||||
CONFIG_FILE_MIGRATION_NEEDED
|
||||
CONFIG_FILE_NOT_FOUND
|
||||
CONFIG_FILE_REQUIRE_ERROR
|
||||
CONFIG_FILE_SETUP_NODE_EVENTS_ERROR
|
||||
CONFIG_FILE_UNEXPECTED_ERROR
|
||||
CONFIG_VALIDATION_ERROR
|
||||
CONFIG_VALIDATION_MSG_ERROR
|
||||
COULD_NOT_FIND_SYSTEM_NODE
|
||||
COULD_NOT_PARSE_ARGUMENTS
|
||||
DASHBOARD_ALREADY_COMPLETE
|
||||
DASHBOARD_API_RESPONSE_FAILED_RETRYING
|
||||
DASHBOARD_CANCEL_SKIPPED_SPEC
|
||||
DASHBOARD_CANNOT_CREATE_RUN_OR_INSTANCE
|
||||
DASHBOARD_CANNOT_PROCEED_IN_PARALLEL
|
||||
DASHBOARD_CANNOT_PROCEED_IN_SERIAL
|
||||
DASHBOARD_CANNOT_UPLOAD_RESULTS
|
||||
DASHBOARD_GRAPHQL_ERROR
|
||||
DASHBOARD_INVALID_RUN_REQUEST
|
||||
DASHBOARD_PARALLEL_DISALLOWED
|
||||
DASHBOARD_PARALLEL_GROUP_PARAMS_MISMATCH
|
||||
DASHBOARD_PARALLEL_REQUIRED
|
||||
DASHBOARD_PROJECT_NOT_FOUND
|
||||
DASHBOARD_RECORD_KEY_NOT_VALID
|
||||
DASHBOARD_RUN_GROUP_NAME_NOT_UNIQUE
|
||||
DASHBOARD_STALE_RUN
|
||||
DASHBOARD_UNKNOWN_CREATE_RUN_WARNING
|
||||
DASHBOARD_UNKNOWN_INVALID_REQUEST
|
||||
DEPRECATED_BEFORE_BROWSER_LAUNCH_ARGS
|
||||
DUPLICATE_TASK_KEY
|
||||
ERROR_READING_FILE
|
||||
ERROR_WRITING_FILE
|
||||
EXPERIMENTAL_COMPONENT_TESTING_REMOVED
|
||||
EXPERIMENTAL_NETWORK_STUBBING_REMOVED
|
||||
EXPERIMENTAL_RUN_EVENTS_REMOVED
|
||||
EXPERIMENTAL_SAMESITE_REMOVED
|
||||
EXPERIMENTAL_SHADOW_DOM_REMOVED
|
||||
EXTENSION_NOT_LOADED
|
||||
FIREFOX_COULD_NOT_CONNECT
|
||||
FIREFOX_GC_INTERVAL_REMOVED
|
||||
FIREFOX_MARIONETTE_FAILURE
|
||||
FIXTURE_NOT_FOUND
|
||||
FOLDER_NOT_WRITABLE
|
||||
FREE_PLAN_EXCEEDS_MONTHLY_PRIVATE_TESTS
|
||||
FREE_PLAN_EXCEEDS_MONTHLY_TESTS
|
||||
FREE_PLAN_IN_GRACE_PERIOD_EXCEEDS_MONTHLY_PRIVATE_TESTS
|
||||
FREE_PLAN_IN_GRACE_PERIOD_EXCEEDS_MONTHLY_TESTS
|
||||
FREE_PLAN_IN_GRACE_PERIOD_PARALLEL_FEATURE
|
||||
INCOMPATIBLE_PLUGIN_RETRIES
|
||||
INCORRECT_CI_BUILD_ID_USAGE
|
||||
INDETERMINATE_CI_BUILD_ID
|
||||
INVALID_CONFIG_OPTION
|
||||
INVALID_CYPRESS_INTERNAL_ENV
|
||||
INVALID_REPORTER_NAME
|
||||
INVOKED_BINARY_OUTSIDE_NPM_MODULE
|
||||
LEGACY_CONFIG_FILE
|
||||
MULTIPLE_SUPPORT_FILES_FOUND
|
||||
NODE_VERSION_DEPRECATION_BUNDLED
|
||||
NODE_VERSION_DEPRECATION_SYSTEM
|
||||
NOT_LOGGED_IN
|
||||
NO_DEFAULT_CONFIG_FILE_FOUND
|
||||
NO_PROJECT_FOUND_AT_PROJECT_ROOT
|
||||
NO_PROJECT_ID
|
||||
NO_SPECS_FOUND
|
||||
PAID_PLAN_EXCEEDS_MONTHLY_PRIVATE_TESTS
|
||||
PARALLEL_FEATURE_NOT_AVAILABLE_IN_PLAN
|
||||
PLAN_EXCEEDS_MONTHLY_TESTS
|
||||
PLAN_IN_GRACE_PERIOD_RUN_GROUPING_FEATURE_USED
|
||||
PLUGINS_INVALID_EVENT_NAME_ERROR
|
||||
PLUGINS_RUN_EVENT_ERROR
|
||||
PORT_IN_USE_LONG
|
||||
PORT_IN_USE_SHORT
|
||||
PROJECT_ID_AND_KEY_BUT_MISSING_RECORD_OPTION
|
||||
RECORDING_FROM_FORK_PR
|
||||
RECORD_KEY_MISSING
|
||||
RECORD_PARAMS_WITHOUT_RECORDING
|
||||
RENAMED_CONFIG_OPTION
|
||||
RENDERER_CRASHED
|
||||
RUN_GROUPING_FEATURE_NOT_AVAILABLE_IN_PLAN
|
||||
SETUP_NODE_EVENTS_DO_NOT_SUPPORT_DEV_SERVER
|
||||
SETUP_NODE_EVENTS_IS_NOT_FUNCTION
|
||||
SUPPORT_FILE_NOT_FOUND
|
||||
TESTS_DID_NOT_START_FAILED
|
||||
TESTS_DID_NOT_START_RETRYING
|
||||
UNEXPECTED_BEFORE_BROWSER_LAUNCH_PROPERTIES
|
||||
UNEXPECTED_INTERNAL_ERROR
|
||||
UNEXPECTED_MUTATION_ERROR
|
||||
UNSUPPORTED_BROWSER_VERSION
|
||||
VIDEO_POST_PROCESSING_FAILED
|
||||
VIDEO_RECORDING_FAILED
|
||||
}
|
||||
|
||||
"""Base error"""
|
||||
type ErrorWrapper {
|
||||
"""The code frame to display in relation to the error"""
|
||||
codeFrame: CodeFrame
|
||||
|
||||
"""The markdown formatted content associated with the ErrorTypeEnum"""
|
||||
errorMessage: String!
|
||||
|
||||
"""Name of the error class"""
|
||||
errorName: String!
|
||||
|
||||
"""
|
||||
The error stack of either the original error from the user or from where the internal Cypress error was created
|
||||
"""
|
||||
errorStack: String!
|
||||
errorType: ErrorTypeEnum!
|
||||
|
||||
"""
|
||||
Whether the error came from user code, can be used to determine whether to open a stack trace by default
|
||||
"""
|
||||
isUserCodeError: Boolean!
|
||||
|
||||
"""
|
||||
Optional title of the error. Used to optionally display a title above the error
|
||||
"""
|
||||
title: String
|
||||
}
|
||||
|
||||
input FileDetailsInput {
|
||||
absolute: String!
|
||||
column: Int
|
||||
|
||||
"""
|
||||
When we open a file we take a filePath, either relative to the project root, or absolute on disk
|
||||
"""
|
||||
filePath: String!
|
||||
line: Int
|
||||
}
|
||||
|
||||
@@ -486,6 +635,11 @@ type FileParts implements Node {
|
||||
"""Full name of the file (e.g. MySpec.test.tsx)"""
|
||||
baseName: String!
|
||||
|
||||
"""
|
||||
If provided, used to specify the column of the file to open in openFileInIDE
|
||||
"""
|
||||
column: Int
|
||||
|
||||
"""The contents of the file"""
|
||||
contents: String!
|
||||
|
||||
@@ -498,6 +652,11 @@ type FileParts implements Node {
|
||||
"""Relay style Node ID field for the FileParts field"""
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
If provided, used to specify the line of the file to open in openFileInIDE
|
||||
"""
|
||||
line: Int
|
||||
|
||||
"""Full name of spec file (e.g. MySpec.test.tsx)"""
|
||||
name: String!
|
||||
|
||||
@@ -750,6 +909,9 @@ type Mutation {
|
||||
"""
|
||||
devRelaunch(action: DevRelaunchAction!): Boolean
|
||||
|
||||
"""Dismisses a warning displayed by the frontend"""
|
||||
dismissWarning: Query
|
||||
|
||||
"""user has finished migration component specs - move to next step"""
|
||||
finishedRenamingComponentSpecs: Query
|
||||
|
||||
@@ -827,6 +989,9 @@ type Mutation {
|
||||
"""show the launchpad windows"""
|
||||
reconfigureProject: Boolean!
|
||||
|
||||
"""Re-initializes Cypress from the initial CLI options"""
|
||||
reinitializeCypress: Query
|
||||
|
||||
"""Remove project from projects array and cache"""
|
||||
removeProject(path: String!): Query
|
||||
|
||||
@@ -938,7 +1103,7 @@ type ProjectPreferences {
|
||||
type Query {
|
||||
"""The latest state of the auth process"""
|
||||
authState: AuthState!
|
||||
baseError: BaseError
|
||||
baseError: ErrorWrapper
|
||||
|
||||
"""Returns an object conforming to the Relay spec"""
|
||||
cloudNode(
|
||||
@@ -986,7 +1151,7 @@ type Query {
|
||||
versions: VersionData
|
||||
|
||||
"""A list of warnings"""
|
||||
warnings: [Warning!]!
|
||||
warnings: [ErrorWrapper!]!
|
||||
|
||||
"""Metadata about the wizard"""
|
||||
wizard: Wizard!
|
||||
@@ -1103,13 +1268,6 @@ type VersionData {
|
||||
latest: Version!
|
||||
}
|
||||
|
||||
"""A generic warning"""
|
||||
type Warning {
|
||||
details: String
|
||||
message: String!
|
||||
title: String!
|
||||
}
|
||||
|
||||
"""
|
||||
The Wizard is a container for any state associated with initial onboarding to Cypress
|
||||
"""
|
||||
|
||||
@@ -3,5 +3,3 @@ export { graphqlSchema } from './schema'
|
||||
export { execute, parse, print } from 'graphql'
|
||||
|
||||
export { remoteSchemaWrapped } from './stitching/remoteSchemaWrapped'
|
||||
|
||||
//
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
// created by autobarrel, do not modify directly
|
||||
|
||||
export * from './nexusDebugFieldPlugin'
|
||||
export * from './nexusMutationErrorPlugin'
|
||||
export * from './nexusNodePlugin'
|
||||
export * from './nexusSlowGuardPlugin'
|
||||
|
||||
35
packages/graphql/src/plugins/nexusMutationErrorPlugin.ts
Normal file
35
packages/graphql/src/plugins/nexusMutationErrorPlugin.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { plugin } from 'nexus'
|
||||
import { getError } from '@packages/errors'
|
||||
import type { DataContext } from '@packages/data-context'
|
||||
import { getNamedType, isObjectType } from 'graphql'
|
||||
|
||||
export const mutationErrorPlugin = plugin({
|
||||
name: 'MutationErrorPlugin',
|
||||
description: 'Wraps any mutation fields and handles any uncaught errors',
|
||||
onCreateFieldResolver: (def) => {
|
||||
if (def.parentTypeConfig.name !== 'Mutation') {
|
||||
return
|
||||
}
|
||||
|
||||
return (source, args, ctx: DataContext, info, next) => {
|
||||
return plugin.completeValue(next(source, args, ctx, info), (v) => v, (err) => {
|
||||
ctx.update((d) => {
|
||||
d.baseError = {
|
||||
cypressError: err.isCypressErr
|
||||
? err
|
||||
: getError('UNEXPECTED_MUTATION_ERROR', def.fieldConfig.name, args, err),
|
||||
}
|
||||
})
|
||||
|
||||
const returnType = getNamedType(info.returnType)
|
||||
|
||||
// If we're returning a query, we're getting the "baseError" here anyway
|
||||
if (isObjectType(returnType) && returnType.name === 'Query') {
|
||||
return {}
|
||||
}
|
||||
|
||||
throw err
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import { makeSchema, connectionPlugin } from 'nexus'
|
||||
import * as schemaTypes from './schemaTypes/'
|
||||
import { nodePlugin } from './plugins/nexusNodePlugin'
|
||||
import { remoteSchemaWrapped } from './stitching/remoteSchemaWrapped'
|
||||
import { nexusDebugLogPlugin, nexusSlowGuardPlugin } from './plugins'
|
||||
import { mutationErrorPlugin, nexusDebugLogPlugin, nexusSlowGuardPlugin } from './plugins'
|
||||
|
||||
const isCodegen = Boolean(process.env.CYPRESS_INTERNAL_NEXUS_CODEGEN)
|
||||
|
||||
@@ -32,6 +32,7 @@ export const graphqlSchema = makeSchema({
|
||||
plugins: [
|
||||
nexusSlowGuardPlugin,
|
||||
nexusDebugLogPlugin,
|
||||
mutationErrorPlugin,
|
||||
connectionPlugin({
|
||||
nonNullDefaults: {
|
||||
output: true,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { enumType } from 'nexus'
|
||||
import { AllCypressErrors } from '@packages/errors'
|
||||
|
||||
export const ErrorTypeEnum = enumType({
|
||||
name: 'ErrorTypeEnum',
|
||||
members: Object.keys(AllCypressErrors),
|
||||
})
|
||||
@@ -4,6 +4,7 @@
|
||||
export * from './gql-BrowserFamilyEnum'
|
||||
export * from './gql-BrowserStatus'
|
||||
export * from './gql-CodeGenTypeEnum'
|
||||
export * from './gql-ErrorTypeEnum'
|
||||
export * from './gql-ProjectEnums'
|
||||
export * from './gql-SpecEnum'
|
||||
export * from './gql-WizardEnums'
|
||||
|
||||
@@ -3,7 +3,10 @@ import { inputObjectType } from 'nexus'
|
||||
export const FileDetailsInput = inputObjectType({
|
||||
name: 'FileDetailsInput',
|
||||
definition (t) {
|
||||
t.nonNull.string('absolute')
|
||||
t.nonNull.string('filePath', {
|
||||
description: 'When we open a file we take a filePath, either relative to the project root, or absolute on disk',
|
||||
})
|
||||
|
||||
t.int('column')
|
||||
t.int('line')
|
||||
},
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { objectType } from 'nexus'
|
||||
|
||||
export interface BaseErrorSource {
|
||||
title?: string
|
||||
message: string
|
||||
stack?: string
|
||||
}
|
||||
|
||||
export const BaseError = objectType({
|
||||
name: 'BaseError',
|
||||
description: 'Base error',
|
||||
definition (t) {
|
||||
t.string('title')
|
||||
t.string('message')
|
||||
t.string('stack')
|
||||
},
|
||||
sourceType: {
|
||||
module: __filename,
|
||||
export: 'BaseErrorSource',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import { objectType } from 'nexus'
|
||||
import { FileParts } from './gql-FileParts'
|
||||
|
||||
export const CodeFrame = objectType({
|
||||
name: 'CodeFrame',
|
||||
description: 'A code frame to display for a file, used when displaying code related to errors',
|
||||
definition (t) {
|
||||
t.int('line', {
|
||||
description: 'The line number of the code snippet to display',
|
||||
})
|
||||
|
||||
t.int('column', {
|
||||
description: 'The column of the error to display',
|
||||
})
|
||||
|
||||
t.string('codeBlock', {
|
||||
description: 'Source of the code frame to display',
|
||||
})
|
||||
|
||||
t.nonNull.field('file', {
|
||||
type: FileParts,
|
||||
resolve (source, args, ctx) {
|
||||
return { absolute: source.absolute }
|
||||
},
|
||||
})
|
||||
},
|
||||
sourceType: {
|
||||
module: '@packages/data-context/src/sources/ErrorDataSource',
|
||||
export: 'CodeFrameShape',
|
||||
},
|
||||
})
|
||||
@@ -1,7 +1,6 @@
|
||||
import { PACKAGE_MANAGERS } from '@packages/types'
|
||||
import { enumType, nonNull, objectType, stringArg } from 'nexus'
|
||||
import path from 'path'
|
||||
import { BaseError } from '.'
|
||||
import { BrowserStatusEnum } from '..'
|
||||
import { cloudProjectBySlug } from '../../stitching/remoteGraphQLCalls'
|
||||
import { TestingTypeEnum } from '../enumTypes/gql-WizardEnums'
|
||||
@@ -29,16 +28,6 @@ export const CurrentProject = objectType({
|
||||
resolve: (source, args, ctx) => ctx.coreData.packageManager,
|
||||
})
|
||||
|
||||
t.field('errorLoadingConfigFile', {
|
||||
type: BaseError,
|
||||
description: 'If there is an error loading the config file, it is represented here',
|
||||
})
|
||||
|
||||
t.field('errorLoadingNodeEvents', {
|
||||
type: BaseError,
|
||||
description: 'If there is an error related to the node events, it is represented here',
|
||||
})
|
||||
|
||||
t.boolean('isLoadingConfigFile', {
|
||||
description: 'Whether we are currently loading the configFile',
|
||||
})
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { stripAnsi } from '@packages/errors'
|
||||
import { objectType } from 'nexus'
|
||||
|
||||
import { ErrorTypeEnum } from '../enumTypes/gql-ErrorTypeEnum'
|
||||
import { CodeFrame } from './gql-CodeFrame'
|
||||
|
||||
export const ErrorWrapper = objectType({
|
||||
name: 'ErrorWrapper',
|
||||
description: 'Base error',
|
||||
definition (t) {
|
||||
t.string('title', {
|
||||
description: 'Optional title of the error. Used to optionally display a title above the error',
|
||||
})
|
||||
|
||||
t.nonNull.string('errorName', {
|
||||
description: 'Name of the error class',
|
||||
resolve (source) {
|
||||
return source.cypressError.originalError?.name || source.cypressError.name
|
||||
},
|
||||
})
|
||||
|
||||
t.nonNull.string('errorStack', {
|
||||
description: 'The error stack of either the original error from the user or from where the internal Cypress error was created',
|
||||
resolve (source) {
|
||||
return stripAnsi(source.cypressError.stack || '')
|
||||
},
|
||||
})
|
||||
|
||||
t.nonNull.field('errorType', {
|
||||
type: ErrorTypeEnum,
|
||||
resolve: (source) => source.cypressError.type ?? 'UNEXPECTED_INTERNAL_ERROR',
|
||||
})
|
||||
|
||||
t.nonNull.string('errorMessage', {
|
||||
description: 'The markdown formatted content associated with the ErrorTypeEnum',
|
||||
resolve (source) {
|
||||
return source.cypressError.messageMarkdown
|
||||
},
|
||||
})
|
||||
|
||||
t.nonNull.boolean('isUserCodeError', {
|
||||
description: 'Whether the error came from user code, can be used to determine whether to open a stack trace by default',
|
||||
resolve (source, args, ctx) {
|
||||
return ctx.error.isUserCodeError(source)
|
||||
},
|
||||
})
|
||||
|
||||
t.field('codeFrame', {
|
||||
type: CodeFrame,
|
||||
description: 'The code frame to display in relation to the error',
|
||||
resolve: (source, args, ctx) => ctx.error.codeFrame(source),
|
||||
})
|
||||
},
|
||||
sourceType: {
|
||||
module: '@packages/errors',
|
||||
export: 'ErrorWrapperSource',
|
||||
},
|
||||
})
|
||||
@@ -2,8 +2,11 @@ import { objectType } from 'nexus'
|
||||
import path from 'path'
|
||||
|
||||
export interface FilePartsShape {
|
||||
line?: number
|
||||
column?: number
|
||||
absolute: string
|
||||
// For when we're merging the file
|
||||
// For when we're merging / creating the file and might not have the file contents on-disk yet
|
||||
// used in the scaffolding
|
||||
contents?: string
|
||||
}
|
||||
|
||||
@@ -57,6 +60,14 @@ export const FileParts = objectType({
|
||||
return root.contents || ctx.file.readFile(root.absolute)
|
||||
},
|
||||
})
|
||||
|
||||
t.int('line', {
|
||||
description: 'If provided, used to specify the line of the file to open in openFileInIDE',
|
||||
})
|
||||
|
||||
t.int('column', {
|
||||
description: 'If provided, used to specify the column of the file to open in openFileInIDE',
|
||||
})
|
||||
},
|
||||
sourceType: {
|
||||
module: __filename,
|
||||
|
||||
@@ -11,6 +11,17 @@ import { ScaffoldedFile } from './gql-ScaffoldedFile'
|
||||
|
||||
export const mutation = mutationType({
|
||||
definition (t) {
|
||||
t.field('reinitializeCypress', {
|
||||
type: 'Query',
|
||||
description: 'Re-initializes Cypress from the initial CLI options',
|
||||
resolve: async (_, args, ctx) => {
|
||||
await ctx.reinitializeCypress(ctx.modeOptions)
|
||||
await ctx.initializeMode()
|
||||
|
||||
return {}
|
||||
},
|
||||
})
|
||||
|
||||
t.field('devRelaunch', {
|
||||
type: 'Boolean',
|
||||
description: 'Development only: Triggers or dismisses a prompted refresh by touching the file watched by our development scripts',
|
||||
@@ -136,11 +147,7 @@ export const mutation = mutationType({
|
||||
// if necessary init the wizard for configuration
|
||||
if (ctx.coreData.currentTestingType
|
||||
&& !ctx.lifecycleManager.isTestingTypeConfigured(ctx.coreData.currentTestingType)) {
|
||||
try {
|
||||
await ctx.actions.wizard.initialize()
|
||||
} catch (e) {
|
||||
ctx.coreData.baseError = e as Error
|
||||
}
|
||||
await ctx.actions.wizard.initialize()
|
||||
}
|
||||
|
||||
return {}
|
||||
@@ -249,16 +256,13 @@ export const mutation = mutationType({
|
||||
|
||||
t.field('launchOpenProject', {
|
||||
type: CurrentProject,
|
||||
slowLogThreshold: false,
|
||||
description: 'Launches project from open_project global singleton',
|
||||
args: {
|
||||
specPath: stringArg(),
|
||||
},
|
||||
resolve: async (_, args, ctx) => {
|
||||
try {
|
||||
await ctx.actions.project.launchProject(ctx.coreData.currentTestingType, {}, args.specPath)
|
||||
} catch (e) {
|
||||
ctx.coreData.baseError = e as Error
|
||||
}
|
||||
await ctx.actions.project.launchProject(ctx.coreData.currentTestingType, {}, args.specPath)
|
||||
|
||||
return ctx.lifecycleManager
|
||||
},
|
||||
@@ -310,18 +314,7 @@ export const mutation = mutationType({
|
||||
path: nonNull(stringArg()),
|
||||
},
|
||||
resolve: async (_, args, ctx) => {
|
||||
try {
|
||||
await ctx.actions.project.setCurrentProject(args.path)
|
||||
ctx.coreData.baseError = null
|
||||
} catch (error) {
|
||||
const e = error as Error
|
||||
|
||||
ctx.coreData.baseError = {
|
||||
title: 'Cypress Configuration Error',
|
||||
message: e.message,
|
||||
stack: e.stack,
|
||||
}
|
||||
}
|
||||
await ctx.actions.project.setCurrentProject(args.path)
|
||||
|
||||
return {}
|
||||
},
|
||||
@@ -456,7 +449,7 @@ export const mutation = mutationType({
|
||||
},
|
||||
resolve: (_, args, ctx) => {
|
||||
ctx.actions.file.openFile(
|
||||
args.input.absolute,
|
||||
args.input.filePath,
|
||||
args.input.line || 1,
|
||||
args.input.column || 1,
|
||||
)
|
||||
@@ -489,17 +482,7 @@ export const mutation = mutationType({
|
||||
},
|
||||
resolve: async (_, { skip, before, after }, ctx) => {
|
||||
if (!skip && before && after) {
|
||||
try {
|
||||
await ctx.actions.migration.renameSpecFiles(before, after)
|
||||
} catch (error) {
|
||||
const e = error as Error
|
||||
|
||||
ctx.coreData.baseError = {
|
||||
title: 'Spec Files Migration Error',
|
||||
message: e.message,
|
||||
stack: e.stack,
|
||||
}
|
||||
}
|
||||
await ctx.actions.migration.renameSpecFiles(before, after)
|
||||
}
|
||||
|
||||
await ctx.actions.migration.nextStep()
|
||||
@@ -512,20 +495,7 @@ export const mutation = mutationType({
|
||||
description: 'When the user decides to skip specs rename',
|
||||
type: Query,
|
||||
resolve: async (_, args, ctx) => {
|
||||
try {
|
||||
await ctx.actions.migration.renameSpecsFolder()
|
||||
} catch (error) {
|
||||
const e = error as Error
|
||||
|
||||
const message = e.message === 'dest already exists.' ? 'e2e folder already exists.' : e.message
|
||||
|
||||
ctx.coreData.baseError = {
|
||||
title: 'Spec Folder Migration Error',
|
||||
message,
|
||||
stack: e.stack,
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.actions.migration.renameSpecsFolder()
|
||||
await ctx.actions.migration.nextStep()
|
||||
|
||||
return {}
|
||||
@@ -566,17 +536,7 @@ export const mutation = mutationType({
|
||||
description: 'While migrating to 10+ launch renaming of support file',
|
||||
type: Query,
|
||||
resolve: async (_, args, ctx) => {
|
||||
try {
|
||||
await ctx.actions.migration.renameSupportFile()
|
||||
} catch (error) {
|
||||
const e = error as Error
|
||||
|
||||
ctx.coreData.baseError = {
|
||||
title: 'Support File Migration Error',
|
||||
message: e.message,
|
||||
stack: e.stack,
|
||||
}
|
||||
}
|
||||
await ctx.actions.migration.renameSupportFile()
|
||||
await ctx.actions.migration.nextStep()
|
||||
|
||||
return {}
|
||||
@@ -588,18 +548,7 @@ export const mutation = mutationType({
|
||||
type: Query,
|
||||
slowLogThreshold: 5000, // This mutation takes a little time
|
||||
resolve: async (_, args, ctx) => {
|
||||
try {
|
||||
await ctx.actions.migration.createConfigFile()
|
||||
} catch (error) {
|
||||
const e = error as Error
|
||||
|
||||
ctx.coreData.baseError = {
|
||||
title: 'Config File Migration Error',
|
||||
message: e.message,
|
||||
stack: e.stack,
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.actions.migration.createConfigFile()
|
||||
await ctx.actions.migration.nextStep()
|
||||
|
||||
return {}
|
||||
@@ -679,5 +628,13 @@ export const mutation = mutationType({
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
||||
t.field('dismissWarning', {
|
||||
type: Query,
|
||||
description: `Dismisses a warning displayed by the frontend`,
|
||||
resolve: (source) => {
|
||||
return {}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { objectType } from 'nexus'
|
||||
import { BaseError } from '.'
|
||||
import { ProjectLike, ScaffoldedFile, TestingTypeEnum } from '..'
|
||||
import { CurrentProject } from './gql-CurrentProject'
|
||||
import { DevState } from './gql-DevState'
|
||||
@@ -8,19 +7,19 @@ import { LocalSettings } from './gql-LocalSettings'
|
||||
import { Migration } from './gql-Migration'
|
||||
import { VersionData } from './gql-VersionData'
|
||||
import { Wizard } from './gql-Wizard'
|
||||
import { Warning } from './gql-Warning'
|
||||
import { ErrorWrapper } from './gql-ErrorWrapper'
|
||||
|
||||
export const Query = objectType({
|
||||
name: 'Query',
|
||||
description: 'The root "Query" type containing all entry fields for our querying',
|
||||
definition (t) {
|
||||
t.field('baseError', {
|
||||
type: BaseError,
|
||||
type: ErrorWrapper,
|
||||
resolve: (root, args, ctx) => ctx.baseError,
|
||||
})
|
||||
|
||||
t.nonNull.list.nonNull.field('warnings', {
|
||||
type: Warning,
|
||||
type: ErrorWrapper,
|
||||
description: 'A list of warnings',
|
||||
resolve: (source, args, ctx) => {
|
||||
return ctx.coreData.warnings
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { objectType } from 'nexus'
|
||||
|
||||
export const Warning = objectType({
|
||||
name: 'Warning',
|
||||
description: 'A generic warning',
|
||||
definition (t) {
|
||||
t.nonNull.string('title')
|
||||
t.nonNull.string('message')
|
||||
t.string('details')
|
||||
},
|
||||
sourceType: {
|
||||
module: '@packages/types',
|
||||
export: 'Warning',
|
||||
},
|
||||
})
|
||||
@@ -2,12 +2,13 @@
|
||||
// created by autobarrel, do not modify directly
|
||||
|
||||
export * from './gql-AuthState'
|
||||
export * from './gql-BaseError'
|
||||
export * from './gql-Browser'
|
||||
export * from './gql-CodeFrame'
|
||||
export * from './gql-CodeGenGlobs'
|
||||
export * from './gql-CurrentProject'
|
||||
export * from './gql-DevState'
|
||||
export * from './gql-Editor'
|
||||
export * from './gql-ErrorWrapper'
|
||||
export * from './gql-FileParts'
|
||||
export * from './gql-GenerateSpecResponse'
|
||||
export * from './gql-GeneratedSpecError'
|
||||
@@ -24,7 +25,6 @@ export * from './gql-Storybook'
|
||||
export * from './gql-TestingTypeInfo'
|
||||
export * from './gql-Version'
|
||||
export * from './gql-VersionData'
|
||||
export * from './gql-Warning'
|
||||
export * from './gql-Wizard'
|
||||
export * from './gql-WizardBundler'
|
||||
export * from './gql-WizardCodeLanguage'
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json'
|
||||
|
||||
// TODO: add when we land the lifecycle management
|
||||
const expectStackToBe = (mode: 'open' | 'closed') => {
|
||||
cy.get(`[data-cy="stack-open-${mode === 'open' ? 'true' : 'false'}"]`)
|
||||
}
|
||||
|
||||
describe('Config files error handling', () => {
|
||||
beforeEach(() => {
|
||||
cy.scaffoldProject('pristine')
|
||||
@@ -18,13 +21,13 @@ describe('Config files error handling', () => {
|
||||
cy.openProject('pristine-with-e2e-testing')
|
||||
cy.visitLaunchpad()
|
||||
|
||||
cy.get('body').should('contain.text', 'Something went wrong')
|
||||
cy.get('body').should('contain.text', 'Please remove one of the two and try again')
|
||||
expectStackToBe('closed')
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.removeFileInProject('cypress.config.js')
|
||||
})
|
||||
|
||||
cy.get('body')
|
||||
.should('not.contain.text', 'Something went wrong')
|
||||
cy.get('h1').should('contain', 'Welcome to Cypress')
|
||||
})
|
||||
|
||||
it('shows the upgrade screen if there is a legacy config file', () => {
|
||||
@@ -52,43 +55,98 @@ describe('Config files error handling', () => {
|
||||
cy.visitLaunchpad()
|
||||
|
||||
cy.get('body').should('contain.text', 'Cypress no longer supports')
|
||||
expectStackToBe('closed')
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.removeFileInProject('cypress.json')
|
||||
})
|
||||
|
||||
cy.get('body').should('not.contain.text', 'Cypress no longer supports')
|
||||
cy.get('h1').should('contain', 'Welcome to Cypress')
|
||||
})
|
||||
|
||||
it('handles deprecated config fields', () => {
|
||||
cy.openProject('pristine')
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = { experimentalComponentTesting: true, e2e: {} }')
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = { e2e: { supportFile: false, experimentalComponentTesting: true } }')
|
||||
})
|
||||
|
||||
cy.openProject('pristine')
|
||||
|
||||
cy.visitLaunchpad()
|
||||
cy.get('[data-cy-testingType=e2e]').click()
|
||||
cy.get('body').should('contain.text', 'Something went wrong')
|
||||
cy.get('body').should('contain.text', 'It looks like there\'s some issues that need to be resolved before we continue.')
|
||||
cy.findByText('Error Loading Config')
|
||||
})
|
||||
|
||||
it('handles deprecated fields on root config', () => {
|
||||
cy.openProject('pristine')
|
||||
|
||||
cy.get('body', { timeout: 10000 }).should('contain.text', 'was removed in Cypress version')
|
||||
expectStackToBe('closed')
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', `module.exports = { supportFile: 'cypress/support.ts', e2e: {} }`)
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = { e2e: { supportFile: false } }')
|
||||
})
|
||||
|
||||
cy.openProject('pristine')
|
||||
|
||||
cy.visitLaunchpad()
|
||||
cy.get('[data-cy-testingType=e2e]').click()
|
||||
cy.get('body').should('contain.text', 'Something went wrong')
|
||||
cy.get('body').should('contain.text', 'It looks like there\'s some issues that need to be resolved before we continue.')
|
||||
cy.findByText('Error Loading Config')
|
||||
cy.get('h1').should('contain', 'Choose a Browser')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Launchpad: Error System Tests', () => {
|
||||
it('Handles an error thrown from the root of the config file', () => {
|
||||
cy.scaffoldProject('plugins-root-sync-error')
|
||||
cy.openProject('plugins-root-sync-error', ['--e2e'])
|
||||
cy.visitLaunchpad()
|
||||
cy.get('h1').should('contain', 'Error')
|
||||
expectStackToBe('open')
|
||||
})
|
||||
|
||||
it('Handles an error thrown asynchronously in the root of the config', () => {
|
||||
cy.scaffoldProject('plugins-root-async-error')
|
||||
cy.openProject('plugins-root-async-error', ['--e2e'])
|
||||
cy.visitLaunchpad()
|
||||
cy.get('h1').should('not.exist') // No title set on unhandled error
|
||||
expectStackToBe('open')
|
||||
})
|
||||
|
||||
it('Handles an synchronously in setupNodeEvents', () => {
|
||||
cy.scaffoldProject('plugins-function-sync-error')
|
||||
cy.openProject('plugins-function-sync-error', ['--e2e'])
|
||||
cy.visitLaunchpad()
|
||||
cy.get('h1').should('contain', 'Error Loading Config')
|
||||
expectStackToBe('open')
|
||||
})
|
||||
|
||||
it('Handles an error thrown while validating config', () => {
|
||||
cy.scaffoldProject('config-with-invalid-browser')
|
||||
cy.openProject('config-with-invalid-browser', ['--e2e'])
|
||||
cy.visitLaunchpad()
|
||||
cy.get('h1').should('contain', 'Error Loading Config')
|
||||
expectStackToBe('closed')
|
||||
})
|
||||
|
||||
it('Handles an error thrown from the tasks', () => {
|
||||
cy.scaffoldProject('plugins-function-sync-error')
|
||||
cy.openProject('plugins-function-sync-error', ['--e2e'])
|
||||
cy.visitLaunchpad()
|
||||
cy.get('h1').should('contain', 'Error Loading Config')
|
||||
expectStackToBe('open')
|
||||
})
|
||||
|
||||
it('Handles a TS syntax error when loading the config', () => {
|
||||
cy.scaffoldProject('config-with-ts-syntax-error')
|
||||
cy.openProject('config-with-ts-syntax-error')
|
||||
cy.visitLaunchpad()
|
||||
cy.get('h1').should('contain', 'Error Loading Config')
|
||||
cy.percySnapshot()
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.ts', 'module.exports = { e2e: { supportFile: false } }')
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Try again' }).click()
|
||||
|
||||
cy.get('h1').should('contain', 'Welcome to Cypress')
|
||||
})
|
||||
|
||||
it('shows correct user file instead of node file', () => {
|
||||
cy.scaffoldProject('config-with-import-error')
|
||||
cy.openProject('config-with-import-error')
|
||||
cy.visitLaunchpad()
|
||||
cy.get('h1').should('contain', 'Error Loading Config')
|
||||
cy.percySnapshot()
|
||||
|
||||
cy.get('[data-testid="error-code-frame"]').should('contain', 'cypress.config.js:4:23')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,7 +9,6 @@ describe('Error handling', () => {
|
||||
cy.get('[data-cy-testingType=e2e]').click()
|
||||
|
||||
cy.get('body')
|
||||
.should('contain.text', 'Please confirm that everything looks correct in your cypress.config.js file.')
|
||||
.and('contain.text', 'threw an error from')
|
||||
|
||||
cy.get('[data-cy="collapsible-header"]')
|
||||
@@ -42,7 +41,6 @@ describe('Error handling', () => {
|
||||
cy.visitLaunchpad()
|
||||
|
||||
cy.get('body')
|
||||
.should('contain.text', 'Please confirm that everything looks correct in your cypress.config.js file.')
|
||||
.should('contain.text', 'Error Loading Config')
|
||||
.and('contain.text', 'Error thrown from Config')
|
||||
|
||||
|
||||
@@ -187,14 +187,14 @@ describe('Launchpad: Setup Project', () => {
|
||||
|
||||
// project has a cypress.configuration file with component testing configured
|
||||
describe('project that has not been configured for e2e', () => {
|
||||
// FIXME: ProjectLifecycleManager is skipping straight to browser pages when it should show setup page.
|
||||
it.skip('shows the configuration setup page when selecting e2e tests', () => {
|
||||
it('shows the configuration setup page when selecting e2e tests', () => {
|
||||
scaffoldAndOpenProject('pristine-with-ct-testing')
|
||||
cy.visitLaunchpad()
|
||||
|
||||
verifyWelcomePage({ e2eIsConfigured: false, ctIsConfigured: true })
|
||||
|
||||
cy.get('[data-cy-testingtype="e2e"]').click()
|
||||
cy.findByRole('button', { name: 'Next Step' }).click()
|
||||
|
||||
cy.contains('h1', 'Configuration Files')
|
||||
cy.findByText('We added the following files to your project.')
|
||||
@@ -217,23 +217,20 @@ describe('Launchpad: Setup Project', () => {
|
||||
])
|
||||
})
|
||||
|
||||
// FIXME: ProjectLifecycleManager is skipping straight to browser pages when it should show setup page.
|
||||
it.skip('moves to "Choose a Browser" page after clicking "Continue" button in first step in configuration page', () => {
|
||||
scaffoldAndOpenProject('pristine-with-ct-testing')
|
||||
it('moves to "Choose a Browser" page after clicking "Continue" button in first step in configuration page', () => {
|
||||
scaffoldAndOpenProject('pristine')
|
||||
cy.visitLaunchpad()
|
||||
|
||||
verifyWelcomePage({ e2eIsConfigured: false, ctIsConfigured: true })
|
||||
verifyWelcomePage({ e2eIsConfigured: false, ctIsConfigured: false })
|
||||
|
||||
cy.get('[data-cy-testingtype="e2e"]').click()
|
||||
cy.findByRole('button', { name: 'Next Step' }).click()
|
||||
|
||||
cy.contains('h1', 'Configuration Files')
|
||||
cy.findByText('We added the following files to your project.')
|
||||
|
||||
cy.get('[data-cy=changes]').within(() => {
|
||||
cy.contains('cypress.config.js')
|
||||
})
|
||||
|
||||
cy.get('[data-cy=valid]').within(() => {
|
||||
cy.contains('cypress.config.js')
|
||||
cy.containsPath('cypress/support/e2e.js')
|
||||
cy.containsPath('cypress/support/commands.js')
|
||||
cy.containsPath('cypress/fixtures/example.json')
|
||||
@@ -912,7 +909,7 @@ describe('Launchpad: Setup Project', () => {
|
||||
cy.get('[data-cy-testingtype="component"]').click()
|
||||
cy.get('[data-testid="select-framework"]').click()
|
||||
cy.findByText('Nuxt.js (v2)').click()
|
||||
cy.findByText('Next Step').click()
|
||||
cy.findByRole('button', { name: 'Next Step' }).should('not.be.disabled').click()
|
||||
fakeInstalledDeps()
|
||||
cy.findByRole('button', { name: 'Continue' }).click()
|
||||
cy.intercept('POST', 'mutation-ExternalLink_OpenExternal', { 'data': { 'openExternal': true } }).as('OpenExternal')
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<BaseError
|
||||
v-if="query.data.value.baseError"
|
||||
:gql="query.data.value.baseError"
|
||||
:retry="reinitializeCypress"
|
||||
/>
|
||||
<GlobalPage
|
||||
v-else-if="query.data.value.isInGlobalMode || !query.data.value?.currentProject"
|
||||
@@ -20,14 +21,6 @@
|
||||
v-if="query.data.value.scaffoldedFiles"
|
||||
:gql="query.data.value"
|
||||
/>
|
||||
<BaseError
|
||||
v-else-if="currentProject?.errorLoadingConfigFile"
|
||||
:gql="currentProject.errorLoadingConfigFile"
|
||||
/>
|
||||
<BaseError
|
||||
v-else-if="currentProject?.errorLoadingNodeEvents"
|
||||
:gql="currentProject.errorLoadingNodeEvents"
|
||||
/>
|
||||
<Spinner v-else-if="currentProject?.isLoadingConfigFile" />
|
||||
<template v-else-if="currentProject?.isLoadingNodeEvents">
|
||||
<LaunchpadHeader
|
||||
@@ -79,8 +72,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { gql, useQuery } from '@urql/vue'
|
||||
import { MainLaunchpadQueryDocument } from './generated/graphql'
|
||||
import { gql, useMutation, useQuery } from '@urql/vue'
|
||||
import { MainLaunchpadQueryDocument, Main_ReinitializeCypressDocument } from './generated/graphql'
|
||||
import TestingTypeCards from './setup/TestingTypeCards.vue'
|
||||
import Wizard from './setup/Wizard.vue'
|
||||
import ScaffoldLanguageSelect from './setup/ScaffoldLanguageSelect.vue'
|
||||
@@ -107,7 +100,7 @@ fragment MainLaunchpadQueryData on Query {
|
||||
...Wizard
|
||||
...ScaffoldLanguageSelect
|
||||
baseError {
|
||||
...BaseError_Data
|
||||
...BaseError
|
||||
}
|
||||
currentTestingType
|
||||
currentProject {
|
||||
@@ -118,12 +111,6 @@ fragment MainLaunchpadQueryData on Query {
|
||||
isLoadingNodeEvents
|
||||
needsLegacyConfigMigration
|
||||
currentTestingType
|
||||
errorLoadingConfigFile {
|
||||
...BaseError_Data
|
||||
}
|
||||
errorLoadingNodeEvents {
|
||||
...BaseError_Data
|
||||
}
|
||||
}
|
||||
isInGlobalMode
|
||||
...GlobalPage
|
||||
@@ -137,6 +124,22 @@ query MainLaunchpadQuery {
|
||||
}
|
||||
`
|
||||
|
||||
gql`
|
||||
mutation Main_ReinitializeCypress {
|
||||
reinitializeCypress {
|
||||
...MainLaunchpadQueryData
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const mutation = useMutation(Main_ReinitializeCypressDocument)
|
||||
|
||||
const reinitializeCypress = () => {
|
||||
if (!mutation.fetching.value) {
|
||||
mutation.executeMutation({})
|
||||
}
|
||||
}
|
||||
|
||||
const query = useQuery({ query: MainLaunchpadQueryDocument })
|
||||
const currentProject = computed(() => query.data.value?.currentProject)
|
||||
</script>
|
||||
|
||||
@@ -80,7 +80,8 @@ import Button from '@cy/components/Button.vue'
|
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
import Badge from '@cy/components/Badge.vue'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import ShikiHighlight, { CyLangType, langsSupported } from '@cy/components/ShikiHighlight.vue'
|
||||
import ShikiHighlight, { langsSupported } from '@cy/components/ShikiHighlight.vue'
|
||||
import type { CyLangType } from '@cy/components/ShikiHighlight.vue'
|
||||
import ListRowHeader from '@cy/components/ListRowHeader.vue'
|
||||
import Collapsible from '@cy/components/Collapsible.vue'
|
||||
import AddedIcon from '~icons/cy/file-changes-added_x24.svg'
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { defaultMessages } from '@cy/i18n'
|
||||
import { codeFrameColumns } from '@babel/code-frame'
|
||||
import BaseError from './BaseError.vue'
|
||||
import Button from '@cy/components/Button.vue'
|
||||
import { BaseError_DataFragmentDoc } from '../generated/graphql-test'
|
||||
import { BaseErrorFragmentDoc } from '../generated/graphql-test'
|
||||
import dedent from 'dedent'
|
||||
|
||||
// Selectors
|
||||
const headerSelector = '[data-testid=error-header]'
|
||||
const messageSelector = '[data-testid=error-message]'
|
||||
const retryButtonSelector = '[data-testid=error-retry-button]'
|
||||
const docsButtonSelector = '[data-testid=error-read-the-docs-button]'
|
||||
const customFooterSelector = '[data-testid=custom-error-footer]'
|
||||
|
||||
// Constants
|
||||
const messages = defaultMessages.launchpadErrors.generic
|
||||
const messages = cy.i18n.launchpadErrors.generic
|
||||
const customHeaderMessage = 'Well, this was unexpected!'
|
||||
const customMessage = `Don't worry, just click the "It's fixed now" button to try again.`
|
||||
const customFooterText = `Yikes, try again!`
|
||||
@@ -23,30 +23,43 @@ describe('<BaseError />', () => {
|
||||
})
|
||||
|
||||
it('renders the default error the correct messages', () => {
|
||||
cy.mountFragment(BaseError_DataFragmentDoc, {
|
||||
onResult: (result) => {
|
||||
result.title = messages.header
|
||||
},
|
||||
render: (gqlVal) => <BaseError gql={gqlVal} retry={() => {}} />,
|
||||
cy.mountFragment(BaseErrorFragmentDoc, {
|
||||
render: (gqlVal) => <BaseError gql={gqlVal} />,
|
||||
})
|
||||
.get(headerSelector)
|
||||
.should('contain.text', messages.header)
|
||||
.should('contain.text', cy.gqlStub.ErrorWrapper.title)
|
||||
.get(messageSelector)
|
||||
.should('contain.text', `It looks like there's some issues that need to be resolved before we continue`)
|
||||
.should('contain.text', cy.gqlStub.ErrorWrapper.errorMessage.replace(/\`/g, '').slice(0, 10))
|
||||
.get(retryButtonSelector)
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('renders the retry button if retry is passed', () => {
|
||||
cy.mountFragment(BaseErrorFragmentDoc, {
|
||||
render: (gqlVal) => <BaseError gql={gqlVal} retry={() => {}} />,
|
||||
})
|
||||
.get(retryButtonSelector)
|
||||
.should('contain.text', messages.retryButton)
|
||||
.get(docsButtonSelector)
|
||||
.should('contain.text', messages.readTheDocsButton)
|
||||
})
|
||||
|
||||
it('does not open the stack by default if it is not a user error', () => {
|
||||
cy.mountFragment(BaseErrorFragmentDoc, {
|
||||
onResult (result) {
|
||||
result.isUserCodeError = false
|
||||
},
|
||||
render: (gqlVal) => <BaseError gql={gqlVal} />,
|
||||
}).then(() => {
|
||||
cy.get('[data-cy=stack-open-true]').should('not.exist')
|
||||
cy.contains('Stack Trace').click()
|
||||
cy.contains('Error: foobar').should('be.visible')
|
||||
cy.get('[data-cy=stack-open-true]')
|
||||
})
|
||||
})
|
||||
|
||||
it('calls the retry function passed in', () => {
|
||||
const retrySpy = cy.spy().as('retry')
|
||||
|
||||
cy.mountFragment(BaseError_DataFragmentDoc, {
|
||||
onResult: (result) => {
|
||||
result.title = messages.header
|
||||
result.message = null
|
||||
},
|
||||
cy.mountFragment(BaseErrorFragmentDoc, {
|
||||
render: (gqlVal) => (<div class="p-16px">
|
||||
<BaseError gql={gqlVal} retry={retrySpy} />,
|
||||
</div>),
|
||||
@@ -59,11 +72,11 @@ describe('<BaseError />', () => {
|
||||
})
|
||||
|
||||
it('renders custom error messages and headers with props', () => {
|
||||
cy.mountFragment(BaseError_DataFragmentDoc, {
|
||||
cy.mountFragment(BaseErrorFragmentDoc, {
|
||||
onResult: (result) => {
|
||||
result.title = customHeaderMessage
|
||||
result.message = customMessage
|
||||
result.stack = customStack
|
||||
result.errorMessage = customMessage
|
||||
result.errorStack = customStack
|
||||
},
|
||||
render: (gqlVal) => (<div class="p-16px">
|
||||
<BaseError gql={gqlVal} />
|
||||
@@ -76,10 +89,10 @@ describe('<BaseError />', () => {
|
||||
})
|
||||
|
||||
it('renders the header, message, and footer slots', () => {
|
||||
cy.mountFragment(BaseError_DataFragmentDoc, {
|
||||
cy.mountFragment(BaseErrorFragmentDoc, {
|
||||
onResult: (result) => {
|
||||
result.title = messages.header
|
||||
result.message = messages.message
|
||||
result.errorMessage = messages.message
|
||||
},
|
||||
render: (gqlVal) => (
|
||||
<BaseError
|
||||
@@ -96,4 +109,42 @@ describe('<BaseError />', () => {
|
||||
.get(customFooterSelector)
|
||||
.should('contain.text', customFooterText)
|
||||
})
|
||||
|
||||
it('renders the header, message, and footer slots', () => {
|
||||
cy.mountFragment(BaseErrorFragmentDoc, {
|
||||
onResult: (result) => {
|
||||
result.title = messages.header
|
||||
result.codeFrame = {
|
||||
__typename: 'CodeFrame',
|
||||
line: 12,
|
||||
column: 25,
|
||||
codeBlock: codeFrameColumns(dedent`
|
||||
const x = 1;
|
||||
|
||||
throw new Error('Some Error');
|
||||
|
||||
const y = 2;
|
||||
`, {
|
||||
start: {
|
||||
line: 3,
|
||||
column: 5,
|
||||
},
|
||||
}, {
|
||||
linesAbove: 2,
|
||||
linesBelow: 4,
|
||||
}),
|
||||
file: {
|
||||
id: `FileParts:/absolute/full/path/cypress/e2e/file.cy.js`,
|
||||
__typename: 'FileParts',
|
||||
relative: 'cypress/e2e/file.cy.js',
|
||||
absolute: '/absolute/full/path/cypress/e2e/file.cy.js',
|
||||
},
|
||||
}
|
||||
},
|
||||
render: (gqlVal) => (
|
||||
<BaseError gql={gqlVal} />),
|
||||
})
|
||||
|
||||
cy.findByText('cypress/e2e/file.cy.js:12:25').should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
<template>
|
||||
<div class="mx-auto space-y-32px text-center min-w-476px max-w-848px py-16px children:text-center">
|
||||
<div
|
||||
v-if="baseError"
|
||||
class="mx-auto space-y-32px text-center min-w-476px max-w-848px pt-16px children:text-center"
|
||||
>
|
||||
<div>
|
||||
<h1
|
||||
class="font-medium leading-snug text-32px text-gray-900"
|
||||
v-if="baseError.title"
|
||||
class="font-medium leading-snug pb-24px text-32px text-gray-900"
|
||||
data-testid="error-header"
|
||||
>
|
||||
<slot name="header">
|
||||
{{ headerText }}
|
||||
{{ baseError.title }}
|
||||
</slot>
|
||||
</h1>
|
||||
<!-- eslint-disable vue/multiline-html-element-content-newline -->
|
||||
|
||||
<slot name="message">
|
||||
<!-- Can't pull this out because of the i18n-t component -->
|
||||
<i18n-t
|
||||
scope="global"
|
||||
keypath="launchpadErrors.generic.message"
|
||||
tag="p"
|
||||
class="font-light pb-24px"
|
||||
data-testid="error-message"
|
||||
>
|
||||
<OpenConfigFileInIDE />
|
||||
</i18n-t>
|
||||
<Alert
|
||||
:title="props.gql.title ?? ''"
|
||||
:title="baseError.errorName"
|
||||
status="error"
|
||||
body-class="px-0px bg-red-50"
|
||||
alert-class="bg-red-50"
|
||||
@@ -32,25 +26,31 @@
|
||||
icon-classes="icon-dark-red-400"
|
||||
max-height="none"
|
||||
>
|
||||
<p
|
||||
v-if="errorMessage"
|
||||
class="border-b-1 border-b-red-100 p-16px pt-0 text-red-500"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
<p
|
||||
v-if="stack"
|
||||
<div class="border-b-1 border-b-red-100 p-16px pt-0">
|
||||
<div
|
||||
ref="markdownTarget"
|
||||
class="text-red-500"
|
||||
data-testid="error-message"
|
||||
v-html="markdown"
|
||||
/>
|
||||
<ErrorCodeFrame
|
||||
v-if="baseError.codeFrame"
|
||||
:gql="baseError.codeFrame"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="m-16px mb-0 overflow-hidden"
|
||||
>
|
||||
<Collapsible
|
||||
disable
|
||||
max-height="none"
|
||||
initially-open
|
||||
:initially-open="baseError.isUserCodeError"
|
||||
>
|
||||
<template #target="{open, toggle}">
|
||||
<p
|
||||
class="gap-8px inline-flex items-center justify-center"
|
||||
:class="{'pb-8px': open}"
|
||||
:data-cy="`stack-open-${open}`"
|
||||
>
|
||||
<i-cy-chevron-right-small_x16
|
||||
class="min-w-8px min-h-8px transform duration-150 icon-dark-red-400"
|
||||
@@ -65,12 +65,12 @@
|
||||
</p>
|
||||
</template>
|
||||
<pre
|
||||
v-if="stack"
|
||||
data-testid="error-header"
|
||||
class="bg-white rounded font-light border-1 border-red-200 p-16px overflow-auto"
|
||||
v-html="stack"
|
||||
v-html="baseError.errorStack"
|
||||
/>
|
||||
</Collapsible>
|
||||
</p>
|
||||
</div>
|
||||
</Alert>
|
||||
</slot>
|
||||
<!-- eslint-enable vue/multiline-html-element-content-newline -->
|
||||
@@ -80,7 +80,7 @@
|
||||
<div class="w-full gap-16px inline-flex">
|
||||
<slot name="footer">
|
||||
<Button
|
||||
v-if="retry"
|
||||
v-if="props.retry"
|
||||
size="lg"
|
||||
variant="primary"
|
||||
data-testid="error-retry-button"
|
||||
@@ -90,52 +90,47 @@
|
||||
>
|
||||
{{ t('launchpadErrors.generic.retryButton') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
data-testid="error-read-the-docs-button"
|
||||
@click="openDocs"
|
||||
>
|
||||
{{ t('launchpadErrors.generic.readTheDocsButton') }}
|
||||
</Button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { gql } from '@urql/vue'
|
||||
import Button from '@cy/components/Button.vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import type { BaseError_DataFragment } from '../generated/graphql'
|
||||
import type { BaseErrorFragment } from '../generated/graphql'
|
||||
import Alert from '@cy/components/Alert.vue'
|
||||
import OpenConfigFileInIDE from '@packages/frontend-shared/src/gql-components/OpenConfigFileInIDE.vue'
|
||||
import Collapsible from '@cy/components/Collapsible.vue'
|
||||
import { useMarkdown } from '@packages/frontend-shared/src/composables/useMarkdown'
|
||||
import RestartIcon from '~icons/cy/restart_x16.svg'
|
||||
import { useExternalLink } from '@packages/frontend-shared/src/gql-components/useExternalLink'
|
||||
import ErrorOutlineIcon from '~icons/cy/status-errored-outline_x16.svg'
|
||||
import ErrorCodeFrame from './ErrorCodeFrame.vue'
|
||||
|
||||
gql`
|
||||
fragment BaseError_Data on BaseError {
|
||||
fragment BaseError on ErrorWrapper {
|
||||
title
|
||||
message
|
||||
stack
|
||||
errorName
|
||||
errorStack
|
||||
errorType
|
||||
errorMessage
|
||||
isUserCodeError
|
||||
codeFrame {
|
||||
...ErrorCodeFrame
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const openDocs = useExternalLink('https://on.cypress.io/')
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
gql: BaseError_DataFragment
|
||||
gql: BaseErrorFragment
|
||||
retry?: () => void
|
||||
onReadDocs?: () => void
|
||||
}>()
|
||||
|
||||
const headerText = computed(() => t('launchpadErrors.generic.header'))
|
||||
const errorMessage = computed(() => props.gql.message ?? null)
|
||||
const stack = computed(() => props.gql.stack ?? null)
|
||||
|
||||
const markdownTarget = ref()
|
||||
const baseError = computed(() => props.gql)
|
||||
const { markdown } = useMarkdown(markdownTarget, computed(() => props.gql.errorMessage), { classes: { code: ['bg-error-200'] } })
|
||||
</script>
|
||||
|
||||
55
packages/launchpad/src/error/ErrorCodeFrame.vue
Normal file
55
packages/launchpad/src/error/ErrorCodeFrame.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<OpenFileInIDE
|
||||
v-slot="{onClick}"
|
||||
:file-path="props.gql.file.absolute"
|
||||
:line="props.gql.line ?? 0"
|
||||
:column="props.gql.column ?? 0"
|
||||
>
|
||||
<div
|
||||
class="border rounded cursor-pointer flex flex-row bg-gray-50 border-red-100 mt-16px text-indigo-500 text-14px leading-24px items-center"
|
||||
tab-index="1"
|
||||
data-testid="error-code-frame"
|
||||
@click="onClick"
|
||||
>
|
||||
<i-cy-document-text_x16 class="h-16px m-12px mr-8px w-16px icon-dark-indigo-500 icon-light-indigo-100" />
|
||||
<code>{{ fileText }}</code>
|
||||
</div>
|
||||
</OpenFileInIDE>
|
||||
<ShikiHighlight
|
||||
v-if="props.gql.codeBlock"
|
||||
:code="props.gql.codeBlock"
|
||||
lang="js"
|
||||
skip-trim
|
||||
codeframe
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { gql } from '@urql/vue'
|
||||
import { computed } from 'vue'
|
||||
import ShikiHighlight from '@packages/frontend-shared/src/components/ShikiHighlight.vue'
|
||||
import OpenFileInIDE from '@packages/frontend-shared/src/gql-components/OpenFileInIDE.vue'
|
||||
import type { ErrorCodeFrameFragment } from '../generated/graphql'
|
||||
|
||||
gql`
|
||||
fragment ErrorCodeFrame on CodeFrame {
|
||||
line
|
||||
column
|
||||
codeBlock
|
||||
file {
|
||||
id
|
||||
absolute
|
||||
relative
|
||||
}
|
||||
}`
|
||||
|
||||
const props = defineProps<{
|
||||
gql: ErrorCodeFrameFragment
|
||||
}>()
|
||||
|
||||
const fileText = computed(() => {
|
||||
const { file, line, column } = props.gql
|
||||
|
||||
return `${file.relative}${(line && column) ? `:${line}:${column}` : ''}`
|
||||
})
|
||||
</script>
|
||||
@@ -171,7 +171,7 @@ const { t } = useI18n()
|
||||
gql`
|
||||
fragment MigrationBaseError on Query {
|
||||
baseError {
|
||||
...BaseError_Data
|
||||
...BaseError
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -80,7 +80,8 @@ import MigrationTitle from './fragments/MigrationTitle.vue'
|
||||
import MigrationList from './fragments/MigrationList.vue'
|
||||
import MigrationListItem from './fragments/MigrationListItem.vue'
|
||||
import { gql, useMutation } from '@urql/vue'
|
||||
import { RenameSpecsManualFragment, RenameSpecsManual_CloseWatcherDocument } from '../generated/graphql'
|
||||
import { RenameSpecsManual_CloseWatcherDocument } from '../generated/graphql'
|
||||
import type { RenameSpecsManualFragment } from '../generated/graphql'
|
||||
|
||||
gql`
|
||||
fragment RenameSpecsManual on Migration {
|
||||
|
||||
@@ -4,19 +4,14 @@ import faker from 'faker'
|
||||
import { defaultMessages } from '@cy/i18n'
|
||||
|
||||
const warningSelector = '[data-cy=warning-alert]'
|
||||
const message = faker.hacker.phrase()
|
||||
const title = faker.hacker.ingverb()
|
||||
|
||||
const createWarning = (props = {}) => ({
|
||||
__typename: 'Warning' as const,
|
||||
title,
|
||||
message,
|
||||
details: null,
|
||||
...cy.gqlStub.ErrorWrapper,
|
||||
...props,
|
||||
})
|
||||
|
||||
const firstWarning = createWarning({ title: faker.hacker.ingverb(), message: faker.hacker.phrase(), setupStep: null })
|
||||
const secondWarning = createWarning({ title: faker.hacker.ingverb(), message: faker.hacker.phrase(), setupStep: null })
|
||||
const firstWarning = createWarning({ title: faker.hacker.ingverb(), errorMessage: faker.hacker.phrase() })
|
||||
const secondWarning = createWarning({ title: faker.hacker.ingverb(), errorMessage: faker.hacker.phrase() })
|
||||
|
||||
describe('<WarningList />', () => {
|
||||
it('does not render warning if there are none', () => {
|
||||
@@ -33,7 +28,10 @@ describe('<WarningList />', () => {
|
||||
it('renders multiple warnings', () => {
|
||||
cy.mountFragment(WarningListFragmentDoc, {
|
||||
onResult (result) {
|
||||
result.warnings = [firstWarning, secondWarning]
|
||||
result.warnings = [
|
||||
firstWarning,
|
||||
secondWarning,
|
||||
]
|
||||
},
|
||||
render: (gqlVal) => <div class="p-4"><WarningList gql={gqlVal} /></div>,
|
||||
})
|
||||
@@ -50,10 +48,10 @@ describe('<WarningList />', () => {
|
||||
})
|
||||
|
||||
cy.get(warningSelector).should('have.length', 2)
|
||||
cy.contains(firstWarning.message)
|
||||
cy.contains(firstWarning.errorMessage)
|
||||
|
||||
cy.findAllByLabelText(defaultMessages.components.modal.dismiss).first().click()
|
||||
cy.get(warningSelector).should('have.length', 1)
|
||||
cy.contains(firstWarning.message).should('not.exist')
|
||||
cy.contains(firstWarning.errorMessage).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user