rwsdk 1.0.0-alpha.10 → 1.0.0-alpha.11

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.
@@ -1,6 +1,10 @@
1
- import { setTimeout } from "node:timers/promises";
1
+ import { setTimeout as sleep } from "node:timers/promises";
2
2
  import debug from "debug";
3
3
  import { $ } from "../../lib/$.mjs";
4
+ import { poll } from "./poll.mjs";
5
+ const DEV_SERVER_CHECK_TIMEOUT = process.env.RWSDK_DEV_SERVER_CHECK_TIMEOUT
6
+ ? parseInt(process.env.RWSDK_DEV_SERVER_CHECK_TIMEOUT, 10)
7
+ : 5 * 60 * 1000;
4
8
  const log = debug("rwsdk:e2e:dev");
5
9
  /**
6
10
  * Run the local development server and return the URL
@@ -12,59 +16,47 @@ export async function runDevServer(packageManager = "pnpm", cwd) {
12
16
  let isErrorExpected = false;
13
17
  const stopDev = async () => {
14
18
  isErrorExpected = true;
15
- if (!devProcess) {
16
- log("No dev process to stop");
19
+ if (!devProcess || !devProcess.pid) {
20
+ log("No dev process to stop or PID is missing");
17
21
  return;
18
22
  }
19
23
  console.log("Stopping development server...");
20
24
  try {
21
- // Send a regular termination signal first
22
- devProcess.kill();
23
- // Wait for the process to terminate with a timeout
24
- const terminationTimeout = 5000; // 5 seconds timeout
25
- const terminationPromise = Promise.race([
26
- // Wait for natural process termination
27
- (async () => {
28
- try {
29
- await devProcess;
30
- log("Dev server process was terminated normally");
31
- return true;
32
- }
33
- catch (e) {
34
- // Expected error when the process is killed
35
- log("Dev server process was terminated");
36
- return true;
37
- }
38
- })(),
39
- // Or timeout
40
- (async () => {
41
- await setTimeout(terminationTimeout);
42
- return false;
43
- })(),
44
- ]);
45
- // Check if process terminated within timeout
46
- const terminated = await terminationPromise;
47
- // If not terminated within timeout, force kill
48
- if (!terminated) {
49
- log("Dev server process did not terminate within timeout, force killing with SIGKILL");
50
- console.log("⚠️ Development server not responding after 5 seconds timeout, force killing...");
51
- // Try to kill with SIGKILL if the process still has a pid
52
- if (devProcess.pid) {
53
- try {
54
- // Use process.kill with SIGKILL for a stronger termination
55
- process.kill(devProcess.pid, "SIGKILL");
56
- log("Sent SIGKILL to process %d", devProcess.pid);
57
- }
58
- catch (killError) {
59
- log("Error sending SIGKILL to process: %O", killError);
60
- // Non-fatal, as the process might already be gone
61
- }
62
- }
63
- }
25
+ // Send a regular termination signal to the entire process group first
26
+ process.kill(-devProcess.pid, "SIGTERM");
64
27
  }
65
28
  catch (e) {
66
- // Process might already have exited
67
- log("Could not kill dev server process: %O", e);
29
+ log("Could not send SIGTERM to dev server process group: %O", e);
30
+ }
31
+ // Wait for the process to terminate with a timeout
32
+ const terminationTimeout = 5000; // 5 seconds
33
+ const processExitPromise = devProcess.catch(() => {
34
+ // We expect this promise to reject when the process is killed,
35
+ // so we catch and ignore the error.
36
+ });
37
+ const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(undefined), terminationTimeout));
38
+ await Promise.race([processExitPromise, timeoutPromise]);
39
+ // Check if the process is still alive. We can't reliably check exitCode
40
+ // on a detached process, so we try sending a signal 0, which errors
41
+ // if the process doesn't exist.
42
+ let isAlive = true;
43
+ try {
44
+ // Sending signal 0 doesn't kill the process, but checks if it exists
45
+ process.kill(-devProcess.pid, 0);
46
+ }
47
+ catch (e) {
48
+ isAlive = false;
49
+ }
50
+ // If not terminated within timeout, force kill the entire process group
51
+ if (isAlive) {
52
+ log("Dev server process did not terminate within timeout, force killing with SIGKILL");
53
+ console.log("⚠️ Development server not responding after 5 seconds timeout, force killing...");
54
+ try {
55
+ process.kill(-devProcess.pid, "SIGKILL");
56
+ }
57
+ catch (e) {
58
+ log("Could not send SIGKILL to dev server process group: %O", e);
59
+ }
68
60
  }
69
61
  console.log("Development server stopped");
70
62
  };
@@ -96,7 +88,7 @@ export async function runDevServer(packageManager = "pnpm", cwd) {
96
88
  // Use the provided cwd if available
97
89
  devProcess = $({
98
90
  all: true,
99
- detached: false, // Keep attached so we can access streams
91
+ detached: true, // Run in a new process group so we can kill the entire group
100
92
  cleanup: false, // Don't auto-kill on exit
101
93
  cwd: cwd || process.cwd(), // Use provided directory or current directory
102
94
  env, // Pass the updated environment variables
@@ -104,8 +96,10 @@ export async function runDevServer(packageManager = "pnpm", cwd) {
104
96
  }) `${pm} run dev`;
105
97
  devProcess.catch((error) => {
106
98
  if (!isErrorExpected) {
107
- // Just throw the error, let the caller handle it.
108
- throw error;
99
+ // Don't re-throw. The error will be handled gracefully by the polling
100
+ // logic in `waitForUrl`, which will detect that the process has exited.
101
+ // Re-throwing here would cause an unhandled promise rejection.
102
+ log("Dev server process exited unexpectedly:", error.shortMessage);
109
103
  }
110
104
  });
111
105
  log("Development server process spawned in directory: %s", cwd || process.cwd());
@@ -211,7 +205,7 @@ export async function runDevServer(packageManager = "pnpm", cwd) {
211
205
  log("ERROR: Development server process exited with code %d. Final output: %s", devProcess.exitCode, allOutput);
212
206
  throw new Error(`Development server process exited with code ${devProcess.exitCode}`);
213
207
  }
214
- await setTimeout(500); // Check every 500ms
208
+ await sleep(500); // Check every 500ms
215
209
  }
216
210
  log("ERROR: Timed out waiting for dev server URL. Final accumulated output: %s", allOutput);
217
211
  throw new Error("Timed out waiting for dev server URL");
@@ -219,6 +213,22 @@ export async function runDevServer(packageManager = "pnpm", cwd) {
219
213
  // Wait for the URL
220
214
  const serverUrl = await waitForUrl();
221
215
  console.log(`✅ Development server started at ${serverUrl}`);
216
+ // Poll the URL to ensure it's live before proceeding
217
+ await poll(async () => {
218
+ try {
219
+ const response = await fetch(serverUrl, {
220
+ signal: AbortSignal.timeout(1000),
221
+ });
222
+ // We consider any response (even 4xx or 5xx) as success,
223
+ // as it means the worker is routable.
224
+ return response.status > 0;
225
+ }
226
+ catch (e) {
227
+ return false;
228
+ }
229
+ }, {
230
+ timeout: DEV_SERVER_CHECK_TIMEOUT,
231
+ });
222
232
  return { url: serverUrl, stopDev };
223
233
  }
224
234
  catch (error) {
@@ -7,7 +7,7 @@ export declare function setupTestEnvironment(options?: SmokeTestOptions): Promis
7
7
  /**
8
8
  * Copy project to a temporary directory with a unique name
9
9
  */
10
- export declare function copyProjectToTempDir(projectDir: string, resourceUniqueKey: string, packageManager?: PackageManager): Promise<{
10
+ export declare function copyProjectToTempDir(projectDir: string, resourceUniqueKey: string, packageManager?: PackageManager, monorepoRoot?: string): Promise<{
11
11
  tempDir: tmp.DirectoryResult;
12
12
  targetDir: string;
13
13
  workerName: string;
@@ -10,6 +10,8 @@ import { createHash } from "crypto";
10
10
  import { $ } from "../../lib/$.mjs";
11
11
  import { ROOT_DIR } from "../constants.mjs";
12
12
  import path from "node:path";
13
+ import os from "os";
14
+ import { retry } from "./retry.mjs";
13
15
  const log = debug("rwsdk:e2e:environment");
14
16
  const createSdkTarball = async () => {
15
17
  const packResult = await $({ cwd: ROOT_DIR, stdio: "pipe" }) `npm pack`;
@@ -24,11 +26,11 @@ const createSdkTarball = async () => {
24
26
  };
25
27
  return { tarballPath, cleanupTarball };
26
28
  };
27
- const setTarballDependency = async (targetDir, tarballPath) => {
29
+ const setTarballDependency = async (targetDir, tarballName) => {
28
30
  const filePath = join(targetDir, "package.json");
29
31
  const packageJson = await fs.promises.readFile(filePath, "utf-8");
30
32
  const packageJsonContent = JSON.parse(packageJson);
31
- packageJsonContent.dependencies.rwsdk = `file:${tarballPath}`;
33
+ packageJsonContent.dependencies.rwsdk = `file:${tarballName}`;
32
34
  await fs.promises.writeFile(filePath, JSON.stringify(packageJsonContent, null, 2));
33
35
  };
34
36
  /**
@@ -87,20 +89,26 @@ export async function setupTestEnvironment(options = {}) {
87
89
  /**
88
90
  * Copy project to a temporary directory with a unique name
89
91
  */
90
- export async function copyProjectToTempDir(projectDir, resourceUniqueKey, packageManager) {
92
+ export async function copyProjectToTempDir(projectDir, resourceUniqueKey, packageManager, monorepoRoot) {
91
93
  const { tarballPath, cleanupTarball } = await createSdkTarball();
92
94
  try {
93
95
  log("Creating temporary directory for project");
94
96
  // Create a temporary directory
95
97
  const tempDir = await tmp.dir({ unsafeCleanup: true });
98
+ // Determine the source directory to copy from
99
+ const sourceDir = monorepoRoot || projectDir;
96
100
  // Create unique project directory name
97
- const originalDirName = basename(projectDir);
101
+ const originalDirName = basename(sourceDir);
98
102
  const workerName = `${originalDirName}-test-${resourceUniqueKey}`;
99
- const targetDir = resolve(tempDir.path, workerName);
100
- console.log(`Copying project from ${projectDir} to ${targetDir}`);
103
+ const tempCopyRoot = resolve(tempDir.path, workerName);
104
+ // If it's a monorepo, the targetDir for commands is a subdirectory
105
+ const targetDir = monorepoRoot
106
+ ? resolve(tempCopyRoot, relative(monorepoRoot, projectDir))
107
+ : tempCopyRoot;
108
+ console.log(`Copying project from ${sourceDir} to ${tempCopyRoot}`);
101
109
  // Read project's .gitignore if it exists
102
110
  let ig = ignore();
103
- const gitignorePath = join(projectDir, ".gitignore");
111
+ const gitignorePath = join(sourceDir, ".gitignore");
104
112
  if (await pathExists(gitignorePath)) {
105
113
  log("Found .gitignore file at %s", gitignorePath);
106
114
  const gitignoreContent = await fs.promises.readFile(gitignorePath, "utf-8");
@@ -123,10 +131,10 @@ export async function copyProjectToTempDir(projectDir, resourceUniqueKey, packag
123
131
  }
124
132
  // Copy the project directory, respecting .gitignore
125
133
  log("Starting copy process with ignored patterns");
126
- await copy(projectDir, targetDir, {
134
+ await copy(sourceDir, tempCopyRoot, {
127
135
  filter: (src) => {
128
136
  // Get path relative to project directory
129
- const relativePath = relative(projectDir, src);
137
+ const relativePath = relative(sourceDir, src);
130
138
  if (!relativePath)
131
139
  return true; // Include the root directory
132
140
  // Check against ignore patterns
@@ -135,6 +143,32 @@ export async function copyProjectToTempDir(projectDir, resourceUniqueKey, packag
135
143
  },
136
144
  });
137
145
  log("Project copy completed successfully");
146
+ // Copy the SDK tarball into the target directory
147
+ const tarballFilename = basename(tarballPath);
148
+ const tempTarballPath = join(targetDir, tarballFilename);
149
+ await fs.promises.copyFile(tarballPath, tempTarballPath);
150
+ if (monorepoRoot) {
151
+ log("⚙️ Configuring monorepo workspace...");
152
+ const rwsdkWsPath = join(tempCopyRoot, "rwsdk-workspace.json");
153
+ if (await pathExists(rwsdkWsPath)) {
154
+ const rwsdkWs = JSON.parse(await fs.promises.readFile(rwsdkWsPath, "utf-8"));
155
+ const workspaces = rwsdkWs.workspaces;
156
+ if (packageManager === "pnpm") {
157
+ const pnpmWsPath = join(tempCopyRoot, "pnpm-workspace.yaml");
158
+ const pnpmWsConfig = `packages:\n${workspaces.map((w) => ` - '${w}'`).join("\n")}\n`;
159
+ await fs.promises.writeFile(pnpmWsPath, pnpmWsConfig);
160
+ log("Created pnpm-workspace.yaml");
161
+ }
162
+ else {
163
+ // For npm and yarn, add a workspaces property to package.json
164
+ const pkgJsonPath = join(tempCopyRoot, "package.json");
165
+ const pkgJson = JSON.parse(await fs.promises.readFile(pkgJsonPath, "utf-8"));
166
+ pkgJson.workspaces = workspaces;
167
+ await fs.promises.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
168
+ log("Added workspaces to package.json");
169
+ }
170
+ }
171
+ }
138
172
  // Configure temp project to not use frozen lockfile
139
173
  log("⚙️ Configuring temp project to not use frozen lockfile...");
140
174
  const npmrcPath = join(targetDir, ".npmrc");
@@ -142,17 +176,24 @@ export async function copyProjectToTempDir(projectDir, resourceUniqueKey, packag
142
176
  // For yarn, create .yarnrc.yml to disable PnP and allow lockfile changes
143
177
  if (packageManager === "yarn") {
144
178
  const yarnrcPath = join(targetDir, ".yarnrc.yml");
179
+ const yarnCacheDir = path.join(os.tmpdir(), "yarn-cache");
180
+ await fs.promises.mkdir(yarnCacheDir, { recursive: true });
145
181
  const yarnConfig = [
146
182
  // todo(justinvdm, 23-09-23): Support yarn pnpm
147
183
  "nodeLinker: node-modules",
148
184
  "enableImmutableInstalls: false",
185
+ `cacheFolder: "${yarnCacheDir}"`,
149
186
  ].join("\n");
150
187
  await fs.promises.writeFile(yarnrcPath, yarnConfig);
151
188
  log("Created .yarnrc.yml to allow lockfile changes for yarn");
152
189
  }
153
- await setTarballDependency(targetDir, tarballPath);
190
+ await setTarballDependency(targetDir, tarballFilename);
154
191
  // Install dependencies in the target directory
155
- await installDependencies(targetDir, packageManager);
192
+ const installDir = monorepoRoot ? tempCopyRoot : targetDir;
193
+ await retry(() => installDependencies(installDir, packageManager), {
194
+ retries: 3,
195
+ delay: 1000,
196
+ });
156
197
  // Return the environment details
157
198
  return { tempDir, targetDir, workerName };
158
199
  }
@@ -193,9 +234,11 @@ async function installDependencies(targetDir, packageManager = "pnpm") {
193
234
  });
194
235
  }
195
236
  }
237
+ const npmCacheDir = path.join(os.tmpdir(), "npm-cache");
238
+ await fs.promises.mkdir(npmCacheDir, { recursive: true });
196
239
  const installCommand = {
197
240
  pnpm: ["pnpm", "install"],
198
- npm: ["npm", "install"],
241
+ npm: ["npm", "install", "--cache", npmCacheDir],
199
242
  yarn: ["yarn", "install"],
200
243
  "yarn-classic": ["yarn"],
201
244
  }[packageManager];
@@ -5,3 +5,4 @@ export * from "./dev.mjs";
5
5
  export * from "./browser.mjs";
6
6
  export * from "./release.mjs";
7
7
  export * from "./types.mjs";
8
+ export * from "./poll.mjs";
@@ -5,3 +5,4 @@ export * from "./dev.mjs";
5
5
  export * from "./browser.mjs";
6
6
  export * from "./release.mjs";
7
7
  export * from "./types.mjs";
8
+ export * from "./poll.mjs";
@@ -0,0 +1,8 @@
1
+ export interface PollOptions {
2
+ timeout: number;
3
+ interval: number;
4
+ minTries: number;
5
+ onRetry?: (error: unknown, tries: number) => void;
6
+ }
7
+ export declare function poll(fn: () => Promise<boolean>, options?: Partial<PollOptions>): Promise<void>;
8
+ export declare function pollValue<T>(fn: () => Promise<T>, options?: Partial<PollOptions>): Promise<T>;
@@ -0,0 +1,31 @@
1
+ import { setTimeout } from "node:timers/promises";
2
+ const POLL_TIMEOUT = process.env.RWSDK_POLL_TIMEOUT
3
+ ? parseInt(process.env.RWSDK_POLL_TIMEOUT, 10)
4
+ : 2 * 60 * 1000;
5
+ export async function poll(fn, options = {}) {
6
+ const { timeout = POLL_TIMEOUT, interval = 100, minTries = 3, onRetry, } = options;
7
+ const startTime = Date.now();
8
+ let tries = 0;
9
+ while (Date.now() - startTime < timeout || tries < minTries) {
10
+ tries++;
11
+ try {
12
+ if (await fn()) {
13
+ return;
14
+ }
15
+ }
16
+ catch (error) {
17
+ onRetry?.(error, tries);
18
+ // Continue polling on errors
19
+ }
20
+ await setTimeout(interval);
21
+ }
22
+ throw new Error(`Polling timed out after ${Date.now() - startTime}ms and ${tries} attempts`);
23
+ }
24
+ export async function pollValue(fn, options = {}) {
25
+ let value;
26
+ await poll(async () => {
27
+ value = await fn();
28
+ return true;
29
+ }, options);
30
+ return value;
31
+ }
@@ -0,0 +1,4 @@
1
+ export declare function retry<T>(fn: () => Promise<T>, options: {
2
+ retries: number;
3
+ delay: number;
4
+ }): Promise<T>;
@@ -0,0 +1,16 @@
1
+ import { setTimeout } from "node:timers/promises";
2
+ const log = console.log;
3
+ export async function retry(fn, options) {
4
+ let lastError;
5
+ for (let i = 0; i < options.retries; i++) {
6
+ try {
7
+ return await fn();
8
+ }
9
+ catch (e) {
10
+ lastError = e;
11
+ log(`Attempt ${i + 1} failed. Retrying in ${options.delay}ms...`);
12
+ await setTimeout(options.delay);
13
+ }
14
+ }
15
+ throw lastError;
16
+ }
@@ -0,0 +1,2 @@
1
+ export { launchBrowser } from "./browser.mjs";
2
+ export type { Browser } from "puppeteer-core";
@@ -0,0 +1 @@
1
+ export { launchBrowser } from "./browser.mjs";
@@ -1,5 +1,6 @@
1
1
  interface SetupTarballOptions {
2
2
  projectDir: string;
3
+ monorepoRoot?: string;
3
4
  packageManager?: "pnpm" | "npm" | "yarn";
4
5
  }
5
6
  interface TarballEnvironment {
@@ -9,5 +10,5 @@ interface TarballEnvironment {
9
10
  /**
10
11
  * Creates a tarball-based test environment similar to the release script approach
11
12
  */
12
- export declare function setupTarballEnvironment({ projectDir, packageManager, }: SetupTarballOptions): Promise<TarballEnvironment>;
13
+ export declare function setupTarballEnvironment({ projectDir, monorepoRoot, packageManager, }: SetupTarballOptions): Promise<TarballEnvironment>;
13
14
  export {};
@@ -63,7 +63,7 @@ async function copyWranglerCache(targetDir, sdkRoot) {
63
63
  /**
64
64
  * Creates a tarball-based test environment similar to the release script approach
65
65
  */
66
- export async function setupTarballEnvironment({ projectDir, packageManager = "pnpm", }) {
66
+ export async function setupTarballEnvironment({ projectDir, monorepoRoot, packageManager = "pnpm", }) {
67
67
  log(`🚀 Setting up tarball environment for ${projectDir}`);
68
68
  // Generate a resource unique key for this test run
69
69
  const uniqueNameSuffix = uniqueNamesGenerator({
@@ -79,7 +79,7 @@ export async function setupTarballEnvironment({ projectDir, packageManager = "pn
79
79
  .substring(0, 8);
80
80
  const resourceUniqueKey = `${uniqueNameSuffix}-${hash}`;
81
81
  try {
82
- const { tempDir, targetDir } = await copyProjectToTempDir(projectDir, resourceUniqueKey, packageManager);
82
+ const { tempDir, targetDir } = await copyProjectToTempDir(projectDir, resourceUniqueKey, packageManager, monorepoRoot);
83
83
  // Copy wrangler cache to improve deployment performance
84
84
  const sdkRoot = ROOT_DIR;
85
85
  await copyWranglerCache(targetDir, sdkRoot);
@@ -1,8 +1,6 @@
1
- import type { Browser, Page } from "puppeteer-core";
2
- interface PlaygroundEnvironment {
3
- projectDir: string;
4
- cleanup: () => Promise<void>;
5
- }
1
+ import { test } from "vitest";
2
+ import { type Browser, type Page } from "puppeteer-core";
3
+ export type { Browser, Page } from "puppeteer-core";
6
4
  interface DevServerInstance {
7
5
  url: string;
8
6
  stopDev: () => Promise<void>;
@@ -12,39 +10,48 @@ interface DeploymentInstance {
12
10
  workerName: string;
13
11
  resourceUniqueKey: string;
14
12
  projectDir: string;
13
+ cleanup: () => Promise<void>;
14
+ }
15
+ export interface SetupPlaygroundEnvironmentOptions {
16
+ /**
17
+ * The directory of the playground project to set up.
18
+ * Can be an absolute path, or a `import.meta.url` `file://` string.
19
+ * If not provided, it will be inferred from the test file's path.
20
+ */
21
+ sourceProjectDir?: string;
22
+ /**
23
+ * The root directory of the monorepo, if the project is part of one.
24
+ * This is used to correctly set up the test environment for monorepo projects.
25
+ */
26
+ monorepoRoot?: string;
27
+ /**
28
+ * Whether to provision a dev server for the test suite.
29
+ * @default true
30
+ */
31
+ dev?: boolean;
32
+ /**
33
+ * Whether to provision a deployment for the test suite.
34
+ * @default true
35
+ */
36
+ deploy?: boolean;
15
37
  }
16
38
  /**
17
- * Sets up a playground environment for the entire test suite.
18
- * Automatically registers beforeAll and afterAll hooks.
19
- *
20
- * @param sourceProjectDir - Explicit path to playground directory, or import.meta.url to auto-detect
21
- */
22
- export declare function setupPlaygroundEnvironment(sourceProjectDir?: string): void;
23
- /**
24
- * Gets the current playground environment.
25
- * Throws if no environment has been set up.
39
+ * A Vitest hook that sets up a playground environment for a test file.
40
+ * It creates a temporary directory, copies the playground project into it,
41
+ * and installs dependencies using a tarball of the SDK.
42
+ * This ensures that tests run in a clean, isolated environment.
26
43
  */
27
- export declare function getPlaygroundEnvironment(): PlaygroundEnvironment;
44
+ export declare function setupPlaygroundEnvironment(options?: string | SetupPlaygroundEnvironmentOptions): void;
28
45
  /**
29
46
  * Creates a dev server instance using the shared playground environment.
30
47
  * Automatically registers cleanup to run after the test.
31
48
  */
32
- export declare function createDevServer(): Promise<DevServerInstance>;
49
+ export declare function createDevServer(projectDir: string): Promise<DevServerInstance>;
33
50
  /**
34
51
  * Creates a deployment instance using the shared playground environment.
35
52
  * Automatically registers cleanup to run after the test.
36
53
  */
37
- export declare function createDeployment(): Promise<DeploymentInstance>;
38
- /**
39
- * Manually cleans up a deployment instance (deletes worker and D1 database).
40
- * This is optional since cleanup happens automatically after each test.
41
- */
42
- export declare function cleanupDeployment(deployment: DeploymentInstance): Promise<void>;
43
- /**
44
- * Creates a browser instance for testing.
45
- * Automatically registers cleanup to run after the test.
46
- */
47
- export declare function createBrowser(): Promise<Browser>;
54
+ export declare function createDeployment(projectDir: string): Promise<DeploymentInstance>;
48
55
  /**
49
56
  * Executes a test function with a retry mechanism for specific error codes.
50
57
  * @param name - The name of the test, used for logging.
@@ -53,23 +60,24 @@ export declare function createBrowser(): Promise<Browser>;
53
60
  * return a cleanup function. The cleanup function will be
54
61
  * called automatically on failure.
55
62
  */
56
- export declare function runTestWithRetries(name: string, attemptFn: () => Promise<{
57
- cleanup: () => Promise<void>;
58
- }>): Promise<void>;
63
+ export declare function runTestWithRetries(name: string, attemptFn: () => Promise<void>): Promise<void>;
64
+ declare function createTestRunner(testFn: (typeof test | typeof test.only)["concurrent"], envType: "dev" | "deploy"): (name: string, testLogic: (context: {
65
+ devServer?: DevServerInstance;
66
+ deployment?: DeploymentInstance;
67
+ browser: Browser;
68
+ page: Page;
69
+ url: string;
70
+ }) => Promise<void>) => void;
59
71
  /**
60
72
  * High-level test wrapper for dev server tests.
61
73
  * Automatically skips if RWSDK_SKIP_DEV=1
62
74
  */
63
- export declare function testDev(name: string, testFn: (context: {
64
- devServer: DevServerInstance;
65
- browser: Browser;
66
- page: Page;
67
- url: string;
68
- }) => Promise<void>): void;
75
+ export declare function testDev(...args: Parameters<ReturnType<typeof createTestRunner>>): void;
69
76
  export declare namespace testDev {
70
77
  var skip: (name: string, testFn?: any) => void;
71
- var only: (name: string, testFn: (context: {
72
- devServer: DevServerInstance;
78
+ var only: (name: string, testLogic: (context: {
79
+ devServer?: DevServerInstance;
80
+ deployment?: DeploymentInstance;
73
81
  browser: Browser;
74
82
  page: Page;
75
83
  url: string;
@@ -79,16 +87,12 @@ export declare namespace testDev {
79
87
  * High-level test wrapper for deployment tests.
80
88
  * Automatically skips if RWSDK_SKIP_DEPLOY=1
81
89
  */
82
- export declare function testDeploy(name: string, testFn: (context: {
83
- deployment: DeploymentInstance;
84
- browser: Browser;
85
- page: Page;
86
- url: string;
87
- }) => Promise<void>): void;
90
+ export declare function testDeploy(...args: Parameters<ReturnType<typeof createTestRunner>>): void;
88
91
  export declare namespace testDeploy {
89
92
  var skip: (name: string, testFn?: any) => void;
90
- var only: (name: string, testFn: (context: {
91
- deployment: DeploymentInstance;
93
+ var only: (name: string, testLogic: (context: {
94
+ devServer?: DevServerInstance;
95
+ deployment?: DeploymentInstance;
92
96
  browser: Browser;
93
97
  page: Page;
94
98
  url: string;
@@ -116,8 +120,7 @@ export declare namespace testDevAndDeploy {
116
120
  }) => Promise<void>) => void;
117
121
  }
118
122
  /**
119
- * Utility function for polling/retrying assertions
123
+ * Waits for the page to be fully loaded and hydrated.
124
+ * This should be used before any user interaction is simulated.
120
125
  */
121
- export declare function poll(fn: () => Promise<boolean>, timeout?: number, // 2 minutes
122
- interval?: number): Promise<void>;
123
- export {};
126
+ export declare function waitForHydration(page: Page): Promise<void>;