-
Notifications
You must be signed in to change notification settings - Fork 92
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat,tests: make 'Edit Attributes' a WebView subclass; add unit tests
Signed-off-by: Trae Yelovich <[email protected]>
- Loading branch information
Showing
5 changed files
with
285 additions
and
89 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
packages/zowe-explorer/__tests__/__unit__/uss/AttributeView.unit.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
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()}`); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.