rwsdk 1.0.0-alpha.9 → 1.0.0-beta.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 (159) hide show
  1. package/dist/lib/constants.mjs +1 -2
  2. package/dist/lib/e2e/browser.d.mts +1 -1
  3. package/dist/lib/e2e/browser.mjs +3 -4
  4. package/dist/lib/e2e/dev.mjs +62 -52
  5. package/dist/lib/e2e/environment.d.mts +2 -6
  6. package/dist/lib/e2e/environment.mjs +59 -72
  7. package/dist/lib/e2e/index.d.mts +5 -4
  8. package/dist/lib/e2e/index.mjs +5 -4
  9. package/dist/lib/e2e/poll.d.mts +8 -0
  10. package/dist/lib/e2e/poll.mjs +31 -0
  11. package/dist/lib/e2e/release.mjs +4 -4
  12. package/dist/lib/e2e/retry.d.mts +4 -0
  13. package/dist/lib/e2e/retry.mjs +16 -0
  14. package/dist/lib/e2e/setup.d.mts +2 -0
  15. package/dist/lib/e2e/setup.mjs +1 -0
  16. package/dist/lib/e2e/tarball.d.mts +3 -2
  17. package/dist/lib/e2e/tarball.mjs +5 -5
  18. package/dist/lib/e2e/testHarness.d.mts +59 -50
  19. package/dist/lib/e2e/testHarness.mjs +289 -343
  20. package/dist/lib/getShortName.mjs +1 -2
  21. package/dist/lib/getShortName.test.mjs +2 -2
  22. package/dist/lib/getSrcPaths.js +2 -2
  23. package/dist/lib/hasPkgScript.test.mjs +2 -2
  24. package/dist/lib/jsonUtils.test.mjs +2 -2
  25. package/dist/lib/normalizeModulePath.test.mjs +2 -2
  26. package/dist/lib/setupEnvFiles.mjs +2 -2
  27. package/dist/lib/smokeTests/artifacts.mjs +2 -2
  28. package/dist/lib/smokeTests/browser.d.mts +1 -1
  29. package/dist/lib/smokeTests/browser.mjs +6 -7
  30. package/dist/lib/smokeTests/cleanup.mjs +6 -9
  31. package/dist/lib/smokeTests/codeUpdates.mjs +5 -5
  32. package/dist/lib/smokeTests/development.mjs +2 -2
  33. package/dist/lib/smokeTests/environment.d.mts +2 -3
  34. package/dist/lib/smokeTests/environment.mjs +17 -3
  35. package/dist/lib/smokeTests/release.d.mts +2 -2
  36. package/dist/lib/smokeTests/release.mjs +3 -3
  37. package/dist/lib/smokeTests/reporting.mjs +2 -2
  38. package/dist/lib/smokeTests/runSmokeTests.mjs +4 -4
  39. package/dist/lib/smokeTests/utils.mjs +3 -3
  40. package/dist/lib/testUtils/stubEnvVars.mjs +1 -1
  41. package/dist/llms/rules/middleware.d.ts +1 -1
  42. package/dist/llms/rules/middleware.js +4 -4
  43. package/dist/runtime/client/client.d.ts +2 -2
  44. package/dist/runtime/client/client.js +2 -2
  45. package/dist/runtime/client/navigation.test.js +1 -1
  46. package/dist/runtime/client/types.d.ts +1 -1
  47. package/dist/runtime/entries/client.d.ts +2 -2
  48. package/dist/runtime/entries/client.js +2 -2
  49. package/dist/runtime/entries/router.d.ts +1 -1
  50. package/dist/runtime/entries/router.js +1 -1
  51. package/dist/runtime/entries/worker.d.ts +5 -6
  52. package/dist/runtime/entries/worker.js +5 -6
  53. package/dist/runtime/imports/worker.js +1 -1
  54. package/dist/runtime/lib/auth/session.d.ts +2 -2
  55. package/dist/runtime/lib/auth/session.js +5 -5
  56. package/dist/runtime/lib/db/DOWorkerDialect.d.ts +1 -1
  57. package/dist/runtime/lib/db/DOWorkerDialect.js +1 -1
  58. package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
  59. package/dist/runtime/lib/db/index.d.ts +2 -2
  60. package/dist/runtime/lib/db/index.js +2 -2
  61. package/dist/runtime/lib/db/migrations.d.ts +1 -1
  62. package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +3 -3
  63. package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +1 -1
  64. package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +2 -2
  65. package/dist/runtime/lib/db/typeInference/builders/createView.d.ts +1 -1
  66. package/dist/runtime/lib/db/typeInference/builders/dropTable.d.ts +1 -1
  67. package/dist/runtime/lib/db/typeInference/builders/dropView.d.ts +1 -1
  68. package/dist/runtime/lib/db/typeInference/builders/schema.d.ts +3 -3
  69. package/dist/runtime/lib/db/typeInference/database.d.ts +2 -2
  70. package/dist/runtime/lib/memoizeOnId.test.js +1 -1
  71. package/dist/runtime/lib/realtime/client.js +2 -2
  72. package/dist/runtime/lib/realtime/durableObject.js +1 -1
  73. package/dist/runtime/lib/realtime/protocol.test.js +1 -1
  74. package/dist/runtime/lib/realtime/shared.test.js +1 -1
  75. package/dist/runtime/lib/realtime/validateUpgradeRequest.test.js +1 -1
  76. package/dist/runtime/lib/realtime/worker.js +2 -2
  77. package/dist/runtime/lib/router.d.ts +1 -1
  78. package/dist/runtime/lib/router.test.js +2 -3
  79. package/dist/runtime/lib/rwContext.d.ts +1 -1
  80. package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +18 -0
  81. package/dist/runtime/lib/stitchDocumentAndAppStreams.js +143 -0
  82. package/dist/runtime/lib/turnstile/useTurnstile.js +1 -1
  83. package/dist/runtime/lib/turnstile/verifyTurnstileToken.test.js +1 -1
  84. package/dist/runtime/register/worker.d.ts +1 -1
  85. package/dist/runtime/register/worker.js +34 -22
  86. package/dist/runtime/render/assembleDocument.d.ts +1 -1
  87. package/dist/runtime/render/createThenableFromReadableStream.js +1 -1
  88. package/dist/runtime/render/preloads.d.ts +2 -2
  89. package/dist/runtime/render/renderDocumentHtmlStream.js +6 -6
  90. package/dist/runtime/render/renderHtmlStream.d.ts +1 -1
  91. package/dist/runtime/render/renderToRscStream.d.ts +4 -1
  92. package/dist/runtime/render/renderToRscStream.js +11 -1
  93. package/dist/runtime/render/renderToStream.d.ts +1 -1
  94. package/dist/runtime/render/renderToStream.js +2 -2
  95. package/dist/runtime/render/stylesheets.d.ts +1 -1
  96. package/dist/runtime/requestInfo/types.d.ts +0 -2
  97. package/dist/runtime/requestInfo/worker.d.ts +1 -1
  98. package/dist/runtime/requestInfo/worker.js +1 -9
  99. package/dist/runtime/script.js +1 -1
  100. package/dist/runtime/ssrBridge.d.ts +2 -2
  101. package/dist/runtime/ssrBridge.js +2 -2
  102. package/dist/runtime/worker.d.ts +1 -1
  103. package/dist/runtime/worker.js +3 -11
  104. package/dist/scripts/addon.d.mts +1 -0
  105. package/dist/scripts/addon.mjs +70 -0
  106. package/dist/scripts/debug-sync.mjs +106 -137
  107. package/dist/scripts/ensure-deploy-env.mjs +6 -6
  108. package/dist/scripts/migrate-new.mjs +3 -4
  109. package/dist/scripts/smoke-test.mjs +2 -2
  110. package/dist/scripts/worker-run.mjs +7 -9
  111. package/dist/vite/buildApp.mjs +1 -1
  112. package/dist/vite/checkIsUsingPrisma.test.mjs +1 -1
  113. package/dist/vite/configPlugin.mjs +33 -6
  114. package/dist/vite/createDirectiveLookupPlugin.mjs +1 -1
  115. package/dist/vite/createDirectiveLookupPlugin.test.mjs +2 -2
  116. package/dist/vite/createViteAwareResolver.d.mts +1 -2
  117. package/dist/vite/createViteAwareResolver.mjs +1 -1
  118. package/dist/vite/directiveModulesDevPlugin.mjs +4 -4
  119. package/dist/vite/directiveModulesDevPlugin.test.mjs +2 -2
  120. package/dist/vite/directivesPlugin.mjs +3 -3
  121. package/dist/vite/directivesPlugin.test.mjs +1 -1
  122. package/dist/vite/ensureAliasArray.test.mjs +1 -1
  123. package/dist/vite/findSpecifiers.mjs +1 -1
  124. package/dist/vite/findSpecifiers.test.mjs +2 -2
  125. package/dist/vite/findSsrSpecifiers.mjs +1 -1
  126. package/dist/vite/findSsrSpecifiers.test.mjs +1 -1
  127. package/dist/vite/getViteEsbuild.mjs +1 -1
  128. package/dist/vite/hasDirective.test.mjs +1 -1
  129. package/dist/vite/index.d.mts +1 -1
  130. package/dist/vite/invalidateCacheIfPrismaClientChanged.mjs +2 -2
  131. package/dist/vite/isJsFile.test.mjs +1 -1
  132. package/dist/vite/{reactConditionsResolverPlugin.d.mts → knownDepsResolverPlugin.d.mts} +3 -3
  133. package/dist/vite/{reactConditionsResolverPlugin.mjs → knownDepsResolverPlugin.mjs} +29 -24
  134. package/dist/vite/linkerPlugin.mjs +2 -2
  135. package/dist/vite/linkerPlugin.test.mjs +1 -1
  136. package/dist/vite/miniflareHMRPlugin.mjs +5 -5
  137. package/dist/vite/miniflareHMRPlugin.test.mjs +1 -1
  138. package/dist/vite/prismaPlugin.mjs +1 -1
  139. package/dist/vite/redwoodPlugin.d.mts +2 -0
  140. package/dist/vite/redwoodPlugin.mjs +36 -17
  141. package/dist/vite/redwoodPlugin.test.mjs +2 -2
  142. package/dist/vite/resolveForcedPaths.d.mts +4 -0
  143. package/dist/vite/resolveForcedPaths.mjs +9 -0
  144. package/dist/vite/runDirectivesScan.d.mts +2 -1
  145. package/dist/vite/runDirectivesScan.mjs +53 -17
  146. package/dist/vite/runDirectivesScan.test.mjs +2 -2
  147. package/dist/vite/ssrBridgePlugin.mjs +10 -3
  148. package/dist/vite/transformClientComponents.mjs +8 -6
  149. package/dist/vite/transformClientComponents.test.mjs +117 -59
  150. package/dist/vite/transformJsxScriptTagsPlugin.mjs +1 -1
  151. package/dist/vite/transformJsxScriptTagsPlugin.test.mjs +2 -2
  152. package/dist/vite/transformServerFunctions.d.mts +1 -1
  153. package/dist/vite/transformServerFunctions.mjs +5 -5
  154. package/dist/vite/transformServerFunctions.test.mjs +3 -3
  155. package/package.json +54 -44
  156. package/dist/runtime/imports/resolveSSRValue.d.ts +0 -1
  157. package/dist/runtime/imports/resolveSSRValue.js +0 -8
  158. package/dist/runtime/lib/injectHtmlAtMarker.d.ts +0 -11
  159. package/dist/runtime/lib/injectHtmlAtMarker.js +0 -90
@@ -1,16 +1,57 @@
1
- import { test, beforeAll, afterAll, afterEach, } from "vitest";
2
- import { basename } from "path";
3
- import { setupTarballEnvironment } from "./tarball.mjs";
4
- import { runDevServer } from "./dev.mjs";
5
- import { runRelease, deleteWorker, deleteD1Database, isRelatedToTest, } from "./release.mjs";
1
+ import fs from "fs-extra";
2
+ import os from "os";
3
+ import path, { basename, dirname, join as pathJoin } from "path";
4
+ import puppeteer from "puppeteer-core";
5
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, test, } from "vitest";
6
6
  import { launchBrowser } from "./browser.mjs";
7
- const SETUP_PLAYGROUND_ENV_TIMEOUT = 10 * 60 * 1000;
7
+ import { runDevServer } from "./dev.mjs";
8
+ import { poll, pollValue } from "./poll.mjs";
9
+ import { deleteD1Database, deleteWorker, isRelatedToTest, runRelease, } from "./release.mjs";
10
+ import { setupTarballEnvironment } from "./tarball.mjs";
11
+ const SETUP_PLAYGROUND_ENV_TIMEOUT = process.env
12
+ .RWSDK_SETUP_PLAYGROUND_ENV_TIMEOUT
13
+ ? parseInt(process.env.RWSDK_SETUP_PLAYGROUND_ENV_TIMEOUT, 10)
14
+ : 15 * 60 * 1000;
15
+ const DEPLOYMENT_TIMEOUT = process.env.RWSDK_DEPLOYMENT_TIMEOUT
16
+ ? parseInt(process.env.RWSDK_DEPLOYMENT_TIMEOUT, 10)
17
+ : 5 * 60 * 1000;
18
+ const DEPLOYMENT_MIN_TRIES = process.env.RWSDK_DEPLOYMENT_MIN_TRIES
19
+ ? parseInt(process.env.RWSDK_DEPLOYMENT_MIN_TRIES, 10)
20
+ : 5;
21
+ const DEPLOYMENT_CHECK_TIMEOUT = process.env.RWSDK_DEPLOYMENT_CHECK_TIMEOUT
22
+ ? parseInt(process.env.RWSDK_DEPLOYMENT_CHECK_TIMEOUT, 10)
23
+ : 5 * 60 * 1000;
24
+ const PUPPETEER_TIMEOUT = process.env.RWSDK_PUPPETEER_TIMEOUT
25
+ ? parseInt(process.env.RWSDK_PUPPETEER_TIMEOUT, 10)
26
+ : 60 * 1000 * 2;
27
+ const HYDRATION_TIMEOUT = process.env.RWSDK_HYDRATION_TIMEOUT
28
+ ? parseInt(process.env.RWSDK_HYDRATION_TIMEOUT, 10)
29
+ : 5000;
30
+ const DEV_SERVER_TIMEOUT = process.env.RWSDK_DEV_SERVER_TIMEOUT
31
+ ? parseInt(process.env.RWSDK_DEV_SERVER_TIMEOUT, 10)
32
+ : 5 * 60 * 1000;
33
+ const DEV_SERVER_MIN_TRIES = process.env.RWSDK_DEV_SERVER_MIN_TRIES
34
+ ? parseInt(process.env.RWSDK_DEV_SERVER_MIN_TRIES, 10)
35
+ : 5;
36
+ const SETUP_WAIT_TIMEOUT = process.env.RWSDK_SETUP_WAIT_TIMEOUT
37
+ ? parseInt(process.env.RWSDK_SETUP_WAIT_TIMEOUT, 10)
38
+ : 10 * 60 * 1000;
39
+ const TEST_MAX_RETRIES = process.env.RWSDK_TEST_MAX_RETRIES
40
+ ? parseInt(process.env.RWSDK_TEST_MAX_RETRIES, 10)
41
+ : 10;
42
+ const TEST_MAX_RETRIES_PER_CODE = process.env.RWSDK_TEST_MAX_RETRIES_PER_CODE
43
+ ? parseInt(process.env.RWSDK_TEST_MAX_RETRIES_PER_CODE, 10)
44
+ : 6;
8
45
  // Environment variable flags for skipping tests
9
46
  const SKIP_DEV_SERVER_TESTS = process.env.RWSDK_SKIP_DEV === "1";
10
47
  const SKIP_DEPLOYMENT_TESTS = process.env.RWSDK_SKIP_DEPLOY === "1";
11
48
  // Global test environment state
12
- let globalPlaygroundEnv = null;
13
- const cleanupTasks = [];
49
+ let globalDevPlaygroundEnv = null;
50
+ let globalDeployPlaygroundEnv = null;
51
+ let globalDevInstancePromise = null;
52
+ let globalDeploymentInstancePromise = null;
53
+ let globalDevInstance = null;
54
+ let globalDeploymentInstance = null;
14
55
  let hooksRegistered = false;
15
56
  /**
16
57
  * Registers global cleanup hooks automatically
@@ -18,49 +59,29 @@ let hooksRegistered = false;
18
59
  function ensureHooksRegistered() {
19
60
  if (hooksRegistered)
20
61
  return;
21
- // Register global afterEach to clean up resources created during tests
22
- afterEach(async () => {
23
- const tasksToCleanup = [...cleanupTasks];
24
- cleanupTasks.length = 0; // Clear the array
25
- for (const task of tasksToCleanup) {
26
- try {
27
- await task.cleanup();
28
- }
29
- catch (error) {
30
- console.warn(`Failed to cleanup ${task.type} ${task.id}:`, error);
31
- }
32
- }
33
- });
34
62
  // Register global afterAll to clean up the playground environment
35
63
  afterAll(async () => {
36
- if (globalPlaygroundEnv) {
37
- try {
38
- await globalPlaygroundEnv.cleanup();
39
- globalPlaygroundEnv = null;
40
- }
41
- catch (error) {
42
- console.warn("Failed to cleanup playground environment:", error);
43
- }
64
+ const cleanupPromises = [];
65
+ if (globalDevInstance) {
66
+ cleanupPromises.push(globalDevInstance.stopDev());
67
+ }
68
+ if (globalDeploymentInstance) {
69
+ cleanupPromises.push(globalDeploymentInstance.cleanup());
70
+ }
71
+ if (globalDevPlaygroundEnv) {
72
+ cleanupPromises.push(globalDevPlaygroundEnv.cleanup());
44
73
  }
74
+ if (globalDeployPlaygroundEnv) {
75
+ cleanupPromises.push(globalDeployPlaygroundEnv.cleanup());
76
+ }
77
+ await Promise.all(cleanupPromises);
78
+ globalDevInstance = null;
79
+ globalDeploymentInstance = null;
80
+ globalDevPlaygroundEnv = null;
81
+ globalDeployPlaygroundEnv = null;
45
82
  });
46
83
  hooksRegistered = true;
47
84
  }
48
- /**
49
- * Registers a cleanup task to be executed automatically
50
- */
51
- function registerCleanupTask(task) {
52
- ensureHooksRegistered();
53
- cleanupTasks.push(task);
54
- }
55
- /**
56
- * Removes a cleanup task from the registry (when manually cleaned up)
57
- */
58
- function unregisterCleanupTask(id) {
59
- const index = cleanupTasks.findIndex((task) => task.id === id);
60
- if (index !== -1) {
61
- cleanupTasks.splice(index, 1);
62
- }
63
- }
64
85
  /**
65
86
  * Get the project directory for the current test by looking at the call stack
66
87
  */
@@ -70,28 +91,31 @@ function getProjectDirectory() {
70
91
  return "../playground/hello-world";
71
92
  }
72
93
  /**
73
- * Derive the playground directory from import.meta.url
94
+ * Derive the playground directory from import.meta.url by finding the nearest package.json
74
95
  */
75
96
  function getPlaygroundDirFromImportMeta(importMetaUrl) {
76
97
  const url = new URL(importMetaUrl);
77
98
  const testFilePath = url.pathname;
78
- // Extract playground name from path like: /path/to/playground/PLAYGROUND_NAME/__tests__/e2e.test.mts
79
- const playgroundMatch = testFilePath.match(/\/playground\/([^\/]+)\/__tests__\//);
80
- if (playgroundMatch) {
81
- const playgroundName = playgroundMatch[1];
82
- // Return the absolute path to the playground directory
83
- const playgroundPath = testFilePath.replace(/\/__tests__\/.*$/, "");
84
- return playgroundPath;
99
+ let currentDir = dirname(testFilePath);
100
+ // Walk up the tree from the test file's directory
101
+ while (currentDir !== "/") {
102
+ // Check if a package.json exists in the current directory
103
+ if (fs.existsSync(pathJoin(currentDir, "package.json"))) {
104
+ return currentDir;
105
+ }
106
+ currentDir = dirname(currentDir);
85
107
  }
86
- throw new Error(`Could not determine playground directory from import.meta.url: ${importMetaUrl}`);
108
+ throw new Error(`Could not determine playground directory from import.meta.url: ${importMetaUrl}. ` +
109
+ `Failed to find a package.json in any parent directory.`);
87
110
  }
88
111
  /**
89
- * Sets up a playground environment for the entire test suite.
90
- * Automatically registers beforeAll and afterAll hooks.
91
- *
92
- * @param sourceProjectDir - Explicit path to playground directory, or import.meta.url to auto-detect
112
+ * A Vitest hook that sets up a playground environment for a test file.
113
+ * It creates a temporary directory, copies the playground project into it,
114
+ * and installs dependencies using a tarball of the SDK.
115
+ * This ensures that tests run in a clean, isolated environment.
93
116
  */
94
- export function setupPlaygroundEnvironment(sourceProjectDir) {
117
+ export function setupPlaygroundEnvironment(options = {}) {
118
+ const { sourceProjectDir, monorepoRoot, dev = true, deploy = true, } = typeof options === "string" ? { sourceProjectDir: options } : options;
95
119
  ensureHooksRegistered();
96
120
  beforeAll(async () => {
97
121
  let projectDir;
@@ -107,158 +131,129 @@ export function setupPlaygroundEnvironment(sourceProjectDir) {
107
131
  projectDir = sourceProjectDir;
108
132
  }
109
133
  console.log(`Setting up playground environment from ${projectDir}...`);
110
- const tarballEnv = await setupTarballEnvironment({
111
- projectDir,
112
- packageManager: process.env.PACKAGE_MANAGER || "pnpm",
113
- });
114
- globalPlaygroundEnv = {
115
- projectDir: tarballEnv.targetDir,
116
- cleanup: tarballEnv.cleanup,
117
- };
134
+ if (dev) {
135
+ const devEnv = await setupTarballEnvironment({
136
+ projectDir,
137
+ monorepoRoot,
138
+ packageManager: process.env.PACKAGE_MANAGER || "pnpm",
139
+ });
140
+ globalDevPlaygroundEnv = {
141
+ projectDir: devEnv.targetDir,
142
+ cleanup: devEnv.cleanup,
143
+ };
144
+ globalDevInstancePromise = createDevServer(devEnv.targetDir).then((instance) => {
145
+ globalDevInstance = instance;
146
+ return instance;
147
+ });
148
+ // Prevent unhandled promise rejections. The error will be handled inside
149
+ // the test's beforeEach hook where this promise is awaited.
150
+ globalDevInstancePromise.catch(() => { });
151
+ }
152
+ else {
153
+ globalDevInstancePromise = Promise.resolve(null);
154
+ }
155
+ if (deploy) {
156
+ const deployEnv = await setupTarballEnvironment({
157
+ projectDir,
158
+ monorepoRoot,
159
+ packageManager: process.env.PACKAGE_MANAGER || "pnpm",
160
+ });
161
+ globalDeployPlaygroundEnv = {
162
+ projectDir: deployEnv.targetDir,
163
+ cleanup: deployEnv.cleanup,
164
+ };
165
+ globalDeploymentInstancePromise = createDeployment(deployEnv.targetDir).then((instance) => {
166
+ globalDeploymentInstance = instance;
167
+ return instance;
168
+ });
169
+ // Prevent unhandled promise rejections
170
+ globalDeploymentInstancePromise.catch(() => { });
171
+ }
172
+ else {
173
+ globalDeploymentInstancePromise = Promise.resolve(null);
174
+ }
118
175
  }, SETUP_PLAYGROUND_ENV_TIMEOUT);
119
176
  }
120
- /**
121
- * Gets the current playground environment.
122
- * Throws if no environment has been set up.
123
- */
124
- export function getPlaygroundEnvironment() {
125
- if (!globalPlaygroundEnv) {
126
- throw new Error("No playground environment set up. Call setupPlaygroundEnvironment() in beforeAll()");
127
- }
128
- return globalPlaygroundEnv;
129
- }
130
177
  /**
131
178
  * Creates a dev server instance using the shared playground environment.
132
179
  * Automatically registers cleanup to run after the test.
133
180
  */
134
- export async function createDevServer() {
181
+ export async function createDevServer(projectDir) {
135
182
  if (SKIP_DEV_SERVER_TESTS) {
136
183
  throw new Error("Dev server tests are skipped via RWSDK_SKIP_DEV=1");
137
184
  }
138
- const env = getPlaygroundEnvironment();
139
185
  const packageManager = process.env.PACKAGE_MANAGER || "pnpm";
140
- const devResult = await runDevServer(packageManager, env.projectDir);
141
- const serverId = `devServer-${Date.now()}-${Math.random()
142
- .toString(36)
143
- .substring(2, 9)}`;
144
- // Register automatic cleanup
145
- registerCleanupTask({
146
- id: serverId,
147
- type: "devServer",
148
- cleanup: devResult.stopDev,
186
+ const devResult = await pollValue(() => runDevServer(packageManager, projectDir), {
187
+ timeout: DEV_SERVER_TIMEOUT,
188
+ minTries: DEV_SERVER_MIN_TRIES,
189
+ onRetry: (error, tries) => {
190
+ console.log(`Retrying dev server creation (attempt ${tries})... Error: ${error.message}`);
191
+ },
149
192
  });
150
193
  return {
151
194
  url: devResult.url,
152
- stopDev: async () => {
153
- await devResult.stopDev();
154
- unregisterCleanupTask(serverId); // Remove from auto-cleanup since manually cleaned
155
- },
195
+ stopDev: devResult.stopDev,
156
196
  };
157
197
  }
158
198
  /**
159
199
  * Creates a deployment instance using the shared playground environment.
160
200
  * Automatically registers cleanup to run after the test.
161
201
  */
162
- export async function createDeployment() {
202
+ export async function createDeployment(projectDir) {
163
203
  if (SKIP_DEPLOYMENT_TESTS) {
164
204
  throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
165
205
  }
166
- const env = getPlaygroundEnvironment();
167
- // Extract the unique key from the project directory name instead of generating a new one
168
- // The directory name format is: {projectName}-e2e-test-{randomId}
169
- const dirName = basename(env.projectDir);
170
- const match = dirName.match(/-e2e-test-([a-f0-9]+)$/);
171
- const resourceUniqueKey = match
172
- ? match[1]
173
- : Math.random().toString(36).substring(2, 15);
174
- const deployResult = await runRelease(env.projectDir, env.projectDir, resourceUniqueKey);
175
- // Poll the URL to ensure it's live before proceeding
176
- await poll(async () => {
177
- try {
178
- const response = await fetch(deployResult.url);
179
- // We consider any response (even 4xx or 5xx) as success,
180
- // as it means the worker is routable.
181
- return response.status > 0;
182
- }
183
- catch (e) {
184
- return false;
185
- }
186
- }, 60000);
187
- const deploymentId = `deployment-${Date.now()}-${Math.random()
188
- .toString(36)
189
- .substring(2, 9)}`;
190
- // Register automatic cleanup (non-blocking for deployments)
191
- registerCleanupTask({
192
- id: deploymentId,
193
- type: "deployment",
194
- cleanup: async () => {
206
+ return await pollValue(async () => {
207
+ // Extract the unique key from the project directory name instead of generating a new one
208
+ // The directory name format is: {projectName}-e2e-test-{randomId}
209
+ const dirName = basename(projectDir);
210
+ const match = dirName.match(/-e2e-test-([a-f0-9]+)$/);
211
+ const resourceUniqueKey = match
212
+ ? match[1]
213
+ : Math.random().toString(36).substring(2, 15);
214
+ const deployResult = await runRelease(projectDir, projectDir, resourceUniqueKey);
215
+ // Poll the URL to ensure it's live before proceeding
216
+ await poll(async () => {
217
+ try {
218
+ const response = await fetch(deployResult.url);
219
+ // We consider any response (even 4xx or 5xx) as success,
220
+ // as it means the worker is routable.
221
+ return response.status > 0;
222
+ }
223
+ catch (e) {
224
+ return false;
225
+ }
226
+ }, {
227
+ timeout: DEPLOYMENT_CHECK_TIMEOUT,
228
+ });
229
+ const cleanup = async () => {
195
230
  // Run deployment cleanup in background without blocking
196
231
  const performCleanup = async () => {
197
232
  if (isRelatedToTest(deployResult.workerName, resourceUniqueKey)) {
198
- await deleteWorker(deployResult.workerName, env.projectDir, resourceUniqueKey);
233
+ await deleteWorker(deployResult.workerName, projectDir, resourceUniqueKey);
199
234
  }
200
- await deleteD1Database(resourceUniqueKey, env.projectDir, resourceUniqueKey);
235
+ await deleteD1Database(resourceUniqueKey, projectDir, resourceUniqueKey);
201
236
  };
202
237
  // Start cleanup in background and return immediately
203
238
  performCleanup().catch((error) => {
204
239
  console.warn(`Warning: Background deployment cleanup failed: ${error.message}`);
205
240
  });
206
241
  return Promise.resolve();
242
+ };
243
+ return {
244
+ url: deployResult.url,
245
+ workerName: deployResult.workerName,
246
+ resourceUniqueKey,
247
+ projectDir: projectDir,
248
+ cleanup,
249
+ };
250
+ }, {
251
+ timeout: DEPLOYMENT_TIMEOUT,
252
+ minTries: DEPLOYMENT_MIN_TRIES,
253
+ onRetry: (error, tries) => {
254
+ console.log(`Retrying deployment creation (attempt ${tries})... Error: ${error.message}`);
207
255
  },
208
256
  });
209
- return {
210
- url: deployResult.url,
211
- workerName: deployResult.workerName,
212
- resourceUniqueKey,
213
- projectDir: env.projectDir,
214
- };
215
- }
216
- /**
217
- * Manually cleans up a deployment instance (deletes worker and D1 database).
218
- * This is optional since cleanup happens automatically after each test.
219
- */
220
- export async function cleanupDeployment(deployment) {
221
- const env = getPlaygroundEnvironment();
222
- if (isRelatedToTest(deployment.workerName, deployment.resourceUniqueKey)) {
223
- await deleteWorker(deployment.workerName, env.projectDir, deployment.resourceUniqueKey);
224
- }
225
- await deleteD1Database(deployment.resourceUniqueKey, env.projectDir, deployment.resourceUniqueKey);
226
- // Remove from auto-cleanup registry since manually cleaned
227
- const deploymentId = cleanupTasks.find((task) => task.type === "deployment" &&
228
- task.id.includes(deployment.resourceUniqueKey))?.id;
229
- if (deploymentId) {
230
- unregisterCleanupTask(deploymentId);
231
- }
232
- }
233
- /**
234
- * Creates a browser instance for testing.
235
- * Automatically registers cleanup to run after the test.
236
- */
237
- export async function createBrowser() {
238
- // Check if we should run in headed mode for debugging
239
- const headless = process.env.RWSDK_HEADLESS !== "false";
240
- const browser = await launchBrowser(undefined, headless);
241
- const browserId = `browser-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
242
- // Register automatic cleanup
243
- registerCleanupTask({
244
- id: browserId,
245
- type: "browser",
246
- cleanup: async () => {
247
- try {
248
- await browser.close();
249
- }
250
- catch (error) {
251
- // Browser might already be closed, ignore the error
252
- }
253
- },
254
- });
255
- // Wrap the close method to handle cleanup registration
256
- const originalClose = browser.close.bind(browser);
257
- browser.close = async () => {
258
- await originalClose();
259
- unregisterCleanupTask(browserId); // Remove from auto-cleanup since manually closed
260
- };
261
- return browser;
262
257
  }
263
258
  /**
264
259
  * Executes a test function with a retry mechanism for specific error codes.
@@ -269,193 +264,134 @@ export async function createBrowser() {
269
264
  * called automatically on failure.
270
265
  */
271
266
  export async function runTestWithRetries(name, attemptFn) {
272
- const MAX_RETRIES_PER_CODE = 6;
273
267
  const retryCounts = {};
274
268
  let attempt = 0;
275
- while (true) {
269
+ let lastError;
270
+ while (attempt < TEST_MAX_RETRIES) {
276
271
  attempt++;
277
- let cleanup;
278
272
  try {
279
- const res = await attemptFn();
280
- cleanup = res.cleanup;
273
+ await attemptFn();
281
274
  if (attempt > 1) {
282
275
  console.log(`[runTestWithRetries] Test "${name}" succeeded on attempt ${attempt}.`);
283
276
  }
284
- // On success, we don't run cleanup here. It will be handled by afterEach.
285
277
  return; // Success
286
278
  }
287
279
  catch (e) {
288
- // On failure, run the cleanup from the failed attempt.
289
- // The cleanup function is attached to the error object on failure.
290
- const errorCleanup = e.cleanup;
291
- if (typeof errorCleanup === "function") {
292
- await errorCleanup().catch((err) => console.warn(`[runTestWithRetries] Cleanup failed for "${name}" during retry:`, err));
293
- }
280
+ lastError = e;
294
281
  const errorCode = e?.code;
295
282
  if (typeof errorCode === "string" && errorCode) {
296
283
  const count = (retryCounts[errorCode] || 0) + 1;
297
284
  retryCounts[errorCode] = count;
298
- if (count <= MAX_RETRIES_PER_CODE) {
299
- console.log(`[runTestWithRetries] Attempt ${attempt} for "${name}" failed with code ${errorCode}. Retrying (failure ${count}/${MAX_RETRIES_PER_CODE} for this code)...`);
300
- await new Promise((resolve) => setTimeout(resolve, 1000));
301
- continue; // Next attempt
302
- }
303
- else {
304
- console.error(`[runTestWithRetries] Test "${name}" failed with code ${errorCode} after ${MAX_RETRIES_PER_CODE} retries for this code.`);
305
- throw e; // Give up
285
+ if (count > TEST_MAX_RETRIES_PER_CODE) {
286
+ console.error(`[runTestWithRetries] Test "${name}" failed with code ${errorCode} after ${count - 1} retries. Max per-code retries (${TEST_MAX_RETRIES_PER_CODE}) exceeded.`);
287
+ throw e; // Give up for this specific error code
306
288
  }
307
289
  }
290
+ if (attempt < TEST_MAX_RETRIES) {
291
+ console.log(`[runTestWithRetries] Attempt ${attempt}/${TEST_MAX_RETRIES} for "${name}" failed. Retrying...`);
292
+ await new Promise((resolve) => setTimeout(resolve, 1000));
293
+ }
308
294
  else {
309
- console.error(`[runTestWithRetries] Test "${name}" failed on attempt ${attempt} with a non-retryable error:`, e);
310
- throw e;
295
+ console.error(`[runTestWithRetries] Test "${name}" failed after ${attempt} attempts.`);
311
296
  }
312
297
  }
313
298
  }
299
+ throw lastError;
314
300
  }
315
- /**
316
- * High-level test wrapper for dev server tests.
317
- * Automatically skips if RWSDK_SKIP_DEV=1
318
- */
319
- export function testDev(name, testFn) {
320
- if (SKIP_DEV_SERVER_TESTS) {
321
- test.skip(name, testFn);
322
- return;
323
- }
324
- test(name, async () => {
325
- await runTestWithRetries(name, async () => {
326
- const devServer = await createDevServer();
327
- const browser = await createBrowser();
328
- const page = await browser.newPage();
329
- const cleanup = async () => {
330
- await browser.close();
331
- await devServer.stopDev();
332
- };
333
- try {
334
- await testFn({
335
- devServer,
336
- browser,
337
- page,
338
- url: devServer.url,
301
+ function createTestRunner(testFn, envType) {
302
+ return (name, testLogic) => {
303
+ if ((envType === "dev" && SKIP_DEV_SERVER_TESTS) ||
304
+ (envType === "deploy" && SKIP_DEPLOYMENT_TESTS)) {
305
+ test.skip(name, () => { });
306
+ return;
307
+ }
308
+ describe.concurrent(name, () => {
309
+ let page;
310
+ let instance;
311
+ let browser;
312
+ beforeAll(async () => {
313
+ const tempDir = path.join(os.tmpdir(), "rwsdk-e2e-tests");
314
+ const wsEndpointFile = path.join(tempDir, "wsEndpoint");
315
+ try {
316
+ const wsEndpoint = await fs.readFile(wsEndpointFile, "utf-8");
317
+ browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint });
318
+ }
319
+ catch (error) {
320
+ console.warn("Failed to connect to existing browser instance. " +
321
+ "This might happen if you are running a single test file. " +
322
+ "Launching a new browser instance instead.");
323
+ browser = await launchBrowser();
324
+ }
325
+ }, SETUP_WAIT_TIMEOUT);
326
+ afterAll(async () => {
327
+ if (browser) {
328
+ await browser.disconnect();
329
+ }
330
+ });
331
+ beforeEach(async () => {
332
+ const instancePromise = envType === "dev"
333
+ ? globalDevInstancePromise
334
+ : globalDeploymentInstancePromise;
335
+ if (!instancePromise) {
336
+ throw new Error("Test environment promises not initialized. Call setupPlaygroundEnvironment() in your test file.");
337
+ }
338
+ [instance] = await Promise.all([instancePromise]);
339
+ if (!instance) {
340
+ throw new Error(`No ${envType} instance found. Make sure to enable it in setupPlaygroundEnvironment.`);
341
+ }
342
+ page = await browser.newPage();
343
+ page.setDefaultTimeout(PUPPETEER_TIMEOUT);
344
+ }, SETUP_WAIT_TIMEOUT);
345
+ afterEach(async () => {
346
+ if (page) {
347
+ try {
348
+ await page.close();
349
+ }
350
+ catch (error) {
351
+ // Suppress errors during page close, as the browser might already be disconnecting
352
+ // due to the test suite finishing.
353
+ console.warn(`Suppressing error during page.close() in test "${name}":`, error);
354
+ }
355
+ }
356
+ });
357
+ testFn(">", async () => {
358
+ if (!instance || !browser) {
359
+ throw new Error("Test environment not ready.");
360
+ }
361
+ await runTestWithRetries(name, async () => {
362
+ await testLogic({
363
+ [envType === "dev" ? "devServer" : "deployment"]: instance,
364
+ browser: browser,
365
+ page: page,
366
+ url: instance.url,
367
+ });
339
368
  });
340
- return { cleanup };
341
- }
342
- catch (error) {
343
- // Ensure cleanup is available to the retry wrapper even if testFn fails.
344
- // We re-throw the error to be handled by runTestWithRetries.
345
- throw Object.assign(error, { cleanup });
346
- }
369
+ });
347
370
  });
348
- });
371
+ };
349
372
  }
350
373
  /**
351
- * Skip version of testDev
374
+ * High-level test wrapper for dev server tests.
375
+ * Automatically skips if RWSDK_SKIP_DEV=1
352
376
  */
377
+ export function testDev(...args) {
378
+ return createTestRunner(test.concurrent, "dev")(...args);
379
+ }
353
380
  testDev.skip = (name, testFn) => {
354
381
  test.skip(name, testFn || (() => { }));
355
382
  };
356
- testDev.only = (name, testFn) => {
357
- if (SKIP_DEV_SERVER_TESTS) {
358
- test.skip(name, () => { });
359
- return;
360
- }
361
- test.only(name, async () => {
362
- await runTestWithRetries(name, async () => {
363
- const devServer = await createDevServer();
364
- const browser = await createBrowser();
365
- const page = await browser.newPage();
366
- const cleanup = async () => {
367
- await browser.close();
368
- await devServer.stopDev();
369
- };
370
- try {
371
- await testFn({
372
- devServer,
373
- browser,
374
- page,
375
- url: devServer.url,
376
- });
377
- return { cleanup };
378
- }
379
- catch (error) {
380
- // Ensure cleanup is available to the retry wrapper even if testFn fails.
381
- // We re-throw the error to be handled by runTestWithRetries.
382
- throw Object.assign(error, { cleanup });
383
- }
384
- });
385
- });
386
- };
383
+ testDev.only = createTestRunner(test.only, "dev");
387
384
  /**
388
385
  * High-level test wrapper for deployment tests.
389
386
  * Automatically skips if RWSDK_SKIP_DEPLOY=1
390
387
  */
391
- export function testDeploy(name, testFn) {
392
- if (SKIP_DEPLOYMENT_TESTS) {
393
- test.skip(name, testFn);
394
- return;
395
- }
396
- test(name, async () => {
397
- await runTestWithRetries(name, async () => {
398
- const deployment = await createDeployment();
399
- const browser = await createBrowser();
400
- const page = await browser.newPage();
401
- const cleanup = async () => {
402
- // We don't await this because we want to let it run in the background
403
- // The afterEach hook for deployments already does this.
404
- await cleanupDeployment(deployment);
405
- await browser.close();
406
- };
407
- try {
408
- await testFn({
409
- deployment,
410
- browser,
411
- page,
412
- url: deployment.url,
413
- });
414
- return { cleanup };
415
- }
416
- catch (error) {
417
- throw Object.assign(error, { cleanup });
418
- }
419
- });
420
- });
388
+ export function testDeploy(...args) {
389
+ return createTestRunner(test.concurrent, "deploy")(...args);
421
390
  }
422
- /**
423
- * Skip version of testDeploy
424
- */
425
391
  testDeploy.skip = (name, testFn) => {
426
392
  test.skip(name, testFn || (() => { }));
427
393
  };
428
- testDeploy.only = (name, testFn) => {
429
- if (SKIP_DEPLOYMENT_TESTS) {
430
- test.skip(name, () => { });
431
- return;
432
- }
433
- test.only(name, async () => {
434
- await runTestWithRetries(name, async () => {
435
- const deployment = await createDeployment();
436
- const browser = await createBrowser();
437
- const page = await browser.newPage();
438
- const cleanup = async () => {
439
- // We don't await this because we want to let it run in the background
440
- // The afterEach hook for deployments already does this.
441
- await cleanupDeployment(deployment);
442
- await browser.close();
443
- };
444
- try {
445
- await testFn({
446
- deployment,
447
- browser,
448
- page,
449
- url: deployment.url,
450
- });
451
- return { cleanup };
452
- }
453
- catch (error) {
454
- throw Object.assign(error, { cleanup });
455
- }
456
- });
457
- });
458
- };
394
+ testDeploy.only = createTestRunner(test.only, "deploy");
459
395
  /**
460
396
  * Unified test function that runs the same test against both dev server and deployment.
461
397
  * Automatically skips based on environment variables.
@@ -475,22 +411,32 @@ testDevAndDeploy.only = (name, testFn) => {
475
411
  testDeploy.only(`${name} (deployment)`, testFn);
476
412
  };
477
413
  /**
478
- * Utility function for polling/retrying assertions
414
+ * Waits for the page to be fully loaded and hydrated.
415
+ * This should be used before any user interaction is simulated.
479
416
  */
480
- export async function poll(fn, timeout = 2 * 60 * 1000, // 2 minutes
481
- interval = 100) {
482
- const startTime = Date.now();
483
- while (Date.now() - startTime < timeout) {
484
- try {
485
- const result = await fn();
486
- if (result) {
487
- return;
488
- }
489
- }
490
- catch (error) {
491
- // Continue polling on errors
417
+ export async function waitForHydration(page) {
418
+ // 1. Wait for the document to be fully loaded.
419
+ await page.waitForFunction('document.readyState === "complete"');
420
+ // 2. Wait a short, fixed amount of time for client-side hydration to finish.
421
+ // This is a pragmatic approach to ensure React has mounted.
422
+ await new Promise((resolve) => setTimeout(resolve, HYDRATION_TIMEOUT));
423
+ }
424
+ export function trackPageErrors(page) {
425
+ const consoleErrors = [];
426
+ const failedRequests = [];
427
+ page.on("requestfailed", (request) => {
428
+ failedRequests.push(`${request.url()} | ${request.failure()?.errorText}`);
429
+ });
430
+ page.on("console", (msg) => {
431
+ if (msg.type() === "error") {
432
+ consoleErrors.push(msg.text());
492
433
  }
493
- await new Promise((resolve) => setTimeout(resolve, interval));
494
- }
495
- throw new Error(`Polling timed out after ${timeout}ms`);
434
+ });
435
+ return {
436
+ get: () => ({
437
+ // context(justinvdm, 25 Sep 2025): Filter out irrelevant 404s (e.g. favicon)
438
+ consoleErrors: consoleErrors.filter((e) => !e.includes("404")),
439
+ failedRequests,
440
+ }),
441
+ };
496
442
  }