Skip to content

Commit

Permalink
feat,tests: make 'Edit Attributes' a WebView subclass; add unit tests
Browse files Browse the repository at this point in the history
Signed-off-by: Trae Yelovich <[email protected]>
  • Loading branch information
traeok committed Aug 17, 2023
1 parent 3ec7609 commit 9b91418
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 89 deletions.
2 changes: 1 addition & 1 deletion packages/samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ This folder contains sample packages for reference during development, for both

## vue-webview-sample

Demonstrates the use of the `WebView` class from Zowe Explorer API to create a webview panel, powered by the Vite bundler and Vue JavaScript framework.
Demonstrates the use of the `WebView` class from Zowe Explorer API to create a webview panel, powered by the Vite bundler and Vue JavaScript framework.
67 changes: 67 additions & 0 deletions packages/zowe-explorer/__mocks__/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,57 @@
*
*/

export enum ViewColumn {
/**
* A *symbolic* editor column representing the currently active column. This value
* can be used when opening editors, but the *resolved* {@link TextEditor.viewColumn viewColumn}-value
* of editors will always be `One`, `Two`, `Three`,... or `undefined` but never `Active`.
*/
Active = -1,
/**
* A *symbolic* editor column representing the column to the side of the active one. This value
* can be used when opening editors, but the *resolved* {@link TextEditor.viewColumn viewColumn}-value
* of editors will always be `One`, `Two`, `Three`,... or `undefined` but never `Beside`.
*/
Beside = -2,
/**
* The first editor column.
*/
One = 1,
/**
* The second editor column.
*/
Two = 2,
/**
* The third editor column.
*/
Three = 3,
/**
* The fourth editor column.
*/
Four = 4,
/**
* The fifth editor column.
*/
Five = 5,
/**
* The sixth editor column.
*/
Six = 6,
/**
* The seventh editor column.
*/
Seven = 7,
/**
* The eighth editor column.
*/
Eight = 8,
/**
* The ninth editor column.
*/
Nine = 9,
}

/**
* A provider result represents the values a provider, like the [`HoverProvider`](#HoverProvider),
* may return. For once this is the actual result type `T`, like `Hover`, or a thenable that resolves
Expand Down Expand Up @@ -239,6 +290,22 @@ export namespace window {
return this;
}

export function createWebviewPanel(
viewType: string,
title: string,
showOptions: ViewColumn | { viewColumn: ViewColumn; preserveFocus?: boolean },
options?: any
): any {
return {
onDidDispose: jest.fn(),
webview: {
asWebviewUri: jest.fn(),
postMessage: jest.fn(),
onDidReceiveMessage: jest.fn(),
},
};
}

/**
* Options to configure the behavior of the message.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { ExtensionContext } from "vscode";
import { AttributeView } from "../../../src/uss/AttributeView";
import { IZoweTree, IZoweUSSTreeNode, ZoweExplorerApi } from "@zowe/zowe-explorer-api";
import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister";

describe("AttributeView unit tests", () => {
let view: AttributeView;
const context = { extensionPath: "some/fake/ext/path" } as unknown as ExtensionContext;
const treeProvider = { refreshElement: jest.fn(), refresh: jest.fn() } as unknown as IZoweTree<IZoweUSSTreeNode>;
const node = {
attributes: {
perms: "----------",
},
label: "example node",
fullPath: "/z/some/path",
getParent: jest.fn(),
getProfile: jest.fn(),
onUpdate: jest.fn(),
} as unknown as IZoweUSSTreeNode;
const updateAttributesMock = jest.fn();

beforeAll(() => {
jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValue({
updateAttributes: updateAttributesMock,
} as unknown as ZoweExplorerApi.IUss);
view = new AttributeView(context, treeProvider, node);
});

afterEach(() => {
node.onUpdate = jest.fn();
});

it("refreshes properly when webview sends 'refresh' command", () => {
// case 1: node is a root node
(view as any).onDidReceiveMessage({ command: "refresh" });
expect(treeProvider.refresh).toHaveBeenCalled();

// case 2: node is a child node
node.getParent = jest.fn().mockReturnValueOnce({ label: "parent node" } as IZoweUSSTreeNode);
(view as any).onDidReceiveMessage({ command: "refresh" });
expect(treeProvider.refreshElement).toHaveBeenCalled();

expect(node.onUpdate).toHaveBeenCalledTimes(2);
});

it("dispatches node data to webview when 'ready' command is received", () => {
(view as any).onDidReceiveMessage({ command: "ready" });
expect(view.panel.webview.postMessage).toHaveBeenCalledWith({
attributes: node.attributes,
name: node.fullPath,
readonly: false,
});
});

it("updates attributes when 'update-attributes' command is received", async () => {
// case 1: no attributes provided from webview (sanity check)
(view as any).onDidReceiveMessage({ command: "update-attributes" });
expect(updateAttributesMock).not.toHaveBeenCalled();

const attributes = {
owner: "owner",
group: "group",
perms: "-rwxrwxrwx",
};

// case 2: attributes provided from webview, pass owner/group as name
await (view as any).onDidReceiveMessage({
command: "update-attributes",
attrs: attributes,
});
expect(updateAttributesMock).toHaveBeenCalled();
expect(view.panel.webview.postMessage).toHaveBeenCalledWith({
updated: true,
});

// case 2: attributes provided from webview, pass owner/group as IDs
await (view as any).onDidReceiveMessage({
command: "update-attributes",
attrs: {
...attributes,
owner: "1",
group: "9001",
},
});
expect(updateAttributesMock).toHaveBeenCalled();
expect(view.panel.webview.postMessage).toHaveBeenCalled();
});

it("handles any errors while updating attributes", async () => {
// case 3: error thrown while updating attributes
updateAttributesMock.mockRejectedValueOnce(new Error("Failed to update attributes"));
await (view as any).onDidReceiveMessage({
command: "update-attributes",
attrs: {},
});
expect(updateAttributesMock).toHaveBeenCalled();
expect(view.panel.webview.postMessage).toHaveBeenCalledWith({
updated: false,
});
});
});
113 changes: 113 additions & 0 deletions packages/zowe-explorer/src/uss/AttributeView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { FileAttributes, Gui, IZoweTree, IZoweUSSTreeNode, WebView, ZoweExplorerApi } from "@zowe/zowe-explorer-api";
import { Disposable, ExtensionContext } from "vscode";
import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister";

export class AttributeView extends WebView {
private treeProvider: IZoweTree<IZoweUSSTreeNode>;
private readonly ussNode: IZoweUSSTreeNode;
private readonly ussApi: ZoweExplorerApi.IUss;
private readonly canUpdate: boolean;

private onUpdateDisposable: Disposable;

public constructor(context: ExtensionContext, treeProvider: IZoweTree<IZoweUSSTreeNode>, node: IZoweUSSTreeNode) {
const label = node?.label ? `Edit Attributes: ${node.label}` : "Edit Attributes";
super(label, "edit-attributes", context, (message: object) => this.onDidReceiveMessage(message));
this.treeProvider = treeProvider;
this.ussNode = node;
this.canUpdate = node.onUpdate != null;
this.ussApi = ZoweExplorerApiRegister.getUssApi(this.ussNode.getProfile());
}

protected async onDidReceiveMessage(message: any): Promise<void> {
switch (message.command) {
case "refresh":
if (this.canUpdate) {

Check failure on line 25 in packages/zowe-explorer/src/uss/AttributeView.ts

View workflow job for this annotation

GitHub Actions / lint

Invalid type "string | TreeItemLabel" of template literal expression
this.onUpdateDisposable = this.ussNode.onUpdate(async (node) => {
await this.panel.webview.postMessage({
attributes: node.attributes,
name: node.fullPath,
readonly: this.ussApi.updateAttributes == null,
});
this.onUpdateDisposable.dispose();
});

if (this.ussNode.getParent()) {
this.treeProvider.refreshElement(this.ussNode.getParent());
} else {
this.treeProvider.refresh();
}
}
break;
case "ready":
await this.panel.webview.postMessage({
attributes: this.ussNode.attributes,
name: this.ussNode.fullPath,
readonly: this.ussApi.updateAttributes == null,
});
break;
case "update-attributes":
await this.updateAttributes(message);
break;
default:
break;
}
}

private async updateAttributes(message: any): Promise<void> {
if (!this.ussApi.updateAttributes) {
// The condition in this if statement should never be satisfied; the "Apply Changes" button is disabled
// when this API doesn't exist. But, this ensures the webview will be blocked from making update requests.
return;
}

try {
if (Object.keys(message.attrs).length > 0) {
const attrs = message.attrs;
const newAttrs: Partial<FileAttributes> = {};
if (!isNaN(parseInt(attrs.owner))) {
const uid = parseInt(attrs.owner);
newAttrs.uid = uid;

// set owner to the UID to prevent mismatched UIDs/owners
newAttrs.owner = attrs.owner;
} else if (this.ussNode.attributes.owner !== attrs.owner) {
newAttrs.owner = attrs.owner;
}

if (!isNaN(parseInt(attrs.group))) {
const gid = parseInt(attrs.group);
// must provide owner when changing group
newAttrs.owner = attrs.owner;
newAttrs.gid = gid;

// set group to the GID to prevent mismatched GIDs/groups
newAttrs.group = attrs.group;
} else if (this.ussNode.attributes.group !== attrs.group) {
// must provide owner when changing group
newAttrs.owner = attrs.owner;
newAttrs.group = attrs.group;
}

if (this.ussNode.attributes.perms !== attrs.perms) {
newAttrs.perms = attrs.perms;
}

await this.ussApi.updateAttributes(this.ussNode.fullPath, newAttrs);
this.ussNode.attributes = { ...(this.ussNode.attributes ?? {}), ...newAttrs } as FileAttributes;

await this.panel.webview.postMessage({
updated: true,
});
await Gui.infoMessage(`Updated file attributes for ${this.ussNode.fullPath}`);
}
} catch (err) {
await this.panel.webview.postMessage({
updated: false,
});
if (err instanceof Error) {
await Gui.errorMessage(`Failed to set file attributes for ${this.ussNode.fullPath}: ${err.toString()}`);
}
}
}
}
Loading

0 comments on commit 9b91418

Please sign in to comment.