Skip to content

Commit

Permalink
perf: refresh add plugin treeview item (#12376)
Browse files Browse the repository at this point in the history
* perf: refresh add plugin treeview item

* test: ut
  • Loading branch information
yuqizhou77 authored Sep 12, 2024
1 parent 7a85440 commit bc1eb6c
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 9 deletions.
2 changes: 2 additions & 0 deletions packages/vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ import { ReleaseNote } from "./utils/releaseNote";
import { ExtensionSurvey } from "./utils/survey";
import { getSettingsVersion, projectVersionCheck } from "./utils/telemetryUtils";
import { createPluginWithManifest } from "./handlers/createPluginWithManifestHandler";
import { manifestListener } from "./manifestListener";

export async function activate(context: vscode.ExtensionContext) {
const value = IsChatParticipantEnabled && semver.gte(vscode.version, "1.90.0");
Expand Down Expand Up @@ -317,6 +318,7 @@ function activateTeamsFxRegistration(context: vscode.ExtensionContext) {

if (vscode.workspace.isTrusted) {
registerLanguageFeatures(context);
context.subscriptions.push(manifestListener());
}

registerDebugConfigProviders(context);
Expand Down
8 changes: 7 additions & 1 deletion packages/vscode-extension/src/globalVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
isManifestOnlyOfficeAddinProject,
manifestUtils,
} from "@microsoft/teamsfx-core";
import { Tools } from "@microsoft/teamsfx-api";
import { TeamsAppManifest, Tools } from "@microsoft/teamsfx-api";

/**
* Common variables used throughout the extension. They must be initialized in the activate() method of extension.ts
Expand Down Expand Up @@ -86,6 +86,12 @@ export function checkIsDeclarativeCopilotApp(directory: string): boolean {
}
}

export function updateIsDeclarativeCopilotApp(manifest: TeamsAppManifest): boolean {
const value = manifestUtils.getCapabilities(manifest).includes("copilotGpt");
isDeclarativeCopilotApp = value;
return isDeclarativeCopilotApp;
}

export function setCommandIsRunning(isRunning: boolean) {
commandIsRunning = isRunning;
}
Expand Down
75 changes: 75 additions & 0 deletions packages/vscode-extension/src/manifestListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

"use strict";

import * as vscode from "vscode";
import {
isDeclarativeCopilotApp,
updateIsDeclarativeCopilotApp,
workspaceUri,
} from "./globalVariables";
import path from "path";
import {
AppPackageFolderName,
ManifestTemplateFileName,
TeamsAppManifest,
} from "@microsoft/teamsfx-api";
import TreeViewManagerInstance from "./treeview/treeViewManager";
import { ExtTelemetry } from "./telemetry/extTelemetry";
import { TelemetryEvent, TelemetryProperty } from "./telemetry/extTelemetryEvents";
import { isValidProjectV3 } from "@microsoft/teamsfx-core";

function setAbortableTimeout(ms: number, signal: any) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
// Resolve the promise after 5 seconds
resolve("After timeout. Checking app.");
}, ms);

// Listen for the abort event
signal.addEventListener("abort", () => {
// Clear the timeout and reject the promise if aborted
clearTimeout(timeoutId);
reject("resolved after clear");
});
});
}

export function manifestListener(): vscode.Disposable {
let abortController: undefined | AbortController;
const disposable = vscode.workspace.onDidSaveTextDocument(
async (event): Promise<boolean | undefined> => {
try {
if (
workspaceUri &&
isValidProjectV3(workspaceUri.fsPath) &&
event.fileName ===
path.join(workspaceUri.fsPath, AppPackageFolderName, ManifestTemplateFileName)
) {
if (abortController) {
abortController.abort();
}
abortController = new AbortController();

await setAbortableTimeout(5000, abortController.signal);
if (!abortController.signal.aborted) {
const currValue = isDeclarativeCopilotApp;
const manifest: TeamsAppManifest = JSON.parse(event.getText());
const newValue = updateIsDeclarativeCopilotApp(manifest);
if (currValue !== newValue) {
ExtTelemetry.sendTelemetryEvent(TelemetryEvent.UpdateAddPluginTreeview, {
[TelemetryProperty.ShowAddPluginTreeView]: newValue.toString(),
});
TreeViewManagerInstance.updateDevelopmentTreeView();
}

return currValue !== newValue;
}
}
} catch (error) {}
}
);

return disposable;
}
3 changes: 3 additions & 0 deletions packages/vscode-extension/src/telemetry/extTelemetryEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ export enum TelemetryEvent {

InstallKiota = "install-kiota",
Configuration = "vsc-configuration",

UpdateAddPluginTreeview = "update-add-plugin-tree-view",
}

export enum TelemetryProperty {
Expand Down Expand Up @@ -415,6 +417,7 @@ export enum TelemetryProperty {
CopilotChatRequestToken = "copilot-chat-request-token",
CopilotChatResponseToken = "copilot-chat-response-token",
KiotaInstalled = "kiota-installed",
ShowAddPluginTreeView = "show-add-plugin-tree-view",
}

export enum TelemetryMeasurements {
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode-extension/src/treeview/treeViewManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class TreeViewManager {
}
}

public updateTreeViewsOnSPFxChanged(): void {
public updateDevelopmentTreeView(): void {
const developmentTreeviewProvider = this.getTreeView(
"teamsfx-development"
) as CommandsTreeViewProvider;
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode-extension/src/utils/fileSystemWatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function addFileSystemWatcher(workspacePath: string) {

export function refreshSPFxTreeOnFileChanged() {
initializeGlobalVariables(context);
TreeViewManagerInstance.updateTreeViewsOnSPFxChanged();
TreeViewManagerInstance.updateDevelopmentTreeView();
}

export async function sendSDKVersionTelemetry(filePath: string) {
Expand Down
19 changes: 19 additions & 0 deletions packages/vscode-extension/test/extension/globalVariables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,23 @@ describe("Global Variables", () => {
chai.expect(res).to.be.false;
});
});

it("updateIsDeclarativeCopilotApp", () => {
const manifest = new TeamsAppManifest();
let res = globalVariables.updateIsDeclarativeCopilotApp(manifest);
chai.assert.isFalse(res);

res = globalVariables.updateIsDeclarativeCopilotApp({
...manifest,
copilotExtensions: {
declarativeCopilots: [
{
id: "1",
file: "test",
},
],
},
});
chai.assert.isTrue(res);
});
});
180 changes: 180 additions & 0 deletions packages/vscode-extension/test/handlers/manifestListener.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import * as chai from "chai";
import * as sinon from "sinon";
import * as vscode from "vscode";
import * as globalVariables from "../../src/globalVariables";
import { manifestListener } from "../../src/manifestListener";
import { TeamsAppManifest } from "@microsoft/teamsfx-api";
import path from "path";
import TreeViewManagerInstance from "../../src/treeview/treeViewManager";
import * as projectSettingsHelper from "@microsoft/teamsfx-core/build/common/projectSettingsHelper";
import { ExtTelemetry } from "../../src/telemetry/extTelemetry";

describe("registerManifestListener", () => {
const sandbox = sinon.createSandbox();
let clock: sinon.SinonFakeTimers;

beforeEach(() => {
sandbox.stub(ExtTelemetry, "sendTelemetryEvent").returns();
});

afterEach(() => {
sandbox.restore();
if (clock) {
clock.restore();
}
});
it("successfully refresh item", async () => {
clock = sandbox.useFakeTimers();
let handler = async (event: any) => {};
sandbox.stub(projectSettingsHelper, "isValidProjectV3").returns(true);
sandbox.stub(vscode.workspace, "onDidSaveTextDocument").callsFake((listener: any) => {
handler = listener;
return new vscode.Disposable(() => {
return;
});
});
sandbox.stub(globalVariables, "isDeclarativeCopilotApp").value(false);
sandbox.stub(globalVariables, "workspaceUri").value(vscode.Uri.file("."));
sandbox
.stub(globalVariables, "updateIsDeclarativeCopilotApp")
.onFirstCall()
.returns(true)
.onSecondCall()
.returns(false);
sandbox.stub(TreeViewManagerInstance, "updateDevelopmentTreeView").returns();

const fakeDocument = {
fileName: path.join(vscode.Uri.file(".").fsPath, "appPackage", "manifest.json"),
getText: () => {
return JSON.stringify(new TeamsAppManifest());
},
};

manifestListener();
let job = handler(fakeDocument);

await clock.tickAsync(5000);
let res = await job;
chai.assert.isTrue(res);

job = handler(fakeDocument);
await clock.tickAsync(5000);
res = await job;
chai.assert.isFalse(res);
});

it("abort previous one", async () => {
clock = sandbox.useFakeTimers();
let handler = async (event: any) => {};
sandbox.stub(projectSettingsHelper, "isValidProjectV3").returns(true);
sandbox.stub(vscode.workspace, "onDidSaveTextDocument").callsFake((listener: any) => {
handler = listener;
return new vscode.Disposable(() => {
return;
});
});
sandbox.stub(globalVariables, "isDeclarativeCopilotApp").value(false);
sandbox.stub(globalVariables, "workspaceUri").value(vscode.Uri.file("."));
sandbox
.stub(globalVariables, "updateIsDeclarativeCopilotApp")
.onFirstCall()
.returns(true)
.onSecondCall()
.returns(false);
sandbox.stub(TreeViewManagerInstance, "updateDevelopmentTreeView").returns();

const fakeDocument = {
fileName: path.join(vscode.Uri.file(".").fsPath, "appPackage", "manifest.json"),
getText: () => {
return JSON.stringify(new TeamsAppManifest());
},
};

manifestListener();
const job1 = handler(fakeDocument);
await clock.tickAsync(1000);
const job2 = handler(fakeDocument);

await clock.tickAsync(5000);
const res1 = await job1;
const res2 = await job2;

chai.assert.isUndefined(res1);
chai.assert.isTrue(res2);
});

it("not run if invalid project", async () => {
clock = sandbox.useFakeTimers();
let handler = async (event: any) => {};
sandbox.stub(projectSettingsHelper, "isValidProjectV3").returns(false);
sandbox.stub(globalVariables, "workspaceUri").value(vscode.Uri.file("."));
sandbox.stub(vscode.workspace, "onDidSaveTextDocument").callsFake((listener: any) => {
handler = listener;
return new vscode.Disposable(() => {
return;
});
});

const fakeDocument = {
fileName: path.join(vscode.Uri.file(".").fsPath, "appPackage", "manifest.json"),
getText: () => {
return JSON.stringify(new TeamsAppManifest());
},
};

manifestListener();
const res = await handler(fakeDocument);

chai.assert.isUndefined(res);
});

it("not run if empty workspace", async () => {
clock = sandbox.useFakeTimers();
let handler = async (event: any) => {};
sandbox.stub(globalVariables, "workspaceUri").value("");
sandbox.stub(projectSettingsHelper, "isValidProjectV3").returns(false);
sandbox.stub(vscode.workspace, "onDidSaveTextDocument").callsFake((listener: any) => {
handler = listener;
return new vscode.Disposable(() => {
return;
});
});

const fakeDocument = {
fileName: path.join(vscode.Uri.file(".").fsPath, "appPackage", "manifest.json"),
getText: () => {
return JSON.stringify(new TeamsAppManifest());
},
};

manifestListener();
const res = await handler(fakeDocument);

chai.assert.isUndefined(res);
});

it("not run if not default app manifest", async () => {
clock = sandbox.useFakeTimers();
let handler = async (event: any) => {};
sandbox.stub(globalVariables, "workspaceUri").value(".");
sandbox.stub(projectSettingsHelper, "isValidProjectV3").returns(false);
sandbox.stub(vscode.workspace, "onDidSaveTextDocument").callsFake((listener: any) => {
handler = listener;
return new vscode.Disposable(() => {
return;
});
});

const fakeDocument = {
fileName: path.join(vscode.Uri.file(".").fsPath, "appPackage", "unknown.json"),
getText: () => {
return JSON.stringify(new TeamsAppManifest());
},
};

manifestListener();
const res = await handler(fakeDocument);

chai.assert.isUndefined(res);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe("TreeViewManager", () => {
chai.assert.equal(setStatusStub.callCount, 2);
});

it("updateTreeViewsOnSPFxChanged", () => {
it("updateDevelopmentTreeView", () => {
sandbox.stub(globalVariables, "isSPFxProject").value(false);
sandbox.stub(featureFlagManager, "getBooleanValue").returns(false);
treeViewManager.registerTreeViews({
Expand All @@ -82,7 +82,7 @@ describe("TreeViewManager", () => {
chai.assert.equal(commands.length, 4);

sandbox.stub(globalVariables, "isSPFxProject").value(true);
treeViewManager.updateTreeViewsOnSPFxChanged();
treeViewManager.updateDevelopmentTreeView();

chai.assert.equal(commands.length, 5);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe("FileSystemWatcher", function () {
const workspacePath = "test";
sandbox.stub(projectSettingsHelper, "isValidProject").returns(true);
sandbox.stub(globalVariables, "initializeGlobalVariables");
sandbox.stub(TreeViewManagerInstance, "updateTreeViewsOnSPFxChanged");
sandbox.stub(TreeViewManagerInstance, "updateDevelopmentTreeView");

const watcher = {
onDidCreate: () => ({ dispose: () => undefined }),
Expand Down Expand Up @@ -87,15 +87,15 @@ describe("FileSystemWatcher", function () {

it("refreshSPFxTreeOnFileChanged", () => {
const initGlobalVariables = sandbox.stub(globalVariables, "initializeGlobalVariables");
const updateTreeViewsOnSPFxChanged = sandbox
const updateDevelopmentTreeView = sandbox
// eslint-disable-next-line no-secrets/no-secrets
.stub(TreeViewManagerInstance, "updateTreeViewsOnSPFxChanged")
.stub(TreeViewManagerInstance, "updateDevelopmentTreeView")
.resolves();

refreshSPFxTreeOnFileChanged();

chai.expect(initGlobalVariables.calledOnce).to.be.true;
chai.expect(updateTreeViewsOnSPFxChanged.calledOnce).to.be.true;
chai.expect(updateDevelopmentTreeView.calledOnce).to.be.true;
});
});

Expand Down

0 comments on commit bc1eb6c

Please sign in to comment.