diff --git a/docs/man_pages/project/testing/dev-test-android.md b/docs/man_pages/project/testing/dev-test-android.md index a23194f3c5..a009ec1269 100644 --- a/docs/man_pages/project/testing/dev-test-android.md +++ b/docs/man_pages/project/testing/dev-test-android.md @@ -9,7 +9,7 @@ Run tests on a selected device | `$ ns test android --device [--watc Runs the tests in your project on connected Android devices and running native emulators.<% if(isConsole) { %> Your project must already be configured for unit testing by running `$ ns test init`.<% } %> ### Options -* `--watch` - If set, when you save changes to the project, changes are automatically synchronized to the connected device and tests are re-run. +* `--watch` - Enabled by default; when you save changes to the project, changes are automatically synchronized to the connected device and tests are re-run. Pass `--no-watch` to run the tests once. Defaults to disabled in CI environments (when the `CI` or `JENKINS_HOME` environment variable is set). * `--device` - Specifies the serial number or the index of the connected device on which to run the tests. To list all connected devices, grouped by platform, run `$ ns device` * `--debug-brk` - Runs the tests under the debugger. The debugger will break just before your tests are executed, so you have a chance to place breakpoints. diff --git a/docs/man_pages/project/testing/dev-test-ios.md b/docs/man_pages/project/testing/dev-test-ios.md index e441336582..f6b914dd53 100644 --- a/docs/man_pages/project/testing/dev-test-ios.md +++ b/docs/man_pages/project/testing/dev-test-ios.md @@ -13,7 +13,7 @@ Runs the tests in your project on connected iOS devices or the iOS Simulator.<% <% if((isConsole && isMacOS) || isHtml) { %> ### Options -* `--watch` - If set, when you save changes to the project, changes are automatically synchronized to the connected device and tests are re-ran. +* `--watch` - Enabled by default; when you save changes to the project, changes are automatically synchronized to the connected device and tests are re-ran. Pass `--no-watch` to run the tests once. Defaults to disabled in CI environments (when the `CI` or `JENKINS_HOME` environment variable is set). * `--device` - Specifies the serial number or the index of the connected device on which you want to run tests. To list all connected devices, grouped by platform, run `$ ns device`. You cannot set `--device` and `--emulator` simultaneously. * `--emulator` - Runs tests on the iOS Simulator. You cannot set `--device` and `--emulator` simultaneously. * `--debug-brk` - Runs the tests under the debugger. The debugger will break just before your tests are executed, so you have a chance to place breakpoints. diff --git a/docs/man_pages/project/testing/test-android.md b/docs/man_pages/project/testing/test-android.md index 46d3f460a5..d81c5fffa7 100644 --- a/docs/man_pages/project/testing/test-android.md +++ b/docs/man_pages/project/testing/test-android.md @@ -18,7 +18,7 @@ Run tests on a selected device | `$ ns test android --device [--watc ### Options -* `--watch` - If set, when you save changes to the project, changes are automatically synchronized to the connected device and tests are re-run. +* `--watch` - Enabled by default; when you save changes to the project, changes are automatically synchronized to the connected device and tests are re-run. Pass `--no-watch` to run the tests once. Defaults to disabled in CI environments (when the `CI` or `JENKINS_HOME` environment variable is set). * `--device` - Specifies the serial number or the index of the connected device on which to run the tests. To list all connected devices, grouped by platform, run `$ ns device`. `` is the device index or identifier as listed by the `$ ns device` command. * `--debug-brk` - Runs the tests under the debugger. The debugger will break just before your tests are executed, so you have a chance to place breakpoints. * `--env.*` - Specifies additional flags that the bundler may process. Can be passed multiple times. Supported additional flags: diff --git a/docs/man_pages/project/testing/test-ios.md b/docs/man_pages/project/testing/test-ios.md index dac2ccfccb..14219f7bc0 100644 --- a/docs/man_pages/project/testing/test-ios.md +++ b/docs/man_pages/project/testing/test-ios.md @@ -23,7 +23,7 @@ Run tests in the iOS Simulator | `$ ns test ios --emulator [--watch] [--debug-br ### Options -* `--watch` - If set, when you save changes to the project, changes are automatically synchronized to the connected device and tests are re-ran. +* `--watch` - Enabled by default; when you save changes to the project, changes are automatically synchronized to the connected device and tests are re-ran. Pass `--no-watch` to run the tests once. Defaults to disabled in CI environments (when the `CI` or `JENKINS_HOME` environment variable is set). * `--device` - Specifies the serial number or the index of the connected device on which you want to run tests. To list all connected devices, grouped by platform, run `$ ns device`. You cannot set `--device` and `--emulator` simultaneously. `` is the device index or identifier as listed by the `$ ns device` command. * `--emulator` - Runs tests on the iOS Simulator. You cannot set `--device` and `--emulator` simultaneously. * `--debug-brk` - Runs the tests under the debugger. The debugger will break just before your tests are executed, so you have a chance to place breakpoints. diff --git a/lib/commands/test.ts b/lib/commands/test.ts index 646af87e3e..ccbdcf97af 100644 --- a/lib/commands/test.ts +++ b/lib/commands/test.ts @@ -1,4 +1,4 @@ -import { hasValidAndroidSigning } from "../common/helpers"; +import { hasValidAndroidSigning, isCIEnvironment } from "../common/helpers"; import { ANDROID_RELEASE_BUILD_ERROR_MESSAGE, ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE, @@ -25,6 +25,13 @@ abstract class TestCommandBase { public allowedParameters: ICommandParameter[] = []; public dashedOptions = { hmr: { type: OptionType.Boolean, default: false, hasSensitiveValue: false }, + // Watch mode keeps the run alive waiting for changes; CI runs must + // execute once and exit, so --watch defaults to off there. + watch: { + type: OptionType.Boolean, + default: !isCIEnvironment(), + hasSensitiveValue: false, + }, }; protected abstract platform: string; @@ -214,7 +221,7 @@ class TestAndroidCommand extends TestCommandBase implements ICommand { } } -class TestIosCommand extends TestCommandBase implements ICommand { +export class TestIosCommand extends TestCommandBase implements ICommand { protected platform = "iOS"; constructor( diff --git a/lib/common/helpers.ts b/lib/common/helpers.ts index 878fcb9206..ad9006070d 100644 --- a/lib/common/helpers.ts +++ b/lib/common/helpers.ts @@ -349,7 +349,7 @@ function isRunningInTTY(): boolean { ); } -function isCIEnvironment(): boolean { +export function isCIEnvironment(): boolean { // The following CI environments set their own environment variables that we respect: // travis: "CI", // circleCI: "CI", diff --git a/lib/services/vitest-execution-service.ts b/lib/services/vitest-execution-service.ts index f4811780e8..9127f4257b 100644 --- a/lib/services/vitest-execution-service.ts +++ b/lib/services/vitest-execution-service.ts @@ -45,7 +45,12 @@ export class VitestExecutionService implements IVitestExecutionService { ); } - if (this.$options.watch) { + // $options.watch is on by default outside CI, so only an explicit flag + // tells us the user actually asked for watch mode. + const watchPassed = process.argv.some( + (arg) => arg === "--watch" || arg.startsWith("--watch="), + ); + if (watchPassed && this.$options.watch) { this.$logger.warn( "'--watch' is not supported for on-device Vitest runs yet; running once.", ); diff --git a/test/commands/test.ts b/test/commands/test.ts new file mode 100644 index 0000000000..edb17ba513 --- /dev/null +++ b/test/commands/test.ts @@ -0,0 +1,120 @@ +import { Yok } from "../../lib/common/yok"; +import { assert } from "chai"; +import { Options } from "../../lib/options"; +import { TestIosCommand } from "../../lib/commands/test"; +import { IOptions } from "../../lib/declarations"; +import { ICommand } from "../../lib/common/definitions/commands"; +import { IInjector } from "../../lib/common/definitions/yok"; +import { IConfigurationSettings } from "../../lib/common/declarations"; + +const CI_ENVIRONMENT_VARIABLES = ["CI", "JENKINS_HOME"]; + +function createTestInjector(): IInjector { + const testInjector = new Yok(); + testInjector.register("settingsService", { + setSettings: (settings: IConfigurationSettings): any => undefined, + getProfileDir: () => "profileDir", + }); + testInjector.register("errors", { + fail: (message: string): never => { + throw new Error(message); + }, + failWithHelp: (message: string): never => { + throw new Error(message); + }, + }); + testInjector.register("logger", { + warn: (message: string): void => undefined, + }); + + testInjector.register("projectData", {}); + testInjector.register("testExecutionService", {}); + testInjector.register("vitestExecutionService", {}); + testInjector.register("analyticsService", {}); + testInjector.register("platformEnvironmentRequirements", {}); + testInjector.register("cleanupService", {}); + testInjector.register("liveSyncCommandHelper", {}); + testInjector.register("devicesService", {}); + testInjector.register("migrateController", {}); + + return testInjector; +} + +interface IResolvedTestCommand { + command: ICommand; + options: IOptions; +} + +function resolveTestIosCommand(): IResolvedTestCommand { + const testInjector = createTestInjector(); + const options = testInjector.resolve(Options); + testInjector.register("options", options); + testInjector.registerCommand("test|ios", TestIosCommand); + const command = testInjector.resolveCommand("test|ios"); + return { command, options }; +} + +function validateWithArgs( + resolved: IResolvedTestCommand, + args: string[] = [], +): void { + args.forEach((arg) => process.argv.push(arg)); + resolved.options.validateOptions(resolved.command.dashedOptions); + args.forEach(() => process.argv.pop()); +} + +describe("test ios command", () => { + const savedCiEnvironment: { [key: string]: string } = {}; + + beforeEach(() => { + CI_ENVIRONMENT_VARIABLES.forEach((name) => { + savedCiEnvironment[name] = process.env[name]; + delete process.env[name]; + }); + }); + + afterEach(() => { + CI_ENVIRONMENT_VARIABLES.forEach((name) => { + if (savedCiEnvironment[name] === undefined) { + delete process.env[name]; + } else { + process.env[name] = savedCiEnvironment[name]; + } + }); + }); + + describe("--watch option", () => { + it("defaults to off in CI environments", () => { + process.env.CI = "true"; + const resolved = resolveTestIosCommand(); + validateWithArgs(resolved); + assert.isFalse(resolved.options.argv.watch); + }); + + it("defaults to off when JENKINS_HOME is set", () => { + process.env.JENKINS_HOME = "/var/jenkins"; + const resolved = resolveTestIosCommand(); + validateWithArgs(resolved); + assert.isFalse(resolved.options.argv.watch); + }); + + it("defaults to on outside CI environments", () => { + const resolved = resolveTestIosCommand(); + validateWithArgs(resolved); + assert.isTrue(resolved.options.argv.watch); + }); + + it("stays on when --watch is passed explicitly in CI", () => { + process.env.CI = "true"; + const resolved = resolveTestIosCommand(); + validateWithArgs(resolved, ["--watch"]); + assert.isTrue(resolved.options.argv.watch); + }); + + it("stays off when --no-watch is passed outside CI", () => { + const resolved = resolveTestIosCommand(); + validateWithArgs(resolved, ["--no-watch"]); + assert.isFalse(resolved.options.argv.watch); + }); + }); +}); diff --git a/test/services/vitest-execution-service.ts b/test/services/vitest-execution-service.ts new file mode 100644 index 0000000000..cc59712766 --- /dev/null +++ b/test/services/vitest-execution-service.ts @@ -0,0 +1,85 @@ +import { assert } from "chai"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { VitestExecutionService } from "../../lib/services/vitest-execution-service"; +import { IProjectData } from "../../lib/definitions/project"; + +const WATCH_WARNING = + "'--watch' is not supported for on-device Vitest runs yet; running once."; + +describe("vitestExecutionService", () => { + let projectDir: string; + let warnings: string[]; + let originalArgv: string[]; + + beforeEach(() => { + projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "ns-vitest-")); + const vitestDir = path.join(projectDir, "node_modules", "vitest"); + fs.mkdirSync(vitestDir, { recursive: true }); + fs.writeFileSync( + path.join(vitestDir, "package.json"), + JSON.stringify({ name: "vitest", version: "0.0.0" }), + ); + warnings = []; + originalArgv = process.argv; + }); + + afterEach(() => { + process.argv = originalArgv; + fs.rmSync(projectDir, { recursive: true, force: true }); + }); + + const startTestRun = async ( + argv: string[], + options: { watch: boolean }, + ): Promise => { + process.argv = ["node", "ns", "test", "ios", ...argv]; + const service = new VitestExecutionService( + { + spawnFromEvent: async () => ({ stdout: "", stderr: "", exitCode: 0 }), + }, + { + fail: (message: string): never => { + throw new Error(message); + }, + }, + {}, + { warn: (message: string) => warnings.push(message) }, + options, + ); + await service.startTestRun("iOS", { projectDir }); + }; + + describe("--watch warning", () => { + it("is not shown when watch is only on by default", async () => { + await startTestRun([], { watch: true }); + assert.notInclude(warnings, WATCH_WARNING); + }); + + it("is shown when --watch is passed explicitly", async () => { + await startTestRun(["--watch"], { watch: true }); + assert.include(warnings, WATCH_WARNING); + }); + + it("is shown when --watch=true is passed", async () => { + await startTestRun(["--watch=true"], { watch: true }); + assert.include(warnings, WATCH_WARNING); + }); + + it("is not shown when --no-watch is passed", async () => { + await startTestRun(["--no-watch"], { watch: false }); + assert.notInclude(warnings, WATCH_WARNING); + }); + + it("is not shown when --watch false is passed", async () => { + await startTestRun(["--watch", "false"], { watch: false }); + assert.notInclude(warnings, WATCH_WARNING); + }); + + it("is not shown when --watch=false is passed", async () => { + await startTestRun(["--watch=false"], { watch: false }); + assert.notInclude(warnings, WATCH_WARNING); + }); + }); +});