@akanjs/devkit 3.0.0-beta.9 → 3.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 (341) hide show
  1. package/DEV_RUNTIME_KNOBS.md +44 -26
  2. package/README.ko.md +21 -7
  3. package/README.md +17 -8
  4. package/abstractDoc.test.ts +6 -15
  5. package/abstractDoc.ts +6 -8
  6. package/agentsIndex.test.ts +4 -7
  7. package/agentsIndex.ts +6 -22
  8. package/akanApp/BackendImportGraph.test.ts +8 -22
  9. package/akanApp/BackendImportGraph.ts +1 -20
  10. package/akanApp/akanApp.host.test.ts +56 -3
  11. package/akanApp/akanApp.host.ts +218 -278
  12. package/akanApp/devBootLatch.test.ts +39 -0
  13. package/akanApp/devBootLatch.ts +39 -0
  14. package/akanApp/devHostPolicy.test.ts +81 -35
  15. package/akanApp/devHostPolicy.ts +41 -102
  16. package/akanConfig/akanBin.test.ts +62 -0
  17. package/akanConfig/akanBin.ts +78 -0
  18. package/akanConfig/akanConfig.test.ts +439 -141
  19. package/akanConfig/akanConfig.ts +359 -275
  20. package/akanConfig/index.ts +1 -0
  21. package/akanConfig/types.ts +22 -8
  22. package/akanContext.ts +110 -160
  23. package/akanMcpContract.ts +28 -73
  24. package/appSelectionMemory.ts +1 -4
  25. package/applicationBuildReporter.ts +37 -5
  26. package/applicationBuildRunner.test.ts +41 -5
  27. package/applicationBuildRunner.ts +71 -63
  28. package/applicationTestPreload.ts +3 -12
  29. package/artifact/implicitRootLayout.test.ts +32 -30
  30. package/artifact/implicitRootLayout.ts +58 -106
  31. package/artifact/routeSeedIndex.test.ts +7 -6
  32. package/artifact/routeSeedIndex.ts +11 -44
  33. package/biome.base.json +35 -2
  34. package/biomeBase.ts +2 -5
  35. package/cloud/cloudApi.test.ts +202 -0
  36. package/cloud/cloudApi.ts +77 -39
  37. package/cloud/configLock.test.ts +64 -0
  38. package/cloud/configLock.ts +96 -0
  39. package/cloud/constants.ts +26 -5
  40. package/cloud/globalConfig.ts +71 -32
  41. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  42. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  43. package/codeAgent/agent/CodeAgent.ts +551 -0
  44. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  45. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  46. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  47. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  48. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  49. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  50. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  51. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  52. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  53. package/codeAgent/agent/CodeMailbox.ts +148 -0
  54. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  55. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  56. package/codeAgent/agent/SubagentPool.ts +214 -0
  57. package/codeAgent/agent/akanCodeModel.ts +47 -0
  58. package/codeAgent/agent/akanCodePaths.ts +31 -0
  59. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  60. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  61. package/codeAgent/agent/codeAgent.test.ts +178 -0
  62. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  63. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  64. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  65. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  66. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  67. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  68. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  69. package/codeAgent/agent/consoleToStderr.ts +8 -0
  70. package/codeAgent/agent/jsonLines.ts +29 -0
  71. package/codeAgent/agent/stringArg.ts +5 -0
  72. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  73. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  74. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  75. package/codeAgent/feedback/PreviewView.ts +95 -0
  76. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  77. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  78. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  79. package/codeAgent/index.ts +34 -0
  80. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  81. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  82. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  83. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  84. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  85. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  86. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  87. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  88. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  89. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  90. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  91. package/codeAgent/tools/AkanCli.ts +50 -0
  92. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  93. package/codeAgent/tools/AkanEditScope.ts +48 -0
  94. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  95. package/codeAgent/tools/AkanToolPack.ts +92 -0
  96. package/codeAgent/tools/AkanVerifier.ts +48 -0
  97. package/codeAgent/tools/AskToolPack.ts +99 -0
  98. package/codeAgent/tools/MailToolPack.ts +76 -0
  99. package/codeAgent/tools/McpClient.ts +187 -0
  100. package/codeAgent/tools/McpOAuth.ts +233 -0
  101. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  102. package/codeAgent/tools/McpServerConfig.ts +129 -0
  103. package/codeAgent/tools/McpSignIn.ts +158 -0
  104. package/codeAgent/tools/McpTokenStore.ts +81 -0
  105. package/codeAgent/tools/McpToolPack.ts +199 -0
  106. package/codeAgent/tools/SessionToolPack.ts +91 -0
  107. package/codeAgent/tools/WebToolPack.ts +147 -0
  108. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  109. package/codeAgent/tools/askToolPack.test.ts +116 -0
  110. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  111. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  112. package/codegenLock.test.ts +4 -13
  113. package/codegenLock.ts +3 -17
  114. package/commandDecorators/argMeta.ts +1 -5
  115. package/commandDecorators/camelToKebabCase.ts +1 -0
  116. package/commandDecorators/command.ts +46 -102
  117. package/commandDecorators/commandBuilder.ts +30 -61
  118. package/commandDecorators/commandDecorators.test.ts +2 -10
  119. package/commandDecorators/dependencyBuilder.ts +10 -1
  120. package/commandDecorators/helpFormatter.ts +68 -145
  121. package/commandDecorators/index.ts +0 -2
  122. package/commandDecorators/targetMeta.ts +9 -7
  123. package/createTunnel.ts +7 -14
  124. package/csrE2e/csrE2eHarness.fixture.ts +505 -0
  125. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  126. package/csrE2e/csrPageStack.e2e.test.ts +252 -0
  127. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  128. package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
  129. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  130. package/csrE2e/registryVendor.e2e.test.ts +221 -0
  131. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  132. package/dependencyScanner.test.ts +4 -16
  133. package/dependencyScanner.ts +31 -80
  134. package/devkitUtils.test.ts +48 -20
  135. package/executors.linkFallback.test.ts +103 -0
  136. package/executors.test.ts +176 -106
  137. package/executors.ts +212 -498
  138. package/fileEditor.ts +9 -37
  139. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  140. package/fileSys.ts +22 -9
  141. package/formSetterScanner.ts +3 -16
  142. package/frontendBuild/allRoutesBuilder.ts +7 -21
  143. package/frontendBuild/autoImportSync.test.ts +27 -38
  144. package/frontendBuild/autoImportSync.ts +31 -65
  145. package/frontendBuild/buildRouteClient.test.ts +123 -37
  146. package/frontendBuild/bundleDefine.test.ts +12 -0
  147. package/frontendBuild/bundleDefine.ts +16 -0
  148. package/frontendBuild/clientBuildTypes.ts +5 -36
  149. package/frontendBuild/clientEntriesBundler.ts +9 -40
  150. package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
  151. package/frontendBuild/clientEntryDiscovery.ts +122 -110
  152. package/frontendBuild/csrArtifactBuilder.ts +9 -31
  153. package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
  154. package/frontendBuild/csrDevArtifactWriter.ts +307 -0
  155. package/frontendBuild/csrDevBundler.test.ts +66 -0
  156. package/frontendBuild/csrDevBundler.ts +95 -0
  157. package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
  158. package/frontendBuild/csrDevFactoryCheck.ts +39 -0
  159. package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
  160. package/frontendBuild/csrDevModuleCompiler.ts +422 -0
  161. package/frontendBuild/csrDevPatcher.test.ts +572 -0
  162. package/frontendBuild/csrDevPatcher.ts +548 -0
  163. package/frontendBuild/csrDevPaths.ts +145 -0
  164. package/frontendBuild/csrDevResolver.test.ts +239 -0
  165. package/frontendBuild/csrDevResolver.ts +363 -0
  166. package/frontendBuild/csrDevTypes.ts +61 -0
  167. package/frontendBuild/csrEntryFiles.ts +24 -0
  168. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  169. package/frontendBuild/cssCandidateCache.ts +6 -41
  170. package/frontendBuild/cssCompiler.ts +31 -82
  171. package/frontendBuild/cssImportResolver.ts +6 -19
  172. package/frontendBuild/devChangePlanner.ts +7 -10
  173. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  174. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  175. package/frontendBuild/devRegistryBundler.ts +192 -0
  176. package/frontendBuild/fontOptimizer.test.ts +6 -14
  177. package/frontendBuild/fontOptimizer.ts +15 -72
  178. package/frontendBuild/fontPruner.test.ts +5 -10
  179. package/frontendBuild/fontPruner.ts +5 -20
  180. package/frontendBuild/frontendBuild.test.ts +57 -70
  181. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  182. package/frontendBuild/hmrWatcher.test.ts +59 -32
  183. package/frontendBuild/hmrWatcher.ts +31 -74
  184. package/frontendBuild/index.ts +4 -0
  185. package/frontendBuild/pagesBundleBuilder.ts +28 -30
  186. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  187. package/frontendBuild/precompressArtifacts.ts +5 -18
  188. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  189. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  190. package/frontendBuild/routeClientBuilder.ts +100 -62
  191. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  192. package/frontendBuild/serverGraphFile.test.ts +77 -0
  193. package/frontendBuild/serverGraphFile.ts +93 -0
  194. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  195. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  196. package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
  197. package/frontendBuild/ssrDevBundler.ts +85 -0
  198. package/frontendBuild/styleContract.ts +1 -8
  199. package/frontendBuild/themeValidator.ts +4 -26
  200. package/frontendBuild/watchRootResolver.test.ts +23 -42
  201. package/frontendBuild/watchRootResolver.ts +5 -27
  202. package/getRelatedCnsts.ts +12 -92
  203. package/incrementalBuilder/buildBatch.proc.ts +147 -63
  204. package/incrementalBuilder/buildBatchProtocol.ts +13 -38
  205. package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
  206. package/incrementalBuilder/buildBatchRunner.ts +26 -38
  207. package/incrementalBuilder/builderChannel.test.ts +24 -49
  208. package/incrementalBuilder/builderChannel.ts +6 -37
  209. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  210. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  211. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  212. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  213. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  214. package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
  215. package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
  216. package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
  217. package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
  218. package/index.ts +5 -19
  219. package/integration/devResourceProbe.ts +50 -56
  220. package/integration/devStability.integration.test.ts +105 -199
  221. package/integration/devStabilityHarness.test.ts +13 -28
  222. package/integration/devStabilityHarness.ts +65 -330
  223. package/integration/ssrMemoryProbe.ts +41 -167
  224. package/libSource.test.ts +4 -15
  225. package/libSource.ts +19 -31
  226. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  227. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  228. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  229. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  230. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  231. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  232. package/lint/gritRules.test.ts +8 -24
  233. package/lint/no-deep-internal-import.grit +2 -2
  234. package/lint/no-document-cookie.grit +29 -0
  235. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  236. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  237. package/lint/no-web-storage.grit +30 -0
  238. package/linter.test.ts +1 -2
  239. package/linter.ts +26 -116
  240. package/mobile/desktopBin.test.ts +182 -0
  241. package/mobile/desktopBin.ts +144 -0
  242. package/mobile/desktopServerStage.test.ts +252 -0
  243. package/mobile/desktopServerStage.ts +178 -0
  244. package/mobile/index.ts +9 -0
  245. package/mobile/mobileTarget.test.ts +57 -8
  246. package/mobile/mobileTarget.ts +45 -26
  247. package/mobile/nativeApi.test.ts +30 -0
  248. package/mobile/nativeApi.ts +40 -0
  249. package/mobile/nativeApp.test.ts +184 -0
  250. package/mobile/nativeApp.ts +454 -0
  251. package/mobile/nativeAppLine.test.ts +69 -0
  252. package/mobile/nativeAppLine.ts +71 -0
  253. package/mobile/nativeConfig.test.ts +438 -0
  254. package/mobile/nativeConfig.ts +294 -0
  255. package/mobile/nativePluginFolders.test.ts +81 -0
  256. package/mobile/nativePluginFolders.ts +60 -0
  257. package/mobile/nativeWebDir.test.ts +80 -0
  258. package/mobile/nativeWebDir.ts +66 -0
  259. package/mobile/placeholderAppId.test.ts +13 -0
  260. package/mobile/placeholderAppId.ts +16 -0
  261. package/mobile/usageDescriptions.test.ts +27 -0
  262. package/mobile/usageDescriptions.ts +24 -0
  263. package/package.json +11 -11
  264. package/packageExports.test.ts +3 -18
  265. package/packageExportsMap.ts +3 -19
  266. package/piDependencyPins.test.ts +46 -0
  267. package/platformTest/BunTestReport.test.ts +60 -0
  268. package/platformTest/BunTestReport.ts +65 -0
  269. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  270. package/platformTest/IdleWatchedProcess.ts +73 -0
  271. package/platformTest/LinuxDockerTarget.ts +105 -0
  272. package/platformTest/PlatformTestRun.test.ts +64 -0
  273. package/platformTest/PlatformTestRun.ts +141 -0
  274. package/platformTest/PlatformTestTarget.ts +142 -0
  275. package/platformTest/TestSnapshot.test.ts +72 -0
  276. package/platformTest/TestSnapshot.ts +103 -0
  277. package/platformTest/WindowsSshTarget.ts +151 -0
  278. package/prompter.ts +3 -13
  279. package/qualityScanner.test.ts +9 -26
  280. package/qualityScanner.ts +200 -286
  281. package/recipeScanner.test.ts +7 -12
  282. package/recipeScanner.ts +9 -38
  283. package/repoIdentity.ts +2 -9
  284. package/routeSourceValidator.test.ts +0 -1
  285. package/routeSourceValidator.ts +7 -42
  286. package/scanInfo.ts +91 -225
  287. package/selectModel.ts +1 -2
  288. package/semver.ts +3 -5
  289. package/slicePlanner.test.ts +6 -24
  290. package/slicePlanner.ts +11 -32
  291. package/spinner.test.ts +3 -8
  292. package/spinner.ts +4 -16
  293. package/sshTunnel.ts +1 -6
  294. package/ssrScanner.test.ts +1 -6
  295. package/ssrScanner.ts +103 -113
  296. package/stripAnsi.ts +3 -0
  297. package/subspace.test.ts +9 -25
  298. package/subspace.ts +48 -115
  299. package/subspaceConfig.ts +3 -11
  300. package/testHelpers.ts +194 -0
  301. package/transforms/asyncDefaultExportDetector.ts +3 -11
  302. package/transforms/barrelAnalyzer.ts +43 -135
  303. package/transforms/barrelImportsPlugin.ts +38 -175
  304. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  305. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  306. package/transforms/moduleSyntax.ts +32 -0
  307. package/transforms/rscUseClientTransform.ts +42 -20
  308. package/transforms/serverEnvPlugin.test.ts +103 -0
  309. package/transforms/serverEnvPlugin.ts +41 -0
  310. package/transforms/transforms.test.ts +166 -44
  311. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  312. package/transforms/useClientBundlePlugin.ts +6 -23
  313. package/typeChecker.ts +7 -63
  314. package/types.ts +2 -0
  315. package/uploadRelease.ts +10 -25
  316. package/workflow/artifacts.ts +18 -0
  317. package/workflow/executor.test.ts +1 -12
  318. package/workflow/executor.ts +101 -110
  319. package/workflow/index.ts +0 -1
  320. package/workflow/moduleIndex.test.ts +4 -15
  321. package/workflow/moduleIndex.ts +9 -49
  322. package/workflow/plan.ts +18 -25
  323. package/workflow/render.ts +80 -96
  324. package/workflow/rolloutGate.test.ts +1 -1
  325. package/workflow/source.ts +52 -69
  326. package/workflow/sourceAst.ts +50 -0
  327. package/workspaceLayout.test.ts +18 -3
  328. package/workspaceLayout.ts +22 -23
  329. package/abstractCompactor.ts +0 -112
  330. package/aiEditor.test.ts +0 -60
  331. package/aiEditor.ts +0 -387
  332. package/applicationReleasePackager.ts +0 -209
  333. package/capacitorApp.test.ts +0 -372
  334. package/capacitorApp.ts +0 -1648
  335. package/commandDecorators/commandMeta.ts +0 -7
  336. package/commandDecorators/types.ts +0 -10
  337. package/getDirname.ts +0 -4
  338. package/guideline.ts +0 -19
  339. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  340. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  341. package/workflow/primitive.ts +0 -59
@@ -0,0 +1,233 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import path from "node:path";
3
+ import { CsrE2eHarness } from "./csrE2eHarness.fixture";
4
+
5
+ const TAB_A = "/e2e/stack/tab-a";
6
+ const workspaceRoot = path.resolve(import.meta.dir, "../../../..");
7
+ const probeFile = path.join(workspaceRoot, "apps/minimal/ui/StackProbe.tsx");
8
+ const tabAFile = path.join(workspaceRoot, "apps/minimal/page/(home)/e2e/stack/tab-a.tsx");
9
+ const tabBFile = path.join(workspaceRoot, "apps/minimal/page/(home)/e2e/stack/tab-b.tsx");
10
+ const itemFile = path.join(workspaceRoot, "apps/minimal/page/(home)/e2e/stack/item.tsx");
11
+ const layoutFile = path.join(workspaceRoot, "apps/minimal/page/(home)/e2e/_layout.tsx");
12
+ const storeFile = path.join(workspaceRoot, "apps/minimal/lib/_minimal/minimal.store.ts");
13
+ const port = Number(process.env.AKAN_CSR_E2E_FAST_REFRESH_PORT ?? 8491);
14
+
15
+ interface CsrWindow {
16
+ __akan?: { generation: number; inspect(): { modules: number } };
17
+ __AKAN_CSR_LAST_UPDATE__?: { generation: number; executed: string[] };
18
+ }
19
+
20
+ const markProbe = (source: string) =>
21
+ source.replace("data-e2e-probe={name}>", 'data-e2e-probe={name} data-e2e-hot="1">');
22
+ const ownSources = (executed: string[] = []) => executed.filter((id) => !id.includes("/.akan/generated/"));
23
+
24
+ describe.skipIf(!CsrE2eHarness.enabled)("CSR Fast Refresh (minimal, AKAN_DEV_CSR=registry)", () => {
25
+ let csr: CsrE2eHarness;
26
+
27
+ beforeAll(async () => {
28
+ csr = await CsrE2eHarness.start({ app: "minimal", port, env: { AKAN_DEV_CSR: "registry" } });
29
+ }, 240_000);
30
+
31
+ afterAll(async () => {
32
+ // biome-ignore lint/suspicious/noUnnecessaryConditions: unassigned when beforeAll threw before the harness started
33
+ await csr?.close();
34
+ }, 60_000);
35
+
36
+ const lastUpdate = async () =>
37
+ await csr.evaluate(() => (window as unknown as CsrWindow).__AKAN_CSR_LAST_UPDATE__ ?? null);
38
+ const generation = async () => await csr.evaluate(() => (window as unknown as CsrWindow).__akan?.generation);
39
+ const generationPasses = async (previous: number) =>
40
+ await csr.waitFor((last: number) => ((window as unknown as CsrWindow).__akan?.generation ?? 0) > last, {
41
+ args: [previous],
42
+ timeout: 20_000,
43
+ });
44
+ const inputValue = async () =>
45
+ await csr.evaluate(
46
+ () => (document.querySelector('[data-e2e-probe="tab-a"] input') as HTMLInputElement | null)?.value,
47
+ );
48
+ const tabAItemIdIs = async (itemId: string) =>
49
+ await csr.waitFor(
50
+ (expected: string) =>
51
+ document.querySelector('[data-e2e-probe="tab-a"] [data-e2e="item-id"]')?.textContent === expected,
52
+ { args: [itemId], timeout: 30_000 },
53
+ );
54
+
55
+ test("the page boots from the registry dev bundle", async () => {
56
+ await csr.open(TAB_A);
57
+ const booted = await csr.evaluate(() => {
58
+ const runtime = (window as unknown as CsrWindow).__akan;
59
+ return { generation: runtime?.generation, modules: runtime?.inspect().modules ?? 0 };
60
+ });
61
+ expect(booted.generation).toBeGreaterThan(0);
62
+ expect(booted.modules).toBeGreaterThan(100);
63
+ }, 60_000);
64
+
65
+ test("a component edit applies in place, keeping hook state and the DOM", async () => {
66
+ await csr.open(TAB_A);
67
+ await csr.type('[data-e2e-probe="tab-a"] input', "typed");
68
+ const mountId = (await csr.probe("tab-a"))?.mountId;
69
+ await csr.editSource(probeFile, markProbe, async () => {
70
+ await csr.waitFor(() => document.querySelector('[data-e2e-hot="1"]') !== null, { timeout: 20_000 });
71
+ expect(await csr.reloaded()).toBe(false);
72
+ expect((await csr.probe("tab-a"))?.mountId).toBe(mountId);
73
+ expect(await inputValue()).toBe("typed");
74
+ expect((await lastUpdate())?.executed).toEqual(["apps/minimal/ui/StackProbe.tsx"]);
75
+ });
76
+ await csr.waitFor(() => document.querySelector('[data-e2e-hot="1"]') === null, { timeout: 20_000 });
77
+ expect(await csr.reloaded()).toBe(false);
78
+ }, 90_000);
79
+
80
+ test("a store edit re-runs the store root and nothing above it", async () => {
81
+ await csr.open(TAB_A);
82
+ await csr.type('[data-e2e-probe="tab-a"] input', "kept");
83
+ const before = (await generation()) ?? 0;
84
+ const edited = await csr.editSource(
85
+ storeFile,
86
+ (source) => source.replace(" // action\n", " // action\n touchForE2e() {\n this.set({});\n }\n"),
87
+ async () => {
88
+ await generationPasses(before);
89
+ expect(await csr.reloaded()).toBe(false);
90
+ expect(((await lastUpdate())?.executed ?? []).sort()).toEqual([
91
+ "apps/minimal/lib/_minimal/minimal.store.ts",
92
+ "apps/minimal/lib/st.ts",
93
+ ]);
94
+ expect(await inputValue()).toBe("kept");
95
+ return (await generation()) ?? 0;
96
+ },
97
+ );
98
+ await generationPasses(edited);
99
+ }, 90_000);
100
+
101
+ test("a build error shows the overlay without reloading, and the fix applies in place", async () => {
102
+ await csr.open(TAB_A);
103
+ await csr.editSource(
104
+ probeFile,
105
+ (source) => source.replace(" return (\n", " return ((\n"),
106
+ async () => {
107
+ await csr.waitFor(() => document.querySelector('.__akan_hmr_overlay[data-status="error"]') !== null, {
108
+ timeout: 20_000,
109
+ });
110
+ expect(await csr.reloaded()).toBe(false);
111
+ },
112
+ );
113
+ await csr.editSource(probeFile, markProbe, async () => {
114
+ await csr.waitFor(() => document.querySelector('[data-e2e-hot="1"]') !== null, { timeout: 20_000 });
115
+ expect(await csr.reloaded()).toBe(false);
116
+ });
117
+ await csr.waitFor(() => document.querySelector('[data-e2e-hot="1"]') === null, { timeout: 20_000 });
118
+ }, 90_000);
119
+
120
+ test("a page edit swaps the route in place, keeping the page's state", async () => {
121
+ await csr.open(TAB_A);
122
+ await csr.type('[data-e2e-probe="tab-a"] input', "kept");
123
+ const mountId = (await csr.probe("tab-a"))?.mountId;
124
+ await csr.editSource(
125
+ tabAFile,
126
+ (source) => source.replace('name="tab-a"', 'name="tab-a" itemId="edited"'),
127
+ async () => {
128
+ await tabAItemIdIs("edited");
129
+ expect(await csr.reloaded()).toBe(false);
130
+ expect((await csr.probe("tab-a"))?.mountId).toBe(mountId);
131
+ expect(await inputValue()).toBe("kept");
132
+ expect((await lastUpdate())?.executed).toEqual(["apps/minimal/page/(home)/e2e/stack/tab-a.tsx"]);
133
+ },
134
+ );
135
+ await tabAItemIdIs("");
136
+ expect(await csr.reloaded()).toBe(false);
137
+ }, 90_000);
138
+
139
+ test("an edit to another page leaves the current page's render alone", async () => {
140
+ await csr.open(TAB_A);
141
+ const before = (await generation()) ?? 0;
142
+ const renders = (await csr.probe("tab-a"))?.renders;
143
+ const edited = await csr.editSource(
144
+ tabBFile,
145
+ (source) => source.replace('name="tab-b"', 'name="tab-b" itemId="edited"'),
146
+ async () => {
147
+ await generationPasses(before);
148
+ await Bun.sleep(300);
149
+ expect((await lastUpdate())?.executed).toEqual(["apps/minimal/page/(home)/e2e/stack/tab-b.tsx"]);
150
+ expect((await csr.probe("tab-a"))?.renders).toBe(renders);
151
+ expect(await csr.reloaded()).toBe(false);
152
+ return (await generation()) ?? 0;
153
+ },
154
+ );
155
+ await generationPasses(edited);
156
+ }, 90_000);
157
+
158
+ test("a layout edit swaps the layout in place, keeping the page under it", async () => {
159
+ await csr.open(TAB_A);
160
+ await csr.type('[data-e2e-probe="tab-a"] input', "kept");
161
+ const mountId = (await csr.probe("tab-a"))?.mountId;
162
+ await csr.editSource(
163
+ layoutFile,
164
+ (source) => source.replace("<>{children}</>", '<>{children}<span data-e2e-layout="edited" /></>'),
165
+ async () => {
166
+ await csr.waitFor(() => document.querySelector('[data-e2e-layout="edited"]') !== null, { timeout: 30_000 });
167
+ expect(await csr.reloaded()).toBe(false);
168
+ expect((await csr.probe("tab-a"))?.mountId).toBe(mountId);
169
+ expect(await inputValue()).toBe("kept");
170
+ //? A layout reaches the entry through the root-layout wrapper route discovery generates around it.
171
+ expect(ownSources((await lastUpdate())?.executed)).toEqual(["apps/minimal/page/(home)/e2e/_layout.tsx"]);
172
+ },
173
+ );
174
+ await csr.waitFor(() => document.querySelector('[data-e2e-layout="edited"]') === null, { timeout: 30_000 });
175
+ expect(await csr.reloaded()).toBe(false);
176
+ }, 90_000);
177
+
178
+ test("a page edit keeps the stack: the page it covers comes back with its state", async () => {
179
+ await csr.open(TAB_A);
180
+ await csr.type('[data-e2e-probe="tab-a"] input', "under");
181
+ const mountId = (await csr.probe("tab-a"))?.mountId;
182
+ await csr.navigate("/e2e/stack/item?id=1");
183
+ const currentItemId = async () =>
184
+ await csr.text('[id^="pageContainer-"]:not([aria-hidden="true"]) [data-e2e-probe="item"] [data-e2e="item-id"]');
185
+ await csr.editSource(
186
+ itemFile,
187
+ (source) => source.replace('itemId={id ?? ""}', 'itemId={"edited-" + (id ?? "")}'),
188
+ async () => {
189
+ await csr.waitFor(
190
+ () =>
191
+ [...document.querySelectorAll('[data-e2e-probe="item"] [data-e2e="item-id"]')].some(
192
+ (element) => element.textContent === "edited-1",
193
+ ),
194
+ { timeout: 30_000 },
195
+ );
196
+ expect(await currentItemId()).toEqual(["edited-1"]);
197
+ expect((await lastUpdate())?.executed).toEqual(["apps/minimal/page/(home)/e2e/stack/item.tsx"]);
198
+ },
199
+ );
200
+ await csr.waitFor(
201
+ () =>
202
+ [...document.querySelectorAll('[data-e2e-probe="item"] [data-e2e="item-id"]')].some(
203
+ (element) => element.textContent === "1",
204
+ ),
205
+ { timeout: 30_000 },
206
+ );
207
+ await csr.back();
208
+ expect(await inputValue()).toBe("under");
209
+ expect((await csr.probe("tab-a"))?.mountId).toBe(mountId);
210
+ expect(await csr.reloaded()).toBe(false);
211
+ }, 90_000);
212
+ });
213
+
214
+ describe.skipIf(!CsrE2eHarness.enabled)("CSR single-file artifact (minimal, AKAN_DEV_CSR=artifact)", () => {
215
+ let csr: CsrE2eHarness;
216
+
217
+ beforeAll(async () => {
218
+ csr = await CsrE2eHarness.start({ app: "minimal", port, env: { AKAN_DEV_CSR: "artifact" } });
219
+ }, 240_000);
220
+
221
+ afterAll(async () => {
222
+ // biome-ignore lint/suspicious/noUnnecessaryConditions: unassigned when beforeAll threw before the harness started
223
+ await csr?.close();
224
+ }, 60_000);
225
+
226
+ test("a component edit reloads the tab onto the rebuilt artifact", async () => {
227
+ await csr.open(TAB_A);
228
+ await csr.editSource(probeFile, markProbe, async () => {
229
+ await csr.waitFor(() => document.querySelector('[data-e2e-hot="1"]') !== null, { timeout: 30_000 });
230
+ expect(await csr.reloaded()).toBe(true);
231
+ });
232
+ }, 90_000);
233
+ });
@@ -0,0 +1,252 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { CsrE2eHarness } from "./csrE2eHarness.fixture";
3
+
4
+ const TAB_A = "/e2e/stack/tab-a";
5
+ const TAB_B = "/e2e/stack/tab-b";
6
+ const item = (id: string) => `/e2e/stack/item?id=${id}`;
7
+
8
+ const routeOf = (pathWithSearch: string) => pathWithSearch.split("?")[0].replace(/^\/[^/]+/, "/:lang");
9
+ const CURRENT = '[id^="pageContainer-"]:not([aria-hidden="true"])';
10
+ const UNDER = '[id^="pageContainer-"][aria-hidden="true"]';
11
+
12
+ describe.skipIf(!CsrE2eHarness.enabled)("CSR page stack (minimal, /e2e/stack)", () => {
13
+ let csr: CsrE2eHarness;
14
+
15
+ beforeAll(async () => {
16
+ csr = await CsrE2eHarness.start({ app: "minimal" });
17
+ }, 240_000);
18
+
19
+ afterAll(async () => {
20
+ // biome-ignore lint/suspicious/noUnnecessaryConditions: unassigned when beforeAll threw before the harness started
21
+ await csr?.close();
22
+ }, 60_000);
23
+
24
+ const itemIdsIn = async (scope: string, options?: { mounted?: boolean }) =>
25
+ await csr.text(`${scope} [data-e2e-probe="item"] [data-e2e="item-id"]`, options);
26
+ const currentInput = `${CURRENT} [data-e2e-probe="item"] [data-e2e="input"]`;
27
+
28
+ test("an entry of the same route is its own page, and the one it covers waits under it with its state", async () => {
29
+ await csr.open(TAB_A);
30
+ await csr.navigate(item("1"));
31
+ await csr.type(currentInput, "first");
32
+ await csr.navigate(item("2"));
33
+ expect(await itemIdsIn(CURRENT)).toEqual(["2"]);
34
+ expect(await itemIdsIn(UNDER)).toEqual(["1"]);
35
+ expect(await csr.values(currentInput)).toEqual([""]);
36
+ await csr.back();
37
+ expect(await itemIdsIn(CURRENT)).toEqual(["1"]);
38
+ expect(await csr.values(currentInput)).toEqual(["first"]);
39
+ expect(await csr.reloaded()).toBe(false);
40
+ }, 60_000);
41
+
42
+ test("the stack keeps three hidden entries under the previous one, releases older ones, and reveals each on back", async () => {
43
+ await csr.open(TAB_A);
44
+ for (const id of ["11", "12", "13"]) await csr.navigate(item(id));
45
+ await csr.type(currentInput, "thirteen");
46
+ for (const id of ["14", "15", "16"]) await csr.navigate(item(id));
47
+ expect((await itemIdsIn('[id^="pageContainer-"]', { mounted: true })).sort()).toEqual([
48
+ "12",
49
+ "13",
50
+ "14",
51
+ "15",
52
+ "16",
53
+ ]);
54
+ for (const expected of ["15", "14", "13"]) {
55
+ await csr.back();
56
+ expect(await itemIdsIn(CURRENT)).toEqual([expected]);
57
+ }
58
+ expect(await csr.values(currentInput)).toEqual(["thirteen"]);
59
+ await csr.back();
60
+ await csr.back();
61
+ expect(await itemIdsIn(CURRENT)).toEqual(["11"]);
62
+ expect(await csr.values(currentInput)).toEqual([""]);
63
+ expect(await csr.reloaded()).toBe(false);
64
+ }, 90_000);
65
+
66
+ test("a replace within one route updates the page in place", async () => {
67
+ await csr.open(item("21"));
68
+ await csr.type(currentInput, "kept");
69
+ await csr.navigate(item("22"), "replace");
70
+ expect(await itemIdsIn(CURRENT)).toEqual(["22"]);
71
+ expect(await csr.values(currentInput)).toEqual(["kept"]);
72
+ expect((await csr.containers()).filter((container) => container.path.endsWith("/item"))).toHaveLength(1);
73
+ }, 60_000);
74
+
75
+ test("the page the session opened on is cached like any other cached page", async () => {
76
+ await csr.open(TAB_A);
77
+ const mountedAs = (await csr.probe("tab-a"))?.mountId;
78
+ await csr.navigate(TAB_B);
79
+ await csr.navigate(item("3"));
80
+ await csr.navigate(TAB_A);
81
+ expect((await csr.probe("tab-a"))?.mountId).toBe(mountedAs);
82
+ expect(await csr.reloaded()).toBe(false);
83
+ }, 60_000);
84
+
85
+ test("a cached page that is not visible pauses its effects and resumes them with its state", async () => {
86
+ await csr.open(TAB_B);
87
+ await csr.navigate(TAB_A);
88
+ const mountedAs = (await csr.probe("tab-a"))?.mountId;
89
+ await csr.navigate(TAB_B);
90
+ await csr.navigate(item("4"));
91
+ expect((await csr.containers()).find((container) => container.path.endsWith("/tab-a"))?.hidden).toBe(true);
92
+ expect(await csr.ticksOver("tab-a")).toBe(0);
93
+ await csr.navigate(TAB_A);
94
+ expect((await csr.probe("tab-a"))?.mountId).toBe(mountedAs);
95
+ expect(await csr.ticksOver("tab-a")).toBeGreaterThan(0);
96
+ }, 60_000);
97
+
98
+ test("the page a transition-less switch left pauses at once, and runs again when switched back", async () => {
99
+ await csr.open(TAB_A);
100
+ const mountedAs = (await csr.probe("tab-a"))?.mountId;
101
+ await csr.navigate(TAB_B);
102
+ expect(await csr.ticksOver("tab-a")).toBe(0);
103
+ await csr.navigate(TAB_A);
104
+ expect((await csr.probe("tab-a"))?.mountId).toBe(mountedAs);
105
+ expect(await csr.ticksOver("tab-a")).toBeGreaterThan(0);
106
+ }, 60_000);
107
+
108
+ test("every page container but the current one is inert and hidden from assistive tech", async () => {
109
+ await csr.open(TAB_A);
110
+ await csr.navigate(TAB_B);
111
+ await csr.navigate(item("5"));
112
+ const current = routeOf(await csr.currentPath());
113
+ const containers = await csr.containers();
114
+ expect(containers.filter((container) => !container.inert).map((container) => container.path)).toEqual([current]);
115
+ expect(containers.every((container) => container.inert === container.ariaHidden)).toBe(true);
116
+ }, 60_000);
117
+
118
+ test("only the current page offers the agent its tools, not the page kept under it for a swipe back", async () => {
119
+ await csr.open(TAB_A);
120
+ expect(await csr.agentTools()).toContain("bumpTabA");
121
+ await csr.navigate(item("7"));
122
+ const tools = await csr.agentTools();
123
+ expect(tools).toContain("bumpItem");
124
+ expect(tools).not.toContain("bumpTabA");
125
+ expect(await csr.ticksOver("tab-a")).toBeGreaterThan(0);
126
+ await csr.back();
127
+ const back = await csr.agentTools();
128
+ expect(back).toContain("bumpTabA");
129
+ expect(back).not.toContain("bumpItem");
130
+ }, 60_000);
131
+
132
+ test("a page is focused while current and settled; the page under it stays live but unfocused", async () => {
133
+ await csr.open(TAB_A);
134
+ expect((await csr.probe("tab-a"))?.focused).toBe(true);
135
+ const focusCount = (await csr.probe("tab-a"))?.focusCount ?? 0;
136
+ await csr.navigate(item("8"));
137
+ expect(await csr.probe("tab-a")).toMatchObject({ activity: "prev", focused: false, mounted: true });
138
+ expect(await csr.probe("item")).toMatchObject({ activity: "current", focused: true });
139
+ await csr.back();
140
+ expect(await csr.probe("tab-a")).toMatchObject({ activity: "current", focused: true, focusCount: focusCount + 1 });
141
+ }, 60_000);
142
+
143
+ test("a reload keeps the stack: the page under the current one is back, the rest waits, and back walks it", async () => {
144
+ const runtimeItem = (id: string) => `${item(id)}&csr=true&akanMobileTarget=e2e`;
145
+ await csr.open(TAB_A);
146
+ await csr.navigate(runtimeItem("31"));
147
+ await csr.navigate(runtimeItem("32"));
148
+ await csr.reload();
149
+ expect(await itemIdsIn(CURRENT)).toEqual(["32"]);
150
+ expect(await itemIdsIn(UNDER)).toEqual(["31"]);
151
+ expect((await csr.containers()).some((container) => container.path.endsWith("/tab-a"))).toBe(false);
152
+ //? The first back after a reload mounts the entry under the one it reveals, which a boot left dormant.
153
+ await csr.back();
154
+ await csr.waitForText(`${CURRENT} [data-e2e-probe="item"] [data-e2e="item-id"]`, ["31"]);
155
+ await csr.back();
156
+ expect(routeOf(await csr.currentPath())).toBe("/:lang/e2e/stack/tab-a");
157
+ expect((await csr.probe("tab-a"))?.mounted).toBe(true);
158
+ expect(await csr.reloaded()).toBe(false);
159
+ }, 90_000);
160
+
161
+ test("a popstate several entries away lands on that entry", async () => {
162
+ await csr.open(TAB_A);
163
+ for (const id of ["41", "42", "43"]) await csr.navigate(item(id));
164
+ await csr.go(-2);
165
+ expect(await itemIdsIn(CURRENT)).toEqual(["41"]);
166
+ expect(await csr.reloaded()).toBe(false);
167
+ }, 60_000);
168
+
169
+ test("the page under the current one pauses while the app is in the background", async () => {
170
+ await csr.open(TAB_A);
171
+ await csr.navigate(item("51"));
172
+ expect(await csr.ticksOver("tab-a")).toBeGreaterThan(0);
173
+ await csr.setVisibility("hidden");
174
+ expect(await csr.ticksOver("tab-a")).toBe(0);
175
+ await csr.setVisibility("visible");
176
+ expect(await csr.ticksOver("tab-a")).toBeGreaterThan(0);
177
+ }, 60_000);
178
+
179
+ test("a page that redirects after an await lands on the target, inside the target's own async layout", async () => {
180
+ await csr.open(TAB_B);
181
+ await csr.evaluate((next: string) => {
182
+ window.dispatchEvent(new CustomEvent("akan:sync-navigation", { detail: { href: next, kind: "push" } }));
183
+ }, "/e2e/stack/redirect?delay=120");
184
+ await csr.waitFor((pathname: string) => location.pathname.endsWith(pathname), {
185
+ args: ["/e2e/tabbed/home"],
186
+ timeout: 10_000,
187
+ });
188
+ await csr.waitFor(() => document.querySelectorAll('[data-e2e="tabbar"]').length === 1, { timeout: 10_000 });
189
+ await csr.waitFor(
190
+ () =>
191
+ (window as unknown as { __akanE2eProbes?: Record<string, { mounted?: boolean }> }).__akanE2eProbes?.home
192
+ ?.mounted,
193
+ { timeout: 10_000 },
194
+ );
195
+ expect(await csr.probe("home")).toMatchObject({ activity: "current", mounted: true });
196
+ expect(await csr.reloaded()).toBe(false);
197
+ }, 60_000);
198
+
199
+ test("an async render that throws is logged with its route instead of leaving a blank page in silence", async () => {
200
+ await csr.open("/e2e/stack/broken");
201
+ await csr.waitFor(() => true);
202
+ await Bun.sleep(500);
203
+ expect(
204
+ csr.consoleMessages().some((line) => /render of page \d+ of \S*\/e2e\/stack\/broken failed/.test(line)),
205
+ ).toBe(true);
206
+ }, 60_000);
207
+
208
+ test("the memory frame trace records navigation without writing it to the console", async () => {
209
+ await csr.open(TAB_A);
210
+ await csr.evaluate(() => localStorage.setItem("akan:debug:frame", "memory"));
211
+ try {
212
+ await csr.open(TAB_A);
213
+ await csr.navigate(item("71"));
214
+ const events = await csr.evaluate(() =>
215
+ ((window as { __AKAN_FRAME_TRACE__?: { event: string }[] }).__AKAN_FRAME_TRACE__ ?? []).map(
216
+ (entry) => entry.event,
217
+ ),
218
+ );
219
+ expect(events).toContain("router.push");
220
+ expect(events).toContain("navigation.commit");
221
+ expect(csr.consoleMessages().some((line) => line.includes("[akan:frame:"))).toBe(false);
222
+ } finally {
223
+ await csr.evaluate(() => localStorage.removeItem("akan:debug:frame"));
224
+ }
225
+ }, 60_000);
226
+
227
+ test("a cached page whose layout redirected renders that layout again when it is back on screen", async () => {
228
+ await csr.evaluate(() => {
229
+ (globalThis as { __e2eSignedIn?: boolean }).__e2eSignedIn = false;
230
+ });
231
+ await csr.open("/e2e/gate/home");
232
+ await csr.waitFor((pathname: string) => location.pathname.endsWith(pathname), {
233
+ args: [TAB_B],
234
+ timeout: 10_000,
235
+ });
236
+ await csr.evaluate(() => {
237
+ (globalThis as { __e2eSignedIn?: boolean }).__e2eSignedIn = true;
238
+ });
239
+ await csr.navigate("/e2e/gate/home", "replace");
240
+ await csr.waitFor(() => document.querySelectorAll('[data-e2e="gate-tabbar"]').length === 1, { timeout: 10_000 });
241
+ expect(await csr.probe("gate-home")).toMatchObject({ activity: "current", mounted: true });
242
+ }, 60_000);
243
+
244
+ test("back returns to the previous entry without a reload", async () => {
245
+ await csr.open(TAB_A);
246
+ await csr.navigate(item("6"));
247
+ await csr.back();
248
+ expect(routeOf(await csr.currentPath())).toBe("/:lang/e2e/stack/tab-a");
249
+ expect(await csr.text('[data-e2e-probe="tab-a"] [data-e2e="item-id"]')).toHaveLength(1);
250
+ expect(await csr.reloaded()).toBe(false);
251
+ }, 60_000);
252
+ });
@@ -0,0 +1,87 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import path from "node:path";
3
+ import { CsrE2eHarness } from "./csrE2eHarness.fixture";
4
+
5
+ const workspaceRoot = path.resolve(import.meta.dir, "../../../..");
6
+ const probeFile = path.join(workspaceRoot, "apps/minimal/ui/StackProbe.tsx");
7
+ const port = Number(process.env.AKAN_CSR_E2E_PATCH_FIRST_PORT ?? 8493);
8
+
9
+ interface CsrWindow {
10
+ __akan?: { generation: number };
11
+ }
12
+
13
+ //? AKAN_CSR_DEV_APP_WRITE_DELAY_MS holds app.js back after the patch is announced, so a tab can boot inside the gap.
14
+ describe.skipIf(!CsrE2eHarness.enabled)("CSR patch before app.js (minimal, app.js written 1.5s late)", () => {
15
+ let csr: CsrE2eHarness | null = null;
16
+ const harness = () => {
17
+ if (!csr) throw new Error("[csr-e2e] the harness did not start");
18
+ return csr;
19
+ };
20
+
21
+ beforeAll(async () => {
22
+ csr = await CsrE2eHarness.start({
23
+ app: "minimal",
24
+ port,
25
+ workspaceRoot,
26
+ env: { AKAN_CSR_DEV_APP_WRITE_DELAY_MS: "1500" },
27
+ });
28
+ }, 240_000);
29
+
30
+ afterAll(async () => {
31
+ await csr?.close();
32
+ }, 60_000);
33
+
34
+ const generation = async () =>
35
+ await harness().evaluate(() => (window as unknown as CsrWindow).__akan?.generation ?? 0);
36
+ const hot = async () =>
37
+ await harness().evaluate(
38
+ () => document.querySelector('[data-e2e-probe="tab-a"]')?.getAttribute("data-e2e-hot") ?? null,
39
+ );
40
+
41
+ test("an open tab takes the patch before app.js is rewritten", async () => {
42
+ await harness().open("/e2e/stack/tab-a");
43
+ const before = await generation();
44
+ await harness().editSource(
45
+ probeFile,
46
+ (source) => source.replace("data-e2e-probe={name}>", 'data-e2e-probe={name} data-e2e-hot="early">'),
47
+ async () => {
48
+ const startedAt = Date.now();
49
+ await harness().waitFor(
50
+ () => document.querySelector('[data-e2e-probe="tab-a"]')?.getAttribute("data-e2e-hot") === "early",
51
+ { timeout: 20_000 },
52
+ );
53
+ expect(Date.now() - startedAt).toBeLessThan(1_500);
54
+ expect(await generation()).toBeGreaterThan(before);
55
+ },
56
+ );
57
+ expect(await harness().reloaded()).toBe(false);
58
+ }, 120_000);
59
+
60
+ test("a tab booting between the patch and app.js starts at the new generation with no second reload", async () => {
61
+ await harness().open("/e2e/stack/tab-a");
62
+ //? The previous test's restore may still be on its way: wait until the page shows it.
63
+ await harness().waitFor(
64
+ () => document.querySelector('[data-e2e-probe="tab-a"]')?.getAttribute("data-e2e-hot") === null,
65
+ { timeout: 20_000 },
66
+ );
67
+ await harness().editSource(
68
+ probeFile,
69
+ (source) => source.replace("data-e2e-probe={name}>", 'data-e2e-probe={name} data-e2e-hot="booted">'),
70
+ async () => {
71
+ await harness().waitFor(
72
+ () => document.querySelector('[data-e2e-probe="tab-a"]')?.getAttribute("data-e2e-hot") === "booted",
73
+ { timeout: 20_000 },
74
+ );
75
+ const target = await generation();
76
+ //? The first document after the reload: a second reload inside the harness's settle would take the marker.
77
+ const booted = await harness().reload();
78
+ expect(await generation()).toBe(target);
79
+ expect(await hot()).toBe("booted");
80
+ await Bun.sleep(2_000);
81
+ expect(await harness().reloaded()).toBe(false);
82
+ expect(await harness().evaluate(() => performance.timeOrigin)).toBe(booted);
83
+ expect(await generation()).toBe(target);
84
+ },
85
+ );
86
+ }, 120_000);
87
+ });