diff --git a/extensions/ql-vscode/src/common/vscode/dialog.ts b/extensions/ql-vscode/src/common/vscode/dialog.ts new file mode 100644 index 000000000..b6ad555fe --- /dev/null +++ b/extensions/ql-vscode/src/common/vscode/dialog.ts @@ -0,0 +1,135 @@ +import { env, Uri, window } from "vscode"; + +/** + * Opens a modal dialog for the user to make a yes/no choice. + * + * @param message The message to show. + * @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can + * be closed even if the user does not make a choice. + * @param yesTitle The text in the box indicating the affirmative choice. + * @param noTitle The text in the box indicating the negative choice. + * + * @return + * `true` if the user clicks 'Yes', + * `false` if the user clicks 'No' or cancels the dialog, + * `undefined` if the dialog is closed without the user making a choice. + */ +export async function showBinaryChoiceDialog( + message: string, + modal = true, + yesTitle = "Yes", + noTitle = "No", +): Promise { + const yesItem = { title: yesTitle, isCloseAffordance: false }; + const noItem = { title: noTitle, isCloseAffordance: true }; + const chosenItem = await window.showInformationMessage( + message, + { modal }, + yesItem, + noItem, + ); + if (!chosenItem) { + return undefined; + } + return chosenItem?.title === yesItem.title; +} + +/** + * Opens a modal dialog for the user to make a yes/no choice. + * + * @param message The message to show. + * @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can + * be closed even if the user does not make a choice. + * + * @return + * `true` if the user clicks 'Yes', + * `false` if the user clicks 'No' or cancels the dialog, + * `undefined` if the dialog is closed without the user making a choice. + */ +export async function showBinaryChoiceWithUrlDialog( + message: string, + url: string, +): Promise { + const urlItem = { title: "More Information", isCloseAffordance: false }; + const yesItem = { title: "Yes", isCloseAffordance: false }; + const noItem = { title: "No", isCloseAffordance: true }; + let chosenItem; + + // Keep the dialog open as long as the user is clicking the 'more information' option. + // To prevent an infinite loop, if the user clicks 'more information' 5 times, close the dialog and return cancelled + let count = 0; + do { + chosenItem = await window.showInformationMessage( + message, + { modal: true }, + urlItem, + yesItem, + noItem, + ); + if (chosenItem === urlItem) { + await env.openExternal(Uri.parse(url, true)); + } + count++; + } while (chosenItem === urlItem && count < 5); + + if (!chosenItem || chosenItem.title === urlItem.title) { + return undefined; + } + return chosenItem.title === yesItem.title; +} + +/** + * Show an information message with a customisable action. + * @param message The message to show. + * @param actionMessage The call to action message. + * + * @return `true` if the user clicks the action, `false` if the user cancels the dialog. + */ +export async function showInformationMessageWithAction( + message: string, + actionMessage: string, +): Promise { + const actionItem = { title: actionMessage, isCloseAffordance: false }; + const chosenItem = await window.showInformationMessage(message, actionItem); + return chosenItem === actionItem; +} + +/** + * Opens a modal dialog for the user to make a choice between yes/no/never be asked again. + * + * @param message The message to show. + * @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can + * be closed even if the user does not make a choice. + * @param yesTitle The text in the box indicating the affirmative choice. + * @param noTitle The text in the box indicating the negative choice. + * @param neverTitle The text in the box indicating the opt out choice. + * + * @return + * `Yes` if the user clicks 'Yes', + * `No` if the user clicks 'No' or cancels the dialog, + * `No, and never ask me again` if the user clicks 'No, and never ask me again', + * `undefined` if the dialog is closed without the user making a choice. + */ +export async function showNeverAskAgainDialog( + message: string, + modal = true, + yesTitle = "Yes", + noTitle = "No", + neverAskAgainTitle = "No, and never ask me again", +): Promise { + const yesItem = { title: yesTitle, isCloseAffordance: true }; + const noItem = { title: noTitle, isCloseAffordance: false }; + const neverAskAgainItem = { + title: neverAskAgainTitle, + isCloseAffordance: false, + }; + const chosenItem = await window.showInformationMessage( + message, + { modal }, + yesItem, + noItem, + neverAskAgainItem, + ); + + return chosenItem?.title; +} diff --git a/extensions/ql-vscode/src/common/vscode/external-files.ts b/extensions/ql-vscode/src/common/vscode/external-files.ts index bf0d17164..220b8d20d 100644 --- a/extensions/ql-vscode/src/common/vscode/external-files.ts +++ b/extensions/ql-vscode/src/common/vscode/external-files.ts @@ -1,9 +1,7 @@ import { Uri, window } from "vscode"; import { AppCommandManager } from "../commands"; -import { - showAndLogExceptionWithTelemetry, - showBinaryChoiceDialog, -} from "../../helpers"; +import { showAndLogExceptionWithTelemetry } from "../../helpers"; +import { showBinaryChoiceDialog } from "./dialog"; import { redactableError } from "../../pure/errors"; import { asError, diff --git a/extensions/ql-vscode/src/databases/local-databases/database-manager.ts b/extensions/ql-vscode/src/databases/local-databases/database-manager.ts index 68ad46c89..687a54fba 100644 --- a/extensions/ql-vscode/src/databases/local-databases/database-manager.ts +++ b/extensions/ql-vscode/src/databases/local-databases/database-manager.ts @@ -13,10 +13,8 @@ import { import { join } from "path"; import { FullDatabaseOptions } from "./database-options"; import { DatabaseItemImpl } from "./database-item-impl"; -import { - showAndLogExceptionWithTelemetry, - showNeverAskAgainDialog, -} from "../../helpers"; +import { showAndLogExceptionWithTelemetry } from "../../helpers"; +import { showNeverAskAgainDialog } from "../../common/vscode/dialog"; import { getFirstWorkspaceFolder, isFolderAlreadyInWorkspace, diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index da84fe807..578db204c 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -60,12 +60,14 @@ import { showAndLogExceptionWithTelemetry, showAndLogInformationMessage, showAndLogWarningMessage, - showBinaryChoiceDialog, - showInformationMessageWithAction, tmpDir, tmpDirDisposal, prepareCodeTour, } from "./helpers"; +import { + showBinaryChoiceDialog, + showInformationMessageWithAction, +} from "./common/vscode/dialog"; import { asError, assertNever, diff --git a/extensions/ql-vscode/src/helpers.ts b/extensions/ql-vscode/src/helpers.ts index 2c532caf7..64bb123d5 100644 --- a/extensions/ql-vscode/src/helpers.ts +++ b/extensions/ql-vscode/src/helpers.ts @@ -1,7 +1,7 @@ import { ensureDirSync, pathExists, ensureDir, writeFile } from "fs-extra"; import { join } from "path"; import { dirSync } from "tmp-promise"; -import { Uri, window as Window, workspace, env } from "vscode"; +import { Uri, window as Window, workspace } from "vscode"; import { CodeQLCliServer } from "./codeql-cli/cli"; import { UserCancellationException } from "./common/vscode/progress"; import { extLogger, OutputChannelLogger } from "./common"; @@ -12,6 +12,7 @@ import { isQueryLanguage, QueryLanguage } from "./common/query-language"; import { isCodespacesTemplate } from "./config"; import { AppCommandManager } from "./common/commands"; import { getOnDiskWorkspaceFolders } from "./common/vscode/workspace-folders"; +import { showBinaryChoiceDialog } from "./common/vscode/dialog"; // Shared temporary folder for the extension. export const tmpDir = dirSync({ @@ -139,140 +140,6 @@ async function internalShowAndLog( return result; } -/** - * Opens a modal dialog for the user to make a yes/no choice. - * - * @param message The message to show. - * @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can - * be closed even if the user does not make a choice. - * @param yesTitle The text in the box indicating the affirmative choice. - * @param noTitle The text in the box indicating the negative choice. - * - * @return - * `true` if the user clicks 'Yes', - * `false` if the user clicks 'No' or cancels the dialog, - * `undefined` if the dialog is closed without the user making a choice. - */ -export async function showBinaryChoiceDialog( - message: string, - modal = true, - yesTitle = "Yes", - noTitle = "No", -): Promise { - const yesItem = { title: yesTitle, isCloseAffordance: false }; - const noItem = { title: noTitle, isCloseAffordance: true }; - const chosenItem = await Window.showInformationMessage( - message, - { modal }, - yesItem, - noItem, - ); - if (!chosenItem) { - return undefined; - } - return chosenItem?.title === yesItem.title; -} - -/** - * Opens a modal dialog for the user to make a yes/no choice. - * - * @param message The message to show. - * @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can - * be closed even if the user does not make a choice. - * - * @return - * `true` if the user clicks 'Yes', - * `false` if the user clicks 'No' or cancels the dialog, - * `undefined` if the dialog is closed without the user making a choice. - */ -export async function showBinaryChoiceWithUrlDialog( - message: string, - url: string, -): Promise { - const urlItem = { title: "More Information", isCloseAffordance: false }; - const yesItem = { title: "Yes", isCloseAffordance: false }; - const noItem = { title: "No", isCloseAffordance: true }; - let chosenItem; - - // Keep the dialog open as long as the user is clicking the 'more information' option. - // To prevent an infinite loop, if the user clicks 'more information' 5 times, close the dialog and return cancelled - let count = 0; - do { - chosenItem = await Window.showInformationMessage( - message, - { modal: true }, - urlItem, - yesItem, - noItem, - ); - if (chosenItem === urlItem) { - await env.openExternal(Uri.parse(url, true)); - } - count++; - } while (chosenItem === urlItem && count < 5); - - if (!chosenItem || chosenItem.title === urlItem.title) { - return undefined; - } - return chosenItem.title === yesItem.title; -} - -/** - * Show an information message with a customisable action. - * @param message The message to show. - * @param actionMessage The call to action message. - * - * @return `true` if the user clicks the action, `false` if the user cancels the dialog. - */ -export async function showInformationMessageWithAction( - message: string, - actionMessage: string, -): Promise { - const actionItem = { title: actionMessage, isCloseAffordance: false }; - const chosenItem = await Window.showInformationMessage(message, actionItem); - return chosenItem === actionItem; -} - -/** - * Opens a modal dialog for the user to make a choice between yes/no/never be asked again. - * - * @param message The message to show. - * @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can - * be closed even if the user does not make a choice. - * @param yesTitle The text in the box indicating the affirmative choice. - * @param noTitle The text in the box indicating the negative choice. - * @param neverTitle The text in the box indicating the opt out choice. - * - * @return - * `Yes` if the user clicks 'Yes', - * `No` if the user clicks 'No' or cancels the dialog, - * `No, and never ask me again` if the user clicks 'No, and never ask me again', - * `undefined` if the dialog is closed without the user making a choice. - */ -export async function showNeverAskAgainDialog( - message: string, - modal = true, - yesTitle = "Yes", - noTitle = "No", - neverAskAgainTitle = "No, and never ask me again", -): Promise { - const yesItem = { title: yesTitle, isCloseAffordance: true }; - const noItem = { title: noTitle, isCloseAffordance: false }; - const neverAskAgainItem = { - title: neverAskAgainTitle, - isCloseAffordance: false, - }; - const chosenItem = await Window.showInformationMessage( - message, - { modal }, - yesItem, - noItem, - neverAskAgainItem, - ); - - return chosenItem?.title; -} - /** Check if the current workspace is the CodeTour and open the workspace folder. * Without this, we can't run the code tour correctly. **/ diff --git a/extensions/ql-vscode/src/local-queries/local-queries.ts b/extensions/ql-vscode/src/local-queries/local-queries.ts index fcde4b9a9..bd629ff28 100644 --- a/extensions/ql-vscode/src/local-queries/local-queries.ts +++ b/extensions/ql-vscode/src/local-queries/local-queries.ts @@ -21,8 +21,8 @@ import { findLanguage, showAndLogErrorMessage, showAndLogWarningMessage, - showBinaryChoiceDialog, } from "../helpers"; +import { showBinaryChoiceDialog } from "../common/vscode/dialog"; import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders"; import { displayQuickQuery } from "./quick-query"; import { CoreCompletedQuery, QueryRunner } from "../query-server"; diff --git a/extensions/ql-vscode/src/local-queries/quick-query.ts b/extensions/ql-vscode/src/local-queries/quick-query.ts index bf5268540..041a3346e 100644 --- a/extensions/ql-vscode/src/local-queries/quick-query.ts +++ b/extensions/ql-vscode/src/local-queries/quick-query.ts @@ -5,7 +5,7 @@ import { CancellationToken, window as Window, workspace, Uri } from "vscode"; import { LSPErrorCodes, ResponseError } from "vscode-languageclient"; import { CodeQLCliServer } from "../codeql-cli/cli"; import { DatabaseUI } from "../databases/local-databases-ui"; -import { showBinaryChoiceDialog } from "../helpers"; +import { showBinaryChoiceDialog } from "../common/vscode/dialog"; import { getInitialQueryContents } from "./query-contents"; import { getPrimaryDbscheme, getQlPackForDbscheme } from "../databases/qlpack"; import { diff --git a/extensions/ql-vscode/src/query-history/query-history-manager.ts b/extensions/ql-vscode/src/query-history/query-history-manager.ts index 19c270778..e727a8e2a 100644 --- a/extensions/ql-vscode/src/query-history/query-history-manager.ts +++ b/extensions/ql-vscode/src/query-history/query-history-manager.ts @@ -17,9 +17,11 @@ import { showAndLogErrorMessage, showAndLogInformationMessage, showAndLogWarningMessage, +} from "../helpers"; +import { showBinaryChoiceDialog, showInformationMessageWithAction, -} from "../helpers"; +} from "../common/vscode/dialog"; import { extLogger } from "../common"; import { URLSearchParams } from "url"; import { DisposableObject } from "../pure/disposable-object"; diff --git a/extensions/ql-vscode/src/telemetry.ts b/extensions/ql-vscode/src/telemetry.ts index 597b5e740..35c219c7f 100644 --- a/extensions/ql-vscode/src/telemetry.ts +++ b/extensions/ql-vscode/src/telemetry.ts @@ -17,7 +17,7 @@ import { import * as appInsights from "applicationinsights"; import { extLogger } from "./common"; import { UserCancellationException } from "./common/vscode/progress"; -import { showBinaryChoiceWithUrlDialog } from "./helpers"; +import { showBinaryChoiceWithUrlDialog } from "./common/vscode/dialog"; import { RedactableError } from "./pure/errors"; import { SemVer } from "semver"; diff --git a/extensions/ql-vscode/src/variant-analysis/export-results.ts b/extensions/ql-vscode/src/variant-analysis/export-results.ts index 167ccac88..f1ff22809 100644 --- a/extensions/ql-vscode/src/variant-analysis/export-results.ts +++ b/extensions/ql-vscode/src/variant-analysis/export-results.ts @@ -7,7 +7,7 @@ import { UserCancellationException, withProgress, } from "../common/vscode/progress"; -import { showInformationMessageWithAction } from "../helpers"; +import { showInformationMessageWithAction } from "../common/vscode/dialog"; import { extLogger } from "../common"; import { createGist } from "./gh-api/gh-api-client"; import { diff --git a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/local-databases.test.ts b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/local-databases.test.ts index aa3cafbb7..a28197ac2 100644 --- a/extensions/ql-vscode/test/vscode-tests/minimal-workspace/local-databases.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/minimal-workspace/local-databases.test.ts @@ -20,7 +20,7 @@ import { } from "../../../src/common/vscode/archive-filesystem-provider"; import { testDisposeHandler } from "../test-dispose-handler"; import { QueryRunner } from "../../../src/query-server/query-runner"; -import * as helpers from "../../../src/helpers"; +import * as dialog from "../../../src/common/vscode/dialog"; import { Setting } from "../../../src/config"; import { QlPackGenerator } from "../../../src/qlpack-generator"; import { mockedObject } from "../utils/mocking.helpers"; @@ -45,7 +45,7 @@ describe("local databases", () => { let logSpy: jest.Mock; let showNeverAskAgainDialogSpy: jest.SpiedFunction< - typeof helpers.showNeverAskAgainDialog + typeof dialog.showNeverAskAgainDialog >; let dir: tmp.DirResult; @@ -64,7 +64,7 @@ describe("local databases", () => { }); showNeverAskAgainDialogSpy = jest - .spyOn(helpers, "showNeverAskAgainDialog") + .spyOn(dialog, "showNeverAskAgainDialog") .mockResolvedValue("Yes"); extensionContextStoragePath = dir.name; @@ -652,7 +652,7 @@ describe("local databases", () => { it("should return early if the user refuses help", async () => { showNeverAskAgainDialogSpy = jest - .spyOn(helpers, "showNeverAskAgainDialog") + .spyOn(dialog, "showNeverAskAgainDialog") .mockResolvedValue("No"); await (databaseManager as any).createSkeletonPacks(mockDbItem); @@ -662,7 +662,7 @@ describe("local databases", () => { it("should return early and write choice to settings if user wants to never be asked again", async () => { showNeverAskAgainDialogSpy = jest - .spyOn(helpers, "showNeverAskAgainDialog") + .spyOn(dialog, "showNeverAskAgainDialog") .mockResolvedValue("No, and never ask me again"); const updateValueSpy = jest.spyOn(Setting.prototype, "updateValue"); @@ -705,7 +705,7 @@ describe("local databases", () => { it("should exit early", async () => { showNeverAskAgainDialogSpy = jest - .spyOn(helpers, "showNeverAskAgainDialog") + .spyOn(dialog, "showNeverAskAgainDialog") .mockResolvedValue("No"); await (databaseManager as any).createSkeletonPacks(mockDbItem); diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/common/vscode/dialog.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/common/vscode/dialog.test.ts new file mode 100644 index 000000000..895928b63 --- /dev/null +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/common/vscode/dialog.test.ts @@ -0,0 +1,167 @@ +import { window } from "vscode"; +import { + showBinaryChoiceDialog, + showBinaryChoiceWithUrlDialog, + showInformationMessageWithAction, + showNeverAskAgainDialog, +} from "../../../../../src/common/vscode/dialog"; + +describe("showBinaryChoiceDialog", () => { + let showInformationMessageSpy: jest.SpiedFunction< + typeof window.showInformationMessage + >; + + beforeEach(() => { + showInformationMessageSpy = jest + .spyOn(window, "showInformationMessage") + .mockResolvedValue(undefined); + }); + + const resolveArg = + (index: number) => + (...args: any[]) => + Promise.resolve(args[index]); + + it("should show a binary choice dialog and return `yes`", async () => { + // pretend user chooses 'yes' + showInformationMessageSpy.mockImplementationOnce(resolveArg(2)); + const val = await showBinaryChoiceDialog("xxx"); + expect(val).toBe(true); + }); + + it("should show a binary choice dialog and return `no`", async () => { + // pretend user chooses 'no' + showInformationMessageSpy.mockImplementationOnce(resolveArg(3)); + const val = await showBinaryChoiceDialog("xxx"); + expect(val).toBe(false); + }); +}); + +describe("showInformationMessageWithAction", () => { + let showInformationMessageSpy: jest.SpiedFunction< + typeof window.showInformationMessage + >; + + beforeEach(() => { + showInformationMessageSpy = jest + .spyOn(window, "showInformationMessage") + .mockResolvedValue(undefined); + }); + + const resolveArg = + (index: number) => + (...args: any[]) => + Promise.resolve(args[index]); + + it("should show an info dialog and confirm the action", async () => { + // pretend user chooses to run action + showInformationMessageSpy.mockImplementationOnce(resolveArg(1)); + const val = await showInformationMessageWithAction("xxx", "yyy"); + expect(val).toBe(true); + }); + + it("should show an action dialog and avoid choosing the action", async () => { + // pretend user does not choose to run action + showInformationMessageSpy.mockResolvedValueOnce(undefined); + const val = await showInformationMessageWithAction("xxx", "yyy"); + expect(val).toBe(false); + }); +}); + +describe("showBinaryChoiceWithUrlDialog", () => { + let showInformationMessageSpy: jest.SpiedFunction< + typeof window.showInformationMessage + >; + + beforeEach(() => { + showInformationMessageSpy = jest + .spyOn(window, "showInformationMessage") + .mockResolvedValue(undefined); + }); + + const resolveArg = + (index: number) => + (...args: any[]) => + Promise.resolve(args[index]); + + it("should show a binary choice dialog with a url and return `yes`", async () => { + // pretend user clicks on the url twice and then clicks 'yes' + showInformationMessageSpy + .mockImplementation(resolveArg(2)) + .mockImplementation(resolveArg(2)) + .mockImplementation(resolveArg(3)); + const val = await showBinaryChoiceWithUrlDialog("xxx", "invalid:url"); + expect(val).toBe(true); + }); + + it("should show a binary choice dialog with a url and return `no`", async () => { + // pretend user clicks on the url twice and then clicks 'no' + showInformationMessageSpy + .mockImplementation(resolveArg(2)) + .mockImplementation(resolveArg(2)) + .mockImplementation(resolveArg(4)); + const val = await showBinaryChoiceWithUrlDialog("xxx", "invalid:url"); + expect(val).toBe(false); + }); + + it("should show a binary choice dialog and exit after clcking `more info` 5 times", async () => { + // pretend user clicks on the url twice and then clicks 'no' + showInformationMessageSpy + .mockImplementation(resolveArg(2)) + .mockImplementation(resolveArg(2)) + .mockImplementation(resolveArg(2)) + .mockImplementation(resolveArg(2)) + .mockImplementation(resolveArg(2)); + const val = await showBinaryChoiceWithUrlDialog("xxx", "invalid:url"); + // No choice was made + expect(val).toBeUndefined(); + expect(showInformationMessageSpy).toHaveBeenCalledTimes(5); + }); +}); + +describe("showNeverAskAgainDialog", () => { + let showInformationMessageSpy: jest.SpiedFunction< + typeof window.showInformationMessage + >; + + beforeEach(() => { + showInformationMessageSpy = jest + .spyOn(window, "showInformationMessage") + .mockResolvedValue(undefined); + }); + + const resolveArg = + (index: number) => + (...args: any[]) => + Promise.resolve(args[index]); + + const title = + "We've noticed you don't have a CodeQL pack available to analyze this database. Can we set up a query pack for you?"; + + it("should show a ternary choice dialog and return `Yes`", async () => { + // pretend user chooses 'Yes' + const yesItem = resolveArg(2); + showInformationMessageSpy.mockImplementationOnce(yesItem); + + const answer = await showNeverAskAgainDialog(title); + expect(answer).toBe("Yes"); + }); + + it("should show a ternary choice dialog and return `No`", async () => { + // pretend user chooses 'No' + const noItem = resolveArg(3); + showInformationMessageSpy.mockImplementationOnce(noItem); + + const answer = await showNeverAskAgainDialog(title); + expect(answer).toBe("No"); + }); + + it("should show a ternary choice dialog and return `No, and never ask me again`", async () => { + // pretend user chooses 'No, and never ask me again' + const neverAskAgainItem = resolveArg(4); + showInformationMessageSpy.mockImplementationOnce(neverAskAgainItem); + + const answer = await showNeverAskAgainDialog(title); + expect(answer).toBe("No, and never ask me again"); + }); +}); diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/helpers.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/helpers.test.ts index bf2661fea..504f15427 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/helpers.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/helpers.test.ts @@ -3,13 +3,7 @@ import * as tmp from "tmp"; import { join } from "path"; import { writeFile, mkdir } from "fs-extra"; -import { - prepareCodeTour, - showBinaryChoiceDialog, - showBinaryChoiceWithUrlDialog, - showInformationMessageWithAction, - showNeverAskAgainDialog, -} from "../../../src/helpers"; +import { prepareCodeTour } from "../../../src/helpers"; import { reportStreamProgress } from "../../../src/common/vscode/progress"; import { Setting } from "../../../src/config"; import { createMockCommandManager } from "../../__mocks__/commandsMock"; @@ -71,166 +65,6 @@ describe("helpers", () => { message: "My prefix (Size unknown)", }); }); - - describe("showBinaryChoiceDialog", () => { - let showInformationMessageSpy: jest.SpiedFunction< - typeof window.showInformationMessage - >; - - beforeEach(() => { - showInformationMessageSpy = jest - .spyOn(window, "showInformationMessage") - .mockResolvedValue(undefined); - }); - - const resolveArg = - (index: number) => - (...args: any[]) => - Promise.resolve(args[index]); - - it("should show a binary choice dialog and return `yes`", async () => { - // pretend user chooses 'yes' - showInformationMessageSpy.mockImplementationOnce(resolveArg(2)); - const val = await showBinaryChoiceDialog("xxx"); - expect(val).toBe(true); - }); - - it("should show a binary choice dialog and return `no`", async () => { - // pretend user chooses 'no' - showInformationMessageSpy.mockImplementationOnce(resolveArg(3)); - const val = await showBinaryChoiceDialog("xxx"); - expect(val).toBe(false); - }); - }); - - describe("showInformationMessageWithAction", () => { - let showInformationMessageSpy: jest.SpiedFunction< - typeof window.showInformationMessage - >; - - beforeEach(() => { - showInformationMessageSpy = jest - .spyOn(window, "showInformationMessage") - .mockResolvedValue(undefined); - }); - - const resolveArg = - (index: number) => - (...args: any[]) => - Promise.resolve(args[index]); - - it("should show an info dialog and confirm the action", async () => { - // pretend user chooses to run action - showInformationMessageSpy.mockImplementationOnce(resolveArg(1)); - const val = await showInformationMessageWithAction("xxx", "yyy"); - expect(val).toBe(true); - }); - - it("should show an action dialog and avoid choosing the action", async () => { - // pretend user does not choose to run action - showInformationMessageSpy.mockResolvedValueOnce(undefined); - const val = await showInformationMessageWithAction("xxx", "yyy"); - expect(val).toBe(false); - }); - }); - - describe("showBinaryChoiceWithUrlDialog", () => { - let showInformationMessageSpy: jest.SpiedFunction< - typeof window.showInformationMessage - >; - - beforeEach(() => { - showInformationMessageSpy = jest - .spyOn(window, "showInformationMessage") - .mockResolvedValue(undefined); - }); - - const resolveArg = - (index: number) => - (...args: any[]) => - Promise.resolve(args[index]); - - it("should show a binary choice dialog with a url and return `yes`", async () => { - // pretend user clicks on the url twice and then clicks 'yes' - showInformationMessageSpy - .mockImplementation(resolveArg(2)) - .mockImplementation(resolveArg(2)) - .mockImplementation(resolveArg(3)); - const val = await showBinaryChoiceWithUrlDialog("xxx", "invalid:url"); - expect(val).toBe(true); - }); - - it("should show a binary choice dialog with a url and return `no`", async () => { - // pretend user clicks on the url twice and then clicks 'no' - showInformationMessageSpy - .mockImplementation(resolveArg(2)) - .mockImplementation(resolveArg(2)) - .mockImplementation(resolveArg(4)); - const val = await showBinaryChoiceWithUrlDialog("xxx", "invalid:url"); - expect(val).toBe(false); - }); - - it("should show a binary choice dialog and exit after clcking `more info` 5 times", async () => { - // pretend user clicks on the url twice and then clicks 'no' - showInformationMessageSpy - .mockImplementation(resolveArg(2)) - .mockImplementation(resolveArg(2)) - .mockImplementation(resolveArg(2)) - .mockImplementation(resolveArg(2)) - .mockImplementation(resolveArg(2)); - const val = await showBinaryChoiceWithUrlDialog("xxx", "invalid:url"); - // No choice was made - expect(val).toBeUndefined(); - expect(showInformationMessageSpy).toHaveBeenCalledTimes(5); - }); - }); - - describe("showNeverAskAgainDialog", () => { - let showInformationMessageSpy: jest.SpiedFunction< - typeof window.showInformationMessage - >; - - beforeEach(() => { - showInformationMessageSpy = jest - .spyOn(window, "showInformationMessage") - .mockResolvedValue(undefined); - }); - - const resolveArg = - (index: number) => - (...args: any[]) => - Promise.resolve(args[index]); - - const title = - "We've noticed you don't have a CodeQL pack available to analyze this database. Can we set up a query pack for you?"; - - it("should show a ternary choice dialog and return `Yes`", async () => { - // pretend user chooses 'Yes' - const yesItem = resolveArg(2); - showInformationMessageSpy.mockImplementationOnce(yesItem); - - const answer = await showNeverAskAgainDialog(title); - expect(answer).toBe("Yes"); - }); - - it("should show a ternary choice dialog and return `No`", async () => { - // pretend user chooses 'No' - const noItem = resolveArg(3); - showInformationMessageSpy.mockImplementationOnce(noItem); - - const answer = await showNeverAskAgainDialog(title); - expect(answer).toBe("No"); - }); - - it("should show a ternary choice dialog and return `No, and never ask me again`", async () => { - // pretend user chooses 'No, and never ask me again' - const neverAskAgainItem = resolveArg(4); - showInformationMessageSpy.mockImplementationOnce(neverAskAgainItem); - - const answer = await showNeverAskAgainDialog(title); - expect(answer).toBe("No, and never ask me again"); - }); - }); }); describe("prepareCodeTour", () => { diff --git a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/query-history-manager.test.ts b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/query-history-manager.test.ts index 2ead1a3c9..c5d874026 100644 --- a/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/query-history-manager.test.ts +++ b/extensions/ql-vscode/test/vscode-tests/no-workspace/query-history/query-history-manager.test.ts @@ -22,7 +22,7 @@ import { createMockVariantAnalysisHistoryItem } from "../../../factories/query-h import { VariantAnalysisHistoryItem } from "../../../../src/query-history/variant-analysis-history-item"; import { QueryStatus } from "../../../../src/query-status"; import { VariantAnalysisStatus } from "../../../../src/variant-analysis/shared/variant-analysis"; -import * as helpers from "../../../../src/helpers"; +import * as dialog from "../../../../src/common/vscode/dialog"; import { mockedQuickPickItem } from "../../utils/mocking.helpers"; import { createMockQueryHistoryDirs } from "../../../factories/query-history/query-history-dirs"; import { createMockApp } from "../../../__mocks__/appMock"; @@ -318,20 +318,20 @@ describe("QueryHistoryManager", () => { describe("when the item is a variant analysis", () => { let showBinaryChoiceDialogSpy: jest.SpiedFunction< - typeof helpers.showBinaryChoiceDialog + typeof dialog.showBinaryChoiceDialog >; let showInformationMessageWithActionSpy: jest.SpiedFunction< - typeof helpers.showInformationMessageWithAction + typeof dialog.showInformationMessageWithAction >; beforeEach(() => { // Choose 'Yes' when asked "Are you sure?" showBinaryChoiceDialogSpy = jest - .spyOn(helpers, "showBinaryChoiceDialog") + .spyOn(dialog, "showBinaryChoiceDialog") .mockResolvedValue(true); showInformationMessageWithActionSpy = jest.spyOn( - helpers, + dialog, "showInformationMessageWithAction", ); });