@salesforce/salesforcedx-vscode-test-tools
Version:
Test automation framework for Salesforce Extensions for VS Code
346 lines (301 loc) • 13.9 kB
text/typescript
import { By, CodeLens, EditorView, Key, TextEditor, Workbench } from 'vscode-extension-tester';
import { executeQuickPick } from './commandPrompt';
import { Duration, log, openFile, pause } from '../core/miscellaneous';
import { getBrowser } from './workbench';
import { retryOperation } from '../retryUtils';
import * as fs from 'fs/promises';
import { createOrOverwriteFile } from '../system-operations/fileSystem';
/**
* Gets a text editor for a specific file
* @param workbench - The VSCode workbench instance
* @param fileName - Name of the file to open and edit
* @returns A TextEditor instance for the specified file
*/
export async function getTextEditor(workbench: Workbench, fileName: string): Promise<TextEditor> {
log(`calling getTextEditor(${fileName})`);
log('getTextEditor() - Attempting to open file');
const inputBox = await executeQuickPick('Go to File...', Duration.seconds(1));
log('getTextEditor() - executeQuickPick() - inputBox');
await inputBox.setText(fileName);
log(`getTextEditor() - executeQuickPick() - inputBox.setText(${fileName})`);
await inputBox.confirm();
log('getTextEditor() - executeQuickPick() - inputBox.confirm()');
await pause(Duration.seconds(1));
log('getTextEditor() - File opened, getting editor view');
const editorView = workbench.getEditorView();
// Add retry logic specifically around the openEditor call that fails
const textEditor = await retryOperation(async () => {
log('getTextEditor() - Attempting to open editor...');
return (await editorView.openEditor(fileName)) as TextEditor;
}, 3, 'Failed to open editor after retries');
await pause(Duration.seconds(2));
return textEditor;
}
/**
* Checks if a file is open in the editor
* @param workbench - The VSCode workbench instance
* @param name - The name of the file to check
* @param options - Optional configuration including timeout and error message
* @throws Timeout error if the file isn't open within the specified timeout
*/
export async function checkFileOpen(
workbench: Workbench,
name: string,
options: { msg?: string; timeout?: Duration } = { timeout: Duration.milliseconds(30_000) }
) {
await getBrowser().wait(
async () => {
try {
const editorView = workbench.getEditorView();
const activeTab = await editorView.getActiveTab();
if (activeTab != undefined && name == (await activeTab.getTitle())) {
return true;
} else return false;
} catch (error) {
return false;
}
},
options.timeout?.milliseconds,
options.msg ?? `Expected to find file ${name} open in TextEditor before ${options.timeout}`,
500 // Check every 500 ms
);
}
/**
* Waits for a file to be open in the editor with a 30-second timeout
* @param workbench - The VSCode workbench instance
* @param fileName - Name of the file to wait for
* @returns Promise that resolves when file is open or rejects on timeout
*/
export async function waitForFileOpen(workbench: Workbench, fileName: string): Promise<void> {
const timeout = 20_000; // 20 seconds
const checkInterval = 500; // Check every 500ms
const startTime = Date.now();
const endTime = startTime + timeout;
let attemptCount = 0;
log(`waitForFileOpen() - Starting wait for file: ${fileName}`);
log(`waitForFileOpen() - Timeout: ${timeout}ms, Check interval: ${checkInterval}ms`);
while (Date.now() < endTime) {
attemptCount++;
const currentTime = Date.now();
const elapsedTime = currentTime - startTime;
const remainingTime = endTime - currentTime;
log(`waitForFileOpen() - Attempt ${attemptCount}, Elapsed: ${elapsedTime}ms, Remaining: ${remainingTime}ms`);
try {
const editorView = workbench.getEditorView();
log(`waitForFileOpen() - Got editor view`);
// Try to get the active tab with a shorter timeout
let activeTab;
try {
// Use a promise race to implement our own timeout for getActiveTab
activeTab = await Promise.race([
editorView.getActiveTab(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('getActiveTab timeout')), 5000)
)
]);
} catch (tabError) {
log(`waitForFileOpen() - getActiveTab failed: ${tabError}`);
// Try alternative approach: check if any tabs exist
try {
const tabs = await editorView.getOpenTabs();
log(`waitForFileOpen() - Found ${tabs.length} open tabs`);
if (tabs.length > 0) {
// Try to find our target file in the open tabs
for (const tab of tabs) {
try {
const tabTitle = await tab.getTitle();
log(`waitForFileOpen() - Checking tab: "${tabTitle}"`);
if (tabTitle === fileName) {
log(`waitForFileOpen() - SUCCESS: Found target file in open tabs after ${elapsedTime}ms and ${attemptCount} attempts`);
return;
}
} catch (titleError) {
log(`waitForFileOpen() - Error getting tab title: ${titleError}`);
}
}
}
} catch (tabsError) {
log(`waitForFileOpen() - Error getting open tabs: ${tabsError}`);
}
// Continue to next iteration if all approaches failed
log(`waitForFileOpen() - All tab approaches failed, continuing to wait...`);
await new Promise(resolve => setTimeout(resolve, checkInterval));
continue;
}
if (activeTab) {
const tabTitle = await activeTab.getTitle();
log(`waitForFileOpen() - Active tab title: "${tabTitle}", Expected: "${fileName}"`);
if (tabTitle === fileName) {
log(`waitForFileOpen() - SUCCESS: File ${fileName} is now open in editor after ${elapsedTime}ms and ${attemptCount} attempts`);
return; // File is open, success!
} else {
log(`waitForFileOpen() - File not matched, continuing to wait...`);
}
} else {
log(`waitForFileOpen() - No active tab found, continuing to wait...`);
}
} catch (error) {
// Continue checking if there's an error getting editor state
log(`waitForFileOpen() - Error checking file open status (attempt ${attemptCount}): ${error}`);
}
// Wait before next check
log(`waitForFileOpen() - Waiting ${checkInterval}ms before next check...`);
await new Promise(resolve => setTimeout(resolve, checkInterval));
}
// If we reach here, timeout occurred
const totalElapsed = Date.now() - startTime;
const errorMsg = `Timeout: File ${fileName} was not found open in editor after ${totalElapsed}ms (${attemptCount} attempts)`;
log(`waitForFileOpen() - TIMEOUT: ${errorMsg}`);
throw new Error(errorMsg);
}
/**
* Opens a file and retrieves its text content
* @param filePath - The path to the file to open
* @returns The text content of the file
*/
export async function attemptToFindTextEditorText(filePath: string): Promise<string> {
await openFile(filePath);
const fileName = filePath.substring(filePath.lastIndexOf(process.platform === 'win32' ? '\\' : '/') + 1);
const editorView = new EditorView();
const editor = await editorView.openEditor(fileName);
return await editor.getText();
}
/**
* Overwrites the entire content of a file using filesystem operations
* @param textEditor - The text editor instance containing the file to modify
* @param classText - The new content to write to the file
* @throws Error if file write operation fails or content verification fails
*/
export async function overrideTextInFile(textEditor: TextEditor, classText: string) {
// Use fs.writeFileSync() to write the new content to the file
try {
const filePath = await textEditor.getFilePath();
// Write new content to file
log(`overrideTextInFile() - Writing content to file: ${filePath}`);
createOrOverwriteFile(filePath, classText);
log(`overrideTextInFile() - Successfully wrote ${classText.length} characters to ${filePath}`);
// Give the editor time to detect the file change and reload
await pause(Duration.seconds(1));
// Verify the write was successful by reading it back
const writtenContent = await fs.readFile(filePath, 'utf8');
const normalizeText = (str: string) => str.replace(/\s+/g, '');
const normalizedWritten = normalizeText(writtenContent);
const normalizedExpected = normalizeText(classText);
if (normalizedWritten !== normalizedExpected) {
throw new Error(`File write verification failed. Written content does not match expected content.`);
}
log('overrideTextInFile() - File content verified successfully');
} catch (error) {
log(`overrideTextInFile() - File system operation failed: ${error}`);
throw new Error(`File system text replacement failed: ${error}`);
}
}
export async function waitForAndGetCodeLens(textEditor: TextEditor, codeLensName: string): Promise<CodeLens> {
log(`waitForAndGetCodeLens() - Waiting for code lens: ${codeLensName}`);
return await retryOperation(async () => {
const lens = await textEditor.getCodeLens(codeLensName);
if (!lens) {
log(`waitForAndGetCodeLens() - Code lens ${codeLensName} NOT FOUND`);
throw new Error(`Code lens ${codeLensName} not found`);
}
log(`waitForAndGetCodeLens() - Code lens ${codeLensName} found`);
return lens;
}, 3);
}
/**
* Wrapper function for moveCursor with fallback mechanism using inputarea monaco-mouse-cursor-text selector
* @param textEditor - The text editor instance
* @param line - The line number to move to
* @param column - The column number to move to
*/
export const moveCursorWithFallback = async (textEditor: TextEditor, line: number, column: number) => {
try {
// First try the original moveCursor approach
await textEditor.moveCursor(line, column);
log(`moveCursorWithFallback() - Successfully moved cursor to line ${line}, column ${column} using original method`);
} catch (error) {
// Fallback when moveCursor fails due to .native-edit-context selector issues
log(`moveCursorWithFallback() - Original moveCursor failed, trying fallback approach: ${String(error)}`);
try {
// Find the editor element using the fallback selector
const browser = getBrowser();
const editorElement = await browser.findElement(By.css('.inputarea.monaco-mouse-cursor-text'));
if (!editorElement) {
throw new Error('Could not find editor element using fallback selector .inputarea.monaco-mouse-cursor-text');
}
log('moveCursorWithFallback() - Found editor element using fallback selector');
// Get current coordinates (fallback method)
const getCurrentCoordinates = async () => {
try {
return await textEditor.getCoordinates();
} catch (coordError) {
log(`moveCursorWithFallback() - getCoordinates() failed: ${String(coordError)}`);
// Return a default position if coordinates can't be retrieved
return [1, 1];
}
};
// Check if we're at the target line
const isAtLine = async (targetLine: number) => {
try {
const coords = await getCurrentCoordinates();
return coords[0] === targetLine;
} catch {
return false;
}
};
// Check if we're at the target column
const isAtColumn = async (targetColumn: number) => {
try {
const coords = await getCurrentCoordinates();
return coords[1] === targetColumn;
} catch {
return false;
}
};
// Move to target line (mimic moveCursorToLine)
const coordinates = await getCurrentCoordinates();
const lineGap = coordinates[0] - line;
const lineKey = lineGap >= 0 ? Key.UP : Key.DOWN;
for (let i = 0; i < Math.abs(lineGap); i++) {
if (await isAtLine(line)) {
break;
}
await editorElement.sendKeys(lineKey);
await pause(Duration.milliseconds(50));
}
log(`moveCursorWithFallback() - Successfully moved cursor to line ${line} using fallback method`);
// Move to target column (mimic moveCursorToColumn)
const currentCoords = await getCurrentCoordinates();
const columnGap = currentCoords[1] - column;
const columnKey = columnGap >= 0 ? Key.LEFT : Key.RIGHT;
for (let i = 0; i < Math.abs(columnGap); i++) {
if (await isAtColumn(column)) {
break;
}
await editorElement.sendKeys(columnKey);
await pause(Duration.milliseconds(50));
// Check if we moved to a different line (mimic original error handling)
const newCoords = await getCurrentCoordinates();
if (newCoords[0] !== currentCoords[0]) {
throw new Error(`Column number ${column} is not accessible on line ${currentCoords[0]}`);
}
}
log(`moveCursorWithFallback() - Successfully moved cursor to column ${column} using fallback method`);
} catch (fallbackError) {
log(`moveCursorWithFallback() - Fallback approach also failed: ${String(fallbackError)}`);
throw new Error(`Failed to move cursor using both original and fallback methods: ${String(fallbackError)}`);
}
}
};
/** Replace a specific line in a file using fs operations
* @param filePath - The path to the file to modify
* @param lineNumber - The line number to replace (1-based index)
* @param newContent - The new content to write
*/
export async function replaceLineInFile(filePath: string, lineNumber: number, newContent: string): Promise<void> {
const fileContent = await fs.readFile(filePath, 'utf8');
const lines = fileContent.split('\n');
// Replace the specific line (1-based to 0-based index)
lines[lineNumber - 1] = newContent;
createOrOverwriteFile(filePath, lines.join('\n'));
}