@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.
- package/DEV_RUNTIME_KNOBS.md +44 -26
- package/README.ko.md +21 -7
- package/README.md +17 -8
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +6 -8
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +56 -3
- package/akanApp/akanApp.host.ts +218 -278
- package/akanApp/devBootLatch.test.ts +39 -0
- package/akanApp/devBootLatch.ts +39 -0
- package/akanApp/devHostPolicy.test.ts +81 -35
- package/akanApp/devHostPolicy.ts +41 -102
- package/akanConfig/akanBin.test.ts +62 -0
- package/akanConfig/akanBin.ts +78 -0
- package/akanConfig/akanConfig.test.ts +439 -141
- package/akanConfig/akanConfig.ts +359 -275
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +22 -8
- package/akanContext.ts +110 -160
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +41 -5
- package/applicationBuildRunner.ts +71 -63
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +32 -30
- package/artifact/implicitRootLayout.ts +58 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +35 -2
- package/biomeBase.ts +2 -5
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +77 -39
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +26 -5
- package/cloud/globalConfig.ts +71 -32
- package/codeAgent/agent/AkanCodeServices.ts +97 -0
- package/codeAgent/agent/AkanContextFiles.ts +94 -0
- package/codeAgent/agent/CodeAgent.ts +551 -0
- package/codeAgent/agent/CodeAgentAsks.ts +66 -0
- package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
- package/codeAgent/agent/CodeAgentGate.ts +93 -0
- package/codeAgent/agent/CodeAgentProxy.ts +82 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
- package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
- package/codeAgent/agent/CodeAgentUi.ts +47 -0
- package/codeAgent/agent/CodeMailbox.ts +148 -0
- package/codeAgent/agent/CodeSessionFork.ts +56 -0
- package/codeAgent/agent/CodeSessionIndex.ts +78 -0
- package/codeAgent/agent/SubagentPool.ts +214 -0
- package/codeAgent/agent/akanCodeModel.ts +47 -0
- package/codeAgent/agent/akanCodePaths.ts +31 -0
- package/codeAgent/agent/akanContextFiles.test.ts +114 -0
- package/codeAgent/agent/akanSystemPrompt.ts +46 -0
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +523 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
- package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
- package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
- package/codeAgent/agent/codeMailbox.test.ts +76 -0
- package/codeAgent/agent/codeSessionFork.test.ts +58 -0
- package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
- package/codeAgent/agent/consoleToStderr.ts +8 -0
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +82 -0
- package/codeAgent/feedback/PreviewChrome.ts +82 -0
- package/codeAgent/feedback/PreviewFeedback.ts +46 -0
- package/codeAgent/feedback/PreviewView.ts +95 -0
- package/codeAgent/feedback/TurnFeedback.ts +98 -0
- package/codeAgent/feedback/VerifyFeedback.ts +40 -0
- package/codeAgent/feedback/previewChrome.test.ts +73 -0
- package/codeAgent/index.ts +34 -0
- package/codeAgent/skills/akan-code/SKILL.md +130 -0
- package/codeAgent/skills/akan-debug/SKILL.md +42 -0
- package/codeAgent/skills/akan-deps/SKILL.md +50 -0
- package/codeAgent/skills/akan-err/SKILL.md +39 -0
- package/codeAgent/skills/akan-module/SKILL.md +45 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
- package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
- package/codeAgent/skills/akan-service/SKILL.md +34 -0
- package/codeAgent/skills/akan-store/SKILL.md +67 -0
- package/codeAgent/skills/akan-ui/SKILL.md +63 -0
- package/codeAgent/skills/akan-validate/SKILL.md +44 -0
- package/codeAgent/tools/AkanCli.ts +50 -0
- package/codeAgent/tools/AkanCodePlugins.ts +136 -0
- package/codeAgent/tools/AkanEditScope.ts +48 -0
- package/codeAgent/tools/AkanEnvKeys.ts +50 -0
- package/codeAgent/tools/AkanToolPack.ts +92 -0
- package/codeAgent/tools/AkanVerifier.ts +48 -0
- package/codeAgent/tools/AskToolPack.ts +99 -0
- package/codeAgent/tools/MailToolPack.ts +76 -0
- package/codeAgent/tools/McpClient.ts +187 -0
- package/codeAgent/tools/McpOAuth.ts +233 -0
- package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
- package/codeAgent/tools/McpServerConfig.ts +129 -0
- package/codeAgent/tools/McpSignIn.ts +158 -0
- package/codeAgent/tools/McpTokenStore.ts +81 -0
- package/codeAgent/tools/McpToolPack.ts +199 -0
- package/codeAgent/tools/SessionToolPack.ts +91 -0
- package/codeAgent/tools/WebToolPack.ts +147 -0
- package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
- package/codeAgent/tools/askToolPack.test.ts +116 -0
- package/codeAgent/tools/mcpOAuth.test.ts +612 -0
- package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +46 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +2 -10
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +68 -145
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/csrE2e/csrE2eHarness.fixture.ts +505 -0
- package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
- package/csrE2e/csrPageStack.e2e.test.ts +252 -0
- package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
- package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
- package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
- package/csrE2e/registryVendor.e2e.test.ts +221 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +31 -80
- package/devkitUtils.test.ts +48 -20
- package/executors.linkFallback.test.ts +103 -0
- package/executors.test.ts +176 -106
- package/executors.ts +212 -498
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +22 -9
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +27 -38
- package/frontendBuild/autoImportSync.ts +31 -65
- package/frontendBuild/buildRouteClient.test.ts +123 -37
- package/frontendBuild/bundleDefine.test.ts +12 -0
- package/frontendBuild/bundleDefine.ts +16 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
- package/frontendBuild/clientEntryDiscovery.ts +122 -110
- package/frontendBuild/csrArtifactBuilder.ts +9 -31
- package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
- package/frontendBuild/csrDevArtifactWriter.ts +307 -0
- package/frontendBuild/csrDevBundler.test.ts +66 -0
- package/frontendBuild/csrDevBundler.ts +95 -0
- package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
- package/frontendBuild/csrDevFactoryCheck.ts +39 -0
- package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
- package/frontendBuild/csrDevModuleCompiler.ts +422 -0
- package/frontendBuild/csrDevPatcher.test.ts +572 -0
- package/frontendBuild/csrDevPatcher.ts +548 -0
- package/frontendBuild/csrDevPaths.ts +145 -0
- package/frontendBuild/csrDevResolver.test.ts +239 -0
- package/frontendBuild/csrDevResolver.ts +363 -0
- package/frontendBuild/csrDevTypes.ts +61 -0
- package/frontendBuild/csrEntryFiles.ts +24 -0
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +31 -82
- package/frontendBuild/cssImportResolver.ts +6 -19
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/devRegistryBundler.ts +192 -0
- package/frontendBuild/fontOptimizer.test.ts +6 -14
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +57 -70
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +59 -32
- package/frontendBuild/hmrWatcher.ts +31 -74
- package/frontendBuild/index.ts +4 -0
- package/frontendBuild/pagesBundleBuilder.ts +28 -30
- package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
- package/frontendBuild/registryMetadataFingerprint.ts +28 -0
- package/frontendBuild/routeClientBuilder.ts +100 -62
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/serverGraphFile.test.ts +77 -0
- package/frontendBuild/serverGraphFile.ts +93 -0
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
- package/frontendBuild/ssrDevBundler.ts +85 -0
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +147 -63
- package/incrementalBuilder/buildBatchProtocol.ts +13 -38
- package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
- package/incrementalBuilder/buildBatchRunner.ts +26 -38
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
- package/incrementalBuilder/builderWorkQueue.ts +126 -0
- package/incrementalBuilder/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
- package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
- package/index.ts +5 -19
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +105 -199
- package/integration/devStabilityHarness.test.ts +13 -28
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
- package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
- package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
- package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
- package/lint/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-document-cookie.grit +29 -0
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/lint/no-web-only-api-outside-webkit.grit +44 -0
- package/lint/no-web-storage.grit +30 -0
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/desktopBin.test.ts +182 -0
- package/mobile/desktopBin.ts +144 -0
- package/mobile/desktopServerStage.test.ts +252 -0
- package/mobile/desktopServerStage.ts +178 -0
- package/mobile/index.ts +9 -0
- package/mobile/mobileTarget.test.ts +57 -8
- package/mobile/mobileTarget.ts +45 -26
- package/mobile/nativeApi.test.ts +30 -0
- package/mobile/nativeApi.ts +40 -0
- package/mobile/nativeApp.test.ts +184 -0
- package/mobile/nativeApp.ts +454 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +438 -0
- package/mobile/nativeConfig.ts +294 -0
- package/mobile/nativePluginFolders.test.ts +81 -0
- package/mobile/nativePluginFolders.ts +60 -0
- package/mobile/nativeWebDir.test.ts +80 -0
- package/mobile/nativeWebDir.ts +66 -0
- package/mobile/placeholderAppId.test.ts +13 -0
- package/mobile/placeholderAppId.ts +16 -0
- package/mobile/usageDescriptions.test.ts +27 -0
- package/mobile/usageDescriptions.ts +24 -0
- package/package.json +11 -11
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +46 -0
- package/platformTest/BunTestReport.test.ts +60 -0
- package/platformTest/BunTestReport.ts +65 -0
- package/platformTest/IdleWatchedProcess.test.ts +56 -0
- package/platformTest/IdleWatchedProcess.ts +73 -0
- package/platformTest/LinuxDockerTarget.ts +105 -0
- package/platformTest/PlatformTestRun.test.ts +64 -0
- package/platformTest/PlatformTestRun.ts +141 -0
- package/platformTest/PlatformTestTarget.ts +142 -0
- package/platformTest/TestSnapshot.test.ts +72 -0
- package/platformTest/TestSnapshot.ts +103 -0
- package/platformTest/WindowsSshTarget.ts +151 -0
- package/prompter.ts +3 -13
- package/qualityScanner.test.ts +9 -26
- package/qualityScanner.ts +200 -286
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.test.ts +0 -1
- package/routeSourceValidator.ts +7 -42
- package/scanInfo.ts +91 -225
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/stripAnsi.ts +3 -0
- package/subspace.test.ts +9 -25
- package/subspace.ts +48 -115
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +194 -0
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +42 -20
- package/transforms/serverEnvPlugin.test.ts +103 -0
- package/transforms/serverEnvPlugin.ts +41 -0
- package/transforms/transforms.test.ts +166 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +6 -23
- package/typeChecker.ts +7 -63
- package/types.ts +2 -0
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +101 -110
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/rolloutGate.test.ts +1 -1
- package/workflow/source.ts +52 -69
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.test.ts +18 -3
- package/workspaceLayout.ts +22 -23
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- package/applicationReleasePackager.ts +0 -209
- package/capacitorApp.test.ts +0 -372
- package/capacitorApp.ts +0 -1648
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/guideline.ts +0 -19
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { CsrE2eHarness } from "./csrE2eHarness.fixture";
|
|
3
|
+
|
|
4
|
+
//* Where a CSR page lands, not only what its console says. CSR's root layout renders the pages in a frame of its own
|
|
5
|
+
//* and hides what it wraps, so a layout the route table puts at the root wraps no page: an app with its root layout
|
|
6
|
+
//* in a route group once rendered nothing, then rendered its dashboard outside the lock screen that wraps it.
|
|
7
|
+
const EXPLORE = "/explore";
|
|
8
|
+
const TAB_A = "/e2e/stack/tab-a";
|
|
9
|
+
//? The web layout draws no bottom chrome, so the `(tab)` layout shows in the route table, not in the text.
|
|
10
|
+
const exploreText = ["Where are you headed today?", "Popular stays right now"];
|
|
11
|
+
|
|
12
|
+
interface RouteTableWindow {
|
|
13
|
+
__akan?: { inspect(): { executed: string[] }; require(id: string): unknown };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
//? A page's element is inside its layout's marker, outside the hidden provider container, and on screen.
|
|
17
|
+
const placement = async (harness: CsrE2eHarness, page: string) =>
|
|
18
|
+
await harness.evaluate((selector: string) => {
|
|
19
|
+
const element = document.querySelector(selector);
|
|
20
|
+
return {
|
|
21
|
+
rendered: element !== null,
|
|
22
|
+
inLayout: element?.closest('[data-e2e="public-layout"]') != null,
|
|
23
|
+
hidden: element?.closest("#csr-provider-children") != null,
|
|
24
|
+
visible: element?.checkVisibility() ?? false,
|
|
25
|
+
};
|
|
26
|
+
}, `[data-e2e="${page}"]`);
|
|
27
|
+
|
|
28
|
+
describe.skipIf(!CsrE2eHarness.enabled)("CSR layouts under a root layout (minimal, (tab))", () => {
|
|
29
|
+
let csr: CsrE2eHarness;
|
|
30
|
+
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
csr = await CsrE2eHarness.start({
|
|
33
|
+
app: "minimal",
|
|
34
|
+
port: Number(process.env.AKAN_CSR_E2E_ROOT_LAYOUT_PORT ?? 8497),
|
|
35
|
+
});
|
|
36
|
+
}, 240_000);
|
|
37
|
+
|
|
38
|
+
afterAll(async () => {
|
|
39
|
+
// biome-ignore lint/suspicious/noUnnecessaryConditions: unassigned when beforeAll threw before the harness started
|
|
40
|
+
await csr?.close();
|
|
41
|
+
}, 60_000);
|
|
42
|
+
|
|
43
|
+
const rendered = async (texts: string[]) =>
|
|
44
|
+
await csr.waitFor(
|
|
45
|
+
(want: string[]) => want.every((text) => (document.getElementById("root")?.innerText ?? "").includes(text)),
|
|
46
|
+
{ args: [texts], timeout: 30_000 },
|
|
47
|
+
);
|
|
48
|
+
//? The table the running bundle built, read through the dev registry: what the router renders from.
|
|
49
|
+
const layerCounts = async () =>
|
|
50
|
+
await csr.evaluate(() => {
|
|
51
|
+
const akan = (window as unknown as RouteTableWindow).__akan;
|
|
52
|
+
const id = akan?.inspect().executed.find((executed) => executed.endsWith("webkit/CsrRouteTable.ts"));
|
|
53
|
+
const table = id
|
|
54
|
+
? (akan?.require(id) as {
|
|
55
|
+
CsrRouteTable: {
|
|
56
|
+
active: {
|
|
57
|
+
snapshot(): { pathRoutes: { path: string; renderRootLayouts: unknown[]; renderLayouts: unknown[] }[] };
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
})
|
|
61
|
+
: null;
|
|
62
|
+
return Object.fromEntries(
|
|
63
|
+
(table?.CsrRouteTable.active.snapshot().pathRoutes ?? []).map((route) => [
|
|
64
|
+
route.path,
|
|
65
|
+
{ root: route.renderRootLayouts.length, layouts: route.renderLayouts.length },
|
|
66
|
+
]),
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("a nested root boundary renders with its pages, not at the root", async () => {
|
|
71
|
+
await csr.open(EXPLORE);
|
|
72
|
+
await rendered(exploreText);
|
|
73
|
+
const counts = await layerCounts();
|
|
74
|
+
expect(counts["/:lang/explore"]?.root).toBe(counts["/:lang/e2e/stack/tab-a"]?.root);
|
|
75
|
+
expect(counts["/:lang/explore"]?.layouts).toBe(1);
|
|
76
|
+
expect(csr.consoleMessages().filter((message) => /no root layout|error occurred/.test(message))).toEqual([]);
|
|
77
|
+
}, 60_000);
|
|
78
|
+
|
|
79
|
+
test("leaving the group and coming back renders each side", async () => {
|
|
80
|
+
await csr.open(EXPLORE);
|
|
81
|
+
await rendered(exploreText);
|
|
82
|
+
await csr.navigate(TAB_A);
|
|
83
|
+
await csr.waitFor(() => document.querySelector('[data-e2e-probe="tab-a"]') !== null, { timeout: 30_000 });
|
|
84
|
+
await csr.back();
|
|
85
|
+
await rendered(exploreText);
|
|
86
|
+
}, 90_000);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe.skipIf(!CsrE2eHarness.enabled)("a root layout in a route group (groupedroot, page/(app)/_layout)", () => {
|
|
90
|
+
let harness: CsrE2eHarness;
|
|
91
|
+
|
|
92
|
+
beforeAll(async () => {
|
|
93
|
+
harness = await CsrE2eHarness.start({
|
|
94
|
+
app: "groupedroot",
|
|
95
|
+
port: Number(process.env.AKAN_CSR_E2E_GROUPED_ROOT_PORT ?? 8498),
|
|
96
|
+
});
|
|
97
|
+
}, 240_000);
|
|
98
|
+
|
|
99
|
+
afterAll(async () => {
|
|
100
|
+
// biome-ignore lint/suspicious/noUnnecessaryConditions: unassigned when beforeAll threw before the harness started
|
|
101
|
+
await harness?.close();
|
|
102
|
+
}, 60_000);
|
|
103
|
+
|
|
104
|
+
const open = async (path: string, page: string, csr: boolean) => {
|
|
105
|
+
await harness.open(path, { csr });
|
|
106
|
+
await harness.waitFor((selector: string) => document.querySelector(selector) !== null, {
|
|
107
|
+
args: [`[data-e2e="${page}"]`],
|
|
108
|
+
timeout: 30_000,
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
test.each([
|
|
113
|
+
["SSR", false],
|
|
114
|
+
["CSR", true],
|
|
115
|
+
] as const)(
|
|
116
|
+
"%s: a page renders inside the (public) layout that wraps it",
|
|
117
|
+
async (_mode, csr) => {
|
|
118
|
+
for (const [path, page] of [
|
|
119
|
+
["/dashboard", "dashboard"],
|
|
120
|
+
["/", "index"],
|
|
121
|
+
] as const) {
|
|
122
|
+
await open(path, page, csr);
|
|
123
|
+
expect(await placement(harness, page)).toEqual({
|
|
124
|
+
rendered: true,
|
|
125
|
+
inLayout: true,
|
|
126
|
+
hidden: false,
|
|
127
|
+
visible: true,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
expect(harness.consoleMessages().filter((message) => /no root layout|error occurred/.test(message))).toEqual([]);
|
|
131
|
+
},
|
|
132
|
+
90_000,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, rename } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { HmrTrace } from "akanjs/server";
|
|
6
|
+
import { CsrE2eHarness } from "./csrE2eHarness.fixture";
|
|
7
|
+
|
|
8
|
+
//? A measurement, not a check: it runs with AKAN_HMR_LATENCY=1 and writes its report to local/csr-e2e.
|
|
9
|
+
const enabled = CsrE2eHarness.enabled && process.env.AKAN_HMR_LATENCY === "1";
|
|
10
|
+
const workspaceRoot = path.resolve(import.meta.dir, "../../../..");
|
|
11
|
+
const port = Number(process.env.AKAN_HMR_LATENCY_PORT ?? 8492);
|
|
12
|
+
const runs = Number(process.env.AKAN_HMR_LATENCY_RUNS ?? 5);
|
|
13
|
+
const probeFile = path.join(workspaceRoot, "apps/minimal/ui/StackProbe.tsx");
|
|
14
|
+
const pageFile = path.join(workspaceRoot, "apps/minimal/page/(home)/e2e/stack/tab-a.tsx");
|
|
15
|
+
const layoutFile = path.join(workspaceRoot, "apps/minimal/page/(home)/e2e/_layout.tsx");
|
|
16
|
+
const probe = `document.querySelector('[data-e2e-probe="tab-a"]')`;
|
|
17
|
+
const input = `document.querySelector('[data-e2e-probe="tab-a"] input')`;
|
|
18
|
+
|
|
19
|
+
interface TraceEntry {
|
|
20
|
+
kind: string;
|
|
21
|
+
generation: number;
|
|
22
|
+
trace: HmrTrace | null;
|
|
23
|
+
receivedAt: number;
|
|
24
|
+
appliedAt: number | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface LatencyWindow {
|
|
28
|
+
__latencyProbe?: { hit: number };
|
|
29
|
+
__latencySeen?: Record<string, number>;
|
|
30
|
+
__latencyObserver?: MutationObserver;
|
|
31
|
+
__AKAN_HMR_TRACES__?: TraceEntry[];
|
|
32
|
+
__AKAN_CSR_LAST_UPDATE__?: { generation: number; appliedAt?: number; refreshedAt?: number };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface Sample {
|
|
36
|
+
total: number;
|
|
37
|
+
stages: Record<string, number>;
|
|
38
|
+
updates: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface Edit {
|
|
42
|
+
file: string;
|
|
43
|
+
transform: (source: string) => string;
|
|
44
|
+
applied: string;
|
|
45
|
+
restored: string;
|
|
46
|
+
write?: (file: string, content: string) => Promise<void>;
|
|
47
|
+
restore?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const median = (values: number[]) => [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)] ?? 0;
|
|
51
|
+
|
|
52
|
+
const summarize = (samples: Sample[]) => {
|
|
53
|
+
const stageNames = [...new Set(samples.flatMap((sample) => Object.keys(sample.stages)))];
|
|
54
|
+
return {
|
|
55
|
+
total: { median: median(samples.map((sample) => sample.total)), samples: samples.map((sample) => sample.total) },
|
|
56
|
+
stages: Object.fromEntries(
|
|
57
|
+
stageNames.map((name) => [
|
|
58
|
+
name,
|
|
59
|
+
median(samples.flatMap((sample) => (name in sample.stages ? [sample.stages[name] ?? 0] : []))),
|
|
60
|
+
]),
|
|
61
|
+
),
|
|
62
|
+
updates: samples.map((sample) => sample.updates),
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
//? Durations between the marks one update collected, in hop order; a mark a path does not reach is skipped.
|
|
67
|
+
const stagesOf = (
|
|
68
|
+
writtenAt: number,
|
|
69
|
+
hit: number,
|
|
70
|
+
entry: TraceEntry | undefined,
|
|
71
|
+
applied: LatencyWindow["__AKAN_CSR_LAST_UPDATE__"],
|
|
72
|
+
) => {
|
|
73
|
+
const trace = entry?.trace ?? {};
|
|
74
|
+
const marks: [string, number | null | undefined][] = [
|
|
75
|
+
["event", trace.eventAt],
|
|
76
|
+
["flush", trace.flushAt],
|
|
77
|
+
["batch", trace.batchAt],
|
|
78
|
+
["spawn", trace.spawnAt],
|
|
79
|
+
["workerStart", trace.workerStartAt],
|
|
80
|
+
["worker", trace.workerAt],
|
|
81
|
+
["patch", trace.patchAt],
|
|
82
|
+
["sent", trace.sentAt],
|
|
83
|
+
["broadcast", trace.broadcastAt],
|
|
84
|
+
["received", entry?.receivedAt],
|
|
85
|
+
["applied", applied?.appliedAt ?? entry?.appliedAt],
|
|
86
|
+
["refreshed", applied?.refreshedAt],
|
|
87
|
+
["dom", hit],
|
|
88
|
+
];
|
|
89
|
+
const stages: Record<string, number> = {};
|
|
90
|
+
let previous = { name: "write", at: writtenAt };
|
|
91
|
+
for (const [name, at] of marks) {
|
|
92
|
+
if (typeof at !== "number" || at < previous.at) continue;
|
|
93
|
+
stages[`${previous.name}→${name}`] = at - previous.at;
|
|
94
|
+
previous = { name, at };
|
|
95
|
+
}
|
|
96
|
+
return stages;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const writeAtomic = async (file: string, content: string) => {
|
|
100
|
+
const temp = `${file}.latency-tmp`;
|
|
101
|
+
await Bun.write(temp, content);
|
|
102
|
+
await rename(temp, file);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
describe.skipIf(!enabled)("dev HMR latency (minimal)", () => {
|
|
106
|
+
let csr: CsrE2eHarness | null = null;
|
|
107
|
+
const harness = () => {
|
|
108
|
+
if (!csr) throw new Error("[hmr-latency] the harness did not start");
|
|
109
|
+
return csr;
|
|
110
|
+
};
|
|
111
|
+
const loadBefore = os.loadavg();
|
|
112
|
+
const report: Record<string, unknown> = {};
|
|
113
|
+
//? The page stack, patch-first and fast-refresh suites edit these files too: a sample that times out mid-edit must not
|
|
114
|
+
//? leave them changed.
|
|
115
|
+
const pristine = new Map<string, string>();
|
|
116
|
+
|
|
117
|
+
beforeAll(async () => {
|
|
118
|
+
for (const file of [probeFile, pageFile, layoutFile]) pristine.set(file, await Bun.file(file).text());
|
|
119
|
+
csr = await CsrE2eHarness.start({ app: "minimal", port, workspaceRoot });
|
|
120
|
+
}, 240_000);
|
|
121
|
+
|
|
122
|
+
afterEach(async () => {
|
|
123
|
+
for (const [file, source] of pristine) if ((await Bun.file(file).text()) !== source) await Bun.write(file, source);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
afterAll(async () => {
|
|
127
|
+
await csr?.close();
|
|
128
|
+
const summary = {
|
|
129
|
+
at: new Date().toISOString(),
|
|
130
|
+
bun: Bun.version,
|
|
131
|
+
platform: `${process.platform}-${process.arch}`,
|
|
132
|
+
cpus: os.cpus().length,
|
|
133
|
+
loadavg: { before: loadBefore, after: os.loadavg() },
|
|
134
|
+
debounceMs: process.env.AKAN_DEV_WATCH_DEBOUNCE_MS ?? "default",
|
|
135
|
+
csrPatcher: process.env.AKAN_DEV_CSR_PATCHER ?? "on",
|
|
136
|
+
backend: process.env.AKAN_CSR_E2E_BACKEND ?? (process.platform === "darwin" ? "webkit" : "chrome"),
|
|
137
|
+
runs,
|
|
138
|
+
...report,
|
|
139
|
+
};
|
|
140
|
+
const dir = path.join(workspaceRoot, "local", "csr-e2e");
|
|
141
|
+
await mkdir(dir, { recursive: true });
|
|
142
|
+
const file = path.join(dir, `hmr-latency-${Date.now()}.json`);
|
|
143
|
+
await Bun.write(file, `${JSON.stringify(summary, null, 2)}\n`);
|
|
144
|
+
console.info(`[hmr-latency] report: ${file}`);
|
|
145
|
+
console.info(JSON.stringify(summary));
|
|
146
|
+
}, 60_000);
|
|
147
|
+
|
|
148
|
+
const armProbe = async (expression: string) =>
|
|
149
|
+
await harness().evaluate((source: string) => {
|
|
150
|
+
const predicate = new Function(`return (${source});`) as () => boolean;
|
|
151
|
+
const state: { hit: number } = { hit: 0 };
|
|
152
|
+
(window as unknown as LatencyWindow).__latencyProbe = state;
|
|
153
|
+
const check = () => {
|
|
154
|
+
if (!state.hit && predicate()) state.hit = Date.now();
|
|
155
|
+
};
|
|
156
|
+
const observer = new MutationObserver(check);
|
|
157
|
+
observer.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
158
|
+
const tick = () => {
|
|
159
|
+
check();
|
|
160
|
+
if (state.hit) observer.disconnect();
|
|
161
|
+
else requestAnimationFrame(tick);
|
|
162
|
+
};
|
|
163
|
+
requestAnimationFrame(tick);
|
|
164
|
+
return true;
|
|
165
|
+
}, expression);
|
|
166
|
+
|
|
167
|
+
const waitForHit = async () =>
|
|
168
|
+
await harness().waitFor(() => ((window as unknown as LatencyWindow).__latencyProbe?.hit ?? 0) > 0, {
|
|
169
|
+
timeout: 30_000,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const timeEdit = async (
|
|
173
|
+
{
|
|
174
|
+
file,
|
|
175
|
+
transform,
|
|
176
|
+
applied,
|
|
177
|
+
restored,
|
|
178
|
+
write = async (target, content) => {
|
|
179
|
+
await Bun.write(target, content);
|
|
180
|
+
},
|
|
181
|
+
restore = true,
|
|
182
|
+
}: Edit,
|
|
183
|
+
kinds: string[],
|
|
184
|
+
) => {
|
|
185
|
+
const original = await Bun.file(file).text();
|
|
186
|
+
await armProbe(applied);
|
|
187
|
+
const writtenAt = Date.now();
|
|
188
|
+
await write(file, transform(original));
|
|
189
|
+
await waitForHit();
|
|
190
|
+
const page = await harness().evaluate(() => {
|
|
191
|
+
const w = window as unknown as LatencyWindow;
|
|
192
|
+
return { hit: w.__latencyProbe?.hit ?? 0, traces: w.__AKAN_HMR_TRACES__ ?? [], last: w.__AKAN_CSR_LAST_UPDATE__ };
|
|
193
|
+
});
|
|
194
|
+
const updates = page.traces.filter((entry) => kinds.includes(entry.kind) && entry.receivedAt >= writtenAt);
|
|
195
|
+
const entry = updates.filter((candidate) => candidate.receivedAt <= page.hit).at(-1);
|
|
196
|
+
const applies = entry && page.last?.generation === entry.generation ? page.last : undefined;
|
|
197
|
+
if (restore) {
|
|
198
|
+
await armProbe(restored);
|
|
199
|
+
await Bun.write(file, original);
|
|
200
|
+
await waitForHit();
|
|
201
|
+
}
|
|
202
|
+
await Bun.sleep(1_500);
|
|
203
|
+
return {
|
|
204
|
+
total: page.hit - writtenAt,
|
|
205
|
+
stages: stagesOf(writtenAt, page.hit, entry, applies),
|
|
206
|
+
updates: updates.length,
|
|
207
|
+
} satisfies Sample;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const componentEdit = (value: string): Edit => ({
|
|
211
|
+
file: probeFile,
|
|
212
|
+
transform: (source) => source.replace("data-e2e-probe={name}>", `data-e2e-probe={name} data-e2e-hot="${value}">`),
|
|
213
|
+
applied: `${probe}?.getAttribute("data-e2e-hot") === "${value}"`,
|
|
214
|
+
restored: `${probe}?.getAttribute("data-e2e-hot") === null`,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const pageEdit = (value: string): Edit => ({
|
|
218
|
+
file: pageFile,
|
|
219
|
+
transform: (source) => source.replace('name="tab-a"', `name="tab-a" itemId="${value}"`),
|
|
220
|
+
applied: `${probe}?.querySelector('[data-e2e="item-id"]')?.textContent === "${value}"`,
|
|
221
|
+
restored: `${probe}?.querySelector('[data-e2e="item-id"]')?.textContent === ""`,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
//? SSR first: the first CSR page arms per-save CSR builds, which would then run ahead of pages in every batch.
|
|
225
|
+
//? Component edits each start from the pristine source and are not undone between samples; the undo to it is timed
|
|
226
|
+
//? once, after them.
|
|
227
|
+
test("SSR tab: component and page edits", async () => {
|
|
228
|
+
const ssrKinds = ["rsc-refresh", "ssr"];
|
|
229
|
+
await harness().open("/e2e/stack/tab-a", { csr: false });
|
|
230
|
+
await Bun.sleep(1_000);
|
|
231
|
+
const component: Sample[] = [];
|
|
232
|
+
const page: Sample[] = [];
|
|
233
|
+
const pristine = await Bun.file(probeFile).text();
|
|
234
|
+
for (let i = 0; i < runs; i += 1) {
|
|
235
|
+
const edit = componentEdit(`s${i}`);
|
|
236
|
+
component.push(await timeEdit({ ...edit, transform: () => edit.transform(pristine), restore: false }, ssrKinds));
|
|
237
|
+
}
|
|
238
|
+
await Bun.write(probeFile, pristine);
|
|
239
|
+
await armProbe(`${probe}?.getAttribute("data-e2e-hot") === null`);
|
|
240
|
+
const undoApplied = await harness()
|
|
241
|
+
.waitFor(() => ((window as unknown as LatencyWindow).__latencyProbe?.hit ?? 0) > 0, { timeout: 5_000 })
|
|
242
|
+
.then(() => true)
|
|
243
|
+
.catch(() => false);
|
|
244
|
+
for (let i = 0; i < runs; i += 1) page.push(await timeEdit(pageEdit(`s${i}`), ssrKinds));
|
|
245
|
+
report.ssr = {
|
|
246
|
+
component: summarize(component),
|
|
247
|
+
page: summarize(page),
|
|
248
|
+
undoApplied,
|
|
249
|
+
reloaded: await harness().reloaded(),
|
|
250
|
+
};
|
|
251
|
+
expect(await harness().reloaded()).toBe(false);
|
|
252
|
+
}, 600_000);
|
|
253
|
+
|
|
254
|
+
//? The loop a typo makes: the error shows, and the fix must patch at the speed of any other edit (a failed build
|
|
255
|
+
//? neither blocks the fast lane nor makes the fixing save wait for its pages build).
|
|
256
|
+
test("SSR tab: a syntax error, then its fix", async () => {
|
|
257
|
+
await harness().open("/e2e/stack/tab-a", { csr: false });
|
|
258
|
+
await Bun.sleep(1_000);
|
|
259
|
+
const pristine = await Bun.file(probeFile).text();
|
|
260
|
+
const samples: number[] = [];
|
|
261
|
+
try {
|
|
262
|
+
for (let i = 0; i < runs; i += 1) {
|
|
263
|
+
await Bun.write(probeFile, pristine.replace("data-e2e-probe={name}>", "data-e2e-probe={name} {>"));
|
|
264
|
+
await harness().waitFor(() => document.querySelector(".__akan_hmr_overlay[data-status=error]") !== null, {
|
|
265
|
+
timeout: 20_000,
|
|
266
|
+
});
|
|
267
|
+
const fixed = componentEdit(`f${i}`);
|
|
268
|
+
await armProbe(fixed.applied);
|
|
269
|
+
const writtenAt = Date.now();
|
|
270
|
+
await Bun.write(probeFile, fixed.transform(pristine));
|
|
271
|
+
await waitForHit();
|
|
272
|
+
const hit = await harness().evaluate(() => (window as unknown as LatencyWindow).__latencyProbe?.hit ?? 0);
|
|
273
|
+
samples.push(hit - writtenAt);
|
|
274
|
+
await Bun.sleep(1_500);
|
|
275
|
+
}
|
|
276
|
+
} finally {
|
|
277
|
+
await Bun.write(probeFile, pristine);
|
|
278
|
+
}
|
|
279
|
+
report.errorFix = { median: median(samples), samples };
|
|
280
|
+
expect(await harness().reloaded()).toBe(false);
|
|
281
|
+
}, 300_000);
|
|
282
|
+
|
|
283
|
+
test("CSR tab: component, page, layout and Tailwind class edits", async () => {
|
|
284
|
+
await harness().open("/e2e/stack/tab-a");
|
|
285
|
+
await Bun.sleep(1_000);
|
|
286
|
+
const samples: { component: Sample[]; page: Sample[]; layout: Sample[]; tailwind: Sample[] } = {
|
|
287
|
+
component: [],
|
|
288
|
+
page: [],
|
|
289
|
+
layout: [],
|
|
290
|
+
tailwind: [],
|
|
291
|
+
};
|
|
292
|
+
for (let i = 0; i < runs; i += 1) {
|
|
293
|
+
samples.component.push(await timeEdit(componentEdit(`v${i}`), ["csr"]));
|
|
294
|
+
samples.page.push(await timeEdit(pageEdit(`v${i}`), ["csr"]));
|
|
295
|
+
samples.layout.push(
|
|
296
|
+
await timeEdit(
|
|
297
|
+
{
|
|
298
|
+
file: layoutFile,
|
|
299
|
+
transform: (source) => source.replace("<>{children}</>", `<>{children}<span data-e2e-layout="v${i}" /></>`),
|
|
300
|
+
applied: `document.querySelector('[data-e2e-layout="v${i}"]') !== null`,
|
|
301
|
+
restored: `document.querySelector('[data-e2e-layout]') === null`,
|
|
302
|
+
},
|
|
303
|
+
["csr"],
|
|
304
|
+
),
|
|
305
|
+
);
|
|
306
|
+
//? An arbitrary value no other file uses: the rule exists only after the css build, the class alone changes nothing.
|
|
307
|
+
const padding = 11 + i;
|
|
308
|
+
samples.tailwind.push(
|
|
309
|
+
await timeEdit(
|
|
310
|
+
{
|
|
311
|
+
file: probeFile,
|
|
312
|
+
transform: (source) => source.replace("bg-background p-2", `bg-background p-[${padding}px]`),
|
|
313
|
+
applied: `${input} && getComputedStyle(${input}).paddingTop === "${padding}px"`,
|
|
314
|
+
restored: `${input} && getComputedStyle(${input}).paddingTop !== "${padding}px"`,
|
|
315
|
+
},
|
|
316
|
+
["csr"],
|
|
317
|
+
),
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
report.csr = Object.fromEntries(Object.entries(samples).map(([name, list]) => [name, summarize(list)]));
|
|
321
|
+
expect(await harness().reloaded()).toBe(false);
|
|
322
|
+
}, 600_000);
|
|
323
|
+
|
|
324
|
+
test("CSR tab: ten saves 300ms apart", async () => {
|
|
325
|
+
const original = await Bun.file(probeFile).text();
|
|
326
|
+
await harness().evaluate(() => {
|
|
327
|
+
const w = window as unknown as LatencyWindow;
|
|
328
|
+
const seen: Record<string, number> = {};
|
|
329
|
+
w.__latencySeen = seen;
|
|
330
|
+
const check = () => {
|
|
331
|
+
const value = document.querySelector('[data-e2e-probe="tab-a"]')?.getAttribute("data-e2e-hot");
|
|
332
|
+
if (value && !(value in seen)) seen[value] = Date.now();
|
|
333
|
+
};
|
|
334
|
+
w.__latencyObserver = new MutationObserver(check);
|
|
335
|
+
w.__latencyObserver.observe(document, { subtree: true, attributes: true, childList: true });
|
|
336
|
+
});
|
|
337
|
+
const writtenAt: number[] = [];
|
|
338
|
+
for (let i = 0; i < 10; i += 1) {
|
|
339
|
+
writtenAt.push(Date.now());
|
|
340
|
+
await Bun.write(probeFile, componentEdit(`c${i}`).transform(original));
|
|
341
|
+
await Bun.sleep(300);
|
|
342
|
+
}
|
|
343
|
+
await harness().waitFor(() => (window as unknown as LatencyWindow).__latencySeen?.c9 !== undefined, {
|
|
344
|
+
timeout: 60_000,
|
|
345
|
+
});
|
|
346
|
+
const seen = await harness().evaluate(() => {
|
|
347
|
+
const w = window as unknown as LatencyWindow;
|
|
348
|
+
w.__latencyObserver?.disconnect();
|
|
349
|
+
return w.__latencySeen ?? {};
|
|
350
|
+
});
|
|
351
|
+
await Bun.write(probeFile, original);
|
|
352
|
+
await Bun.sleep(2_000);
|
|
353
|
+
//? A value the page never showed was overtaken; its save counts until the first later value appeared.
|
|
354
|
+
const latencies = writtenAt.map((at, i) => {
|
|
355
|
+
const later = Object.entries(seen)
|
|
356
|
+
.filter(([value]) => Number(value.slice(1)) >= i)
|
|
357
|
+
.map(([, hitAt]) => hitAt);
|
|
358
|
+
return Math.min(...later) - at;
|
|
359
|
+
});
|
|
360
|
+
report.consecutive = {
|
|
361
|
+
latencies,
|
|
362
|
+
median: median(latencies),
|
|
363
|
+
shown: Object.keys(seen).sort(),
|
|
364
|
+
settledAfterLastSave: (seen.c9 ?? 0) - (writtenAt.at(-1) ?? 0),
|
|
365
|
+
};
|
|
366
|
+
expect(await harness().reloaded()).toBe(false);
|
|
367
|
+
}, 300_000);
|
|
368
|
+
|
|
369
|
+
test("CSR tab: an editor's atomic save and a formatter's second write", async () => {
|
|
370
|
+
const atomic: Sample[] = [];
|
|
371
|
+
const doubled: Sample[] = [];
|
|
372
|
+
for (let i = 0; i < runs; i += 1) {
|
|
373
|
+
atomic.push(await timeEdit({ ...componentEdit(`a${i}`), write: writeAtomic }, ["csr"]));
|
|
374
|
+
doubled.push(
|
|
375
|
+
await timeEdit(
|
|
376
|
+
{
|
|
377
|
+
...componentEdit(`d${i}b`),
|
|
378
|
+
write: async (file, content) => {
|
|
379
|
+
await Bun.write(file, content.replace(`d${i}b`, `d${i}a`));
|
|
380
|
+
await Bun.sleep(30);
|
|
381
|
+
await Bun.write(file, content);
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
["csr"],
|
|
385
|
+
),
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
report.editorWrites = { atomic: summarize(atomic), formatterSecondWrite: summarize(doubled) };
|
|
389
|
+
expect(await harness().reloaded()).toBe(false);
|
|
390
|
+
}, 600_000);
|
|
391
|
+
});
|