@salesforce/plugin-apex
Version:
164 lines • 8.71 kB
JavaScript
/*
* Copyright 2026, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TestService } from '@salesforce/apex-node';
import { Ux } from '@salesforce/sf-plugins-core';
import { Messages, SfError } from '@salesforce/core';
import { TestReporter } from '../reporters/index.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-apex', 'runtestcommon');
export const TestLevelValues = ['RunLocalTests', 'RunAllTestsInOrg', 'RunSpecifiedTests'];
/**
* Shared service for running tests with command-specific behavior
*/
export class TestRunService {
static async runTestCommand(context) {
const { flags, config, connection, cancellationToken } = context;
const testLevel = await TestRunService.validateFlags(flags['class-names'], flags['suite-names'], flags.tests, flags.synchronous, flags['test-level'], flags['test-category'], config);
const testService = new TestService(connection);
// NOTE: This is a *bug*. Synchronous test runs should throw an error when multiple test classes are specified
// This was re-introduced due to https://github.com/forcedotcom/salesforcedx-vscode/issues/3154
// Address with W-9163533
const result = flags.synchronous && testLevel === "RunSpecifiedTests" /* TestLevel.RunSpecifiedTests */
? await TestRunService.runTest(testService, flags, testLevel, config, cancellationToken)
: await TestRunService.runTestAsynchronous(testService, flags, testLevel, config, cancellationToken);
if (cancellationToken.token.isCancellationRequested) {
throw new SfError('Cancelled');
}
if ('summary' in result) {
const testReporter = new TestReporter(new Ux({ jsonEnabled: context.jsonEnabled }), connection);
const reportFlags = {
...flags,
concise: flags.concise ?? false,
...(config.commandType === 'logic' ? { isUnifiedLogic: true } : {}),
};
return testReporter.report(result, reportFlags);
}
else {
// Tests were ran asynchronously or the --wait timed out.
// Log the proper 'get test' command for the user to run later
TestRunService.logAsyncTestInstructions(result, connection.getUsername?.() ?? 'unknown', config, context.log);
context.info(messages.getMessage('runTestSyncInstructions'));
if (flags['output-dir']) {
// testService writes a file with just the test run id in it to test-run-id.txt
// github.com/forcedotcom/salesforcedx-apex/blob/c986abfabee3edf12f396f1d2e43720988fa3911/src/tests/testService.ts#L245-L246
await testService.writeResultFiles(result, { dirPath: flags['output-dir'] }, flags['code-coverage']);
}
return result;
}
}
static async runTest(testService, flags, testLevel, config, cancellationToken) {
const testCategory = TestRunService.getTestCategory(flags, config, testLevel);
const payload = {
...(await testService.buildSyncPayload(testLevel, flags.tests?.join(','), flags['class-names']?.join(','), testCategory)),
skipCodeCoverage: !flags['code-coverage'],
};
try {
return (await testService.runTestSynchronous(payload, flags['code-coverage'], cancellationToken.token));
}
catch (e) {
throw TestRunService.handleTestingServerError(SfError.wrap(e), flags, testLevel, config);
}
}
static async runTestAsynchronous(testService, flags, testLevel, config, cancellationToken) {
const testCategory = TestRunService.getTestCategory(flags, config, testLevel);
const payload = {
...(await testService.buildAsyncPayload(testLevel, flags.tests?.join(','), flags['class-names']?.join(','), flags['suite-names']?.join(','), testCategory)),
skipCodeCoverage: !flags['code-coverage'],
};
try {
// cast as TestRunIdResult because we're building an async payload which will return an async result
return (await testService.runTestAsynchronous(payload, flags['code-coverage'], flags.wait && flags.wait.minutes > 0 ? false : !(flags.synchronous && !flags.json), undefined, cancellationToken.token, flags.wait, flags['poll-interval']));
}
catch (e) {
throw TestRunService.handleTestingServerError(SfError.wrap(e), flags, testLevel, config);
}
}
/**
* Get test category based on command type and flags
* - apex command: returns empty string for RunSpecifiedTests, 'Apex' for other test levels
* - logic command: returns test-category flag value or defaults to all categories
*/
static getTestCategory(flags, config, testLevel) {
if (config.commandType === 'apex') {
if (testLevel === 'RunSpecifiedTests') {
return '';
}
return 'Apex';
}
// logic command
return flags['test-category']?.join(',') ?? '';
}
/**
* Log appropriate async test instructions based on command type
*/
static logAsyncTestInstructions(result, username, config, log) {
if (config.commandType === 'logic') {
log(messages.getMessage('runLogicTestReportCommand', [config.binName, result.testRunId, username]));
}
else {
log(messages.getMessage('runTestReportCommand', [config.binName, result.testRunId, username]));
}
}
/**
* Handle testing server errors with command-specific messaging
*/
static handleTestingServerError(error, flags, testLevel, config) {
if (!error.message.includes('Always provide a classes, suites, tests, or testLevel property')) {
return error;
}
// If error message condition is valid, return the original error.
const hasSpecifiedTestLevel = testLevel === "RunSpecifiedTests" /* TestLevel.RunSpecifiedTests */;
const hasNoTestNames = !flags.tests?.length;
const hasNoClassNames = !flags['class-names']?.length;
const hasNoSuiteNames = !flags['suite-names']?.length;
if (hasSpecifiedTestLevel && hasNoTestNames && hasNoClassNames && hasNoSuiteNames) {
return error;
}
// Otherwise, assume there are no tests in the org and return clearer message.
const testType = config.commandType === 'apex' ? 'Apex' : 'Logic';
return Object.assign(error, {
message: `There are no ${testType} tests to run in this org.`,
actions: [`Ensure ${testType} Tests exist in the org, and try again.`],
});
}
/**
* Validate flags with command-specific logic
*/
static async validateFlags(classNames, suiteNames, tests, synchronous, testLevel, testCategory, config) {
if (synchronous && (Boolean(suiteNames) || (classNames && classNames.length > 1))) {
return config?.commandType === 'apex'
? Promise.reject(new Error(messages.getMessage('syncClassErr')))
: Promise.reject(new Error(messages.getMessage('syncClassErrForUnifiedLogic')));
}
// Validate that test-level is required when test-category is specified (logic command only)
if (config?.commandType === 'logic' && testCategory && testCategory.length > 0 && !testLevel) {
return Promise.reject(new Error('When using --test-category, you must also specify --test-level.'));
}
if ((Boolean(tests) || Boolean(classNames) || suiteNames) &&
testLevel &&
testLevel.toString() !== 'RunSpecifiedTests') {
return Promise.reject(new Error(messages.getMessage('testLevelErr')));
}
if (testLevel) {
return testLevel;
}
if (Boolean(classNames) || Boolean(suiteNames) || tests) {
return "RunSpecifiedTests" /* TestLevel.RunSpecifiedTests */;
}
return "RunLocalTests" /* TestLevel.RunLocalTests */;
}
}
//# sourceMappingURL=TestRunService.js.map