diff --git a/src/shared/TestRunService.ts b/src/shared/TestRunService.ts index bcd4f3e0..ad0b1a15 100644 --- a/src/shared/TestRunService.ts +++ b/src/shared/TestRunService.ts @@ -120,7 +120,7 @@ export class TestRunService { config: TestRunConfig, cancellationToken: CancellationTokenSource ): Promise { - const testCategory = TestRunService.getTestCategory(flags, config); + const testCategory = TestRunService.getTestCategory(flags, config, testLevel); const payload = { ...(await testService.buildSyncPayload( testLevel, @@ -149,7 +149,7 @@ export class TestRunService { config: TestRunConfig, cancellationToken: CancellationTokenSource ): Promise { - const testCategory = TestRunService.getTestCategory(flags, config); + const testCategory = TestRunService.getTestCategory(flags, config, testLevel); const payload = { ...(await testService.buildAsyncPayload( testLevel, @@ -178,11 +178,14 @@ export class TestRunService { /** * Get test category based on command type and flags - * - apex command: always returns 'Apex' + * - apex command: returns empty string for RunSpecifiedTests, 'Apex' for other test levels * - logic command: returns test-category flag value or defaults to all categories */ - private static getTestCategory(flags: TestRunFlags, config: TestRunConfig): string { + private static getTestCategory(flags: TestRunFlags, config: TestRunConfig, testLevel: string): string { if (config.commandType === 'apex') { + if (testLevel === 'RunSpecifiedTests') { + return ''; + } return 'Apex'; } // logic command diff --git a/test/commands/apex/run/test.nut.ts b/test/commands/apex/run/test.nut.ts index bed7aa28..011185c1 100644 --- a/test/commands/apex/run/test.nut.ts +++ b/test/commands/apex/run/test.nut.ts @@ -38,7 +38,7 @@ describe('apex run test', () => { }, ], }); - + addTestSuiteFile(session.project.dir); execCmd('project:deploy:start -o org --source-dir force-app', { ensureExitCode: 0, cli: 'sf' }); }); @@ -210,6 +210,16 @@ describe('apex run test', () => { expect(result2).to.match(/Tests Ran\s+10/); }); + it('will run specified test suites --class-names', async () => { + const result = execCmd('apex:run:test -w 10 --suite-names DreamhouseTestSuite', { ensureExitCode: 0 }) + .shellOutput.stdout; + expect(result).to.match(/Tests Ran\s+10/); + expect(result).to.include('FileUtilitiesTest.'); + expect(result).to.include('TestPropertyController.'); + expect(result).to.include('GeocodingServiceTest.'); + expect(result).to.include('GeocodingServiceTest.'); + }); + it('will run specified tests --tests', async () => { const result = execCmd('apex:run:test -w 10 --tests TestPropertyController', { ensureExitCode: 0 }).shellOutput .stdout; @@ -263,3 +273,19 @@ describe('apex run test', () => { execCmd(`apex:get:test -i ${id}`, { ensureExitCode: 0 }); }); }); + +function addTestSuiteFile(projectDir: string): void { + const testSuitesDir = path.join(projectDir, 'force-app', 'main', 'default', 'testSuites'); + if (!fs.existsSync(testSuitesDir)) { + fs.mkdirSync(testSuitesDir, { recursive: true }); + } + + const xmlContent = ` + + FileUtilitiesTest + GeocodingServiceTest + TestPropertyController + `; + const filePath = path.join(testSuitesDir, 'DreamhouseTestSuite.testSuite-meta.xml'); + fs.writeFileSync(filePath, xmlContent, 'utf8'); +} diff --git a/test/commands/apex/run/test.test.ts b/test/commands/apex/run/test.test.ts index eea556bd..a3391e38 100644 --- a/test/commands/apex/run/test.test.ts +++ b/test/commands/apex/run/test.test.ts @@ -148,7 +148,7 @@ describe('apex:test:run', () => { ]); expect(buildPayloadSpy.calledOnce).to.be.true; expect(runTestSynchronousSpy.calledOnce).to.be.true; - expect(buildPayloadSpy.firstCall.args).to.deep.equal(['RunSpecifiedTests', undefined, 'myApex', 'Apex']); + expect(buildPayloadSpy.firstCall.args).to.deep.equal(['RunSpecifiedTests', undefined, 'myApex', '']); expect(runTestSynchronousSpy.firstCall.args[0]).to.deep.equal({ skipCodeCoverage: false, testLevel: 'RunSpecifiedTests', @@ -166,31 +166,26 @@ describe('apex:test:run', () => { .stub(TestService.prototype, 'runTestAsynchronous') .resolves(runWithCoverage); await Test.run([ - '--class-names', - 'myApex', '--code-coverage', '--result-format', 'human', '--test-level', - 'RunSpecifiedTests', + 'RunLocalTests', ]); expect(buildPayloadSpy.calledOnce).to.be.true; expect(runTestSynchronousSpy.calledOnce).to.be.true; expect(buildPayloadSpy.firstCall.args).to.deep.equal([ - 'RunSpecifiedTests', + 'RunLocalTests', + undefined, undefined, - 'myApex', undefined, 'Apex', ]); expect(runTestSynchronousSpy.firstCall.args[0]).to.deep.equal({ + category: ['Apex'], skipCodeCoverage: false, - testLevel: 'RunSpecifiedTests', - tests: [ - { - className: 'myApex', - }, - ], + suiteNames: undefined, + testLevel: 'RunLocalTests', }); }); @@ -208,7 +203,7 @@ describe('apex:test:run', () => { undefined, 'myApex', undefined, - 'Apex', + '', ]); expect(runTestSynchronousSpy.firstCall.args[0]).to.deep.equal({ skipCodeCoverage: true, @@ -228,7 +223,7 @@ describe('apex:test:run', () => { await Test.run(['--class-names', 'myApex', '--synchronous', '--test-level', 'RunSpecifiedTests']); expect(buildPayloadSpy.calledOnce).to.be.true; expect(runTestSynchronousSpy.calledOnce).to.be.true; - expect(buildPayloadSpy.firstCall.args).to.deep.equal(['RunSpecifiedTests', undefined, 'myApex', 'Apex']); + expect(buildPayloadSpy.firstCall.args).to.deep.equal(['RunSpecifiedTests', undefined, 'myApex', '']); expect(runTestSynchronousSpy.firstCall.args[0]).to.deep.equal({ skipCodeCoverage: true, testLevel: 'RunSpecifiedTests', @@ -331,7 +326,6 @@ describe('apex:test:run', () => { await Test.run(['--suite-names', 'MyApexTests,MySecondTest', '--result-format', 'human']); expect(apexStub.firstCall.args[0]).to.deep.equal({ - category: ['Apex'], skipCodeCoverage: true, testLevel: 'RunSpecifiedTests', suiteNames: 'MyApexTests,MySecondTest', @@ -343,7 +337,6 @@ describe('apex:test:run', () => { await Test.run(['-s', 'MyApexTests', '-s', 'MySecondTest', '--result-format', 'human']); expect(apexStub.firstCall.args[0]).to.deep.equal({ - category: ['Apex'], skipCodeCoverage: true, testLevel: 'RunSpecifiedTests', suiteNames: 'MyApexTests,MySecondTest', @@ -416,7 +409,7 @@ describe('apex:test:run', () => { ]); expect(buildPayloadSpy.calledOnce).to.be.true; expect(runTestSynchronousSpy.calledOnce).to.be.true; - expect(buildPayloadSpy.firstCall.args).to.deep.equal(['RunSpecifiedTests', undefined, 'myApex', 'Apex']); + expect(buildPayloadSpy.firstCall.args).to.deep.equal(['RunSpecifiedTests', undefined, 'myApex', '']); expect(runTestSynchronousSpy.firstCall.args[0]).to.deep.equal({ skipCodeCoverage: false, testLevel: 'RunSpecifiedTests', @@ -434,31 +427,26 @@ describe('apex:test:run', () => { .stub(TestService.prototype, 'runTestAsynchronous') .resolves(runWithCoverage); await Test.run([ - '--class-names', - 'myApex', '--code-coverage', '--result-format', 'human', '--test-level', - 'RunSpecifiedTests', + 'RunLocalTests', ]); expect(buildPayloadSpy.calledOnce).to.be.true; expect(runTestSynchronousSpy.calledOnce).to.be.true; expect(buildPayloadSpy.firstCall.args).to.deep.equal([ - 'RunSpecifiedTests', + 'RunLocalTests', + undefined, undefined, - 'myApex', undefined, 'Apex', ]); expect(runTestSynchronousSpy.firstCall.args[0]).to.deep.equal({ + category: ['Apex'], skipCodeCoverage: false, - testLevel: 'RunSpecifiedTests', - tests: [ - { - className: 'myApex', - }, - ], + suiteNames: undefined, + testLevel: 'RunLocalTests' }); }); @@ -474,7 +462,7 @@ describe('apex:test:run', () => { undefined, 'myApex', undefined, - 'Apex', + '', ]); expect(runTestSynchronousSpy.firstCall.args[0]).to.deep.equal({ skipCodeCoverage: true, @@ -494,7 +482,7 @@ describe('apex:test:run', () => { await Test.run(['--class-names', 'myApex', '--synchronous', '--test-level', 'RunSpecifiedTests']); expect(buildPayloadSpy.calledOnce).to.be.true; expect(runTestSynchronousSpy.calledOnce).to.be.true; - expect(buildPayloadSpy.firstCall.args).to.deep.equal(['RunSpecifiedTests', undefined, 'myApex', 'Apex']); + expect(buildPayloadSpy.firstCall.args).to.deep.equal(['RunSpecifiedTests', undefined, 'myApex', '']); expect(runTestSynchronousSpy.firstCall.args[0]).to.deep.equal({ skipCodeCoverage: true, testLevel: 'RunSpecifiedTests', diff --git a/test/shared/TestRunService.test.js b/test/shared/TestRunService.test.js new file mode 100644 index 00000000..42399fb4 --- /dev/null +++ b/test/shared/TestRunService.test.js @@ -0,0 +1,206 @@ +/* + * Copyright 2025, 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 { expect } from 'chai'; +import sinon from 'sinon'; +import { CancellationTokenSource, TestService } from '@salesforce/apex-node'; +import { TestRunService } from '../../lib/shared/TestRunService.js'; + +describe('Common TestRunService behavior', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('apex command type behavior', () => { + it('should pass empty string as testCategory for RunSpecifiedTests test-level', async () => { + const flags = { + 'test-level': 'RunSpecifiedTests', + 'class-names': ['TestClass'], + synchronous: true, + }; + const config = { + commandType: 'apex', + exclusiveTestSpecifiers: [], + binName: 'sf', + }; + + const mockConnection = {}; + const cancellationToken = new CancellationTokenSource(); + const context = { + flags, + config, + connection: mockConnection, + jsonEnabled: false, + cancellationToken, + log: sinon.stub(), + info: sinon.stub(), + }; + + // Spy on buildSyncPayload to verify the testCategory parameter + const buildSyncPayloadSpy = sandbox.spy(TestService.prototype, 'buildSyncPayload'); + + // Stub runTestSynchronous to avoid actual execution + sandbox.stub(TestService.prototype, 'runTestSynchronous').resolves({ + summary: { + outcome: 'Passed', + testsRan: 1, + failRate: '0%', + orgId: '00D4xx00000FH4IEAW', + passing: 1, + failing: 0, + skipped: 0, + passRate: '100%', + skipRate: '0%', + testStartTime: '2020-08-25T00:48:02.000+0000', + testExecutionTimeInMs: 53, + testTotalTime: '53 ms', + testRunId: '707xx0000AUS2gH', + userId: '005xx000000uEgSAAU', + username: 'test@example.com', + hostname: 'https://na139.salesforce.com', + commandTime: '60 ms', + testExecutionTime: '53 ms' + }, + tests: [] + }); + + await TestRunService.runTestCommand(context); + + expect(buildSyncPayloadSpy.calledOnce).to.be.true; + // The fourth parameter should be empty string for RunSpecifiedTests + expect(buildSyncPayloadSpy.firstCall.args[3]).to.equal(''); + }); + + it('should pass "Apex" as testCategory for RunLocalTests test-level', async () => { + const flags = { + 'test-level': 'RunLocalTests', + }; + const config = { + commandType: 'apex', + exclusiveTestSpecifiers: [], + binName: 'sf', + }; + + const mockConnection = {}; + const cancellationToken = new CancellationTokenSource(); + const context = { + flags, + config, + connection: mockConnection, + jsonEnabled: false, + cancellationToken, + log: sinon.stub(), + info: sinon.stub(), + }; + + // Spy on buildAsyncPayload to verify the testCategory parameter (RunLocalTests uses async by default) + const buildAsyncPayloadSpy = sandbox.spy(TestService.prototype, 'buildAsyncPayload'); + + // Stub runTestAsynchronous to avoid actual execution + sandbox.stub(TestService.prototype, 'runTestAsynchronous').resolves({ + testRunId: '707xx0000AUS2gH', + }); + + await TestRunService.runTestCommand(context); + + expect(buildAsyncPayloadSpy.calledOnce).to.be.true; + // The fifth parameter should be "Apex" for RunLocalTests + expect(buildAsyncPayloadSpy.firstCall.args[4]).to.equal('Apex'); + }); + }); + + describe('logic command', () => { + it('should pass empty string as testCategory when no test-category flag is provided', async () => { + const flags = { + 'test-level': 'RunLocalTests', + }; + const config = { + commandType: 'logic', + exclusiveTestSpecifiers: [], + binName: 'sf', + }; + + const mockConnection = {}; + const cancellationToken = new CancellationTokenSource(); + const context = { + flags, + config, + connection: mockConnection, + jsonEnabled: false, + cancellationToken, + log: sinon.stub(), + info: sinon.stub(), + }; + + // Spy on buildAsyncPayload to verify the testCategory parameter (logic uses async by default) + const buildAsyncPayloadSpy = sandbox.spy(TestService.prototype, 'buildAsyncPayload'); + + // Stub runTestAsynchronous to avoid actual execution + sandbox.stub(TestService.prototype, 'runTestAsynchronous').resolves({ + testRunId: '707xx0000AUS2gH', + }); + + await TestRunService.runTestCommand(context); + + expect(buildAsyncPayloadSpy.calledOnce).to.be.true; + // The fifth parameter should be empty string for logic without test-category + expect(buildAsyncPayloadSpy.firstCall.args[4]).to.equal(''); + }); + + it('should pass test-category value when test-category flag is provided', async () => { + const flags = { + 'test-level': 'RunLocalTests', + 'test-category': ['Flow', 'Apex'], + }; + const config = { + commandType: 'logic', + exclusiveTestSpecifiers: [], + binName: 'sf', + }; + + const mockConnection = {}; + const cancellationToken = new CancellationTokenSource(); + const context = { + flags, + config, + connection: mockConnection, + jsonEnabled: false, + cancellationToken, + log: sinon.stub(), + info: sinon.stub(), + }; + + // Spy on buildAsyncPayload to verify the testCategory parameter (logic uses async by default) + const buildAsyncPayloadSpy = sandbox.spy(TestService.prototype, 'buildAsyncPayload'); + + // Stub runTestAsynchronous to avoid actual execution + sandbox.stub(TestService.prototype, 'runTestAsynchronous').resolves({ + testRunId: '707xx0000AUS2gH', + }); + + await TestRunService.runTestCommand(context); + + expect(buildAsyncPayloadSpy.calledOnce).to.be.true; + // The fifth parameter should be "Flow,Apex" for logic with test-category + expect(buildAsyncPayloadSpy.firstCall.args[4]).to.equal('Flow,Apex'); + }); + }); +}); \ No newline at end of file