Skip to content

Commit 2997e0e

Browse files
authored
feat(unify): Login error states (#20204)
1 parent cbc7cfc commit 2997e0e

File tree

27 files changed

+559
-126
lines changed

27 files changed

+559
-126
lines changed

packages/app/cypress/e2e/top-nav.cy.ts

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import type { AuthMessage } from '../../../data-context/src/actions/AuthActions'
1+
import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json'
2+
import type { AuthStateShape } from '@packages/data-context/src/data'
3+
4+
const loginText = defaultMessages.topNav.login
25

36
describe('App Top Nav Workflows', () => {
47
beforeEach(() => {
@@ -312,7 +315,7 @@ describe('App Top Nav Workflows', () => {
312315
})
313316
})
314317

315-
cy.intercept('mutation-Logout').as('logout')
318+
cy.intercept('mutation-Auth_Logout').as('logout')
316319

317320
cy.findByRole('button', { name: 'Log Out' }).click()
318321

@@ -338,7 +341,7 @@ describe('App Top Nav Workflows', () => {
338341
cy.withCtx((ctx, options) => {
339342
// @ts-ignore sinon is a global in the node process where this is executed
340343
sinon.stub(ctx._apis.authApi, 'logIn').callsFake(async (onMessage) => {
341-
onMessage({ browserOpened: true } as AuthMessage)
344+
onMessage({ browserOpened: true } as AuthStateShape)
342345

343346
return new Promise((resolve) => {
344347
setTimeout(() => {
@@ -415,6 +418,116 @@ describe('App Top Nav Workflows', () => {
415418
cy.get('@logInModal').should('not.exist')
416419
cy.findByTestId('app-header-bar').findByTestId('user-avatar-title').should('be.visible')
417420
})
421+
422+
it('shows correct error when browser cannot launch', () => {
423+
cy.withCtx((ctx) => {
424+
ctx.coreData.authState = {
425+
name: 'AUTH_COULD_NOT_LAUNCH_BROWSER',
426+
message: 'http://127.0.0.1:0000/redirect-to-auth',
427+
browserOpened: false,
428+
}
429+
})
430+
431+
cy.findByTestId('app-header-bar').within(() => {
432+
cy.findByTestId('user-avatar-title').should('not.exist')
433+
cy.findByRole('button', { name: 'Log In' }).click()
434+
})
435+
436+
cy.contains('http://127.0.0.1:0000/redirect-to-auth').should('be.visible')
437+
cy.contains(loginText.titleBrowserError).should('be.visible')
438+
cy.contains(loginText.bodyBrowserError).should('be.visible')
439+
cy.contains(loginText.bodyBrowserErrorDetails).should('be.visible')
440+
441+
// in this state, there is no retry UI, we ask the user to visit the auth url on their own
442+
cy.contains('button', loginText.actionTryAgain).should('not.exist')
443+
cy.contains('button', loginText.actionCancel).should('not.exist')
444+
})
445+
446+
it('shows correct error when error other than browser-launch happens', () => {
447+
cy.withCtx((ctx) => {
448+
ctx.coreData.authState = {
449+
name: 'AUTH_ERROR_DURING_LOGIN',
450+
message: 'An unexpected error occurred',
451+
browserOpened: false,
452+
}
453+
})
454+
455+
cy.findByTestId('app-header-bar').within(() => {
456+
cy.findByTestId('user-avatar-title').should('not.exist')
457+
cy.findByRole('button', { name: 'Log In' }).click()
458+
})
459+
460+
cy.contains(loginText.titleFailed).should('be.visible')
461+
cy.contains(loginText.bodyError).should('be.visible')
462+
cy.contains('An unexpected error occurred').should('be.visible')
463+
464+
cy.contains('button', loginText.actionTryAgain).should('be.visible').as('tryAgain')
465+
cy.contains('button', loginText.actionCancel).should('be.visible')
466+
467+
cy.percySnapshot()
468+
469+
cy.withCtx((ctx) => {
470+
ctx.coreData.authState = {
471+
name: 'AUTH_BROWSER_LAUNCHED',
472+
message: '',
473+
browserOpened: true,
474+
}
475+
})
476+
477+
cy.get('@tryAgain').click()
478+
cy.contains(loginText.titleInitial).should('be.visible')
479+
})
480+
481+
it('cancel button correctly clears error state', () => {
482+
cy.withCtx((ctx) => {
483+
ctx.coreData.authState = {
484+
name: 'AUTH_ERROR_DURING_LOGIN',
485+
message: 'An unexpected error occurred',
486+
browserOpened: false,
487+
}
488+
})
489+
490+
cy.findByTestId('app-header-bar').within(() => {
491+
cy.findByTestId('user-avatar-title').should('not.exist')
492+
cy.findByRole('button', { name: 'Log In' }).as('loginButton').click()
493+
})
494+
495+
cy.contains(loginText.titleFailed).should('be.visible')
496+
cy.contains(loginText.bodyError).should('be.visible')
497+
cy.contains('An unexpected error occurred').should('be.visible')
498+
499+
cy.percySnapshot()
500+
501+
cy.contains('button', loginText.actionTryAgain).should('be.visible')
502+
cy.contains('button', loginText.actionCancel).click()
503+
504+
cy.get('@loginButton').click()
505+
cy.contains(loginText.titleInitial).should('be.visible')
506+
})
507+
508+
it('closing modal correctly clears error state', () => {
509+
cy.withCtx((ctx) => {
510+
ctx.coreData.authState = {
511+
name: 'AUTH_ERROR_DURING_LOGIN',
512+
message: 'An unexpected error occurred',
513+
browserOpened: false,
514+
}
515+
})
516+
517+
cy.findByTestId('app-header-bar').within(() => {
518+
cy.findByTestId('user-avatar-title').should('not.exist')
519+
cy.findByRole('button', { name: 'Log In' }).as('loginButton').click()
520+
})
521+
522+
cy.contains(loginText.titleFailed).should('be.visible')
523+
cy.contains(loginText.bodyError).should('be.visible')
524+
cy.contains('An unexpected error occurred').should('be.visible')
525+
526+
cy.findByLabelText(defaultMessages.actions.close).click()
527+
528+
cy.get('@loginButton').click()
529+
cy.contains(loginText.titleInitial).should('be.visible')
530+
})
418531
})
419532
})
420533
})

packages/data-context/src/actions/AuthActions.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import type { DataContext } from '..'
2-
import type { AuthenticatedUserShape } from '../data'
3-
4-
export interface AuthMessage {type: string, browserOpened: boolean, name: string, message: string}
2+
import type { AuthenticatedUserShape, AuthStateShape } from '../data'
53

64
export interface AuthApiShape {
75
getUser(): Promise<Partial<AuthenticatedUserShape>>
8-
logIn(onMessage: (message: AuthMessage) => void): Promise<AuthenticatedUserShape>
6+
logIn(onMessage: (message: AuthStateShape) => void): Promise<AuthenticatedUserShape>
97
logOut(): Promise<void>
8+
resetAuthState(): Promise<void>
109
}
1110

1211
export class AuthActions {
@@ -45,14 +44,25 @@ export class AuthActions {
4544
}
4645

4746
async login () {
48-
this.setAuthenticatedUser(await this.authApi.logIn(({ browserOpened }) => {
49-
this.ctx.coreData.isAuthBrowserOpened = browserOpened
47+
this.setAuthenticatedUser(await this.authApi.logIn((authState) => {
48+
this.ctx.update((coreData) => {
49+
coreData.authState = authState
50+
})
5051
}))
5152
}
5253

54+
resetAuthState () {
55+
this.ctx.update((coreData) => {
56+
coreData.authState = { browserOpened: false }
57+
})
58+
}
59+
5360
async logout () {
5461
try {
55-
this.ctx.coreData.isAuthBrowserOpened = false
62+
this.ctx.update((coreData) => {
63+
coreData.authState.browserOpened = false
64+
})
65+
5666
await this.authApi.logOut()
5767
} catch (e) {
5868
this.ctx.logTraceError(e)

packages/data-context/src/data/coreDataShape.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BUNDLERS, FoundBrowser, Editor, Warning, AllowedState, AllModeOptions, TestingType, PACKAGE_MANAGERS, BrowserStatus } from '@packages/types'
1+
import { BUNDLERS, FoundBrowser, Editor, Warning, AllowedState, AllModeOptions, TestingType, PACKAGE_MANAGERS, BrowserStatus, AuthStateName } from '@packages/types'
22
import type { NexusGenEnums, NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen'
33
import type { App, BrowserWindow } from 'electron'
44
import type { ChildProcess } from 'child_process'
@@ -87,6 +87,12 @@ export interface BaseErrorDataShape {
8787
stack?: string
8888
}
8989

90+
export interface AuthStateShape {
91+
name?: AuthStateName
92+
message?: string
93+
browserOpened: boolean
94+
}
95+
9096
export interface ForceReconfigureProjectDataShape {
9197
e2e?: boolean | null
9298
component?: boolean | null
@@ -116,7 +122,7 @@ export interface CoreDataShape {
116122
migration: MigrationDataShape | null
117123
user: AuthenticatedUserShape | null
118124
electron: ElectronShape
119-
isAuthBrowserOpened: boolean
125+
authState: AuthStateShape
120126
scaffoldedFiles: NexusGenObjects['ScaffoldedFile'][] | null
121127
warnings: Warning[]
122128
packageManager: typeof PACKAGE_MANAGERS[number]
@@ -152,7 +158,9 @@ export function makeCoreData (modeOptions: Partial<AllModeOptions> = {}): CoreDa
152158
preferences: {},
153159
refreshing: null,
154160
},
155-
isAuthBrowserOpened: false,
161+
authState: {
162+
browserOpened: false,
163+
},
156164
currentProject: modeOptions.projectRoot ?? null,
157165
currentTestingType: modeOptions.testingType ?? null,
158166
wizard: {

packages/data-context/src/util/urqlCacheKeys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const urqlCacheKeys: Partial<UrqlCacheKeys> = {
3232
ScaffoldedFile: () => null,
3333
LocalSettings: (data) => data.__typename,
3434
LocalSettingsPreferences: () => null,
35+
AuthState: () => null,
3536
CloudProjectNotFound: (data) => data.__typename,
3637
CloudProjectUnauthorized: (data) => data.__typename,
3738
GeneratedSpecError: () => null,

packages/frontend-shared/cypress/support/mock-graphql/clientTestContext.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AuthenticatedUserShape } from '@packages/data-context/src/data'
1+
import type { AuthenticatedUserShape, AuthStateShape } from '@packages/data-context/src/data'
22
import type {
33
CurrentProject,
44
Browser,
@@ -22,7 +22,7 @@ export interface ClientTestContext {
2222
browsers: Browser[] | null
2323
}
2424
versions: VersionData
25-
isAuthBrowserOpened: boolean
25+
authState: AuthStateShape
2626
localSettings: LocalSettings
2727
wizard: {
2828
chosenBundler: WizardBundler | null
@@ -69,7 +69,9 @@ export function makeClientTestContext (): ClientTestContext {
6969
released: '2021-10-11T19:40:49.036Z',
7070
},
7171
},
72-
isAuthBrowserOpened: false,
72+
authState: {
73+
browserOpened: false,
74+
},
7375
wizard: {
7476
chosenBundler: null,
7577
chosenFramework: null,

packages/frontend-shared/cypress/support/mock-graphql/stubgql-Mutation.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,18 @@ export const stubMutation: MaybeResolver<Mutation> = {
3535
resetLatestVersionTelemetry () {
3636
return true
3737
},
38-
focusActiveBrowserWindow (sourc, args, ctx) {
38+
focusActiveBrowserWindow (source, args, ctx) {
3939
return true
4040
},
4141
hideBrowserWindow (source, args, ctx) {
4242
return true
4343
},
44+
45+
resetAuthState (source, args, ctx) {
46+
ctx.authState = { browserOpened: false }
47+
48+
return { }
49+
},
4450
setProjectPreferences (source, args, ctx) {
4551
return {}
4652
},

packages/frontend-shared/cypress/support/mock-graphql/stubgql-Query.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export const stubQuery: MaybeResolver<Query> = {
2525
versions (source, args, ctx) {
2626
return ctx.versions
2727
},
28-
isAuthBrowserOpened (source, args, ctx) {
29-
return ctx.isAuthBrowserOpened
28+
authState (source, args, ctx) {
29+
return ctx.authState
3030
},
3131
isInGlobalMode (source, args, ctx) {
3232
return !ctx.currentProject
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import CopyText from './CopyText.vue'
2+
import { defaultMessages } from '@cy/i18n'
3+
4+
describe('<CopyText />', () => {
5+
it('renders without overflow', { viewportWidth: 800, viewportHeight: 120 }, () => {
6+
cy.mount(() => (
7+
<div class="p-6">
8+
<CopyText text="https://www.test.test"/>
9+
</div>
10+
))
11+
12+
cy.contains('button', defaultMessages.clipboard.copy)
13+
.should('be.visible')
14+
.percySnapshot()
15+
})
16+
17+
it('overflows nicely', { viewportWidth: 800, viewportHeight: 120 }, () => {
18+
const text = 'yarn workspace @packages/frontend-shared cypress:run --record --key 123as4d56asda987das'
19+
20+
cy.mount(() => (
21+
<div class="p-6">
22+
<CopyText text={text}/>
23+
</div>
24+
))
25+
26+
cy.contains(text)
27+
cy.contains('button', defaultMessages.clipboard.copy)
28+
.should('be.visible')
29+
.percySnapshot()
30+
})
31+
})
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<template>
2+
<code class="border rounded flex font-light bg-gray-50 border-gray-100 px-16px text-gray-600 text-14px leading-40px relative items-center whitespace-nowrap overflow-hidden">
3+
<i-cy-globe_x16 class="flex-shrink-0 h-16px mr-8px w-16px icon-dark-gray-500 icon-light-gray-100" />
4+
{{ props.text }}
5+
<div class="font-sans opacity-gradient p-4px pl-32px top-0 right-0 bottom-0 absolute">
6+
<CopyButton
7+
class="bg-indigo-100"
8+
:text="text"
9+
/>
10+
</div>
11+
</code>
12+
</template>
13+
14+
<script lang="ts" setup>
15+
import CopyButton from './CopyButton.vue'
16+
17+
const props = defineProps<{
18+
text: string
19+
}>()
20+
</script>
21+
22+
<style lang="scss" scoped>
23+
.opacity-gradient {
24+
background: linear-gradient(to right, rgba(255,255,255,.3) 0%, rgba(243, 244, 250, 1) 25%, rgba(243, 244, 250,1) 100%);
25+
}
26+
</style>

0 commit comments

Comments
 (0)