Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/default-to-existing-device.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"vitest-mobile": patch
---

Default the device picker to an existing simulator/AVD instead of "Create new"

The interactive device picker (used by `bootstrap` and `boot-device`) now
defaults to an existing device rather than prompting to create a dedicated
one. On iOS the pre-selected device is the most recently booted simulator,
matching Expo CLI's heuristic; on Android it's the first available AVD.

"Create new dedicated simulator/AVD" is still available at the bottom of
the list for users who want isolation. The non-interactive (CI) fallback
is unchanged — it still auto-creates a project-scoped device.

This prevents vitest-mobile from stealing Expo CLI's default simulator:
previously, creating and booting a `VitestMobile-*` sim made it macOS
Simulator.app's "most recently used" device, so Expo would target it
on the next `expo start` → `i` press.
68 changes: 34 additions & 34 deletions packages/vitest-mobile/src/cli/device-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
* the device deterministically from the project path (matches the previous
* `VitestMobile-<hash>` / `vitest-mobile-<hash>` pattern).
*
* Defaults to an existing device — on iOS the most recently booted
* simulator (matching Expo CLI's heuristic), on Android the first
* available AVD. "Create new" appears at the end of the list for users
* who want a dedicated device.
*
* The result feeds into the device-mapping store; subsequent test runs
* read that mapping and don't re-prompt.
*
Expand All @@ -15,7 +20,7 @@
*/

import { isCancel, select } from '@clack/prompts';
import { listAllIOSSimulators, primarySimulatorName } from '../node/device/ios';
import { getBootedSimulators, listAllIOSSimulators, primarySimulatorName } from '../node/device/ios';
import { listAllAvds, avdNameForProject, hasAvdProvisioningTools } from '../node/device/android';
import { getDeviceMapping, setDeviceMapping, type DeviceMapping } from '../node/device/mapping';
import type { Platform } from '../node/types';
Expand Down Expand Up @@ -55,31 +60,32 @@ async function pickIOS(appDir: string, currentChoice?: string): Promise<PickedDe
const sims = listAllIOSSimulators();
const projectSim = primarySimulatorName(appDir);

const existingSims = sims
.filter(s => !s.name.startsWith('VitestMobile-') || s.name === projectSim)
.sort((a, b) => a.name.localeCompare(b.name))
.map(s => ({ value: s.name, label: s.name, hint: s.runtime }));

const options = [
...existingSims,
{
value: CREATE_NEW,
label: `Create new dedicated simulator (${projectSim})`,
hint: 'recommended',
},
...sims
// Hide auto-created simulators for *other* projects — they'd be
// confusing to pick.
.filter(s => !s.name.startsWith('VitestMobile-') || s.name === projectSim)
.sort((a, b) => a.name.localeCompare(b.name))
.map(s => ({ value: s.name, label: s.name, hint: s.runtime })),
];

// Preference for the pre-selected option:
// 1. The currently-mapped device (user's "keep the same" default).
// 2. The existing project simulator (for pre-mapping migrations).
// 3. Otherwise prompt to create a new one.
const projectExists = sims.some(s => s.name === projectSim);
// Default to the most recently booted simulator (matches Expo's heuristic),
// falling back to the first available simulator, then "Create new" only if
// the host has no simulators at all.
const booted = getBootedSimulators();
const firstBooted = booted.find(b => existingSims.some(s => s.value === b.name));
const initialValue =
currentChoice && options.some(o => o.value === currentChoice)
? currentChoice
: projectExists
? projectSim
: CREATE_NEW;
: firstBooted
? firstBooted.name
: existingSims.length > 0
? existingSims[0]!.value
: CREATE_NEW;

const choice = await select({
message: 'Which simulator should vitest-mobile use for this project?',
Expand All @@ -89,44 +95,38 @@ async function pickIOS(appDir: string, currentChoice?: string): Promise<PickedDe
if (isCancel(choice)) fail('Cancelled.');

if (choice === CREATE_NEW) return { name: projectSim, createdByUs: true };
// If the user picked the existing per-project sim, treat as createdByUs=true
// so `reset-device` still cleans it up.
return { name: choice as string, createdByUs: choice === projectSim };
}

async function pickAndroid(appDir: string, currentChoice?: string): Promise<PickedDevice> {
const avds = listAllAvds();
const projectAvd = avdNameForProject(appDir);
const canCreate = hasAvdProvisioningTools();
const existing = avds.includes(projectAvd);

const createHint = canCreate ? 'recommended' : 'requires Android cmdline-tools (sdkmanager + avdmanager)';
const existingAvds = avds
.filter(a => (!a.startsWith('vitest-mobile-') && a !== 'vitest-mobile') || a === projectAvd)
.sort()
.map(a => ({ value: a, label: a }));

const createHint = canCreate ? undefined : 'requires Android cmdline-tools (sdkmanager + avdmanager)';

const options = [
...existingAvds,
{
value: CREATE_NEW,
label: `Create new dedicated AVD (${projectAvd})`,
hint: createHint,
},
...avds
.filter(a => (!a.startsWith('vitest-mobile-') && a !== 'vitest-mobile') || a === projectAvd)
.sort()
.map(a => ({ value: a, label: a })),
];

// Preference for the pre-selected option:
// 1. Currently-mapped device (user's "keep the same" default).
// 2. Existing project AVD.
// 3. "Create new" when we can actually create one.
// 4. First existing AVD otherwise.
// Default to the first existing AVD (matches the "use what you already have"
// convention), falling back to "Create new" only if no AVDs exist.
const initialValue =
currentChoice && options.some(o => o.value === currentChoice)
? currentChoice
: existing
? projectAvd
: canCreate
? CREATE_NEW
: (avds[0] ?? CREATE_NEW);
: existingAvds.length > 0
? existingAvds[0]!.value
: CREATE_NEW;

const choice = await select({
message: 'Which AVD should vitest-mobile use for this project?',
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest-mobile/src/node/device/ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export function getBootedSimulator(): string | null {
return info?.udid ?? null;
}

function getBootedSimulators(excludeIds: string[] = []): SimulatorInfo[] {
export function getBootedSimulators(excludeIds: string[] = []): SimulatorInfo[] {
const json = run('xcrun simctl list devices booted -j');
if (!json) return [];
const devices = parseSimctlDevicesJson(json);
Expand Down
8 changes: 2 additions & 6 deletions test-packages/counter/tests/counter.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import React from 'react';
import { describe, it, expect, afterEach } from 'vitest';
import { render, cleanup, waitFor } from 'vitest-mobile/runtime';
import { describe, it, expect } from 'vitest';
import { render, waitFor } from 'vitest-mobile/runtime';
import { CounterModule } from '../CounterModule';

afterEach(async () => {
await cleanup();
});

describe('CounterModule', () => {
it('renders initial count of zero', async () => {
const screen = await render(<CounterModule userId="123" />);
Expand Down
8 changes: 2 additions & 6 deletions test-packages/greeting/tests/greeting.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import React from 'react';
import { describe, it, expect, afterEach } from 'vitest';
import { render, cleanup } from 'vitest-mobile/runtime';
import { describe, it, expect } from 'vitest';
import { render } from 'vitest-mobile/runtime';
import { GreetingModule } from '../GreetingModule';

afterEach(async () => {
await cleanup();
});

describe('GreetingModule', () => {
it('shows placeholder when no name is entered', async () => {
const screen = await render(<GreetingModule />);
Expand Down
8 changes: 2 additions & 6 deletions test-packages/todo-list/tests/todo-list.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import React from 'react';
import { describe, it, expect, afterEach } from 'vitest';
import { render, cleanup } from 'vitest-mobile/runtime';
import { describe, it, expect } from 'vitest';
import { render } from 'vitest-mobile/runtime';
import { TodoListModule } from '../TodoListModule';

afterEach(async () => {
await cleanup();
});

describe('TodoListModule', () => {
it('shows empty message when no items', async () => {
const screen = await render(<TodoListModule />);
Expand Down
8 changes: 2 additions & 6 deletions test-packages/toggle/tests/toggle.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import React from 'react';
import { describe, it, expect, afterEach } from 'vitest';
import { render, cleanup, waitFor } from 'vitest-mobile/runtime';
import { describe, it, expect } from 'vitest';
import { render, waitFor } from 'vitest-mobile/runtime';
import { ToggleModule } from '../ToggleModule';

afterEach(async () => {
await cleanup();
});

describe('ToggleModule', () => {
it('renders in off state by default', async () => {
const screen = await render(<ToggleModule label="Dark Mode" />);
Expand Down
Loading