Adding some webview panel and user survey tests. (#18296)

* adding more tests

* Adding more tests

* Fixing typo and adding a single command for test coverage

* some survey tests

* adding more tests

* removing unused var

* Adding tests for later and never
This commit is contained in:
Aasim Khan 2024-10-23 12:28:26 -07:00 коммит произвёл GitHub
Родитель cbd96484f7
Коммит 9336e40ebc
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
8 изменённых файлов: 570 добавлений и 22 удалений

Просмотреть файл

@ -61,7 +61,8 @@
"prepare": "husky",
"lint-staged": "lint-staged --quiet",
"precommit": "run-p lint-staged localization",
"clean-package": "git clean -xfd && yarn install && yarn build && yarn gulp package:online"
"clean-package": "git clean -xfd && yarn install && yarn build && yarn gulp package:online",
"testWithCoverage": "yarn test && yarn gulp cover"
},
"lint-staged": {
"*.ts": "eslint --quiet --cache",

Просмотреть файл

@ -105,18 +105,17 @@ export abstract class ReactWebviewBaseController<State, Reducers>
protected _getHtmlTemplate() {
const nonce = getNonce();
const baseUrl =
this._getWebview()
.asWebviewUri(
vscode.Uri.joinPath(
this._context.extensionUri,
"out",
"src",
"reactviews",
"assets",
),
)
.toString() + "/";
const baseUrl = this._getWebview().asWebviewUri(
vscode.Uri.joinPath(
this._context.extensionUri,
"out",
"src",
"reactviews",
"assets",
),
);
const baseUrlString = baseUrl.toString() + "/";
return `
<!DOCTYPE html>
@ -125,7 +124,7 @@ export abstract class ReactWebviewBaseController<State, Reducers>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mssqlwebview</title>
<base href="${baseUrl}"> <!-- Required for loading relative resources in the webview -->
<base href="${baseUrlString}"> <!-- Required for loading relative resources in the webview -->
<style>
html, body {
margin: 0;

Просмотреть файл

@ -186,7 +186,7 @@ export function sendSurveyTelemetry(surveyId: string, answers: Answers): void {
sendActionEvent(
TelemetryViews.UserSurvey,
TelemetryActions.SurverySubmit,
TelemetryActions.SurveySubmit,
{
surveyId: surveyId,
...stringAnswers,

Просмотреть файл

@ -43,7 +43,7 @@ export enum TelemetryActions {
Publish = "Publish",
ContinueEditing = "ContinueEditing",
Close = "Close",
SurverySubmit = "SurveySubmit",
SurveySubmit = "SurveySubmit",
SaveResults = "SaveResults",
CopyResults = "CopyResults",
CopyResultsHeaders = "CopyResultsHeaders",

Просмотреть файл

@ -10,7 +10,7 @@ import * as vscode from "vscode";
import Sinon, * as sinon from "sinon";
import { ReactWebviewBaseController } from "../../src/controllers/reactWebviewBaseController";
import { stubTelemetery as stubTelemetry } from "./utils";
import { stubTelemetry } from "./utils";
suite("ReactWebviewController Tests", () => {
let controller: TestWebviewController;

Просмотреть файл

@ -0,0 +1,321 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from "assert";
import * as locConstants from "../../src/constants/locConstants";
import * as sinon from "sinon";
import * as utils from "../../src/utils/utils";
import * as vscode from "vscode";
import { MssqlWebviewPanelOptions } from "../../src/sharedInterfaces/webview";
import { ReactWebviewPanelController } from "../../src/controllers/reactWebviewPanelController";
import { stubTelemetry } from "./utils";
suite("ReactWebviewPanelController", () => {
let sandbox: sinon.SinonSandbox;
let mockContext: vscode.ExtensionContext;
let createWebviewPanelStub: sinon.SinonStub;
let locConstantsStub: any;
// Mock WebviewPanel and Webview
let mockWebview: vscode.Webview;
let mockPanel: vscode.WebviewPanel;
let showInformationMessageStub;
setup(() => {
sandbox = sinon.createSandbox();
mockWebview = {
postMessage: sandbox.stub(),
asWebviewUri: sandbox
.stub()
.returns(vscode.Uri.parse("https://example.com/")),
onDidReceiveMessage: sandbox.stub(),
} as any;
showInformationMessageStub = sandbox.stub(
vscode.window,
"showInformationMessage",
);
mockPanel = {
webview: mockWebview,
title: "Test Panel",
viewColumn: vscode.ViewColumn.One,
options: {},
reveal: sandbox.stub(),
dispose: sandbox.stub(),
onDidDispose: sandbox.stub(),
onDidChangeViewState: sandbox.stub(),
iconPath: undefined,
} as any;
createWebviewPanelStub = sandbox
.stub(vscode.window, "createWebviewPanel")
.returns(mockPanel);
stubTelemetry(sandbox);
// Stub locConstants
locConstantsStub = {
Webview: {
webviewRestorePrompt: sandbox
.stub()
.returns("Restore webview?"),
Restore: "Restore",
},
};
sandbox.stub(locConstants, "Webview").value(locConstantsStub.Webview);
mockContext = {
extensionUri: vscode.Uri.parse("https://localhost"),
extensionPath: "path",
} as unknown as vscode.ExtensionContext;
sandbox.stub(utils, "getNonce").returns("test-nonce");
});
teardown(() => {
sandbox.restore();
});
function createController(options: Partial<MssqlWebviewPanelOptions> = {}) {
const defaultOptions: MssqlWebviewPanelOptions = {
title: "Test Panel",
viewColumn: vscode.ViewColumn.One,
iconPath: vscode.Uri.file("path"),
showRestorePromptAfterClose: true,
};
const controller = new TestReactWebviewPanelController(mockContext, {
...defaultOptions,
...options,
});
return controller;
}
test("should create a WebviewPanel with correct options upon initialization", () => {
const options = {
title: "My Test Panel",
viewColumn: vscode.ViewColumn.Two,
iconPath: vscode.Uri.file("/path/to/test-icon.png"),
showRestorePromptAfterClose: true,
};
createController(options);
assert.ok(createWebviewPanelStub.calledOnce);
assert.ok(
createWebviewPanelStub.calledWith(
"mssql-react-webview",
options.title,
options.viewColumn,
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.file(mockContext.extensionPath),
],
},
),
);
assert.ok(mockPanel.webview.html.includes("testSource.js"));
assert.strictEqual(mockPanel.iconPath, options.iconPath);
});
test("should register onDidDispose handler that disposes the controller", async () => {
createController();
const disposeSpy = mockPanel.onDidDispose as sinon.SinonSpy;
const disposeHandler = disposeSpy.firstCall.args[0];
assert.ok(disposeSpy.called, "onDidDispose should be called once");
assert.strictEqual(
typeof disposeHandler,
"function",
"Dispose handler should be a function",
);
});
test("Should register onDidReceiveMessage handler", () => {
createController();
const onDidReceiveMessageSpy =
mockWebview.onDidReceiveMessage as sinon.SinonSpy;
assert.ok(
onDidReceiveMessageSpy.calledOnce,
"onDidReceiveMessage should be called once",
);
const onDidReceiveMessageHandler =
onDidReceiveMessageSpy.firstCall.args[0];
assert.strictEqual(
typeof onDidReceiveMessageHandler,
"function",
"onDidReceiveMessage handler should be a function",
);
});
test("Should reveal the panel to the foreground", () => {
const controller = createController();
const revealSpy = mockPanel.reveal as sinon.SinonSpy;
controller.revealToForeground();
assert.ok(revealSpy.calledOnce, "reveal should be called once");
assert.ok(revealSpy.calledWith(vscode.ViewColumn.One, true));
});
test("Should reveal the panel to the foreground with the specified view column", () => {
const controller = createController();
const revealSpy = mockPanel.reveal as sinon.SinonSpy;
controller.revealToForeground(vscode.ViewColumn.Two);
assert.ok(revealSpy.calledOnce, "reveal should be called once");
assert.ok(revealSpy.calledWith(vscode.ViewColumn.Two, true));
});
test("should show restore prompt when showRestorePromptAfterClose is true", async () => {
const options = {
showRestorePromptAfterClose: true,
};
createController(options);
// Set up the stub to return the Restore option
const restoreOption = {
title: "Restore",
run: sinon.stub().resolves(),
};
showInformationMessageStub.resolves(restoreOption);
// Simulate panel disposal
const disposeHandler = (mockPanel.onDidDispose as sinon.SinonStub)
.firstCall.args[0];
await disposeHandler();
// Expect showInformationMessage to be called with the correct prompt
assert.strictEqual(
showInformationMessageStub.calledOnce,
true,
"showInformationMessage should be called once",
);
const promptCallerArgs = showInformationMessageStub.firstCall.args;
assert.deepEqual(
promptCallerArgs[0],
"Restore webview?",
"prompt message is not correct",
);
assert.deepEqual(
promptCallerArgs[1],
{
modal: true,
},
"Prompt should be modal",
);
assert.strictEqual(
promptCallerArgs[2].title,
"Restore",
"Restore button title is not correct",
);
assert.strictEqual(
restoreOption.run.calledOnce,
true,
"Restore option run should be called once",
);
// Disposing the panel should not be called
assert.strictEqual(
(mockPanel.dispose as sinon.SinonStub).calledOnce,
false,
"Panel should not be disposed",
);
});
test("should dispose without showing restore prompt when showRestorePromptAfterClose is false", async () => {
const options = {
showRestorePromptAfterClose: false,
};
const onDidReceiveMessageStub =
mockWebview.onDidReceiveMessage as sinon.SinonStub;
onDidReceiveMessageStub.returns({
dispose: sinon.stub().returns(true),
});
const onDidDispose = mockPanel.onDidDispose as sinon.SinonStub;
onDidDispose.returns({
dispose: sinon.stub().returns(true),
});
const controller = createController(options);
sandbox.stub(controller, "dispose").resolves();
// Simulate panel disposal
const disposeHandler = (mockPanel.onDidDispose as sinon.SinonStub)
.firstCall.args[0];
await disposeHandler();
// Expect showInformationMessage to not be called
assert.strictEqual(
showInformationMessageStub.calledOnce,
false,
"showInformationMessage should not be called",
);
// Disposing the panel should be called
assert.strictEqual(
(controller.dispose as sinon.SinonStub).calledOnce,
true,
"Panel should be disposed",
);
});
test("should set showRestorePromptAfterClose correctly via setter", () => {
const controller = createController();
controller.showRestorePromptAfterClose = true;
// To verify, we need to access the private _options
const options = (controller as any)._options;
assert.strictEqual(
options.showRestorePromptAfterClose,
true,
"showRestorePromptAfterClose should be set to true",
);
});
test("Should generate correct HTML template", () => {
const asWebviewUriStub = mockWebview.asWebviewUri as sinon.SinonStub;
asWebviewUriStub.returns(vscode.Uri.parse("https://example.com/"));
const controller = createController();
const html = controller["_getHtmlTemplate"]();
assert.strictEqual(typeof html, "string", "HTML should be a string");
assert.ok(
html.includes("testSource.css"),
"HTML should include testSource.css",
);
assert.ok(
html.includes("testSource.js"),
"HTML should include testSource.js",
);
assert.ok(
html.includes('nonce="test-nonce"'),
"HTML should include the nonce",
);
assert.ok(
html.includes('<base href="https://example.com//">'),
"HTML should include the correct base href",
);
});
});
interface TestState {
count: number;
}
interface TestReducers {
increment: { amount: number };
decrement: { amount: number };
}
class TestReactWebviewPanelController extends ReactWebviewPanelController<
TestState,
TestReducers
> {
constructor(
context: vscode.ExtensionContext,
options: MssqlWebviewPanelOptions,
) {
super(context, "testSource", { count: 0 }, options);
}
}

Просмотреть файл

@ -0,0 +1,212 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from "assert";
import * as locConstants from "../../src/constants/locConstants";
import * as sinon from "sinon";
import * as vscode from "vscode";
import {
TelemetryActions,
TelemetryViews,
} from "../../src/sharedInterfaces/telemetry";
import { UserSurvey } from "../../src/nps/userSurvey";
import { stubTelemetry } from "./utils";
suite("UserSurvey Tests", () => {
let sandbox;
let globalState;
let context;
let showInformationMessageStub: sinon.SinonStub;
setup(() => {
sandbox = sinon.createSandbox();
globalState = {
get: sandbox.stub(),
update: sandbox.stub(),
};
context = {
globalState: globalState,
extensionUri: vscode.Uri.file("test"),
};
showInformationMessageStub = sandbox.stub(
vscode.window,
"showInformationMessage",
);
UserSurvey.createInstance(context);
});
teardown(() => {
sandbox.restore();
});
test("should create and return the same UserSurvey instance", () => {
const instance = UserSurvey.getInstance();
assert.strictEqual(instance, UserSurvey.getInstance());
});
test("should not prompt the user if they opted out of the survey", async () => {
globalState.get.withArgs("nps/never", false).returns(true);
const instance = UserSurvey.getInstance();
await instance.promptUserForNPSFeedback();
assert.strictEqual(
(globalState.get as sinon.SinonStub).calledWith("nps/never", false),
true,
"globalState.get should be called with 'nps/never' and false",
);
assert.strictEqual(
showInformationMessageStub.called,
false,
"showInformationMessage should not be called",
);
});
test("Should not prompt use if skip version is set", async () => {
sinon.stub(vscode.extensions, "getExtension").returns({
packageJSON: {
version: "someVersion",
},
} as any);
globalState.get.withArgs("nps/skipVersion", "").returns("someVersion");
const instance = UserSurvey.getInstance();
await instance.promptUserForNPSFeedback();
assert.strictEqual(
showInformationMessageStub.called,
false,
"showInformationMessage should not be called",
);
});
test("should prompt for feedback after session count reaches threshold", async () => {
globalState.get.withArgs("nps/never").returns(false);
globalState.get.withArgs("nps/skipVersion").returns("");
globalState.get.withArgs("nps/lastSessionDate").returns("01/01/2023");
globalState.get.withArgs("nps/sessionCount").returns(5);
globalState.get.withArgs("nps/isCandidate").returns(true);
showInformationMessageStub.resolves({
title: locConstants.UserSurvey.takeSurvey,
run: sandbox.stub(),
});
const userSurvey = UserSurvey.getInstance();
await userSurvey.promptUserForNPSFeedback();
assert.strictEqual(
showInformationMessageStub.calledOnce,
true,
"showInformationMessage should be called",
);
});
test("should update global state and send telemetry after survey submission", async () => {
const { sendActionEvent } = stubTelemetry(sandbox);
globalState.get.withArgs("nps/isCandidate").returns(true);
showInformationMessageStub.callsFake(
async (_text, takeButton, _laterButton, _neverButton) => {
return takeButton;
},
);
const userSurvey = UserSurvey.getInstance();
sandbox.stub(userSurvey, "launchSurvey").resolves();
const onSubmitStub = sandbox.stub();
const onCancelStub = sandbox.stub();
// Mock the webview controller
const mockWebviewController = {
revealToForeground: sandbox.stub(),
updateState: sandbox.stub(),
isDisposed: false,
onSubmit: onSubmitStub,
onCancel: onCancelStub,
};
// Use callsFake to simulate onSubmit getting triggered when it's called
onSubmitStub.callsFake((callback) => {
callback({
q1: "answer1",
q2: "answer2",
q3: 3,
}); // Simulate submitting empty answers
});
(userSurvey as any)._webviewController = mockWebviewController;
await userSurvey.promptUserForNPSFeedback();
assert.strictEqual(
mockWebviewController.revealToForeground.calledOnce,
true,
"launchSurvey should be called",
);
assert.strictEqual(
sendActionEvent.calledOnce,
true,
"sendActionEvent should be called",
);
assert.strictEqual(
sendActionEvent.calledWith(
TelemetryViews.UserSurvey,
TelemetryActions.SurveySubmit,
{
surveyId: "nps",
q1: "answer1",
q2: "answer2",
},
{
q3: 3,
},
),
true,
"sendActionEvent should be called with correct arguments",
);
});
test('Should reduce session count when user clicks "Later"', async () => {
globalState.get.withArgs("nps/isCandidate").returns(true);
globalState.get.withArgs("nps/sessionCount").returns(5);
showInformationMessageStub.callsFake(
async (_text, takeButton, _laterButton, _neverButton) => {
return _laterButton;
},
);
const userSurvey = UserSurvey.getInstance();
sandbox.stub(userSurvey, "launchSurvey").resolves();
await userSurvey.promptUserForNPSFeedback();
assert.strictEqual(
globalState.update.calledWith("nps/sessionCount", 3),
true,
"session count should be decremented",
);
});
test("Should set never key when user clicks 'Never'", async () => {
globalState.get.withArgs("nps/isCandidate").returns(true);
showInformationMessageStub.callsFake(
async (_text, takeButton, _laterButton, neverButton) => {
return neverButton;
},
);
const userSurvey = UserSurvey.getInstance();
sandbox.stub(userSurvey, "launchSurvey").resolves();
await userSurvey.promptUserForNPSFeedback();
assert.strictEqual(
globalState.update.calledWith("nps/never", true),
true,
"should set never key",
);
});
});

Просмотреть файл

@ -15,12 +15,27 @@ export async function activateExtension() {
}
// Stubs the telemetry code
export function stubTelemetery(sandbox: sinon.SinonSandbox) {
export function stubTelemetry(sandbox?: sinon.SinonSandbox): {
sendActionEvent: sinon.SinonStub;
sendErrorEvent: sinon.SinonStub;
} {
if (sandbox) {
sandbox.stub(telemetry, "sendActionEvent").callsFake(() => {});
sandbox.stub(telemetry, "sendErrorEvent").callsFake(() => {});
return {
sendActionEvent: sandbox
.stub(telemetry, "sendActionEvent")
.callsFake(() => {}),
sendErrorEvent: sandbox
.stub(telemetry, "sendErrorEvent")
.callsFake(() => {}),
};
} else {
sinon.stub(telemetry, "sendActionEvent").callsFake(() => {});
sinon.stub(telemetry, "sendErrorEvent").callsFake(() => {});
return {
sendActionEvent: sinon
.stub(telemetry, "sendActionEvent")
.callsFake(() => {}),
sendErrorEvent: sinon
.stub(telemetry, "sendErrorEvent")
.callsFake(() => {}),
};
}
}