UNPKG

@salesforce/salesforcedx-vscode-test-tools

Version:
303 lines (265 loc) 10.1 kB
/* * Copyright (c) 2025, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import os from 'os'; import { EnvironmentSettings } from '../environmentSettings'; import { attemptToFindOutputPanelText, clearOutputView } from '../ui-interaction/outputView'; import { clickFilePathOkButton, executeQuickPick, findQuickPickItem } from '../ui-interaction/commandPrompt'; import * as DurationKit from '@salesforce/kit'; import path from 'path'; import { PredicateWithTimeout } from '../testing/predicates'; import { By, WebElement } from 'vscode-extension-tester'; import { getBrowser, getWorkbench } from '../ui-interaction/workbench'; import { expect } from 'chai'; import { retryOperation, verifyNotificationWithRetry } from '../retryUtils'; import { clickButtonOnModalDialog } from '../ui-interaction'; /** * Pauses execution for a specified duration * @param duration - The amount of time to pause; defaults to 1 second */ export async function pause(duration: Duration = Duration.seconds(1)): Promise<void> { const throttleFactor = EnvironmentSettings.getInstance().throttleFactor; await sleep(duration.milliseconds * throttleFactor); } /** * Logs a message to the console if the log level allows it * @param message - The message to log */ export function log(message: string): void { if (EnvironmentSettings.getInstance().logLevel !== 'silent') { console.log(message); } } /** * Logs a debug message to the console if the log level is debug or trace * @param message - The debug message to log */ export function debug(message: string): void { if (EnvironmentSettings.getInstance().logLevel in ['debug', 'trace']) { const timestamp = new Date().toISOString(); console.debug(`${timestamp}:${message}`); } } /** * Logs an error message to the console if the log level is error * @param message - The error message to log */ export function error(message: string): void { if (EnvironmentSettings.getInstance().logLevel === 'error') { console.error(`Error: ${message}`); } } /** * Gets the username of the current OS user * @returns The current OS username */ export function currentOsUserName(): string { const userName = os.userInfo().username || process.env.SUDO_USER || process.env.C9_USER || process.env.LOGNAME || process.env.USER || process.env.LNAME || process.env.USERNAME; if (!userName) { throw new Error('Unable to determine current OS username'); } return userName; } /** * Returns a transformed version of the current username with periods replaced by underscores * Used to work around an issue where InputBox.setText() truncates strings with periods * @returns The transformed username */ export function transformedUserName(): string { debug('transformedUsername()'); return currentOsUserName().replace('.', '_'); } /** * @param type type of html tag we want to find * @param attribute attribute that holds the given text * @param labelText text of the element we want to find * @param waitForClickable whether to wait until the element is clickable * @param waitOptions options for waiting until the element is clickable * @returns element that contains the given text */ export async function findElementByText( type: string, attribute: string, labelText: string | undefined, waitForClickable: boolean | undefined = false, waitOptions?: { timeout?: Duration; interval?: Duration; reverse?: boolean; timeoutMsg?: string; } ): Promise<WebElement> { if (!labelText) { throw new Error('labelText must be defined'); } debug(`findElementByText //${type}[@${attribute}="${labelText}"]`); const element = await getWorkbench().findElement(By.xpath(`//${type}[@${attribute}="${labelText}"]`)); if (!element) { throw new Error(`Element with selector: "${type}[${attribute}=\"${labelText}\"]" not found}`); } if (waitForClickable) { await getBrowser().wait( async () => { const isDisplayedAndEnabled = (await element.isDisplayed()) && (await element.isEnabled()); return waitOptions?.reverse ? !isDisplayedAndEnabled : isDisplayedAndEnabled; }, waitOptions?.timeout?.milliseconds ?? Duration.seconds(5).milliseconds, waitOptions?.timeoutMsg, waitOptions?.interval?.milliseconds ?? Duration.milliseconds(500).milliseconds ); } return element; } export async function createCommand( type: string, name: string, folder: string, extension: string ): Promise<string | undefined> { await clearOutputView(); const inputBox = await executeQuickPick(`SFDX: Create ${type}`, Duration.seconds(1)); // Set the name of the new component to name. await retryOperation(async () => { await inputBox.setText(name); await inputBox.confirm(); await pause(Duration.seconds(1)); await clickButtonOnModalDialog('Overwrite', false); await pause(Duration.seconds(1)); }); // Select the default directory (press Enter/Return). await inputBox.confirm(); await verifyNotificationWithRetry( new RegExp(`SFDX: Create ${type} successfully ran`), Duration.minutes(10) ); const outputPanelText = await attemptToFindOutputPanelText(`Salesforce CLI`, `Finished SFDX: Create ${type}`, 10); // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(outputPanelText).not.to.be.undefined; const typePath = path.join(`force-app`, `main`, `default`, folder, `${name}.${extension}`); expect(outputPanelText).to.include(`create ${typePath}`); const metadataPath = path.join(`force-app`, `main`, `default`, folder, `${name}.${extension}-meta.xml`); expect(outputPanelText).to.include(`create ${metadataPath}`); return outputPanelText; } /** * Sets the specified org as the default org * @param targetOrg - The name of the org to set as default */ export async function setDefaultOrg(targetOrg: string): Promise<void> { const inputBox = await executeQuickPick('SFDX: Set a Default Org'); await findQuickPickItem(inputBox, targetOrg, false, true); } /** * Type guard function to check if the argument is a Duration * @param predicateOrWait - The value to check * @returns True if the value is a Duration, false otherwise */ export function isDuration(predicateOrWait: PredicateWithTimeout | Duration): predicateOrWait is Duration { return (predicateOrWait as Duration).milliseconds !== undefined; } export enum Unit { MINUTES = DurationKit.Duration.Unit.MINUTES, MILLISECONDS = DurationKit.Duration.Unit.MILLISECONDS, SECONDS = DurationKit.Duration.Unit.SECONDS, HOURS = DurationKit.Duration.Unit.HOURS, DAYS = DurationKit.Duration.Unit.DAYS, WEEKS = DurationKit.Duration.Unit.WEEKS } export class Duration extends DurationKit.Duration { private scaleFactor: number; constructor(quantity: number, unit: Unit, scaleFactor?: number) { super(quantity, unit); if (scaleFactor !== undefined) { this.scaleFactor = scaleFactor; } else { this.scaleFactor = EnvironmentSettings.getInstance().throttleFactor; } } public get minutes(): number { return super.minutes * this.scaleFactor; } public get hours(): number { return super.hours * this.scaleFactor; } public get milliseconds(): number { return super.milliseconds * this.scaleFactor; } public get seconds(): number { return super.seconds * this.scaleFactor; } public get days(): number { return super.days * this.scaleFactor; } public get weeks(): number { return super.weeks * this.scaleFactor; } public static ONE_MINUTE = Duration.minutes(1); public static FIVE_MINUTES = Duration.minutes(5); public static TEN_MINUTES = Duration.minutes(10); // Static methods for creating new instances without specifying scaleFactor public static milliseconds(quantity: number): Duration { return new Duration(quantity, Unit.MILLISECONDS); } public static seconds(quantity: number): Duration { return new Duration(quantity, Unit.SECONDS); } public static minutes(quantity: number): Duration { return new Duration(quantity, Unit.MINUTES); } public static hours(quantity: number): Duration { return new Duration(quantity, Unit.HOURS); } public static days(quantity: number): Duration { return new Duration(quantity, Unit.DAYS); } public static weeks(quantity: number): Duration { return new Duration(quantity, Unit.WEEKS); } } /** * Pauses execution for a specified number of milliseconds * @param duration - The number of milliseconds to sleep * @returns A promise that resolves after the specified duration */ export async function sleep(duration: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, duration)); } /* * VSCode will be working on the new workspace, and the previous one is closed. */ export async function openFolder(path: string) { await retryOperation(async () => { const prompt = await executeQuickPick('File: Open Folder...'); // use this cmd palette to open await pause(Duration.seconds(2)); // Set the location of the project await prompt.setText(path); await pause(Duration.seconds(2)); const projectName = path.substring(path.lastIndexOf('/') + 1); await prompt.selectQuickPick(projectName); await clickFilePathOkButton(); await pause(Duration.seconds(2)); }); } /** * A definite alternative of getTextEditor to open a file in text editor * @param path */ export async function openFile(path: string) { const prompt = await executeQuickPick('File: Open File...'); // use this cmd palette to open // Set the location of the project await prompt.setText(path); await pause(Duration.seconds(2)); const fileName = path.substring(path.lastIndexOf(process.platform === 'win32' ? '\\' : '/') + 1); await prompt.selectQuickPick(fileName); await pause(Duration.seconds(5)); // wait for the file to open }