diff --git a/.changeset/simulator-control-help.md b/.changeset/simulator-control-help.md new file mode 100644 index 0000000000..7172be7659 --- /dev/null +++ b/.changeset/simulator-control-help.md @@ -0,0 +1,5 @@ +--- +'eas-cli': patch +--- + +Fix simulator control action help routing and show help when a flag is incomplete. diff --git a/packages/build-tools/src/steps/functions/__tests__/startAppiumRemoteSession.test.ts b/packages/build-tools/src/steps/functions/__tests__/startAppiumRemoteSession.test.ts index 2b5c902503..5f45289215 100644 --- a/packages/build-tools/src/steps/functions/__tests__/startAppiumRemoteSession.test.ts +++ b/packages/build-tools/src/steps/functions/__tests__/startAppiumRemoteSession.test.ts @@ -266,7 +266,9 @@ describe('createStartAppiumRemoteSessionBuildFunction', () => { '--allow-insecure', '*:session_discovery', '--default-capabilities', - '{"appium:eventTimings":true}', + runtimePlatform === BuildRuntimePlatform.DARWIN + ? '{"appium:eventTimings":true,"appium:isHeadless":true}' + : '{"appium:eventTimings":true}', ], env: { ...env, APPIUM_HOME: launch.env.APPIUM_HOME }, logger, diff --git a/packages/eas-cli/src/__tests__/fixtures/simulator-control-lifecycle.cjs b/packages/eas-cli/src/__tests__/fixtures/simulator-control-lifecycle.cjs new file mode 100644 index 0000000000..47b637f161 --- /dev/null +++ b/packages/eas-cli/src/__tests__/fixtures/simulator-control-lifecycle.cjs @@ -0,0 +1,83 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const os = require('node:os'); +const { createRequire } = require('node:module'); + +const root = fs.realpathSync(process.argv[2]); +const cli = path.join(root, 'packages/eas-cli'); +const cliRequire = createRequire(path.join(cli, 'package.json')); +cliRequire('ts-node').register({ project: path.join(cli, 'tsconfig.json'), transpileOnly: true }); +const { Config } = cliRequire('@oclif/core'); +const EasCommand = require(path.join(cli, 'src/commandUtils/EasCommand')).default; +const Control = require(path.join(cli, 'src/commands/simulator/control')).default; +const EasHelp = require(path.join(cli, 'src/help')).default; +const cases = JSON.parse(process.argv[3]); +const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'eas-help-lifecycle-')); +const events = []; +globalThis.__helpLifecycleEvents = events; + +// Replace command work (auth/device I/O), keeping discovery, parsing and hooks real. +EasCommand.prototype.run = async function () { + events.push({ event: 'run', id: this.id }); +}; +EasCommand.prototype.finally = async function () {}; + +(async () => { + try { + fs.writeFileSync( + path.join(directory, 'package.json'), + JSON.stringify({ + name: 'eas-help-lifecycle-fixture', + version: '1.0.0', + oclif: { + bin: 'eas', + commands: { strategy: 'explicit', target: './commands.cjs' }, + hooks: { prerun: './prerun.cjs', postrun: './postrun.cjs' }, + }, + }) + ); + fs.writeFileSync( + path.join(directory, 'commands.cjs'), + `exports.default = { 'simulator:control': require(${JSON.stringify(path.join(cli, 'src/commands/simulator/control'))}).default };` + ); + for (const event of ['prerun', 'postrun']) { + fs.writeFileSync( + path.join(directory, `${event}.cjs`), + `module.exports = async function({Command}) { globalThis.__helpLifecycleEvents.push({event:${JSON.stringify(event)},id:Command.id}); };` + ); + } + process.env.XDG_DATA_HOME = directory; + process.env.XDG_CACHE_HOME = directory; + const config = await Config.load({ root: directory }); + const results = []; + for (const args of cases) { + // Published manifests discover metadata without loading the command class. + // Clear eager discovery's assignment to exercise that cold-start boundary. + delete Control.id; + const start = events.length; + const write = process.stdout.write; + let stdout = '', + error; + process.stdout.write = (chunk, encoding, callback) => { + stdout += String(chunk); + const done = typeof encoding === 'function' ? encoding : callback; + done?.(); + return true; + }; + try { + await new EasHelp(config).showHelp(args); + } catch (err) { + error = err.message; + } finally { + process.stdout.write = write; + } + results.push({ stdout, events: events.slice(start), error }); + } + console.log('LIFECYCLE_RESULT ' + JSON.stringify(results)); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } +})().catch(err => { + console.error(err); + process.exitCode = 1; +}); diff --git a/packages/eas-cli/src/__tests__/help.lifecycle.test.ts b/packages/eas-cli/src/__tests__/help.lifecycle.test.ts new file mode 100644 index 0000000000..ff37bf911e --- /dev/null +++ b/packages/eas-cli/src/__tests__/help.lifecycle.test.ts @@ -0,0 +1,51 @@ +import { execFile } from 'node:child_process'; +import path from 'node:path'; +import { promisify } from 'node:util'; + +const execFileAsync = promisify(execFile); +const root = path.resolve(__dirname, '../../../..'); +const fixture = path.join(__dirname, 'fixtures/simulator-control-lifecycle.cjs'); +const actionHelp = [ + ['simulator:control', 'rotate', '--help'], + ['sim:control', 'screenshot.capture', '-h', '--id', 'session-123'], + ['simulator:control', '--id', 'session-123', '--help', 'rotate'], +]; +const incompleteFlags = [ + ['simulator:control', 'screenshot.capture', '--help', '--output'], + ['sim:control', 'rotate', '-h', '--id'], +]; +let results: { stdout: string; events: unknown[]; error?: string }[]; + +beforeAll(async () => { + // Load TypeScript once, outside per-case timeouts, including on coverage runners. + const { stdout } = await execFileAsync( + process.execPath, + [fixture, root, JSON.stringify([...actionHelp, ...incompleteFlags])], + { timeout: 20_000 } + ); + results = JSON.parse(stdout.split('LIFECYCLE_RESULT ').at(-1)!); +}, 30_000); + +describe('simulator action help through real oclif lookup and hooks', () => { + it.each(actionHelp.map((argv, index) => ({ argv, index })))( + 'keeps canonical command identity and executes lifecycle hooks: $argv', + ({ index }) => { + expect(results[index].error).toBeUndefined(); + expect(results[index].events).toEqual([ + { event: 'prerun', id: 'simulator:control' }, + { event: 'run', id: 'simulator:control' }, + { event: 'postrun', id: 'simulator:control' }, + ]); + } + ); + + it.each(incompleteFlags.map((argv, index) => ({ argv, index })))( + 'shows ordinary help without starting the command for an incomplete flag: $argv', + ({ index }) => { + const { stdout, events, error } = results[index + actionHelp.length]; + expect(error).toBeUndefined(); + expect(stdout).toContain('USAGE'); + expect(events).toEqual([]); + } + ); +}); diff --git a/packages/eas-cli/src/__tests__/help.test.ts b/packages/eas-cli/src/__tests__/help.test.ts index d0cb73dc2c..f5254631cc 100644 --- a/packages/eas-cli/src/__tests__/help.test.ts +++ b/packages/eas-cli/src/__tests__/help.test.ts @@ -1,6 +1,5 @@ import { Config, Help } from '@oclif/core'; -import SimulatorControl from '../commands/simulator/control'; import EasHelp from '../help'; describe(EasHelp, () => { @@ -9,7 +8,7 @@ describe(EasHelp, () => { let showHelp: jest.SpyInstance; beforeEach(() => { - run = jest.spyOn(SimulatorControl, 'run').mockResolvedValue(Promise.resolve()); + run = jest.spyOn(config, 'runCommand').mockResolvedValue(Promise.resolve()); showHelp = jest.spyOn(Help.prototype, 'showHelp').mockResolvedValue(); }); @@ -21,12 +20,14 @@ describe(EasHelp, () => { ['simulator:control', '--id', 'session-123', '--help', 'rotate'], ])('routes action help through the command: %j', async (...argv) => { await new EasHelp(config).showHelp(argv); - expect(run).toHaveBeenCalledWith(argv.slice(1), config); + expect(run).toHaveBeenCalledWith('simulator:control', argv.slice(1)); expect(showHelp).not.toHaveBeenCalled(); }); it.each([ ['simulator:control', '--help'], + ['simulator:control', 'screenshot.capture', '--help', '--output'], + ['sim:control', 'rotate', '-h', '--id'], ['sim:control', '-h'], ['build', '--help'], ['simulator:control', 'clipboard.write', '--', '--help'], diff --git a/packages/eas-cli/src/help.ts b/packages/eas-cli/src/help.ts index 455232f8eb..c1af7344d2 100644 --- a/packages/eas-cli/src/help.ts +++ b/packages/eas-cli/src/help.ts @@ -6,13 +6,19 @@ export default class EasHelp extends Help { const [command, ...args] = argv; if (command === 'simulator:control' || command === 'sim:control') { const { default: SimulatorControl } = await import('./commands/simulator/control'); - const parsed = await Parser.parse(args, { - args: SimulatorControl.args, - flags: SimulatorControl.flags, - strict: false, - }); + let parsed; + try { + parsed = await Parser.parse(args, { + args: SimulatorControl.args, + flags: SimulatorControl.flags, + strict: false, + }); + } catch { + await super.showHelp(argv); + return; + } if (parsed.args.action && parsed.flags.help) { - await SimulatorControl.run(args, this.config); + await this.config.runCommand('simulator:control', args); return; } }