Skip to content

Commit 74bae64

Browse files
authored
internal: (studio) display cert error (#32657)
1 parent 1804d35 commit 74bae64

File tree

16 files changed

+374
-172
lines changed

16 files changed

+374
-172
lines changed

packages/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"devDependencies": {
2525
"@cypress-design/icon-registry": "^1.5.1",
2626
"@cypress-design/vue-button": "^1.6.0",
27-
"@cypress-design/vue-icon": "^1.18.0",
27+
"@cypress-design/vue-icon": "^1.33.0",
2828
"@cypress-design/vue-spinner": "^1.0.0",
2929
"@cypress-design/vue-statusicon": "^1.0.0",
3030
"@cypress-design/vue-tabs": "^1.2.2",

packages/app/src/runner/SpecRunnerOpenMode.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
:on-studio-panel-close="handleStudioPanelClose"
108108
:event-manager="eventManager"
109109
:studio-status="studioStatus"
110+
:is-cert-error="isCertError"
110111
:aut-url-selector="autUrlSelector"
111112
:user-project-status-store="userProjectStatusStore"
112113
:has-requested-project-access="hasRequestedProjectAccess"
@@ -248,6 +249,7 @@ gql`
248249
subscription StudioStatus_Change {
249250
studioStatusChange {
250251
status
252+
isCertError
251253
canAccessStudioAI
252254
}
253255
}
@@ -304,12 +306,14 @@ const isSpecsListOpenPreferences = computed(() => {
304306
return props.gql.localSettings.preferences.isSpecsListOpen ?? false
305307
})
306308
307-
// Initialize with null and wait for subscription to update
309+
// Initialize and wait for subscription to update
308310
const studioStatus = ref<string | null>(null)
311+
const isCertError = ref<boolean | null>(null)
309312
310313
useSubscription({ query: StudioStatus_ChangeDocument }, (_, data) => {
311314
if (data?.studioStatusChange) {
312315
studioStatus.value = data.studioStatusChange.status
316+
isCertError.value = data.studioStatusChange.isCertError
313317
studioStore.setCanAccessStudioAI(data.studioStatusChange.canAccessStudioAI)
314318
}
315319

packages/app/src/studio/StudioErrorPanel.cy.tsx

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import StudioErrorPanel from './StudioErrorPanel.vue'
22
import type { EventManager } from '../runner/event-manager'
3+
import { IconCypressStudio } from '@cypress-design/vue-icon'
4+
import { h } from 'vue'
35

46
describe('<StudioErrorPanel />', () => {
57
it('renders error state with correct content', () => {
6-
const mockEventManager = {
7-
emit: cy.stub(),
8-
} as unknown as EventManager
8+
const mockEventManager = {} as unknown as EventManager
99

1010
cy.mount(
1111
<StudioErrorPanel
@@ -32,11 +32,59 @@ describe('<StudioErrorPanel />', () => {
3232
.should('contain', 'Retry')
3333
})
3434

35-
it('calls onRetry when retry button is clicked', () => {
35+
it('renders error state with custom content', () => {
3636
const mockEventManager = {
37-
emit: cy.stub(),
37+
ws: {
38+
emit: cy.stub().as('emit'),
39+
},
3840
} as unknown as EventManager
3941

42+
cy.mount(
43+
<StudioErrorPanel
44+
eventManager={mockEventManager}
45+
onRetry={() => {}}
46+
title="Custom title"
47+
message="Custom message"
48+
icon={() => {
49+
return h(IconCypressStudio, {
50+
size: '48',
51+
'fill-color': 'gray-500',
52+
})
53+
}}
54+
learnMoreUrl="https://on.cypress.io/custom-learn-more-url"
55+
/>,
56+
)
57+
58+
// Check that the error panel is displayed
59+
cy.findByTestId('studio-error-panel').should('be.visible')
60+
61+
// Check for the error icon
62+
cy.findByTestId('studio-error-panel')
63+
.find('svg')
64+
.should('be.visible')
65+
66+
// Check for the error title
67+
cy.findByTestId('studio-error-title').should('contain.text', 'Custom title')
68+
// Check for the error description
69+
cy.findByTestId('studio-error-message').should('contain.text', 'Custom message')
70+
71+
// Check for the learn more button
72+
cy.findByTestId('studio-error-learn-more-button')
73+
.should('be.visible')
74+
.should('contain', 'Learn more')
75+
.click()
76+
77+
cy.get('@emit').should('have.been.calledOnceWith', 'external:open', 'https://on.cypress.io/custom-learn-more-url')
78+
79+
// Check for the retry button
80+
cy.findByTestId('studio-error-retry-button')
81+
.should('be.visible')
82+
.should('contain', 'Retry')
83+
})
84+
85+
it('calls onRetry when retry button is clicked', () => {
86+
const mockEventManager = {} as unknown as EventManager
87+
4088
const onRetry = cy.stub().as('onRetry')
4189

4290
cy.mount(
@@ -52,9 +100,7 @@ describe('<StudioErrorPanel />', () => {
52100
})
53101

54102
it('shows Studio button in header', () => {
55-
const mockEventManager = {
56-
emit: cy.stub(),
57-
} as unknown as EventManager
103+
const mockEventManager = {} as unknown as EventManager
58104

59105
cy.mount(
60106
<StudioErrorPanel

packages/app/src/studio/StudioErrorPanel.vue

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,80 @@
44
data-cy="studio-error-panel"
55
container-class="text-center"
66
>
7-
<div class="relative">
8-
<IconTechnologyDashboardFail
9-
size="48"
10-
stroke-color="gray-500"
11-
fill-color="gray-900"
12-
secondary-fill-color="red-200"
13-
secondary-stroke-color="red-500"
14-
/>
7+
<div
8+
data-cy="studio-error-icon"
9+
class="relative"
10+
>
11+
<component :is="props.icon" />
1512
</div>
1613

1714
<div class="flex flex-col items-center gap-[4px] max-w-[448px]">
18-
<h2 class="text-white text-[16px] leading-[24px] font-medium">
19-
Something went wrong
15+
<h2
16+
data-cy="studio-error-title"
17+
class="text-white text-[16px] leading-[24px] font-medium"
18+
>
19+
{{ props.title }}
2020
</h2>
21-
<p class="text-gray-400 text-[16px] leading-[24px]">
22-
There was a problem with Cypress Studio. Our team has been notified.
23-
If the problem persists, please try again later.
21+
<p
22+
data-cy="studio-error-message"
23+
class="text-gray-400 text-[16px] leading-[24px]"
24+
>
25+
{{ props.message }}
2426
</p>
2527
</div>
2628

27-
<Button
28-
variant="outline-dark"
29-
size="32"
30-
data-cy="studio-error-retry-button"
31-
@click="onRetry"
32-
>
33-
<IconActionRefresh
34-
size="16"
35-
class="mr-2 pt-[1px]"
36-
stroke-color="gray-500"
37-
/>
38-
Retry
39-
</Button>
29+
<div class="flex gap-3">
30+
<Button
31+
v-if="props.learnMoreUrl"
32+
variant="outline-dark"
33+
size="32"
34+
data-cy="studio-error-learn-more-button"
35+
@click="() => props.eventManager?.ws?.emit('external:open', props.learnMoreUrl)"
36+
>
37+
Learn more
38+
</Button>
39+
<Button
40+
variant="outline-dark"
41+
size="32"
42+
data-cy="studio-error-retry-button"
43+
@click="props.onRetry"
44+
>
45+
<IconActionRefresh
46+
size="16"
47+
class="mr-2 pt-[1px]"
48+
stroke-color="gray-500"
49+
/>
50+
Retry
51+
</Button>
52+
</div>
4053
</StudioPanelContainer>
4154
</template>
4255

4356
<script lang="ts" setup>
57+
import { withDefaults, h } from 'vue'
4458
import Button from '@cypress-design/vue-button'
4559
import { IconTechnologyDashboardFail, IconActionRefresh } from '@cypress-design/vue-icon'
4660
import StudioPanelContainer from './StudioPanelContainer.vue'
4761
import type { EventManager } from '../runner/event-manager'
4862
49-
const props = defineProps<{
63+
const props = withDefaults(defineProps<{
5064
eventManager: EventManager
65+
title?: string
66+
message?: string
67+
icon?: any
68+
learnMoreUrl?: string
5169
onRetry: () => void
52-
}>()
70+
}>(), {
71+
title: 'Something went wrong',
72+
message: 'There was a problem with Cypress Studio. Our team has been notified. If the problem persists, please try again later.',
73+
icon: () => {
74+
return h(IconTechnologyDashboardFail, {
75+
size: '48',
76+
'stroke-color': 'gray-500',
77+
'fill-color': 'gray-900',
78+
'secondary-fill-color': 'red-200',
79+
'secondary-stroke-color': 'red-500',
80+
})
81+
},
82+
})
5383
</script>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { UserProjectStatusStore } from '@cy/store/user-project-status-store'
2+
import type { EventManager } from '../runner/event-manager'
3+
import type { UseMutationResponse } from '@urql/vue'
4+
import StudioPanel from './StudioPanel.vue'
5+
6+
describe('StudioPanel', () => {
7+
it('renders the error panel with a certificate error', () => {
8+
const mockEventManager = {} as unknown as EventManager
9+
const mockUserProjectStatusStore = {} as unknown as UserProjectStatusStore
10+
const mockRequestProjectAccessMutation = {} as unknown as UseMutationResponse<any, any>
11+
12+
cy.mount(<StudioPanel
13+
isCertError={true}
14+
eventManager={mockEventManager}
15+
studioStatus="IN_ERROR"
16+
onStudioPanelClose={() => {}}
17+
canAccessStudioAI={true}
18+
userProjectStatusStore={mockUserProjectStatusStore}
19+
hasRequestedProjectAccess={false}
20+
requestProjectAccessMutation={mockRequestProjectAccessMutation}
21+
autUrlSelector="https://example.com"
22+
/>)
23+
24+
cy.findByTestId('studio-error-panel').should('be.visible')
25+
cy.findByTestId('studio-error-icon').should('be.visible')
26+
cy.findByTestId('studio-error-title').should('have.text', 'Configure your proxy to use Cypress Studio')
27+
cy.findByTestId('studio-error-message').should('have.text', 'Cypress Studio requires an internet connection. To continue, you may need to configure Cypress with your proxy settings.')
28+
cy.findByTestId('studio-error-learn-more-button').should('have.text', ' Learn more ')
29+
cy.findByTestId('studio-error-retry-button').should('have.text', ' Retry ')
30+
31+
cy.percySnapshot()
32+
})
33+
})

packages/app/src/studio/StudioPanel.vue

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
<div v-else-if="props.studioStatus === 'IN_ERROR' || error">
1212
<StudioErrorPanel
1313
:event-manager="props.eventManager"
14-
:on-retry="handleRetry"
14+
:title="errorPanelProps.title"
15+
:message="errorPanelProps.message"
16+
:icon="errorPanelProps.icon"
17+
:learn-more-url="errorPanelProps.learnMoreUrl"
18+
:on-retry="errorPanelProps.onRetry"
1519
/>
1620
</div>
1721
<div
@@ -25,14 +29,15 @@
2529
</div>
2630
</template>
2731
<script lang="ts" setup>
28-
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
32+
import { ref, onMounted, onBeforeUnmount, watch, computed, h } from 'vue'
2933
import { init, loadRemote, registerRemotes } from '@module-federation/runtime'
3034
import type { StudioAppDefaultShape, StudioPanelShape } from './studio-app-types'
3135
import type { UserProjectStatusStore } from '@cy/store/user-project-status-store'
3236
import LoadingStudioPanel from './LoadingStudioPanel.vue'
3337
import StudioErrorPanel from './StudioErrorPanel.vue'
3438
import type { EventManager } from '../runner/event-manager'
3539
import { useMutation, gql, UseMutationResponse } from '@urql/vue'
40+
import { IconCypressStudio } from '@cypress-design/vue-icon'
3641
3742
// Mirrors the ReactDOM.Root type since incorporating those types
3843
// messes up vue typing elsewhere
@@ -52,6 +57,7 @@ const props = defineProps<{
5257
onStudioPanelClose: () => void
5358
eventManager: EventManager
5459
studioStatus: string | null
60+
isCertError?: boolean | null
5561
cloudStudioSessionId?: string
5662
autUrlSelector: string
5763
userProjectStatusStore: UserProjectStatusStore
@@ -68,6 +74,27 @@ const containerReactRootMap = new WeakMap<HTMLElement, Root>()
6874
6975
const retryStudioMutation = useMutation(retryStudioMutationGql)
7076
77+
const errorPanelProps = computed(() => {
78+
if (props.isCertError) {
79+
return {
80+
title: 'Configure your proxy to use Cypress Studio',
81+
message: 'Cypress Studio requires an internet connection. To continue, you may need to configure Cypress with your proxy settings.',
82+
icon: () => {
83+
return h(IconCypressStudio, {
84+
size: '48',
85+
'fill-color': 'gray-500',
86+
})
87+
},
88+
learnMoreUrl: 'https://on.cypress.io/proxy-configuration',
89+
onRetry: handleRetry,
90+
}
91+
}
92+
93+
return {
94+
onRetry: handleRetry,
95+
}
96+
})
97+
7198
const maybeRenderReactComponent = () => {
7299
// Skip rendering if studio is initializing or errored out
73100
if (props.studioStatus === 'INITIALIZING' || props.studioStatus === 'IN_ERROR') {

packages/data-context/graphql/schemaTypes/objectTypes/gql-Subscription.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export const StudioStatusPayload = objectType({
1111
type: StudioStatusTypeEnum,
1212
})
1313

14+
t.boolean('isCertError', {
15+
description: 'Whether the studio status is an IN_ERROR due to a certificate error',
16+
})
17+
1418
t.nonNull.boolean('canAccessStudioAI')
1519
},
1620
})
@@ -70,6 +74,7 @@ export const Subscription = subscriptionType({
7074
if (currentStatus === 'IN_ERROR') {
7175
return {
7276
status: 'IN_ERROR' as const,
77+
isCertError: ctx.coreData.studioLifecycleManager?.getIsCertError(),
7378
canAccessStudioAI: false,
7479
}
7580
}

packages/data-context/schemas/schema.graphql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2169,12 +2169,14 @@ enum SpecType {
21692169

21702170
type StudioStatusPayload {
21712171
canAccessStudioAI: Boolean!
2172+
2173+
"""Whether the studio status is an IN_ERROR due to a certificate error"""
2174+
isCertError: Boolean
21722175
status: StudioStatusType!
21732176
}
21742177

21752178
enum StudioStatusType {
21762179
ENABLED
2177-
INITIALIZED
21782180
INITIALIZING
21792181
IN_ERROR
21802182
NOT_INITIALIZED

packages/driver/cypress/e2e/commands/prompt/prompt-initialization-error.cy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ describe('src/cy/commands/prompt', () => {
4646
backendStub.withArgs('wait:for:prompt:ready').resolves({ success: false, error })
4747

4848
cy.on('fail', (err) => {
49-
expect(err.message).to.include('`cy.prompt` requires an internet connection to work. To continue, you may need to configure Cypress with your proxy settings.')
49+
expect(err.message).to.include('`cy.prompt` requires an internet connection. To continue, you may need to configure Cypress with your proxy settings.')
5050
done()
5151
})
5252

packages/driver/src/cypress/error_messages.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1356,7 +1356,7 @@ export default {
13561356
},
13571357
promptProxyError: {
13581358
message: stripIndent`\
1359-
\`cy.prompt\` requires an internet connection to work. To continue, you may need to configure Cypress with your proxy settings.
1359+
\`cy.prompt\` requires an internet connection. To continue, you may need to configure Cypress with your proxy settings.
13601360
`,
13611361
docsUrl: 'https://on.cypress.io/proxy-configuration',
13621362
},

0 commit comments

Comments
 (0)