@essential-apps/shopify-test-runner 1.0.27 → 1.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/edge/edgeProxy.d.ts +2 -0
- package/dist/edge/edgeProxy.d.ts.map +1 -1
- package/dist/edge/edgeProxy.js +10 -1
- package/dist/edge/edgeProxy.js.map +1 -1
- package/dist/lib/neonWsProxy.d.ts +1 -1
- package/dist/lib/neonWsProxy.js +1 -1
- package/dist/playwright/baseConfig.d.ts +1 -1
- package/dist/scripts/devOnlineBackend.js +1 -1
- package/dist/scripts/devOnlineBackend.js.map +1 -1
- package/dist/scripts/runOffline.js +12 -12
- package/dist/scripts/runOffline.js.map +1 -1
- package/dist/scripts/runOfflineTests.d.ts +3 -0
- package/dist/scripts/runOfflineTests.d.ts.map +1 -0
- package/dist/scripts/{runOfflineFullTests.js → runOfflineTests.js} +80 -81
- package/dist/scripts/runOfflineTests.js.map +1 -0
- package/dist/scripts/runTests.js +1 -1
- package/dist/scripts/runTests.js.map +1 -1
- package/dist/scripts/runVm.js +2 -2
- package/dist/scripts/runVm.js.map +1 -1
- package/dist/vite/onlineConfig.d.ts +1 -1
- package/dist/vite/onlineConfig.js +1 -1
- package/package.json +1 -1
- package/src/edge/edgeProxy.ts +13 -1
- package/src/lib/neonWsProxy.ts +1 -1
- package/src/playwright/baseConfig.ts +1 -1
- package/src/scripts/devOnlineBackend.ts +1 -1
- package/src/scripts/runOffline.ts +12 -12
- package/src/scripts/{runOfflineFullTests.ts → runOfflineTests.ts} +79 -80
- package/src/scripts/runTests.ts +1 -1
- package/src/scripts/runVm.ts +2 -2
- package/src/vite/onlineConfig.ts +1 -1
- package/dist/scripts/runOfflineFullTests.d.ts +0 -3
- package/dist/scripts/runOfflineFullTests.d.ts.map +0 -1
- package/dist/scripts/runOfflineFullTests.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* runOfflineTests — full-stack offline orchestrator.
|
|
4
4
|
*
|
|
5
5
|
* Boots the entire offline stack in ONE Node process:
|
|
6
6
|
*
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
* reaches our Admin GraphQL mock
|
|
14
14
|
* - Playwright as a child process, with mock URLs in env
|
|
15
15
|
*
|
|
16
|
-
* The Playwright fixture (`
|
|
16
|
+
* The Playwright fixture (`offlineTest`, orchestrated mode)
|
|
17
17
|
* reads the mock URLs from env and routes browser requests via
|
|
18
18
|
* `page.route()`. Tests mutate ShopState via control-plane HTTP
|
|
19
19
|
* endpoints exposed by the storefront mock (mounted under
|
|
20
20
|
* `/__test__/state/*`).
|
|
21
21
|
*
|
|
22
22
|
* This is the offline counterpart to runTests.ts (online mode).
|
|
23
|
-
* Online tests still go via runTests.ts; offline
|
|
23
|
+
* Online tests still go via runTests.ts; offline tests via this.
|
|
24
24
|
*
|
|
25
25
|
* Env it sets in the spawned Remix backend:
|
|
26
26
|
* SHOPIFY_API_SECRET = mock JWT secret (so JWTs validate)
|
|
@@ -135,7 +135,7 @@ function dropOrphanDatabases(prefix: string): void {
|
|
|
135
135
|
.map((s) => s.trim())
|
|
136
136
|
.filter((s) => s.length > 0);
|
|
137
137
|
if (orphans.length === 0) return;
|
|
138
|
-
console.log(`[
|
|
138
|
+
console.log(`[runOfflineTests] cleanup: dropping ${orphans.length} orphan DB(s)…`);
|
|
139
139
|
for (const name of orphans) {
|
|
140
140
|
try {
|
|
141
141
|
// `--force` (psql 13+) terminates any leftover connections
|
|
@@ -152,7 +152,7 @@ function dropOrphanDatabases(prefix: string): void {
|
|
|
152
152
|
} catch (err) {
|
|
153
153
|
// If psql isn't available at all we just skip cleanup; the
|
|
154
154
|
// primary createdb below will fail loudly anyway.
|
|
155
|
-
console.warn(`[
|
|
155
|
+
console.warn(`[runOfflineTests] orphan-DB cleanup failed: ${(err as Error).message}`);
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
@@ -289,7 +289,7 @@ function ensureOfflineSetup(): void {
|
|
|
289
289
|
try {
|
|
290
290
|
hostsContent = readFileSync(hostsPath, 'utf8');
|
|
291
291
|
} catch {
|
|
292
|
-
console.warn(`[
|
|
292
|
+
console.warn(`[runOfflineTests] could not read ${hostsPath}; skipping /etc/hosts setup`);
|
|
293
293
|
return;
|
|
294
294
|
}
|
|
295
295
|
const hostsToAdd: string[] = [];
|
|
@@ -302,6 +302,10 @@ function ensureOfflineSetup(): void {
|
|
|
302
302
|
// `font_face` filter output (which contains real
|
|
303
303
|
// fonts.shopifycdn.com URLs) just works in-browser.
|
|
304
304
|
'fonts.shopifycdn.com',
|
|
305
|
+
// Partner API — so in-VM Node test code (the issue-credit helper)
|
|
306
|
+
// reaches the mock through the edge proxy, same as the backend's
|
|
307
|
+
// hard-coded https://partners.shopify.com fetch. No app-side override.
|
|
308
|
+
'partners.shopify.com',
|
|
305
309
|
]) {
|
|
306
310
|
if (!new RegExp(`^127\\.0\\.0\\.1\\s+${host.replace(/\./g, '\\.')}\\s*$`, 'm').test(hostsContent)) {
|
|
307
311
|
hostsToAdd.push(`127.0.0.1 ${host}`);
|
|
@@ -312,10 +316,10 @@ function ensureOfflineSetup(): void {
|
|
|
312
316
|
if (hostsToAdd.length > 0) {
|
|
313
317
|
try {
|
|
314
318
|
writeFileSync(hostsPath, hostsContent + '\n' + hostsToAdd.join('\n') + '\n');
|
|
315
|
-
console.log(`[
|
|
319
|
+
console.log(`[runOfflineTests] wrote ${hostsToAdd.length} /etc/hosts entries`);
|
|
316
320
|
} catch (err) {
|
|
317
321
|
console.warn(
|
|
318
|
-
`[
|
|
322
|
+
`[runOfflineTests] could not write ${hostsPath}: ${(err as Error).message}. ` +
|
|
319
323
|
`Browser may resolve Shopify hostnames to real Shopify on the internet.`,
|
|
320
324
|
);
|
|
321
325
|
}
|
|
@@ -336,12 +340,12 @@ function ensureOfflineSetup(): void {
|
|
|
336
340
|
if (existing !== TEST_CA_CERT_PEM) {
|
|
337
341
|
writeFileSync(systemCertDest, TEST_CA_CERT_PEM);
|
|
338
342
|
execSync('update-ca-certificates >/dev/null 2>&1 || true', { stdio: 'ignore' });
|
|
339
|
-
console.log('[
|
|
343
|
+
console.log('[runOfflineTests] installed edge CA into system trust store');
|
|
340
344
|
}
|
|
341
345
|
process.env['NODE_EXTRA_CA_CERTS'] = systemCertDest;
|
|
342
346
|
} catch (err) {
|
|
343
347
|
console.warn(
|
|
344
|
-
`[
|
|
348
|
+
`[runOfflineTests] could not install edge CA: ${(err as Error).message}. ` +
|
|
345
349
|
`TLS connections to the edge may fail.`,
|
|
346
350
|
);
|
|
347
351
|
}
|
|
@@ -381,10 +385,10 @@ function ensureOfflineSetup(): void {
|
|
|
381
385
|
{ stdio: 'ignore' },
|
|
382
386
|
);
|
|
383
387
|
unlinkSync(tmpCaPath);
|
|
384
|
-
console.log('[
|
|
388
|
+
console.log('[runOfflineTests] installed edge CA into NSS DB (Chrome)');
|
|
385
389
|
} catch (err) {
|
|
386
390
|
console.warn(
|
|
387
|
-
`[
|
|
391
|
+
`[runOfflineTests] could not install edge CA into NSS DB: ${(err as Error).message}. ` +
|
|
388
392
|
`Chrome will reject the edge's cert. Verify libnss3-tools is installed in the image.`,
|
|
389
393
|
);
|
|
390
394
|
}
|
|
@@ -421,7 +425,7 @@ async function startOfflineDnsRedirect(): Promise<OfflineResolver | null> {
|
|
|
421
425
|
resolver = await startOfflineResolver({ upstream });
|
|
422
426
|
} catch (err) {
|
|
423
427
|
console.warn(
|
|
424
|
-
`[
|
|
428
|
+
`[runOfflineTests] offline DNS resolver failed to bind 127.0.0.1:53: ` +
|
|
425
429
|
`${(err as Error).message}. page.request may resolve Shopify ` +
|
|
426
430
|
`hostnames to real Shopify on the internet.`,
|
|
427
431
|
);
|
|
@@ -437,12 +441,12 @@ async function startOfflineDnsRedirect(): Promise<OfflineResolver | null> {
|
|
|
437
441
|
// so an aggressive timeout would spuriously fail real lookups.
|
|
438
442
|
writeFileSync('/etc/resolv.conf', 'nameserver 127.0.0.1\n');
|
|
439
443
|
console.log(
|
|
440
|
-
'[
|
|
444
|
+
'[runOfflineTests] offline DNS resolver on 127.0.0.1:53 ' +
|
|
441
445
|
`(Shopify → loopback${upstream ? `, else → ${upstream}` : ', else → SERVFAIL'})`,
|
|
442
446
|
);
|
|
443
447
|
} catch (err) {
|
|
444
448
|
console.warn(
|
|
445
|
-
`[
|
|
449
|
+
`[runOfflineTests] could not repoint /etc/resolv.conf: ` +
|
|
446
450
|
`${(err as Error).message}. c-ares clients may bypass the resolver.`,
|
|
447
451
|
);
|
|
448
452
|
}
|
|
@@ -548,7 +552,7 @@ function seedSession(env: Record<string, string>, shopDomain: string): void {
|
|
|
548
552
|
* - Supervisor (no `TEST_OFFLINE_SHARD_INDEX` set, N>1):
|
|
549
553
|
* 1. Build cache check + build once (so shards don't race)
|
|
550
554
|
* 2. Orphan-DB cleanup once
|
|
551
|
-
* 3. Spawn N child processes (`node ...
|
|
555
|
+
* 3. Spawn N child processes (`node ... runOfflineTests.ts`)
|
|
552
556
|
* each with `TEST_OFFLINE_SHARD_INDEX=k`, `TEST_OFFLINE_SKIP_BUILD=true`
|
|
553
557
|
* 4. Stream child stdout/stderr prefixed with [shard k]
|
|
554
558
|
* 5. Wait for all, exit with worst exit code
|
|
@@ -580,14 +584,14 @@ async function main(): Promise<void> {
|
|
|
580
584
|
// strategy or invasive Chromium/Node bypasses. We tried that path
|
|
581
585
|
// (page.route() + host-resolver-rules + a Node socket-layer shim);
|
|
582
586
|
// it worked but maintained ~400 lines of bypass code in parallel.
|
|
583
|
-
// Use `npm run test:offline
|
|
587
|
+
// Use `npm run test:offline` from the consuming app — it
|
|
584
588
|
// routes through `runDockerOffline.ts` which spawns the
|
|
585
589
|
// `essential-upsell-test:latest` image where TEST_IN_CONTAINER=true
|
|
586
590
|
// is set by docker/entrypoint.sh.
|
|
587
591
|
if (process.env['TEST_IN_CONTAINER'] !== 'true') {
|
|
588
592
|
console.error(
|
|
589
|
-
'[
|
|
590
|
-
' Run: `npm run test:offline
|
|
593
|
+
'[runOfflineTests] FATAL: must run inside the offline container.\n' +
|
|
594
|
+
' Run: `npm run test:offline` (which invokes runDockerOffline.ts)\n' +
|
|
591
595
|
' If you are seeing this from inside what should be a container, the\n' +
|
|
592
596
|
" entrypoint script (tests/offline/docker/entrypoint.sh) didn't run or\n" +
|
|
593
597
|
" didn't export TEST_IN_CONTAINER=true.",
|
|
@@ -642,7 +646,7 @@ async function main(): Promise<void> {
|
|
|
642
646
|
const scopes = process.env['SCOPES'] ?? 'write_products,read_products,write_discounts,read_discounts';
|
|
643
647
|
const playwrightConfig =
|
|
644
648
|
process.env['TEST_PLAYWRIGHT_CONFIG'] ??
|
|
645
|
-
'tests/offline/playwright.offline
|
|
649
|
+
'tests/offline/playwright.offline.config.ts';
|
|
646
650
|
const isChildShard = shardIndex !== null;
|
|
647
651
|
// Backend port: shard k uses BASE + (k-1); single-worker uses BASE.
|
|
648
652
|
const baseBackendPort = Number(process.env['TEST_OFFLINE_BACKEND_PORT'] ?? '8181');
|
|
@@ -650,9 +654,9 @@ async function main(): Promise<void> {
|
|
|
650
654
|
isChildShard ? baseBackendPort + (shardIndex - 1) : baseBackendPort,
|
|
651
655
|
);
|
|
652
656
|
// Production Remix serves via plain HTTP (remix-serve has no TLS).
|
|
653
|
-
// Vite dev had self-signed HTTPS; offline
|
|
657
|
+
// Vite dev had self-signed HTTPS; offline uses HTTP. The mock
|
|
654
658
|
// admin shell serves the iframe pointing here; mixed-content
|
|
655
|
-
// warnings are silenced via Chrome flags in the
|
|
659
|
+
// warnings are silenced via Chrome flags in the offlineStack
|
|
656
660
|
// fixture.
|
|
657
661
|
const backendUrl = `http://localhost:${backendPort}`;
|
|
658
662
|
// Skip the build if TEST_OFFLINE_SKIP_BUILD=true and a recent build
|
|
@@ -667,8 +671,8 @@ async function main(): Promise<void> {
|
|
|
667
671
|
const dbConn = dbUrl(dbName);
|
|
668
672
|
const preserveDb = process.env['TEST_PRESERVE_DB'] === 'true';
|
|
669
673
|
|
|
670
|
-
console.log(`[
|
|
671
|
-
console.log(`[
|
|
674
|
+
console.log(`[runOfflineTests] Test DB: ${dbConn}`);
|
|
675
|
+
console.log(`[runOfflineTests] Backend: ${backendUrl}`);
|
|
672
676
|
|
|
673
677
|
// Drop orphan DBs from previous crashed runs. The supervisor
|
|
674
678
|
// already did this once — children skip.
|
|
@@ -676,7 +680,7 @@ async function main(): Promise<void> {
|
|
|
676
680
|
dropOrphanDatabases(dbPrefix);
|
|
677
681
|
}
|
|
678
682
|
|
|
679
|
-
console.log('[
|
|
683
|
+
console.log('[runOfflineTests] createdb…');
|
|
680
684
|
execSync(`createdb -h ${PG_HOST} -p ${PG_PORT} ${dbName}`, { stdio: 'inherit' });
|
|
681
685
|
|
|
682
686
|
// ── Mock servers (in-process, sharing one ShopState) ─────
|
|
@@ -702,7 +706,7 @@ async function main(): Promise<void> {
|
|
|
702
706
|
seedExploreProducts(state);
|
|
703
707
|
}
|
|
704
708
|
|
|
705
|
-
console.log('[
|
|
709
|
+
console.log('[runOfflineTests] booting mock servers…');
|
|
706
710
|
// 127.0.0.1 binding is the safe default — orchestrator and child
|
|
707
711
|
// both reach the mocks on loopback. Used to be subtly broken
|
|
708
712
|
// because we ran Playwright via execSync (sync, blocks the event
|
|
@@ -732,7 +736,7 @@ async function main(): Promise<void> {
|
|
|
732
736
|
: undefined;
|
|
733
737
|
if (postPurchaseExtensionPath && !existsSync(postPurchaseExtensionPath)) {
|
|
734
738
|
throw new Error(
|
|
735
|
-
`[
|
|
739
|
+
`[runOfflineTests] TEST_OFFLINE_POST_PURCHASE_EXT points at ${postPurchaseExtensionPath} but the file doesn't exist. ` +
|
|
736
740
|
`Build the extension first — the bundled .js must exist on disk before the test run.`,
|
|
737
741
|
);
|
|
738
742
|
}
|
|
@@ -778,7 +782,7 @@ async function main(): Promise<void> {
|
|
|
778
782
|
startOpts,
|
|
779
783
|
);
|
|
780
784
|
console.log(
|
|
781
|
-
`[
|
|
785
|
+
`[runOfflineTests] mocks ready:\n` +
|
|
782
786
|
` shell: ${adminShell.baseUrl}\n` +
|
|
783
787
|
` admin GQL: ${adminApi.baseUrl}\n` +
|
|
784
788
|
` storefront GQL: ${storefrontApi.baseUrl}\n` +
|
|
@@ -796,9 +800,9 @@ async function main(): Promise<void> {
|
|
|
796
800
|
for (const p of probes) {
|
|
797
801
|
try {
|
|
798
802
|
const r = await fetch(p.url, p.init);
|
|
799
|
-
console.log(`[
|
|
803
|
+
console.log(`[runOfflineTests] probe ${p.name}: ${r.status}`);
|
|
800
804
|
} catch (e) {
|
|
801
|
-
console.log(`[
|
|
805
|
+
console.log(`[runOfflineTests] probe ${p.name}: FAILED ${(e as Error).message}`);
|
|
802
806
|
}
|
|
803
807
|
}
|
|
804
808
|
|
|
@@ -831,6 +835,7 @@ async function main(): Promise<void> {
|
|
|
831
835
|
adminApi: adminApi.baseUrl,
|
|
832
836
|
storefrontApi: storefrontApi.baseUrl,
|
|
833
837
|
adminShell: adminShell.baseUrl,
|
|
838
|
+
partnerApi: partnerApi.baseUrl,
|
|
834
839
|
},
|
|
835
840
|
shopDomain: DEFAULT_SHOP_DOMAIN,
|
|
836
841
|
// Previously bound dual-stack `::` so /etc/hosts entries for
|
|
@@ -842,7 +847,7 @@ async function main(): Promise<void> {
|
|
|
842
847
|
hostname: '0.0.0.0',
|
|
843
848
|
port: 443,
|
|
844
849
|
});
|
|
845
|
-
console.log(`[
|
|
850
|
+
console.log(`[runOfflineTests] edge proxy: ${edge.baseUrl}`);
|
|
846
851
|
|
|
847
852
|
// ── Neon-local WS proxy (zero-app-touch) ──────────────────
|
|
848
853
|
// Bridge the app's @neondatabase/serverless driver to THIS run's local
|
|
@@ -851,7 +856,7 @@ async function main(): Promise<void> {
|
|
|
851
856
|
// the proxy reads the driver's `?address=` and forwards to Postgres.
|
|
852
857
|
const neonProxy = await startNeonWsProxy({ targetHost: PG_HOST, targetPort: Number(PG_PORT) });
|
|
853
858
|
console.log(
|
|
854
|
-
`[
|
|
859
|
+
`[runOfflineTests] neon ws-proxy: 127.0.0.1:${neonProxy.port} → Postgres ${PG_HOST}:${PG_PORT}`,
|
|
855
860
|
);
|
|
856
861
|
|
|
857
862
|
// ── Backend env ───────────────────────────────────────────
|
|
@@ -951,7 +956,7 @@ async function main(): Promise<void> {
|
|
|
951
956
|
: {}),
|
|
952
957
|
};
|
|
953
958
|
|
|
954
|
-
console.log('[
|
|
959
|
+
console.log('[runOfflineTests] prisma migrate deploy…');
|
|
955
960
|
// Strip the MSW loader from NODE_OPTIONS for prisma's invocation.
|
|
956
961
|
// MSW shouldn't intercept Postgres connections (it's HTTP-only),
|
|
957
962
|
// but loading it from inside `npx prisma` triggers a silent
|
|
@@ -974,19 +979,19 @@ async function main(): Promise<void> {
|
|
|
974
979
|
// a no-op for apps that don't need it.
|
|
975
980
|
const extraSchema = resolve(process.cwd(), 'tests/offline/offline-extra-schema.sql');
|
|
976
981
|
if (existsSync(extraSchema)) {
|
|
977
|
-
console.log('[
|
|
982
|
+
console.log('[runOfflineTests] applying extra schema (tests/offline/offline-extra-schema.sql)…');
|
|
978
983
|
execSync(
|
|
979
984
|
`psql -h ${PG_HOST} -p ${PG_PORT} -d ${dbName} -v ON_ERROR_STOP=1 -f ${JSON.stringify(extraSchema)}`,
|
|
980
985
|
{ stdio: 'inherit', env: prismaEnv },
|
|
981
986
|
);
|
|
982
987
|
}
|
|
983
988
|
|
|
984
|
-
console.log('[
|
|
989
|
+
console.log('[runOfflineTests] seeding session row…');
|
|
985
990
|
try {
|
|
986
991
|
seedSession(prismaEnv as Record<string, string>, DEFAULT_SHOP_DOMAIN);
|
|
987
992
|
} catch (err) {
|
|
988
993
|
console.warn(
|
|
989
|
-
`[
|
|
994
|
+
`[runOfflineTests] session seed failed (continuing): ${(err as Error).message}`,
|
|
990
995
|
);
|
|
991
996
|
}
|
|
992
997
|
|
|
@@ -1044,7 +1049,7 @@ async function main(): Promise<void> {
|
|
|
1044
1049
|
* the conventional exit code (`128 + signal`).
|
|
1045
1050
|
*/
|
|
1046
1051
|
const onSignal = (sig: 'SIGINT' | 'SIGTERM'): void => {
|
|
1047
|
-
console.log(`\n[
|
|
1052
|
+
console.log(`\n[runOfflineTests] received ${sig}, tearing down…`);
|
|
1048
1053
|
cleanup();
|
|
1049
1054
|
// 128 + signal number is the POSIX convention.
|
|
1050
1055
|
process.exit(sig === 'SIGINT' ? 130 : 143);
|
|
@@ -1082,15 +1087,15 @@ async function main(): Promise<void> {
|
|
|
1082
1087
|
const skipBuildBecauseCache = cachedHash === currentBuildHash;
|
|
1083
1088
|
|
|
1084
1089
|
if (skipBuildBecauseExplicit) {
|
|
1085
|
-
console.log('[
|
|
1090
|
+
console.log('[runOfflineTests] skipping build (TEST_OFFLINE_SKIP_BUILD=true)');
|
|
1086
1091
|
} else if (skipBuildBecauseCache) {
|
|
1087
1092
|
console.log(
|
|
1088
|
-
`[
|
|
1093
|
+
`[runOfflineTests] build cache HIT (${currentBuildHash.slice(0, 12)}) — skipping npm run build. ` +
|
|
1089
1094
|
`Force with TEST_OFFLINE_FORCE_BUILD=true.`,
|
|
1090
1095
|
);
|
|
1091
1096
|
} else {
|
|
1092
1097
|
console.log(
|
|
1093
|
-
`[
|
|
1098
|
+
`[runOfflineTests] build cache MISS — running npm run build. ` +
|
|
1094
1099
|
`(${cachedHash ? `was ${cachedHash.slice(0, 12)}, now ` : 'no cache, '}${currentBuildHash.slice(0, 12)})`,
|
|
1095
1100
|
);
|
|
1096
1101
|
execSync('npm run build', {
|
|
@@ -1101,7 +1106,7 @@ async function main(): Promise<void> {
|
|
|
1101
1106
|
// crashed build leaves stale `build/` we shouldn't endorse.
|
|
1102
1107
|
writeFileSync(buildCachePath, currentBuildHash);
|
|
1103
1108
|
}
|
|
1104
|
-
console.log('[
|
|
1109
|
+
console.log('[runOfflineTests] booting Remix prod server (remix-serve)…');
|
|
1105
1110
|
// Resolve remix-serve's CLI path directly — `npx remix-serve`
|
|
1106
1111
|
// on some systems alters NODE_OPTIONS (it spawns a child via
|
|
1107
1112
|
// npm-cli with PATH manipulation), which silently drops our
|
|
@@ -1140,9 +1145,9 @@ async function main(): Promise<void> {
|
|
|
1140
1145
|
env: backendRuntimeEnv,
|
|
1141
1146
|
},
|
|
1142
1147
|
);
|
|
1143
|
-
console.log(`[
|
|
1148
|
+
console.log(`[runOfflineTests] waiting for backend at ${backendUrl}…`);
|
|
1144
1149
|
await waitForReady(backendUrl, READY_TIMEOUT_MS);
|
|
1145
|
-
console.log('[
|
|
1150
|
+
console.log('[runOfflineTests] backend ready.');
|
|
1146
1151
|
|
|
1147
1152
|
// ── Pre-warm Remix routes ──────────────────────────────────
|
|
1148
1153
|
// remix-serve lazy-compiles each route's loader/action module on
|
|
@@ -1162,7 +1167,7 @@ async function main(): Promise<void> {
|
|
|
1162
1167
|
//
|
|
1163
1168
|
// Failures are non-fatal — the next real test request will
|
|
1164
1169
|
// surface a clearer error than "warm-up failed".
|
|
1165
|
-
console.log('[
|
|
1170
|
+
console.log('[runOfflineTests] pre-warming hot Remix routes…');
|
|
1166
1171
|
const warmupToken = await mintIdToken({
|
|
1167
1172
|
shop: DEFAULT_SHOP_DOMAIN,
|
|
1168
1173
|
clientId: MOCK_CLIENT_ID,
|
|
@@ -1197,7 +1202,7 @@ async function main(): Promise<void> {
|
|
|
1197
1202
|
authorization: `Bearer ${warmupToken}`,
|
|
1198
1203
|
// shopify-app-remix's `isbot` check returns 410 for
|
|
1199
1204
|
// Node's default User-Agent. Match the same UA the
|
|
1200
|
-
// browser context uses in tests (see
|
|
1205
|
+
// browser context uses in tests (see offlineStack
|
|
1201
1206
|
// fixture) so the request reaches the loader.
|
|
1202
1207
|
'user-agent':
|
|
1203
1208
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
|
|
@@ -1218,7 +1223,7 @@ async function main(): Promise<void> {
|
|
|
1218
1223
|
}),
|
|
1219
1224
|
);
|
|
1220
1225
|
console.log(
|
|
1221
|
-
`[
|
|
1226
|
+
`[runOfflineTests] route pre-warm done (${Date.now() - t0}ms):`,
|
|
1222
1227
|
);
|
|
1223
1228
|
for (const r of warmupResults) {
|
|
1224
1229
|
const status =
|
|
@@ -1246,7 +1251,7 @@ async function main(): Promise<void> {
|
|
|
1246
1251
|
const adminUrl =
|
|
1247
1252
|
process.env['TEST_OFFLINE_EXPLORE_URL'] ??
|
|
1248
1253
|
`https://admin.shopify.com/store/test/apps/${DEFAULT_APP_HANDLE}/app`;
|
|
1249
|
-
console.log('[
|
|
1254
|
+
console.log('[runOfflineTests] explore mode — launching Chrome at:');
|
|
1250
1255
|
console.log(` ${adminUrl}`);
|
|
1251
1256
|
console.log('');
|
|
1252
1257
|
console.log(' Useful URLs you can navigate to (all routed via the edge proxy):');
|
|
@@ -1356,7 +1361,7 @@ async function main(): Promise<void> {
|
|
|
1356
1361
|
await sfPage.goto(storefrontUrl, { waitUntil: 'domcontentloaded' });
|
|
1357
1362
|
} catch (err) {
|
|
1358
1363
|
console.error(
|
|
1359
|
-
`[
|
|
1364
|
+
`[runOfflineTests] explore: storefront tab failed to load (${(err as Error).message}) — continuing`,
|
|
1360
1365
|
);
|
|
1361
1366
|
}
|
|
1362
1367
|
// Leave the admin tab focused as the active one.
|
|
@@ -1381,7 +1386,7 @@ async function main(): Promise<void> {
|
|
|
1381
1386
|
}
|
|
1382
1387
|
|
|
1383
1388
|
// ── Playwright ─────────────────────────────────────────
|
|
1384
|
-
console.log('[
|
|
1389
|
+
console.log('[runOfflineTests] running Playwright…');
|
|
1385
1390
|
// Headed-under-Xvfb is the DEFAULT for offline runs (production-
|
|
1386
1391
|
// faithful, ~4% slower than headless). Opt OUT with
|
|
1387
1392
|
// TEST_OFFLINE_HEADLESS=true. Full-screen recording (TEST_OFFLINE_VIDEO)
|
|
@@ -1406,7 +1411,7 @@ async function main(): Promise<void> {
|
|
|
1406
1411
|
}
|
|
1407
1412
|
if (!hasFfmpeg) {
|
|
1408
1413
|
console.error(
|
|
1409
|
-
'[
|
|
1414
|
+
'[runOfflineTests] TEST_OFFLINE_VIDEO set but `ffmpeg` is missing from the ' +
|
|
1410
1415
|
'VM image — rebuild it (it predates video support): ' +
|
|
1411
1416
|
'`container image rm essential-apps/shopify-test-vm:latest` then re-run. ' +
|
|
1412
1417
|
'Continuing headed WITHOUT recording.',
|
|
@@ -1433,7 +1438,7 @@ async function main(): Promise<void> {
|
|
|
1433
1438
|
headedReady = existsSync('/tmp/.X11-unix/X99');
|
|
1434
1439
|
if (!headedReady) {
|
|
1435
1440
|
console.error(
|
|
1436
|
-
'[
|
|
1441
|
+
'[runOfflineTests] WARNING: Xvfb :99 did not start — falling back to headless',
|
|
1437
1442
|
);
|
|
1438
1443
|
videoOn = false;
|
|
1439
1444
|
headedOn = false;
|
|
@@ -1441,7 +1446,7 @@ async function main(): Promise<void> {
|
|
|
1441
1446
|
process.env['TEST_OFFLINE_HEADED'] = 'false';
|
|
1442
1447
|
} else {
|
|
1443
1448
|
console.error(
|
|
1444
|
-
`[
|
|
1449
|
+
`[runOfflineTests] headed under Xvfb :99 (${videoOn ? 'recording' : 'no recording'})`,
|
|
1445
1450
|
);
|
|
1446
1451
|
}
|
|
1447
1452
|
}
|
|
@@ -1487,19 +1492,13 @@ async function main(): Promise<void> {
|
|
|
1487
1492
|
// (backendEnv sets it too). Without it the helper's credit comes back
|
|
1488
1493
|
// test:false and the assertion fails.
|
|
1489
1494
|
BILLING_IS_TEST: process.env['BILLING_IS_TEST'] ?? 'true',
|
|
1490
|
-
//
|
|
1491
|
-
//
|
|
1492
|
-
//
|
|
1493
|
-
//
|
|
1494
|
-
//
|
|
1495
|
-
//
|
|
1496
|
-
//
|
|
1497
|
-
// hit `page.request.post(...)`. Instead the app's PartnerApiClient
|
|
1498
|
-
// honours this env override for the base origin (a prod-safe test
|
|
1499
|
-
// seam; unset in prod → real URL). The mock serves the identical
|
|
1500
|
-
// `/:org/api/:ver/graphql.json` path, so only the origin changes —
|
|
1501
|
-
// the real client / GraphQL / response-parsing all still run.
|
|
1502
|
-
SHOPIFY_PARTNER_API_BASE_URL: partnerApi.baseUrl,
|
|
1495
|
+
// NB: the app's Partner API client fetches its hard-coded
|
|
1496
|
+
// https://partners.shopify.com endpoint with NO override. That host
|
|
1497
|
+
// is routed to the in-VM partner mock via /etc/hosts → the edge
|
|
1498
|
+
// proxy (see ensureOfflineSetup + edgeProxy's partners.shopify.com
|
|
1499
|
+
// case), so the issue-credit helper's Node fetch reaches the mock
|
|
1500
|
+
// with no app-side change and no MSW preload (which would corrupt
|
|
1501
|
+
// Playwright's page.request).
|
|
1503
1502
|
};
|
|
1504
1503
|
// Use async spawn (NOT execSync) — execSync blocks the
|
|
1505
1504
|
// orchestrator's event loop, so the in-process mock servers
|
|
@@ -1526,7 +1525,7 @@ async function main(): Promise<void> {
|
|
|
1526
1525
|
// a file path, `--repeat-each`, etc. The docker runner already
|
|
1527
1526
|
// forwards user args verbatim into this script's argv; we just
|
|
1528
1527
|
// pass them through to playwright so single-test iteration is
|
|
1529
|
-
// possible (`npm run test:offline
|
|
1528
|
+
// possible (`npm run test:offline -- --grep "PDP form"`).
|
|
1530
1529
|
// Skip our own known flags (currently none after the launcher
|
|
1531
1530
|
// strips them — process.argv[0] is `node`, argv[1] is this
|
|
1532
1531
|
// script).
|
|
@@ -1549,9 +1548,9 @@ async function main(): Promise<void> {
|
|
|
1549
1548
|
}
|
|
1550
1549
|
exitCode = 0;
|
|
1551
1550
|
} catch (err) {
|
|
1552
|
-
console.error(`[
|
|
1551
|
+
console.error(`[runOfflineTests] failed: ${(err as Error).message}`);
|
|
1553
1552
|
} finally {
|
|
1554
|
-
console.log('[
|
|
1553
|
+
console.log('[runOfflineTests] tearing down…');
|
|
1555
1554
|
// `cleanup()` kills the backend AND drops the DB (with
|
|
1556
1555
|
// preserveDb honored). It's the same function the signal
|
|
1557
1556
|
// handlers call, so the teardown path is identical whether
|
|
@@ -1571,7 +1570,7 @@ async function main(): Promise<void> {
|
|
|
1571
1570
|
]);
|
|
1572
1571
|
|
|
1573
1572
|
if (preserveDb) {
|
|
1574
|
-
console.log(`[
|
|
1573
|
+
console.log(`[runOfflineTests] DB preserved: ${dbName}`);
|
|
1575
1574
|
}
|
|
1576
1575
|
}
|
|
1577
1576
|
|
|
@@ -1600,28 +1599,28 @@ function loadExtensions(): ExtensionConfig[] {
|
|
|
1600
1599
|
parsed = JSON.parse(raw);
|
|
1601
1600
|
} catch (err) {
|
|
1602
1601
|
throw new Error(
|
|
1603
|
-
`[
|
|
1602
|
+
`[runOfflineTests] TEST_OFFLINE_EXTENSIONS_JSON is not valid JSON: ${(err as Error).message}`,
|
|
1604
1603
|
);
|
|
1605
1604
|
}
|
|
1606
1605
|
if (!Array.isArray(parsed)) {
|
|
1607
1606
|
throw new Error(
|
|
1608
|
-
`[
|
|
1607
|
+
`[runOfflineTests] TEST_OFFLINE_EXTENSIONS_JSON must be a JSON array; got ${typeof parsed}`,
|
|
1609
1608
|
);
|
|
1610
1609
|
}
|
|
1611
1610
|
const out: ExtensionConfig[] = [];
|
|
1612
1611
|
for (const entry of parsed) {
|
|
1613
1612
|
if (typeof entry !== 'object' || entry === null) {
|
|
1614
|
-
throw new Error(`[
|
|
1613
|
+
throw new Error(`[runOfflineTests] extension entry must be an object: ${JSON.stringify(entry)}`);
|
|
1615
1614
|
}
|
|
1616
1615
|
const e = entry as { name?: unknown; rootDir?: unknown; enabled?: unknown };
|
|
1617
1616
|
if (typeof e.name !== 'string' || typeof e.rootDir !== 'string') {
|
|
1618
1617
|
throw new Error(
|
|
1619
|
-
`[
|
|
1618
|
+
`[runOfflineTests] each extension needs { name: string, rootDir: string }; got ${JSON.stringify(entry)}`,
|
|
1620
1619
|
);
|
|
1621
1620
|
}
|
|
1622
1621
|
const rootDir = resolve(process.cwd(), e.rootDir);
|
|
1623
1622
|
if (!existsSync(rootDir)) {
|
|
1624
|
-
throw new Error(`[
|
|
1623
|
+
throw new Error(`[runOfflineTests] extension rootDir does not exist: ${rootDir}`);
|
|
1625
1624
|
}
|
|
1626
1625
|
out.push({
|
|
1627
1626
|
name: e.name,
|
|
@@ -1631,7 +1630,7 @@ function loadExtensions(): ExtensionConfig[] {
|
|
|
1631
1630
|
}
|
|
1632
1631
|
if (out.length > 0) {
|
|
1633
1632
|
console.log(
|
|
1634
|
-
`[
|
|
1633
|
+
`[runOfflineTests] loaded ${out.length} extension(s): ${out.map((e) => `${e.name}=${e.rootDir}`).join(', ')}`,
|
|
1635
1634
|
);
|
|
1636
1635
|
}
|
|
1637
1636
|
return out;
|
|
@@ -1755,7 +1754,7 @@ function seedExploreProducts(state: ShopState): void {
|
|
|
1755
1754
|
}
|
|
1756
1755
|
|
|
1757
1756
|
async function runSupervisor(workers: number): Promise<number> {
|
|
1758
|
-
console.log(`[
|
|
1757
|
+
console.log(`[runOfflineTests] supervisor: spawning ${workers} shard(s)…`);
|
|
1759
1758
|
|
|
1760
1759
|
// Pre-build: each child would otherwise duplicate the build (or
|
|
1761
1760
|
// race on the cache). Build here once, then pass SKIP_BUILD=true
|
|
@@ -1769,9 +1768,9 @@ async function runSupervisor(workers: number): Promise<number> {
|
|
|
1769
1768
|
? readFileSync(buildCachePath, 'utf8').trim()
|
|
1770
1769
|
: null;
|
|
1771
1770
|
if (cachedHash === currentBuildHash) {
|
|
1772
|
-
console.log(`[
|
|
1771
|
+
console.log(`[runOfflineTests] supervisor: build cache HIT (${currentBuildHash.slice(0, 12)})`);
|
|
1773
1772
|
} else {
|
|
1774
|
-
console.log(`[
|
|
1773
|
+
console.log(`[runOfflineTests] supervisor: build cache MISS — running npm run build…`);
|
|
1775
1774
|
execSync('npm run build', {
|
|
1776
1775
|
stdio: 'inherit',
|
|
1777
1776
|
// `TEST_OFFLINE=true` switches the consuming app's
|
|
@@ -1807,7 +1806,7 @@ async function runSupervisor(workers: number): Promise<number> {
|
|
|
1807
1806
|
children.push(
|
|
1808
1807
|
new Promise<number>((resolveChild, rejectChild) => {
|
|
1809
1808
|
// Re-exec ourselves through `node --import tsx` so TS still
|
|
1810
|
-
// parses. argv[1] points at the user's `
|
|
1809
|
+
// parses. argv[1] points at the user's `runOfflineTests.ts`
|
|
1811
1810
|
// by the time we're invoked from the consuming app's
|
|
1812
1811
|
// npm script.
|
|
1813
1812
|
const proc = spawn(
|
|
@@ -1823,7 +1822,7 @@ async function runSupervisor(workers: number): Promise<number> {
|
|
|
1823
1822
|
const exitCodes = await Promise.all(children);
|
|
1824
1823
|
const worst = exitCodes.reduce((a, b) => Math.max(a, b), 0);
|
|
1825
1824
|
console.log(
|
|
1826
|
-
`[
|
|
1825
|
+
`[runOfflineTests] supervisor: shard exit codes [${exitCodes.join(', ')}]; overall=${worst}`,
|
|
1827
1826
|
);
|
|
1828
1827
|
return worst;
|
|
1829
1828
|
}
|
package/src/scripts/runTests.ts
CHANGED
|
@@ -164,7 +164,7 @@ async function main(): Promise<void> {
|
|
|
164
164
|
// from .env.test only.
|
|
165
165
|
const backendEnv: NodeJS.ProcessEnv = {
|
|
166
166
|
PATH: process.env['PATH'] ?? '',
|
|
167
|
-
// See
|
|
167
|
+
// See runOfflineTests.ts for the long-form note — empty HOME
|
|
168
168
|
// makes npm write its cache to literal `~/.npm` in cwd.
|
|
169
169
|
HOME: process.env['HOME'] || '/root',
|
|
170
170
|
USER: process.env['USER'] ?? '',
|
package/src/scripts/runVm.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* ───────── ────────────
|
|
11
11
|
* amd64 image (Rosetta) arm64 image (native)
|
|
12
12
|
* ~10 s cold boot ~5 s cold bake / ~1 s warm restore
|
|
13
|
-
* ~30 s/test wall-clock ~3× faster end-to-end on offline
|
|
13
|
+
* ~30 s/test wall-clock ~3× faster end-to-end on offline
|
|
14
14
|
* Google Chrome amd64 in Xvfb Playwright bundled chromium in
|
|
15
15
|
* Xvfb (patchright stealth still works)
|
|
16
16
|
*
|
|
@@ -553,7 +553,7 @@ async function main(): Promise<void> {
|
|
|
553
553
|
});
|
|
554
554
|
|
|
555
555
|
// Stream + collect output. Hard 10-min cap by default, with a
|
|
556
|
-
// 60 s no-output stall guard (mirrors the offline
|
|
556
|
+
// 60 s no-output stall guard (mirrors the offline bench's
|
|
557
557
|
// proven pattern for live processes).
|
|
558
558
|
const maxMs = Number(process.env['TEST_ONLINE_VM_TIMEOUT_MS'] ?? 10 * 60 * 1000);
|
|
559
559
|
// Stall guard: kill the test process if no output for N seconds.
|
package/src/vite/onlineConfig.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { loadConfigFromFile, mergeConfig, type ConfigEnv, type UserConfig } from
|
|
|
5
5
|
* suite — `runTests.ts` boots the dev server with
|
|
6
6
|
* `vite --config <this file>`. It is never referenced by an app's own
|
|
7
7
|
* `vite dev` / `vite build`, so it cannot change any app's production
|
|
8
|
-
* or local-dev behaviour. (Offline
|
|
8
|
+
* or local-dev behaviour. (Offline serves a production build via
|
|
9
9
|
* remix-serve; conformance runs VM probes — neither uses Vite dev, so
|
|
10
10
|
* this is online-only.)
|
|
11
11
|
*
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"runOfflineFullTests.d.ts","sourceRoot":"","sources":["../../src/scripts/runOfflineFullTests.ts"],"names":[],"mappings":""}
|