@essential-apps/shopify-test-runner 1.0.0

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.
Files changed (265) hide show
  1. package/dist/contracts/normalize.d.ts +61 -0
  2. package/dist/contracts/normalize.d.ts.map +1 -0
  3. package/dist/contracts/normalize.js +99 -0
  4. package/dist/contracts/normalize.js.map +1 -0
  5. package/dist/contracts/normalizeHtml.d.ts +37 -0
  6. package/dist/contracts/normalizeHtml.d.ts.map +1 -0
  7. package/dist/contracts/normalizeHtml.js +89 -0
  8. package/dist/contracts/normalizeHtml.js.map +1 -0
  9. package/dist/edge/cert.d.ts +44 -0
  10. package/dist/edge/cert.d.ts.map +1 -0
  11. package/dist/edge/cert.js +117 -0
  12. package/dist/edge/cert.js.map +1 -0
  13. package/dist/edge/edgeProxy.d.ts +43 -0
  14. package/dist/edge/edgeProxy.d.ts.map +1 -0
  15. package/dist/edge/edgeProxy.js +297 -0
  16. package/dist/edge/edgeProxy.js.map +1 -0
  17. package/dist/edge/nodeShim.d.ts +2 -0
  18. package/dist/edge/nodeShim.d.ts.map +1 -0
  19. package/dist/edge/nodeShim.js +217 -0
  20. package/dist/edge/nodeShim.js.map +1 -0
  21. package/dist/index.d.ts +39 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +36 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/lib/buildSourceBundle.d.ts +56 -0
  26. package/dist/lib/buildSourceBundle.d.ts.map +1 -0
  27. package/dist/lib/buildSourceBundle.js +153 -0
  28. package/dist/lib/buildSourceBundle.js.map +1 -0
  29. package/dist/lib/freePort.d.ts +5 -0
  30. package/dist/lib/freePort.d.ts.map +1 -0
  31. package/dist/lib/freePort.js +33 -0
  32. package/dist/lib/freePort.js.map +1 -0
  33. package/dist/lib/functionBuild.d.ts +99 -0
  34. package/dist/lib/functionBuild.d.ts.map +1 -0
  35. package/dist/lib/functionBuild.js +413 -0
  36. package/dist/lib/functionBuild.js.map +1 -0
  37. package/dist/lib/neonWsProxy.d.ts +41 -0
  38. package/dist/lib/neonWsProxy.d.ts.map +1 -0
  39. package/dist/lib/neonWsProxy.js +101 -0
  40. package/dist/lib/neonWsProxy.js.map +1 -0
  41. package/dist/lib/sourceZipUpload.d.ts +45 -0
  42. package/dist/lib/sourceZipUpload.d.ts.map +1 -0
  43. package/dist/lib/sourceZipUpload.js +129 -0
  44. package/dist/lib/sourceZipUpload.js.map +1 -0
  45. package/dist/lib/stealthLaunch.d.ts +35 -0
  46. package/dist/lib/stealthLaunch.d.ts.map +1 -0
  47. package/dist/lib/stealthLaunch.js +46 -0
  48. package/dist/lib/stealthLaunch.js.map +1 -0
  49. package/dist/lib/storeAutomation.d.ts +22 -0
  50. package/dist/lib/storeAutomation.d.ts.map +1 -0
  51. package/dist/lib/storeAutomation.js +85 -0
  52. package/dist/lib/storeAutomation.js.map +1 -0
  53. package/dist/playwright/baseConfig.d.ts +62 -0
  54. package/dist/playwright/baseConfig.d.ts.map +1 -0
  55. package/dist/playwright/baseConfig.js +68 -0
  56. package/dist/playwright/baseConfig.js.map +1 -0
  57. package/dist/playwright/globalSetup.d.ts +2 -0
  58. package/dist/playwright/globalSetup.d.ts.map +1 -0
  59. package/dist/playwright/globalSetup.js +139 -0
  60. package/dist/playwright/globalSetup.js.map +1 -0
  61. package/dist/playwright/index.d.ts +9 -0
  62. package/dist/playwright/index.d.ts.map +1 -0
  63. package/dist/playwright/index.js +9 -0
  64. package/dist/playwright/index.js.map +1 -0
  65. package/dist/probes/fonts.d.ts +4 -0
  66. package/dist/probes/fonts.d.ts.map +1 -0
  67. package/dist/probes/fonts.js +255 -0
  68. package/dist/probes/fonts.js.map +1 -0
  69. package/dist/probes/mirror.d.ts +4 -0
  70. package/dist/probes/mirror.d.ts.map +1 -0
  71. package/dist/probes/mirror.js +260 -0
  72. package/dist/probes/mirror.js.map +1 -0
  73. package/dist/probes/runProbe.d.ts +3 -0
  74. package/dist/probes/runProbe.d.ts.map +1 -0
  75. package/dist/probes/runProbe.js +219 -0
  76. package/dist/probes/runProbe.js.map +1 -0
  77. package/dist/probes/types.d.ts +72 -0
  78. package/dist/probes/types.d.ts.map +1 -0
  79. package/dist/probes/types.js +2 -0
  80. package/dist/probes/types.js.map +1 -0
  81. package/dist/scripts/_probeSourceUrl.d.ts +3 -0
  82. package/dist/scripts/_probeSourceUrl.d.ts.map +1 -0
  83. package/dist/scripts/_probeSourceUrl.js +119 -0
  84. package/dist/scripts/_probeSourceUrl.js.map +1 -0
  85. package/dist/scripts/addStore.d.ts +3 -0
  86. package/dist/scripts/addStore.d.ts.map +1 -0
  87. package/dist/scripts/addStore.js +46 -0
  88. package/dist/scripts/addStore.js.map +1 -0
  89. package/dist/scripts/buildDockerImage.d.ts +3 -0
  90. package/dist/scripts/buildDockerImage.d.ts.map +1 -0
  91. package/dist/scripts/buildDockerImage.js +60 -0
  92. package/dist/scripts/buildDockerImage.js.map +1 -0
  93. package/dist/scripts/captureAuth.d.ts +3 -0
  94. package/dist/scripts/captureAuth.d.ts.map +1 -0
  95. package/dist/scripts/captureAuth.js +124 -0
  96. package/dist/scripts/captureAuth.js.map +1 -0
  97. package/dist/scripts/captureContracts.d.ts +3 -0
  98. package/dist/scripts/captureContracts.d.ts.map +1 -0
  99. package/dist/scripts/captureContracts.js +517 -0
  100. package/dist/scripts/captureContracts.js.map +1 -0
  101. package/dist/scripts/captureRestContracts.d.ts +3 -0
  102. package/dist/scripts/captureRestContracts.d.ts.map +1 -0
  103. package/dist/scripts/captureRestContracts.js +245 -0
  104. package/dist/scripts/captureRestContracts.js.map +1 -0
  105. package/dist/scripts/checkOperationCoverage.d.ts +3 -0
  106. package/dist/scripts/checkOperationCoverage.d.ts.map +1 -0
  107. package/dist/scripts/checkOperationCoverage.js +302 -0
  108. package/dist/scripts/checkOperationCoverage.js.map +1 -0
  109. package/dist/scripts/cleanupStores.d.ts +3 -0
  110. package/dist/scripts/cleanupStores.d.ts.map +1 -0
  111. package/dist/scripts/cleanupStores.js +77 -0
  112. package/dist/scripts/cleanupStores.js.map +1 -0
  113. package/dist/scripts/createStores.d.ts +3 -0
  114. package/dist/scripts/createStores.d.ts.map +1 -0
  115. package/dist/scripts/createStores.js +66 -0
  116. package/dist/scripts/createStores.js.map +1 -0
  117. package/dist/scripts/deployAppVersion.d.ts +3 -0
  118. package/dist/scripts/deployAppVersion.d.ts.map +1 -0
  119. package/dist/scripts/deployAppVersion.js +591 -0
  120. package/dist/scripts/deployAppVersion.js.map +1 -0
  121. package/dist/scripts/devE2eBackend.d.ts +3 -0
  122. package/dist/scripts/devE2eBackend.d.ts.map +1 -0
  123. package/dist/scripts/devE2eBackend.js +117 -0
  124. package/dist/scripts/devE2eBackend.js.map +1 -0
  125. package/dist/scripts/devOnlineBackend.d.ts +3 -0
  126. package/dist/scripts/devOnlineBackend.d.ts.map +1 -0
  127. package/dist/scripts/devOnlineBackend.js +117 -0
  128. package/dist/scripts/devOnlineBackend.js.map +1 -0
  129. package/dist/scripts/installApp.d.ts +3 -0
  130. package/dist/scripts/installApp.d.ts.map +1 -0
  131. package/dist/scripts/installApp.js +163 -0
  132. package/dist/scripts/installApp.js.map +1 -0
  133. package/dist/scripts/listStores.d.ts +3 -0
  134. package/dist/scripts/listStores.d.ts.map +1 -0
  135. package/dist/scripts/listStores.js +18 -0
  136. package/dist/scripts/listStores.js.map +1 -0
  137. package/dist/scripts/runDocker.d.ts +3 -0
  138. package/dist/scripts/runDocker.d.ts.map +1 -0
  139. package/dist/scripts/runDocker.js +88 -0
  140. package/dist/scripts/runDocker.js.map +1 -0
  141. package/dist/scripts/runDockerAuth.d.ts +3 -0
  142. package/dist/scripts/runDockerAuth.d.ts.map +1 -0
  143. package/dist/scripts/runDockerAuth.js +108 -0
  144. package/dist/scripts/runDockerAuth.js.map +1 -0
  145. package/dist/scripts/runDockerOffline.d.ts +3 -0
  146. package/dist/scripts/runDockerOffline.d.ts.map +1 -0
  147. package/dist/scripts/runDockerOffline.js +129 -0
  148. package/dist/scripts/runDockerOffline.js.map +1 -0
  149. package/dist/scripts/runDockerOfflineExplore.d.ts +3 -0
  150. package/dist/scripts/runDockerOfflineExplore.d.ts.map +1 -0
  151. package/dist/scripts/runDockerOfflineExplore.js +116 -0
  152. package/dist/scripts/runDockerOfflineExplore.js.map +1 -0
  153. package/dist/scripts/runIsolatedDockerOffline.d.ts +3 -0
  154. package/dist/scripts/runIsolatedDockerOffline.d.ts.map +1 -0
  155. package/dist/scripts/runIsolatedDockerOffline.js +351 -0
  156. package/dist/scripts/runIsolatedDockerOffline.js.map +1 -0
  157. package/dist/scripts/runOffline.d.ts +3 -0
  158. package/dist/scripts/runOffline.d.ts.map +1 -0
  159. package/dist/scripts/runOffline.js +521 -0
  160. package/dist/scripts/runOffline.js.map +1 -0
  161. package/dist/scripts/runOfflineE2e.d.ts +3 -0
  162. package/dist/scripts/runOfflineE2e.d.ts.map +1 -0
  163. package/dist/scripts/runOfflineE2e.js +408 -0
  164. package/dist/scripts/runOfflineE2e.js.map +1 -0
  165. package/dist/scripts/runOfflineFullTests.d.ts +3 -0
  166. package/dist/scripts/runOfflineFullTests.d.ts.map +1 -0
  167. package/dist/scripts/runOfflineFullTests.js +1456 -0
  168. package/dist/scripts/runOfflineFullTests.js.map +1 -0
  169. package/dist/scripts/runSupermachine.d.ts +3 -0
  170. package/dist/scripts/runSupermachine.d.ts.map +1 -0
  171. package/dist/scripts/runSupermachine.js +474 -0
  172. package/dist/scripts/runSupermachine.js.map +1 -0
  173. package/dist/scripts/runSupermachineAuth.d.ts +3 -0
  174. package/dist/scripts/runSupermachineAuth.d.ts.map +1 -0
  175. package/dist/scripts/runSupermachineAuth.js +454 -0
  176. package/dist/scripts/runSupermachineAuth.js.map +1 -0
  177. package/dist/scripts/runTests.d.ts +3 -0
  178. package/dist/scripts/runTests.d.ts.map +1 -0
  179. package/dist/scripts/runTests.js +278 -0
  180. package/dist/scripts/runTests.js.map +1 -0
  181. package/dist/scripts/runVm.d.ts +3 -0
  182. package/dist/scripts/runVm.d.ts.map +1 -0
  183. package/dist/scripts/runVm.js +524 -0
  184. package/dist/scripts/runVm.js.map +1 -0
  185. package/dist/scripts/runVmAuth.d.ts +3 -0
  186. package/dist/scripts/runVmAuth.d.ts.map +1 -0
  187. package/dist/scripts/runVmAuth.js +475 -0
  188. package/dist/scripts/runVmAuth.js.map +1 -0
  189. package/dist/scripts/runVmScript.d.ts +3 -0
  190. package/dist/scripts/runVmScript.d.ts.map +1 -0
  191. package/dist/scripts/runVmScript.js +242 -0
  192. package/dist/scripts/runVmScript.js.map +1 -0
  193. package/dist/scripts/setupTestDb.d.ts +3 -0
  194. package/dist/scripts/setupTestDb.d.ts.map +1 -0
  195. package/dist/scripts/setupTestDb.js +61 -0
  196. package/dist/scripts/setupTestDb.js.map +1 -0
  197. package/dist/scripts/verifyContracts.d.ts +3 -0
  198. package/dist/scripts/verifyContracts.d.ts.map +1 -0
  199. package/dist/scripts/verifyContracts.js +258 -0
  200. package/dist/scripts/verifyContracts.js.map +1 -0
  201. package/dist/scripts/verifyRestContracts.d.ts +3 -0
  202. package/dist/scripts/verifyRestContracts.d.ts.map +1 -0
  203. package/dist/scripts/verifyRestContracts.js +237 -0
  204. package/dist/scripts/verifyRestContracts.js.map +1 -0
  205. package/dist/vite/offlineConfig.d.ts +34 -0
  206. package/dist/vite/offlineConfig.d.ts.map +1 -0
  207. package/dist/vite/offlineConfig.js +61 -0
  208. package/dist/vite/offlineConfig.js.map +1 -0
  209. package/dist/vite/onlineConfig.d.ts +42 -0
  210. package/dist/vite/onlineConfig.d.ts.map +1 -0
  211. package/dist/vite/onlineConfig.js +56 -0
  212. package/dist/vite/onlineConfig.js.map +1 -0
  213. package/docker/Dockerfile +67 -0
  214. package/docker/Dockerfile.vm +137 -0
  215. package/docker/README.md +50 -0
  216. package/docker/entrypoint.sh +198 -0
  217. package/package.json +85 -0
  218. package/src/contracts/normalize.ts +96 -0
  219. package/src/contracts/normalizeHtml.ts +98 -0
  220. package/src/edge/ca.cnf +14 -0
  221. package/src/edge/ca.crt +22 -0
  222. package/src/edge/ca.key +28 -0
  223. package/src/edge/cert.ts +117 -0
  224. package/src/edge/edgeProxy.ts +390 -0
  225. package/src/edge/server.cnf +28 -0
  226. package/src/edge/server.crt +26 -0
  227. package/src/edge/server.key +28 -0
  228. package/src/index.ts +67 -0
  229. package/src/lib/buildSourceBundle.ts +197 -0
  230. package/src/lib/freePort.ts +33 -0
  231. package/src/lib/functionBuild.ts +490 -0
  232. package/src/lib/neonWsProxy.ts +124 -0
  233. package/src/lib/sourceZipUpload.ts +168 -0
  234. package/src/lib/stealthLaunch.ts +57 -0
  235. package/src/lib/storeAutomation.ts +110 -0
  236. package/src/playwright/baseConfig.ts +120 -0
  237. package/src/playwright/globalSetup.ts +179 -0
  238. package/src/playwright/index.ts +11 -0
  239. package/src/probes/fonts.ts +279 -0
  240. package/src/probes/mirror.ts +283 -0
  241. package/src/probes/runProbe.ts +257 -0
  242. package/src/probes/types.ts +73 -0
  243. package/src/scripts/addStore.ts +59 -0
  244. package/src/scripts/buildDockerImage.ts +66 -0
  245. package/src/scripts/captureAuth.ts +145 -0
  246. package/src/scripts/captureContracts.ts +675 -0
  247. package/src/scripts/captureRestContracts.ts +319 -0
  248. package/src/scripts/checkOperationCoverage.ts +365 -0
  249. package/src/scripts/cleanupStores.ts +91 -0
  250. package/src/scripts/createStores.ts +77 -0
  251. package/src/scripts/deployAppVersion.ts +692 -0
  252. package/src/scripts/devOnlineBackend.ts +141 -0
  253. package/src/scripts/installApp.ts +188 -0
  254. package/src/scripts/listStores.ts +19 -0
  255. package/src/scripts/runDockerAuth.ts +120 -0
  256. package/src/scripts/runOffline.ts +577 -0
  257. package/src/scripts/runOfflineFullTests.ts +1634 -0
  258. package/src/scripts/runTests.ts +306 -0
  259. package/src/scripts/runVm.ts +562 -0
  260. package/src/scripts/runVmAuth.ts +541 -0
  261. package/src/scripts/runVmScript.ts +282 -0
  262. package/src/scripts/setupTestDb.ts +71 -0
  263. package/src/scripts/verifyContracts.ts +310 -0
  264. package/src/scripts/verifyRestContracts.ts +275 -0
  265. package/src/vite/onlineConfig.ts +60 -0
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Probe runner — entry point for the online-store inspection framework.
4
+ *
5
+ * Invocation:
6
+ * node --import tsx \
7
+ * node_modules/@essential-apps/shopify-test-runner/src/probes/runProbe.ts \
8
+ * <probe-name> [--key=value …]
9
+ *
10
+ * What it does:
11
+ * 1. Loads the consuming app's `.env.test` so probes can read
12
+ * env (e.g. STOREFRONT_PASSWORD).
13
+ * 2. Picks an installed store from `tests/test-online/.stores.json`.
14
+ * 3. Launches Playwright chromium via patchright with the
15
+ * saved Partner storage state — same recipe online tests use.
16
+ * 4. Hands control to the probe.
17
+ *
18
+ * Adding a probe: drop a file in `packages/runner/src/probes/<name>.ts`
19
+ * default-exporting a {@link Probe}, and add a row to `PROBES` below.
20
+ */
21
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
22
+ import { resolve, dirname } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+ import { type BrowserContext } from '@playwright/test';
25
+ import {
26
+ readRegistry,
27
+ storageStatePath,
28
+ type Store,
29
+ } from '@essential-apps/shopify-test-core';
30
+ import type { Probe, ProbeContext } from './types.js';
31
+ import fontsProbe from './fonts.js';
32
+ import mirrorProbe from './mirror.js';
33
+ import { launchStealthBrowser } from '../lib/stealthLaunch.js';
34
+
35
+ // ── Registry of available probes ──────────────────────────────
36
+ // Order matters only for `--help` listing.
37
+ const PROBES: Record<string, Probe> = {
38
+ [fontsProbe.name]: fontsProbe,
39
+ [mirrorProbe.name]: mirrorProbe,
40
+ };
41
+
42
+ // ── CLI arg parsing ───────────────────────────────────────────
43
+
44
+ interface ParsedArgs {
45
+ name: string;
46
+ args: Record<string, string>;
47
+ help: boolean;
48
+ }
49
+
50
+ function parseArgs(argv: string[]): ParsedArgs {
51
+ const [name, ...rest] = argv;
52
+ const args: Record<string, string> = {};
53
+ let help = false;
54
+ for (const a of rest) {
55
+ if (a === '--help' || a === '-h') {
56
+ help = true;
57
+ continue;
58
+ }
59
+ const m = a.match(/^--([^=]+)=(.*)$/);
60
+ if (m) {
61
+ args[m[1]!] = m[2]!;
62
+ continue;
63
+ }
64
+ // Bare flag: --foo (no value) → true
65
+ const bare = a.match(/^--(.+)$/);
66
+ if (bare) {
67
+ args[bare[1]!] = '';
68
+ }
69
+ }
70
+ return { name: name ?? '', args, help };
71
+ }
72
+
73
+ function printUsage(): void {
74
+ console.error('Usage: runProbe <name> [--key=value …]');
75
+ console.error('');
76
+ console.error('Available probes:');
77
+ for (const p of Object.values(PROBES)) {
78
+ console.error(` ${p.name.padEnd(14)} ${p.description}`);
79
+ }
80
+ console.error('');
81
+ console.error('Global flags:');
82
+ console.error(' --help Show this help');
83
+ console.error(' --store=<slug> Use a specific store from the pool (default: any available)');
84
+ console.error('');
85
+ }
86
+
87
+ // ── Workspace path discovery ──────────────────────────────────
88
+
89
+ /**
90
+ * Locate the workspace root. When this script runs from a packed
91
+ * tarball inside a consumer's node_modules, the workspace is the
92
+ * shopify-test repo's root — the package nearest us with
93
+ * `name === "@essential-apps/shopify-test"`. When running from the
94
+ * repo itself, that's the repo root.
95
+ *
96
+ * We walk up from this file looking for a `package.json` whose
97
+ * `name` matches, falling back to the nearest directory containing
98
+ * a `packages/` folder (matches the monorepo layout).
99
+ */
100
+ function findWorkspaceRoot(): string {
101
+ // When invoked from a consumer, the runner's source lives under
102
+ // `node_modules/@essential-apps/shopify-test-runner/src/`. That
103
+ // path doesn't help us reach the workspace's themes/, etc.
104
+ // Look upward from this file for the workspace root marker.
105
+ let dir = dirname(fileURLToPath(import.meta.url));
106
+ for (let i = 0; i < 10; i++) {
107
+ const pj = resolve(dir, 'package.json');
108
+ if (existsSync(pj)) {
109
+ try {
110
+ const pkg = JSON.parse(readFileSync(pj, 'utf8')) as { name?: string };
111
+ if (pkg.name === '@essential-apps/shopify-test') return dir;
112
+ } catch {
113
+ /* fall through */
114
+ }
115
+ }
116
+ const parent = resolve(dir, '..');
117
+ if (parent === dir) break;
118
+ dir = parent;
119
+ }
120
+ // Fallback: assume cwd is the consumer app — outputs go to its
121
+ // tests/test-online/probe-output/ if we can't find a workspace.
122
+ return process.cwd();
123
+ }
124
+
125
+ // ── Store selection ───────────────────────────────────────────
126
+
127
+ function pickStore(slug?: string): Store {
128
+ const { stores } = readRegistry();
129
+ if (slug) {
130
+ const s = stores.find((x) => x.slug === slug);
131
+ if (!s) {
132
+ throw new Error(
133
+ `Store with slug "${slug}" not found in registry. ` +
134
+ `Available: ${stores.map((x) => x.slug).join(', ')}`,
135
+ );
136
+ }
137
+ return s;
138
+ }
139
+ const candidate =
140
+ stores.find((s) => s.appInstalled && s.status === 'available') ??
141
+ stores.find((s) => s.appInstalled) ??
142
+ stores[0];
143
+ if (!candidate) {
144
+ throw new Error(
145
+ 'No stores in registry. Run `npm run test:online:add` + `npm run test:online:install` first.',
146
+ );
147
+ }
148
+ return candidate;
149
+ }
150
+
151
+ /**
152
+ * Synthesize a Store record from a bare URL. Used by `--url=<full>`
153
+ * mode so probes can run against arbitrary public Shopify storefronts
154
+ * (e.g. `theme-dawn-demo.myshopify.com` for canonical Dawn font
155
+ * mirroring) without needing a Partner-auth registry entry.
156
+ */
157
+ function storeFromUrl(rawUrl: string): Store {
158
+ const u = new URL(rawUrl);
159
+ const shop = u.hostname;
160
+ const slug = shop.replace(/\.myshopify\.com$/, '');
161
+ return {
162
+ shop,
163
+ slug,
164
+ plan: 'PUBLIC',
165
+ appInstalled: false,
166
+ status: 'available',
167
+ };
168
+ }
169
+
170
+ // ── Browser bootstrap ─────────────────────────────────────────
171
+
172
+ async function launchBrowser(opts: { needAuth: boolean }): Promise<BrowserContext> {
173
+ // Reuse the same chrome+patchright recipe as the online-test store
174
+ // pool: real Google Chrome via patchright. For probes against
175
+ // Partner-owned stores we layer in the saved Partner storage
176
+ // state so password gates / admin redirects work; for public
177
+ // storefronts (--url mode) we skip that — no auth needed.
178
+ const browser = await launchStealthBrowser({ headless: true });
179
+ const storageState =
180
+ opts.needAuth && existsSync(storageStatePath)
181
+ ? JSON.parse(readFileSync(storageStatePath, 'utf8'))
182
+ : undefined;
183
+ if (opts.needAuth && !storageState) {
184
+ throw new Error(
185
+ `Storage state not found at ${storageStatePath}. ` +
186
+ `Run online tests at least once to capture Partner auth cookies, ` +
187
+ `or pass --url=<public-store-url> to skip auth.`,
188
+ );
189
+ }
190
+ const context = await browser.newContext({
191
+ viewport: { width: 1400, height: 900 },
192
+ ignoreHTTPSErrors: true,
193
+ ...(storageState ? { storageState } : {}),
194
+ });
195
+ return context;
196
+ }
197
+
198
+ // ── Main ──────────────────────────────────────────────────────
199
+
200
+ async function main(): Promise<void> {
201
+ const parsed = parseArgs(process.argv.slice(2));
202
+ if (parsed.help || !parsed.name) {
203
+ printUsage();
204
+ process.exit(parsed.name ? 0 : 1);
205
+ }
206
+
207
+ const probe = PROBES[parsed.name];
208
+ if (!probe) {
209
+ console.error(`Unknown probe: "${parsed.name}"\n`);
210
+ printUsage();
211
+ process.exit(1);
212
+ }
213
+
214
+ const workspaceRoot = findWorkspaceRoot();
215
+ // --url=<full> mode: synthesize a Store record from the URL and
216
+ // skip auth. For probing arbitrary public storefronts (e.g.
217
+ // theme-dawn-demo.myshopify.com for canonical Dawn font mirroring).
218
+ const urlMode = parsed.args['url'];
219
+ const store = urlMode ? storeFromUrl(urlMode) : pickStore(parsed.args['store']);
220
+ const outputDir = resolve(
221
+ workspaceRoot,
222
+ probe.outputDir.replace(/\$\{name\}/g, probe.name),
223
+ );
224
+ mkdirSync(outputDir, { recursive: true });
225
+
226
+ console.log(`[runProbe] probe: ${probe.name}`);
227
+ console.log(`[runProbe] store: ${store.shop} (${store.slug})${urlMode ? ' [public --url mode]' : ''}`);
228
+ console.log(`[runProbe] output: ${outputDir}`);
229
+
230
+ const browserContext = await launchBrowser({ needAuth: !urlMode });
231
+ const page = await browserContext.newPage();
232
+
233
+ const ctx: ProbeContext = {
234
+ store,
235
+ browserContext,
236
+ page,
237
+ outputDir,
238
+ args: parsed.args,
239
+ workspaceRoot,
240
+ };
241
+
242
+ try {
243
+ await probe.run(ctx);
244
+ console.log(`[runProbe] ✓ ${probe.name} complete`);
245
+ } catch (err) {
246
+ console.error(`[runProbe] ✗ ${probe.name} failed:`, err);
247
+ process.exitCode = 1;
248
+ } finally {
249
+ await browserContext.close().catch(() => {});
250
+ await browserContext.browser()?.close().catch(() => {});
251
+ }
252
+ }
253
+
254
+ main().catch((err) => {
255
+ console.error(err);
256
+ process.exit(1);
257
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Probe framework — a reusable harness for asking real Shopify
3
+ * "what does this actually look like?" against the online test store
4
+ * pool.
5
+ *
6
+ * Why this exists: every offline mock has a long tail of "is our
7
+ * shape right?" questions. Examples we've already hit:
8
+ * - what URLs does Shopify's `font_face` Liquid filter emit?
9
+ * - what does the real `themes/{id}/assets.json` response look like?
10
+ * - what HTTP headers does fonts.shopifycdn.com set on a .woff2?
11
+ * - what's the exact `settings_data.json` shape when an app embed
12
+ * is activated?
13
+ *
14
+ * Each probe is a small Node script that drives a Playwright browser
15
+ * against a real online store (picked from `tests/test-online/.stores.json`),
16
+ * captures structured fixture data, and writes it somewhere the
17
+ * mocks can read at runtime.
18
+ *
19
+ * Adding a probe: create `packages/runner/src/probes/<name>.ts`,
20
+ * default-export an object conforming to {@link Probe}, and add a
21
+ * row to the registry in `runProbe.ts`.
22
+ *
23
+ * Invoke from the consuming app's repo:
24
+ * node --import tsx \
25
+ * node_modules/@essential-apps/shopify-test-runner/src/probes/runProbe.ts \
26
+ * <probe-name> [--key=value …]
27
+ */
28
+ import type { BrowserContext, Page } from '@playwright/test';
29
+ import type { Store } from '@essential-apps/shopify-test-core';
30
+
31
+ /**
32
+ * Per-probe execution context. The runner sets up the browser +
33
+ * store before invoking the probe; the probe focuses on what it
34
+ * captures, not how to get a browser.
35
+ */
36
+ export interface ProbeContext {
37
+ /** The online store this probe runs against. */
38
+ store: Store;
39
+ /** Logged-in Playwright BrowserContext (storage state pre-loaded). */
40
+ browserContext: BrowserContext;
41
+ /** Convenience: a fresh page in the context. Probes can open more. */
42
+ page: Page;
43
+ /**
44
+ * Absolute path where probe artifacts go. Probes write fixture
45
+ * files (JSON, binary, etc.) under here so the mock packages
46
+ * can read them later. Conventionally the workspace path
47
+ * `packages/<owner-package>/fixtures/<probe-name>/`.
48
+ */
49
+ outputDir: string;
50
+ /** Parsed --key=value CLI args. */
51
+ args: Record<string, string>;
52
+ /** Workspace root absolute path (the shopify-test repo). */
53
+ workspaceRoot: string;
54
+ }
55
+
56
+ export interface Probe {
57
+ /** Probe name — what you type after `runProbe`. */
58
+ name: string;
59
+ /** One-line description shown in `runProbe --help`. */
60
+ description: string;
61
+ /**
62
+ * Where outputs should go, relative to workspaceRoot. The harness
63
+ * creates this dir and passes its absolute path as
64
+ * `ctx.outputDir`. Use `${name}` etc. — substituted at runtime.
65
+ */
66
+ outputDir: string;
67
+ /**
68
+ * Main entry point. Throws on failure; logs progress to stdout.
69
+ * Should be idempotent — re-running a probe should refresh
70
+ * outputs cleanly.
71
+ */
72
+ run(ctx: ProbeContext): Promise<void>;
73
+ }
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import {
4
+ loadEnv,
5
+ printEnvSummary,
6
+ appendStore,
7
+ readRegistry,
8
+ } from '@essential-apps/shopify-test-core';
9
+
10
+ async function main(): Promise<void> {
11
+ const { values } = parseArgs({
12
+ options: {
13
+ shop: { type: 'string' },
14
+ plan: { type: 'string' },
15
+ },
16
+ });
17
+
18
+ const shop = values.shop?.trim();
19
+ if (!shop) {
20
+ console.error(
21
+ 'Usage: npm run test:online:add -- --shop online-1.myshopify.com [--plan UNLIMITED_APP_DEVELOPMENT]',
22
+ );
23
+ process.exit(1);
24
+ }
25
+ if (!/^[a-z0-9-]+\.myshopify\.com$/.test(shop)) {
26
+ console.error(`Invalid shop format. Expected "<slug>.myshopify.com", got "${shop}".`);
27
+ process.exit(1);
28
+ }
29
+
30
+ const env = loadEnv();
31
+ printEnvSummary(env);
32
+
33
+ const reg = readRegistry();
34
+ if (reg.stores.some((s) => s.shop === shop)) {
35
+ console.error(`Already in registry: ${shop}`);
36
+ process.exit(1);
37
+ }
38
+
39
+ const slug = shop.replace('.myshopify.com', '');
40
+ appendStore({
41
+ shop,
42
+ slug,
43
+ plan: values.plan ?? env.plan,
44
+ createdAt: new Date().toISOString(),
45
+ appInstalled: false,
46
+ status: 'available',
47
+ });
48
+
49
+ console.log(`✓ Added ${shop} to pool.`);
50
+ console.log('');
51
+ console.log('Next: install the dev app on this store.');
52
+ console.log(` Make sure your dev server is running, then:`);
53
+ console.log(` npm run test:online:install -- --shop ${shop}`);
54
+ }
55
+
56
+ main().catch((err) => {
57
+ console.error(err);
58
+ process.exit(1);
59
+ });
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build the canonical `shopify-test` image from the Dockerfile
4
+ * bundled in this package. Run from the shopify-test workspace
5
+ * root:
6
+ *
7
+ * npm run docker:build
8
+ *
9
+ * Or directly:
10
+ *
11
+ * node --import tsx packages/runner/src/scripts/buildDockerImage.ts
12
+ *
13
+ * The image is amd64-only (Rosetta on Apple Silicon) — see
14
+ * docker/README.md for why.
15
+ */
16
+ import { spawn } from 'node:child_process';
17
+ import { existsSync } from 'node:fs';
18
+ import { dirname, resolve } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ // Mirrors the npm scope (`@essential-apps/shopify-test`) so anyone
22
+ // inspecting `container images` sees the same name they `npm install`.
23
+ const DEFAULT_TAG = 'essential-apps/shopify-test:latest';
24
+
25
+ function dockerContextDir(): string {
26
+ // This file lives at packages/runner/src/scripts/ (or dist/scripts/
27
+ // after build). Walk up + sideways to docker/.
28
+ const here = dirname(fileURLToPath(import.meta.url));
29
+ // here = .../packages/runner/{src,dist}/scripts
30
+ const candidates = [
31
+ resolve(here, '../../docker'),
32
+ resolve(here, '../docker'),
33
+ ];
34
+ for (const c of candidates) {
35
+ if (existsSync(resolve(c, 'Dockerfile'))) return c;
36
+ }
37
+ throw new Error(
38
+ `Could not locate the Dockerfile under packages/runner/docker/. ` +
39
+ `Tried: ${candidates.join(', ')}`,
40
+ );
41
+ }
42
+
43
+ async function main(): Promise<void> {
44
+ const tag = process.env['SHOPIFY_TEST_IMAGE_TAG'] ?? DEFAULT_TAG;
45
+ const context = dockerContextDir();
46
+
47
+ console.log('────────────────────────────────────────────────────────────');
48
+ console.log(' shopify-test docker image build');
49
+ console.log('────────────────────────────────────────────────────────────');
50
+ console.log(` Tag : ${tag}`);
51
+ console.log(` Context : ${context}`);
52
+ console.log(` Arch : amd64`);
53
+ console.log('────────────────────────────────────────────────────────────');
54
+ console.log('');
55
+
56
+ const args = ['build', '--arch', 'amd64', '--tag', tag, context];
57
+ const p = spawn('container', args, { stdio: 'inherit' });
58
+ p.on('exit', (code) => process.exit(code ?? 0));
59
+ process.on('SIGINT', () => p.kill('SIGINT'));
60
+ process.on('SIGTERM', () => p.kill('SIGTERM'));
61
+ }
62
+
63
+ main().catch((err) => {
64
+ console.error(err);
65
+ process.exit(1);
66
+ });
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Capture Shopify auth state into tests/test-online/.auth/storageState.json.
4
+ *
5
+ * The captured cookies are fingerprint-bound (UA, sec-ch-ua-platform,
6
+ * canvas/WebGL hashes, etc.), so this script MUST run in the same
7
+ * browser environment that tests use:
8
+ * - On host (macOS): Google Chrome via patchright.
9
+ * - In container: Google Chrome via patchright (same channel),
10
+ * under Xvfb. The runDockerAuth.ts wrapper exposes the Xvfb
11
+ * display over VNC so the user can interactively log in.
12
+ *
13
+ * If you re-capture on host but tests run in container (or vice-versa),
14
+ * Cloudflare's bot heuristics will see UA / fingerprint inconsistency
15
+ * and challenge or block the session.
16
+ */
17
+ import { mkdirSync } from 'node:fs';
18
+ import { createRequire } from 'node:module';
19
+ import { dirname } from 'node:path';
20
+ import { createInterface } from 'node:readline';
21
+ import type { BrowserContext, chromium as playwrightChromium } from '@playwright/test';
22
+ import { storageStatePath, assertInVm } from '@essential-apps/shopify-test-core';
23
+
24
+ // Auth capture must run INSIDE the VM (via runVmAuth, which sets
25
+ // TEST_IN_CONTAINER + drives this over VNC) — never on the host, or the
26
+ // captured cf_clearance/session is fingerprint-mismatched against the VM.
27
+ assertInVm('capture Shopify auth');
28
+
29
+ const require = createRequire(import.meta.url);
30
+ const chromium = (require('patchright') as { chromium: typeof playwrightChromium }).chromium;
31
+
32
+ // Test Partner account (throwaway; not prod). Shown in the terminal AND
33
+ // pinned to a bar at the top of the browser so it's readable while
34
+ // logging in over VNC.
35
+ const LOGIN_EMAIL = 'essential-apps-test-1@supercorp.ai';
36
+ const LOGIN_PASSWORD = 'essential-apps-test-1-password';
37
+
38
+ // A fixed top bar injected into every page (survives navigation) showing
39
+ // the login creds. `pointer-events:none` so it never blocks the form;
40
+ // re-pinned on an interval because Shopify's accounts pages re-render.
41
+ const credsBarInitScript = `(() => {
42
+ const ID = '__ea_auth_creds_bar__';
43
+ const text = ${JSON.stringify(`TEST LOGIN · ${LOGIN_EMAIL} · password: ${LOGIN_PASSWORD}`)};
44
+ const render = () => {
45
+ if (!document.documentElement) return;
46
+ let b = document.getElementById(ID);
47
+ if (!b) {
48
+ b = document.createElement('div');
49
+ b.id = ID;
50
+ b.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:2147483647;background:#0a7d3b;color:#fff;font:600 13px/26px ui-monospace,SFMono-Regular,Menlo,monospace;text-align:center;height:26px;padding:0 10px;box-shadow:0 1px 4px rgba(0,0,0,.35);pointer-events:none';
51
+ (document.body || document.documentElement).appendChild(b);
52
+ }
53
+ b.textContent = text;
54
+ };
55
+ render();
56
+ document.addEventListener('DOMContentLoaded', render);
57
+ setInterval(render, 800);
58
+ })();`;
59
+
60
+ async function main(): Promise<void> {
61
+ console.log('Opening Chrome. Please:');
62
+ console.log(` Log in as: ${LOGIN_EMAIL}`);
63
+ console.log(` Password: ${LOGIN_PASSWORD} (also in the green bar atop the browser)`);
64
+ console.log(' 1. Log in to Shopify Partners.');
65
+ console.log(' 2. Then visit your test store admin (e.g. https://admin.shopify.com/store/<your-store>) —');
66
+ console.log(' if a Cloudflare "Verify you are human" challenge appears, click through it.');
67
+ console.log(' Without this step, tests will hit Turnstile and fail.');
68
+ console.log(' 3. When both are done, return here and press Enter.');
69
+ console.log('');
70
+
71
+ // chromium.launch + newContext (matches storePool fixture model).
72
+ // Critically, this is NOT launchPersistentContext — the persistent
73
+ // profile accumulates fingerprint state that triggers Cloudflare's
74
+ // bot detection. Fresh-context model means the browser fingerprint
75
+ // CF sees here (during capture) is the SAME fingerprint it'll see
76
+ // at test time (when a fresh context is built from this saved
77
+ // storageState). cf_clearance is fingerprint-bound, so this
78
+ // consistency is what makes the saved cookie actually work.
79
+ //
80
+ // Patchright (no `channel: 'chrome'`) — uses its bundled
81
+ // chromium with stealth patches. Same browser as storePool +
82
+ // conformance probes, so cf_clearance carries across all three.
83
+ // Real Google Chrome was tried earlier but its fingerprint
84
+ // doesn't match patchright's, causing CF re-challenges.
85
+ const browser = await chromium.launch({
86
+ headless: false,
87
+ args: [
88
+ '--ip-address-space-overrides=127.0.0.1:0=public,[::1]:0=public',
89
+ '--disable-features=LocalNetworkAccessChecks,LocalNetworkAccessChecksWebSockets,LocalNetworkAccessChecksWebTransport,BlockInsecurePrivateNetworkRequests,PrivateNetworkAccessRespectPreflightResults,PrivateNetworkAccessSendPreflights',
90
+ '--local-network-access-permissions-policy-default-enabled',
91
+ ],
92
+ });
93
+ const context = (await browser.newContext({
94
+ viewport: { width: 1400, height: 900 },
95
+ ignoreHTTPSErrors: true,
96
+ })) as unknown as BrowserContext;
97
+
98
+ // Pin the creds to a bar at the top of the (VNC'd) browser.
99
+ await context.addInitScript(credsBarInitScript);
100
+
101
+ const page = await context.newPage();
102
+ await page.goto('https://accounts.shopify.com/lookup');
103
+
104
+ await waitForEnter('Press Enter once logged in AND past any admin Turnstile... ');
105
+
106
+ // Sanity-check cf_clearance presence and warn loudly if absent —
107
+ // capture without it works for Partner-API-only flows, but online
108
+ // tests that drive admin.shopify.com will fail on Turnstile.
109
+ const cookies = await context.cookies();
110
+ const hasCfClearance = cookies.some(
111
+ (c) => c.name === 'cf_clearance' && c.domain.includes('shopify.com'),
112
+ );
113
+ if (!hasCfClearance) {
114
+ console.warn(
115
+ '⚠️ No cf_clearance cookie found for *.shopify.com. The online suite will hit Turnstile.',
116
+ );
117
+ console.warn(
118
+ ' Visit your test store admin in this browser, pass the Cloudflare challenge, then re-run.',
119
+ );
120
+ } else {
121
+ console.log('✓ cf_clearance cookie captured (admin.shopify.com Turnstile bypass).');
122
+ }
123
+
124
+ mkdirSync(dirname(storageStatePath), { recursive: true });
125
+ await context.storageState({ path: storageStatePath });
126
+ console.log(`✓ Auth state saved: ${storageStatePath}`);
127
+
128
+ await context.close();
129
+ await browser.close();
130
+ }
131
+
132
+ function waitForEnter(prompt: string): Promise<void> {
133
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
134
+ return new Promise((res) =>
135
+ rl.question(prompt, () => {
136
+ rl.close();
137
+ res();
138
+ }),
139
+ );
140
+ }
141
+
142
+ main().catch((err) => {
143
+ console.error(err);
144
+ process.exit(1);
145
+ });