@salesforce/salesforcedx-vscode-test-tools
Version:
Test automation framework for Salesforce Extensions for VS Code
370 lines (327 loc) • 13.4 kB
text/typescript
/*
* Copyright (c) 2024, 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 { By, Setting, SettingsEditor, WebElement } from 'vscode-extension-tester';
import { executeQuickPick } from '../ui-interaction/commandPrompt';
import { Duration, findElementByText, log, pause } from '../core/miscellaneous';
import { getBrowser } from '../ui-interaction/workbench';
/**
* Finds a setting in VSCode settings UI and checks its value
* @param id - The ID of the setting to find
* @returns Object containing the setting and its current value
* @throws Error if the setting cannot be found
* @private
*/
async function findAndCheckSetting(id: string): Promise<{ checkButton: Setting; checkButtonValue: string | null }> {
log(`enter findAndCheckSetting for id: ${id}`);
await executeQuickPick('Preferences: Clear Settings Search Results', Duration.seconds(2));
try {
const input = await getBrowser().findElement(By.css('div.suggest-input-container'));
await input.click();
const textArea = await getBrowser().findElement(By.css('textarea.inputarea.monaco-mouse-cursor-text'));
await textArea.sendKeys(id);
await pause(Duration.seconds(2));
let checkButton: Setting | null = null;
let checkButtonValue: string | null = null;
await getBrowser().wait(
async () => {
checkButton = (await findElementByText('div', 'aria-label', id)) as Setting;
if (checkButton) {
checkButtonValue = await checkButton.getAttribute('aria-checked');
log(`found setting checkbox with value "${checkButtonValue}"`);
return true;
}
return false;
},
5000,
`Could not find setting with name: ${id}`
);
if (!checkButton) {
throw new Error(`Could not find setting with name: ${id}`);
}
log(`findAndCheckSetting result for ${id} found ${!!checkButton} value: ${checkButtonValue}`);
return { checkButton, checkButtonValue };
} catch (error) {
log(`error in findAndCheckSetting for ${id}: ${error}`);
log(`trying to find setting by id: ${id}`);
const settingsEditor = new SettingsEditor();
const setting = await settingsEditor.findSettingByID(id);
log(`found setting by id: ${id} ${setting}`);
return { checkButton: setting, checkButtonValue: String(await setting.getValue()) };
}
}
/**
* Opens the workspace settings editor
*/
export async function inWorkspaceSettings(): Promise<void> {
await executeQuickPick('Preferences: Open Workspace Settings', Duration.seconds(5));
}
/**
* Opens the user settings editor
*/
export async function inUserSettings(): Promise<void> {
await executeQuickPick('Preferences: Open User Settings', Duration.seconds(5));
}
/**
* Toggles a boolean setting to the specified state
* @param id - The ID of the setting to toggle
* @param finalState - The desired final state (true for enabled, false for disabled)
* @param settingsType - Whether to modify user or workspace settings
* @returns The final state of the setting after toggling
* @private
*/
async function toggleBooleanSetting(
id: string,
finalState: boolean | undefined,
settingsType: 'user' | 'workspace'
): Promise<boolean> {
const settingsFunction = settingsType === 'workspace' ? inWorkspaceSettings : inUserSettings;
await settingsFunction();
let result = await findAndCheckSetting(id);
if (finalState !== undefined) {
if ((finalState && result.checkButtonValue === 'true') || (!finalState && result.checkButtonValue === 'false')) {
return true;
}
}
await result.checkButton.click();
result = await findAndCheckSetting(id);
return result.checkButtonValue === 'true';
}
/**
* Enables a boolean setting
* @param id - The ID of the setting to enable
* @param settingsType - Whether to modify user or workspace settings, defaults to workspace
* @returns True if the setting was successfully enabled
*/
export async function enableBooleanSetting(
id: string,
settingsType: 'user' | 'workspace' = 'workspace'
): Promise<boolean> {
log(`enableBooleanSetting ${id}`);
return toggleBooleanSetting(id, true, settingsType);
}
/**
* Disables a boolean setting
* @param id - The ID of the setting to disable
* @param settingsType - Whether to modify user or workspace settings, defaults to workspace
* @returns True if the setting was successfully disabled
*/
export async function disableBooleanSetting(
id: string,
settingsType: 'user' | 'workspace' = 'workspace'
): Promise<boolean> {
log(`disableBooleanSetting ${id}`);
return toggleBooleanSetting(id, false, settingsType);
}
/**
* Checks if a boolean setting is enabled
* @param id - The ID of the setting to check
* @param settingsType - Whether to check user or workspace settings, defaults to workspace
* @returns True if the setting is enabled, false otherwise
*/
export async function isBooleanSettingEnabled(
id: string,
settingsType: 'user' | 'workspace' = 'workspace'
): Promise<boolean> {
const settingsFunction = settingsType === 'workspace' ? inWorkspaceSettings : inUserSettings;
await settingsFunction();
const { checkButtonValue } = await findAndCheckSetting(id);
return checkButtonValue === 'true';
}
/**
* Sets the value of a specified setting in VSCode.
* @param id - The unique identifier of the setting to be updated.
* @param value - The new value to set for the specified setting.
* @param isWorkspace - True if the setting is a workspace setting; false if it's a user setting.
* @returns A promise that resolves when the setting value has been updated.
*/
export const setSettingValue = async (id: string, value: string | boolean, isWorkspace: boolean): Promise<void> => {
await (isWorkspace ? inWorkspaceSettings() : inUserSettings());
try {
// Try the original implementation using RedHat page objects
const settingsEditor = new SettingsEditor();
const logLevelSetting = await settingsEditor.findSettingByID(id);
await logLevelSetting?.setValue(value);
} catch (error) {
// Fallback for VS Code 1.90.0+ where .native-edit-context selector doesn't exist
log(`Primary findSettingByID failed, using fallback implementation for VS Code 1.90.0+: ${error}`);
try {
const browser = getBrowser();
// Alternative selectors for the search input in newer VS Code versions
const searchSelectors = [
'.inputarea.monaco-mouse-cursor-text',
'input[placeholder*="Search settings"]',
];
let searchInput: WebElement | undefined;
for (const selector of searchSelectors) {
try {
searchInput = await browser.findElement(By.css(selector));
break;
} catch {
// Try next selector
}
}
if (!searchInput) {
throw new Error('Could not find settings search input with any known selector');
}
// Clear and search for the setting
await searchInput.clear();
await searchInput.sendKeys(id);
await pause(Duration.seconds(1));
// Find the setting by its title (last part of the ID) using robust approach
const title = id.split('.').pop();
let settingElement: WebElement | undefined;
// Wait for settings to load and stabilize
await pause(Duration.seconds(1));
// Try to find setting items using multiple selector approaches
const itemSelectors = [
'.setting-item',
'.settings-row-inner-container',
'.setting-list-item',
'[data-key]'
];
let items: WebElement[] = [];
for (const selector of itemSelectors) {
try {
items = await browser.findElements(By.css(selector));
if (items.length > 0) break;
} catch {
// Try next selector
}
}
if (items.length === 0) {
throw new Error('Could not find any setting items with known selectors');
}
// Search through items to find the matching setting
for (const item of items) {
try {
// Try to find the setting label/title within the item
const labelSelectors = [
'.setting-item-label',
'.setting-item-title',
'.label-name',
'.setting-key-label',
'.monaco-label'
];
let itemTitle = '';
for (const labelSelector of labelSelectors) {
try {
const labelElement = await item.findElement(By.css(labelSelector));
itemTitle = await labelElement.getText();
if (itemTitle) break;
} catch {
// Try next selector
}
}
// If no label found, try getting text content directly
if (!itemTitle) {
itemTitle = await item.getText();
}
// Normalize text comparison like SettingsEditor.js does
const normalizedTitle = title?.toLowerCase().replace(/\s/g, '').trim() || '';
const normalizedItemTitle = itemTitle.toLowerCase().replace(/\s/g, '').trim();
if (normalizedItemTitle.includes(normalizedTitle) || normalizedTitle.includes(normalizedItemTitle)) {
settingElement = item;
break;
}
} catch (err) {
// Continue to next item
}
}
if (!settingElement) {
throw new Error(`Could not find setting element for: ${title}`);
}
// Find the input/control within the setting element and set the value
const parent = await settingElement.findElement(By.xpath('..'));
const settingContainer = await parent.findElement(By.xpath('..'));
if (typeof value === 'boolean') {
const checkbox = await settingContainer.findElement(By.css('input[type="checkbox"]'));
const isChecked = await checkbox.isSelected();
if (isChecked !== value) {
await checkbox.click();
}
} else {
// Try to find and handle different input types
let inputHandled = false;
// First, try to find a select element (dropdown)
try {
const selectElement = await settingContainer.findElement(By.css('select'));
// For select elements, we need to find the option with the desired value
const options = await selectElement.findElements(By.css('option'));
for (const option of options) {
const optionValue = await option.getAttribute('value');
const optionText = await option.getText();
// Match by value or text content
if (optionValue === value.toString() || optionText === value.toString()) {
await option.click();
inputHandled = true;
break;
}
}
if (!inputHandled) {
// If no exact match found, try partial matching
for (const option of options) {
const optionValue = await option.getAttribute('value');
const optionText = await option.getText();
if (optionValue.toLowerCase().includes(value.toString().toLowerCase()) ||
optionText.toLowerCase().includes(value.toString().toLowerCase())) {
await option.click();
inputHandled = true;
break;
}
}
}
} catch {
// Select element not found, continue to text input
}
// If no select element found or handled, try text input
if (!inputHandled) {
try {
const textInput = await settingContainer.findElement(By.css('input[type="text"], textarea'));
await textInput.clear();
await textInput.sendKeys(value.toString());
inputHandled = true;
} catch {
// Text input not found
}
}
// If still not handled, try more input types
if (!inputHandled) {
try {
const input = await settingContainer.findElement(By.css('input, textarea, select'));
const inputType = await input.getTagName();
if (inputType === 'select') {
// Handle select that wasn't caught above
const options = await input.findElements(By.css('option'));
for (const option of options) {
const optionText = await option.getText();
if (optionText === value.toString()) {
await option.click();
inputHandled = true;
break;
}
}
} else {
// Handle other input types
await input.clear();
await input.sendKeys(value.toString());
inputHandled = true;
}
} catch {
// No input found
}
}
if (!inputHandled) {
throw new Error(`Could not find or handle input control for setting: ${title}`);
}
}
log(`Successfully set setting ${id} to ${value} using fallback implementation`);
} catch (fallbackError) {
log(`Both primary and fallback implementations failed: ${fallbackError}`);
throw new Error(`Failed to set setting ${id}: ${fallbackError}`);
}
}
};