diff --git a/.gitignore b/.gitignore index c22795ad6e..9d2c8f6a74 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ testProfileData.ts results *.log npm-shrinkwrap.json -tsconfig.tsbuildinfo +**/*.tsbuildinfo vscode-extension-for-zowe*.vsix .vscode/settings.json .vscode/*.env diff --git a/package.json b/package.json index c62d3b5bfd..7590cc1b08 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "devDependencies": { "@types/jest": "^29.2.3", "@types/mocha": "^10.0.1", - "@types/node": "^14.18.12", + "@types/node": "^18.19.14", "@types/vscode": "^1.73.0", "@typescript-eslint/eslint-plugin": "^5.53.0", "@typescript-eslint/parser": "^5.53.0", diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index 4052861eab..5fbd38f074 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -10,6 +10,16 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - The new `getJobsByParameters` API is meant to replace `getJobsByOwnerAndPrefix`, and it adds new capabilities such as querying by status and limiting the amount of returned jobs. - **Breaking:** Removed string as a return type of the `uploadFromBuffer` method, since the z/OSMF API has been fixed to return a response object that includes an etag. [#2785](https://github.com/zowe/zowe-explorer-vscode/issues/2785) - Added `Commands` value to the `PersistenceSchemaEnum` enum for storing MVS, TSO, and USS command history. [#2788](https://github.com/zowe/zowe-explorer-vscode/issues/2788) +- Changed the type for the options parameter in the `getContents` function (`MainframeInteraction.IUss` and `MainframeInteraction.IMvs` interfaces) from `zosfiles.IDownloadOptions` to `zosfiles.IDownloadSingleOptions`. [#2207](https://github.com/zowe/zowe-explorer-vscode/issues/2207) + - The type was changed to match the function's intended behavior (to get the contents of a **single** resource). +- Added the `getEncoding` optional function to the `IZoweDatasetTreeNode` and `IZoweUSSTreeNode` interfaces. [#2207](https://github.com/zowe/zowe-explorer-vscode/issues/2207) + - **Breaking:** Removed the `encoding` property from the `IZoweUSSTreeNode` interface in favor of the new `getEncoding` function. [#2207](https://github.com/zowe/zowe-explorer-vscode/issues/2207) +- Added an optional function `nodeDataChanged` to the `IZoweTree` interface to signal an event when a tree node needs updated. [#2207](https://github.com/zowe/zowe-explorer-vscode/issues/2207) +- Added the optional `vscode.DragAndDropController` interface to the `IZoweTree` interface to allow Zowe tree views to support drag and drop. [#2207](https://github.com/zowe/zowe-explorer-vscode/issues/2207) +- Added a `ZoweScheme` enum to expose the core FileSystemProvider schemes for USS files, data sets and jobs. [#2207](https://github.com/zowe/zowe-explorer-vscode/issues/2207) +- Added optional function `move` to the `MainframeInteraction.IUss` interface to move USS folders/files from one path to another. [#2207](https://github.com/zowe/zowe-explorer-vscode/issues/2207) +- Added the `buildUniqueSpoolName` function to build spool names for Zowe resource URIs and VS Code editor tabs. [#2207](https://github.com/zowe/zowe-explorer-vscode/issues/2207) +- Added the `isNodeInEditor` function to determine whether a tree node is open in the editor. [#2207](https://github.com/zowe/zowe-explorer-vscode/issues/2207) ### Bug fixes diff --git a/packages/zowe-explorer-api/__mocks__/mockUtils.ts b/packages/zowe-explorer-api/__mocks__/mockUtils.ts new file mode 100644 index 0000000000..f790f210d0 --- /dev/null +++ b/packages/zowe-explorer-api/__mocks__/mockUtils.ts @@ -0,0 +1,138 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +// Used for the MockedProperty class (polyfills for Symbol.{asyncDispose, dispose}) +require("disposablestack/auto"); + +// Idea is borrowed from: https://github.com/kulshekhar/ts-jest/blob/master/src/util/testing.ts +export const mocked = any>(fn: T): jest.Mock> => fn as any; + +enum MockedValueType { + Primitive, + Ref, + Function, +} + +/** + * _Please use this when possible instead of Object.defineProperty!_ + * + * A safer approach to "mocking" the value for a property that cannot be easily mocked using Jest.\ + * Uses TypeScript 5.2's Explicit Resource Management to restore the original value for the given object and property key. + */ +export class MockedProperty { + #key: PropertyKey; + #val: any; + #valType: MockedValueType; + #objRef: any; + #originalDescriptor?: PropertyDescriptor; + + private initValueType() { + if (typeof this.#val === "function" || jest.isMockFunction(this.#val)) { + this.#valType = MockedValueType.Function; + } else if (typeof this.#val === "object" || Array.isArray(this.#val)) { + this.#valType = MockedValueType.Ref; + } else { + this.#valType = MockedValueType.Primitive; + } + } + + constructor(object: any, key: PropertyKey, descriptor?: PropertyDescriptor, value?: any) { + if (object == null) { + throw new Error("Null or undefined object passed to MockedProperty"); + } + this.#objRef = object; + this.#originalDescriptor = descriptor ?? Object.getOwnPropertyDescriptor(object, key); + + if (!value) { + this.#val = jest.fn(); + this.#valType = MockedValueType.Function; + Object.defineProperty(object, key, { + value: this.#val, + configurable: true, + }); + return; + } + + const isValFn = typeof value === "function"; + + if (isValFn || (typeof descriptor?.value === "function" && value == null)) { + // wrap provided function around a Jest function, if needed + this.#val = jest.isMockFunction(value) ? value : jest.fn().mockImplementation(value); + } else { + this.#val = value; + } + + this.initValueType(); + + Object.defineProperty(object, key, { + value: this.#val, + configurable: true, + }); + } + + [Symbol.dispose](): void { + const isObjValid = this.#objRef != null; + if (isObjValid && !this.#originalDescriptor) { + // didn't exist to begin with, just delete it + delete this.#objRef[this.#key]; + return; + } + + if (this.#valType === MockedValueType.Function && jest.isMockFunction(this.#val)) { + this.#val.mockRestore(); + } + + if (isObjValid) { + Object.defineProperty(this.#objRef, this.#key, this.#originalDescriptor!); + } + } + + public get mock() { + if (!jest.isMockFunction(this.#val)) { + throw Error("MockedValue.mock called, but mocked value is not a Jest function"); + } + + return this.#val; + } + + public get value() { + return this.#val; + } + + public valueAs() { + return this.#val as T; + } +} + +export function isMockedProperty(val: any): val is MockedProperty { + return "Symbol.dispose" in val; +} + +export class MockCollection { + #obj: Record; + + constructor(obj: Record) { + this.#obj = obj; + } + + [Symbol.dispose](): void { + for (const k of Object.keys(this.#obj)) { + const property = this.#obj[k]; + if (isMockedProperty(property)) { + property[Symbol.dispose](); + } + } + } + + public dispose() { + this[Symbol.dispose](); + } +} diff --git a/packages/zowe-explorer-api/__mocks__/vscode.ts b/packages/zowe-explorer-api/__mocks__/vscode.ts index b278acb18f..dededd2a0b 100644 --- a/packages/zowe-explorer-api/__mocks__/vscode.ts +++ b/packages/zowe-explorer-api/__mocks__/vscode.ts @@ -148,7 +148,139 @@ export namespace extensions { export interface QuickPickItem {} export interface QuickPick {} +export enum QuickPickItemKind { + Separator = -1, + Default = 0, +} + +/** + * Represents a tab within a {@link TabGroup group of tabs}. + * Tabs are merely the graphical representation within the editor area. + * A backing editor is not a guarantee. + */ +export interface Tab { + /** + * The text displayed on the tab. + */ + readonly label: string; + + /** + * The group which the tab belongs to. + */ + readonly group: TabGroup; + + /** + * Defines the structure of the tab i.e. text, notebook, custom, etc. + * Resource and other useful properties are defined on the tab kind. + */ + readonly input: unknown; + + /** + * Whether or not the tab is currently active. + * This is dictated by being the selected tab in the group. + */ + readonly isActive: boolean; + + /** + * Whether or not the dirty indicator is present on the tab. + */ + readonly isDirty: boolean; + + /** + * Whether or not the tab is pinned (pin icon is present). + */ + readonly isPinned: boolean; + + /** + * Whether or not the tab is in preview mode. + */ + readonly isPreview: boolean; +} + +/** + * Represents a group of tabs. A tab group itself consists of multiple tabs. + */ +export interface TabGroup { + /** + * Whether or not the group is currently active. + * + * *Note* that only one tab group is active at a time, but that multiple tab + * groups can have an {@link activeTab active tab}. + * + * @see {@link Tab.isActive} + */ + readonly isActive: boolean; + + /** + * The view column of the group. + */ + readonly viewColumn: ViewColumn; + + /** + * The active {@link Tab tab} in the group. This is the tab whose contents are currently + * being rendered. + * + * *Note* that there can be one active tab per group but there can only be one {@link TabGroups.activeTabGroup active group}. + */ + readonly activeTab: Tab | undefined; + + /** + * The list of tabs contained within the group. + * This can be empty if the group has no tabs open. + */ + readonly tabs: readonly Tab[]; +} + +/** + * Represents the main editor area which consists of multiple groups which contain tabs. + */ +export interface TabGroups { + /** + * All the groups within the group container. + */ + readonly all: readonly TabGroup[]; + + /** + * The currently active group. + */ + readonly activeTabGroup: TabGroup; + + /** + * Closes the tab. This makes the tab object invalid and the tab + * should no longer be used for further actions. + * Note: In the case of a dirty tab, a confirmation dialog will be shown which may be cancelled. If cancelled the tab is still valid + * + * @param tab The tab to close. + * @param preserveFocus When `true` focus will remain in its current position. If `false` it will jump to the next tab. + * @returns A promise that resolves to `true` when all tabs have been closed. + */ + close(tab: Tab | readonly Tab[], preserveFocus?: boolean): Thenable; + + /** + * Closes the tab group. This makes the tab group object invalid and the tab group + * should no longer be used for further actions. + * @param tabGroup The tab group to close. + * @param preserveFocus When `true` focus will remain in its current position. + * @returns A promise that resolves to `true` when all tab groups have been closed. + */ + close(tabGroup: TabGroup | readonly TabGroup[], preserveFocus?: boolean): Thenable; +} + export namespace window { + /** + * Represents the grid widget within the main editor area + */ + export const tabGroups: TabGroups = { + all: [], + activeTabGroup: { + isActive: true, + viewColumn: ViewColumn.One, + activeTab: undefined, + tabs: [], + }, + close: jest.fn(), + }; + /** * Show an information message to users. Optionally provide an array of items which will be presented as * clickable buttons. @@ -286,6 +418,185 @@ export interface TreeDataProvider { getParent?(element: T): ProviderResult; } +export class Uri { + public static file(path: string): Uri { + return Uri.parse(path); + } + public static parse(value: string, strict?: boolean): Uri { + const newUri = new Uri(); + newUri.path = value; + + return newUri; + } + + public with(change: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): Uri { + let newUri = Uri.from(this); + + if (change.scheme) { + newUri.scheme = change.scheme; + } + + if (change.authority) { + newUri.authority = change.authority; + } + + if (change.path) { + newUri.path = change.path; + } + + if (change.query) { + newUri.query = change.query; + } + + if (change.fragment) { + newUri.fragment = change.fragment; + } + + return newUri !== this ? newUri : this; + } + + public static from(components: { + readonly scheme: string; + readonly authority?: string; + readonly path?: string; + readonly query?: string; + readonly fragment?: string; + }): Uri { + let uri = new Uri(); + if (components.path) { + uri.path = components.path; + } + if (components.scheme) { + uri.scheme = components.scheme; + } + if (components.authority) { + uri.authority = components.authority; + } + if (components.query) { + uri.query = components.query; + } + if (components.fragment) { + uri.fragment = components.fragment; + } + return uri; + } + + /** + * Scheme is the `http` part of `http://www.example.com/some/path?query#fragment`. + * The part before the first colon. + */ + scheme: string; + + /** + * Authority is the `www.example.com` part of `http://www.example.com/some/path?query#fragment`. + * The part between the first double slashes and the next slash. + */ + authority: string; + + /** + * Path is the `/some/path` part of `http://www.example.com/some/path?query#fragment`. + */ + path: string; + + /** + * Query is the `query` part of `http://www.example.com/some/path?query#fragment`. + */ + query: string; + + /** + * Fragment is the `fragment` part of `http://www.example.com/some/path?query#fragment`. + */ + fragment: string; + + /** + * The string representing the corresponding file system path of this Uri. + * + * Will handle UNC paths and normalize windows drive letters to lower-case. Also + * uses the platform specific path separator. + * + * * Will *not* validate the path for invalid characters and semantics. + * * Will *not* look at the scheme of this Uri. + * * The resulting string shall *not* be used for display purposes but + * for disk operations, like `readFile` et al. + * + * The *difference* to the {@linkcode Uri.path path}-property is the use of the platform specific + * path separator and the handling of UNC paths. The sample below outlines the difference: + * ```ts + * const u = URI.parse('file://server/c$/folder/file.txt') + * u.authority === 'server' + * u.path === '/shares/c$/file.txt' + * u.fsPath === '\\server\c$\folder\file.txt' + * ``` + */ + fsPath: string; + + public toString(): string { + let result = this.scheme ? `${this.scheme}://` : ""; + + if (this.authority) { + result += `${this.authority}`; + } + + if (this.path) { + result += `${this.path}`; + } + + if (this.query) { + result += `?${this.query}`; + } + + if (this.fragment) { + result += `#${this.fragment}`; + } + + return result; + } +} + +/** + * Enumeration of file types. The types `File` and `Directory` can also be + * a symbolic links, in that case use `FileType.File | FileType.SymbolicLink` and + * `FileType.Directory | FileType.SymbolicLink`. + */ +export enum FileType { + /** + * The file type is unknown. + */ + Unknown = 0, + /** + * A regular file. + */ + File = 1, + /** + * A directory. + */ + Directory = 2, + /** + * A symbolic link to a file. + */ + SymbolicLink = 64, +} + +export namespace l10n { + export function t( + options: + | { + message: string; + args?: Array | Record; + comment?: string | string[]; + } + | string + ): string { + if (typeof options === "string") { + return options; + } + options.args?.forEach((arg: string, i: number) => { + options.message = options.message.replace(`{${i}}`, arg); + }); + return options.message; + } +} + export class TreeItem { /** * A human-readable string describing this item. When `falsy`, it is derived from [resourceUri](#TreeItem.resourceUri). @@ -411,6 +722,167 @@ export class EventEmitter { //dispose(): void; } +export enum FilePermission { + /** + * The file is readonly. + * + * *Note:* All `FileStat` from a `FileSystemProvider` that is registered with + * the option `isReadonly: true` will be implicitly handled as if `FilePermission.Readonly` + * is set. As a consequence, it is not possible to have a readonly file system provider + * registered where some `FileStat` are not readonly. + */ + Readonly = 1, +} + +/** + * The `FileStat`-type represents metadata about a file + */ +export interface FileStat { + /** + * The type of the file, e.g. is a regular file, a directory, or symbolic link + * to a file. + * + * *Note:* This value might be a bitmask, e.g. `FileType.File | FileType.SymbolicLink`. + */ + type: FileType; + /** + * The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ + ctime: number; + /** + * The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + * + * *Note:* If the file changed, it is important to provide an updated `mtime` that advanced + * from the previous value. Otherwise there may be optimizations in place that will not show + * the updated file contents in an editor for example. + */ + mtime: number; + /** + * The size in bytes. + * + * *Note:* If the file changed, it is important to provide an updated `size`. Otherwise there + * may be optimizations in place that will not show the updated file contents in an editor for + * example. + */ + size: number; + /** + * The permissions of the file, e.g. whether the file is readonly. + * + * *Note:* This value might be a bitmask, e.g. `FilePermission.Readonly | FilePermission.Other`. + */ + permissions?: FilePermission; +} + +/** + * Enumeration of file change types. + */ +export enum FileChangeType { + /** + * The contents or metadata of a file have changed. + */ + Changed = 1, + + /** + * A file has been created. + */ + Created = 2, + + /** + * A file has been deleted. + */ + Deleted = 3, +} + +/** + * The event filesystem providers must use to signal a file change. + */ +export interface FileChangeEvent { + /** + * The type of change. + */ + readonly type: FileChangeType; + + /** + * The uri of the file that has changed. + */ + readonly uri: Uri; +} + +/** + * A type that filesystem providers should use to signal errors. + * + * This class has factory methods for common error-cases, like `FileNotFound` when + * a file or folder doesn't exist, use them like so: `throw vscode.FileSystemError.FileNotFound(someUri);` + */ +export class FileSystemError extends Error { + /** + * Create an error to signal that a file or folder wasn't found. + * @param messageOrUri Message or uri. + */ + static FileNotFound(messageOrUri?: string | Uri): FileSystemError { + return new FileSystemError("file not found"); + } + + /** + * Create an error to signal that a file or folder already exists, e.g. when + * creating but not overwriting a file. + * @param messageOrUri Message or uri. + */ + static FileExists(messageOrUri?: string | Uri): FileSystemError { + return new FileSystemError("file exists"); + } + + /** + * Create an error to signal that a file is not a folder. + * @param messageOrUri Message or uri. + */ + static FileNotADirectory(messageOrUri?: string | Uri): FileSystemError { + return new FileSystemError("file not a directory"); + } + + /** + * Create an error to signal that a file is a folder. + * @param messageOrUri Message or uri. + */ + static FileIsADirectory(messageOrUri?: string | Uri): FileSystemError { + return new FileSystemError("file is a directory"); + } + + /** + * Create an error to signal that an operation lacks required permissions. + * @param messageOrUri Message or uri. + */ + static NoPermissions(messageOrUri?: string | Uri): FileSystemError { + return new FileSystemError("no permissions"); + } + + /** + * Create an error to signal that the file system is unavailable or too busy to + * complete a request. + * @param messageOrUri Message or uri. + */ + static Unavailable(messageOrUri?: string | Uri): FileSystemError { + return new FileSystemError("unavailable"); + } + + /** + * Creates a new filesystem error. + * + * @param messageOrUri Message or uri. + */ + constructor(messageOrUri?: string | Uri) { + super(typeof messageOrUri === "string" ? messageOrUri : undefined); + } + + /** + * A code that identifies this error. + * + * Possible values are names of errors, like {@linkcode FileSystemError.FileNotFound FileNotFound}, + * or `Unknown` for unspecified errors. + */ + readonly code: string; +} + /** * Namespace for dealing with the current workspace. A workspace is the representation * of the folder that has been opened. There is no workspace when just a file but not a @@ -421,6 +893,7 @@ export class EventEmitter { * the editor-process so that they should be always used instead of nodejs-equivalents. */ export namespace workspace { + export const textDocuments: TextDocument[] = []; export function getConfiguration(configuration: string) { return { update: () => { @@ -440,6 +913,10 @@ export namespace workspace { }; } + export function onDidCloseTextDocument(event) { + return Disposable; + } + export function onWillSaveTextDocument(event) { return Disposable; } @@ -478,6 +955,134 @@ export namespace workspace { */ readonly index: number; } + + export namespace fs { + /** + * Retrieve metadata about a file. + * + * Note that the metadata for symbolic links should be the metadata of the file they refer to. + * Still, the {@link FileType.SymbolicLink SymbolicLink}-type must be used in addition to the actual type, e.g. + * `FileType.SymbolicLink | FileType.Directory`. + * + * @param uri The uri of the file to retrieve metadata about. + * @returns The file metadata about the file. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist. + */ + export function stat(uri: Uri): FileStat | Thenable { + return {} as FileStat; + } + + /** + * Retrieve all entries of a {@link FileType.Directory directory}. + * + * @param uri The uri of the folder. + * @returns An array of name/type-tuples or a thenable that resolves to such. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist. + */ + export function readDirectory(uri: Uri): Array<[string, FileType]> | Thenable> { + return []; + } + + /** + * Create a new directory (Note, that new files are created via `write`-calls). + * + * @param uri The uri of the new folder. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when the parent of `uri` doesn't exist, e.g. no mkdirp-logic required. + * @throws {@linkcode FileSystemError.FileExists FileExists} when `uri` already exists. + * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. + */ + export function createDirectory(uri: Uri): void | Thenable { + return; + } + + /** + * Read the entire contents of a file. + * + * @param uri The uri of the file. + * @returns An array of bytes or a thenable that resolves to such. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist. + */ + export function readFile(uri: Uri): Uint8Array | Thenable { + return new Uint8Array(); + } + + /** + * Write data to a file, replacing its entire contents. + * + * @param uri The uri of the file. + * @param content The new content of the file. + * @param options Defines if missing files should or must be created. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist and `create` is not set. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when the parent of `uri` doesn't exist and `create` is set, e.g. no mkdirp-logic required. + * @throws {@linkcode FileSystemError.FileExists FileExists} when `uri` already exists, `create` is set but `overwrite` is not set. + * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. + */ + export function writeFile( + uri: Uri, + content: Uint8Array, + options: { + /** + * Create the file if it does not exist already. + */ + readonly create: boolean; + /** + * Overwrite the file if it does exist. + */ + readonly overwrite: boolean; + } + ): void | Thenable { + return; + } + + /** + * Rename a file or folder. + * + * @param oldUri The existing file. + * @param newUri The new location. + * @param options Defines if existing files should be overwritten. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `oldUri` doesn't exist. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when parent of `newUri` doesn't exist, e.g. no mkdirp-logic required. + * @throws {@linkcode FileSystemError.FileExists FileExists} when `newUri` exists and when the `overwrite` option is not `true`. + * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. + */ + export function rename( + oldUri: Uri, + newUri: Uri, + options: { + /** + * Overwrite the file if it does exist. + */ + readonly overwrite: boolean; + } + ): void | Thenable { + return; + } + + /** + * Copy files or folders. Implementing this function is optional but it will speedup + * the copy operation. + * + * @param source The existing file. + * @param destination The destination location. + * @param options Defines if existing files should be overwritten. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `source` doesn't exist. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when parent of `destination` doesn't exist, e.g. no mkdirp-logic required. + * @throws {@linkcode FileSystemError.FileExists FileExists} when `destination` exists and when the `overwrite` option is not `true`. + * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. + */ + export function copy( + source: Uri, + destination: Uri, + options: { + /** + * Overwrite the file if it does exist. + */ + readonly overwrite: boolean; + } + ): void | Thenable { + return; + } + } } export interface InputBoxOptions { diff --git a/packages/zowe-explorer-api/__tests__/__unit__/fs/BaseProvider.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/fs/BaseProvider.unit.test.ts new file mode 100644 index 0000000000..6d98611831 --- /dev/null +++ b/packages/zowe-explorer-api/__tests__/__unit__/fs/BaseProvider.unit.test.ts @@ -0,0 +1,638 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import * as vscode from "vscode"; +import { BaseProvider, ConflictViewSelection, DirEntry, FileEntry, ZoweScheme } from "../../../src/fs"; +import { Gui } from "../../../src/globals"; +import { MockedProperty } from "../../../__mocks__/mockUtils"; + +function getGlobalMocks() { + return { + testFileUri: vscode.Uri.from({ scheme: ZoweScheme.USS, path: "/file.txt" }), + testFolderUri: vscode.Uri.from({ scheme: ZoweScheme.USS, path: "/folder" }), + fileFsEntry: { + name: "file.txt", + conflictData: null, + data: new Uint8Array([1, 2, 3]), + metadata: { + path: "/file.txt", + }, + wasAccessed: true, + type: vscode.FileType.File, + }, + folderFsEntry: { + name: "folder", + metadata: { + path: "/folder", + }, + entries: new Map(), + wasAccessed: true, + type: vscode.FileType.Directory, + }, + }; +} +const globalMocks = getGlobalMocks(); + +describe("diffOverwrite", () => { + function getBlockMocks() { + return { + lookupAsFileMock: jest.spyOn((BaseProvider as any).prototype, "_lookupAsFile"), + writeFileMock: jest.spyOn(vscode.workspace.fs, "writeFile").mockImplementation(), + }; + } + + it("calls writeFile if URI exists in the file system", async () => { + const blockMocks = getBlockMocks(); + const fsEntry = { + ...globalMocks.fileFsEntry, + conflictData: { + contents: new Uint8Array([4, 5, 6]), + etag: undefined, + size: 3, + }, + }; + const statusBarMsgMock = jest.spyOn(Gui, "setStatusBarMessage").mockImplementation(); + blockMocks.lookupAsFileMock.mockReturnValueOnce(fsEntry); + + const prov = new (BaseProvider as any)(); + await prov.diffOverwrite(globalMocks.testFileUri); + + expect(blockMocks.lookupAsFileMock).toHaveBeenCalled(); + expect(blockMocks.writeFileMock).toHaveBeenCalledWith( + globalMocks.testFileUri.with({ + query: "forceUpload=true", + }), + fsEntry.data + ); + blockMocks.writeFileMock.mockClear(); + expect(statusBarMsgMock.mock.calls[0][0]).toBe("$(check) Overwrite applied for file.txt"); + expect(fsEntry.conflictData).toBeNull(); + }); + + it("returns early if the URI is not present in the file system", async () => { + const blockMocks = getBlockMocks(); + blockMocks.lookupAsFileMock.mockReturnValueOnce(undefined); + const prov = new (BaseProvider as any)(); + await prov.diffOverwrite(globalMocks.testFileUri); + expect(blockMocks.lookupAsFileMock).toHaveBeenCalledWith(globalMocks.testFileUri); + expect(blockMocks.writeFileMock).not.toHaveBeenCalled(); + }); +}); + +describe("diffUseRemote", () => { + function getBlockMocks(prov) { + return { + lookupAsFileMock: jest.spyOn(prov, "_lookupAsFile"), + writeFileMock: jest.spyOn(vscode.workspace.fs, "writeFile").mockImplementation(), + }; + } + + it("calls writeFile if the final data is different from the conflict data", async () => { + const conflictArr = new Uint8Array([4, 5, 6]); + const fsEntry = { + ...globalMocks.fileFsEntry, + conflictData: { + contents: conflictArr, + etag: undefined, + size: 3, + }, + }; + const statusBarMsgMock = jest.spyOn(Gui, "setStatusBarMessage").mockImplementation(); + const executeCommandMock = jest.spyOn(vscode.commands, "executeCommand").mockResolvedValueOnce(undefined); + + const prov = new (BaseProvider as any)(); + const blockMocks = getBlockMocks(prov); + blockMocks.lookupAsFileMock.mockReturnValueOnce(fsEntry); + await prov.diffUseRemote(globalMocks.testFileUri); + + expect(blockMocks.lookupAsFileMock).toHaveBeenCalled(); + expect(blockMocks.writeFileMock).toHaveBeenCalledWith( + globalMocks.testFileUri.with({ + query: "forceUpload=true", + }), + conflictArr + ); + blockMocks.writeFileMock.mockClear(); + expect(statusBarMsgMock.mock.calls[0][0]).toBe("$(check) Overwrite applied for file.txt"); + expect(fsEntry.conflictData).toBeNull(); + expect(executeCommandMock).toHaveBeenCalledWith("workbench.action.closeActiveEditor"); + }); + + it("does not call writeFile if the final data is the same as the conflict data", async () => { + const fsEntry = { + ...globalMocks.fileFsEntry, + conflictData: { + contents: new Uint8Array([1, 2, 3]), + etag: undefined, + size: 3, + }, + }; + const statusBarMsgMock = jest.spyOn(Gui, "setStatusBarMessage").mockImplementation(); + const executeCommandMock = jest.spyOn(vscode.commands, "executeCommand").mockResolvedValueOnce(undefined); + + const prov = new (BaseProvider as any)(); + const blockMocks = getBlockMocks(prov); + blockMocks.lookupAsFileMock.mockReturnValueOnce(fsEntry); + await prov.diffUseRemote(globalMocks.testFileUri); + + expect(blockMocks.lookupAsFileMock).toHaveBeenCalled(); + expect(blockMocks.writeFileMock).not.toHaveBeenCalled(); + expect(statusBarMsgMock.mock.calls[0][0]).toBe("$(check) Overwrite applied for file.txt"); + expect(fsEntry.conflictData).toBeNull(); + expect(executeCommandMock).toHaveBeenCalledWith("workbench.action.closeActiveEditor"); + }); + + it("returns early if the URI is not present in the file system", async () => { + const prov = new (BaseProvider as any)(); + const blockMocks = getBlockMocks(prov); + blockMocks.lookupAsFileMock.mockReturnValueOnce(undefined); + await prov.diffUseRemote(globalMocks.testFileUri); + expect(blockMocks.lookupAsFileMock).toHaveBeenCalledWith(globalMocks.testFileUri); + expect(blockMocks.writeFileMock).not.toHaveBeenCalled(); + }); +}); + +describe("exists", () => { + function getBlockMocks() { + return { + lookupMock: jest.spyOn((BaseProvider as any).prototype, "_lookup"), + }; + } + + afterAll(() => { + const blockMocks = getBlockMocks(); + blockMocks.lookupMock.mockRestore(); + }); + + it("returns false when a URI does not exist in the provider", () => { + const blockMocks = getBlockMocks(); + blockMocks.lookupMock.mockReturnValueOnce({ ...globalMocks.fileFsEntry }); + const prov: BaseProvider = new (BaseProvider as any)(); + expect(prov.exists(globalMocks.testFileUri)).toBe(true); + expect(blockMocks.lookupMock).toHaveBeenCalledWith(globalMocks.testFileUri, true); + }); + + it("returns true when a URI exists in the provider", () => { + const blockMocks = getBlockMocks(); + const prov: BaseProvider = new (BaseProvider as any)(); + expect(prov.exists(globalMocks.testFileUri)).toBe(false); + expect(blockMocks.lookupMock).toHaveBeenCalledWith(globalMocks.testFileUri, true); + }); +}); + +describe("getEncodingForFile", () => { + it("gets the encoding for a file entry", () => { + const prov = new (BaseProvider as any)(); + const fileEntry = { ...globalMocks.fileFsEntry, encoding: { kind: "text" } }; + const _lookupAsFileMock = jest.spyOn(prov, "_lookup").mockReturnValueOnce(fileEntry); + expect(prov.getEncodingForFile(globalMocks.testFileUri)).toStrictEqual({ kind: "text" }); + _lookupAsFileMock.mockRestore(); + }); +}); + +describe("removeEntry", () => { + it("returns true if it successfully removed an entry", () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.entries.set("file.txt", { ...globalMocks.fileFsEntry }); + + expect(prov.removeEntry(globalMocks.testFileUri)).toBe(true); + }); + + it("returns false if it couldn't find the entry", () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + + expect(prov.removeEntry(globalMocks.testFileUri)).toBe(false); + }); + + it("returns early if it could not find the parent directory of the item to remove", () => { + const parentDirMock = jest.spyOn(BaseProvider.prototype as any, "_lookupParentDirectory").mockReturnValue(undefined); + const prov = new (BaseProvider as any)(); + expect(prov.removeEntry(globalMocks.testFileUri)).toBe(false); + parentDirMock.mockRestore(); + }); +}); + +describe("cacheOpenedUri", () => { + it("caches the URI for later use", () => { + const prov = new (BaseProvider as any)(); + prov.cacheOpenedUri(globalMocks.testFileUri); + expect(prov.openedUris).toContain(globalMocks.testFileUri); + }); +}); + +describe("invalidateFileAtUri", () => { + it("returns true if it was able to invalidate the URI", () => { + const fileEntry = { ...globalMocks.fileFsEntry }; + jest.spyOn((BaseProvider as any).prototype, "_lookup").mockReturnValueOnce(fileEntry); + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.entries.set("file.txt", fileEntry); + expect(prov.invalidateFileAtUri(globalMocks.testFileUri)).toBe(true); + }); + + it("returns false if the entry is not a file or undefined", () => { + // case 1: folder + const folderEntry = { ...globalMocks.folderFsEntry }; + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.entries.set("folder", folderEntry); + expect(prov.invalidateFileAtUri(globalMocks.testFolderUri)).toBe(false); + + // case 2: undefined + expect(prov.invalidateFileAtUri(globalMocks.testFileUri)).toBe(false); + }); +}); + +describe("invalidateDirAtUri", () => { + it("returns true if it was able to invalidate the URI", () => { + const folderEntry = { ...globalMocks.folderFsEntry }; + jest.spyOn((BaseProvider as any).prototype, "_lookup").mockReturnValueOnce(folderEntry); + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.entries.set("folder", folderEntry); + expect(prov.invalidateDirAtUri(globalMocks.testFolderUri)).toBe(true); + expect(folderEntry.entries.size).toBe(0); + }); + + it("returns false if the entry is not a folder or undefined", () => { + // case 1: file + const fileEntry = { ...globalMocks.fileFsEntry }; + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.entries.set("file.txt", fileEntry); + expect(prov.invalidateDirAtUri(globalMocks.testFileUri)).toBe(false); + + // case 2: undefined + expect(prov.invalidateDirAtUri(globalMocks.testFolderUri)).toBe(false); + }); +}); + +describe("setEncodingForFile", () => { + it("sets the encoding for a file entry", () => { + const prov = new (BaseProvider as any)(); + const fileEntry = { ...globalMocks.fileFsEntry, encoding: undefined }; + const _lookupAsFileMock = jest.spyOn(prov, "_lookup").mockReturnValueOnce(fileEntry); + prov.setEncodingForFile(globalMocks.testFileUri, { kind: "text" }); + expect(fileEntry.encoding).toStrictEqual({ kind: "text" }); + _lookupAsFileMock.mockRestore(); + }); +}); + +describe("_updateResourceInEditor", () => { + it("executes vscode.open and workbench.action.files.revert commands", async () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.entries.set("file.txt", { + name: "file.txt", + data: new Uint8Array([1, 2, 3]), + wasAccessed: true, + type: vscode.FileType.File, + }); + const executeCommandMock = jest.spyOn(vscode.commands, "executeCommand").mockResolvedValueOnce(undefined).mockResolvedValueOnce(undefined); + await prov._updateResourceInEditor(globalMocks.testFileUri); + expect(executeCommandMock).toHaveBeenCalledWith("vscode.open", globalMocks.testFileUri); + expect(executeCommandMock).toHaveBeenCalledWith("workbench.action.files.revert"); + }); + + it("returns early if the provided URI is not a file entry", async () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + + const executeCommandSpy = jest.spyOn(vscode.commands, "executeCommand").mockClear(); + await prov._updateResourceInEditor(globalMocks.testFolderUri); + expect(executeCommandSpy).not.toHaveBeenCalled(); + }); +}); + +describe("_lookup", () => { + it("returns a valid file entry if it exists in the file system", () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.entries.set("file.txt", { ...globalMocks.fileFsEntry }); + const entry = prov._lookup(globalMocks.testFileUri); + expect(entry).toStrictEqual(globalMocks.fileFsEntry); + }); + + it("returns a valid folder entry if it exists in the file system", () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.entries.set("folder", { ...globalMocks.folderFsEntry }); + const entry = prov._lookup(globalMocks.testFolderUri); + expect(entry).toStrictEqual(globalMocks.folderFsEntry); + }); +}); + +describe("_lookupAsDirectory", () => { + it("returns a valid entry if it exists in the file system", () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.entries.set("folder", { ...globalMocks.folderFsEntry }); + const entry = prov._lookupAsDirectory(globalMocks.testFolderUri); + expect(entry).toStrictEqual(globalMocks.folderFsEntry); + }); + + it("throws an error if the provided URI is a file", () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.entries.set("file.txt", { ...globalMocks.fileFsEntry }); + expect((): unknown => prov._lookupAsDirectory(globalMocks.testFileUri)).toThrow("file not a directory"); + }); +}); + +describe("_lookupAsFile", () => { + it("returns a valid entry if it exists in the file system", () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + const fileEntry = new FileEntry(globalMocks.fileFsEntry.name); + prov.root.entries.set("file.txt", fileEntry); + expect(prov._lookupAsFile(globalMocks.testFileUri)).toBe(fileEntry); + }); + + it("throws an error if the provided URI is a directory", () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.entries.set("folder", globalMocks.folderFsEntry); + expect((): any => prov._lookupAsFile(globalMocks.testFolderUri)).toThrow("file is a directory"); + }); +}); + +describe("_lookupParentDirectory", () => { + it("calls lookupAsDirectory for a given URI", () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.entries.set("folder", { ...globalMocks.folderFsEntry }); + + const lookupAsDirSpy = jest.spyOn((BaseProvider as any).prototype, "_lookupAsDirectory"); + expect(prov._lookupParentDirectory(globalMocks.testFolderUri)).toBe(prov.root); + expect(lookupAsDirSpy).toHaveBeenCalledWith( + vscode.Uri.from({ + scheme: ZoweScheme.USS, + path: "/", + }), + false + ); + }); +}); + +describe("_updateChildPaths", () => { + it("updates the paths for all child entries", () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + const relocatedFolder = { ...globalMocks.folderFsEntry }; + const relocatedFile = { ...globalMocks.fileFsEntry }; + prov.root.entries.set("folder", relocatedFolder); + prov.root.entries.set("file.txt", relocatedFile); + prov.root.metadata = { + path: "/root/", + }; + prov._updateChildPaths(prov.root); + expect(relocatedFile.metadata.path).toBe("/root/file.txt"); + expect(relocatedFolder.metadata.path).toBe("/root/folder"); + }); +}); + +describe("_getDeleteInfo", () => { + it("returns the correct deletion info for a URI", () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.entries.set("folder", globalMocks.folderFsEntry); + expect(prov._getDeleteInfo(globalMocks.testFolderUri)).toStrictEqual({ + entryToDelete: globalMocks.folderFsEntry, + parent: prov.root, + parentUri: vscode.Uri.from({ + scheme: ZoweScheme.USS, + path: "/", + }), + }); + }); + + it("throws an error if given an invalid URI", () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + expect((): unknown => prov._getDeleteInfo(globalMocks.testFolderUri)).toThrow("file not found"); + }); +}); + +describe("_createFile", () => { + it("successfully creates a file entry", async () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.metadata = { + profile: { name: "testProfile" } as any, + path: "/", + }; + + const entry = await prov._createFile(globalMocks.testFileUri); + expect(entry.metadata.path).toBe(globalMocks.testFileUri.path); + }); + + it("throws an error if the file already exists and overwrite is true", async () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.metadata = { + profile: { name: "testProfile" } as any, + path: "/", + }; + + const entry = await prov._createFile(globalMocks.testFileUri); + expect(entry.metadata.path).toBe(globalMocks.testFileUri.path); + + try { + await prov._createFile(globalMocks.testFileUri, { overwrite: true }); + } catch (err) { + expect(err.message).toBe("file exists"); + } + }); + + it("throws an error if a folder already exists with the same URI", async () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.metadata = { + profile: { name: "testProfile" } as any, + path: "/", + }; + const dirEntry = new DirEntry("file.txt"); + dirEntry.metadata = { + ...prov.root.metadata, + path: "/file.txt", + }; + prov.root.entries.set("file.txt", dirEntry); + jest.spyOn((BaseProvider as any).prototype, "_lookupParentDirectory").mockReturnValueOnce(prov.root); + expect((): unknown => prov._createFile(globalMocks.testFileUri)).toThrow("file is a directory"); + }); +}); + +describe("_fireSoon", () => { + jest.useFakeTimers(); + + it("adds to bufferedEvents and calls setTimeout", () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + jest.spyOn(global, "setTimeout"); + prov._fireSoon({ + type: vscode.FileChangeType.Deleted, + uri: globalMocks.testFileUri, + }); + expect(prov._bufferedEvents.length).toBe(1); + expect(setTimeout).toHaveBeenCalled(); + }); + + it("calls clearTimeout if fireSoonHandle is defined", () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + jest.spyOn(global, "setTimeout"); + jest.spyOn(global, "clearTimeout"); + prov._fireSoon({ + type: vscode.FileChangeType.Deleted, + uri: globalMocks.testFileUri, + }); + expect(prov._bufferedEvents.length).toBe(1); + expect(setTimeout).toHaveBeenCalled(); + + prov._fireSoon({ + type: vscode.FileChangeType.Created, + uri: globalMocks.testFileUri, + }); + expect(clearTimeout).toHaveBeenCalled(); + }); +}); + +describe("_handleConflict", () => { + it("returns 'ConflictViewSelection.UserDismissed' when user dismisses conflict prompt", async () => { + jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce(undefined); + const prov = new (BaseProvider as any)(); + expect(await prov._handleConflict(globalMocks.testFileUri, globalMocks.fileFsEntry)).toBe(ConflictViewSelection.UserDismissed); + }); + + it("returns 'ConflictViewSelection.Compare' when user selects 'Compare'", async () => { + jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("Compare"); + const prov = new (BaseProvider as any)(); + const onDidCloseTextDocMock = jest.spyOn(vscode.workspace, "onDidCloseTextDocument"); + const executeCmdMock = jest.spyOn(vscode.commands, "executeCommand").mockResolvedValueOnce(undefined); + expect(await prov._handleConflict(globalMocks.testFileUri, globalMocks.fileFsEntry)).toBe(ConflictViewSelection.Compare); + expect(onDidCloseTextDocMock).toHaveBeenCalled(); + expect(executeCmdMock).toHaveBeenCalledWith( + "vscode.diff", + globalMocks.testFileUri.with({ + query: "conflict=true", + }), + globalMocks.testFileUri.with({ + query: "inDiff=true", + }), + "file.txt (Remote) ↔ file.txt" + ); + executeCmdMock.mockRestore(); + }); + + it("returns 'ConflictViewSelection.Overwrite' when user selects 'Overwrite'", async () => { + jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("Overwrite"); + const prov = new (BaseProvider as any)(); + const diffOverwriteMock = jest.spyOn(prov, "diffOverwrite").mockImplementation(); + expect(await prov._handleConflict(globalMocks.testFileUri, globalMocks.fileFsEntry)).toBe(ConflictViewSelection.Overwrite); + expect(diffOverwriteMock).toHaveBeenCalledWith(globalMocks.testFileUri); + }); +}); + +describe("_relocateEntry", () => { + it("returns early if the entry does not exist in the file system", () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + const lookupAsDirMock = jest.spyOn(prov, "_lookupAsDirectory").mockReturnValueOnce(undefined); + lookupAsDirMock.mockClear(); + + prov._relocateEntry( + globalMocks.testFileUri, + globalMocks.testFileUri.with({ + path: "/file2.txt", + }), + "/file2.txt" + ); + expect(lookupAsDirMock).not.toHaveBeenCalled(); + }); + + it("returns early if one of the parent paths does not exist in the file system", async () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.entries.set("file.txt", { ...globalMocks.fileFsEntry }); + const writeFileMock = jest.spyOn(vscode.workspace.fs, "writeFile"); + const createDirMock = jest.spyOn(vscode.workspace.fs, "createDirectory"); + createDirMock.mockClear(); + const lookupAsDirMock = jest.spyOn(prov, "_lookupAsDirectory").mockReturnValueOnce(globalMocks.folderFsEntry).mockReturnValueOnce(undefined); + lookupAsDirMock.mockClear(); + + await prov._relocateEntry( + globalMocks.testFileUri, + globalMocks.testFileUri.with({ + path: "/file2.txt", + }), + "/file2.txt" + ); + expect(lookupAsDirMock).toHaveBeenCalledTimes(2); + expect(writeFileMock).not.toHaveBeenCalled(); + expect(createDirMock).not.toHaveBeenCalled(); + lookupAsDirMock.mockRestore(); + }); + + it("writes new entry in the file system once relocated", async () => { + const prov = new (BaseProvider as any)(); + prov.root = new DirEntry(""); + prov.root.entries.set("file.txt", { ...globalMocks.fileFsEntry }); + const deleteEntrySpy = jest.spyOn(prov.root.entries, "delete"); + const fireSoonSpy = jest.spyOn(prov, "_fireSoon"); + const writeFileMock = jest.spyOn(vscode.workspace.fs, "writeFile"); + const createDirMock = jest.spyOn(vscode.workspace.fs, "createDirectory"); + const reopenEditorMock = jest.spyOn(prov, "_reopenEditorForRelocatedUri").mockResolvedValueOnce(undefined); + jest.spyOn(prov, "_lookupAsFile").mockReturnValueOnce({ + ...globalMocks.fileFsEntry, + metadata: { ...globalMocks.fileFsEntry.metadata, path: "/file2.txt" }, + }); + createDirMock.mockClear(); + + const oldUri = globalMocks.testFileUri; + const newUri = globalMocks.testFileUri.with({ + path: "/file2.txt", + }); + await prov._relocateEntry(oldUri, newUri, "/file2.txt"); + expect(writeFileMock).toHaveBeenCalled(); + expect(createDirMock).not.toHaveBeenCalled(); + expect(deleteEntrySpy).toHaveBeenCalledWith("file.txt"); + expect(fireSoonSpy).toHaveBeenCalledWith({ type: vscode.FileChangeType.Deleted, uri: globalMocks.testFileUri }); + expect(reopenEditorMock).toHaveBeenCalledWith(oldUri, newUri); + }); +}); + +describe("_reopenEditorForRelocatedUri", () => { + it("closes the old URI and opens the new, relocated URI", async () => { + const tab = { + input: { uri: globalMocks.testFileUri }, + viewColumn: vscode.ViewColumn.One, + }; + const tabGroupsMock = new MockedProperty(vscode.window.tabGroups, "all", undefined, [ + { + isActive: true, + tabs: [tab], + }, + ]); + const closeTabMock = jest.spyOn(vscode.window.tabGroups, "close").mockImplementation(); + const executeCmdMock = jest.spyOn(vscode.commands, "executeCommand").mockResolvedValueOnce(undefined); + const oldUri = globalMocks.testFileUri; + const newUri = globalMocks.testFileUri.with({ + path: "/file2.txt", + }); + const prov = new (BaseProvider as any)(); + await prov._reopenEditorForRelocatedUri(oldUri, newUri); + expect(closeTabMock).toHaveBeenCalledWith(tab); + expect(executeCmdMock).toHaveBeenCalled(); + tabGroupsMock[Symbol.dispose](); + }); +}); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/fs/types/abstract.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/fs/types/abstract.unit.test.ts new file mode 100644 index 0000000000..e5b7547d3f --- /dev/null +++ b/packages/zowe-explorer-api/__tests__/__unit__/fs/types/abstract.unit.test.ts @@ -0,0 +1,45 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { FilePermission } from "vscode"; +import { BufferBuilder, FileEntry, FilterEntry } from "../../../../src"; + +describe("BufferBuilder", () => { + it("calls the given callback on write", () => { + const bufBuilder = new BufferBuilder(); + const callbackMock = jest.fn(); + bufBuilder._write(new Uint8Array([1, 2, 3]), "binary", callbackMock); + expect(callbackMock).toHaveBeenCalled(); + }); + + it("calls 'push' on read", () => { + const bufBuilder = new BufferBuilder(); + const callbackMock = jest.fn(); + const pushMock = jest.spyOn(bufBuilder, "push"); + bufBuilder._write(new Uint8Array([1, 2, 3]), "binary", callbackMock); + bufBuilder._read(3); + expect(pushMock).toHaveBeenCalledTimes(2); + }); +}); + +describe("FileEntry", () => { + it("handles read-only entries", () => { + const newEntry = new FileEntry("testFile", true); + expect(newEntry.permissions).toBe(FilePermission.Readonly); + }); +}); + +describe("FilterEntry", () => { + it("calls DirEntry constructor on initialization", () => { + const newEntry = new FilterEntry("testFilter"); + expect(newEntry.name).toBe("testFilter"); + }); +}); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/fs/types/datasets.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/fs/types/datasets.unit.test.ts new file mode 100644 index 0000000000..7825970886 --- /dev/null +++ b/packages/zowe-explorer-api/__tests__/__unit__/fs/types/datasets.unit.test.ts @@ -0,0 +1,54 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { DsEntry, DsEntryMetadata, PdsEntry } from "../../../../src/"; + +describe("DsEntry", () => { + it("calls the FileEntry class constructor when initialized", () => { + const entry = new DsEntry("TEST.DS"); + expect(entry.name).toBe("TEST.DS"); + }); +}); + +describe("PdsEntry", () => { + it("calls the DirEntry class constructor when initialized", () => { + const entry = new PdsEntry("TEST.PDS"); + expect(entry.name).toBe("TEST.PDS"); + expect(entry.entries).toStrictEqual(new Map()); + }); +}); + +describe("DsEntryMetadata", () => { + it("sets the profile and path provided in constructor", () => { + const fakeProfile: any = { name: "testProfile" }; + const entryMeta = new DsEntryMetadata({ + profile: fakeProfile, + path: "/TEST.DS", + }); + expect(entryMeta.profile).toBe(fakeProfile); + expect(entryMeta.path).toBe("/TEST.DS"); + }); + + it("returns a proper value for dsName", () => { + const fakeProfile: any = { name: "testProfile" }; + const entryMeta = new DsEntryMetadata({ + profile: fakeProfile, + path: "/TEST.DS", + }); + expect(entryMeta.dsName).toBe("TEST.DS"); + + const pdsEntryMeta = new DsEntryMetadata({ + profile: fakeProfile, + path: "/TEST.PDS/MEMBER", + }); + expect(pdsEntryMeta.dsName).toBe("TEST.PDS(MEMBER)"); + }); +}); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/fs/types/jobs.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/fs/types/jobs.unit.test.ts new file mode 100644 index 0000000000..5b140850d0 --- /dev/null +++ b/packages/zowe-explorer-api/__tests__/__unit__/fs/types/jobs.unit.test.ts @@ -0,0 +1,26 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { JobEntry, SpoolEntry } from "../../../../src"; + +describe("JobEntry", () => { + it("calls DirEntry constructor on initialization", () => { + const entry = new JobEntry("TESTJOB(JOB1234)"); + expect(entry.name).toBe("TESTJOB(JOB1234)"); + }); +}); + +describe("SpoolEntry", () => { + it("calls DirEntry constructor on initialization", () => { + const entry = new SpoolEntry("SPOOL.JES2.NAME.TEST"); + expect(entry.name).toBe("SPOOL.JES2.NAME.TEST"); + }); +}); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/fs/types/uss.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/fs/types/uss.unit.test.ts new file mode 100644 index 0000000000..5f37d4e668 --- /dev/null +++ b/packages/zowe-explorer-api/__tests__/__unit__/fs/types/uss.unit.test.ts @@ -0,0 +1,29 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { UssDirectory, UssFile } from "../../../../src"; + +describe("UssFile", () => { + it("calls FileEntry constructor on initialization", () => { + const newFile = new UssFile("testFile.txt"); + expect(newFile.name).toBe("testFile.txt"); + }); +}); + +describe("UssDirectory", () => { + it("calls DirEntry constructor on initialization", () => { + const newFolder = new UssDirectory("testFolder"); + expect(newFolder.name).toBe("testFolder"); + + const rootFolder = new UssDirectory(); + expect(rootFolder.name).toBe(""); + }); +}); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/fs/utils/abstract.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/fs/utils/abstract.unit.test.ts new file mode 100644 index 0000000000..68613be857 --- /dev/null +++ b/packages/zowe-explorer-api/__tests__/__unit__/fs/utils/abstract.unit.test.ts @@ -0,0 +1,97 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { + DirEntry, + FileEntry, + FilterEntry, + findDocMatchingUri, + getInfoForUri, + isDirectoryEntry, + isFileEntry, + isFilterEntry, + ZoweScheme, +} from "../../../../src"; +import { MockedProperty } from "../../../../__mocks__/mockUtils"; +import * as vscode from "vscode"; + +const fakeUri = vscode.Uri.from({ scheme: ZoweScheme.USS, path: "/test.lpar/file.txt" }); + +describe("getInfoForUri", () => { + it("returns the correct info for an inner URI", () => { + expect(getInfoForUri(fakeUri)).toStrictEqual({ + isRoot: false, + slashAfterProfilePos: fakeUri.path.indexOf("/", 1), + profileName: "test.lpar", + profile: null, + }); + }); + + it("returns the correct info for a root URI", () => { + expect(getInfoForUri(vscode.Uri.from({ scheme: ZoweScheme.USS, path: "/test.lpar" }))).toStrictEqual({ + isRoot: true, + slashAfterProfilePos: -1, + profileName: "test.lpar", + profile: null, + }); + }); +}); + +describe("findDocMatchingUri", () => { + it("returns a TextDocument if found", () => { + const fakeDoc = { uri: fakeUri }; + const mockedTextDocs = new MockedProperty(vscode.workspace, "textDocuments", undefined, [fakeDoc]); + expect(findDocMatchingUri(fakeUri)).toBe(fakeDoc); + mockedTextDocs[Symbol.dispose](); + }); + + it("returns undefined if not found", () => { + const mockedTextDocs = new MockedProperty(vscode.workspace, "textDocuments", undefined, []); + expect(findDocMatchingUri(fakeUri)).toBeUndefined(); + mockedTextDocs[Symbol.dispose](); + }); +}); + +describe("isDirectoryEntry", () => { + it("returns true if value is a DirEntry", () => { + const dirEntry = new DirEntry("testFolder"); + expect(isDirectoryEntry(dirEntry)).toBe(true); + }); + + it("returns false if value is not a DirEntry", () => { + const file = new FileEntry("test"); + expect(isDirectoryEntry(file)).toBe(false); + }); +}); + +describe("isFileEntry", () => { + it("returns true if value is a FileEntry", () => { + const file = new FileEntry("test"); + expect(isFileEntry(file)).toBe(true); + }); + + it("returns false if value is not a FileEntry", () => { + const dirEntry = new DirEntry("testFolder"); + expect(isFileEntry(dirEntry)).toBe(false); + }); +}); + +describe("isFilterEntry", () => { + it("returns true if value is a FilterEntry", () => { + const filterEntry = new FilterEntry("testFilter"); + expect(isFilterEntry(filterEntry)).toBe(true); + }); + + it("returns false if value is not a FilterEntry", () => { + const file = new FileEntry("test"); + expect(isFilterEntry(file)).toBe(false); + }); +}); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/fs/utils/datasets.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/fs/utils/datasets.unit.test.ts new file mode 100644 index 0000000000..73a1e772fc --- /dev/null +++ b/packages/zowe-explorer-api/__tests__/__unit__/fs/utils/datasets.unit.test.ts @@ -0,0 +1,48 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { DsEntry, MemberEntry, PdsEntry, isDsEntry, isMemberEntry, isPdsEntry } from "../../../../src"; + +describe("isDsEntry", () => { + it("returns true if value is a DsEntry", () => { + const entry = new DsEntry("TEST.DS"); + expect(isDsEntry(entry)).toBe(true); + }); + + it("returns false if value is not a DsEntry", () => { + const pds = new PdsEntry("TEST.PDS"); + expect(isDsEntry(pds)).toBe(false); + }); +}); + +describe("isMemberEntry", () => { + it("returns true if value is a MemberEntry", () => { + const entry = new MemberEntry("TESTMEMB"); + expect(isMemberEntry(entry)).toBe(true); + }); + + it("returns false if value is not a MemberEntry", () => { + const pds = new PdsEntry("TEST.PDS"); + expect(isMemberEntry(pds)).toBe(false); + }); +}); + +describe("isPdsEntry", () => { + it("returns true if value is a PdsEntry", () => { + const spoolEntry = new PdsEntry("TESTJOB.TEST.SPOOL.JES"); + expect(isPdsEntry(spoolEntry)).toBe(true); + }); + + it("returns false if value is not a PdsEntry", () => { + const ds = new DsEntry("TEST.DS"); + expect(isPdsEntry(ds)).toBe(false); + }); +}); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/fs/utils/jobs.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/fs/utils/jobs.unit.test.ts new file mode 100644 index 0000000000..baffeeec06 --- /dev/null +++ b/packages/zowe-explorer-api/__tests__/__unit__/fs/utils/jobs.unit.test.ts @@ -0,0 +1,36 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { DirEntry, FileEntry, JobEntry, SpoolEntry, isJobEntry, isSpoolEntry } from "../../../../src"; + +describe("isJobEntry", () => { + it("returns true if value is a JobEntry", () => { + const jobEntry = new JobEntry("TESTJOB(JOB1234)"); + expect(isJobEntry(jobEntry)).toBe(true); + }); + + it("returns false if value is not a JobEntry", () => { + const file = new FileEntry("test"); + expect(isJobEntry(file)).toBe(false); + }); +}); + +describe("isSpoolEntry", () => { + it("returns true if value is a SpoolEntry", () => { + const spoolEntry = new SpoolEntry("TESTJOB.TEST.SPOOL.JES"); + expect(isSpoolEntry(spoolEntry)).toBe(true); + }); + + it("returns false if value is not a SpoolEntry", () => { + const folder = new DirEntry("test"); + expect(isSpoolEntry(folder)).toBe(false); + }); +}); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ZoweExplorerZosmfApi.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ZoweExplorerZosmfApi.unit.test.ts index aa7f1951a7..d241d51260 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ZoweExplorerZosmfApi.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ZoweExplorerZosmfApi.unit.test.ts @@ -348,6 +348,17 @@ describe("ZosmfUssApi", () => { expect(changeTagSpy).toHaveBeenCalledTimes(1); }); + it("calls putUSSPayload to move a directory from old path to new path", async () => { + const api = new ZoweExplorerZosmf.UssApi(); + const putUssPayloadSpy = jest.fn(); + Object.defineProperty(zosfiles.Utilities, "putUSSPayload", { + value: putUssPayloadSpy, + configurable: true, + }); + await expect(api.move("/old/path", "/new/path")).resolves.not.toThrow(); + expect(putUssPayloadSpy).toHaveBeenCalledWith(api.getSession(), "/new/path", { request: "move", from: "/old/path" }); + }); + const ussApis: ITestApi[] = [ { name: "isFileTagBinOrAscii", diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts index 43ad585ec6..2bd91bbc2f 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ZoweVsCodeExtension.unit.test.ts @@ -11,8 +11,8 @@ import * as vscode from "vscode"; import { Gui } from "../../../src/globals/Gui"; -import { IProfileLoaded, Session } from "@zowe/imperative"; -import { MessageSeverity, IZoweLogger } from "../../../src/logger"; +import { Session } from "@zowe/imperative"; +import { MessageSeverity } from "../../../src/logger"; import { PromptCredentialsOptions, ZoweVsCodeExtension } from "../../../src/vscode"; import { ProfilesCache, Types } from "../../../src"; import { Login, Logout } from "@zowe/core-for-zowe-sdk"; @@ -93,7 +93,7 @@ describe("ZoweVsCodeExtension", () => { describe("deprecated methods", () => { it("showVsCodeMessage should pass on params to Gui module", () => { const showMessageSpy = jest.spyOn(Gui, "showMessage").mockImplementation(); - ZoweVsCodeExtension.showVsCodeMessage("test", MessageSeverity.INFO, undefined as unknown as IZoweLogger); + ZoweVsCodeExtension.showVsCodeMessage("test", MessageSeverity.INFO, undefined as any); expect(showMessageSpy).toHaveBeenCalledWith("test", { severity: MessageSeverity.INFO, logger: undefined, @@ -126,7 +126,7 @@ describe("ZoweVsCodeExtension", () => { }), }); const showInputBoxSpy = jest.spyOn(Gui, "showInputBox").mockResolvedValueOnce("fakeUser").mockResolvedValueOnce("fakePassword"); - const profileLoaded: IProfileLoaded = await ZoweVsCodeExtension.promptCredentials(promptCredsOptions); + const profileLoaded: imperative.IProfileLoaded = await ZoweVsCodeExtension.promptCredentials(promptCredsOptions); expect(profileLoaded.profile?.user).toBe("fakeUser"); expect(profileLoaded.profile?.password).toBe("fakePassword"); expect(showInputBoxSpy).toHaveBeenCalledTimes(2); @@ -402,7 +402,7 @@ describe("ZoweVsCodeExtension", () => { }); const showInputBoxSpy = jest.spyOn(Gui, "showInputBox").mockResolvedValueOnce("fakeUser").mockResolvedValueOnce("fakePassword"); const saveCredentialsSpy = jest.spyOn(ZoweVsCodeExtension as any, "saveCredentials"); - const profileLoaded: IProfileLoaded = await ZoweVsCodeExtension.updateCredentials( + const profileLoaded: imperative.IProfileLoaded = await ZoweVsCodeExtension.updateCredentials( promptCredsOptions, undefined as unknown as Types.IApiRegisterClient ); @@ -427,7 +427,7 @@ describe("ZoweVsCodeExtension", () => { }); const showInputBoxSpy = jest.spyOn(Gui, "showInputBox").mockResolvedValueOnce("fakeUser").mockResolvedValueOnce("fakePassword"); const saveCredentialsSpy = jest.spyOn(ZoweVsCodeExtension as any, "saveCredentials"); - const profileLoaded: IProfileLoaded = await ZoweVsCodeExtension.updateCredentials( + const profileLoaded: imperative.IProfileLoaded = await ZoweVsCodeExtension.updateCredentials( { ...promptCredsOptions, rePrompt: true, @@ -456,7 +456,7 @@ describe("ZoweVsCodeExtension", () => { const showInputBoxSpy = jest.spyOn(Gui, "showInputBox").mockResolvedValueOnce("fakeUser").mockResolvedValueOnce("fakePassword"); jest.spyOn(Gui, "showMessage").mockResolvedValueOnce("yes"); const saveCredentialsSpy = jest.spyOn(ZoweVsCodeExtension as any, "saveCredentials"); - const profileLoaded: IProfileLoaded = await ZoweVsCodeExtension.updateCredentials( + const profileLoaded: imperative.IProfileLoaded = await ZoweVsCodeExtension.updateCredentials( promptCredsOptions, undefined as unknown as Types.IApiRegisterClient ); @@ -482,7 +482,7 @@ describe("ZoweVsCodeExtension", () => { const showInputBoxSpy = jest.spyOn(Gui, "showInputBox").mockResolvedValueOnce("fakeUser").mockResolvedValueOnce("fakePassword"); jest.spyOn(Gui, "showMessage").mockResolvedValueOnce(undefined); const saveCredentialsSpy = jest.spyOn(ZoweVsCodeExtension as any, "saveCredentials"); - const profileLoaded: IProfileLoaded = await ZoweVsCodeExtension.updateCredentials( + const profileLoaded: imperative.IProfileLoaded = await ZoweVsCodeExtension.updateCredentials( promptCredsOptions, undefined as unknown as Types.IApiRegisterClient ); diff --git a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/WebView.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/WebView.unit.test.ts index e9d3b61755..fd19e42967 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/WebView.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/vscode/ui/WebView.unit.test.ts @@ -26,11 +26,6 @@ describe("WebView unit tests", () => { onDidDispose: jest.fn(), }), }); - Object.defineProperty(vscode, "Uri", { - value: { - file: jest.fn(), - }, - }); }); it("Successfully creates a WebView", () => { const createWebviewPanelSpy = jest.spyOn(vscode.window, "createWebviewPanel"); diff --git a/packages/zowe-explorer-api/src/extend/MainframeInteraction.ts b/packages/zowe-explorer-api/src/extend/MainframeInteraction.ts index 9a19069ecf..cf34969577 100644 --- a/packages/zowe-explorer-api/src/extend/MainframeInteraction.ts +++ b/packages/zowe-explorer-api/src/extend/MainframeInteraction.ts @@ -119,13 +119,22 @@ export namespace MainframeInteraction { * @param {string} ussFilePath * @param {zosfiles.IDownloadOptions} options */ - getContents(ussFilePath: string, options: zosfiles.IDownloadOptions): Promise; + getContents(ussFilePath: string, options: zosfiles.IDownloadSingleOptions): Promise; /** * Uploads a given buffer as the contents of a file on USS. + * @param {Buffer} buffer + * @param {string} filePath + * @param [options] Upload options + * @returns {Promise} */ uploadFromBuffer(buffer: Buffer, filePath: string, options?: zosfiles.IUploadOptions): Promise; + /** + * Moves a file or folder to the new path provided. + */ + move?(oldPath: string, newPath: string): Promise; + /** * Uploads the file at the given path. Use for Save. * @@ -223,14 +232,14 @@ export namespace MainframeInteraction { * @param {zosfiles.IDownloadOptions} [options] * @returns {Promise} */ - getContents(dataSetName: string, options?: zosfiles.IDownloadOptions): Promise; + getContents(dataSetName: string, options?: zosfiles.IDownloadSingleOptions): Promise; /** * Uploads a given buffer as the contents of a file to a data set or member. * * @param {Buffer} buffer * @param {string} dataSetName - * @param {zowe.IUploadOptions} [options] + * @param [options] Upload options * @returns {Promise} */ uploadFromBuffer(buffer: Buffer, dataSetName: string, options?: zosfiles.IUploadOptions): Promise; @@ -486,7 +495,7 @@ export namespace MainframeInteraction { * * @param {string} command * @param {zostso.IStartTsoParms} parms - * @returns {zostso.IIssueResponse} + * @returns {Promise} * @memberof ICommand */ issueTsoCommandWithParms?(command: string, parms?: zostso.IStartTsoParms): Promise; @@ -495,7 +504,7 @@ export namespace MainframeInteraction { * Issues a MVS Command and returns a Console Command API response. * * @param {string} command - * @returns {zosconsole.IConsoleResponse} + * @returns {Promise} * @memberof ICommand */ issueMvsCommand?(command: string): Promise; @@ -506,7 +515,7 @@ export namespace MainframeInteraction { * @param {string} command * @param {string} cwd * @param {boolean} flag - * @returns {string>} + * @returns {Promise} * @memberof ICommand */ issueUnixCommand?(sshSession: zosuss.SshSession, command: string, cwd: string, flag: boolean): Promise; diff --git a/packages/zowe-explorer-api/src/fs/BaseProvider.ts b/packages/zowe-explorer-api/src/fs/BaseProvider.ts new file mode 100644 index 0000000000..e8f40b28c5 --- /dev/null +++ b/packages/zowe-explorer-api/src/fs/BaseProvider.ts @@ -0,0 +1,447 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import * as vscode from "vscode"; +import { DirEntry, FileEntry, IFileSystemEntry, FS_PROVIDER_DELAY, ConflictViewSelection, DeleteMetadata } from "./types"; +import * as path from "path"; +import { isDirectoryEntry, isFileEntry } from "./utils"; +import { Gui } from "../globals/Gui"; +import { ZosEncoding } from "../tree"; + +export class BaseProvider { + // eslint-disable-next-line no-magic-numbers + private readonly FS_PROVIDER_UI_TIMEOUT = 4000; + + protected _onDidChangeFileEmitter = new vscode.EventEmitter(); + protected _bufferedEvents: vscode.FileChangeEvent[] = []; + protected _fireSoonHandle?: NodeJS.Timeout; + + public onDidChangeFile: vscode.Event = this._onDidChangeFileEmitter.event; + protected root: DirEntry; + public openedUris: vscode.Uri[] = []; + + protected constructor() {} + + /** + * Compares the data for 2 Uint8Arrays, byte by byte. + * @param a The first Uint8Array to compare + * @param b The second Uint8Array to compare + * @returns `true` if the arrays are equal, `false` otherwise + */ + public static areContentsEqual(a: Uint8Array, b: Uint8Array): boolean { + return a.byteLength === b.byteLength && a.every((byte, i) => byte === b[i]); + } + + /** + * Action for overwriting the remote contents with local data from the provider. + * @param remoteUri The "remote conflict" URI shown in the diff view + */ + public async diffOverwrite(uri: vscode.Uri): Promise { + const fsEntry = this._lookupAsFile(uri); + if (fsEntry == null) { + return; + } + await vscode.workspace.fs.writeFile(uri.with({ query: "forceUpload=true" }), fsEntry.data); + Gui.setStatusBarMessage( + vscode.l10n.t({ + message: "$(check) Overwrite applied for {0}", + args: [fsEntry.name], + comment: "File name", + }), + this.FS_PROVIDER_UI_TIMEOUT + ); + fsEntry.conflictData = null; + vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + } + + /** + * Action for replacing the local data in the provider with remote contents. + * @param localUri The local URI shown in the diff view + */ + public async diffUseRemote(uri: vscode.Uri): Promise { + const fsEntry = this._lookupAsFile(uri); + if (fsEntry == null) { + return; + } + + // If the data in the diff is different from the conflict data, we need to make another API request to push those changes. + // If the data is equal, we can just assign the data in the FileSystem and avoid making an API request. + const isDataEqual = BaseProvider.areContentsEqual(fsEntry.data, fsEntry.conflictData.contents); + if (!isDataEqual) { + await vscode.workspace.fs.writeFile(uri.with({ query: "forceUpload=true" }), fsEntry.conflictData.contents); + } + Gui.setStatusBarMessage( + vscode.l10n.t({ + message: "$(discard) Used remote content for {0}", + args: [fsEntry.name], + comment: "File name", + }), + this.FS_PROVIDER_UI_TIMEOUT + ); + fsEntry.conflictData = null; + vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + } + + public exists(uri: vscode.Uri): boolean { + const entry = this._lookup(uri, true); + return entry != null; + } + + /** + * Removes a local entry from the FS provider if it exists, without making any API requests. + * @param uri The URI pointing to a local entry in the FS provider + */ + public removeEntry(uri: vscode.Uri): boolean { + const parentEntry = this._lookupParentDirectory(uri, true); + if (parentEntry == null) { + return false; + } + + const entryName = path.posix.basename(uri.path); + if (!parentEntry.entries.has(entryName)) { + return false; + } + + parentEntry.entries.delete(entryName); + this._fireSoon({ type: vscode.FileChangeType.Deleted, uri: uri }); + return true; + } + + /** + * Adds an opened URI to the cache so we can see if it has been closed. + * @param uri the opened URI to keep track of + */ + public cacheOpenedUri(uri: vscode.Uri): void { + this.openedUris.push(uri); + } + + /** + * Invalidates the data for a file entry at a given URI. + * Also removes the URI from the opened URI cache. + * @param uri the URI whose data should be invalidated + */ + public invalidateFileAtUri(uri: vscode.Uri): boolean { + const entry = this._lookup(uri, true); + if (!isFileEntry(entry)) { + return false; + } + + entry.data = null; + entry.wasAccessed = false; + this.openedUris = this.openedUris.filter((u) => u !== uri); + return true; + } + + /** + * Invalidates the data for a directory entry at a given URI. + * Also removes the URI from the opened URI cache. + * @param uri the URI whose data should be invalidated + */ + public invalidateDirAtUri(uri: vscode.Uri): boolean { + const entry = this._lookup(uri, true); + if (!isDirectoryEntry(entry)) { + return false; + } + + entry.entries.clear(); + this._lookupParentDirectory(uri).entries.delete(entry.name); + return true; + } + + /** + * Returns the encoding for a file entry matching the given URI. + * @param uri The URI that corresponds to an existing file entry + * @returns The encoding for the file + */ + public getEncodingForFile(uri: vscode.Uri): ZosEncoding { + const entry = this._lookup(uri, false) as FileEntry | DirEntry; + if (isDirectoryEntry(entry)) { + return undefined; + } + + return entry.encoding; + } + + /** + * Sets the encoding for a file entry matching the given URI. + * @param uri The URI that corresponds to an existing file entry + * @param encoding The new encoding for the file entry + */ + public setEncodingForFile(uri: vscode.Uri, encoding: ZosEncoding): void { + const fileEntry = this._lookupAsFile(uri); + fileEntry.encoding = encoding; + } + + /** + * Triggers an update for the resource at the given URI to show its latest changes in the editor. + * @param uri The URI that is open in an editor tab + */ + protected async _updateResourceInEditor(uri: vscode.Uri): Promise { + const entry = this._lookup(uri, true); + if (!isFileEntry(entry)) { + return; + } + // NOTE: This does not work for editors that aren't the active one, so... + // Make VS Code switch to this editor, and then "revert the file" to show the latest contents + await vscode.commands.executeCommand("vscode.open", uri); + await BaseProvider.revertFileInEditor(); + } + + /** + * This function is used to revert changes in the active editor. + * It can also be used to update an editor with the newest contents of a resource. + * + * https://github.com/microsoft/vscode/issues/110493#issuecomment-726542367 + */ + public static async revertFileInEditor(): Promise { + await vscode.commands.executeCommand("workbench.action.files.revert"); + } + + /** + * Update the child path metadata in the provider with the parent's updated entry (recursively). + * @param entry The parent directory whose children need updated + */ + protected _updateChildPaths(entry: DirEntry): void { + // update child entries + for (const child of entry.entries.values()) { + child.metadata.path = path.posix.join(entry.metadata.path, child.name); + if (isDirectoryEntry(child)) { + this._updateChildPaths(child); + } + } + } + + private async _reopenEditorForRelocatedUri(oldUri: vscode.Uri, newUri: vscode.Uri): Promise { + const tabGroups = vscode.window.tabGroups.all; + const allTabs = tabGroups.reduce((acc: vscode.Tab[], group) => acc.concat(group.tabs), []); + const tabWithOldUri = allTabs.find((t) => (t.input as any).uri.path === oldUri.path); + if (tabWithOldUri) { + const parent = tabGroups.find((g) => g.tabs.find((t) => t === tabWithOldUri)); + const editorCol = parent.viewColumn; + // close old uri and reopen new uri + await vscode.window.tabGroups.close(tabWithOldUri); + await vscode.commands.executeCommand("vscode.openWith", newUri, "default", editorCol); + } + } + + /** + * Relocates an entry in the provider from `oldUri` to `newUri`. + * @param oldUri The old, source URI in the provider that needs moved + * @param newUri The new, destination URI for the file or folder + * @param newUssPath The new path for this entry in USS + */ + protected async _relocateEntry(oldUri: vscode.Uri, newUri: vscode.Uri, newUssPath: string): Promise { + const entry = this._lookup(oldUri, true); + if (!entry) { + return; + } + + const oldParentUri = vscode.Uri.parse(oldUri.path.substring(0, oldUri.path.lastIndexOf("/"))); + const oldParent = this._lookupAsDirectory(oldParentUri, false); + + const parentUri = vscode.Uri.parse(newUri.path.substring(0, newUri.path.lastIndexOf("/"))); + const newParent = this._lookupAsDirectory(parentUri, false); + + // both parent paths must be valid in order to perform a relocation + if (!oldParent || !newParent) { + return; + } + + entry.metadata.path = newUssPath; + const isFile = isFileEntry(entry); + // write new entry in FS + if (isFile) { + // put new contents in relocated file + await vscode.workspace.fs.writeFile(newUri, entry.data); + const newEntry = this._lookupAsFile(newUri); + newEntry.etag = entry.etag; + } else { + // create directory in FS; when expanded in the tree, it will fetch any files + vscode.workspace.fs.createDirectory(newUri); + } + // delete entry from old parent + oldParent.entries.delete(entry.name); + this._fireSoon({ type: vscode.FileChangeType.Deleted, uri: oldUri }); + if (isFile) { + return this._reopenEditorForRelocatedUri(oldUri, newUri); + } + } + + protected _getDeleteInfo(uri: vscode.Uri): DeleteMetadata { + const basename = path.posix.basename(uri.path); + const parent = this._lookupParentDirectory(uri, false); + + // Throw an error if the entry does not exist + if (!parent.entries.has(basename)) { + throw vscode.FileSystemError.FileNotFound(uri); + } + + // get the entry data before deleting the URI + const entryToDelete = this._lookup(uri, false); + + return { + entryToDelete, + parent, + parentUri: uri.with({ path: path.posix.join(uri.path, "..") }), + }; + } + + // This event removes the "diff view" flag from the local file, + // so that API calls can continue after the conflict dialog is closed. + private static onCloseEvent(provider: BaseProvider, e: vscode.TextDocument): void { + if (e.uri.query && e.uri.scheme.startsWith("zowe-")) { + const queryParams = new URLSearchParams(e.uri.query); + if (queryParams.has("conflict")) { + const fsEntry = provider._lookupAsFile(e.uri, { silent: true }); + if (fsEntry) { + fsEntry.inDiffView = false; + } + } + } + } + + /** + * Utility functions for conflict management: + */ + + /** + * Handles the conflict that occurred while trying to write to a file. + * @param api The API to use during compare/overwrite - must support `getContents` and `uploadFromBuffer` functions + * @param conflictData The required data for conflict handling + * @returns The user's action/selection as an enum value + */ + protected async _handleConflict(uri: vscode.Uri, entry: FileEntry): Promise { + const conflictOptions = [vscode.l10n.t("Compare"), vscode.l10n.t("Overwrite")]; + const userSelection = await Gui.errorMessage( + "There is a newer version of this file on the mainframe. Compare with remote contents or overwrite?", + { + items: conflictOptions, + } + ); + if (userSelection == null) { + return ConflictViewSelection.UserDismissed; + } + + // User selected "Compare", show diff with local contents and LPAR contents + if (userSelection === conflictOptions[0]) { + vscode.workspace.onDidCloseTextDocument(BaseProvider.onCloseEvent.bind(this)); + await vscode.commands.executeCommand( + "vscode.diff", + uri.with({ query: "conflict=true" }), + uri.with({ query: "inDiff=true" }), + `${entry.name} (Remote) ↔ ${entry.name}` + ); + return ConflictViewSelection.Compare; + } + + // User selected "Overwrite" + await this.diffOverwrite(uri); + return ConflictViewSelection.Overwrite; + } + + /** + * Internal VSCode function for the FileSystemProvider to fire events from the event queue + */ + protected _fireSoon(...events: vscode.FileChangeEvent[]): void { + this._bufferedEvents.push(...events); + + if (this._fireSoonHandle) { + clearTimeout(this._fireSoonHandle); + } + + this._fireSoonHandle = setTimeout(() => { + this._onDidChangeFileEmitter.fire(this._bufferedEvents); + this._bufferedEvents.length = 0; + }, FS_PROVIDER_DELAY); + } + + /** + * VScode utility functions for entries in the provider: + */ + + protected _lookup(uri: vscode.Uri, silent: boolean = false): IFileSystemEntry | undefined { + if (uri.path === "/") { + return this.root; + } + + const parts = uri.path.split("/").filter(Boolean); + let entry: IFileSystemEntry = this.root; + + for (const part of parts) { + if (!part) { + continue; + } + let child: IFileSystemEntry | undefined; + if (isDirectoryEntry(entry)) { + child = entry.entries.get(part); + } + if (!child) { + if (!silent) { + throw vscode.FileSystemError.FileNotFound(uri); + } else { + return undefined; + } + } + entry = child; + } + return entry; + } + + protected _lookupAsDirectory(uri: vscode.Uri, silent: boolean): DirEntry { + const entry = this._lookup(uri, silent); + if (isDirectoryEntry(entry)) { + return entry; + } + throw vscode.FileSystemError.FileNotADirectory(uri); + } + + protected _createFile(uri: vscode.Uri, options?: { overwrite: boolean }): FileEntry { + const basename = path.posix.basename(uri.path); + const parent = this._lookupParentDirectory(uri); + let entry = parent.entries.get(basename); + if (isDirectoryEntry(entry)) { + throw vscode.FileSystemError.FileIsADirectory(uri); + } + if (entry) { + if (options.overwrite ?? false) { + throw vscode.FileSystemError.FileExists(uri); + } else { + return entry; + } + } + + entry = new FileEntry(basename); + entry.data = new Uint8Array(); + const filePath = parent.metadata.path.concat(basename); + const profInfo = { ...parent.metadata, path: filePath }; + entry.metadata = profInfo; + parent.entries.set(basename, entry); + this._fireSoon({ type: vscode.FileChangeType.Created, uri }); + return entry; + } + + protected _lookupAsFile(uri: vscode.Uri, opts?: { silent?: boolean }): FileEntry { + const entry = this._lookup(uri, opts?.silent ?? false); + if (isFileEntry(entry)) { + return entry; + } + throw vscode.FileSystemError.FileIsADirectory(uri); + } + + protected _lookupParentDirectory(uri: vscode.Uri, silent?: boolean): DirEntry { + return this._lookupAsDirectory( + uri.with({ + path: path.posix.join(uri.path, ".."), + }), + silent ?? false + ); + } +} diff --git a/packages/zowe-explorer/src/config/constants.ts b/packages/zowe-explorer-api/src/fs/index.ts similarity index 60% rename from packages/zowe-explorer/src/config/constants.ts rename to packages/zowe-explorer-api/src/fs/index.ts index 62ccbf3fff..565815e96a 100644 --- a/packages/zowe-explorer/src/config/constants.ts +++ b/packages/zowe-explorer-api/src/fs/index.ts @@ -9,7 +9,7 @@ * */ -export const workspaceUtilTabSwitchDelay = 200; -export const workspaceUtilMaxEmptyWindowsInTheRow = 3; -export const workspaceUtilFileSaveInterval = 200; -export const workspaceUtilFileSaveMaxIterationCount = 25; +export * from "./types"; +export * from "./utils"; + +export * from "./BaseProvider"; diff --git a/packages/zowe-explorer-api/src/fs/types/abstract.ts b/packages/zowe-explorer-api/src/fs/types/abstract.ts new file mode 100644 index 0000000000..d8906be775 --- /dev/null +++ b/packages/zowe-explorer-api/src/fs/types/abstract.ts @@ -0,0 +1,143 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { Duplex } from "stream"; +import { IProfileLoaded } from "@zowe/imperative"; +import * as vscode from "vscode"; +import { ZosEncoding } from "../../tree"; + +export enum ZoweScheme { + DS = "zowe-ds", + Jobs = "zowe-jobs", + USS = "zowe-uss", +} + +export enum ConflictViewSelection { + UserDismissed = 0, + Compare = 1, + Overwrite = 2, +} + +export type DeleteMetadata = { + entryToDelete: IFileSystemEntry; + parent: DirEntry; + parentUri: vscode.Uri; +}; + +export class BufferBuilder extends Duplex { + private chunks: Uint8Array[]; + + public constructor() { + super(); + this.chunks = []; + } + + public _write(chunk: any, encoding: BufferEncoding, callback: (error?: Error) => void): void { + this.chunks.push(chunk); + callback(); + } + + public _read(_size: number): void { + const concatBuf = Buffer.concat(this.chunks); + this.push(concatBuf); + this.push(null); + } +} + +export const FS_PROVIDER_DELAY = 5; + +export type EntryMetadata = { + profile: IProfileLoaded; + path: string; +}; + +export type ConflictData = { + contents: Uint8Array; + etag?: string; + size: number; +}; + +export interface IFileSystemEntry extends vscode.FileStat { + name: string; + metadata: EntryMetadata; + type: vscode.FileType; + wasAccessed: boolean; + data?: Uint8Array; +} + +export class FileEntry implements IFileSystemEntry { + public name: string; + public metadata: EntryMetadata; + public type: vscode.FileType; + public data: Uint8Array; + public wasAccessed: boolean; + public ctime: number; + public mtime: number; + public size: number; + public permissions?: vscode.FilePermission; + /** + * Remote encoding of the data set + */ + public encoding?: ZosEncoding; + + // optional types for conflict and file management that some FileSystems will not leverage + public conflictData?: ConflictData; + public inDiffView?: boolean; + public etag?: string; + public constructor(n: string, readOnly?: boolean) { + this.name = n; + this.type = vscode.FileType.File; + this.ctime = Date.now(); + this.mtime = Date.now(); + this.size = 0; + this.data = new Uint8Array(); + this.wasAccessed = false; + this.encoding = undefined; + if (readOnly) { + this.permissions = vscode.FilePermission.Readonly; + } + } +} + +export class DirEntry implements IFileSystemEntry { + public name: string; + public metadata: EntryMetadata; + public type: vscode.FileType; + public wasAccessed: boolean; + public ctime: number; + public mtime: number; + public size: number; + public permissions?: vscode.FilePermission; + public entries: Map; + + public constructor(n: string) { + this.name = n; + this.type = vscode.FileType.Directory; + this.ctime = Date.now(); + this.mtime = Date.now(); + this.size = 0; + this.entries = new Map(); + } +} + +export class FilterEntry extends DirEntry { + public filter: Record = {}; + + public constructor(n: string) { + super(n); + } +} + +export type LocalConflict = { + fsEntry: FileEntry; + uri: vscode.Uri; + content: Uint8Array; +}; diff --git a/packages/zowe-explorer-api/src/fs/types/datasets.ts b/packages/zowe-explorer-api/src/fs/types/datasets.ts new file mode 100644 index 0000000000..3c73f730fe --- /dev/null +++ b/packages/zowe-explorer-api/src/fs/types/datasets.ts @@ -0,0 +1,47 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { DirEntry, EntryMetadata, FileEntry } from "../types"; +import { IProfileLoaded } from "@zowe/imperative"; + +export class DsEntry extends FileEntry { + public metadata: DsEntryMetadata; + + public constructor(name: string) { + super(name); + } +} + +export class MemberEntry extends DsEntry {} + +export class PdsEntry extends DirEntry { + public entries: Map; + + public constructor(name: string) { + super(name); + this.entries = new Map(); + } +} + +export class DsEntryMetadata implements EntryMetadata { + public profile: IProfileLoaded; + public path: string; + + public constructor(metadata: EntryMetadata) { + this.profile = metadata.profile; + this.path = metadata.path; + } + + public get dsName(): string { + const segments = this.path.split("/").filter(Boolean); + return segments[1] ? `${segments[0]}(${segments[1]})` : segments[0]; + } +} diff --git a/packages/zowe-explorer-api/src/fs/types/index.ts b/packages/zowe-explorer-api/src/fs/types/index.ts new file mode 100644 index 0000000000..81b747c299 --- /dev/null +++ b/packages/zowe-explorer-api/src/fs/types/index.ts @@ -0,0 +1,15 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +export * from "./abstract"; +export * from "./datasets"; +export * from "./jobs"; +export * from "./uss"; diff --git a/packages/zowe-explorer-api/src/fs/types/jobs.ts b/packages/zowe-explorer-api/src/fs/types/jobs.ts new file mode 100644 index 0000000000..1a1e9a78f7 --- /dev/null +++ b/packages/zowe-explorer-api/src/fs/types/jobs.ts @@ -0,0 +1,48 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { IJob, IJobFile } from "@zowe/zos-jobs-for-zowe-sdk"; +import { DirEntry, EntryMetadata, FileEntry } from "../types"; +import { FilePermission, FileType } from "vscode"; + +export type JobFilter = { + searchId: string; + owner: string; + prefix: string; + status: string; +}; + +export class SpoolEntry extends FileEntry { + public name: string; + public type: FileType; + public wasAccessed: boolean; + + public data: Uint8Array; + public permissions?: FilePermission; + public ctime: number; + public mtime: number; + public size: number; + public metadata: EntryMetadata; + public spool?: IJobFile; + + public constructor(name: string) { + super(name); + } +} + +export class JobEntry extends DirEntry { + public entries: Map; + public job?: IJob; + + public constructor(name: string) { + super(name); + } +} diff --git a/packages/zowe-explorer-api/src/fs/types/uss.ts b/packages/zowe-explorer-api/src/fs/types/uss.ts new file mode 100644 index 0000000000..fdedf916f1 --- /dev/null +++ b/packages/zowe-explorer-api/src/fs/types/uss.ts @@ -0,0 +1,38 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import * as vscode from "vscode"; +import { ConflictData, DirEntry, EntryMetadata, FileEntry } from "."; + +export class UssFile extends FileEntry { + public name: string; + public metadata: EntryMetadata; + public type: vscode.FileType; + public wasAccessed: boolean; + + public ctime: number; + public mtime: number; + public size: number; + public conflictData?: ConflictData; + public data: Uint8Array; + public etag?: string; + public permissions?: vscode.FilePermission; + + public constructor(name: string) { + super(name); + } +} + +export class UssDirectory extends DirEntry { + public constructor(name?: string) { + super(name ?? ""); + } +} diff --git a/packages/zowe-explorer-api/src/fs/utils/abstract.ts b/packages/zowe-explorer-api/src/fs/utils/abstract.ts new file mode 100644 index 0000000000..cde5a62aaf --- /dev/null +++ b/packages/zowe-explorer-api/src/fs/utils/abstract.ts @@ -0,0 +1,87 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import * as vscode from "vscode"; +import { ProfilesCache } from "../../profiles/ProfilesCache"; +import { IProfileLoaded } from "@zowe/imperative"; +import { DirEntry, FileEntry, FilterEntry, IFileSystemEntry } from "../types"; +import { Gui } from "../.."; +import { posix } from "path"; + +export type UriFsInfo = { + isRoot: boolean; + slashAfterProfilePos: number; + profileName: string; + profile?: IProfileLoaded; +}; + +/** + * Returns the metadata for a given URI in the FileSystem. + * @param uri The "Zowe-compliant" URI to extract info from + * @returns a metadata type with info about the URI + */ +export function getInfoForUri(uri: vscode.Uri, profilesCache?: ProfilesCache): UriFsInfo { + // Paths pointing to the session root will have the format `:/{lpar_name}` + const slashAfterProfilePos = uri.path.indexOf("/", 1); + const isRoot = slashAfterProfilePos === -1; + + // Determine where to parse profile name based on location of first slash + const startPathPos = isRoot ? uri.path.length : slashAfterProfilePos; + + // Load profile that matches the parsed name + const profileName = uri.path.substring(1, startPathPos); + const profile = profilesCache?.loadNamedProfile ? profilesCache.loadNamedProfile(profileName) : null; + + return { + isRoot, + slashAfterProfilePos, + profileName, + profile, + }; +} + +export function findDocMatchingUri(uri: vscode.Uri): vscode.TextDocument { + return vscode.workspace.textDocuments.find((doc) => doc.uri.toString() === uri.toString()); +} + +export async function confirmForUnsavedDoc(uri: vscode.Uri): Promise { + const doc = findDocMatchingUri(uri); + if (doc?.isDirty) { + const confirmItem = vscode.l10n.t("Confirm"); + return ( + (await Gui.warningMessage( + vscode.l10n.t( + "{0} is opened and has pending changes in the editor. By selecting 'Confirm', any unsaved changes will be lost.", + posix.basename(doc.fileName) + ), + { + items: [confirmItem], + vsCodeOpts: { modal: true }, + } + )) === confirmItem + ); + } + + // either the document doesn't exist or it isn't dirty + return true; +} + +export function isDirectoryEntry(entry: IFileSystemEntry): entry is DirEntry { + return entry != null && entry["type"] === vscode.FileType.Directory; +} + +export function isFileEntry(entry: IFileSystemEntry): entry is FileEntry { + return entry != null && entry["type"] === vscode.FileType.File; +} + +export function isFilterEntry(entry: IFileSystemEntry): entry is FilterEntry { + return entry != null && "filter" in entry; +} diff --git a/packages/zowe-explorer-api/src/fs/utils/datasets.ts b/packages/zowe-explorer-api/src/fs/utils/datasets.ts new file mode 100644 index 0000000000..d626e9fb0d --- /dev/null +++ b/packages/zowe-explorer-api/src/fs/utils/datasets.ts @@ -0,0 +1,24 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { IFileSystemEntry, DsEntry, MemberEntry, PdsEntry } from "../"; + +export function isDsEntry(entry: IFileSystemEntry): entry is DsEntry { + return entry instanceof DsEntry; +} + +export function isMemberEntry(entry: IFileSystemEntry): entry is MemberEntry { + return entry != null && entry instanceof MemberEntry; +} + +export function isPdsEntry(entry: IFileSystemEntry): entry is PdsEntry { + return entry != null && entry instanceof PdsEntry; +} diff --git a/packages/zowe-explorer-api/src/fs/utils/index.ts b/packages/zowe-explorer-api/src/fs/utils/index.ts new file mode 100644 index 0000000000..b2d630987f --- /dev/null +++ b/packages/zowe-explorer-api/src/fs/utils/index.ts @@ -0,0 +1,14 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +export * from "./abstract"; +export * from "./datasets"; +export * from "./jobs"; diff --git a/packages/zowe-explorer-api/src/fs/utils/jobs.ts b/packages/zowe-explorer-api/src/fs/utils/jobs.ts new file mode 100644 index 0000000000..27bf1e2b39 --- /dev/null +++ b/packages/zowe-explorer-api/src/fs/utils/jobs.ts @@ -0,0 +1,26 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { IJobFile } from "@zowe/zos-jobs-for-zowe-sdk"; +import { IFileSystemEntry, JobEntry, SpoolEntry } from "../types"; + +export function isJobEntry(entry: IFileSystemEntry): entry is JobEntry { + return entry instanceof JobEntry; +} + +export function isSpoolEntry(entry: IFileSystemEntry): entry is SpoolEntry { + return entry instanceof SpoolEntry; +} + +export function buildUniqueSpoolName(spool: IJobFile): string { + const spoolSegments = [spool.jobname, spool.jobid, spool.stepname, spool.procstep, spool.ddname, spool.id?.toString()]; + return spoolSegments.filter((v) => v?.length).join("."); +} diff --git a/packages/zowe-explorer-api/src/index.ts b/packages/zowe-explorer-api/src/index.ts index 6c26cc354b..272be63b57 100644 --- a/packages/zowe-explorer-api/src/index.ts +++ b/packages/zowe-explorer-api/src/index.ts @@ -19,4 +19,5 @@ export * from "./tree"; export * from "./utils"; export * from "./vscode"; export * from "./Types"; +export * from "./fs"; export * as imperative from "@zowe/imperative"; diff --git a/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts b/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts index 244e2834fa..c27aa4f0fe 100644 --- a/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts +++ b/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts @@ -126,7 +126,7 @@ export namespace ZoweExplorerZosmf { return zosfiles.Utilities.isFileTagBinOrAscii(this.getSession(), ussFilePath); } - public getContents(inputFilePath: string, options: zosfiles.IDownloadOptions): Promise { + public getContents(inputFilePath: string, options: zosfiles.IDownloadSingleOptions): Promise { return zosfiles.Download.ussFile(this.getSession(), inputFilePath, options); } @@ -134,6 +134,13 @@ export namespace ZoweExplorerZosmf { return zosfiles.Utilities.putUSSPayload(this.getSession(), outputPath, { ...(options ?? {}), request: "copy" }); } + public async move(oldPath: string, newPath: string): Promise { + await zosfiles.Utilities.putUSSPayload(this.getSession(), newPath, { + request: "move", + from: oldPath, + }); + } + public uploadFromBuffer(buffer: Buffer, filePath: string, options?: zosfiles.IUploadOptions): Promise { return zosfiles.Upload.bufferToUssFile(this.getSession(), filePath, buffer, options); } @@ -235,7 +242,7 @@ export namespace ZoweExplorerZosmf { return zosfiles.List.allMembers(this.getSession(), dataSetName, options); } - public getContents(dataSetName: string, options?: zosfiles.IDownloadOptions): Promise { + public getContents(dataSetName: string, options?: zosfiles.IDownloadSingleOptions): Promise { return zosfiles.Download.dataSet(this.getSession(), dataSetName, options); } diff --git a/packages/zowe-explorer-api/src/tree/IZoweTree.ts b/packages/zowe-explorer-api/src/tree/IZoweTree.ts index 79d58c0a2e..efc6a7566b 100644 --- a/packages/zowe-explorer-api/src/tree/IZoweTree.ts +++ b/packages/zowe-explorer-api/src/tree/IZoweTree.ts @@ -24,7 +24,7 @@ import { Types } from "../Types"; * @extends {vscode.TreeDataProvider} * @template T provide a subtype of vscode.TreeItem */ -export interface IZoweTree extends vscode.TreeDataProvider { +export interface IZoweTree extends vscode.TreeDataProvider, Partial> { /** * Root session nodes */ @@ -134,6 +134,11 @@ export interface IZoweTree extends vscode.TreeDataProvider { * @param favorite Node to refresh */ refreshElement(node: IZoweTreeNode): void; + /** + * Signals that node data has changed in the tree view + * @param element to pass to event listener callback + */ + nodeDataChanged?(node: IZoweTreeNode): void; /** * Event Emitters used to notify subscribers that the refresh event has fired */ diff --git a/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts b/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts index eb18096c6b..6a2d611f3f 100644 --- a/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts +++ b/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts @@ -37,32 +37,11 @@ export type ZosEncoding = TextEncoding | BinaryEncoding | OtherEncoding; * @export * @interface IZoweTreeNode */ -export interface IZoweTreeNode { - /** - * The icon path or [ThemeIcon](#ThemeIcon) for the tree item. - */ - iconPath?: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri } | vscode.ThemeIcon; +export interface IZoweTreeNode extends vscode.TreeItem { /** * Indicator that the child data may have become stale and requires refreshing. */ dirty: boolean; - /** - * A human-readable string describing this item. - */ - label?: string | vscode.TreeItemLabel; - /** - * A description for this tree item. - */ - description?: string | boolean; - /** - * A unique identifier for this tree item. - * Used to prevent VScode from losing track of TreeItems in a TreeProvider. - */ - id?: string; - /** - * The tooltip text when you hover over this item. - */ - tooltip?: string | vscode.MarkdownString | undefined; /** * Describes the full path of a file */ @@ -71,16 +50,6 @@ export interface IZoweTreeNode { * Children nodes of this node */ children?: IZoweTreeNode[]; - /** - * [TreeItemCollapsibleState](#TreeItemCollapsibleState) of the tree item. - */ - collapsibleState?: vscode.TreeItemCollapsibleState; - /** - * Context value of the tree item. This can be used to contribute item specific actions in the tree. - * - * This will show action `extension.deleteFolder` only for items with `contextValue` is `folder`. - */ - contextValue?: string; /** * Any ongoing actions that must be awaited before continuing */ @@ -199,6 +168,12 @@ export interface IZoweDatasetTreeNode extends IZoweTreeNode { * @param datasetFileProvider the tree provider */ openDs?(download: boolean, previewFile: boolean, datasetFileProvider: Types.IZoweDatasetTreeType): Promise; + /** + * Gets the codepage value for the file + * + * @param {string} + */ + getEncoding?(): ZosEncoding; /** * Sets the codepage value for the file * @@ -235,13 +210,6 @@ export interface IZoweUSSTreeNode extends IZoweTreeNode { * Event that fires whenever an existing node is updated. */ onUpdateEmitter?: vscode.EventEmitter; - /** - * Remote encoding of the data set - * - * * `null` = user selected z/OS default codepage - * * `undefined` = user did not specify - */ - encoding?: string; /** * Event that fires whenever an existing node is updated. */ @@ -270,6 +238,12 @@ export interface IZoweUSSTreeNode extends IZoweTreeNode { * @param {string} newNamePath */ rename?(newNamePath: string); + /** + * Gets the codepage value for the file + * + * @param {string} + */ + getEncoding?(): ZosEncoding; /** * Sets the codepage value for the file * diff --git a/packages/zowe-explorer-api/src/utils/index.ts b/packages/zowe-explorer-api/src/utils/index.ts index 0c8855aae2..a89e6c2f35 100644 --- a/packages/zowe-explorer-api/src/utils/index.ts +++ b/packages/zowe-explorer-api/src/utils/index.ts @@ -9,5 +9,18 @@ * */ +import { IZoweTreeNode } from "../tree"; +import { workspace } from "vscode"; + export * from "./Poller"; export * from "./FileManagement"; + +/** + * Getter to check dirty flag for nodes opened in the editor. + * + * NOTE: Only works for nodes that use resource URIs (see the `resourceUri` variable in IZoweTreeNode) + * @returns {boolean} whether the URI is open in the editor and unsaved + */ +export function isNodeInEditor(node: IZoweTreeNode): boolean { + return workspace.textDocuments.some(({ uri, isDirty }) => uri.path === node.resourceUri?.path && isDirty); +} diff --git a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts index d40a37d7bf..fdb9ee7efc 100644 --- a/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts +++ b/packages/zowe-explorer-api/src/vscode/ZoweVsCodeExtension.ts @@ -23,7 +23,10 @@ import { Types } from "../Types"; * Collection of utility functions for writing Zowe Explorer VS Code extensions. */ export class ZoweVsCodeExtension { - private static get profilesCache(): ProfilesCache { + /** + * @internal + */ + public static get profilesCache(): ProfilesCache { return new ProfilesCache(imperative.Logger.getAppLogger(), vscode.workspace.workspaceFolders?.[0]?.uri.fsPath); } diff --git a/packages/zowe-explorer-api/tsconfig.json b/packages/zowe-explorer-api/tsconfig.json index d0808119db..59ff6515ec 100644 --- a/packages/zowe-explorer-api/tsconfig.json +++ b/packages/zowe-explorer-api/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "esModuleInterop": true, "types": ["node"], "target": "es2015", "module": "commonjs", @@ -11,6 +12,7 @@ "removeComments": false, "pretty": true, "sourceMap": true, + "stripInternal": true, "newLine": "lf", "resolveJsonModule": true, "composite": true, diff --git a/packages/zowe-explorer-ftp-extension/CHANGELOG.md b/packages/zowe-explorer-ftp-extension/CHANGELOG.md index 500eb695a3..63006aa8ad 100644 --- a/packages/zowe-explorer-ftp-extension/CHANGELOG.md +++ b/packages/zowe-explorer-ftp-extension/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to the "zowe-explorer-ftp-extension" extension will be docum ### New features and enhancements +- Changed the type for the options parameter in the `getContents` function (`MainframeInteraction.IUss` and `MainframeInteraction.IMvs` interfaces) from `zosfiles.IDownloadOptions` to `zosfiles.IDownloadSingleOptions`. [#2207](https://github.com/zowe/zowe-explorer-vscode/issues/2207) +- Added support for streams to the `getContents` and `putContents` functions (`FtpMvsApi` and `FtpUssApi` interfaces). +- **Breaking:** updated the `FtpMvsApi.putContents` function to throw an error when an e-tag conflict is found. + - This establishes consistency with the `FtpUssApi.putContents` function which has always thrown an error for this scenario. +- **Breaking:** Removed the deprecated `FtpUssApi.putContents` function in favor of the `FtpUssApi.putContent` function. + - The `putContents` function was deprecated in v2 in favor of the replacement function that offers the same capabilities, as well as the feature to upload from a buffer. + ### Bug fixes - Updated the SDK dependencies to `8.0.0-next.202403041352` for technical currency [#2754](https://github.com/zowe/vscode-extension-for-zowe/pull/2754). diff --git a/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts b/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts index 9336519172..4098e5d4dd 100644 --- a/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts +++ b/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Mvs/ZoweExplorerFtpMvsApi.unit.test.ts @@ -456,21 +456,9 @@ describe("FtpMvsApi", () => { const buf = Buffer.from("abc123"); const blockMocks = getBlockMocks(); - await MvsApi.uploadFromBuffer(buf, "SOME.DS(MEMB)"); - expect(blockMocks.tmpFileSyncMock).toHaveBeenCalled(); - expect(blockMocks.writeSyncMock).toHaveBeenCalled(); - expect(blockMocks.processNewlinesSpy).toHaveBeenCalled(); - }); - - it("should not process new lines when uploading buffer as binary", async () => { - const buf = Buffer.from("abc123"); - const blockMocks = getBlockMocks(); - blockMocks.processNewlinesSpy.mockImplementation(); - - await MvsApi.uploadFromBuffer(buf, "SOME.DS(MEMB)", { binary: true }); - expect(blockMocks.tmpFileSyncMock).toHaveBeenCalled(); - expect(blockMocks.writeSyncMock).toHaveBeenCalled(); - expect(blockMocks.processNewlinesSpy).not.toHaveBeenCalled(); + const dsName = "SOME.DS(MEMB)"; + await MvsApi.uploadFromBuffer(buf, dsName); + expect(blockMocks.putContents).toHaveBeenCalledWith(buf, dsName, undefined); }); }); }); diff --git a/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Uss/ZoweExplorerFtpUssApi.unit.test.ts b/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Uss/ZoweExplorerFtpUssApi.unit.test.ts index 3e0bfe4839..358f2344a8 100644 --- a/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Uss/ZoweExplorerFtpUssApi.unit.test.ts +++ b/packages/zowe-explorer-ftp-extension/__tests__/__unit__/Uss/ZoweExplorerFtpUssApi.unit.test.ts @@ -318,21 +318,9 @@ describe("FtpUssApi", () => { const buf = Buffer.from("abc123"); const blockMocks = getBlockMocks(); - await UssApi.uploadFromBuffer(buf, "/some/fs/path"); - expect(blockMocks.tmpFileSyncMock).toHaveBeenCalled(); - expect(blockMocks.writeSyncMock).toHaveBeenCalled(); - expect(blockMocks.processNewlinesSpy).toHaveBeenCalled(); - }); - - it("should not process new lines when uploading buffer as binary", async () => { - const buf = Buffer.from("abc123"); - const blockMocks = getBlockMocks(); - blockMocks.processNewlinesSpy.mockImplementation(); - - await UssApi.uploadFromBuffer(buf, "/some/fs/path", { binary: true }); - expect(blockMocks.tmpFileSyncMock).toHaveBeenCalled(); - expect(blockMocks.writeSyncMock).toHaveBeenCalled(); - expect(blockMocks.processNewlinesSpy).not.toHaveBeenCalled(); + const ussPath = "/some/fs/path"; + await UssApi.uploadFromBuffer(buf, ussPath); + expect(blockMocks.putContent).toHaveBeenCalledWith(buf, ussPath, undefined); }); }); }); diff --git a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerAbstractFtpApi.ts b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerAbstractFtpApi.ts index 2b13c079bd..a636bac582 100644 --- a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerAbstractFtpApi.ts +++ b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerAbstractFtpApi.ts @@ -10,6 +10,7 @@ */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import * as crypto from "crypto"; import { FTPConfig, IZosFTPProfile } from "@zowe/zos-ftp-for-zowe-cli"; import { imperative, MainframeInteraction } from "@zowe/zowe-explorer-api"; import * as globals from "./globals"; @@ -49,6 +50,12 @@ export abstract class AbstractFtpApi implements MainframeInteraction.ICommon { return this.session; } + protected hashBuffer(buffer: Buffer): string { + const hash = crypto.createHash("sha1"); + hash.update(buffer); + return hash.digest("hex"); + } + public getProfileTypeName(): string { return AbstractFtpApi.getProfileTypeName(); } diff --git a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts index af43445aaf..f9ce096983 100644 --- a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts +++ b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpMvsApi.ts @@ -12,10 +12,9 @@ import * as fs from "fs"; import * as crypto from "crypto"; import * as tmp from "tmp"; -import * as path from "path"; import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; -import { Gui, imperative, MainframeInteraction, MessageSeverity } from "@zowe/zowe-explorer-api"; +import { BufferBuilder, Gui, imperative, MainframeInteraction, MessageSeverity } from "@zowe/zowe-explorer-api"; import { DataSetUtils, TRANSFER_TYPE_ASCII, TRANSFER_TYPE_BINARY } from "@zowe/zos-ftp-for-zowe-cli"; import { AbstractFtpApi } from "./ZoweExplorerAbstractFtpApi"; import * as globals from "./globals"; @@ -80,27 +79,34 @@ export class FtpMvsApi extends AbstractFtpApi implements MainframeInteraction.IM } } - public async getContents(dataSetName: string, options: zosfiles.IDownloadOptions): Promise { + public async getContents(dataSetName: string, options: zosfiles.IDownloadSingleOptions): Promise { const result = this.getDefaultResponse(); - const targetFile = options.file; const transferOptions = { - transferType: options.binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, - localFile: targetFile, encoding: options.encoding, + localFile: undefined, + transferType: options.binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, }; + const fileOrStreamSpecified = options.file != null || options.stream != null; let connection; try { connection = await this.ftpClient(this.checkedProfile()); - if (connection && targetFile) { - imperative.IO.createDirsSyncFromFilePath(targetFile); - await DataSetUtils.downloadDataSet(connection, dataSetName, transferOptions); - result.success = true; - result.commandResponse = ""; - result.apiResponse.etag = await this.hashFile(targetFile); - } else { + if (!connection || !fileOrStreamSpecified) { globals.LOGGER.logImperativeMessage(result.commandResponse, MessageSeverity.ERROR); throw new Error(result.commandResponse); } + if (options.file) { + transferOptions.localFile = options.file; + imperative.IO.createDirsSyncFromFilePath(transferOptions.localFile); + await DataSetUtils.downloadDataSet(connection, dataSetName, transferOptions); + result.apiResponse.etag = await this.hashFile(transferOptions.localFile); + } else if (options.stream) { + const buffer = await DataSetUtils.downloadDataSet(connection, dataSetName, transferOptions); + result.apiResponse.etag = this.hashBuffer(buffer); + options.stream.write(buffer); + options.stream.end(); + } + result.success = true; + result.commandResponse = ""; return result; } catch (err) { throw new ZoweFtpExtensionError(err.message); @@ -110,47 +116,25 @@ export class FtpMvsApi extends AbstractFtpApi implements MainframeInteraction.IM } public async uploadFromBuffer(buffer: Buffer, dataSetName: string, options?: zosfiles.IUploadOptions): Promise { - const tempFile = tmp.fileSync(); - if (options?.binary) { - fs.writeSync(tempFile.fd, buffer); - } else { - const text = imperative.IO.processNewlines(buffer.toString()); - fs.writeSync(tempFile.fd, text); - } - - const result = await this.putContents(tempFile.name, dataSetName, options); + const result = await this.putContents(buffer, dataSetName, options); return result; } - public async putContents(inputFilePath: string, dataSetName: string, options: zosfiles.IUploadOptions): Promise { - const file = path.basename(inputFilePath).replace(/[^a-z0-9]+/gi, ""); - const member = file.substr(0, MAX_MEMBER_NAME_LEN); - let targetDataset: string; - const end = dataSetName.indexOf("("); - let dataSetNameWithoutMember: string; - if (end > 0) { - dataSetNameWithoutMember = dataSetName.substr(0, end); - } else { - dataSetNameWithoutMember = dataSetName; - } + public async putContents(input: string | Buffer, dataSetName: string, options: zosfiles.IUploadOptions): Promise { + const openParens = dataSetName.indexOf("("); + const dataSetNameWithoutMember = openParens > 0 ? dataSetName.substring(0, openParens) : dataSetName; const dsAtrribute = await this.dataSet(dataSetNameWithoutMember); - const dsorg = dsAtrribute.apiResponse.items[0].dsorg; - if (dsorg === "PS" || dataSetName.substr(dataSetName.length - 1) == ")") { - targetDataset = dataSetName; - } else { - targetDataset = dataSetName + "(" + member + ")"; - } const result = this.getDefaultResponse(); const profile = this.checkedProfile(); + const inputIsBuffer = input instanceof Buffer; + // Save-Save with FTP requires loading the file first // (moved this block above connection request so only one connection is active at a time) if (options.returnEtag && options.etag) { - const contentsTag = await this.getContentsTag(dataSetName); + const contentsTag = await this.getContentsTag(dataSetName, inputIsBuffer); if (contentsTag && contentsTag !== options.etag) { - result.success = false; - result.commandResponse = "Rest API failure with HTTP(S) status 412 Save conflict."; - return result; + throw Error("Rest API failure with HTTP(S) status 412: Save conflict"); } } let connection; @@ -161,11 +145,12 @@ export class FtpMvsApi extends AbstractFtpApi implements MainframeInteraction.IM throw new Error(result.commandResponse); } const lrecl: number = dsAtrribute.apiResponse.items[0].lrecl; - const data = fs.readFileSync(inputFilePath, { encoding: "utf8" }); + const data = inputIsBuffer ? input.toString() : fs.readFileSync(input, { encoding: "utf8" }); const transferOptions: Record = { - transferType: options.binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, - localFile: inputFilePath, + content: inputIsBuffer ? input : undefined, encoding: options.encoding, + localFile: inputIsBuffer ? undefined : input, + transferType: options.binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, }; if (profile.profile.secureFtp && data === "") { // substitute single space for empty DS contents when saving (avoids FTPS error) @@ -188,18 +173,16 @@ export class FtpMvsApi extends AbstractFtpApi implements MainframeInteraction.IM return result; } } - await DataSetUtils.uploadDataSet(connection, targetDataset, transferOptions); + await DataSetUtils.uploadDataSet(connection, dataSetName, transferOptions); result.success = true; if (options.returnEtag) { // release this connection instance because a new one will be made with getContentsTag this.releaseConnection(connection); connection = null; - const contentsTag = await this.getContentsTag(dataSetName); - result.apiResponse = [ - { - etag: contentsTag, - }, - ]; + const etag = await this.getContentsTag(dataSetName, inputIsBuffer); + result.apiResponse = { + etag, + }; } result.commandResponse = "Data set uploaded successfully."; return result; @@ -375,16 +358,21 @@ export class FtpMvsApi extends AbstractFtpApi implements MainframeInteraction.IM } } - private async getContentsTag(dataSetName: string): Promise { + private async getContentsTag(dataSetName: string, buffer?: boolean): Promise { + if (buffer) { + const builder = new BufferBuilder(); + const loadResult = await this.getContents(dataSetName, { binary: false, stream: builder }); + return loadResult.apiResponse.etag as string; + } + const tmpFileName = tmp.tmpNameSync(); const options: zosfiles.IDownloadOptions = { binary: false, file: tmpFileName, }; const loadResult = await this.getContents(dataSetName, options); - const etag: string = loadResult.apiResponse.etag; fs.rmSync(tmpFileName, { force: true }); - return etag; + return loadResult.apiResponse.etag as string; } private getDefaultResponse(): zosfiles.IZosFilesResponse { return { diff --git a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpUssApi.ts b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpUssApi.ts index 9de4eed312..7268dd4512 100644 --- a/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpUssApi.ts +++ b/packages/zowe-explorer-ftp-extension/src/ZoweExplorerFtpUssApi.ts @@ -15,11 +15,12 @@ import * as crypto from "crypto"; import * as tmp from "tmp"; import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; -import { imperative, MainframeInteraction } from "@zowe/zowe-explorer-api"; +import { BufferBuilder, imperative, MainframeInteraction, MessageSeverity } from "@zowe/zowe-explorer-api"; import { CoreUtils, UssUtils, TRANSFER_TYPE_ASCII, TRANSFER_TYPE_BINARY } from "@zowe/zos-ftp-for-zowe-cli"; import { Buffer } from "buffer"; import { AbstractFtpApi } from "./ZoweExplorerAbstractFtpApi"; import { ZoweFtpExtensionError } from "./ZoweFtpExtensionError"; +import { LOGGER } from "./globals"; // The Zowe FTP CLI plugin is written and uses mostly JavaScript, so relax the rules here. /* eslint-disable @typescript-eslint/no-unsafe-member-access */ @@ -59,25 +60,32 @@ export class FtpUssApi extends AbstractFtpApi implements MainframeInteraction.IU return false; // TODO: needs to be implemented checking file type } - public async getContents(ussFilePath: string, options: zosfiles.IDownloadOptions): Promise { + public async getContents(ussFilePath: string, options: zosfiles.IDownloadSingleOptions): Promise { const result = this.getDefaultResponse(); - const targetFile = options.file; const transferOptions = { transferType: options.binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, - localFile: targetFile, + localFile: undefined, size: 1, }; + const fileOrStreamSpecified = options.file != null || options.stream != null; let connection; try { connection = await this.ftpClient(this.checkedProfile()); - if (connection && targetFile) { - imperative.IO.createDirsSyncFromFilePath(targetFile); + if (!connection || !fileOrStreamSpecified) { + LOGGER.logImperativeMessage(result.commandResponse, MessageSeverity.ERROR); + throw new Error(result.commandResponse); + } + if (options.file) { + imperative.IO.createDirsSyncFromFilePath(options.file); await UssUtils.downloadFile(connection, ussFilePath, transferOptions); result.success = true; result.commandResponse = ""; - result.apiResponse.etag = await this.hashFile(targetFile); - } else { - throw new Error(result.commandResponse); + result.apiResponse.etag = await this.hashFile(options.file); + } else if (options.stream) { + const buffer = await UssUtils.downloadFile(connection, ussFilePath, transferOptions); + result.apiResponse.etag = this.hashBuffer(buffer); + options.stream.write(buffer); + options.stream.end(); } return result; } catch (err) { @@ -87,62 +95,41 @@ export class FtpUssApi extends AbstractFtpApi implements MainframeInteraction.IU } } - public async uploadFromBuffer(buffer: Buffer, filePath: string, options?: zosfiles.IUploadOptions): Promise { - const tempFile = tmp.fileSync(); - if (options?.binary) { - fs.writeSync(tempFile.fd, buffer); - } else { - const text = imperative.IO.processNewlines(buffer.toString()); - fs.writeSync(tempFile.fd, text); - } - - const result = await this.putContent(tempFile.name, filePath, options); - return result; - } - /** - * Upload a file (located at the input path) to the destination path. - * @param inputFilePath The input file path - * @param ussFilePath The destination file path on USS + * Uploads a USS file from the given buffer. + * @param buffer The buffer containing the contents of the USS file + * @param filePath The path for the USS file * @param options Any options for the upload * - * @returns A file response containing the results of the operation. + * @returns A file response with the results of the upload operation. */ - public putContent(inputFilePath: string, ussFilePath: string, options?: zosfiles.IUploadOptions): Promise { - return this.putContents(inputFilePath, ussFilePath, options?.binary, options?.localEncoding, options?.etag, options?.returnEtag); + public async uploadFromBuffer(buffer: Buffer, filePath: string, options?: zosfiles.IUploadOptions): Promise { + const result = await this.putContent(buffer, filePath, options); + return result; } /** * Upload a file (located at the input path) to the destination path. * - * @deprecated in favor of `putContent` - * @param inputFilePath The input file path + * @param input The input file path or buffer to upload * @param ussFilePath The destination file path on USS - * @param binary Whether the contents are binary - * @param localEncoding The local encoding for the file - * @param etag The e-tag associated with the file on the mainframe (optional) - * @param returnEtag Whether to return the e-tag after uploading the file + * @param options Any options for the upload * * @returns A file response containing the results of the operation. */ - public async putContents( - inputFilePath: string, - ussFilePath: string, - binary?: boolean, - localEncoding?: string, - etag?: string, - returnEtag?: boolean - ): Promise { + public async putContent(input: string | Buffer, ussFilePath: string, options?: zosfiles.IUploadOptions): Promise { + const inputIsBuffer = input instanceof Buffer; const transferOptions = { - transferType: binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, - localFile: inputFilePath, + content: inputIsBuffer ? input : undefined, + localFile: inputIsBuffer ? undefined : input, + transferType: options?.binary ? TRANSFER_TYPE_BINARY : TRANSFER_TYPE_ASCII, }; const result = this.getDefaultResponse(); // Save-Save with FTP requires loading the file first // (moved this block above connection request so only one connection is active at a time) - if (returnEtag && etag) { - const contentsTag = await this.getContentsTag(ussFilePath); - if (contentsTag && contentsTag !== etag) { + if (options?.returnEtag && options.etag) { + const contentsTag = await this.getContentsTag(ussFilePath, inputIsBuffer); + if (contentsTag && contentsTag !== options.etag) { throw new Error("Rest API failure with HTTP(S) status 412 Save conflict."); } } @@ -155,11 +142,11 @@ export class FtpUssApi extends AbstractFtpApi implements MainframeInteraction.IU await UssUtils.uploadFile(connection, ussFilePath, transferOptions); result.success = true; - if (returnEtag) { + if (options?.returnEtag) { // release this connection instance because a new one will be made with getContentsTag this.releaseConnection(connection); connection = null; - const contentsTag = await this.getContentsTag(ussFilePath); + const contentsTag = await this.getContentsTag(ussFilePath, inputIsBuffer); result.apiResponse.etag = contentsTag; } result.commandResponse = "File uploaded successfully."; @@ -285,16 +272,21 @@ export class FtpUssApi extends AbstractFtpApi implements MainframeInteraction.IU } } - private async getContentsTag(ussFilePath: string): Promise { + private async getContentsTag(ussFilePath: string, buffer?: boolean): Promise { + if (buffer) { + const writable = new BufferBuilder(); + const loadResult = await this.getContents(ussFilePath, { stream: writable }); + return loadResult.apiResponse.etag as string; + } + const tmpFileName = tmp.tmpNameSync(); const options: zosfiles.IDownloadOptions = { binary: false, file: tmpFileName, }; const loadResult = await this.getContents(ussFilePath, options); - const etag: string = loadResult.apiResponse.etag; fs.rmSync(tmpFileName, { force: true }); - return etag; + return loadResult.apiResponse.etag as string; } private getDefaultResponse(): zosfiles.IZosFilesResponse { diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 7bc85ce459..528c180d25 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -6,6 +6,13 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen ### New features and enhancements +- Implemented the FileSystemProvider for the Data Sets, Jobs and USS trees to handle all read/write actions as well as conflict resolution. [#2207](https://github.com/zowe/zowe-explorer-vscode/issues/2207) +- **Breaking:** Removed the `zowe.jobs.zosJobsOpenSpool` command in favor of using `vscode.open` with a spool URI. See the [FileSystemProvider wiki page](https://github.com/zowe/zowe-explorer-vscode/wiki/FileSystemProvider#file-paths-vs-uris) for more information on spool URIs. [#2207](https://github.com/zowe/zowe-explorer-vscode/issues/2207) +- **Breaking:** Removed the `zowe.ds.ZoweNode.openPS` command in favor of using `vscode.open` with a data set URI. See the [FileSystemProvider wiki page](https://github.com/zowe/zowe-explorer-vscode/wiki/FileSystemProvider#file-paths-vs-uris) for more information on data set URIs. [#2207](https://github.com/zowe/zowe-explorer-vscode/issues/2207) +- **Breaking:** Removed the `zowe.uss.ZoweUSSNode.open` command in favor of using `vscode.open` with a USS URI. See the [FileSystemProvider wiki page](https://github.com/zowe/zowe-explorer-vscode/wiki/FileSystemProvider#file-paths-vs-uris) for more information on USS URIs. [#2207](https://github.com/zowe/zowe-explorer-vscode/issues/2207) +- Added the `onResourceChanged` function to the `ZoweExplorerApiRegister` class to allow extenders to subscribe to any changes to Zowe resources (Data Sets, USS files/folders, Jobs, etc.). See the [FileSystemProvider wiki page](https://github.com/zowe/zowe-explorer-vscode/wiki/FileSystemProvider) for more information on Zowe resources. +- Added the `addFileSystemEvent` function to the `ZoweExplorerApiRegister` class to allow extenders to register their FileSystemProvider "onDidChangeFile" events. See the [FileSystemProvider wiki page](https://github.com/zowe/zowe-explorer-vscode/wiki/FileSystemProvider) for more information on the FileSystemProvider. + ### Bug fixes - Fixed issue where "Allocate Like" input box placeholder was showing a localization ID instead of the intended message ("Enter a name for the new data set"). [#2759](https://github.com/zowe/vscode-extension-for-zowe/issues/2759) diff --git a/packages/zowe-explorer/__mocks__/mockCreators/shared.ts b/packages/zowe-explorer/__mocks__/mockCreators/shared.ts index 1883f0ae5e..baf2eef400 100644 --- a/packages/zowe-explorer/__mocks__/mockCreators/shared.ts +++ b/packages/zowe-explorer/__mocks__/mockCreators/shared.ts @@ -330,7 +330,9 @@ export function createInstanceOfProfile(profile: imperative.IProfileLoaded) { validateProfiles: jest.fn(), getBaseProfile: jest.fn(), enableValidationContext: jest.fn(), + enableValidation: jest.fn(), disableValidationContext: jest.fn(), + disableValidation: jest.fn(), getProfileSetting: jest.fn(), resetValidationSettings: jest.fn(), getValidSession: jest.fn(), diff --git a/packages/zowe-explorer/__mocks__/mockCreators/uss.ts b/packages/zowe-explorer/__mocks__/mockCreators/uss.ts index b3b85b569b..211a3ba43c 100644 --- a/packages/zowe-explorer/__mocks__/mockCreators/uss.ts +++ b/packages/zowe-explorer/__mocks__/mockCreators/uss.ts @@ -30,6 +30,7 @@ export function createUSSTree(favoriteNodes: ZoweUSSNode[], sessionNodes: ZoweUS newTree.resetSearchHistory = jest.fn(); newTree.resetFileHistory = jest.fn(); newTree.refresh = jest.fn(); + newTree.nodeDataChanged = jest.fn(); newTree.checkCurrentProfile = jest.fn(); newTree.refreshElement = jest.fn(); newTree.getChildren = jest.fn(); @@ -54,17 +55,17 @@ export function createUSSNode(session, profile) { collapsibleState: vscode.TreeItemCollapsibleState.Expanded, session, profile, + contextOverride: globals.USS_SESSION_CONTEXT, }); + parentNode.fullPath = "/u/myuser"; const ussNode = new ZoweUSSNode({ label: "usstest", collapsibleState: vscode.TreeItemCollapsibleState.Expanded, parentNode, session, profile, + contextOverride: globals.USS_DIR_CONTEXT, }); - parentNode.contextValue = globals.USS_SESSION_CONTEXT; - ussNode.contextValue = globals.USS_DIR_CONTEXT; - parentNode.fullPath = "/u/myuser"; ussNode.fullPath = "/u/myuser/usstest"; return ussNode; } @@ -77,7 +78,7 @@ export function createUSSSessionNode(session: imperative.Session, profile: imper profile, parentPath: "/", }); - zoweUSSNode.fullPath = "test"; + zoweUSSNode.fullPath = "/test"; zoweUSSNode.contextValue = globals.USS_SESSION_CONTEXT; const targetIcon = getIconByNode(zoweUSSNode); if (targetIcon) { diff --git a/packages/zowe-explorer/__mocks__/mockUtils.ts b/packages/zowe-explorer/__mocks__/mockUtils.ts index 06a03e3e0a..f790f210d0 100644 --- a/packages/zowe-explorer/__mocks__/mockUtils.ts +++ b/packages/zowe-explorer/__mocks__/mockUtils.ts @@ -9,5 +9,130 @@ * */ +// Used for the MockedProperty class (polyfills for Symbol.{asyncDispose, dispose}) +require("disposablestack/auto"); + // Idea is borrowed from: https://github.com/kulshekhar/ts-jest/blob/master/src/util/testing.ts export const mocked = any>(fn: T): jest.Mock> => fn as any; + +enum MockedValueType { + Primitive, + Ref, + Function, +} + +/** + * _Please use this when possible instead of Object.defineProperty!_ + * + * A safer approach to "mocking" the value for a property that cannot be easily mocked using Jest.\ + * Uses TypeScript 5.2's Explicit Resource Management to restore the original value for the given object and property key. + */ +export class MockedProperty { + #key: PropertyKey; + #val: any; + #valType: MockedValueType; + #objRef: any; + #originalDescriptor?: PropertyDescriptor; + + private initValueType() { + if (typeof this.#val === "function" || jest.isMockFunction(this.#val)) { + this.#valType = MockedValueType.Function; + } else if (typeof this.#val === "object" || Array.isArray(this.#val)) { + this.#valType = MockedValueType.Ref; + } else { + this.#valType = MockedValueType.Primitive; + } + } + + constructor(object: any, key: PropertyKey, descriptor?: PropertyDescriptor, value?: any) { + if (object == null) { + throw new Error("Null or undefined object passed to MockedProperty"); + } + this.#objRef = object; + this.#originalDescriptor = descriptor ?? Object.getOwnPropertyDescriptor(object, key); + + if (!value) { + this.#val = jest.fn(); + this.#valType = MockedValueType.Function; + Object.defineProperty(object, key, { + value: this.#val, + configurable: true, + }); + return; + } + + const isValFn = typeof value === "function"; + + if (isValFn || (typeof descriptor?.value === "function" && value == null)) { + // wrap provided function around a Jest function, if needed + this.#val = jest.isMockFunction(value) ? value : jest.fn().mockImplementation(value); + } else { + this.#val = value; + } + + this.initValueType(); + + Object.defineProperty(object, key, { + value: this.#val, + configurable: true, + }); + } + + [Symbol.dispose](): void { + const isObjValid = this.#objRef != null; + if (isObjValid && !this.#originalDescriptor) { + // didn't exist to begin with, just delete it + delete this.#objRef[this.#key]; + return; + } + + if (this.#valType === MockedValueType.Function && jest.isMockFunction(this.#val)) { + this.#val.mockRestore(); + } + + if (isObjValid) { + Object.defineProperty(this.#objRef, this.#key, this.#originalDescriptor!); + } + } + + public get mock() { + if (!jest.isMockFunction(this.#val)) { + throw Error("MockedValue.mock called, but mocked value is not a Jest function"); + } + + return this.#val; + } + + public get value() { + return this.#val; + } + + public valueAs() { + return this.#val as T; + } +} + +export function isMockedProperty(val: any): val is MockedProperty { + return "Symbol.dispose" in val; +} + +export class MockCollection { + #obj: Record; + + constructor(obj: Record) { + this.#obj = obj; + } + + [Symbol.dispose](): void { + for (const k of Object.keys(this.#obj)) { + const property = this.#obj[k]; + if (isMockedProperty(property)) { + property[Symbol.dispose](); + } + } + } + + public dispose() { + this[Symbol.dispose](); + } +} diff --git a/packages/zowe-explorer/__mocks__/path.ts b/packages/zowe-explorer/__mocks__/path.ts deleted file mode 100644 index 00adfac79b..0000000000 --- a/packages/zowe-explorer/__mocks__/path.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - * - */ - -export function join(...paths: string[]): string { - const value = paths.join("/").replace("//", "/"); - return value; -} - -export function normalize(p: string): string { - return p; -} - -export function extname(file: string): string { - return ""; -} - -export function parse(file: string) { - return { name: file }; -} - -export function basename(path: string) { - return jest.requireActual("path")["basename"](path); -} diff --git a/packages/zowe-explorer/__mocks__/vscode.ts b/packages/zowe-explorer/__mocks__/vscode.ts index bd8d185e3e..0c92231fd6 100644 --- a/packages/zowe-explorer/__mocks__/vscode.ts +++ b/packages/zowe-explorer/__mocks__/vscode.ts @@ -60,6 +60,129 @@ export enum ViewColumn { Nine = 9, } +export interface DataTransferFile { + /** + * The name of the file. + */ + readonly name: string; + + /** + * The full file path of the file. + * + * May be `undefined` on web. + */ + readonly uri?: Uri; + + /** + * The full file contents of the file. + */ + data(): Thenable; +} + +/** + * Encapsulates data transferred during drag and drop operations. + */ +export class DataTransferItem { + /** + * Get a string representation of this item. + * + * If {@linkcode DataTransferItem.value} is an object, this returns the result of json stringifying {@linkcode DataTransferItem.value} value. + */ + async asString(): Promise { + return this.value ? this.value.toString() : null; + } + + /** + * Try getting the {@link DataTransferFile file} associated with this data transfer item. + * + * Note that the file object is only valid for the scope of the drag and drop operation. + * + * @returns The file for the data transfer or `undefined` if the item is either not a file or the + * file data cannot be accessed. + */ + asFile(): DataTransferFile | undefined { + return undefined; + } + + /** + * Custom data stored on this item. + * + * You can use `value` to share data across operations. The original object can be retrieved so long as the extension that + * created the `DataTransferItem` runs in the same extension host. + */ + readonly value: any; + + /** + * @param value Custom data stored on this item. Can be retrieved using {@linkcode DataTransferItem.value}. + */ + constructor(value: any) { + this.value = value; + } +} + +/** + * A map containing a mapping of the mime type of the corresponding transferred data. + * + * Drag and drop controllers that implement {@link TreeDragAndDropController.handleDrag `handleDrag`} can add additional mime types to the + * data transfer. These additional mime types will only be included in the `handleDrop` when the the drag was initiated from + * an element in the same drag and drop controller. + */ +export class DataTransfer { + /** + * Retrieves the data transfer item for a given mime type. + * + * @param mimeType The mime type to get the data transfer item for, such as `text/plain` or `image/png`. + * Mimes type look ups are case-insensitive. + * + * Special mime types: + * - `text/uri-list` — A string with `toString()`ed Uris separated by `\r\n`. To specify a cursor position in the file, + * set the Uri's fragment to `L3,5`, where 3 is the line number and 5 is the column number. + */ + get(mimeType: string): DataTransferItem | undefined { + return undefined; + } + + /** + * Sets a mime type to data transfer item mapping. + * + * @param mimeType The mime type to set the data for. Mimes types stored in lower case, with case-insensitive looks up. + * @param value The data transfer item for the given mime type. + */ + set(mimeType: string, value: DataTransferItem): void {} + + /** + * Allows iteration through the data transfer items. + * + * @param callbackfn Callback for iteration through the data transfer items. + * @param thisArg The `this` context used when invoking the handler function. + */ + forEach(callbackfn: (item: DataTransferItem, mimeType: string, dataTransfer: DataTransfer) => void, thisArg?: any): void {} +} + +/** + * Enumeration of file types. The types `File` and `Directory` can also be + * a symbolic links, in that case use `FileType.File | FileType.SymbolicLink` and + * `FileType.Directory | FileType.SymbolicLink`. + */ +export enum FileType { + /** + * The file type is unknown. + */ + Unknown = 0, + /** + * A regular file. + */ + File = 1, + /** + * A directory. + */ + Directory = 2, + /** + * A symbolic link to a file. + */ + SymbolicLink = 64, +} + /** * 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 @@ -240,6 +363,7 @@ export interface FileDecorationProvider { } export namespace window { + export const visibleTextEditors = []; /** * Options for creating a {@link TreeView} */ @@ -363,6 +487,13 @@ export namespace window { return {}; } } + +export namespace languages { + export function setTextDocumentLanguage(document: TextDocument, languageId: string): Thenable { + return {} as Thenable; + } +} + export namespace commands { /** * Registers a command that can be invoked via a keyboard shortcut, @@ -380,8 +511,8 @@ export namespace commands { return undefined as any; } - export function executeCommand(command: string): undefined { - return undefined; + export function executeCommand(command: string, ...rest: any[]): Thenable { + return undefined as any; } } export class Disposable { @@ -615,6 +746,307 @@ export class EventEmitter { //dispose(): void; } +export enum FilePermission { + /** + * The file is readonly. + * + * *Note:* All `FileStat` from a `FileSystemProvider` that is registered with + * the option `isReadonly: true` will be implicitly handled as if `FilePermission.Readonly` + * is set. As a consequence, it is not possible to have a readonly file system provider + * registered where some `FileStat` are not readonly. + */ + Readonly = 1, +} + +/** + * The `FileStat`-type represents metadata about a file + */ +export interface FileStat { + /** + * The type of the file, e.g. is a regular file, a directory, or symbolic link + * to a file. + * + * *Note:* This value might be a bitmask, e.g. `FileType.File | FileType.SymbolicLink`. + */ + type: FileType; + /** + * The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ + ctime: number; + /** + * The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + * + * *Note:* If the file changed, it is important to provide an updated `mtime` that advanced + * from the previous value. Otherwise there may be optimizations in place that will not show + * the updated file contents in an editor for example. + */ + mtime: number; + /** + * The size in bytes. + * + * *Note:* If the file changed, it is important to provide an updated `size`. Otherwise there + * may be optimizations in place that will not show the updated file contents in an editor for + * example. + */ + size: number; + /** + * The permissions of the file, e.g. whether the file is readonly. + * + * *Note:* This value might be a bitmask, e.g. `FilePermission.Readonly | FilePermission.Other`. + */ + permissions?: FilePermission; +} + +/** + * Enumeration of file change types. + */ +export enum FileChangeType { + /** + * The contents or metadata of a file have changed. + */ + Changed = 1, + + /** + * A file has been created. + */ + Created = 2, + + /** + * A file has been deleted. + */ + Deleted = 3, +} + +/** + * The event filesystem providers must use to signal a file change. + */ +export interface FileChangeEvent { + /** + * The type of change. + */ + readonly type: FileChangeType; + + /** + * The uri of the file that has changed. + */ + readonly uri: Uri; +} + +/** + * The filesystem provider defines what the editor needs to read, write, discover, + * and to manage files and folders. It allows extensions to serve files from remote places, + * like ftp-servers, and to seamlessly integrate those into the editor. + * + * * *Note 1:* The filesystem provider API works with {@link Uri uris} and assumes hierarchical + * paths, e.g. `foo:/my/path` is a child of `foo:/my/` and a parent of `foo:/my/path/deeper`. + * * *Note 2:* There is an activation event `onFileSystem:` that fires when a file + * or folder is being accessed. + * * *Note 3:* The word 'file' is often used to denote all {@link FileType kinds} of files, e.g. + * folders, symbolic links, and regular files. + */ +export interface FileSystemProvider { + /** + * An event to signal that a resource has been created, changed, or deleted. This + * event should fire for resources that are being {@link FileSystemProvider.watch watched} + * by clients of this provider. + * + * *Note:* It is important that the metadata of the file that changed provides an + * updated `mtime` that advanced from the previous value in the {@link FileStat stat} and a + * correct `size` value. Otherwise there may be optimizations in place that will not show + * the change in an editor for example. + */ + readonly onDidChangeFile: Event; + + /** + * Subscribes to file change events in the file or folder denoted by `uri`. For folders, + * the option `recursive` indicates whether subfolders, sub-subfolders, etc. should + * be watched for file changes as well. With `recursive: false`, only changes to the + * files that are direct children of the folder should trigger an event. + * + * The `excludes` array is used to indicate paths that should be excluded from file + * watching. It is typically derived from the `files.watcherExclude` setting that + * is configurable by the user. Each entry can be be: + * - the absolute path to exclude + * - a relative path to exclude (for example `build/output`) + * - a simple glob pattern (for example `**​/build`, `output/**`) + * + * It is the file system provider's job to call {@linkcode FileSystemProvider.onDidChangeFile onDidChangeFile} + * for every change given these rules. No event should be emitted for files that match any of the provided + * excludes. + * + * @param uri The uri of the file or folder to be watched. + * @param options Configures the watch. + * @returns A disposable that tells the provider to stop watching the `uri`. + */ + watch(uri: Uri, options: { readonly recursive: boolean; readonly excludes: readonly string[] }): Disposable; + + /** + * Retrieve metadata about a file. + * + * Note that the metadata for symbolic links should be the metadata of the file they refer to. + * Still, the {@link FileType.SymbolicLink SymbolicLink}-type must be used in addition to the actual type, e.g. + * `FileType.SymbolicLink | FileType.Directory`. + * + * @param uri The uri of the file to retrieve metadata about. + * @return The file metadata about the file. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist. + */ + stat(uri: Uri): FileStat | Thenable; + + /** + * Retrieve all entries of a {@link FileType.Directory directory}. + * + * @param uri The uri of the folder. + * @return An array of name/type-tuples or a thenable that resolves to such. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist. + */ + readDirectory(uri: Uri): [string, FileType][] | Thenable<[string, FileType][]>; + + /** + * Create a new directory (Note, that new files are created via `write`-calls). + * + * @param uri The uri of the new folder. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when the parent of `uri` doesn't exist, e.g. no mkdirp-logic required. + * @throws {@linkcode FileSystemError.FileExists FileExists} when `uri` already exists. + * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. + */ + createDirectory(uri: Uri): void | Thenable; + + /** + * Read the entire contents of a file. + * + * @param uri The uri of the file. + * @return An array of bytes or a thenable that resolves to such. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist. + */ + readFile(uri: Uri): Uint8Array | Thenable; + + /** + * Write data to a file, replacing its entire contents. + * + * @param uri The uri of the file. + * @param content The new content of the file. + * @param options Defines if missing files should or must be created. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist and `create` is not set. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when the parent of `uri` doesn't exist and `create` is set, e.g. no mkdirp-logic required. + * @throws {@linkcode FileSystemError.FileExists FileExists} when `uri` already exists, `create` is set but `overwrite` is not set. + * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. + */ + writeFile(uri: Uri, content: Uint8Array, options: { readonly create: boolean; readonly overwrite: boolean }): void | Thenable; + + /** + * Delete a file. + * + * @param uri The resource that is to be deleted. + * @param options Defines if deletion of folders is recursive. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist. + * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. + */ + delete(uri: Uri, options: { readonly recursive: boolean }): void | Thenable; + + /** + * Rename a file or folder. + * + * @param oldUri The existing file. + * @param newUri The new location. + * @param options Defines if existing files should be overwritten. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `oldUri` doesn't exist. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when parent of `newUri` doesn't exist, e.g. no mkdirp-logic required. + * @throws {@linkcode FileSystemError.FileExists FileExists} when `newUri` exists and when the `overwrite` option is not `true`. + * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. + */ + rename(oldUri: Uri, newUri: Uri, options: { readonly overwrite: boolean }): void | Thenable; + + /** + * Copy files or folders. Implementing this function is optional but it will speedup + * the copy operation. + * + * @param source The existing file. + * @param destination The destination location. + * @param options Defines if existing files should be overwritten. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `source` doesn't exist. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when parent of `destination` doesn't exist, e.g. no mkdirp-logic required. + * @throws {@linkcode FileSystemError.FileExists FileExists} when `destination` exists and when the `overwrite` option is not `true`. + * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. + */ + copy?(source: Uri, destination: Uri, options: { readonly overwrite: boolean }): void | Thenable; +} + +/** + * A type that filesystem providers should use to signal errors. + * + * This class has factory methods for common error-cases, like `FileNotFound` when + * a file or folder doesn't exist, use them like so: `throw vscode.FileSystemError.FileNotFound(someUri);` + */ +export class FileSystemError extends Error { + /** + * Create an error to signal that a file or folder wasn't found. + * @param messageOrUri Message or uri. + */ + static FileNotFound(messageOrUri?: string | Uri): FileSystemError { + return new FileSystemError("file not found"); + } + + /** + * Create an error to signal that a file or folder already exists, e.g. when + * creating but not overwriting a file. + * @param messageOrUri Message or uri. + */ + static FileExists(messageOrUri?: string | Uri): FileSystemError { + return new FileSystemError("file exists"); + } + + /** + * Create an error to signal that a file is not a folder. + * @param messageOrUri Message or uri. + */ + static FileNotADirectory(messageOrUri?: string | Uri): FileSystemError { + return new FileSystemError("file not a directory"); + } + + /** + * Create an error to signal that a file is a folder. + * @param messageOrUri Message or uri. + */ + static FileIsADirectory(messageOrUri?: string | Uri): FileSystemError { + return new FileSystemError("file is a directory"); + } + + /** + * Create an error to signal that an operation lacks required permissions. + * @param messageOrUri Message or uri. + */ + static NoPermissions(messageOrUri?: string | Uri): FileSystemError { + return new FileSystemError("no permissions"); + } + + /** + * Create an error to signal that the file system is unavailable or too busy to + * complete a request. + * @param messageOrUri Message or uri. + */ + static Unavailable(messageOrUri?: string | Uri): FileSystemError { + return new FileSystemError("unavailable"); + } + + /** + * Creates a new filesystem error. + * + * @param messageOrUri Message or uri. + */ + constructor(messageOrUri?: string | Uri) { + super(typeof messageOrUri === "string" ? messageOrUri : undefined); + } + + /** + * A code that identifies this error. + * + * Possible values are names of errors, like {@linkcode FileSystemError.FileNotFound FileNotFound}, + * or `Unknown` for unspecified errors. + */ + readonly code: string; +} + /** * Namespace for dealing with the current workspace. A workspace is the representation * of the folder that has been opened. There is no workspace when just a file but not a @@ -625,8 +1057,29 @@ export class EventEmitter { * the editor-process so that they should be always used instead of nodejs-equivalents. */ export namespace workspace { - export function onDidSaveTextDocument(listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) {} + export const textDocuments: TextDocument[] = []; + /** + * Register a filesystem provider for a given scheme, e.g. `ftp`. + * + * There can only be one provider per scheme and an error is being thrown when a scheme + * has been claimed by another provider or when it is reserved. + * + * @param scheme The uri-{@link Uri.scheme scheme} the provider registers for. + * @param provider The filesystem provider. + * @param options Immutable metadata about the provider. + * @return A {@link Disposable} that unregisters this provider when being disposed. + */ + export function registerFileSystemProvider( + scheme: string, + provider: FileSystemProvider, + options?: { readonly isCaseSensitive?: boolean; readonly isReadonly?: boolean } + ): Disposable { + return new Disposable(); + } + export function onDidCloseTextDocument(listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) {} + export function onDidOpenTextDocument(listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) {} + export function onDidSaveTextDocument(listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) {} export function getConfiguration(configuration: string) { return { @@ -676,6 +1129,134 @@ export namespace workspace { */ readonly index: number; } + + export namespace fs { + /** + * Retrieve metadata about a file. + * + * Note that the metadata for symbolic links should be the metadata of the file they refer to. + * Still, the {@link FileType.SymbolicLink SymbolicLink}-type must be used in addition to the actual type, e.g. + * `FileType.SymbolicLink | FileType.Directory`. + * + * @param uri The uri of the file to retrieve metadata about. + * @returns The file metadata about the file. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist. + */ + export function stat(uri: Uri): FileStat | Thenable { + return {} as FileStat; + } + + /** + * Retrieve all entries of a {@link FileType.Directory directory}. + * + * @param uri The uri of the folder. + * @returns An array of name/type-tuples or a thenable that resolves to such. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist. + */ + export function readDirectory(uri: Uri): Array<[string, FileType]> | Thenable> { + return []; + } + + /** + * Create a new directory (Note, that new files are created via `write`-calls). + * + * @param uri The uri of the new folder. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when the parent of `uri` doesn't exist, e.g. no mkdirp-logic required. + * @throws {@linkcode FileSystemError.FileExists FileExists} when `uri` already exists. + * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. + */ + export function createDirectory(uri: Uri): void | Thenable { + return; + } + + /** + * Read the entire contents of a file. + * + * @param uri The uri of the file. + * @returns An array of bytes or a thenable that resolves to such. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist. + */ + export function readFile(uri: Uri): Uint8Array | Thenable { + return new Uint8Array(); + } + + /** + * Write data to a file, replacing its entire contents. + * + * @param uri The uri of the file. + * @param content The new content of the file. + * @param options Defines if missing files should or must be created. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist and `create` is not set. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when the parent of `uri` doesn't exist and `create` is set, e.g. no mkdirp-logic required. + * @throws {@linkcode FileSystemError.FileExists FileExists} when `uri` already exists, `create` is set but `overwrite` is not set. + * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. + */ + export function writeFile( + uri: Uri, + content: Uint8Array, + options: { + /** + * Create the file if it does not exist already. + */ + readonly create: boolean; + /** + * Overwrite the file if it does exist. + */ + readonly overwrite: boolean; + } + ): void | Thenable { + return; + } + + /** + * Rename a file or folder. + * + * @param oldUri The existing file. + * @param newUri The new location. + * @param options Defines if existing files should be overwritten. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `oldUri` doesn't exist. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when parent of `newUri` doesn't exist, e.g. no mkdirp-logic required. + * @throws {@linkcode FileSystemError.FileExists FileExists} when `newUri` exists and when the `overwrite` option is not `true`. + * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. + */ + export function rename( + oldUri: Uri, + newUri: Uri, + options: { + /** + * Overwrite the file if it does exist. + */ + readonly overwrite: boolean; + } + ): void | Thenable { + return; + } + + /** + * Copy files or folders. Implementing this function is optional but it will speedup + * the copy operation. + * + * @param source The existing file. + * @param destination The destination location. + * @param options Defines if existing files should be overwritten. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `source` doesn't exist. + * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when parent of `destination` doesn't exist, e.g. no mkdirp-logic required. + * @throws {@linkcode FileSystemError.FileExists FileExists} when `destination` exists and when the `overwrite` option is not `true`. + * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. + */ + export function copy( + source: Uri, + destination: Uri, + options: { + /** + * Overwrite the file if it does exist. + */ + readonly overwrite: boolean; + } + ): void | Thenable { + return; + } + } } export interface InputBoxOptions { @@ -696,13 +1277,128 @@ export class Uri { return newUri; } - public with(_fragment: string): Uri { - return this; + + public with(change: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): Uri { + let newUri = Uri.from(this); + + if (change.scheme) { + newUri.scheme = change.scheme; + } + + if (change.authority) { + newUri.authority = change.authority; + } + + if (change.path) { + newUri.path = change.path; + } + + if (change.query) { + newUri.query = change.query; + } + + if (change.fragment) { + newUri.fragment = change.fragment; + } + + return newUri !== this ? newUri : this; + } + + public static from(components: { + readonly scheme: string; + readonly authority?: string; + readonly path?: string; + readonly query?: string; + readonly fragment?: string; + }): Uri { + let uri = new Uri(); + if (components.path != null) { + uri.path = components.path; + } + if (components.scheme) { + uri.scheme = components.scheme; + } + if (components.authority) { + uri.authority = components.authority; + } + if (components.query) { + uri.query = components.query; + } + if (components.fragment) { + uri.fragment = components.fragment; + } + return uri; } - public path: string; + /** + * Scheme is the `http` part of `http://www.example.com/some/path?query#fragment`. + * The part before the first colon. + */ + scheme: string; + + /** + * Authority is the `www.example.com` part of `http://www.example.com/some/path?query#fragment`. + * The part between the first double slashes and the next slash. + */ + authority: string; + + /** + * Path is the `/some/path` part of `http://www.example.com/some/path?query#fragment`. + */ + path: string; + + /** + * Query is the `query` part of `http://www.example.com/some/path?query#fragment`. + */ + query: string; + + /** + * Fragment is the `fragment` part of `http://www.example.com/some/path?query#fragment`. + */ + fragment: string; + + /** + * The string representing the corresponding file system path of this Uri. + * + * Will handle UNC paths and normalize windows drive letters to lower-case. Also + * uses the platform specific path separator. + * + * * Will *not* validate the path for invalid characters and semantics. + * * Will *not* look at the scheme of this Uri. + * * The resulting string shall *not* be used for display purposes but + * for disk operations, like `readFile` et al. + * + * The *difference* to the {@linkcode Uri.path path}-property is the use of the platform specific + * path separator and the handling of UNC paths. The sample below outlines the difference: + * ```ts + * const u = URI.parse('file://server/c$/folder/file.txt') + * u.authority === 'server' + * u.path === '/shares/c$/file.txt' + * u.fsPath === '\\server\c$\folder\file.txt' + * ``` + */ + fsPath: string; + public toString(): string { - return this.path; + let result = this.scheme ? `${this.scheme}://` : ""; + + if (this.authority) { + result += `${this.authority}`; + } + + if (this.path) { + result += `${this.path}`; + } + + if (this.query) { + result += `?${this.query}`; + } + + if (this.fragment) { + result += `#${this.fragment}`; + } + + return result; } } diff --git a/packages/zowe-explorer/__tests__/__common__/testUtils.ts b/packages/zowe-explorer/__tests__/__common__/testUtils.ts index 57cfe92a5d..4f02e9e198 100644 --- a/packages/zowe-explorer/__tests__/__common__/testUtils.ts +++ b/packages/zowe-explorer/__tests__/__common__/testUtils.ts @@ -28,15 +28,13 @@ export interface IJestIt { title?: string; } -export function spyOnSubscriptions(subscriptions: any[]) { - subscriptions.forEach((sub) => { - sub.mock.forEach((mock) => { - if (mock.ret) { - mock.spy.mockClear().mockReturnValueOnce(mock.ret); - } else { - mock.spy.mockClear().mockImplementation(jest.fn()); - } - }); +function spyOnSubscription(sub: IJestIt) { + sub.mock.forEach((mock) => { + if (mock.ret) { + mock.spy.mockClear().mockReturnValue(mock.ret); + } else { + mock.spy.mockClear().mockImplementation(jest.fn()); + } }); } @@ -45,6 +43,7 @@ export function processSubscriptions(subscriptions: IJestIt[], test: ITestContex return str.indexOf(":") >= 0 ? str.substring(0, str.indexOf(":")) : str; }; subscriptions.forEach((sub) => { + spyOnSubscription(sub); it(sub.title ?? `Test: ${sub.name}`, async () => { const parms = sub.parm ?? [test.value]; await test.context.subscriptions.find((s) => Object.keys(s)[0] === getName(sub.name))?.[getName(sub.name)](...parms); diff --git a/packages/zowe-explorer/__tests__/__integration__/DatasetTree.integration.test.ts b/packages/zowe-explorer/__tests__/__integration__/DatasetTree.integration.test.ts index 4195a6aa10..60ce2dd402 100644 --- a/packages/zowe-explorer/__tests__/__integration__/DatasetTree.integration.test.ts +++ b/packages/zowe-explorer/__tests__/__integration__/DatasetTree.integration.test.ts @@ -127,9 +127,6 @@ describe("DatasetTree Integration Tests", async () => { ]; sampleRChildren[2].dirty = false; // Because getChildren was subsequently called. - sampleRChildren[0].command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [sampleRChildren[0]] }; - sampleRChildren[3].command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [sampleRChildren[3]] }; - const samplePChildren: ZoweDatasetNode[] = [ new ZoweDatasetNode({ label: "TCHILD1", @@ -142,9 +139,6 @@ describe("DatasetTree Integration Tests", async () => { parentNode: sampleRChildren[2], }), ]; - - samplePChildren[0].command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [samplePChildren[0]] }; - samplePChildren[1].command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [samplePChildren[1]] }; sampleRChildren[2].children = samplePChildren; // Checking that the rootChildren are what they are expected to be diff --git a/packages/zowe-explorer/__tests__/__integration__/USSTree.integration.test.ts b/packages/zowe-explorer/__tests__/__integration__/USSTree.integration.test.ts index 8deca22451..db64b9736d 100644 --- a/packages/zowe-explorer/__tests__/__integration__/USSTree.integration.test.ts +++ b/packages/zowe-explorer/__tests__/__integration__/USSTree.integration.test.ts @@ -115,14 +115,14 @@ describe("USSTree Integration Tests", async () => { ]; sampleRChildren[0].command = { - command: "zowe.uss.ZoweUSSNode.open", + command: "vscode.open", title: "", - arguments: [sampleRChildren[0]], + arguments: [sampleRChildren[0].resourceUri], }; sampleRChildren[3].command = { - command: "zowe.uss.ZoweUSSNode.open", + command: "vscode.open", title: "", - arguments: [sampleRChildren[3]], + arguments: [sampleRChildren[3].resourceUri], }; const samplePChildren: ZoweUSSNode[] = [ @@ -131,14 +131,14 @@ describe("USSTree Integration Tests", async () => { ]; samplePChildren[0].command = { - command: "zowe.uss.ZoweUSSNode.open", + command: "vscode.open", title: "", - arguments: [samplePChildren[0]], + arguments: [samplePChildren[0].resourceUri], }; samplePChildren[1].command = { - command: "zowe.uss.ZoweUSSNode.open", + command: "vscode.open", title: "", - arguments: [samplePChildren[1]], + arguments: [samplePChildren[1].resourceUri], }; sampleRChildren[2].children = samplePChildren; diff --git a/packages/zowe-explorer/__tests__/__integration__/ZoweNode.integration.test.ts b/packages/zowe-explorer/__tests__/__integration__/ZoweNode.integration.test.ts index 7ac1329bb1..a9a3148e8b 100644 --- a/packages/zowe-explorer/__tests__/__integration__/ZoweNode.integration.test.ts +++ b/packages/zowe-explorer/__tests__/__integration__/ZoweNode.integration.test.ts @@ -115,9 +115,6 @@ describe("ZoweNode Integration Tests", async () => { new ZoweDatasetNode({ label: pattern + ".PUBLIC.TPS", collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: sessNode }), ]; - sampleChildren[0].command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [sampleChildren[0]] }; - sampleChildren[3].command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [sampleChildren[3]] }; - // Checking that the rootChildren are what they are expected to be expect(sessChildren).toEqual(sampleChildren); }).timeout(TIMEOUT); diff --git a/packages/zowe-explorer/__tests__/__integration__/ZoweUSSNode.integration.test.ts b/packages/zowe-explorer/__tests__/__integration__/ZoweUSSNode.integration.test.ts index 18dd7e5a32..395c136751 100644 --- a/packages/zowe-explorer/__tests__/__integration__/ZoweUSSNode.integration.test.ts +++ b/packages/zowe-explorer/__tests__/__integration__/ZoweUSSNode.integration.test.ts @@ -104,8 +104,8 @@ describe("ZoweUSSNode Integration Tests", async () => { new ZoweUSSNode({ label: path + "/group/aDir6", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, parentNode: sessNode }), ]; - sampleChildren[0].command = { command: "zowe.uss.ZoweUSSNode.open", title: "", arguments: [sampleChildren[0]] }; - sampleChildren[1].command = { command: "zowe.uss.ZoweUSSNode.open", title: "", arguments: [sampleChildren[1]] }; + sampleChildren[0].command = { command: "vscode.open", title: "", arguments: [sampleChildren[0].resourceUri] }; + sampleChildren[1].command = { command: "vscode.open", title: "", arguments: [sampleChildren[1].resourceUri] }; // Checking that the rootChildren are what they are expected to be expect(sessChildren.length).toBe(4); diff --git a/packages/zowe-explorer/__tests__/__integration__/extension.integration.test.ts b/packages/zowe-explorer/__tests__/__integration__/extension.integration.test.ts index eac957ba1c..91b4ece76d 100644 --- a/packages/zowe-explorer/__tests__/__integration__/extension.integration.test.ts +++ b/packages/zowe-explorer/__tests__/__integration__/extension.integration.test.ts @@ -331,103 +331,6 @@ describe("Extension Integration Tests", async () => { }).timeout(TIMEOUT); }); - describe("Opening a PS", () => { - it("should open a PS", async () => { - const node = new ZoweDatasetNode({ - label: pattern + ".EXT.PS", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: sessionNode, - }); - await node.openDs(false, true, testTree); - expect( - path.relative(vscode.window.activeTextEditor.document.fileName, sharedUtils.getDocumentFilePath(pattern + ".EXT.PS", node)) - ).to.equal(""); - expect(fs.existsSync(sharedUtils.getDocumentFilePath(pattern + ".EXT.PS", node))).to.equal(true); - }).timeout(TIMEOUT); - - it("should display an error message when openDs is passed an invalid node", async () => { - const node = new ZoweDatasetNode({ - label: pattern + ".GARBAGE", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: sessionNode, - }); - const errorMessageStub = sandbox.spy(vscode.window, "showErrorMessage"); - await expect(node.openDs(false, true, testTree)).to.eventually.be.rejectedWith(Error); - - const called = errorMessageStub.called; - expect(called).to.equal(true); - }).timeout(TIMEOUT); - }); - - describe("Saving a File", () => { - it("should download, change, and re-upload a PS", async () => { - // Test for PS under HLQ - const profiles = await testTree.getChildren(); - profiles[1].dirty = true; - const children = await profiles[1].getChildren(); - children[1].dirty = true; - await children[1].openDs(false, true, testTree); - - const changedData = "PS Upload Test"; - - fs.writeFileSync(path.join(globals.ZOWETEMPFOLDER, children[1].label.toString() + "[" + profiles[1].label.toString() + "]"), changedData); - - // Upload file - const doc = await vscode.workspace.openTextDocument( - path.join(globals.ZOWETEMPFOLDER, children[1].label.toString() + "[" + profiles[1].label.toString() + "]") - ); - await dsActions.saveFile(doc, testTree); - - // Download file - await children[1].openDs(false, true, testTree); - - expect(doc.getText().trim()).to.deep.equal("PS Upload Test"); - - // Change contents back - const originalData = ""; - fs.writeFileSync(path.join(path.join(globals.ZOWETEMPFOLDER, children[1].label.toString())), originalData); - }).timeout(TIMEOUT); - - it("should download, change, and re-upload a PDS member", async () => { - // Test for PS under HLQ - const profiles = await testTree.getChildren(); - profiles[1].dirty = true; - const children = await profiles[1].getChildren(); - children[0].dirty = true; - - // Test for member under PO - const childrenMembers = await testTree.getChildren(children[0]); - await childrenMembers[0].openDs(false, true, testTree); - - const changedData2 = "PO Member Upload Test"; - - fs.writeFileSync( - path.join(globals.ZOWETEMPFOLDER, children[0].label.toString() + "(" + childrenMembers[0].label.toString() + ")"), - changedData2 - ); - - // Upload file - const doc2 = await vscode.workspace.openTextDocument( - path.join(globals.ZOWETEMPFOLDER, children[0].label.toString() + "(" + childrenMembers[0].label.toString() + ")") - ); - await dsActions.saveFile(doc2, testTree); - - // Download file - await childrenMembers[0].openDs(false, true, testTree); - - expect(doc2.getText().trim()).to.deep.equal("PO Member Upload Test"); - - // Change contents back - const originalData2 = ""; - fs.writeFileSync( - path.join(globals.ZOWETEMPFOLDER, children[0].label.toString() + "(" + childrenMembers[0].label.toString() + ")"), - originalData2 - ); - }).timeout(TIMEOUT); - - // TODO add tests for saving data set from favorites - }); - describe("Copying data sets", () => { beforeEach(() => { const favProfileNode = new ZoweDatasetNode({ @@ -1136,41 +1039,6 @@ describe("Extension Integration Tests - USS", () => { expect(gotCalled).to.equal(true); }).timeout(TIMEOUT); }); - - describe("Saving a USS File", () => { - // TODO Move to appropriate class - it("should download, change, and re-upload a file", async () => { - const changedData = "File Upload Test " + Math.random().toString(36).slice(2); - - const rootChildren = await ussTestTree.getChildren(); - rootChildren[0].dirty = true; - const sessChildren1 = await ussTestTree.getChildren(rootChildren[0]); - sessChildren1[3].dirty = true; - const sessChildren2 = await ussTestTree.getChildren(sessChildren1[3]); - sessChildren2[2].dirty = true; - const dirChildren = await ussTestTree.getChildren(sessChildren2[2]); - const localPath = path.join(globals.USS_DIR, testConst.profile.name, dirChildren[0].fullPath || ""); - - await dirChildren[0].openUSS(false, true, ussTestTree); - const doc = await vscode.workspace.openTextDocument(localPath); - - const originalData = doc.getText().trim(); - - // write new data - fs.writeFileSync(localPath, changedData); - - // Upload file - await ussActions.saveUSSFile(doc, ussTestTree); - await fs.unlinkSync(localPath); - - // Download file - await dirChildren[0].openUSS(false, true, ussTestTree); - - // Change contents back - fs.writeFileSync(localPath, originalData); - await ussActions.saveUSSFile(doc, ussTestTree); - }).timeout(TIMEOUT); - }); }); describe("TreeView", () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/Profiles.extended.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/Profiles.extended.unit.test.ts index d4f55af90f..1e1069c00a 100644 --- a/packages/zowe-explorer/__tests__/__unit__/Profiles.extended.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/Profiles.extended.unit.test.ts @@ -42,6 +42,9 @@ import { ZoweLogger } from "../../src/utils/ZoweLogger"; import { ZoweLocalStorage } from "../../src/utils/ZoweLocalStorage"; jest.mock("../../src/utils/ZoweLogger"); import { TreeProviders } from "../../src/shared/TreeProviders"; +import { UssFSProvider } from "../../src/uss/UssFSProvider"; +import { JobFSProvider } from "../../src/job/JobFSProvider"; +import { DatasetFSProvider } from "../../src/dataset/DatasetFSProvider"; jest.mock("child_process"); jest.mock("fs"); @@ -86,8 +89,15 @@ async function createGlobalMocks() { mockProfilesCache: null, mockConfigInstance: createConfigInstance(), mockConfigLoad: null, + FileSystemProvider: { + createDirectory: jest.fn(), + }, }; + jest.spyOn(DatasetFSProvider.instance, "createDirectory").mockImplementation(newMocks.FileSystemProvider.createDirectory); + jest.spyOn(JobFSProvider.instance, "createDirectory").mockImplementation(newMocks.FileSystemProvider.createDirectory); + jest.spyOn(UssFSProvider.instance, "createDirectory").mockImplementation(newMocks.FileSystemProvider.createDirectory); + newMocks.mockProfilesCache = new ProfilesCache(imperative.Logger.getAppLogger()); newMocks.withProgress = jest.fn().mockImplementation((_progLocation, _callback) => { return newMocks.mockCallback; diff --git a/packages/zowe-explorer/__tests__/__unit__/SpoolProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/SpoolProvider.unit.test.ts deleted file mode 100644 index a944c0cdf7..0000000000 --- a/packages/zowe-explorer/__tests__/__unit__/SpoolProvider.unit.test.ts +++ /dev/null @@ -1,300 +0,0 @@ -/** - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - * - */ - -import SpoolProvider, { decodeJobFile, encodeJobFile, SpoolFile, matchSpool, getSpoolFiles } from "../../src/SpoolProvider"; -import * as zosjobs from "@zowe/zos-jobs-for-zowe-sdk"; -import { imperative } from "@zowe/zowe-explorer-api"; -import * as vscode from "vscode"; -import { Profiles } from "../../src/Profiles"; -import { createIProfile, createISessionWithoutCredentials } from "../../__mocks__/mockCreators/shared"; -import { bindJesApi, createJesApi } from "../../__mocks__/mockCreators/api"; -import { createJobSessionNode } from "../../__mocks__/mockCreators/jobs"; - -describe("SpoolProvider Unit Tests", () => { - const iJobFile: zosjobs.IJobFile = { - "byte-count": 128, - "job-correlator": "", - "record-count": 1, - "records-url": "fake/records", - class: "A", - ddname: "STDOUT", - id: 100, - jobid: "100", - jobname: "TESTJOB", - lrecl: 80, - procstep: "", - recfm: "FB", - stepname: "", - subsystem: "", - }; - const uriString = - 'zosspool:TESTJOB.100.STDOUT?["sessionName",{"byte-count":128,"job-correlator":"",' + - '"record-count":1,"records-url":"fake/records","class":"A","ddname":"STDOUT","id":100,"job' + - 'id":"100","jobname":"TESTJOB","lrecl":80,"procstep":"","recfm":"FB","stepname":"","subsystem":""}]'; - - const uriObj: vscode.Uri = { - scheme: "zosspool", - authority: "", - path: "TESTJOB.100.STDOUT", - query: - '["sessionName",{"byte-count":128,"job-correlator":"",' + - '"record-count":1,"records-url":"fake/records","class":"A","ddname":"STDOUT","id":100,"job' + - 'id":"100","jobname":"TESTJOB","lrecl":80,"procstep":"","recfm":"FB","stepname":"","subsystem":""}]', - fragment: "", - fsPath: "", - with: jest.fn(), - toJSON: jest.fn(), - }; - const fullIJobFile: zosjobs.IJobFile = { - "byte-count": 128, - "job-correlator": "", - "record-count": 1, - "records-url": "fake/records", - class: "A", - ddname: "STDOUT", - id: 100, - jobid: "JOB100", - jobname: "TESTJOB", - lrecl: 80, - procstep: "TESTPROC", - recfm: "FB", - stepname: "TESTSTEP", - subsystem: "", - }; - const fullSpoolFilePath = "TESTJOB.JOB100.TESTSTEP.TESTPROC.STDOUT.100"; - const profilesForValidation = { status: "active", name: "fake" }; - - Object.defineProperty(Profiles, "getInstance", { - value: jest.fn(() => { - return { - allProfiles: [{ name: "firstName" }, { name: "secondName" }], - defaultProfile: { name: "firstName" }, - checkCurrentProfile: jest.fn(() => { - return profilesForValidation; - }), - profilesForValidation: [], - validateProfiles: jest.fn(), - }; - }), - }); - Object.defineProperty(Profiles, "getDefaultProfile", { - value: jest.fn(() => { - return { - name: "firstName", - }; - }), - }); - Object.defineProperty(Profiles, "loadNamedProfile", { - value: jest.fn(() => { - return { - name: "firstName", - }; - }), - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("Tests that the URI is encoded", () => { - const uriMock = jest.fn(); - Object.defineProperty(vscode, "Uri", { value: uriMock }); - const mockUri = { - scheme: "testScheme", - authority: "testAuthority", - path: "testPath", - query: "testQuery", - fragment: "testFragment", - fsPath: "testFsPath", - with: jest.fn().mockReturnValue(uriString), - toJSON: jest.fn(), - }; - - const parse = jest.fn().mockReturnValue(mockUri); - Object.defineProperty(uriMock, "parse", { value: parse }); - const query = jest.fn(); - Object.defineProperty(uriMock, "query", { value: query }); - - encodeJobFile("sessionName", iJobFile); - expect(mockUri.with.mock.calls.length).toEqual(1); - expect(mockUri.with.mock.calls[0][0]).toEqual({ - path: "TESTJOB.100.STDOUT.100", - query: - '["sessionName",{' + - '"byte-count":128,' + - '"job-correlator":"",' + - '"record-count":1,' + - '"records-url":"fake/records",' + - '"class":"A",' + - '"ddname":"STDOUT",' + - '"id":100,' + - '"jobid":"100",' + - '"jobname":"TESTJOB",' + - '"lrecl":80,' + - '"procstep":"",' + - '"recfm":"FB",' + - '"stepname":"",' + - '"subsystem":""' + - "}]", - scheme: "zosspool", - }); - }); - - it("Tests that URI is encoded with all present segments", () => { - const uriMock = jest.fn(); - Object.defineProperty(vscode, "Uri", { value: uriMock }); - const mockUri = { - with: jest.fn().mockImplementation((v) => ({ - scheme: "testScheme", - authority: "testAuthority", - path: v.path, - query: "testQuery", - fragment: "testFragment", - fsPath: "testFsPath", - })), - toJSON: jest.fn(), - }; - - const parse = jest.fn().mockReturnValue(mockUri); - Object.defineProperty(uriMock, "parse", { value: parse }); - - const uri = encodeJobFile("sessionName", fullIJobFile); - expect(uri.path).toEqual(fullSpoolFilePath); - }); - - it("Tests that the URI is decoded", () => { - const [sessionName, spool] = decodeJobFile(uriObj); - expect(sessionName).toEqual(sessionName); - expect(spool).toEqual(iJobFile); - }); - - it("Tests that the spool content is returned", async () => { - const getSpoolContentById = jest.fn(); - const profileOne: imperative.IProfileLoaded = { - name: "sessionName", - profile: { - user: undefined, - password: undefined, - }, - type: "zosmf", - message: "", - failNotFound: false, - }; - const mockLoadNamedProfile = jest.fn(); - mockLoadNamedProfile.mockReturnValue(profileOne); - Object.defineProperty(Profiles, "getInstance", { - value: jest.fn(() => { - return { - allProfiles: [profileOne, { name: "secondName" }], - defaultProfile: profileOne, - checkCurrentProfile: jest.fn(() => { - return profilesForValidation; - }), - validateProfiles: jest.fn(), - loadNamedProfile: mockLoadNamedProfile, - }; - }), - }); - Object.defineProperty(zosjobs.GetJobs, "getSpoolContentById", { value: getSpoolContentById }); - getSpoolContentById.mockReturnValue("spool content"); - - const provider = new SpoolProvider(); - - // the first time the file is provided by SpoolProvider, it will fetch the latest spool content - const fetchContentSpy = jest.spyOn(SpoolFile.prototype, "fetchContent"); - const content = await provider.provideTextDocumentContent(uriObj); - expect(fetchContentSpy).toHaveBeenCalled(); - - expect(content).toBe("spool content"); - expect(getSpoolContentById.mock.calls.length).toEqual(1); - expect(getSpoolContentById.mock.calls[0][1]).toEqual(iJobFile.jobname); - expect(getSpoolContentById.mock.calls[0][2]).toEqual(iJobFile.jobid); - expect(getSpoolContentById.mock.calls[0][3]).toEqual(iJobFile.id); - await provider.provideTextDocumentContent(uriObj); - }); - - it("disposes the event emitter when the content provider is disposed", () => { - SpoolProvider.onDidChangeEmitter = { - dispose: jest.fn(), - } as unknown as vscode.EventEmitter; - const testProvider = new SpoolProvider(); - testProvider.dispose(); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(SpoolProvider.onDidChangeEmitter.dispose).toHaveBeenCalled(); - }); - - describe("matchSpool", () => { - it("should match spool to the selected node", () => { - const spool: zosjobs.IJobFile = { ...iJobFile, stepname: "test", ddname: "dd", "record-count": 1, procstep: "proc" }; - let match = matchSpool(spool, { label: "test:dd - 1" } as any); - expect(match).toBe(true); - - match = matchSpool(spool, { label: "test:dd - proc" } as any); - expect(match).toBe(true); - - // Different record-count - match = matchSpool(spool, { label: "test:dd - 2" } as any); - expect(match).toBe(false); - - // Different procstep - match = matchSpool(spool, { label: "test:dd - abc" } as any); - expect(match).toBe(false); - - // Different stepname - match = matchSpool(spool, { label: "other:dd - 1" } as any); - expect(match).toBe(false); - - // Different ddname - match = matchSpool(spool, { label: "test:new - proc" } as any); - expect(match).toBe(false); - }); - }); - - describe("getSpoolFiles", () => { - afterEach(() => { - jest.restoreAllMocks(); - }); - - it("should gather all spool files for a given job", async () => { - const profile = createIProfile(); - const session = createISessionWithoutCredentials(); - const newJobSession = createJobSessionNode(session, profile); - - const jesApi = createJesApi(profile); - bindJesApi(jesApi); - - const spoolOk: zosjobs.IJobFile = { ...iJobFile, stepname: "test", ddname: "dd", "record-count": 1, procstep: "proc" }; - const { id, ddname, stepname, ...withoutIdDdStep } = spoolOk; - - newJobSession.job = spoolOk as any; - - const getSpoolFilesSpy = jest.spyOn(jesApi, "getSpoolFiles").mockResolvedValue([spoolOk, withoutIdDdStep] as any); - - const spools = await getSpoolFiles(newJobSession); - - expect(getSpoolFilesSpy).toHaveBeenCalledWith("TESTJOB", "100"); - expect(spools).toEqual([spoolOk]); - }); - - it("should return an empty array of the node.job is null", async () => { - const profile = createIProfile(); - const session = createISessionWithoutCredentials(); - const newJobSession = createJobSessionNode(session, profile); - - const jesApi = createJesApi(profile); - bindJesApi(jesApi); - - const spools = await getSpoolFiles(newJobSession); - - expect(spools).toEqual([]); - }); - }); -}); diff --git a/packages/zowe-explorer/__tests__/__unit__/SpoolUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/SpoolUtils.unit.test.ts new file mode 100644 index 0000000000..adcd88e61f --- /dev/null +++ b/packages/zowe-explorer/__tests__/__unit__/SpoolUtils.unit.test.ts @@ -0,0 +1,136 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { matchSpool, getSpoolFiles } from "../../src/SpoolUtils"; +import * as zosjobs from "@zowe/zos-jobs-for-zowe-sdk"; +import { Profiles } from "../../src/Profiles"; +import { createIProfile, createISessionWithoutCredentials } from "../../__mocks__/mockCreators/shared"; +import { bindJesApi, createJesApi } from "../../__mocks__/mockCreators/api"; +import { createJobSessionNode } from "../../__mocks__/mockCreators/jobs"; + +describe("SpoolProvider Unit Tests", () => { + const iJobFile: zosjobs.IJobFile = { + "byte-count": 128, + "job-correlator": "", + "record-count": 1, + "records-url": "fake/records", + class: "A", + ddname: "STDOUT", + id: 100, + jobid: "100", + jobname: "TESTJOB", + lrecl: 80, + procstep: "", + recfm: "FB", + stepname: "", + subsystem: "", + }; + const profilesForValidation = { status: "active", name: "fake" }; + + Object.defineProperty(Profiles, "getInstance", { + value: jest.fn(() => { + return { + allProfiles: [{ name: "firstName" }, { name: "secondName" }], + defaultProfile: { name: "firstName" }, + checkCurrentProfile: jest.fn(() => { + return profilesForValidation; + }), + profilesForValidation: [], + validateProfiles: jest.fn(), + }; + }), + }); + Object.defineProperty(Profiles, "getDefaultProfile", { + value: jest.fn(() => { + return { + name: "firstName", + }; + }), + }); + Object.defineProperty(Profiles, "loadNamedProfile", { + value: jest.fn(() => { + return { + name: "firstName", + }; + }), + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("matchSpool", () => { + it("should match spool to the selected node", () => { + const spool: zosjobs.IJobFile = { ...iJobFile, stepname: "test", ddname: "dd", "record-count": 1, procstep: "proc" }; + let match = matchSpool(spool, { label: "test:dd - 1" } as any); + expect(match).toBe(true); + + match = matchSpool(spool, { label: "test:dd - proc" } as any); + expect(match).toBe(true); + + // Different record-count + match = matchSpool(spool, { label: "test:dd - 2" } as any); + expect(match).toBe(false); + + // Different procstep + match = matchSpool(spool, { label: "test:dd - abc" } as any); + expect(match).toBe(false); + + // Different stepname + match = matchSpool(spool, { label: "other:dd - 1" } as any); + expect(match).toBe(false); + + // Different ddname + match = matchSpool(spool, { label: "test:new - proc" } as any); + expect(match).toBe(false); + }); + }); + + describe("getSpoolFiles", () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("should gather all spool files for a given job", async () => { + const profile = createIProfile(); + const session = createISessionWithoutCredentials(); + const newJobSession = createJobSessionNode(session, profile); + + const jesApi = createJesApi(profile); + bindJesApi(jesApi); + + const spoolOk: zosjobs.IJobFile = { ...iJobFile, stepname: "test", ddname: "dd", "record-count": 1, procstep: "proc" }; + const { id, ddname, stepname, ...withoutIdDdStep } = spoolOk; + + newJobSession.job = spoolOk as any; + + const getSpoolFilesSpy = jest.spyOn(jesApi, "getSpoolFiles").mockResolvedValue([spoolOk, withoutIdDdStep] as any); + + const spools = await getSpoolFiles(newJobSession); + + expect(getSpoolFilesSpy).toHaveBeenCalledWith("TESTJOB", "100"); + expect(spools).toEqual([spoolOk]); + }); + + it("should return an empty array of the node.job is null", async () => { + const profile = createIProfile(); + const session = createISessionWithoutCredentials(); + const newJobSession = createJobSessionNode(session, profile); + + const jesApi = createJesApi(profile); + bindJesApi(jesApi); + + const spools = await getSpoolFiles(newJobSession); + + expect(spools).toEqual([]); + }); + }); +}); diff --git a/packages/zowe-explorer/__tests__/__unit__/ZoweExplorerExtender.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/ZoweExplorerExtender.unit.test.ts index c7b9454c31..01534b9a81 100644 --- a/packages/zowe-explorer/__tests__/__unit__/ZoweExplorerExtender.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/ZoweExplorerExtender.unit.test.ts @@ -25,6 +25,7 @@ import { ProfilesUtils } from "../../src/utils/ProfilesUtils"; import { ZoweLogger } from "../../src/utils/ZoweLogger"; import { ZoweLocalStorage } from "../../src/utils/ZoweLocalStorage"; import { SettingsConfig } from "../../src/utils/SettingsConfig"; +import { UssFSProvider } from "../../src/uss/UssFSProvider"; jest.mock("fs"); describe("ZoweExplorerExtender unit tests", () => { @@ -42,8 +43,13 @@ describe("ZoweExplorerExtender unit tests", () => { mockErrorMessage: jest.fn(), mockExistsSync: jest.fn(), mockTextDocument: jest.fn(), + FileSystemProvider: { + createDirectory: jest.fn(), + }, }; + jest.spyOn(UssFSProvider.instance, "createDirectory").mockImplementation(newMocks.FileSystemProvider.createDirectory); + Object.defineProperty(fs, "existsSync", { value: newMocks.mockExistsSync, configurable: true }); jest.spyOn(ZoweExplorerExtender.prototype, "getProfilesCache").mockReturnValue(newMocks.profiles); Object.defineProperty(vscode.window, "createTreeView", { @@ -117,8 +123,8 @@ describe("ZoweExplorerExtender unit tests", () => { const blockMocks = await createBlockMocks(); ZoweExplorerExtender.createInstance(); - Object.defineProperty(vscode.Uri, "file", { value: jest.fn(), configurable: true }); Object.defineProperty(Gui, "showTextDocument", { value: jest.fn(), configurable: true }); + const uriFileMock = jest.spyOn(vscode.Uri, "file").mockImplementation(); const zoweDir = FileManagement.getZoweDir(); const userInputs = [ @@ -172,7 +178,7 @@ describe("ZoweExplorerExtender unit tests", () => { expect(Gui.showTextDocument).not.toHaveBeenCalled(); } else { if (userInput.v1) { - expect(vscode.Uri.file).toHaveBeenCalledWith(path.join(zoweDir, "profiles", "exampleType", "exampleType_meta.yaml")); + expect(uriFileMock).toHaveBeenCalledWith(path.join(zoweDir, "profiles", "exampleType", "exampleType_meta.yaml")); } else { for (const fileName of userInput.fileChecks) { expect(blockMocks.mockExistsSync).toHaveBeenCalledWith(path.join(zoweDir, fileName)); diff --git a/packages/zowe-explorer/__tests__/__unit__/ZoweNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/ZoweNode.unit.test.ts index aaf9ce79f0..4e7495f42b 100644 --- a/packages/zowe-explorer/__tests__/__unit__/ZoweNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/ZoweNode.unit.test.ts @@ -18,6 +18,7 @@ import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; import { Profiles } from "../../src/Profiles"; import * as globals from "../../src/globals"; import { imperative, Sorting } from "@zowe/zowe-explorer-api"; +import { DatasetFSProvider } from "../../src/dataset/DatasetFSProvider"; describe("Unit Tests (Jest)", () => { // Globals @@ -75,143 +76,6 @@ describe("Unit Tests (Jest)", () => { expect(testNode.getSession()).toBeDefined(); }); - /************************************************************************************************************* - * Creates sample ZoweDatasetNode list and checks that getChildren() returns the correct array - *************************************************************************************************************/ - it("Testing that getChildren returns the correct Thenable", async () => { - Object.defineProperty(Profiles, "getInstance", { - value: jest.fn(() => { - return { - loadNamedProfile: jest.fn().mockReturnValue(profileOne), - }; - }), - }); - // Creating a rootNode - const rootNode = new ZoweDatasetNode({ - label: "root", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - session, - profile: profileOne, - }); - rootNode.dirty = true; - rootNode.contextValue = globals.DS_SESSION_CONTEXT; - rootNode.pattern = "SAMPLE, SAMPLE.PUBLIC, SAMPLE"; - let rootChildren = await rootNode.getChildren(); - - // Creating structure of files and folders under BRTVS99 profile - const sampleChildren: ZoweDatasetNode[] = [ - new ZoweDatasetNode({ - label: "BRTVS99", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: rootNode, - profile: profileOne, - }), - new ZoweDatasetNode({ - label: "BRTVS99.CA10", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: rootNode, - profile: profileOne, - contextOverride: globals.DS_MIGRATED_FILE_CONTEXT, - }), - new ZoweDatasetNode({ - label: "BRTVS99.CA11.SPFTEMP0.CNTL", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: rootNode, - profile: profileOne, - }), - new ZoweDatasetNode({ - label: "BRTVS99.DDIR", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: rootNode, - profile: profileOne, - }), - new ZoweDatasetNode({ - label: "BRTVS99.VS1", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: rootNode, - profile: profileOne, - contextOverride: globals.VSAM_CONTEXT, - }), - ]; - sampleChildren[0].command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [sampleChildren[0]] }; - - // Checking that the rootChildren are what they are expected to be - expect(rootChildren).toEqual(sampleChildren); - - rootNode.dirty = true; - // Check the dirty and children variable have been set - rootChildren = await rootNode.getChildren(); - - // Checking that the rootChildren are what they are expected to be - expect(rootChildren).toEqual(sampleChildren); - - // Check that error is thrown when label is blank - const errorNode = new ZoweDatasetNode({ - label: "", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - session, - profile: profileOne, - }); - errorNode.dirty = true; - await expect(errorNode.getChildren()).rejects.toEqual(Error("Invalid node")); - - // Check that label is different when label contains a [] - const rootNode2 = new ZoweDatasetNode({ - label: "root[test]", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - session, - profile: profileOne, - }); - rootNode2.dirty = true; - rootChildren = await rootNode2.getChildren(); - }); - - /************************************************************************************************************* - * Creates sample ZoweDatasetNode list and checks that getChildren() returns the correct array for a PO - *************************************************************************************************************/ - it("Testing that getChildren returns the correct Thenable for a PO", async () => { - Object.defineProperty(Profiles, "getInstance", { - value: jest.fn(() => { - return { - loadNamedProfile: jest.fn().mockReturnValue(profileOne), - }; - }), - }); - // Creating a rootNode - const rootNode = new ZoweDatasetNode({ label: "root", collapsibleState: vscode.TreeItemCollapsibleState.None, session, profile: profileOne }); - rootNode.contextValue = globals.DS_SESSION_CONTEXT; - rootNode.dirty = true; - const subNode = new ZoweDatasetNode({ - label: "sub", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: rootNode, - profile: profileOne, - }); - subNode.dirty = true; - const subChildren = await subNode.getChildren(); - - // Creating structure of files and folders under BRTVS99 profile - const sampleChildren: ZoweDatasetNode[] = [ - new ZoweDatasetNode({ - label: "BRTVS99", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: subNode, - profile: profileOne, - }), - new ZoweDatasetNode({ - label: "BRTVS99.DDIR", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: subNode, - profile: profileOne, - }), - ]; - - sampleChildren[0].command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [sampleChildren[0]] }; - sampleChildren[1].command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [sampleChildren[1]] }; - // Checking that the rootChildren are what they are expected to be - expect(subChildren).toEqual(sampleChildren); - }); - /************************************************************************************************************* * Checks that returning an unsuccessful response results in an error being thrown and caught *************************************************************************************************************/ @@ -272,6 +136,10 @@ describe("Unit Tests (Jest)", () => { profile: profileOne, contextOverride: globals.INFORMATION_CONTEXT, }); + infoChild.command = { + command: "zowe.placeholderCommand", + title: "Placeholder", + }; rootNode.contextValue = globals.DS_SESSION_CONTEXT; rootNode.dirty = false; await expect(await rootNode.getChildren()).toEqual([infoChild]); @@ -295,6 +163,10 @@ describe("Unit Tests (Jest)", () => { profile: profileOne, contextOverride: globals.INFORMATION_CONTEXT, }); + infoChild.command = { + command: "zowe.placeholderCommand", + title: "Placeholder", + }; rootNode.contextValue = globals.DS_SESSION_CONTEXT; await expect(await rootNode.getChildren()).toEqual([infoChild]); }); @@ -422,6 +294,9 @@ describe("Unit Tests (Jest)", () => { }, }; }); + jest.spyOn(DatasetFSProvider.instance, "exists").mockReturnValue(false); + jest.spyOn(DatasetFSProvider.instance, "writeFile").mockImplementation(); + jest.spyOn(DatasetFSProvider.instance, "createDirectory").mockImplementation(); Object.defineProperty(zosfiles.List, "allMembers", { value: allMembers }); const pdsChildren = await pds.getChildren(); expect(pdsChildren[0].label).toEqual("BADMEM\ufffd"); diff --git a/packages/zowe-explorer/__tests__/__unit__/abstract/ZoweSaveQueue.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/abstract/ZoweSaveQueue.unit.test.ts index ef7eb9f263..307c5b42bd 100644 --- a/packages/zowe-explorer/__tests__/__unit__/abstract/ZoweSaveQueue.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/abstract/ZoweSaveQueue.unit.test.ts @@ -11,8 +11,6 @@ import { ZoweSaveQueue } from "../../../src/abstract/ZoweSaveQueue"; import { createUSSTree } from "../../../__mocks__/mockCreators/uss"; -import { saveUSSFile } from "../../../src/uss/actions"; -import * as workspaceUtils from "../../../src/utils/workspace"; import { Gui } from "@zowe/zowe-explorer-api"; import * as vscode from "vscode"; import { ZoweLogger } from "../../../src/utils/ZoweLogger"; @@ -33,7 +31,6 @@ describe("ZoweSaveQueue - unit tests", () => { jest.spyOn(Gui, "createTreeView").mockReturnValue({ onDidCollapseElement: jest.fn() } as any); const globalMocks = { errorMessageSpy: jest.spyOn(Gui, "errorMessage"), - markDocumentUnsavedSpy: jest.spyOn(workspaceUtils, "markDocumentUnsaved"), processNextSpy: jest.spyOn(ZoweSaveQueue as any, "processNext"), allSpy: jest.spyOn(ZoweSaveQueue, "all"), trees: { @@ -48,7 +45,7 @@ describe("ZoweSaveQueue - unit tests", () => { it("does promise chaining when pushing to queue", () => { ZoweSaveQueue.push({ fileProvider: globalMocks.trees.uss, - uploadRequest: saveUSSFile, + uploadRequest: async () => {}, savedFile: { isDirty: true, uri: vscode.Uri.parse(""), @@ -118,7 +115,6 @@ describe("ZoweSaveQueue - unit tests", () => { fail("ZoweSaveQueue.all should fail here"); } catch (err) { expect(ZoweLogger.error).toHaveBeenCalledWith(EXAMPLE_ERROR); - expect(globalMocks.markDocumentUnsavedSpy).toHaveBeenCalledWith(FAILING_FILE); expect(globalMocks.errorMessageSpy).toHaveBeenCalledWith( 'Failed to upload changes for [failingFile](command:vscode.open?["/some/failing/path"]): Example error' ); diff --git a/packages/zowe-explorer/__tests__/__unit__/abstract/ZoweTreeProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/abstract/ZoweTreeProvider.unit.test.ts index 82ed6030ce..0554f0a7e8 100644 --- a/packages/zowe-explorer/__tests__/__unit__/abstract/ZoweTreeProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/abstract/ZoweTreeProvider.unit.test.ts @@ -34,6 +34,7 @@ import { createDatasetSessionNode } from "../../../__mocks__/mockCreators/datase import { TreeProviders } from "../../../src/shared/TreeProviders"; import { createDatasetTree } from "../../../src/dataset/DatasetTree"; import * as sharedActions from "../../../src/shared/actions"; +import { UssFSProvider } from "../../../src/uss/UssFSProvider"; async function createGlobalMocks() { Object.defineProperty(ZoweLocalStorage, "storage", { @@ -83,8 +84,13 @@ async function createGlobalMocks() { }), mockProfileInfo: createInstanceOfProfileInfo(), mockProfilesCache: new ProfilesCache(imperative.Logger.getAppLogger()), + FileSystemProvider: { + createDirectory: jest.fn(), + }, }; + jest.spyOn(UssFSProvider.instance, "createDirectory").mockImplementation(globalMocks.FileSystemProvider.createDirectory); + Object.defineProperty(globalMocks.mockProfilesCache, "getProfileInfo", { value: jest.fn(() => { return { value: globalMocks.mockProfileInfo, configurable: true }; diff --git a/packages/zowe-explorer/__tests__/__unit__/api/ZoweExplorerApiRegister.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/api/ZoweExplorerApiRegister.unit.test.ts index 0976300d9f..381b684d16 100644 --- a/packages/zowe-explorer/__tests__/__unit__/api/ZoweExplorerApiRegister.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/api/ZoweExplorerApiRegister.unit.test.ts @@ -10,10 +10,13 @@ */ import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; -import { imperative, MainframeInteraction, ZoweExplorerZosmf } from "@zowe/zowe-explorer-api"; +import { imperative, MainframeInteraction, ZoweExplorerZosmf, ZoweScheme } from "@zowe/zowe-explorer-api"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; import { createInstanceOfProfile, createValidIProfile } from "../../../__mocks__/mockCreators/shared"; import { ZoweExplorerExtender } from "../../../src/ZoweExplorerExtender"; +import { DatasetFSProvider } from "../../../src/dataset/DatasetFSProvider"; +import { UssFSProvider } from "../../../src/uss/UssFSProvider"; +import { JobFSProvider } from "../../../src/job/JobFSProvider"; class MockUssApi1 implements MainframeInteraction.IUss { public profile?: imperative.IProfileLoaded; @@ -308,4 +311,16 @@ describe("ZoweExplorerApiRegister unit testing", () => { expect(ZoweExplorerApiRegister.getInstance().onProfilesUpdate).toEqual({}); ZoweExplorerApiRegister.getInstance()["onProfilesUpdateCallback"] = undefined; }); + + it("provides access to the appropriate event for onResourceChanged", () => { + expect(ZoweExplorerApiRegister.onResourceChanged(ZoweScheme.DS)).toBe(DatasetFSProvider.instance.onDidChangeFile); + }); + + it("provides access to the onUssChanged event", () => { + expect(ZoweExplorerApiRegister.onResourceChanged(ZoweScheme.USS)).toBe(UssFSProvider.instance.onDidChangeFile); + }); + + it("provides access to the onJobChanged event", () => { + expect(ZoweExplorerApiRegister.onResourceChanged(ZoweScheme.Jobs)).toBe(JobFSProvider.instance.onDidChangeFile); + }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/dataset/DatasetFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/dataset/DatasetFSProvider.unit.test.ts new file mode 100644 index 0000000000..2a1595b201 --- /dev/null +++ b/packages/zowe-explorer/__tests__/__unit__/dataset/DatasetFSProvider.unit.test.ts @@ -0,0 +1,737 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { Disposable, FilePermission, FileType, languages, TextDocument, TextEditor, Uri } from "vscode"; +import { DatasetFSProvider } from "../../../src/dataset/DatasetFSProvider"; +import { createIProfile } from "../../../__mocks__/mockCreators/shared"; +import { DirEntry, DsEntry, FileEntry, FilterEntry, Gui, PdsEntry, ZoweScheme } from "@zowe/zowe-explorer-api"; +import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; +import { MockedProperty } from "../../../__mocks__/mockUtils"; +import { ZoweLogger } from "../../../src/utils/ZoweLogger"; + +const testProfile = createIProfile(); +const testEntries = { + ps: { + ...new DsEntry("USER.DATA.PS"), + metadata: { + profile: testProfile, + path: "/USER.DATA.PS", + }, + etag: "OLDETAG", + } as DsEntry, + pds: { + ...new PdsEntry("USER.DATA.PDS"), + metadata: { + profile: testProfile, + path: "/USER.DATA.PDS", + }, + } as PdsEntry, + pdsMember: { + ...new DsEntry("MEMBER1"), + metadata: { + profile: testProfile, + path: "/USER.DATA.PDS/MEMBER1", + }, + } as DsEntry, + session: { + ...new FilterEntry("sestest"), + metadata: { + profile: testProfile, + path: "/", + }, + }, +}; + +type TestUris = Record>; +const testUris: TestUris = { + ps: Uri.from({ scheme: ZoweScheme.DS, path: "/sestest/USER.DATA.PS" }), + pds: Uri.from({ scheme: ZoweScheme.DS, path: "/sestest/USER.DATA.PDS" }), + pdsMember: Uri.from({ scheme: ZoweScheme.DS, path: "/sestest/USER.DATA.PDS/MEMBER1" }), + session: Uri.from({ scheme: ZoweScheme.DS, path: "/sestest" }), +}; + +describe("createDirectory", () => { + it("creates a directory for a session entry", () => { + const fakeRoot = { ...(DatasetFSProvider.instance as any).root }; + const rootMock = new MockedProperty(DatasetFSProvider.instance, "root", undefined, fakeRoot); + DatasetFSProvider.instance.createDirectory(testUris.session); + expect(fakeRoot.entries.has("sestest")).toBe(true); + rootMock[Symbol.dispose](); + }); + + it("creates a directory for a PDS entry", () => { + const fakeSessionEntry = new FilterEntry("sestest"); + fakeSessionEntry.metadata = testEntries.session.metadata; + jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(fakeSessionEntry); + DatasetFSProvider.instance.createDirectory(testUris.pds); + expect(fakeSessionEntry.entries.has("USER.DATA.PDS")).toBe(true); + }); +}); + +describe("readDirectory", () => { + describe("filter entry (session)", () => { + it("calls dataSetsMatchingPattern when reading directories if it exists", async () => { + const mockSessionEntry = { ...testEntries.session, filter: {}, metadata: { profile: testProfile, path: "/" } }; + mockSessionEntry.filter["pattern"] = "USER.*"; + const mockMvsApi = { + dataSetsMatchingPattern: jest.fn().mockResolvedValueOnce({ + apiResponse: [ + { dsname: "USER.DATA.DS" }, + { dsname: "USER.DATA.PDS", dsorg: "PO" }, + { dsname: "USER.ZFS", dsorg: "VS" }, + { dsname: "USER.ZFS.DATA", dsorg: "VS" }, + { dsname: "USER.DATA.MIGRATED", migr: "yes" }, + { dsname: "USER.DATA.DS2" }, + ], + }), + }; + const _lookupAsDirectoryMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupAsDirectory").mockReturnValueOnce(mockSessionEntry); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + expect(await DatasetFSProvider.instance.readDirectory(testUris.session)).toStrictEqual([ + ["USER.DATA.DS", FileType.File], + ["USER.DATA.PDS", FileType.Directory], + ["USER.DATA.MIGRATED", FileType.File], + ["USER.DATA.DS2", FileType.File], + ]); + expect(mockMvsApi.dataSetsMatchingPattern).toHaveBeenCalledWith([mockSessionEntry.filter["pattern"]]); + _lookupAsDirectoryMock.mockRestore(); + mvsApiMock.mockRestore(); + }); + + it("calls dataSet if dataSetsMatchingPattern API is unavailable", async () => { + const mockSessionEntry = { ...testEntries.session, filter: {}, metadata: { profile: testProfile, path: "/" } }; + mockSessionEntry.filter["pattern"] = "USER.*"; + const mockMvsApi = { + dataSet: jest.fn().mockResolvedValueOnce({ + apiResponse: [ + { dsname: "USER.DATA.DS" }, + { dsname: "USER.DATA.PDS", dsorg: "PO" }, + { dsname: "USER.ZFS", dsorg: "VS" }, + { dsname: "USER.ZFS.DATA", dsorg: "VS" }, + { dsname: "USER.DATA.MIGRATED", migr: "yes" }, + { dsname: "USER.DATA.DS2" }, + ], + }), + }; + const _lookupAsDirectoryMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupAsDirectory").mockReturnValueOnce(mockSessionEntry); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + expect(await DatasetFSProvider.instance.readDirectory(testUris.session)).toStrictEqual([ + ["USER.DATA.DS", FileType.File], + ["USER.DATA.PDS", FileType.Directory], + ["USER.DATA.MIGRATED", FileType.File], + ["USER.DATA.DS2", FileType.File], + ]); + expect(mockMvsApi.dataSet).toHaveBeenCalledWith(mockSessionEntry.filter["pattern"]); + _lookupAsDirectoryMock.mockRestore(); + mvsApiMock.mockRestore(); + }); + }); + + describe("PDS entry", () => { + it("calls allMembers to fetch the members of a PDS", async () => { + const mockPdsEntry = { ...testEntries.pds, metadata: { ...testEntries.pds.metadata } }; + const mockMvsApi = { + allMembers: jest.fn().mockResolvedValueOnce({ + apiResponse: { + items: [{ member: "MEMB1" }, { member: "MEMB2" }, { member: "MEMB3" }, { member: "MEMB4" }], + }, + }), + }; + const _lookupAsDirectoryMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupAsDirectory").mockReturnValueOnce(mockPdsEntry); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + expect(await DatasetFSProvider.instance.readDirectory(testUris.pds)).toStrictEqual([ + ["MEMB1", FileType.File], + ["MEMB2", FileType.File], + ["MEMB3", FileType.File], + ["MEMB4", FileType.File], + ]); + expect(mockMvsApi.allMembers).toHaveBeenCalledWith(testEntries.pds.name); + _lookupAsDirectoryMock.mockRestore(); + mvsApiMock.mockRestore(); + }); + }); +}); +describe("fetchDatasetAtUri", () => { + it("fetches a data set at the given URI", async () => { + const contents = "dataset contents"; + const mockMvsApi = { + getContents: jest.fn((dsn, opts) => { + opts.stream.write(contents); + + return { + apiResponse: { + etag: "123ANETAG", + }, + }; + }), + }; + const fakePo = { ...testEntries.ps }; + const lookupAsFileMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupAsFile").mockReturnValueOnce(fakePo); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + await DatasetFSProvider.instance.fetchDatasetAtUri(testUris.ps); + expect(fakePo.data.toString()).toStrictEqual(contents.toString()); + expect(fakePo.etag).toBe("123ANETAG"); + + lookupAsFileMock.mockRestore(); + mvsApiMock.mockRestore(); + }); + + it("fetches a data set at the given URI - conflict view", async () => { + const contents = "dataset contents"; + const mockMvsApi = { + getContents: jest.fn((dsn, opts) => { + opts.stream.write(contents); + + return { + apiResponse: { + etag: "123ANETAG", + }, + }; + }), + }; + const fakePo = { ...testEntries.ps }; + const lookupAsFileMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupAsFile").mockReturnValueOnce(fakePo); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + await DatasetFSProvider.instance.fetchDatasetAtUri(testUris.ps, { isConflict: true }); + expect(fakePo.conflictData?.contents.toString()).toStrictEqual(contents.toString()); + expect(fakePo.conflictData?.etag).toBe("123ANETAG"); + expect(fakePo.conflictData?.size).toBe(contents.length); + + lookupAsFileMock.mockRestore(); + mvsApiMock.mockRestore(); + }); + + it("calls _updateResourceInEditor if 'editor' is specified", async () => { + const contents = "dataset contents"; + const mockMvsApi = { + getContents: jest.fn((dsn, opts) => { + opts.stream.write(contents); + + return { + apiResponse: { + etag: "123ANETAG", + }, + }; + }), + }; + const fakePo = { ...testEntries.ps }; + const _updateResourceInEditorMock = jest.spyOn(DatasetFSProvider.instance as any, "_updateResourceInEditor").mockImplementation(); + const lookupAsFileMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupAsFile").mockReturnValueOnce(fakePo); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + await DatasetFSProvider.instance.fetchDatasetAtUri(testUris.ps, { editor: {} as TextEditor, isConflict: false }); + expect(fakePo.data.toString()).toStrictEqual(contents.toString()); + expect(fakePo.etag).toBe("123ANETAG"); + expect(_updateResourceInEditorMock).toHaveBeenCalledWith(testUris.ps); + + lookupAsFileMock.mockRestore(); + mvsApiMock.mockRestore(); + _updateResourceInEditorMock.mockRestore(); + }); +}); +describe("readFile", () => { + it("throws an error if the entry does not have a profile", async () => { + const _lookupAsFileMock = jest + .spyOn(DatasetFSProvider.instance as any, "_lookupAsFile") + .mockReturnValueOnce({ ...testEntries.ps, metadata: { profile: null } }); + + await expect(DatasetFSProvider.instance.readFile(testUris.ps)).rejects.toThrow("file not found"); + expect(_lookupAsFileMock).toHaveBeenCalledWith(testUris.ps); + _lookupAsFileMock.mockRestore(); + }); + + it("calls fetchDatasetAtUri if the entry has not yet been accessed", async () => { + const _lookupAsFileMock = jest + .spyOn(DatasetFSProvider.instance as any, "_lookupAsFile") + .mockReturnValueOnce({ ...testEntries.ps, wasAccessed: false }); + const _getInfoFromUriMock = jest.spyOn(DatasetFSProvider.instance as any, "_getInfoFromUri").mockReturnValueOnce({ + profile: testProfile, + path: "/USER.DATA.PS", + }); + const fetchDatasetAtUriMock = jest.spyOn(DatasetFSProvider.instance, "fetchDatasetAtUri").mockImplementation(); + + await DatasetFSProvider.instance.readFile(testUris.ps); + expect(_lookupAsFileMock).toHaveBeenCalledWith(testUris.ps); + expect(fetchDatasetAtUriMock).toHaveBeenCalled(); + fetchDatasetAtUriMock.mockRestore(); + _getInfoFromUriMock.mockRestore(); + }); + + it("returns the data for an entry", async () => { + const fakePs = { ...testEntries.ps, wasAccessed: true, data: new Uint8Array([1, 2, 3]) }; + const _lookupAsFileMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupAsFile").mockReturnValueOnce(fakePs); + const _getInfoFromUriMock = jest.spyOn(DatasetFSProvider.instance as any, "_getInfoFromUri").mockReturnValueOnce({ + profile: testProfile, + path: "/USER.DATA.PS", + }); + + expect(await DatasetFSProvider.instance.readFile(testUris.ps)).toBe(fakePs.data); + expect(_lookupAsFileMock).toHaveBeenCalledWith(testUris.ps); + _getInfoFromUriMock.mockRestore(); + _lookupAsFileMock.mockRestore(); + }); +}); + +describe("writeFile", () => { + it("updates a PS in the FSP and remote system", async () => { + const mockMvsApi = { + uploadFromBuffer: jest.fn().mockResolvedValueOnce({ + apiResponse: { + etag: "NEWETAG", + }, + }), + }; + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + const statusMsgMock = jest.spyOn(Gui, "setStatusBarMessage"); + const psEntry = { ...testEntries.ps, metadata: testEntries.ps.metadata } as DsEntry; + const sessionEntry = { ...testEntries.session }; + sessionEntry.entries.set("USER.DATA.PS", psEntry); + const lookupParentDirMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(sessionEntry); + const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookup").mockReturnValueOnce(psEntry); + const newContents = new Uint8Array([3, 6, 9]); + await DatasetFSProvider.instance.writeFile(testUris.ps, newContents, { create: false, overwrite: true }); + + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.ps); + expect(statusMsgMock).toHaveBeenCalledWith("$(sync~spin) Saving data set..."); + expect(mockMvsApi.uploadFromBuffer).toHaveBeenCalledWith(Buffer.from(newContents), testEntries.ps.name, { + binary: false, + encoding: undefined, + etag: testEntries.ps.etag, + returnEtag: true, + }); + expect(psEntry.etag).toBe("NEWETAG"); + expect(psEntry.data).toBe(newContents); + mvsApiMock.mockRestore(); + lookupMock.mockRestore(); + }); + + it("throws an error when there is an error unrelated to etag", async () => { + const mockMvsApi = { + uploadFromBuffer: jest.fn().mockImplementation(() => { + throw new Error("Unknown error on remote system"); + }), + }; + const disposeMock = jest.fn(); + const setStatusBarMsg = jest.spyOn(Gui, "setStatusBarMessage").mockReturnValueOnce({ dispose: disposeMock }); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + const psEntry = { ...testEntries.ps, metadata: testEntries.ps.metadata } as DsEntry; + const sessionEntry = { ...testEntries.session }; + sessionEntry.entries.set("USER.DATA.PS", psEntry); + const lookupParentDirMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(sessionEntry); + const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookup").mockReturnValueOnce(psEntry); + const newContents = new Uint8Array([3, 6, 9]); + await expect(DatasetFSProvider.instance.writeFile(testUris.ps, newContents, { create: false, overwrite: true })).rejects.toThrow( + "Unknown error on remote system" + ); + + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.ps); + expect(setStatusBarMsg).toHaveBeenCalled(); + expect(mockMvsApi.uploadFromBuffer).toHaveBeenCalledWith(Buffer.from(newContents), testEntries.ps.name, { + binary: false, + encoding: undefined, + etag: testEntries.ps.etag, + returnEtag: true, + }); + expect(disposeMock).toHaveBeenCalled(); + setStatusBarMsg.mockRestore(); + mvsApiMock.mockRestore(); + lookupMock.mockRestore(); + }); + + it("calls _handleConflict when there is an e-tag error", async () => { + const mockMvsApi = { + uploadFromBuffer: jest.fn().mockRejectedValueOnce(new Error("Rest API failure with HTTP(S) status 412")), + }; + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + const statusMsgMock = jest.spyOn(Gui, "setStatusBarMessage"); + const psEntry = { ...testEntries.ps, metadata: testEntries.ps.metadata } as DsEntry; + const sessionEntry = { ...testEntries.session }; + sessionEntry.entries.set("USER.DATA.PS", psEntry); + const lookupParentDirMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(sessionEntry); + const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookup").mockReturnValueOnce(psEntry); + const handleConflictMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleConflict").mockImplementation(); + const newContents = new Uint8Array([3, 6, 9]); + await DatasetFSProvider.instance.writeFile(testUris.ps, newContents, { create: false, overwrite: true }); + + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.ps); + expect(statusMsgMock).toHaveBeenCalledWith("$(sync~spin) Saving data set..."); + expect(mockMvsApi.uploadFromBuffer).toHaveBeenCalledWith(Buffer.from(newContents), testEntries.ps.name, { + binary: false, + encoding: undefined, + etag: testEntries.ps.etag, + returnEtag: true, + }); + expect(handleConflictMock).toHaveBeenCalled(); + handleConflictMock.mockRestore(); + mvsApiMock.mockRestore(); + lookupMock.mockRestore(); + }); + + it("upload changes to a remote DS even if its not yet in the FSP", async () => { + const mockMvsApi = { + uploadFromBuffer: jest.fn().mockResolvedValueOnce({ + apiResponse: { + etag: "NEWETAG", + }, + }), + }; + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + const statusMsgMock = jest.spyOn(Gui, "setStatusBarMessage"); + const session = { + ...testEntries.session, + entries: new Map(), + }; + const lookupParentDirMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(session); + const newContents = new Uint8Array([3, 6, 9]); + await DatasetFSProvider.instance.writeFile(testUris.ps, newContents, { create: true, overwrite: true }); + + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.ps); + expect(statusMsgMock).toHaveBeenCalledWith("$(sync~spin) Saving data set..."); + expect(mockMvsApi.uploadFromBuffer).toHaveBeenCalledWith(Buffer.from(newContents), testEntries.ps.name, { + binary: false, + encoding: undefined, + etag: undefined, + returnEtag: true, + }); + const psEntry = session.entries.get("USER.DATA.PS")!; + expect(psEntry.etag).toBe("NEWETAG"); + expect(psEntry.data).toBe(newContents); + mvsApiMock.mockRestore(); + }); + + it("updates an empty, unaccessed PS entry in the FSP without sending data", async () => { + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce({} as any); + const session = { + ...testEntries.session, + entries: new Map([[testEntries.ps.name, { ...testEntries.ps, wasAccessed: false }]]), + }; + const lookupParentDirMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(session); + const newContents = new Uint8Array([]); + await DatasetFSProvider.instance.writeFile(testUris.ps, newContents, { create: false, overwrite: true }); + + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.ps); + const psEntry = session.entries.get(testEntries.ps.name)!; + expect(psEntry.data.length).toBe(0); + mvsApiMock.mockRestore(); + }); + + it("updates a PS without uploading when open in the diff view", async () => { + const session = { + ...testEntries.session, + entries: new Map([[testEntries.ps.name, { ...testEntries.ps }]]), + }; + const lookupParentDirMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(session); + const newContents = new Uint8Array([]); + await DatasetFSProvider.instance.writeFile( + testUris.ps.with({ + query: "inDiff=true", + }), + newContents, + { create: false, overwrite: true } + ); + + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.ps); + const psEntry = session.entries.get("USER.DATA.PS")!; + expect(psEntry.data.length).toBe(0); + expect(psEntry.inDiffView).toBe(true); + }); + + it("throws an error if entry doesn't exist and 'create' option is false", async () => { + const session = { + ...testEntries.session, + entries: new Map(), + }; + const lookupParentDirMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(session); + await expect(DatasetFSProvider.instance.writeFile(testUris.ps, new Uint8Array([]), { create: false, overwrite: true })).rejects.toThrow( + "file not found" + ); + lookupParentDirMock.mockRestore(); + }); + + it("throws an error if entry exists and 'overwrite' option is false", async () => { + const session = { + ...testEntries.session, + entries: new Map([[testEntries.ps.name, { ...testEntries.ps, wasAccessed: false }]]), + }; + const lookupParentDirMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(session); + await expect(DatasetFSProvider.instance.writeFile(testUris.ps, new Uint8Array([]), { create: true, overwrite: false })).rejects.toThrow( + "file exists" + ); + lookupParentDirMock.mockRestore(); + }); + + it("throws an error if the given URI is an existing PDS", async () => { + const session = { + ...testEntries.session, + entries: new Map([[testEntries.ps.name, { ...testEntries.pds }]]), + }; + const lookupParentDirMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(session); + await expect(DatasetFSProvider.instance.writeFile(testUris.ps, new Uint8Array([]), { create: true, overwrite: false })).rejects.toThrow( + "file is a directory" + ); + lookupParentDirMock.mockRestore(); + }); +}); + +describe("watch", () => { + it("returns an empty Disposable object", () => { + expect(DatasetFSProvider.instance.watch(testUris.pds, { recursive: false, excludes: [] })).toStrictEqual(new Disposable(() => {})); + }); +}); +describe("stat", () => { + it("returns the result of the 'lookup' function", () => { + const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookup").mockImplementation(); + DatasetFSProvider.instance.stat(testUris.ps); + expect(lookupMock).toHaveBeenCalledWith(testUris.ps, false); + lookupMock.mockRestore(); + }); + it("returns readonly if the URI is in the conflict view", () => { + const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookup").mockImplementation(); + const conflictUri = testUris.ps.with({ query: "conflict=true" }); + expect(DatasetFSProvider.instance.stat(conflictUri).permissions).toBe(FilePermission.Readonly); + expect(lookupMock).toHaveBeenCalledWith(conflictUri, false); + lookupMock.mockRestore(); + }); +}); +describe("updateFilterForUri", () => { + it("returns early if the entry is not a FilterEntry", () => { + const fakeEntry = { ...testEntries.ps }; + const _lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookup").mockReturnValueOnce(fakeEntry); + DatasetFSProvider.instance.updateFilterForUri(testUris.ps, "SOME.PATTERN.*"); + expect(fakeEntry).not.toHaveProperty("filter"); + _lookupMock.mockRestore(); + }); + + it("updates the filter on a FilterEntry", () => { + const fakeEntry = { ...testEntries.session }; + const _lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookup").mockReturnValueOnce(fakeEntry); + DatasetFSProvider.instance.updateFilterForUri(testUris.session, "SOME.PATTERN.*"); + expect(fakeEntry.filter).toStrictEqual({ pattern: "SOME.PATTERN.*" }); + _lookupMock.mockRestore(); + }); +}); + +describe("delete", () => { + it("successfully deletes a PS entry", async () => { + const fakePs = { ...testEntries.ps }; + const fakeSession = { ...testEntries.session, entries: new Map() }; + fakeSession.entries.set("USER.DATA.PS", fakePs); + const mockMvsApi = { + deleteDataSet: jest.fn(), + }; + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + const _lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookup").mockReturnValueOnce(fakePs); + const _fireSoonMock = jest.spyOn(DatasetFSProvider.instance as any, "_fireSoon").mockImplementation(); + jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(fakeSession); + + await DatasetFSProvider.instance.delete(testUris.ps, { recursive: false }); + expect(mockMvsApi.deleteDataSet).toHaveBeenCalledWith(fakePs.name, { responseTimeout: undefined }); + expect(_lookupMock).toHaveBeenCalledWith(testUris.ps, false); + expect(_fireSoonMock).toHaveBeenCalled(); + + expect(fakeSession.entries.has(fakePs.name)).toBe(false); + mvsApiMock.mockRestore(); + }); + + it("successfully deletes a PDS member", async () => { + const fakePdsMember = { ...testEntries.pdsMember }; + const fakePds = new PdsEntry("USER.DATA.PDS"); + fakePds.entries.set("MEMBER1", fakePdsMember); + const mockMvsApi = { + deleteDataSet: jest.fn(), + }; + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + const _lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookup").mockReturnValueOnce(fakePdsMember); + const _fireSoonMock = jest.spyOn(DatasetFSProvider.instance as any, "_fireSoon").mockImplementation(); + jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(fakePds); + + await DatasetFSProvider.instance.delete(testUris.pdsMember, { recursive: false }); + expect(mockMvsApi.deleteDataSet).toHaveBeenCalledWith(`${fakePds.name}(${fakePdsMember.name})`, { responseTimeout: undefined }); + expect(_lookupMock).toHaveBeenCalledWith(testUris.pdsMember, false); + expect(_fireSoonMock).toHaveBeenCalled(); + + expect(fakePds.entries.has(fakePdsMember.name)).toBe(false); + mvsApiMock.mockRestore(); + }); + + it("throws an error if it could not delete an entry", async () => { + const fakePs = { ...testEntries.ps }; + const fakeSession = { ...testEntries.session, entries: new Map() }; + fakeSession.entries.set("USER.DATA.PS", fakePs); + const mockMvsApi = { + deleteDataSet: jest.fn().mockRejectedValueOnce(new Error("Data set does not exist on remote")), + }; + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + const _lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookup").mockReturnValueOnce(fakePs); + const _fireSoonMock = jest.spyOn(DatasetFSProvider.instance as any, "_fireSoon").mockImplementation(); + const errorMsgMock = jest.spyOn(Gui, "errorMessage").mockImplementation(); + jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(fakeSession); + + await DatasetFSProvider.instance.delete(testUris.ps, { recursive: false }); + expect(mockMvsApi.deleteDataSet).toHaveBeenCalledWith(fakePs.name, { responseTimeout: undefined }); + expect(_lookupMock).toHaveBeenCalledWith(testUris.ps, false); + expect(_fireSoonMock).toHaveBeenCalled(); + expect(errorMsgMock).toHaveBeenCalledWith("Deleting /USER.DATA.PS failed due to API error: Data set does not exist on remote"); + expect(fakeSession.entries.has(fakePs.name)).toBe(true); + mvsApiMock.mockRestore(); + errorMsgMock.mockRestore(); + }); +}); + +describe("makeEmptyDsWithEncoding", () => { + it("creates an empty data set in the provider with the given encoding", () => { + const fakeSession = { ...testEntries.session }; + const parentDirMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(fakeSession); + expect(DatasetFSProvider.instance.makeEmptyDsWithEncoding(testUris.ps, { kind: "binary" })); + expect(fakeSession.entries.has(testEntries.ps.name)).toBe(true); + parentDirMock.mockRestore(); + }); +}); + +describe("rename", () => { + it("renames a PS", async () => { + const oldPs = { ...testEntries.ps }; + const mockMvsApi = { + renameDataSetMember: jest.fn(), + }; + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + const _lookupMock = jest + .spyOn(DatasetFSProvider.instance as any, "_lookup") + .mockImplementation((uri): DirEntry | FileEntry => ((uri as Uri).path.includes("USER.DATA.PS2") ? (null as any) : oldPs)); + const _lookupParentDirectoryMock = jest + .spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory") + .mockReturnValueOnce({ ...testEntries.session }); + await DatasetFSProvider.instance.rename(testUris.ps, testUris.ps.with({ path: "/USER.DATA.PS2" }), { overwrite: true }); + expect(mockMvsApi.renameDataSetMember).toHaveBeenCalledWith("", "USER.DATA.PS", "USER.DATA.PS2"); + _lookupMock.mockRestore(); + mvsApiMock.mockRestore(); + _lookupParentDirectoryMock.mockRestore(); + }); + + it("renames a PDS", async () => { + const oldPds = new PdsEntry("USER.DATA.PDS"); + oldPds.metadata = testEntries.pds.metadata; + const mockMvsApi = { + renameDataSet: jest.fn(), + }; + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + const _lookupMock = jest + .spyOn(DatasetFSProvider.instance as any, "_lookup") + .mockImplementation((uri): DirEntry | FileEntry => ((uri as Uri).path.includes("USER.DATA.PDS2") ? (undefined as any) : oldPds)); + const _lookupParentDirectoryMock = jest + .spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory") + .mockReturnValueOnce({ ...testEntries.session }); + await DatasetFSProvider.instance.rename(testUris.pds, testUris.pds.with({ path: "/USER.DATA.PDS2" }), { overwrite: true }); + expect(mockMvsApi.renameDataSet).toHaveBeenCalledWith("USER.DATA.PDS", "USER.DATA.PDS2"); + _lookupMock.mockRestore(); + mvsApiMock.mockRestore(); + _lookupParentDirectoryMock.mockRestore(); + }); + + it("throws an error if 'overwrite' is false and the entry already exists", async () => { + const newPs = { ...testEntries.ps, name: "USER.DATA.PS2" }; + const _lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookup").mockReturnValueOnce(newPs); + await expect( + DatasetFSProvider.instance.rename(testUris.ps, testUris.ps.with({ path: "/USER.DATA.PS2" }), { overwrite: false }) + ).rejects.toThrow("file exists"); + _lookupMock.mockRestore(); + }); + + it("displays an error message when renaming fails on the remote system", async () => { + const oldPds = new PdsEntry("USER.DATA.PDS"); + oldPds.metadata = testEntries.pds.metadata; + const mockMvsApi = { + renameDataSet: jest.fn().mockRejectedValueOnce(new Error("could not upload data set")), + }; + const errMsgSpy = jest.spyOn(Gui, "errorMessage"); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + const _lookupMock = jest + .spyOn(DatasetFSProvider.instance as any, "_lookup") + .mockImplementation((uri): DirEntry | FileEntry => ((uri as Uri).path.includes("USER.DATA.PDS2") ? (undefined as any) : oldPds)); + const _lookupParentDirectoryMock = jest + .spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory") + .mockReturnValueOnce({ ...testEntries.session }); + await DatasetFSProvider.instance.rename(testUris.pds, testUris.pds.with({ path: "/USER.DATA.PDS2" }), { overwrite: true }); + expect(mockMvsApi.renameDataSet).toHaveBeenCalledWith("USER.DATA.PDS", "USER.DATA.PDS2"); + expect(errMsgSpy).toHaveBeenCalledWith("Renaming USER.DATA.PDS failed due to API error: could not upload data set"); + _lookupMock.mockRestore(); + mvsApiMock.mockRestore(); + _lookupParentDirectoryMock.mockRestore(); + }); +}); + +describe("onDidOpenTextDocument", () => { + const setTextDocLanguage = jest.spyOn(languages, "setTextDocumentLanguage"); + + afterEach(() => { + setTextDocLanguage.mockClear(); + }); + + it("handles ZoweScheme.DS documents", async () => { + const dsUri = Uri.from({ + path: "/profile/USER.WONDROUS.C/AMAZING", + scheme: ZoweScheme.DS, + }); + const doc = { + uri: dsUri, + languageId: undefined, + } as unknown as TextDocument; + await DatasetFSProvider.onDidOpenTextDocument(doc); + expect(setTextDocLanguage).toHaveBeenCalledWith(doc, "c"); + }); + + it("returns early if the language ID could not be identified", async () => { + const dsUri = Uri.from({ + path: "/profile/TEST.DS/AMAZING", + scheme: ZoweScheme.DS, + }); + const doc = { + uri: dsUri, + languageId: undefined, + } as unknown as TextDocument; + await DatasetFSProvider.onDidOpenTextDocument(doc); + expect(setTextDocLanguage).not.toHaveBeenCalled(); + }); + + it("returns early if the scheme is not ZoweScheme.DS", async () => { + const fileUri = Uri.from({ + path: "/var/www/AMAZING.txt", + scheme: "file", + }); + const doc = { + uri: fileUri, + languageId: "plaintext", + } as unknown as TextDocument; + await DatasetFSProvider.onDidOpenTextDocument(doc); + expect(setTextDocLanguage).not.toHaveBeenCalled(); + expect(doc.languageId).toBe("plaintext"); + }); + + it("handles an error when setting the language ID", async () => { + setTextDocLanguage.mockImplementationOnce(() => { + throw new Error("Not available"); + }); + const dsUri = Uri.from({ + path: "/profile/TEST.C.DS/MISSING", + scheme: ZoweScheme.DS, + }); + const doc = { + fileName: "MISSING", + uri: dsUri, + languageId: "rust", + } as unknown as TextDocument; + + const warnSpy = jest.spyOn(ZoweLogger, "warn"); + await DatasetFSProvider.onDidOpenTextDocument(doc); + expect(setTextDocLanguage).toHaveBeenCalled(); + expect(warnSpy).toHaveBeenCalledWith("Could not set document language for MISSING - tried languageId 'c'"); + expect(doc.languageId).toBe("rust"); + }); +}); diff --git a/packages/zowe-explorer/__tests__/__unit__/dataset/DatasetTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/dataset/DatasetTree.unit.test.ts index c04da1fe97..3b2a559f33 100644 --- a/packages/zowe-explorer/__tests__/__unit__/dataset/DatasetTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/dataset/DatasetTree.unit.test.ts @@ -15,7 +15,7 @@ import * as fs from "fs"; import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; import { DatasetTree } from "../../../src/dataset/DatasetTree"; import { ZoweDatasetNode } from "../../../src/dataset/ZoweDatasetNode"; -import { Gui, imperative, IZoweDatasetTreeNode, ProfilesCache, Validation, Sorting } from "@zowe/zowe-explorer-api"; +import { Gui, imperative, IZoweDatasetTreeNode, ProfilesCache, Validation, Sorting, ZoweScheme } from "@zowe/zowe-explorer-api"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; import { Profiles } from "../../../src/Profiles"; import * as utils from "../../../src/utils/ProfilesUtils"; @@ -37,7 +37,6 @@ import { } from "../../../__mocks__/mockCreators/shared"; import { createDatasetSessionNode, createDatasetTree, createDatasetFavoritesNode } from "../../../__mocks__/mockCreators/datasets"; import { bindMvsApi, createMvsApi } from "../../../__mocks__/mockCreators/api"; -import * as workspaceUtils from "../../../src/utils/workspace"; import { PersistentFilters } from "../../../src/PersistentFilters"; import * as dsUtils from "../../../src/dataset/utils"; import { SettingsConfig } from "../../../src/utils/SettingsConfig"; @@ -47,7 +46,8 @@ import { ZoweLocalStorage } from "../../../src/utils/ZoweLocalStorage"; import { TreeProviders } from "../../../src/shared/TreeProviders"; import { join } from "path"; import * as sharedUtils from "../../../src/shared/utils"; -import { mocked } from "../../../__mocks__/mockUtils"; +import { mocked, MockedProperty } from "../../../__mocks__/mockUtils"; +import { DatasetFSProvider } from "../../../src/dataset/DatasetFSProvider"; jest.mock("fs"); jest.mock("util"); @@ -90,6 +90,7 @@ function createGlobalMocks() { Object.defineProperty(vscode.window, "showQuickPick", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.window, "createQuickPick", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.window, "showInputBox", { value: jest.fn(), configurable: true }); + jest.spyOn(Gui, "showInputBox").mockImplementation(); Object.defineProperty(zosfiles, "Rename", { value: jest.fn(), configurable: true }); Object.defineProperty(zosfiles.Rename, "dataSet", { value: jest.fn(), configurable: true }); Object.defineProperty(zosfiles.Rename, "dataSetMember", { value: jest.fn(), configurable: true }); @@ -100,7 +101,6 @@ function createGlobalMocks() { Object.defineProperty(fs, "unlinkSync", { value: jest.fn(), configurable: true }); Object.defineProperty(fs, "existsSync", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.commands, "executeCommand", { value: jest.fn(), configurable: true }); - Object.defineProperty(workspaceUtils, "closeOpenedTextFile", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode, "ProgressLocation", { value: jest.fn().mockImplementation(() => { return { @@ -240,7 +240,6 @@ describe("Dataset Tree Unit tests - Function initializeFavChildNodeForProfile", datasetSessionNode, }; } - it("Checking function for PDS favorite", async () => { createGlobalMocks(); const blockMocks = createBlockMocks(); @@ -257,8 +256,11 @@ describe("Dataset Tree Unit tests - Function initializeFavChildNodeForProfile", parentNode: favProfileNode, contextOverride: globals.PDS_FAV_CONTEXT, }); + node.resourceUri = blockMocks.datasetSessionNode.resourceUri?.with({ + path: `/${blockMocks.datasetSessionNode.label as string}/${node.label as string}`, + }); - const favChildNodeForProfile = await testTree.initializeFavChildNodeForProfile("BRTVS99.PUBLIC", globals.DS_PDS_CONTEXT, favProfileNode); + const favChildNodeForProfile = testTree.initializeFavChildNodeForProfile("BRTVS99.PUBLIC", globals.DS_PDS_CONTEXT, favProfileNode); expect(favChildNodeForProfile).toEqual(node); }); @@ -266,21 +268,23 @@ describe("Dataset Tree Unit tests - Function initializeFavChildNodeForProfile", createGlobalMocks(); const blockMocks = createBlockMocks(); const testTree = new DatasetTree(); - const favProfileNode = new ZoweDatasetNode({ - label: "testProfile", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - favProfileNode.contextValue = globals.FAV_PROFILE_CONTEXT; + blockMocks.datasetSessionNode.contextValue = globals.FAV_PROFILE_CONTEXT; const node = new ZoweDatasetNode({ label: "BRTVS99.PS", collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: favProfileNode, + parentNode: blockMocks.datasetSessionNode, contextOverride: globals.DS_FAV_CONTEXT, }); - node.command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [node] }; + node.resourceUri = blockMocks.datasetSessionNode.resourceUri?.with({ + path: `/${blockMocks.datasetSessionNode.label as string}/${node.label as string}`, + }); + node.command = { command: "vscode.open", title: "", arguments: [node.resourceUri] }; - const favChildNodeForProfile = await testTree.initializeFavChildNodeForProfile("BRTVS99.PS", globals.DS_DS_CONTEXT, favProfileNode); + const favChildNodeForProfile = await testTree.initializeFavChildNodeForProfile( + "BRTVS99.PS", + globals.DS_DS_CONTEXT, + blockMocks.datasetSessionNode + ); expect(favChildNodeForProfile).toEqual(node); }); @@ -340,149 +344,7 @@ describe("Dataset Tree Unit Tests - Function getChildren", () => { expect(favoriteSessionNode).toMatchObject(children[0]); expect(blockMocks.datasetSessionNode).toMatchObject(children[1]); }); - it("Checking function for session node", async () => { - createGlobalMocks(); - const blockMocks = createBlockMocks(); - - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profile); - mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); - const testTree = new DatasetTree(); - blockMocks.datasetSessionNode.pattern = "test"; - testTree.mSessionNodes.push(blockMocks.datasetSessionNode); - testTree.mSessionNodes[1].dirty = true; - const sampleChildren: ZoweDatasetNode[] = [ - new ZoweDatasetNode({ - label: "BRTVS99", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: testTree.mSessionNodes[1], - profile: blockMocks.imperativeProfile, - }), - new ZoweDatasetNode({ - label: "BRTVS99.CA10", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: testTree.mSessionNodes[1], - profile: blockMocks.imperativeProfile, - contextOverride: globals.DS_MIGRATED_FILE_CONTEXT, - }), - new ZoweDatasetNode({ - label: "BRTVS99.CA11.SPFTEMP0.CNTL", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: testTree.mSessionNodes[1], - profile: blockMocks.imperativeProfile, - }), - new ZoweDatasetNode({ - label: "BRTVS99.DDIR", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: testTree.mSessionNodes[1], - profile: blockMocks.imperativeProfile, - }), - new ZoweDatasetNode({ - label: "BRTVS99.VS1", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: testTree.mSessionNodes[1], - profile: blockMocks.imperativeProfile, - contextOverride: globals.VSAM_CONTEXT, - }), - ]; - sampleChildren[0].command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [sampleChildren[0]] }; - - const children = await testTree.getChildren(testTree.mSessionNodes[1]); - expect(children.map((c) => c.label)).toEqual(sampleChildren.map((c) => c.label)); - expect(children).toEqual(sampleChildren); - }); - it("Checking function for session node with an imperative error", async () => { - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const testError = new imperative.ImperativeError({ msg: "test" }); - const spyOnDataSetsMatchingPattern = jest.spyOn(zosfiles.List, "dataSetsMatchingPattern"); - spyOnDataSetsMatchingPattern.mockResolvedValueOnce({ - success: true, - commandResponse: null, - apiResponse: [ - { dsname: "HLQ.USER", dsorg: "PS" }, - { dsname: "HLQ.USER.IMP.ERROR", error: testError }, - { dsname: "HLQ.USER.MIGRAT", dsorg: "PS", migr: "YES" }, - ], - }); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profile); - mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); - const testTree = new DatasetTree(); - blockMocks.datasetSessionNode.pattern = "test"; - testTree.mSessionNodes.push(blockMocks.datasetSessionNode); - testTree.mSessionNodes[1].dirty = true; - const nodeOk = new ZoweDatasetNode({ - label: "HLQ.USER", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: testTree.mSessionNodes[1], - profile: blockMocks.imperativeProfile, - }); - const nodeImpError = new ZoweDatasetNode({ - label: "HLQ.USER.IMP.ERROR", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: testTree.mSessionNodes[1], - profile: blockMocks.imperativeProfile, - contextOverride: globals.DS_FILE_ERROR_CONTEXT, - }); - nodeImpError.errorDetails = testError; - const nodeMigrated = new ZoweDatasetNode({ - label: "HLQ.USER.MIGRAT", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: testTree.mSessionNodes[1], - profile: blockMocks.imperativeProfile, - contextOverride: globals.DS_MIGRATED_FILE_CONTEXT, - }); - const sampleChildren: ZoweDatasetNode[] = [nodeOk, nodeImpError, nodeMigrated]; - sampleChildren[0].command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [sampleChildren[0]] }; - - const children = await testTree.getChildren(testTree.mSessionNodes[1]); - expect(children.map((c) => c.label)).toEqual(sampleChildren.map((c) => c.label)); - expect(children).toEqual(sampleChildren); - spyOnDataSetsMatchingPattern.mockRestore(); - }); - it("Checking that we fallback to old dataSet API if newer dataSetsMatchingPattern does not exist", async () => { - createGlobalMocks(); - const blockMocks = createBlockMocks(); - - const mockMvsApi = await ZoweExplorerApiRegister.getMvsApi(blockMocks.profile); - mockMvsApi.dataSetsMatchingPattern = null; - const getMvsApiMock = jest.fn(); - getMvsApiMock.mockReturnValue(mockMvsApi); - ZoweExplorerApiRegister.getMvsApi = getMvsApiMock.bind(ZoweExplorerApiRegister); - - const spyOnDataSetsMatchingPattern = jest.spyOn(zosfiles.List, "dataSetsMatchingPattern"); - const spyOnDataSet = jest.spyOn(zosfiles.List, "dataSet"); - spyOnDataSet.mockResolvedValueOnce({ - success: true, - commandResponse: null, - apiResponse: { - items: [{ dsname: "HLQ.USER", dsorg: "PS" }], - }, - }); - mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profile); - blockMocks.datasetSessionNode.pattern = "test"; - const testTree = new DatasetTree(); - testTree.mSessionNodes.push(blockMocks.datasetSessionNode); - testTree.mSessionNodes[1].dirty = true; - const sampleChildren: ZoweDatasetNode[] = [ - new ZoweDatasetNode({ - label: "HLQ.USER", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: testTree.mSessionNodes[1], - profile: blockMocks.imperativeProfile, - }), - ]; - sampleChildren[0].command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [sampleChildren[0]] }; - - const children = await testTree.getChildren(testTree.mSessionNodes[1]); - expect(children.map((c) => c.label)).toEqual(sampleChildren.map((c) => c.label)); - expect(children).toEqual(sampleChildren); - expect(spyOnDataSet).toHaveBeenCalled(); - expect(spyOnDataSetsMatchingPattern).not.toHaveBeenCalled(); - spyOnDataSet.mockRestore(); - spyOnDataSetsMatchingPattern.mockRestore(); - }); it("Checking function for favorite node", async () => { createGlobalMocks(); const blockMocks = createBlockMocks(); @@ -541,32 +403,8 @@ describe("Dataset Tree Unit Tests - Function getChildren", () => { expect(loadProfilesForFavoritesSpy).toHaveBeenCalledWith(log, favProfileNode); }); - it("Checking function for PDS Dataset node", async () => { - createGlobalMocks(); - const blockMocks = createBlockMocks(); - - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profile); - mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); - const testTree = new DatasetTree(); - testTree.mSessionNodes.push(blockMocks.datasetSessionNode); - const parent = new ZoweDatasetNode({ - label: "BRTVS99.PUBLIC", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: testTree.mSessionNodes[1], - }); - parent.dirty = true; - const sampleChildren: ZoweDatasetNode[] = [ - new ZoweDatasetNode({ label: "BRTVS99", collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: parent }), - new ZoweDatasetNode({ label: "BRTVS99.DDIR", collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: parent }), - ]; - sampleChildren[0].command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [sampleChildren[0]] }; - sampleChildren[1].command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [sampleChildren[1]] }; - - const children = await testTree.getChildren(parent); - expect(children).toEqual(sampleChildren); - }); - it("Checking function for return if element.getChildren is undefined", async () => { + it("returns 'No data sets found' if there are no children", async () => { createGlobalMocks(); const blockMocks = createBlockMocks(); @@ -580,11 +418,14 @@ describe("Dataset Tree Unit Tests - Function getChildren", () => { parentNode: testTree.mSessionNodes[1], }); parent.dirty = true; - jest.spyOn(parent, "getChildren").mockResolvedValueOnce(undefined as any); + jest.spyOn(parent, "getChildren").mockResolvedValueOnce([]); const children = await testTree.getChildren(parent); - expect(children).not.toBeDefined(); + // This function should never return undefined. + expect(children).toBeDefined(); + expect(children).toHaveLength(1); + expect(children[0].label).toBe("No data sets found"); }); }); describe("Dataset Tree Unit Tests - Function loadProfilesForFavorites", () => { @@ -723,7 +564,7 @@ describe("Dataset Tree Unit Tests - Function loadProfilesForFavorites", () => { expect(resultFavPdsNode).toEqual(expectedFavPdsNode); }); - it("Checking that loaded profile/session from profile node in Favorites gets passed to child favorites without profile/session", async () => { + it("Checking that loaded profile/session from profile node in Favorites is inherited for child nodes", async () => { createGlobalMocks(); const blockMocks = createBlockMocks(); const favProfileNode = new ZoweDatasetNode({ @@ -734,10 +575,10 @@ describe("Dataset Tree Unit Tests - Function loadProfilesForFavorites", () => { profile: blockMocks.imperativeProfile, contextOverride: globals.FAV_PROFILE_CONTEXT, }); - // Leave mParent parameter undefined for favPDsNode and expectedFavPdsNode to test undefined profile/session condition const favPdsNode = new ZoweDatasetNode({ label: "favoritePds", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + profile: blockMocks.imperativeProfile, contextOverride: globals.PDS_FAV_CONTEXT, }); const testTree = new DatasetTree(); @@ -1093,6 +934,9 @@ describe("Dataset Tree Unit Tests - Function addFavorite", () => { const treeView = createTreeView(); const datasetSessionNode = createDatasetSessionNode(session, imperativeProfile); const profile = createInstanceOfProfile(imperativeProfile); + const fspStat = jest.spyOn(DatasetFSProvider.instance, "stat").mockReturnValue({ + etag: "123ABC", + } as any); return { session, @@ -1100,6 +944,7 @@ describe("Dataset Tree Unit Tests - Function addFavorite", () => { treeView, profile, imperativeProfile, + fspStat, }; } @@ -1578,7 +1423,7 @@ describe("Dataset Tree Unit Tests - Function datasetFilterPrompt", () => { const blockMocks = await createBlockMocks(globalMocks); mocked(vscode.window.showQuickPick).mockResolvedValueOnce(new utils.FilterDescriptor("\uFF0B " + "Create a new filter")); - mocked(vscode.window.showInputBox).mockResolvedValueOnce("HLQ.PROD1.STUFF"); + mocked(Gui.showInputBox).mockResolvedValueOnce("HLQ.PROD1.STUFF"); mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); const testTree = new DatasetTree(); testTree.mSessionNodes.push(blockMocks.datasetSessionNode); @@ -1603,7 +1448,7 @@ describe("Dataset Tree Unit Tests - Function datasetFilterPrompt", () => { const blockMocks = await createBlockMocks(globalMocks); mocked(vscode.window.showQuickPick).mockResolvedValueOnce(new utils.FilterDescriptor("\uFF0B " + "Create a new filter")); - mocked(vscode.window.showInputBox).mockResolvedValueOnce("HLQ.PROD(STUF*),HLQ.PROD1*"); + mocked(Gui.showInputBox).mockResolvedValueOnce("HLQ.PROD(STUF*),HLQ.PROD1*"); mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); const testTree = new DatasetTree(); testTree.mSessionNodes.push(blockMocks.datasetSessionNode); @@ -1631,7 +1476,7 @@ describe("Dataset Tree Unit Tests - Function datasetFilterPrompt", () => { const blockMocks = await createBlockMocks(globalMocks); mocked(vscode.window.showQuickPick).mockResolvedValueOnce(new utils.FilterDescriptor("\uFF0B " + "Create a new filter")); - mocked(vscode.window.showInputBox).mockResolvedValueOnce("HLQ.PROD1(MEMBER)"); + mocked(Gui.showInputBox).mockResolvedValueOnce("HLQ.PROD1(MEMBER)"); mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); const testTree = new DatasetTree(); testTree.mSessionNodes.push(blockMocks.datasetSessionNode); @@ -1657,7 +1502,7 @@ describe("Dataset Tree Unit Tests - Function datasetFilterPrompt", () => { }); mocked(vscode.window.showQuickPick).mockResolvedValueOnce(new utils.FilterDescriptor("\uFF0B " + "Create a new filter")); - mocked(vscode.window.showInputBox).mockResolvedValueOnce("HLQ.PROD1.STUFF"); + mocked(Gui.showInputBox).mockResolvedValueOnce("HLQ.PROD1.STUFF"); mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); const testTree = new DatasetTree(); testTree.mSessionNodes.push(blockMocks.datasetSessionNode); @@ -1690,7 +1535,7 @@ describe("Dataset Tree Unit Tests - Function datasetFilterPrompt", () => { createQuickPickContent("HLQ.PROD1.STUFF", [quickPickItem], blockMocks.qpPlaceholder) ); mocked(vscode.window.showQuickPick).mockResolvedValueOnce(quickPickItem); - mocked(vscode.window.showInputBox).mockResolvedValueOnce("HLQ.PROD1.STUFF"); + mocked(Gui.showInputBox).mockResolvedValueOnce("HLQ.PROD1.STUFF"); mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); const resolveQuickPickSpy = jest.spyOn(Gui, "resolveQuickPick"); resolveQuickPickSpy.mockResolvedValueOnce(quickPickItem); @@ -1768,11 +1613,10 @@ describe("Dataset Tree Unit Tests - Function datasetFilterPrompt", () => { const globalMocks = createGlobalMocks(); const blockMocks = await createBlockMocks(globalMocks); - const errorSpy = jest.spyOn(utils, "errorHandling"); - const debugSpy = jest.spyOn(ZoweLogger, "debug"); + const errorHandlingMock = new MockedProperty(utils, "errorHandling"); mocked(vscode.window.showQuickPick).mockResolvedValueOnce(new utils.FilterDescriptor("\uFF0B " + "Create a new filter")); - mocked(vscode.window.showInputBox).mockResolvedValueOnce("HLQ.PROD1.STUFF"); + mocked(Gui.showInputBox).mockResolvedValueOnce("HLQ.PROD1.STUFF"); mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); const testTree = new DatasetTree(); testTree.mSessionNodes.push(blockMocks.datasetSessionNode); @@ -1788,10 +1632,8 @@ describe("Dataset Tree Unit Tests - Function datasetFilterPrompt", () => { }); expect(await testTree.datasetFilterPrompt(testTree.mSessionNodes[1])).not.toBeDefined(); - expect(debugSpy).toHaveBeenCalled(); - expect(errorSpy).toHaveBeenCalled(); - debugSpy.mockClear(); - errorSpy.mockClear(); + expect(errorHandlingMock.mock).toHaveBeenCalled(); + errorHandlingMock[Symbol.dispose](); }); it("Checking function for return if element.getChildren returns undefined", async () => { const globalMocks = createGlobalMocks(); @@ -1992,89 +1834,8 @@ describe("Dataset Tree Unit Tests - Function renameNode", () => { datasetSessionNode, }; } - - it("Checking common run of function", async () => { - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const testTree = new DatasetTree(); - const beforeNode = new ZoweDatasetNode({ - label: "TO.RENAME", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: blockMocks.datasetSessionNode, - session: blockMocks.session, - profile: blockMocks.imperativeProfile, - contextOverride: globals.DS_PDS_CONTEXT, - }); - const afterNode = new ZoweDatasetNode({ - label: "RENAMED", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: blockMocks.datasetSessionNode, - session: blockMocks.session, - profile: blockMocks.imperativeProfile, - contextOverride: globals.DS_PDS_CONTEXT, - }); - // the IDs will never match, so for the sake of this test, - // going to fake the IDs so that the expect passes - afterNode.id = ".TO.RENAME"; - blockMocks.datasetSessionNode.children.push(beforeNode); - testTree.mSessionNodes.push(blockMocks.datasetSessionNode); - - await testTree.renameNode("sestest", "TO.RENAME", "RENAMED"); - - expect(testTree.mSessionNodes[1].children[0]).toEqual({ ...afterNode, id: beforeNode.id }); - }); }); -describe("Dataset Tree Unit Tests - Function renameFavorite", () => { - function createBlockMocks() { - const session = createISession(); - const imperativeProfile = createIProfile(); - const datasetSessionNode = createDatasetSessionNode(session, imperativeProfile); - return { - session, - imperativeProfile, - datasetSessionNode, - }; - } - - it("Checking common run of function", async () => { - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const testTree = new DatasetTree(); - const nodeFromSession = new ZoweDatasetNode({ - label: "TO.RENAME", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: blockMocks.datasetSessionNode, - session: blockMocks.session, - profile: blockMocks.imperativeProfile, - contextOverride: globals.DS_PDS_CONTEXT, - }); - // Parent is normally a profile node in Favorites section, but is null here because it does not matter for this test - const matchingFavNode = new ZoweDatasetNode({ - label: "TO.RENAME", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - session: blockMocks.session, - profile: blockMocks.imperativeProfile, - contextOverride: globals.DS_PDS_CONTEXT + globals.FAV_SUFFIX, - }); - const expectedMatchingFavNodeResult = new ZoweDatasetNode({ - label: "RENAMED", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - session: blockMocks.session, - profile: blockMocks.imperativeProfile, - contextOverride: globals.DS_PDS_CONTEXT + globals.FAV_SUFFIX, - }); - Object.defineProperty(testTree, "findFavoritedNode", { - value: jest.fn(() => { - return matchingFavNode; - }), - }); - - await testTree.renameFavorite(nodeFromSession, "RENAMED"); - - expect(matchingFavNode).toEqual({ ...expectedMatchingFavNodeResult, id: matchingFavNode.id }); - }); -}); describe("Dataset Tree Unit Tests - Function findFavoritedNode", () => { function createBlockMocks() { const session = createISession(); @@ -2210,13 +1971,13 @@ describe("Dataset Tree Unit Tests - Function openItemFromPath", () => { label: "TEST.DS", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, parentNode: blockMocks.datasetSessionNode, + contextOverride: globals.DS_DS_CONTEXT, }); testTree.mSessionNodes[1].children.push(node); testTree.mSessionNodes[1].pattern = "test"; jest.spyOn(testTree.mSessionNodes[1], "getChildren").mockReturnValue(Promise.resolve([node])); await testTree.openItemFromPath(`[${blockMocks.datasetSessionNode.label}]: ${node.label}`, blockMocks.datasetSessionNode); - expect(testTree.getSearchHistory()).toEqual([node.label]); }); @@ -2239,7 +2000,6 @@ describe("Dataset Tree Unit Tests - Function openItemFromPath", () => { jest.spyOn(parent, "getChildren").mockReturnValue(Promise.resolve([child])); await testTree.openItemFromPath(`[${blockMocks.datasetSessionNode.label}]: ${parent.label}(${child.label})`, blockMocks.datasetSessionNode); - expect(testTree.getSearchHistory()).toEqual([`${parent.label}(${child.label})`]); }); }); @@ -2357,6 +2117,7 @@ describe("Dataset Tree Unit Tests - Function rename", () => { mvsApi, profileInstance, mockCheckCurrentProfile, + rename: jest.spyOn(DatasetFSProvider.instance, "rename").mockImplementation(), }; } @@ -2365,8 +2126,7 @@ describe("Dataset Tree Unit Tests - Function rename", () => { createGlobalMocks(); const blockMocks = createBlockMocks(); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - mocked(workspaceUtils.closeOpenedTextFile).mockResolvedValueOnce(false); - mocked(vscode.window.showInputBox).mockResolvedValueOnce("HLQ.TEST.RENAME.NODE.NEW"); + mocked(Gui.showInputBox).mockResolvedValueOnce("HLQ.TEST.RENAME.NODE.NEW"); mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); const testTree = new DatasetTree(); testTree.mSessionNodes.push(blockMocks.datasetSessionNode); @@ -2375,12 +2135,15 @@ describe("Dataset Tree Unit Tests - Function rename", () => { collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: testTree.mSessionNodes[1], session: blockMocks.session, + profile: testTree.mSessionNodes[1].getProfile(), }); - const renameDataSetSpy = jest.spyOn(blockMocks.mvsApi, "renameDataSet"); await testTree.rename(node); - - expect(renameDataSetSpy).toHaveBeenLastCalledWith("HLQ.TEST.RENAME.NODE", "HLQ.TEST.RENAME.NODE.NEW"); + expect(blockMocks.rename).toHaveBeenLastCalledWith( + { path: "/sestest/HLQ.TEST.RENAME.NODE", scheme: ZoweScheme.DS }, + { path: "/sestest/HLQ.TEST.RENAME.NODE.NEW", scheme: ZoweScheme.DS }, + { overwrite: false } + ); }); it("Checking function with PS Dataset using Unverified profile", async () => { @@ -2400,7 +2163,6 @@ describe("Dataset Tree Unit Tests - Function rename", () => { }; }), }); - mocked(workspaceUtils.closeOpenedTextFile).mockResolvedValueOnce(false); mocked(vscode.window.showInputBox).mockResolvedValueOnce("HLQ.TEST.RENAME.NODE.NEW"); mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); const testTree = new DatasetTree(); @@ -2411,11 +2173,10 @@ describe("Dataset Tree Unit Tests - Function rename", () => { parentNode: testTree.mSessionNodes[1], session: blockMocks.session, }); - const renameDataSetSpy = jest.spyOn(blockMocks.mvsApi, "renameDataSet"); + const renameDataSetSpy = jest.spyOn((DatasetTree as any).prototype, "renameDataSet"); await testTree.rename(node); - - expect(renameDataSetSpy).toHaveBeenLastCalledWith("HLQ.TEST.RENAME.NODE", "HLQ.TEST.RENAME.NODE.NEW"); + expect(renameDataSetSpy).toHaveBeenLastCalledWith(node); }); it("Checking function with PS Dataset given lowercase name", async () => { @@ -2423,7 +2184,6 @@ describe("Dataset Tree Unit Tests - Function rename", () => { createGlobalMocks(); const blockMocks = createBlockMocks(); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - mocked(workspaceUtils.closeOpenedTextFile).mockResolvedValueOnce(false); mocked(vscode.window.showInputBox).mockResolvedValueOnce("HLQ.TEST.RENAME.NODE.new"); mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); const testTree = new DatasetTree(); @@ -2434,11 +2194,10 @@ describe("Dataset Tree Unit Tests - Function rename", () => { parentNode: testTree.mSessionNodes[1], session: blockMocks.session, }); - const renameDataSetSpy = jest.spyOn(blockMocks.mvsApi, "renameDataSet"); + const renameDataSetSpy = jest.spyOn((DatasetTree as any).prototype, "renameDataSet"); await testTree.rename(node); - - expect(renameDataSetSpy).toHaveBeenLastCalledWith("HLQ.TEST.RENAME.NODE", "HLQ.TEST.RENAME.NODE.NEW"); + expect(renameDataSetSpy).toHaveBeenLastCalledWith(node); }); it("Checking function with Favorite PS Dataset", async () => { @@ -2447,7 +2206,6 @@ describe("Dataset Tree Unit Tests - Function rename", () => { const blockMocks = createBlockMocks(); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - mocked(workspaceUtils.closeOpenedTextFile).mockResolvedValueOnce(false); mocked(vscode.window.showInputBox).mockResolvedValueOnce("HLQ.TEST.RENAME.NODE.NEW"); mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); const testTree = new DatasetTree(); @@ -2460,22 +2218,18 @@ describe("Dataset Tree Unit Tests - Function rename", () => { }); node.contextValue = "ds_fav"; testTree.mSessionNodes[1].children.push(node); - const renameDataSetSpy = jest.spyOn(blockMocks.mvsApi, "renameDataSet"); + const renameDataSetSpy = jest.spyOn((DatasetTree as any).prototype, "renameDataSet"); await testTree.rename(node); - - expect(renameDataSetSpy).toHaveBeenLastCalledWith("HLQ.TEST.RENAME.NODE", "HLQ.TEST.RENAME.NODE.NEW"); + expect(renameDataSetSpy).toHaveBeenLastCalledWith(node); }); it("Checking failed attempt to rename PS Dataset", async () => { globals.defineGlobals(""); createGlobalMocks(); const blockMocks = createBlockMocks(); const defaultError = new Error("Default error message"); + const renameDataSetSpy = jest.spyOn((DatasetTree as any).prototype, "renameDataSet").mockRejectedValueOnce(defaultError); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - mocked(workspaceUtils.closeOpenedTextFile).mockResolvedValueOnce(false); - mocked(zosfiles.Rename.dataSet).mockImplementation(() => { - throw defaultError; - }); mocked(vscode.window.showInputBox).mockResolvedValueOnce("HLQ.TEST.RENAME.NODE.NEW"); mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); const testTree = new DatasetTree(); @@ -2486,7 +2240,6 @@ describe("Dataset Tree Unit Tests - Function rename", () => { parentNode: testTree.mSessionNodes[1], session: blockMocks.session, }); - const renameDataSetSpy = jest.spyOn(blockMocks.mvsApi, "renameDataSet"); let error; try { @@ -2495,7 +2248,7 @@ describe("Dataset Tree Unit Tests - Function rename", () => { error = err; } - expect(renameDataSetSpy).toHaveBeenLastCalledWith("HLQ.TEST.RENAME.NODE", "HLQ.TEST.RENAME.NODE.NEW"); + expect(renameDataSetSpy).toHaveBeenLastCalledWith(node); expect(error).toBe(defaultError); }); it("Checking function with PDS Member", async () => { @@ -2503,7 +2256,6 @@ describe("Dataset Tree Unit Tests - Function rename", () => { createGlobalMocks(); const blockMocks = createBlockMocks(); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - mocked(workspaceUtils.closeOpenedTextFile).mockResolvedValueOnce(false); mocked(vscode.window.showInputBox).mockResolvedValueOnce("MEM2"); mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); const testTree = new DatasetTree(); @@ -2551,27 +2303,21 @@ describe("Dataset Tree Unit Tests - Function rename", () => { favProfileNode.children.push(favParent); testTree.mFavorites.push(favProfileNode); - const renameDataSetMemberSpy = jest.spyOn(blockMocks.mvsApi, "renameDataSetMember"); - - const testValidDsName = async (text: string) => { - mocked(vscode.window.showInputBox).mockImplementation((options) => { - options.validateInput(text); - return Promise.resolve(text); - }); - await testTree.rename(child); - expect(renameDataSetMemberSpy).toHaveBeenLastCalledWith("HLQ.TEST.RENAME.NODE", "mem1", "MEM2"); - }; - - await testValidDsName("HLQ.TEST.RENAME.NODE"); + const renameDataSetMemberSpy = jest.spyOn((DatasetTree as any).prototype, "renameDataSetMember"); + mocked(vscode.window.showInputBox).mockImplementation((options) => { + options.validateInput("HLQ.TEST.RENAME.NODE"); + return Promise.resolve("HLQ.TEST.RENAME.NODE"); + }); await testTree.rename(child); + expect(renameDataSetMemberSpy).toHaveBeenLastCalledWith(child); }); + it("Checking function with PDS Member given in lowercase", async () => { globals.defineGlobals(""); createGlobalMocks(); const blockMocks = createBlockMocks(); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - mocked(workspaceUtils.closeOpenedTextFile).mockResolvedValueOnce(false); mocked(vscode.window.showInputBox).mockResolvedValueOnce("mem2"); mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); const testTree = new DatasetTree(); @@ -2618,18 +2364,20 @@ describe("Dataset Tree Unit Tests - Function rename", () => { favParent.children.push(favChild); favProfileNode.children.push(favParent); testTree.mFavorites.push(favProfileNode); - const renameDataSetMemberSpy = jest.spyOn(blockMocks.mvsApi, "renameDataSetMember"); + const renameDataSetMemberSpy = jest.spyOn((DatasetTree as any).prototype, "renameDataSetMember"); + const renameMock = jest.spyOn(DatasetFSProvider.instance, "rename").mockImplementation(); await testTree.rename(child); - expect(renameDataSetMemberSpy).toHaveBeenLastCalledWith("HLQ.TEST.RENAME.NODE", "mem1", "MEM2"); + expect(renameDataSetMemberSpy).toHaveBeenLastCalledWith(child); + expect(renameMock).toHaveBeenCalled(); + renameMock.mockRestore(); }); it("Checking function with favorite PDS Member", async () => { globals.defineGlobals(""); createGlobalMocks(); const blockMocks = createBlockMocks(); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - mocked(workspaceUtils.closeOpenedTextFile).mockResolvedValueOnce(false); mocked(vscode.window.showInputBox).mockResolvedValueOnce("MEM2"); mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); const testTree = new DatasetTree(); @@ -2679,11 +2427,11 @@ describe("Dataset Tree Unit Tests - Function rename", () => { favParent.children.push(favChild); favProfileNode.children.push(favParent); testTree.mFavorites.push(favProfileNode); - const renameDataSetMemberSpy = jest.spyOn(blockMocks.mvsApi, "renameDataSetMember"); + const renameDataSetMemberSpy = jest.spyOn((DatasetTree as any).prototype, "renameDataSetMember"); await testTree.rename(favChild); - expect(renameDataSetMemberSpy).toHaveBeenLastCalledWith("HLQ.TEST.RENAME.NODE", "mem1", "MEM2"); + expect(renameDataSetMemberSpy).toHaveBeenLastCalledWith(favChild); }); it("Checking failed attempt to rename PDS Member", async () => { globals.defineGlobals(""); @@ -2691,8 +2439,8 @@ describe("Dataset Tree Unit Tests - Function rename", () => { const blockMocks = createBlockMocks(); const defaultError = new Error("Default error message"); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - mocked(workspaceUtils.closeOpenedTextFile).mockResolvedValueOnce(false); - mocked(zosfiles.Rename.dataSetMember).mockImplementation(() => { + const renameDataSetMemberSpy = jest.spyOn((DatasetTree as any).prototype, "renameDataSetMember"); + renameDataSetMemberSpy.mockImplementation(() => { throw defaultError; }); mocked(vscode.window.showInputBox).mockResolvedValueOnce("MEM2"); @@ -2712,7 +2460,6 @@ describe("Dataset Tree Unit Tests - Function rename", () => { session: blockMocks.session, }); child.contextValue = globals.DS_MEMBER_CONTEXT; - const renameDataSetMemberSpy = jest.spyOn(blockMocks.mvsApi, "renameDataSetMember"); let error; try { @@ -2721,7 +2468,7 @@ describe("Dataset Tree Unit Tests - Function rename", () => { error = err; } - expect(renameDataSetMemberSpy).toHaveBeenLastCalledWith("HLQ.TEST.RENAME.NODE", "mem1", "MEM2"); + expect(renameDataSetMemberSpy).toHaveBeenLastCalledWith(child); expect(error).toBe(defaultError); }); it("Checking validate validateDataSetName util function successfully execution", async () => { @@ -2742,7 +2489,6 @@ describe("Dataset Tree Unit Tests - Function rename", () => { createGlobalMocks(); const blockMocks = createBlockMocks(); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - mocked(workspaceUtils.closeOpenedTextFile).mockResolvedValueOnce(false); mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); const testTree = new DatasetTree(); testTree.mSessionNodes.push(blockMocks.datasetSessionNode); @@ -2752,7 +2498,7 @@ describe("Dataset Tree Unit Tests - Function rename", () => { parentNode: testTree.mSessionNodes[1], session: blockMocks.session, }); - const renameDataSetSpy = jest.spyOn(blockMocks.mvsApi, "renameDataSet"); + const renameDataSetSpy = jest.spyOn((DatasetTree as any).prototype, "renameDataSet"); const testValidDsName = async (text: string) => { mocked(vscode.window.showInputBox).mockImplementation((options) => { options.validateInput(text); @@ -2760,7 +2506,7 @@ describe("Dataset Tree Unit Tests - Function rename", () => { }); const oldName = node.label; await testTree.rename(node); - expect(renameDataSetSpy).toHaveBeenLastCalledWith(oldName, text); + expect(renameDataSetSpy).toHaveBeenLastCalledWith(node); }; await testValidDsName("HLQ.TEST.RENAME.NODE.NEW.TEST"); @@ -3287,32 +3033,37 @@ describe("Dataset Tree Unit Tests - Sorting and Filtering operations", () => { describe("Dataset Tree Unit Tests - Function openWithEncoding", () => { it("sets binary encoding if selection was made", async () => { + const setEncodingMock = jest.spyOn(DatasetFSProvider.instance, "setEncodingForFile").mockImplementation(); const node = new ZoweDatasetNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); node.openDs = jest.fn(); jest.spyOn(sharedUtils, "promptForEncoding").mockResolvedValueOnce({ kind: "binary" }); await DatasetTree.prototype.openWithEncoding(node); expect(node.binary).toBe(true); - expect(node.encoding).toBeUndefined(); + expect(setEncodingMock).toHaveBeenCalledWith(node.resourceUri, { kind: "binary" }); expect(node.openDs).toHaveBeenCalledTimes(1); + setEncodingMock.mockRestore(); }); it("sets text encoding if selection was made", async () => { + const setEncodingMock = jest.spyOn(DatasetFSProvider.instance, "setEncodingForFile").mockImplementation(); const node = new ZoweDatasetNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); node.openDs = jest.fn(); jest.spyOn(sharedUtils, "promptForEncoding").mockResolvedValueOnce({ kind: "text" }); await DatasetTree.prototype.openWithEncoding(node); expect(node.binary).toBe(false); - expect(node.encoding).toBeNull(); + expect(setEncodingMock).toHaveBeenCalledWith(node.resourceUri, { kind: "text" }); expect(node.openDs).toHaveBeenCalledTimes(1); + setEncodingMock.mockRestore(); }); it("does not set encoding if prompt was cancelled", async () => { + const setEncodingSpy = jest.spyOn(DatasetFSProvider.instance, "setEncodingForFile"); const node = new ZoweDatasetNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); node.openDs = jest.fn(); jest.spyOn(sharedUtils, "promptForEncoding").mockResolvedValueOnce(undefined); await DatasetTree.prototype.openWithEncoding(node); expect(node.binary).toBe(false); - expect(node.encoding).toBeUndefined(); + expect(setEncodingSpy).not.toHaveBeenCalled(); expect(node.openDs).toHaveBeenCalledTimes(0); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/dataset/ZoweDatasetNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/dataset/ZoweDatasetNode.unit.test.ts index eb047958f6..0bcf70c21c 100644 --- a/packages/zowe-explorer/__tests__/__unit__/dataset/ZoweDatasetNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/dataset/ZoweDatasetNode.unit.test.ts @@ -28,6 +28,7 @@ import * as fs from "fs"; import * as sharedUtils from "../../../src/shared/utils"; import { Profiles } from "../../../src/Profiles"; import { ZoweLogger } from "../../../src/utils/ZoweLogger"; +import { DatasetFSProvider } from "../../../src/dataset/DatasetFSProvider"; // Missing the definition of path module, because I need the original logic for tests jest.mock("fs"); @@ -44,6 +45,7 @@ function createGlobalMocks() { profileInstance: null, getContentsSpy: null, mvsApi: null, + openTextDocument: jest.fn(), }; newMocks.profileInstance = createInstanceOfProfile(newMocks.imperativeProfile); @@ -110,8 +112,7 @@ describe("ZoweDatasetNode Unit Tests - Function node.openDs()", () => { await node.openDs(false, true, blockMocks.testDatasetTree); - expect(mocked(fs.existsSync)).toHaveBeenCalledWith(path.join(globals.DS_DIR, node.getSessionNode().label.toString(), node.label.toString())); - expect(mocked(vscode.workspace.openTextDocument)).toHaveBeenCalledWith(sharedUtils.getDocumentFilePath(node.label.toString(), node)); + expect(mocked(vscode.commands.executeCommand)).toHaveBeenCalledWith("vscode.open", node.resourceUri); }); it("Checking of opening for common dataset with unverified profile", async () => { @@ -141,42 +142,14 @@ describe("ZoweDatasetNode Unit Tests - Function node.openDs()", () => { }); await node.openDs(false, true, blockMocks.testDatasetTree); - - expect(mocked(fs.existsSync)).toHaveBeenCalledWith(path.join(globals.DS_DIR, node.getSessionNode().label.toString(), node.label.toString())); - expect(mocked(vscode.workspace.openTextDocument)).toHaveBeenCalledWith(sharedUtils.getDocumentFilePath(node.label.toString(), node)); - }); - - it("Checking of opening for common dataset without supporting ongoing actions", async () => { - globals.defineGlobals(""); - createGlobalMocks(); - const blockMocks = createBlockMocks(); - - mocked(blockMocks.mvsApi.getContents).mockResolvedValueOnce({ - success: true, - commandResponse: "", - apiResponse: { - etag: "123", - }, - }); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const node = new ZoweDatasetNode({ - label: "node", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - node.ongoingActions = undefined as any; - - await node.openDs(false, true, blockMocks.testDatasetTree); - - expect(mocked(fs.existsSync)).toHaveBeenCalledWith(path.join(globals.DS_DIR, node.getSessionNode().label.toString(), node.label.toString())); - expect(mocked(vscode.workspace.openTextDocument)).toHaveBeenCalledWith(sharedUtils.getDocumentFilePath(node.label.toString(), node)); + expect(mocked(vscode.commands.executeCommand)).toHaveBeenCalledWith("vscode.open", node.resourceUri); }); it("Checking of failed attempt to open dataset", async () => { globals.defineGlobals(""); const globalMocks = createGlobalMocks(); const blockMocks = createBlockMocks(); - globalMocks.getContentsSpy.mockRejectedValueOnce(new Error("testError")); + mocked(vscode.commands.executeCommand).mockRejectedValueOnce(new Error("testError")); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); const node = new ZoweDatasetNode({ label: "node", @@ -190,28 +163,7 @@ describe("ZoweDatasetNode Unit Tests - Function node.openDs()", () => { // do nothing } - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Error: testError"); - }); - - it("Check for invalid/null response when contents are already fetched", async () => { - globals.defineGlobals(""); - const globalMocks = createGlobalMocks(); - const blockMocks = createBlockMocks(); - globalMocks.getContentsSpy.mockClear(); - mocked(fs.existsSync).mockReturnValueOnce(true); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const node = new ZoweDatasetNode({ - label: "node", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - etag: "abc", - }); - node.ongoingActions = undefined as any; - - await node.openDs(false, true, blockMocks.testDatasetTree); - - expect(globalMocks.getContentsSpy).not.toHaveBeenCalled(); - expect(node.getEtag()).toBe("abc"); + expect(mocked(Gui.errorMessage)).toBeCalledWith("Error: testError"); }); it("Checking of opening for PDS Member", async () => { @@ -238,12 +190,7 @@ describe("ZoweDatasetNode Unit Tests - Function node.openDs()", () => { await child.openDs(false, true, blockMocks.testDatasetTree); - expect(mocked(fs.existsSync)).toHaveBeenCalledWith( - path.join(globals.DS_DIR, child.getSessionNode().label.toString(), `${parent.label.toString()}(${child.label.toString()})`) - ); - expect(mocked(vscode.workspace.openTextDocument)).toHaveBeenCalledWith( - sharedUtils.getDocumentFilePath(`${parent.label.toString()}(${child.label.toString()})`, child) - ); + expect(mocked(vscode.commands.executeCommand)).toHaveBeenCalledWith("vscode.open", child.resourceUri); }); it("Checking of opening for PDS Member of favorite dataset", async () => { globals.defineGlobals(""); @@ -269,12 +216,7 @@ describe("ZoweDatasetNode Unit Tests - Function node.openDs()", () => { await child.openDs(false, true, blockMocks.testDatasetTree); - expect(mocked(fs.existsSync)).toHaveBeenCalledWith( - path.join(globals.DS_DIR, child.getSessionNode().label.toString(), `${parent.label.toString()}(${child.label.toString()})`) - ); - expect(mocked(vscode.workspace.openTextDocument)).toHaveBeenCalledWith( - sharedUtils.getDocumentFilePath(`${parent.label.toString()}(${child.label.toString()})`, child) - ); + expect(mocked(vscode.commands.executeCommand)).toHaveBeenCalledWith("vscode.open", child.resourceUri); }); it("Checking of opening for sequential DS of favorite session", async () => { globals.defineGlobals(""); @@ -300,31 +242,9 @@ describe("ZoweDatasetNode Unit Tests - Function node.openDs()", () => { await child.openDs(false, true, blockMocks.testDatasetTree); - expect(mocked(fs.existsSync)).toHaveBeenCalledWith(path.join(globals.DS_DIR, blockMocks.imperativeProfile.name, child.label.toString())); - expect(mocked(vscode.workspace.openTextDocument)).toHaveBeenCalledWith(sharedUtils.getDocumentFilePath(child.label.toString(), child)); + expect(mocked(vscode.commands.executeCommand)).toHaveBeenCalledWith("vscode.open", child.resourceUri); }); - it("Checks that openDs fails if called from an invalid node", async () => { - globals.defineGlobals(""); - createGlobalMocks(); - const blockMocks = createBlockMocks(); - - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const node = new ZoweDatasetNode({ - label: "parent", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - profile: blockMocks.imperativeProfile, - }); - blockMocks.datasetSessionNode.contextValue = "aieieiieeeeooooo"; - try { - await node.openDs(false, true, blockMocks.testDatasetTree); - } catch (err) { - // Prevent exception from failing test - } - - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Invalid data set or member."); - }); it("Checking that error is displayed and logged for opening of node with invalid context value", async () => { createGlobalMocks(); const blockMocks = createBlockMocks(); @@ -353,98 +273,37 @@ describe("ZoweDatasetNode Unit Tests - Function node.openDs()", () => { // Do nothing } - expect(showErrorMessageSpy).toHaveBeenCalledWith("Invalid data set or member."); + expect(showErrorMessageSpy).toHaveBeenCalledWith("Cannot download, item invalid."); expect(logErrorSpy).toHaveBeenCalledTimes(1); }); }); -describe("ZoweDatasetNode Unit Tests - Function node.downloadDs()", () => { - function createBlockMocks() { - const session = createISession(); - const imperativeProfile = createIProfile(); - const profileInstance = createInstanceOfProfile(imperativeProfile); - const datasetSessionNode = createDatasetSessionNode(session, imperativeProfile); - const pdsSessionNode = new ZoweDatasetNode({ - label: "sestest", - collapsibleState: vscode.TreeItemCollapsibleState.Expanded, - parentNode: datasetSessionNode, - session, - profile: profileInstance, - }); - pdsSessionNode.contextValue = globals.DS_PDS_CONTEXT; - - return { - imperativeProfile, - pdsSessionNode, - }; - } - - it("Testing downloadDs() called with invalid node", async () => { - globals.defineGlobals(""); - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new ZoweDatasetNode({ - label: "HLQ.TEST.TO.NODE", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.pdsSessionNode, - profile: blockMocks.imperativeProfile, - }); - blockMocks.pdsSessionNode.contextValue = "fakeContext"; - - try { - await node.downloadDs(true); - } catch (err) { - /* Do nothing */ - } - - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Invalid data set or member."); - }); - - it("Testing downloadDs() called with a member", async () => { - globals.defineGlobals(""); - const globalMocks = createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new ZoweDatasetNode({ - label: "HLQ.TEST.TO.NODE", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.pdsSessionNode, - profile: blockMocks.imperativeProfile, - }); - globalMocks.getContentsSpy.mockResolvedValueOnce({ - success: true, - commandResponse: null, - apiResponse: { - etag: "123", - }, - }); +describe("ZoweDatasetNode Unit Tests - Function node.setEncoding()", () => { + const setEncodingForFileMock = jest.spyOn(DatasetFSProvider.instance, "setEncodingForFile").mockImplementation(); - const label = node.getParent().getLabel().toString() + "(" + node.getLabel().toString() + ")"; - const filePathSpy = jest.spyOn(sharedUtils, "getDocumentFilePath"); - await node.downloadDs(true); - expect(filePathSpy).toHaveBeenCalledWith(label, node); + afterAll(() => { + setEncodingForFileMock.mockRestore(); }); -}); -describe("ZoweDatasetNode Unit Tests - Function node.setEncoding()", () => { it("sets encoding to binary", () => { const node = new ZoweDatasetNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); node.setEncoding({ kind: "binary" }); expect(node.binary).toEqual(true); - expect(node.encoding).toBeUndefined(); + expect(setEncodingForFileMock).toHaveBeenCalledWith(node.resourceUri, { kind: "binary" }); }); it("sets encoding to text", () => { const node = new ZoweDatasetNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); node.setEncoding({ kind: "text" }); expect(node.binary).toEqual(false); - expect(node.encoding).toBeNull(); + expect(setEncodingForFileMock).toHaveBeenCalledWith(node.resourceUri, { kind: "text" }); }); it("sets encoding to other codepage", () => { const node = new ZoweDatasetNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); node.setEncoding({ kind: "other", codepage: "IBM-1047" }); expect(node.binary).toEqual(false); - expect(node.encoding).toEqual("IBM-1047"); + expect(setEncodingForFileMock).toHaveBeenCalledWith(node.resourceUri, { kind: "other", codepage: "IBM-1047" }); }); it("sets encoding for favorite node", () => { @@ -456,14 +315,14 @@ describe("ZoweDatasetNode Unit Tests - Function node.setEncoding()", () => { const node = new ZoweDatasetNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode }); node.setEncoding({ kind: "text" }); expect(node.binary).toEqual(false); - expect(node.encoding).toBeNull(); + expect(setEncodingForFileMock).toHaveBeenCalledWith(node.resourceUri, { kind: "text" }); }); it("resets encoding to undefined", () => { const node = new ZoweDatasetNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); node.setEncoding(undefined as any); expect(node.binary).toEqual(false); - expect(node.encoding).toBeUndefined(); + expect(setEncodingForFileMock).toHaveBeenCalledWith(node.resourceUri, undefined); }); it("fails to set encoding for session node", () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/dataset/actions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/dataset/actions.unit.test.ts index 1a728e3ab4..d2789ea10c 100644 --- a/packages/zowe-explorer/__tests__/__unit__/dataset/actions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/dataset/actions.unit.test.ts @@ -39,12 +39,12 @@ import * as fs from "fs"; import * as sharedUtils from "../../../src/shared/utils"; import { Profiles } from "../../../src/Profiles"; import * as utils from "../../../src/utils/ProfilesUtils"; -import * as wsUtils from "../../../src/utils/workspace"; import { getNodeLabels } from "../../../src/dataset/utils"; import { ZoweLogger } from "../../../src/utils/ZoweLogger"; import * as context from "../../../src/shared/context"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; import { mocked } from "../../../__mocks__/mockUtils"; +import { DatasetFSProvider } from "../../../src/dataset/DatasetFSProvider"; // Missing the definition of path module, because I need the original logic for tests jest.mock("fs"); @@ -71,10 +71,12 @@ function createGlobalMocks() { testFavoritesNode: createDatasetFavoritesNode(), testDatasetTree: null, getContentsSpy: null, + fspDelete: jest.spyOn(DatasetFSProvider.prototype, "delete").mockImplementation(), statusBarMsgSpy: null, mvsApi: null, mockShowWarningMessage: jest.fn(), }; + newMocks.fspDelete.mockClear(); newMocks.profileInstance = createInstanceOfProfile(newMocks.imperativeProfile); newMocks.datasetSessionNode = createDatasetSessionNode(newMocks.session, newMocks.imperativeProfile); @@ -98,6 +100,7 @@ function createGlobalMocks() { value: newMocks.mockShowWarningMessage, configurable: true, }); + Object.defineProperty(vscode.workspace.fs, "delete", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.window, "showInputBox", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.workspace, "openTextDocument", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.workspace, "getConfiguration", { value: jest.fn(), configurable: true }); @@ -113,8 +116,6 @@ function createGlobalMocks() { Object.defineProperty(zosfiles, "Create", { value: jest.fn(), configurable: true }); Object.defineProperty(zosfiles.Create, "dataSet", { value: jest.fn(), configurable: true }); Object.defineProperty(zosfiles.Create, "dataSetLike", { value: jest.fn(), configurable: true }); - Object.defineProperty(fs, "unlinkSync", { value: jest.fn(), configurable: true }); - Object.defineProperty(fs, "existsSync", { value: jest.fn(), configurable: true }); Object.defineProperty(sharedUtils, "concatChildNodes", { value: jest.fn(), configurable: true }); Object.defineProperty(Profiles, "getInstance", { value: jest.fn(), configurable: true }); Object.defineProperty(zosfiles, "List", { value: jest.fn(), configurable: true }); @@ -135,6 +136,7 @@ const createBlockMocksShared = () => { const datasetSessionNode = createDatasetSessionNode(session, imperativeProfile); const testDatasetTree = createDatasetTree(datasetSessionNode, treeView); const mvsApi = createMvsApi(imperativeProfile); + const fetchDsAtUri = jest.spyOn(DatasetFSProvider.instance, "fetchDatasetAtUri").mockImplementation(); bindMvsApi(mvsApi); return { @@ -145,6 +147,7 @@ const createBlockMocksShared = () => { datasetSessionNode, mvsApi, testDatasetTree, + fetchDsAtUri, }; }; @@ -279,30 +282,8 @@ describe("Dataset Actions Unit Tests - Function refreshPS", () => { parentNode: blockMocks.datasetSessionNode, }); - mocked(vscode.workspace.openTextDocument).mockResolvedValueOnce({ isDirty: true } as any); - mocked(zosfiles.Download.dataSet).mockResolvedValueOnce({ - success: true, - commandResponse: null, - apiResponse: { - etag: "123", - }, - }); - await dsActions.refreshPS(node); - - expect(mocked(zosfiles.Download.dataSet)).toHaveBeenCalledWith( - blockMocks.zosmfSession, - node.label, - expect.objectContaining({ - file: path.join(globals.DS_DIR, node.getSessionNode().label.toString(), node.label.toString()), - returnEtag: true, - }) - ); - expect(mocked(vscode.workspace.openTextDocument)).toHaveBeenCalledWith( - path.join(globals.DS_DIR, node.getSessionNode().label.toString(), node.label.toString()) - ); - expect(mocked(vscode.window.showTextDocument)).toHaveBeenCalledTimes(2); - expect(mocked(vscode.commands.executeCommand)).toHaveBeenCalledWith("workbench.action.closeActiveEditor"); + expect(blockMocks.fetchDsAtUri).toHaveBeenCalledWith(node.resourceUri, { editor: undefined }); }); it("Checking duplicate PS dataset refresh attempt", async () => { globals.defineGlobals(""); @@ -337,15 +318,11 @@ describe("Dataset Actions Unit Tests - Function refreshPS", () => { parentNode: blockMocks.datasetSessionNode, }); - mocked(vscode.workspace.openTextDocument).mockResolvedValueOnce({ isDirty: true } as any); - mocked(zosfiles.Download.dataSet).mockRejectedValueOnce(Error("not found")); - - globalMocks.getContentsSpy.mockRejectedValueOnce(new Error("not found")); + blockMocks.fetchDsAtUri.mockRejectedValueOnce(Error("not found")); await dsActions.refreshPS(node); - expect(mocked(Gui.showMessage)).toHaveBeenCalledWith("Unable to find file " + node.label); - expect(mocked(vscode.commands.executeCommand)).not.toHaveBeenCalled(); + expect(mocked(Gui.showMessage)).toHaveBeenCalledWith("Unable to find file " + (node.label as string)); }); it("Checking failed attempt to refresh PDS Member", async () => { globals.defineGlobals(""); @@ -358,20 +335,10 @@ describe("Dataset Actions Unit Tests - Function refreshPS", () => { }); const child = new ZoweDatasetNode({ label: "child", collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: parent }); - mocked(vscode.workspace.openTextDocument).mockResolvedValueOnce({ isDirty: true } as any); - mocked(zosfiles.Download.dataSet).mockRejectedValueOnce(Error("")); + blockMocks.fetchDsAtUri.mockRejectedValueOnce(Error("not found")); await dsActions.refreshPS(child); - - expect(mocked(zosfiles.Download.dataSet)).toHaveBeenCalledWith( - blockMocks.zosmfSession, - child.getParent().getLabel() + "(" + child.label + ")", - expect.objectContaining({ - file: path.join(globals.DS_DIR, child.getSessionNode().label.toString(), `${child.getParent().label}(${child.label})`), - returnEtag: true, - }) - ); - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Error"); + expect(mocked(Gui.showMessage)).toHaveBeenCalledWith(`Unable to find file ${parent.label}(${child.label})`); }); it("Checking favorite empty PDS refresh", async () => { globals.defineGlobals(""); @@ -384,19 +351,8 @@ describe("Dataset Actions Unit Tests - Function refreshPS", () => { }); node.contextValue = globals.DS_PDS_CONTEXT + globals.FAV_SUFFIX; - mocked(vscode.workspace.openTextDocument).mockResolvedValueOnce({ isDirty: true } as any); - mocked(zosfiles.Download.dataSet).mockResolvedValueOnce({ - success: true, - commandResponse: null, - apiResponse: { - etag: "123", - }, - }); - await dsActions.refreshPS(node); - expect(mocked(vscode.workspace.openTextDocument)).toHaveBeenCalled(); - expect(mocked(vscode.window.showTextDocument)).toHaveBeenCalledTimes(2); - expect(mocked(vscode.commands.executeCommand)).toHaveBeenCalledWith("workbench.action.closeActiveEditor"); + expect(blockMocks.fetchDsAtUri).toHaveBeenCalledWith(node.resourceUri, { editor: undefined }); }); it("Checking favorite PDS Member refresh", async () => { globals.defineGlobals(""); @@ -410,19 +366,8 @@ describe("Dataset Actions Unit Tests - Function refreshPS", () => { const child = new ZoweDatasetNode({ label: "child", collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: parent }); parent.contextValue = globals.DS_PDS_CONTEXT + globals.FAV_SUFFIX; - mocked(vscode.workspace.openTextDocument).mockResolvedValueOnce({ isDirty: true } as any); - mocked(zosfiles.Download.dataSet).mockResolvedValueOnce({ - success: true, - commandResponse: null, - apiResponse: { - etag: "123", - }, - }); - await dsActions.refreshPS(child); - expect(mocked(vscode.workspace.openTextDocument)).toHaveBeenCalled(); - expect(mocked(vscode.window.showTextDocument)).toHaveBeenCalledTimes(2); - expect(mocked(vscode.commands.executeCommand)).toHaveBeenCalledWith("workbench.action.closeActiveEditor"); + expect(blockMocks.fetchDsAtUri).toHaveBeenCalledWith(child.resourceUri, { editor: undefined }); }); it("Checking favorite PS refresh", async () => { globals.defineGlobals(""); @@ -436,19 +381,8 @@ describe("Dataset Actions Unit Tests - Function refreshPS", () => { const child = new ZoweDatasetNode({ label: "child", collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: parent }); child.contextValue = globals.DS_FAV_CONTEXT; - mocked(vscode.workspace.openTextDocument).mockResolvedValueOnce({ isDirty: true } as any); - mocked(zosfiles.Download.dataSet).mockResolvedValueOnce({ - success: true, - commandResponse: null, - apiResponse: { - etag: "123", - }, - }); - await dsActions.refreshPS(child); - expect(mocked(vscode.workspace.openTextDocument)).toHaveBeenCalled(); - expect(mocked(vscode.window.showTextDocument)).toHaveBeenCalledTimes(2); - expect(mocked(vscode.commands.executeCommand)).toHaveBeenCalledWith("workbench.action.closeActiveEditor"); + expect(blockMocks.fetchDsAtUri).toHaveBeenCalledWith(child.resourceUri, { editor: undefined }); }); }); @@ -560,6 +494,7 @@ describe("Dataset Actions Unit Tests - Function deleteDatasetPrompt", () => { blockMocks.testDatasetTree.getTreeView.mockReturnValueOnce(treeView); globalMocks.mockShowWarningMessage.mockResolvedValueOnce("Delete"); + jest.spyOn(DatasetFSProvider.instance, "delete").mockImplementation(); await dsActions.deleteDatasetPrompt(blockMocks.testDatasetTree); expect(mocked(Gui.showMessage)).toHaveBeenCalledWith( @@ -733,7 +668,7 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { it("Checking common PS dataset deletion", async () => { globals.defineGlobals(""); - createGlobalMocks(); + const globalMocks = createGlobalMocks(); const blockMocks = createBlockMocks(); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); const node = new ZoweDatasetNode({ @@ -743,15 +678,8 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { profile: blockMocks.imperativeProfile, }); - mocked(fs.existsSync).mockReturnValueOnce(true); - mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - const deleteSpy = jest.spyOn(blockMocks.mvsApi, "deleteDataSet"); - await dsActions.deleteDataset(node, blockMocks.testDatasetTree); - - expect(deleteSpy).toHaveBeenCalledWith(node.label, { responseTimeout: blockMocks.imperativeProfile.profile?.responseTimeout }); - expect(mocked(fs.existsSync)).toHaveBeenCalledWith(path.join(globals.DS_DIR, node.getSessionNode().label.toString(), node.label.toString())); - expect(mocked(fs.unlinkSync)).toHaveBeenCalledWith(path.join(globals.DS_DIR, node.getSessionNode().label.toString(), node.label.toString())); + expect(globalMocks.fspDelete).toHaveBeenCalledWith(node.resourceUri, { recursive: false }); }); it("Checking common PS dataset deletion with Unverified profile", async () => { globals.defineGlobals(""); @@ -776,15 +704,10 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { profile: blockMocks.imperativeProfile, }); - mocked(fs.existsSync).mockReturnValueOnce(true); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - const deleteSpy = jest.spyOn(blockMocks.mvsApi, "deleteDataSet"); - + const deleteSpy = jest.spyOn(DatasetFSProvider.instance, "delete").mockImplementation(); await dsActions.deleteDataset(node, blockMocks.testDatasetTree); - - expect(deleteSpy).toHaveBeenCalledWith(node.label, { responseTimeout: blockMocks.imperativeProfile.profile?.responseTimeout }); - expect(mocked(fs.existsSync)).toHaveBeenCalledWith(path.join(globals.DS_DIR, node.getSessionNode().label.toString(), node.label.toString())); - expect(mocked(fs.unlinkSync)).toHaveBeenCalledWith(path.join(globals.DS_DIR, node.getSessionNode().label.toString(), node.label.toString())); + expect(deleteSpy).toHaveBeenCalledWith(node.resourceUri, { recursive: false }); }); it("Checking common PS dataset deletion with not existing local file", async () => { globals.defineGlobals(""); @@ -798,14 +721,10 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { profile: blockMocks.imperativeProfile, }); - mocked(fs.existsSync).mockReturnValueOnce(false); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - const deleteSpy = jest.spyOn(blockMocks.mvsApi, "deleteDataSet"); - + const deleteSpy = jest.spyOn(DatasetFSProvider.instance, "delete").mockImplementation(); await dsActions.deleteDataset(node, blockMocks.testDatasetTree); - - expect(mocked(fs.unlinkSync)).not.toHaveBeenCalled(); - expect(deleteSpy).toHaveBeenCalledWith(node.label, { responseTimeout: blockMocks.imperativeProfile.profile?.responseTimeout }); + expect(deleteSpy).toHaveBeenCalledWith(node.resourceUri, { recursive: false }); }); it("Checking common PS dataset failed deletion attempt due to absence on remote", async () => { globals.defineGlobals(""); @@ -819,13 +738,9 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { profile: blockMocks.imperativeProfile, }); - mocked(fs.existsSync).mockReturnValueOnce(true); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - const deleteSpy = jest.spyOn(blockMocks.mvsApi, "deleteDataSet"); - deleteSpy.mockRejectedValueOnce(Error("not found")); - - await expect(dsActions.deleteDataset(node, blockMocks.testDatasetTree)).rejects.toEqual(Error("not found")); - + jest.spyOn(DatasetFSProvider.instance, "delete").mockRejectedValueOnce(Error("not found")); + await expect(dsActions.deleteDataset(node, blockMocks.testDatasetTree)).rejects.toThrow("not found"); expect(mocked(Gui.showMessage)).toHaveBeenCalledWith("Unable to find file " + node.label); }); it("Checking common PS dataset failed deletion attempt", async () => { @@ -840,13 +755,10 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { profile: blockMocks.imperativeProfile, }); - mocked(fs.existsSync).mockReturnValueOnce(true); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - const deleteSpy = jest.spyOn(blockMocks.mvsApi, "deleteDataSet"); - deleteSpy.mockRejectedValueOnce(Error("")); - - await expect(dsActions.deleteDataset(node, blockMocks.testDatasetTree)).rejects.toEqual(Error("")); - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Error"); + jest.spyOn(DatasetFSProvider.instance, "delete").mockRejectedValueOnce(Error("")); + await expect(dsActions.deleteDataset(node, blockMocks.testDatasetTree)).rejects.toThrow(""); + expect(mocked(Gui.errorMessage)).toBeCalledWith("Error"); }); it("Checking Favorite PDS dataset deletion", async () => { globals.defineGlobals(""); @@ -867,16 +779,13 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { }); node.contextValue = globals.DS_PDS_CONTEXT + globals.FAV_SUFFIX; - mocked(fs.existsSync).mockReturnValueOnce(true); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - const deleteSpy = jest.spyOn(blockMocks.mvsApi, "deleteDataSet"); + const deleteSpy = jest.spyOn(DatasetFSProvider.instance, "delete"); await dsActions.deleteDataset(node, blockMocks.testDatasetTree); - expect(deleteSpy).toHaveBeenCalledWith(node.label, { responseTimeout: blockMocks.imperativeProfile.profile?.responseTimeout }); + expect(deleteSpy).toHaveBeenCalledWith(node.resourceUri, { recursive: false }); expect(blockMocks.testDatasetTree.removeFavorite).toHaveBeenCalledWith(node); - expect(mocked(fs.existsSync)).toHaveBeenCalledWith(path.join(globals.DS_DIR, parent.getSessionNode().label.toString(), "HLQ.TEST.NODE")); - expect(mocked(fs.unlinkSync)).toHaveBeenCalledWith(path.join(globals.DS_DIR, parent.getSessionNode().label.toString(), "HLQ.TEST.NODE")); }); it("Checking Favorite PDS Member deletion", async () => { globals.defineGlobals(""); @@ -891,22 +800,12 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { parent.contextValue = globals.DS_PDS_CONTEXT + globals.FAV_SUFFIX; const child = new ZoweDatasetNode({ label: "child", collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: parent }); - mocked(fs.existsSync).mockReturnValueOnce(true); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - const deleteSpy = jest.spyOn(blockMocks.mvsApi, "deleteDataSet"); + const deleteSpy = jest.spyOn(DatasetFSProvider.instance, "delete").mockImplementation(); await dsActions.deleteDataset(child, blockMocks.testDatasetTree); - - expect(deleteSpy).toHaveBeenCalledWith(`${child.getParent().label.toString()}(${child.label.toString()})`, { - responseTimeout: blockMocks.imperativeProfile.profile?.responseTimeout, - }); - expect(blockMocks.testDatasetTree.removeFavorite).toHaveBeenCalledWith(child); - expect(mocked(fs.existsSync)).toHaveBeenCalledWith( - path.join(globals.DS_DIR, parent.getSessionNode().label.toString(), `${child.getParent().label.toString()}(${child.label.toString()})`) - ); - expect(mocked(fs.unlinkSync)).toHaveBeenCalledWith( - path.join(globals.DS_DIR, parent.getSessionNode().label.toString(), `${child.getParent().label.toString()}(${child.label.toString()})`) - ); + expect(deleteSpy).toHaveBeenCalledWith(child.resourceUri, { recursive: false }); + expect(blockMocks.testDatasetTree.removeFavorite).toBeCalledWith(child); }); it("Checking Favorite PS dataset deletion", async () => { globals.defineGlobals(""); @@ -931,20 +830,11 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { child.contextValue = globals.DS_FAV_CONTEXT; blockMocks.testDatasetTree.mFavorites[0].children.push(child); - mocked(fs.existsSync).mockReturnValueOnce(true); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - const deleteSpy = jest.spyOn(blockMocks.mvsApi, "deleteDataSet"); - + const deleteSpy = jest.spyOn(DatasetFSProvider.instance, "delete").mockImplementation(); await dsActions.deleteDataset(child, blockMocks.testDatasetTree); - - expect(deleteSpy).toHaveBeenCalledWith("HLQ.TEST.DELETE.NODE", { responseTimeout: blockMocks.imperativeProfile.profile?.responseTimeout }); + expect(deleteSpy).toHaveBeenCalledWith(child.resourceUri, { recursive: false }); expect(blockMocks.testDatasetTree.removeFavorite).toHaveBeenCalledWith(child); - expect(mocked(fs.existsSync)).toHaveBeenCalledWith( - path.join(globals.DS_DIR, parent.getSessionNode().label.toString(), "HLQ.TEST.DELETE.NODE") - ); - expect(mocked(fs.unlinkSync)).toHaveBeenCalledWith( - path.join(globals.DS_DIR, parent.getSessionNode().label.toString(), "HLQ.TEST.DELETE.NODE") - ); }); it("Checking incorrect dataset failed deletion attempt", async () => { globals.defineGlobals(""); @@ -964,12 +854,10 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { profile: blockMocks.imperativeProfile, }); - mocked(fs.existsSync).mockReturnValueOnce(true); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - const deleteSpy = jest.spyOn(blockMocks.mvsApi, "deleteDataSet"); + const deleteSpy = jest.spyOn(DatasetFSProvider.instance, "delete").mockImplementation(); deleteSpy.mockClear(); - - await expect(dsActions.deleteDataset(child, blockMocks.testDatasetTree)).rejects.toEqual(Error("Cannot delete, item invalid.")); + await expect(dsActions.deleteDataset(child, blockMocks.testDatasetTree)).rejects.toThrow("Cannot delete, item invalid."); expect(deleteSpy).not.toHaveBeenCalled(); }); }); @@ -1025,599 +913,6 @@ describe("Dataset Actions Unit Tests - Function enterPattern", () => { }); }); -describe("Dataset Actions Unit Tests - Function saveFile", () => { - function createBlockMocks() { - const session = createISession(); - const sessionWithoutCredentials = createISessionWithoutCredentials(); - const imperativeProfile = createIProfile(); - const profileInstance = createInstanceOfProfile(imperativeProfile); - const zosmfSession = createSessCfgFromArgs(imperativeProfile); - const treeView = createTreeView(); - const datasetSessionNode = createDatasetSessionNode(session, imperativeProfile); - const datasetFavoritesNode = createDatasetFavoritesNode(); - const testDatasetTree = createDatasetTree(datasetSessionNode, treeView, datasetFavoritesNode); - const mvsApi = createMvsApi(imperativeProfile); - bindMvsApi(mvsApi); - - return { - session, - sessionWithoutCredentials, - zosmfSession, - treeView, - imperativeProfile, - datasetSessionNode, - datasetFavoritesNode, - mvsApi, - profileInstance, - testDatasetTree, - }; - } - - afterAll(() => jest.restoreAllMocks()); - - it("To check Compare Function is getting triggered from Favorites", async () => { - globals.defineGlobals(""); - const globalMocks = createGlobalMocks(); - const blockMocks = createBlockMocks(); - - // Create nodes for Session section - const node = new ZoweDatasetNode({ - label: "HLQ.TEST.AFILE", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - profile: blockMocks.imperativeProfile, - }); - node.contextValue = globals.DS_PDS_CONTEXT; - const childNode = new ZoweDatasetNode({ - label: "MEM", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: node, - profile: blockMocks.imperativeProfile, - }); - - // Create nodes for Favorites section - const favProfileNode = new ZoweDatasetNode({ - label: "sestest", - collapsibleState: vscode.TreeItemCollapsibleState.Expanded, - parentNode: blockMocks.datasetFavoritesNode, - contextOverride: globals.FAV_PROFILE_CONTEXT, - }); - const favoriteNode = new ZoweDatasetNode({ - label: "HLQ.TEST.AFILE", - collapsibleState: vscode.TreeItemCollapsibleState.Expanded, - parentNode: favProfileNode, - profile: blockMocks.imperativeProfile, - }); - favoriteNode.contextValue = globals.DS_PDS_CONTEXT + globals.FAV_SUFFIX; - const favoriteChildNode = new ZoweDatasetNode({ - label: "MEM", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: favoriteNode, - profile: blockMocks.imperativeProfile, - }); - - // Push nodes into respective Session or Favorites sections - node.children.push(childNode); - blockMocks.testDatasetTree.mSessionNodes.find((child) => child.label.toString().trim() === "sestest").children.push(node); - favoriteNode.children.push(favoriteChildNode); - blockMocks.testDatasetTree.mFavorites.push(favProfileNode); - blockMocks.testDatasetTree.mFavorites[0].children.push(favoriteNode); - - mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([favoriteNode, favoriteChildNode]); - blockMocks.testDatasetTree.getChildren.mockReturnValueOnce(blockMocks.testDatasetTree.mSessionNodes); - mocked(zosfiles.List.dataSet).mockResolvedValue({ - success: true, - commandResponse: "", - apiResponse: { - items: [{ dsname: "HLQ.TEST.AFILE" }, { dsname: "HLQ.TEST.AFILE(MEM)" }], - }, - }); - mocked(zosfiles.Upload.pathToDataSet).mockResolvedValueOnce({ - success: true, - commandResponse: "success", - apiResponse: [ - { - etag: "123", - }, - ], - }); - mocked(vscode.window.withProgress).mockImplementation((progLocation, callback) => { - return callback(); - }); - blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(blockMocks.imperativeProfile); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const testDocument = createTextDocument("HLQ.TEST.AFILE(MEM)", blockMocks.datasetSessionNode); - jest.spyOn(favoriteChildNode, "getEtag").mockImplementation(() => "123"); - (testDocument as any).fileName = path.join(globals.DS_DIR, blockMocks.imperativeProfile.name, testDocument.fileName); - await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); - - expect(mocked(sharedUtils.concatChildNodes)).toHaveBeenCalled(); - expect(mocked(globalMocks.statusBarMsgSpy)).toHaveBeenCalledWith("success", globals.STATUS_BAR_TIMEOUT_MS); - expect(blockMocks.profileInstance.loadNamedProfile).toHaveBeenCalledWith(blockMocks.imperativeProfile.name); - }); - - it("Checking common dataset saving action when no session is defined", async () => { - globals.defineGlobals(""); - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const nodeWithoutSession = new ZoweDatasetNode({ - label: "HLQ.TEST.AFILE", - collapsibleState: vscode.TreeItemCollapsibleState.None, - profile: blockMocks.imperativeProfile, - }); - - blockMocks.testDatasetTree.getChildren.mockReturnValueOnce([nodeWithoutSession]); - mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([nodeWithoutSession]); - blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(blockMocks.imperativeProfile); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const getSessionSpy = jest.spyOn(blockMocks.mvsApi, "getSession").mockReturnValueOnce(blockMocks.sessionWithoutCredentials); - const testDocument = createTextDocument("HLQ.TEST.AFILE", blockMocks.datasetSessionNode); - (testDocument as any).fileName = path.join(globals.DS_DIR, testDocument.fileName); - - await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); - - expect(getSessionSpy).toReturnWith(blockMocks.sessionWithoutCredentials); - expect(mocked(vscode.workspace.applyEdit)).toHaveBeenCalledTimes(2); - }); - it("Checking common dataset saving failed attempt due to inability to locate session and profile", async () => { - globals.defineGlobals(""); - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const nodeWithoutSession = new ZoweDatasetNode({ - label: "HLQ.TEST.AFILE", - collapsibleState: vscode.TreeItemCollapsibleState.None, - profile: blockMocks.imperativeProfile, - }); - - blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(undefined); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - blockMocks.testDatasetTree.getChildren.mockReturnValueOnce([nodeWithoutSession]); - const testDocument = createTextDocument("HLQ.TEST.AFILE", blockMocks.datasetSessionNode); - (testDocument as any).fileName = path.join(globals.DS_DIR, testDocument.fileName); - - await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); - - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Could not locate session when saving data set."); - }); - it("Checking common dataset saving failed attempt due to its absence on the side of the server", async () => { - globals.defineGlobals(""); - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new ZoweDatasetNode({ - label: "node", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - profile: blockMocks.imperativeProfile, - }); - - blockMocks.testDatasetTree.getChildren.mockReturnValueOnce([node, blockMocks.datasetSessionNode]); - blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(blockMocks.imperativeProfile); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const dataSetSpy = jest.spyOn(blockMocks.mvsApi, "dataSet").mockResolvedValueOnce({ - success: true, - commandResponse: "", - apiResponse: { - items: [], - }, - }); - const testDocument = createTextDocument("HLQ.TEST.AFILE", blockMocks.datasetSessionNode); - (testDocument as any).fileName = path.join(globals.DS_DIR, testDocument.fileName); - - await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); - - expect(dataSetSpy).toHaveBeenCalledWith("HLQ.TEST.AFILE", { responseTimeout: blockMocks.imperativeProfile.profile?.responseTimeout }); - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Data set failed to save. Data set may have been deleted or renamed on mainframe."); - }); - it("Checking common dataset saving", async () => { - globals.defineGlobals(""); - const globalMocks = createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new ZoweDatasetNode({ - label: "HLQ.TEST.AFILE", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - profile: blockMocks.imperativeProfile, - }); - blockMocks.datasetSessionNode.children.push(node); - - mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([node]); - blockMocks.testDatasetTree.getChildren.mockReturnValueOnce([blockMocks.datasetSessionNode]); - mocked(zosfiles.List.dataSet).mockResolvedValue({ - success: true, - commandResponse: "", - apiResponse: { - items: [{ dsname: "HLQ.TEST.AFILE" }, { dsname: "HLQ.TEST.AFILE(mem)" }], - }, - }); - mocked(zosfiles.Upload.pathToDataSet).mockResolvedValueOnce({ - success: true, - commandResponse: "success", - apiResponse: [ - { - etag: "123", - }, - ], - }); - mocked(vscode.window.withProgress).mockImplementation((progLocation, callback) => { - return callback(); - }); - blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(blockMocks.imperativeProfile); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const mockSetEtag = jest.spyOn(node, "setEtag").mockImplementation(() => null); - const testDocument = createTextDocument("HLQ.TEST.AFILE", blockMocks.datasetSessionNode); - (testDocument as any).fileName = path.join(globals.DS_DIR, testDocument.fileName); - - await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); - - expect(mocked(sharedUtils.concatChildNodes)).toHaveBeenCalled(); - expect(mockSetEtag).toHaveBeenCalledWith("123"); - expect(mocked(globalMocks.statusBarMsgSpy)).toHaveBeenCalledWith("success", globals.STATUS_BAR_TIMEOUT_MS); - }); - it("Checking common dataset failed saving attempt", async () => { - globals.defineGlobals(""); - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new ZoweDatasetNode({ - label: "HLQ.TEST.AFILE", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - profile: blockMocks.imperativeProfile, - }); - blockMocks.datasetSessionNode.children.push(node); - - mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([node]); - blockMocks.testDatasetTree.getChildren.mockReturnValueOnce([blockMocks.datasetSessionNode]); - mocked(zosfiles.List.dataSet).mockResolvedValue({ - success: true, - commandResponse: "", - apiResponse: { - items: [{ dsname: "HLQ.TEST.AFILE" }, { dsname: "HLQ.TEST.AFILE(mem)" }], - }, - }); - mocked(zosfiles.Upload.pathToDataSet).mockResolvedValueOnce({ - success: false, - commandResponse: "failed", - apiResponse: [ - { - etag: "123", - }, - ], - }); - mocked(vscode.window.withProgress).mockImplementation((progLocation, callback) => { - return callback(); - }); - blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(blockMocks.imperativeProfile); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const testDocument = createTextDocument("HLQ.TEST.AFILE", blockMocks.datasetSessionNode); - (testDocument as any).fileName = path.join(globals.DS_DIR, testDocument.fileName); - - await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); - - expect(mocked(sharedUtils.concatChildNodes)).toHaveBeenCalled(); - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("failed"); - expect(mocked(vscode.workspace.applyEdit)).toHaveBeenCalledTimes(2); - }); - it("Checking favorite dataset saving", async () => { - globals.defineGlobals(""); - const globalMocks = createGlobalMocks(); - const blockMocks = createBlockMocks(); - const favoriteNode = new ZoweDatasetNode({ - label: "[TestSessionName]: HLQ.TEST.AFILE", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - profile: blockMocks.imperativeProfile, - }); - const node = new ZoweDatasetNode({ - label: "HLQ.TEST.AFILE", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: favoriteNode, - profile: blockMocks.imperativeProfile, - }); - favoriteNode.contextValue = globals.DS_DS_CONTEXT + globals.FAV_SUFFIX; - node.contextValue = globals.DS_DS_CONTEXT + globals.FAV_SUFFIX; - favoriteNode.children.push(node); - blockMocks.testDatasetTree.mFavorites.push(favoriteNode); - - mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([node]); - blockMocks.testDatasetTree.getChildren.mockReturnValueOnce([blockMocks.datasetSessionNode]); - mocked(zosfiles.List.dataSet).mockResolvedValue({ - success: true, - commandResponse: "", - apiResponse: { - items: [{ dsname: "HLQ.TEST.AFILE" }, { dsname: "HLQ.TEST.AFILE(mem)" }], - }, - }); - mocked(zosfiles.Upload.pathToDataSet).mockResolvedValueOnce({ - success: true, - commandResponse: "success", - apiResponse: [ - { - etag: "123", - }, - ], - }); - mocked(vscode.window.withProgress).mockImplementation((progLocation, callback) => { - return callback(); - }); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - blockMocks.profileInstance.loadNamedProfile.mockReturnValue(blockMocks.imperativeProfile); - const mockSetEtag = jest.spyOn(node, "setEtag").mockImplementation(() => null); - const testDocument = createTextDocument("HLQ.TEST.AFILE", blockMocks.datasetSessionNode); - (testDocument as any).fileName = path.join(globals.DS_DIR, blockMocks.imperativeProfile.name, testDocument.fileName); - - await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); - - expect(mocked(sharedUtils.concatChildNodes)).toHaveBeenCalled(); - expect(mocked(globalMocks.statusBarMsgSpy)).toHaveBeenCalledWith("success", globals.STATUS_BAR_TIMEOUT_MS); - }); - it("Checking favorite PDS Member saving", async () => { - globals.defineGlobals(""); - const globalMocks = createGlobalMocks(); - const blockMocks = createBlockMocks(); - // Create nodes for Session section - const node = new ZoweDatasetNode({ - label: "HLQ.TEST.AFILE", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - profile: blockMocks.imperativeProfile, - }); - node.contextValue = globals.DS_PDS_CONTEXT; - const childNode = new ZoweDatasetNode({ - label: "MEM", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: node, - profile: blockMocks.imperativeProfile, - }); - // Create nodes for Favorites section - const favProfileNode = new ZoweDatasetNode({ - label: "testProfile", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: blockMocks.datasetFavoritesNode, - contextOverride: globals.FAV_PROFILE_CONTEXT, - }); - const favoriteNode = new ZoweDatasetNode({ - label: "HLQ.TEST.AFILE", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: favProfileNode, - profile: blockMocks.imperativeProfile, - }); - favoriteNode.contextValue = globals.DS_PDS_CONTEXT + globals.FAV_SUFFIX; - const favoriteChildNode = new ZoweDatasetNode({ - label: "MEM", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: favoriteNode, - profile: blockMocks.imperativeProfile, - }); - // Push nodes into respective Session or Favorites sections - node.children.push(childNode); - favoriteNode.children.push(favoriteChildNode); - blockMocks.testDatasetTree.mFavorites.push(favProfileNode); - blockMocks.testDatasetTree.mFavorites[0].children.push(favoriteNode); - - mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([node, childNode]); - blockMocks.testDatasetTree.getChildren.mockReturnValueOnce([]); - mocked(zosfiles.List.dataSet).mockResolvedValue({ - success: true, - commandResponse: "", - apiResponse: { - items: [{ dsname: "HLQ.TEST.AFILE" }, { dsname: "HLQ.TEST.AFILE(MEM)" }], - }, - }); - mocked(zosfiles.Upload.pathToDataSet).mockResolvedValueOnce({ - success: true, - commandResponse: "success", - apiResponse: [ - { - etag: "123", - }, - ], - }); - mocked(vscode.window.withProgress).mockImplementation((progLocation, callback) => { - return callback(); - }); - blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(blockMocks.imperativeProfile); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const mockSetEtag = jest.spyOn(childNode, "setEtag").mockImplementation(() => null); - const testDocument = createTextDocument("HLQ.TEST.AFILE(MEM)", blockMocks.datasetSessionNode); - (testDocument as any).fileName = path.join(globals.DS_DIR, blockMocks.imperativeProfile.name, testDocument.fileName); - - await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); - - expect(mocked(sharedUtils.concatChildNodes)).toHaveBeenCalled(); - expect(mockSetEtag).toHaveBeenCalledWith("123"); - expect(mocked(globalMocks.statusBarMsgSpy)).toHaveBeenCalledWith("success", globals.STATUS_BAR_TIMEOUT_MS); - expect(blockMocks.profileInstance.loadNamedProfile).toHaveBeenCalledWith(blockMocks.imperativeProfile.name); - }); - it("Checking common dataset failed saving attempt due to incorrect document path", async () => { - globals.defineGlobals(""); - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new ZoweDatasetNode({ - label: "HLQ.TEST.AFILE", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - profile: blockMocks.imperativeProfile, - }); - blockMocks.datasetSessionNode.children.push(node); - - mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([node]); - blockMocks.testDatasetTree.getChildren.mockReturnValueOnce([blockMocks.datasetSessionNode]); - blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(blockMocks.imperativeProfile); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const testDocument = createTextDocument("HLQ.TEST.AFILE", blockMocks.datasetSessionNode); - - await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); - - expect(mocked(zosfiles.List.dataSet)).not.toHaveBeenCalled(); - expect(mocked(zosfiles.Upload.pathToDataSet)).not.toHaveBeenCalled(); - }); - it("Checking PDS member saving attempt", async () => { - globals.defineGlobals(""); - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new ZoweDatasetNode({ - label: "HLQ.TEST.AFILE(mem)", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - profile: blockMocks.imperativeProfile, - }); - blockMocks.datasetSessionNode.children.push(node); - - mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([node]); - blockMocks.testDatasetTree.getChildren.mockReturnValueOnce([blockMocks.datasetSessionNode]); - mocked(zosfiles.List.dataSet).mockResolvedValue({ - success: true, - commandResponse: "", - apiResponse: { - items: [{ dsname: "HLQ.TEST.AFILE" }, { dsname: "HLQ.TEST.AFILE(mem)" }], - }, - }); - mocked(zosfiles.Upload.pathToDataSet).mockResolvedValueOnce({ - success: true, - commandResponse: "success", - apiResponse: [ - { - etag: "123", - }, - ], - }); - mocked(vscode.window.withProgress).mockImplementation((progLocation, callback) => { - return callback(); - }); - blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(blockMocks.imperativeProfile); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const testDocument = createTextDocument("HLQ.TEST.AFILE(mem)", blockMocks.datasetSessionNode); - (testDocument as any).fileName = path.join(globals.DS_DIR, testDocument.fileName); - - await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); - - expect(mocked(sharedUtils.concatChildNodes)).toHaveBeenCalled(); - expect(mocked(Gui.setStatusBarMessage)).toHaveBeenCalledWith("success", globals.STATUS_BAR_TIMEOUT_MS); - }); - it("Checking common dataset saving failed due to conflict with server version", async () => { - globals.defineGlobals(""); - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new ZoweDatasetNode({ - label: "HLQ.TEST.AFILE", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - profile: blockMocks.imperativeProfile, - }); - blockMocks.datasetSessionNode.children.push(node); - - mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([node]); - blockMocks.testDatasetTree.getChildren.mockReturnValueOnce([blockMocks.datasetSessionNode]); - mocked(zosfiles.List.dataSet).mockResolvedValue({ - success: true, - commandResponse: "", - apiResponse: { - items: [{ dsname: "HLQ.TEST.AFILE" }], - }, - }); - mocked(zosfiles.Upload.pathToDataSet).mockResolvedValueOnce({ - success: false, - commandResponse: "Rest API failure with HTTP(S) status 412", - apiResponse: [], - }); - - mocked(vscode.window.withProgress).mockImplementation((progLocation, callback) => { - return callback(); - }); - const profile = blockMocks.imperativeProfile; - profile.profile.encoding = 1047; - blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(blockMocks.imperativeProfile); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - Object.defineProperty(wsUtils, "markDocumentUnsaved", { - value: jest.fn(), - configurable: true, - }); - Object.defineProperty(context, "isTypeUssTreeNode", { - value: jest.fn().mockReturnValueOnce(false), - configurable: true, - }); - Object.defineProperty(ZoweExplorerApiRegister.getMvsApi, "getContents", { - value: jest.fn(), - configurable: true, - }); - - const testDocument = createTextDocument("HLQ.TEST.AFILE", blockMocks.datasetSessionNode); - (testDocument as any).fileName = path.join(globals.DS_DIR, testDocument.fileName); - const logSpy = jest.spyOn(ZoweLogger, "warn"); - const commandSpy = jest.spyOn(vscode.commands, "executeCommand"); - - await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); - - expect(logSpy).toHaveBeenCalledWith("Remote file has changed. Presenting with way to resolve file."); - expect(mocked(sharedUtils.concatChildNodes)).toHaveBeenCalled(); - expect(commandSpy).toHaveBeenCalledWith("workbench.files.action.compareWithSaved"); - logSpy.mockClear(); - commandSpy.mockClear(); - }); - - it("Checking common dataset saving failed due to conflict with server version when file size has not changed", async () => { - globals.defineGlobals(""); - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new ZoweDatasetNode({ - label: "HLQ.TEST.AFILE", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - profile: blockMocks.imperativeProfile, - }); - blockMocks.datasetSessionNode.children.push(node); - - mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([node]); - blockMocks.testDatasetTree.getChildren.mockReturnValueOnce([blockMocks.datasetSessionNode]); - mocked(zosfiles.List.dataSet).mockResolvedValue({ - success: true, - commandResponse: "", - apiResponse: { - items: [{ dsname: "HLQ.TEST.AFILE" }], - }, - }); - mocked(zosfiles.Upload.pathToDataSet).mockResolvedValueOnce({ - success: false, - commandResponse: "Rest API failure with HTTP(S) status 412", - apiResponse: [], - }); - - mocked(vscode.window.withProgress).mockImplementation((progLocation, callback) => { - return callback(); - }); - const profile = blockMocks.imperativeProfile; - profile.profile.encoding = 1047; - blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(blockMocks.imperativeProfile); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - Object.defineProperty(wsUtils, "markDocumentUnsaved", { - value: jest.fn(), - configurable: true, - }); - Object.defineProperty(context, "isTypeUssTreeNode", { - value: jest.fn().mockReturnValueOnce(false), - configurable: true, - }); - Object.defineProperty(ZoweExplorerApiRegister.getMvsApi, "getContents", { - value: jest.fn(), - configurable: true, - }); - - const testDocument = createTextDocument("HLQ.TEST.AFILE", blockMocks.datasetSessionNode); - (testDocument as any).fileName = path.join(globals.DS_DIR, testDocument.fileName); - const logSpy = jest.spyOn(ZoweLogger, "warn"); - const commandSpy = jest.spyOn(vscode.commands, "executeCommand"); - jest.spyOn(fs, "statSync").mockReturnValueOnce({ size: 0 } as any); - - await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); - - expect(logSpy).toHaveBeenCalledWith("Remote file has changed. Presenting with way to resolve file."); - expect(mocked(sharedUtils.concatChildNodes)).toHaveBeenCalled(); - expect(commandSpy).toHaveBeenCalledWith("workbench.files.action.compareWithSaved"); - logSpy.mockClear(); - commandSpy.mockClear(); - }); -}); - describe("Dataset Actions Unit Tests - Function showAttributes", () => { function createBlockMocks() { const session = createISession(); @@ -2161,6 +1456,7 @@ describe("Dataset Actions Unit Tests - Function copyDataSets", () => { fn(); return Promise.resolve(params); }); + jest.spyOn(DatasetFSProvider.instance, "stat").mockReturnValue({ etag: "123ABC" } as any); await dsActions.copyDataSets(dsNode, null, blockMocks.testDatasetTree); await expect(mocked(Gui.errorMessage)).not.toHaveBeenCalled(); @@ -3602,7 +2898,6 @@ describe("Dataset Actions Unit Tests - Function allocateLike", () => { jest.spyOn(datasetSessionNode, "getChildren").mockResolvedValue([testNode, testSDSNode]); testDatasetTree.createFilterString.mockReturnValue("test"); jest.spyOn(Gui, "resolveQuickPick").mockResolvedValue(quickPickItem); - jest.spyOn(ZoweDatasetNode.prototype, "openDs").mockImplementation(() => null); return { session, diff --git a/packages/zowe-explorer/__tests__/__unit__/dataset/init.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/dataset/init.unit.test.ts index 1c956499c6..98632c9757 100644 --- a/packages/zowe-explorer/__tests__/__unit__/dataset/init.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/dataset/init.unit.test.ts @@ -17,7 +17,8 @@ import * as dsActions from "../../../src/dataset/actions"; import * as sharedExtension from "../../../src/shared/init"; import { initDatasetProvider } from "../../../src/dataset/init"; import { Profiles } from "../../../src/Profiles"; -import { IJestIt, ITestContext, processSubscriptions, spyOnSubscriptions } from "../../__common__/testUtils"; +import { IJestIt, ITestContext, processSubscriptions } from "../../__common__/testUtils"; +import { ZoweScheme } from "@zowe/zowe-explorer-api"; describe("Test src/dataset/extension", () => { describe("initDatasetProvider", () => { @@ -26,7 +27,12 @@ describe("Test src/dataset/extension", () => { let spyCreateDatasetTree; const test: ITestContext = { context: { subscriptions: [] }, - value: { label: "test", getParent: () => "test", openDs: jest.fn() }, + value: { + label: "test", + getParent: () => "test", + openDs: jest.fn(), + command: { command: "vscode.open", title: "", arguments: [vscode.Uri.from({ scheme: ZoweScheme.DS, path: "TEST.DS" })] }, + }, _: { _: "_" }, }; const dsProvider: { [key: string]: jest.Mock } = { @@ -90,10 +96,6 @@ describe("Test src/dataset/extension", () => { name: "zowe.ds.editSession", mock: [{ spy: jest.spyOn(dsProvider, "editSession"), arg: [test.value, dsProvider] }], }, - { - name: "zowe.ds.ZoweNode.openPS", - mock: [{ spy: jest.spyOn(test.value, "openDs"), arg: [false, true, dsProvider] }], - }, { name: "zowe.ds.createDataset", mock: [{ spy: jest.spyOn(dsActions, "createFile"), arg: [test.value, dsProvider] }], @@ -123,7 +125,6 @@ describe("Test src/dataset/extension", () => { mock: [ { spy: jest.spyOn(contextuals, "isDs"), arg: [test.value], ret: false }, { spy: jest.spyOn(contextuals, "isDsMember"), arg: [test.value], ret: true }, - { spy: jest.spyOn(test.value, "openDs"), arg: [false, false, dsProvider] }, ], }, { @@ -131,7 +132,6 @@ describe("Test src/dataset/extension", () => { mock: [ { spy: jest.spyOn(contextuals, "isDs"), arg: [test.value], ret: false }, { spy: jest.spyOn(contextuals, "isDsMember"), arg: [test.value], ret: true }, - { spy: jest.spyOn(test.value, "openDs"), arg: [false, false, dsProvider] }, ], }, { @@ -241,7 +241,7 @@ describe("Test src/dataset/extension", () => { { spy: jest.spyOn(Profiles, "getInstance"), arg: [], - ret: { enableValidation: jest.fn() }, + ret: { enableValidation: jest.fn(), disableValidation: jest.fn() }, }, ], }, @@ -284,7 +284,6 @@ describe("Test src/dataset/extension", () => { Object.defineProperty(vscode.workspace, "onDidChangeConfiguration", { value: onDidChangeConfiguration }); spyCreateDatasetTree.mockResolvedValue(dsProvider as any); - spyOnSubscriptions(commands); jest.spyOn(vscode.workspace, "onDidCloseTextDocument").mockImplementation(dsProvider.onDidCloseTextDocument); await initDatasetProvider(test.context); }); @@ -299,7 +298,7 @@ describe("Test src/dataset/extension", () => { it("should not initialize if it is unable to create the dataset tree", async () => { spyCreateDatasetTree.mockResolvedValue(null); - const myProvider = await initDatasetProvider({} as any); + const myProvider = await initDatasetProvider(test.context); expect(myProvider).toBe(null); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/dataset/utils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/dataset/utils.unit.test.ts new file mode 100644 index 0000000000..0b5b000f75 --- /dev/null +++ b/packages/zowe-explorer/__tests__/__unit__/dataset/utils.unit.test.ts @@ -0,0 +1,37 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { getLanguageId } from "../../../src/dataset/utils"; + +describe("Dataset utils unit tests - function getLanguageId", () => { + it("returns the proper language ID", () => { + const pairs = [ + { name: "TEST.DS.C", languageId: "c" }, + { name: "TEST.PDS.C(MEMBER)", languageId: "c" }, + { name: "TEST.DS.JCL", languageId: "jcl" }, + { name: "TEST.DS.CBL", languageId: "cobol" }, + { name: "TEST.PDS.CPY(M1)", languageId: "copybook" }, + { name: "TEST.DS.INCLUDE", languageId: "inc" }, + { name: "TEST.DS.PLX", languageId: "pli" }, + { name: "TEST.DS.SHELL", languageId: "shellscript" }, + { name: "TEST.DS.EXEC", languageId: "rexx" }, + { name: "TEST.DS.XML", languageId: "xml" }, + { name: "TEST.DS.ASM", languageId: "asm" }, + { name: "TEST.DS.LOG", languageId: "log" }, + ]; + for (const pair of pairs) { + expect(getLanguageId(pair.name)).toBe(pair.languageId); + } + }); + it("returns null if no language ID was found", () => { + expect(getLanguageId("TEST.DS")).toBe(null); + }); +}); diff --git a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts index 0e02e3dba0..ddb4be5159 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts @@ -30,9 +30,9 @@ import { DatasetTree } from "../../src/dataset/DatasetTree"; import { USSTree } from "../../src/uss/USSTree"; import { ZoweSaveQueue } from "../../src/abstract/ZoweSaveQueue"; import { ZoweLocalStorage } from "../../src/utils/ZoweLocalStorage"; +import { ProfilesUtils } from "../../src/utils/ProfilesUtils"; jest.mock("../../src/utils/LoggerUtils"); jest.mock("../../src/utils/ZoweLogger"); -import { ProfilesUtils } from "../../src/utils/ProfilesUtils"; jest.mock("vscode"); jest.mock("fs"); @@ -150,7 +150,6 @@ async function createGlobalMocks() { "zowe.ds.refreshDataset", "zowe.ds.pattern", "zowe.ds.editSession", - "zowe.ds.ZoweNode.openPS", "zowe.ds.createDataset", "zowe.ds.createMember", "zowe.ds.deleteDataset", @@ -190,7 +189,6 @@ async function createGlobalMocks() { "zowe.uss.refreshDirectory", "zowe.uss.fullPath", "zowe.uss.editSession", - "zowe.uss.ZoweUSSNode.open", "zowe.uss.removeSession", "zowe.uss.createFile", "zowe.uss.createFolder", @@ -210,7 +208,6 @@ async function createGlobalMocks() { "zowe.uss.pasteUssFile", "zowe.uss.copyUssFile", "zowe.uss.openWithEncoding", - "zowe.jobs.zosJobsOpenspool", "zowe.jobs.deleteJob", "zowe.jobs.runModifyCommand", "zowe.jobs.runStopCommand", @@ -249,6 +246,8 @@ async function createGlobalMocks() { "zowe.editHistory", "zowe.promptCredentials", "zowe.profileManagement", + "zowe.diff.useLocalContent", + "zowe.diff.useRemoteContent", "zowe.openRecentMember", "zowe.searchInAllLoadedItems", "zowe.ds.deleteProfile", @@ -262,6 +261,7 @@ async function createGlobalMocks() { "zowe.compareWithSelected", "zowe.compareWithSelectedReadOnly", "zowe.compareFileStarted", + "zowe.placeholderCommand", "zowe.extRefresh", ], }; @@ -275,7 +275,6 @@ async function createGlobalMocks() { value: globalMocks.mockCreateTreeView, configurable: true, }); - Object.defineProperty(vscode, "Uri", { value: globalMocks.mockUri, configurable: true }); Object.defineProperty(vscode.commands, "registerCommand", { value: globalMocks.mockRegisterCommand, configurable: true, diff --git a/packages/zowe-explorer/__tests__/__unit__/job/ZosJobsProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/job/ZosJobsProvider.unit.test.ts index 0c58f50cc9..a314758f11 100644 --- a/packages/zowe-explorer/__tests__/__unit__/job/ZosJobsProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/job/ZosJobsProvider.unit.test.ts @@ -38,6 +38,7 @@ import { SettingsConfig } from "../../../src/utils/SettingsConfig"; import { mocked } from "../../../__mocks__/mockUtils"; import { ZoweLocalStorage } from "../../../src/utils/ZoweLocalStorage"; import { TreeProviders } from "../../../src/shared/TreeProviders"; +import { JobFSProvider } from "../../../src/job/JobFSProvider"; jest.mock("vscode"); const showMock = jest.fn(); @@ -127,7 +128,14 @@ async function createGlobalMocks() { WorkspaceFolder: 3, }; }), + FileSystemProvider: { + createDirectory: jest.fn(), + delete: jest.fn(), + }, }; + + jest.spyOn(JobFSProvider.instance, "createDirectory").mockImplementation(globalMocks.FileSystemProvider.createDirectory); + jest.spyOn(JobFSProvider.instance, "delete").mockImplementation(globalMocks.FileSystemProvider.delete); jest.spyOn(Gui, "createTreeView").mockImplementation(globalMocks.createTreeView); Object.defineProperty(ProfilesCache, "getConfigInstance", { value: jest.fn(() => { @@ -361,7 +369,6 @@ describe("ZosJobsProvider unit tests - Function initializeFavChildNodeForProfile job: new MockJobDetail("testJob(JOB123)"), }); node.contextValue = globals.JOBS_JOB_CONTEXT + globals.FAV_SUFFIX; - node.command = { command: "zowe.zosJobsSelectjob", title: "", arguments: [node] }; const targetIcon = getIconByNode(node); if (targetIcon) { node.iconPath = targetIcon.path; @@ -428,6 +435,7 @@ describe("ZosJobsProvider unit tests - Function loadProfilesForFavorites", () => label: "testProfile", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, parentNode: blockMocks.jobFavoritesNode, + profile: blockMocks.imperativeProfile, }); favProfileNode.contextValue = globals.FAV_PROFILE_CONTEXT; const testTree = new ZosJobsProvider(); @@ -568,18 +576,24 @@ describe("ZosJobsProvider unit tests - Function loadProfilesForFavorites", () => }); favProfileNode.contextValue = globals.FAV_PROFILE_CONTEXT; // Leave mParent parameter undefined for favJobNode and expectedFavPdsNode to test undefined profile/session condition - const favJobNode = new ZoweJobNode({ label: "JOBTEST(JOB1234)", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed }); - favJobNode.contextValue = globals.JOBS_JOB_CONTEXT + globals.FAV_SUFFIX; + const favJobNode = new ZoweJobNode({ + label: "JOBTEST(JOB1234)", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextOverride: globals.JOBS_JOB_CONTEXT + globals.FAV_SUFFIX, + parentNode: blockMocks.jobFavoritesNode, + profile: blockMocks.imperativeProfile, + }); const testTree = new ZosJobsProvider(); favProfileNode.children.push(favJobNode); testTree.mFavorites.push(favProfileNode); const expectedFavJobNode = new ZoweJobNode({ label: "JOBTEST(JOB1234)", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextOverride: globals.JOBS_JOB_CONTEXT + globals.FAV_SUFFIX, session: blockMocks.session, + parentNode: blockMocks.jobFavoritesNode, profile: blockMocks.imperativeProfile, }); - expectedFavJobNode.contextValue = globals.JOBS_JOB_CONTEXT + globals.FAV_SUFFIX; await testTree.loadProfilesForFavorites(blockMocks.log, favProfileNode); const resultFavJobNode = testTree.mFavorites[0].children[0]; diff --git a/packages/zowe-explorer/__tests__/__unit__/job/ZoweJobNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/job/ZoweJobNode.unit.test.ts index 794517018f..93b1e319de 100644 --- a/packages/zowe-explorer/__tests__/__unit__/job/ZoweJobNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/job/ZoweJobNode.unit.test.ts @@ -33,6 +33,7 @@ import * as contextually from "../../../src/shared/context"; import { ZoweLocalStorage } from "../../../src/utils/ZoweLocalStorage"; import { bindJesApi, createJesApi } from "../../../__mocks__/mockCreators/api"; import { TreeProviders } from "../../../src/shared/TreeProviders"; +import { JobFSProvider } from "../../../src/job/JobFSProvider"; async function createGlobalMocks() { const globalMocks = { @@ -90,8 +91,15 @@ async function createGlobalMocks() { mockProfileInfo: createInstanceOfProfileInfo(), mockProfilesCache: new ProfilesCache(imperative.Logger.getAppLogger()), mockTreeProviders: createTreeProviders(), + FileSystemProvider: { + createDirectory: jest.fn(), + writeFile: jest.fn(), + }, }; + jest.spyOn(JobFSProvider.instance, "createDirectory").mockImplementation(globalMocks.FileSystemProvider.createDirectory); + jest.spyOn(JobFSProvider.instance, "writeFile").mockImplementation(globalMocks.FileSystemProvider.writeFile); + Object.defineProperty(globalMocks.mockProfilesCache, "getProfileInfo", { value: jest.fn(() => { return { value: globalMocks.mockProfileInfo, configurable: true }; @@ -393,7 +401,13 @@ describe("ZoweJobNode unit tests - Function getChildren", () => { label: "Use the search button to display jobs", collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: globalMocks.testJobNode, + profile: globalMocks.testProfile, + contextOverride: globals.INFORMATION_CONTEXT, }); + expectedJob.command = { + command: "zowe.placeholderCommand", + title: "Placeholder", + }; globalMocks.testJobNode._owner = null; jest.spyOn(contextually, "isSession").mockReturnValueOnce(true); @@ -402,20 +416,81 @@ describe("ZoweJobNode unit tests - Function getChildren", () => { it("should return 'No jobs found' if no children is found", async () => { const globalMocks = await createGlobalMocks(); - const expectedJob = [ - new ZoweJobNode({ - label: "No jobs found", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: globalMocks.testJobsProvider.mSessionNodes[1], - }), - ]; - expectedJob[0].iconPath = null; - expectedJob[0].contextValue = "information"; + const job = new ZoweJobNode({ + label: "No jobs found", + collapsibleState: vscode.TreeItemCollapsibleState.None, + parentNode: globalMocks.testJobsProvider.mSessionNodes[1], + }); + job.iconPath = undefined; + job.contextValue = "information"; + job.command = { + title: "Placeholder", + command: "zowe.placeholderCommand", + }; await globalMocks.testJobsProvider.addSession("fake"); globalMocks.testJobsProvider.mSessionNodes[1].filtered = true; jest.spyOn(globalMocks.testJobsProvider.mSessionNodes[1], "getJobs").mockResolvedValue([]); const jobs = await globalMocks.testJobsProvider.mSessionNodes[1].getChildren(); - expect(jobs).toEqual(expectedJob); + expect(jobs[0]).toEqual(job); + }); + + it("To check smfid field in Jobs Tree View", async () => { + const globalMocks = await createGlobalMocks(); + + await globalMocks.testJobsProvider.addSession("fake"); + globalMocks.testJobsProvider.mSessionNodes[1].searchId = "JOB1234"; + globalMocks.testJobsProvider.mSessionNodes[1].dirty = true; + globalMocks.testJobsProvider.mSessionNodes[1].filtered = true; + globalMocks.testIJob.retcode = "ACTIVE"; + + const jobs = await globalMocks.testJobsProvider.mSessionNodes[1].getChildren(); + expect(jobs[0].label).toEqual("TESTJOB(JOB1234) - sampleMember - ACTIVE"); + }); + + it("smfid field is not in Jobs Tree View", async () => { + const globalMocks = await createGlobalMocks(); + + await globalMocks.testJobsProvider.addSession("fake"); + globalMocks.testJobsProvider.mSessionNodes[1].searchId = "JOB1234"; + globalMocks.testJobsProvider.mSessionNodes[1].dirty = true; + globalMocks.testJobsProvider.mSessionNodes[1].filtered = true; + globalMocks.testIJob.retcode = "ACTIVE"; + globalMocks.testIJob["exec-member"] = ""; + const jobs = await globalMocks.testJobsProvider.mSessionNodes[1].getChildren(); + expect(jobs[0].label).toEqual("TESTJOB(JOB1234) - ACTIVE"); + }); + + it("To check smfid field when return code is undefined", async () => { + const globalMocks = await createGlobalMocks(); + + await globalMocks.testJobsProvider.addSession("fake"); + globalMocks.testJobsProvider.mSessionNodes[1].searchId = "JOB1234"; + globalMocks.testJobsProvider.mSessionNodes[1].dirty = true; + globalMocks.testJobsProvider.mSessionNodes[1].filtered = true; + + const jobs = await globalMocks.testJobsProvider.mSessionNodes[1].getChildren(); + expect(jobs[0].label).toEqual("TESTJOB(JOB1234) - ACTIVE"); + }); + + it("To check Order of Spool files don't reverse when the job is Expanded and Collapsed", async () => { + const globalMocks = await createGlobalMocks(); + globalMocks.testJobsProvider.mSessionNodes[1]._owner = null; + globalMocks.testJobsProvider.mSessionNodes[1]._prefix = "*"; + globalMocks.testJobsProvider.mSessionNodes[1]._searchId = ""; + globalMocks.testJobNode.session.ISession = globalMocks.testSessionNoCred; + jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce({ + getSpoolFiles: jest.fn().mockReturnValueOnce([ + { ...globalMocks.mockIJobFile, stepname: "JES2", ddname: "JESMSGLG", "record-count": 11 }, + { ...globalMocks.mockIJobFile, stepname: "JES2", ddname: "JESJCL", "record-count": 21 }, + { ...globalMocks.mockIJobFile, stepname: "JES2", ddname: "JESYSMSG", "record-count": 6 }, + ]), + } as any); + jest.spyOn(contextually, "isSession").mockReturnValueOnce(false); + const spoolFiles = await globalMocks.testJobNode.getChildren(); + expect(spoolFiles.length).toBe(3); + expect(spoolFiles[0].label).toBe("JES2:JESMSGLG(101)"); + expect(spoolFiles[1].label).toBe("JES2:JESJCL(101)"); + expect(spoolFiles[2].label).toBe("JES2:JESYSMSG(101)"); }); it("To check smfid field in Jobs Tree View", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/job/actions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/job/actions.unit.test.ts index 16dd76b209..b34d2f39ff 100644 --- a/packages/zowe-explorer/__tests__/__unit__/job/actions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/job/actions.unit.test.ts @@ -38,17 +38,16 @@ import * as dsActions from "../../../src/dataset/actions"; import * as globals from "../../../src/globals"; import { createDatasetSessionNode, createDatasetTree } from "../../../__mocks__/mockCreators/datasets"; import { Profiles } from "../../../src/Profiles"; -import * as SpoolProvider from "../../../src/SpoolProvider"; +import * as SpoolProvider from "../../../src/SpoolUtils"; import * as refreshActions from "../../../src/shared/refresh"; import * as sharedUtils from "../../../src/shared/utils"; import { ZoweLogger } from "../../../src/utils/ZoweLogger"; -import { SpoolFile } from "../../../src/SpoolProvider"; import { ZosJobsProvider } from "../../../src/job/ZosJobsProvider"; import { ZoweLocalStorage } from "../../../src/utils/ZoweLocalStorage"; import { LocalFileManagement } from "../../../src/utils/LocalFileManagement"; import { ProfileManagement } from "../../../src/utils/ProfileManagement"; -import { TreeProviders } from "../../../src/shared/TreeProviders"; import { mocked } from "../../../__mocks__/mockUtils"; +import { JobFSProvider } from "../../../src/job/JobFSProvider"; const activeTextEditorDocument = jest.fn(); @@ -870,6 +869,7 @@ describe("Jobs Actions Unit Tests - Function submitMember", () => { createGlobalMocks(); const blockMocks = createBlockMocks(); + mocked(Profiles.getInstance).mockClear(); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); const dataset = new ZoweDatasetNode({ label: "TESTUSER.DATASET", @@ -927,145 +927,6 @@ describe("Jobs Actions Unit Tests - Function submitMember", () => { }); }); -describe("Jobs Actions Unit Tests - Function getSpoolContent", () => { - async function createBlockMocks() { - const session = createISessionWithoutCredentials(); - const iJob = createIJobObject(); - const iJobFile = createIJobFile(); - const imperativeProfile = createIProfile(); - const datasetSessionNode = createDatasetSessionNode(session, imperativeProfile); - const profileInstance = createInstanceOfProfile(imperativeProfile); - const treeView = createTreeView(); - const testJobTree = createJobsTree(session, iJob, imperativeProfile, treeView); - const jesApi = createJesApi(imperativeProfile); - jesApi.getSpoolFiles = jest.fn().mockReturnValue([ - { - stepName: undefined, - ddname: "test", - "record-count": "testJob", - procstep: "testJob", - }, - ]); - const mockCheckCurrentProfile = jest.fn(); - const mockUri: vscode.Uri = { - scheme: "testScheme", - authority: "testAuthority", - path: "testPath", - query: "testQuery", - fragment: "testFragment", - fsPath: "testFsPath", - with: jest.fn(), - toJSON: jest.fn(), - }; - bindJesApi(jesApi); - await TreeProviders.initializeProviders(null as any, { - ds: async (ctx) => null as any, - uss: async (ctx) => null as any, - job: async (ctx) => testJobTree, - }); - - return { - session, - iJob, - iJobFile, - imperativeProfile, - datasetSessionNode, - profileInstance, - jesApi, - testJobTree, - mockCheckCurrentProfile, - mockUri, - }; - } - - it("should call showTextDocument with encoded uri", async () => { - createGlobalMocks(); - const blockMocks = await createBlockMocks(); - const session = "sessionName"; - const spoolFile = blockMocks.iJobFile; - mocked(SpoolProvider.encodeJobFile).mockReturnValueOnce(blockMocks.mockUri); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - - await jobActions.getSpoolContent(session, { spool: spoolFile } as any); - - expect(mocked(Gui.showTextDocument)).toHaveBeenCalledWith(blockMocks.mockUri, { preview: false }); - }); - it("should call showTextDocument with encoded uri with unverified profile", async () => { - createGlobalMocks(); - const blockMocks = await createBlockMocks(); - const session = "sessionName"; - const spoolFile = blockMocks.iJobFile; - Object.defineProperty(Profiles, "getInstance", { - value: jest.fn(() => { - return { - checkCurrentProfile: blockMocks.mockCheckCurrentProfile.mockReturnValueOnce({ - name: blockMocks.imperativeProfile.name, - status: "unverified", - }), - validProfile: Validation.ValidationType.UNVERIFIED, - }; - }), - }); - mocked(SpoolProvider.encodeJobFile).mockReturnValueOnce(blockMocks.mockUri); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - - await jobActions.getSpoolContent(session, { spool: spoolFile } as any); - - expect(mocked(Gui.showTextDocument)).toHaveBeenCalledWith(blockMocks.mockUri, { preview: false }); - }); - it("should show error message for non existing profile", async () => { - createGlobalMocks(); - const blockMocks = await createBlockMocks(); - const session = "sessionName"; - const spoolFile = blockMocks.iJobFile; - const anyTimestamp = Date.now(); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - mocked(blockMocks.profileInstance.loadNamedProfile).mockImplementationOnce(() => { - throw new Error("Test"); - }); - - await jobActions.getSpoolContent(session, { spool: spoolFile } as any); - - expect(mocked(vscode.window.showTextDocument)).not.toHaveBeenCalled(); - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Error: Test"); - }); - it("should show an error message in case document cannot be shown for some reason", async () => { - createGlobalMocks(); - const blockMocks = await createBlockMocks(); - const session = "sessionName"; - const spoolFile = blockMocks.iJobFile; - const anyTimestamp = Date.now(); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - mocked(SpoolProvider.encodeJobFile).mockReturnValueOnce(blockMocks.mockUri); - mocked(vscode.window.showTextDocument).mockImplementationOnce(() => { - throw new Error("Test"); - }); - - await jobActions.getSpoolContent(session, { spool: spoolFile } as any); - - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Error: Test"); - }); - it("should fetch the spool content successfully", async () => { - createGlobalMocks(); - const blockMocks = await createBlockMocks(); - const testNode = new ZoweJobNode({ - label: "undefined:test - testJob", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: createJobFavoritesNode(), - session: createISessionWithoutCredentials(), - profile: createIProfile(), - }); - jest.spyOn(ZoweSpoolNode.prototype, "getProfile").mockReturnValue({ - name: "test", - } as any); - mocked(SpoolProvider.toUniqueJobFileUri).mockReturnValueOnce(() => blockMocks.mockUri); - mocked(vscode.window.showTextDocument).mockImplementationOnce(() => { - throw new Error("Test"); - }); - await expect(jobActions.getSpoolContentFromMainframe(testNode)).resolves.not.toThrow(); - }); -}); - describe("focusing on a job in the tree view", () => { it("should focus on the job in the existing tree view session", async () => { // arrange @@ -1455,16 +1316,10 @@ describe("Job Actions Unit Tests - Misc. functions", () => { query: '["some.profile",{"recfm":"UA","records-url":"https://some.url/","stepname":"STEP1","subsystem":"SUB1","job-correlator":"someid","byte-count":1298,"lrecl":133,"jobid":"JOB12345","ddname":"JESMSGLG","id":2,"record-count":19,"class":"A","jobname":"IEFBR14T","procstep":null}]', }, } as unknown as vscode.TextDocument; - const eventEmitter = { - fire: jest.fn(), - }; - // add a fake spool file to SpoolProvider - SpoolProvider.default.files[testDoc.uri.path] = new SpoolFile(testDoc.uri, eventEmitter as unknown as vscode.EventEmitter); - - const fetchContentSpy = jest.spyOn(SpoolFile.prototype, "fetchContent").mockImplementation(); + const fetchSpoolAtUriSpy = jest.spyOn(JobFSProvider.instance, "fetchSpoolAtUri").mockImplementation(); const statusMsgSpy = jest.spyOn(Gui, "setStatusBarMessage"); await jobActions.spoolFilePollEvent(testDoc); - expect(fetchContentSpy).toHaveBeenCalled(); + expect(fetchSpoolAtUriSpy).toHaveBeenCalled(); expect(statusMsgSpy).toHaveBeenCalledWith(`$(sync~spin) Polling: ${testDoc.fileName}...`); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/job/fs/JobFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/job/fs/JobFSProvider.unit.test.ts new file mode 100644 index 0000000000..b93d0ab3d6 --- /dev/null +++ b/packages/zowe-explorer/__tests__/__unit__/job/fs/JobFSProvider.unit.test.ts @@ -0,0 +1,352 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { Disposable, FilePermission, FileType, Uri } from "vscode"; +import { JobFSProvider } from "../../../../src/job/JobFSProvider"; +import { buildUniqueSpoolName, FilterEntry, Gui, JobEntry, SpoolEntry, ZoweScheme } from "@zowe/zowe-explorer-api"; +import { ZoweExplorerApiRegister } from "../../../../src/ZoweExplorerApiRegister"; +import { createIProfile } from "../../../../__mocks__/mockCreators/shared"; +import { createIJobFile, createIJobObject } from "../../../../__mocks__/mockCreators/jobs"; +import { MockedProperty } from "../../../../__mocks__/mockUtils"; + +const testProfile = createIProfile(); + +type TestUris = Record>; +const testUris: TestUris = { + spool: Uri.from({ scheme: ZoweScheme.Jobs, path: "/sestest/TESTJOB(JOB1234) - ACTIVE/JES2.JESMSGLG.2" }), + job: Uri.from({ scheme: ZoweScheme.Jobs, path: "/sestest/TESTJOB(JOB1234) - ACTIVE" }), + session: Uri.from({ scheme: ZoweScheme.Jobs, path: "/sestest" }), +}; + +const testEntries = { + job: { + ...new JobEntry("TESTJOB(JOB1234) - ACTIVE"), + job: createIJobObject(), + metadata: { + profile: testProfile, + path: "/TESTJOB(JOB1234) - ACTIVE", + }, + } as JobEntry, + spool: { + ...new SpoolEntry("JES2.JESMSGLG.2"), + data: new Uint8Array([1, 2, 3]), + metadata: { + profile: testProfile, + path: "/TESTJOB(JOB1234) - ACTIVE/JES2.JESMSGLG.2", + }, + spool: { + id: "SOMEID", + } as any, + } as SpoolEntry, + session: { + ...new FilterEntry("sestest"), + metadata: { + profile: testProfile, + path: "/", + }, + }, +}; + +describe("watch", () => { + it("returns an empty Disposable object", () => { + expect(JobFSProvider.instance.watch(testUris.job, { recursive: false, excludes: [] })).toStrictEqual(new Disposable(() => {})); + }); +}); +describe("stat", () => { + it("returns a spool entry as read-only", () => { + const fakeSpool = new SpoolEntry(testEntries.spool.name); + const lookupMock = jest.spyOn(JobFSProvider.instance as any, "_lookup").mockReturnValueOnce(fakeSpool); + expect(JobFSProvider.instance.stat(testUris.spool)).toStrictEqual({ + ...fakeSpool, + permissions: FilePermission.Readonly, + }); + lookupMock.mockRestore(); + }); + + it("returns a job entry", () => { + const lookupMock = jest.spyOn(JobFSProvider.instance as any, "_lookup").mockReturnValueOnce(testEntries.job); + expect(JobFSProvider.instance.stat(testUris.spool)).toStrictEqual({ + ...testEntries.job, + }); + lookupMock.mockRestore(); + }); +}); + +describe("refreshSpool", () => { + it("returns early if the node is not a spool file", async () => { + const statusBarMsgMock = jest.spyOn(Gui, "setStatusBarMessage").mockImplementation(); + statusBarMsgMock.mockReset(); + const node = { resourceUri: testUris.spool, contextValue: "job" } as any; + await JobFSProvider.refreshSpool(node); + expect(statusBarMsgMock).not.toHaveBeenCalledWith("$(sync~spin) Fetching spool file..."); + statusBarMsgMock.mockRestore(); + }); + + it("calls fetchSpoolAtUri for a valid spool node", async () => { + const fetchSpoolAtUriMock = jest.spyOn(JobFSProvider.instance, "fetchSpoolAtUri").mockImplementation(); + const disposeMock = jest.fn(); + const statusBarMsgMock = jest.spyOn(Gui, "setStatusBarMessage").mockReturnValue({ dispose: disposeMock }); + const node = { resourceUri: testUris.spool, contextValue: "spool" } as any; + await JobFSProvider.refreshSpool(node); + expect(statusBarMsgMock).toHaveBeenCalledWith("$(sync~spin) Fetching spool file..."); + expect(fetchSpoolAtUriMock).toHaveBeenCalledWith(node.resourceUri); + expect(disposeMock).toHaveBeenCalled(); + fetchSpoolAtUriMock.mockRestore(); + statusBarMsgMock.mockRestore(); + }); +}); + +describe("readDirectory", () => { + it("throws an error if getJobsByParameters does not exist", async () => { + const mockJesApi = {}; + const jesApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockJesApi as any); + const lookupAsDirMock = jest.spyOn(JobFSProvider.instance as any, "_lookupAsDirectory").mockReturnValueOnce({ + ...testEntries.session, + filter: { ...testEntries.session.filter, owner: "USER", prefix: "JOB*", status: "*" }, + entries: new Map(), + } as any); + await expect(JobFSProvider.instance.readDirectory(testUris.session)).rejects.toThrow( + "Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API." + ); + expect(lookupAsDirMock).toHaveBeenCalledWith(testUris.session, false); + jesApiMock.mockRestore(); + }); + + it("calls getJobsByParameters to list jobs under a session", async () => { + const fakeJob2 = { ...createIJobObject(), jobid: "JOB3456" }; + const mockJesApi = { + getJobsByParameters: jest.fn().mockResolvedValueOnce([createIJobObject(), fakeJob2]), + }; + const jesApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockJesApi as any); + const lookupAsDirMock = jest.spyOn(JobFSProvider.instance as any, "_lookupAsDirectory").mockReturnValueOnce({ + ...testEntries.session, + filter: { ...testEntries.session.filter, owner: "USER", prefix: "JOB*", status: "*" }, + entries: new Map(), + } as any); + expect(await JobFSProvider.instance.readDirectory(testUris.session)).toStrictEqual([ + ["JOB1234", FileType.Directory], + ["JOB3456", FileType.Directory], + ]); + expect(lookupAsDirMock).toHaveBeenCalledWith(testUris.session, false); + expect(mockJesApi.getJobsByParameters).toHaveBeenCalledWith({ + owner: "USER", + prefix: "JOB*", + status: "*", + }); + jesApiMock.mockRestore(); + }); + + it("calls getSpoolFiles to list spool files under a job", async () => { + const fakeSpool = createIJobFile(); + const fakeSpool2 = { ...createIJobFile(), id: 102 }; + const mockJesApi = { + getSpoolFiles: jest.fn().mockResolvedValueOnce([fakeSpool, fakeSpool2]), + }; + const jesApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockJesApi as any); + const fakeJob = new JobEntry(testEntries.job.name); + fakeJob.job = testEntries.job.job; + const lookupAsDirMock = jest.spyOn(JobFSProvider.instance as any, "_lookupAsDirectory").mockReturnValueOnce(fakeJob); + expect(await JobFSProvider.instance.readDirectory(testUris.job)).toStrictEqual([ + [buildUniqueSpoolName(fakeSpool), FileType.File], + [buildUniqueSpoolName(fakeSpool2), FileType.File], + ]); + expect(lookupAsDirMock).toHaveBeenCalledWith(testUris.job, false); + expect(mockJesApi.getSpoolFiles).toHaveBeenCalledWith(testEntries.job.job?.jobname, testEntries.job.job?.jobid); + jesApiMock.mockRestore(); + }); +}); + +describe("updateFilterForUri", () => { + it("updates the session entry with the given filter", () => { + const sessionEntry = { ...testEntries.session }; + const newFilter = { + owner: "TESTUSER", + prefix: "JOB*", + searchId: "", + status: "ACTIVE", + }; + const lookupAsDirMock = jest.spyOn(JobFSProvider.instance as any, "_lookupAsDirectory").mockReturnValueOnce(sessionEntry); + JobFSProvider.instance.updateFilterForUri(testUris.session, newFilter); + expect(sessionEntry.filter).toStrictEqual(newFilter); + lookupAsDirMock.mockRestore(); + }); +}); + +describe("createDirectory", () => { + it("creates a directory for a session entry", () => { + const fakeRoot = { ...(JobFSProvider.instance as any).root }; + const rootMock = new MockedProperty(JobFSProvider.instance, "root", undefined, fakeRoot); + JobFSProvider.instance.createDirectory(testUris.session); + expect(fakeRoot.entries.has("sestest")).toBe(true); + rootMock[Symbol.dispose](); + }); + + it("creates a directory for a job entry", () => { + const fakeSessionEntry = new FilterEntry("sestest"); + fakeSessionEntry.metadata = testEntries.session.metadata; + jest.spyOn(JobFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(fakeSessionEntry); + JobFSProvider.instance.createDirectory(testUris.job); + expect(fakeSessionEntry.entries.has("TESTJOB(JOB1234) - ACTIVE")).toBe(true); + }); +}); + +describe("fetchSpoolAtUri", () => { + it("fetches the spool contents for a given URI", async () => { + const lookupAsFileMock = jest + .spyOn(JobFSProvider.instance as any, "_lookupAsFile") + .mockReturnValueOnce({ ...testEntries.spool, data: new Uint8Array() }); + const newData = "spool contents"; + const mockJesApi = { + downloadSingleSpool: jest.fn((opts) => { + opts.stream.write(newData); + }), + }; + const jesApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockJesApi as any); + const entry = await JobFSProvider.instance.fetchSpoolAtUri(testUris.spool); + expect(mockJesApi.downloadSingleSpool).toHaveBeenCalled(); + expect(entry.data.toString()).toStrictEqual(newData.toString()); + jesApiMock.mockRestore(); + lookupAsFileMock.mockRestore(); + }); +}); + +describe("readFile", () => { + it("returns data for the spool file", async () => { + const spoolEntry = { ...testEntries.spool }; + const lookupAsFileMock = jest.spyOn(JobFSProvider.instance as any, "_lookupAsFile").mockReturnValueOnce(spoolEntry); + const fetchSpoolAtUriMock = jest.spyOn(JobFSProvider.instance, "fetchSpoolAtUri").mockResolvedValueOnce(spoolEntry); + expect(await JobFSProvider.instance.readFile(testUris.spool)).toBe(spoolEntry.data); + expect(spoolEntry.wasAccessed).toBe(true); + lookupAsFileMock.mockRestore(); + fetchSpoolAtUriMock.mockRestore(); + }); +}); + +describe("writeFile", () => { + it("adds a spool entry to the FSP", () => { + const jobEntry = { + ...testEntries.job, + entries: new Map(), + metadata: testEntries.job.metadata, + }; + const lookupParentDirMock = jest.spyOn(JobFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(jobEntry); + const newContents = new Uint8Array([3, 6, 9]); + JobFSProvider.instance.writeFile(testUris.spool, newContents, { create: true, overwrite: false }); + + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.spool); + const spoolEntry = jobEntry.entries.get("JES2.JESMSGLG.2")!; + expect(spoolEntry.data).toBe(newContents); + }); + + it("updates a spool entry in the FSP", () => { + const jobEntry = { + ...testEntries.job, + entries: new Map([[testEntries.spool.name, { ...testEntries.spool }]]), + }; + const lookupParentDirMock = jest.spyOn(JobFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(jobEntry); + const newContents = new Uint8Array([3, 6, 9]); + JobFSProvider.instance.writeFile(testUris.spool, newContents, { create: false, overwrite: true }); + + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.spool); + const spoolEntry = jobEntry.entries.get("JES2.JESMSGLG.2")!; + expect(spoolEntry.data).toBe(newContents); + }); + + it("updates an empty, unaccessed spool entry in the FSP without sending data", () => { + const jobEntry = { + ...testEntries.job, + entries: new Map([[testEntries.spool.name, { ...testEntries.spool, wasAccessed: false }]]), + }; + const lookupParentDirMock = jest.spyOn(JobFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(jobEntry); + const newContents = new Uint8Array([]); + JobFSProvider.instance.writeFile(testUris.spool, newContents, { create: false, overwrite: true }); + + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.spool); + const spoolEntry = jobEntry.entries.get("JES2.JESMSGLG.2")!; + expect(spoolEntry.data.length).toBe(0); + }); + + it("throws an error if entry doesn't exist and 'create' option is false", () => { + expect(() => JobFSProvider.instance.writeFile(testUris.spool, new Uint8Array([]), { create: false, overwrite: true })).toThrow( + "file not found" + ); + }); + + it("throws an error if the entry exists, 'create' opt is true and 'overwrite' opt is false", () => { + const jobEntry = { + ...testEntries.job, + entries: new Map([[testEntries.spool.name, { ...testEntries.spool, wasAccessed: false }]]), + }; + const lookupParentDirMock = jest.spyOn(JobFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(jobEntry); + expect(() => JobFSProvider.instance.writeFile(testUris.spool, new Uint8Array([]), { create: true, overwrite: false })).toThrow("file exists"); + lookupParentDirMock.mockRestore(); + }); +}); + +describe("delete", () => { + it("deletes a job from the FSP and remote file system", async () => { + const mockUssApi = { + deleteJob: jest.fn(), + }; + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockUssApi as any); + const fakeJob = new JobEntry(testEntries.job.name); + fakeJob.job = testEntries.job.job; + const lookupMock = jest.spyOn(JobFSProvider.instance as any, "_lookup").mockReturnValueOnce(fakeJob); + const lookupParentDirMock = jest + .spyOn(JobFSProvider.instance as any, "_lookupParentDirectory") + .mockReturnValueOnce({ ...testEntries.session }); + await JobFSProvider.instance.delete(testUris.job, { recursive: true, deleteRemote: true }); + const jobInfo = testEntries.job.job; + expect(jobInfo).not.toBeUndefined(); + expect(mockUssApi.deleteJob).toHaveBeenCalledWith(jobInfo?.jobname || "TESTJOB", jobInfo?.jobid || "JOB12345"); + ussApiMock.mockRestore(); + lookupMock.mockRestore(); + lookupParentDirMock.mockRestore(); + }); + + it("does not delete a spool from the FSP and remote file system", async () => { + const mockUssApi = { + deleteJob: jest.fn(), + }; + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockUssApi as any); + const fakeSpool = new SpoolEntry(testEntries.spool.name); + fakeSpool.spool = testEntries.spool.spool; + const fakeJob = new JobEntry(testEntries.job.name); + fakeJob.job = testEntries.job.job; + const lookupMock = jest.spyOn(JobFSProvider.instance as any, "_lookup").mockReturnValueOnce(fakeSpool); + const lookupParentDirMock = jest.spyOn(JobFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(fakeJob); + await JobFSProvider.instance.delete(testUris.spool, { recursive: true, deleteRemote: true }); + expect(mockUssApi.deleteJob).not.toHaveBeenCalled(); + expect(lookupParentDirMock).not.toHaveBeenCalled(); + ussApiMock.mockRestore(); + lookupMock.mockRestore(); + lookupParentDirMock.mockRestore(); + }); +}); + +describe("rename", () => { + it("throws an error as renaming is not supported for jobs", async () => { + await expect(async () => + JobFSProvider.instance.rename(testUris.job, testUris.job.with({ path: "/sestest/TESTJOB(JOB54321) - ACTIVE" }), { + overwrite: true, + }) + ).rejects.toThrow("Renaming is not supported for jobs."); + }); +}); + +describe("_getInfoFromUri", () => { + it("removes session segment from path", () => { + expect((JobFSProvider.instance as any)._getInfoFromUri(testUris.job)).toStrictEqual({ + profile: null, + path: "/TESTJOB(JOB1234) - ACTIVE", + }); + }); +}); diff --git a/packages/zowe-explorer/__tests__/__unit__/job/init.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/job/init.unit.test.ts index 5f962e62f7..a6c5e576c7 100644 --- a/packages/zowe-explorer/__tests__/__unit__/job/init.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/job/init.unit.test.ts @@ -17,7 +17,7 @@ import * as jobActions from "../../../src/job/actions"; import * as sharedExtension from "../../../src/shared/init"; import { initJobsProvider } from "../../../src/job/init"; import { Profiles } from "../../../src/Profiles"; -import { IJestIt, ITestContext, processSubscriptions, spyOnSubscriptions } from "../../__common__/testUtils"; +import { IJestIt, ITestContext, processSubscriptions } from "../../__common__/testUtils"; import { ZoweLogger } from "../../../src/utils/ZoweLogger"; import { ZoweLocalStorage } from "../../../src/utils/ZoweLocalStorage"; import { createISession, createIProfile } from "../../../__mocks__/mockCreators/shared"; @@ -59,11 +59,6 @@ describe("Test src/jobs/extension", () => { filterJobsDialog: jest.fn(), }; const commands: IJestIt[] = [ - { - name: "zowe.jobs.zosJobsOpenspool", - parm: [test._, test.value], - mock: [{ spy: jest.spyOn(jobActions, "getSpoolContent"), arg: [test._, test.value] }], - }, { name: "zowe.jobs.deleteJob", parm: [test.value, test._], @@ -90,17 +85,6 @@ describe("Test src/jobs/extension", () => { parm: [{ mParent: test.value }], mock: [{ spy: jest.spyOn(jobActions, "refreshJob"), arg: [test.value, jobsProvider] }], }, - { - name: "zowe.jobs.refreshSpool", - parm: [{ mParent: { mParent: test.value } }], - mock: [ - { - spy: jest.spyOn(jobActions, "getSpoolContentFromMainframe"), - arg: [{ mParent: { mParent: test.value } }], - }, - { spy: jest.spyOn(jobActions, "refreshJob"), arg: [test.value, jobsProvider] }, - ], - }, { name: "zowe.jobs.downloadSingleSpool", mock: [{ spy: jest.spyOn(jobActions, "downloadSingleSpool"), arg: [[test.value], false] }], @@ -193,7 +177,7 @@ describe("Test src/jobs/extension", () => { { spy: jest.spyOn(Profiles, "getInstance"), arg: [], - ret: { enableValidation: jest.fn() }, + ret: { enableValidation: jest.fn(), disableValidation: jest.fn() }, }, ], }, @@ -251,7 +235,6 @@ describe("Test src/jobs/extension", () => { }); spyCreateJobsTree.mockResolvedValue(jobsProvider as any); - spyOnSubscriptions(commands); jest.spyOn(vscode.workspace, "onDidCloseTextDocument").mockImplementation(jobsProvider.onDidCloseTextDocument); await initJobsProvider(test.context); }); @@ -266,7 +249,7 @@ describe("Test src/jobs/extension", () => { it("should not initialize if it is unable to create the jobs tree", async () => { spyCreateJobsTree.mockResolvedValue(null); - const myProvider = await initJobsProvider({} as any); + const myProvider = await initJobsProvider(test.context); expect(myProvider).toBe(null); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/shared/HistoryView.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/shared/HistoryView.unit.test.ts index 06c4509a84..839dfd60e0 100644 --- a/packages/zowe-explorer/__tests__/__unit__/shared/HistoryView.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/shared/HistoryView.unit.test.ts @@ -24,6 +24,7 @@ import { createIJobObject, createJobsTree } from "../../../__mocks__/mockCreator import { Gui } from "@zowe/zowe-explorer-api"; import { Profiles } from "../../../src/Profiles"; import { ZoweLocalStorage } from "../../../src/utils/ZoweLocalStorage"; +import { UssFSProvider } from "../../../src/uss/UssFSProvider"; async function initializeHistoryViewMock(blockMocks: any, globalMocks: any): Promise { return new HistoryView( @@ -44,7 +45,12 @@ function createGlobalMocks(): any { testSession: createISession(), treeView: createTreeView(), imperativeProfile: createIProfile(), + FileSystemProvider: { + createDirectory: jest.fn(), + }, }; + + jest.spyOn(UssFSProvider.instance, "createDirectory").mockImplementation(globalMocks.FileSystemProvider.createDirectory); Object.defineProperty(Gui, "showMessage", { value: jest.fn(), configurable: true }); Object.defineProperty(Gui, "resolveQuickPick", { value: jest.fn(), configurable: true }); Object.defineProperty(Gui, "createQuickPick", { value: jest.fn(), configurable: true }); diff --git a/packages/zowe-explorer/__tests__/__unit__/shared/actions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/shared/actions.unit.test.ts index 5ee707193f..fa97a7c020 100644 --- a/packages/zowe-explorer/__tests__/__unit__/shared/actions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/shared/actions.unit.test.ts @@ -32,6 +32,7 @@ import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; import { ZoweLogger } from "../../../src/utils/ZoweLogger"; import { ZoweLocalStorage } from "../../../src/utils/ZoweLocalStorage"; import { mocked } from "../../../__mocks__/mockUtils"; +import { UssFSProvider } from "../../../src/uss/UssFSProvider"; async function createGlobalMocks() { const globalMocks = { @@ -46,8 +47,13 @@ async function createGlobalMocks() { }; }), qpPlaceholder: 'Choose "Create new..." to define a new profile or select an existing profile to add to the Data Set Explorer', + FileSystemProvider: { + createDirectory: jest.fn(), + }, }; + jest.spyOn(UssFSProvider.instance, "createDirectory").mockImplementation(globalMocks.FileSystemProvider.createDirectory); + Object.defineProperty(vscode.window, "withProgress", { value: jest.fn().mockImplementation((progLocation, callback) => { const progress = { @@ -81,7 +87,6 @@ async function createGlobalMocks() { Object.defineProperty(Gui, "showMessage", { value: jest.fn(), configurable: true }); Object.defineProperty(Gui, "resolveQuickPick", { value: jest.fn(), configurable: true }); Object.defineProperty(Gui, "createQuickPick", { value: jest.fn(), configurable: true }); - Object.defineProperty(vscode.commands, "executeCommand", { value: globalMocks.withProgress, configurable: true }); Object.defineProperty(vscode, "ProgressLocation", { value: globalMocks.ProgressLocation, configurable: true }); Object.defineProperty(Profiles, "getInstance", { value: jest.fn().mockReturnValue(createInstanceOfProfile(globalMocks.imperativeProfile)), @@ -141,6 +146,11 @@ describe("Shared Actions Unit Tests - Function searchInAllLoadedItems", () => { parentNode: blockMocks.datasetSessionNode, session: globalMocks.session, }); + testNode.command = { + command: "vscode.open", + title: "", + arguments: [testNode.resourceUri], + }; const testDatasetTree = createDatasetTree(blockMocks.datasetSessionNode, globalMocks.treeView); jest.spyOn(ZoweDatasetNode.prototype, "openDs").mockResolvedValueOnce(undefined); @@ -217,11 +227,15 @@ describe("Shared Actions Unit Tests - Function searchInAllLoadedItems", () => { parentNode: testNode, session: globalMocks.session, }); + testMember.command = { + command: "vscode.open", + title: "", + arguments: [testMember.resourceUri], + }; testNode.children.push(testMember); const testDatasetTree = createDatasetTree(blockMocks.datasetSessionNode, globalMocks.treeView); testDatasetTree.getChildren.mockReturnValue([blockMocks.datasetSessionNode]); - jest.spyOn(ZoweDatasetNode.prototype, "openDs").mockResolvedValueOnce(undefined); testDatasetTree.getAllLoadedItems.mockResolvedValueOnce([testMember]); const testUssTree = createUSSTree([], [blockMocks.ussSessionNode], globalMocks.treeView); Object.defineProperty(testUssTree, "getAllLoadedItems", { diff --git a/packages/zowe-explorer/__tests__/__unit__/shared/init.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/shared/init.unit.test.ts index b2cdc1a3a7..6def24920e 100644 --- a/packages/zowe-explorer/__tests__/__unit__/shared/init.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/shared/init.unit.test.ts @@ -18,14 +18,11 @@ import { Profiles } from "../../../src/Profiles"; import * as profUtils from "../../../src/utils/ProfilesUtils"; import * as tempFolder from "../../../src/utils/TempFolder"; import * as core from "@zowe/core-for-zowe-sdk"; -import { IJestIt, ITestContext, processSubscriptions, spyOnSubscriptions } from "../../__common__/testUtils"; +import { IJestIt, ITestContext, processSubscriptions } from "../../__common__/testUtils"; import { TsoCommandHandler } from "../../../src/command/TsoCommandHandler"; import { MvsCommandHandler } from "../../../src/command/MvsCommandHandler"; import { UnixCommandHandler } from "../../../src/command/UnixCommandHandler"; -import { saveFile } from "../../../src/dataset/actions"; -import { saveUSSFile } from "../../../src/uss/actions"; import { ZoweLogger } from "../../../src/utils/ZoweLogger"; -import { ZoweSaveQueue } from "../../../src/abstract/ZoweSaveQueue"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; import * as HistoryView from "../../../src/shared/HistoryView"; import { LocalFileManagement } from "../../../src/utils/LocalFileManagement"; @@ -53,6 +50,7 @@ describe("Test src/shared/extension", () => { }, _: { _: "_" }, }; + const profileMocks = { deleteProfile: jest.fn(), refresh: jest.fn() }; const commands: IJestIt[] = [ { name: "zowe.updateSecureCredentials", @@ -101,7 +99,7 @@ describe("Test src/shared/extension", () => { { spy: jest.spyOn(test.value, "affectsConfiguration"), arg: [globals.SETTINGS_LOGS_FOLDER_PATH], ret: false }, { spy: jest.spyOn(test.value, "affectsConfiguration"), arg: [globals.SETTINGS_TEMP_FOLDER_PATH], ret: false }, { spy: jest.spyOn(test.value, "affectsConfiguration"), arg: [globals.SETTINGS_AUTOMATIC_PROFILE_VALIDATION], ret: true }, - { spy: jest.spyOn(Profiles, "getInstance"), arg: [], ret: { refresh: jest.fn() } }, + { spy: jest.spyOn(Profiles, "getInstance"), arg: [], ret: profileMocks }, { spy: jest.spyOn(refreshActions, "refreshAll"), arg: ["ds"] }, { spy: jest.spyOn(refreshActions, "refreshAll"), arg: ["uss"] }, { spy: jest.spyOn(refreshActions, "refreshAll"), arg: ["job"] }, @@ -147,56 +145,24 @@ describe("Test src/shared/extension", () => { }, { name: "onDidSaveTextDocument:2", - parm: [{ fileName: "DS_DIR", isDirty: true, uri: vscode.Uri.parse("") }], - mock: [ - { - spy: jest.spyOn(ZoweSaveQueue, "push"), - arg: [ - { - fileProvider: "ds", - savedFile: { fileName: "DS_DIR", uri: vscode.Uri.parse(""), isDirty: true }, - uploadRequest: saveFile, - }, - ], - }, - ], - }, - { - name: "onDidSaveTextDocument:3", - parm: [{ fileName: "USS_DIR", isDirty: true, uri: vscode.Uri.parse("") }], - mock: [ - { - spy: jest.spyOn(ZoweSaveQueue, "push"), - arg: [ - { - fileProvider: "uss", - savedFile: { fileName: "USS_DIR", isDirty: true, uri: vscode.Uri.parse("") }, - uploadRequest: saveUSSFile, - }, - ], - }, - ], - }, - { - name: "onDidSaveTextDocument:4", parm: [{ isDirty: true, fileName: "NOT_DATASET" }], mock: [], }, { name: "zowe.ds.deleteProfile", - mock: [{ spy: jest.spyOn(Profiles, "getInstance"), arg: [], ret: { deleteProfile: jest.fn() } }], + mock: [{ spy: jest.spyOn(Profiles, "getInstance"), arg: [], ret: profileMocks }], }, { name: "zowe.cmd.deleteProfile", - mock: [{ spy: jest.spyOn(Profiles, "getInstance"), arg: [], ret: { deleteProfile: jest.fn() } }], + mock: [{ spy: jest.spyOn(Profiles, "getInstance"), arg: [], ret: profileMocks }], }, { name: "zowe.uss.deleteProfile", - mock: [{ spy: jest.spyOn(Profiles, "getInstance"), arg: [], ret: { deleteProfile: jest.fn() } }], + mock: [{ spy: jest.spyOn(Profiles, "getInstance"), arg: [], ret: profileMocks }], }, { name: "zowe.jobs.deleteProfile", - mock: [{ spy: jest.spyOn(Profiles, "getInstance"), arg: [], ret: { deleteProfile: jest.fn() } }], + mock: [{ spy: jest.spyOn(Profiles, "getInstance"), arg: [], ret: profileMocks }], }, { name: "zowe.issueTsoCmd:1", @@ -261,7 +227,6 @@ describe("Test src/shared/extension", () => { Object.defineProperty(globals, "USS_DIR", { value: testGlobals.USS_DIR }); Object.defineProperty(globals, "SETTINGS_TEMP_FOLDER_LOCATION", { value: "/some/old/temp/location" }); Object.defineProperty(vscode.workspace, "onDidSaveTextDocument", { value: onDidSaveTextDocument }); - spyOnSubscriptions(commands); await sharedExtension.registerCommonCommands(test.context, test.value.providers); }); afterAll(() => { diff --git a/packages/zowe-explorer/__tests__/__unit__/shared/utils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/shared/utils.unit.test.ts index 85cfe94c4d..1fd11b3cfd 100644 --- a/packages/zowe-explorer/__tests__/__unit__/shared/utils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/shared/utils.unit.test.ts @@ -13,22 +13,14 @@ import * as sharedUtils from "../../../src/shared/utils"; import * as globals from "../../../src/globals"; import { ZoweDatasetNode } from "../../../src/dataset/ZoweDatasetNode"; import * as vscode from "vscode"; -import * as path from "path"; -import { - createIProfile, - createISessionWithoutCredentials, - createISession, - createFileResponse, - createInstanceOfProfile, - createTextDocument, -} from "../../../__mocks__/mockCreators/shared"; +import { createIProfile, createISession, createInstanceOfProfile } from "../../../__mocks__/mockCreators/shared"; import { createDatasetSessionNode } from "../../../__mocks__/mockCreators/datasets"; import { ZoweUSSNode } from "../../../src/uss/ZoweUSSNode"; import { ZoweJobNode } from "../../../src/job/ZoweJobNode"; -import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; import * as utils from "../../../src/utils/ProfilesUtils"; -import { Gui, imperative, IZoweTreeNode, ProfilesCache, ZosEncoding } from "@zowe/zowe-explorer-api"; +import { BaseProvider, Gui, imperative, IZoweTreeNode, ProfilesCache, ZosEncoding } from "@zowe/zowe-explorer-api"; import { ZoweLocalStorage } from "../../../src/utils/ZoweLocalStorage"; +import { UssFSProvider } from "../../../src/uss/UssFSProvider"; async function createGlobalMocks() { const newMocks = { @@ -37,7 +29,9 @@ async function createGlobalMocks() { mockGetInstance: jest.fn(), mockProfileInstance: null, mockProfilesCache: null, + createDirectory: jest.fn(), }; + jest.spyOn(UssFSProvider.instance, "createDirectory").mockImplementation(newMocks.createDirectory); newMocks.mockProfilesCache = new ProfilesCache(imperative.Logger.getAppLogger()); newMocks.mockProfileInstance = createInstanceOfProfile(createIProfile()); Object.defineProperty(globals, "PROFILES_CACHE", { @@ -193,161 +187,6 @@ describe("Negative testing for ZoweJobTreeNode", () => { }); }); -describe("Test uploadContents", () => { - it("should test with uss node that new API method is called if it exists", async () => { - const putContent = jest.fn(); - ZoweExplorerApiRegister.getUssApi = jest.fn>( - (profile: imperative.IProfileLoaded) => { - return { - putContent, - }; - } - ); - - await sharedUtils.uploadContent( - new ZoweUSSNode({ label: "", collapsibleState: vscode.TreeItemCollapsibleState.None }), - { - fileName: "whatever", - } as any, - null, - { - profile: { - encoding: 123, - }, - } as any - ); - expect(ZoweExplorerApiRegister.getUssApi(null).putContent).toHaveBeenCalled(); - }); -}); - -describe("Test force upload", () => { - async function createBlockMocks() { - const newVariables = { - dsNode: new ZoweDatasetNode({ label: "", collapsibleState: vscode.TreeItemCollapsibleState.None }), - ussNode: new ZoweUSSNode({ label: "", collapsibleState: vscode.TreeItemCollapsibleState.None }), - showInformationMessage: jest.fn(), - showWarningMessage: jest.fn(), - showErrorMessage: jest.fn(), - getMvsApi: jest.fn(), - getUssApi: jest.fn(), - withProgress: jest.fn(), - fileResponse: createFileResponse([{ etag: null }]), - ProgressLocation: jest.fn().mockImplementation(() => { - return { - Notification: 15, - }; - }), - mockDoc: createTextDocument("mocDoc"), - }; - - Object.defineProperty(vscode.window, "showInformationMessage", { - value: newVariables.showInformationMessage, - configurable: true, - }); - Object.defineProperty(vscode.window, "showWarningMessage", { - value: newVariables.showWarningMessage, - configurable: true, - }); - Object.defineProperty(vscode.window, "showErrorMessage", { - value: newVariables.showErrorMessage, - configurable: true, - }); - Object.defineProperty(ZoweExplorerApiRegister, "getMvsApi", { - value: newVariables.getMvsApi, - configurable: true, - }); - Object.defineProperty(ZoweExplorerApiRegister, "getUssApi", { - value: newVariables.getUssApi, - configurable: true, - }); - Object.defineProperty(vscode.window, "withProgress", { value: newVariables.withProgress, configurable: true }); - Object.defineProperty(vscode.window, "activeTextEditor", { value: { edit: jest.fn() }, configurable: true }); - Object.defineProperty(vscode, "Position", { - value: jest.fn(() => { - return {}; - }), - configurable: true, - }); - Object.defineProperty(vscode, "Range", { - value: jest.fn(() => { - return {}; - }), - configurable: true, - }); - Object.defineProperty(vscode, "ProgressLocation", { value: newVariables.ProgressLocation, configurable: true }); - - return newVariables; - } - - it("should successfully call upload for a USS file if user clicks 'Yes'", async () => { - const blockMocks = await createBlockMocks(); - blockMocks.showInformationMessage.mockResolvedValueOnce("Yes"); - blockMocks.withProgress.mockResolvedValueOnce(blockMocks.fileResponse); - await sharedUtils.willForceUpload(blockMocks.ussNode, blockMocks.mockDoc, null); - expect(blockMocks.withProgress).toHaveBeenCalledWith( - { - location: vscode.ProgressLocation.Notification, - title: "Saving file...", - }, - expect.any(Function) - ); - expect(blockMocks.showInformationMessage.mock.calls[1][0]).toBe(blockMocks.fileResponse.commandResponse); - }); - - it("should successfully call upload for a data set if user clicks 'Yes'", async () => { - const blockMocks = await createBlockMocks(); - blockMocks.showInformationMessage.mockResolvedValueOnce("Yes"); - blockMocks.withProgress.mockResolvedValueOnce(blockMocks.fileResponse); - await sharedUtils.willForceUpload(blockMocks.dsNode, blockMocks.mockDoc, null); - expect(blockMocks.withProgress).toHaveBeenCalledWith( - { - location: vscode.ProgressLocation.Notification, - title: "Saving Data Set...", - }, - expect.any(Function) - ); - expect(blockMocks.showInformationMessage.mock.calls[1][0]).toBe(blockMocks.fileResponse.commandResponse); - }); - - it("should cancel upload if user clicks 'No'", async () => { - const blockMocks = await createBlockMocks(); - blockMocks.showInformationMessage.mockResolvedValueOnce("No"); - await sharedUtils.willForceUpload(blockMocks.dsNode, blockMocks.mockDoc, null); - expect(blockMocks.showInformationMessage.mock.calls[1][0]).toBe("Upload cancelled."); - }); - - it("should show error message if file fails to upload", async () => { - const blockMocks = await createBlockMocks(); - blockMocks.showInformationMessage.mockResolvedValueOnce("Yes"); - blockMocks.withProgress.mockResolvedValueOnce({ ...blockMocks.fileResponse, success: false }); - await sharedUtils.willForceUpload(blockMocks.ussNode, blockMocks.mockDoc, null); - expect(blockMocks.withProgress).toHaveBeenCalledWith( - { - location: vscode.ProgressLocation.Notification, - title: "Saving file...", - }, - expect.any(Function) - ); - expect(blockMocks.showErrorMessage.mock.calls[0][0]).toBe(blockMocks.fileResponse.commandResponse); - }); - - it("should show error message if upload throws an error", async () => { - const blockMocks = await createBlockMocks(); - blockMocks.showInformationMessage.mockResolvedValueOnce("Yes"); - const testError = new Error("Task failed successfully"); - blockMocks.withProgress.mockRejectedValueOnce(testError); - await sharedUtils.willForceUpload(blockMocks.ussNode, blockMocks.mockDoc, null, { name: "fakeProfile" } as any); - expect(blockMocks.withProgress).toHaveBeenCalledWith( - { - location: vscode.ProgressLocation.Notification, - title: "Saving file...", - }, - expect.any(Function) - ); - expect(blockMocks.showErrorMessage.mock.calls[0][0]).toBe(`Error: ${testError.message}`); - }); -}); - describe("Shared Utils Unit Tests - Function filterTreeByString", () => { it("Testing that filterTreeByString returns the correct array", async () => { const qpItems = [ @@ -371,188 +210,6 @@ describe("Shared Utils Unit Tests - Function filterTreeByString", () => { }); }); -describe("Shared Utils Unit Tests - Function getDocumentFilePath", () => { - let blockMocks; - function createBlockMocks() { - const session = createISessionWithoutCredentials(); - const imperativeProfile = createIProfile(); - const datasetSessionNode = createDatasetSessionNode(session, imperativeProfile); - - return { - session, - imperativeProfile, - datasetSessionNode, - }; - } - - it("Testing that the add Suffix for datasets works", async () => { - blockMocks = createBlockMocks(); - globals.defineGlobals("/test/path/"); - - let node = new ZoweDatasetNode({ - label: "AUSER.TEST.JCL(member)", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.TEST.JCL(member).jcl") - ); - node = new ZoweDatasetNode({ - label: "AUSER.TEST.ASM(member)", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.TEST.ASM(member).asm") - ); - node = new ZoweDatasetNode({ - label: "AUSER.COBOL.TEST(member)", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.COBOL.TEST(member).cbl") - ); - node = new ZoweDatasetNode({ - label: "AUSER.PROD.PLI(member)", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.PROD.PLI(member).pli") - ); - node = new ZoweDatasetNode({ - label: "AUSER.PROD.PLX(member)", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.PROD.PLX(member).pli") - ); - node = new ZoweDatasetNode({ - label: "AUSER.PROD.SH(member)", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.PROD.SH(member).sh") - ); - node = new ZoweDatasetNode({ - label: "AUSER.REXX.EXEC(member)", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.REXX.EXEC(member).rexx") - ); - node = new ZoweDatasetNode({ - label: "AUSER.TEST.XML(member)", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.TEST.XML(member).xml") - ); - - node = new ZoweDatasetNode({ - label: "AUSER.TEST.XML", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.TEST.XML.xml") - ); - node = new ZoweDatasetNode({ - label: "AUSER.TEST.TXML", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.TEST.TXML") - ); - node = new ZoweDatasetNode({ - label: "AUSER.XML.TGML", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.XML.TGML.xml") - ); - node = new ZoweDatasetNode({ - label: "AUSER.XML.ASM", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.XML.ASM.asm") - ); - node = new ZoweDatasetNode({ - label: "AUSER", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER") - ); - node = new ZoweDatasetNode({ - label: "AUSER.XML.TEST(member)", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.XML.TEST(member).xml") - ); - node = new ZoweDatasetNode({ - label: "XML.AUSER.TEST(member)", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "XML.AUSER.TEST(member)") - ); - node = new ZoweDatasetNode({ - label: "AUSER.COBOL.PL1.XML.TEST(member)", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.COBOL.PL1.XML.TEST(member).xml") - ); - node = new ZoweDatasetNode({ - label: "AUSER.COBOL.PL1.XML.ASSEMBLER.TEST(member)", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.COBOL.PL1.XML.ASSEMBLER.TEST(member).asm") - ); - node = new ZoweDatasetNode({ - label: "AUSER.TEST.COPYBOOK", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.TEST.COPYBOOK.cpy") - ); - node = new ZoweDatasetNode({ - label: "AUSER.TEST.PLINC", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toBe( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.TEST.PLINC.inc") - ); - node = new ZoweDatasetNode({ - label: "AUSER.TEST.SPFLOG1", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.datasetSessionNode, - }); - expect(sharedUtils.getDocumentFilePath(node.label.toString(), node)).toEqual( - path.join(path.sep, "test", "path", "temp", "_D_", "sestest", "AUSER.TEST.SPFLOG1.log") - ); - }); -}); - describe("Shared Utils Unit Tests - Function getSelectedNodeList", () => { it("Testing that getSelectedNodeList returns the correct array when single node is selected", async () => { const selectedNodes = []; @@ -646,6 +303,9 @@ describe("Shared utils unit tests - function promptForEncoding", () => { const showQuickPick = jest.spyOn(Gui, "showQuickPick").mockResolvedValue(undefined); const localStorageGet = jest.spyOn(ZoweLocalStorage, "getValue").mockReturnValue(undefined); const localStorageSet = jest.spyOn(ZoweLocalStorage, "setValue").mockReturnValue(undefined); + const getEncodingForFile = jest.spyOn((BaseProvider as any).prototype, "getEncodingForFile"); + const setEncodingForFile = jest.spyOn((BaseProvider as any).prototype, "setEncodingForFile").mockReturnValue(undefined); + const fetchEncodingForUri = jest.spyOn(UssFSProvider.instance, "fetchEncodingForUri").mockResolvedValue(undefined as any); return { profile: createIProfile(), @@ -654,6 +314,9 @@ describe("Shared utils unit tests - function promptForEncoding", () => { showQuickPick, localStorageGet, localStorageSet, + getEncodingForFile, + setEncodingForFile, + fetchEncodingForUri, }; } @@ -671,6 +334,7 @@ describe("Shared utils unit tests - function promptForEncoding", () => { parentPath: "/root", }); blockMocks.showQuickPick.mockImplementationOnce(async (items) => items[0]); + blockMocks.getEncodingForFile.mockReturnValueOnce(undefined); const encoding = await sharedUtils.promptForEncoding(node); expect(blockMocks.showQuickPick).toHaveBeenCalled(); expect(encoding).toEqual(textEncoding); @@ -738,7 +402,9 @@ describe("Shared utils unit tests - function promptForEncoding", () => { await sharedUtils.promptForEncoding(node, "IBM-1047"); expect(blockMocks.showQuickPick).toHaveBeenCalled(); expect(await blockMocks.showQuickPick.mock.calls[0][0][0]).toEqual({ label: "IBM-1047", description: "USS file tag" }); - expect(blockMocks.showQuickPick.mock.calls[0][1]).toEqual(expect.objectContaining({ placeHolder: "Current encoding is Binary" })); + expect(blockMocks.showQuickPick.mock.calls[0][1]).toEqual( + expect.objectContaining({ placeHolder: "Current encoding is binary", title: "Choose encoding for testFile" }) + ); }); it("prompts for encoding for USS file when profile contains encoding", async () => { @@ -751,7 +417,7 @@ describe("Shared utils unit tests - function promptForEncoding", () => { profile: blockMocks.profile, parentPath: "/root", }); - node.setEncoding(textEncoding); + blockMocks.getEncodingForFile.mockReturnValueOnce({ kind: "text" }); await sharedUtils.promptForEncoding(node); expect(blockMocks.showQuickPick).toHaveBeenCalled(); expect(await blockMocks.showQuickPick.mock.calls[0][0][0]).toEqual({ @@ -771,6 +437,7 @@ describe("Shared utils unit tests - function promptForEncoding", () => { parentPath: "/root", }); node.setEncoding(otherEncoding); + blockMocks.getEncodingForFile.mockReturnValueOnce(otherEncoding); const encodingHistory = ["IBM-123", "IBM-456", "IBM-789"]; blockMocks.localStorageGet.mockReturnValueOnce(encodingHistory); blockMocks.showQuickPick.mockImplementationOnce(async (items) => items[4]); @@ -791,7 +458,7 @@ describe("Shared utils unit tests - function promptForEncoding", () => { parentPath: "/root", }); node.setEncoding(binaryEncoding); - delete node.encoding; // Reset encoding property so that cache is used + blockMocks.getEncodingForFile.mockReturnValueOnce(binaryEncoding); await sharedUtils.promptForEncoding(node); expect(blockMocks.showQuickPick).toHaveBeenCalled(); expect(blockMocks.showQuickPick.mock.calls[0][1]).toEqual(expect.objectContaining({ placeHolder: "Current encoding is Binary" })); @@ -799,14 +466,16 @@ describe("Shared utils unit tests - function promptForEncoding", () => { it("remembers cached encoding for data set node", async () => { const blockMocks = createBlockMocks(); + const sessionNode = createDatasetSessionNode(blockMocks.session, blockMocks.profile); const node = new ZoweDatasetNode({ label: "TEST.PS", collapsibleState: vscode.TreeItemCollapsibleState.None, session: blockMocks.session, profile: blockMocks.profile, + parentNode: sessionNode, }); - node.setEncoding(textEncoding); - delete node.encoding; // Reset encoding property so that cache is used + sessionNode.encodingMap["TEST.PS"] = { kind: "text" }; + blockMocks.getEncodingForFile.mockReturnValueOnce(undefined); await sharedUtils.promptForEncoding(node); expect(blockMocks.showQuickPick).toHaveBeenCalled(); expect(blockMocks.showQuickPick.mock.calls[0][1]).toEqual(expect.objectContaining({ placeHolder: "Current encoding is EBCDIC" })); @@ -827,7 +496,7 @@ describe("Shared utils unit tests - function promptForEncoding", () => { contextOverride: globals.DS_MEMBER_CONTEXT, }); node.setEncoding(otherEncoding); - delete node.encoding; // Reset encoding property so that cache is used + blockMocks.getEncodingForFile.mockReturnValueOnce(undefined); await sharedUtils.promptForEncoding(node); expect(blockMocks.showQuickPick).toHaveBeenCalled(); expect(blockMocks.showQuickPick.mock.calls[0][1]).toEqual(expect.objectContaining({ placeHolder: "Current encoding is IBM-1047" })); diff --git a/packages/zowe-explorer/__tests__/__unit__/uss/USSTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/uss/USSTree.unit.test.ts index 24a4fa5b4b..0ed35af298 100644 --- a/packages/zowe-explorer/__tests__/__unit__/uss/USSTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/uss/USSTree.unit.test.ts @@ -9,7 +9,7 @@ * */ -import { Gui, imperative, IZoweUSSTreeNode, ProfilesCache, Validation } from "@zowe/zowe-explorer-api"; +import { Gui, imperative, IZoweUSSTreeNode, ProfilesCache, Validation, ZoweScheme } from "@zowe/zowe-explorer-api"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; import { Profiles } from "../../../src/Profiles"; import * as utils from "../../../src/utils/ProfilesUtils"; @@ -31,14 +31,14 @@ import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; import * as zosmf from "@zowe/zosmf-for-zowe-sdk"; import { createUSSNode, createFavoriteUSSNode, createUSSSessionNode } from "../../../__mocks__/mockCreators/uss"; import { getIconByNode } from "../../../src/generators/icons"; -import * as workspaceUtils from "../../../src/utils/workspace"; import { createUssApi, bindUssApi } from "../../../__mocks__/mockCreators/api"; import { ZoweLocalStorage } from "../../../src/utils/ZoweLocalStorage"; import { PersistentFilters } from "../../../src/PersistentFilters"; import { TreeProviders } from "../../../src/shared/TreeProviders"; import { join } from "path"; import * as sharedUtils from "../../../src/shared/utils"; -import { mocked } from "../../../__mocks__/mockUtils"; +import { mocked, MockedProperty } from "../../../__mocks__/mockUtils"; +import { UssFSProvider } from "../../../src/uss/UssFSProvider"; async function createGlobalMocks() { const globalMocks = { @@ -51,7 +51,7 @@ async function createGlobalMocks() { showInformationMessage: jest.fn(), mockShowWarningMessage: jest.fn(), showErrorMessage: jest.fn(), - showInputBox: jest.fn(), + showInputBox: jest.spyOn(Gui, "showInputBox").mockImplementation(), filters: jest.fn(), getFilters: jest.fn(), createTreeView: jest.fn().mockReturnValue({ onDidCollapseElement: jest.fn() }), @@ -63,8 +63,16 @@ async function createGlobalMocks() { mockDisableValidationContext: jest.fn(), mockEnableValidationContext: jest.fn(), mockCheckCurrentProfile: jest.fn(), - mockTextDocumentDirty: { fileName: `/test/path/temp/_U_/sestest/test/node`, isDirty: true }, - mockTextDocumentClean: { fileName: `/test/path/temp/_U_/sestest/testClean/node`, isDirty: false }, + mockTextDocumentDirty: { + fileName: `/test/path/temp/_U_/sestest/test/node`, + isDirty: true, + uri: vscode.Uri.from({ scheme: "file", path: "/test/path/temp/_U_/sestest/test/node" }), + }, + mockTextDocumentClean: { + fileName: `/test/path/temp/_U_/sestest/testClean/node`, + isDirty: false, + uri: vscode.Uri.from({ scheme: "file", path: "/test/path/temp/_U_/sestest/testClean/node" }), + }, mockTextDocuments: [], mockProfilesInstance: null, withProgress: jest.fn(), @@ -84,8 +92,15 @@ async function createGlobalMocks() { profilesForValidation: { status: "active", name: "fake" }, mockProfilesCache: new ProfilesCache(imperative.Logger.getAppLogger()), mockTreeProviders: createTreeProviders(), + FileSystemProvider: { + createDirectory: jest.fn(), + rename: jest.fn(), + }, }; + jest.spyOn(UssFSProvider.instance, "createDirectory").mockImplementation(globalMocks.FileSystemProvider.createDirectory); + jest.spyOn(UssFSProvider.instance, "rename").mockImplementation(globalMocks.FileSystemProvider.rename); + globalMocks.mockTextDocuments.push(globalMocks.mockTextDocumentDirty); globalMocks.mockTextDocuments.push(globalMocks.mockTextDocumentClean); globalMocks.testBaseProfile.profile.tokenType = "tokenType"; @@ -106,10 +121,6 @@ async function createGlobalMocks() { value: globalMocks.mockShowWarningMessage, configurable: true, }); - Object.defineProperty(workspaceUtils, "closeOpenedTextFile", { - value: globalMocks.closeOpenedTextFile, - configurable: true, - }); Object.defineProperty(vscode.window, "createTreeView", { value: globalMocks.createTreeView, configurable: true }); Object.defineProperty(vscode.commands, "executeCommand", { value: globalMocks.executeCommand, configurable: true }); Object.defineProperty(globalMocks.Utilities, "renameUSSFile", { @@ -231,7 +242,7 @@ describe("USSTree Unit Tests - Function initializeFavorites", () => { expectedUSSFavorites.forEach((node) => (node.contextValue += globals.FAV_SUFFIX)); expectedUSSFavorites.forEach((node) => { if (node.contextValue !== globals.USS_DIR_CONTEXT + globals.FAV_SUFFIX) { - node.command = { command: "zowe.uss.ZoweUSSNode.open", title: "Open", arguments: [node] }; + node.command = { command: "vscode.open", title: "Open", arguments: [node.resourceUri] }; } }); expect(favProfileNode.children[0].fullPath).toEqual("/u/aDir"); @@ -241,7 +252,7 @@ describe("USSTree Unit Tests - Function initializeFavorites", () => { describe("USSTree Unit Tests - Function initializeFavChildNodeForProfile", () => { it("Tests initializeFavChildNodeForProfile() for favorited search", async () => { - await createGlobalMocks(); + const globalMocks = await createGlobalMocks(); jest.spyOn(PersistentFilters.prototype, "readFavorites").mockReturnValueOnce([ "[test]: /u/aDir{directory}", @@ -255,6 +266,7 @@ describe("USSTree Unit Tests - Function initializeFavChildNodeForProfile", () => label: "/u/fakeuser", collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: favProfileNode, + profile: globalMocks.testProfile, }); expectedFavSearchNode.contextValue = globals.USS_SESSION_CONTEXT + globals.FAV_SUFFIX; expectedFavSearchNode.fullPath = label; @@ -277,6 +289,7 @@ describe("USSTree Unit Tests - Function createProfileNodeForFavs", () => { label: "testProfile", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, parentNode: globalMocks.testTree.mFavoriteSession, + profile: globalMocks.testProfile, }); expectedFavProfileNode.contextValue = globals.FAV_PROFILE_CONTEXT; @@ -290,6 +303,7 @@ describe("USSTree Unit Tests - Function createProfileNodeForFavs", () => { label: "testProfile", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, parentNode: globalMocks.testTree.mFavoriteSession, + profile: globalMocks.testProfile, }); expectedFavProfileNode.contextValue = globals.FAV_PROFILE_CONTEXT; const icons = await import("../../../src/generators/icons"); @@ -909,6 +923,8 @@ describe("USSTree Unit Tests - Function rename", () => { ussFavNodeParent.children.push(ussFavNode); globalMocks.testTree.mFavorites.push(ussFavNodeParent); + globalMocks.FileSystemProvider.rename.mockClear(); + const newMocks = { ussFavNode, ussFavNodeParent, @@ -917,7 +933,13 @@ describe("USSTree Unit Tests - Function rename", () => { return newMocks; } - it("Tests that USSTree.rename() shows error if an open dirty file's fullpath includes that of the node being renamed", async () => { + const getEncodingForFileMock = jest.spyOn(UssFSProvider.instance, "getEncodingForFile").mockReturnValue({ kind: "text" }); + + afterAll(() => { + getEncodingForFileMock.mockRestore(); + }); + + it("Tests that USSTree.rename() shows no error if an open dirty file's fullpath includes that of the node being renamed", async () => { // Open dirty file defined by globalMocks.mockTextDocumentDirty, with filepath including "sestest/test/node" const globalMocks = await createGlobalMocks(); createBlockMocks(globalMocks); @@ -929,19 +951,10 @@ describe("USSTree Unit Tests - Function rename", () => { profile: globalMocks.testProfile, parentPath: "/", }); - Object.defineProperty(testUSSDir, "getUSSDocumentFilePath", { - value: jest.fn(() => { - return "/test/path/temp/_U_/sestest/test"; - }), - }); const vscodeErrorMsgSpy = jest.spyOn(vscode.window, "showErrorMessage"); - const getAllLoadedItemsSpy = jest.spyOn(globalMocks.testTree, "getAllLoadedItems"); - await globalMocks.testTree.rename(testUSSDir); - expect(vscodeErrorMsgSpy.mock.calls.length).toBe(1); - expect(vscodeErrorMsgSpy.mock.calls[0][0]).toContain("because you have unsaved changes in this"); - expect(getAllLoadedItemsSpy.mock.calls.length).toBe(0); + expect(vscodeErrorMsgSpy.mock.calls.length).toBe(0); }); it("Tests that USSTree.rename() shows no error if an open clean file's fullpath includes that of the node being renamed", async () => { @@ -956,11 +969,6 @@ describe("USSTree Unit Tests - Function rename", () => { profile: globalMocks.testProfile, parentPath: "/", }); - Object.defineProperty(testUSSDir, "getUSSDocumentFilePath", { - value: jest.fn(() => { - return "/test/path/temp/_U_/sestest/testClean"; - }), - }); const vscodeErrorMsgSpy = jest.spyOn(vscode.window, "showErrorMessage"); await globalMocks.testTree.rename(testUSSDir); @@ -987,7 +995,7 @@ describe("USSTree Unit Tests - Function rename", () => { await globalMocks.testTree.rename(globalMocks.testUSSNode); expect(globalMocks.showErrorMessage.mock.calls.length).toBe(0); - expect(globalMocks.renameUSSFile.mock.calls.length).toBe(1); + expect(renameNode.mock.calls.length).toBe(1); expect(renameFavoriteSpy).toHaveBeenCalledTimes(1); }); @@ -1006,7 +1014,7 @@ describe("USSTree Unit Tests - Function rename", () => { await globalMocks.testTree.rename(globalMocks.testUSSNode); expect(globalMocks.showErrorMessage.mock.calls.length).toBe(0); - expect(globalMocks.renameUSSFile.mock.calls.length).toBe(1); + expect(renameNode.mock.calls.length).toBe(1); expect(renameUSSNodeSpy).toHaveBeenCalledTimes(0); expect(renameFavoriteSpy).toHaveBeenCalledTimes(0); }); @@ -1019,11 +1027,11 @@ describe("USSTree Unit Tests - Function rename", () => { const renameFavoriteSpy = jest.spyOn(globalMocks.testTree, "renameFavorite"); globalMocks.showInputBox.mockReturnValueOnce("new name"); + globalMocks.FileSystemProvider.rename.mockClear(); await globalMocks.testTree.rename(blockMocks.ussFavNode); expect(globalMocks.showErrorMessage.mock.calls.length).toBe(0); - expect(globalMocks.renameUSSFile.mock.calls.length).toBe(1); expect(renameUSSNodeSpy.mock.calls.length).toBe(1); expect(renameFavoriteSpy.mock.calls.length).toBe(1); }); @@ -1037,7 +1045,6 @@ describe("USSTree Unit Tests - Function rename", () => { await globalMocks.testTree.rename(blockMocks.ussFavNode); expect(globalMocks.showErrorMessage.mock.calls.length).toBe(0); - expect(globalMocks.renameUSSFile.mock.calls.length).toBe(1); expect(renameUSSNode.mock.calls.length).toBe(1); }); @@ -1058,7 +1065,7 @@ describe("USSTree Unit Tests - Function rename", () => { const globalMocks = await createGlobalMocks(); createBlockMocks(globalMocks); globalMocks.showInputBox.mockReturnValueOnce("new name"); - globalMocks.renameUSSFile.mockRejectedValueOnce(Error("testError")); + globalMocks.FileSystemProvider.rename.mockRejectedValueOnce(Error("testError")); try { await globalMocks.testTree.rename(globalMocks.testUSSNode); @@ -1207,28 +1214,22 @@ describe("USSTree Unit Tests - Function getChildren", () => { const rootChildren = await globalMocks.testTree.getChildren(); // Creating rootNode const sessNode = [ - new ZoweUSSNode({ label: "Favorites", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed }), + new ZoweUSSNode({ + label: "Favorites", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextOverride: globals.FAVORITE_CONTEXT, + }), new ZoweUSSNode({ label: "sestest", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextOverride: globals.USS_SESSION_CONTEXT, session: globalMocks.testSession, profile: globalMocks.testProfile, parentPath: "/", }), ]; - sessNode[0].contextValue = globals.FAVORITE_CONTEXT; - sessNode[1].contextValue = globals.USS_SESSION_CONTEXT; - sessNode[1].fullPath = "test"; - // Set icon - let targetIcon = getIconByNode(sessNode[0]); - if (targetIcon) { - sessNode[0].iconPath = targetIcon.path; - } - targetIcon = getIconByNode(sessNode[1]); - if (targetIcon) { - sessNode[1].iconPath = targetIcon.path; - } + sessNode[1].fullPath = "/test"; expect(sessNode).toEqual(rootChildren); expect(JSON.stringify(sessNode[0].iconPath)).toContain("folder-root-favorite-star-closed.svg"); @@ -1292,7 +1293,7 @@ describe("USSTree Unit Tests - Function getChildren", () => { }); const file = new ZoweUSSNode({ label: "myFile.txt", collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: directory }); const sampleChildren: ZoweUSSNode[] = [file]; - sampleChildren[0].command = { command: "zowe.uss.ZoweUSSNode.open", title: "", arguments: [sampleChildren[0]] }; + sampleChildren[0].command = { command: "vscode.open", title: "", arguments: [sampleChildren[0].resourceUri] }; directory.children.push(file); directory.dirty = true; const mockApiResponseItems = { @@ -1362,49 +1363,6 @@ describe("USSTree Unit Tests - Function loadProfilesForFavorites", () => { }; } - it("Tests that loaded profile and session values are added to the profile grouping node in Favorites", async () => { - const globalMocks = await createGlobalMocks(); - const blockMocks = createBlockMocks(globalMocks); - const favProfileNode = new ZoweUSSNode({ - label: "sestest", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: globalMocks.testTree.mFavoriteSession, - }); - globalMocks.testTree.mFavorites.push(favProfileNode); - const expectedFavProfileNode = new ZoweUSSNode({ - label: "sestest", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: globalMocks.testTree.mFavoriteSession, - session: globalMocks.testSession, - profile: globalMocks.testProfile, - }); - - // Mock successful loading of profile/session - Object.defineProperty(Profiles, "getInstance", { - value: jest.fn(() => { - return { - loadNamedProfile: jest.fn(() => { - return globalMocks.testProfile; - }), - checkCurrentProfile: jest.fn(() => { - return globalMocks.profilesForValidation; - }), - validProfile: Validation.ValidationType.VALID, - }; - }), - configurable: true, - }); - Object.defineProperty(blockMocks.ussApi, "getSession", { - value: jest.fn(() => { - return globalMocks.testSession; - }), - }); - - await globalMocks.testTree.loadProfilesForFavorites(blockMocks.log, favProfileNode); - const resultFavProfileNode = globalMocks.testTree.mFavorites[0]; - - expect(resultFavProfileNode).toEqual(expectedFavProfileNode); - }); it("Tests that error is handled if profile not successfully loaded for profile grouping node in Favorites", async () => { const globalMocks = await createGlobalMocks(); const blockMocks = createBlockMocks(globalMocks); @@ -1638,32 +1596,167 @@ describe("USSTree Unit Tests - Function openWithEncoding", () => { }); it("sets binary encoding if selection was made", async () => { + const setEncodingMock = jest.spyOn(UssFSProvider.instance, "setEncodingForFile").mockImplementation(); const node = new ZoweUSSNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); node.openUSS = jest.fn(); + const setEncodingNodeMock = jest.spyOn(node, "setEncoding").mockImplementation(); jest.spyOn(sharedUtils, "promptForEncoding").mockResolvedValueOnce({ kind: "binary" }); await USSTree.prototype.openWithEncoding(node); - expect(node.binary).toBe(true); - expect(node.encoding).toBeUndefined(); + expect(setEncodingNodeMock).toHaveBeenCalledWith({ kind: "binary" }); expect(node.openUSS).toHaveBeenCalledTimes(1); + setEncodingMock.mockRestore(); + setEncodingNodeMock.mockRestore(); }); it("sets text encoding if selection was made", async () => { + const setEncodingMock = jest.spyOn(UssFSProvider.instance, "setEncodingForFile").mockImplementation(); + const getEncodingMock = jest.spyOn(UssFSProvider.instance, "getEncodingForFile").mockReturnValueOnce(null as any); const node = new ZoweUSSNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); node.openUSS = jest.fn(); jest.spyOn(sharedUtils, "promptForEncoding").mockResolvedValueOnce({ kind: "text" }); await USSTree.prototype.openWithEncoding(node); expect(node.binary).toBe(false); - expect(node.encoding).toBeNull(); + expect(setEncodingMock).toHaveBeenCalledWith(node.resourceUri, { kind: "text" }); expect(node.openUSS).toHaveBeenCalledTimes(1); + getEncodingMock.mockRestore(); }); it("does not set encoding if prompt was cancelled", async () => { + const setEncodingSpy = jest.spyOn(UssFSProvider.instance, "setEncodingForFile").mockClear(); + const getEncodingMock = jest.spyOn(UssFSProvider.instance, "getEncodingForFile").mockReturnValueOnce(null as any); const node = new ZoweUSSNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); node.openUSS = jest.fn(); jest.spyOn(sharedUtils, "promptForEncoding").mockResolvedValueOnce(undefined); await USSTree.prototype.openWithEncoding(node); expect(node.binary).toBe(false); - expect(node.encoding).toBeUndefined(); + expect(setEncodingSpy).not.toHaveBeenCalled(); expect(node.openUSS).toHaveBeenCalledTimes(0); + getEncodingMock.mockRestore(); + }); +}); + +describe("USSTree Unit Tests - Function handleDrag", () => { + it("adds a DataTransferItem containing info about the dragged USS node", async () => { + const globalMocks = await createGlobalMocks(); + const ussnode = createUSSNode(globalMocks.testSession, globalMocks.testProfile); + const dataTransferSetMock = jest.fn(); + globalMocks.testTree.handleDrag([ussnode], { set: dataTransferSetMock }, undefined); + expect(dataTransferSetMock).toHaveBeenCalledWith( + "application/vnd.code.tree.zowe.uss.explorer", + new vscode.DataTransferItem([ + { + label: ussnode.label, + uri: ussnode.resourceUri, + }, + ]) + ); + }); +}); + +describe("USSTree Unit Tests - Function handleDrop", () => { + it("returns early if there are no items in the dataTransfer object", async () => { + const globalMocks = await createGlobalMocks(); + const statusBarMsgSpy = jest.spyOn(Gui, "setStatusBarMessage"); + const getDataTransferMock = jest.spyOn(vscode.DataTransfer.prototype, "get").mockReturnValueOnce(undefined); + await globalMocks.testTree.handleDrop(globalMocks.testUSSNode, new vscode.DataTransfer(), undefined); + expect(statusBarMsgSpy).not.toHaveBeenCalled(); + getDataTransferMock.mockRestore(); + }); + + it("handles moving files and folders within the same profile", async () => { + const globalMocks = await createGlobalMocks(); + const statusBarMsgSpy = jest.spyOn(Gui, "setStatusBarMessage"); + const warningMsgSpy = jest.spyOn(Gui, "warningMessage").mockClear().mockResolvedValueOnce(undefined); + const ussSession = createUSSSessionNode(globalMocks.testSession, globalMocks.testProfile); + const ussDirNode = new ZoweUSSNode({ + label: "folder", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + parentNode: ussSession, + parentPath: ussSession.fullPath, + profile: ussSession.getProfile(), + }); + const ussFileNode = new ZoweUSSNode({ + label: "file.txt", + collapsibleState: vscode.TreeItemCollapsibleState.None, + parentNode: ussSession, + parentPath: ussSession.fullPath, + profile: ussDirNode.getProfile(), + }); + ussDirNode.children = []; + ussSession.children = [ussDirNode, ussFileNode]; + const dataTransfer = new vscode.DataTransfer(); + const getDataTransferMock = jest.spyOn(dataTransfer, "get").mockReturnValueOnce({ + value: [ + { + label: ussFileNode.label as string, + uri: ussFileNode.resourceUri, + }, + ], + } as any); + const moveMock = jest.fn(); + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ + move: moveMock, + } as any); + const fspMoveMock = jest.spyOn(UssFSProvider.instance, "move").mockResolvedValue(true); + const draggedNodeMock = new MockedProperty(globalMocks.testTree, "draggedNodes", undefined, { + [ussFileNode.resourceUri.path]: ussFileNode, + }); + await globalMocks.testTree.handleDrop(ussDirNode, dataTransfer, undefined); + expect(statusBarMsgSpy).toHaveBeenCalledWith("$(sync~spin) Moving USS files..."); + expect(fspMoveMock).toHaveBeenCalled(); + getDataTransferMock.mockRestore(); + draggedNodeMock[Symbol.dispose](); + warningMsgSpy.mockRestore(); + ussApiMock.mockRestore(); + fspMoveMock.mockRestore(); + }); +}); + +describe("USSTree Unit Tests - Function crossLparMove", () => { + it("calls the function recursively for directories, and calls appropriate APIs for files", async () => { + const globalMocks = await createGlobalMocks(); + const ussDirNode = createUSSNode(globalMocks.testSession, globalMocks.testProfile); + ussDirNode.children = [ + new ZoweUSSNode({ + label: "file.txt", + collapsibleState: vscode.TreeItemCollapsibleState.None, + profile: ussDirNode.getProfile(), + }), + ]; + + const deleteMock = jest.spyOn(UssFSProvider.instance, "delete").mockResolvedValue(undefined); + const readFileMock = jest.spyOn(UssFSProvider.instance, "readFile").mockResolvedValue(new Uint8Array([1, 2, 3])); + const writeFileMock = jest.spyOn(UssFSProvider.instance, "writeFile").mockResolvedValue(undefined); + const existsMock = jest.spyOn(UssFSProvider.instance, "exists").mockReturnValueOnce(false); + + const createDirMock = jest.spyOn(UssFSProvider.instance as any, "createDirectory").mockResolvedValueOnce(undefined); + const createMock = jest.fn(); + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValue({ + create: createMock, + } as any); + const profilesMock = jest.spyOn(Profiles, "getInstance").mockReturnValueOnce({ + loadNamedProfile: jest.fn().mockResolvedValueOnce(globalMocks.testProfile), + } as any); + + await globalMocks.testTree.crossLparMove( + ussDirNode, + ussDirNode.resourceUri, + ussDirNode.resourceUri?.with({ path: "/sestest/u/myuser/subfolder/usstest" }) + ); + const newUri = vscode.Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest/u/myuser/subfolder/usstest", + }); + expect(existsMock).toHaveBeenCalledWith(newUri); + expect(createMock).toHaveBeenCalledWith("/u/myuser/subfolder/usstest/file.txt", "file"); + expect(createDirMock).toHaveBeenCalledWith(newUri); + expect(deleteMock).toHaveBeenCalledWith(ussDirNode.resourceUri, { recursive: true }); + createDirMock.mockRestore(); + deleteMock.mockRestore(); + existsMock.mockRestore(); + profilesMock.mockRestore(); + readFileMock.mockRestore(); + writeFileMock.mockRestore(); + ussApiMock.mockRestore(); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/uss/ZoweUSSNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/uss/ZoweUSSNode.unit.test.ts index ccecab09b2..3fc19bc378 100644 --- a/packages/zowe-explorer/__tests__/__unit__/uss/ZoweUSSNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/uss/ZoweUSSNode.unit.test.ts @@ -12,11 +12,11 @@ import * as vscode from "vscode"; import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; import * as zosmf from "@zowe/zosmf-for-zowe-sdk"; -import { Gui, imperative, Validation } from "@zowe/zowe-explorer-api"; +import { Gui, imperative, Validation, ZoweScheme } from "@zowe/zowe-explorer-api"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; import { Profiles } from "../../../src/Profiles"; import { ZoweUSSNode } from "../../../src/uss/ZoweUSSNode"; -import { UssFileType, UssFileUtils } from "../../../src/uss/FileStructure"; +import { UssFileType } from "../../../src/uss/FileStructure"; import { createISession, createISessionWithoutCredentials, @@ -27,40 +27,36 @@ import { createValidIProfile, } from "../../../__mocks__/mockCreators/shared"; import { createUSSTree } from "../../../__mocks__/mockCreators/uss"; -import * as fs from "fs"; -import * as path from "path"; -import * as workspaceUtils from "../../../src/utils/workspace"; import * as globals from "../../../src/globals"; import * as ussUtils from "../../../src/uss/utils"; import { ZoweLocalStorage } from "../../../src/utils/ZoweLocalStorage"; -import { LocalFileManagement } from "../../../src/utils/LocalFileManagement"; import { TreeProviders } from "../../../src/shared/TreeProviders"; +import { UssFSProvider } from "../../../src/uss/UssFSProvider"; +import { MockedProperty } from "../../../__mocks__/mockUtils"; +import { getSessionLabel } from "../../../src/utils/ProfilesUtils"; +import { ZoweLogger } from "../../../src/utils/ZoweLogger"; jest.mock("fs"); -jest.mock("path"); async function createGlobalMocks() { const globalMocks = { ussFile: jest.fn(), Download: jest.fn(), - mockIsDirtyInEditor: jest.fn(), - mockTextDocument: { fileName: `/test/path/temp/_U_/sestest/test/node`, isDirty: true }, - mockTextDocuments: [], + mockTextDocument: { uri: vscode.Uri.from({ scheme: ZoweScheme.USS, path: "/sestest/path/to/node" }) } as vscode.TextDocument, + textDocumentsArray: new Array(), openedDocumentInstance: jest.fn(), onDidSaveTextDocument: jest.fn(), showErrorMessage: jest.fn(), - openTextDocument: jest.fn(), mockShowTextDocument: jest.fn(), showInformationMessage: jest.fn(), getConfiguration: jest.fn(), downloadUSSFile: jest.fn(), - setStatusBarMessage: jest.fn().mockReturnValue({ dispose: jest.fn() }), + setStatusBarMessage: jest.spyOn(Gui, "setStatusBarMessage").mockReturnValue({ dispose: jest.fn() }), showInputBox: jest.fn(), mockExecuteCommand: jest.fn(), mockLoadNamedProfile: jest.fn(), showQuickPick: jest.fn(), isFileTagBinOrAscii: jest.fn(), - existsSync: jest.fn(), Delete: jest.fn(), Utilities: jest.fn(), withProgress: jest.fn(), @@ -82,10 +78,18 @@ async function createGlobalMocks() { readText: jest.fn(), fileToUSSFile: jest.fn(), basePath: jest.fn(), + FileSystemProvider: { + createDirectory: jest.fn(), + }, + loggerError: jest.spyOn(ZoweLogger, "error").mockImplementation(), }; - globalMocks.openTextDocument.mockResolvedValue(globalMocks.mockTextDocument); - globalMocks.mockTextDocuments.push(globalMocks.mockTextDocument); + globalMocks["textDocumentsMock"] = new MockedProperty(vscode.workspace, "textDocuments", undefined, globalMocks.textDocumentsArray); + globalMocks["readTextMock"] = new MockedProperty(vscode.env.clipboard, "readText", undefined, globalMocks.readText); + + jest.spyOn(UssFSProvider.instance, "createDirectory").mockImplementation(globalMocks.FileSystemProvider.createDirectory); + + globalMocks.textDocumentsArray.push(globalMocks.mockTextDocument); globalMocks.profileOps = createInstanceOfProfile(globalMocks.profileOne); Object.defineProperty(globalMocks.profileOps, "loadNamedProfile", { value: jest.fn(), @@ -95,27 +99,16 @@ async function createGlobalMocks() { globalMocks.getUssApiMock.mockReturnValue(globalMocks.ussApi); ZoweExplorerApiRegister.getUssApi = globalMocks.getUssApiMock.bind(ZoweExplorerApiRegister); - Object.defineProperty(Gui, "setStatusBarMessage", { - value: globalMocks.setStatusBarMessage, - configurable: true, - }); Object.defineProperty(vscode.workspace, "onDidSaveTextDocument", { value: globalMocks.onDidSaveTextDocument, configurable: true, }); - Object.defineProperty(vscode.workspace, "textDocuments", { - value: globalMocks.mockTextDocuments, - configurable: true, - }); Object.defineProperty(vscode.commands, "executeCommand", { value: globalMocks.mockExecuteCommand, configurable: true, }); Object.defineProperty(vscode.window, "showQuickPick", { value: globalMocks.showQuickPick, configurable: true }); - Object.defineProperty(vscode.workspace, "openTextDocument", { - value: globalMocks.openTextDocument, - configurable: true, - }); + Object.defineProperty(vscode.window, "showInformationMessage", { value: globalMocks.showInformationMessage, configurable: true, @@ -128,6 +121,7 @@ async function createGlobalMocks() { value: globalMocks.showErrorMessage, configurable: true, }); + jest.spyOn(Gui, "errorMessage").mockImplementation(globalMocks.showErrorMessage); Object.defineProperty(vscode.window, "showWarningMessage", { value: globalMocks.mockShowWarningMessage, configurable: true, @@ -148,10 +142,8 @@ async function createGlobalMocks() { }); Object.defineProperty(zosfiles, "Download", { value: globalMocks.Download, configurable: true }); Object.defineProperty(zosfiles, "Utilities", { value: globalMocks.Utilities, configurable: true }); - Object.defineProperty(workspaceUtils, "closeOpenedTextFile", { value: jest.fn(), configurable: true }); Object.defineProperty(globalMocks.Download, "ussFile", { value: globalMocks.ussFile, configurable: true }); Object.defineProperty(zosfiles, "Delete", { value: globalMocks.Delete, configurable: true }); - jest.spyOn(fs, "existsSync").mockImplementation(globalMocks.existsSync); Object.defineProperty(globalMocks.Delete, "ussFile", { value: globalMocks.ussFile, configurable: true }); Object.defineProperty(Profiles, "createInstance", { value: jest.fn(() => globalMocks.profileOps), @@ -167,8 +159,6 @@ async function createGlobalMocks() { value: globalMocks.fileExistsCaseSensitveSync, configurable: true, }); - Object.defineProperty(vscode.env.clipboard, "readText", { value: globalMocks.readText, configurable: true }); - jest.spyOn(path, "basename").mockImplementation(globalMocks.basePath); Object.defineProperty(ZoweLocalStorage, "storage", { value: { get: () => ({ persistence: true, favorites: [], history: [], sessions: ["zosmf"], searchHistory: [], fileHistory: [] }), @@ -188,27 +178,30 @@ describe("ZoweUSSNode Unit Tests - Initialization of class", () => { return callback(); }); const rootNode = new ZoweUSSNode({ - label: "root", + label: "sestest", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextOverride: globals.USS_SESSION_CONTEXT, session: globalMocks.session, profile: globalMocks.profileOne, }); - rootNode.contextValue = globals.USS_SESSION_CONTEXT; + rootNode.fullPath = "/"; rootNode.dirty = true; const testDir = new ZoweUSSNode({ label: "testDir", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, parentNode: rootNode, + parentPath: rootNode.fullPath, profile: globalMocks.profileOne, }); const testFile = new ZoweUSSNode({ label: "testFile", collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: testDir, + parentPath: testDir.fullPath, profile: globalMocks.profileOne, }); testFile.contextValue = globals.USS_TEXT_FILE_CONTEXT; - expect(JSON.stringify(rootNode.iconPath)).toContain("folder-closed.svg"); + expect(JSON.stringify(rootNode.iconPath)).toContain("folder-root-unverified-closed.svg"); expect(JSON.stringify(testDir.iconPath)).toContain("folder-closed.svg"); expect(JSON.stringify(testFile.iconPath)).toContain("document.svg"); rootNode.iconPath = "Ref: 'folder.svg'"; @@ -309,8 +302,11 @@ describe("ZoweUSSNode Unit Tests - Function node.refreshUSS()", () => { session: globalMocks.session, profile: globalMocks.profileOne, }), + fetchFileAtUri: jest.fn(), }; + jest.spyOn(UssFSProvider.instance, "fetchFileAtUri").mockImplementation(newMocks.fetchFileAtUri); + newMocks.ussNode.contextValue = globals.USS_SESSION_CONTEXT; newMocks.ussNode.fullPath = "/u/myuser"; newMocks.node = new ZoweUSSNode({ @@ -328,7 +324,6 @@ describe("ZoweUSSNode Unit Tests - Function node.refreshUSS()", () => { return callback(); }); - Object.defineProperty(newMocks.node, "isDirtyInEditor", { get: globalMocks.mockIsDirtyInEditor }); Object.defineProperty(newMocks.node, "openedDocumentInstance", { get: globalMocks.openedDocumentInstance }); Object.defineProperty(globalMocks.Utilities, "putUSSPayload", { value: newMocks.putUSSPayload, @@ -338,51 +333,15 @@ describe("ZoweUSSNode Unit Tests - Function node.refreshUSS()", () => { return newMocks; } - it("Tests that node.refreshUSS() works correctly for dirty file state, when user didn't cancel file save", async () => { + it("Tests that node.refreshUSS() works correctly", async () => { const globalMocks = await createGlobalMocks(); const blockMocks = await createBlockMocks(globalMocks); globalMocks.ussFile.mockResolvedValue(globalMocks.response); - globalMocks.mockIsDirtyInEditor.mockReturnValueOnce(true); - globalMocks.mockIsDirtyInEditor.mockReturnValueOnce(false); await blockMocks.node.refreshUSS(); - expect(globalMocks.ussFile.mock.calls.length).toBe(1); - expect(globalMocks.mockShowTextDocument.mock.calls.length).toBe(2); - expect(globalMocks.mockExecuteCommand.mock.calls.length).toBe(3); - expect(blockMocks.node.downloaded).toBe(true); - }); - - it("Tests that node.refreshUSS() works correctly for dirty file state, when user cancelled file save", async () => { - const globalMocks = await createGlobalMocks(); - const blockMocks = await createBlockMocks(globalMocks); - - globalMocks.ussFile.mockResolvedValueOnce(globalMocks.response); - globalMocks.mockIsDirtyInEditor.mockReturnValueOnce(true); - globalMocks.mockIsDirtyInEditor.mockReturnValueOnce(true); - - await blockMocks.node.refreshUSS(); - - expect(globalMocks.ussFile.mock.calls.length).toBe(0); - expect(globalMocks.mockShowTextDocument.mock.calls.length).toBe(1); - expect(globalMocks.mockExecuteCommand.mock.calls.length).toBe(1); - expect(blockMocks.node.downloaded).toBe(false); - }); - - it("Tests that node.refreshUSS() works correctly for not dirty file state", async () => { - const globalMocks = await createGlobalMocks(); - const blockMocks = await createBlockMocks(globalMocks); - - globalMocks.ussFile.mockResolvedValueOnce(globalMocks.response); - globalMocks.mockIsDirtyInEditor.mockReturnValueOnce(false); - globalMocks.mockIsDirtyInEditor.mockReturnValueOnce(false); - - await blockMocks.node.refreshUSS(); - - expect(globalMocks.ussFile.mock.calls.length).toBe(1); - expect(globalMocks.mockShowTextDocument.mock.calls.length).toBe(0); - expect(globalMocks.mockExecuteCommand.mock.calls.length).toBe(2); + expect(blockMocks.fetchFileAtUri.mock.calls.length).toBe(1); expect(blockMocks.node.downloaded).toBe(true); }); @@ -390,17 +349,13 @@ describe("ZoweUSSNode Unit Tests - Function node.refreshUSS()", () => { const globalMocks = await createGlobalMocks(); const blockMocks = await createBlockMocks(globalMocks); - globalMocks.ussFile.mockRejectedValueOnce(Error("")); - globalMocks.mockIsDirtyInEditor.mockReturnValueOnce(true); - globalMocks.mockIsDirtyInEditor.mockReturnValueOnce(false); - + blockMocks.fetchFileAtUri.mockRejectedValueOnce(Error("")); await blockMocks.node.refreshUSS(); - expect(globalMocks.ussFile.mock.calls.length).toBe(1); - expect(globalMocks.mockShowTextDocument.mock.calls.length).toBe(1); - expect(globalMocks.mockExecuteCommand.mock.calls.length).toBe(2); + expect(blockMocks.fetchFileAtUri.mock.calls.length).toBe(1); expect(blockMocks.node.downloaded).toBe(false); }); + it("Tests that node.refreshUSS() throws an error when context value is invalid", async () => { const globalMocks = await createGlobalMocks(); const blockMocks = await createBlockMocks(globalMocks); @@ -427,14 +382,10 @@ describe("ZoweUSSNode Unit Tests - Function node.refreshUSS()", () => { const blockMocks = await createBlockMocks(globalMocks); blockMocks.ussNode.contextValue = globals.USS_DIR_CONTEXT; globalMocks.ussFile.mockResolvedValueOnce(globalMocks.response); - globalMocks.mockIsDirtyInEditor.mockReturnValueOnce(false); - globalMocks.mockIsDirtyInEditor.mockReturnValueOnce(false); await blockMocks.node.refreshUSS(); - expect(globalMocks.ussFile.mock.calls.length).toBe(1); - expect(globalMocks.mockShowTextDocument.mock.calls.length).toBe(0); - expect(globalMocks.mockExecuteCommand.mock.calls.length).toBe(2); + expect(blockMocks.fetchFileAtUri.mock.calls.length).toBe(1); expect(blockMocks.node.downloaded).toBe(true); }); it("Tests that node.refreshUSS() works correctly for favorited files/directories", async () => { @@ -442,14 +393,10 @@ describe("ZoweUSSNode Unit Tests - Function node.refreshUSS()", () => { const blockMocks = await createBlockMocks(globalMocks); blockMocks.ussNode.contextValue = globals.FAV_PROFILE_CONTEXT; globalMocks.ussFile.mockResolvedValueOnce(globalMocks.response); - globalMocks.mockIsDirtyInEditor.mockReturnValueOnce(false); - globalMocks.mockIsDirtyInEditor.mockReturnValueOnce(false); await blockMocks.node.refreshUSS(); - expect(globalMocks.ussFile.mock.calls.length).toBe(1); - expect(globalMocks.mockShowTextDocument.mock.calls.length).toBe(0); - expect(globalMocks.mockExecuteCommand.mock.calls.length).toBe(2); + expect(blockMocks.fetchFileAtUri.mock.calls.length).toBe(1); expect(blockMocks.node.downloaded).toBe(true); }); }); @@ -468,22 +415,6 @@ describe("ZoweUSSNode Unit Tests - Function node.getEtag()", () => { }); }); -describe("ZoweUSSNode Unit Tests - Function node.setEtag()", () => { - it("Tests that setEtag() assigns a value", async () => { - const globalMocks = await createGlobalMocks(); - - const rootNode = new ZoweUSSNode({ - label: "gappy", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - session: globalMocks.session, - profile: globalMocks.profileOne, - }); - expect(rootNode.getEtag() === "123"); - rootNode.setEtag("ABC"); - expect(rootNode.getEtag() === "ABC"); - }); -}); - describe("ZoweUSSNode Unit Tests - Function node.rename()", () => { async function createBlockMocks(globalMocks) { const newMocks = { @@ -499,6 +430,8 @@ describe("ZoweUSSNode Unit Tests - Function node.rename()", () => { uss: { addSingleSession: jest.fn(), mSessionNodes: [], refresh: jest.fn() } as any, job: { addSingleSession: jest.fn(), mSessionNodes: [], refresh: jest.fn() } as any, }), + renameSpy: jest.spyOn(UssFSProvider.instance, "rename").mockImplementation(), + getEncodingForFile: jest.spyOn(UssFSProvider.instance as any, "getEncodingForFile").mockReturnValue(undefined), }; newMocks.ussDir.contextValue = globals.USS_DIR_CONTEXT; return newMocks; @@ -576,8 +509,8 @@ describe("ZoweUSSNode Unit Tests - Function node.reopen()", () => { await ussFile.reopen(hasClosedTab); - expect(vscodeCommandSpy.mock.calls[0][0]).toEqual("zowe.uss.ZoweUSSNode.open"); - expect(vscodeCommandSpy.mock.calls[0][1]).toEqual(ussFile); + expect(vscodeCommandSpy.mock.calls[0][0]).toEqual("vscode.open"); + expect(vscodeCommandSpy.mock.calls[0][1]).toEqual(ussFile.resourceUri); vscodeCommandSpy.mockClear(); }); @@ -596,36 +529,53 @@ describe("ZoweUSSNode Unit Tests - Function node.reopen()", () => { await rootNode.reopen(true); - expect(vscodeCommandSpy).toHaveBeenCalledWith("zowe.uss.ZoweUSSNode.open", rootNode); + expect(vscodeCommandSpy).toHaveBeenCalledWith("vscode.open", rootNode.resourceUri); vscodeCommandSpy.mockClear(); }); }); describe("ZoweUSSNode Unit Tests - Function node.setEncoding()", () => { + const setEncodingForFileMock = jest.spyOn(UssFSProvider.instance, "setEncodingForFile").mockImplementation(); + const getEncodingMock = jest.spyOn(UssFSProvider.instance as any, "getEncodingForFile"); + + afterAll(() => { + setEncodingForFileMock.mockRestore(); + }); + + afterEach(() => { + getEncodingMock.mockReset(); + }); + it("sets encoding to binary", () => { + const binaryEncoding = { kind: "binary" }; + getEncodingMock.mockReturnValue(binaryEncoding); const node = new ZoweUSSNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); - node.setEncoding({ kind: "binary" }); + node.setEncoding(binaryEncoding); expect(node.binary).toEqual(true); - expect(node.encoding).toBeUndefined(); + expect(setEncodingForFileMock).toHaveBeenCalledWith(node.resourceUri, binaryEncoding); expect(node.tooltip).toContain("Encoding: Binary"); expect(node.contextValue).toEqual(globals.USS_BINARY_FILE_CONTEXT); expect(JSON.stringify(node.iconPath)).toContain("document-binary.svg"); }); it("sets encoding to text", () => { + const textEncoding = { kind: "text" }; + getEncodingMock.mockReturnValue(textEncoding); const node = new ZoweUSSNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); - node.setEncoding({ kind: "text" }); + node.setEncoding(textEncoding); expect(node.binary).toEqual(false); - expect(node.encoding).toBeNull(); + expect(setEncodingForFileMock).toHaveBeenCalledWith(node.resourceUri, textEncoding); expect(node.tooltip).not.toContain("Encoding:"); expect(node.contextValue).toEqual(globals.USS_TEXT_FILE_CONTEXT); }); it("sets encoding to other codepage", () => { + const otherEncoding = { kind: "other", codepage: "IBM-1047" }; + getEncodingMock.mockReturnValue(otherEncoding); const node = new ZoweUSSNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); - node.setEncoding({ kind: "other", codepage: "IBM-1047" }); + node.setEncoding(otherEncoding); expect(node.binary).toEqual(false); - expect(node.encoding).toEqual("IBM-1047"); + expect(setEncodingForFileMock).toHaveBeenCalledWith(node.resourceUri, otherEncoding); expect(node.tooltip).toContain("Encoding: IBM-1047"); }); @@ -635,18 +585,21 @@ describe("ZoweUSSNode Unit Tests - Function node.setEncoding()", () => { collapsibleState: vscode.TreeItemCollapsibleState.Expanded, }); parentNode.contextValue = globals.FAV_PROFILE_CONTEXT; + const textEncoding = { kind: "text" }; + getEncodingMock.mockReturnValue(textEncoding); const node = new ZoweUSSNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode }); - node.setEncoding({ kind: "text" }); + node.setEncoding(textEncoding); expect(node.binary).toEqual(false); - expect(node.encoding).toBeNull(); + expect(setEncodingForFileMock).toHaveBeenCalledWith(node.resourceUri, textEncoding); expect(node.contextValue).toEqual(globals.USS_TEXT_FILE_CONTEXT + globals.FAV_SUFFIX); }); it("resets encoding to undefined", () => { const node = new ZoweUSSNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); + getEncodingMock.mockReturnValue(undefined); node.setEncoding(undefined as any); expect(node.binary).toEqual(false); - expect(node.encoding).toBeUndefined(); + expect(setEncodingForFileMock).toHaveBeenCalledWith(node.resourceUri, undefined); }); it("fails to set encoding for session node", () => { @@ -670,6 +623,7 @@ describe("ZoweUSSNode Unit Tests - Function node.deleteUSSNode()", () => { session: globalMocks.session, profile: globalMocks.profileOne, }), + fspDelete: jest.spyOn(UssFSProvider.instance, "delete").mockImplementation(), }; newMocks.ussNode = new ZoweUSSNode({ @@ -694,7 +648,7 @@ describe("ZoweUSSNode Unit Tests - Function node.deleteUSSNode()", () => { const blockMocks = await createBlockMocks(globalMocks); globalMocks.mockShowWarningMessage.mockResolvedValueOnce("Delete"); await blockMocks.ussNode.deleteUSSNode(blockMocks.testUSSTree, "", false); - expect(blockMocks.testUSSTree.refresh).toHaveBeenCalled(); + expect(blockMocks.testUSSTree.nodeDataChanged).toHaveBeenCalled(); }); it("Tests that node is not deleted if user did not verify", async () => { @@ -702,7 +656,7 @@ describe("ZoweUSSNode Unit Tests - Function node.deleteUSSNode()", () => { const blockMocks = await createBlockMocks(globalMocks); globalMocks.mockShowWarningMessage.mockResolvedValueOnce("Cancel"); await blockMocks.ussNode.deleteUSSNode(blockMocks.testUSSTree, "", true); - expect(blockMocks.testUSSTree.refresh).not.toHaveBeenCalled(); + expect(blockMocks.testUSSTree.nodeDataChanged).not.toHaveBeenCalled(); }); it("Tests that node is not deleted if user cancelled", async () => { @@ -710,14 +664,14 @@ describe("ZoweUSSNode Unit Tests - Function node.deleteUSSNode()", () => { const blockMocks = await createBlockMocks(globalMocks); globalMocks.mockShowWarningMessage.mockResolvedValueOnce(undefined); await blockMocks.ussNode.deleteUSSNode(blockMocks.testUSSTree, "", true); - expect(blockMocks.testUSSTree.refresh).not.toHaveBeenCalled(); + expect(blockMocks.testUSSTree.nodeDataChanged).not.toHaveBeenCalled(); }); it("Tests that node is not deleted if an error thrown", async () => { const globalMocks = await createGlobalMocks(); const blockMocks = await createBlockMocks(globalMocks); globalMocks.mockShowWarningMessage.mockResolvedValueOnce("Delete"); - globalMocks.ussFile.mockImplementationOnce(() => { + jest.spyOn(UssFSProvider.instance, "delete").mockImplementationOnce(() => { throw Error("testError"); }); @@ -785,9 +739,9 @@ describe("ZoweUSSNode Unit Tests - Function node.getChildren()", () => { }), ]; sampleChildren[1].command = { - command: "zowe.uss.ZoweUSSNode.open", + command: "vscode.open", title: "Open", - arguments: [sampleChildren[1]], + arguments: [sampleChildren[1].resourceUri], }; blockMocks.rootNode.children.push(sampleChildren[0]); @@ -883,34 +837,11 @@ describe("ZoweUSSNode Unit Tests - Function node.getChildren()", () => { blockMocks.childNode.fullPath = "Throw Error"; blockMocks.childNode.dirty = true; blockMocks.childNode.profile = globalMocks.profileOne; - - await blockMocks.childNode.getChildren(); - expect(globalMocks.showErrorMessage.mock.calls.length).toEqual(1); - expect(globalMocks.showErrorMessage.mock.calls[0][0]).toEqual( - "Retrieving response from uss-file-list Error: Throwing an error to check error handling for unit tests!" - ); - } - ); - - it( - "Tests that when bright.List returns an unsuccessful response, " + "node.getChildren() throws an error and the catch block is reached", - async () => { - const globalMocks = await createGlobalMocks(); - const blockMocks = await createBlockMocks(globalMocks); - - blockMocks.childNode.contextValue = globals.USS_SESSION_CONTEXT; - blockMocks.childNode.dirty = true; - blockMocks.childNode.profile = globalMocks.profileOne; - const subNode = new ZoweUSSNode({ - label: "Response Fail", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: blockMocks.childNode, - profile: globalMocks.profileOne, + jest.spyOn(UssFSProvider.instance, "listFiles").mockImplementation(() => { + throw new Error("Throwing an error to check error handling for unit tests!"); }); - subNode.fullPath = "THROW ERROR"; - subNode.dirty = true; - await subNode.getChildren(); + await blockMocks.childNode.getChildren(); expect(globalMocks.showErrorMessage.mock.calls.length).toEqual(1); expect(globalMocks.showErrorMessage.mock.calls[0][0]).toEqual( "Retrieving response from uss-file-list Error: Throwing an error to check error handling for unit tests!" @@ -924,15 +855,7 @@ describe("ZoweUSSNode Unit Tests - Function node.getChildren()", () => { blockMocks.rootNode.contextValue = globals.USS_SESSION_CONTEXT; blockMocks.rootNode.dirty = false; - - expect(await blockMocks.rootNode.getChildren()).toEqual([]); - }); - - it("Tests that when passing a globalMocks.session node with no hlq the node.getChildren() method is exited early", async () => { - const globalMocks = await createGlobalMocks(); - const blockMocks = await createBlockMocks(globalMocks); - - blockMocks.rootNode.contextValue = globals.USS_SESSION_CONTEXT; + blockMocks.rootNode.fullPath = "/some/path"; expect(await blockMocks.rootNode.getChildren()).toEqual([]); }); @@ -951,7 +874,9 @@ describe("ZoweUSSNode Unit Tests - Function node.openUSS()", () => { session: globalMocks.session, profile: globalMocks.profileOne, }), + initializeFileOpening: jest.spyOn(ZoweUSSNode.prototype, "initializeFileOpening"), }; + newMocks.initializeFileOpening.mockClear(); newMocks.testUSSTree = createUSSTree([], [newMocks.ussNode], createTreeView()); newMocks.dsNode = new ZoweUSSNode({ label: "testSess", @@ -964,7 +889,6 @@ describe("ZoweUSSNode Unit Tests - Function node.openUSS()", () => { globalMocks.createSessCfgFromArgs.mockReturnValue(globalMocks.session); globalMocks.ussFile.mockReturnValue(globalMocks.response); globalMocks.withProgress.mockReturnValue(globalMocks.response); - globalMocks.openTextDocument.mockResolvedValue("test.doc"); globalMocks.showInputBox.mockReturnValue("fake"); globals.defineGlobals("/test/path/"); @@ -1020,21 +944,13 @@ describe("ZoweUSSNode Unit Tests - Function node.openUSS()", () => { profile: globalMocks.profileOne, parentPath: "/", }); - globalMocks.existsSync.mockReturnValue(null); // Tests that correct file is downloaded await node.openUSS(false, true, blockMocks.testUSSTree); - expect(globalMocks.existsSync.mock.calls.length).toBe(1); - expect(globalMocks.existsSync.mock.calls[0][0]).toBe(path.join(globals.USS_DIR, node.getProfileName(), node.fullPath)); - expect(globalMocks.setStatusBarMessage).toHaveBeenCalledWith("$(sync~spin) Downloading USS file..."); + expect(globalMocks.setStatusBarMessage).toBeCalledWith("$(sync~spin) Downloading USS file..."); - // Tests that correct file is opened in editor - globalMocks.withProgress(globalMocks.downloadUSSFile); - expect(globalMocks.withProgress).toHaveBeenCalledWith(globalMocks.downloadUSSFile); - expect(globalMocks.openTextDocument.mock.calls.length).toBe(1); - expect(globalMocks.openTextDocument.mock.calls[0][0]).toBe(node.getUSSDocumentFilePath()); - expect(globalMocks.mockShowTextDocument.mock.calls.length).toBe(1); - expect(globalMocks.mockShowTextDocument.mock.calls[0][0]).toStrictEqual("test.doc"); + // Tests that correct URI is passed to initializeFileOpening + expect(blockMocks.initializeFileOpening).toHaveBeenCalledWith(node.resourceUri); }); it("Tests that node.openUSS() is executed successfully with Unverified profile", async () => { @@ -1062,20 +978,12 @@ describe("ZoweUSSNode Unit Tests - Function node.openUSS()", () => { profile: globalMocks.profileOne, parentPath: "/", }); - globalMocks.existsSync.mockReturnValue(null); // Tests that correct file is downloaded await node.openUSS(false, true, blockMocks.testUSSTree); - expect(globalMocks.existsSync.mock.calls.length).toBe(1); - expect(globalMocks.existsSync.mock.calls[0][0]).toBe(path.join(globals.USS_DIR, node.getProfileName(), node.fullPath)); + // Tests that correct URI is passed to initializeFileOpening + expect(blockMocks.initializeFileOpening).toHaveBeenCalledWith(node.resourceUri); expect(globalMocks.setStatusBarMessage).toHaveBeenCalledWith("$(sync~spin) Downloading USS file..."); - // Tests that correct file is opened in editor - globalMocks.withProgress(globalMocks.downloadUSSFile); - expect(globalMocks.withProgress).toHaveBeenCalledWith(globalMocks.downloadUSSFile); - expect(globalMocks.openTextDocument.mock.calls.length).toBe(1); - expect(globalMocks.openTextDocument.mock.calls[0][0]).toBe(node.getUSSDocumentFilePath()); - expect(globalMocks.mockShowTextDocument.mock.calls.length).toBe(1); - expect(globalMocks.mockShowTextDocument.mock.calls[0][0]).toStrictEqual("test.doc"); }); it("Tests that node.openUSS() fails when an error is thrown", async () => { @@ -1096,24 +1004,14 @@ describe("ZoweUSSNode Unit Tests - Function node.openUSS()", () => { profile: globalMocks.profileOne, parentPath: "/parent", }); - const mockFileInfo = { - name: child.label, - path: path.join(globals.USS_DIR, child.getProfileName(), child.fullPath), - }; - Object.defineProperty(LocalFileManagement, "downloadUnixFile", { value: jest.fn().mockResolvedValueOnce(mockFileInfo), configurable: true }); - globalMocks.mockShowTextDocument.mockRejectedValueOnce(Error("testError")); + blockMocks.initializeFileOpening.mockRejectedValueOnce(Error("Failed to open USS file")); try { await child.openUSS(false, true, blockMocks.testUSSTree); } catch (err) { // Prevent exception from failing test } - - expect(globalMocks.openTextDocument.mock.calls.length).toBe(1); - expect(globalMocks.openTextDocument.mock.calls[0][0]).toBe(path.join(globals.USS_DIR, child.getProfileName(), child.fullPath)); - expect(globalMocks.mockShowTextDocument.mock.calls.length).toBe(1); - expect(globalMocks.showErrorMessage.mock.calls.length).toBe(1); - expect(globalMocks.showErrorMessage.mock.calls[0][0]).toBe("Error: testError"); + expect(globalMocks.loggerError).toHaveBeenCalled(); }); it("Tests that node.openUSS() executes successfully for favorited file", async () => { @@ -1152,7 +1050,8 @@ describe("ZoweUSSNode Unit Tests - Function node.openUSS()", () => { // For each node, make sure that code below the log.debug statement is execute await favoriteFile.openUSS(false, true, blockMocks.testUSSTree); - expect(globalMocks.mockShowTextDocument.mock.calls.length).toBe(1); + expect(blockMocks.initializeFileOpening.mock.calls.length).toBe(1); + expect(blockMocks.initializeFileOpening).toHaveBeenCalledWith(favoriteFile.resourceUri); }); it("Tests that node.openUSS() executes successfully for child file of favorited directory", async () => { @@ -1187,8 +1086,7 @@ describe("ZoweUSSNode Unit Tests - Function node.openUSS()", () => { child.contextValue = globals.USS_TEXT_FILE_CONTEXT; await child.openUSS(false, true, blockMocks.testUSSTree); - expect(globalMocks.mockShowTextDocument.mock.calls.length).toBe(1); - globalMocks.mockShowTextDocument.mockReset(); + expect(blockMocks.initializeFileOpening).toHaveBeenCalledWith(child.resourceUri); }); it("Tests that node.openUSS() is executed successfully when chtag says binary", async () => { @@ -1196,7 +1094,6 @@ describe("ZoweUSSNode Unit Tests - Function node.openUSS()", () => { const blockMocks = await createBlockMocks(globalMocks); globalMocks.isFileTagBinOrAscii.mockResolvedValue(true); - globalMocks.existsSync.mockReturnValue(null); const node = new ZoweUSSNode({ label: "node", @@ -1209,15 +1106,8 @@ describe("ZoweUSSNode Unit Tests - Function node.openUSS()", () => { // Make sure correct file is downloaded await node.openUSS(false, true, blockMocks.testUSSTree); - expect(globalMocks.existsSync.mock.calls.length).toBe(1); - expect(globalMocks.existsSync.mock.calls[0][0]).toBe(path.join(globals.USS_DIR, node.getProfileName() || "", node.fullPath)); + expect(blockMocks.initializeFileOpening).toHaveBeenCalledWith(node.resourceUri); expect(globalMocks.setStatusBarMessage).toHaveBeenCalledWith("$(sync~spin) Downloading USS file..."); - // Make sure correct file is displayed in the editor - globalMocks.withProgress(globalMocks.downloadUSSFile); - expect(globalMocks.openTextDocument.mock.calls.length).toBe(1); - expect(globalMocks.openTextDocument.mock.calls[0][0]).toBe(node.getUSSDocumentFilePath()); - expect(globalMocks.mockShowTextDocument.mock.calls.length).toBe(1); - expect(globalMocks.mockShowTextDocument.mock.calls[0][0]).toStrictEqual("test.doc"); }); it("Tests that node.openUSS() fails when passed an invalid node", async () => { @@ -1238,60 +1128,9 @@ describe("ZoweUSSNode Unit Tests - Function node.openUSS()", () => { // Prevent exception from failing test } - expect(globalMocks.ussFile.mock.calls.length).toBe(0); - expect(globalMocks.showErrorMessage.mock.calls.length).toBe(2); + expect(blockMocks.initializeFileOpening.mock.calls.length).toBe(0); + expect(globalMocks.showErrorMessage.mock.calls.length).toBe(1); expect(globalMocks.showErrorMessage.mock.calls[0][0]).toBe("open() called from invalid node."); - expect(globalMocks.showErrorMessage.mock.calls[1][0]).toBe("Error: open() called from invalid node."); - }); -}); - -describe("ZoweUSSNode Unit Tests - Function node.isDirtyInEditor()", () => { - it("Tests that node.isDirtyInEditor() returns true if the file is open", async () => { - const globalMocks = await createGlobalMocks(); - - // Creating a test node - const rootNode = new ZoweUSSNode({ - label: "root", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - session: globalMocks.session, - profile: globalMocks.profileOne, - }); - rootNode.contextValue = globals.USS_SESSION_CONTEXT; - const testNode = new ZoweUSSNode({ - label: globals.DS_PDS_CONTEXT, - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: rootNode, - profile: globalMocks.profileOne, - }); - testNode.fullPath = "test/node"; - - const isDirty = testNode.isDirtyInEditor; - expect(isDirty).toBeTruthy(); - }); - - it("Tests that node.isDirtyInEditor() returns false if the file is not open", async () => { - const globalMocks = await createGlobalMocks(); - - globalMocks.mockTextDocuments.pop(); - - // Creating a test node - const rootNode = new ZoweUSSNode({ - label: "root", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - session: globalMocks.session, - profile: globalMocks.profileOne, - }); - rootNode.contextValue = globals.USS_SESSION_CONTEXT; - const testNode = new ZoweUSSNode({ - label: globals.DS_PDS_CONTEXT, - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: rootNode, - profile: globalMocks.profileOne, - }); - testNode.fullPath = "test/node"; - - const isDirty = testNode.isDirtyInEditor; - expect(isDirty).toBeFalsy(); }); }); @@ -1301,28 +1140,32 @@ describe("ZoweUSSNode Unit Tests - Function node.openedDocumentInstance()", () = // Creating a test node const rootNode = new ZoweUSSNode({ - label: "root", + label: "sestest", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, session: globalMocks.session, profile: globalMocks.profileOne, + contextOverride: globals.USS_SESSION_CONTEXT, + }); + rootNode.fullPath = "/path/to"; + rootNode.resourceUri = rootNode.resourceUri?.with({ + path: `/${getSessionLabel(rootNode)}/${rootNode.fullPath}`, }); - rootNode.contextValue = globals.USS_SESSION_CONTEXT; const testNode = new ZoweUSSNode({ - label: globals.DS_PDS_CONTEXT, + label: "node", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, parentNode: rootNode, profile: globalMocks.profileOne, + parentPath: rootNode.fullPath, }); - testNode.fullPath = "test/node"; const returnedDoc = testNode.openedDocumentInstance; expect(returnedDoc).toEqual(globalMocks.mockTextDocument); }); - it("Tests that node.openedDocumentInstance() returns null if the file is not open", async () => { + it("Tests that node.openedDocumentInstance() returns undefined if the file is not open", async () => { const globalMocks = await createGlobalMocks(); - globalMocks.mockTextDocuments.pop(); + globalMocks.textDocumentsArray.pop(); // Creating a test node const rootNode = new ZoweUSSNode({ @@ -1337,89 +1180,37 @@ describe("ZoweUSSNode Unit Tests - Function node.openedDocumentInstance()", () = collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, parentNode: rootNode, profile: globalMocks.profileOne, + parentPath: rootNode.fullPath, }); testNode.fullPath = "test/node"; const returnedDoc = testNode.openedDocumentInstance; - expect(returnedDoc).toBeNull(); + expect(returnedDoc).toBeUndefined(); }); }); describe("ZoweUSSNode Unit Tests - Function node.initializeFileOpening()", () => { - it("Tests that node.initializeFileOpening() successfully handles binary files that should be re-downloaded", async () => { - const globalMocks = await createGlobalMocks(); - - jest.spyOn(vscode.workspace, "openTextDocument").mockRejectedValue("Test error!"); - jest.spyOn(Gui, "errorMessage").mockResolvedValue("Re-download"); - - // Creating a test node - const rootNode = new ZoweUSSNode({ - label: "root", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - session: globalMocks.session, - profile: globalMocks.profileOne, - }); - rootNode.contextValue = globals.USS_SESSION_CONTEXT; - const testNode = new ZoweUSSNode({ - label: globals.DS_PDS_CONTEXT, - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: rootNode, - profile: globalMocks.profileOne, - }); - testNode.fullPath = "test/node"; - - await testNode.initializeFileOpening(testNode.fullPath); - expect(globalMocks.mockExecuteCommand).toHaveBeenCalledWith("zowe.uss.binary", testNode); - }); - - it("Tests that node.initializeFileOpening() successfully handles text files that should be previewed", async () => { - const globalMocks = await createGlobalMocks(); - - jest.spyOn(vscode.workspace, "openTextDocument").mockResolvedValue(globalMocks.mockTextDocument as vscode.TextDocument); - - // Creating a test node - const rootNode = new ZoweUSSNode({ - label: "root", - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - session: globalMocks.session, - profile: globalMocks.profileOne, - }); - rootNode.contextValue = globals.USS_SESSION_CONTEXT; - const testNode = new ZoweUSSNode({ - label: globals.DS_PDS_CONTEXT, - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: rootNode, - profile: globalMocks.profileOne, - }); - testNode.fullPath = "test/node"; - - await testNode.initializeFileOpening(testNode.fullPath, true); - expect(globalMocks.mockShowTextDocument).toHaveBeenCalledWith(globalMocks.mockTextDocument); - }); - - it("Tests that node.initializeFileOpening() successfully handles text files that shouldn't be previewed", async () => { + it("Tests that node.initializeFileOpening() successfully handles USS files", async () => { const globalMocks = await createGlobalMocks(); - jest.spyOn(vscode.workspace, "openTextDocument").mockResolvedValue(globalMocks.mockTextDocument as vscode.TextDocument); - // Creating a test node const rootNode = new ZoweUSSNode({ - label: "root", + label: "sestest", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, session: globalMocks.session, profile: globalMocks.profileOne, + contextOverride: globals.USS_SESSION_CONTEXT, }); - rootNode.contextValue = globals.USS_SESSION_CONTEXT; const testNode = new ZoweUSSNode({ - label: globals.DS_PDS_CONTEXT, + label: "node", collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, parentNode: rootNode, profile: globalMocks.profileOne, + parentPath: "/test", }); - testNode.fullPath = "test/node"; - await testNode.initializeFileOpening(testNode.fullPath, false); - expect(globalMocks.mockShowTextDocument).toHaveBeenCalledWith(globalMocks.mockTextDocument, { preview: false }); + await testNode.initializeFileOpening(testNode.resourceUri); + expect(globalMocks.mockExecuteCommand).toHaveBeenCalledWith("vscode.open", testNode.resourceUri); }); }); @@ -1495,7 +1286,7 @@ describe("ZoweUSSNode Unit Tests - Function node.pasteUssTree()", () => { mockUssApi: ZoweExplorerApiRegister.getUssApi(createIProfileFakeEncoding()), getUssApiMock: jest.fn(), testNode: testNode, - pasteSpy: jest.spyOn(testNode, "paste"), + pasteSpy: jest.spyOn(ZoweUSSNode.prototype, "paste"), }; newMocks.testNode.fullPath = "/users/temp/test"; @@ -1523,81 +1314,6 @@ describe("ZoweUSSNode Unit Tests - Function node.pasteUssTree()", () => { return newMocks; } - it("Tests node.pasteUssTree() reads clipboard contents and uploads files successfully", async () => { - const globalMocks = await createGlobalMocks(); - const blockMocks = createBlockMocks(globalMocks); - blockMocks.pasteSpy.mockClear(); - - jest.spyOn(blockMocks.mockUssApi, "fileList").mockResolvedValue(blockMocks.fileResponse); - jest.spyOn(blockMocks.mockUssApi, "putContent").mockResolvedValue(blockMocks.fileResponse); - jest.spyOn(blockMocks.mockUssApi, "uploadDirectory").mockResolvedValue(blockMocks.fileResponse); - const mockCopyApi = jest.spyOn(blockMocks.mockUssApi, "copy").mockResolvedValue(Buffer.from("")); - - // Scenario 1: multiple files, pasting within same session (use new copy API) - const mockToSameSession = jest.spyOn(UssFileUtils, "toSameSession").mockReturnValue(true); - await blockMocks.testNode.pasteUssTree(); - expect(blockMocks.pasteSpy).toHaveBeenCalledTimes(3); - expect(mockCopyApi).toHaveBeenCalledTimes(3); - - blockMocks.pasteSpy.mockClear(); - mockCopyApi.mockClear(); - - // Scenario 2: copying multiple files between two sessions (should fallback to create/putContent APIs) - mockToSameSession.mockReturnValue(false); - await blockMocks.testNode.pasteUssTree(); - expect(blockMocks.pasteSpy).toHaveBeenCalledTimes(3); - expect(blockMocks.mockUssApi.putContent).toHaveBeenCalledTimes(3); - - // Scenario 3: a directory, pasting within same session - globalMocks.readText.mockResolvedValue( - JSON.stringify({ - children: [ - { - ussPath: "/path/folder1", - type: UssFileType.Directory, - }, - ], - }) - ); - - mockToSameSession.mockReturnValue(true); - jest.spyOn(blockMocks.mockUssApi, "fileList").mockResolvedValueOnce(blockMocks.fileRespWithFolder); - jest.spyOn(blockMocks.mockUssApi, "putContent").mockResolvedValueOnce(blockMocks.fileRespWithFolder); - blockMocks.pasteSpy.mockClear(); - - await blockMocks.testNode.pasteUssTree(); - // Only one paste operation is needed, copy API handles recursion out-of-the-box - expect(blockMocks.pasteSpy).toHaveBeenCalledTimes(1); - expect(mockCopyApi).toHaveBeenCalledTimes(1); - }); - - it("paste renames duplicate files before copying when needed", async () => { - const globalMocks = await createGlobalMocks(); - const blockMocks = createBlockMocks(globalMocks); - blockMocks.pasteSpy.mockClear(); - - const example_tree = { - baseName: "testFile", - ussPath: "/path/testFile", - type: UssFileType.File, - children: [], - }; - - blockMocks.testNode.fullPath = "/path/testFile"; - - jest.spyOn(blockMocks.mockUssApi, "fileList").mockResolvedValueOnce(blockMocks.fileResponse); - const copyMock = jest.spyOn(blockMocks.mockUssApi, "copy").mockResolvedValueOnce(blockMocks.fileResponse); - - // Testing paste within same session (copy API) - jest.spyOn(UssFileUtils, "toSameSession").mockReturnValueOnce(true); - await blockMocks.testNode.paste("example_session", "/path", { tree: example_tree, api: blockMocks.mockUssApi }); - - expect(copyMock).toHaveBeenCalledWith("/path/testFile (1)", { - from: "/path/testFile", - recursive: false, - }); - }); - it("Tests node.pasteUssTree() reads clipboard contents finds same file name on destination directory", async () => { const globalMocks = await createGlobalMocks(); const blockMocks = createBlockMocks(globalMocks); @@ -1633,6 +1349,7 @@ describe("ZoweUSSNode Unit Tests - Function node.pasteUssTree()", () => { const fileListSpy = jest.spyOn(blockMocks.mockUssApi, "fileList"); fileListSpy.mockClear(); const pasteSpy = jest.spyOn(blockMocks.testNode, "paste"); + pasteSpy.mockClear(); expect(await blockMocks.testNode.pasteUssTree()).toEqual(undefined); expect(pasteSpy).not.toHaveBeenCalled(); diff --git a/packages/zowe-explorer/__tests__/__unit__/uss/__snapshots__/ZoweUSSNode.unit.test.ts.snap b/packages/zowe-explorer/__tests__/__unit__/uss/__snapshots__/ZoweUSSNode.unit.test.ts.snap index 55102fbbcb..dfdeb64764 100644 --- a/packages/zowe-explorer/__tests__/__unit__/uss/__snapshots__/ZoweUSSNode.unit.test.ts.snap +++ b/packages/zowe-explorer/__tests__/__unit__/uss/__snapshots__/ZoweUSSNode.unit.test.ts.snap @@ -11,7 +11,7 @@ ZoweUSSNode { "downloadedTime": null, "encodingMap": {}, "etag": "", - "fullPath": "", + "fullPath": "/testDir/testFile", "iconPath": "Ref: 'document.svg'", "label": "testFile", "mParent": ZoweUSSNode { @@ -24,7 +24,7 @@ ZoweUSSNode { "downloadedTime": null, "encodingMap": {}, "etag": "", - "fullPath": "", + "fullPath": "/testDir", "iconPath": "Ref: 'folder.svg'", "label": "testDir", "mParent": ZoweUSSNode { @@ -37,13 +37,13 @@ ZoweUSSNode { "downloadedTime": null, "encodingMap": {}, "etag": "", - "fullPath": "", + "fullPath": "/", "iconPath": "Ref: 'folder.svg'", - "label": "root", + "id": "uss.sestest", + "label": "sestest", "mParent": undefined, "onUpdateEmitter": EventEmitter {}, "parentPath": undefined, - "prevPath": "", "profile": { "failNotFound": false, "message": "", @@ -57,6 +57,10 @@ ZoweUSSNode { }, "type": "zosmf", }, + "resourceUri": Uri { + "path": "/sestest", + "scheme": "zowe-uss", + }, "session": Session { "ISession": { "hostname": "fake", @@ -70,8 +74,7 @@ ZoweUSSNode { "shortLabel": "", }, "onUpdateEmitter": EventEmitter {}, - "parentPath": undefined, - "prevPath": "", + "parentPath": "/", "profile": { "failNotFound": false, "message": "", @@ -85,12 +88,16 @@ ZoweUSSNode { }, "type": "zosmf", }, + "resourceUri": Uri { + "path": "/sestest/testDir", + "scheme": "zowe-uss", + }, "session": undefined, "shortLabel": "", + "tooltip": "/testDir", }, "onUpdateEmitter": EventEmitter {}, - "parentPath": undefined, - "prevPath": "", + "parentPath": "/testDir", "profile": { "failNotFound": false, "message": "", @@ -104,7 +111,12 @@ ZoweUSSNode { }, "type": "zosmf", }, + "resourceUri": Uri { + "path": "/sestest/testDir/testFile", + "scheme": "zowe-uss", + }, "session": undefined, "shortLabel": "", + "tooltip": "/testDir/testFile", } `; diff --git a/packages/zowe-explorer/__tests__/__unit__/uss/actions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/uss/actions.unit.test.ts index d2d1d340ea..1fa3be0139 100644 --- a/packages/zowe-explorer/__tests__/__unit__/uss/actions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/uss/actions.unit.test.ts @@ -23,7 +23,6 @@ import { createTextDocument, createFileResponse, createValidIProfile, - createInstanceOfProfile, } from "../../../__mocks__/mockCreators/shared"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; import { Profiles } from "../../../src/Profiles"; @@ -40,7 +39,7 @@ import * as refreshActions from "../../../src/shared/refresh"; import { ZoweLogger } from "../../../src/utils/ZoweLogger"; import { ZoweLocalStorage } from "../../../src/utils/ZoweLocalStorage"; import { AttributeView } from "../../../src/uss/AttributeView"; -import { mocked } from "../../../__mocks__/mockUtils"; +import { UssFSProvider } from "../../../src/uss/UssFSProvider"; jest.mock("../../../src/utils/ZoweLogger"); @@ -79,12 +78,16 @@ function createGlobalMocks() { Notification: 15, }; }), + FileSystemProvider: { + createDirectory: jest.fn(), + }, }; globalMocks.mockLoadNamedProfile.mockReturnValue(globalMocks.testProfile); globals.defineGlobals(""); const profilesForValidation = { status: "active", name: "fake" }; + jest.spyOn(UssFSProvider.instance, "createDirectory").mockImplementation(globalMocks.FileSystemProvider.createDirectory); Object.defineProperty(Gui, "setStatusBarMessage", { value: globalMocks.setStatusBarMessage, configurable: true }); Object.defineProperty(vscode.window, "showInputBox", { value: globalMocks.mockShowInputBox, configurable: true }); Object.defineProperty(vscode.window, "showQuickPick", { value: globalMocks.showQuickPick, configurable: true }); @@ -403,170 +406,6 @@ describe("USS Action Unit Tests - Function copyPath", () => { }); }); -describe("USS Action Unit Tests - Function saveUSSFile", () => { - async function createBlockMocks(globalMocks) { - const newMocks = { - node: null, - mockGetEtag: null, - profileInstance: createInstanceOfProfile(globalMocks.testProfile), - testUSSTree: null, - testResponse: createFileResponse({ items: [] }), - testDoc: createTextDocument(path.join(globals.USS_DIR, "usstest", "u", "myuser", "testFile")), - ussNode: createUSSNode(globalMocks.testSession, createIProfile()), - putUSSPayload: jest.fn().mockResolvedValue(`{"stdout":[""]}`), - }; - - newMocks.node = new ZoweUSSNode({ - label: "u/myuser/testFile", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: newMocks.ussNode, - parentPath: "/", - }); - newMocks.ussNode.children.push(newMocks.node); - newMocks.testUSSTree = createUSSTree( - [createFavoriteUSSNode(globalMocks.testSession, globalMocks.testProfile)], - [newMocks.ussNode], - createTreeView() - ); - newMocks.mockGetEtag = jest.spyOn(newMocks.node, "getEtag").mockImplementation(() => "123"); - - Object.defineProperty(globalMocks.Utilities, "putUSSPayload", { - value: newMocks.putUSSPayload, - configurable: true, - }); - - return newMocks; - } - - it("To check Compare Function is getting triggered from Favorites", async () => { - const globalMocks = createGlobalMocks(); - const blockMocks = await createBlockMocks(globalMocks); - - // Create nodes for Session section - const node = new ZoweUSSNode({ - label: "HLQ.TEST.AFILE", - collapsibleState: vscode.TreeItemCollapsibleState.Expanded, - parentNode: blockMocks.node, - parentPath: "/", - }); - const childNode = new ZoweUSSNode({ - label: "MEM", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: node, - parentPath: "/", - }); - node.children.push(childNode); - blockMocks.testUSSTree.mSessionNodes.find((child) => child.label.toString().trim() === "usstest").children.push(node); - - // Create nodes for Favorites section - const favProfileNode = new ZoweUSSNode({ - label: "usstest", - collapsibleState: vscode.TreeItemCollapsibleState.Expanded, - parentNode: blockMocks.node, - parentPath: "/", - }); - const favoriteNode = new ZoweUSSNode({ - label: "HLQ.TEST.AFILE", - collapsibleState: vscode.TreeItemCollapsibleState.Expanded, - parentNode: favProfileNode, - parentPath: "/", - }); - const favoriteChildNode = new ZoweUSSNode({ - label: "MEM", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: favoriteNode, - parentPath: "/", - }); - favoriteNode.children.push(favoriteChildNode); - blockMocks.testUSSTree.mFavorites.push(favProfileNode); - blockMocks.testUSSTree.mFavorites[0].children.push(favoriteNode); - mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([favoriteNode, favoriteChildNode]); - - const testDocument = createTextDocument("HLQ.TEST.AFILE(MEM)", blockMocks.ussNode); - jest.spyOn(favoriteChildNode, "getEtag").mockImplementation(() => "123"); - (testDocument as any).fileName = path.join(globals.USS_DIR, "usstest/user/usstest/HLQ.TEST.AFILE/MEM"); - - await ussNodeActions.saveUSSFile(testDocument, blockMocks.testUSSTree); - - expect(mocked(sharedUtils.concatChildNodes)).toHaveBeenCalled(); - }); - - it("Testing that saveUSSFile is executed successfully", async () => { - const globalMocks = createGlobalMocks(); - const blockMocks = await createBlockMocks(globalMocks); - - globalMocks.withProgress.mockImplementation((progLocation, callback) => callback()); - globalMocks.fileToUSSFile.mockResolvedValue(blockMocks.testResponse); - globalMocks.concatChildNodes.mockReturnValue([blockMocks.ussNode.children[0]]); - blockMocks.testResponse.apiResponse.items = [{ name: "testFile", mode: "-rwxrwx" }]; - blockMocks.testResponse.success = true; - - globalMocks.fileList.mockResolvedValueOnce(blockMocks.testResponse); - globalMocks.withProgress.mockReturnValueOnce(blockMocks.testResponse); - blockMocks.testUSSTree.getChildren.mockReturnValueOnce([ - new ZoweUSSNode({ - label: "testFile", - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: blockMocks.ussNode, - parentPath: "/", - }), - globalMocks.testSession, - ]); - - await ussNodeActions.saveUSSFile(blockMocks.testDoc, blockMocks.testUSSTree); - expect(globalMocks.concatChildNodes.mock.calls.length).toBe(1); - expect(blockMocks.mockGetEtag).toHaveBeenCalledTimes(1); - expect(blockMocks.mockGetEtag).toReturnWith("123"); - }); - - it("Tests that saveUSSFile fails when save fails", async () => { - const globalMocks = createGlobalMocks(); - const blockMocks = await createBlockMocks(globalMocks); - - globalMocks.withProgress.mockImplementation((progLocation, callback) => callback()); - globalMocks.fileToUSSFile.mockResolvedValue(blockMocks.testResponse); - globalMocks.concatChildNodes.mockReturnValue([blockMocks.ussNode.children[0]]); - blockMocks.testResponse.success = false; - blockMocks.testResponse.commandResponse = "Save failed"; - - globalMocks.withProgress.mockReturnValueOnce(blockMocks.testResponse); - - await ussNodeActions.saveUSSFile(blockMocks.testDoc, blockMocks.testUSSTree); - expect(globalMocks.showErrorMessage.mock.calls.length).toBe(1); - expect(globalMocks.showErrorMessage.mock.calls[0][0]).toBe("Save failed"); - expect(mocked(vscode.workspace.applyEdit)).toHaveBeenCalledTimes(2); - }); - - it("Tests that saveUSSFile fails when error occurs", async () => { - const globalMocks = createGlobalMocks(); - const blockMocks = await createBlockMocks(globalMocks); - - globalMocks.withProgress.mockImplementation((progLocation, callback) => callback()); - globalMocks.fileToUSSFile.mockResolvedValue(blockMocks.testResponse); - globalMocks.concatChildNodes.mockReturnValue([blockMocks.ussNode.children[0]]); - globalMocks.withProgress.mockRejectedValueOnce(Error("Test Error")); - - await ussNodeActions.saveUSSFile(blockMocks.testDoc, blockMocks.testUSSTree); - expect(globalMocks.showErrorMessage.mock.calls.length).toBe(1); - expect(globalMocks.showErrorMessage.mock.calls[0][0]).toBe("Error: Test Error"); - expect(mocked(vscode.workspace.applyEdit)).toHaveBeenCalledTimes(2); - }); - - it("Tests that saveUSSFile fails when session cannot be located", async () => { - const globalMocks = createGlobalMocks(); - const blockMocks = await createBlockMocks(globalMocks); - - blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(undefined); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const testDocument = createTextDocument("u/myuser/testFile", blockMocks.node); - (testDocument as any).fileName = path.join(globals.USS_DIR, testDocument.fileName); - - await ussNodeActions.saveUSSFile(testDocument, blockMocks.testUSSTree); - expect(globalMocks.showErrorMessage.mock.calls.length).toBe(1); - expect(globalMocks.showErrorMessage.mock.calls[0][0]).toBe("Could not locate session when saving USS file."); - }); -}); - describe("USS Action Unit Tests - Functions uploadDialog & uploadFile", () => { async function createBlockMocks(globalMocks) { const newMocks = { @@ -607,7 +446,7 @@ describe("USS Action Unit Tests - Functions uploadDialog & uploadFile", () => { await ussNodeActions.uploadDialog(blockMocks.ussNode, blockMocks.testUSSTree); expect(globalMocks.showOpenDialog).toHaveBeenCalled(); expect(globalMocks.openTextDocument).toHaveBeenCalled(); - expect(blockMocks.testUSSTree.refresh).toHaveBeenCalled(); + expect(blockMocks.testUSSTree.refreshElement).toHaveBeenCalled(); }); it("Tests that uploadDialog() works for binary file", async () => { @@ -621,7 +460,7 @@ describe("USS Action Unit Tests - Functions uploadDialog & uploadFile", () => { await ussNodeActions.uploadDialog(blockMocks.ussNode, blockMocks.testUSSTree); expect(globalMocks.showOpenDialog).toHaveBeenCalled(); - expect(blockMocks.testUSSTree.refresh).toHaveBeenCalled(); + expect(blockMocks.testUSSTree.refreshElement).toHaveBeenCalled(); }); it("Tests that uploadDialog() throws an error successfully", async () => { @@ -785,28 +624,21 @@ describe("USS Action Unit Tests - copy file / directory", () => { expect(isSameSession).toBe(true); }); - it("paste calls relevant USS API functions", async () => { + it("paste calls relevant function in FileSystemProvider", async () => { const globalMocks = createGlobalMocks(); const blockMocks = await createBlockMocks(globalMocks); const rootTree: UssFileTree = { children: [], baseName: blockMocks.nodes[1].getLabel() as string, - ussPath: "", + ussPath: "/", sessionName: blockMocks.treeNodes.ussNode.getLabel() as string, type: UssFileType.Directory, + localUri: blockMocks.nodes[1].resourceUri, }; - blockMocks.treeNodes.ussApi.fileList = jest.fn().mockResolvedValue({ - apiResponse: { - items: [blockMocks.nodes[0].getLabel() as string, blockMocks.nodes[1].getLabel() as string], - }, - }); - blockMocks.treeNodes.ussApi.copy = jest.fn(); - await blockMocks.nodes[1].paste(rootTree.sessionName, rootTree.ussPath, { tree: rootTree, api: blockMocks.treeNodes.ussApi }); - expect(blockMocks.treeNodes.ussApi.fileList).toHaveBeenCalled(); - expect(blockMocks.treeNodes.ussApi.copy).toHaveBeenCalledWith(`/${blockMocks.nodes[1].getLabel()}`, { - from: "", - recursive: true, - }); + + const copySpy = jest.spyOn(UssFSProvider.instance, "copy").mockImplementation(); + await blockMocks.nodes[1].paste(blockMocks.nodes[1].resourceUri, { tree: rootTree, api: { copy: jest.fn(), fileList: jest.fn() } }); + expect(copySpy).toHaveBeenCalled(); }); it("paste throws an error if required APIs are not available", async () => { @@ -815,7 +647,7 @@ describe("USS Action Unit Tests - copy file / directory", () => { const rootTree: UssFileTree = { children: [], baseName: blockMocks.nodes[1].getLabel() as string, - ussPath: "", + ussPath: "/", sessionName: blockMocks.treeNodes.ussNode.getLabel() as string, type: UssFileType.Directory, }; @@ -823,20 +655,21 @@ describe("USS Action Unit Tests - copy file / directory", () => { const originalFileList = blockMocks.treeNodes.ussApi.fileList; blockMocks.treeNodes.ussApi.copy = blockMocks.treeNodes.ussApi.fileList = undefined; try { - await blockMocks.nodes[1].paste(rootTree.sessionName, rootTree.ussPath, { tree: rootTree, api: blockMocks.treeNodes.ussApi }); + await blockMocks.nodes[1].paste(blockMocks.nodes[1].resourceUri, { tree: rootTree, api: blockMocks.treeNodes.ussApi }); } catch (err) { expect(err).toBeDefined(); - expect(err.message).toBe("Required API functions for pasting (fileList, copy and/or putContent) were not found."); + expect(err.message).toBe("Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found."); } - // Test for putContent also being undefined + // Test for uploadFromBuffer also being undefined blockMocks.treeNodes.ussApi.fileList = originalFileList; - blockMocks.treeNodes.ussApi.putContent = undefined; + blockMocks.treeNodes.ussApi.copy = jest.fn(); + blockMocks.treeNodes.ussApi.uploadFromBuffer = undefined; try { - await blockMocks.nodes[1].paste(rootTree.sessionName, rootTree.ussPath, { tree: rootTree, api: blockMocks.treeNodes.ussApi }); + await blockMocks.nodes[1].paste(blockMocks.nodes[1].resourceUri, { tree: rootTree, api: blockMocks.treeNodes.ussApi }); } catch (err) { expect(err).toBeDefined(); - expect(err.message).toBe("Required API functions for pasting (fileList, copy and/or putContent) were not found."); + expect(err.message).toBe("Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found."); } }); diff --git a/packages/zowe-explorer/__tests__/__unit__/uss/fs/UssFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/uss/fs/UssFSProvider.unit.test.ts new file mode 100644 index 0000000000..2534f113ea --- /dev/null +++ b/packages/zowe-explorer/__tests__/__unit__/uss/fs/UssFSProvider.unit.test.ts @@ -0,0 +1,1092 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { Disposable, FilePermission, FileType, TextEditor, Uri } from "vscode"; +import { UssFSProvider } from "../../../../src/uss/UssFSProvider"; +import { createIProfile } from "../../../../__mocks__/mockCreators/shared"; +import { ZoweExplorerApiRegister } from "../../../../src/ZoweExplorerApiRegister"; +import { BaseProvider, DirEntry, FileEntry, Gui, UssFile, ZoweScheme } from "@zowe/zowe-explorer-api"; +import { Profiles } from "../../../../src/Profiles"; +import { UssFileType } from "../../../../src/uss/FileStructure"; + +const testProfile = createIProfile(); +const testProfileB = { ...createIProfile(), name: "sestest2", profile: { ...testProfile.profile, host: "fake2" } }; + +type TestUris = Record>; +const testUris: TestUris = { + conflictFile: Uri.from({ scheme: ZoweScheme.USS, path: "/sestest/aFile.txt", query: "conflict=true" }), + file: Uri.from({ scheme: ZoweScheme.USS, path: "/sestest/aFile.txt" }), + folder: Uri.from({ scheme: ZoweScheme.USS, path: "/sestest/aFolder" }), + session: Uri.from({ scheme: ZoweScheme.USS, path: "/sestest" }), +}; + +const testEntries = { + file: { + name: "aFile.txt", + conflictData: { + contents: new Uint8Array([4, 5, 6]), + etag: undefined, + size: 3, + }, + data: new Uint8Array([1, 2, 3]), + etag: "A123SEEMINGLY456RANDOM789ETAG", + metadata: { + profile: { name: "sestest" } as any, + path: "/aFile.txt", + }, + type: FileType.File, + wasAccessed: true, + } as FileEntry, + folder: { + name: "aFolder", + entries: new Map(), + metadata: { + profile: { name: "sestest" } as any, + path: "/aFolder", + }, + type: FileType.Directory, + wasAccessed: false, + } as DirEntry, + session: { + name: "sestest", + entries: new Map(), + metadata: { + profile: { name: "sestest" } as any, + path: "/", + }, + size: 0, + type: FileType.Directory, + wasAccessed: false, + } as DirEntry, +}; + +describe("stat", () => { + const lookupMock = jest.spyOn((UssFSProvider as any).prototype, "_lookup"); + + it("returns a file entry", () => { + lookupMock.mockReturnValueOnce(testEntries.file); + expect(UssFSProvider.instance.stat(testUris.file)).toStrictEqual(testEntries.file); + expect(lookupMock).toHaveBeenCalledWith(testUris.file, false); + }); + it("returns a file as 'read-only' when query has conflict parameter", () => { + lookupMock.mockReturnValueOnce(testEntries.file); + expect(UssFSProvider.instance.stat(testUris.conflictFile)).toStrictEqual({ ...testEntries.file, permissions: FilePermission.Readonly }); + expect(lookupMock).toHaveBeenCalledWith(testUris.conflictFile, false); + }); +}); + +describe("move", () => { + const getInfoFromUriMock = jest.spyOn((UssFSProvider as any).prototype, "_getInfoFromUri"); + const newUri = testUris.file.with({ path: "/sestest/aFile2.txt" }); + + it("returns true if it successfully moved a valid, old URI to the new URI", async () => { + getInfoFromUriMock + .mockReturnValueOnce({ + // info for new URI + path: "/aFile2.txt", + profile: testProfile, + }) + .mockReturnValueOnce({ + // info about old URI + path: "/aFile.txt", + profile: testProfile, + }); + const moveStub = jest.fn(); + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ + move: moveStub, + } as any); + const relocateEntryMock = jest.spyOn((UssFSProvider as any).prototype, "_relocateEntry").mockResolvedValueOnce(undefined); + expect(await UssFSProvider.instance.move(testUris.file, newUri)).toBe(true); + expect(getInfoFromUriMock).toHaveBeenCalledTimes(2); + expect(moveStub).toHaveBeenCalledWith("/aFile.txt", "/aFile2.txt"); + expect(relocateEntryMock).toHaveBeenCalledWith(testUris.file, newUri, "/aFile2.txt"); + }); + it("returns false if the 'move' API is not implemented", async () => { + getInfoFromUriMock.mockReturnValueOnce({ + // info for new URI + path: "/aFile2.txt", + profile: testProfile, + }); + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({} as any); + const errorMsgMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce(undefined); + expect(await UssFSProvider.instance.move(testUris.file, newUri)).toBe(false); + expect(errorMsgMock).toHaveBeenCalledWith("The 'move' function is not implemented for this USS API."); + }); +}); + +describe("listFiles", () => { + it("throws an error when called with a URI with an empty path", async () => { + await expect( + UssFSProvider.instance.listFiles( + testProfile, + Uri.from({ + scheme: ZoweScheme.USS, + path: "", + }) + ) + ).rejects.toThrow("Could not list USS files: Empty path provided in URI"); + }); + it("removes '.', '..', and '...' from IZosFilesResponse items when successful", async () => { + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ + fileList: jest.fn().mockResolvedValueOnce({ + success: true, + commandResponse: "", + apiResponse: { + items: [{ name: "." }, { name: ".." }, { name: "..." }, { name: "test.txt" }], + }, + }), + } as any); + expect(await UssFSProvider.instance.listFiles(testProfile, testUris.folder)).toStrictEqual({ + success: true, + commandResponse: "", + apiResponse: { + items: [{ name: "test.txt" }], + }, + }); + }); + it("properly returns an unsuccessful response", async () => { + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ + fileList: jest.fn().mockResolvedValueOnce({ + success: false, + commandResponse: "", + apiResponse: {}, + }), + } as any); + expect(await UssFSProvider.instance.listFiles(testProfile, testUris.folder)).toStrictEqual({ + success: false, + commandResponse: "", + apiResponse: { + items: [], + }, + }); + }); +}); + +describe("readDirectory", () => { + it("returns the correct list of entries inside a folder", async () => { + const lookupAsDirMock = jest.spyOn((UssFSProvider as any).prototype, "_lookupAsDirectory").mockReturnValueOnce(testEntries.folder); + const listFilesMock = jest.spyOn(UssFSProvider.instance, "listFiles").mockResolvedValueOnce({ + success: true, + commandResponse: "", + apiResponse: { + items: [ + { name: "test.txt", mode: "-rwxrwxrwx" }, + { name: "innerFolder", mode: "drwxrwxrwx" }, + ], + }, + }); + + expect(await UssFSProvider.instance.readDirectory(testUris.folder)).toStrictEqual([ + ["test.txt", FileType.File], + ["innerFolder", FileType.Directory], + ]); + lookupAsDirMock.mockRestore(); + listFilesMock.mockRestore(); + }); +}); + +describe("fetchFileAtUri", () => { + it("calls getContents to get the data for a file entry", async () => { + const fileEntry = { ...testEntries.file }; + const lookupAsFileMock = jest.spyOn((UssFSProvider as any).prototype, "_lookupAsFile").mockReturnValueOnce(fileEntry); + const exampleData = "hello world!"; + const autoDetectEncodingMock = jest.spyOn(UssFSProvider.instance, "autoDetectEncoding").mockImplementation(); + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ + getContents: jest.fn().mockImplementationOnce((filePath, opts) => { + opts.stream.write(exampleData); + return { + apiResponse: { + etag: "123abc", + }, + }; + }), + } as any); + + await UssFSProvider.instance.fetchFileAtUri(testUris.file); + + expect(lookupAsFileMock).toHaveBeenCalledWith(testUris.file); + expect(autoDetectEncodingMock).toHaveBeenCalledWith(fileEntry); + expect(fileEntry.data.toString()).toBe(exampleData); + expect(fileEntry.etag).toBe("123abc"); + expect(fileEntry.data.byteLength).toBe(exampleData.length); + autoDetectEncodingMock.mockRestore(); + }); + it("assigns conflictData if the 'isConflict' option is specified", async () => { + const fileEntry = { ...testEntries.file }; + const lookupAsFileMock = jest.spyOn((UssFSProvider as any).prototype, "_lookupAsFile").mockReturnValueOnce(fileEntry); + const exampleData = ""; + const autoDetectEncodingMock = jest.spyOn(UssFSProvider.instance, "autoDetectEncoding").mockImplementation(); + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ + getContents: jest.fn().mockImplementationOnce((filePath, opts) => { + opts.stream.write(exampleData); + return { + apiResponse: { + etag: "321cba", + }, + }; + }), + } as any); + + await UssFSProvider.instance.fetchFileAtUri(testUris.file, { isConflict: true }); + + expect(lookupAsFileMock).toHaveBeenCalledWith(testUris.file); + expect(autoDetectEncodingMock).toHaveBeenCalledWith(fileEntry); + expect(fileEntry.conflictData?.contents.toString()).toBe(exampleData); + expect(fileEntry.conflictData?.etag).toBe("321cba"); + expect(fileEntry.conflictData?.contents.byteLength).toBe(exampleData.length); + autoDetectEncodingMock.mockRestore(); + }); + it("calls '_updateResourceInEditor' if the 'editor' option is specified", async () => { + const fileEntry = { ...testEntries.file }; + const lookupAsFileMock = jest.spyOn((UssFSProvider as any).prototype, "_lookupAsFile").mockReturnValueOnce(fileEntry); + const autoDetectEncodingMock = jest.spyOn(UssFSProvider.instance, "autoDetectEncoding").mockImplementation(); + const exampleData = "hello world!"; + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ + getContents: jest.fn().mockImplementationOnce((filePath, opts) => { + opts.stream.write(exampleData); + return { + apiResponse: { + etag: "123abc", + }, + }; + }), + } as any); + + const _updateResourceInEditorMock = jest.spyOn((UssFSProvider as any).prototype, "_updateResourceInEditor").mockResolvedValueOnce(undefined); + await UssFSProvider.instance.fetchFileAtUri(testUris.file, { editor: {} as TextEditor }); + + expect(lookupAsFileMock).toHaveBeenCalledWith(testUris.file); + expect(autoDetectEncodingMock).toHaveBeenCalledWith(fileEntry); + expect(fileEntry.data.toString()).toBe(exampleData); + expect(fileEntry.etag).toBe("123abc"); + expect(fileEntry.data.byteLength).toBe(exampleData.length); + expect(_updateResourceInEditorMock).toHaveBeenCalledWith(testUris.file); + autoDetectEncodingMock.mockRestore(); + }); +}); + +describe("fetchEncodingForUri", () => { + it("returns the correct encoding for a URI", async () => { + const fileEntry = { ...testEntries.file }; + const lookupAsFileMock = jest.spyOn((UssFSProvider as any).prototype, "_lookupAsFile").mockReturnValueOnce(fileEntry); + const autoDetectEncodingMock = jest.spyOn(UssFSProvider.instance, "autoDetectEncoding").mockImplementation(async (entry) => { + entry.encoding = { kind: "text" }; + }); + await UssFSProvider.instance.fetchEncodingForUri(testUris.file); + + expect(lookupAsFileMock).toHaveBeenCalledWith(testUris.file); + expect(autoDetectEncodingMock).toHaveBeenCalledWith(fileEntry); + expect(fileEntry.encoding).toStrictEqual({ kind: "text" }); + autoDetectEncodingMock.mockRestore(); + }); +}); + +describe("autoDetectEncoding", () => { + const getTagMock = jest.fn(); + let mockUssApi; + + beforeEach(() => { + mockUssApi = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValue({ + getTag: getTagMock.mockClear(), + } as any); + }); + + it("sets encoding if file tagged as binary", async () => { + getTagMock.mockResolvedValueOnce("binary"); + const testEntry = new UssFile("testFile"); + testEntry.metadata = { + path: "/testFile", + profile: testProfile, + }; + await UssFSProvider.instance.autoDetectEncoding(testEntry); + expect(getTagMock).toHaveBeenCalledTimes(1); + expect(testEntry.encoding).toStrictEqual({ kind: "binary" }); + }); + + it("sets encoding if file tagged as binary - old API", async () => { + const isFileTagBinOrAsciiMock = jest.fn().mockResolvedValueOnce(true); + mockUssApi.mockReturnValueOnce({ + isFileTagBinOrAscii: isFileTagBinOrAsciiMock, + } as any); + const testEntry = new UssFile("testFile"); + testEntry.metadata = { + path: "/testFile", + profile: testProfile, + }; + await UssFSProvider.instance.autoDetectEncoding(testEntry); + expect(testEntry.encoding?.kind).toBe("binary"); + expect(isFileTagBinOrAsciiMock).toHaveBeenCalledTimes(1); + }); + + it("sets encoding if file tagged as EBCDIC", async () => { + getTagMock.mockResolvedValueOnce("IBM-1047"); + const testEntry = new UssFile("testFile"); + testEntry.metadata = { + path: "/testFile", + profile: testProfile, + }; + await UssFSProvider.instance.autoDetectEncoding(testEntry); + expect(testEntry.encoding).toStrictEqual({ + kind: "other", + codepage: "IBM-1047", + }); + expect(getTagMock).toHaveBeenCalledTimes(1); + }); + + it("does not set encoding if file is untagged", async () => { + getTagMock.mockResolvedValueOnce("untagged"); + const testEntry = new UssFile("testFile"); + testEntry.metadata = { + path: "/testFile", + profile: testProfile, + }; + await UssFSProvider.instance.autoDetectEncoding(testEntry); + expect(testEntry.encoding).toBe(undefined); + expect(getTagMock).toHaveBeenCalledTimes(1); + }); + + it("does not set encoding if already defined on node", async () => { + const testEntry = new UssFile("testFile"); + testEntry.metadata = { + path: "/testFile", + profile: testProfile, + }; + testEntry.encoding = { kind: "binary" }; + await UssFSProvider.instance.autoDetectEncoding(testEntry); + expect(testEntry.encoding.kind).toBe("binary"); + expect(getTagMock).toHaveBeenCalledTimes(0); + }); +}); + +describe("readFile", () => { + const lookupAsFileMock = jest.spyOn((UssFSProvider as any).prototype, "_lookupAsFile"); + const getInfoFromUriMock = jest.spyOn((UssFSProvider as any).prototype, "_getInfoFromUri"); + + it("throws an error when trying to read a file that doesn't have a profile registered", async () => { + lookupAsFileMock.mockReturnValueOnce(testEntries.file); + getInfoFromUriMock.mockReturnValueOnce({ + profile: null, + path: "/aFile.txt", + }); + + await expect(UssFSProvider.instance.readFile(testUris.file)).rejects.toThrow("file not found"); + }); + + it("returns data for a file", async () => { + lookupAsFileMock.mockReturnValueOnce(testEntries.file); + getInfoFromUriMock.mockReturnValueOnce({ + profile: testProfile, + path: "/aFile.txt", + }); + const fetchFileAtUriMock = jest.spyOn(UssFSProvider.instance, "fetchFileAtUri").mockResolvedValueOnce(undefined); + expect((await UssFSProvider.instance.readFile(testUris.file)).toString()).toStrictEqual([1, 2, 3].toString()); + fetchFileAtUriMock.mockRestore(); + }); + + it("returns conflict data for a file with the conflict query parameter", async () => { + lookupAsFileMock.mockReturnValueOnce(testEntries.file); + getInfoFromUriMock.mockReturnValueOnce({ + profile: testProfile, + path: "/aFile.txt", + }); + const fetchFileAtUriMock = jest.spyOn(UssFSProvider.instance, "fetchFileAtUri").mockResolvedValueOnce(undefined); + + expect( + ( + await UssFSProvider.instance.readFile( + testUris.file.with({ + query: "conflict=true", + }) + ) + ).toString() + ).toStrictEqual([4, 5, 6].toString()); + expect(fetchFileAtUriMock).toHaveBeenCalled(); + fetchFileAtUriMock.mockRestore(); + }); +}); + +describe("writeFile", () => { + it("updates a file in the FSP and remote system", async () => { + const mockUssApi = { + uploadFromBuffer: jest.fn().mockResolvedValueOnce({ + apiResponse: { + etag: "NEWETAG", + }, + }), + }; + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); + const statusMsgMock = jest.spyOn(Gui, "setStatusBarMessage"); + const folder = { + ...testEntries.folder, + entries: new Map([[testEntries.file.name, { ...testEntries.file }]]), + }; + const lookupParentDirMock = jest.spyOn(UssFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(folder); + const newContents = new Uint8Array([3, 6, 9]); + await UssFSProvider.instance.writeFile(testUris.file, newContents, { create: false, overwrite: true }); + + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.file); + expect(statusMsgMock).toHaveBeenCalledWith("$(sync~spin) Saving USS file..."); + expect(mockUssApi.uploadFromBuffer).toHaveBeenCalledWith(Buffer.from(newContents), testEntries.file.metadata.path, { + binary: false, + encoding: undefined, + etag: testEntries.file.etag, + returnEtag: true, + }); + const fileEntry = folder.entries.get("aFile.txt")!; + expect(fileEntry.etag).toBe("NEWETAG"); + expect(fileEntry.data).toBe(newContents); + ussApiMock.mockRestore(); + }); + + it("throws an error when there is an error unrelated to etag", async () => { + const mockUssApi = { + uploadFromBuffer: jest.fn().mockRejectedValueOnce(new Error("Unknown error on remote system")), + }; + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); + const folder = { + ...testEntries.folder, + entries: new Map([[testEntries.file.name, { ...testEntries.file }]]), + }; + const lookupParentDirMock = jest.spyOn(UssFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(folder); + const autoDetectEncodingMock = jest.spyOn(UssFSProvider.instance, "autoDetectEncoding").mockResolvedValue(undefined); + const newContents = new Uint8Array([3, 6, 9]); + await expect(UssFSProvider.instance.writeFile(testUris.file, newContents, { create: false, overwrite: true })).rejects.toThrow( + "Unknown error on remote system" + ); + + lookupParentDirMock.mockRestore(); + ussApiMock.mockRestore(); + autoDetectEncodingMock.mockRestore(); + }); + + it("calls _handleConflict when there is an etag error", async () => { + const mockUssApi = { + uploadFromBuffer: jest.fn().mockRejectedValueOnce(new Error("Rest API failure with HTTP(S) status 412")), + }; + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); + const statusMsgMock = jest.spyOn(Gui, "setStatusBarMessage"); + const folder = { + ...testEntries.folder, + entries: new Map([[testEntries.file.name, { ...testEntries.file }]]), + }; + const lookupParentDirMock = jest.spyOn(UssFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(folder); + const autoDetectEncodingMock = jest.spyOn(UssFSProvider.instance, "autoDetectEncoding").mockResolvedValue(undefined); + const newContents = new Uint8Array([3, 6, 9]); + const handleConflictMock = jest.spyOn(UssFSProvider.instance as any, "_handleConflict").mockImplementation(); + await UssFSProvider.instance.writeFile(testUris.file, newContents, { create: false, overwrite: true }); + + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.file); + expect(statusMsgMock).toHaveBeenCalledWith("$(sync~spin) Saving USS file..."); + expect(mockUssApi.uploadFromBuffer).toHaveBeenCalledWith(Buffer.from(newContents), testEntries.file.metadata.path, { + binary: false, + encoding: undefined, + etag: testEntries.file.etag, + returnEtag: true, + }); + expect(handleConflictMock).toHaveBeenCalled(); + handleConflictMock.mockRestore(); + ussApiMock.mockRestore(); + autoDetectEncodingMock.mockRestore(); + }); + + it("upload changes to a remote file even if its not yet in the FSP", async () => { + const mockUssApi = { + uploadFromBuffer: jest.fn().mockResolvedValueOnce({ + apiResponse: { + etag: "NEWETAG", + }, + }), + }; + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); + const statusMsgMock = jest.spyOn(Gui, "setStatusBarMessage"); + const folder = { + ...testEntries.session, + entries: new Map(), + }; + const lookupParentDirMock = jest.spyOn(UssFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(folder); + const autoDetectEncodingMock = jest.spyOn(UssFSProvider.instance, "autoDetectEncoding").mockResolvedValue(undefined); + const newContents = new Uint8Array([3, 6, 9]); + await UssFSProvider.instance.writeFile(testUris.file, newContents, { create: true, overwrite: true }); + + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.file); + expect(statusMsgMock).toHaveBeenCalledWith("$(sync~spin) Saving USS file..."); + expect(mockUssApi.uploadFromBuffer).toHaveBeenCalledWith(Buffer.from(newContents), testEntries.file.metadata.path, { + binary: false, + encoding: undefined, + etag: undefined, + returnEtag: true, + }); + const fileEntry = folder.entries.get("aFile.txt")!; + expect(fileEntry.etag).toBe("NEWETAG"); + expect(fileEntry.data).toBe(newContents); + ussApiMock.mockRestore(); + autoDetectEncodingMock.mockRestore(); + }); + + it("updates an empty, unaccessed file entry in the FSP without sending data", async () => { + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({} as any); + const folder = { + ...testEntries.folder, + entries: new Map([[testEntries.file.name, { ...testEntries.file, wasAccessed: false }]]), + }; + const lookupParentDirMock = jest.spyOn(UssFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(folder); + const newContents = new Uint8Array([]); + await UssFSProvider.instance.writeFile(testUris.file, newContents, { create: false, overwrite: true }); + + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.file); + const fileEntry = folder.entries.get("aFile.txt")!; + expect(fileEntry.data.length).toBe(0); + ussApiMock.mockRestore(); + }); + + it("updates a file when open in the diff view", async () => { + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi"); + const folder = { + ...testEntries.folder, + entries: new Map([[testEntries.file.name, { ...testEntries.file, wasAccessed: false }]]), + }; + const lookupParentDirMock = jest.spyOn(UssFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(folder); + const newContents = new Uint8Array([]); + await UssFSProvider.instance.writeFile( + testUris.file.with({ + query: "inDiff=true", + }), + newContents, + { create: false, overwrite: true } + ); + + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.file); + const fileEntry = folder.entries.get("aFile.txt")!; + expect(fileEntry.data.length).toBe(0); + expect(fileEntry.inDiffView).toBe(true); + expect(ussApiMock).not.toHaveBeenCalled(); + }); + + it("throws an error if entry doesn't exist and 'create' option is false", async () => { + await expect(UssFSProvider.instance.writeFile(testUris.file, new Uint8Array([]), { create: false, overwrite: true })).rejects.toThrow( + "file not found" + ); + }); + + it("throws an error if entry exists and 'overwrite' option is false", async () => { + const rootFolder = { + ...testEntries.session, + entries: new Map([[testEntries.file.name, { ...testEntries.file, wasAccessed: false }]]), + }; + const lookupParentDirMock = jest.spyOn(UssFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(rootFolder); + await expect(UssFSProvider.instance.writeFile(testUris.file, new Uint8Array([]), { create: true, overwrite: false })).rejects.toThrow( + "file exists" + ); + lookupParentDirMock.mockRestore(); + }); + + it("throws an error if the given URI is a directory", async () => { + const rootFolder = { + ...testEntries.session, + entries: new Map([[testEntries.folder.name, { ...testEntries.folder }]]), + }; + const lookupParentDirMock = jest.spyOn(UssFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(rootFolder); + await expect(UssFSProvider.instance.writeFile(testUris.folder, new Uint8Array([]), { create: true, overwrite: false })).rejects.toThrow( + "file is a directory" + ); + lookupParentDirMock.mockRestore(); + }); +}); + +describe("makeEmptyFileWithEncoding", () => { + it("creates an empty file in the provider with the given encoding", () => { + const fakeSession = { ...testEntries.session }; + const parentDirMock = jest.spyOn(UssFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(fakeSession); + expect(UssFSProvider.instance.makeEmptyFileWithEncoding(testUris.file, { kind: "binary" })); + expect(fakeSession.entries.has(testEntries.file.name)).toBe(true); + parentDirMock.mockRestore(); + }); +}); + +describe("rename", () => { + it("throws an error if entry exists and 'overwrite' is false", async () => { + const lookupMock = jest.spyOn(UssFSProvider.instance as any, "_lookup").mockReturnValueOnce({ ...testEntries.file }); + await expect( + UssFSProvider.instance.rename(testUris.file, testUris.file.with({ path: "/sestest/aFile2.txt" }), { overwrite: false }) + ).rejects.toThrow("file exists"); + + lookupMock.mockRestore(); + }); + + it("renames a file entry in the FSP and remote system", async () => { + const mockUssApi = { + rename: jest.fn(), + }; + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); + const lookupMock = jest.spyOn(UssFSProvider.instance as any, "_lookup").mockReturnValueOnce({ ...testEntries.file }); + const fileEntry = { ...testEntries.file, metadata: { ...testEntries.file.metadata } }; + const sessionEntry = { + ...testEntries.session, + entries: new Map([[testEntries.file.name, fileEntry]]), + }; + (UssFSProvider.instance as any).root.entries.set("sestest", sessionEntry); + + await UssFSProvider.instance.rename(testUris.file, testUris.file.with({ path: "/sestest/aFile2.txt" }), { overwrite: true }); + expect(mockUssApi.rename).toHaveBeenCalledWith("/aFile.txt", "/aFile2.txt"); + expect(fileEntry.metadata.path).toBe("/aFile2.txt"); + expect(sessionEntry.entries.has("aFile2.txt")).toBe(true); + + lookupMock.mockRestore(); + ussApiMock.mockRestore(); + }); + + it("renames a folder entry in the FSP and remote system, updating child paths", async () => { + const mockUssApi = { + rename: jest.fn(), + }; + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); + const lookupMock = jest.spyOn(UssFSProvider.instance as any, "_lookup").mockReturnValueOnce({ ...testEntries.folder }); + const updChildPathsMock = jest.spyOn(UssFSProvider.instance as any, "_updateChildPaths").mockResolvedValueOnce(undefined); + const folderEntry = { ...testEntries.folder, metadata: { ...testEntries.folder.metadata } }; + const sessionEntry = { + ...testEntries.session, + entries: new Map([[testEntries.folder.name, folderEntry]]), + }; + (UssFSProvider.instance as any).root.entries.set("sestest", sessionEntry); + + await UssFSProvider.instance.rename(testUris.folder, testUris.folder.with({ path: "/sestest/aFolder2" }), { overwrite: true }); + expect(mockUssApi.rename).toHaveBeenCalledWith("/aFolder", "/aFolder2"); + expect(folderEntry.metadata.path).toBe("/aFolder2"); + expect(sessionEntry.entries.has("aFolder2")).toBe(true); + expect(updChildPathsMock).toHaveBeenCalledWith(folderEntry); + + lookupMock.mockRestore(); + ussApiMock.mockRestore(); + updChildPathsMock.mockRestore(); + }); + + it("displays an error message when renaming fails on the remote system", async () => { + const mockUssApi = { + rename: jest.fn().mockRejectedValueOnce(new Error("could not upload file")), + }; + const errMsgSpy = jest.spyOn(Gui, "errorMessage"); + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); + const lookupMock = jest.spyOn(UssFSProvider.instance as any, "_lookup").mockReturnValueOnce({ ...testEntries.folder }); + const folderEntry = { ...testEntries.folder, metadata: { ...testEntries.folder.metadata } }; + const sessionEntry = { + ...testEntries.session, + entries: new Map([[testEntries.folder.name, folderEntry]]), + }; + (UssFSProvider.instance as any).root.entries.set("sestest", sessionEntry); + + await UssFSProvider.instance.rename(testUris.folder, testUris.folder.with({ path: "/sestest/aFolder2" }), { overwrite: true }); + expect(mockUssApi.rename).toHaveBeenCalledWith("/aFolder", "/aFolder2"); + expect(folderEntry.metadata.path).toBe("/aFolder"); + expect(sessionEntry.entries.has("aFolder2")).toBe(false); + expect(errMsgSpy).toHaveBeenCalledWith("Renaming /aFolder failed due to API error: could not upload file"); + + lookupMock.mockRestore(); + ussApiMock.mockRestore(); + }); +}); + +describe("delete", () => { + it("successfully deletes an entry", async () => { + testEntries.session.entries.set("aFile.txt", testEntries.file); + testEntries.session.size = 1; + const getDelInfoMock = jest.spyOn((BaseProvider as any).prototype, "_getDeleteInfo").mockReturnValueOnce({ + entryToDelete: testEntries.file, + parent: testEntries.session, + parentUri: Uri.from({ scheme: ZoweScheme.USS, path: "/sestest" }), + }); + const deleteMock = jest.fn().mockResolvedValueOnce(undefined); + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ + delete: deleteMock, + } as any); + expect(await UssFSProvider.instance.delete(testUris.file, { recursive: false })).toBe(undefined); + expect(getDelInfoMock).toHaveBeenCalledWith(testUris.file); + expect(deleteMock).toHaveBeenCalledWith(testEntries.file.metadata.path, false); + expect(testEntries.session.entries.has("aFile.txt")).toBe(false); + expect(testEntries.session.size).toBe(0); + }); + it("displays an error message if it fails to delete the entry from the remote system", async () => { + const sesEntry = { ...testEntries.session }; + sesEntry.entries.set("aFile.txt", testEntries.file); + sesEntry.size = 1; + const getDelInfoMock = jest.spyOn((BaseProvider as any).prototype, "_getDeleteInfo").mockReturnValueOnce({ + entryToDelete: testEntries.file, + parent: sesEntry, + parentUri: Uri.from({ scheme: ZoweScheme.USS, path: "/sestest" }), + }); + const errorMsgMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce(undefined); + const deleteMock = jest.fn().mockRejectedValueOnce(new Error("insufficient permissions")); + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ + delete: deleteMock, + } as any); + await UssFSProvider.instance.delete(testUris.file, { recursive: false }); + expect(getDelInfoMock).toHaveBeenCalledWith(testUris.file); + expect(deleteMock).toHaveBeenCalledWith(testEntries.file.metadata.path, false); + expect(errorMsgMock).toHaveBeenCalledWith("Deleting /aFile.txt failed due to API error: insufficient permissions"); + expect(sesEntry.entries.has("aFile.txt")).toBe(true); + expect(sesEntry.size).toBe(1); + }); +}); + +describe("copy", () => { + const copyTreeMock = jest.spyOn((UssFSProvider as any).prototype, "copyTree"); + it("returns early if the source URI does not have a file tree in its query", async () => { + await UssFSProvider.instance.copy( + testUris.file, + testUris.file.with({ + path: "/sestest/aFile2.txt", + }), + { overwrite: true } + ); + expect(copyTreeMock).not.toHaveBeenCalled(); + }); + + it("calls copyTree with the given URIs and options", async () => { + copyTreeMock.mockResolvedValueOnce(undefined); + const fileTree = { + localUri: testUris.file, + ussPath: "/aFile.txt", + baseName: "aFile.txt", + binary: false, + children: [], + sessionName: "sestest", + type: UssFileType.File, + }; + const uriWithTree = testUris.file.with({ + query: `tree=${encodeURIComponent(JSON.stringify(fileTree))}`, + }); + const destUri = testUris.file.with({ + path: "/sestest/aFile2.txt", + }); + await UssFSProvider.instance.copy(uriWithTree, destUri, { overwrite: true }); + expect(copyTreeMock).toHaveBeenCalledWith(uriWithTree, destUri, { overwrite: true, tree: fileTree }); + }); + + afterAll(() => { + copyTreeMock.mockRestore(); + }); +}); + +describe("buildFileName", () => { + it("returns a file name without copy suffix if no collisions are found", () => { + expect( + (UssFSProvider.instance as any).buildFileName( + [ + { + name: "apple.txt", + }, + { + name: "orange.txt", + }, + { + name: "banana.txt", + }, + { + name: "strawberry.txt", + }, + ], + "pear.txt" + ) + ).toBe("pear.txt"); + }); + it("returns a file name with copy suffix if collisions are found", () => { + expect( + (UssFSProvider.instance as any).buildFileName( + [ + { + name: "apple.txt", + }, + { + name: "orange.txt", + }, + { + name: "pear.txt", + }, + { + name: "strawberry.txt", + }, + ], + "pear.txt" + ) + ).toBe("pear (1).txt"); + }); + it("returns a file name with '(2)' suffix if more collisions are found after adding copy suffix", () => { + expect( + (UssFSProvider.instance as any).buildFileName( + [ + { + name: "apple.txt", + }, + { + name: "orange.txt", + }, + { + name: "pear.txt", + }, + { + name: "pear (1).txt", + }, + ], + "pear.txt" + ) + ).toBe("pear (2).txt"); + }); +}); + +describe("copyTree", () => { + describe("copying a file tree - same profiles (copy API)", () => { + it("with naming collisions", async () => { + const getInfoFromUri = jest + .spyOn((UssFSProvider as any).prototype, "_getInfoFromUri") + // destination info + .mockReturnValueOnce({ + profile: testProfile, + path: "/bFile.txt", + }) + // source info + .mockReturnValueOnce({ + profile: testProfile, + path: "/aFile.txt", + }); + const mockUssApi = { + copy: jest.fn(), + create: jest.fn(), + fileList: jest.fn().mockResolvedValueOnce({ + apiResponse: { + items: [{ name: "bFile.txt" }], + }, + }), + uploadFromBuffer: jest.fn(), + }; + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); + await (UssFSProvider.instance as any).copyTree( + testUris.file, + testUris.file.with({ + path: "/sestest/bFile.txt", + }), + { tree: { type: UssFileType.File } } + ); + expect(mockUssApi.copy).toHaveBeenCalledWith("/bFile (1).txt", { + from: "/aFile.txt", + recursive: false, + overwrite: true, + }); + getInfoFromUri.mockRestore(); + }); + + it("without naming collisions", async () => { + const getInfoFromUri = jest + .spyOn((UssFSProvider as any).prototype, "_getInfoFromUri") + // destination info + .mockReturnValueOnce({ + profile: testProfile, + path: "/bFile.txt", + }) + // source info + .mockReturnValueOnce({ + profile: testProfile, + path: "/aFile.txt", + }); + const mockUssApi = { + copy: jest.fn(), + create: jest.fn(), + fileList: jest.fn().mockResolvedValueOnce({ + apiResponse: { + items: [{ name: "aFile.txt" }], + }, + }), + uploadFromBuffer: jest.fn(), + }; + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); + await (UssFSProvider.instance as any).copyTree( + testUris.file, + testUris.file.with({ + path: "/sestest/bFile.txt", + }), + { tree: { type: UssFileType.File } } + ); + expect(mockUssApi.copy).toHaveBeenCalledWith("/bFile.txt", { + from: "/aFile.txt", + recursive: false, + overwrite: true, + }); + getInfoFromUri.mockRestore(); + }); + }); + + describe("copying - different profiles", () => { + it("file: with naming collisions", async () => { + const getInfoFromUri = jest + .spyOn((UssFSProvider as any).prototype, "_getInfoFromUri") + // destination info + .mockReturnValueOnce({ + profile: testProfileB, + path: "/aFile.txt", + }) + // source info + .mockReturnValueOnce({ + profile: testProfile, + path: "/aFile.txt", + }); + const mockUssApi = { + copy: jest.fn(), + create: jest.fn(), + fileList: jest.fn().mockResolvedValueOnce({ + apiResponse: { + items: [{ name: "bFile.txt" }], + }, + }), + uploadFromBuffer: jest.fn(), + }; + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); + jest.spyOn((UssFSProvider as any).instance, "_lookup").mockReturnValueOnce(testEntries.file); + jest.spyOn((UssFSProvider as any).instance, "readFile").mockResolvedValueOnce(testEntries.file.data); + await (UssFSProvider.instance as any).copyTree( + testUris.file, + testUris.file.with({ + path: "/sestest2/bFile.txt", + }), + { tree: { type: UssFileType.File } } + ); + expect(mockUssApi.uploadFromBuffer).toHaveBeenCalledWith(Buffer.from(testEntries.file.data), "/aFile.txt"); + getInfoFromUri.mockRestore(); + }); + it("file: without naming collisions", async () => { + const getInfoFromUri = jest + .spyOn((UssFSProvider as any).prototype, "_getInfoFromUri") + // destination info + .mockReturnValueOnce({ + profile: testProfileB, + path: "/aFile.txt", + }) + // source info + .mockReturnValueOnce({ + profile: testProfile, + path: "/aFile.txt", + }); + const mockUssApi = { + copy: jest.fn(), + create: jest.fn(), + fileList: jest.fn().mockResolvedValueOnce({ + apiResponse: { + items: [{ name: "aFile.txt" }], + }, + }), + uploadFromBuffer: jest.fn(), + }; + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); + jest.spyOn((UssFSProvider as any).instance, "_lookup").mockReturnValueOnce(testEntries.file); + jest.spyOn((UssFSProvider as any).instance, "readFile").mockResolvedValueOnce(testEntries.file.data); + await (UssFSProvider.instance as any).copyTree( + testUris.file, + testUris.file.with({ + path: "/sestest2/aFile.txt", + }), + { tree: { type: UssFileType.File } } + ); + expect(mockUssApi.uploadFromBuffer).toHaveBeenCalledWith(Buffer.from(testEntries.file.data), "/aFile (1).txt"); + getInfoFromUri.mockRestore(); + }); + it("folder", async () => { + const getInfoFromUri = jest + .spyOn((UssFSProvider as any).prototype, "_getInfoFromUri") + // destination info + .mockReturnValueOnce({ + profile: testProfileB, + path: "/aFolder", + }) + // source info + .mockReturnValueOnce({ + profile: testProfile, + path: "/aFolder", + }); + const mockUssApi = { + create: jest.fn(), + fileList: jest.fn().mockResolvedValue({ + apiResponse: { + items: [{ name: "aFile.txt" }], + }, + }), + uploadFromBuffer: jest.fn(), + }; + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValue(mockUssApi as any); + + const copyTreeSpy = jest.spyOn(UssFSProvider.instance as any, "copyTree"); + const fileInPathTree = { + baseName: "someFile.txt", + localUri: Uri.from({ scheme: ZoweScheme.USS, path: "/sestest/aFolder/someFile.txt" }), + type: UssFileType.File, + }; + const ussFileTree = { + type: UssFileType.Directory, + children: [fileInPathTree], + }; + await (UssFSProvider.instance as any).copyTree( + testUris.folder, + testUris.folder.with({ + path: "/sestest2/aFolder", + }), + { tree: ussFileTree } + ); + expect(mockUssApi.create).toHaveBeenCalledWith("/aFolder", "directory"); + expect(copyTreeSpy).toHaveBeenCalledTimes(2); + getInfoFromUri.mockRestore(); + ussApiMock.mockRestore(); + }); + }); +}); + +describe("createDirectory", () => { + it("creates a session directory with the given URI", () => { + const root = (UssFSProvider.instance as any).root; + root.entries.clear(); + const oldSize: number = root.size; + const getInfoFromUri = jest.spyOn((UssFSProvider as any).prototype, "_getInfoFromUri").mockReturnValueOnce({ + profile: testProfile, + path: "/", + }); + UssFSProvider.instance.createDirectory(testUris.session); + expect(root.entries.has("sestest")).toBe(true); + expect(root.size).toBe(oldSize + 1); + getInfoFromUri.mockRestore(); + }); + + it("creates an inner directory with the given URI", () => { + const root = (UssFSProvider.instance as any).root; + const sesEntry = new DirEntry("sestest"); + sesEntry.metadata = { + profile: testProfile, + path: "/", + }; + root.entries.set("sestest", sesEntry); + const oldSize: number = sesEntry.size; + UssFSProvider.instance.createDirectory(testUris.folder); + expect(sesEntry.entries.has("aFolder")).toBe(true); + expect(sesEntry.size).toBe(oldSize + 1); + }); +}); + +describe("watch", () => { + it("returns a new, empty Disposable object", () => { + expect(UssFSProvider.instance.watch(testUris.file)).toStrictEqual(new Disposable(() => {})); + }); +}); + +describe("_getInfoFromUri", () => { + it("returns the correct info for a given URI when ProfilesCache is available", () => { + jest.spyOn(Profiles, "getInstance").mockReturnValueOnce({ + loadNamedProfile: jest.fn().mockReturnValueOnce(testProfile), + } as any); + expect((UssFSProvider.instance as any)._getInfoFromUri(testUris.file)).toStrictEqual({ + profile: testProfile, + path: "/aFile.txt", + }); + }); +}); diff --git a/packages/zowe-explorer/__tests__/__unit__/uss/init.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/uss/init.unit.test.ts index 64e919510f..8c51445d52 100644 --- a/packages/zowe-explorer/__tests__/__unit__/uss/init.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/uss/init.unit.test.ts @@ -17,16 +17,15 @@ import * as ussActions from "../../../src/uss/actions"; import * as sharedExtension from "../../../src/shared/init"; import { initUSSProvider } from "../../../src/uss/init"; import { Profiles } from "../../../src/Profiles"; -import { IJestIt, ITestContext, processSubscriptions, spyOnSubscriptions } from "../../__common__/testUtils"; -import { ZoweLogger } from "../../../src/utils/ZoweLogger"; +import { IJestIt, ITestContext, processSubscriptions } from "../../__common__/testUtils"; -describe("Test src/dataset/extension", () => { - describe("initDatasetProvider", () => { +describe("Test src/uss/extension", () => { + describe("initUSSProvider", () => { let registerCommand; let onDidChangeConfiguration; let spyCreateUssTree; const test: ITestContext = { - context: { subscriptions: [] }, + context: { subscriptions: new Array() }, value: { test: "uss", refreshUSS: jest.fn(), openUSS: jest.fn(), deleteUSSNode: jest.fn(), getUSSDocumentFilePath: jest.fn() }, _: { _: "_" }, }; @@ -71,7 +70,7 @@ describe("Test src/dataset/extension", () => { name: "zowe.uss.refreshUSS", mock: [ { spy: jest.spyOn(contextuals, "isDocument"), arg: [test.value], ret: true }, - { spy: jest.spyOn(test.value, "refreshUSS"), arg: [] }, + { spy: jest.spyOn(contextuals, "isUssDirectory"), arg: [test.value], ret: false }, ], }, { @@ -93,10 +92,6 @@ describe("Test src/dataset/extension", () => { name: "zowe.uss.editSession", mock: [{ spy: jest.spyOn(ussFileProvider, "editSession"), arg: [test.value, ussFileProvider] }], }, - { - name: "zowe.uss.ZoweUSSNode.open", - mock: [{ spy: jest.spyOn(test.value, "openUSS"), arg: [false, true, ussFileProvider] }], - }, { name: "zowe.uss.removeSession", mock: [ @@ -115,11 +110,10 @@ describe("Test src/dataset/extension", () => { { name: "zowe.uss.deleteNode", mock: [ - { spy: jest.spyOn(contextuals, "isDocument"), arg: [test.value], ret: false }, + { spy: jest.spyOn(contextuals, "isDocument"), arg: [test.value], ret: true }, { spy: jest.spyOn(contextuals, "isUssDirectory"), arg: [test.value], ret: true }, - { spy: jest.spyOn(ussActions, "deleteUSSFilesPrompt"), arg: [[test.value]], ret: true }, - { spy: jest.spyOn(test.value, "getUSSDocumentFilePath"), arg: [], ret: "dummy" }, - { spy: jest.spyOn(test.value, "deleteUSSNode"), arg: [ussFileProvider, "dummy", true] }, + { spy: jest.spyOn(ussActions, "deleteUSSFilesPrompt"), arg: [[test.value]], ret: false }, + { spy: jest.spyOn(test.value, "deleteUSSNode"), arg: [ussFileProvider, ""], ret: true }, ], }, { @@ -167,7 +161,7 @@ describe("Test src/dataset/extension", () => { { spy: jest.spyOn(Profiles, "getInstance"), arg: [], - ret: { enableValidation: jest.fn() }, + ret: { enableValidation: jest.fn(), disableValidation: jest.fn() }, }, ], }, @@ -214,7 +208,6 @@ describe("Test src/dataset/extension", () => { Object.defineProperty(vscode.workspace, "onDidChangeConfiguration", { value: onDidChangeConfiguration }); spyCreateUssTree.mockResolvedValue(ussFileProvider as any); - spyOnSubscriptions(commands); jest.spyOn(vscode.workspace, "onDidCloseTextDocument").mockImplementation(ussFileProvider.onDidCloseTextDocument); await initUSSProvider(test.context); }); @@ -227,9 +220,9 @@ describe("Test src/dataset/extension", () => { processSubscriptions(commands, test); - it("should not initialize if it is unable to create the dataset tree", async () => { + it("should not initialize if it is unable to create the USS tree", async () => { spyCreateUssTree.mockResolvedValue(null); - const myProvider = await initUSSProvider({} as any); + const myProvider = await initUSSProvider(test.context); expect(myProvider).toBe(null); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/uss/utils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/uss/utils.unit.test.ts deleted file mode 100644 index 12b90529aa..0000000000 --- a/packages/zowe-explorer/__tests__/__unit__/uss/utils.unit.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - * - */ - -import * as vscode from "vscode"; -import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; -import { ZoweUSSNode } from "../../../src/uss/ZoweUSSNode"; -import * as utils from "../../../src/uss/utils"; -import { createIProfile } from "../../../__mocks__/mockCreators/shared"; - -describe("USS utils unit tests - function autoDetectEncoding", () => { - const getTagMock = jest.fn(); - let mockUssApi; - - beforeEach(() => { - mockUssApi = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValue({ - getTag: getTagMock.mockClear(), - } as any); - }); - - afterAll(() => { - jest.restoreAllMocks(); - }); - - it("sets encoding if file tagged as binary", async () => { - const node = new ZoweUSSNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); - getTagMock.mockResolvedValueOnce("binary"); - await utils.autoDetectEncoding(node); - expect(node.binary).toBe(true); - expect(node.encoding).toBeUndefined(); - expect(getTagMock).toHaveBeenCalledTimes(1); - }); - - it("sets encoding if file tagged as binary - old API", async () => { - const node = new ZoweUSSNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); - const isFileTagBinOrAsciiMock = jest.fn().mockResolvedValueOnce(true); - mockUssApi.mockReturnValueOnce({ - isFileTagBinOrAscii: isFileTagBinOrAsciiMock, - } as any); - await utils.autoDetectEncoding(node); - expect(node.binary).toBe(true); - expect(node.encoding).toBeUndefined(); - expect(isFileTagBinOrAsciiMock).toHaveBeenCalledTimes(1); - }); - - it("sets encoding if file tagged as EBCDIC", async () => { - const node = new ZoweUSSNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); - getTagMock.mockResolvedValueOnce("IBM-1047"); - await utils.autoDetectEncoding(node); - expect(node.binary).toBe(false); - expect(node.encoding).toBe("IBM-1047"); - expect(getTagMock).toHaveBeenCalledTimes(1); - }); - - it("does not set encoding if file is untagged", async () => { - const node = new ZoweUSSNode({ label: "encodingTest", collapsibleState: vscode.TreeItemCollapsibleState.None }); - getTagMock.mockResolvedValueOnce("untagged"); - await utils.autoDetectEncoding(node); - expect(node.binary).toBe(false); - expect(node.encoding).toBeUndefined(); - expect(getTagMock).toHaveBeenCalledTimes(1); - }); - - it("does not set encoding if already defined on node", async () => { - const node = new ZoweUSSNode({ - label: "encodingTest", - collapsibleState: vscode.TreeItemCollapsibleState.None, - profile: createIProfile(), - encoding: { kind: "text" }, - }); - await utils.autoDetectEncoding(node); - expect(node.binary).toBe(false); - expect(node.encoding).toBeNull(); - expect(getTagMock).toHaveBeenCalledTimes(0); - }); -}); diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/LocalFileManagement.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/LocalFileManagement.unit.test.ts index 6ff05c535b..beccaf8a28 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/LocalFileManagement.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/LocalFileManagement.unit.test.ts @@ -9,7 +9,6 @@ * */ -import * as globals from "../../../src/globals"; import * as vscode from "vscode"; import * as sharedMock from "../../../__mocks__/mockCreators/shared"; import { ZoweDatasetNode } from "../../../src/dataset/ZoweDatasetNode"; @@ -17,39 +16,31 @@ import * as dsMock from "../../../__mocks__/mockCreators/datasets"; import { LocalFileManagement } from "../../../src/utils/LocalFileManagement"; import { ZoweLogger } from "../../../src/utils/ZoweLogger"; import { ZoweUSSNode } from "../../../src/uss/ZoweUSSNode"; -import { createUSSSessionNode } from "../../../__mocks__/mockCreators/uss"; -import { LocalFileInfo } from "../../../src/shared/utils"; +import { createUSSSessionNode, createUSSNode } from "../../../__mocks__/mockCreators/uss"; +import { createISession, createIProfile } from "../../../__mocks__/mockCreators/shared"; +import { UssFSProvider } from "../../../src/uss/UssFSProvider"; +import { DatasetFSProvider } from "../../../src/dataset/DatasetFSProvider"; jest.mock("vscode"); describe("LocalFileManagement unit tests", () => { - beforeEach(() => { - globals.resetCompareChoices(); - }); - afterEach(() => { jest.clearAllMocks(); }); function createGlobalMocks() { - globals.defineGlobals(""); const profile = sharedMock.createValidIProfile(); const session = sharedMock.createISession(); - // jest.spyOn(DatasetFSProvider.instance, "createDirectory").mockImplementation(); - // jest.spyOn(UssFSProvider.instance, "createDirectory").mockImplementation(); + jest.spyOn(DatasetFSProvider.instance, "createDirectory").mockImplementation(); + jest.spyOn(UssFSProvider.instance, "createDirectory").mockImplementation(); const dsSession = dsMock.createDatasetSessionNode(session, profile); const ussSession = createUSSSessionNode(session, profile); - const mockFileInfo = { path: "/u/fake/path/file.txt" } as LocalFileInfo; - - jest.spyOn(ZoweDatasetNode.prototype, "downloadDs").mockResolvedValue(mockFileInfo); - jest.spyOn(ZoweUSSNode.prototype, "downloadUSS").mockResolvedValue(mockFileInfo); return { session, profile, dsSession, ussSession, - mockFileInfo, fileNodes: { ds: [ new ZoweDatasetNode({ label: "test", collapsibleState: vscode.TreeItemCollapsibleState.None, profile, parentNode: dsSession }), @@ -60,6 +51,7 @@ describe("LocalFileManagement unit tests", () => { new ZoweUSSNode({ label: "test4", collapsibleState: vscode.TreeItemCollapsibleState.None, profile, parentNode: ussSession }), ], }, + mockFileInfo: { path: "/u/fake/path/file.txt" }, warnLogSpy: jest.spyOn(ZoweLogger, "warn").mockImplementation(), executeCommand: jest.spyOn(vscode.commands, "executeCommand").mockImplementation(), }; @@ -68,24 +60,60 @@ describe("LocalFileManagement unit tests", () => { describe("CompareChosenFileContent method unit tests", () => { it("should pass with 2 MVS files chosen", async () => { const mocks = createGlobalMocks(); - globals.filesToCompare.push(mocks.fileNodes.ds[0]); + LocalFileManagement.filesToCompare = [mocks.fileNodes.ds[0]]; await LocalFileManagement.compareChosenFileContent(mocks.fileNodes.ds[1]); - expect(mocks.executeCommand).toHaveBeenCalledWith("vscode.diff", mocks.mockFileInfo, mocks.mockFileInfo); + expect(mocks.executeCommand).toHaveBeenCalledWith("vscode.diff", mocks.fileNodes.ds[0].resourceUri, mocks.fileNodes.ds[1].resourceUri); expect(mocks.warnLogSpy).not.toHaveBeenCalled(); }); it("should pass with 2 UNIX files chosen", async () => { const mocks = createGlobalMocks(); - globals.filesToCompare.push(mocks.fileNodes.uss[0]); + LocalFileManagement.filesToCompare = [mocks.fileNodes.uss[0]]; await LocalFileManagement.compareChosenFileContent(mocks.fileNodes.uss[1]); - expect(mocks.executeCommand).toHaveBeenCalledWith("vscode.diff", mocks.mockFileInfo, mocks.mockFileInfo); + expect(mocks.executeCommand).toHaveBeenCalledWith("vscode.diff", mocks.fileNodes.uss[0].resourceUri, mocks.fileNodes.uss[1].resourceUri); expect(mocks.warnLogSpy).not.toHaveBeenCalled(); }); it("should pass with 1 MVS file & 1 UNIX file chosen", async () => { const mocks = createGlobalMocks(); - globals.filesToCompare.push(mocks.fileNodes.uss[0]); + LocalFileManagement.filesToCompare = [mocks.fileNodes.uss[0]]; await LocalFileManagement.compareChosenFileContent(mocks.fileNodes.ds[0]); - expect(mocks.executeCommand).toHaveBeenCalledWith("vscode.diff", mocks.mockFileInfo, mocks.mockFileInfo); + expect(mocks.executeCommand).toHaveBeenCalledWith("vscode.diff", mocks.fileNodes.uss[0].resourceUri, mocks.fileNodes.ds[0].resourceUri); + expect(mocks.warnLogSpy).not.toHaveBeenCalled(); + }); + + it("should pass with 2 UNIX files chosen - readonly", async () => { + const mocks = createGlobalMocks(); + LocalFileManagement.filesToCompare = [mocks.fileNodes.uss[0]]; + await LocalFileManagement.compareChosenFileContent(mocks.fileNodes.uss[1], true); + expect(mocks.executeCommand).toHaveBeenCalledWith("vscode.diff", mocks.fileNodes.uss[0].resourceUri, mocks.fileNodes.uss[1].resourceUri); expect(mocks.warnLogSpy).not.toHaveBeenCalled(); + expect(mocks.executeCommand).toHaveBeenCalledWith("workbench.action.files.setActiveEditorReadonlyInSession"); + }); + }); + + describe("selectFileForCompare", () => { + it("calls reset when elements exist in filesToCompare array", () => { + const node2 = createUSSNode(createISession(), createIProfile()); + node2.label = "node2"; + LocalFileManagement.filesToCompare = [createUSSNode(createISession(), createIProfile())]; + const setCompareSelectionSpy = jest.spyOn(LocalFileManagement, "setCompareSelection"); + const resetSpy = jest.spyOn(LocalFileManagement, "resetCompareSelection"); + const traceSpy = jest.spyOn(ZoweLogger, "trace"); + LocalFileManagement.selectFileForCompare(node2); + expect(resetSpy).toHaveBeenCalled(); + expect(setCompareSelectionSpy).toHaveBeenCalledWith(true); + expect(LocalFileManagement.filesToCompare[0]).toBe(node2); + expect(traceSpy).toHaveBeenCalledWith("node2 selected for compare."); + }); + }); + + describe("resetCompareSelection", () => { + it("resets the compare selection and calls setCompareSelection", () => { + const node = createUSSNode(createISession(), createIProfile()); + const setCompareSelectionSpy = jest.spyOn(LocalFileManagement, "setCompareSelection"); + LocalFileManagement.filesToCompare = [node]; + LocalFileManagement.resetCompareSelection(); + expect(setCompareSelectionSpy).toHaveBeenCalled(); + expect(LocalFileManagement.filesToCompare.length).toBe(0); }); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/ProfileManagement.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/ProfileManagement.unit.test.ts index 56a4cdb054..859fc153df 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/ProfileManagement.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/ProfileManagement.unit.test.ts @@ -24,6 +24,7 @@ import { ZoweUSSNode } from "../../../src/uss/ZoweUSSNode"; import { ZoweJobNode } from "../../../src/job/ZoweJobNode"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; import { TreeProviders } from "../../../src/shared/TreeProviders"; +import { UssFSProvider } from "../../../src/uss/UssFSProvider"; jest.mock("fs"); jest.mock("vscode"); @@ -69,7 +70,12 @@ describe("ProfileManagement unit tests", () => { newMocks.mockTreeProviders.uss.mSessionNodes.push(newMocks.mockUnixSessionNode); newMocks.mockTreeProviders.job.mSessionNodes.push(newMocks.mockJobSessionNode); }, + FileSystemProvider: { + createDirectory: jest.fn(), + }, }; + + jest.spyOn(UssFSProvider.instance, "createDirectory").mockImplementation(newMocks.FileSystemProvider.createDirectory); Object.defineProperty(profUtils.ProfilesUtils, "promptCredentials", { value: jest.fn(), configurable: true }); newMocks.promptSpy = jest.spyOn(profUtils.ProfilesUtils, "promptCredentials"); Object.defineProperty(ZoweLogger, "debug", { value: jest.fn(), configurable: true }); diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts index 8f8c3a9452..9230056949 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts @@ -589,7 +589,7 @@ describe("ProfilesUtils unit tests", () => { await profUtils.ProfilesUtils.initializeZoweProfiles((msg) => ZoweExplorerExtender.showZoweConfigError(msg)); expect(initZoweFolderSpy).toHaveBeenCalledTimes(1); expect(readConfigFromDiskSpy).toHaveBeenCalledTimes(1); - expect(Gui.errorMessage).toHaveBeenCalledWith(expect.stringContaining(testError.message)); + expect(Gui.errorMessage).toHaveBeenCalledWith(expect.stringContaining("initializeZoweFolder failed")); }); it("should handle Imperative error thrown on read config from disk", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/workspace.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/workspace.unit.test.ts deleted file mode 100644 index 467ca10dd8..0000000000 --- a/packages/zowe-explorer/__tests__/__unit__/utils/workspace.unit.test.ts +++ /dev/null @@ -1,218 +0,0 @@ -/** - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - * - */ - -import * as vscode from "vscode"; -import * as workspaceUtils from "../../../src/utils/workspace"; -import { workspaceUtilMaxEmptyWindowsInTheRow } from "../../../src/config/constants"; - -function createGlobalMocks() { - const activeTextEditor = jest.fn(); - const executeCommand = jest.fn(); - - Object.defineProperty(vscode.window, "activeTextEditor", { get: activeTextEditor, configurable: true }); - Object.defineProperty(vscode.commands, "executeCommand", { value: executeCommand, configurable: true }); - - return { - activeTextEditor, - executeCommand, - }; -} - -/** - * Function which imitates looping through an array - */ -const generateCycledMock = (mock: any[]) => { - let currentIndex = 0; - - return () => { - if (currentIndex === mock.length) { - currentIndex = 0; - } - - const entry = mock[currentIndex]; - currentIndex++; - - return entry; - }; -}; - -describe("Workspace Utils Unit Test - Function checkTextFileIsOpened", () => { - it("Checking logic when no tabs available", async () => { - const globalMocks = createGlobalMocks(); - const targetTextFile = "/doc"; - globalMocks.activeTextEditor.mockReturnValue(null); - - const result = await workspaceUtils.checkTextFileIsOpened(targetTextFile); - expect(result).toBe(false); - expect(globalMocks.executeCommand.mock.calls.map((call) => call[0])).toEqual( - Array(workspaceUtilMaxEmptyWindowsInTheRow).fill("workbench.action.nextEditor") - ); - }); - it("Checking logic when target tab is available", async () => { - const globalMocks = createGlobalMocks(); - const targetTextFile = "/doc3"; - const mockDocuments = [ - { - id: 1, - document: { - fileName: "/doc1", - }, - }, - { - id: 2, - document: { - fileName: "/doc2", - }, - }, - { - id: 3, - document: { - fileName: "/doc3", - }, - }, - ]; - globalMocks.activeTextEditor.mockImplementation(generateCycledMock(mockDocuments)); - - const result = await workspaceUtils.checkTextFileIsOpened(targetTextFile); - expect(result).toBe(true); - expect(globalMocks.executeCommand.mock.calls.map((call) => call[0])).toEqual([ - "workbench.action.nextEditor", - "workbench.action.nextEditor", - "workbench.action.nextEditor", - ]); - }); - it("Checking logic when target tab is not available", async () => { - const globalMocks = createGlobalMocks(); - const targetTextFile = "/doc"; - const mockDocuments = [ - { - id: 1, - document: { - fileName: "/doc1", - }, - }, - { - id: 2, - document: { - fileName: "/doc2", - }, - }, - { - id: 3, - document: { - fileName: "/doc3", - }, - }, - ]; - globalMocks.activeTextEditor.mockImplementation(generateCycledMock(mockDocuments)); - - const result = await workspaceUtils.checkTextFileIsOpened(targetTextFile); - expect(result).toBe(false); - expect(globalMocks.executeCommand.mock.calls.map((call) => call[0])).toEqual([ - "workbench.action.nextEditor", - "workbench.action.nextEditor", - "workbench.action.nextEditor", - ]); - }); -}); -describe("Workspace Utils Unit Test - Function closeOpenedTextFile", () => { - it("Checking logic when no tabs available", async () => { - const globalMocks = createGlobalMocks(); - const targetTextFile = "/doc"; - globalMocks.activeTextEditor.mockReturnValueOnce(null); - - const result = await workspaceUtils.closeOpenedTextFile(targetTextFile); - expect(result).toBe(false); - expect(globalMocks.executeCommand.mock.calls.map((call) => call[0])).toEqual( - Array(workspaceUtilMaxEmptyWindowsInTheRow).fill("workbench.action.nextEditor") - ); - }); - it("Checking logic when target tab is available", async () => { - const globalMocks = createGlobalMocks(); - const targetTextFile = "/doc3"; - const mockDocuments = [ - { - id: 1, - document: { - fileName: "/doc1", - }, - }, - { - id: 2, - document: { - fileName: "/doc2", - }, - }, - { - id: 3, - document: { - fileName: "/doc3", - }, - }, - ]; - globalMocks.activeTextEditor.mockImplementation(generateCycledMock(mockDocuments)); - - const result = await workspaceUtils.closeOpenedTextFile(targetTextFile); - expect(result).toBe(true); - expect(globalMocks.executeCommand.mock.calls.map((call) => call[0])).toEqual([ - "workbench.action.nextEditor", - "workbench.action.nextEditor", - "workbench.action.closeActiveEditor", - ]); - }); - it("Checking logic when target tab is not available", async () => { - const globalMocks = createGlobalMocks(); - const targetTextFile = "/doc"; - const mockDocuments = [ - { - id: 1, - document: { - fileName: "/doc1", - }, - }, - { - id: 2, - document: { - fileName: "/doc2", - }, - }, - { - id: 3, - document: { - fileName: "/doc3", - }, - }, - ]; - globalMocks.activeTextEditor.mockImplementation(generateCycledMock(mockDocuments)); - - const result = await workspaceUtils.closeOpenedTextFile(targetTextFile); - expect(result).toBe(false); - expect(globalMocks.executeCommand.mock.calls.map((call) => call[0])).toEqual([ - "workbench.action.nextEditor", - "workbench.action.nextEditor", - "workbench.action.nextEditor", - ]); - }); -}); - -describe("Workspace Utils Unit Tests - function awaitForDocumentBeingSaved", () => { - it("should hold for a document to be saved", async () => { - let testCount = 0; - const testSaveTimer = setInterval(() => { - if (testCount > 5) { - workspaceUtils.setFileSaved(true); - clearInterval(testSaveTimer); - } - testCount++; - }); - await expect(workspaceUtils.awaitForDocumentBeingSaved()).resolves.not.toThrow(); - }); -}); diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index a4c7d936f5..82fe428e73 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -1,31 +1,4 @@ { - "Enter an allocation unit": "Enter an allocation unit", - "Enter the average block length (if allocation unit = BLK)": "Enter the average block length (if allocation unit = BLK)", - "Enter a block size": "Enter a block size", - "Enter an SMS data class": "Enter an SMS data class", - "Enter a device type (unit)": "Enter a device type (unit)", - "Enter the number of directory blocks": "Enter the number of directory blocks", - "Specify the data set type (DSNTYPE)": "Specify the data set type (DSNTYPE)", - "Enter the SMS management class": "Enter the SMS management class", - "Enter a data set name": "Enter a data set name", - "Select a data set organization (DSORG)": "Select a data set organization (DSORG)", - "Enter the primary space allocation": "Enter the primary space allocation", - "Enter the data set's record format": "Enter the data set's record format", - "Enter the logical record length": "Enter the logical record length", - "Enter the secondary space allocation": "Enter the secondary space allocation", - "Enter the size of the data set": "Enter the size of the data set", - "Enter the SMS storage class": "Enter the SMS storage class", - "Enter the volume serial on which the data set should be placed": "Enter the volume serial on which the data set should be placed", - "zowe.separator.recent": "zowe.separator.recent", - "Recent Filters": "Recent Filters", - "Options": "Options", - "Zowe Explorer's temp folder is located at {0}/Zowe temp folder": { - "message": "Zowe Explorer's temp folder is located at {0}", - "comment": [ - "Zowe temp folder" - ] - }, - "Zowe Explorer has activated successfully.": "Zowe Explorer has activated successfully.", "Error encountered when loading your Zowe config. Click \"Show Config\" for more details.": "Error encountered when loading your Zowe config. Click \"Show Config\" for more details.", "Failed to update Zowe schema: insufficient permissions or read-only file. {0}/Error message": { "message": "Failed to update Zowe schema: insufficient permissions or read-only file. {0}", @@ -145,6 +118,33 @@ "Apply to all trees": "Apply to all trees", "No": "No", "Apply to current tree selected": "Apply to current tree selected", + "Enter an allocation unit": "Enter an allocation unit", + "Enter the average block length (if allocation unit = BLK)": "Enter the average block length (if allocation unit = BLK)", + "Enter a block size": "Enter a block size", + "Enter an SMS data class": "Enter an SMS data class", + "Enter a device type (unit)": "Enter a device type (unit)", + "Enter the number of directory blocks": "Enter the number of directory blocks", + "Specify the data set type (DSNTYPE)": "Specify the data set type (DSNTYPE)", + "Enter the SMS management class": "Enter the SMS management class", + "Enter a data set name": "Enter a data set name", + "Select a data set organization (DSORG)": "Select a data set organization (DSORG)", + "Enter the primary space allocation": "Enter the primary space allocation", + "Enter the data set's record format": "Enter the data set's record format", + "Enter the logical record length": "Enter the logical record length", + "Enter the secondary space allocation": "Enter the secondary space allocation", + "Enter the size of the data set": "Enter the size of the data set", + "Enter the SMS storage class": "Enter the SMS storage class", + "Enter the volume serial on which the data set should be placed": "Enter the volume serial on which the data set should be placed", + "Recent": "Recent", + "Recent Filters": "Recent Filters", + "Options": "Options", + "Zowe Explorer's temp folder is located at {0}/Zowe temp folder": { + "message": "Zowe Explorer's temp folder is located at {0}", + "comment": [ + "Zowe temp folder" + ] + }, + "Zowe Explorer has activated successfully.": "Zowe Explorer has activated successfully.", "Refresh": "Refresh", "Delete Selected": "Delete Selected", "Select an item before deleting": "Select an item before deleting", @@ -309,60 +309,11 @@ "CLI setting" ] }, - "Remote file has changed. Presenting with way to resolve file.": "Remote file has changed. Presenting with way to resolve file.", - "Something went wrong with compare of files.": "Something went wrong with compare of files.", - "Downloaded: {0}/Download time": { - "message": "Downloaded: {0}", - "comment": [ - "Download time" - ] - }, - "Binary": "Binary", - "Encoding: {0}/Encoding name": { - "message": "Encoding: {0}", - "comment": [ - "Encoding name" - ] - }, - "{0} location/Node type": { - "message": "{0} location", - "comment": [ - "Node type" - ] - }, - "Choose a location to create the {0}/Node type": { - "message": "Choose a location to create the {0}", - "comment": [ - "Node type" - ] - }, - "Name of file or directory": "Name of file or directory", - "There is already a file with the same name.\n Please change your OS file system settings if you want to give case sensitive file names.": "There is already a file with the same name.\n Please change your OS file system settings if you want to give case sensitive file names.", - "Unable to create node:": "Unable to create node:", - "Uploading USS file": "Uploading USS file", - "save requested for USS file {0}/Document file name": { - "message": "save requested for USS file {0}", - "comment": [ - "Document file name" - ] - }, - "Could not locate session when saving USS file.": "Could not locate session when saving USS file.", - "Saving file...": "Saving file...", - "Delete": "Delete", - "Are you sure you want to delete the following item?\nThis will permanently remove the following file or folder from your system.\n\n{0}/File names": { - "message": "Are you sure you want to delete the following item?\nThis will permanently remove the following file or folder from your system.\n\n{0}", - "comment": [ - "File names" - ] - }, - "Delete action was canceled.": "Delete action was canceled.", - "Copying file structure...": "Copying file structure...", - "The paste operation is not supported for this node.": "The paste operation is not supported for this node.", - "Pasting files...": "Pasting files...", + "Favorites": "Favorites", + "Use the search button to list USS files": "Use the search button to list USS files", "Invalid node": "Invalid node", "Profile auth error": "Profile auth error", "Profile is not authenticated, please log in to continue": "Profile is not authenticated, please log in to continue", - "The response from Zowe CLI was not successful": "The response from Zowe CLI was not successful", "Retrieving response from uss-file-list": "Retrieving response from uss-file-list", "Open": "Open", "Delete action was cancelled.": "Delete action was cancelled.", @@ -379,30 +330,36 @@ ] }, "open() called from invalid node.": "open() called from invalid node.", - "There is already a file with the same name.\n Please change your OS file system settings if you want to give case sensitive file names": "There is already a file with the same name.\n Please change your OS file system settings if you want to give case sensitive file names", - "Downloading {0}/Label": { - "message": "Downloading {0}", - "comment": [ - "Label" - ] - }, "$(sync~spin) Downloading USS file...": "$(sync~spin) Downloading USS file...", "refreshUSS() called from invalid node.": "refreshUSS() called from invalid node.", "not found": "not found", - "Unable to find file: {0} was probably deleted./Label": { - "message": "Unable to find file: {0} was probably deleted.", + "Unable to find file: {0}/Error message": { + "message": "Unable to find file: {0}", "comment": [ - "Label" + "Error message" ] }, - "Re-download": "Re-download", - "Cancel": "Cancel", - "Failed to open file as text. Re-download file as binary?": "Failed to open file as text. Re-download file as binary?", - "Required API functions for pasting (fileList, copy and/or putContent) were not found.": "Required API functions for pasting (fileList, copy and/or putContent) were not found.", + "Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found.": "Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found.", "Uploading USS files...": "Uploading USS files...", "Error uploading files": "Error uploading files", + "Binary": "Binary", + "Downloaded: {0}/Download time": { + "message": "Downloaded: {0}", + "comment": [ + "Download time" + ] + }, + "Encoding: {0}/Encoding name": { + "message": "Encoding: {0}", + "comment": [ + "Encoding name" + ] + }, "$(plus) Create a new filter": "$(plus) Create a new filter", - "Favorites": "Favorites", + "Failed to move file {0}: {1}": "Failed to move file {0}: {1}", + "Confirm": "Confirm", + "One or more items may be overwritten from this drop operation. Confirm or cancel?": "One or more items may be overwritten from this drop operation. Confirm or cancel?", + "$(sync~spin) Moving USS files...": "$(sync~spin) Moving USS files...", "Unable to rename {0} because you have unsaved changes in this {1}. Please save your work before renaming the {1}./Node pathNode type": { "message": "Unable to rename {0} because you have unsaved changes in this {1}. Please save your work before renaming the {1}.", "comment": [ @@ -454,15 +411,56 @@ }, "initializeUSSFavorites.error.buttonRemove": "initializeUSSFavorites.error.buttonRemove", "File does not exist. It may have been deleted.": "File does not exist. It may have been deleted.", + "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", + "Could not list USS files: Empty path provided in URI": "Could not list USS files: Empty path provided in URI", + "Profile does not exist for this file.": "Profile does not exist for this file.", + "$(sync~spin) Saving USS file...": "$(sync~spin) Saving USS file...", + "Renaming {0} failed due to API error: {1}/File pathError message": { + "message": "Renaming {0} failed due to API error: {1}", + "comment": [ + "File path", + "Error message" + ] + }, + "Deleting {0} failed due to API error: {1}/File nameError message": { + "message": "Deleting {0} failed due to API error: {1}", + "comment": [ + "File name", + "Error message" + ] + }, + "{0} location/Node type": { + "message": "{0} location", + "comment": [ + "Node type" + ] + }, + "Choose a location to create the {0}/Node type": { + "message": "Choose a location to create the {0}", + "comment": [ + "Node type" + ] + }, + "Name of file or directory": "Name of file or directory", + "Unable to create node:": "Unable to create node:", + "Uploading USS file": "Uploading USS file", + "Delete": "Delete", + "Are you sure you want to delete the following item?\nThis will permanently remove the following file or folder from your system.\n\n{0}/File names": { + "message": "Are you sure you want to delete the following item?\nThis will permanently remove the following file or folder from your system.\n\n{0}", + "comment": [ + "File names" + ] + }, + "Delete action was canceled.": "Delete action was canceled.", + "Copying file structure...": "Copying file structure...", + "The paste operation is not supported for this node.": "The paste operation is not supported for this node.", + "Pasting files...": "Pasting files...", "Disabled": "Disabled", "Your jobs": "Your jobs", "Other user jobs": "Other user jobs", "All jobs": "All jobs", "Ascending": "Ascending", "Descending": "Descending", - "Saving Data Set...": "Saving Data Set...", - "Would you like to overwrite the remote file?": "Would you like to overwrite the remote file?", - "Upload cancelled.": "Upload cancelled.", "Invalid job owner": "Invalid job owner", "Invalid job prefix": "Invalid job prefix", "EBCDIC": "EBCDIC", @@ -490,58 +488,108 @@ ] }, "Enter a codepage (e.g., 1047, IBM-1047)": "Enter a codepage (e.g., 1047, IBM-1047)", - "File was saved -- determining whether the file is a USS file or Data set.\n \n Comparing (case insensitive) {0} against directory {1} and {2}/Saved file nameData Set directoryUSS directory": { - "message": "File was saved -- determining whether the file is a USS file or Data set.\n \n Comparing (case insensitive) {0} against directory {1} and {2}", - "comment": [ - "Saved file name", - "Data Set directory", - "USS directory" - ] - }, - "File is a Data Set-- saving ": "File is a Data Set-- saving ", - "File is a USS file -- saving": "File is a USS file -- saving", - "File {0} is not a Data Set or USS file/Saved file name": { - "message": "File {0} is not a Data Set or USS file", - "comment": [ - "Saved file name" - ] - }, "Team config file created, refreshing Zowe Explorer.": "Team config file created, refreshing Zowe Explorer.", "Team config file deleted, refreshing Zowe Explorer.": "Team config file deleted, refreshing Zowe Explorer.", "Team config file updated.": "Team config file updated.", + "Type the new pattern to add to history": "Type the new pattern to add to history", + "action is not supported for this property type.": "action is not supported for this property type.", + "Clear all history items for this persistent property?": "Clear all history items for this persistent property?", "Enter a filter": "Enter a filter", "No items are loaded in the tree.": "No items are loaded in the tree.", "You must enter a pattern.": "You must enter a pattern.", "Prompting the user to choose a recent member for editing": "Prompting the user to choose a recent member for editing", "Select a recent member to open": "Select a recent member to open", "No recent members found.": "No recent members found.", - "Compare": "Compare", - "Overwrite": "Overwrite", - "The content of the file is newer. Compare your version with latest or overwrite the content of the file with your changes.": "The content of the file is newer. Compare your version with latest or overwrite the content of the file with your changes.", - "Type the new pattern to add to history": "Type the new pattern to add to history", - "action is not supported for this property type.": "action is not supported for this property type.", - "Clear all history items for this persistent property?": "Clear all history items for this persistent property?", + "Use the search button to display jobs": "Use the search button to display jobs", + "There are no JES spool messages to display": "There are no JES spool messages to display", + "No jobs found": "No jobs found", + "Retrieving response from zowe.GetJobs": "Retrieving response from zowe.GetJobs", + "Create new...": "Create new...", + "$(check) Submit this query": "$(check) Submit this query", + "Enter job owner ID": "Enter job owner ID", + "Enter job prefix": "Enter job prefix", + "Enter job status": "Enter job status", + "$(plus) Create job search filter": "$(plus) Create job search filter", + "$(search) Search by job ID": "$(search) Search by job ID", + "Initializing profiles with jobs favorites.": "Initializing profiles with jobs favorites.", + "No jobs favorites found.": "No jobs favorites found.", + "Loading profile: {0} for jobs favorites/Profile name": { + "message": "Loading profile: {0} for jobs favorites", + "comment": [ + "Profile name" + ] + }, + "Error: You have Zowe Job favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Jobs view.\n Would you like to do this now? {1}/Profile nameApplication name": { + "message": "Error: You have Zowe Job favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Jobs view.\n Would you like to do this now? {1}", + "comment": [ + "Profile name", + "Application name" + ] + }, + "Remove": "Remove", + "This will remove all favorited Jobs items for profile {0}. Continue?/Profile name": { + "message": "This will remove all favorited Jobs items for profile {0}. Continue?", + "comment": [ + "Profile name" + ] + }, + "Enter a job ID": "Enter a job ID", + "Job search cancelled.": "Job search cancelled.", + "The polling interval must be greater than or equal to 1000ms.": "The polling interval must be greater than or equal to 1000ms.", + "Poll interval (in ms) for: {0}/URI path": { + "message": "Poll interval (in ms) for: {0}", + "comment": [ + "URI path" + ] + }, + "Polling dismissed for {0}; operation cancelled./Encoded URI path": { + "message": "Polling dismissed for {0}; operation cancelled.", + "comment": [ + "Encoded URI path" + ] + }, + "$(sync~spin) Polling: {0}.../Unique Spool name": { + "message": "$(sync~spin) Polling: {0}...", + "comment": [ + "Unique Spool name" + ] + }, + "Filter: {0}/The new filter": { + "message": "Filter: {0}", + "comment": [ + "The new filter" + ] + }, + "Set a filter...": "Set a filter...", + "$(clear-all) Clear filter for profile": "$(clear-all) Clear filter for profile", + "$(check) Filter cleared for {0}/Job label": { + "message": "$(check) Filter cleared for {0}", + "comment": [ + "Job label" + ] + }, + "Enter local filter...": "Enter local filter...", + "$(check) Filter updated for {0}/Job label": { + "message": "$(check) Filter updated for {0}", + "comment": [ + "Job label" + ] + }, "$(list-ordered) Job ID (default)": "$(list-ordered) Job ID (default)", "$(calendar) Date Submitted": "$(calendar) Date Submitted", "$(case-sensitive) Job Name": "$(case-sensitive) Job Name", "$(symbol-numeric) Return Code": "$(symbol-numeric) Return Code", "$(fold) Sort Direction": "$(fold) Sort Direction", "Go to Local Filtering": "Go to Local Filtering", - "$(clear-all) Clear filter for profile": "$(clear-all) Clear filter for profile", + "$(sync~spin) Fetching spool file...": "$(sync~spin) Fetching spool file...", + "Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API.": "Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API.", "Download Single Spool operation not implemented by extender. Please contact the extension developer(s).": "Download Single Spool operation not implemented by extender. Please contact the extension developer(s).", - "$(sync~spin) Opening spool file.../Label": { - "message": "$(sync~spin) Opening spool file...", - "comment": [ - "Label" - ] - }, "$(sync~spin) Polling: {0}.../Document file name": { "message": "$(sync~spin) Polling: {0}...", "comment": [ "Document file name" ] }, - "$(sync~spin) Fetching spool files...": "$(sync~spin) Fetching spool files...", "Modify Command": "Modify Command", "Command response: {0}/Command response": { "message": "Command response: {0}", @@ -606,85 +654,136 @@ "Session label" ] }, - "Use the search button to display jobs": "Use the search button to display jobs", - "There are no JES spool messages to display": "There are no JES spool messages to display", - "No jobs found": "No jobs found", - "Filtering by job status is not yet supported with this profile type. Will show jobs with all statuses.": "Filtering by job status is not yet supported with this profile type. Will show jobs with all statuses.", - "Retrieving response from zowe.GetJobs": "Retrieving response from zowe.GetJobs", - "Create new...": "Create new...", - "$(check) Submit this query": "$(check) Submit this query", - "Enter job owner ID": "Enter job owner ID", - "Enter job prefix": "Enter job prefix", - "Enter job status": "Enter job status", - "$(plus) Create job search filter": "$(plus) Create job search filter", - "$(search) Search by job ID": "$(search) Search by job ID", - "Initializing profiles with jobs favorites.": "Initializing profiles with jobs favorites.", - "No jobs favorites found.": "No jobs favorites found.", - "Loading profile: {0} for jobs favorites/Profile name": { - "message": "Loading profile: {0} for jobs favorites", + "Use the search button to display data sets": "Use the search button to display data sets", + "The response from Zowe CLI was not successful": "The response from Zowe CLI was not successful", + "Cannot access member with control characters in the name: {0}/Data Set member": { + "message": "Cannot access member with control characters in the name: {0}", + "comment": [ + "Data Set member" + ] + }, + "No data sets found": "No data sets found", + "Cannot download, item invalid.": "Cannot download, item invalid.", + "$(case-sensitive) Name (default)": "$(case-sensitive) Name (default)", + "$(calendar) Date Created": "$(calendar) Date Created", + "$(calendar) Date Modified": "$(calendar) Date Modified", + "$(account) User ID": "$(account) User ID", + "$(plus) Create a new filter. For example: HLQ.*, HLQ.aaa.bbb, HLQ.ccc.ddd(member)": "$(plus) Create a new filter. For example: HLQ.*, HLQ.aaa.bbb, HLQ.ccc.ddd(member)", + "Initializing profiles with data set favorites.": "Initializing profiles with data set favorites.", + "No data set favorites found.": "No data set favorites found.", + "Invalid Data Sets favorite: {0}. Please check formatting of the zowe.ds.history 'favorites' settings in the {1} user settings./Data Sets Favorite lineApplication name": { + "message": "Invalid Data Sets favorite: {0}. Please check formatting of the zowe.ds.history 'favorites' settings in the {1} user settings.", + "comment": [ + "Data Sets Favorite line", + "Application name" + ] + }, + "Error creating data set favorite node: {0} for profile {1}./LabelProfile name": { + "message": "Error creating data set favorite node: {0} for profile {1}.", "comment": [ + "Label", "Profile name" ] }, - "Error: You have Zowe Job favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Jobs view.\n Would you like to do this now? {1}/Profile nameApplication name": { - "message": "Error: You have Zowe Job favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Jobs view.\n Would you like to do this now? {1}", + "Loading profile: {0} for data set favorites/Profile name": { + "message": "Loading profile: {0} for data set favorites", + "comment": [ + "Profile name" + ] + }, + "Error: You have Zowe Data Set favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Data Sets view.\n Would you like to do this now? {1}/Profile nameApplication name": { + "message": "Error: You have Zowe Data Set favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Data Sets view.\n Would you like to do this now? {1}", "comment": [ "Profile name", "Application name" ] }, - "Remove": "Remove", - "This will remove all favorited Jobs items for profile {0}. Continue?/Profile name": { - "message": "This will remove all favorited Jobs items for profile {0}. Continue?", + "PDS already in favorites": "PDS already in favorites", + "This will remove all favorited Data Sets items for profile {0}. Continue?/Profile name": { + "message": "This will remove all favorited Data Sets items for profile {0}. Continue?", "comment": [ "Profile name" ] }, - "Enter a job ID": "Enter a job ID", - "Job search cancelled.": "Job search cancelled.", - "The polling interval must be greater than or equal to 1000ms.": "The polling interval must be greater than or equal to 1000ms.", - "Poll interval (in ms) for: {0}/URI path": { - "message": "Poll interval (in ms) for: {0}", + "Node does not exist. It may have been deleted.": "Node does not exist. It may have been deleted.", + "Prompting the user for a data set pattern": "Prompting the user for a data set pattern", + "Search Data Sets: use a comma to separate multiple patterns": "Search Data Sets: use a comma to separate multiple patterns", + "Enter valid member name": "Enter valid member name", + "Rename operation cancelled.": "Rename operation cancelled.", + "Renaming data set {0}/Old Data Set name": { + "message": "Renaming data set {0}", "comment": [ - "URI path" + "Old Data Set name" ] }, - "Polling dismissed for {0}; operation cancelled./Encoded URI path": { - "message": "Polling dismissed for {0}; operation cancelled.", + "Enter a valid data set name.": "Enter a valid data set name.", + "all PDS members in {0}/Node label": { + "message": "all PDS members in {0}", "comment": [ - "Encoded URI path" + "Node label" ] }, - "$(sync~spin) Polling: {0}.../Encoded URI path": { - "message": "$(sync~spin) Polling: {0}...", + "the PDS members in {0}/Node label": { + "message": "the PDS members in {0}", "comment": [ - "Encoded URI path" + "Node label" ] }, - "Filter: {0}/The new filter": { + "Select a sorting option for {0}/Specifier": { + "message": "Select a sorting option for {0}", + "comment": [ + "Specifier" + ] + }, + "$(check) Sorting updated for {0}/Node label": { + "message": "$(check) Sorting updated for {0}", + "comment": [ + "Node label" + ] + }, + "Filter: {0}/Filter value": { "message": "Filter: {0}", "comment": [ - "The new filter" + "Filter value" ] }, - "Set a filter...": "Set a filter...", - "$(check) Filter cleared for {0}/Job label": { + "$(clear-all) Clear filter for PDS": "$(clear-all) Clear filter for PDS", + "Set a filter for {0}/Specifier": { + "message": "Set a filter for {0}", + "comment": [ + "Specifier" + ] + }, + "$(check) Filter cleared for {0}/Node label": { "message": "$(check) Filter cleared for {0}", "comment": [ - "Job label" + "Node label" ] }, - "Enter local filter...": "Enter local filter...", - "$(check) Filter updated for {0}/Job label": { + "Invalid date format specified": "Invalid date format specified", + "Enter a value to filter by": "Enter a value to filter by", + "Invalid filter specified": "Invalid filter specified", + "$(check) Filter updated for {0}/Node label": { "message": "$(check) Filter updated for {0}", "comment": [ - "Job label" + "Node label" + ] + }, + "$(sync~spin) Saving data set...": "$(sync~spin) Saving data set...", + "Deleting {0} failed due to API error: {1}/File pathError message": { + "message": "Deleting {0} failed due to API error: {1}", + "comment": [ + "File path", + "Error message" + ] + }, + "Renaming {0} failed due to API error: {1}/File nameError message": { + "message": "Renaming {0} failed due to API error: {1}", + "comment": [ + "File name", + "Error message" ] }, - "$(case-sensitive) Name (default)": "$(case-sensitive) Name (default)", - "$(calendar) Date Created": "$(calendar) Date Created", - "$(calendar) Date Modified": "$(calendar) Date Modified", - "$(account) User ID": "$(account) User ID", "Partitioned Data Set: Binary": "Partitioned Data Set: Binary", "Partitioned Data Set: C": "Partitioned Data Set: C", "Partitioned Data Set: Classic": "Partitioned Data Set: Classic", @@ -698,7 +797,6 @@ "Select the profile to which the original data set belongs": "Select the profile to which the original data set belongs", "You must select a profile.": "You must select a profile.", "Enter the name of the data set to copy attributes from": "Enter the name of the data set to copy attributes from", - "Enter a valid data set name.": "Enter a valid data set name.", "You must enter a new data set name.": "You must enter a new data set name.", "Allocating data set like {0}./Like Data Set name": { "message": "Allocating data set like {0}.", @@ -740,7 +838,6 @@ ] }, "Name of Member": "Name of Member", - "Enter valid member name": "Enter valid member name", "Creating new data set member {0}/Data Set member name": { "message": "Creating new data set member {0}", "comment": [ @@ -821,13 +918,13 @@ ] }, "Item invalid.": "Item invalid.", + "$(sync~spin) Fetching data set...": "$(sync~spin) Fetching data set...", "Error encountered when refreshing data set view. {0}/Stringified JSON error": { "message": "Error encountered when refreshing data set view. {0}", "comment": [ "Stringified JSON error" ] }, - "Search Data Sets: use a comma to separate multiple patterns": "Search Data Sets: use a comma to separate multiple patterns", "Prompted for a data set pattern, recieved {0}./Data Set pattern": { "message": "Prompted for a data set pattern, recieved {0}.", "comment": [ @@ -850,143 +947,13 @@ "Unable to gather more information": "Unable to gather more information", "Invalid paste. Copy data set(s) first.": "Invalid paste. Copy data set(s) first.", "Name of Data Set Member": "Name of Data Set Member", - "Requested to save data set {0}/Document file name": { - "message": "Requested to save data set {0}", - "comment": [ - "Document file name" - ] - }, - "path.relative returned a non-blank directory. Assuming we are not in the DS_DIR directory: {0}/Relative path to Data Set directory": { - "message": "path.relative returned a non-blank directory. Assuming we are not in the DS_DIR directory: {0}", - "comment": [ - "Relative path to Data Set directory" - ] - }, - "Could not locate session when saving data set.": "Could not locate session when saving data set.", - "Could not find session node": "Could not find session node", - "Saving file {0}/Label": { - "message": "Saving file {0}", - "comment": [ - "Label" - ] - }, - "Data set failed to save. Data set may have been deleted or renamed on mainframe.": "Data set failed to save. Data set may have been deleted or renamed on mainframe.", - "Saving data set...": "Saving data set...", "Copying data sets is not supported.": "Copying data sets is not supported.", "Replace": "Replace", + "Cancel": "Cancel", "The data set member already exists.\nDo you want to replace it?": "The data set member already exists.\nDo you want to replace it?", "The physical sequential (PS) data set already exists.\nDo you want to replace it?": "The physical sequential (PS) data set already exists.\nDo you want to replace it?", "The partitioned (PO) data set already exists.\nDo you want to merge them while replacing any existing members?": "The partitioned (PO) data set already exists.\nDo you want to merge them while replacing any existing members?", "Unable to copy data set.": "Unable to copy data set.", - "Use the search button to display data sets": "Use the search button to display data sets", - "Cannot access member with control characters in the name: {0}/Data Set member": { - "message": "Cannot access member with control characters in the name: {0}", - "comment": [ - "Data Set member" - ] - }, - "No data sets found": "No data sets found", - "Invalid data set or member.": "Invalid data set or member.", - "$(sync~spin) Downloading data set...": "$(sync~spin) Downloading data set...", - "$(plus) Create a new filter. For example: HLQ.*, HLQ.aaa.bbb, HLQ.ccc.ddd(member)": "$(plus) Create a new filter. For example: HLQ.*, HLQ.aaa.bbb, HLQ.ccc.ddd(member)", - "Initializing profiles with data set favorites.": "Initializing profiles with data set favorites.", - "No data set favorites found.": "No data set favorites found.", - "Invalid Data Sets favorite: {0}. Please check formatting of the zowe.ds.history 'favorites' settings in the {1} user settings./Data Sets Favorite lineApplication name": { - "message": "Invalid Data Sets favorite: {0}. Please check formatting of the zowe.ds.history 'favorites' settings in the {1} user settings.", - "comment": [ - "Data Sets Favorite line", - "Application name" - ] - }, - "Error creating data set favorite node: {0} for profile {1}./LabelProfile name": { - "message": "Error creating data set favorite node: {0} for profile {1}.", - "comment": [ - "Label", - "Profile name" - ] - }, - "Loading profile: {0} for data set favorites/Profile name": { - "message": "Loading profile: {0} for data set favorites", - "comment": [ - "Profile name" - ] - }, - "Error: You have Zowe Data Set favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Data Sets view.\n Would you like to do this now? {1}/Profile nameApplication name": { - "message": "Error: You have Zowe Data Set favorites that refer to a non-existent CLI profile named: {0}.\n To resolve this, you can remove {0} from the Favorites section of Zowe Explorer's Data Sets view.\n Would you like to do this now? {1}", - "comment": [ - "Profile name", - "Application name" - ] - }, - "PDS already in favorites": "PDS already in favorites", - "This will remove all favorited Data Sets items for profile {0}. Continue?/Profile name": { - "message": "This will remove all favorited Data Sets items for profile {0}. Continue?", - "comment": [ - "Profile name" - ] - }, - "Node does not exist. It may have been deleted.": "Node does not exist. It may have been deleted.", - "Prompting the user for a data set pattern": "Prompting the user for a data set pattern", - "Rename operation cancelled.": "Rename operation cancelled.", - "Renaming data set {0}/Old Data Set name": { - "message": "Renaming data set {0}", - "comment": [ - "Old Data Set name" - ] - }, - "Unable to rename data set:": "Unable to rename data set:", - "all PDS members in {0}/Node label": { - "message": "all PDS members in {0}", - "comment": [ - "Node label" - ] - }, - "the PDS members in {0}/Node label": { - "message": "the PDS members in {0}", - "comment": [ - "Node label" - ] - }, - "Select a sorting option for {0}/Specifier": { - "message": "Select a sorting option for {0}", - "comment": [ - "Specifier" - ] - }, - "$(check) Sorting updated for {0}/Node label": { - "message": "$(check) Sorting updated for {0}", - "comment": [ - "Node label" - ] - }, - "Filter: {0}/Filter value": { - "message": "Filter: {0}", - "comment": [ - "Filter value" - ] - }, - "$(clear-all) Clear filter for PDS": "$(clear-all) Clear filter for PDS", - "Set a filter for {0}/Specifier": { - "message": "Set a filter for {0}", - "comment": [ - "Specifier" - ] - }, - "$(check) Filter cleared for {0}/Node label": { - "message": "$(check) Filter cleared for {0}", - "comment": [ - "Node label" - ] - }, - "Invalid date format specified": "Invalid date format specified", - "Enter a value to filter by": "Enter a value to filter by", - "Invalid filter specified": "Invalid filter specified", - "$(check) Filter updated for {0}/Node label": { - "message": "$(check) Filter updated for {0}", - "comment": [ - "Node label" - ] - }, "$(plus) Create a new Unix command": "$(plus) Create a new Unix command", "Zowe Unix Command": "Zowe Unix Command", "Issuing Commands is not supported for this profile.": "Issuing Commands is not supported for this profile.", diff --git a/packages/zowe-explorer/package.json b/packages/zowe-explorer/package.json index 97ad9a0b36..cd8d2d35a4 100644 --- a/packages/zowe-explorer/package.json +++ b/packages/zowe-explorer/package.json @@ -29,7 +29,10 @@ "resources" ], "activationEvents": [ - "onStartupFinished" + "onStartupFinished", + "onFileSystem:zowe-ds", + "onFileSystem:zowe-uss", + "onFileSystem:zowe-jobs" ], "main": "./out/src/main.extension", "l10n": "./l10n", @@ -836,6 +839,21 @@ "command": "zowe.uss.openWithEncoding", "title": "%openWithEncoding%", "category": "Zowe Explorer" + }, + { + "command": "zowe.diff.useLocalContent", + "title": "%diff.overwrite%", + "icon": "$(check)" + }, + { + "command": "zowe.diff.useRemoteContent", + "title": "%diff.useRemote%", + "icon": "$(discard)" + }, + { + "command": "zowe.placeholderCommand", + "title": "%zowe.placeholderCommand%", + "enablement": "false" } ], "menus": { @@ -853,6 +871,18 @@ "group": "000_zowe_dsMainframeInteraction@1" } ], + "editor/title": [ + { + "command": "zowe.diff.useLocalContent", + "group": "navigation@0", + "when": "resourceScheme =~ /zowe-.*/ && isInDiffEditor" + }, + { + "command": "zowe.diff.useRemoteContent", + "group": "navigation@1", + "when": "resourceScheme =~ /zowe-.*/ && isInDiffEditor" + } + ], "view/title": [ { "when": "view == zowe.ds.explorer", @@ -2033,8 +2063,9 @@ "scripts": { "build": "pnpm clean:bundle && pnpm license && webpack --mode development && pnpm madge", "build:integration": "pnpm createTestProfileData && pnpm license && tsc --pretty --project tsconfig-tests.json", - "test:unit": "jest \".*__tests__.*\\.unit\\.test\\.ts\" --coverage", "test": "pnpm test:unit", + "test:unit": "jest \".*__tests__.*\\.unit\\.test\\.ts\" --coverage", + "test:silent": "pnpm test:unit --reporters \"jest-silent-reporter\"", "vscode:prepublish": "pnpm clean:bundle && pnpm generateLocalization && webpack --mode production --devtool hidden-source-map", "package": "vsce package --no-dependencies && node ../../scripts/mv-pack.js vscode-extension-for-zowe vsix", "license": "node ../../scripts/updateLicenses.js", @@ -2064,27 +2095,24 @@ "@napi-rs/cli": "^2.16.1", "@types/chai": "^4.2.6", "@types/chai-as-promised": "^7.1.0", - "@types/copy-webpack-plugin": "^10.1.0", "@types/expect": "^1.20.3", "@types/fs-extra": "^7.0.0", "@types/promise-queue": "^2.2.0", "@types/selenium-webdriver": "^3.0.4", - "@types/yargs": "^11.0.0", "chai": "^4.1.2", "chai-as-promised": "^7.1.1", "chalk": "^2.4.1", "copy-webpack-plugin": "^12.0.2", "cross-env": "^5.2.0", "del": "^4.1.1", + "disposablestack": "^1.1.4", "eslint-plugin-zowe-explorer": "3.0.0-next-SNAPSHOT", "event-stream": "^4.0.1", "expect": "^24.8.0", - "geckodriver": "^1.19.1", "glob": "^7.1.6", - "jsdom": "^16.0.0", + "jest-silent-reporter": "^0.5.0", "log4js": "^6.4.6", "markdownlint-cli": "^0.33.0", - "mem": "^6.0.1", "run-sequence": "^2.2.1", "selenium-webdriver": "^3.6.0", "sinon": "^6.1.0" @@ -2099,10 +2127,7 @@ "dayjs": "^1.11.10", "fs-extra": "8.0.1", "isbinaryfile": "4.0.4", - "js-yaml": "3.13.1", - "promise-queue": "2.2.5", - "promise-status-async": "^1.2.10", - "yamljs": "0.3.0" + "promise-queue": "2.2.5" }, "jest": { "moduleFileExtensions": [ diff --git a/packages/zowe-explorer/package.nls.json b/packages/zowe-explorer/package.nls.json index a24276b142..80baf5c426 100644 --- a/packages/zowe-explorer/package.nls.json +++ b/packages/zowe-explorer/package.nls.json @@ -2,6 +2,7 @@ "displayName": "Zowe Explorer", "description": "VS Code extension, powered by Zowe CLI, that streamlines interaction with mainframe data sets, USS files, and jobs", "viewsContainers.activitybar": "Zowe Explorer", + "zowe.placeholderCommand": "Placeholder", "zowe.promptCredentials": "Update Credentials", "zowe.profileManagement": "Manage Profile", "zowe.extRefresh": "Refresh Zowe Explorer", @@ -13,6 +14,8 @@ "uss.createFolder": "Create Directory", "uss.deleteNode": "Delete", "uss.renameNode": "Rename", + "diff.overwrite": "Overwrite", + "diff.useRemote": "Use Remote", "addFavorite": "Add to Favorites", "removeFavProfile": "Remove profile from Favorites", "uss.addFavorite": "Add to Favorites", diff --git a/packages/zowe-explorer/src/PersistentFilters.ts b/packages/zowe-explorer/src/PersistentFilters.ts index 3bd1efdfdb..82240f4c27 100644 --- a/packages/zowe-explorer/src/PersistentFilters.ts +++ b/packages/zowe-explorer/src/PersistentFilters.ts @@ -35,7 +35,6 @@ export class PersistentFilters { private static readonly searchHistory: string = "searchHistory"; private static readonly fileHistory: string = "fileHistory"; private static readonly sessions: string = "sessions"; - private static readonly templates: string = "templates"; public schema: PersistenceSchemaEnum; private mSearchHistory: string[] = []; diff --git a/packages/zowe-explorer/src/Profiles.ts b/packages/zowe-explorer/src/Profiles.ts index 8971ed0611..be3bc04ee0 100644 --- a/packages/zowe-explorer/src/Profiles.ts +++ b/packages/zowe-explorer/src/Profiles.ts @@ -44,6 +44,7 @@ export class Profiles extends ProfilesCache { Profiles.loader = new Profiles(log, vscode.workspace.workspaceFolders?.[0]?.uri.fsPath); globals.setProfilesCache(Profiles.loader); await Profiles.loader.refresh(ZoweExplorerApiRegister.getInstance()); + await Profiles.getInstance().getProfileInfo(); return Profiles.loader; } @@ -529,10 +530,6 @@ export class Profiles extends ProfilesCache { public async promptCredentials(profile: string | imperative.IProfileLoaded, rePrompt?: boolean): Promise { ZoweLogger.trace("Profiles.promptCredentials called."); - let profType = ""; - if (typeof profile !== "string") { - profType = profile.type; - } const userInputBoxOptions: vscode.InputBoxOptions = { placeHolder: vscode.l10n.t(`User Name`), prompt: vscode.l10n.t(`Enter the user name for the connection. Leave blank to not store.`), @@ -892,40 +889,6 @@ export class Profiles extends ProfilesCache { .map((arg) => arg.argName); } - private async loginWithBaseProfile(serviceProfile: imperative.IProfileLoaded, loginTokenType: string, node?: Types.IZoweNodeType): Promise { - const baseProfile = await this.fetchBaseProfile(); - if (baseProfile) { - const creds = await this.loginCredentialPrompt(); - if (!creds) { - return; - } - const updSession = new imperative.Session({ - hostname: serviceProfile.profile.host, - port: serviceProfile.profile.port, - user: creds[0], - password: creds[1], - rejectUnauthorized: serviceProfile.profile.rejectUnauthorized, - tokenType: loginTokenType, - type: imperative.SessConstants.AUTH_TYPE_TOKEN, - }); - const loginToken = await ZoweExplorerApiRegister.getInstance().getCommonApi(serviceProfile).login(updSession); - const updBaseProfile: imperative.IProfile = { - tokenType: loginTokenType, - tokenValue: loginToken, - }; - await this.updateBaseProfileFileLogin(baseProfile, updBaseProfile); - const baseIndex = this.allProfiles.findIndex((profile) => profile.name === baseProfile.name); - this.allProfiles[baseIndex] = { ...baseProfile, profile: { ...baseProfile.profile, ...updBaseProfile } }; - if (node) { - node.setProfileToChoice({ - ...node.getProfile(), - profile: { ...node.getProfile().profile, ...updBaseProfile }, - }); - } - Gui.showMessage(vscode.l10n.t("Login to authentication service was successful.")); - } - } - private async loginWithRegularProfile(serviceProfile: imperative.IProfileLoaded, node?: Types.IZoweNodeType): Promise { let session: imperative.Session; if (node) { diff --git a/packages/zowe-explorer/src/SpoolProvider.ts b/packages/zowe-explorer/src/SpoolProvider.ts deleted file mode 100644 index 2531dd1051..0000000000 --- a/packages/zowe-explorer/src/SpoolProvider.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - * - */ - -import * as vscode from "vscode"; -import * as zosjobs from "@zowe/zos-jobs-for-zowe-sdk"; -import { ZoweExplorerApiRegister } from "./ZoweExplorerApiRegister"; -import { Profiles } from "./Profiles"; -import { ZoweLogger } from "./utils/ZoweLogger"; -import { IZoweJobTreeNode } from "@zowe/zowe-explorer-api"; - -export default class SpoolProvider implements vscode.TextDocumentContentProvider { - // Track files that have been opened previously through the SpoolProvider - public static files: { [key: string]: SpoolFile } = {}; - - public static scheme = "zosspool"; - public static onDidChangeEmitter = new vscode.EventEmitter(); - public onDidChange = SpoolProvider.onDidChangeEmitter.event; - - public async provideTextDocumentContent(uri: vscode.Uri): Promise { - ZoweLogger.trace("SpoolProvider.provideTextDocumentContent called."); - const spoolFile = SpoolProvider.files[uri.path]; - if (spoolFile) { - // Use latest cached content from stored SpoolFile object - return spoolFile.content; - } - - // Track the new spool file and pass the event emitter for future updates - const newSpoolFile = new SpoolFile(uri, SpoolProvider.onDidChangeEmitter); - await newSpoolFile.fetchContent(); - SpoolProvider.files[uri.path] = newSpoolFile; - return newSpoolFile.content; - } - - public dispose(): void { - SpoolProvider.onDidChangeEmitter.dispose(); - } -} - -/** - * Manage spool content for each file that is opened through the SpoolProvider. - */ -export class SpoolFile { - public content: string = ""; - private readonly emitter: vscode.EventEmitter; - private sessionName: string = ""; - private spool: zosjobs.IJobFile; - public uri: vscode.Uri; - - public constructor(uri: vscode.Uri, emitter: vscode.EventEmitter) { - this.uri = uri; - this.emitter = emitter; - [this.sessionName, this.spool] = decodeJobFile(this.uri); - } - - /** - * Caches content changes to the spool file for the SpoolProvider to display. - */ - public async fetchContent(): Promise { - const profile = Profiles.getInstance().loadNamedProfile(this.sessionName); - const result = await ZoweExplorerApiRegister.getJesApi(profile).getSpoolContentById(this.spool.jobname, this.spool.jobid, this.spool.id); - this.content = result; - - // Signal to the SpoolProvider that the new contents should be rendered for this file - this.emitter.fire(this.uri); - } -} - -/** - * (use {@link toUniqueJobFileUri} instead to use VSCode's cache invalidation) - * - * Encode the information needed to get the Spool content. - * - * @param session The name of the Zowe profile to use to get the Spool Content - * @param spool The IJobFile to get the spool content for. - */ -export function encodeJobFile(session: string, spool: zosjobs.IJobFile): vscode.Uri { - ZoweLogger.trace("SpoolProvider.encodeJobFile called."); - const query = JSON.stringify([session, spool]); - - const spoolSegments = [spool.jobname, spool.jobid, spool.stepname, spool.procstep, spool.ddname, spool.id?.toString()]; - - const path = spoolSegments.filter((v) => v && v.length).join("."); - - return vscode.Uri.parse("").with({ - scheme: SpoolProvider.scheme, - path, - query, - }); -} - -/** - * Encode the information needed to get the Spool content with support of the built in VSCode cache invalidation. - * - * VSCode built in cache will be applied automatically in case of several requests for the same URI, - * so consumers can control the amount of spool content requests by specifying different unique fragments - * - * Should be used carefully because of the possible memory leaks. - * - * @param session The name of the Zowe profile to use to get the Spool Content - * @param spool The IJobFile to get the spool content for. - * @param uniqueFragment The unique fragment of the encoded uri (can be timestamp, for example) - */ -export const toUniqueJobFileUri = - (session: string, spool: zosjobs.IJobFile) => - (uniqueFragment: string): vscode.Uri => { - ZoweLogger.trace("SpoolProvider.toUniqueJobFileUri called."); - const encodedUri = encodeJobFile(session, spool); - return encodedUri.with({ - fragment: uniqueFragment, - }); - }; - -/** - * Gather all spool files for a given job - * @param node Selected node for which to extract all spool files - * @returns Array of spool files - */ -export async function getSpoolFiles(node: IZoweJobTreeNode): Promise { - ZoweLogger.trace("SpoolProvider.getSpoolFiles called."); - if (node.job == null) { - return []; - } - let spools: zosjobs.IJobFile[] = []; - spools = await ZoweExplorerApiRegister.getJesApi(node.getProfile()).getSpoolFiles(node.job.jobname, node.job.jobid); - spools = spools - // filter out all the objects which do not seem to be correct Job File Document types - // see an issue #845 for the details - .filter((item) => !(item.id === undefined && item.ddname === undefined && item.stepname === undefined)); - return spools; -} - -/** - * Determine whether or not a spool file matches a selected node - * - * @param spool Individual spool file to match the node with - * @param node Selected node - * @returns true if the selected node matches the spool file, false otherwise - */ -export function matchSpool(spool: zosjobs.IJobFile, node: IZoweJobTreeNode): boolean { - return ( - `${spool.stepname}:${spool.ddname} - ${spool["record-count"]}` === node.label.toString() || - `${spool.stepname}:${spool.ddname} - ${spool.procstep}` === node.label.toString() - ); -} - -/** - * Decode the information needed to get the Spool content. - * - * @param uri The URI passed to TextDocumentContentProvider - */ -export function decodeJobFile(uri: vscode.Uri): [string, zosjobs.IJobFile] { - ZoweLogger.trace("SpoolProvider.decodeJobFile called."); - const [session, spool] = JSON.parse(uri.query) as [string, zosjobs.IJobFile]; - return [session, spool]; -} - -export function initializeSpoolProvider(context: vscode.ExtensionContext): void { - ZoweLogger.trace("SpoolProvider.initializeSpoolProvider called."); - const spoolProvider = new SpoolProvider(); - const providerRegistration = vscode.Disposable.from(vscode.workspace.registerTextDocumentContentProvider(SpoolProvider.scheme, spoolProvider)); - context.subscriptions.push(spoolProvider, providerRegistration); -} diff --git a/packages/zowe-explorer/src/SpoolUtils.ts b/packages/zowe-explorer/src/SpoolUtils.ts new file mode 100644 index 0000000000..ced2b50c12 --- /dev/null +++ b/packages/zowe-explorer/src/SpoolUtils.ts @@ -0,0 +1,48 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import * as zosjobs from "@zowe/zos-jobs-for-zowe-sdk"; +import { ZoweExplorerApiRegister } from "./ZoweExplorerApiRegister"; +import { ZoweLogger } from "./utils/ZoweLogger"; +import { IZoweJobTreeNode } from "@zowe/zowe-explorer-api"; + +/** + * Gather all spool files for a given job + * @param node Selected node for which to extract all spool files + * @returns Array of spool files + */ +export async function getSpoolFiles(node: IZoweJobTreeNode): Promise { + ZoweLogger.trace("SpoolProvider.getSpoolFiles called."); + if (node.job == null) { + return []; + } + let spools: zosjobs.IJobFile[] = []; + spools = await ZoweExplorerApiRegister.getJesApi(node.getProfile()).getSpoolFiles(node.job.jobname, node.job.jobid); + spools = spools + // filter out all the objects which do not seem to be correct Job File Document types + // see an issue #845 for the details + .filter((item) => !(item.id === undefined && item.ddname === undefined && item.stepname === undefined)); + return spools; +} + +/** + * Determine whether or not a spool file matches a selected node + * + * @param spool Individual spool file to match the node with + * @param node Selected node + * @returns true if the selected node matches the spool file, false otherwise + */ +export function matchSpool(spool: zosjobs.IJobFile, node: IZoweJobTreeNode): boolean { + return ( + `${spool.stepname}:${spool.ddname} - ${spool["record-count"]}` === node.label.toString() || + `${spool.stepname}:${spool.ddname} - ${spool.procstep}` === node.label.toString() + ); +} diff --git a/packages/zowe-explorer/src/ZoweExplorerApiRegister.ts b/packages/zowe-explorer/src/ZoweExplorerApiRegister.ts index e304220500..b6d58743ac 100644 --- a/packages/zowe-explorer/src/ZoweExplorerApiRegister.ts +++ b/packages/zowe-explorer/src/ZoweExplorerApiRegister.ts @@ -9,7 +9,7 @@ * */ -import { imperative, IApiExplorerExtender, MainframeInteraction, Types, Validation, ZoweExplorerZosmf } from "@zowe/zowe-explorer-api"; +import { imperative, IApiExplorerExtender, MainframeInteraction, Types, Validation, ZoweExplorerZosmf, ZoweScheme } from "@zowe/zowe-explorer-api"; import { ZoweExplorerExtender } from "./ZoweExplorerExtender"; import { ZoweLogger } from "./utils/ZoweLogger"; import * as vscode from "vscode"; @@ -20,6 +20,7 @@ import * as vscode from "vscode"; */ export class ZoweExplorerApiRegister implements Types.IApiRegisterClient { public static ZoweExplorerApiRegisterInst: ZoweExplorerApiRegister; + private static eventMap: Record> = {}; /** * Access the singleton instance. @@ -348,4 +349,22 @@ export class ZoweExplorerApiRegister implements Types.IApiRegisterClient { public get onProfilesUpdate(): vscode.Event { return this.onProfilesUpdateEmitter.event; } + + /** + * Access the specific event that fires when a resource from the given scheme is updated (changed/created/deleted). + * @param scheme The scheme of the resource (Data Sets, USS, Jobs, or an extender scheme) + * @returns an instance of the event to add listeners to + */ + public static onResourceChanged(scheme: ZoweScheme | string): vscode.Event { + return ZoweExplorerApiRegister.eventMap[scheme]; + } + + /** + * Access the specific event that fires when a resource from the given scheme is updated (changed/created/deleted). + * @param scheme The scheme of the resource (Data Sets, USS, or Jobs, or an extender scheme) + * @param event The event that fires when changes are made to URIs matching the given scheme + */ + public static addFileSystemEvent(scheme: ZoweScheme | string, event: vscode.Event): void { + ZoweExplorerApiRegister.eventMap[scheme] = event; + } } diff --git a/packages/zowe-explorer/src/ZoweExplorerExtender.ts b/packages/zowe-explorer/src/ZoweExplorerExtender.ts index 698338d912..9c44197093 100644 --- a/packages/zowe-explorer/src/ZoweExplorerExtender.ts +++ b/packages/zowe-explorer/src/ZoweExplorerExtender.ts @@ -12,7 +12,6 @@ import * as PromiseQueue from "promise-queue"; import * as path from "path"; import * as fs from "fs"; -import * as globals from "./globals"; import * as vscode from "vscode"; import { IApiExplorerExtender, @@ -26,6 +25,7 @@ import { } from "@zowe/zowe-explorer-api"; import { getProfile, ProfilesUtils } from "./utils/ProfilesUtils"; import { ZoweLogger } from "./utils/ZoweLogger"; +import * as globals from "./globals"; /** * The Zowe Explorer API Register singleton that gets exposed to other VS Code diff --git a/packages/zowe-explorer/src/abstract/ZoweSaveQueue.ts b/packages/zowe-explorer/src/abstract/ZoweSaveQueue.ts index c70ec1d2de..f2a5749e58 100644 --- a/packages/zowe-explorer/src/abstract/ZoweSaveQueue.ts +++ b/packages/zowe-explorer/src/abstract/ZoweSaveQueue.ts @@ -12,7 +12,6 @@ import * as path from "path"; import * as vscode from "vscode"; import { Gui, IZoweTree, IZoweTreeNode } from "@zowe/zowe-explorer-api"; -import { markDocumentUnsaved } from "../utils/workspace"; import { ZoweLogger } from "../utils/ZoweLogger"; interface SaveRequest { @@ -62,7 +61,6 @@ export class ZoweSaveQueue { await nextRequest.uploadRequest(nextRequest.savedFile, nextRequest.fileProvider); } catch (err) { ZoweLogger.error(err); - await markDocumentUnsaved(nextRequest.savedFile); await Gui.errorMessage( vscode.l10n.t({ message: "Failed to upload changes for {0}: {1}", diff --git a/packages/zowe-explorer/src/abstract/ZoweTreeProvider.ts b/packages/zowe-explorer/src/abstract/ZoweTreeProvider.ts index 188ddb2ec6..d6117fc8b3 100644 --- a/packages/zowe-explorer/src/abstract/ZoweTreeProvider.ts +++ b/packages/zowe-explorer/src/abstract/ZoweTreeProvider.ts @@ -78,16 +78,6 @@ export class ZoweTreeProvider { } } - /** - * Fire the "onDidChangeTreeData" event to signal that a node in the tree has changed. - * Unlike `refreshElement`, this function does *not* signal a refresh for the given node - - * it simply tells VS Code to repaint the node in the tree. - * @param node The node that should be repainted - */ - public nodeDataChanged(node: IZoweTreeNode): void { - this.mOnDidChangeTreeData.fire(node); - } - /** * Called whenever the tree needs to be refreshed, and fires the data change event * @@ -98,13 +88,24 @@ export class ZoweTreeProvider { this.mOnDidChangeTreeData.fire(element); } + /** + * Fire the "onDidChangeTreeData" event to signal that a node in the tree has changed. + * Unlike `refreshElement`, this function does *not* signal a refresh for the given node - + * it simply tells VS Code to repaint the node in the tree. + * @param node The node that should be repainted + */ + public nodeDataChanged(element: IZoweTreeNode): void { + ZoweLogger.trace("ZoweTreeProvider.nodeDataChanged called."); + this.mOnDidChangeTreeData.fire(element); + } + /** * Called whenever the tree needs to be refreshed, and fires the data change event * */ public refresh(): void { ZoweLogger.trace("ZoweTreeProvider.refresh called."); - this.mOnDidChangeTreeData.fire(); + this.mOnDidChangeTreeData.fire(null); } /** diff --git a/packages/zowe-explorer/src/command/UnixCommandHandler.ts b/packages/zowe-explorer/src/command/UnixCommandHandler.ts index f8dcc2ef6c..5806df6482 100644 --- a/packages/zowe-explorer/src/command/UnixCommandHandler.ts +++ b/packages/zowe-explorer/src/command/UnixCommandHandler.ts @@ -11,7 +11,7 @@ import * as vscode from "vscode"; import * as globals from "../globals"; -import { Gui, Validation, imperative, IZoweTreeNode } from "@zowe/zowe-explorer-api"; +import { Gui, imperative, IZoweTreeNode } from "@zowe/zowe-explorer-api"; import { Profiles } from "../Profiles"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; import { errorHandling, FilterDescriptor, FilterItem } from "../utils/ProfilesUtils"; diff --git a/packages/zowe-explorer/src/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/dataset/DatasetFSProvider.ts new file mode 100644 index 0000000000..ec52ee1fdc --- /dev/null +++ b/packages/zowe-explorer/src/dataset/DatasetFSProvider.ts @@ -0,0 +1,484 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { + BaseProvider, + BufferBuilder, + DirEntry, + DsEntry, + DsEntryMetadata, + MemberEntry, + PdsEntry, + getInfoForUri, + isDirectoryEntry, + isFilterEntry, + isPdsEntry, + FilterEntry, + Gui, + ZosEncoding, + ZoweScheme, +} from "@zowe/zowe-explorer-api"; +import * as path from "path"; +import * as vscode from "vscode"; +import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; + +// Set up localization +import { IZosFilesResponse } from "@zowe/zos-files-for-zowe-sdk"; +import { Profiles } from "../Profiles"; +import { getLanguageId } from "../dataset/utils"; +import { ZoweLogger } from "../utils/ZoweLogger"; + +export class DatasetFSProvider extends BaseProvider implements vscode.FileSystemProvider { + private static _instance: DatasetFSProvider; + private constructor() { + super(); + ZoweExplorerApiRegister.addFileSystemEvent(ZoweScheme.DS, this.onDidChangeFile); + this.root = new DirEntry(""); + } + + /** + * @returns the Data Set FileSystemProvider singleton instance + */ + public static get instance(): DatasetFSProvider { + if (!DatasetFSProvider._instance) { + DatasetFSProvider._instance = new DatasetFSProvider(); + } + + return DatasetFSProvider._instance; + } + + /** + * onDidOpenTextDocument event listener for the dataset provider. + * Updates the opened document with the correct language ID. + * @param doc The document received from the onDidOpenTextDocument event. + */ + public static async onDidOpenTextDocument(this: void, doc: vscode.TextDocument): Promise { + if (doc.uri.scheme !== ZoweScheme.DS) { + return; + } + + const parentPath = path.posix.basename(path.posix.join(doc.uri.path, "..")); + const languageId = getLanguageId(parentPath); + if (languageId == null) { + return; + } + + try { + await vscode.languages.setTextDocumentLanguage(doc, languageId); + } catch (err) { + ZoweLogger.warn(`Could not set document language for ${doc.fileName} - tried languageId '${languageId}'`); + } + } + + public watch(_uri: vscode.Uri, _options: { readonly recursive: boolean; readonly excludes: readonly string[] }): vscode.Disposable { + // ignore, fires for all changes... + return new vscode.Disposable(() => {}); + } + + /** + * Returns file statistics about a given URI. + * @param uri A URI that must exist as an entry in the provider + * @returns A structure containing file type, time, size and other metrics + */ + public stat(uri: vscode.Uri): vscode.FileStat { + if (uri.query) { + const queryParams = new URLSearchParams(uri.query); + if (queryParams.has("conflict")) { + return { ...this._lookup(uri, false), permissions: vscode.FilePermission.Readonly }; + } + } + return this._lookup(uri, false); + } + + /** + * Reads a directory located at the given URI. + * @param uri A valid URI within the provider + * @returns An array of tuples containing each entry name and type + */ + public async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> { + const dsEntry = this._lookupAsDirectory(uri, false); + const uriInfo = getInfoForUri(uri, Profiles.getInstance()); + + const results: [string, vscode.FileType][] = []; + + if (isFilterEntry(dsEntry)) { + const mvsApi = ZoweExplorerApiRegister.getMvsApi(uriInfo.profile); + const datasetResponses: IZosFilesResponse[] = []; + const dsPatterns = [ + ...new Set( + dsEntry.filter["pattern"] + .toUpperCase() + .split(",") + .map((p) => p.trim()) + ), + ]; + + if (mvsApi.dataSetsMatchingPattern) { + datasetResponses.push(await mvsApi.dataSetsMatchingPattern(dsPatterns)); + } else { + for (const dsp of dsPatterns) { + datasetResponses.push(await mvsApi.dataSet(dsp)); + } + } + + for (const resp of datasetResponses) { + for (const ds of resp.apiResponse?.items ?? resp.apiResponse ?? []) { + let tempEntry = dsEntry.entries.get(ds.dsname); + if (tempEntry == null) { + if (ds.dsorg === "PO" || ds.dsorg === "PO-E") { + // Entry is a PDS + tempEntry = new PdsEntry(ds.dsname); + } else if (ds.dsorg === "VS") { + // TODO: Add VSAM and ZFS support in Zowe Explorer + continue; + } else if (ds.migr?.toUpperCase() === "YES") { + // migrated + tempEntry = new DsEntry(ds.dsname); + } else { + // PS + tempEntry = new DsEntry(ds.dsname); + } + tempEntry.metadata = new DsEntryMetadata({ ...dsEntry.metadata, path: path.posix.join(dsEntry.metadata.path, ds.dsname) }); + dsEntry.entries.set(ds.dsname, tempEntry); + } + results.push([tempEntry.name, tempEntry instanceof DsEntry ? vscode.FileType.File : vscode.FileType.Directory]); + } + } + } else if (isDirectoryEntry(dsEntry)) { + const members = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(dsEntry.name); + + for (const ds of members.apiResponse?.items || []) { + let tempEntry = dsEntry.entries.get(ds.member); + if (tempEntry == null) { + tempEntry = new MemberEntry(ds.member); + tempEntry.metadata = new DsEntryMetadata({ ...dsEntry.metadata, path: path.posix.join(dsEntry.metadata.path, ds.member) }); + dsEntry.entries.set(ds.member, tempEntry); + } + results.push([tempEntry.name, vscode.FileType.File]); + } + } + + return results; + } + + public updateFilterForUri(uri: vscode.Uri, pattern: string): void { + const filterEntry = this._lookup(uri, false); + if (!isFilterEntry(filterEntry)) { + return; + } + + filterEntry.filter["pattern"] = pattern; + } + + /** + * Creates a directory entry in the provider at the given URI. + * @param uri The URI that represents a new directory path + */ + public createDirectory(uri: vscode.Uri, filter?: string): void { + const basename = path.posix.basename(uri.path); + const parent = this._lookupParentDirectory(uri, false); + if (parent.entries.has(basename)) { + return; + } + const profInfo = + parent !== this.root + ? new DsEntryMetadata({ + profile: parent.metadata.profile, + // we can strip profile name from path because its not involved in API calls + path: path.posix.join(parent.metadata.path, basename), + }) + : this._getInfoFromUri(uri); + + if (isFilterEntry(parent)) { + const entry = new PdsEntry(basename); + entry.metadata = profInfo; + parent.entries.set(entry.name, entry); + } else { + const entry = new FilterEntry(basename); + entry.filter["pattern"] = filter; + entry.metadata = profInfo; + parent.entries.set(entry.name, entry); + } + + parent.mtime = Date.now(); + parent.size += 1; + this._fireSoon( + { type: vscode.FileChangeType.Changed, uri: uri.with({ path: path.posix.join(uri.path, "..") }) }, + { type: vscode.FileChangeType.Created, uri } + ); + } + + /** + * Fetches a data set from the remote system at the given URI. + * @param uri The URI pointing to a valid file to fetch from the remote system + * @param editor (optional) An editor instance to reload if the URI is already open + */ + public async fetchDatasetAtUri(uri: vscode.Uri, options?: { editor?: vscode.TextEditor | null; isConflict?: boolean }): Promise { + const file = this._lookupAsFile(uri) as DsEntry; + // we need to fetch the contents from the mainframe since the file hasn't been accessed yet + const bufBuilder = new BufferBuilder(); + const metadata = file.metadata ?? this._getInfoFromUri(uri); + const profileEncoding = file.encoding ? null : file.metadata.profile.profile?.encoding; + const resp = await ZoweExplorerApiRegister.getMvsApi(metadata.profile).getContents(metadata.dsName, { + binary: file.encoding?.kind === "binary", + encoding: file.encoding?.kind === "other" ? file.encoding.codepage : profileEncoding, + responseTimeout: metadata.profile.profile?.responseTimeout, + returnEtag: true, + stream: bufBuilder, + }); + const data: Uint8Array = bufBuilder.read() ?? new Uint8Array(); + + if (options?.isConflict) { + file.conflictData = { + contents: data, + etag: resp.apiResponse.etag, + size: data.byteLength, + }; + } else { + file.data = data; + file.etag = resp.apiResponse.etag; + file.size = file.data.byteLength; + } + + if (options?.editor) { + await this._updateResourceInEditor(uri); + } + } + + /** + * Reads a data set at the given URI and fetches it from the remote system (if not yet accessed). + * @param uri The URI pointing to a valid data set on the remote system + * @returns The data set's contents as an array of bytes + */ + public async readFile(uri: vscode.Uri): Promise { + const file = this._lookupAsFile(uri); + const profInfo = this._getInfoFromUri(uri); + + if (profInfo.profile == null) { + throw vscode.FileSystemError.FileNotFound(vscode.l10n.t("Profile does not exist for this file.")); + } + + const urlQuery = new URLSearchParams(uri.query); + const isConflict = urlQuery.has("conflict"); + + // we need to fetch the contents from the mainframe if the file hasn't been accessed yet + if (!file.wasAccessed || isConflict) { + await this.fetchDatasetAtUri(uri, { isConflict }); + if (!isConflict) { + file.wasAccessed = true; + } + } + + return isConflict ? file.conflictData.contents : file.data; + } + + public makeEmptyDsWithEncoding(uri: vscode.Uri, encoding: ZosEncoding): void { + const parentDir = this._lookupParentDirectory(uri); + const fileName = path.posix.basename(uri.path); + const entry = new DsEntry(fileName); + entry.encoding = encoding; + entry.metadata = new DsEntryMetadata({ + ...parentDir.metadata, + path: path.posix.join(parentDir.metadata.path, fileName), + }); + entry.data = new Uint8Array(); + parentDir.entries.set(fileName, entry); + } + + private async uploadEntry(parent: DirEntry, entry: DsEntry, content: Uint8Array, forceUpload?: boolean): Promise { + const statusMsg = Gui.setStatusBarMessage(vscode.l10n.t("$(sync~spin) Saving data set...")); + const isPdsMember = isPdsEntry(parent) && !isFilterEntry(parent); + const fullName = isPdsMember ? `${parent.name}(${entry.name})` : entry.name; + let resp: IZosFilesResponse; + try { + const mvsApi = ZoweExplorerApiRegister.getMvsApi(entry.metadata.profile); + const profileEncoding = entry.encoding ? null : entry.metadata.profile.profile?.encoding; + resp = await mvsApi.uploadFromBuffer(Buffer.from(content), fullName, { + binary: entry.encoding?.kind === "binary", + encoding: entry.encoding?.kind === "other" ? entry.encoding.codepage : profileEncoding, + etag: forceUpload ? undefined : entry.etag, + returnEtag: true, + }); + } catch (err) { + statusMsg.dispose(); + throw err; + } + statusMsg.dispose(); + return resp; + } + + /** + * Attempts to write a data set at the given URI. + * @param uri The URI pointing to a data set entry that should be written + * @param content The content to write to the data set, as an array of bytes + * @param options Options for writing the data set + * - `create` - Creates the data set if it does not exist + * - `overwrite` - Overwrites the content if the data set exists + */ + public async writeFile(uri: vscode.Uri, content: Uint8Array, options: { readonly create: boolean; readonly overwrite: boolean }): Promise { + const basename = path.posix.basename(uri.path); + const parent = this._lookupParentDirectory(uri); + let entry = parent.entries.get(basename); + if (isDirectoryEntry(entry)) { + throw vscode.FileSystemError.FileIsADirectory(uri); + } + if (!entry && !options.create) { + throw vscode.FileSystemError.FileNotFound(uri); + } + if (entry && options.create && !options.overwrite) { + throw vscode.FileSystemError.FileExists(uri); + } + + // Attempt to write data to remote system, and handle any conflicts from e-tag mismatch + const urlQuery = new URLSearchParams(uri.query); + const forceUpload = urlQuery.has("forceUpload"); + // Attempt to write data to remote system, and handle any conflicts from e-tag mismatch + + try { + if (!entry) { + entry = new DsEntry(basename); + entry.data = content; + const profInfo = parent.metadata + ? new DsEntryMetadata({ + profile: parent.metadata.profile, + path: path.posix.join(parent.metadata.path, basename), + }) + : this._getInfoFromUri(uri); + entry.metadata = profInfo; + + if (content.byteLength > 0) { + // Update e-tag if write was successful. + const resp = await this.uploadEntry(parent, entry as DsEntry, content, forceUpload); + entry.etag = resp.apiResponse.etag; + entry.data = content; + } + parent.entries.set(basename, entry); + this._fireSoon({ type: vscode.FileChangeType.Created, uri }); + } else { + if (urlQuery.has("inDiff")) { + // Allow users to edit files in diff view. + // If in diff view, we don't want to make any API calls, just keep track of latest + // changes to data. + entry.data = content; + entry.mtime = Date.now(); + entry.size = content.byteLength; + entry.inDiffView = true; + return; + } + + if (entry.wasAccessed || content.length > 0) { + const resp = await this.uploadEntry(parent, entry as DsEntry, content, forceUpload); + entry.etag = resp.apiResponse.etag; + } + entry.data = content; + } + } catch (err) { + if (!err.message.includes("Rest API failure with HTTP(S) status 412")) { + throw err; + } + + entry.data = content; + // Prompt the user with the conflict dialog + await this._handleConflict(uri, entry); + return; + } + + entry.mtime = Date.now(); + entry.size = content.byteLength; + this._fireSoon({ type: vscode.FileChangeType.Changed, uri }); + } + + /** + * Returns metadata about the data set entry from the context of z/OS. + * @param uri A URI with a path in the format `zowe-*:/{lpar_name}/{full_path}?` + * @returns Metadata for the URI that contains the profile instance and path + */ + private _getInfoFromUri(uri: vscode.Uri): DsEntryMetadata { + const uriInfo = getInfoForUri(uri, Profiles.getInstance()); + return new DsEntryMetadata({ + profile: uriInfo.profile, + path: uri.path.substring(uriInfo.slashAfterProfilePos), + }); + } + + public async delete(uri: vscode.Uri, _options: { readonly recursive: boolean }): Promise { + const entry = this._lookup(uri, false); + const parent = this._lookupParentDirectory(uri); + let fullName: string = ""; + if (isPdsEntry(parent)) { + fullName = `${parent.name}(${entry.name})`; + } else { + fullName = entry.name; + } + + try { + await ZoweExplorerApiRegister.getMvsApi(entry.metadata.profile).deleteDataSet(fullName, { + responseTimeout: entry.metadata.profile.profile?.responseTimeout, + }); + } catch (err) { + await Gui.errorMessage( + vscode.l10n.t({ + message: "Deleting {0} failed due to API error: {1}", + args: [entry.metadata.path, err.message], + comment: ["File path", "Error message"], + }) + ); + return; + } + + parent.entries.delete(entry.name); + parent.size -= 1; + + this._fireSoon({ type: vscode.FileChangeType.Deleted, uri }); + } + + public async rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { readonly overwrite: boolean }): Promise { + const newUriEntry = this._lookup(newUri, true); + if (!options.overwrite && newUriEntry) { + throw vscode.FileSystemError.FileExists(`Rename failed: ${path.posix.basename(newUri.path)} already exists`); + } + + const entry = this._lookup(oldUri, false) as PdsEntry | DsEntry; + const parentDir = this._lookupParentDirectory(oldUri); + + const oldName = entry.name; + const newName = path.posix.basename(newUri.path); + + try { + if (isPdsEntry(entry)) { + await ZoweExplorerApiRegister.getMvsApi(entry.metadata.profile).renameDataSet(oldName, newName); + } else { + const pdsName = path.basename(path.posix.join(entry.metadata.path, "..")); + await ZoweExplorerApiRegister.getMvsApi(entry.metadata.profile).renameDataSetMember(pdsName, oldName, newName); + } + } catch (err) { + await Gui.errorMessage( + vscode.l10n.t({ + message: "Renaming {0} failed due to API error: {1}", + args: [oldName, err.message], + comment: ["File name", "Error message"], + }) + ); + return; + } + + parentDir.entries.delete(entry.name); + entry.name = newName; + + // Build the new path using the previous path and new file/folder name. + const newPath = path.posix.join(entry.metadata.path, "..", newName); + + entry.metadata.path = newPath; + parentDir.entries.set(newName, entry); + + this._fireSoon({ type: vscode.FileChangeType.Deleted, uri: oldUri }, { type: vscode.FileChangeType.Created, uri: newUri }); + } +} diff --git a/packages/zowe-explorer/src/dataset/DatasetTree.ts b/packages/zowe-explorer/src/dataset/DatasetTree.ts index 2a5303480b..dea0133ca7 100644 --- a/packages/zowe-explorer/src/dataset/DatasetTree.ts +++ b/packages/zowe-explorer/src/dataset/DatasetTree.ts @@ -9,6 +9,7 @@ * */ +import * as path from "path"; import * as vscode from "vscode"; import * as globals from "../globals"; import { @@ -22,24 +23,24 @@ import { IZoweTreeNode, Sorting, ZosEncoding, + confirmForUnsavedDoc, } from "@zowe/zowe-explorer-api"; import { Profiles } from "../Profiles"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; import { FilterDescriptor, FilterItem, errorHandling, syncSessionNode } from "../utils/ProfilesUtils"; -import { sortTreeItems, getAppName, getDocumentFilePath, SORT_DIRS, promptForEncoding, updateOpenFiles } from "../shared/utils"; +import { sortTreeItems, getAppName, SORT_DIRS, promptForEncoding, updateOpenFiles } from "../shared/utils"; import { ZoweTreeProvider } from "../abstract/ZoweTreeProvider"; import { ZoweDatasetNode } from "./ZoweDatasetNode"; import { getIconById, getIconByNode, IconId, IIconItem } from "../generators/icons"; import * as dayjs from "dayjs"; -import * as fs from "fs"; import * as contextually from "../shared/context"; -import { closeOpenedTextFile } from "../utils/workspace"; import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; import { DATASET_FILTER_OPTS, DATASET_SORT_OPTS, validateDataSetName, validateMemberName } from "./utils"; import { SettingsConfig } from "../utils/SettingsConfig"; import { ZoweLogger } from "../utils/ZoweLogger"; import { TreeViewUtils } from "../utils/TreeViewUtils"; import { TreeProviders } from "../shared/TreeProviders"; +import { DatasetFSProvider } from "./DatasetFSProvider"; /** * Creates the Dataset tree that contains nodes of sessions and data sets @@ -48,7 +49,7 @@ import { TreeProviders } from "../shared/TreeProviders"; */ export async function createDatasetTree(log: imperative.Logger): Promise { const tree = new DatasetTree(); - tree.initializeFavorites(log); + await tree.initializeFavorites(log); await tree.addSession(undefined, undefined, tree); return tree; } @@ -74,6 +75,9 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT private treeView: vscode.TreeView; public openFiles: Record = {}; + public dragMimeTypes: string[] = ["application/vnd.code.tree.zowe.ds.explorer"]; + public dropMimeTypes: string[] = ["application/vnd.code.tree.zowe.ds.explorer"]; + public constructor() { super( DatasetTree.persistenceSchema, @@ -154,16 +158,15 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT const favsForProfile = this.loadProfilesForFavorites(this.log, element); return favsForProfile; } - const finalResponse: IZoweDatasetTreeNode[] = []; - let response; + let response: IZoweDatasetTreeNode[] = []; try { response = await element.getChildren(); } catch (error) { await errorHandling(error, String(element.label)); + return []; } - if (!response) { - return; - } + + const finalResponse: IZoweDatasetTreeNode[] = []; for (const item of response) { if (item.pattern && item.memberPattern) { finalResponse.push(item); @@ -176,6 +179,7 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT } item.contextValue = contextually.withProfile(item); } + if (finalResponse.length === 0) { return (element.children = [ new ZoweDatasetNode({ @@ -207,7 +211,7 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT * @param profileName Name of profile * @returns {ZoweDatasetNode} */ - public createProfileNodeForFavs(profileName: string): ZoweDatasetNode { + public createProfileNodeForFavs(profileName: string, profile?: imperative.IProfileLoaded): ZoweDatasetNode { ZoweLogger.trace("DatasetTree.createProfileNodeForFavs called."); const favProfileNode = new ZoweDatasetNode({ label: profileName, @@ -229,7 +233,7 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT * Profile loading only occurs in loadProfilesForFavorites when the profile node in Favorites is clicked on. * @param log */ - public initializeFavorites(log: imperative.Logger): void { + public async initializeFavorites(log: imperative.Logger): Promise { ZoweLogger.trace("DatasetTree.initializeFavorites called."); this.log = log; ZoweLogger.debug(vscode.l10n.t("Initializing profiles with data set favorites.")); @@ -262,7 +266,7 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT let profileNodeInFavorites = this.findMatchingProfileInArray(this.mFavorites, profileName); if (profileNodeInFavorites === undefined) { // If favorite node for profile doesn't exist yet, create a new one for it - profileNodeInFavorites = this.createProfileNodeForFavs(profileName); + profileNodeInFavorites = this.createProfileNodeForFavs(profileName, await Profiles.getInstance().getLoadedProfConfig(profileName)); } // Initialize and attach favorited item nodes under their respective profile node in Favorrites const favChildNodeForProfile = this.initializeFavChildNodeForProfile(favLabel, favContextValue, profileNodeInFavorites); @@ -296,7 +300,7 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT parentNode, contextOverride: contextValue, }); - node.command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [node] }; + node.command = { command: "vscode.open", title: "", arguments: [node.resourceUri] }; } node.contextValue = contextually.asFavorite(node); const icon = getIconByNode(node); @@ -495,7 +499,7 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT let profileNodeInFavorites = this.findMatchingProfileInArray(this.mFavorites, profileName); if (profileNodeInFavorites === undefined) { // If favorite node for profile doesn't exist yet, create a new one for it - profileNodeInFavorites = this.createProfileNodeForFavs(profileName); + profileNodeInFavorites = this.createProfileNodeForFavs(profileName, node.getProfile()); } if (contextually.isDsMember(node)) { if (contextually.isFavoritePds(node.getParent())) { @@ -519,6 +523,7 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT await this.checkCurrentProfile(node); temp.contextValue = globals.DS_SESSION_CONTEXT + globals.FAV_SUFFIX; + temp.resourceUri = node.resourceUri; const icon = getIconByNode(temp); if (icon) { temp.iconPath = icon.path; @@ -537,8 +542,9 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT profile: node.getProfile(), }); temp.contextValue = contextually.asFavorite(temp); + temp.resourceUri = node.resourceUri; if (contextually.isFavoriteDs(temp)) { - temp.command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [temp] }; + temp.command = { command: "vscode.open", title: "", arguments: [temp.resourceUri] }; } const icon = getIconByNode(temp); @@ -874,11 +880,11 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT } else { memberNode.getParent().collapsibleState = vscode.TreeItemCollapsibleState.Expanded; this.addSearchHistory(`${parentName}(${memberName})`); - await memberNode.openDs(false, true, this); + await vscode.commands.executeCommand(memberNode.command.command, memberNode.resourceUri); } } else { this.addSearchHistory(parentName); - await parentNode.openDs(false, true, this); + await vscode.commands.executeCommand(parentNode.command.command, parentNode.resourceUri); } } @@ -1002,13 +1008,13 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT } else { nonFaveNode.tooltip = nonFaveNode.pattern = pattern.toUpperCase(); } - let response; + let response: IZoweDatasetTreeNode[] = []; try { response = await this.getChildren(nonFaveNode); } catch (err) { await errorHandling(err, String(node.label)); } - if (!response) { + if (response.length === 0) { nonFaveNode.tooltip = nonFaveNode.pattern = undefined; return; } @@ -1097,6 +1103,7 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT } this.addSearchHistory(pattern); } + DatasetFSProvider.instance.updateFilterForUri(nonFaveNode.resourceUri, pattern); await TreeViewUtils.expandNode(nonFaveNode, this); } @@ -1167,7 +1174,6 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT private async renameDataSetMember(node: IZoweDatasetTreeNode): Promise { ZoweLogger.trace("DatasetTree.renameDataSetMember called."); const beforeMemberName = node.label as string; - const dataSetName = node.getParent().getLabel() as string; const options: vscode.InputBoxOptions = { value: beforeMemberName, validateInput: (text) => { @@ -1180,8 +1186,6 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT return; } afterMemberName = afterMemberName.toUpperCase(); - const beforeFullPath = getDocumentFilePath(`${dataSetName}(${node.getLabel().toString()})`, node); - const closedOpenedInstance = await closeOpenedTextFile(beforeFullPath); ZoweLogger.debug( vscode.l10n.t({ @@ -1191,16 +1195,13 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT }) ); if (afterMemberName && afterMemberName !== beforeMemberName) { - try { - await ZoweExplorerApiRegister.getMvsApi(node.getProfile()).renameDataSetMember(dataSetName, beforeMemberName, afterMemberName); - node.label = afterMemberName; - node.tooltip = afterMemberName; - } catch (err) { - if (err instanceof Error) { - await errorHandling(err, dataSetName, vscode.l10n.t("Unable to rename data set:")); - } - throw err; - } + const newUri = node.resourceUri.with({ + path: path.posix.join(path.posix.dirname(node.resourceUri.path), afterMemberName), + }); + await DatasetFSProvider.instance.rename(node.resourceUri, newUri, { overwrite: false }); + node.resourceUri = newUri; + node.label = afterMemberName; + node.tooltip = afterMemberName; const otherParent = this.findEquivalentNode(node.getParent(), contextually.isFavorite(node.getParent())); if (otherParent) { const otherMember = otherParent.children.find((child) => child.label === beforeMemberName); @@ -1210,13 +1211,7 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT this.refreshElement(otherMember); } } - this.refresh(); - if (fs.existsSync(beforeFullPath)) { - fs.unlinkSync(beforeFullPath); - } - if (closedOpenedInstance) { - vscode.commands.executeCommand("zowe.ds.ZoweNode.openPS", node); - } + this.refreshElement(node.getParent()); } } @@ -1240,8 +1235,6 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT return; } afterDataSetName = afterDataSetName.toUpperCase(); - const beforeFullPath = getDocumentFilePath(node.getLabel() as string, node); - const closedOpenedInstance = await closeOpenedTextFile(beforeFullPath); ZoweLogger.debug( vscode.l10n.t({ @@ -1251,34 +1244,24 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT }) ); if (afterDataSetName && afterDataSetName !== beforeDataSetName) { - try { - await ZoweExplorerApiRegister.getMvsApi(node.getProfile()).renameDataSet(beforeDataSetName, afterDataSetName); - // Rename corresponding node in Sessions or Favorites section (whichever one Rename wasn't called from) - if (contextually.isFavorite(node)) { - const profileName = node.getProfileName(); - this.renameNode(profileName, beforeDataSetName, afterDataSetName); - } else { - this.renameFavorite(node, afterDataSetName); - } - // Rename the node that was clicked on - node.label = afterDataSetName; - node.tooltip = afterDataSetName; - this.refresh(); - this.updateFavorites(); - - if (fs.existsSync(beforeFullPath)) { - fs.unlinkSync(beforeFullPath); - } + const newUri = node.resourceUri.with({ + path: path.posix.join(path.posix.dirname(node.resourceUri.path), afterDataSetName), + }); + await DatasetFSProvider.instance.rename(node.resourceUri, newUri, { overwrite: false }); - if (closedOpenedInstance) { - vscode.commands.executeCommand("zowe.ds.ZoweNode.openPS", node); - } - } catch (err) { - if (err instanceof Error) { - await errorHandling(err, node.label.toString(), vscode.l10n.t("Unable to rename data set:")); - } - throw err; + // Rename corresponding node in Sessions or Favorites section (whichever one Rename wasn't called from) + if (contextually.isFavorite(node)) { + const profileName = node.getProfileName(); + this.renameNode(profileName, beforeDataSetName, afterDataSetName); + } else { + this.renameFavorite(node, afterDataSetName); } + // Rename the node that was clicked on + node.resourceUri = newUri; + node.label = afterDataSetName; + node.tooltip = afterDataSetName; + this.refreshElement(node.getParent()); + this.updateFavorites(); } } @@ -1562,6 +1545,9 @@ export class DatasetTree extends ZoweTreeProvider implements Types.IZoweDatasetT public async openWithEncoding(node: IZoweDatasetTreeNode, encoding?: ZosEncoding): Promise { encoding = encoding ?? (await promptForEncoding(node)); if (encoding !== undefined) { + if (!(await confirmForUnsavedDoc(node.resourceUri))) { + return; + } node.setEncoding(encoding); await node.openDs(true, false, this); } diff --git a/packages/zowe-explorer/src/dataset/ZoweDatasetNode.ts b/packages/zowe-explorer/src/dataset/ZoweDatasetNode.ts index 6cbef2a9d7..4f272b23f0 100644 --- a/packages/zowe-explorer/src/dataset/ZoweDatasetNode.ts +++ b/packages/zowe-explorer/src/dataset/ZoweDatasetNode.ts @@ -12,7 +12,7 @@ import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; import * as vscode from "vscode"; import * as globals from "../globals"; -import { errorHandling } from "../utils/ProfilesUtils"; +import { errorHandling, getSessionLabel } from "../utils/ProfilesUtils"; import { Sorting, Types, @@ -23,6 +23,8 @@ import { ZoweTreeNode, ZosEncoding, Validation, + DsEntry, + ZoweScheme, } from "@zowe/zowe-explorer-api"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; import { getIconByNode } from "../generators/icons"; @@ -30,10 +32,8 @@ import * as contextually from "../shared/context"; import { Profiles } from "../Profiles"; import { ZoweLogger } from "../utils/ZoweLogger"; import * as dayjs from "dayjs"; -import * as fs from "fs"; -import { promiseStatus, PromiseStatuses } from "promise-status-async"; -import { LocalFileInfo, getDocumentFilePath } from "../shared/utils"; import { IZoweDatasetTreeOpts } from "../shared/IZoweTreeOpts"; +import { DatasetFSProvider } from "./DatasetFSProvider"; /** * A type of TreeItem used to represent sessions and data sets @@ -49,7 +49,6 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod public dirty = true; public children: ZoweDatasetNode[] = []; public binary = false; - public encoding?: string; public encodingMap = {}; public errorDetails: imperative.ImperativeError; public ongoingActions: Record> = {}; @@ -57,7 +56,7 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod public stats: Types.DatasetStats; public sort?: Sorting.NodeSort; public filter?: Sorting.DatasetFilter; - private etag?: string; + public resourceUri?: vscode.Uri; /** * Creates an instance of ZoweDatasetNode @@ -67,10 +66,9 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod public constructor(opts: IZoweDatasetTreeOpts) { super(opts.label, opts.collapsibleState, opts.parentNode, opts.session, opts.profile); this.binary = opts.encoding?.kind === "binary"; - if (!this.binary && opts.encoding != null) { - this.encoding = opts.encoding.kind === "other" ? opts.encoding.codepage : null; + if (opts.encoding != null) { + this.setEncoding(opts.encoding); } - this.etag = opts.etag; if (opts.contextOverride) { this.contextValue = opts.contextOverride; } else if (opts.collapsibleState !== vscode.TreeItemCollapsibleState.None) { @@ -97,6 +95,49 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod if (contextually.isSession(this)) { this.id = this.label as string; } + + if (this.label !== vscode.l10n.t("Favorites")) { + const sessionLabel = opts.profile?.name ?? getSessionLabel(this); + if (this.getParent() == null) { + this.resourceUri = vscode.Uri.from({ + scheme: ZoweScheme.DS, + path: `/${sessionLabel}/`, + }); + DatasetFSProvider.instance.createDirectory(this.resourceUri, this.pattern); + } else if ( + this.contextValue === globals.DS_DS_CONTEXT || + this.contextValue === globals.DS_PDS_CONTEXT || + this.contextValue === globals.DS_MIGRATED_FILE_CONTEXT + ) { + this.resourceUri = vscode.Uri.from({ + scheme: ZoweScheme.DS, + path: `/${sessionLabel}/${this.label as string}`, + }); + if (this.contextValue === globals.DS_DS_CONTEXT) { + this.command = { + command: "vscode.open", + title: "", + arguments: [this.resourceUri], + }; + } + } else if (this.contextValue === globals.DS_MEMBER_CONTEXT) { + this.resourceUri = vscode.Uri.from({ + scheme: ZoweScheme.DS, + path: `/${sessionLabel}/${this.getParent().label as string}/${this.label as string}`, + }); + this.command = { + command: "vscode.open", + title: "", + arguments: [this.resourceUri], + }; + } else { + this.resourceUri = null; + } + + if (opts.encoding != null) { + DatasetFSProvider.instance.makeEmptyDsWithEncoding(this.resourceUri, opts.encoding); + } + } } public updateStats(item: any): void { @@ -125,14 +166,18 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod public async getChildren(): Promise { ZoweLogger.trace("ZoweDatasetNode.getChildren called."); if (!this.pattern && contextually.isSessionNotFav(this)) { - return [ - new ZoweDatasetNode({ - label: vscode.l10n.t("Use the search button to display data sets"), - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: this, - contextOverride: globals.INFORMATION_CONTEXT, - }), - ]; + const placeholder = new ZoweDatasetNode({ + label: vscode.l10n.t("Use the search button to display data sets"), + collapsibleState: vscode.TreeItemCollapsibleState.None, + parentNode: this, + contextOverride: globals.INFORMATION_CONTEXT, + profile: null, + }); + placeholder.command = { + command: "zowe.placeholderCommand", + title: "Placeholder", + }; + return [placeholder]; } if (contextually.isDocument(this) || contextually.isInformation(this)) { return []; @@ -167,12 +212,13 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod for (const item of response.apiResponse.items ?? response.apiResponse) { const dsEntry = item.dsname ?? item.member; const existing = this.children.find((element) => element.label.toString() === dsEntry); + let temp = existing; if (existing) { existing.updateStats(item); elementChildren[existing.label.toString()] = existing; // Creates a ZoweDatasetNode for a PDS } else if (item.dsorg === "PO" || item.dsorg === "PO-E") { - const temp = new ZoweDatasetNode({ + temp = new ZoweDatasetNode({ label: item.dsname, collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, parentNode: this, @@ -181,7 +227,7 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod elementChildren[temp.label.toString()] = temp; // Creates a ZoweDatasetNode for a dataset with imperative errors } else if (item.error instanceof imperative.ImperativeError) { - const temp = new ZoweDatasetNode({ + temp = new ZoweDatasetNode({ label: item.dsname, collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: this, @@ -192,7 +238,7 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod elementChildren[temp.label.toString()] = temp; // Creates a ZoweDatasetNode for a migrated dataset } else if (item.migr && item.migr.toUpperCase() === "YES") { - const temp = new ZoweDatasetNode({ + temp = new ZoweDatasetNode({ label: item.dsname, collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: this, @@ -222,20 +268,20 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod } else if (contextually.isSessionNotFav(this)) { // Creates a ZoweDatasetNode for a PS const cachedEncoding = this.getSessionNode().encodingMap[item.dsname]; - const temp = new ZoweDatasetNode({ + temp = new ZoweDatasetNode({ label: item.dsname, collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: this, encoding: cachedEncoding, profile: this.getProfile(), }); - temp.command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [temp] }; + temp.command = { command: "vscode.open", title: "", arguments: [temp.resourceUri] }; elementChildren[temp.label.toString()] = temp; } else { // Creates a ZoweDatasetNode for a PDS member const memberInvalid = item.member?.includes("\ufffd"); const cachedEncoding = this.getSessionNode().encodingMap[`${item.dsname as string}(${item.member as string})`]; - const temp = new ZoweDatasetNode({ + temp = new ZoweDatasetNode({ label: item.member, collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: this, @@ -244,7 +290,7 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod profile: this.getProfile(), }); if (!memberInvalid) { - temp.command = { command: "zowe.ds.ZoweNode.openPS", title: "", arguments: [temp] }; + temp.command = { command: "vscode.open", title: "", arguments: [temp.resourceUri] }; } else { temp.errorDetails = new imperative.ImperativeError({ msg: vscode.l10n.t({ @@ -259,19 +305,45 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod temp.updateStats(item); elementChildren[temp.label.toString()] = temp; } + + if (temp == null) { + continue; + } + + if (temp.resourceUri) { + if (temp.collapsibleState !== vscode.TreeItemCollapsibleState.None) { + // Create an entry for the PDS if it doesn't exist. + if (!DatasetFSProvider.instance.exists(temp.resourceUri)) { + vscode.workspace.fs.createDirectory(temp.resourceUri); + } + } else { + // Create an entry for the data set if it doesn't exist. + if (!DatasetFSProvider.instance.exists(temp.resourceUri)) { + await vscode.workspace.fs.writeFile(temp.resourceUri, new Uint8Array()); + } + temp.command = { + command: "vscode.open", + title: vscode.l10n.t("Open"), + arguments: [temp.resourceUri], + }; + } + } } } this.dirty = false; if (Object.keys(elementChildren).length === 0) { - this.children = [ - new ZoweDatasetNode({ - label: vscode.l10n.t("No data sets found"), - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: this, - contextOverride: globals.INFORMATION_CONTEXT, - }), - ]; + const placeholder = new ZoweDatasetNode({ + label: vscode.l10n.t("No data sets found"), + collapsibleState: vscode.TreeItemCollapsibleState.None, + parentNode: this, + contextOverride: globals.INFORMATION_CONTEXT, + }); + placeholder.command = { + command: "zowe.placeholderCommand", + title: "Placeholder", + }; + this.children = [placeholder]; } else { const newChildren = Object.keys(elementChildren) .filter((label) => this.children.find((c) => (c.label as string) === label) == null) @@ -411,17 +483,8 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod */ public getEtag(): string { ZoweLogger.trace("ZoweDatasetNode.getEtag called."); - return this.etag; - } - - /** - * Set the [etag] for this node - * - * @returns {void} - */ - public setEtag(etagValue): void { - ZoweLogger.trace("ZoweDatasetNode.setEtag called."); - this.etag = etagValue; + const fileEntry = DatasetFSProvider.instance.stat(this.resourceUri) as DsEntry; + return fileEntry.etag; } private async getDatasets(): Promise { @@ -468,41 +531,35 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod return responses; } - public async openDs(forceDownload: boolean, previewMember: boolean, datasetProvider: Types.IZoweDatasetTreeType): Promise { + public async openDs(forceDownload: boolean, _previewMember: boolean, datasetProvider: Types.IZoweDatasetTreeType): Promise { ZoweLogger.trace("ZoweDatasetNode.openDs called."); await datasetProvider.checkCurrentProfile(this); - - // Status of last "open action" promise - // If the node doesn't support pending actions, assume last action was resolved to pull new contents - const lastActionStatus = - this.ongoingActions?.[ZoweTreeNodeActions.Interactions.Download] != null - ? await promiseStatus(this.ongoingActions[ZoweTreeNodeActions.Interactions.Download]) - : PromiseStatuses.PROMISE_RESOLVED; - - // Cache status of double click if the node has the "wasDoubleClicked" property: - // allows subsequent clicks to register as double-click if node is not done fetching contents - const doubleClicked = Gui.utils.wasDoubleClicked(this, datasetProvider); - const shouldPreview = doubleClicked ? false : previewMember; - if (this.wasDoubleClicked != null) { - this.wasDoubleClicked = doubleClicked; - } - - // Prevent future "open actions" until last action is completed - if (lastActionStatus == PromiseStatuses.PROMISE_PENDING) { - return; + const invalidItem = vscode.l10n.t("Cannot download, item invalid."); + switch (true) { + case contextually.isFavorite(this): + case contextually.isSessionNotFav(this.getParent()): + break; + case contextually.isFavoritePds(this.getParent()): + case contextually.isPdsNotFav(this.getParent()): + break; + default: + ZoweLogger.error("ZoweDatasetNode.openDs: " + invalidItem); + Gui.errorMessage(invalidItem); + throw Error(invalidItem); } if (Profiles.getInstance().validProfile !== Validation.ValidationType.INVALID) { try { - const fileInfo = await this.downloadDs(forceDownload); - const document = await vscode.workspace.openTextDocument(getDocumentFilePath(fileInfo.name, this)); - await Gui.showTextDocument(document, { preview: this.wasDoubleClicked != null ? !this.wasDoubleClicked : shouldPreview }); - // discard ongoing action to allow new requests on this node - if (this.ongoingActions) { - this.ongoingActions[ZoweTreeNodeActions.Interactions.Download] = null; + if (forceDownload) { + // if the encoding has changed, fetch the contents with the new encoding + await DatasetFSProvider.instance.fetchDatasetAtUri(this.resourceUri); + await vscode.commands.executeCommand("vscode.open", this.resourceUri); + await DatasetFSProvider.revertFileInEditor(); + } else { + await vscode.commands.executeCommand("vscode.open", this.resourceUri); } if (datasetProvider) { - datasetProvider.addFileHistory(`[${this.getProfileName()}]: ${fileInfo.name}`); + datasetProvider.addFileHistory(`[${this.getProfileName()}]: ${this.label as string}`); } } catch (err) { await errorHandling(err, this.getProfileName()); @@ -511,68 +568,8 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod } } - public async downloadDs(forceDownload: boolean): Promise { - ZoweLogger.trace("dataset.actions.downloadDs called."); - const fileInfo = {} as LocalFileInfo; - const defaultMessage = vscode.l10n.t("Invalid data set or member."); - switch (true) { - // For favorited or non-favorited sequential DS: - case contextually.isFavorite(this): - case contextually.isSessionNotFav(this.getParent()): - fileInfo.name = this.label as string; - break; - // For favorited or non-favorited data set members: - case contextually.isFavoritePds(this.getParent()): - case contextually.isPdsNotFav(this.getParent()): - fileInfo.name = this.getParent().getLabel().toString() + "(" + this.getLabel().toString() + ")"; - break; - default: - Gui.errorMessage(defaultMessage); - throw Error(defaultMessage); - } - // if local copy exists, open that instead of pulling from mainframe - fileInfo.path = getDocumentFilePath(fileInfo.name, this); - let responsePromise = this.ongoingActions ? this.ongoingActions[ZoweTreeNodeActions.Interactions.Download] : null; - // If there is no ongoing action and the local copy does not exist, fetch contents - if (forceDownload || (responsePromise == null && !fs.existsSync(fileInfo.path))) { - if (this.ongoingActions) { - this.ongoingActions[ZoweTreeNodeActions.Interactions.Download] = this.downloadDsApiCall(fileInfo.path, fileInfo.name); - responsePromise = this.ongoingActions[ZoweTreeNodeActions.Interactions.Download]; - } else { - responsePromise = this.downloadDsApiCall(fileInfo.path, fileInfo.name); - } - } - if (responsePromise != null) { - const response = await responsePromise; - this.setEtag(response.apiResponse?.etag); - } - return fileInfo; - } - - private async downloadDsApiCall(documentFilePath: string, label: string): Promise { - const prof = this.getProfile(); - ZoweLogger.info( - vscode.l10n.t({ - message: "Downloading {0}", - args: [label], - comment: ["Label"], - }) - ); - const statusMsg = Gui.setStatusBarMessage(vscode.l10n.t("$(sync~spin) Downloading data set...")); - try { - const response = await ZoweExplorerApiRegister.getMvsApi(prof).getContents(label, { - file: documentFilePath, - returnEtag: true, - binary: this.binary, - encoding: this.encoding !== undefined ? this.encoding : prof.profile?.encoding, - responseTimeout: prof.profile?.responseTimeout, - }); - statusMsg.dispose(); - return response; - } catch (error) { - statusMsg.dispose(); - throw error; - } + public getEncoding(): ZosEncoding { + return DatasetFSProvider.instance.getEncodingForFile(this.resourceUri); } public setEncoding(encoding: ZosEncoding): void { @@ -584,12 +581,11 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod if (encoding?.kind === "binary") { this.contextValue = isMemberNode ? globals.DS_MEMBER_BINARY_CONTEXT : globals.DS_DS_BINARY_CONTEXT; this.binary = true; - this.encoding = undefined; } else { this.contextValue = isMemberNode ? globals.DS_MEMBER_CONTEXT : globals.DS_DS_CONTEXT; this.binary = false; - this.encoding = encoding?.kind === "text" ? null : encoding?.codepage; } + DatasetFSProvider.instance.setEncodingForFile(this.resourceUri, encoding); const fullPath = isMemberNode ? `${this.getParent().label as string}(${this.label as string})` : (this.label as string); if (encoding != null) { this.getSessionNode().encodingMap[fullPath] = encoding; diff --git a/packages/zowe-explorer/src/dataset/actions.ts b/packages/zowe-explorer/src/dataset/actions.ts index 9f5ddbde74..f1f2020cac 100644 --- a/packages/zowe-explorer/src/dataset/actions.ts +++ b/packages/zowe-explorer/src/dataset/actions.ts @@ -11,30 +11,21 @@ import * as dsUtils from "../dataset/utils"; import * as vscode from "vscode"; -import * as fs from "fs"; import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; import * as globals from "../globals"; import * as path from "path"; import { FilterItem, errorHandling } from "../utils/ProfilesUtils"; -import { - getDocumentFilePath, - concatChildNodes, - checkForAddedSuffix, - getSelectedNodeList, - JobSubmitDialogOpts, - JOB_SUBMIT_DIALOG_OPTS, - uploadContent, -} from "../shared/utils"; +import { getSelectedNodeList, JobSubmitDialogOpts, JOB_SUBMIT_DIALOG_OPTS } from "../shared/utils"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; import { Profiles } from "../Profiles"; import { getIconByNode } from "../generators/icons"; import { ZoweDatasetNode } from "./ZoweDatasetNode"; import * as contextually from "../shared/context"; -import { markDocumentUnsaved, setFileSaved } from "../utils/workspace"; import { ZoweLogger } from "../utils/ZoweLogger"; import { ProfileManagement } from "../utils/ProfileManagement"; import { LocalFileManagement } from "../utils/LocalFileManagement"; -import { Gui, imperative, IZoweDatasetTreeNode, Validation, Types } from "@zowe/zowe-explorer-api"; +import { Gui, imperative, IZoweDatasetTreeNode, Validation, Types, confirmForUnsavedDoc } from "@zowe/zowe-explorer-api"; +import { DatasetFSProvider } from "./DatasetFSProvider"; let typeEnum: zosfiles.CreateDataSetTypeEnum; // Make a nice new mutable array for the DS properties @@ -326,7 +317,8 @@ export async function deleteDatasetPrompt(datasetProvider: Types.IZoweDatasetTre ); const deleteButton = vscode.l10n.t("Delete"); const message = vscode.l10n.t({ - message: `Are you sure you want to delete the following {0} item(s)?\nThis will permanently remove these data sets and/or members from your system.\n\n{1}`, + message: `Are you sure you want to delete the following {0} item(s)? +This will permanently remove these data sets and/or members from your system.\n\n{1}`, args: [nodesToDelete.length, nodesToDelete.toString().replace(/(,)/g, "\n")], comment: ["Data Sets to delete length", "Data Sets to delete"], }); @@ -436,19 +428,20 @@ export async function createMember(parent: IZoweDatasetTreeNode, datasetProvider parent.dirty = true; datasetProvider.refreshElement(parent); - await new ZoweDatasetNode({ + const newNode = new ZoweDatasetNode({ label: name, collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: parent, profile: parent.getProfile(), - }).openDs(false, true, datasetProvider); + }); + await vscode.workspace.fs.writeFile(newNode.resourceUri, new Uint8Array()); // Refresh corresponding tree parent to reflect addition const otherTreeParent = datasetProvider.findEquivalentNode(parent, contextually.isFavorite(parent)); if (otherTreeParent != null) { datasetProvider.refreshElement(otherTreeParent); } - + await vscode.commands.executeCommand("vscode.open", newNode.resourceUri); datasetProvider.refresh(); } } @@ -650,7 +643,7 @@ function getTemplateNames(datasetProvider: Types.IZoweDatasetTreeType): string[] const templates = datasetProvider.getDsTemplates(); const templateNames: string[] = []; templates?.forEach((template) => { - Object.entries(template).forEach(([key, value]) => { + Object.entries(template).forEach(([key, _value]) => { templateNames.push(key); }); }); @@ -1179,8 +1172,7 @@ export async function deleteDataset(node: IZoweDatasetTreeNode, datasetProvider: } await datasetProvider.checkCurrentProfile(node); if (Profiles.getInstance().validProfile !== Validation.ValidationType.INVALID) { - const profile = node.getProfile(); - await ZoweExplorerApiRegister.getMvsApi(profile).deleteDataSet(label, { responseTimeout: profile.profile?.responseTimeout }); + await DatasetFSProvider.instance.delete(node.resourceUri, { recursive: false }); } else { return; } @@ -1231,16 +1223,6 @@ export async function deleteDataset(node: IZoweDatasetTreeNode, datasetProvider: } datasetProvider.refreshElement(node.getSessionNode()); - - // remove local copy of file - const fileName = getDocumentFilePath(label, node); - try { - if (fs.existsSync(fileName)) { - fs.unlinkSync(fileName); - } - } catch (err) { - ZoweLogger.warn(err); - } } /** @@ -1257,6 +1239,7 @@ export async function refreshPS(node: IZoweDatasetTreeNode): Promise { // For favorited or non-favorited sequential DS: case contextually.isFavorite(node): case contextually.isSessionNotFav(node.getParent()): + case contextually.isDs(node): label = node.label as string; break; // For favorited or non-favorited data set members: @@ -1267,24 +1250,16 @@ export async function refreshPS(node: IZoweDatasetTreeNode): Promise { default: throw Error(vscode.l10n.t("Item invalid.")); } - const documentFilePath = getDocumentFilePath(label, node); - const prof = node.getProfile(); - const response = await ZoweExplorerApiRegister.getMvsApi(prof).getContents(label, { - file: documentFilePath, - returnEtag: true, - binary: node.binary, - encoding: node.encoding !== undefined ? node.encoding : prof.profile?.encoding, - responseTimeout: prof.profile?.responseTimeout, - }); - node.setEtag(response.apiResponse.etag); - - const document = await vscode.workspace.openTextDocument(documentFilePath); - Gui.showTextDocument(document, { preview: false }); - // if there are unsaved changes, vscode won't automatically display the updates, so close and reopen - if (document.isDirty) { - await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); - Gui.showTextDocument(document, { preview: false }); + + if (!(await confirmForUnsavedDoc(node.resourceUri))) { + return; } + + const statusMsg = Gui.setStatusBarMessage(vscode.l10n.t("$(sync~spin) Fetching data set...")); + await DatasetFSProvider.instance.fetchDatasetAtUri(node.resourceUri, { + editor: vscode.window.visibleTextEditors.find((v) => v.document.uri.path === node.resourceUri.path), + }); + statusMsg.dispose(); } catch (err) { if (err.message.includes(vscode.l10n.t("not found"))) { ZoweLogger.error( @@ -1592,136 +1567,6 @@ export async function pasteMember(node: IZoweDatasetTreeNode, datasetProvider: T } } -/** - * Uploads the file to the mainframe - * - * @export - * @param {vscode.TextDocument} doc - TextDocument that is being saved - */ -export async function saveFile(doc: vscode.TextDocument, datasetProvider: Types.IZoweDatasetTreeType): Promise { - ZoweLogger.trace("dataset.actions.saveFile called."); - // Check if file is a data set, instead of some other file - const docPath = path.join(doc.fileName, ".."); - ZoweLogger.debug( - vscode.l10n.t({ - message: "Requested to save data set {0}", - args: [doc.fileName], - comment: ["Document file name"], - }) - ); - if (docPath.toUpperCase().indexOf(globals.DS_DIR.toUpperCase()) === -1) { - ZoweLogger.error( - vscode.l10n.t({ - message: "path.relative returned a non-blank directory. Assuming we are not in the DS_DIR directory: {0}", - args: [path.relative(docPath, globals.DS_DIR)], - comment: ["Relative path to Data Set directory"], - }) - ); - return; - } - const start = path.join(globals.DS_DIR + path.sep).length; - const ending = doc.fileName.substring(start); - const sesName = ending.substring(0, ending.indexOf(path.sep)); - const profile = Profiles.getInstance().loadNamedProfile(sesName); - const fileLabel = path.basename(doc.fileName); - const dataSetName = fileLabel.substring(0, fileLabel.indexOf("(")); - const memberName = fileLabel.substring(fileLabel.indexOf("(") + 1, fileLabel.indexOf(")")); - if (!profile) { - const sessionError = vscode.l10n.t("Could not locate session when saving data set."); - ZoweLogger.error(sessionError); - await Gui.errorMessage(sessionError); - return; - } - - const etagFavorites = ( - datasetProvider.mFavorites - .find((child) => child.label.toString().trim() === sesName) - ?.children.find((child) => child.label.toString().trim() === dataSetName) - ?.children.find((child) => child.label.toString().trim() === memberName) as IZoweDatasetTreeNode - )?.getEtag(); - const sesNode = - etagFavorites !== "" && etagFavorites !== undefined - ? datasetProvider.mFavorites.find((child) => child.label.toString().trim() === sesName) - : datasetProvider.mSessionNodes.find((child) => child.label.toString().trim() === sesName); - if (!sesNode) { - // if saving from favorites, a session might not exist for this node - ZoweLogger.debug(vscode.l10n.t("Could not find session node")); - } - - // If not a member - let label = doc.fileName.substring( - doc.fileName.lastIndexOf(path.sep) + 1, - checkForAddedSuffix(doc.fileName) ? doc.fileName.lastIndexOf(".") : doc.fileName.length - ); - label = label.toUpperCase().trim(); - ZoweLogger.info( - vscode.l10n.t({ - message: "Saving file {0}", - args: [label], - comment: ["Label"], - }) - ); - const dsname = label.includes("(") ? label.slice(0, label.indexOf("(")) : label; - try { - // Checks if file still exists on server - const response = await ZoweExplorerApiRegister.getMvsApi(profile).dataSet(dsname, { responseTimeout: profile.profile?.responseTimeout }); - if (!response.apiResponse.items.length) { - const saveError = vscode.l10n.t("Data set failed to save. Data set may have been deleted or renamed on mainframe."); - ZoweLogger.error(saveError); - await Gui.errorMessage(saveError); - return; - } - } catch (err) { - await errorHandling(err, sesName); - } - // Get specific node based on label and parent tree (session / favorites) - const nodes = concatChildNodes(sesNode ? [sesNode] : datasetProvider.mSessionNodes); - const node: IZoweDatasetTreeNode = - nodes.find((zNode) => { - if (contextually.isDsMember(zNode)) { - const zNodeDetails = dsUtils.getProfileAndDataSetName(zNode); - return `${zNodeDetails.profileName}(${zNodeDetails.dataSetName})` === `${label}`; - } else if (contextually.isDs(zNode) || contextually.isDsSession(zNode)) { - return zNode.label.toString().trim() === label; - } else { - return false; - } - }) ?? datasetProvider.openFiles?.[doc.uri.fsPath]; - - // define upload options - const uploadOptions: zosfiles.IUploadOptions = { - etag: node?.getEtag(), - returnEtag: true, - }; - - const prof = node?.getProfile() ?? profile; - try { - const uploadResponse = await Gui.withProgress( - { - location: vscode.ProgressLocation.Window, - title: vscode.l10n.t("Saving data set..."), - }, - () => { - return uploadContent(node, doc, label, prof, uploadOptions.etag, uploadOptions.returnEtag); - } - ); - if (uploadResponse.success) { - Gui.setStatusBarMessage(uploadResponse.commandResponse, globals.STATUS_BAR_TIMEOUT_MS); - // set local etag with the new etag from the updated file on mainframe - node?.setEtag(uploadResponse.apiResponse[0].etag); - setFileSaved(true); - } else if (!uploadResponse.success && uploadResponse.commandResponse.includes("Rest API failure with HTTP(S) status 412")) { - await LocalFileManagement.compareSavedFileContent(doc, node, label, profile); - } else { - await markDocumentUnsaved(doc); - Gui.errorMessage(uploadResponse.commandResponse); - } - } catch (err) { - await markDocumentUnsaved(doc); - await errorHandling(err, sesName); - } -} - /** * Paste members * diff --git a/packages/zowe-explorer/src/dataset/init.ts b/packages/zowe-explorer/src/dataset/init.ts index 0f610c5655..480e0765e4 100644 --- a/packages/zowe-explorer/src/dataset/init.ts +++ b/packages/zowe-explorer/src/dataset/init.ts @@ -13,7 +13,7 @@ import * as vscode from "vscode"; import * as globals from "../globals"; import * as dsActions from "./actions"; import * as refreshActions from "../shared/refresh"; -import { IZoweDatasetTreeNode, IZoweTreeNode, ZosEncoding } from "@zowe/zowe-explorer-api"; +import { IZoweDatasetTreeNode, IZoweTreeNode, ZosEncoding, ZoweScheme } from "@zowe/zowe-explorer-api"; import { Profiles } from "../Profiles"; import { DatasetTree, createDatasetTree } from "./DatasetTree"; import { ZoweDatasetNode } from "./ZoweDatasetNode"; @@ -22,9 +22,13 @@ import { getSelectedNodeList } from "../shared/utils"; import { initSubscribers } from "../shared/init"; import { ZoweLogger } from "../utils/ZoweLogger"; import { TreeViewUtils } from "../utils/TreeViewUtils"; +import { DatasetFSProvider } from "./DatasetFSProvider"; export async function initDatasetProvider(context: vscode.ExtensionContext): Promise { ZoweLogger.trace("dataset.init.initDatasetProvider called."); + + context.subscriptions.push(vscode.workspace.registerFileSystemProvider(ZoweScheme.DS, DatasetFSProvider.instance, { isCaseSensitive: true })); + const datasetProvider = await createDatasetTree(globals.LOG); if (datasetProvider == null) { return null; @@ -71,9 +75,6 @@ export async function initDatasetProvider(context: vscode.ExtensionContext): Pro context.subscriptions.push( vscode.commands.registerCommand("zowe.ds.editSession", async (node) => datasetProvider.editSession(node, datasetProvider)) ); - context.subscriptions.push( - vscode.commands.registerCommand("zowe.ds.ZoweNode.openPS", async (node: IZoweDatasetTreeNode) => node.openDs(false, true, datasetProvider)) - ); context.subscriptions.push(vscode.commands.registerCommand("zowe.ds.createDataset", async (node) => dsActions.createFile(node, datasetProvider))); context.subscriptions.push( vscode.commands.registerCommand("zowe.ds.createMember", async (node) => dsActions.createMember(node, datasetProvider)) @@ -95,7 +96,7 @@ export async function initDatasetProvider(context: vscode.ExtensionContext): Pro let selectedNodes = getSelectedNodeList(node, nodeList) as IZoweDatasetTreeNode[]; selectedNodes = selectedNodes.filter((element) => contextuals.isDs(element) || contextuals.isDsMember(element)); for (const item of selectedNodes) { - await item.openDs(false, false, datasetProvider); + await vscode.commands.executeCommand(item.command.command, item.resourceUri); } }) ); @@ -104,7 +105,7 @@ export async function initDatasetProvider(context: vscode.ExtensionContext): Pro let selectedNodes = getSelectedNodeList(node, nodeList) as IZoweDatasetTreeNode[]; selectedNodes = selectedNodes.filter((element) => contextuals.isDs(element) || contextuals.isDsMember(element)); for (const item of selectedNodes) { - await item.openDs(false, false, datasetProvider); + await vscode.commands.executeCommand(item.command.command, item.resourceUri); } }) ); @@ -221,6 +222,7 @@ export async function initDatasetProvider(context: vscode.ExtensionContext): Pro await datasetProvider.onDidChangeConfiguration(e); }) ); + context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(DatasetFSProvider.onDidOpenTextDocument)); context.subscriptions.push(vscode.workspace.onDidCloseTextDocument(DatasetTree.onDidCloseTextDocument)); initSubscribers(context, datasetProvider); diff --git a/packages/zowe-explorer/src/dataset/utils.ts b/packages/zowe-explorer/src/dataset/utils.ts index 1ed2cf4329..6c1a6a8aeb 100644 --- a/packages/zowe-explorer/src/dataset/utils.ts +++ b/packages/zowe-explorer/src/dataset/utils.ts @@ -64,3 +64,48 @@ export function validateMemberName(member: string): boolean { } return globals.MEMBER_NAME_REGEX_CHECK.test(member); } + +/** + * Get the language ID of a Data Set for use with `vscode.languages.setTextDocumentLanguage` + */ +export function getLanguageId(label: string): string | null { + const limit = 5; + const bracket = label.indexOf("("); + const split = bracket > -1 ? label.substring(0, bracket).split(".", limit) : label.split(".", limit); + for (let i = split.length - 1; i > 0; i--) { + if (split[i] === "C") { + return "c"; + } + if (["JCL", "JCLLIB", "CNTL", "PROC", "PROCLIB"].includes(split[i])) { + return "jcl"; + } + if (["COBOL", "CBL", "COB", "SCBL"].includes(split[i])) { + return "cobol"; + } + if (["COPYBOOK", "COPY", "CPY", "COBCOPY"].includes(split[i])) { + return "copybook"; + } + if (["INC", "INCLUDE", "PLINC"].includes(split[i])) { + return "inc"; + } + if (["PLI", "PL1", "PLX", "PCX"].includes(split[i])) { + return "pli"; + } + if (["SH", "SHELL"].includes(split[i])) { + return "shellscript"; + } + if (["REXX", "REXEC", "EXEC"].includes(split[i])) { + return "rexx"; + } + if (split[i] === "XML") { + return "xml"; + } + if (split[i] === "ASM" || split[i].indexOf("ASSEMBL") > -1) { + return "asm"; + } + if (split[i] === "LOG" || split[i].indexOf("SPFLOG") > -1) { + return "log"; + } + } + return null; +} diff --git a/packages/zowe-explorer/src/extension.ts b/packages/zowe-explorer/src/extension.ts index 8b10437bfa..f956c650a7 100644 --- a/packages/zowe-explorer/src/extension.ts +++ b/packages/zowe-explorer/src/extension.ts @@ -15,7 +15,6 @@ import { ZoweExplorerApiRegister } from "./ZoweExplorerApiRegister"; import { ZoweExplorerExtender } from "./ZoweExplorerExtender"; import { Profiles } from "./Profiles"; import { ProfilesUtils } from "./utils/ProfilesUtils"; -import { initializeSpoolProvider } from "./SpoolProvider"; import { cleanTempDir } from "./utils/TempFolder"; import { initZoweLogger, registerCommonCommands, registerRefreshCommand, watchConfigProfile, watchForZoweButtonClick } from "./shared/init"; import { ZoweLogger } from "./utils/ZoweLogger"; @@ -39,14 +38,13 @@ export async function activate(context: vscode.ExtensionContext): Promise ZoweExplorerExtender.showZoweConfigError(msg)); await Profiles.createInstance(ZoweLogger.imperativeLogger); - initializeSpoolProvider(context); const providers = await TreeProviders.initializeProviders(context, { ds: initDatasetProvider, uss: initUSSProvider, job: initJobsProvider }); registerCommonCommands(context, providers); registerRefreshCommand(context, activate, deactivate); ZoweExplorerExtender.createInstance(providers.ds, providers.uss, providers.job); - await watchConfigProfile(context, providers); + watchConfigProfile(context, providers); await watchForZoweButtonClick(); return ZoweExplorerApiRegister.getInstance(); diff --git a/packages/zowe-explorer/src/globals.ts b/packages/zowe-explorer/src/globals.ts index 6e2a45fa15..0fbb1b88f5 100644 --- a/packages/zowe-explorer/src/globals.ts +++ b/packages/zowe-explorer/src/globals.ts @@ -11,7 +11,7 @@ import * as path from "path"; import * as vscode from "vscode"; -import { FileManagement, imperative, IZoweTreeNode, PersistenceSchemaEnum } from "@zowe/zowe-explorer-api"; +import { FileManagement, imperative, PersistenceSchemaEnum } from "@zowe/zowe-explorer-api"; import { ZoweLogger } from "./utils/ZoweLogger"; import type { Profiles } from "./Profiles"; @@ -91,8 +91,6 @@ export const MEMBER_NAME_REGEX_CHECK = /^[a-zA-Z#@$][a-zA-Z0-9#@$]{0,7}$/; export let ACTIVATED = false; export let SAVED_PROFILE_CONTENTS = new Uint8Array(); export const JOBS_MAX_PREFIX = 8; -export let FILE_SELECTED_TO_COMPARE: boolean; -export let filesToCompare: IZoweTreeNode[]; export let PROFILES_CACHE: Profiles; // Works around circular dependency, see https://github.com/zowe/zowe-explorer-vscode/issues/2756 // Dictionary describing translation from old configuration names to new standardized names @@ -280,7 +278,7 @@ export enum JobPickerTypes { export const SEPARATORS = { BLANK: { kind: vscode.QuickPickItemKind.Separator, label: "" }, - RECENT: { kind: vscode.QuickPickItemKind.Separator, label: vscode.l10n.t("zowe.separator.recent", "Recent") }, + RECENT: { kind: vscode.QuickPickItemKind.Separator, label: vscode.l10n.t("Recent") }, RECENT_FILTERS: { kind: vscode.QuickPickItemKind.Separator, label: vscode.l10n.t(`Recent Filters`) }, OPTIONS: { kind: vscode.QuickPickItemKind.Separator, label: vscode.l10n.t(`Options`) }, }; @@ -324,16 +322,6 @@ export function setSavedProfileContents(value: Uint8Array): void { SAVED_PROFILE_CONTENTS = value; } -export function setCompareSelection(val: boolean): void { - FILE_SELECTED_TO_COMPARE = val; - vscode.commands.executeCommand("setContext", "zowe.compareFileStarted", val); -} - -export function resetCompareChoices(): void { - setCompareSelection(false); - filesToCompare = []; -} - export function setProfilesCache(profilesCache: Profiles): void { PROFILES_CACHE = profilesCache; } diff --git a/packages/zowe-explorer/src/job/JobFSProvider.ts b/packages/zowe-explorer/src/job/JobFSProvider.ts new file mode 100644 index 0000000000..696713bd39 --- /dev/null +++ b/packages/zowe-explorer/src/job/JobFSProvider.ts @@ -0,0 +1,314 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import * as vscode from "vscode"; +import { + BaseProvider, + BufferBuilder, + buildUniqueSpoolName, + DirEntry, + EntryMetadata, + FilterEntry, + getInfoForUri, + Gui, + isFilterEntry, + isJobEntry, + isSpoolEntry, + IZoweJobTreeNode, + JobEntry, + JobFilter, + SpoolEntry, + ZoweScheme, +} from "@zowe/zowe-explorer-api"; +import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; +import * as path from "path"; +import { IJob, IJobFile } from "@zowe/zos-jobs-for-zowe-sdk"; +import * as contextually from "../shared/context"; +import { Profiles } from "../Profiles"; + +export class JobFSProvider extends BaseProvider implements vscode.FileSystemProvider { + private static _instance: JobFSProvider; + private constructor() { + super(); + ZoweExplorerApiRegister.addFileSystemEvent(ZoweScheme.Jobs, this.onDidChangeFile); + this.root = new DirEntry(""); + } + + public static get instance(): JobFSProvider { + if (!JobFSProvider._instance) { + JobFSProvider._instance = new JobFSProvider(); + } + + return JobFSProvider._instance; + } + + public watch(uri: vscode.Uri, options: { readonly recursive: boolean; readonly excludes: readonly string[] }): vscode.Disposable { + // ignore, fires for all changes... + return new vscode.Disposable(() => {}); + } + + public static async refreshSpool(node: IZoweJobTreeNode): Promise { + if (!contextually.isSpoolFile(node)) { + return; + } + const statusBarMsg = Gui.setStatusBarMessage(vscode.l10n.t("$(sync~spin) Fetching spool file...")); + await JobFSProvider.instance.fetchSpoolAtUri(node.resourceUri); + statusBarMsg.dispose(); + } + + /** + * Returns file statistics about a given URI. + * @param uri A URI that must exist as an entry in the provider + * @returns A structure containing file type, time, size and other metrics + */ + public stat(uri: vscode.Uri): vscode.FileStat | Thenable { + const entry = this._lookup(uri, false); + if (isSpoolEntry(entry)) { + return { ...entry, permissions: vscode.FilePermission.Readonly }; + } + + return entry; + } + + /** + * Reads a directory located at the given URI. + * @param uri A valid URI within the provider + * @returns An array of tuples containing each entry name and type + */ + public async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> { + const fsEntry = this._lookupAsDirectory(uri, false) as FilterEntry | JobEntry; + const uriInfo = getInfoForUri(uri, Profiles.getInstance()); + const results: [string, vscode.FileType][] = []; + + const jesApi = ZoweExplorerApiRegister.getJesApi(uriInfo.profile); + if (isFilterEntry(fsEntry)) { + if (!jesApi.getJobsByParameters) { + throw new Error(vscode.l10n.t("Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API.")); + } + + const jobFiles = await jesApi.getJobsByParameters({ + owner: fsEntry.filter["owner"] ?? "*", + status: fsEntry.filter["status"] ?? "*", + prefix: fsEntry.filter["prefix"] ?? "*", + }); + for (const job of jobFiles) { + if (!fsEntry.entries.has(job.jobid)) { + const newJob = new JobEntry(job.jobid); + newJob.job = job; + fsEntry.entries.set(job.jobid, newJob); + } + } + } else if (isJobEntry(fsEntry)) { + const spoolFiles = await jesApi.getSpoolFiles(fsEntry.job.jobname, fsEntry.job.jobid); + for (const spool of spoolFiles) { + const spoolName = buildUniqueSpoolName(spool); + if (!fsEntry.entries.has(spoolName)) { + const newSpool = new SpoolEntry(spoolName); + newSpool.spool = spool; + fsEntry.entries.set(spoolName, newSpool); + } + } + } + + for (const entry of fsEntry.entries) { + results.push([entry[0], entry[1].type]); + } + + return results; + } + + /** + * Updates a filter entry in the FileSystem with the given job filter. + * @param uri The URI associated with the filter entry to update + * @param filter The filter info to assign to the filter entry (owner, status, prefix) + */ + public updateFilterForUri(uri: vscode.Uri, filter: JobFilter): void { + const filterEntry = this._lookupAsDirectory(uri, false); + if (!isFilterEntry(filterEntry)) { + return; + } + + filterEntry.filter = { + ...filter, + }; + } + + /** + * Creates a directory entry (for jobs/profiles) in the provider at the given URI. + * @param uri The URI that represents a new directory path + * @param options Options for creating the directory + * - `isFilter` - (optional) Whether the directory entry is considered a "filter entry" (profile level) in the FileSystem + * - `job` - (optional) The job document associated with the "job entry" in the FileSystem + */ + public createDirectory(uri: vscode.Uri, options?: { isFilter?: boolean; job?: IJob }): void { + const basename = path.posix.basename(uri.path); + const parent = this._lookupParentDirectory(uri, false); + + const EntryType = options?.isFilter ? FilterEntry : JobEntry; + const entry = new EntryType(basename); + if (isJobEntry(entry) && options?.job) { + entry.job = options.job; + } + + const profInfo = + parent !== this.root + ? { + profile: parent.metadata.profile, + // we can strip profile name from path because its not involved in API calls + path: path.posix.join(parent.metadata.path, basename), + } + : this._getInfoFromUri(uri); + entry.metadata = profInfo; + + parent.entries.set(entry.name, entry); + parent.mtime = Date.now(); + parent.size += 1; + this._fireSoon( + { type: vscode.FileChangeType.Changed, uri: uri.with({ path: path.posix.join(uri.path, "..") }) }, + { type: vscode.FileChangeType.Created, uri } + ); + } + + /** + * Fetches a file from the remote system at the given URI. + * @param uri The URI pointing to a valid file to fetch from the remote system + * @param editor (optional) An editor instance to reload if the URI is already open + */ + public async fetchSpoolAtUri(uri: vscode.Uri, editor?: vscode.TextEditor | null): Promise { + const spoolEntry = this._lookupAsFile(uri) as SpoolEntry; + + // we need to fetch the contents from the mainframe since the file hasn't been accessed yet + const bufBuilder = new BufferBuilder(); + + const jesApi = ZoweExplorerApiRegister.getJesApi(spoolEntry.metadata.profile); + + if (jesApi.downloadSingleSpool) { + await jesApi.downloadSingleSpool({ + jobFile: spoolEntry.spool, + stream: bufBuilder, + }); + } else { + const jobEntry = this._lookupParentDirectory(uri) as JobEntry; + bufBuilder.write(await jesApi.getSpoolContentById(jobEntry.job.jobname, jobEntry.job.jobid, spoolEntry.spool.id)); + } + + this._fireSoon({ type: vscode.FileChangeType.Changed, uri }); + spoolEntry.data = bufBuilder.read() ?? new Uint8Array(); + if (editor) { + await this._updateResourceInEditor(uri); + } + + return spoolEntry; + } + + /** + * Reads a spool file at the given URI and fetches it from the remote system (if not yet accessed). + * @param uri The URI pointing to a valid spool file to fetch from the remote system + * @returns The spool file's contents as an array of bytes + */ + public async readFile(uri: vscode.Uri): Promise { + const spoolEntry = this._lookupAsFile(uri) as SpoolEntry; + if (!spoolEntry.wasAccessed) { + await this.fetchSpoolAtUri(uri); + spoolEntry.wasAccessed = true; + } + + return spoolEntry.data; + } + + /** + * Attempts to write a file at the given URI. + * @param uri The URI pointing to a file entry that should be written + * @param content The content to write to the file, as an array of bytes + * @param options Options for writing the file + * - `create` - Creates the file if it does not exist + * - `overwrite` - Overwrites the content if the file exists + * - `name` - (optional) Provide a name for the file to write in the FileSystem + * - `spool` - (optional) The "spool document" containing data from the API + */ + public writeFile( + uri: vscode.Uri, + content: Uint8Array, + options: { readonly create: boolean; readonly overwrite: boolean; readonly name?: string; readonly spool?: IJobFile } + ): void { + const basename = path.posix.basename(uri.path); + const spoolName = options.name ?? basename; + const parent = this._lookupParentDirectory(uri); + let entry = parent.entries.get(spoolName) as JobEntry | SpoolEntry; + if (isJobEntry(entry)) { + throw vscode.FileSystemError.FileIsADirectory(uri); + } + if (!entry && !options.create) { + throw vscode.FileSystemError.FileNotFound(uri); + } + if (entry && options.create && !options.overwrite) { + throw vscode.FileSystemError.FileExists(uri); + } + + if (!entry) { + entry = new SpoolEntry(spoolName); + entry.spool = options.spool; + entry.data = content; + entry.metadata = { + ...parent.metadata, + path: path.posix.join(parent.metadata.path, basename), + }; + parent.entries.set(spoolName, entry); + this._fireSoon({ type: vscode.FileChangeType.Created, uri }); + } else { + entry.data = content; + entry.mtime = Date.now(); + entry.size = content.byteLength; + } + } + + /** + * Deletes a spool file or job at the given URI. + * @param uri The URI that points to the file/folder to delete + * @param options Options for deleting the spool file or job + * - `deleteRemote` - Deletes the job from the remote system if set to true. + */ + public async delete(uri: vscode.Uri, options: { readonly recursive: boolean; readonly deleteRemote: boolean }): Promise { + const entry = this._lookup(uri, false); + const isJob = isJobEntry(entry); + if (!isJob) { + return; + } + + const parent = this._lookupParentDirectory(uri, false); + + const profInfo = getInfoForUri(uri, Profiles.getInstance()); + + if (options.deleteRemote) { + await ZoweExplorerApiRegister.getJesApi(profInfo.profile).deleteJob(entry.job.jobname, entry.job.jobid); + } + parent.entries.delete(entry.name); + this._fireSoon({ type: vscode.FileChangeType.Deleted, uri }); + } + + // unsupported + public rename(_oldUri: vscode.Uri, _newUri: vscode.Uri, _options: { readonly overwrite: boolean }): void | Thenable { + throw new Error("Renaming is not supported for jobs."); + } + + /** + * Returns metadata about the spool file or job entry from the context of z/OS. + * @param uri A URI with a path in the format `zowe-*:/{lpar_name}/{full_path}?` + * @returns Metadata for the URI that contains the profile instance and resource path + */ + private _getInfoFromUri(uri: vscode.Uri): EntryMetadata { + const uriInfo = getInfoForUri(uri, Profiles.getInstance()); + return { + profile: uriInfo.profile, + path: uriInfo.isRoot ? "/" : uri.path.substring(uriInfo.slashAfterProfilePos), + }; + } +} diff --git a/packages/zowe-explorer/src/job/ZosJobsProvider.ts b/packages/zowe-explorer/src/job/ZosJobsProvider.ts index 84d36a291a..357c0d3497 100644 --- a/packages/zowe-explorer/src/job/ZosJobsProvider.ts +++ b/packages/zowe-explorer/src/job/ZosJobsProvider.ts @@ -16,19 +16,20 @@ import { Gui, Validation, imperative, IZoweTree, IZoweTreeNode, IZoweJobTreeNode import { FilterItem, errorHandling } from "../utils/ProfilesUtils"; import { Profiles } from "../Profiles"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; -import { ZoweJobNode, ZoweSpoolNode } from "./ZoweJobNode"; +import { ZoweJobNode } from "./ZoweJobNode"; import { getAppName, sortTreeItems, jobStringValidator, updateOpenFiles } from "../shared/utils"; import { ZoweTreeProvider } from "../abstract/ZoweTreeProvider"; import { getIconByNode } from "../generators/icons"; import * as contextually from "../shared/context"; import { SettingsConfig } from "../utils/SettingsConfig"; import { ZoweLogger } from "../utils/ZoweLogger"; -import SpoolProvider, { encodeJobFile } from "../SpoolProvider"; import { Poller } from "@zowe/zowe-explorer-api/src/utils"; import { PollDecorator } from "../utils/DecorationProviders"; import { TreeViewUtils } from "../utils/TreeViewUtils"; import { TreeProviders } from "../shared/TreeProviders"; import { JOB_FILTER_OPTS } from "./utils"; +import * as path from "path"; +import { JobFSProvider } from "./JobFSProvider"; interface IJobSearchCriteria { Owner: string | undefined; @@ -63,7 +64,7 @@ interface IJobPickerOption { export async function createJobsTree(log: imperative.Logger): Promise { ZoweLogger.trace("ZosJobsProvider.createJobsTree called."); const tree = new ZosJobsProvider(); - await tree.initializeJobsTree(log); + tree.initializeJobsTree(log); await tree.addSession(undefined, undefined, tree); return tree; } @@ -78,6 +79,8 @@ export class ZosJobsProvider extends ZoweTreeProvider implements Types.IZoweJobT private static readonly submitJobQueryLabel = vscode.l10n.t("$(check) Submit this query"); private static readonly chooseJobStatusLabel = "Job Status"; public openFiles: Record = {}; + public dragMimeTypes: string[] = ["application/vnd.code.tree.zowe.jobs.explorer"]; + public dropMimeTypes: string[] = ["application/vnd.code.tree.zowe.jobs.explorer"]; public JOB_PROPERTIES = [ { @@ -263,8 +266,11 @@ export class ZosJobsProvider extends ZoweTreeProvider implements Types.IZoweJobT public async delete(node: IZoweJobTreeNode): Promise { ZoweLogger.trace("ZosJobsProvider.delete called."); - await ZoweExplorerApiRegister.getJesApi(node.getProfile()).deleteJob(node.job.jobname, node.job.jobid); - await this.removeFavorite(this.createJobsFavorite(node)); + + await JobFSProvider.instance.delete(node.resourceUri, { recursive: false, deleteRemote: true }); + const favNode = this.relabelFavoritedJob(node); + favNode.contextValue = contextually.asFavorite(favNode); + await this.removeFavorite(favNode); node.getSessionNode().children = node.getSessionNode().children.filter((n) => n !== node); this.refresh(); } @@ -317,22 +323,22 @@ export class ZosJobsProvider extends ZoweTreeProvider implements Types.IZoweJobT * @param profileName Name of profile * @returns {ZoweJobNode} */ - public createProfileNodeForFavs(profileName: string): ZoweJobNode { + public createProfileNodeForFavs(profileName: string, profile?: imperative.IProfileLoaded): ZoweJobNode { ZoweLogger.trace("ZosJobsProvider.createProfileNodeForFavs called."); const favProfileNode = new ZoweJobNode({ label: profileName, collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, parentNode: this.mFavoriteSession, + profile, }); - - // Fake context value to pull correct icon - favProfileNode.contextValue = globals.JOBS_SESSION_CONTEXT + globals.HOME_SUFFIX; + favProfileNode.contextValue = globals.FAV_PROFILE_CONTEXT; + if (!JobFSProvider.instance.exists(favProfileNode.resourceUri)) { + JobFSProvider.instance.createDirectory(favProfileNode.resourceUri); + } const icon = getIconByNode(favProfileNode); if (icon) { favProfileNode.iconPath = icon.path; } - favProfileNode.contextValue = globals.FAV_PROFILE_CONTEXT; - this.mFavorites.push(favProfileNode); return favProfileNode; } @@ -341,7 +347,7 @@ export class ZosJobsProvider extends ZoweTreeProvider implements Types.IZoweJobT * Initialize the favorites and history information * @param log - Logger */ - public async initializeJobsTree(log: imperative.Logger): Promise { + public initializeJobsTree(log: imperative.Logger): void { ZoweLogger.trace("ZosJobsProvider.initializeJobsTree called."); this.log = log; ZoweLogger.debug(vscode.l10n.t("Initializing profiles with jobs favorites.")); @@ -358,10 +364,10 @@ export class ZosJobsProvider extends ZoweTreeProvider implements Types.IZoweJobT let profileNodeInFavorites = this.findMatchingProfileInArray(this.mFavorites, profileName); if (profileNodeInFavorites === undefined) { // If favorite node for profile doesn't exist yet, create a new one for it - profileNodeInFavorites = this.createProfileNodeForFavs(profileName); + profileNodeInFavorites = this.createProfileNodeForFavs(profileName, Profiles.getInstance().loadNamedProfile(profileName)); } // Initialize and attach favorited item nodes under their respective profile node in Favorrites - const favChildNodeForProfile = await this.initializeFavChildNodeForProfile(favLabel, favContextValue, profileNodeInFavorites); + const favChildNodeForProfile = this.initializeFavChildNodeForProfile(favLabel, favContextValue, profileNodeInFavorites); profileNodeInFavorites.children.push(favChildNodeForProfile); } } @@ -381,20 +387,24 @@ export class ZosJobsProvider extends ZoweTreeProvider implements Types.IZoweJobT favJob = new ZoweJobNode({ label, collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextOverride: globals.JOBS_JOB_CONTEXT + globals.FAV_SUFFIX, parentNode, + profile: parentNode.getProfile(), job: new JobDetail(label), }); - favJob.contextValue = globals.JOBS_JOB_CONTEXT + globals.FAV_SUFFIX; - favJob.command = { command: "zowe.zosJobsSelectjob", title: "", arguments: [favJob] }; + if (JobFSProvider.instance.exists(favJob.resourceUri)) { + JobFSProvider.instance.createDirectory(favJob.resourceUri, { job: favJob.job }); + } } else { // for search favJob = new ZoweJobNode({ label, collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: globals.JOBS_SESSION_CONTEXT + globals.FAV_SUFFIX, parentNode, + profile: parentNode.getProfile(), }); favJob.command = { command: "zowe.jobs.search", title: "", arguments: [favJob] }; - favJob.contextValue = globals.JOBS_SESSION_CONTEXT + globals.FAV_SUFFIX; } const icon = getIconByNode(favJob); if (icon) { @@ -500,7 +510,7 @@ export class ZosJobsProvider extends ZoweTreeProvider implements Types.IZoweJobT let profileNodeInFavorites = this.findMatchingProfileInArray(this.mFavorites, profileName); if (profileNodeInFavorites === undefined) { // If favorite node for profile doesn't exist yet, create a new one for it - profileNodeInFavorites = this.createProfileNodeForFavs(profileName); + profileNodeInFavorites = this.createProfileNodeForFavs(profileName, node.getProfile()); profileNodeInFavorites.iconPath = node.iconPath; } if (contextually.isSession(node)) { @@ -508,12 +518,12 @@ export class ZosJobsProvider extends ZoweTreeProvider implements Types.IZoweJobT favJob = new ZoweJobNode({ label: this.createSearchLabel(node.owner, node.prefix, node.searchId, node.status), collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: globals.JOBS_SESSION_CONTEXT + globals.FAV_SUFFIX, parentNode: profileNodeInFavorites, session: node.getSession(), profile: node.getProfile(), job: node.job, }); - favJob.contextValue = globals.JOBS_SESSION_CONTEXT + globals.FAV_SUFFIX; favJob.command = { command: "zowe.jobs.search", title: "", arguments: [favJob] }; this.saveSearch(favJob); } else { @@ -521,14 +531,13 @@ export class ZosJobsProvider extends ZoweTreeProvider implements Types.IZoweJobT favJob = new ZoweJobNode({ label: node.label as string, collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextOverride: node.contextValue + globals.FAV_SUFFIX, parentNode: profileNodeInFavorites, session: node.getSession(), profile: node.getProfile(), job: node.job, }); - favJob.contextValue = node.contextValue; - favJob.command = { command: "zowe.zosJobsSelectjob", title: "", arguments: [favJob] }; - this.createJobsFavorite(favJob); + this.relabelFavoritedJob(favJob); } const icon = getIconByNode(favJob); if (icon) { @@ -1006,10 +1015,9 @@ export class ZosJobsProvider extends ZoweTreeProvider implements Types.IZoweJobT } } - private createJobsFavorite(node: IZoweJobTreeNode): IZoweJobTreeNode { - ZoweLogger.trace("ZosJobsProvider.createJobsFavorite called."); + private relabelFavoritedJob(node: IZoweJobTreeNode): IZoweJobTreeNode { + ZoweLogger.trace("ZosJobsProvider.relabelFavoritedJob called."); node.label = node.label.toString().substring(0, node.label.toString().lastIndexOf(")") + 1); - node.contextValue = contextually.asFavorite(node); return node; } @@ -1036,7 +1044,7 @@ export class ZosJobsProvider extends ZoweTreeProvider implements Types.IZoweJobT const intervalInput = await Gui.showInputBox({ title: vscode.l10n.t({ message: "Poll interval (in ms) for: {0}", - args: [uri.path], + args: [path.posix.basename(uri.path)], comment: ["URI path"], }), value: pollValue.toString(), @@ -1055,37 +1063,25 @@ export class ZosJobsProvider extends ZoweTreeProvider implements Types.IZoweJobT return; } - const session = node.getSessionNode(); - - // Interpret node as spool to get spool data - const spoolData = (node as ZoweSpoolNode).spool; - const encodedUri = encodeJobFile(session.label as string, spoolData); - // If the uri is already being polled, mark it as ready for removal - if (encodedUri.path in Poller.pollRequests && contextually.isPolling(node)) { - Poller.pollRequests[encodedUri.path].dispose = true; - PollDecorator.updateIcon(encodedUri); + if (node.resourceUri.path in Poller.pollRequests && contextually.isPolling(node)) { + Poller.pollRequests[node.resourceUri.path].dispose = true; + PollDecorator.updateIcon(node.resourceUri); node.contextValue = node.contextValue.replace(globals.POLL_CONTEXT, ""); // Fire "tree changed event" to reflect removal of polling context value - this.mOnDidChangeTreeData.fire(); + this.mOnDidChangeTreeData.fire(node); return; } - // Add spool file to provider if it wasn't previously opened in the editor - const fileInEditor = SpoolProvider.files[encodedUri.path]; - if (!fileInEditor) { - await Gui.showTextDocument(encodedUri); - } - // Always prompt the user for a poll interval - const pollInterval = await this.showPollOptions(encodedUri); + const pollInterval = await this.showPollOptions(node.resourceUri); if (pollInterval === 0) { Gui.showMessage( vscode.l10n.t({ message: "Polling dismissed for {0}; operation cancelled.", - args: [encodedUri.path], + args: [node.resourceUri.path], comment: ["Encoded URI path"], }) ); @@ -1093,26 +1089,26 @@ export class ZosJobsProvider extends ZoweTreeProvider implements Types.IZoweJobT } // Pass request function to the poller for continuous updates - Poller.addRequest(encodedUri.path, { + Poller.addRequest(node.resourceUri.path, { msInterval: pollInterval, request: async () => { const statusMsg = Gui.setStatusBarMessage( vscode.l10n.t({ message: `$(sync~spin) Polling: {0}...`, - args: [encodedUri.path], - comment: ["Encoded URI path"], + args: [path.posix.basename(node.resourceUri.path)], + comment: ["Unique Spool name"], }), globals.STATUS_BAR_TIMEOUT_MS ); - await fileInEditor.fetchContent.bind(SpoolProvider.files[encodedUri.path])(); + await JobFSProvider.instance.fetchSpoolAtUri(node.resourceUri); statusMsg.dispose(); }, }); - PollDecorator.updateIcon(encodedUri); + PollDecorator.updateIcon(node.resourceUri); node.contextValue += globals.POLL_CONTEXT; // Fire "tree changed event" to reflect added polling context value - this.mOnDidChangeTreeData.fire(); + this.refreshElement(node); } public sortBy(session: IZoweJobTreeNode): void { @@ -1128,7 +1124,7 @@ export class ZosJobsProvider extends ZoweTreeProvider implements Types.IZoweJobT * @param newFilter Either a valid `JobFilter` object, or `null` to reset the filter * @param isSession Whether the node is a session */ - public updateFilterForJob(job: IZoweJobTreeNode, newFilter: string | null, isSession: boolean): void { + public updateFilterForJob(job: IZoweJobTreeNode, newFilter: string | null): void { job.filter = newFilter; job.description = newFilter ? vscode.l10n.t({ @@ -1154,12 +1150,11 @@ export class ZosJobsProvider extends ZoweTreeProvider implements Types.IZoweJobT }); const filterMethod = JOB_FILTER_OPTS.indexOf(selection); - const isSession = contextually.isSession(job); const userDismissed = filterMethod < 0; const clearFilterOpt = vscode.l10n.t("$(clear-all) Clear filter for profile"); if (userDismissed || selection === clearFilterOpt) { if (selection === clearFilterOpt) { - this.updateFilterForJob(job, null, isSession); + this.updateFilterForJob(job, null); Gui.setStatusBarMessage( vscode.l10n.t({ message: "$(check) Filter cleared for {0}", @@ -1190,7 +1185,7 @@ export class ZosJobsProvider extends ZoweTreeProvider implements Types.IZoweJobT : `${item["job"].jobname}(${item["job"].jobid}) - ${item["job"].retcode}`.includes(query) ); TreeProviders.job.refresh(); - this.updateFilterForJob(job, query, isSession); + this.updateFilterForJob(job, query); Gui.setStatusBarMessage( vscode.l10n.t({ message: "$(check) Filter updated for {0}", diff --git a/packages/zowe-explorer/src/job/ZoweJobNode.ts b/packages/zowe-explorer/src/job/ZoweJobNode.ts index 2de71ffa0f..c399eb0922 100644 --- a/packages/zowe-explorer/src/job/ZoweJobNode.ts +++ b/packages/zowe-explorer/src/job/ZoweJobNode.ts @@ -13,19 +13,21 @@ import * as vscode from "vscode"; import * as zosjobs from "@zowe/zos-jobs-for-zowe-sdk"; import * as globals from "../globals"; import * as contextually from "../shared/context"; -import { Gui, imperative, IZoweJobTreeNode, Sorting, ZoweTreeNode } from "@zowe/zowe-explorer-api"; +import * as path from "path"; +import { buildUniqueSpoolName, imperative, IZoweJobTreeNode, Sorting, ZoweScheme, ZoweTreeNode } from "@zowe/zowe-explorer-api"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; -import { errorHandling, syncSessionNode } from "../utils/ProfilesUtils"; +import { errorHandling, getSessionLabel, syncSessionNode } from "../utils/ProfilesUtils"; import { getIconByNode } from "../generators/icons"; import { JOB_SORT_KEYS } from "./utils"; import { Profiles } from "../Profiles"; import { ZoweLogger } from "../utils/ZoweLogger"; -import { encodeJobFile } from "../SpoolProvider"; import { IZoweJobTreeOpts } from "../shared/IZoweTreeOpts"; +import { JobFSProvider } from "./JobFSProvider"; export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { public children: IZoweJobTreeNode[] = []; public dirty = true; + public resourceUri?: vscode.Uri; public sort: Sorting.NodeSort; private _owner: string; private _prefix: string; @@ -58,8 +60,17 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { this.tooltip = opts.label; this.job = opts.job ?? null; // null instead of undefined to satisfy isZoweJobTreeNode - if (opts.parentNode == null && opts.label !== "Favorites") { + const isFavoritesNode = opts.label === "Favorites"; + const sessionLabel = opts.profile?.name ?? getSessionLabel(this); + + if (!isFavoritesNode && this.job == null) { + // non-favorited, session node this.contextValue = globals.JOBS_SESSION_CONTEXT; + this.resourceUri = vscode.Uri.from({ + scheme: ZoweScheme.Jobs, + path: `/${sessionLabel}/`, + }); + JobFSProvider.instance.createDirectory(this.resourceUri, { isFilter: true }); } if (opts.session) { @@ -69,6 +80,10 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { } } + if (opts.contextOverride) { + this.contextValue = opts.contextOverride; + } + const icon = getIconByNode(this); if (icon) { this.iconPath = icon.path; @@ -79,7 +94,14 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { method: Sorting.JobSortOpts.Id, direction: Sorting.SortDirection.Ascending, }; - this.id = this.label as string; + if (this.getParent()?.label !== vscode.l10n.t("Favorites")) { + this.id = this.label as string; + } + } else if (this.job != null) { + this.resourceUri = vscode.Uri.from({ + scheme: ZoweScheme.Jobs, + path: `/${sessionLabel}/${this.job.jobid}`, + }); } } @@ -95,13 +117,18 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { return this.children; } if (contextually.isSession(this) && !this.filtered && !contextually.isFavorite(this)) { - return [ - new ZoweJobNode({ - label: vscode.l10n.t("Use the search button to display jobs"), - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: this, - }), - ]; + const placeholder = new ZoweJobNode({ + label: vscode.l10n.t("Use the search button to display jobs"), + collapsibleState: vscode.TreeItemCollapsibleState.None, + parentNode: this, + profile: thisSessionNode.getProfile(), + contextOverride: globals.INFORMATION_CONTEXT, + }); + placeholder.command = { + command: "zowe.placeholderCommand", + title: "Placeholder", + }; + return [placeholder]; } if (!this.dirty) { @@ -123,13 +150,12 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { label: vscode.l10n.t("There are no JES spool messages to display"), collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: this, + profile: this.getProfile(), }); - noSpoolNode.iconPath = null; + noSpoolNode.iconPath = undefined; return [noSpoolNode]; } - const refreshTimestamp = Date.now(); spools.forEach((spool) => { - const sessionName = this.getProfileName(); const procstep = spool.procstep ? spool.procstep : undefined; let newLabel: string; if (procstep) { @@ -157,14 +183,20 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { job: this.job, spool, }); + JobFSProvider.instance.writeFile(spoolNode.resourceUri, new Uint8Array(), { + create: true, + overwrite: true, + name: spoolNode.uniqueName, + spool, + }); const icon = getIconByNode(spoolNode); if (icon) { spoolNode.iconPath = icon.path; } spoolNode.command = { - command: "zowe.jobs.zosJobsOpenspool", + command: "vscode.open", title: "", - arguments: [sessionName, spoolNode], + arguments: [spoolNode.resourceUri], }; elementChildren[newLabel] = spoolNode; } @@ -177,9 +209,14 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { label: vscode.l10n.t("No jobs found"), collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode: this, + profile: this.getProfile(), }); noJobsNode.contextValue = globals.INFORMATION_CONTEXT; - noJobsNode.iconPath = null; + noJobsNode.iconPath = undefined; + noJobsNode.command = { + command: "zowe.placeholderCommand", + title: "Placeholder", + }; return [noJobsNode]; } jobs.forEach((job) => { @@ -207,6 +244,8 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { profile: this.getProfile(), job, }); + JobFSProvider.instance.createDirectory(jobNode.resourceUri, { job }); + jobNode.contextValue = globals.JOBS_JOB_CONTEXT; if (job.retcode) { jobNode.contextValue += globals.RC_SUFFIX + job.retcode; @@ -317,15 +356,6 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { return this._searchId; } - private statusNotSupportedMsg(status: string): void { - ZoweLogger.trace("ZoweJobNode.statusNotSupportedMsg called."); - if (status !== "*") { - Gui.warningMessage( - vscode.l10n.t("Filtering by job status is not yet supported with this profile type. Will show jobs with all statuses.") - ); - } - } - private async getJobs(owner: string, prefix: string, searchId: string, status: string): Promise { ZoweLogger.trace("ZoweJobNode.getJobs called."); let jobsInternal: zosjobs.IJob[] = []; @@ -374,18 +404,19 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { } export class ZoweSpoolNode extends ZoweJobNode { + public uniqueName: string; public spool: zosjobs.IJobFile; public constructor(opts: IZoweJobTreeOpts & { spool?: zosjobs.IJobFile }) { super(opts); + this.uniqueName = opts.spool ? buildUniqueSpoolName(opts.spool).replace("/", "") : ""; + this.resourceUri = opts.parentNode?.resourceUri.with({ + path: path.posix.join(opts.parentNode.resourceUri.path, this.uniqueName), + }); this.contextValue = globals.JOBS_SPOOL_CONTEXT; this.spool = opts.spool; const icon = getIconByNode(this); - // parent of parent should be the session; tie resourceUri with TreeItem for file decorator - if (opts.parentNode && opts.parentNode.getParent()) { - this.resourceUri = encodeJobFile(opts.parentNode.getParent().label as string, opts.spool); - } if (icon) { this.iconPath = icon.path; } diff --git a/packages/zowe-explorer/src/job/actions.ts b/packages/zowe-explorer/src/job/actions.ts index fc2df2f4d8..854b2e5ab7 100644 --- a/packages/zowe-explorer/src/job/actions.ts +++ b/packages/zowe-explorer/src/job/actions.ts @@ -12,18 +12,17 @@ import * as vscode from "vscode"; import * as zosjobs from "@zowe/zos-jobs-for-zowe-sdk"; import { errorHandling } from "../utils/ProfilesUtils"; -import { Profiles } from "../Profiles"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; -import { Gui, imperative, IZoweJobTreeNode, Sorting, Types } from "@zowe/zowe-explorer-api"; -import { ZoweJobNode, ZoweSpoolNode } from "./ZoweJobNode"; -import SpoolProvider, { encodeJobFile, getSpoolFiles, matchSpool } from "../SpoolProvider"; +import { Gui, IZoweJobTreeNode, Sorting, Types } from "@zowe/zowe-explorer-api"; +import { ZoweJobNode } from "./ZoweJobNode"; +import { getSpoolFiles, matchSpool } from "../SpoolUtils"; import { ZoweLogger } from "../utils/ZoweLogger"; import { LocalFileManagement } from "../utils/LocalFileManagement"; -import { SORT_DIRS, updateOpenFiles } from "../shared/utils"; +import { SORT_DIRS } from "../shared/utils"; import { ZosJobsProvider } from "./ZosJobsProvider"; import { JOB_SORT_OPTS } from "./utils"; import * as globals from "../globals"; -import { TreeProviders } from "../shared/TreeProviders"; +import { JobFSProvider } from "./JobFSProvider"; /** * Download all the spool content for the specified job. @@ -90,55 +89,6 @@ export async function downloadSingleSpool(nodes: IZoweJobTreeNode[], binary?: bo } } -/** - * Download the spool content for the specified job - * - * @param session The session to which the job belongs - * @param spool The IJobFile to get the spool content for - * @param refreshTimestamp The timestamp of the last job node refresh - */ -export async function getSpoolContent(session: string, spoolNode: ZoweSpoolNode): Promise { - ZoweLogger.trace("job.actions.getSpoolContent called."); - const profiles = Profiles.getInstance(); - let zosmfProfile: imperative.IProfileLoaded; - try { - zosmfProfile = profiles.loadNamedProfile(session); - } catch (error) { - await errorHandling(error, session); - return; - } - - const statusMsg = Gui.setStatusBarMessage( - vscode.l10n.t({ - message: "$(sync~spin) Opening spool file...", - args: [this.label as string], - comment: ["Label"], - }) - ); - const uri = encodeJobFile(session, spoolNode.spool); - try { - const spoolFile = SpoolProvider.files[uri.path]; - if (spoolFile) { - // Fetch any changes to the spool file if it exists in the SpoolProvider - await spoolFile.fetchContent(); - } - updateOpenFiles(TreeProviders.job, uri.path, spoolNode); - await Gui.showTextDocument(uri, { preview: false }); - } catch (error) { - const isTextDocActive = - vscode.window.activeTextEditor && - vscode.window.activeTextEditor.document.uri?.path === `${spoolNode.spool.jobname}.${spoolNode.spool.jobid}.${spoolNode.spool.ddname}`; - - statusMsg.dispose(); - if (isTextDocActive && String(error.message).includes("Failed to show text document")) { - return; - } - await errorHandling(error, session); - return; - } - statusMsg.dispose(); -} - /** * Triggers a refresh for a spool file w/ the provided text document. * @param doc The document to update, associated with the spool file @@ -151,37 +101,7 @@ export async function spoolFilePollEvent(doc: vscode.TextDocument): Promise { - statusMsg.dispose(); - }, 250); -} - -export async function getSpoolContentFromMainframe(node: IZoweJobTreeNode): Promise { - ZoweLogger.trace("job.actions.getSpoolContentFromMainframe called."); - const statusMsg = await Gui.setStatusBarMessage(vscode.l10n.t("$(sync~spin) Fetching spool files...")); - const spools = await getSpoolFiles(node); - for (const spool of spools) { - if (matchSpool(spool, node)) { - let prefix = spool.stepname; - if (prefix === undefined) { - prefix = spool.procstep; - } - - const newLabel = `${spool.stepname}:${spool.ddname} - ${spool.procstep ?? spool["record-count"]}`; - - const spoolNode = new ZoweSpoolNode({ - label: newLabel, - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: node.getParent(), - session: node.getSession(), - profile: node.getProfile(), - job: node.job, - spool, - }); - node = spoolNode; - } - } + await JobFSProvider.instance.fetchSpoolAtUri(doc.uri); statusMsg.dispose(); } diff --git a/packages/zowe-explorer/src/job/init.ts b/packages/zowe-explorer/src/job/init.ts index 2b20fefce3..6d67ba3d0e 100644 --- a/packages/zowe-explorer/src/job/init.ts +++ b/packages/zowe-explorer/src/job/init.ts @@ -9,11 +9,10 @@ * */ -import * as globals from "../globals"; import * as vscode from "vscode"; import * as jobActions from "./actions"; import * as refreshActions from "../shared/refresh"; -import { IZoweJobTreeNode, IZoweTreeNode } from "@zowe/zowe-explorer-api"; +import { IZoweJobTreeNode, IZoweTreeNode, ZoweScheme } from "@zowe/zowe-explorer-api"; import { Profiles } from "../Profiles"; import { ZosJobsProvider, createJobsTree } from "./ZosJobsProvider"; import * as contextuals from "../shared/context"; @@ -21,17 +20,22 @@ import { ZoweJobNode } from "./ZoweJobNode"; import { getSelectedNodeList } from "../shared/utils"; import { initSubscribers } from "../shared/init"; import { ZoweLogger } from "../utils/ZoweLogger"; +import { JobFSProvider } from "./JobFSProvider"; +import * as globals from "../globals"; +import { PollDecorator } from "../utils/DecorationProviders"; export async function initJobsProvider(context: vscode.ExtensionContext): Promise { ZoweLogger.trace("job.init.initJobsProvider called."); + + context.subscriptions.push(vscode.workspace.registerFileSystemProvider(ZoweScheme.Jobs, JobFSProvider.instance, { isCaseSensitive: false })); + const jobsProvider = await createJobsTree(globals.LOG); if (jobsProvider == null) { return null; } - context.subscriptions.push( - vscode.commands.registerCommand("zowe.jobs.zosJobsOpenspool", (session, spoolNode) => jobActions.getSpoolContent(session, spoolNode)) - ); + PollDecorator.register(); + context.subscriptions.push( vscode.commands.registerCommand("zowe.jobs.deleteJob", async (job, jobs) => { await jobActions.deleteCommand(jobsProvider, job, jobs); @@ -55,12 +59,7 @@ export async function initJobsProvider(context: vscode.ExtensionContext): Promis }) ); context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.refreshJob", (job) => jobActions.refreshJob(job.mParent, jobsProvider))); - context.subscriptions.push( - vscode.commands.registerCommand("zowe.jobs.refreshSpool", async (node) => { - await jobActions.getSpoolContentFromMainframe(node); - jobActions.refreshJob(node.mParent.mParent, jobsProvider); - }) - ); + context.subscriptions.push(vscode.commands.registerCommand("zowe.jobs.refreshSpool", async (node) => JobFSProvider.refreshSpool(node))); const downloadSingleSpoolHandler = (binary: boolean) => async (node, nodeList) => { const selectedNodes = getSelectedNodeList(node, nodeList) as IZoweJobTreeNode[]; @@ -178,6 +177,15 @@ export async function initJobsProvider(context: vscode.ExtensionContext): Promis ); context.subscriptions.push(vscode.workspace.onDidCloseTextDocument(ZosJobsProvider.onDidCloseTextDocument)); + context.subscriptions.push( + vscode.workspace.onDidOpenTextDocument((doc) => { + if (doc.uri.scheme !== ZoweScheme.Jobs) { + return; + } + + JobFSProvider.instance.cacheOpenedUri(doc.uri); + }) + ); initSubscribers(context, jobsProvider); return jobsProvider; diff --git a/packages/zowe-explorer/src/job/utils.ts b/packages/zowe-explorer/src/job/utils.ts index aa36922fc4..b1a5b83d24 100644 --- a/packages/zowe-explorer/src/job/utils.ts +++ b/packages/zowe-explorer/src/job/utils.ts @@ -10,8 +10,6 @@ */ import { Sorting } from "@zowe/zowe-explorer-api"; -import { ZoweLogger } from "../utils/ZoweLogger"; -import { FilterItem } from "../utils/ProfilesUtils"; import * as zosjobs from "@zowe/zos-jobs-for-zowe-sdk"; import * as vscode from "vscode"; @@ -31,11 +29,3 @@ export const JOB_SORT_KEYS: Record { - ZoweLogger.trace("job.utils.resolveQuickPickHelper called."); - return new Promise((c) => { - quickpick.onDidAccept(() => c(quickpick.activeItems[0])); - quickpick.onDidHide(() => c(undefined)); - }); -} diff --git a/packages/zowe-explorer/src/shared/IZoweTreeOpts.ts b/packages/zowe-explorer/src/shared/IZoweTreeOpts.ts index 40d3fee80b..21810284f5 100644 --- a/packages/zowe-explorer/src/shared/IZoweTreeOpts.ts +++ b/packages/zowe-explorer/src/shared/IZoweTreeOpts.ts @@ -19,10 +19,10 @@ export interface IZoweTreeOpts { parentNode?: IZoweTreeNode; session?: imperative.Session; profile?: imperative.IProfileLoaded; + contextOverride?: string; } export interface IZoweDatasetTreeOpts extends IZoweTreeOpts { - contextOverride?: string; encoding?: ZosEncoding; etag?: string; } diff --git a/packages/zowe-explorer/src/shared/actions.ts b/packages/zowe-explorer/src/shared/actions.ts index 42a56ec195..607ffa7f16 100644 --- a/packages/zowe-explorer/src/shared/actions.ts +++ b/packages/zowe-explorer/src/shared/actions.ts @@ -11,15 +11,13 @@ import * as vscode from "vscode"; import * as globals from "../globals"; -import { Gui, IZoweDatasetTreeNode, IZoweUSSTreeNode, Types, imperative } from "@zowe/zowe-explorer-api"; +import { Gui, IZoweDatasetTreeNode, IZoweUSSTreeNode, Types } from "@zowe/zowe-explorer-api"; import { Profiles } from "../Profiles"; -import { filterTreeByString, willForceUpload } from "../shared/utils"; +import { filterTreeByString } from "../shared/utils"; import { FilterItem, FilterDescriptor } from "../utils/ProfilesUtils"; import * as contextually from "../shared/context"; import { getIconById, IconId } from "../generators/icons"; import { ZoweLogger } from "../utils/ZoweLogger"; -import { markDocumentUnsaved } from "../utils/workspace"; -import { LocalFileManagement } from "../utils/LocalFileManagement"; /** * Search for matching items loaded in data set or USS tree @@ -132,7 +130,7 @@ export async function searchInAllLoadedItems(datasetProvider?: Types.IZoweDatase // Open in workspace datasetProvider.addSearchHistory(`${nodeName}(${memberName})`); - await member.openDs(false, true, datasetProvider); + await vscode.commands.executeCommand(member.command.command, member.resourceUri); } else { // PDS & SDS await datasetProvider.getTreeView().reveal(node, { select: true, focus: true, expand: false }); @@ -140,7 +138,7 @@ export async function searchInAllLoadedItems(datasetProvider?: Types.IZoweDatase // If selected node was SDS, open it in workspace if (contextually.isDs(node)) { datasetProvider.addSearchHistory(nodeName); - await node.openDs(false, true, datasetProvider); + await vscode.commands.executeCommand(node.command.command, node.resourceUri); } } } @@ -219,38 +217,3 @@ export function resetValidationSettings(node: Types.IZoweNodeType, setting: bool } return node; } - -export function resolveFileConflict( - node: IZoweDatasetTreeNode | IZoweUSSTreeNode, - profile: imperative.IProfileLoaded, - doc: vscode.TextDocument, - label?: string -): void { - const compareBtn = vscode.l10n.t("Compare"); - const overwriteBtn = vscode.l10n.t("Overwrite"); - const infoMsg = vscode.l10n.t( - "The content of the file is newer. Compare your version with latest or overwrite the content of the file with your changes." - ); - ZoweLogger.info(infoMsg); - Gui.infoMessage(infoMsg, { - items: [compareBtn, overwriteBtn], - }).then(async (selection) => { - switch (selection) { - case compareBtn: { - ZoweLogger.info(`${compareBtn} chosen.`); - await LocalFileManagement.compareSavedFileContent(doc, node, label, profile); - break; - } - case overwriteBtn: { - ZoweLogger.info(`${overwriteBtn} chosen.`); - await willForceUpload(node, doc, label, profile); - break; - } - default: { - ZoweLogger.info("Operation cancelled, file unsaved."); - await markDocumentUnsaved(doc); - break; - } - } - }); -} diff --git a/packages/zowe-explorer/src/shared/init.ts b/packages/zowe-explorer/src/shared/init.ts index 6bce10f347..2dcd9c39fc 100644 --- a/packages/zowe-explorer/src/shared/init.ts +++ b/packages/zowe-explorer/src/shared/init.ts @@ -13,19 +13,16 @@ import * as globals from "../globals"; import * as vscode from "vscode"; import * as refreshActions from "./refresh"; import * as sharedActions from "./actions"; -import { FileManagement, IZoweTree, IZoweTreeNode, Validation } from "@zowe/zowe-explorer-api"; +import { FileManagement, IZoweTree, IZoweTreeNode, Validation, ZoweScheme } from "@zowe/zowe-explorer-api"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; import { Profiles } from "../Profiles"; import { hideTempFolder, moveTempFolder } from "../utils/TempFolder"; import { TsoCommandHandler } from "../command/TsoCommandHandler"; import { MvsCommandHandler } from "../command/MvsCommandHandler"; import { UnixCommandHandler } from "../command/UnixCommandHandler"; -import { saveFile } from "../dataset/actions"; -import { saveUSSFile } from "../uss/actions"; import { ProfilesUtils } from "../utils/ProfilesUtils"; import { LoggerUtils } from "../utils/LoggerUtils"; import { ZoweLogger } from "../utils/ZoweLogger"; -import { ZoweSaveQueue } from "../abstract/ZoweSaveQueue"; import { SettingsConfig } from "../utils/SettingsConfig"; import { spoolFilePollEvent } from "../job/actions"; import { HistoryView } from "./HistoryView"; @@ -33,6 +30,8 @@ import { ProfileManagement } from "../utils/ProfileManagement"; import { LocalFileManagement } from "../utils/LocalFileManagement"; import { TreeProviders } from "./TreeProviders"; import { IZoweProviders } from "./IZoweProviders"; +import { UssFSProvider } from "../uss/UssFSProvider"; +import { DatasetFSProvider } from "../dataset/DatasetFSProvider"; export function registerRefreshCommand( context: vscode.ExtensionContext, @@ -98,6 +97,25 @@ export function registerCommonCommands(context: vscode.ExtensionContext, provide }) ); + context.subscriptions.push( + vscode.commands.registerCommand("zowe.diff.useLocalContent", async (localUri) => { + if (localUri.scheme === ZoweScheme.USS) { + await UssFSProvider.instance.diffOverwrite(localUri); + } else if (localUri.scheme === ZoweScheme.DS) { + await DatasetFSProvider.instance.diffOverwrite(localUri); + } + }) + ); + context.subscriptions.push( + vscode.commands.registerCommand("zowe.diff.useRemoteContent", async (localUri) => { + if (localUri.scheme === ZoweScheme.USS) { + await UssFSProvider.instance.diffUseRemote(localUri); + } else if (localUri.scheme === ZoweScheme.DS) { + await DatasetFSProvider.instance.diffUseRemote(localUri); + } + }) + ); + // Register functions & event listeners context.subscriptions.push( vscode.workspace.onDidChangeConfiguration(async (e) => { @@ -138,33 +156,6 @@ export function registerCommonCommands(context: vscode.ExtensionContext, provide sharedActions.searchInAllLoadedItems(providers.ds, providers.uss) ) ); - context.subscriptions.push( - vscode.workspace.onDidSaveTextDocument((savedFile) => { - ZoweLogger.debug( - vscode.l10n.t({ - message: `File was saved -- determining whether the file is a USS file or Data set. - \n Comparing (case insensitive) {0} against directory {1} and {2}`, - args: [savedFile.fileName, globals.DS_DIR, globals.USS_DIR], - comment: ["Saved file name", "Data Set directory", "USS directory"], - }) - ); - if (savedFile.fileName.toUpperCase().indexOf(globals.DS_DIR.toUpperCase()) >= 0) { - ZoweLogger.debug(vscode.l10n.t("File is a Data Set-- saving ")); - ZoweSaveQueue.push({ uploadRequest: saveFile, savedFile, fileProvider: providers.ds }); - } else if (savedFile.fileName.toUpperCase().indexOf(globals.USS_DIR.toUpperCase()) >= 0) { - ZoweLogger.debug(vscode.l10n.t("File is a USS file -- saving")); - ZoweSaveQueue.push({ uploadRequest: saveUSSFile, savedFile, fileProvider: providers.uss }); - } else { - ZoweLogger.debug( - vscode.l10n.t({ - message: "File {0} is not a Data Set or USS file", - args: [savedFile.fileName], - comment: ["Saved file name"], - }) - ); - } - }) - ); } if (providers.ds || providers.uss || providers.job) { context.subscriptions.push( @@ -230,12 +221,17 @@ export function registerCommonCommands(context: vscode.ExtensionContext, provide }) ); context.subscriptions.push( - vscode.commands.registerCommand("zowe.compareFileStarted", () => { - return globals.FILE_SELECTED_TO_COMPARE; + vscode.commands.registerCommand("zowe.compareFileStarted", (): boolean => { + return LocalFileManagement.fileSelectedToCompare; + }) + ); + context.subscriptions.push( + vscode.commands.registerCommand("zowe.placeholderCommand", () => { + // This command does nothing, its here to let us disable individual items in the tree view }) ); // initialize the globals.filesToCompare array during initialization - globals.resetCompareChoices(); + LocalFileManagement.resetCompareSelection(); } } diff --git a/packages/zowe-explorer/src/shared/utils.ts b/packages/zowe-explorer/src/shared/utils.ts index 7a8054c891..c6eceb2cbe 100644 --- a/packages/zowe-explorer/src/shared/utils.ts +++ b/packages/zowe-explorer/src/shared/utils.ts @@ -14,23 +14,11 @@ import * as vscode from "vscode"; import * as path from "path"; import * as globals from "../globals"; -import { - Gui, - imperative, - IZoweTreeNode, - IZoweDatasetTreeNode, - IZoweUSSTreeNode, - IZoweJobTreeNode, - IZoweTree, - Types, - ZosEncoding, -} from "@zowe/zowe-explorer-api"; -import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; -import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; +import { Gui, IZoweTreeNode, IZoweDatasetTreeNode, IZoweUSSTreeNode, IZoweJobTreeNode, IZoweTree, Types, ZosEncoding } from "@zowe/zowe-explorer-api"; import { ZoweLogger } from "../utils/ZoweLogger"; -import { markDocumentUnsaved } from "../utils/workspace"; -import { errorHandling } from "../utils/ProfilesUtils"; import { LocalStorageKey, ZoweLocalStorage } from "../utils/ZoweLocalStorage"; +import { zosEncodingToString } from "../uss/utils"; +import { UssFSProvider } from "../uss/UssFSProvider"; export enum JobSubmitDialogOpts { Disabled, @@ -119,182 +107,11 @@ export function getAppName(): "VS Code" { return "VS Code"; } -/** - * Returns the file path for the IZoweTreeNode - * - * @export - * @param {string} label - If node is a member, label includes the name of the PDS - * @param {IZoweTreeNode} node - */ -export function getDocumentFilePath(label: string, node: IZoweTreeNode): string { - const dsDir = globals.DS_DIR; - const profName = node.getProfileName(); - const suffix = appendSuffix(label); - return path.join(dsDir, profName || "", suffix); -} - -/** - * Append a suffix on a ds file so it can be interpretted with syntax highlighter - * - * Rules of mapping: - * 1. Start with LLQ and work backwards as it is at this end usually - * the language is specified - * 2. Dont do this for the top level HLQ - */ -function appendSuffix(label: string): string { - const limit = 5; - const bracket = label.indexOf("("); - const split = bracket > -1 ? label.substr(0, bracket).split(".", limit) : label.split(".", limit); - for (let i = split.length - 1; i > 0; i--) { - if (["JCL", "JCLLIB", "CNTL", "PROC", "PROCLIB"].includes(split[i])) { - return label.concat(".jcl"); - } - if (["COBOL", "CBL", "COB", "SCBL"].includes(split[i])) { - return label.concat(".cbl"); - } - if (["COPYBOOK", "COPY", "CPY", "COBCOPY"].includes(split[i])) { - return label.concat(".cpy"); - } - if (["INC", "INCLUDE", "PLINC"].includes(split[i])) { - return label.concat(".inc"); - } - if (["PLI", "PL1", "PLX", "PCX"].includes(split[i])) { - return label.concat(".pli"); - } - if (["SH", "SHELL"].includes(split[i])) { - return label.concat(".sh"); - } - if (["REXX", "REXEC", "EXEC"].includes(split[i])) { - return label.concat(".rexx"); - } - if (split[i] === "XML") { - return label.concat(".xml"); - } - if (split[i] === "ASM" || split[i].indexOf("ASSEMBL") > -1) { - return label.concat(".asm"); - } - if (split[i] === "LOG" || split[i].indexOf("SPFLOG") > -1) { - return label.concat(".log"); - } - } - return label; -} - -export function checkForAddedSuffix(filename: string): boolean { - // identify how close to the end of the string the last . is - const dotPos = filename.length - (1 + filename.lastIndexOf(".")); - return ( - dotPos >= 2 && - dotPos <= 4 && // if the last characters are 2 to 4 long and lower case it has been added - filename.substring(filename.length - dotPos) === filename.substring(filename.length - dotPos).toLowerCase() - ); -} - export function checkIfChildPath(parentPath: string, childPath: string): boolean { const relativePath = path.relative(parentPath, childPath); return relativePath && !relativePath.startsWith("..") && !path.isAbsolute(relativePath); } -/** - * Function that rewrites the document in the active editor thus marking it dirty - * @param {vscode.TextDocument} doc - document to rewrite - * @returns void - */ - -export function markFileAsDirty(doc: vscode.TextDocument): void { - const docText = doc.getText(); - const startPosition = new vscode.Position(0, 0); - const endPosition = new vscode.Position(doc.lineCount, 0); - const deleteRange = new vscode.Range(startPosition, endPosition); - vscode.window.activeTextEditor.edit((editBuilder) => { - editBuilder.delete(deleteRange); - editBuilder.insert(startPosition, docText); - }); -} - -export async function uploadContent( - node: IZoweDatasetTreeNode | IZoweUSSTreeNode, - doc: vscode.TextDocument, - remotePath: string, - profile?: imperative.IProfileLoaded, - etagToUpload?: string, - returnEtag?: boolean -): Promise { - const uploadOptions: zosfiles.IUploadOptions = { - etag: etagToUpload, - returnEtag: returnEtag ?? true, - binary: node.binary, - encoding: node.encoding !== undefined ? node.encoding : profile.profile?.encoding, - responseTimeout: profile.profile?.responseTimeout, - }; - if (isZoweDatasetTreeNode(node)) { - return ZoweExplorerApiRegister.getMvsApi(profile).putContents(doc.fileName, remotePath, uploadOptions); - } else { - const task: imperative.ITaskWithStatus = { - percentComplete: 0, - statusMessage: vscode.l10n.t("Uploading USS file"), - stageName: 0, // TaskStage.IN_PROGRESS - https://github.com/kulshekhar/ts-jest/issues/281 - }; - const result = ZoweExplorerApiRegister.getUssApi(profile).putContent(doc.fileName, remotePath, { - task, - ...uploadOptions, - }); - return result; - } -} - -/** - * Function that will forcefully upload a file and won't check for matching Etag - */ -export function willForceUpload( - node: IZoweDatasetTreeNode | IZoweUSSTreeNode, - doc: vscode.TextDocument, - remotePath: string, - profile?: imperative.IProfileLoaded -): Thenable { - // setup to handle both cases (dataset & USS) - let title: string; - if (isZoweDatasetTreeNode(node)) { - title = vscode.l10n.t("Saving Data Set..."); - } else { - title = vscode.l10n.t("Saving file..."); - } - // Don't wait for prompt to return since this would block the save queue - return Gui.infoMessage(vscode.l10n.t("Would you like to overwrite the remote file?"), { - items: [vscode.l10n.t("Yes"), vscode.l10n.t("No")], - }).then(async (selection) => { - if (selection === vscode.l10n.t("Yes")) { - try { - const uploadResponse = await Gui.withProgress( - { - location: vscode.ProgressLocation.Notification, - title, - }, - () => { - return uploadContent(node, doc, remotePath, profile, null, true); - } - ); - if (uploadResponse.success) { - Gui.showMessage(uploadResponse.commandResponse); - if (node) { - // Upload API returns a singleton array for data sets and an object for USS files - node.setEtag(uploadResponse.apiResponse[0]?.etag ?? uploadResponse.apiResponse.etag); - } - } else { - await markDocumentUnsaved(doc); - Gui.errorMessage(uploadResponse.commandResponse); - } - } catch (err) { - await markDocumentUnsaved(doc); - await errorHandling(err, profile.name); - } - } else { - Gui.showMessage(vscode.l10n.t("Upload cancelled.")); - markFileAsDirty(doc); - } - }); -} - // Type guarding for current IZoweNodeType. // Makes it possible to have multiple types in a function signature, but still be able to use type specific code inside the function definition export function isZoweDatasetTreeNode(node: Types.IZoweNodeType): node is IZoweDatasetTreeNode { @@ -340,7 +157,7 @@ export function updateOpenFiles(treeProvider: IZoweTree } } -function getCachedEncoding(node: IZoweTreeNode): string | undefined { +export function getCachedEncoding(node: IZoweTreeNode): string | undefined { let cachedEncoding: ZosEncoding; if (isZoweUSSTreeNode(node)) { cachedEncoding = (node.getSessionNode() as IZoweUSSTreeNode).encodingMap[node.fullPath]; @@ -384,10 +201,14 @@ export async function promptForEncoding(node: IZoweDatasetTreeNode | IZoweUSSTre }); } - let currentEncoding = node.encoding ?? getCachedEncoding(node); - if (node.binary || currentEncoding === "binary") { + let zosEncoding = node.getEncoding(); + if (zosEncoding === undefined && isZoweUSSTreeNode(node)) { + zosEncoding = await UssFSProvider.instance.fetchEncodingForUri(node.resourceUri); + } + let currentEncoding = zosEncoding ? zosEncodingToString(zosEncoding) : getCachedEncoding(node); + if (zosEncoding?.kind === "binary") { currentEncoding = binaryItem.label; - } else if (node.encoding === null || currentEncoding === "text") { + } else if (zosEncoding === null || zosEncoding?.kind === "text" || currentEncoding === null || currentEncoding === "text") { currentEncoding = ebcdicItem.label; } const encodingHistory = ZoweLocalStorage.getValue(LocalStorageKey.ENCODING_HISTORY) ?? []; diff --git a/packages/zowe-explorer/src/uss/FileStructure.ts b/packages/zowe-explorer/src/uss/FileStructure.ts index d2ece7db6a..a69bc3e2be 100644 --- a/packages/zowe-explorer/src/uss/FileStructure.ts +++ b/packages/zowe-explorer/src/uss/FileStructure.ts @@ -10,6 +10,7 @@ */ import { ZoweLogger } from "../utils/ZoweLogger"; +import * as vscode from "vscode"; /** * File types within the USS tree structure @@ -20,8 +21,7 @@ export enum UssFileType { } export interface UssFileTree { - // The path of the file on the local file system, if it exists - localPath?: string; + localUri?: vscode.Uri; // The path of the file/directory as defined in USS ussPath: string; @@ -33,7 +33,7 @@ export interface UssFileTree { binary?: boolean; // Any files/directory trees within this file tree - children: UssFileTree[]; + children?: UssFileTree[]; // The session where this node comes from (optional for root) sessionName?: string; diff --git a/packages/zowe-explorer/src/uss/USSTree.ts b/packages/zowe-explorer/src/uss/USSTree.ts index 4297ab68cf..f049751043 100644 --- a/packages/zowe-explorer/src/uss/USSTree.ts +++ b/packages/zowe-explorer/src/uss/USSTree.ts @@ -16,6 +16,8 @@ import * as contextually from "../shared/context"; import { FilterItem, FilterDescriptor, errorHandling, syncSessionNode } from "../utils/ProfilesUtils"; import { sortTreeItems, getAppName, checkIfChildPath, updateOpenFiles, promptForEncoding } from "../shared/utils"; import { + confirmForUnsavedDoc, + getInfoForUri, Gui, imperative, IZoweTree, @@ -25,6 +27,7 @@ import { Types, Validation, ZosEncoding, + ZoweScheme, } from "@zowe/zowe-explorer-api"; import { Profiles } from "../Profiles"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; @@ -34,6 +37,7 @@ import { getIconByNode } from "../generators/icons"; import { ZoweLogger } from "../utils/ZoweLogger"; import { TreeViewUtils } from "../utils/TreeViewUtils"; import { TreeProviders } from "../shared/TreeProviders"; +import { UssFSProvider } from "./UssFSProvider"; /** * Creates the USS tree that contains nodes of sessions and data sets @@ -66,6 +70,12 @@ export class USSTree extends ZoweTreeProvider implements Types.IZoweUSSTreeType public copying: Promise; public openFiles: Record = {}; + // only support drag and drop ops within the USS tree at this point + public dragMimeTypes: string[] = []; + public dropMimeTypes: string[] = ["application/vnd.code.tree.zowe.uss.explorer"]; + + private draggedNodes: Record = {}; + public constructor() { super( USSTree.persistenceSchema, @@ -82,11 +92,172 @@ export class USSTree extends ZoweTreeProvider implements Types.IZoweUSSTreeType this.mSessionNodes = [this.mFavoriteSession as IZoweUSSTreeNode]; this.treeView = Gui.createTreeView("zowe.uss.explorer", { treeDataProvider: this, + dragAndDropController: this, canSelectMany: true, }); this.treeView.onDidCollapseElement(TreeViewUtils.refreshIconOnCollapse([contextually.isUssDirectory, contextually.isUssSession], this)); } + public handleDrag(source: IZoweUSSTreeNode[], dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): void { + const items = []; + for (const srcItem of source) { + this.draggedNodes[srcItem.resourceUri.path] = srcItem; + items.push({ + label: srcItem.label, + uri: srcItem.resourceUri, + }); + } + dataTransfer.set("application/vnd.code.tree.zowe.uss.explorer", new vscode.DataTransferItem(items)); + } + + private async crossLparMove(sourceNode: IZoweUSSTreeNode, sourceUri: vscode.Uri, destUri: vscode.Uri, recursiveCall?: boolean): Promise { + const destinationInfo = getInfoForUri(destUri, Profiles.getInstance()); + + if (contextually.isUssDirectory(sourceNode)) { + if (!UssFSProvider.instance.exists(destUri)) { + // create directory on remote FS + try { + await ZoweExplorerApiRegister.getUssApi(destinationInfo.profile).create( + destUri.path.substring(destinationInfo.slashAfterProfilePos), + "directory" + ); + } catch (err) { + // The directory might already exist. Ignore the error and try to move files + } + // create directory entry in local FS + UssFSProvider.instance.createDirectory(destUri); + } + const children = await sourceNode.getChildren(); + for (const childNode of children) { + // move any files within the folder to the destination + await this.crossLparMove( + childNode, + sourceUri.with({ + path: path.posix.join(sourceUri.path, childNode.label as string), + }), + destUri.with({ + path: path.posix.join(destUri.path, childNode.label as string), + }), + true + ); + } + await UssFSProvider.instance.delete(sourceUri, { recursive: true }); + } else { + // create a file on the remote system for writing + try { + await ZoweExplorerApiRegister.getUssApi(destinationInfo.profile).create( + destUri.path.substring(destinationInfo.slashAfterProfilePos), + "file" + ); + } catch (err) { + // The file might already exist. Ignore the error and try to write it to the LPAR + } + // read the contents from the source LPAR + const contents = await UssFSProvider.instance.readFile(sourceNode.resourceUri); + // write the contents to the destination LPAR + try { + await UssFSProvider.instance.writeFile( + destUri.with({ + query: "forceUpload=true", + }), + contents, + { create: true, overwrite: true, noStatusMsg: true } + ); + } catch (err) { + // If the write fails, we cannot move to the next file. + if (err instanceof Error) { + Gui.errorMessage( + vscode.l10n.t("Failed to move file {0}: {1}", destUri.path.substring(destinationInfo.slashAfterProfilePos), err.message) + ); + } + return; + } + + if (!recursiveCall) { + // Delete any files from the selection on the source LPAR + await UssFSProvider.instance.delete(sourceNode.resourceUri, { recursive: false }); + } + } + } + + public async handleDrop( + targetNode: IZoweUSSTreeNode | undefined, + dataTransfer: vscode.DataTransfer, + token: vscode.CancellationToken + ): Promise { + const droppedItems = dataTransfer.get("application/vnd.code.tree.zowe.uss.explorer"); + if (!droppedItems) { + return; + } + + // get the closest parent folder if the target is a file node + let target = targetNode; + if (!contextually.isUssDirectory(target)) { + target = target.getParent(); + } + + // If the target path fully contains the path of the dragged node, + // the user is trying to move a parent node into its child - invalid operation + const movedIntoChild = Object.values(this.draggedNodes).some((n) => target.resourceUri.path.startsWith(n.resourceUri.path)); + if (movedIntoChild) { + this.draggedNodes = {}; + return; + } + + // determine if any overwrites may occur + const willOverwrite = Object.values(this.draggedNodes).some((n) => target.children?.find((tc) => tc.label === n.label) != null); + if (willOverwrite) { + const userOpts = [vscode.l10n.t("Confirm")]; + const resp = await Gui.warningMessage( + vscode.l10n.t("One or more items may be overwritten from this drop operation. Confirm or cancel?"), + { + items: userOpts, + vsCodeOpts: { + modal: true, + }, + } + ); + if (resp == null || resp !== userOpts[0]) { + return; + } + } + + const movingMsg = Gui.setStatusBarMessage(vscode.l10n.t("$(sync~spin) Moving USS files...")); + const parentsToUpdate = new Set(); + + for (const item of droppedItems.value) { + const node = this.draggedNodes[item.uri.path]; + if (node.getParent() === target) { + // skip nodes that are direct children of the target node + continue; + } + + const newUriForNode = vscode.Uri.from({ + scheme: ZoweScheme.USS, + path: path.posix.join("/", target.getProfile().name, target.fullPath, item.label as string), + }); + const prof = node.getProfile(); + const hasMoveApi = ZoweExplorerApiRegister.getUssApi(prof).move != null; + + if (target.getProfile() !== prof || !hasMoveApi) { + // Cross-LPAR, or the "move" API does not exist: write the folders/files on the destination LPAR and delete from source LPAR + await this.crossLparMove(node, node.resourceUri, newUriForNode); + } else if (await UssFSProvider.instance.move(item.uri, newUriForNode)) { + // remove node from old parent and relocate to new parent + const oldParent = node.getParent(); + oldParent.children = oldParent.children.filter((c) => c !== node); + node.resourceUri = newUriForNode; + } + parentsToUpdate.add(node.getParent()); + } + for (const parent of parentsToUpdate) { + this.refreshElement(parent); + } + this.refreshElement(target); + movingMsg.dispose(); + this.draggedNodes = {}; + } + /** * Method for renaming a USS Node. This could be a Favorite Node * @@ -95,7 +266,7 @@ export class USSTree extends ZoweTreeProvider implements Types.IZoweUSSTreeType */ public async rename(originalNode: IZoweUSSTreeNode): Promise { ZoweLogger.trace("USSTree.rename called."); - const currentFilePath = originalNode.getUSSDocumentFilePath(); // The user's complete local file path for the node + const currentFilePath = originalNode.resourceUri.path; // The user's complete local file path for the node const openedTextDocuments: readonly vscode.TextDocument[] = vscode.workspace.textDocuments; // Array of all documents open in VS Code const nodeType = contextually.isFolder(originalNode) ? "folder" : "file"; const parentPath = path.dirname(originalNode.fullPath); @@ -138,17 +309,12 @@ export class USSTree extends ZoweTreeProvider implements Types.IZoweUSSTreeType }), value: originalNode.label.toString().replace(/^\[.+\]:\s/, ""), ignoreFocusOut: true, - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands validateInput: (value) => this.checkDuplicateLabel(parentPath + value, loadedNodes), }; const newName = await Gui.showInputBox(options); if (newName && parentPath + newName !== originalNode.fullPath) { try { const newNamePath = path.posix.join(parentPath, newName); - const oldNamePath = originalNode.fullPath; - - // // Handle rename in back-end: - await ZoweExplorerApiRegister.getUssApi(originalNode.getProfile()).rename(oldNamePath, newNamePath); // Handle rename in UI: if (oldFavorite) { @@ -162,8 +328,16 @@ export class USSTree extends ZoweTreeProvider implements Types.IZoweUSSTreeType await this.renameFavorite(originalNode, newNamePath); } // Rename originalNode in UI - const hasClosedTab = await originalNode.rename(newNamePath); - await originalNode.reopen(hasClosedTab); + await originalNode.rename(newNamePath); + // only reassign URI for renamed files + if (!contextually.isUssDirectory(originalNode)) { + originalNode.command = { + command: "vscode.open", + title: vscode.l10n.t("Open"), + arguments: [originalNode.resourceUri], + }; + } + this.mOnDidChangeTreeData.fire(); this.updateFavorites(); } catch (err) { if (err instanceof Error) { @@ -414,9 +588,10 @@ export class USSTree extends ZoweTreeProvider implements Types.IZoweUSSTreeType profile: node.getProfile(), parentPath: node.getParent().fullPath, }); + temp.resourceUri = node.resourceUri; temp.contextValue = contextually.asFavorite(temp); if (contextually.isFavoriteTextOrBinary(temp)) { - temp.command = { command: "zowe.uss.ZoweUSSNode.open", title: "Open", arguments: [temp] }; + temp.command = { command: "vscode.open", title: "Open", arguments: [temp.resourceUri] }; } } const icon = getIconByNode(temp); @@ -667,6 +842,7 @@ export class USSTree extends ZoweTreeProvider implements Types.IZoweUSSTreeType label: profileName, collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, parentNode: this.mFavoriteSession, + profile: Profiles.getInstance().loadNamedProfile(profileName), }); favProfileNode.contextValue = globals.FAV_PROFILE_CONTEXT; const icon = getIconByNode(favProfileNode); @@ -715,22 +891,30 @@ export class USSTree extends ZoweTreeProvider implements Types.IZoweUSSTreeType * @param parentNode The profile node in this.mFavorites that the favorite belongs to * @returns IZoweUssTreeNode */ - public initializeFavChildNodeForProfile(label: string, line: string, parentNode: IZoweUSSTreeNode): ZoweUSSNode { + public async initializeFavChildNodeForProfile(label: string, line: string, parentNode: IZoweUSSTreeNode): Promise { ZoweLogger.trace("USSTree.initializeFavChildNodeForProfile called."); const favoriteSearchPattern = /^\[.+\]:\s.*\{ussSession\}$/; const directorySearchPattern = /^\[.+\]:\s.*\{directory\}$/; + + const profile = parentNode.getProfile(); + let node: ZoweUSSNode; if (directorySearchPattern.test(line)) { node = new ZoweUSSNode({ label, collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, parentNode, + profile, }); + if (!UssFSProvider.instance.exists(node.resourceUri)) { + await vscode.workspace.fs.createDirectory(node.resourceUri); + } } else if (favoriteSearchPattern.test(line)) { node = new ZoweUSSNode({ label, collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode, + profile, }); node.contextValue = globals.USS_SESSION_CONTEXT; node.fullPath = label; @@ -742,12 +926,20 @@ export class USSTree extends ZoweTreeProvider implements Types.IZoweUSSTreeType label, collapsibleState: vscode.TreeItemCollapsibleState.None, parentNode, + profile, }); node.command = { - command: "zowe.uss.ZoweUSSNode.open", + command: "vscode.open", title: vscode.l10n.t("Open"), - arguments: [node], + arguments: [node.resourceUri], }; + if (!UssFSProvider.instance.exists(node.resourceUri)) { + const parentUri = node.resourceUri.with({ path: path.posix.join(node.resourceUri.path, "..") }); + if (!UssFSProvider.instance.exists(parentUri)) { + await vscode.workspace.fs.createDirectory(parentUri); + } + await vscode.workspace.fs.writeFile(node.resourceUri, new Uint8Array()); + } } node.contextValue = contextually.asFavorite(node); const icon = getIconByNode(node); @@ -958,7 +1150,10 @@ export class USSTree extends ZoweTreeProvider implements Types.IZoweUSSTreeType encoding = await promptForEncoding(node, taggedEncoding !== "untagged" ? taggedEncoding : undefined); } if (encoding !== undefined) { - node.setEncoding(encoding); + if (!(await confirmForUnsavedDoc(node.resourceUri))) { + return; + } + await node.setEncoding(encoding); await node.openUSS(true, false, this); } } diff --git a/packages/zowe-explorer/src/uss/UssFSProvider.ts b/packages/zowe-explorer/src/uss/UssFSProvider.ts new file mode 100644 index 0000000000..82cc8ecd79 --- /dev/null +++ b/packages/zowe-explorer/src/uss/UssFSProvider.ts @@ -0,0 +1,587 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { + BaseProvider, + BufferBuilder, + getInfoForUri, + isDirectoryEntry, + imperative, + Gui, + EntryMetadata, + UssDirectory, + UssFile, + ZosEncoding, + ZoweScheme, +} from "@zowe/zowe-explorer-api"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; +import { UssFileTree, UssFileType } from "./FileStructure"; +import { IZosFilesResponse } from "@zowe/zos-files-for-zowe-sdk"; +import { Profiles } from "../Profiles"; + +export class UssFSProvider extends BaseProvider implements vscode.FileSystemProvider { + // Event objects for provider + + private static _instance: UssFSProvider; + private constructor() { + super(); + ZoweExplorerApiRegister.addFileSystemEvent(ZoweScheme.USS, this.onDidChangeFile); + this.root = new UssDirectory(); + } + + /** + * @returns the USS FileSystemProvider singleton instance + */ + public static get instance(): UssFSProvider { + if (!UssFSProvider._instance) { + UssFSProvider._instance = new UssFSProvider(); + } + + return UssFSProvider._instance; + } + + /* Public functions: File operations */ + + /** + * Returns file statistics about a given URI. + * @param uri A URI that must exist as an entry in the provider + * @returns A structure containing file type, time, size and other metrics + */ + public stat(uri: vscode.Uri): vscode.FileStat { + if (uri.query) { + const queryParams = new URLSearchParams(uri.query); + if (queryParams.has("conflict")) { + return { ...this._lookup(uri, false), permissions: vscode.FilePermission.Readonly }; + } + } + return this._lookup(uri, false); + } + + /** + * Moves an entry in the file system, both remotely and within the provider. + * @param oldUri The old, source URI pointing to an entry that needs moved + * @param newUri The new, destination URI for the file or folder + * @returns Whether the move operation was successful + */ + public async move(oldUri: vscode.Uri, newUri: vscode.Uri): Promise { + const info = this._getInfoFromUri(newUri); + const ussApi = ZoweExplorerApiRegister.getUssApi(info.profile); + + if (!ussApi.move) { + await Gui.errorMessage(vscode.l10n.t("The 'move' function is not implemented for this USS API.")); + return false; + } + + const oldInfo = this._getInfoFromUri(oldUri); + + await ussApi.move(oldInfo.path, info.path); + await this._relocateEntry(oldUri, newUri, info.path); + return true; + } + + public async listFiles(profile: imperative.IProfileLoaded, uri: vscode.Uri): Promise { + const ussPath = uri.path.substring(uri.path.indexOf("/", 1)); + if (ussPath.length === 0) { + throw new imperative.ImperativeError({ + msg: vscode.l10n.t("Could not list USS files: Empty path provided in URI"), + }); + } + const response = await ZoweExplorerApiRegister.getUssApi(profile).fileList(ussPath); + return { + ...response, + apiResponse: { + ...response.apiResponse, + items: (response.apiResponse.items ?? []).filter((it) => !/^\.{1,3}$/.exec(it.name as string)), + }, + }; + } + + /** + * Reads a directory located at the given URI. + * @param uri A valid URI within the provider + * @returns An array of tuples containing each entry name and type + */ + public async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> { + /** + * TODOs: + * - Look into pre-fetching a directory level below the one given + * - Should we support symlinks and can we use z/OSMF "report" option? + */ + const dir = this._lookupAsDirectory(uri, false); + + const result: [string, vscode.FileType][] = []; + if (!dir.wasAccessed && dir !== this.root) { + const fileList = await this.listFiles(dir.metadata.profile, uri); + for (const item of fileList.apiResponse.items) { + const itemName = item.name as string; + + const isDirectory = item.mode.startsWith("d"); + const newEntryType = isDirectory ? vscode.FileType.Directory : vscode.FileType.File; + // skip over existing entries if they are the same type + const entry = dir.entries.get(itemName); + if (entry && entry.type === newEntryType) { + continue; + } + + // create new entries for any files/folders that aren't in the provider + const UssType = item.mode.startsWith("d") ? UssDirectory : UssFile; + const newEntry = new UssType(itemName); + newEntry.metadata = { ...dir.metadata, path: path.posix.join(dir.metadata.path, itemName) }; + dir.entries.set(itemName, newEntry); + } + } + + for (const [name, child] of dir.entries) { + result.push([name, child.type]); + } + return result; + } + + /** + * Fetches a file from the remote system at the given URI. + * @param uri The URI pointing to a valid file to fetch from the remote system + * @param editor (optional) An editor instance to reload if the URI is already open + */ + public async fetchFileAtUri(uri: vscode.Uri, options?: { editor?: vscode.TextEditor | null; isConflict?: boolean }): Promise { + const file = this._lookupAsFile(uri); + const uriInfo = getInfoForUri(uri, Profiles.getInstance()); + const bufBuilder = new BufferBuilder(); + const filePath = uri.path.substring(uriInfo.slashAfterProfilePos); + const metadata = file.metadata; + const profileEncoding = file.encoding ? null : file.metadata.profile.profile?.encoding; + const resp = await ZoweExplorerApiRegister.getUssApi(metadata.profile).getContents(filePath, { + binary: file.encoding?.kind === "binary", + encoding: file.encoding?.kind === "other" ? file.encoding.codepage : profileEncoding, + responseTimeout: metadata.profile.profile?.responseTimeout, + returnEtag: true, + stream: bufBuilder, + }); + await this.autoDetectEncoding(file); + + const data: Uint8Array = bufBuilder.read() ?? new Uint8Array(); + if (options?.isConflict) { + file.conflictData = { + contents: data, + etag: resp.apiResponse.etag, + size: data.byteLength, + }; + } else { + file.data = data; + file.etag = resp.apiResponse.etag; + file.size = file.data.byteLength; + } + + this._fireSoon({ type: vscode.FileChangeType.Changed, uri: uri }); + if (options?.editor) { + await this._updateResourceInEditor(uri); + } + } + + public async autoDetectEncoding(entry: UssFile): Promise { + if (entry.encoding !== undefined) { + return; + } + + const ussApi = ZoweExplorerApiRegister.getUssApi(entry.metadata.profile); + if (ussApi.getTag != null) { + const taggedEncoding = await ussApi.getTag(entry.metadata.path); + if (taggedEncoding === "binary" || taggedEncoding === "mixed") { + entry.encoding = { kind: "binary" }; + } else if (taggedEncoding !== "untagged") { + entry.encoding = { kind: "other", codepage: taggedEncoding }; + } + } else { + const isBinary = await ussApi.isFileTagBinOrAscii(entry.metadata.path); + entry.encoding = isBinary ? { kind: "binary" } : undefined; + } + } + + public async fetchEncodingForUri(uri: vscode.Uri): Promise { + const file = this._lookupAsFile(uri); + await this.autoDetectEncoding(file); + + return file.encoding; + } + + /** + * Reads a file at the given URI and fetches it from the remote system (if not yet accessed). + * @param uri The URI pointing to a valid file on the remote system + * @returns The file's contents as an array of bytes + */ + public async readFile(uri: vscode.Uri): Promise { + const file = this._lookupAsFile(uri, { silent: false }); + const profInfo = this._getInfoFromUri(uri); + + if (profInfo.profile == null) { + throw vscode.FileSystemError.FileNotFound(vscode.l10n.t("Profile does not exist for this file.")); + } + + const urlQuery = new URLSearchParams(uri.query); + const isConflict = urlQuery.has("conflict"); + + // Fetch contents from the mainframe if: + // - the file hasn't been accessed yet + // - fetching a conflict from the remote FS + if (!file.wasAccessed || isConflict) { + await this.fetchFileAtUri(uri, { isConflict }); + if (!isConflict) { + file.wasAccessed = true; + } + } + + return isConflict ? file.conflictData.contents : file.data; + } + + private async uploadEntry( + entry: UssFile, + content: Uint8Array, + options?: { forceUpload?: boolean; noStatusMsg?: boolean } + ): Promise { + const statusMsg = + // only show a status message if "noStatusMsg" is not specified, + // or if the entry does not exist and the new contents are empty (new placeholder entry) + options?.noStatusMsg || (!entry && content.byteLength === 0) + ? new vscode.Disposable(() => {}) + : Gui.setStatusBarMessage(vscode.l10n.t("$(sync~spin) Saving USS file...")); + + let resp: IZosFilesResponse; + try { + const ussApi = ZoweExplorerApiRegister.getUssApi(entry.metadata.profile); + await this.autoDetectEncoding(entry); + const profileEncoding = entry.encoding ? null : entry.metadata.profile.profile?.encoding; + + resp = await ussApi.uploadFromBuffer(Buffer.from(content), entry.metadata.path, { + binary: entry.encoding?.kind === "binary", + encoding: entry.encoding?.kind === "other" ? entry.encoding.codepage : profileEncoding, + etag: options?.forceUpload || entry.etag == null ? undefined : entry.etag, + returnEtag: true, + }); + } catch (err) { + statusMsg.dispose(); + throw err; + } + + statusMsg.dispose(); + return resp; + } + + /** + * Attempts to write a file at the given URI. + * @param uri The URI pointing to a file entry that should be written + * @param content The content to write to the file, as an array of bytes + * @param options Options for writing the file + * - `create` - Creates the file if it does not exist + * - `overwrite` - Overwrites the content if the file exists + */ + public async writeFile( + uri: vscode.Uri, + content: Uint8Array, + options: { create: boolean; overwrite: boolean; noStatusMsg?: boolean } + ): Promise { + const fileName = path.posix.basename(uri.path); + const parentDir = this._lookupParentDirectory(uri); + + let entry = parentDir.entries.get(fileName); + if (isDirectoryEntry(entry)) { + throw vscode.FileSystemError.FileIsADirectory(uri); + } + if (!entry && !options.create) { + throw vscode.FileSystemError.FileNotFound(uri); + } + if (entry && options.create && !options.overwrite) { + throw vscode.FileSystemError.FileExists(uri); + } + + // Attempt to write data to remote system, and handle any conflicts from e-tag mismatch + const urlQuery = new URLSearchParams(uri.query); + const forceUpload = urlQuery.has("forceUpload"); + try { + if (!entry) { + entry = new UssFile(fileName); + // Build the metadata for the file using the parent's metadata (if available), + // or build it using the helper function + entry.metadata = { + ...parentDir.metadata, + path: path.posix.join(parentDir.metadata.path, fileName), + }; + + if (content.byteLength > 0) { + // user is trying to edit a file that was just deleted: make the API call + const resp = await this.uploadEntry(entry as UssFile, content, { forceUpload }); + entry.etag = resp.apiResponse.etag; + } + entry.data = content; + parentDir.entries.set(fileName, entry); + this._fireSoon({ type: vscode.FileChangeType.Created, uri }); + } else { + if (entry.inDiffView || urlQuery.has("inDiff")) { + // Allow users to edit the local copy of a file in the diff view, but don't make any API calls. + entry.inDiffView = true; + entry.data = content; + entry.mtime = Date.now(); + entry.size = content.byteLength; + return; + } + + if (entry.wasAccessed || content.length > 0) { + const resp = await this.uploadEntry(entry as UssFile, content, { forceUpload }); + entry.etag = resp.apiResponse.etag; + } + entry.data = content; + } + } catch (err) { + if (!err.message.includes("Rest API failure with HTTP(S) status 412")) { + // Some unknown error happened, don't update the entry + throw err; + } + + entry.data = content; + // Prompt the user with the conflict dialog + await this._handleConflict(uri, entry); + return; + } + + entry.mtime = Date.now(); + entry.size = content.byteLength; + this._fireSoon({ type: vscode.FileChangeType.Changed, uri }); + } + + public makeEmptyFileWithEncoding(uri: vscode.Uri, encoding: ZosEncoding): void { + const parentDir = this._lookupParentDirectory(uri); + const fileName = path.posix.basename(uri.path); + const entry = new UssFile(fileName); + entry.encoding = encoding; + entry.metadata = { + ...parentDir.metadata, + path: path.posix.join(parentDir.metadata.path, fileName), + }; + entry.data = new Uint8Array(); + parentDir.entries.set(fileName, entry); + } + + /** + * Attempts to rename an entry from the old, source URI to the new, destination URI. + * @param oldUri The source URI of the file/folder + * @param newUri The destination URI of the file/folder + * @param options Options for renaming the file/folder + * - `overwrite` - Overwrites the file if the new URI already exists + */ + public async rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean }): Promise { + const newUriEntry = this._lookup(newUri, true); + if (!options.overwrite && newUriEntry) { + throw vscode.FileSystemError.FileExists( + `Rename failed: ${path.posix.basename(newUri.path)} already exists in ${path.posix.join(newUriEntry.metadata.path, "..")}` + ); + } + + const entry = this._lookup(oldUri, false) as UssDirectory | UssFile; + const parentDir = this._lookupParentDirectory(oldUri); + + const newName = path.posix.basename(newUri.path); + + // Build the new path using the previous path and new file/folder name. + const newPath = path.posix.join(entry.metadata.path, "..", newName); + + try { + await ZoweExplorerApiRegister.getUssApi(entry.metadata.profile).rename(entry.metadata.path, newPath); + } catch (err) { + await Gui.errorMessage( + vscode.l10n.t({ + message: "Renaming {0} failed due to API error: {1}", + args: [entry.metadata.path, err.message], + comment: ["File path", "Error message"], + }) + ); + return; + } + + parentDir.entries.delete(entry.name); + entry.name = newName; + + entry.metadata.path = newPath; + // We have to update the path for all child entries if they exist in the FileSystem + // This way any further API requests in readFile will use the latest paths on the LPAR + if (isDirectoryEntry(entry)) { + this._updateChildPaths(entry); + } + parentDir.entries.set(newName, entry); + this._fireSoon({ type: vscode.FileChangeType.Deleted, uri: oldUri }, { type: vscode.FileChangeType.Created, uri: newUri }); + } + + /** + * Deletes a file or folder at the given URI. + * @param uri The URI that points to the file/folder to delete + */ + public async delete(uri: vscode.Uri, options: { recursive: boolean }): Promise { + const { entryToDelete, parent, parentUri } = this._getDeleteInfo(uri); + + try { + await ZoweExplorerApiRegister.getUssApi(parent.metadata.profile).delete( + entryToDelete.metadata.path, + entryToDelete instanceof UssDirectory + ); + } catch (err) { + await Gui.errorMessage( + vscode.l10n.t({ + message: "Deleting {0} failed due to API error: {1}", + args: [entryToDelete.metadata.path, err.message], + comment: ["File name", "Error message"], + }) + ); + return; + } + + parent.entries.delete(entryToDelete.name); + parent.mtime = Date.now(); + parent.size -= 1; + + this._fireSoon({ type: vscode.FileChangeType.Changed, uri: parentUri }, { uri, type: vscode.FileChangeType.Deleted }); + } + + public async copy(source: vscode.Uri, destination: vscode.Uri, options: { readonly overwrite: boolean }): Promise { + const uriQuery = new URLSearchParams(source.query); + if (!uriQuery.has("tree")) { + return; + } + + const sourceTree = JSON.parse(decodeURIComponent(uriQuery.get("tree"))); + return this.copyTree(source, destination, { ...options, tree: sourceTree }); + } + + private buildFileName(fileList: any[], fileName: string): string { + // Check root path for conflicts + if (fileList?.find((file) => file.name === fileName) != null) { + // If file names match, build the copy suffix + let dupCount = 1; + const extension = path.extname(fileName); + const baseNameForFile = path.parse(fileName)?.name; + let dupName = `${baseNameForFile} (${dupCount})${extension}`; + while (fileList.find((file) => file.name === dupName) != null) { + dupCount++; + dupName = `${baseNameForFile} (${dupCount})${extension}`; + } + return dupName; + } + + return fileName; + } + + /** + * Copy a file/folder from a source URI to destination URI. + * @param source The source URI for the file/folder to copy + * @param destination The new, destination URI for the file/folder + * @param options Options for copying the file/folder + * - `overwrite` - Overwrites the entry at the destination URI if it exists + * - `tree` - A tree representation of the file structure to copy + * @returns + */ + private async copyTree( + source: vscode.Uri, + destination: vscode.Uri, + options: { readonly overwrite: boolean; readonly tree: UssFileTree } + ): Promise { + const destInfo = this._getInfoFromUri(destination); + const sourceInfo = this._getInfoFromUri(source); + const api = ZoweExplorerApiRegister.getUssApi(destInfo.profile); + + const hasCopyApi = api.copy != null; + + const apiResponse = await api.fileList(path.posix.join(destInfo.path, "..")); + const fileList = apiResponse.apiResponse?.items; + + const fileName = this.buildFileName(fileList, path.basename(destInfo.path)); + const outputPath = path.posix.join(destInfo.path, "..", fileName); + + if (hasCopyApi && sourceInfo.profile.profile === destInfo.profile.profile) { + await api.copy(outputPath, { + from: sourceInfo.path, + recursive: options.tree.type === UssFileType.Directory, + overwrite: options.overwrite ?? true, + }); + } else if (options.tree.type === UssFileType.Directory) { + // Not all APIs respect the recursive option, so it's best to + // create a directory and copy recursively to avoid missing any files/folders + await api.create(outputPath, "directory"); + if (options.tree.children) { + for (const child of options.tree.children) { + await this.copyTree( + child.localUri, + vscode.Uri.from({ + scheme: ZoweScheme.USS, + path: path.posix.join(destInfo.profile.name, outputPath, child.baseName), + }), + { ...options, tree: child } + ); + } + } + } else { + const fileEntry = this._lookup(source, true); + if (fileEntry == null) { + return; + } + + if (!fileEntry.wasAccessed) { + // must fetch contents of file first before pasting in new path + await this.readFile(source); + } + await api.uploadFromBuffer(Buffer.from(fileEntry.data), outputPath); + } + } + + /** + * Creates a directory entry in the provider at the given URI. + * @param uri The URI that represents a new directory path + */ + public createDirectory(uri: vscode.Uri): void { + const basename = path.posix.basename(uri.path); + const parent = this._lookupParentDirectory(uri, false); + if (parent.entries.has(basename)) { + return; + } + + const entry = new UssDirectory(basename); + const profInfo = + parent !== this.root + ? { + profile: parent.metadata.profile, + // we can strip profile name from path because its not involved in API calls + path: path.posix.join(parent.metadata.path, basename), + } + : this._getInfoFromUri(uri); + entry.metadata = profInfo; + + parent.entries.set(entry.name, entry); + parent.mtime = Date.now(); + parent.size += 1; + this._fireSoon( + { type: vscode.FileChangeType.Changed, uri: uri.with({ path: path.posix.join(uri.path, "..") }) }, + { type: vscode.FileChangeType.Created, uri } + ); + } + + public watch(_resource: vscode.Uri, _options?: { readonly recursive: boolean; readonly excludes: readonly string[] }): vscode.Disposable { + // ignore, fires for all changes... + return new vscode.Disposable(() => {}); + } + + /** + * Returns metadata about the file entry from the context of z/OS. + * @param uri A URI with a path in the format `zowe-*:/{lpar_name}/{full_path}?` + * @returns Metadata for the URI that contains the profile instance and path + */ + private _getInfoFromUri(uri: vscode.Uri): EntryMetadata { + const uriInfo = getInfoForUri(uri, Profiles.getInstance()); + return { profile: uriInfo.profile, path: uriInfo.isRoot ? "/" : uri.path.substring(uriInfo.slashAfterProfilePos) }; + } +} diff --git a/packages/zowe-explorer/src/uss/ZoweUSSNode.ts b/packages/zowe-explorer/src/uss/ZoweUSSNode.ts index da14c3ff4f..9af8bb9cd7 100644 --- a/packages/zowe-explorer/src/uss/ZoweUSSNode.ts +++ b/packages/zowe-explorer/src/uss/ZoweUSSNode.ts @@ -12,21 +12,29 @@ import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; import * as globals from "../globals"; import * as vscode from "vscode"; -import * as fs from "fs"; import * as path from "path"; -import { Gui, imperative, IZoweUSSTreeNode, ZoweTreeNode, Types, Validation, MainframeInteraction, ZosEncoding } from "@zowe/zowe-explorer-api"; +import { + Gui, + imperative, + IZoweUSSTreeNode, + ZoweTreeNode, + Types, + Validation, + MainframeInteraction, + ZosEncoding, + ZoweScheme, +} from "@zowe/zowe-explorer-api"; import { Profiles } from "../Profiles"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; -import { errorHandling, syncSessionNode } from "../utils/ProfilesUtils"; +import { errorHandling, getSessionLabel, syncSessionNode } from "../utils/ProfilesUtils"; import { getIconByNode } from "../generators/icons/index"; -import { autoDetectEncoding, fileExistsCaseSensitiveSync, injectAdditionalDataToTooltip } from "../uss/utils"; +import { injectAdditionalDataToTooltip } from "../uss/utils"; import * as contextually from "../shared/context"; -import { closeOpenedTextFile } from "../utils/workspace"; -import { UssFileTree, UssFileType, UssFileUtils } from "./FileStructure"; +import { UssFileTree } from "./FileStructure"; import { ZoweLogger } from "../utils/ZoweLogger"; +import { UssFSProvider } from "./UssFSProvider"; import { IZoweUssTreeOpts } from "../shared/IZoweTreeOpts"; import { TreeProviders } from "../shared/TreeProviders"; -import { LocalFileInfo } from "../shared/utils"; /** * A type of TreeItem used to represent sessions and USS directories and files @@ -37,16 +45,16 @@ import { LocalFileInfo } from "../shared/utils"; */ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { public command: vscode.Command; - public prevPath = ""; - public fullPath = ""; public dirty = true; public children: IZoweUSSTreeNode[] = []; + public collapsibleState: vscode.TreeItemCollapsibleState; public binary = false; - public encoding?: string; public encodingMap = {}; public shortLabel = ""; - public downloadedTime = null as string; + public downloadedTime = null; private downloadedInternal = false; + public fullPath: string; + public resourceUri?: vscode.Uri; public attributes?: Types.FileAttributes; public onUpdateEmitter: vscode.EventEmitter; @@ -61,9 +69,6 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { public constructor(opts: IZoweUssTreeOpts) { super(opts.label, opts.collapsibleState, opts.parentNode, opts.session, opts.profile); this.binary = opts.encoding?.kind === "binary"; - if (!this.binary && opts.encoding != null) { - this.encoding = opts.encoding.kind === "other" ? opts.encoding.codepage : null; - } this.parentPath = opts.parentPath; if (opts.collapsibleState !== vscode.TreeItemCollapsibleState.None) { this.contextValue = globals.USS_DIR_CONTEXT; @@ -73,7 +78,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { this.contextValue = globals.USS_TEXT_FILE_CONTEXT; } if (this.parentPath) { - this.fullPath = this.tooltip = this.parentPath + "/" + opts.label; + this.fullPath = this.tooltip = path.posix.join(this.parentPath, opts.label); if (opts.parentPath === "/") { // Keep fullPath of root level nodes preceded by a single slash this.fullPath = this.tooltip = "/" + opts.label; @@ -88,14 +93,37 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { this.tooltip = this.fullPath; } this.etag = opts.etag ? opts.etag : ""; + + if (opts.contextOverride) { + this.contextValue = opts.contextOverride; + } + const icon = getIconByNode(this); if (icon) { this.iconPath = icon.path; } - if (contextually.isSession(this)) { + const isSession = opts.parentNode == null; + if (isSession) { this.id = `uss.${this.label.toString()}`; } + if (opts.profile) { + this.profile = opts.profile; + } this.onUpdateEmitter = new vscode.EventEmitter(); + if (opts.label !== vscode.l10n.t("Favorites")) { + const sessionLabel = opts.profile?.name ?? getSessionLabel(this); + this.resourceUri = vscode.Uri.from({ + scheme: ZoweScheme.USS, + path: path.posix.join("/", sessionLabel, this.fullPath), + }); + if (isSession) { + UssFSProvider.instance.createDirectory(this.resourceUri); + } + + if (opts.encoding != null) { + UssFSProvider.instance.makeEmptyFileWithEncoding(this.resourceUri, opts.encoding); + } + } } public get onUpdate(): vscode.Event { @@ -115,7 +143,17 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { public async getChildren(): Promise { ZoweLogger.trace("ZoweUSSNode.getChildren called."); if ((!this.fullPath && contextually.isSession(this)) || contextually.isDocument(this)) { - return []; + const placeholder = new ZoweUSSNode({ + label: vscode.l10n.t("Use the search button to list USS files"), + collapsibleState: vscode.TreeItemCollapsibleState.None, + parentNode: this, + contextOverride: globals.INFORMATION_CONTEXT, + }); + placeholder.command = { + command: "zowe.placeholderCommand", + title: "Placeholder", + }; + return [placeholder]; } if (!this.dirty) { @@ -130,9 +168,10 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { throw Error("Invalid node"); } - // Get the directories from the fullPath and display any thrown errors + // Get the list of files/folders at the given USS path and handle any errors let response: zosfiles.IZosFilesResponse; const sessNode = this.getSessionNode(); + let nodeProfile; try { const cachedProfile = Profiles.getInstance().loadNamedProfile(this.getProfileName()); if (!ZoweExplorerApiRegister.getUssApi(cachedProfile).getSession(cachedProfile)) { @@ -142,11 +181,16 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { errorCode: `${imperative.RestConstants.HTTP_STATUS_401}`, }); } - response = await ZoweExplorerApiRegister.getUssApi(cachedProfile).fileList(this.fullPath); - - // Throws reject if the Zowe command does not throw an error but does not succeed - if (!response.success) { - throw Error(vscode.l10n.t("The response from Zowe CLI was not successful")); + nodeProfile = cachedProfile; + if (contextually.isSession(this)) { + response = await UssFSProvider.instance.listFiles( + nodeProfile, + this.resourceUri.with({ + path: path.posix.join(this.resourceUri.path, this.fullPath), + }) + ); + } else { + response = await UssFSProvider.instance.listFiles(nodeProfile, this.resourceUri); } } catch (err) { await errorHandling(err, this.label.toString(), vscode.l10n.t("Retrieving response from uss-file-list")); @@ -155,16 +199,13 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { } // If search path has changed, invalidate all children - if (this.fullPath?.length > 0 && this.prevPath !== this.fullPath) { + if (this.resourceUri.path !== this.fullPath) { this.children = []; } - // Build a list of nodes based on the API response const responseNodes: IZoweUSSTreeNode[] = []; for (const item of response.apiResponse.items) { - if (item.name === "." || item.name === "..") { - continue; - } + // ".", "..", and "..." have already been filtered out const existing = this.children.find( // Ensure both parent path and short label match. @@ -186,62 +227,64 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { continue; } - if (item.mode.startsWith("d")) { - // Create a node for the USS directory. - const temp = new ZoweUSSNode({ - label: item.name, - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, - parentNode: this, - parentPath: this.fullPath, - profile: this.profile, - }); - temp.attributes = { - gid: item.gid, - uid: item.uid, - group: item.group, - perms: item.mode, - owner: item.user, - }; - responseNodes.push(temp); + const isDir = item.mode.startsWith("d"); + const collapseState = isDir ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None; + const temp = new ZoweUSSNode({ + label: item.name, + collapsibleState: collapseState, + parentNode: this, + parentPath: this.fullPath, + profile: nodeProfile, + encoding: isDir ? undefined : this.getSessionNode().encodingMap[`${this.fullPath}/${item.name as string}`], + }); + temp.attributes = { + gid: item.gid, + uid: item.uid, + group: item.group, + perms: item.mode, + owner: item.user, + }; + if (isDir) { + // Create an entry for the USS folder if it doesn't exist. + if (!UssFSProvider.instance.exists(temp.resourceUri)) { + vscode.workspace.fs.createDirectory(temp.resourceUri); + } } else { - // Create a node for the USS file. - const cachedEncoding = this.getSessionNode().encodingMap[`${this.fullPath}/${item.name as string}`]; - const temp = new ZoweUSSNode({ - label: item.name, - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: this, - profile: this.profile, - parentPath: this.fullPath, - encoding: cachedEncoding, - }); - temp.attributes = { - gid: item.gid, - uid: item.uid, - group: item.group, - perms: item.mode, - owner: item.user, - }; + // Create an entry for the USS file if it doesn't exist. + if (!UssFSProvider.instance.exists(temp.resourceUri)) { + await vscode.workspace.fs.writeFile(temp.resourceUri, new Uint8Array()); + } temp.command = { - command: "zowe.uss.ZoweUSSNode.open", + command: "vscode.open", title: vscode.l10n.t("Open"), - arguments: [temp], + arguments: [temp.resourceUri], }; - responseNodes.push(temp); } + responseNodes.push(temp); } const nodesToAdd = responseNodes.filter((c) => !this.children.includes(c)); const nodesToRemove = this.children.filter((c) => !responseNodes.includes(c)); + // remove any entries from FS provider that were deleted from mainframe when tree view is refreshed + for (const node of nodesToRemove) { + if (node.resourceUri) { + UssFSProvider.instance.removeEntry(node.resourceUri); + } + } + this.children = this.children .concat(nodesToAdd) .filter((c) => !nodesToRemove.includes(c)) - .sort((a, b) => ((a.label as string) < (b.label as string) ? -1 : 1)); - this.prevPath = this.fullPath; + .sort((a, b) => (a.label as string).localeCompare(b.label as string)); this.dirty = false; return this.children; } + public getEncoding(): ZosEncoding { + return UssFSProvider.instance.getEncodingForFile(this.resourceUri); + } + public setEncoding(encoding: ZosEncoding): void { ZoweLogger.trace("ZoweUSSNode.setEncoding called."); if (!(this.contextValue.startsWith(globals.USS_BINARY_FILE_CONTEXT) || this.contextValue.startsWith(globals.USS_TEXT_FILE_CONTEXT))) { @@ -250,12 +293,11 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { if (encoding?.kind === "binary") { this.contextValue = globals.USS_BINARY_FILE_CONTEXT; this.binary = true; - this.encoding = undefined; } else { this.contextValue = globals.USS_TEXT_FILE_CONTEXT; this.binary = false; - this.encoding = encoding?.kind === "text" ? null : encoding?.codepage; } + UssFSProvider.instance.setEncodingForFile(this.resourceUri, encoding); if (encoding != null) { this.getSessionNode().encodingMap[this.fullPath] = encoding; } else { @@ -276,37 +318,25 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { this.dirty = true; } - /** - * Helper getter to check dirtiness of node inside opened editor tabs, can be more accurate than saved value - * - * @returns {boolean} - */ - public get isDirtyInEditor(): boolean { - ZoweLogger.trace("ZoweUSSNode.isDirtyInEditor called."); - const openedTextDocuments = vscode.workspace.textDocuments; - const currentFilePath = this.getUSSDocumentFilePath(); - - for (const document of openedTextDocuments) { - if (document.fileName === currentFilePath) { - return document.isDirty; - } - } - - return false; - } - public get openedDocumentInstance(): vscode.TextDocument { ZoweLogger.trace("ZoweUSSNode.openedDocumentInstance called."); - const openedTextDocuments = vscode.workspace.textDocuments; - const currentFilePath = this.getUSSDocumentFilePath(); + return vscode.workspace.textDocuments.find((doc) => doc.uri.toString() === this.resourceUri.toString()); + } - for (const document of openedTextDocuments) { - if (document.fileName === currentFilePath) { - return document; - } - } + private renameChild(parentUri: vscode.Uri): void { + const childPath = path.posix.join(parentUri.path, this.label as string); + this.fullPath = childPath; + this.resourceUri = parentUri.with({ + path: childPath, + }); + this.label = this.shortLabel = path.posix.basename(this.fullPath); + this.tooltip = injectAdditionalDataToTooltip(this, childPath); - return null; + if (this.children.length > 0) { + this.children.forEach((c) => { + (c as ZoweUSSNode).renameChild(this.resourceUri); + }); + } } /** @@ -315,22 +345,36 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { */ public async rename(newFullPath: string): Promise { ZoweLogger.trace("ZoweUSSNode.rename called."); - const currentFilePath = this.getUSSDocumentFilePath(); - const hasClosedInstance = await closeOpenedTextFile(currentFilePath); + + const oldUri = vscode.Uri.from({ + scheme: ZoweScheme.USS, + path: path.posix.join("/", this.profile.name, this.fullPath), + }); + const newUri = vscode.Uri.from({ + scheme: ZoweScheme.USS, + path: path.posix.join("/", this.profile.name, newFullPath), + }); + + try { + await UssFSProvider.instance.rename(oldUri, newUri, { overwrite: false }); + } catch (err) { + Gui.errorMessage(err.message); + return; + } + this.fullPath = newFullPath; - this.shortLabel = newFullPath.split("/").pop(); - this.label = this.shortLabel; + this.resourceUri = newUri; + this.label = this.shortLabel = path.posix.basename(newFullPath); this.tooltip = injectAdditionalDataToTooltip(this, newFullPath); // Update the full path of any children already loaded locally if (this.children.length > 0) { this.children.forEach((child) => { - const newChildFullPath = newFullPath + "/" + child.shortLabel; - child.rename(newChildFullPath); + (child as ZoweUSSNode).renameChild(newUri); }); } const providers = TreeProviders.providers; providers.uss.refresh(); - return hasClosedInstance; + return true; } /** @@ -340,7 +384,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { public async reopen(hasClosedInstance = false): Promise { ZoweLogger.trace("ZoweUSSNode.reopen called."); if (!this.isFolder && (hasClosedInstance || (this.binary && this.downloaded))) { - await vscode.commands.executeCommand("zowe.uss.ZoweUSSNode.open", this); + await vscode.commands.executeCommand("vscode.open", this.resourceUri); } } @@ -364,23 +408,14 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { vscode.commands.executeCommand("zowe.uss.refreshUSSInTree", this); } - public async deleteUSSNode(ussFileProvider: Types.IZoweUSSTreeType, filePath: string, cancelled: boolean = false): Promise { + public async deleteUSSNode(ussFileProvider: Types.IZoweUSSTreeType, _filePath: string, cancelled: boolean = false): Promise { ZoweLogger.trace("ZoweUSSNode.deleteUSSNode called."); - const cachedProfile = Profiles.getInstance().loadNamedProfile(this.getProfileName()); if (cancelled) { Gui.showMessage(vscode.l10n.t("Delete action was cancelled.")); return; } try { - await ZoweExplorerApiRegister.getUssApi(cachedProfile).delete(this.fullPath, contextually.isUssDirectory(this)); - this.getParent().dirty = true; - try { - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - } - } catch (err) { - // ignore error as the path likely doesn't exist - } + await UssFSProvider.instance.delete(this.resourceUri, { recursive: this.isFolder }); } catch (err) { ZoweLogger.error(err); if (err instanceof Error) { @@ -404,9 +439,13 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { ); // Remove node from the USS Favorites tree - ussFileProvider.removeFavorite(this); + await ussFileProvider.removeFavorite(this); ussFileProvider.removeFileHistory(`[${this.getProfileName()}]: ${this.parentPath}/${this.label.toString()}`); - ussFileProvider.refresh(); + const parent = this.getParent(); + parent.children = parent.children.filter((c) => c !== this); + if (ussFileProvider.nodeDataChanged) { + ussFileProvider.nodeDataChanged(parent); + } } /** @@ -419,16 +458,6 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { return this.etag; } - /** - * Set the [etag] for this node - * - * @returns {void} - */ - public setEtag(etagValue): void { - ZoweLogger.trace("ZoweUSSNode.setEtag called."); - this.etag = etagValue; - } - /** * Getter for downloaded property * @@ -446,7 +475,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { this.downloadedInternal = value; if (value) { - this.downloadedTime = new Date().toISOString(); + this.downloadedTime = new Date(); this.tooltip = injectAdditionalDataToTooltip(this, this.fullPath); } @@ -468,30 +497,8 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { * * @param {IZoweTreeNode} node */ - public async openUSS(forceDownload: boolean, previewFile: boolean, ussFileProvider: Types.IZoweUSSTreeType): Promise { + public async openUSS(download: boolean, _previewFile: boolean, ussFileProvider: Types.IZoweUSSTreeType): Promise { ZoweLogger.trace("ZoweUSSNode.openUSS called."); - await ussFileProvider.checkCurrentProfile(this); - - const doubleClicked = Gui.utils.wasDoubleClicked(this, ussFileProvider); - const shouldPreview = doubleClicked ? false : previewFile; - if (Profiles.getInstance().validProfile !== Validation.ValidationType.INVALID) { - try { - const fileInfo = await this.downloadUSS(forceDownload); - this.downloaded = true; - // Add document name to recently-opened files - ussFileProvider.addFileHistory(`[${this.getProfile().name}]: ${this.fullPath}`); - ussFileProvider.getTreeView().reveal(this, { select: true, focus: true, expand: false }); - - await this.initializeFileOpening(fileInfo.path, shouldPreview); - } catch (err) { - await errorHandling(err, this.getProfileName()); - throw err; - } - } - } - - public async downloadUSS(forceDownload: boolean): Promise { - const fileInfo = {} as LocalFileInfo; const errorMsg = vscode.l10n.t("open() called from invalid node."); switch (true) { // For opening favorited and non-favorited files @@ -507,55 +514,20 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { throw Error(errorMsg); } - fileInfo.path = this.getUSSDocumentFilePath(); - fileInfo.name = String(this.label); - // check if some other file is already created with the same name avoid opening file warn user - const fileExists = fs.existsSync(fileInfo.path); - if (fileExists && !fileExistsCaseSensitiveSync(fileInfo.path)) { - Gui.showMessage( - vscode.l10n.t( - `There is already a file with the same name. - Please change your OS file system settings if you want to give case sensitive file names` - ) - ); - return; - } - // if local copy exists, open that instead of pulling from mainframe - let response: zosfiles.IZosFilesResponse; - if (forceDownload || !fileExists) { - response = await this.downloadUSSApiCall(fileInfo.path, fileInfo.name); - } - if (response != null) { - this.setEtag(response.apiResponse.etag); - } - return fileInfo; - } + await ussFileProvider.checkCurrentProfile(this); - private async downloadUSSApiCall(documentFilePath: string, label: string): Promise { - ZoweLogger.info( - vscode.l10n.t({ - message: "Downloading {0}", - args: [label], - comment: ["Label"], - }) - ); - try { - const cachedProfile = Profiles.getInstance().loadNamedProfile(this.getProfileName()); - await autoDetectEncoding(this, cachedProfile); - - const statusMsg = Gui.setStatusBarMessage(vscode.l10n.t("$(sync~spin) Downloading USS file...")); - const response = await ZoweExplorerApiRegister.getUssApi(cachedProfile).getContents(this.fullPath, { - file: documentFilePath, - binary: this.binary, - returnEtag: true, - encoding: this.encoding !== undefined ? this.encoding : cachedProfile.profile?.encoding, - responseTimeout: cachedProfile.profile?.responseTimeout, - }); - statusMsg.dispose(); - return response; - } catch (err) { - await errorHandling(err, this.getProfileName()); - throw err; + if (Profiles.getInstance().validProfile !== Validation.ValidationType.INVALID) { + try { + // Add document name to recently-opened files + ussFileProvider.addFileHistory(`[${this.getProfile().name}]: ${this.fullPath}`); + ussFileProvider.getTreeView().reveal(this, { select: true, focus: true, expand: false }); + const statusMsg = Gui.setStatusBarMessage(vscode.l10n.t("$(sync~spin) Downloading USS file...")); + await this.initializeFileOpening(download ? this.resourceUri.with({ query: "redownload=true" }) : this.resourceUri); + statusMsg.dispose(); + } catch (err) { + await errorHandling(err, this.getProfileName()); + throw err; + } } } @@ -567,63 +539,29 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { // This is not a UI refresh. public async refreshUSS(): Promise { ZoweLogger.trace("ZoweUSSNode.refreshUSS called."); - let label: string; switch (true) { case contextually.isUssDirectory(this.getParent()): - label = this.fullPath; break; // For favorited and non-favorited files case this.getParent().contextValue === globals.FAV_PROFILE_CONTEXT: case contextually.isUssSession(this.getParent()): - label = this.label.toString(); break; default: Gui.errorMessage(vscode.l10n.t("refreshUSS() called from invalid node.")); throw Error(vscode.l10n.t("refreshUSS() called from invalid node.")); } - try { - const ussDocumentFilePath = this.getUSSDocumentFilePath(); - const isDirty = this.isDirtyInEditor; - let wasSaved = false; - - if (isDirty) { - attachRecentSaveListener(); - - Gui.showTextDocument(this.openedDocumentInstance); - await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); - wasSaved = getRecentSaveStatus(); - disposeRecentSaveListener(); - } - - if ((isDirty && !this.isDirtyInEditor && !wasSaved) || !isDirty) { - const cachedProfile = Profiles.getInstance().loadNamedProfile(this.getProfileName()); - await autoDetectEncoding(this, cachedProfile); - - const response = await ZoweExplorerApiRegister.getUssApi(cachedProfile).getContents(this.fullPath, { - file: ussDocumentFilePath, - binary: this.binary, - returnEtag: true, - encoding: this.encoding !== undefined ? this.encoding : cachedProfile?.profile?.encoding, - responseTimeout: cachedProfile?.profile?.responseTimeout, - }); - this.setEtag(response.apiResponse.etag); - this.downloaded = true; - - if (isDirty) { - await this.initializeFileOpening(ussDocumentFilePath, true); - } - } else if (wasSaved) { - await this.initializeFileOpening(ussDocumentFilePath, true); - } + try { + await UssFSProvider.instance.fetchFileAtUri(this.resourceUri); + this.downloaded = true; } catch (err) { if (err instanceof Error && err.message.includes(vscode.l10n.t("not found"))) { ZoweLogger.warn(err.toString()); Gui.showMessage( vscode.l10n.t({ - message: "Unable to find file: {0} was probably deleted.", - args: [label], - comment: ["Label"], + message: "Unable to find file: {0}", + args: [err.message], + comment: ["Error message"], }) ); } else { @@ -632,40 +570,27 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { } } - public async initializeFileOpening(documentPath: string, previewFile?: boolean): Promise { + public async initializeFileOpening(uri?: vscode.Uri): Promise { ZoweLogger.trace("ZoweUSSNode.initializeFileOpening called."); - let document; - let openingTextFailed = false; - - if (!this.binary) { - try { - document = await vscode.workspace.openTextDocument(documentPath); - } catch (err) { - ZoweLogger.warn(err); - openingTextFailed = true; - } - - if (openingTextFailed) { - const yesResponse = vscode.l10n.t("Re-download"); - const noResponse = vscode.l10n.t("Cancel"); - - const response = await Gui.errorMessage(vscode.l10n.t("Failed to open file as text. Re-download file as binary?"), { - items: [yesResponse, noResponse], - }); + if (uri == null) { + ZoweLogger.trace("ZoweUSSNode.initializeFileOpening called with invalid URI, exiting..."); + return; + } - if (response === yesResponse) { - await vscode.commands.executeCommand("zowe.uss.binary", this); - } + const urlQuery = new URLSearchParams(uri.query); + try { + if (urlQuery.has("redownload")) { + // if the encoding has changed, fetch the contents with the new encoding + await UssFSProvider.instance.fetchFileAtUri(uri); + await vscode.commands.executeCommand("vscode.open", uri.with({ query: "" })); + await UssFSProvider.revertFileInEditor(); } else { - if (previewFile === true) { - await Gui.showTextDocument(document); - } else { - await Gui.showTextDocument(document, { preview: false }); - } + await vscode.commands.executeCommand("vscode.open", uri); } - } else { - const uriPath = vscode.Uri.file(documentPath); - await vscode.commands.executeCommand("vscode.open", uriPath); + + this.downloaded = true; + } catch (err) { + ZoweLogger.warn(err); } } @@ -686,72 +611,23 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { * @param ussApi The USS API to use for this operation */ public async paste( - sessionName: string, - rootPath: string, - uss: { tree: UssFileTree; api: MainframeInteraction.IUss; options?: zosfiles.IUploadOptions } + destUri: vscode.Uri, + uss: { tree: UssFileTree; api?: MainframeInteraction.IUss; options?: zosfiles.IUploadOptions } ): Promise { ZoweLogger.trace("ZoweUSSNode.paste called."); - const hasCopyApi = uss.api.copy != null; - const hasPutContentApi = uss.api.putContent != null; - if (!uss.api.fileList || (!hasCopyApi && !hasPutContentApi)) { - throw new Error(vscode.l10n.t("Required API functions for pasting (fileList, copy and/or putContent) were not found.")); - } - - const apiResponse = await uss.api.fileList(rootPath); - const fileList = apiResponse.apiResponse?.items; - - // Check root path for conflicts before pasting nodes in this path - let fileName = uss.tree.baseName; - if (fileList?.find((file) => file.name === fileName) != null) { - // If file names match, build the copy suffix - let dupCount = 1; - const extension = path.extname(uss.tree.baseName); - const baseNameForFile = path.parse(uss.tree.baseName)?.name; - let dupName = `${baseNameForFile} (${dupCount})${extension}`; - while (fileList.find((file) => file.name === dupName) != null) { - dupCount++; - dupName = `${baseNameForFile} (${dupCount})${extension}`; - } - fileName = dupName; + if (!uss.api) { + ZoweLogger.trace("\terror: paste called with invalid API"); + return; } - const outputPath = `${rootPath}/${fileName}`; - - if (hasCopyApi && UssFileUtils.toSameSession(uss.tree, sessionName)) { - await uss.api.copy(outputPath, { - from: uss.tree.ussPath, - recursive: uss.tree.type === UssFileType.Directory, - }); - } else { - const existsLocally = fs.existsSync(uss.tree.localPath); - switch (uss.tree.type) { - case UssFileType.Directory: - if (!existsLocally) { - // We will need to build the file structure locally, to pull files down if needed - fs.mkdirSync(uss.tree.localPath, { recursive: true }); - } - // Not all APIs respect the recursive option, so it's best to - // recurse within this operation to avoid missing files/folders - await uss.api.create(outputPath, "directory"); - if (uss.tree.children) { - for (const child of uss.tree.children) { - await this.paste(sessionName, outputPath, { api: uss.api, tree: child, options: uss.options }); - } - } - break; - case UssFileType.File: - if (!existsLocally) { - await uss.api.getContents(uss.tree.ussPath, { - file: uss.tree.localPath, - binary: uss.tree.binary, - returnEtag: true, - encoding: this.profile.profile?.encoding, - responseTimeout: this.profile.profile?.responseTimeout, - }); - } - await uss.api.putContent(uss.tree.localPath, outputPath, uss.options); - break; - } + const hasCopy = uss.api.copy != null; + const hasUploadFromBuffer = uss.api.uploadFromBuffer != null; + if (!uss.api.fileList || (!hasCopy && !hasUploadFromBuffer)) { + throw new Error(vscode.l10n.t("Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found.")); } + + await UssFSProvider.instance.copy(uss.tree.localUri.with({ query: `tree=${encodeURIComponent(JSON.stringify(uss.tree))}` }), destUri, { + overwrite: true, + }); } /** @@ -768,7 +644,6 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { try { const fileTreeToPaste: UssFileTree = JSON.parse(clipboardContents); const api = ZoweExplorerApiRegister.getUssApi(this.profile); - const sessionName = this.getSessionNode().getLabel() as string; const task: imperative.ITaskWithStatus = { percentComplete: 0, @@ -782,52 +657,16 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { }; for (const subnode of fileTreeToPaste.children) { - await this.paste(sessionName, this.fullPath, { api, tree: subnode, options }); + await this.paste( + vscode.Uri.from({ + scheme: ZoweScheme.USS, + path: `/${this.profile.name}${this.fullPath}`, + }), + { api, tree: subnode, options } + ); } } catch (error) { await errorHandling(error, this.label.toString(), vscode.l10n.t("Error uploading files")); } } } - -let wasSavedRecently = false; -let saveListener = null; - -/** - * Helper function which sets up listener for save wiping out the data after certain delay to prevent the fact of second save - * @param wipeOutTime {number} - */ -export function attachRecentSaveListener(wipeOutTime: number = 500): void { - ZoweLogger.trace("ZoweUSSNode.attachRecentSaveListener called."); - if (saveListener) { - saveListener.dispose(); - } - - saveListener = vscode.workspace.onDidSaveTextDocument(() => { - wasSavedRecently = true; - - setTimeout(() => { - wasSavedRecently = false; - }, wipeOutTime); - }); -} - -/** - * Helper function which returns saved save flag - * - * @returns {boolean} - */ -export function getRecentSaveStatus(): boolean { - ZoweLogger.trace("ZoweUSSNode.getRecentSaveStatus called."); - return wasSavedRecently; -} - -/** - * Helper function which disposes recent save listener - */ -export function disposeRecentSaveListener(): void { - ZoweLogger.trace("ZoweUSSNode.disposeRecentSaveListener called."); - if (saveListener) { - saveListener.dispose(); - } -} diff --git a/packages/zowe-explorer/src/uss/actions.ts b/packages/zowe-explorer/src/uss/actions.ts index f68babcc4c..90cb691937 100644 --- a/packages/zowe-explorer/src/uss/actions.ts +++ b/packages/zowe-explorer/src/uss/actions.ts @@ -13,17 +13,15 @@ import * as vscode from "vscode"; import * as fs from "fs"; import * as globals from "../globals"; import * as path from "path"; -import { concatChildNodes, uploadContent, getSelectedNodeList } from "../shared/utils"; +import { getSelectedNodeList } from "../shared/utils"; import { errorHandling } from "../utils/ProfilesUtils"; import { Gui, imperative, Validation, IZoweUSSTreeNode, Types } from "@zowe/zowe-explorer-api"; import { Profiles } from "../Profiles"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; import { isBinaryFileSync } from "isbinaryfile"; import * as contextually from "../shared/context"; -import { markDocumentUnsaved, setFileSaved } from "../utils/workspace"; import { refreshAll } from "../shared/refresh"; import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; -import { autoDetectEncoding, fileExistsCaseSensitiveSync } from "./utils"; import { UssFileTree, UssFileType } from "./FileStructure"; import { ZoweLogger } from "../utils/ZoweLogger"; import { AttributeView } from "./AttributeView"; @@ -70,7 +68,13 @@ export async function createUSSNode( if (name && filePath) { try { filePath = `${filePath}/${name}`; + const uri = node.resourceUri.with({ path: path.posix.join(node.resourceUri.path, name) }); await ZoweExplorerApiRegister.getUssApi(node.getProfile()).create(filePath, nodeType); + if (nodeType === "file") { + await vscode.workspace.fs.writeFile(uri, new Uint8Array()); + } else { + await vscode.workspace.fs.createDirectory(uri); + } if (isTopLevel) { await refreshAll(ussFileProvider); } else { @@ -79,17 +83,6 @@ export async function createUSSNode( const newNode = await node.getChildren().then((children) => children.find((child) => child.label === name)); await ussFileProvider.getTreeView().reveal(node, { select: true, focus: true }); ussFileProvider.getTreeView().reveal(newNode, { select: true, focus: true }); - const localPath = `${node.getUSSDocumentFilePath()}/${name}`; - const fileExists = fs.existsSync(localPath); - if (fileExists && !fileExistsCaseSensitiveSync(localPath)) { - Gui.showMessage( - vscode.l10n.t( - `There is already a file with the same name. - Please change your OS file system settings if you want to give case sensitive file names.` - ) - ); - ussFileProvider.refreshElement(node); - } } catch (err) { if (err instanceof Error) { await errorHandling(err, node.getProfileName(), vscode.l10n.t("Unable to create node:")); @@ -171,7 +164,7 @@ export async function uploadDialog(node: IZoweUSSTreeNode, ussFileProvider: Type } }) ); - ussFileProvider.refresh(); + ussFileProvider.refreshElement(node); } export async function uploadBinaryFile(node: IZoweUSSTreeNode, filePath: string): Promise { @@ -224,113 +217,6 @@ export function copyPath(node: IZoweUSSTreeNode): void { vscode.env.clipboard.writeText(node.fullPath); } -function findEtag(node: IZoweUSSTreeNode, directories: Array, index: number): boolean { - if (node === undefined || directories.indexOf(node.label.toString().trim()) === -1) { - return false; - } - if (directories.indexOf(node.label.toString().trim()) === directories.length - 1) { - return node.getEtag() !== ""; - } - - let flag: boolean = false; - for (const child of node.children) { - flag = flag || findEtag(child, directories, directories.indexOf(node.label.toString().trim()) + 1); - } - return flag; -} - -/** - * Uploads the file to the mainframe - * - * @export - * @param {Session} session - Desired session - * @param {vscode.TextDocument} doc - TextDocument that is being saved - */ -export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: Types.IZoweUSSTreeType): Promise { - ZoweLogger.trace("uss.actions.saveUSSFile called."); - ZoweLogger.debug( - vscode.l10n.t({ - message: "save requested for USS file {0}", - args: [doc.fileName], - comment: ["Document file name"], - }) - ); - const start = path.join(globals.USS_DIR + path.sep).length; - const ending = doc.fileName.substring(start); - const sesName = ending.substring(0, ending.indexOf(path.sep)); - const profile = Profiles.getInstance().loadNamedProfile(sesName); - if (!profile) { - const sessionError = vscode.l10n.t("Could not locate session when saving USS file."); - ZoweLogger.error(sessionError); - await Gui.errorMessage(sessionError); - return; - } - - const remote = ending.substring(sesName.length).replace(/\\/g, "/"); - const directories = doc.fileName.split(path.sep).splice(doc.fileName.split(path.sep).indexOf("_U_") + 1); - directories.splice(1, 2); - const profileSesnode: IZoweUSSTreeNode = ussFileProvider.mSessionNodes.find((child) => child.label.toString().trim() === sesName); - const etagProfiles = findEtag(profileSesnode, directories, 0); - const favoritesSesNode: IZoweUSSTreeNode = ussFileProvider.mFavorites.find((child) => child.label.toString().trim() === sesName); - const etagFavorites = findEtag(favoritesSesNode, directories, 0); - - // get session from session name - let sesNode: IZoweUSSTreeNode; - if ((etagProfiles && etagFavorites) || etagProfiles) { - sesNode = profileSesnode; - } else if (etagFavorites) { - sesNode = favoritesSesNode; - } - // Get specific node based on label and parent tree (session / favorites) - const nodes: IZoweUSSTreeNode[] = concatChildNodes(sesNode ? [sesNode] : ussFileProvider.mSessionNodes); - const node: IZoweUSSTreeNode = - nodes.find((zNode) => { - if (contextually.isText(zNode)) { - return zNode.fullPath.trim() === remote; - } else { - return false; - } - }) ?? ussFileProvider.openFiles?.[doc.uri.fsPath]; - - // define upload options - const etagToUpload = node?.getEtag(); - const returnEtag = etagToUpload != null; - - const prof = node?.getProfile() ?? profile; - try { - await autoDetectEncoding(node, prof); - - const uploadResponse: zosfiles.IZosFilesResponse = await Gui.withProgress( - { - location: vscode.ProgressLocation.Window, - title: vscode.l10n.t("Saving file..."), - }, - () => { - return uploadContent(node, doc, remote, prof, etagToUpload, returnEtag); - } - ); - if (uploadResponse.success) { - Gui.setStatusBarMessage(uploadResponse.commandResponse, globals.STATUS_BAR_TIMEOUT_MS); - // set local etag with the new etag from the updated file on mainframe - node?.setEtag(uploadResponse.apiResponse.etag); - setFileSaved(true); - // this part never runs! zowe.Upload.fileToUSSFile doesn't return success: false, it just throws the error which is caught below!!!!! - } else { - await markDocumentUnsaved(doc); - Gui.errorMessage(uploadResponse.commandResponse); - } - } catch (err) { - // TODO: error handling must not be zosmf specific - const errorMessage = err ? err.message : err.toString(); - if (errorMessage.includes("Rest API failure with HTTP(S) status 412")) { - await LocalFileManagement.compareSavedFileContent(doc, node, remote, prof); - } else { - await markDocumentUnsaved(doc); - await errorHandling(err, sesName); - } - } -} - export async function deleteUSSFilesPrompt(nodes: IZoweUSSTreeNode[]): Promise { ZoweLogger.trace("uss.actions.deleteUSSFilesPrompt called."); const fileNames = nodes.reduce((label, currentVal) => { @@ -367,7 +253,7 @@ export async function buildFileStructure(node: IZoweUSSTreeNode): Promise { ZoweLogger.trace("init.initUSSProvider called."); + + context.subscriptions.push(vscode.workspace.registerFileSystemProvider(ZoweScheme.USS, UssFSProvider.instance, { isCaseSensitive: true })); const ussFileProvider: USSTree = await createUSSTree(globals.LOG); if (ussFileProvider == null) { return null; @@ -58,7 +61,20 @@ export async function initUSSProvider(context: vscode.ExtensionContext): Promise let selectedNodes = getSelectedNodeList(node, nodeList) as IZoweUSSTreeNode[]; selectedNodes = selectedNodes.filter((x) => contextuals.isDocument(x)); for (const item of selectedNodes) { - await item.refreshUSS(); + if (contextuals.isUssDirectory(item)) { + // just refresh item to grab latest files + ussFileProvider.refreshElement(item); + } else { + if (!(await confirmForUnsavedDoc(node.resourceUri))) { + return; + } + const statusMsg = Gui.setStatusBarMessage("$(sync~spin) Fetching USS file..."); + // need to pull content for file and apply to FS entry + await UssFSProvider.instance.fetchFileAtUri(item.resourceUri, { + editor: vscode.window.visibleTextEditors.find((v) => v.document.uri.path === item.resourceUri.path), + }); + statusMsg.dispose(); + } } }) ); @@ -75,14 +91,11 @@ export async function initUSSProvider(context: vscode.ExtensionContext): Promise }) ); context.subscriptions.push( - vscode.commands.registerCommand("zowe.uss.fullPath", (node: IZoweUSSTreeNode): Promise => ussFileProvider.filterPrompt(node)) + vscode.commands.registerCommand("zowe.uss.fullPath", async (node: IZoweUSSTreeNode): Promise => ussFileProvider.filterPrompt(node)) ); context.subscriptions.push( vscode.commands.registerCommand("zowe.uss.editSession", async (node) => ussFileProvider.editSession(node, ussFileProvider)) ); - context.subscriptions.push( - vscode.commands.registerCommand("zowe.uss.ZoweUSSNode.open", async (node: IZoweUSSTreeNode) => node.openUSS(false, true, ussFileProvider)) - ); context.subscriptions.push( vscode.commands.registerCommand("zowe.uss.removeSession", async (node: IZoweUSSTreeNode, nodeList, hideFromAllTrees: boolean) => { let selectedNodes = getSelectedNodeList(node, nodeList); @@ -108,13 +121,17 @@ export async function initUSSProvider(context: vscode.ExtensionContext): Promise let selectedNodes = getSelectedNodeList(node, nodeList) as IZoweUSSTreeNode[]; selectedNodes = selectedNodes.filter((x) => contextuals.isDocument(x) || contextuals.isUssDirectory(x)); const cancelled = await ussActions.deleteUSSFilesPrompt(selectedNodes); + if (cancelled) { + return; + } + for (const item of selectedNodes) { - await item.deleteUSSNode(ussFileProvider, item.getUSSDocumentFilePath(), cancelled); + await item.deleteUSSNode(ussFileProvider, ""); } }) ); context.subscriptions.push( - vscode.commands.registerCommand("zowe.uss.renameNode", (node: IZoweUSSTreeNode): Promise => ussFileProvider.rename(node)) + vscode.commands.registerCommand("zowe.uss.renameNode", async (node: IZoweUSSTreeNode): Promise => ussFileProvider.rename(node)) ); context.subscriptions.push( vscode.commands.registerCommand("zowe.uss.uploadDialog", async (node: IZoweUSSTreeNode) => ussActions.uploadDialog(node, ussFileProvider)) @@ -129,16 +146,21 @@ export async function initUSSProvider(context: vscode.ExtensionContext): Promise ) ); context.subscriptions.push( - vscode.commands.registerCommand( - "zowe.uss.saveSearch", - (node: IZoweUSSTreeNode): Promise => ussFileProvider.saveSearch(node) - ) + vscode.commands.registerCommand("zowe.uss.saveSearch", async (node: IZoweUSSTreeNode): Promise => { + await ussFileProvider.saveSearch(node); + }) ); context.subscriptions.push( - vscode.commands.registerCommand("zowe.uss.removeSavedSearch", (node: IZoweUSSTreeNode): Promise => ussFileProvider.removeFavorite(node)) + vscode.commands.registerCommand( + "zowe.uss.removeSavedSearch", + async (node: IZoweUSSTreeNode): Promise => ussFileProvider.removeFavorite(node) + ) ); context.subscriptions.push( - vscode.commands.registerCommand("zowe.uss.removeFavProfile", (node): Promise => ussFileProvider.removeFavProfile(node.label, true)) + vscode.commands.registerCommand( + "zowe.uss.removeFavProfile", + async (node): Promise => ussFileProvider.removeFavProfile(node.label, true) + ) ); context.subscriptions.push( vscode.commands.registerCommand("zowe.uss.disableValidation", (node) => { @@ -153,10 +175,10 @@ export async function initUSSProvider(context: vscode.ExtensionContext): Promise }) ); context.subscriptions.push( - vscode.commands.registerCommand("zowe.uss.ssoLogin", (node: IZoweTreeNode): Promise => ussFileProvider.ssoLogin(node)) + vscode.commands.registerCommand("zowe.uss.ssoLogin", async (node: IZoweTreeNode): Promise => ussFileProvider.ssoLogin(node)) ); context.subscriptions.push( - vscode.commands.registerCommand("zowe.uss.ssoLogout", (node: IZoweTreeNode): Promise => ussFileProvider.ssoLogout(node)) + vscode.commands.registerCommand("zowe.uss.ssoLogout", async (node: IZoweTreeNode): Promise => ussFileProvider.ssoLogout(node)) ); context.subscriptions.push( vscode.commands.registerCommand("zowe.uss.pasteUssFile", async (node: IZoweUSSTreeNode) => { @@ -184,6 +206,15 @@ export async function initUSSProvider(context: vscode.ExtensionContext): Promise await ussFileProvider.onDidChangeConfiguration(e); }) ); + context.subscriptions.push( + vscode.workspace.onDidOpenTextDocument((doc) => { + if (doc.uri.scheme !== ZoweScheme.USS) { + return; + } + + UssFSProvider.instance.cacheOpenedUri(doc.uri); + }) + ); context.subscriptions.push(vscode.workspace.onDidCloseTextDocument(USSTree.onDidCloseTextDocument)); initSubscribers(context, ussFileProvider); diff --git a/packages/zowe-explorer/src/uss/utils.ts b/packages/zowe-explorer/src/uss/utils.ts index df83b55cd2..32d4749904 100644 --- a/packages/zowe-explorer/src/uss/utils.ts +++ b/packages/zowe-explorer/src/uss/utils.ts @@ -9,13 +9,22 @@ * */ -import * as path from "path"; -import * as fs from "fs"; import * as vscode from "vscode"; import type { ZoweUSSNode } from "./ZoweUSSNode"; import { ZoweLogger } from "../utils/ZoweLogger"; -import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; -import { imperative, IZoweUSSTreeNode } from "@zowe/zowe-explorer-api"; +import * as contextually from "../shared/context"; +import { ZosEncoding } from "@zowe/zowe-explorer-api"; + +export function zosEncodingToString(encoding: ZosEncoding): string { + switch (encoding.kind) { + case "binary": + return vscode.l10n.t("Binary"); + case "other": + return encoding.codepage; + case "text": + return null; + } +} /** * Injects extra data to tooltip based on node status and other conditions @@ -36,38 +45,22 @@ export function injectAdditionalDataToTooltip(node: ZoweUSSNode, tooltip: string }); } - const encodingString = node.binary ? vscode.l10n.t("Binary") : node.encoding; - if (encodingString != null) { - tooltip += - " \n" + - vscode.l10n.t({ - message: "Encoding: {0}", - args: [encodingString], - comment: ["Encoding name"], - }); + if (!contextually.isUssDirectory(node)) { + const zosEncoding = node.getEncoding(); + const encodingString = zosEncoding ? zosEncodingToString(zosEncoding) : null; + if (encodingString != null) { + tooltip += + " \n" + + vscode.l10n.t({ + message: "Encoding: {0}", + args: [encodingString], + comment: ["Encoding name"], + }); + } } - return tooltip; } -/** - * Checks whether file already exists while case sensitivity taken into account - * @param filepath - * @returns {boolean} - */ -export function fileExistsCaseSensitiveSync(filepath: string): boolean { - ZoweLogger.trace("uss.utils.fileExistsCaseSensitveSync called."); - const dir = path.dirname(filepath); - if (dir === path.dirname(dir)) { - return true; - } - const filenames = fs.readdirSync(dir); - if (filenames.indexOf(path.basename(filepath)) === -1) { - return false; - } - return fileExistsCaseSensitiveSync(dir); -} - /** * Removes clipboard contents * @returns {void} @@ -76,21 +69,3 @@ export function disposeClipboardContents(): void { ZoweLogger.trace("uss.utils.disposeClipboardContents called."); vscode.env.clipboard.writeText(""); } - -export async function autoDetectEncoding(node: IZoweUSSTreeNode, profile?: imperative.IProfileLoaded): Promise { - if (node.binary || node.encoding !== undefined) { - return; - } - const ussApi = ZoweExplorerApiRegister.getUssApi(profile ?? node.getProfile()); - if (ussApi.getTag != null) { - const taggedEncoding = await ussApi.getTag(node.fullPath); - if (taggedEncoding === "binary" || taggedEncoding === "mixed") { - node.setEncoding({ kind: "binary" }); - } else { - node.setEncoding(taggedEncoding !== "untagged" ? { kind: "other", codepage: taggedEncoding } : undefined); - } - } else { - const isBinary = await ussApi.isFileTagBinOrAscii(node.fullPath); - node.setEncoding(isBinary ? { kind: "binary" } : undefined); - } -} diff --git a/packages/zowe-explorer/src/utils/LocalFileManagement.ts b/packages/zowe-explorer/src/utils/LocalFileManagement.ts index d99ed2d411..c5796b8ef2 100644 --- a/packages/zowe-explorer/src/utils/LocalFileManagement.ts +++ b/packages/zowe-explorer/src/utils/LocalFileManagement.ts @@ -10,73 +10,35 @@ */ import * as vscode from "vscode"; -import * as globals from "../globals"; import * as os from "os"; -import { IZoweDatasetTreeNode, IZoweTreeNode, IZoweUSSTreeNode, imperative } from "@zowe/zowe-explorer-api"; -import { markDocumentUnsaved } from "./workspace"; -import { isTypeUssTreeNode } from "../shared/context"; -import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; +import { IZoweTreeNode } from "@zowe/zowe-explorer-api"; import { ZoweLogger } from "./ZoweLogger"; -import { LocalFileInfo, isZoweDatasetTreeNode, isZoweUSSTreeNode } from "../shared/utils"; -import { ZoweDatasetNode } from "../dataset/ZoweDatasetNode"; -import { ZoweUSSNode } from "../uss/ZoweUSSNode"; export class LocalFileManagement { + public static filesToCompare: IZoweTreeNode[] = []; + public static fileSelectedToCompare: boolean = false; + public static getDefaultUri(): vscode.Uri { return vscode.workspace.workspaceFolders?.[0]?.uri ?? vscode.Uri.file(os.homedir()); } - /** - * Function that triggers compare of the old and new document in the active editor - * @param {vscode.TextDocument} doc - document to update and compare with previous content - * @param {IZoweDatasetTreeNode | IZoweUSSTreeNode} node - IZoweTreeNode - * @param {string} label - {optional} used by IZoweDatasetTreeNode to getContents of file - * @param {boolean} binary - {optional} used by IZoweUSSTreeNode to getContents of file - * @param {imperative.IProfileLoaded} profile - {optional} - * @returns {Promise} - */ - public static async compareSavedFileContent( - doc: vscode.TextDocument, - node: IZoweDatasetTreeNode | IZoweUSSTreeNode, - label?: string, - profile?: imperative.IProfileLoaded - ): Promise { - await markDocumentUnsaved(doc); - const prof = node ? node.getProfile() : profile; - let downloadResponse; - if (isTypeUssTreeNode(node)) { - downloadResponse = await ZoweExplorerApiRegister.getUssApi(prof).getContents(node.fullPath, { - file: node.getUSSDocumentFilePath(), - binary: node.binary, - returnEtag: true, - encoding: node.encoding !== undefined ? node.encoding : prof.profile?.encoding, - responseTimeout: prof.profile?.responseTimeout, - }); - } else { - downloadResponse = await ZoweExplorerApiRegister.getMvsApi(prof).getContents(label, { - file: doc.fileName, - binary: node.binary, - returnEtag: true, - encoding: node.encoding !== undefined ? node.encoding : prof.profile?.encoding, - responseTimeout: prof.profile?.responseTimeout, - }); - } - ZoweLogger.warn(vscode.l10n.t("Remote file has changed. Presenting with way to resolve file.")); - vscode.commands.executeCommand("workbench.files.action.compareWithSaved"); - // re-assign etag, so that it can be used with subsequent requests - const downloadEtag = downloadResponse?.apiResponse?.etag; - if (node && downloadEtag !== node.getEtag()) { - node.setEtag(downloadEtag); - } + public static setCompareSelection(val: boolean): void { + LocalFileManagement.fileSelectedToCompare = val; + vscode.commands.executeCommand("setContext", "zowe.compareFileStarted", val); + } + + public static resetCompareSelection(): void { + LocalFileManagement.filesToCompare = []; + LocalFileManagement.setCompareSelection(false); } public static selectFileForCompare(node: IZoweTreeNode): void { - if (globals.filesToCompare.length > 0) { - globals.resetCompareChoices(); + if (LocalFileManagement.filesToCompare.length > 0) { + LocalFileManagement.resetCompareSelection(); } - globals.filesToCompare.push(node); - globals.setCompareSelection(true); - ZoweLogger.trace(`${String(globals.filesToCompare[0].label)} selected for compare.`); + LocalFileManagement.filesToCompare.push(node); + LocalFileManagement.setCompareSelection(true); + ZoweLogger.trace(`${String(LocalFileManagement.filesToCompare[0].label)} selected for compare.`); } /** @@ -84,45 +46,14 @@ export class LocalFileManagement { * @returns {Promise} */ public static async compareChosenFileContent(node: IZoweTreeNode, readOnly = false): Promise { - globals.filesToCompare.push(node); - const docUriArray: vscode.Uri[] = []; - for (const file of globals.filesToCompare) { - const fileInfo = await this.getCompareFilePaths(file); - if (fileInfo.path) { - docUriArray.push(vscode.Uri.file(fileInfo.path)); - } else { - return; - } - } - globals.resetCompareChoices(); + LocalFileManagement.filesToCompare.push(node); + const docUriArray: vscode.Uri[] = LocalFileManagement.filesToCompare.map((n) => n.resourceUri); + LocalFileManagement.resetCompareSelection(); if (docUriArray.length === 2) { - vscode.commands.executeCommand("vscode.diff", docUriArray[0], docUriArray[1]); + await vscode.commands.executeCommand("vscode.diff", docUriArray[0], docUriArray[1]); if (readOnly) { - this.readOnlyFile(); + vscode.commands.executeCommand("workbench.action.files.setActiveEditorReadonlyInSession"); } } } - - private static async getCompareFilePaths(node: IZoweTreeNode): Promise { - ZoweLogger.info(`Getting files ${String(globals.filesToCompare[0].label)} and ${String(globals.filesToCompare[1].label)} for comparison.`); - let fileInfo = {} as LocalFileInfo; - switch (true) { - case isZoweDatasetTreeNode(node): { - fileInfo = await (node as ZoweDatasetNode).downloadDs(true); - break; - } - case isZoweUSSTreeNode(node): { - fileInfo = await (node as ZoweUSSNode).downloadUSS(true); - break; - } - default: { - ZoweLogger.warn(vscode.l10n.t("Something went wrong with compare of files.")); - } - } - return fileInfo; - } - - private static readOnlyFile(): void { - vscode.commands.executeCommand("workbench.action.files.setActiveEditorReadonlyInSession"); - } } diff --git a/packages/zowe-explorer/src/utils/ProfileManagement.ts b/packages/zowe-explorer/src/utils/ProfileManagement.ts index 3c495881b3..0beddfcfeb 100644 --- a/packages/zowe-explorer/src/utils/ProfileManagement.ts +++ b/packages/zowe-explorer/src/utils/ProfileManagement.ts @@ -17,7 +17,6 @@ import { ProfilesUtils } from "./ProfilesUtils"; import { Profiles } from "../Profiles"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; import { getSessionType } from "../shared/context"; -import { TreeProviders } from "../shared/TreeProviders"; export class ProfileManagement { public static getRegisteredProfileNameList(registeredTree: globals.Trees): string[] { diff --git a/packages/zowe-explorer/src/utils/ProfilesUtils.ts b/packages/zowe-explorer/src/utils/ProfilesUtils.ts index f8626ce63b..06dc16050e 100644 --- a/packages/zowe-explorer/src/utils/ProfilesUtils.ts +++ b/packages/zowe-explorer/src/utils/ProfilesUtils.ts @@ -17,6 +17,7 @@ import * as path from "path"; import * as fs from "fs"; import * as util from "util"; import { IZoweTreeNode, ZoweTreeNode, FileManagement, Gui, ProfilesCache, MainframeInteraction, imperative } from "@zowe/zowe-explorer-api"; +import * as contextually from "../shared/context"; import { ZoweLogger } from "./ZoweLogger"; import { SettingsConfig } from "./SettingsConfig"; import { TreeProviders } from "../shared/TreeProviders"; @@ -102,6 +103,10 @@ export async function errorHandling(errorDetails: Error | string, label?: string Gui.errorMessage(moreInfo + errorDetails.toString().replace(/\n/g, " | ")); } +export function getSessionLabel(node: IZoweTreeNode): string { + return (contextually.isSession(node) ? node : node.getSessionNode()).label as string; +} + /** * Function to update session and profile information in provided node * @param profiles is data source to find profiles diff --git a/packages/zowe-explorer/src/utils/SettingsConfig.ts b/packages/zowe-explorer/src/utils/SettingsConfig.ts index 26dccc9915..f4b248211c 100644 --- a/packages/zowe-explorer/src/utils/SettingsConfig.ts +++ b/packages/zowe-explorer/src/utils/SettingsConfig.ts @@ -115,10 +115,6 @@ export class SettingsConfig { return Object.keys(SettingsConfig.configurations).filter((key) => key.match(new RegExp("Zowe-*|Zowe\\s*", "g"))); } - private static get currentVersionNumber(): unknown { - return vscode.extensions.getExtension("zowe.vscode-extension-for-zowe").packageJSON.version as unknown; - } - private static async promptReload(): Promise { // Prompt user to reload VS Code window const reloadButton = vscode.l10n.t("Reload Window"); diff --git a/packages/zowe-explorer/src/utils/workspace.ts b/packages/zowe-explorer/src/utils/workspace.ts deleted file mode 100644 index c572cf62ae..0000000000 --- a/packages/zowe-explorer/src/utils/workspace.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - * - */ - -import * as vscode from "vscode"; -import { - workspaceUtilTabSwitchDelay, - workspaceUtilMaxEmptyWindowsInTheRow, - workspaceUtilFileSaveInterval, - workspaceUtilFileSaveMaxIterationCount, -} from "../config/constants"; - -interface IExtTextEditor extends vscode.TextEditor { - id: string; -} - -/** - * Opens the next tab in editor with given delay - */ -function openNextTab(delay: number): Promise { - return new Promise((resolve) => { - vscode.commands.executeCommand("workbench.action.nextEditor"); - setTimeout(() => resolve(), delay); - }); -} - -let fileWasSaved = false; - -export function setFileSaved(status: boolean): void { - fileWasSaved = status; -} - -export async function awaitForDocumentBeingSaved(): Promise { - fileWasSaved = false; - return new Promise((resolve) => { - let count = 0; - const saveWaitIntervalId = setInterval(() => { - if (workspaceUtilFileSaveMaxIterationCount > count) { - count++; - if (fileWasSaved) { - fileWasSaved = false; - clearInterval(saveWaitIntervalId); - resolve(); - } - } else { - clearInterval(saveWaitIntervalId); - resolve(); - } - }, workspaceUtilFileSaveInterval); - }); -} - -/** - * Checks if file is opened using iteration through tabs - * This kind of method is caused by incompleteness of VSCode API, which allows to check only buffered status of files - * There's an issue on GitHub for such feature: https://github.com/Microsoft/vscode/issues/15178 let's track it - * Idea of the approach was borrowed from the another extension: https://github.com/eamodio/vscode-restore-editors/blob/master/src/documentManager.ts - * Also notice that timer delay as well as iteration through opened tabs can cause side-effects on slow machines - */ -export async function checkTextFileIsOpened(path: string): Promise { - const openedWindows = [] as IExtTextEditor[]; - - let emptySelectedCountInTheRow = 0; - let selectedEditor = vscode.window.activeTextEditor as IExtTextEditor; - - // The idea of the condition is we can meet binary files opened, which have no text editor - // So we should set some maximum occurrences point and get out of the loop - while ( - emptySelectedCountInTheRow < workspaceUtilMaxEmptyWindowsInTheRow && - !openedWindows.some((window) => selectedEditor && window.id === selectedEditor.id) - ) { - if (selectedEditor) { - emptySelectedCountInTheRow = 0; - openedWindows.push(selectedEditor); - } else { - emptySelectedCountInTheRow++; - } - - await openNextTab(workspaceUtilTabSwitchDelay); - selectedEditor = vscode.window.activeTextEditor as IExtTextEditor; - } - - return openedWindows.some((window) => window.document.fileName === path); -} - -/** - * Closes opened file tab using iteration through the tabs - * This kind of method is caused by incompleteness of VSCode API, which allows to close only currently selected editor - * For us it means we need to select editor first, which is again not possible via existing VSCode APIs - */ -export async function closeOpenedTextFile(path: string): Promise { - const openedWindows = [] as IExtTextEditor[]; - - let emptySelectedCountInTheRow = 0; - let selectedEditor = vscode.window.activeTextEditor as IExtTextEditor; - - // The idea of the condition is we can meet binary files opened, which have no text editor - // So we should set some maximum occurrences point and get out of the loop - while ( - emptySelectedCountInTheRow < workspaceUtilMaxEmptyWindowsInTheRow && - !openedWindows.some((window) => selectedEditor && window.id === selectedEditor.id) - ) { - if (selectedEditor) { - emptySelectedCountInTheRow = 0; - openedWindows.push(selectedEditor); - } else { - emptySelectedCountInTheRow++; - } - - await openNextTab(workspaceUtilTabSwitchDelay); - selectedEditor = vscode.window.activeTextEditor as IExtTextEditor; - - if (selectedEditor && selectedEditor.document.fileName === path) { - const isDirty = selectedEditor.document.isDirty; - await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); - if (isDirty) { - await awaitForDocumentBeingSaved(); - } - return true; - } - } - - return false; -} - -/** - * Mark a text document as dirty (unsaved) if contents failed to upload. - * Based on https://stackoverflow.com/questions/74224108 - */ -export async function markDocumentUnsaved(document: vscode.TextDocument): Promise { - const edits = new vscode.WorkspaceEdit(); - edits.insert(document.uri, new vscode.Position(0, 0), " "); - await vscode.workspace.applyEdit(edits); - - const edits2 = new vscode.WorkspaceEdit(); - edits2.delete(document.uri, new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1))); - await vscode.workspace.applyEdit(edits2); -} diff --git a/packages/zowe-explorer/tsconfig.json b/packages/zowe-explorer/tsconfig.json index e8259b3304..388007e878 100644 --- a/packages/zowe-explorer/tsconfig.json +++ b/packages/zowe-explorer/tsconfig.json @@ -10,7 +10,7 @@ /* Strict Type-Checking Option */ "strict": false /* enable all strict type-checking options */, /* Additional Checks */ - "noUnusedLocals": false /* Report errors on unused locals. */, + "noUnusedLocals": true /* Report errors on unused locals. */, // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 300f37b769..1e9ea77694 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^10.0.1 version: 10.0.4 '@types/node': - specifier: ^14.18.12 - version: 14.18.63 + specifier: ^18.19.14 + version: 18.19.31 '@types/vscode': specifier: ^1.73.0 version: 1.84.1 @@ -61,7 +61,7 @@ importers: version: 6.0.0 jest: specifier: ^29.3.1 - version: 29.7.0(@types/node@14.18.63)(ts-node@9.1.1) + version: 29.7.0(@types/node@18.19.31)(ts-node@9.1.1) jest-html-reporter: specifier: ^3.7.0 version: 3.10.2(jest@29.7.0)(typescript@5.3.3) @@ -155,18 +155,9 @@ importers: isbinaryfile: specifier: 4.0.4 version: 4.0.4 - js-yaml: - specifier: 3.13.1 - version: 3.13.1 promise-queue: specifier: 2.2.5 version: 2.2.5 - promise-status-async: - specifier: ^1.2.10 - version: 1.2.10 - yamljs: - specifier: 0.3.0 - version: 0.3.0 devDependencies: '@napi-rs/cli': specifier: ^2.16.1 @@ -177,9 +168,6 @@ importers: '@types/chai-as-promised': specifier: ^7.1.0 version: 7.1.8 - '@types/copy-webpack-plugin': - specifier: ^10.1.0 - version: 10.1.0(webpack@5.91.0) '@types/expect': specifier: ^1.20.3 version: 1.20.4 @@ -192,9 +180,6 @@ importers: '@types/selenium-webdriver': specifier: ^3.0.4 version: 3.0.26 - '@types/yargs': - specifier: ^11.0.0 - version: 11.1.8 chai: specifier: ^4.1.2 version: 4.3.10 @@ -213,6 +198,9 @@ importers: del: specifier: ^4.1.1 version: 4.1.1 + disposablestack: + specifier: ^1.1.4 + version: 1.1.4 eslint-plugin-zowe-explorer: specifier: 3.0.0-next-SNAPSHOT version: link:../eslint-plugin-zowe-explorer @@ -222,24 +210,18 @@ importers: expect: specifier: ^24.8.0 version: 24.9.0 - geckodriver: - specifier: ^1.19.1 - version: 1.22.3 glob: specifier: ^7.1.6 version: 7.2.3 - jsdom: - specifier: ^16.0.0 - version: 16.7.0 + jest-silent-reporter: + specifier: ^0.5.0 + version: 0.5.0 log4js: specifier: ^6.4.6 version: 6.9.1 markdownlint-cli: specifier: ^0.33.0 version: 0.33.0 - mem: - specifier: ^6.0.1 - version: 6.1.1 run-sequence: specifier: ^2.2.1 version: 2.2.1 @@ -350,7 +332,7 @@ importers: version: 5.3.3 vite: specifier: ^4.5.2 - version: 4.5.2(@types/node@14.18.63) + version: 4.5.2(@types/node@18.19.31) vite-plugin-checker: specifier: ^0.6.2 version: 0.6.2(eslint@8.54.0)(typescript@5.3.3)(vite@4.5.2) @@ -1372,7 +1354,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 14.18.63 + '@types/node': 18.19.31 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -1393,14 +1375,14 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 14.18.63 + '@types/node': 18.19.31 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@14.18.63)(ts-node@9.1.1) + jest-config: 29.7.0(@types/node@18.19.31)(ts-node@9.1.1) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -1428,7 +1410,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 14.18.63 + '@types/node': 18.19.31 jest-mock: 29.7.0 dev: true @@ -1455,7 +1437,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 14.18.63 + '@types/node': 18.19.31 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -1488,7 +1470,7 @@ packages: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.20 - '@types/node': 14.18.63 + '@types/node': 18.19.31 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -1595,6 +1577,17 @@ packages: '@types/yargs': 13.0.12 dev: true + /@jest/types@26.6.2: + resolution: {integrity: sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==} + engines: {node: '>= 10.14.2'} + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 18.19.31 + '@types/yargs': 15.0.19 + chalk: 4.1.2 + dev: true + /@jest/types@29.6.3: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1602,7 +1595,7 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 14.18.63 + '@types/node': 18.19.31 '@types/yargs': 17.0.32 chalk: 4.1.2 dev: true @@ -1843,7 +1836,7 @@ packages: debug: 4.3.4(supports-color@8.1.1) kolorist: 1.8.0 resolve: 1.22.8 - vite: 4.5.2(@types/node@14.18.63) + vite: 4.5.2(@types/node@18.19.31) transitivePeerDependencies: - preact - supports-color @@ -1877,7 +1870,7 @@ packages: '@prefresh/utils': 1.2.0 '@rollup/pluginutils': 4.2.1 preact: 10.19.2 - vite: 4.5.2(@types/node@14.18.63) + vite: 4.5.2(@types/node@18.19.31) transitivePeerDependencies: - supports-color dev: true @@ -2044,15 +2037,6 @@ packages: resolution: {integrity: sha512-of+ICnbqjmFCiixUnqRulbylyXQrPqIGf/B3Jax1wIF3DvSheysQxAWvqHhZiW3IQrycvokcLcFQlveGp+vyNg==} dev: true - /@types/copy-webpack-plugin@10.1.0(webpack@5.91.0): - resolution: {integrity: sha512-Dk0NUW3X6hVQdkH2n9R7NejjPNCocZBiv8XF8Ac5su2d6EKzCcG/yWDwnWGrEsAWvogoADJyUKULwncx0G9Jkg==} - deprecated: This is a stub types definition. copy-webpack-plugin provides its own type definitions, so you do not need this installed. - dependencies: - copy-webpack-plugin: 12.0.2(webpack@5.91.0) - transitivePeerDependencies: - - webpack - dev: true - /@types/eslint-scope@3.7.7: resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} dependencies: @@ -2085,13 +2069,13 @@ packages: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 14.18.63 + '@types/node': 18.19.31 dev: true /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: - '@types/node': 14.18.63 + '@types/node': 18.19.31 dev: true /@types/istanbul-lib-coverage@2.0.6: @@ -2132,12 +2116,6 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true - /@types/keyv@3.1.4: - resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - dependencies: - '@types/node': 14.18.63 - dev: true - /@types/lodash.isequal@4.5.8: resolution: {integrity: sha512-uput6pg4E/tj2LGxCZo9+y27JNyB2OZuuI/T5F+ylVDYuqICLG2/ktjxx0v6GvVntAf8TvEzeQLcV0ffRirXuA==} dependencies: @@ -2160,14 +2138,14 @@ packages: resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} dev: true - /@types/promise-queue@2.2.3: - resolution: {integrity: sha512-CuEQpGSYKvHr3SQ7C7WkluLg9CFjVORbn8YFRsQ5u6mqGbZVfSOv03ic9t95HtZuMchnlNqnIsQGFOpxqdhjTQ==} + /@types/node@18.19.31: + resolution: {integrity: sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==} + dependencies: + undici-types: 5.26.5 dev: true - /@types/responselike@1.0.3: - resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} - dependencies: - '@types/node': 14.18.63 + /@types/promise-queue@2.2.3: + resolution: {integrity: sha512-CuEQpGSYKvHr3SQ7C7WkluLg9CFjVORbn8YFRsQ5u6mqGbZVfSOv03ic9t95HtZuMchnlNqnIsQGFOpxqdhjTQ==} dev: true /@types/selenium-webdriver@3.0.26: @@ -2200,16 +2178,18 @@ packages: /@types/yargs-parser@21.0.3: resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - /@types/yargs@11.1.8: - resolution: {integrity: sha512-49Pmk3GBUOrs/ZKJodGMJeEeiulv2VdfAYpGgkTCSXpNWx7KCX36+PbrkItwzrjTDHO2QoEZDpbhFoMN1lxe9A==} - dev: true - /@types/yargs@13.0.12: resolution: {integrity: sha512-qCxJE1qgz2y0hA4pIxjBR+PelCH0U5CK1XJXFwCNqfmliatKp47UCXXE9Dyk1OXBDLvsCF57TqQEJaeLfDYEOQ==} dependencies: '@types/yargs-parser': 21.0.3 dev: true + /@types/yargs@15.0.19: + resolution: {integrity: sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==} + dependencies: + '@types/yargs-parser': 21.0.3 + dev: true + /@types/yargs@17.0.32: resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} dependencies: @@ -2870,10 +2850,6 @@ packages: '@zowe/imperative': 8.0.0-next.202404032038 dev: false - /abab@2.0.6: - resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} - dev: true - /abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} dev: true @@ -2883,13 +2859,6 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: false - /acorn-globals@6.0.0: - resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} - dependencies: - acorn: 7.4.1 - acorn-walk: 7.2.0 - dev: true - /acorn-import-assertions@1.9.0(acorn@8.11.2): resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} peerDependencies: @@ -2922,15 +2891,11 @@ packages: acorn: 8.11.2 dev: true - /acorn-walk@7.2.0: - resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} - engines: {node: '>=0.4.0'} - dev: true - /acorn@7.4.1: resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} engines: {node: '>=0.4.0'} hasBin: true + dev: false /acorn@8.11.2: resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} @@ -2944,11 +2909,6 @@ packages: hasBin: true dev: true - /adm-zip@0.5.3: - resolution: {integrity: sha512-zsoTXEwRNCxBzRHLENFLuecCcwzzXiEhWo1r3GP68iwi8Q/hW2RrqgeY1nfJ/AhNQNWnZq/4v0TbfMsUkI+TYw==} - engines: {node: '>=6.0'} - dev: true - /agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -3173,6 +3133,14 @@ packages: engines: {node: '>=0.10.0'} dev: true + /array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + dev: true + /array-from@2.1.1: resolution: {integrity: sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==} dev: true @@ -3208,6 +3176,20 @@ packages: engines: {node: '>=0.10.0'} dev: true + /arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + dev: true + /asn1@0.2.6: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} dependencies: @@ -3242,16 +3224,19 @@ packages: engines: {node: '>=8'} dev: false - /asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: true - /atob@2.1.2: resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} engines: {node: '>= 4.5.0'} hasBin: true dev: true + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: true + /azure-devops-node-api@11.2.0: resolution: {integrity: sha512-XdiGPhrpaT5J8wdERRKs5g8E0Zy1pvOYTli7z9E8nmOn3YGp4FhtjhrOyFmX/8veWCwdI69mCHKJw6l+4J/bHA==} dependencies: @@ -3398,10 +3383,6 @@ packages: resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} dev: true - /bluebird@3.7.2: - resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - dev: true - /boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} dev: true @@ -3449,10 +3430,6 @@ packages: dependencies: fill-range: 7.0.1 - /browser-process-hrtime@1.0.0: - resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} - dev: true - /browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} dev: true @@ -3567,10 +3544,21 @@ packages: resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} dependencies: function-bind: 1.1.2 - get-intrinsic: 1.2.2 + get-intrinsic: 1.2.4 set-function-length: 1.1.1 dev: true + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + dev: true + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -3593,11 +3581,6 @@ packages: resolution: {integrity: sha512-cjUJTQkk9fQlJR2s4HMuPMvTiRggl0rAVMtthQuyOlDWuqHXqN8azLq+pi8B2TjwKJ32diHjUqRIKeFX4z1FoA==} dev: true - /capture-stack-trace@1.0.2: - resolution: {integrity: sha512-X/WM2UQs6VMHUtjUDnZTRI+i1crWteJySFzr9UpGoQa4WQffXVTTXuekjl7TjZRlcF2XfjgITT0HxZ9RnxeT0w==} - engines: {node: '>=0.10.0'} - dev: true - /chai-as-promised@7.1.1(chai@4.3.10): resolution: {integrity: sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==} peerDependencies: @@ -3720,6 +3703,7 @@ packages: /chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} + dev: false /chrome-trace-event@1.0.3: resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} @@ -3861,13 +3845,6 @@ packages: engines: {node: '>=0.1.90'} dev: false - /combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - dependencies: - delayed-stream: 1.0.0 - dev: true - /commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -3990,14 +3967,7 @@ packages: dev: false optional: true - /create-error-class@3.0.2: - resolution: {integrity: sha512-gYTKKexFO3kh200H1Nit76sRwRtOY32vQd3jpAQKpLtZqyNsSQNfI4N7o3eP2wUjV35pTWKRYqFUDBvUha/Pkw==} - engines: {node: '>=0.10.0'} - dependencies: - capture-stack-trace: 1.0.2 - dev: true - - /create-jest@29.7.0(@types/node@14.18.63)(ts-node@9.1.1): + /create-jest@29.7.0(@types/node@18.19.31)(ts-node@9.1.1): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -4006,7 +3976,7 @@ packages: chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@14.18.63)(ts-node@9.1.1) + jest-config: 29.7.0(@types/node@18.19.31)(ts-node@9.1.1) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -4066,28 +4036,31 @@ packages: engines: {node: '>= 6'} dev: true - /cssom@0.3.8: - resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} - dev: true - - /cssom@0.4.4: - resolution: {integrity: sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==} + /data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 dev: true - /cssstyle@2.3.0: - resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} - engines: {node: '>=8'} + /data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} dependencies: - cssom: 0.3.8 + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 dev: true - /data-urls@2.0.0: - resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==} - engines: {node: '>=10'} + /data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} dependencies: - abab: 2.0.6 - whatwg-mimetype: 2.3.0 - whatwg-url: 8.7.0 + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 dev: true /dataobject-parser@1.2.25: @@ -4158,10 +4131,6 @@ packages: engines: {node: '>=10'} dev: true - /decimal.js@10.4.3: - resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} - dev: true - /decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} @@ -4218,9 +4187,27 @@ packages: resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} engines: {node: '>= 0.4'} dependencies: - get-intrinsic: 1.2.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: true + + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 gopd: 1.0.1 + dev: true + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 has-property-descriptors: 1.0.1 + object-keys: 1.1.1 dev: true /define-property@0.2.5: @@ -4258,11 +4245,6 @@ packages: rimraf: 2.7.1 dev: true - /delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - dev: true - /dependency-tree@9.0.0: resolution: {integrity: sha512-osYHZJ1fBSon3lNLw70amAXsQ+RGzXsPvk9HbBgTLbp/bQBmpH5mOmsUvqXU+YEWVU0ZLewsmzOET/8jWswjDQ==} engines: {node: ^10.13 || ^12 || >=14} @@ -4502,6 +4484,22 @@ packages: path-type: 4.0.0 dev: true + /disposablestack@1.1.4: + resolution: {integrity: sha512-h5QKYSeI6L9QxG2Oywp+srU6dpJUi+dnGJhEYZXUyp91Rz5JOvzHQZW/DNWOlAra1EoOZBVxr7U7dofOpDijPQ==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-set-tostringtag: 2.0.3 + get-intrinsic: 1.2.4 + globalthis: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + suppressed-error: 1.0.3 + dev: true + /doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} @@ -4520,13 +4518,6 @@ packages: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} dev: true - /domexception@2.0.1: - resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==} - engines: {node: '>=8'} - dependencies: - webidl-conversions: 5.0.0 - dev: true - /domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} @@ -4671,6 +4662,70 @@ packages: is-arrayish: 0.2.1 dev: true + /es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + dev: true + + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + dev: true + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: true + /es-module-lexer@1.4.1: resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} dev: true @@ -4679,6 +4734,31 @@ packages: resolution: {integrity: sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==} dev: true + /es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + dev: true + + /es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + /esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} engines: {node: '>=12'} @@ -5235,6 +5315,12 @@ packages: optional: true dev: true + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + /for-in@1.0.2: resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} engines: {node: '>=0.10.0'} @@ -5247,15 +5333,6 @@ packages: cross-spawn: 7.0.3 signal-exit: 4.1.0 - /form-data@3.0.1: - resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} - engines: {node: '>= 6'} - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - dev: true - /fragment-cache@0.2.1: resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} engines: {node: '>=0.10.0'} @@ -5312,6 +5389,7 @@ packages: engines: {node: '>= 8'} dependencies: minipass: 3.3.6 + dev: false /fs-minipass@3.0.3: resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} @@ -5352,22 +5430,22 @@ packages: /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + functions-have-names: 1.2.3 + dev: true + /functional-red-black-tree@1.0.1: resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} dev: false - /geckodriver@1.22.3: - resolution: {integrity: sha512-HJvImEC5m/2J7aIn+AdiZml1yTOSFZAb8h8lmZBSUgGSCPdNTd0/6YxBVBsvzpaTuaDQHbMUr+8ikaFKF+Sj/A==} - hasBin: true - requiresBuild: true - dependencies: - adm-zip: 0.5.3 - bluebird: 3.7.2 - got: 5.6.0 - https-proxy-agent: 5.0.0 - tar: 6.0.2 - transitivePeerDependencies: - - supports-color + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true /gensync@1.0.0-beta.2: @@ -5399,13 +5477,15 @@ packages: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: true - /get-intrinsic@1.2.2: - resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} dependencies: + es-errors: 1.3.0 function-bind: 1.1.2 has-proto: 1.0.1 has-symbols: 1.0.3 - hasown: 2.0.1 + hasown: 2.0.2 dev: true /get-own-enumerable-property-symbols@3.0.2: @@ -5432,6 +5512,15 @@ packages: engines: {node: '>=10'} dev: true + /get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + dev: true + /get-value@2.0.6: resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} engines: {node: '>=0.10.0'} @@ -5526,6 +5615,13 @@ packages: dependencies: type-fest: 0.20.2 + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + dev: true + /globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -5572,31 +5668,7 @@ packages: /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: - get-intrinsic: 1.2.2 - dev: true - - /got@5.6.0: - resolution: {integrity: sha512-MnypzkaW8dldA8AbJFjMs7y14+ykd2V8JCLKSvX1Gmzx1alH3Y+3LArywHDoAF2wS3pnZp4gacoYtvqBeF6drQ==} - engines: {node: '>=0.10.0'} - dependencies: - '@types/keyv': 3.1.4 - '@types/responselike': 1.0.3 - create-error-class: 3.0.2 - duplexer2: 0.1.4 - is-plain-obj: 1.1.0 - is-redirect: 1.0.0 - is-retry-allowed: 1.2.0 - is-stream: 1.1.0 - lowercase-keys: 1.0.1 - node-status-codes: 1.0.0 - object-assign: 4.1.1 - parse-json: 2.2.0 - pinkie-promise: 2.0.1 - read-all-stream: 3.1.0 - readable-stream: 2.3.8 - timed-out: 2.0.0 - unzip-response: 1.0.2 - url-parse-lax: 1.0.0 + get-intrinsic: 1.2.4 dev: true /graceful-fs@4.2.11: @@ -5626,6 +5698,10 @@ packages: ansi-regex: 2.1.1 dev: true + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -5642,7 +5718,13 @@ packages: /has-property-descriptors@1.0.1: resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} dependencies: - get-intrinsic: 1.2.2 + get-intrinsic: 1.2.4 + dev: true + + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 dev: true /has-proto@1.0.1: @@ -5650,11 +5732,23 @@ packages: engines: {node: '>= 0.4'} dev: true + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + dev: true + /has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} dev: true + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + /has-value@0.3.1: resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} engines: {node: '>=0.10.0'} @@ -5692,6 +5786,13 @@ packages: dependencies: function-bind: 1.1.2 + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + /he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -5741,13 +5842,6 @@ packages: lru-cache: 10.2.0 dev: false - /html-encoding-sniffer@2.0.1: - resolution: {integrity: sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==} - engines: {node: '>=10'} - dependencies: - whatwg-encoding: 1.0.5 - dev: true - /html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true @@ -5785,16 +5879,6 @@ packages: transitivePeerDependencies: - supports-color - /https-proxy-agent@5.0.0: - resolution: {integrity: sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==} - engines: {node: '>= 6'} - dependencies: - agent-base: 6.0.2 - debug: 4.3.4(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - dev: true - /https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -5824,13 +5908,6 @@ packages: hasBin: true dev: true - /iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - dev: true - /iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -5917,6 +5994,15 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} dev: true + /internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.4 + dev: true + /interpret@3.1.1: resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} engines: {node: '>=10.13.0'} @@ -5937,10 +6023,24 @@ packages: hasown: 2.0.1 dev: true + /is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + dev: true + /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: true + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -5948,10 +6048,23 @@ packages: binary-extensions: 2.2.0 dev: true + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + dev: true + /is-buffer@1.1.6: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} dev: true + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + /is-ci@2.0.0: resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} hasBin: true @@ -5971,6 +6084,20 @@ packages: hasown: 2.0.1 dev: true + /is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + dependencies: + is-typed-array: 1.1.13 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + /is-descriptor@0.1.7: resolution: {integrity: sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==} engines: {node: '>= 0.4'} @@ -6032,6 +6159,18 @@ packages: resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} dev: false + /is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + /is-number@3.0.0: resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} engines: {node: '>=0.10.0'} @@ -6072,11 +6211,6 @@ packages: engines: {node: '>=8'} dev: true - /is-plain-obj@1.1.0: - resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} - engines: {node: '>=0.10.0'} - dev: true - /is-plain-obj@2.1.0: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} @@ -6089,13 +6223,12 @@ packages: isobject: 3.0.1 dev: true - /is-potential-custom-element-name@1.0.1: - resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - dev: true - - /is-redirect@1.0.0: - resolution: {integrity: sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==} - engines: {node: '>=0.10.0'} + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 dev: true /is-regexp@1.0.0: @@ -6107,14 +6240,11 @@ packages: resolution: {integrity: sha512-i1h+y50g+0hRbBD+dbnInl3JlJ702aar58snAeX+MxBAPvzXGej7sYoPMhlnykabt0ZzCJNBEyzMlekuQZN7fA==} dev: true - /is-retry-allowed@1.2.0: - resolution: {integrity: sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==} - engines: {node: '>=0.10.0'} - dev: true - - /is-stream@1.1.0: - resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} - engines: {node: '>=0.10.0'} + /is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 dev: true /is-stream@2.0.1: @@ -6122,6 +6252,27 @@ packages: engines: {node: '>=8'} dev: true + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.15 + dev: true + /is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -6136,6 +6287,12 @@ packages: resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} dev: true + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.7 + dev: true + /is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -6148,6 +6305,10 @@ packages: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} dev: true + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + /isbinaryfile@4.0.4: resolution: {integrity: sha512-pEutbN134CzcjlLS1myKX/uxNjwU5eBVSprvkpv3+3dqhBHUZLIWJQowC40w5c0Zf19vBY8mrZl88y5J4RAPbQ==} engines: {node: '>= 8.0.0'} @@ -6257,7 +6418,7 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 14.18.63 + '@types/node': 18.19.31 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -6278,7 +6439,7 @@ packages: - supports-color dev: true - /jest-cli@29.7.0(@types/node@14.18.63)(ts-node@9.1.1): + /jest-cli@29.7.0(@types/node@18.19.31)(ts-node@9.1.1): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -6292,10 +6453,10 @@ packages: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@14.18.63)(ts-node@9.1.1) + create-jest: 29.7.0(@types/node@18.19.31)(ts-node@9.1.1) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@14.18.63)(ts-node@9.1.1) + jest-config: 29.7.0(@types/node@18.19.31)(ts-node@9.1.1) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -6306,7 +6467,7 @@ packages: - ts-node dev: true - /jest-config@29.7.0(@types/node@14.18.63)(ts-node@9.1.1): + /jest-config@29.7.0(@types/node@18.19.31)(ts-node@9.1.1): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -6321,7 +6482,7 @@ packages: '@babel/core': 7.23.3 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 14.18.63 + '@types/node': 18.19.31 babel-jest: 29.7.0(@babel/core@7.23.3) chalk: 4.1.2 ci-info: 3.9.0 @@ -6391,7 +6552,7 @@ packages: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 14.18.63 + '@types/node': 18.19.31 jest-mock: 29.7.0 jest-util: 29.7.0 dev: true @@ -6411,7 +6572,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 14.18.63 + '@types/node': 18.19.31 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -6434,7 +6595,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 dateformat: 3.0.2 - jest: 29.7.0(@types/node@14.18.63)(ts-node@9.1.1) + jest: 29.7.0(@types/node@18.19.31)(ts-node@9.1.1) mkdirp: 1.0.4 strip-ansi: 6.0.1 typescript: 5.3.3 @@ -6515,7 +6676,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 14.18.63 + '@types/node': 18.19.31 jest-util: 29.7.0 dev: true @@ -6575,7 +6736,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 14.18.63 + '@types/node': 18.19.31 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -6606,7 +6767,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 14.18.63 + '@types/node': 18.19.31 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.2 @@ -6625,6 +6786,13 @@ packages: - supports-color dev: true + /jest-silent-reporter@0.5.0: + resolution: {integrity: sha512-epdLt8Oj0a1AyRiR6F8zx/1SVT1Mi7VU3y4wB2uOBHs/ohIquC7v2eeja7UN54uRPyHInIKWdL+RdG228n5pJQ==} + dependencies: + chalk: 4.1.2 + jest-util: 26.6.2 + dev: true + /jest-snapshot@29.7.0: resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6680,12 +6848,24 @@ packages: - supports-color dev: true + /jest-util@26.6.2: + resolution: {integrity: sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==} + engines: {node: '>= 10.14.2'} + dependencies: + '@jest/types': 26.6.2 + '@types/node': 18.19.31 + chalk: 4.1.2 + graceful-fs: 4.2.11 + is-ci: 2.0.0 + micromatch: 4.0.5 + dev: true + /jest-util@29.7.0: resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 14.18.63 + '@types/node': 18.19.31 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -6710,7 +6890,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 14.18.63 + '@types/node': 18.19.31 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -6722,7 +6902,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 14.18.63 + '@types/node': 18.19.31 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -6731,13 +6911,13 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 14.18.63 + '@types/node': 18.19.31 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest@29.7.0(@types/node@14.18.63)(ts-node@9.1.1): + /jest@29.7.0(@types/node@18.19.31)(ts-node@9.1.1): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -6750,7 +6930,7 @@ packages: '@jest/core': 29.7.0(ts-node@9.1.1) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@14.18.63)(ts-node@9.1.1) + jest-cli: 29.7.0(@types/node@18.19.31)(ts-node@9.1.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -6765,14 +6945,6 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - /js-yaml@3.13.1: - resolution: {integrity: sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==} - hasBin: true - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - dev: false - /js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -6790,48 +6962,6 @@ packages: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} dev: false - /jsdom@16.7.0: - resolution: {integrity: sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==} - engines: {node: '>=10'} - peerDependencies: - canvas: ^2.5.0 - peerDependenciesMeta: - canvas: - optional: true - dependencies: - abab: 2.0.6 - acorn: 8.11.2 - acorn-globals: 6.0.0 - cssom: 0.4.4 - cssstyle: 2.3.0 - data-urls: 2.0.0 - decimal.js: 10.4.3 - domexception: 2.0.1 - escodegen: 2.1.0 - form-data: 3.0.1 - html-encoding-sniffer: 2.0.1 - http-proxy-agent: 4.0.1 - https-proxy-agent: 5.0.1 - is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.7 - parse5: 6.0.1 - saxes: 5.0.1 - symbol-tree: 3.2.4 - tough-cookie: 4.1.3 - w3c-hr-time: 1.0.2 - w3c-xmlserializer: 2.0.0 - webidl-conversions: 6.1.0 - whatwg-encoding: 1.0.5 - whatwg-mimetype: 2.3.0 - whatwg-url: 8.7.0 - ws: 7.5.9 - xml-name-validator: 3.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: true - /jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} @@ -7111,11 +7241,6 @@ packages: get-func-name: 2.0.2 dev: true - /lowercase-keys@1.0.1: - resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} - engines: {node: '>=0.10.0'} - dev: true - /lru-cache@10.2.0: resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} engines: {node: 14 || >=16.14} @@ -7205,13 +7330,6 @@ packages: tmpl: 1.0.5 dev: true - /map-age-cleaner@0.1.3: - resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} - engines: {node: '>=6'} - dependencies: - p-defer: 1.0.0 - dev: true - /map-cache@0.2.2: resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} engines: {node: '>=0.10.0'} @@ -7299,14 +7417,6 @@ packages: /mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - /mem@6.1.1: - resolution: {integrity: sha512-Ci6bIfq/UgcxPTYa8dQQ5FY3BzKkT894bwXWXxC/zqs0XgMO2cT20CGkOqda7gZNkmK5VP4x89IGZ6K7hfbn3Q==} - engines: {node: '>=8'} - dependencies: - map-age-cleaner: 0.1.3 - mimic-fn: 3.1.0 - dev: true - /memory-fs@0.5.0: resolution: {integrity: sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==} engines: {node: '>=4.3.0 <5.0.0 || >=5.10'} @@ -7374,11 +7484,6 @@ packages: engines: {node: '>=6'} dev: true - /mimic-fn@3.1.0: - resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} - engines: {node: '>=8'} - dev: true - /mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -7464,6 +7569,7 @@ packages: engines: {node: '>=8'} dependencies: yallist: 4.0.0 + dev: false /minipass@5.0.0: resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} @@ -7480,6 +7586,7 @@ packages: dependencies: minipass: 3.3.6 yallist: 4.0.0 + dev: false /mixin-deep@1.3.2: resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} @@ -7781,11 +7888,6 @@ packages: '@babel/parser': 7.23.3 dev: true - /node-status-codes@1.0.0: - resolution: {integrity: sha512-1cBMgRxdMWE8KeWCqk2RIOrvUb0XCwYfEsY5/y2NlXyq4Y/RumnOZvTj4Nbr77+Vb2C+kyBoRTdkNOS8L3d/aQ==} - engines: {node: '>=0.10.0'} - dev: true - /noms@0.0.0: resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==} dependencies: @@ -7907,10 +8009,6 @@ packages: boolbase: 1.0.0 dev: true - /nwsapi@2.2.7: - resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} - dev: true - /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -7929,6 +8027,11 @@ packages: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} dev: true + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + /object-visit@1.0.1: resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} engines: {node: '>=0.10.0'} @@ -7936,6 +8039,16 @@ packages: isobject: 3.0.1 dev: true + /object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + /object.pick@1.3.0: resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} engines: {node: '>=0.10.0'} @@ -8007,11 +8120,6 @@ packages: - debug dev: true - /p-defer@1.0.0: - resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} - engines: {node: '>=4'} - dev: true - /p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -8100,13 +8208,6 @@ packages: dependencies: callsites: 3.1.0 - /parse-json@2.2.0: - resolution: {integrity: sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==} - engines: {node: '>=0.10.0'} - dependencies: - error-ex: 1.3.2 - dev: true - /parse-json@4.0.0: resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} engines: {node: '>=4'} @@ -8148,10 +8249,6 @@ packages: parse5: 7.1.2 dev: true - /parse5@6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - dev: true - /parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} dependencies: @@ -8319,6 +8416,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: true + /postcss-values-parser@2.0.1: resolution: {integrity: sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==} engines: {node: '>=6.14.4'} @@ -8419,11 +8521,6 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - /prepend-http@1.0.4: - resolution: {integrity: sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==} - engines: {node: '>=0.10.0'} - dev: true - /prettier-linter-helpers@1.0.0: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} @@ -8506,10 +8603,6 @@ packages: retry: 0.12.0 dev: false - /promise-status-async@1.2.10: - resolution: {integrity: sha512-MECA3pc+uWN+D6IiATrNVzXtBU6WrsKvWmxS9Syd6d14roJqfEpxh/1Ocr4lpWqZ4zorxXJ+bqMTTrqqZ302yg==} - dev: false - /prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -8532,10 +8625,6 @@ packages: yargs: 17.7.2 dev: true - /psl@1.9.0: - resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} - dev: true - /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} dependencies: @@ -8567,10 +8656,6 @@ packages: side-channel: 1.0.4 dev: true - /querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - dev: true - /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -8608,14 +8693,6 @@ packages: loose-envify: 1.4.0 dev: false - /read-all-stream@3.1.0: - resolution: {integrity: sha512-DI1drPHbmBcUDWrJ7ull/F2Qb8HkwBncVx8/RpKYFSIACYaVRQReISYPdZz/mt1y1+qMCOrfReTopERmaxtP6w==} - engines: {node: '>=0.10.0'} - dependencies: - pinkie-promise: 2.0.1 - readable-stream: 2.3.8 - dev: true - /read-package-json-fast@3.0.2: resolution: {integrity: sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -8722,6 +8799,16 @@ packages: safe-regex: 1.1.0 dev: true + /regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + dev: true + /regexpp@3.2.0: resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} engines: {node: '>=8'} @@ -8767,10 +8854,6 @@ packages: hasBin: true dev: true - /requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - dev: true - /resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -8896,6 +8979,16 @@ packages: tslib: 1.14.1 dev: true + /safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true + /safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} dev: true @@ -8904,6 +8997,15 @@ packages: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} dev: true + /safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + dev: true + /safe-regex@1.1.0: resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} dependencies: @@ -8912,6 +9014,7 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false /sass-lookup@3.0.0: resolution: {integrity: sha512-TTsus8CfFRn1N44bvdEai1no6PqdmDiQUiqW5DlpmtT+tYnIt1tXtDIph5KA1efC+LmioJXSnCtUVpcK9gaKIg==} @@ -8925,13 +9028,6 @@ packages: resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} dev: true - /saxes@5.0.1: - resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} - engines: {node: '>=10'} - dependencies: - xmlchars: 2.2.0 - dev: true - /schema-utils@3.3.0: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} @@ -8996,11 +9092,33 @@ packages: engines: {node: '>= 0.4'} dependencies: define-data-property: 1.1.1 - get-intrinsic: 1.2.2 + get-intrinsic: 1.2.4 gopd: 1.0.1 has-property-descriptors: 1.0.1 dev: true + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + dev: true + + /set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + dev: true + /set-value@2.0.1: resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} engines: {node: '>=0.10.0'} @@ -9048,7 +9166,7 @@ packages: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: call-bind: 1.0.5 - get-intrinsic: 1.2.2 + get-intrinsic: 1.2.4 object-inspect: 1.13.1 dev: true @@ -9374,6 +9492,33 @@ packages: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + /string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + dev: true + + /string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + /string_decoder@0.10.31: resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} @@ -9494,8 +9639,18 @@ packages: engines: {node: '>= 0.4'} dev: true - /symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + /suppressed-error@1.0.3: + resolution: {integrity: sha512-6+ZiCVUmDLFRyYRswTrDTYWaM/IT01W/cqQBLnnyg8T0njVrWj3tP+EXFevXk6qK61yDXnmZsOFVzFfYoUy/KA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + function-bind: 1.1.2 + globalthis: 1.0.3 + has-property-descriptors: 1.0.1 + set-function-name: 2.0.2 dev: true /tabbable@5.3.3: @@ -9545,18 +9700,6 @@ packages: readable-stream: 3.6.2 dev: true - /tar@6.0.2: - resolution: {integrity: sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg==} - engines: {node: '>= 10'} - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 3.3.6 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 - dev: true - /tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} @@ -9656,11 +9799,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /timed-out@2.0.0: - resolution: {integrity: sha512-pqqJOi1rF5zNs/ps4vmbE4SFCrM4iR7LW+GHAsHqO/EumqbIWceioevYLM5xZRgQSH6gFgL9J/uB7EcJhQ9niQ==} - engines: {node: '>=0.10.0'} - dev: true - /tiny-invariant@1.3.1: resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} dev: true @@ -9718,23 +9856,6 @@ packages: safe-regex: 1.1.0 dev: true - /tough-cookie@4.1.3: - resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} - engines: {node: '>=6'} - dependencies: - psl: 1.9.0 - punycode: 2.3.1 - universalify: 0.2.0 - url-parse: 1.5.10 - dev: true - - /tr46@2.1.0: - resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==} - engines: {node: '>=8'} - dependencies: - punycode: 2.3.1 - dev: true - /traverse@0.3.9: resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} dev: true @@ -9773,7 +9894,7 @@ packages: '@babel/core': 7.24.4 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@14.18.63)(ts-node@9.1.1) + jest: 29.7.0(@types/node@18.19.31)(ts-node@9.1.1) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -9908,6 +10029,50 @@ packages: engines: {node: '>=10'} dev: true + /typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + dev: true + + /typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + dev: true + + /typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + dev: true + + /typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + dev: true + /typed-rest-client@1.8.11: resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} dependencies: @@ -9949,9 +10114,22 @@ packages: dev: false optional: true + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.7 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + /underscore@1.13.6: resolution: {integrity: sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==} + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + /unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} @@ -9989,11 +10167,6 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} - /universalify@0.2.0: - resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} - engines: {node: '>= 4.0.0'} - dev: true - /universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -10011,11 +10184,6 @@ packages: engines: {node: '>=8'} dev: true - /unzip-response@1.0.2: - resolution: {integrity: sha512-pwCcjjhEcpW45JZIySExBHYv5Y9EeL2OIGEfrSKp2dMUFGFv4CpvZkwJbVge8OvGH2BNNtJBx67DuKuJhf+N5Q==} - engines: {node: '>=0.10'} - dev: true - /unzipper@0.10.14: resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} dependencies: @@ -10067,20 +10235,6 @@ packages: resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} dev: true - /url-parse-lax@1.0.0: - resolution: {integrity: sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==} - engines: {node: '>=0.10.0'} - dependencies: - prepend-http: 1.0.4 - dev: true - - /url-parse@1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - dependencies: - querystringify: 2.2.0 - requires-port: 1.0.0 - dev: true - /use@3.1.1: resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} engines: {node: '>=0.10.0'} @@ -10167,14 +10321,14 @@ packages: strip-ansi: 6.0.1 tiny-invariant: 1.3.1 typescript: 5.3.3 - vite: 4.5.2(@types/node@14.18.63) + vite: 4.5.2(@types/node@18.19.31) vscode-languageclient: 7.0.0 vscode-languageserver: 7.0.0 vscode-languageserver-textdocument: 1.0.11 vscode-uri: 3.0.8 dev: true - /vite@4.5.2(@types/node@14.18.63): + /vite@4.5.2(@types/node@18.19.31): resolution: {integrity: sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -10202,7 +10356,7 @@ packages: terser: optional: true dependencies: - '@types/node': 14.18.63 + '@types/node': 18.19.31 esbuild: 0.18.20 postcss: 8.4.31 rollup: 3.29.4 @@ -10250,20 +10404,6 @@ packages: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} dev: true - /w3c-hr-time@1.0.2: - resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} - deprecated: Use your platform's native performance.now() and performance.timeOrigin. - dependencies: - browser-process-hrtime: 1.0.0 - dev: true - - /w3c-xmlserializer@2.0.0: - resolution: {integrity: sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==} - engines: {node: '>=10'} - dependencies: - xml-name-validator: 3.0.0 - dev: true - /walkdir@0.4.1: resolution: {integrity: sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==} engines: {node: '>=6.0.0'} @@ -10301,16 +10441,6 @@ packages: resolution: {integrity: sha512-weOVgZ3aAARgdnb220GqYuh7+rZU0Ka9k9yfKtGAzEYMa6GgiCzW9JjQRJyCJakvibQW+dfjJdihjInKuuCAUQ==} dev: true - /webidl-conversions@5.0.0: - resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} - engines: {node: '>=8'} - dev: true - - /webidl-conversions@6.1.0: - resolution: {integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==} - engines: {node: '>=10.4'} - dev: true - /webpack-cli@5.1.4(webpack@5.89.0): resolution: {integrity: sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==} engines: {node: '>=14.15.0'} @@ -10440,27 +10570,29 @@ packages: - uglify-js dev: true - /whatwg-encoding@1.0.5: - resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: - iconv-lite: 0.4.24 + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 dev: true - /whatwg-mimetype@2.3.0: - resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} + /which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} dev: true - /whatwg-url@8.7.0: - resolution: {integrity: sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==} - engines: {node: '>=10'} + /which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} dependencies: - lodash: 4.17.21 - tr46: 2.1.0 - webidl-conversions: 6.1.0 - dev: true - - /which-module@2.0.1: - resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 dev: true /which@1.3.1: @@ -10539,23 +10671,6 @@ packages: signal-exit: 3.0.7 dev: true - /ws@7.5.9: - resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: true - - /xml-name-validator@3.0.0: - resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==} - dev: true - /xml2js@0.4.23: resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==} engines: {node: '>=4.0.0'} @@ -10586,10 +10701,6 @@ packages: engines: {node: '>=8.0'} dev: true - /xmlchars@2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - dev: true - /xregexp@2.0.0: resolution: {integrity: sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==} dev: false diff --git a/samples/menu-item-sample/package.json b/samples/menu-item-sample/package.json index 0dfe56382e..f4c733b453 100644 --- a/samples/menu-item-sample/package.json +++ b/samples/menu-item-sample/package.json @@ -49,7 +49,7 @@ "@zowe/zowe-explorer-api": "file:../../packages/zowe-explorer-api" }, "devDependencies": { - "@types/node": "^16.18.34", + "@types/node": "^18.19.14", "@types/vscode": "^1.53.2", "@typescript-eslint/eslint-plugin": "^5.42.0", "@typescript-eslint/parser": "^5.42.0", diff --git a/samples/tree-view-sample/package.json b/samples/tree-view-sample/package.json index 941c90283a..70e094fdbf 100644 --- a/samples/tree-view-sample/package.json +++ b/samples/tree-view-sample/package.json @@ -52,7 +52,7 @@ "@zowe/zowe-explorer-api": "file:../../packages/zowe-explorer-api" }, "devDependencies": { - "@types/node": "^16.18.34", + "@types/node": "^18.19.14", "@types/vscode": "^1.53.2", "@typescript-eslint/eslint-plugin": "^5.42.0", "@typescript-eslint/parser": "^5.42.0", diff --git a/samples/uss-profile-sample/package.json b/samples/uss-profile-sample/package.json index 59d52ea977..e6c798f7c8 100644 --- a/samples/uss-profile-sample/package.json +++ b/samples/uss-profile-sample/package.json @@ -37,7 +37,7 @@ "ssh2-sftp-client": "^9.1.0" }, "devDependencies": { - "@types/node": "^16.18.34", + "@types/node": "^18.19.14", "@types/ssh2-sftp-client": "^9.0.0", "@types/vscode": "^1.53.2", "@typescript-eslint/eslint-plugin": "^5.42.0", diff --git a/samples/vue-webview-sample/package.json b/samples/vue-webview-sample/package.json index d574be5cf2..3e09721f33 100644 --- a/samples/vue-webview-sample/package.json +++ b/samples/vue-webview-sample/package.json @@ -30,7 +30,7 @@ "@zowe/zowe-explorer-api": "file:../../packages/zowe-explorer-api" }, "devDependencies": { - "@types/node": "^16.18.41", + "@types/node": "^18.19.14", "@types/vscode": "^1.53.2", "@typescript-eslint/eslint-plugin": "^5.42.0", "@typescript-eslint/parser": "^5.42.0",