From c15945e95d97b69722aaea10be00816d29b85bad Mon Sep 17 00:00:00 2001 From: julieg18 Date: Wed, 15 Nov 2023 11:44:27 -0600 Subject: [PATCH 1/5] Add first iteration and frontend tests --- extension/src/setup/index.ts | 7 ++++++- extension/src/setup/studio.ts | 19 +++++++++++++++++-- extension/src/setup/webview/contract.ts | 1 + webview/src/setup/components/App.test.tsx | 11 ++++++++++- webview/src/setup/components/App.tsx | 4 ++++ .../src/setup/components/studio/Connect.tsx | 2 +- .../src/setup/components/studio/Studio.tsx | 9 ++++++++- webview/src/setup/state/dvcSlice.ts | 1 + webview/src/setup/state/studioSlice.ts | 13 ++++++++++--- webview/src/stories/Setup.stories.tsx | 1 + 10 files changed, 59 insertions(+), 9 deletions(-) diff --git a/extension/src/setup/index.ts b/extension/src/setup/index.ts index 90def128cc..9c24d3beac 100644 --- a/extension/src/setup/index.ts +++ b/extension/src/setup/index.ts @@ -147,7 +147,11 @@ export class Setup this.setCommandsAvailability(false) this.setProjectAvailability() - this.studio = new Studio(internalCommands, () => this.getCwd()) + this.studio = new Studio( + internalCommands, + () => this.getCwd(), + () => this.sendDataToWebview() + ) this.onDidChangeStudioConnection = this.studio.onDidChangeStudioConnection this.webviewMessages = this.createWebviewMessageHandler() @@ -407,6 +411,7 @@ export class Setup isPythonExtensionInstalled: this.config.isPythonExtensionInstalled(), isPythonExtensionUsed, isStudioConnected: this.studio.getStudioIsConnected(), + isStudioConnecting: this.studio.getStudioIsConnecting(), needsGitCommit, needsGitInitialized, projectInitialized, diff --git a/extension/src/setup/studio.ts b/extension/src/setup/studio.ts index aba5d9b47d..365784ded0 100644 --- a/extension/src/setup/studio.ts +++ b/extension/src/setup/studio.ts @@ -22,19 +22,23 @@ export class Studio extends Disposable { this.dispose.track(new EventEmitter()) private readonly getCwd: () => string | undefined + private readonly update: () => Promise private readonly internalCommands: InternalCommands private studioAccessToken: string | undefined = undefined private studioIsConnected = false + private studioIsConnecting = false private shareLiveToStudio: boolean | undefined = undefined constructor( internalCommands: InternalCommands, - getCwd: () => string | undefined + getCwd: () => string | undefined, + update: () => Promise ) { super() this.internalCommands = internalCommands this.getCwd = getCwd + this.update = update this.onDidChangeStudioConnection = this.studioConnectionChanged.event } @@ -46,10 +50,19 @@ export class Studio extends Disposable { return this.studioIsConnected } + public getStudioIsConnecting() { + return this.studioIsConnecting + } + public getShareLiveToStudio() { return this.shareLiveToStudio } + public setIsStudioConnecting() { + this.studioIsConnecting = true + void this.update() + } + public async removeStudioAccessToken(dvcRoots: string[]) { if (dvcRoots.length !== 1) { const cwd = getFirstWorkspaceFolder() @@ -86,7 +99,8 @@ export class Studio extends Disposable { const storedToken = this.getStudioAccessToken() const isConnected = isStudioAccessToken(storedToken) this.studioIsConnected = isConnected - return setContextValue(ContextKey.STUDIO_CONNECTED, isConnected) + await setContextValue(ContextKey.STUDIO_CONNECTED, isConnected) + this.studioIsConnecting = false } public async updateStudioOffline(shareLive: boolean) { @@ -170,6 +184,7 @@ export class Studio extends Disposable { } private async requestStudioToken(deviceCode: string, tokenUri: string) { + this.setIsStudioConnecting() const token = await this.fetchStudioToken(deviceCode, tokenUri) const cwd = this.getCwd() diff --git a/extension/src/setup/webview/contract.ts b/extension/src/setup/webview/contract.ts index 5598b4f5ff..c9dd9c197d 100644 --- a/extension/src/setup/webview/contract.ts +++ b/extension/src/setup/webview/contract.ts @@ -16,6 +16,7 @@ export type SetupData = { isPythonExtensionInstalled: boolean isPythonExtensionUsed: boolean isStudioConnected: boolean + isStudioConnecting: boolean needsGitCommit: boolean needsGitInitialized: boolean | undefined projectInitialized: boolean diff --git a/webview/src/setup/components/App.test.tsx b/webview/src/setup/components/App.test.tsx index 77894b5d2d..601ab4d72e 100644 --- a/webview/src/setup/components/App.test.tsx +++ b/webview/src/setup/components/App.test.tsx @@ -35,6 +35,7 @@ const DEFAULT_DATA = { isPythonExtensionInstalled: false, isPythonExtensionUsed: false, isStudioConnected: false, + isStudioConnecting: false, needsGitCommit: false, needsGitInitialized: false, projectInitialized: true, @@ -631,7 +632,7 @@ describe('App', () => { }) describe('Studio not connected', () => { - it('should show buttons which request a token from Studio or add an already creatd one', () => { + it('should show buttons which request a token from Studio or add an already created one', () => { renderApp() mockPostMessage.mockClear() const getTokenButton = screen.getByText('Get Token') @@ -650,6 +651,14 @@ describe('App', () => { }) }) + it('should show a loading message if studio is connecting', () => { + renderApp({ isStudioConnecting: true }) + + const message = screen.getByText('Connecting to Studio...') + + expect(message).toBeInTheDocument() + }) + it('should show an error icon if dvc is not compatible', () => { renderApp({ cliCompatible: false diff --git a/webview/src/setup/components/App.tsx b/webview/src/setup/components/App.tsx index 23b5ddd134..c720666225 100644 --- a/webview/src/setup/components/App.tsx +++ b/webview/src/setup/components/App.tsx @@ -33,6 +33,7 @@ import { import { updateRemoteList } from '../state/remoteSlice' import { updateIsStudioConnected, + updateIsStudioConnecting, updateShareLiveToStudio } from '../state/studioSlice' import { setStudioShareExperimentsLive } from '../util/messages' @@ -101,6 +102,9 @@ export const feedStore = ( case 'isStudioConnected': dispatch(updateIsStudioConnected(data.data.isStudioConnected)) continue + case 'isStudioConnecting': + dispatch(updateIsStudioConnecting(data.data.isStudioConnecting)) + continue case 'needsGitCommit': dispatch(updateNeedsGitCommit(data.data.needsGitCommit)) continue diff --git a/webview/src/setup/components/studio/Connect.tsx b/webview/src/setup/components/studio/Connect.tsx index 0a6726905a..05d1d2ab77 100644 --- a/webview/src/setup/components/studio/Connect.tsx +++ b/webview/src/setup/components/studio/Connect.tsx @@ -7,7 +7,7 @@ import { Button } from '../../../shared/components/button/Button' export const Connect: React.FC = () => { return ( -
+

Connect to Studio

diff --git a/webview/src/setup/components/studio/Studio.tsx b/webview/src/setup/components/studio/Studio.tsx index 41bf477d00..40bca6d300 100644 --- a/webview/src/setup/components/studio/Studio.tsx +++ b/webview/src/setup/components/studio/Studio.tsx @@ -5,12 +5,19 @@ import { Connect } from './Connect' import { Settings } from './Settings' import { SetupState } from '../../store' import { CliIncompatible } from '../shared/CliIncompatible' +import { EmptyState } from '../../../shared/components/emptyState/EmptyState' export const Studio: React.FC<{ cliCompatible: boolean setShareLiveToStudio: (shareLiveToStudio: boolean) => void }> = ({ cliCompatible, setShareLiveToStudio }) => { - const { isStudioConnected } = useSelector((state: SetupState) => state.studio) + const { isStudioConnected, isStudioConnecting } = useSelector( + (state: SetupState) => state.studio + ) + + if (isStudioConnecting) { + return Connecting to Studio... + } if (!cliCompatible) { return ( diff --git a/webview/src/setup/state/dvcSlice.ts b/webview/src/setup/state/dvcSlice.ts index 3368e8604e..c2cc45c20f 100644 --- a/webview/src/setup/state/dvcSlice.ts +++ b/webview/src/setup/state/dvcSlice.ts @@ -5,6 +5,7 @@ export type DvcState = Omit< SetupData, | 'hasData' | 'isStudioConnected' + | 'isStudioConnecting' | 'needsGitCommit' | 'sectionCollapsed' | 'shareLiveToStudio' diff --git a/webview/src/setup/state/studioSlice.ts b/webview/src/setup/state/studioSlice.ts index f2ce1628ce..c60a35595e 100644 --- a/webview/src/setup/state/studioSlice.ts +++ b/webview/src/setup/state/studioSlice.ts @@ -3,11 +3,12 @@ import { SetupData } from 'dvc/src/setup/webview/contract' export type StudioState = Pick< SetupData, - 'shareLiveToStudio' | 'isStudioConnected' + 'shareLiveToStudio' | 'isStudioConnected' | 'isStudioConnecting' > export const studioInitialState: StudioState = { isStudioConnected: false, + isStudioConnecting: false, shareLiveToStudio: false } @@ -18,13 +19,19 @@ export const studioSlice = createSlice({ updateIsStudioConnected: (state, action: PayloadAction) => { state.isStudioConnected = action.payload }, + updateIsStudioConnecting: (state, action: PayloadAction) => { + state.isStudioConnecting = action.payload + }, updateShareLiveToStudio: (state, action: PayloadAction) => { state.shareLiveToStudio = action.payload } } }) -export const { updateIsStudioConnected, updateShareLiveToStudio } = - studioSlice.actions +export const { + updateIsStudioConnected, + updateIsStudioConnecting, + updateShareLiveToStudio +} = studioSlice.actions export default studioSlice.reducer diff --git a/webview/src/stories/Setup.stories.tsx b/webview/src/stories/Setup.stories.tsx index 9c10a44791..1fd079a3c9 100644 --- a/webview/src/stories/Setup.stories.tsx +++ b/webview/src/stories/Setup.stories.tsx @@ -21,6 +21,7 @@ const DEFAULT_DATA: SetupData = { isPythonExtensionInstalled: true, isPythonExtensionUsed: true, isStudioConnected: true, + isStudioConnecting: false, needsGitCommit: false, needsGitInitialized: false, projectInitialized: true, From ce6cf505b7c4bd4aaa1f90661c403c22d45add1f Mon Sep 17 00:00:00 2001 From: julieg18 Date: Thu, 16 Nov 2023 11:59:29 -0600 Subject: [PATCH 2/5] Fix broken tests and add n --- extension/src/setup/studio.ts | 19 +++++++----- extension/src/test/suite/setup/index.test.ts | 31 +++++++++++++------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/extension/src/setup/studio.ts b/extension/src/setup/studio.ts index 365784ded0..856ffceba0 100644 --- a/extension/src/setup/studio.ts +++ b/extension/src/setup/studio.ts @@ -58,11 +58,6 @@ export class Studio extends Disposable { return this.shareLiveToStudio } - public setIsStudioConnecting() { - this.studioIsConnecting = true - void this.update() - } - public async removeStudioAccessToken(dvcRoots: string[]) { if (dvcRoots.length !== 1) { const cwd = getFirstWorkspaceFolder() @@ -100,7 +95,7 @@ export class Studio extends Disposable { const isConnected = isStudioAccessToken(storedToken) this.studioIsConnected = isConnected await setContextValue(ContextKey.STUDIO_CONNECTED, isConnected) - this.studioIsConnecting = false + this.setIsStudioConnecting(this.studioIsConnecting) } public async updateStudioOffline(shareLive: boolean) { @@ -162,6 +157,15 @@ export class Studio extends Disposable { }) } + private setIsStudioConnecting(isConnecting: boolean) { + this.studioIsConnecting = isConnecting + } + + private updateIsStudioConnecting(isConnecting: boolean) { + this.setIsStudioConnecting(isConnecting) + void this.update() + } + private async fetchStudioToken(deviceCode: string, tokenUri: string) { const response = await this.fetchFromStudio(tokenUri, { code: deviceCode @@ -184,11 +188,12 @@ export class Studio extends Disposable { } private async requestStudioToken(deviceCode: string, tokenUri: string) { - this.setIsStudioConnecting() + this.updateIsStudioConnecting(true) const token = await this.fetchStudioToken(deviceCode, tokenUri) const cwd = this.getCwd() if (!token || !cwd) { + this.updateIsStudioConnecting(false) return } diff --git a/extension/src/test/suite/setup/index.test.ts b/extension/src/test/suite/setup/index.test.ts index aca811838c..1ae8058419 100644 --- a/extension/src/test/suite/setup/index.test.ts +++ b/extension/src/test/suite/setup/index.test.ts @@ -17,7 +17,8 @@ import { closeAllEditors, getMessageReceivedEmitter, quickPickInitialized, - selectQuickPickItem + selectQuickPickItem, + waitForSpyCall } from '../util' import { WEBVIEW_TEST_TIMEOUT } from '../timeouts' import { MessageFromWebviewType } from '../../../webview/contract' @@ -382,6 +383,7 @@ suite('Setup Test Suite', () => { isPythonExtensionInstalled: false, isPythonExtensionUsed: false, isStudioConnected: false, + isStudioConnecting: false, needsGitCommit: true, needsGitInitialized: true, projectInitialized: false, @@ -428,6 +430,7 @@ suite('Setup Test Suite', () => { isPythonExtensionInstalled: false, isPythonExtensionUsed: false, isStudioConnected: false, + isStudioConnecting: false, needsGitCommit: true, needsGitInitialized: true, projectInitialized: false, @@ -482,6 +485,7 @@ suite('Setup Test Suite', () => { isPythonExtensionInstalled: false, isPythonExtensionUsed: false, isStudioConnected: false, + isStudioConnecting: false, needsGitCommit: false, needsGitInitialized: false, projectInitialized: false, @@ -536,6 +540,7 @@ suite('Setup Test Suite', () => { isPythonExtensionInstalled: false, isPythonExtensionUsed: false, isStudioConnected: false, + isStudioConnecting: false, needsGitCommit: true, needsGitInitialized: false, projectInitialized: true, @@ -923,27 +928,27 @@ suite('Setup Test Suite', () => { `${mockStudioRes.verification_uri}?redirect_uri=${mockCallbackUrl}&code=${mockStudioRes.user_code}` ) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const mockGetCwd = stub(studio as any, 'getCwd') const mockModalShowError = stub(Modal, 'errorWithOptions') const mockSaveStudioToken = stub(studio, 'saveStudioAccessTokenInConfig') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const studioUpdateSpy = spy(studio as any, 'update') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const updateIsConnectingSpy = spy(studio as any, 'setIsStudioConnecting') mockFetch.onSecondCall().resolves({ json: () => Promise.resolve({ detail: 'Request failed for some reason.' }), status: 500 } as Fetch.Response) - const failedTokenEvent = new Promise(resolve => - mockGetCwd.onFirstCall().callsFake(() => { - resolve(undefined) - return dvcDemoPath - }) - ) + const failedTokenEvent = waitForSpyCall(studioUpdateSpy, 1) mockOnStudioResponse() await failedTokenEvent + expect(updateIsConnectingSpy).to.be.calledTwice + expect(studioUpdateSpy).to.be.calledTwice + expect(updateIsConnectingSpy.firstCall.firstArg).to.equal(true) expect(mockFetch).to.be.calledTwice expect(mockFetch).to.be.calledWithExactly(mockStudioRes.token_uri, { body: JSON.stringify({ @@ -958,6 +963,7 @@ suite('Setup Test Suite', () => { expect(mockModalShowError).to.be.calledWithExactly( 'Unable to get token. Failed with "Request failed for some reason."' ) + expect(updateIsConnectingSpy.secondCall.firstArg).to.equal(false) expect(mockSaveStudioToken).not.to.be.called const mockToken = 'isat_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' @@ -966,9 +972,9 @@ suite('Setup Test Suite', () => { status: 200 } as Fetch.Response) const tokenEvent = new Promise(resolve => - mockGetCwd.onSecondCall().callsFake(() => { + mockSaveStudioToken.onFirstCall().callsFake(() => { resolve(undefined) - return dvcDemoPath + return Promise.resolve(mockToken) }) ) @@ -976,6 +982,9 @@ suite('Setup Test Suite', () => { await tokenEvent + expect(studioUpdateSpy).to.be.calledThrice + expect(updateIsConnectingSpy).to.be.calledThrice + expect(updateIsConnectingSpy.thirdCall.firstArg).to.equal(true) expect(mockFetch).to.be.calledThrice expect(mockSaveStudioToken).to.be.calledOnce expect(mockSaveStudioToken).to.be.calledWithExactly( From fd57a8f556105b9597d02744c89057d77fdfeeef Mon Sep 17 00:00:00 2001 From: julieg18 Date: Thu, 16 Nov 2023 19:40:53 -0600 Subject: [PATCH 3/5] Use Toast instead --- extension/src/setup/index.ts | 7 +-- extension/src/setup/studio.ts | 46 ++++++++----------- extension/src/setup/webview/contract.ts | 1 - extension/src/test/suite/setup/index.test.ts | 33 +++++-------- extension/src/test/util/mocha/index.ts | 2 +- webview/src/setup/components/App.test.tsx | 9 ---- webview/src/setup/components/App.tsx | 4 -- .../src/setup/components/studio/Studio.tsx | 9 +--- webview/src/setup/state/dvcSlice.ts | 1 - webview/src/setup/state/studioSlice.ts | 13 ++---- webview/src/stories/Setup.stories.tsx | 1 - 11 files changed, 35 insertions(+), 91 deletions(-) diff --git a/extension/src/setup/index.ts b/extension/src/setup/index.ts index 9c24d3beac..90def128cc 100644 --- a/extension/src/setup/index.ts +++ b/extension/src/setup/index.ts @@ -147,11 +147,7 @@ export class Setup this.setCommandsAvailability(false) this.setProjectAvailability() - this.studio = new Studio( - internalCommands, - () => this.getCwd(), - () => this.sendDataToWebview() - ) + this.studio = new Studio(internalCommands, () => this.getCwd()) this.onDidChangeStudioConnection = this.studio.onDidChangeStudioConnection this.webviewMessages = this.createWebviewMessageHandler() @@ -411,7 +407,6 @@ export class Setup isPythonExtensionInstalled: this.config.isPythonExtensionInstalled(), isPythonExtensionUsed, isStudioConnected: this.studio.getStudioIsConnected(), - isStudioConnecting: this.studio.getStudioIsConnecting(), needsGitCommit, needsGitInitialized, projectInitialized, diff --git a/extension/src/setup/studio.ts b/extension/src/setup/studio.ts index 856ffceba0..990fb1c4f4 100644 --- a/extension/src/setup/studio.ts +++ b/extension/src/setup/studio.ts @@ -8,6 +8,7 @@ import { ContextKey, setContextValue } from '../vscode/context' import { Disposable } from '../class/dispose' import { getCallBackUrl, openUrl, waitForUriResponse } from '../vscode/external' import { Modal } from '../vscode/modal' +import { Toast } from '../vscode/toast' export const isStudioAccessToken = (text?: string): boolean => { if (!text) { @@ -22,23 +23,19 @@ export class Studio extends Disposable { this.dispose.track(new EventEmitter()) private readonly getCwd: () => string | undefined - private readonly update: () => Promise private readonly internalCommands: InternalCommands private studioAccessToken: string | undefined = undefined private studioIsConnected = false - private studioIsConnecting = false private shareLiveToStudio: boolean | undefined = undefined constructor( internalCommands: InternalCommands, - getCwd: () => string | undefined, - update: () => Promise + getCwd: () => string | undefined ) { super() this.internalCommands = internalCommands this.getCwd = getCwd - this.update = update this.onDidChangeStudioConnection = this.studioConnectionChanged.event } @@ -50,10 +47,6 @@ export class Studio extends Disposable { return this.studioIsConnected } - public getStudioIsConnecting() { - return this.studioIsConnecting - } - public getShareLiveToStudio() { return this.shareLiveToStudio } @@ -95,7 +88,6 @@ export class Studio extends Disposable { const isConnected = isStudioAccessToken(storedToken) this.studioIsConnected = isConnected await setContextValue(ContextKey.STUDIO_CONNECTED, isConnected) - this.setIsStudioConnecting(this.studioIsConnecting) } public async updateStudioOffline(shareLive: boolean) { @@ -157,15 +149,6 @@ export class Studio extends Disposable { }) } - private setIsStudioConnecting(isConnecting: boolean) { - this.studioIsConnecting = isConnecting - } - - private updateIsStudioConnecting(isConnecting: boolean) { - this.setIsStudioConnecting(isConnecting) - void this.update() - } - private async fetchStudioToken(deviceCode: string, tokenUri: string) { const response = await this.fetchFromStudio(tokenUri, { code: deviceCode @@ -187,17 +170,24 @@ export class Studio extends Disposable { return accessToken } - private async requestStudioToken(deviceCode: string, tokenUri: string) { - this.updateIsStudioConnecting(true) - const token = await this.fetchStudioToken(deviceCode, tokenUri) - const cwd = this.getCwd() + private requestStudioToken(deviceCode: string, tokenUri: string) { + return Toast.showProgress('Connecting to Studio', async progress => { + progress.report({ increment: 0 }) + progress.report({ increment: 25, message: 'Grabbing token...' }) + const token = await this.fetchStudioToken(deviceCode, tokenUri) + const cwd = this.getCwd() - if (!token || !cwd) { - this.updateIsStudioConnecting(false) - return - } + if (!token || !cwd) { + return Toast.reportProgressError('Connecting failed', progress) + } - return this.saveStudioAccessTokenInConfig(cwd, token) + await this.saveStudioAccessTokenInConfig(cwd, token) + progress.report({ + increment: 75, + message: 'Token added' + }) + return Toast.delayProgressClosing(15000) + }) } private async setStudioValues() { diff --git a/extension/src/setup/webview/contract.ts b/extension/src/setup/webview/contract.ts index c9dd9c197d..5598b4f5ff 100644 --- a/extension/src/setup/webview/contract.ts +++ b/extension/src/setup/webview/contract.ts @@ -16,7 +16,6 @@ export type SetupData = { isPythonExtensionInstalled: boolean isPythonExtensionUsed: boolean isStudioConnected: boolean - isStudioConnecting: boolean needsGitCommit: boolean needsGitInitialized: boolean | undefined projectInitialized: boolean diff --git a/extension/src/test/suite/setup/index.test.ts b/extension/src/test/suite/setup/index.test.ts index 1ae8058419..afbd9f7067 100644 --- a/extension/src/test/suite/setup/index.test.ts +++ b/extension/src/test/suite/setup/index.test.ts @@ -54,6 +54,7 @@ import { DvcConfig } from '../../../cli/dvc/config' import * as QuickPickUtil from '../../../setup/quickPick' import { EventName } from '../../../telemetry/constants' import { Modal } from '../../../vscode/modal' +import { Toast } from '../../../vscode/toast' suite('Setup Test Suite', () => { const disposable = Disposable.fn() @@ -383,7 +384,6 @@ suite('Setup Test Suite', () => { isPythonExtensionInstalled: false, isPythonExtensionUsed: false, isStudioConnected: false, - isStudioConnecting: false, needsGitCommit: true, needsGitInitialized: true, projectInitialized: false, @@ -430,7 +430,6 @@ suite('Setup Test Suite', () => { isPythonExtensionInstalled: false, isPythonExtensionUsed: false, isStudioConnected: false, - isStudioConnecting: false, needsGitCommit: true, needsGitInitialized: true, projectInitialized: false, @@ -485,7 +484,6 @@ suite('Setup Test Suite', () => { isPythonExtensionInstalled: false, isPythonExtensionUsed: false, isStudioConnected: false, - isStudioConnecting: false, needsGitCommit: false, needsGitInitialized: false, projectInitialized: false, @@ -540,7 +538,6 @@ suite('Setup Test Suite', () => { isPythonExtensionInstalled: false, isPythonExtensionUsed: false, isStudioConnected: false, - isStudioConnecting: false, needsGitCommit: true, needsGitInitialized: false, projectInitialized: true, @@ -930,25 +927,21 @@ suite('Setup Test Suite', () => { const mockModalShowError = stub(Modal, 'errorWithOptions') const mockSaveStudioToken = stub(studio, 'saveStudioAccessTokenInConfig') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const studioUpdateSpy = spy(studio as any, 'update') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const updateIsConnectingSpy = spy(studio as any, 'setIsStudioConnecting') + const showProgressSpy = spy(Toast, 'showProgress') + const reportProgressErrorSpy = spy(Toast, 'reportProgressError') + const delayProgressClosingSpy = spy(Toast, 'delayProgressClosing') mockFetch.onSecondCall().resolves({ json: () => Promise.resolve({ detail: 'Request failed for some reason.' }), status: 500 } as Fetch.Response) - const failedTokenEvent = waitForSpyCall(studioUpdateSpy, 1) + const failedTokenEvent = waitForSpyCall(delayProgressClosingSpy, 0) mockOnStudioResponse() await failedTokenEvent - expect(updateIsConnectingSpy).to.be.calledTwice - expect(studioUpdateSpy).to.be.calledTwice - expect(updateIsConnectingSpy.firstCall.firstArg).to.equal(true) expect(mockFetch).to.be.calledTwice expect(mockFetch).to.be.calledWithExactly(mockStudioRes.token_uri, { body: JSON.stringify({ @@ -963,7 +956,8 @@ suite('Setup Test Suite', () => { expect(mockModalShowError).to.be.calledWithExactly( 'Unable to get token. Failed with "Request failed for some reason."' ) - expect(updateIsConnectingSpy.secondCall.firstArg).to.equal(false) + expect(showProgressSpy).to.be.calledOnce + expect(reportProgressErrorSpy).to.be.calledOnce expect(mockSaveStudioToken).not.to.be.called const mockToken = 'isat_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' @@ -971,26 +965,21 @@ suite('Setup Test Suite', () => { json: () => Promise.resolve({ access_token: mockToken }), status: 200 } as Fetch.Response) - const tokenEvent = new Promise(resolve => - mockSaveStudioToken.onFirstCall().callsFake(() => { - resolve(undefined) - return Promise.resolve(mockToken) - }) - ) + const tokenEvent = waitForSpyCall(delayProgressClosingSpy, 1) mockOnStudioResponse() await tokenEvent - expect(studioUpdateSpy).to.be.calledThrice - expect(updateIsConnectingSpy).to.be.calledThrice - expect(updateIsConnectingSpy.thirdCall.firstArg).to.equal(true) expect(mockFetch).to.be.calledThrice expect(mockSaveStudioToken).to.be.calledOnce expect(mockSaveStudioToken).to.be.calledWithExactly( dvcDemoPath, mockToken ) + expect(showProgressSpy).to.be.calledTwice + expect(reportProgressErrorSpy).not.to.be.calledTwice + expect(delayProgressClosingSpy).to.be.calledTwice }).timeout(WEBVIEW_TEST_TIMEOUT) it("should handle a message from the webview to manually save the user's Studio access token", async () => { diff --git a/extension/src/test/util/mocha/index.ts b/extension/src/test/util/mocha/index.ts index 1b49a01bb4..ddb4627875 100644 --- a/extension/src/test/util/mocha/index.ts +++ b/extension/src/test/util/mocha/index.ts @@ -29,7 +29,7 @@ export const runMocha = async ( try { // eslint-disable-next-line sonarjs/cognitive-complexity await new Promise((resolve, reject) => { - glob(`**/**.test.${ext}`, { cwd }, (err, files) => { + glob(`**/setup/index.test.${ext}`, { cwd }, (err, files) => { if (err) { return reject(err) } diff --git a/webview/src/setup/components/App.test.tsx b/webview/src/setup/components/App.test.tsx index 601ab4d72e..8dbb56589a 100644 --- a/webview/src/setup/components/App.test.tsx +++ b/webview/src/setup/components/App.test.tsx @@ -35,7 +35,6 @@ const DEFAULT_DATA = { isPythonExtensionInstalled: false, isPythonExtensionUsed: false, isStudioConnected: false, - isStudioConnecting: false, needsGitCommit: false, needsGitInitialized: false, projectInitialized: true, @@ -651,14 +650,6 @@ describe('App', () => { }) }) - it('should show a loading message if studio is connecting', () => { - renderApp({ isStudioConnecting: true }) - - const message = screen.getByText('Connecting to Studio...') - - expect(message).toBeInTheDocument() - }) - it('should show an error icon if dvc is not compatible', () => { renderApp({ cliCompatible: false diff --git a/webview/src/setup/components/App.tsx b/webview/src/setup/components/App.tsx index c720666225..23b5ddd134 100644 --- a/webview/src/setup/components/App.tsx +++ b/webview/src/setup/components/App.tsx @@ -33,7 +33,6 @@ import { import { updateRemoteList } from '../state/remoteSlice' import { updateIsStudioConnected, - updateIsStudioConnecting, updateShareLiveToStudio } from '../state/studioSlice' import { setStudioShareExperimentsLive } from '../util/messages' @@ -102,9 +101,6 @@ export const feedStore = ( case 'isStudioConnected': dispatch(updateIsStudioConnected(data.data.isStudioConnected)) continue - case 'isStudioConnecting': - dispatch(updateIsStudioConnecting(data.data.isStudioConnecting)) - continue case 'needsGitCommit': dispatch(updateNeedsGitCommit(data.data.needsGitCommit)) continue diff --git a/webview/src/setup/components/studio/Studio.tsx b/webview/src/setup/components/studio/Studio.tsx index 40bca6d300..41bf477d00 100644 --- a/webview/src/setup/components/studio/Studio.tsx +++ b/webview/src/setup/components/studio/Studio.tsx @@ -5,19 +5,12 @@ import { Connect } from './Connect' import { Settings } from './Settings' import { SetupState } from '../../store' import { CliIncompatible } from '../shared/CliIncompatible' -import { EmptyState } from '../../../shared/components/emptyState/EmptyState' export const Studio: React.FC<{ cliCompatible: boolean setShareLiveToStudio: (shareLiveToStudio: boolean) => void }> = ({ cliCompatible, setShareLiveToStudio }) => { - const { isStudioConnected, isStudioConnecting } = useSelector( - (state: SetupState) => state.studio - ) - - if (isStudioConnecting) { - return Connecting to Studio... - } + const { isStudioConnected } = useSelector((state: SetupState) => state.studio) if (!cliCompatible) { return ( diff --git a/webview/src/setup/state/dvcSlice.ts b/webview/src/setup/state/dvcSlice.ts index c2cc45c20f..3368e8604e 100644 --- a/webview/src/setup/state/dvcSlice.ts +++ b/webview/src/setup/state/dvcSlice.ts @@ -5,7 +5,6 @@ export type DvcState = Omit< SetupData, | 'hasData' | 'isStudioConnected' - | 'isStudioConnecting' | 'needsGitCommit' | 'sectionCollapsed' | 'shareLiveToStudio' diff --git a/webview/src/setup/state/studioSlice.ts b/webview/src/setup/state/studioSlice.ts index c60a35595e..f2ce1628ce 100644 --- a/webview/src/setup/state/studioSlice.ts +++ b/webview/src/setup/state/studioSlice.ts @@ -3,12 +3,11 @@ import { SetupData } from 'dvc/src/setup/webview/contract' export type StudioState = Pick< SetupData, - 'shareLiveToStudio' | 'isStudioConnected' | 'isStudioConnecting' + 'shareLiveToStudio' | 'isStudioConnected' > export const studioInitialState: StudioState = { isStudioConnected: false, - isStudioConnecting: false, shareLiveToStudio: false } @@ -19,19 +18,13 @@ export const studioSlice = createSlice({ updateIsStudioConnected: (state, action: PayloadAction) => { state.isStudioConnected = action.payload }, - updateIsStudioConnecting: (state, action: PayloadAction) => { - state.isStudioConnecting = action.payload - }, updateShareLiveToStudio: (state, action: PayloadAction) => { state.shareLiveToStudio = action.payload } } }) -export const { - updateIsStudioConnected, - updateIsStudioConnecting, - updateShareLiveToStudio -} = studioSlice.actions +export const { updateIsStudioConnected, updateShareLiveToStudio } = + studioSlice.actions export default studioSlice.reducer diff --git a/webview/src/stories/Setup.stories.tsx b/webview/src/stories/Setup.stories.tsx index 1fd079a3c9..9c10a44791 100644 --- a/webview/src/stories/Setup.stories.tsx +++ b/webview/src/stories/Setup.stories.tsx @@ -21,7 +21,6 @@ const DEFAULT_DATA: SetupData = { isPythonExtensionInstalled: true, isPythonExtensionUsed: true, isStudioConnected: true, - isStudioConnecting: false, needsGitCommit: false, needsGitInitialized: false, projectInitialized: true, From 409e417f7a7c7b26353ca49a94cc7758d40025dd Mon Sep 17 00:00:00 2001 From: julieg18 Date: Fri, 17 Nov 2023 07:11:17 -0600 Subject: [PATCH 4/5] Fix mocha util typo --- extension/src/test/util/mocha/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src/test/util/mocha/index.ts b/extension/src/test/util/mocha/index.ts index ddb4627875..1b49a01bb4 100644 --- a/extension/src/test/util/mocha/index.ts +++ b/extension/src/test/util/mocha/index.ts @@ -29,7 +29,7 @@ export const runMocha = async ( try { // eslint-disable-next-line sonarjs/cognitive-complexity await new Promise((resolve, reject) => { - glob(`**/setup/index.test.${ext}`, { cwd }, (err, files) => { + glob(`**/**.test.${ext}`, { cwd }, (err, files) => { if (err) { return reject(err) } From 1324e20748d0d735b22bc5da81fa8d5457ea0d93 Mon Sep 17 00:00:00 2001 From: julieg18 Date: Fri, 17 Nov 2023 07:35:46 -0600 Subject: [PATCH 5/5] Improve error handling --- extension/src/setup/studio.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/extension/src/setup/studio.ts b/extension/src/setup/studio.ts index 990fb1c4f4..c6381eeb9d 100644 --- a/extension/src/setup/studio.ts +++ b/extension/src/setup/studio.ts @@ -173,19 +173,22 @@ export class Studio extends Disposable { private requestStudioToken(deviceCode: string, tokenUri: string) { return Toast.showProgress('Connecting to Studio', async progress => { progress.report({ increment: 0 }) - progress.report({ increment: 25, message: 'Grabbing token...' }) + progress.report({ increment: 25, message: 'Fetching token...' }) const token = await this.fetchStudioToken(deviceCode, tokenUri) const cwd = this.getCwd() if (!token || !cwd) { - return Toast.reportProgressError('Connecting failed', progress) + const error = new Error('Connection failed') + return Toast.reportProgressError(error, progress) } await this.saveStudioAccessTokenInConfig(cwd, token) + progress.report({ increment: 75, - message: 'Token added' + message: 'Token saved' }) + return Toast.delayProgressClosing(15000) }) }