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
5 changes: 5 additions & 0 deletions .changeset/private-simulator-preview-sharing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@expo/build-tools': minor
---

Respect saved sharing preferences for iOS simulator preview sessions.
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ describe.each([
['Appium', createStartAppiumRemoteSessionBuildFunction],
['Agent Device', createStartAgentDeviceSessionBuildFunction],
] as const)('%s host ownership', (name, createFunction) => {
async function runAsync() {
async function runAsync(publicPreview?: boolean) {
const fn = createFunction({} as CustomBuildContext);
await fn.fn!(
{
Expand All @@ -117,9 +117,12 @@ describe.each([
} as unknown as BuildStepContext,
{
// Inputs a step does not get default to undefined, like in a real step call.
inputs: new Proxy({} as Record<string, { value: unknown }>, {
get: (target, id: string) => target[id] ?? { value: undefined },
}),
inputs: new Proxy(
{ public_preview: { value: publicPreview } } as Record<string, { value: unknown }>,
{
get: (target, id: string) => target[id] ?? { value: undefined },
}
),
outputs: {},
env: {
DEVICE_RUN_SESSION_ID: 'session-id',
Expand Down Expand Up @@ -168,6 +171,14 @@ describe.each([
}
});

it.each([undefined, true, false])('maps public_preview=%s to the host', async publicPreview => {
await runAsync(publicPreview);
expect(startDeviceSessionHostAsync).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ disableShare: publicPreview === false })
);
});

it('stops automation while recording finalization or upload is pending', async () => {
let release!: () => void;
let stopped!: () => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ const NO_CAPTURE: CaptureInputs = { networkCapture: false, networkCaptureFields:
async function runAsync(
logger: { info: jest.Mock; warn: jest.Mock; child: jest.Mock },
runtimePlatform: BuildRuntimePlatform,
capture: CaptureInputs = NO_CAPTURE
capture: CaptureInputs = NO_CAPTURE,
disableShare = false,
privatePreviewUrl?: string
): Promise<void> {
await runAgentDeviceRemoteSessionAsync(ctx, {
env: {},
Expand All @@ -89,6 +91,8 @@ async function runAsync(
maxIdleTimeMinutes: undefined,
maxDurationSeconds: undefined,
capture,
disableShare,
privatePreviewUrl,
tasks: createStartupTasks(logger as never),
device: { booted: Promise.resolve(), ready: Promise.resolve() },
});
Expand Down Expand Up @@ -199,6 +203,24 @@ describe('runAgentDeviceRemoteSessionAsync orchestration', () => {
);
});

it('hands a private preview choice to the session host', async () => {
const logger = { info: jest.fn(), warn: jest.fn(), child: jest.fn().mockReturnThis() };
await runAsync(
logger,
BuildRuntimePlatform.DARWIN,
NO_CAPTURE,
true,
'https://expo.dev/private-preview'
);
expect(startDeviceSessionHostAsync).toHaveBeenCalledWith(
ctx,
expect.objectContaining({
disableShare: true,
privatePreviewUrl: 'https://expo.dev/private-preview',
})
);
});

it('starts the session host without waiting for the agent-device daemon', async () => {
const logger = { info: jest.fn(), warn: jest.fn(), child: jest.fn().mockReturnThis() };
const sessionHost = await jest.mocked(startDeviceSessionHostAsync).getMockImplementation()!(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ function runStep(
'open_url',
'network_capture',
'network_capture_fields',
'public_preview',
'private_preview_url',
'package_version',
'max_idle_time_minutes',
'max_duration_seconds',
Expand Down Expand Up @@ -413,12 +415,31 @@ describe(createStartAgentDeviceSessionBuildFunction, () => {
'open_url',
'network_capture',
'network_capture_fields',
'public_preview',
'private_preview_url',
'package_version',
'max_idle_time_minutes',
'max_duration_seconds',
]);
});

it.each([undefined, true, false])(
'passes public_preview=%s to the iOS session host',
async publicPreview => {
await runStep(BuildRuntimePlatform.DARWIN, {
public_preview: publicPreview,
private_preview_url: 'https://expo.dev/private-preview',
});
expect(runAgentDeviceRemoteSessionAsync).toHaveBeenCalledWith(
ctx,
expect.objectContaining({
disableShare: publicPreview === false,
privatePreviewUrl: 'https://expo.dev/private-preview',
})
);
}
);

it('passes network capture to the session', async () => {
await runStep(BuildRuntimePlatform.DARWIN, {
network_capture: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,15 @@ describe('createStartAppiumRemoteSessionBuildFunction', () => {

expect(
buildFunction.inputProviders?.map(provider => provider(globalCtx, 'Test step').id)
).toEqual(expect.arrayContaining(['launch_app_identifier', 'launch_args', 'open_url']));
).toEqual(
expect.arrayContaining([
'launch_app_identifier',
'launch_args',
'open_url',
'public_preview',
'private_preview_url',
])
);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ describe('createStartArgentRemoteSessionBuildFunction orchestration', () => {
expect(mockTunnelStopAsync).toHaveBeenCalledTimes(1);
expect(startDeviceSessionHostAsync).toHaveBeenCalledWith(
ctx,
expect.objectContaining({ runtimePlatform: BuildRuntimePlatform.LINUX })
expect.objectContaining({ runtimePlatform: BuildRuntimePlatform.LINUX, disableShare: false })
);
expect(uploadRemoteSessionConfigAsync).toHaveBeenCalledWith(
expect.objectContaining({
Expand All @@ -394,6 +394,34 @@ describe('createStartArgentRemoteSessionBuildFunction orchestration', () => {
expect(mockPreviewStopAsync).toHaveBeenCalledTimes(1);
});

it('passes public_preview=false to the session host', async () => {
const ctx = {} as CustomBuildContext;
const buildFunction = createStartArgentRemoteSessionBuildFunction(ctx);
await buildFunction.fn!(
{
logger: { info: jest.fn(), warn: jest.fn() },
global: { runtimePlatform: BuildRuntimePlatform.LINUX },
} as unknown as BuildStepContext,
{
inputs: {
package_version: { value: undefined },
max_idle_time_minutes: { value: undefined },
public_preview: { value: false },
private_preview_url: { value: 'https://expo.dev/private-preview' },
},
outputs: {},
env: {},
} as never
);
expect(startDeviceSessionHostAsync).toHaveBeenCalledWith(
ctx,
expect.objectContaining({
disableShare: true,
privatePreviewUrl: 'https://expo.dev/private-preview',
})
);
});

it('continues the session when disabling the video watermark fails', async () => {
const error = new Error('bun x failed');
jest
Expand Down Expand Up @@ -549,7 +577,15 @@ describe('createStartArgentRemoteSessionBuildFunction orchestration', () => {

expect(
buildFunction.inputProviders?.map(provider => provider(globalCtx, 'Test step').id)
).toEqual(expect.arrayContaining(['launch_app_identifier', 'launch_args', 'open_url']));
).toEqual(
expect.arrayContaining([
'launch_app_identifier',
'launch_args',
'open_url',
'public_preview',
'private_preview_url',
])
);
});

it('stops automation without waiting for recording upload', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ describe(createStartWebPreviewRemoteSessionBuildFunction, () => {
jest.mocked(startDeviceSessionHostAsync).mockResolvedValue({
openPreviewAsync: jest.fn().mockResolvedValue({
previewPageUrl: 'https://expo.dev/simulator-preview/preview-id',
sharePageUrl: 'https://expo.dev/simulator-preview/preview-id',
apiUrl: 'https://web-preview.example.test',
closeAsync: jest.fn(),
}),
Expand All @@ -91,6 +92,7 @@ describe(createStartWebPreviewRemoteSessionBuildFunction, () => {
jest.mocked(startDeviceSessionHostAsync).mockResolvedValue({
openPreviewAsync: jest.fn().mockResolvedValue({
previewPageUrl: 'https://expo.dev/simulator-preview/preview-id',
sharePageUrl: 'https://expo.dev/simulator-preview/preview-id',
apiUrl: 'https://web-preview.example.test',
previewToken: 'tok-1',
closeAsync: jest.fn(),
Expand All @@ -113,6 +115,54 @@ describe(createStartWebPreviewRemoteSessionBuildFunction, () => {
);
});

it('logs the authenticated link while publishing the public frame URL', async () => {
const privatePreviewUrl = 'https://expo.dev/private-preview';
jest.mocked(startDeviceSessionHostAsync).mockResolvedValueOnce({
openPreviewAsync: jest.fn().mockResolvedValue({
previewPageUrl: 'https://expo.dev/simulator-preview/preview-id',
sharePageUrl: privatePreviewUrl,
apiUrl: 'https://web-preview.example.test',
closeAsync: jest.fn(),
}),
finishAsync: stopAsync,
});

await runAsync(BuildRuntimePlatform.DARWIN, {
public_preview: { value: false },
private_preview_url: { value: privatePreviewUrl },
});

expect(logger.info).toHaveBeenCalledWith(
`Preview URL: ${privatePreviewUrl} (server: https://web-preview.example.test).`
);
expect(uploadRemoteSessionConfigAsync).toHaveBeenCalledWith(
expect.objectContaining({
remoteConfig: expect.objectContaining({
webPreviewUrl: 'https://expo.dev/simulator-preview/preview-id',
}),
})
);
});

it('does not log the public link when a private job lacks its authenticated URL', async () => {
jest.mocked(startDeviceSessionHostAsync).mockResolvedValueOnce({
openPreviewAsync: jest.fn().mockResolvedValue({
previewPageUrl: 'https://expo.dev/simulator-preview/preview-id',
apiUrl: 'https://web-preview.example.test',
closeAsync: jest.fn(),
}),
finishAsync: stopAsync,
});
await runAsync(BuildRuntimePlatform.DARWIN, { public_preview: { value: false } });

expect(logger.info).toHaveBeenCalledWith('Private preview URL unavailable for this session.');
expect(jest.mocked(logger.info).mock.calls).not.toEqual(
expect.arrayContaining([
expect.arrayContaining([expect.stringContaining('/simulator-preview/preview-id')]),
])
);
});

it.each(['preview', 'config', 'wait'])('finishes the session after %s fails', async phase => {
const error = new Error(`${phase} failed`);
if (phase === 'preview') {
Expand All @@ -135,6 +185,15 @@ describe(createStartWebPreviewRemoteSessionBuildFunction, () => {
])('starts the web preview for %s', async (runtimePlatform, selectsXcode) => {
await runAsync(runtimePlatform, selectsXcode ? { boot_simulator: { value: true } } : {});

expect(
jest
.mocked(logger.info)
.mock.calls.filter(
([message]) =>
message ===
'Preview URL: https://expo.dev/simulator-preview/preview-id (server: https://web-preview.example.test).'
)
).toHaveLength(1);
expect(selectXcodeDeveloperDirectoryAsync).toHaveBeenCalledTimes(selectsXcode ? 1 : 0);
expect(startDeviceSessionHostAsync).toHaveBeenCalledWith(ctx, {
runtimePlatform,
Expand All @@ -150,6 +209,7 @@ describe(createStartWebPreviewRemoteSessionBuildFunction, () => {
}
: {}),
packageVersion: '1.2.3',
disableShare: false,
launchAppIdentifier: undefined,
launchArgs: [],
openUrl: undefined,
Expand Down Expand Up @@ -196,6 +256,8 @@ describe(createStartWebPreviewRemoteSessionBuildFunction, () => {
'install_app_path',
'network_capture',
'network_capture_fields',
'public_preview',
'private_preview_url',
'package_version',
'max_duration_seconds',
]);
Expand Down Expand Up @@ -243,6 +305,22 @@ describe(createStartWebPreviewRemoteSessionBuildFunction, () => {
})
);
});
it.each([true, false])(
'maps public_preview=%s to the native Share option',
async publicPreview => {
await runAsync(BuildRuntimePlatform.DARWIN, {
public_preview: { value: publicPreview },
private_preview_url: { value: 'https://expo.dev/private-preview' },
});
expect(startDeviceSessionHostAsync).toHaveBeenCalledWith(
ctx,
expect.objectContaining({
disableShare: !publicPreview,
privatePreviewUrl: 'https://expo.dev/private-preview',
})
);
}
);

it('hands network capture to the session host', async () => {
await runAsync(BuildRuntimePlatform.DARWIN, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ export async function runAgentDeviceRemoteSessionAsync(
maxIdleTimeMinutes,
maxDurationSeconds,
capture,
disableShare = false,
privatePreviewUrl,
tasks,
device,
}: {
Expand All @@ -108,6 +110,8 @@ export async function runAgentDeviceRemoteSessionAsync(
maxIdleTimeMinutes: number | undefined;
maxDurationSeconds: number | undefined;
capture: ReturnType<typeof parseNetworkCaptureInputs>;
disableShare?: boolean;
privatePreviewUrl?: string;
tasks: StartupTasks;
device: { booted: Promise<unknown>; ready: Promise<unknown> };
}
Expand Down Expand Up @@ -163,6 +167,8 @@ export async function runAgentDeviceRemoteSessionAsync(
env,
logger: taskLogger,
timeoutMs: STARTUP_TIMEOUT_MS,
disableShare,
privatePreviewUrl,
networkCapture: capture.networkCapture,
networkCaptureFields: capture.networkCaptureFields,
});
Expand All @@ -172,7 +178,9 @@ export async function runAgentDeviceRemoteSessionAsync(
authtoken: ngrokAuthtoken,
});
taskLogger.info(
`Web preview URL: ${webPreview.previewPageUrl} (server: ${webPreview.apiUrl}).`
webPreview.sharePageUrl
? `Web preview URL: ${webPreview.sharePageUrl} (server: ${webPreview.apiUrl}).`
: 'Private preview URL unavailable for this session.'
);
return webPreview;
});
Expand Down
Loading
Loading