Skip to content

Commit 14d89c5

Browse files
committed
Skip simulator builds; add tests for uploadEmbeddedBundleAsync
1 parent 7718c31 commit 14d89c5

2 files changed

Lines changed: 203 additions & 6 deletions

File tree

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { Platform } from '@expo/eas-build-job';
2+
3+
import { BuildContext } from '../../context';
4+
import * as expoUpdates from '../expoUpdates';
5+
import { uploadEmbeddedBundleAsync } from '../expoUpdatesEmbedded';
6+
import * as easCli from '../easCli';
7+
import * as artifacts from '../artifacts';
8+
9+
jest.mock('../expoUpdates');
10+
jest.mock('../easCli');
11+
jest.mock('../artifacts');
12+
13+
const mockZipEntries = jest.fn();
14+
const mockZipExtract = jest.fn();
15+
const mockZipClose = jest.fn();
16+
17+
jest.mock('node-stream-zip', () => ({
18+
__esModule: true,
19+
default: {
20+
async: jest.fn(() => ({
21+
entries: mockZipEntries,
22+
extract: mockZipExtract,
23+
close: mockZipClose,
24+
})),
25+
},
26+
}));
27+
28+
function zipEntryMap(entries: Record<string, true>): Record<string, { name: string }> {
29+
return Object.fromEntries(Object.keys(entries).map(name => [name, { name }]));
30+
}
31+
32+
function makeCtx(overrides: {
33+
platform: Platform;
34+
simulator?: boolean;
35+
channel?: string;
36+
env?: Record<string, string>;
37+
}): BuildContext<any> {
38+
const job =
39+
overrides.platform === Platform.IOS
40+
? {
41+
platform: Platform.IOS,
42+
simulator: overrides.simulator ?? false,
43+
updates: overrides.channel ? { channel: overrides.channel } : undefined,
44+
}
45+
: {
46+
platform: Platform.ANDROID,
47+
updates: overrides.channel ? { channel: overrides.channel } : undefined,
48+
};
49+
50+
return {
51+
job,
52+
env: overrides.env ?? {},
53+
appConfig: Promise.resolve({
54+
updates: { url: 'https://u.expo.dev/project-id' },
55+
}),
56+
logger: {
57+
info: jest.fn(),
58+
warn: jest.fn(),
59+
},
60+
markBuildPhaseSkipped: jest.fn(),
61+
markBuildPhaseHasWarnings: jest.fn(),
62+
getReactNativeProjectDirectory: () => '/project',
63+
} as any;
64+
}
65+
66+
describe('uploadEmbeddedBundleAsync', () => {
67+
beforeEach(() => {
68+
jest.mocked(expoUpdates.isEASUpdateConfigured).mockResolvedValue(true);
69+
jest.mocked(easCli.runEasCliCommand).mockResolvedValue({} as any);
70+
jest.mocked(artifacts.findArtifacts).mockResolvedValue([]);
71+
mockZipEntries.mockResolvedValue({});
72+
mockZipExtract.mockResolvedValue(undefined);
73+
mockZipClose.mockResolvedValue(undefined);
74+
});
75+
76+
afterEach(() => {
77+
jest.clearAllMocks();
78+
jest.restoreAllMocks();
79+
});
80+
81+
it('skips when EAS Update is not configured', async () => {
82+
jest.mocked(expoUpdates.isEASUpdateConfigured).mockResolvedValue(false);
83+
const ctx = makeCtx({ platform: Platform.ANDROID, channel: 'production' });
84+
85+
await uploadEmbeddedBundleAsync(ctx);
86+
87+
expect(ctx.markBuildPhaseSkipped).toHaveBeenCalled();
88+
expect(artifacts.findArtifacts).not.toHaveBeenCalled();
89+
});
90+
91+
it('warns when no channel is configured', async () => {
92+
const ctx = makeCtx({ platform: Platform.ANDROID });
93+
94+
await uploadEmbeddedBundleAsync(ctx);
95+
96+
expect(ctx.logger.warn).toHaveBeenCalledWith(
97+
'Skipping embedded bundle upload: no channel configured for this build profile.'
98+
);
99+
expect(ctx.markBuildPhaseHasWarnings).toHaveBeenCalled();
100+
});
101+
102+
it('uploads from Android APK archives', async () => {
103+
jest.mocked(artifacts.findArtifacts).mockResolvedValue(['/tmp/app-release.apk']);
104+
mockZipEntries.mockResolvedValue(
105+
zipEntryMap({
106+
'assets/index.android.bundle': true,
107+
'assets/app.manifest': true,
108+
})
109+
);
110+
const ctx = makeCtx({
111+
platform: Platform.ANDROID,
112+
channel: 'production',
113+
env: { EAS_BUILD_ID: 'build-123' },
114+
});
115+
116+
await uploadEmbeddedBundleAsync(ctx);
117+
118+
expect(mockZipExtract).toHaveBeenCalledWith(
119+
'assets/index.android.bundle',
120+
expect.stringContaining('index.android.bundle')
121+
);
122+
expect(easCli.runEasCliCommand).toHaveBeenCalledWith(
123+
expect.objectContaining({
124+
args: expect.arrayContaining([
125+
'update:embedded:upload',
126+
'--platform',
127+
Platform.ANDROID,
128+
'--channel',
129+
'production',
130+
'--build-id',
131+
'build-123',
132+
]),
133+
})
134+
);
135+
});
136+
137+
it('uploads from Android AAB archives', async () => {
138+
jest.mocked(artifacts.findArtifacts).mockResolvedValue(['/tmp/app-release.aab']);
139+
mockZipEntries.mockResolvedValue(
140+
zipEntryMap({
141+
'base/assets/index.android.bundle': true,
142+
'base/assets/app.manifest': true,
143+
})
144+
);
145+
const ctx = makeCtx({ platform: Platform.ANDROID, channel: 'production' });
146+
147+
await uploadEmbeddedBundleAsync(ctx);
148+
149+
expect(mockZipExtract).toHaveBeenCalledWith(
150+
'base/assets/index.android.bundle',
151+
expect.stringContaining('index.android.bundle')
152+
);
153+
expect(easCli.runEasCliCommand).toHaveBeenCalled();
154+
});
155+
156+
it('uploads from iOS IPA archives', async () => {
157+
jest.mocked(artifacts.findArtifacts).mockResolvedValue(['/tmp/App.ipa']);
158+
mockZipEntries.mockResolvedValue(
159+
zipEntryMap({
160+
'Payload/App.app/main.jsbundle': true,
161+
'Payload/App.app/EXUpdates.bundle/app.manifest': true,
162+
})
163+
);
164+
const ctx = makeCtx({ platform: Platform.IOS, channel: 'production' });
165+
166+
await uploadEmbeddedBundleAsync(ctx);
167+
168+
expect(easCli.runEasCliCommand).toHaveBeenCalledWith(
169+
expect.objectContaining({
170+
args: expect.arrayContaining(['--platform', Platform.IOS]),
171+
})
172+
);
173+
});
174+
175+
it('skips simulator builds', async () => {
176+
const ctx = makeCtx({ platform: Platform.IOS, simulator: true, channel: 'preview' });
177+
178+
await uploadEmbeddedBundleAsync(ctx);
179+
180+
expect(ctx.markBuildPhaseSkipped).toHaveBeenCalled();
181+
expect(artifacts.findArtifacts).not.toHaveBeenCalled();
182+
});
183+
184+
it('warns when bundle or manifest is missing from the archive', async () => {
185+
jest.mocked(artifacts.findArtifacts).mockResolvedValue(['/tmp/app-release.apk']);
186+
mockZipEntries.mockResolvedValue(
187+
zipEntryMap({
188+
'assets/app.manifest': true,
189+
})
190+
);
191+
const ctx = makeCtx({ platform: Platform.ANDROID, channel: 'production' });
192+
193+
await uploadEmbeddedBundleAsync(ctx);
194+
195+
expect(ctx.logger.warn).toHaveBeenCalledWith(
196+
'Skipping embedded bundle upload: bundle or manifest not found in archive.'
197+
);
198+
expect(easCli.runEasCliCommand).not.toHaveBeenCalled();
199+
});
200+
});

packages/build-tools/src/utils/expoUpdatesEmbedded.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ export async function uploadEmbeddedBundleAsync(ctx: BuildContext<BuildJob>): Pr
1919

2020
const { platform } = ctx.job;
2121
if (platform === Platform.IOS && (ctx.job as Ios.Job).simulator) {
22-
ctx.logger.info(
23-
'Skipping embedded bundle upload: simulator builds do not embed release update bundles.'
24-
);
2522
ctx.markBuildPhaseSkipped();
2623
return;
2724
}
@@ -50,6 +47,9 @@ export async function uploadEmbeddedBundleAsync(ctx: BuildContext<BuildJob>): Pr
5047
}
5148

5249
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-embedded-bundle-'));
50+
const bundleName = platform === Platform.IOS ? 'main.jsbundle' : 'index.android.bundle';
51+
const bundlePath = path.join(tmpDir, bundleName);
52+
const manifestPath = path.join(tmpDir, 'app.manifest');
5353
const zip = new StreamZip.async({ file: archivePath });
5454
try {
5555
const entries = Object.values(await zip.entries());
@@ -70,9 +70,6 @@ export async function uploadEmbeddedBundleAsync(ctx: BuildContext<BuildJob>): Pr
7070
return;
7171
}
7272

73-
const bundleName = platform === Platform.IOS ? 'main.jsbundle' : 'index.android.bundle';
74-
const bundlePath = path.join(tmpDir, bundleName);
75-
const manifestPath = path.join(tmpDir, 'app.manifest');
7673
await zip.extract(bundleEntry.name, bundlePath);
7774
await zip.extract(manifestEntry.name, manifestPath);
7875

0 commit comments

Comments
 (0)