UNPKG

@salesforce/salesforcedx-vscode-test-tools

Version:
356 lines (311 loc) 14.7 kB
/* * 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 * as path from 'path'; import fs from 'fs'; import { EnvironmentSettings as Env } from './environmentSettings'; import * as core from './core/index'; import { ProjectConfig, ProjectShapeOption, ExtensionConfig } from './core/types'; import { deleteScratchOrg, createFolder, generateSfProject, gitRepoExists, getRepoNameFromUrl, getFolderName, gitClone } from './system-operations'; import { verifyExtensionsAreRunning, reloadAndEnableExtensions, checkForUncaughtErrors } from './testing'; import { executeQuickPick, reloadWindow, verifyProjectLoaded } from './ui-interaction'; import { setUpScratchOrg } from './salesforce-components'; import { retryOperation } from './retryUtils'; export class TestSetup { public testSuiteSuffixName = ''; // this needs to be defined in EnvironmentSettings with the default being os.tmpDir() public tempFolderPath = path.join(process.cwd(), 'e2e-temp'); public projectFolderPath: string | undefined; public testDataFolderPath: string | undefined; public aliasAndUserNameWereVerified = false; public scratchOrgAliasName: string | undefined; public scratchOrgId: string | undefined; private configuredExtensions: ExtensionConfig[] = []; private constructor() { // Private constructor to prevent direct instantiation - use TestSetup.setUp() instead } public get tempProjectName(): string { return 'TempProject-' + this.testSuiteSuffixName; } public static async setUp(testReqConfig: core.TestReqConfig): Promise<TestSetup> { const testSetup = new TestSetup(); testSetup.testSuiteSuffixName = testReqConfig.testSuiteSuffixName; core.log(''); core.log(`${testSetup.testSuiteSuffixName} - Starting TestSetup.setUp()...`); // Configure extensions based on test requirements testSetup.configureExtensions(testReqConfig); /* The expected workspace will be open up after setUpTestingWorkspace */ await testSetup.setUpTestingWorkspace(testReqConfig.projectConfig); await executeQuickPick('View: Close All Editors'); if (testReqConfig.projectConfig.projectShape !== ProjectShapeOption.NONE) { await verifyExtensionsAreRunning(testSetup.getExtensionsToVerify()); if (process.platform === 'darwin') testSetup.setJavaHomeConfigEntry(); // Extra config needed for Apex LSP on GHA if (testReqConfig.isOrgRequired) { await setUpScratchOrg(testSetup); } await reloadAndEnableExtensions(); // This is necessary in order to update JAVA home path } testSetup.setWindowDialogStyle(); testSetup.setWorkbenchHoverDelay(); testSetup.setMaximumWindowSize(); await reloadWindow(); // reload window to apply settings core.log(`${testSetup.testSuiteSuffixName} - ...finished TestSetup.setUp()`); return testSetup; } /** * Configure extensions based on test requirements * @param testReqConfig Test requirement configuration */ private configureExtensions(testReqConfig: core.TestReqConfig): void { // Use provided extensions or empty array if none provided this.configuredExtensions = testReqConfig.extensionConfigs || []; testReqConfig.extensionConfigs = testReqConfig.extensionConfigs || []; // If extensionConfigs is provided, configure the specific extensions if (testReqConfig.extensionConfigs) { if (testReqConfig.extensionConfigs.length === 0) { // Empty array means no extensions this.configuredExtensions = []; } else { // For each configured extension, update the corresponding default extension or add a new one for (const extConfig of testReqConfig.extensionConfigs) { const existingExtIndex = this.configuredExtensions.findIndex( ext => ext.extensionId === extConfig.extensionId ); if (existingExtIndex >= 0) { // Update existing extension configuration this.configuredExtensions[existingExtIndex] = { ...this.configuredExtensions[existingExtIndex], shouldInstall: extConfig.shouldInstall, shouldVerifyActivation: extConfig.shouldVerifyActivation, vsixPath: extConfig.vsixPath || this.configuredExtensions[existingExtIndex].vsixPath }; } else { // Add new extension configuration this.configuredExtensions.push({ extensionId: extConfig.extensionId, name: extConfig.name || extConfig.extensionId, // Use provided name or extensionId as name vsixPath: extConfig.vsixPath || '', shouldInstall: extConfig.shouldInstall, shouldVerifyActivation: extConfig.shouldVerifyActivation }); } } } } // For backward compatibility: handle excludedExtensions by setting shouldInstall to 'never' if (testReqConfig.excludedExtensions && testReqConfig.excludedExtensions.length > 0) { for (const excludedExtId of testReqConfig.excludedExtensions) { const extIndex = this.configuredExtensions.findIndex(ext => ext.extensionId === excludedExtId); if (extIndex >= 0) { this.configuredExtensions[extIndex].shouldInstall = 'never'; this.configuredExtensions[extIndex].shouldVerifyActivation = false; } } } core.log(`${this.testSuiteSuffixName} - Configured ${this.configuredExtensions.length} extensions for testing`); } /** * Get extensions that should be verified during test setup */ public getExtensionsToVerify(): ExtensionConfig[] { return this.configuredExtensions.filter(ext => ext.shouldVerifyActivation); } /** * Get extensions that should be installed with a specific installation option * @param option Installation option to filter by */ public getExtensionsToInstall(option: 'always' | 'never' | 'optional'): ExtensionConfig[] { return this.configuredExtensions.filter(ext => ext.shouldInstall === option); } public async tearDown(shouldCheckForUncaughtErrors = true): Promise<void> { if (shouldCheckForUncaughtErrors) await checkForUncaughtErrors(this.configuredExtensions); try { await deleteScratchOrg(this.scratchOrgAliasName); } catch (error) { core.log(`Deleting scratch org (or info) failed with Error: ${(error as Error).message}`); } } private async initializeNewSfProject() { if (!fs.existsSync(this.tempFolderPath)) { createFolder(this.tempFolderPath); } await generateSfProject(this.tempProjectName, this.tempFolderPath); // generate a sf project for 'new' this.projectFolderPath = path.join(this.tempFolderPath, this.tempProjectName); } public async setUpTestingWorkspace(projectConfig: ProjectConfig) { core.log(`${this.testSuiteSuffixName} - Starting setUpTestingWorkspace()...`); let projectName: string | null; switch (projectConfig.projectShape) { case ProjectShapeOption.NEW: await this.initializeNewSfProject(); break; case ProjectShapeOption.NAMED: if (projectConfig.githubRepoUrl) { // verify if folder matches the github repo url const repoExists = await gitRepoExists(projectConfig.githubRepoUrl); if (!repoExists) { this.throwError(`Repository does not exist or is inaccessible: ${projectConfig.githubRepoUrl}`); } const repoName = getRepoNameFromUrl(projectConfig.githubRepoUrl); if (!repoName) { this.throwError(`Unable to determine repository name from URL: ${projectConfig.githubRepoUrl}`); } else { projectName = repoName; if (projectConfig.folderPath) { const localProjName = getFolderName(projectConfig.folderPath); if (localProjName !== repoName) { this.throwError( `The local project ${localProjName} does not match the required Github repo ${repoName}` ); } else { // If it is a match, use the local folder directly. Local dev use only. this.projectFolderPath = projectConfig.folderPath; } } else { // Clone the project from Github URL directly this.projectFolderPath = path.join(this.tempFolderPath, repoName); await gitClone(projectConfig.githubRepoUrl, this.projectFolderPath); } } } else { // missing info, throw an error this.throwError(`githubRepoUrl is required for named project shape`); } break; case ProjectShapeOption.ANY: // ANY: workspace is designated to open when redhat vscode-extension-tester is initialized if (projectConfig.folderPath) { this.projectFolderPath = projectConfig.folderPath; projectName = getFolderName(projectConfig.folderPath); } else { // Fallback: if no folder specified, create a new sf project instead await this.initializeNewSfProject(); } break; case ProjectShapeOption.NONE: // NONE: no project open in the workspace by default /* create the e2e-temp folder to benefit further testing */ this.projectFolderPath = path.join(this.tempFolderPath, this.tempProjectName); if (!fs.existsSync(this.tempFolderPath)) { createFolder(this.tempFolderPath); } break; default: this.throwError(`Invalid project shape: ${projectConfig.projectShape}`); } if ([ProjectShapeOption.NAMED, ProjectShapeOption.NEW].includes(projectConfig.projectShape)) { core.log(`Project folder to open: ${this.projectFolderPath}`); // Verify the project was loaded. await retryOperation(async () => { if (this.projectFolderPath) { await core.openFolder(this.projectFolderPath); await verifyProjectLoaded(projectName ?? this.tempProjectName); } else { this.throwError('Project folder path is not set'); } }); } } private throwError(message: string) { core.log(message); throw new Error(message); } public updateScratchOrgDefWithEdition(scratchOrgEdition: core.OrgEdition) { if (scratchOrgEdition === 'enterprise') { if (!this.projectFolderPath) { this.throwError('Project folder path is not set'); return; // This line will never be reached, but helps TypeScript understand } const projectScratchDefPath = path.join(this.projectFolderPath, 'config', 'project-scratch-def.json'); let projectScratchDef = fs.readFileSync(projectScratchDefPath, 'utf8'); projectScratchDef = projectScratchDef.replace(`"edition": "Developer"`, `"edition": "Enterprise"`); fs.writeFileSync(projectScratchDefPath, projectScratchDef, 'utf8'); } } private setJavaHomeConfigEntry(): void { if (!this.projectFolderPath) { this.throwError('Project folder path is not set'); return; // This line will never be reached, but helps TypeScript understand } const vscodeSettingsPath = path.join(this.projectFolderPath, '.vscode', 'settings.json'); if (!Env.getInstance().javaHome) { return; } if (!fs.existsSync(path.dirname(vscodeSettingsPath))) { fs.mkdirSync(path.dirname(vscodeSettingsPath), { recursive: true }); } let settings = fs.existsSync(vscodeSettingsPath) ? JSON.parse(fs.readFileSync(vscodeSettingsPath, 'utf8')) : {}; settings = { ...settings, ...(process.env.JAVA_HOME ? { 'salesforcedx-vscode-apex.java.home': process.env.JAVA_HOME } : {}) }; fs.writeFileSync(vscodeSettingsPath, JSON.stringify(settings, null, 2), 'utf8'); core.log( `${this.testSuiteSuffixName} - Set 'salesforcedx-vscode-apex.java.home' to '${process.env.JAVA_HOME}' in ${vscodeSettingsPath}` ); } private setWorkbenchHoverDelay(): void { if (!this.projectFolderPath) { this.throwError('Project folder path is not set'); return; // This line will never be reached, but helps TypeScript understand } const vscodeSettingsPath = path.join(this.projectFolderPath, '.vscode', 'settings.json'); if (!fs.existsSync(path.dirname(vscodeSettingsPath))) { fs.mkdirSync(path.dirname(vscodeSettingsPath), { recursive: true }); } let settings = fs.existsSync(vscodeSettingsPath) ? JSON.parse(fs.readFileSync(vscodeSettingsPath, 'utf8')) : {}; // Update settings to set workbench.hover.delay settings = { ...settings, 'workbench.hover.delay': 300000 }; fs.writeFileSync(vscodeSettingsPath, JSON.stringify(settings, null, 2), 'utf8'); core.log(`${this.testSuiteSuffixName} - Set 'workbench.hover.delay' to '300000' in ${vscodeSettingsPath}`); } private setMaximumWindowSize(): void { if (!this.projectFolderPath) { return; } const vscodeSettingsPath = path.join(this.projectFolderPath, '.vscode', 'settings.json'); if (!fs.existsSync(path.dirname(vscodeSettingsPath))) { fs.mkdirSync(path.dirname(vscodeSettingsPath), { recursive: true }); } let settings = fs.existsSync(vscodeSettingsPath) ? JSON.parse(fs.readFileSync(vscodeSettingsPath, 'utf8')) : {}; // Update settings to set window.newWindowDimensions settings = { ...settings, 'window.newWindowDimensions': 'maximized' }; fs.writeFileSync(vscodeSettingsPath, JSON.stringify(settings, null, 2), 'utf8'); core.log(`${this.testSuiteSuffixName} - Set 'window.newWindowDimensions' to 'maximized' in ${vscodeSettingsPath}`); } private setWindowDialogStyle(): void { if (!this.projectFolderPath) { this.throwError('Project folder path is not set'); return; // This line will never be reached, but helps TypeScript understand } const vscodeSettingsPath = path.join(this.projectFolderPath, '.vscode', 'settings.json'); if (!fs.existsSync(path.dirname(vscodeSettingsPath))) { fs.mkdirSync(path.dirname(vscodeSettingsPath), { recursive: true }); } let settings = fs.existsSync(vscodeSettingsPath) ? JSON.parse(fs.readFileSync(vscodeSettingsPath, 'utf8')) : {}; // Update settings to set window.dialogStyle settings = { ...settings, 'window.dialogStyle': 'custom' }; fs.writeFileSync(vscodeSettingsPath, JSON.stringify(settings, null, 2), 'utf8'); core.log(`${this.testSuiteSuffixName} - Set 'window.dialogStyle' to 'custom' in ${vscodeSettingsPath}`); } }