@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,129 @@
1
+ /**
2
+ * Upload an app-source zip to Shopify's signed-URL bucket, so we can
3
+ * reference it via `AppVersionInput.sourceUrl` in `appVersionCreate`.
4
+ *
5
+ * Why we need this
6
+ * ----------------
7
+ * Theme app extensions can be deployed via `appVersionCreate(version:
8
+ * { source: <manifest with files inline as base64> })` — the file
9
+ * contents fit comfortably in the GraphQL request body. Function
10
+ * (WASM) and post-purchase (JS bundle) extensions, however, are
11
+ * uploaded out-of-band:
12
+ *
13
+ * 1. Call `appRequestSourceUploadUrl(sourceExtension: ZIP, …)` —
14
+ * Shopify returns a 1-hour GCS v4-signed PUT URL.
15
+ * 2. PUT the zip bytes to that URL.
16
+ * 3. Call `appVersionCreate(version: { sourceUrl: <same URL> })`.
17
+ *
18
+ * The CLI does the same — see
19
+ * `Shopify/cli@packages/app/src/cli/services/bundle.ts` (`getUploadURL`
20
+ * + `uploadToGCS`). We confirmed empirically that:
21
+ * - The mutation accepts `sourceExtension: ZIP | BR`
22
+ * (we only emit ZIP).
23
+ * - The returned `sourceUploadUrl` is a GCS v4 presigned URL with
24
+ * `X-Goog-Algorithm=GOOG4-RSA-SHA256` and `X-Goog-Expires=3600`.
25
+ * - `appVersionCreate` validates the URL against an allowlist —
26
+ * URLs from other hosts are rejected with HTTP 500
27
+ * `"URL path is not valid"`. So you can ONLY use a sourceUrl
28
+ * minted by `appRequestSourceUploadUrl`.
29
+ *
30
+ * Quirks
31
+ * ------
32
+ * - The CLI ostensibly builds a multipart form to derive the upload
33
+ * headers, then sends the raw buffer as body. Empirically, a plain
34
+ * PUT with `Content-Type: application/zip` works against the GCS
35
+ * v4 presigned URL — that's what we do.
36
+ * - There's no MAX_SIZE error message in the API response, but the
37
+ * CLI hard-codes a 100 MB cap. Our zips are far below that.
38
+ */
39
+ import { createReadStream, statSync } from 'node:fs';
40
+ import { Readable } from 'node:stream';
41
+ import { appManagementGraphQL } from '@essential-apps/shopify-test-core';
42
+ /**
43
+ * GraphQL mutation to mint a signed upload URL.
44
+ *
45
+ * Defaults to `sourceExtension: BR` (tar+brotli archive) — that's the
46
+ * format that actually works for full-fidelity deploys including
47
+ * functions. The legacy `ZIP` option exists in the schema and returns
48
+ * a working signed URL, but the server 500s on zips containing per-
49
+ * extension folder layouts. See lib/buildSourceBundle.ts.
50
+ *
51
+ * NOTE on schema discovery: schema introspection against the
52
+ * `/unstable/` endpoint is rejected with `404 "Cannot find a valid
53
+ * organization"` for tokens minted via App-automation-token exchange
54
+ * — only targeted queries scoped to an app/org work. Mutation name +
55
+ * arg shape were confirmed empirically.
56
+ */
57
+ const REQUEST_SOURCE_UPLOAD_URL_MUTATION = `
58
+ mutation RequestSourceUploadUrl(
59
+ $organizationId: ID!
60
+ $sourceExtension: SourceExtension!
61
+ ) {
62
+ appRequestSourceUploadUrl(
63
+ sourceExtension: $sourceExtension
64
+ organizationId: $organizationId
65
+ ) {
66
+ sourceUploadUrl
67
+ userErrors { field message }
68
+ }
69
+ }
70
+ `;
71
+ /**
72
+ * Mint a fresh signed upload URL. The URL is valid for 3600 seconds;
73
+ * upload + version-create should both happen well within that window.
74
+ *
75
+ * Returns the GCS URL verbatim — you pass the SAME URL to
76
+ * `appVersionCreate` as `sourceUrl`. Shopify maintains the mapping
77
+ * server-side between the signed URL and the uploaded object.
78
+ */
79
+ export async function requestSourceUploadUrl(opts) {
80
+ const data = await appManagementGraphQL(opts.accessToken, REQUEST_SOURCE_UPLOAD_URL_MUTATION, {
81
+ organizationId: `gid://shopify/Organization/${opts.organizationId}`,
82
+ sourceExtension: opts.sourceExtension ?? 'BR',
83
+ });
84
+ const errs = data.appRequestSourceUploadUrl.userErrors;
85
+ if (errs.length > 0) {
86
+ throw new Error('appRequestSourceUploadUrl userErrors:\n' +
87
+ errs.map((e) => ` • ${(e.field ?? []).join('.')}: ${e.message}`).join('\n'));
88
+ }
89
+ const url = data.appRequestSourceUploadUrl.sourceUploadUrl;
90
+ if (!url) {
91
+ throw new Error('appRequestSourceUploadUrl returned no sourceUploadUrl');
92
+ }
93
+ return url;
94
+ }
95
+ /**
96
+ * PUT an archive file to the signed GCS URL minted by
97
+ * `requestSourceUploadUrl`. The URL embeds its own auth (signed
98
+ * `X-Goog-*` query params), so no bearer is needed on this request.
99
+ *
100
+ * Headers: we send only `Content-Length`. The CLI doesn't set a
101
+ * Content-Type for these uploads (it builds a multipart form to
102
+ * derive headers but sends the raw buffer as body, and GCS ignores
103
+ * the content-type anyway because the signed URL pins everything
104
+ * server-side). We match that for parity.
105
+ *
106
+ * Streams the file so a multi-MB archive doesn't sit in memory.
107
+ */
108
+ export async function uploadSourceToSignedUrl(opts) {
109
+ const size = statSync(opts.archivePath).size;
110
+ const stream = createReadStream(opts.archivePath);
111
+ // Convert Node Readable -> Web ReadableStream for the global
112
+ // `fetch` API. Node 18+ exposes `Readable.toWeb`.
113
+ const body = Readable.toWeb(stream);
114
+ const r = await fetch(opts.uploadUrl, {
115
+ method: 'PUT',
116
+ body,
117
+ headers: { 'Content-Length': String(size) },
118
+ // Node's fetch needs duplex when streaming a body.
119
+ // @ts-expect-error — `duplex` is a recent option not yet in some
120
+ // @types/node releases.
121
+ duplex: 'half',
122
+ });
123
+ if (!r.ok) {
124
+ const text = await r.text().catch(() => '<no body>');
125
+ throw new Error(`Source upload to signed URL failed: HTTP ${r.status} ${r.statusText}\n` +
126
+ ` ${text.slice(0, 400)}`);
127
+ }
128
+ }
129
+ //# sourceMappingURL=sourceZipUpload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sourceZipUpload.js","sourceRoot":"","sources":["../../src/lib/sourceZipUpload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAEzE;;;;;;;;;;;;;;GAcG;AACH,MAAM,kCAAkC,GAAG;;;;;;;;;;;;;CAa1C,CAAC;AAkBF;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,IAAmC;IAEnC,MAAM,IAAI,GAAG,MAAM,oBAAoB,CAKpC,IAAI,CAAC,WAAW,EAAE,kCAAkC,EAAE;QACvD,cAAc,EAAE,8BAA8B,IAAI,CAAC,cAAc,EAAE;QACnE,eAAe,EAAE,IAAI,CAAC,eAAe,IAAI,IAAI;KAC9C,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,IAAI,CAAC,yBAAyB,CAAC,UAAU,CAAC;IACvD,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,yCAAyC;YACvC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAC/E,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,yBAAyB,CAAC,eAAe,CAAC;IAC3D,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AASD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,IAAyB;IAEzB,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC;IAC7C,MAAM,MAAM,GAAG,gBAAgB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAClD,6DAA6D;IAC7D,kDAAkD;IAClD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,CAA0C,CAAC;IAC7E,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE;QACpC,MAAM,EAAE,KAAK;QACb,IAAI;QACJ,OAAO,EAAE,EAAE,gBAAgB,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE;QAC3C,mDAAmD;QACnD,iEAAiE;QACjE,wBAAwB;QACxB,MAAM,EAAE,MAAM;KACf,CAAC,CAAC;IACH,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACV,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,CAAC;QACrD,MAAM,IAAI,KAAK,CACb,4CAA4C,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,UAAU,IAAI;YACtE,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAC5B,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,35 @@
1
+ import type { Browser } from '@playwright/test';
2
+ /**
3
+ * Canonical Cloudflare / Private-Network-Access bypass flags.
4
+ *
5
+ * Kept byte-identical to the admin `storePool` fixture's launch args:
6
+ * cf_clearance binds to the (engine, UA, flags) fingerprint, so any
7
+ * browser that navigates the real Shopify admin — installApp, the
8
+ * globalSetup auth pre-flight — MUST launch the same way the online
9
+ * tests do, or a perfectly good cf_clearance trips Turnstile.
10
+ */
11
+ export declare const stealthLaunchArgs: string[];
12
+ /**
13
+ * Launch patchright Chromium — the ONE stealth engine used across the
14
+ * whole suite (online `storePool` fixture, `captureAuth`, every
15
+ * conformance probe). patchright patches the `Runtime.enable` CDP leak
16
+ * Cloudflare's bot detection keys on, so its bundled Chromium passes
17
+ * Turnstile headless — no StealthPlugin, no real-Chrome channel.
18
+ *
19
+ * This replaces the old playwright-extra + puppeteer-extra-plugin-stealth
20
+ * launcher (real Chrome via `channel: 'chrome'`), which was the lone
21
+ * outlier engine. Standalone scripts (installApp) and the globalSetup
22
+ * auth pre-flight call this; @playwright/test-managed tests get their
23
+ * browser from the `storePool` fixture, which launches patchright the
24
+ * same way.
25
+ */
26
+ export declare function launchStealthBrowser(opts: {
27
+ headless: boolean;
28
+ /**
29
+ * Park the window far offscreen (headed-but-hidden on host) — mirrors
30
+ * storePool's behaviour for headless:false-on-host launches. Ignored
31
+ * under Xvfb in-container (no real display there).
32
+ */
33
+ hideWindow?: boolean;
34
+ }): Promise<Browser>;
35
+ //# sourceMappingURL=stealthLaunch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stealthLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/stealthLaunch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAMhD;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,UAI7B,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE;IAC/C,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAAC,OAAO,CAAC,CAYnB"}
@@ -0,0 +1,46 @@
1
+ import { createRequire } from 'node:module';
2
+ import { assertInVm } from '@essential-apps/shopify-test-core';
3
+ // patchright is published CJS-only; this file is ESM (tsx/Node).
4
+ const require = createRequire(import.meta.url);
5
+ /**
6
+ * Canonical Cloudflare / Private-Network-Access bypass flags.
7
+ *
8
+ * Kept byte-identical to the admin `storePool` fixture's launch args:
9
+ * cf_clearance binds to the (engine, UA, flags) fingerprint, so any
10
+ * browser that navigates the real Shopify admin — installApp, the
11
+ * globalSetup auth pre-flight — MUST launch the same way the online
12
+ * tests do, or a perfectly good cf_clearance trips Turnstile.
13
+ */
14
+ export const stealthLaunchArgs = [
15
+ '--ip-address-space-overrides=127.0.0.1:0=public,[::1]:0=public',
16
+ '--disable-features=LocalNetworkAccessChecks,LocalNetworkAccessChecksWebSockets,LocalNetworkAccessChecksWebTransport,BlockInsecurePrivateNetworkRequests,PrivateNetworkAccessRespectPreflightResults,PrivateNetworkAccessSendPreflights',
17
+ '--local-network-access-permissions-policy-default-enabled',
18
+ ];
19
+ /**
20
+ * Launch patchright Chromium — the ONE stealth engine used across the
21
+ * whole suite (online `storePool` fixture, `captureAuth`, every
22
+ * conformance probe). patchright patches the `Runtime.enable` CDP leak
23
+ * Cloudflare's bot detection keys on, so its bundled Chromium passes
24
+ * Turnstile headless — no StealthPlugin, no real-Chrome channel.
25
+ *
26
+ * This replaces the old playwright-extra + puppeteer-extra-plugin-stealth
27
+ * launcher (real Chrome via `channel: 'chrome'`), which was the lone
28
+ * outlier engine. Standalone scripts (installApp) and the globalSetup
29
+ * auth pre-flight call this; @playwright/test-managed tests get their
30
+ * browser from the `storePool` fixture, which launches patchright the
31
+ * same way.
32
+ */
33
+ export async function launchStealthBrowser(opts) {
34
+ assertInVm('launch a stealth browser');
35
+ const { chromium } = require('patchright');
36
+ return (await chromium.launch({
37
+ headless: opts.headless,
38
+ args: [
39
+ ...stealthLaunchArgs,
40
+ ...(opts.hideWindow
41
+ ? ['--window-position=-3000,-3000', '--window-size=1400,900']
42
+ : []),
43
+ ],
44
+ }));
45
+ }
46
+ //# sourceMappingURL=stealthLaunch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stealthLaunch.js","sourceRoot":"","sources":["../../src/lib/stealthLaunch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;AAE/D,iEAAiE;AACjE,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,gEAAgE;IAChE,wOAAwO;IACxO,2DAA2D;CAC5D,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,IAQ1C;IACC,UAAU,CAAC,0BAA0B,CAAC,CAAC;IACvC,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,YAAY,CAAsC,CAAC;IAChF,OAAO,CAAC,MAAM,QAAQ,CAAC,MAAM,CAAC;QAC5B,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,IAAI,EAAE;YACJ,GAAG,iBAAiB;YACpB,GAAG,CAAC,IAAI,CAAC,UAAU;gBACjB,CAAC,CAAC,CAAC,+BAA+B,EAAE,wBAAwB,CAAC;gBAC7D,CAAC,CAAC,EAAE,CAAC;SACR;KACF,CAAC,CAAuB,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { type BrowserContext, type Page } from '@playwright/test';
2
+ export declare function launchContext(opts: {
3
+ headless: boolean;
4
+ useStorageState: boolean;
5
+ }): Promise<BrowserContext>;
6
+ export declare function generateStoreName(workerIndex: number): string;
7
+ /**
8
+ * Mirrors Shopify CLI's online/setup/store.ts createDevStore flow.
9
+ * Selectors target stable s-internal-* shadow-DOM hooks that survive
10
+ * Polaris churn.
11
+ */
12
+ export declare function createDevStore(page: Page, opts: {
13
+ orgId: string;
14
+ storeName: string;
15
+ plan: string;
16
+ }): Promise<{
17
+ shop: string;
18
+ slug: string;
19
+ }>;
20
+ export declare function uninstallApp(page: Page, slug: string, appHandle: string): Promise<boolean>;
21
+ export declare function deleteStore(page: Page, slug: string): Promise<void>;
22
+ //# sourceMappingURL=storeAutomation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storeAutomation.d.ts","sourceRoot":"","sources":["../../src/lib/storeAutomation.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAIlE,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,EAAE,OAAO,CAAC;CAC1B,GAAG,OAAO,CAAC,cAAc,CAAC,CAkB1B;AAED,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE7D;AASD;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,IAAI,EACV,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACvD,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAqBzC;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAmBlB;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAczE"}
@@ -0,0 +1,85 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { storageStatePath } from '@essential-apps/shopify-test-core';
3
+ import { launchStealthBrowser } from './stealthLaunch.js';
4
+ export async function launchContext(opts) {
5
+ if (opts.useStorageState && !existsSync(storageStatePath)) {
6
+ throw new Error(`Auth state not found at ${storageStatePath}.\n` + `Run: npm run test:online:auth`);
7
+ }
8
+ // patchright (bundled Chromium) via the shared launcher — the one stealth
9
+ // engine, and the only one that works in the arm64 VM (no real Chrome
10
+ // there). assertInVm() inside it refuses a host launch.
11
+ const browser = await launchStealthBrowser({ headless: opts.headless });
12
+ const context = await browser.newContext({
13
+ ...(opts.useStorageState ? { storageState: storageStatePath } : {}),
14
+ viewport: { width: 1400, height: 900 },
15
+ });
16
+ await context.addInitScript(() => {
17
+ Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
18
+ });
19
+ return context;
20
+ }
21
+ export function generateStoreName(workerIndex) {
22
+ return `online-w${workerIndex}-${Date.now()}`;
23
+ }
24
+ const PLAN_LABEL = {
25
+ BASIC_APP_DEVELOPMENT: 'Basic',
26
+ PROFESSIONAL_APP_DEVELOPMENT: 'Professional',
27
+ UNLIMITED_APP_DEVELOPMENT: 'Unlimited',
28
+ SHOPIFY_PLUS_APP_DEVELOPMENT: 'Shopify Plus',
29
+ };
30
+ /**
31
+ * Mirrors Shopify CLI's online/setup/store.ts createDevStore flow.
32
+ * Selectors target stable s-internal-* shadow-DOM hooks that survive
33
+ * Polaris churn.
34
+ */
35
+ export async function createDevStore(page, opts) {
36
+ const url = `https://admin.shopify.com/store-create/organization/${opts.orgId}`;
37
+ await page.goto(url, { waitUntil: 'domcontentloaded' });
38
+ const nameField = page.locator('s-internal-text-field[label="Store name"]').first();
39
+ await nameField.waitFor({ state: 'visible', timeout: 30_000 });
40
+ await nameField.locator('input').fill(opts.storeName);
41
+ const planSelect = page.locator('s-internal-select[label="Shopify plan"]').first();
42
+ await planSelect.click();
43
+ const planLabel = PLAN_LABEL[opts.plan];
44
+ if (!planLabel)
45
+ throw new Error(`Unknown plan: ${opts.plan}`);
46
+ await page.getByRole('option', { name: new RegExp(planLabel, 'i') }).first().click();
47
+ await page.locator('s-internal-button[variant="primary"]').first().click();
48
+ await page.waitForURL(/admin\.shopify\.com\/store\/[^/]+/, { timeout: 90_000 });
49
+ const m = page.url().match(/admin\.shopify\.com\/store\/([^/?#]+)/);
50
+ if (!m)
51
+ throw new Error(`Unexpected post-create URL: ${page.url()}`);
52
+ const slug = m[1];
53
+ return { slug, shop: `${slug}.myshopify.com` };
54
+ }
55
+ export async function uninstallApp(page, slug, appHandle) {
56
+ await page.goto(`https://admin.shopify.com/store/${slug}/settings/apps`, {
57
+ waitUntil: 'domcontentloaded',
58
+ });
59
+ const row = page.getByRole('link', { name: new RegExp(appHandle, 'i') }).first();
60
+ if ((await row.count()) === 0)
61
+ return false;
62
+ await row.click();
63
+ await page.waitForLoadState('domcontentloaded');
64
+ const uninstallBtn = page.getByRole('button', { name: /uninstall/i }).first();
65
+ if ((await uninstallBtn.count()) === 0)
66
+ return false;
67
+ await uninstallBtn.click();
68
+ const confirmBtn = page.getByRole('button', { name: /uninstall app|confirm|delete/i }).last();
69
+ await confirmBtn.click();
70
+ await page.waitForLoadState('networkidle').catch(() => { });
71
+ return true;
72
+ }
73
+ export async function deleteStore(page, slug) {
74
+ await page.goto(`https://admin.shopify.com/store/${slug}/settings/plan/cancel`, {
75
+ waitUntil: 'domcontentloaded',
76
+ });
77
+ const checkbox = page.getByRole('checkbox').first();
78
+ await checkbox.waitFor({ state: 'visible', timeout: 30_000 });
79
+ await checkbox.check();
80
+ await page.getByRole('button', { name: /delete store/i }).click();
81
+ await page
82
+ .waitForURL((u) => !u.toString().includes(slug), { timeout: 60_000 })
83
+ .catch(() => { });
84
+ }
85
+ //# sourceMappingURL=storeAutomation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storeAutomation.js","sourceRoot":"","sources":["../../src/lib/storeAutomation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import { type PlaywrightTestConfig } from '@playwright/test';
2
+ export interface DefinePlaywrightConfigOptions {
3
+ /**
4
+ * Absolute path to the directory containing the app's `*.spec.ts`
5
+ * files. Conventionally `tests/test-online/` or `tests/test-offline/`.
6
+ */
7
+ testDir: string;
8
+ /**
9
+ * Optional: override the Playwright test glob.
10
+ * Defaults to `**​/*.spec.ts`.
11
+ */
12
+ testMatch?: string | string[];
13
+ /**
14
+ * Optional: globs to exclude. Defaults to excluding `*offline*.spec.ts`
15
+ * in online mode (those run via a sibling
16
+ * `playwright.offline-full.config.ts`), and to `[]` in offline mode
17
+ * (where testMatch already filters in only the offline specs).
18
+ */
19
+ testIgnore?: string | string[];
20
+ /** Override default workers (default: env TEST_WORKERS or 1). */
21
+ workers?: number;
22
+ /** Override default retries (default: env TEST_RETRIES or 1). */
23
+ retries?: number;
24
+ /** Per-test timeout in ms. Default 90s. */
25
+ timeout?: number;
26
+ /** Per-expect timeout in ms. Default 15s. */
27
+ expectTimeout?: number;
28
+ /**
29
+ * Extra options that will be merged into the returned config —
30
+ * useful for app-specific reporter tweaks, projects, etc.
31
+ */
32
+ override?: PlaywrightTestConfig;
33
+ /**
34
+ * Offline-only mode: skip every assertion that exists for online
35
+ * cloud runs (Shopify Partner storageState, store registry,
36
+ * NODE_ENV=test, local Postgres). Use this for specs that talk
37
+ * exclusively to the offline storefront mock — they don't touch
38
+ * Cloudflare, Shopify auth, or the app backend, so the online
39
+ * preconditions don't apply.
40
+ *
41
+ * Typical setup: dedicated `playwright.config.offline.ts` in the
42
+ * consuming app, sibling to the online `playwright.config.ts`.
43
+ */
44
+ offline?: boolean;
45
+ }
46
+ /**
47
+ * Playwright config preset for Essential Apps' Shopify test suites.
48
+ *
49
+ * The consuming app's `tests/test-online/playwright.config.ts` should be a
50
+ * thin wrapper:
51
+ *
52
+ * ```ts
53
+ * import { definePlaywrightConfig } from '@essential-apps/shopify-test-runner/playwright';
54
+ * import { fileURLToPath } from 'node:url';
55
+ *
56
+ * export default definePlaywrightConfig({
57
+ * testDir: fileURLToPath(new URL('.', import.meta.url)),
58
+ * });
59
+ * ```
60
+ */
61
+ export declare function definePlaywrightConfig(opts: DefinePlaywrightConfigOptions): PlaywrightTestConfig;
62
+ //# sourceMappingURL=baseConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseConfig.d.ts","sourceRoot":"","sources":["../../src/playwright/baseConfig.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAY3E,MAAM,WAAW,6BAA6B;IAC5C;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC9B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC/B,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,6BAA6B,GAClC,oBAAoB,CA2CtB"}
@@ -0,0 +1,68 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { defineConfig } from '@playwright/test';
4
+ import { storageStatePath } from '@essential-apps/shopify-test-core';
5
+ /**
6
+ * Resolve the absolute path to our globalSetup file. Playwright wants
7
+ * a path string for `globalSetup`; from the consuming app's repo this
8
+ * is somewhere inside `node_modules/@essential-apps/shopify-test-runner/dist`.
9
+ *
10
+ * `.js` (not `.ts`) because this file ships to consumers as built JS.
11
+ */
12
+ const globalSetupPath = fileURLToPath(new URL('./globalSetup.js', import.meta.url));
13
+ /**
14
+ * Playwright config preset for Essential Apps' Shopify test suites.
15
+ *
16
+ * The consuming app's `tests/test-online/playwright.config.ts` should be a
17
+ * thin wrapper:
18
+ *
19
+ * ```ts
20
+ * import { definePlaywrightConfig } from '@essential-apps/shopify-test-runner/playwright';
21
+ * import { fileURLToPath } from 'node:url';
22
+ *
23
+ * export default definePlaywrightConfig({
24
+ * testDir: fileURLToPath(new URL('.', import.meta.url)),
25
+ * });
26
+ * ```
27
+ */
28
+ export function definePlaywrightConfig(opts) {
29
+ // Online-mode prerequisites. Offline mode skips the storageState
30
+ // requirement entirely (offline tests never touch Shopify auth).
31
+ if (!opts.offline && !existsSync(storageStatePath)) {
32
+ throw new Error(`Auth state not found at ${storageStatePath}. Run: npm run test:online:auth`);
33
+ }
34
+ // In online mode, exclude *offline*.spec.ts by default — those need
35
+ // the offline runner's distinct fixtures and would either be wasted
36
+ // (no backend needed) or fail under the online globalSetup. Offline
37
+ // mode doesn't get a default ignore since its testMatch already
38
+ // narrows in.
39
+ const defaultTestIgnore = opts.offline ? [] : ['**/*offline*.spec.ts'];
40
+ const base = {
41
+ testDir: opts.testDir,
42
+ testMatch: opts.testMatch ?? ['**/*.spec.ts'],
43
+ testIgnore: opts.testIgnore ?? defaultTestIgnore,
44
+ // No globalSetup in offline mode — its checks (NODE_ENV=test,
45
+ // local Postgres, store registry with installed apps) are all
46
+ // online-only invariants.
47
+ ...(opts.offline ? {} : { globalSetup: globalSetupPath }),
48
+ timeout: opts.timeout ?? 90_000,
49
+ expect: { timeout: opts.expectTimeout ?? 15_000 },
50
+ fullyParallel: false,
51
+ // Cloudflare flags concurrent contexts from one IP. Bump TEST_WORKERS=2
52
+ // only after the cf_clearance cookie is established in each worker's
53
+ // persistent profile (run once with TEST_WORKERS=N to seed profiles).
54
+ workers: opts.workers ?? Number(process.env['TEST_WORKERS'] ?? 1),
55
+ // CF's bot challenge can fire on profile creation; one retry is enough.
56
+ retries: opts.retries ?? Number(process.env['TEST_RETRIES'] ?? 1),
57
+ reporter: [['list'], ['html', { open: 'never' }]],
58
+ use: {
59
+ trace: 'retain-on-failure',
60
+ screenshot: 'only-on-failure',
61
+ },
62
+ // Browser launch is handled by the per-worker `persistentContext`
63
+ // fixture in @essential-apps/shopify-test-admin (uses
64
+ // launchPersistentContext so cf_clearance + auth cookies persist).
65
+ };
66
+ return defineConfig({ ...base, ...opts.override });
67
+ }
68
+ //# sourceMappingURL=baseConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseConfig.js","sourceRoot":"","sources":["../../src/playwright/baseConfig.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,YAAY,EAA6B,MAAM,kBAAkB,CAAC;AAC3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAErE;;;;;;GAMG;AACH,MAAM,eAAe,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,kBAAkB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AA+CpF;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,sBAAsB,CACpC,IAAmC;IAEnC,iEAAiE;IACjE,iEAAiE;IACjE,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CACb,2BAA2B,gBAAgB,iCAAiC,CAC7E,CAAC;IACJ,CAAC;IAED,oEAAoE;IACpE,oEAAoE;IACpE,oEAAoE;IACpE,gEAAgE;IAChE,cAAc;IACd,MAAM,iBAAiB,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC;IACvE,MAAM,IAAI,GAAyB;QACjC,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,CAAC,cAAc,CAAC;QAC7C,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,iBAAiB;QAChD,8DAA8D;QAC9D,8DAA8D;QAC9D,0BAA0B;QAC1B,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,eAAe,EAAE,CAAC;QACzD,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,MAAM;QAC/B,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,aAAa,IAAI,MAAM,EAAE;QACjD,aAAa,EAAE,KAAK;QACpB,wEAAwE;QACxE,qEAAqE;QACrE,sEAAsE;QACtE,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;QACjE,wEAAwE;QACxE,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;QACjE,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACjD,GAAG,EAAE;YACH,KAAK,EAAE,mBAAmB;YAC1B,UAAU,EAAE,iBAAiB;SAC9B;QACD,kEAAkE;QAClE,sDAAsD;QACtD,mEAAmE;KACpE,CAAC;IAEF,OAAO,YAAY,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;AACrD,CAAC"}
@@ -0,0 +1,2 @@
1
+ export default function globalSetup(): Promise<void>;
2
+ //# sourceMappingURL=globalSetup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"globalSetup.d.ts","sourceRoot":"","sources":["../../src/playwright/globalSetup.ts"],"names":[],"mappings":"AAoBA,wBAA8B,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAmEzD"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Playwright global setup. Runs ONCE before the suite.
3
+ *
4
+ * Hard guards against running tests against non-isolated databases.
5
+ * The runTests.ts orchestrator sets NODE_ENV=test and DATABASE_URL
6
+ * pointing at a fresh UUID-named local Postgres DB before invoking
7
+ * Playwright. We verify both before any test runs.
8
+ *
9
+ * Consuming apps reference this from their `playwright.config.ts` via
10
+ * `definePlaywrightConfig` (which sets `globalSetup` to point at this
11
+ * file's path).
12
+ */
13
+ import { existsSync, readFileSync } from 'node:fs';
14
+ import { registryPath, storageStatePath } from '@essential-apps/shopify-test-core';
15
+ import { launchStealthBrowser } from '../lib/stealthLaunch.js';
16
+ export default async function globalSetup() {
17
+ if (!existsSync(registryPath)) {
18
+ throw new Error(`${registryPath} missing. Run \`npm run test:online:add\` and \`npm run test:online:install\`.`);
19
+ }
20
+ const pool = JSON.parse(readFileSync(registryPath, 'utf8'));
21
+ const installed = pool.stores.filter((s) => s['appInstalled'] === true);
22
+ if (installed.length === 0) {
23
+ throw new Error(`No stores in pool have appInstalled: true. Run \`npm run test:online:install\`.`);
24
+ }
25
+ const dbUrl = process.env['DATABASE_URL'] ?? '';
26
+ const nodeEnv = process.env['NODE_ENV'] ?? '';
27
+ if (nodeEnv !== 'test') {
28
+ throw new Error(`NODE_ENV must be "test" for tests. Got "${nodeEnv}". ` +
29
+ `Run via \`npm run test:online\` (uses shopify-test-run-tests), not the bare ` +
30
+ `Playwright entrypoint.`);
31
+ }
32
+ if (dbUrl && (dbUrl.includes('neon.tech') || dbUrl.includes('aws.neon'))) {
33
+ throw new Error(`DATABASE_URL points at Neon (${dbUrl.replace(/:[^:@]+@/, ':***@')}). ` +
34
+ `Tests must use a local Postgres DB for isolation. Run via \`npm run test:online\`.`);
35
+ }
36
+ if (!dbUrl.includes('localhost') && !dbUrl.includes('127.0.0.1')) {
37
+ throw new Error(`DATABASE_URL must be localhost for test isolation. Got: ${dbUrl.replace(/:[^:@]+@/, ':***@')}`);
38
+ }
39
+ console.log(`[test] ${installed.length} installed store(s) in pool.`);
40
+ console.log(`[test] Test DB: ${dbUrl}`);
41
+ console.log(`[test] NODE_ENV: ${nodeEnv}`);
42
+ // ── Auth validity pre-flight ──────────────────────────────
43
+ // The checks above prove the storageState FILE exists and the registry
44
+ // is populated — but NOT that the captured Shopify session is still
45
+ // VALID. A stale session (or a rotated cf_clearance) makes every online
46
+ // test dead-end identically: openAppFrame navigates to admin.shopify.com,
47
+ // Shopify bounces to login (or Cloudflare shows Turnstile), the embedded
48
+ // app iframe never appears, and the test times out after 30s. With N
49
+ // tests that's N identical, cryptic "waiting for iframe[src*=localhost]"
50
+ // failures with no hint that the real cause is auth.
51
+ //
52
+ // So navigate ONCE here, exactly as the tests do, and abort the whole run
53
+ // with a clear, actionable message if the session is dead. A throw in
54
+ // globalSetup stops everything before any test starts — one message
55
+ // instead of N timeouts. Opt out with TEST_ONLINE_SKIP_AUTH_PREFLIGHT=true.
56
+ if ((process.env['TEST_ONLINE_SKIP_AUTH_PREFLIGHT'] ?? '').toLowerCase() !== 'true') {
57
+ const slug = installed[0]?.['slug'] ?? '';
58
+ if (slug) {
59
+ console.log('[test] auth pre-flight: verifying the captured session is still valid…');
60
+ await assertAuthValid(slug);
61
+ console.log('[test] auth pre-flight: session OK.');
62
+ }
63
+ }
64
+ }
65
+ /**
66
+ * Navigate to an installed store's embedded-app admin URL with the
67
+ * captured storageState — the SAME patchright launch the online tests use
68
+ * (storePool) — and classify the outcome:
69
+ * - app iframe appears, or we reach the signed-in account picker → OK
70
+ * - redirected to the Shopify login page → session expired
71
+ * - Cloudflare Turnstile wall → cf_clearance stale
72
+ * Throws a clear "re-capture auth" message for the two failure modes so the
73
+ * whole run aborts with one diagnosis instead of N iframe-timeout failures.
74
+ */
75
+ async function assertAuthValid(slug) {
76
+ const inContainer = (process.env['TEST_IN_CONTAINER'] ?? '').toLowerCase() === 'true';
77
+ const visible = (process.env['TEST_VISIBLE'] ?? '').toLowerCase() === 'true';
78
+ const headless = inContainer ? false : !visible;
79
+ const appHandle = process.env['SHOPIFY_APP_HANDLE'] ?? 'essential-app';
80
+ const expectedBackend = new URL(process.env['SHOPIFY_APP_URL'] ?? 'https://localhost:8181').hostname;
81
+ const url = `https://admin.shopify.com/store/${slug}/apps/${appHandle}`;
82
+ const fix = `Re-run interactive auth capture (\`npm run test:online:auth\`): a browser opens — ` +
83
+ `sign in and pass Turnstile once, and the fresh session + cf_clearance are saved ` +
84
+ `back into storageState.json. Then re-run \`npm run test:online\`. ` +
85
+ `(Set TEST_ONLINE_SKIP_AUTH_PREFLIGHT=true to skip this check.)`;
86
+ const browser = await launchStealthBrowser({
87
+ headless,
88
+ hideWindow: !visible && !inContainer,
89
+ });
90
+ try {
91
+ const context = await browser.newContext({
92
+ storageState: storageStatePath,
93
+ viewport: { width: 1400, height: 900 },
94
+ ignoreHTTPSErrors: true,
95
+ });
96
+ const page = await context.newPage();
97
+ await page.goto(url, { waitUntil: 'domcontentloaded' }).catch(() => { });
98
+ const deadline = Date.now() + 30_000;
99
+ while (Date.now() < deadline) {
100
+ const u = page.url();
101
+ // Cloudflare Turnstile wall (same probe text openApp uses).
102
+ if (await page
103
+ .getByText('Your connection needs to be verified')
104
+ .isVisible()
105
+ .catch(() => false)) {
106
+ throw new Error(`\n\n❌ Online auth pre-flight: Cloudflare Turnstile is blocking ${url}.\n` +
107
+ ` Your captured cf_clearance is missing or expired.\n ${fix}\n`);
108
+ }
109
+ // Stale session → bounced to the Shopify login / account lookup.
110
+ // (accounts.shopify.com/select is the SIGNED-IN account picker — OK.)
111
+ if (/accounts\.shopify\.com\/(lookup|signin|login|store_login|authentication)/.test(u) ||
112
+ /identity\.myshopify\.com/.test(u)) {
113
+ throw new Error(`\n\n❌ Online auth pre-flight: redirected to the Shopify login page (${u}).\n` +
114
+ ` Your captured session has expired.\n ${fix}\n`);
115
+ }
116
+ // Signed-in account picker — valid session.
117
+ if (u.includes('accounts.shopify.com/select'))
118
+ return;
119
+ // Embedded app iframe present — signed in AND app reachable.
120
+ if ((await page.locator(`iframe[src*="${expectedBackend}"]`).count()) > 0)
121
+ return;
122
+ await page.waitForTimeout(500);
123
+ }
124
+ // 30s without a definitive verdict. If parked on a login page, it's
125
+ // stale; otherwise it's likely a backend/app problem (NOT auth) — warn
126
+ // and let the per-test diagnostics speak rather than block on a guess.
127
+ const finalUrl = page.url();
128
+ if (/accounts\.shopify\.com/.test(finalUrl) && !finalUrl.includes('/select')) {
129
+ throw new Error(`\n\n❌ Online auth pre-flight: stuck on the Shopify login page (${finalUrl}).\n ${fix}\n`);
130
+ }
131
+ console.warn(`[test] auth pre-flight inconclusive (last URL: ${finalUrl}). The session looks ` +
132
+ `valid (no login redirect / Turnstile) but the app iframe didn't appear within ` +
133
+ `30s — if tests fail on iframe load, check the app backend, not auth.`);
134
+ }
135
+ finally {
136
+ await browser.close().catch(() => { });
137
+ }
138
+ }
139
+ //# sourceMappingURL=globalSetup.js.map