rwsdk 1.0.0-alpha.8 → 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.
- package/dist/lib/constants.mjs +1 -2
- package/dist/lib/e2e/browser.d.mts +1 -1
- package/dist/lib/e2e/browser.mjs +21 -5
- package/dist/lib/e2e/dev.mjs +62 -52
- package/dist/lib/e2e/environment.d.mts +2 -6
- package/dist/lib/e2e/environment.mjs +167 -158
- package/dist/lib/e2e/index.d.mts +5 -4
- package/dist/lib/e2e/index.mjs +5 -4
- package/dist/lib/e2e/poll.d.mts +8 -0
- package/dist/lib/e2e/poll.mjs +31 -0
- package/dist/lib/e2e/release.d.mts +1 -1
- package/dist/lib/e2e/release.mjs +56 -34
- package/dist/lib/e2e/retry.d.mts +4 -0
- package/dist/lib/e2e/retry.mjs +16 -0
- package/dist/lib/e2e/setup.d.mts +2 -0
- package/dist/lib/e2e/setup.mjs +1 -0
- package/dist/lib/e2e/tarball.d.mts +3 -3
- package/dist/lib/e2e/tarball.mjs +28 -118
- package/dist/lib/e2e/testHarness.d.mts +74 -40
- package/dist/lib/e2e/testHarness.mjs +320 -271
- package/dist/lib/e2e/types.d.mts +1 -0
- package/dist/lib/getShortName.mjs +1 -2
- package/dist/lib/getShortName.test.mjs +2 -2
- package/dist/lib/getSrcPaths.js +2 -2
- package/dist/lib/hasPkgScript.test.mjs +2 -2
- package/dist/lib/jsonUtils.test.mjs +2 -2
- package/dist/lib/normalizeModulePath.test.mjs +2 -2
- package/dist/lib/setupEnvFiles.mjs +2 -2
- package/dist/lib/smokeTests/artifacts.mjs +2 -2
- package/dist/lib/smokeTests/browser.d.mts +1 -1
- package/dist/lib/smokeTests/browser.mjs +6 -7
- package/dist/lib/smokeTests/cleanup.mjs +6 -9
- package/dist/lib/smokeTests/codeUpdates.mjs +5 -5
- package/dist/lib/smokeTests/development.mjs +2 -2
- package/dist/lib/smokeTests/environment.d.mts +2 -3
- package/dist/lib/smokeTests/environment.mjs +17 -3
- package/dist/lib/smokeTests/release.d.mts +2 -2
- package/dist/lib/smokeTests/release.mjs +3 -3
- package/dist/lib/smokeTests/reporting.mjs +2 -2
- package/dist/lib/smokeTests/runSmokeTests.mjs +4 -4
- package/dist/lib/smokeTests/utils.mjs +3 -3
- package/dist/lib/testUtils/stubEnvVars.mjs +1 -1
- package/dist/llms/rules/middleware.d.ts +1 -1
- package/dist/llms/rules/middleware.js +4 -4
- package/dist/runtime/client/client.d.ts +2 -2
- package/dist/runtime/client/client.js +2 -2
- package/dist/runtime/client/navigation.test.js +1 -1
- package/dist/runtime/client/types.d.ts +1 -1
- package/dist/runtime/entries/client.d.ts +2 -2
- package/dist/runtime/entries/client.js +2 -2
- package/dist/runtime/entries/router.d.ts +1 -1
- package/dist/runtime/entries/router.js +1 -1
- package/dist/runtime/entries/worker.d.ts +5 -6
- package/dist/runtime/entries/worker.js +5 -6
- package/dist/runtime/imports/worker.js +1 -1
- package/dist/runtime/lib/auth/session.d.ts +2 -2
- package/dist/runtime/lib/auth/session.js +5 -5
- package/dist/runtime/lib/db/DOWorkerDialect.d.ts +1 -1
- package/dist/runtime/lib/db/DOWorkerDialect.js +1 -1
- package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
- package/dist/runtime/lib/db/index.d.ts +2 -2
- package/dist/runtime/lib/db/index.js +2 -2
- package/dist/runtime/lib/db/migrations.d.ts +1 -1
- package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +3 -3
- package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +1 -1
- package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +2 -2
- package/dist/runtime/lib/db/typeInference/builders/createView.d.ts +1 -1
- package/dist/runtime/lib/db/typeInference/builders/dropTable.d.ts +1 -1
- package/dist/runtime/lib/db/typeInference/builders/dropView.d.ts +1 -1
- package/dist/runtime/lib/db/typeInference/builders/schema.d.ts +3 -3
- package/dist/runtime/lib/db/typeInference/database.d.ts +2 -2
- package/dist/runtime/lib/memoizeOnId.test.js +1 -1
- package/dist/runtime/lib/realtime/client.js +2 -2
- package/dist/runtime/lib/realtime/durableObject.js +1 -1
- package/dist/runtime/lib/realtime/protocol.test.js +1 -1
- package/dist/runtime/lib/realtime/shared.test.js +1 -1
- package/dist/runtime/lib/realtime/validateUpgradeRequest.test.js +1 -1
- package/dist/runtime/lib/realtime/worker.js +2 -2
- package/dist/runtime/lib/router.d.ts +1 -1
- package/dist/runtime/lib/router.test.js +2 -3
- package/dist/runtime/lib/rwContext.d.ts +1 -1
- package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +18 -0
- package/dist/runtime/lib/stitchDocumentAndAppStreams.js +143 -0
- package/dist/runtime/lib/turnstile/useTurnstile.js +1 -1
- package/dist/runtime/lib/turnstile/verifyTurnstileToken.test.js +1 -1
- package/dist/runtime/register/worker.d.ts +1 -1
- package/dist/runtime/register/worker.js +34 -22
- package/dist/runtime/render/assembleDocument.d.ts +1 -1
- package/dist/runtime/render/createThenableFromReadableStream.js +1 -1
- package/dist/runtime/render/preloads.d.ts +2 -2
- package/dist/runtime/render/renderDocumentHtmlStream.js +6 -6
- package/dist/runtime/render/renderHtmlStream.d.ts +1 -1
- package/dist/runtime/render/renderToRscStream.d.ts +4 -1
- package/dist/runtime/render/renderToRscStream.js +11 -1
- package/dist/runtime/render/renderToStream.d.ts +1 -1
- package/dist/runtime/render/renderToStream.js +2 -2
- package/dist/runtime/render/stylesheets.d.ts +1 -1
- package/dist/runtime/requestInfo/types.d.ts +0 -2
- package/dist/runtime/requestInfo/worker.d.ts +1 -1
- package/dist/runtime/requestInfo/worker.js +1 -9
- package/dist/runtime/script.js +1 -1
- package/dist/runtime/ssrBridge.d.ts +2 -2
- package/dist/runtime/ssrBridge.js +2 -2
- package/dist/runtime/worker.d.ts +1 -1
- package/dist/runtime/worker.js +3 -11
- package/dist/scripts/addon.d.mts +1 -0
- package/dist/scripts/addon.mjs +70 -0
- package/dist/scripts/debug-sync.mjs +106 -137
- package/dist/scripts/ensure-deploy-env.mjs +6 -6
- package/dist/scripts/migrate-new.mjs +3 -4
- package/dist/scripts/smoke-test.mjs +2 -2
- package/dist/scripts/worker-run.mjs +7 -9
- package/dist/vite/buildApp.d.mts +2 -1
- package/dist/vite/buildApp.mjs +10 -6
- package/dist/vite/checkIsUsingPrisma.test.mjs +1 -1
- package/dist/vite/configPlugin.mjs +35 -17
- package/dist/vite/createDirectiveLookupPlugin.mjs +1 -1
- package/dist/vite/createDirectiveLookupPlugin.test.mjs +2 -2
- package/dist/vite/createViteAwareResolver.d.mts +1 -2
- package/dist/vite/createViteAwareResolver.mjs +1 -1
- package/dist/vite/directiveModulesDevPlugin.d.mts +2 -1
- package/dist/vite/directiveModulesDevPlugin.mjs +6 -5
- package/dist/vite/directiveModulesDevPlugin.test.mjs +2 -2
- package/dist/vite/directivesPlugin.mjs +3 -3
- package/dist/vite/directivesPlugin.test.mjs +1 -1
- package/dist/vite/ensureAliasArray.test.mjs +1 -1
- package/dist/vite/findSpecifiers.mjs +1 -1
- package/dist/vite/findSpecifiers.test.mjs +2 -2
- package/dist/vite/findSsrSpecifiers.mjs +1 -1
- package/dist/vite/findSsrSpecifiers.test.mjs +1 -1
- package/dist/vite/getViteEsbuild.mjs +1 -1
- package/dist/vite/hasDirective.d.mts +6 -3
- package/dist/vite/hasDirective.mjs +43 -27
- package/dist/vite/hasDirective.test.mjs +73 -75
- package/dist/vite/index.d.mts +1 -1
- package/dist/vite/invalidateCacheIfPrismaClientChanged.mjs +2 -2
- package/dist/vite/isJsFile.test.mjs +1 -1
- package/dist/vite/{reactConditionsResolverPlugin.d.mts → knownDepsResolverPlugin.d.mts} +3 -3
- package/dist/vite/{reactConditionsResolverPlugin.mjs → knownDepsResolverPlugin.mjs} +29 -24
- package/dist/vite/linkerPlugin.mjs +2 -2
- package/dist/vite/linkerPlugin.test.mjs +1 -1
- package/dist/vite/miniflareHMRPlugin.mjs +5 -5
- package/dist/vite/miniflareHMRPlugin.test.mjs +1 -1
- package/dist/vite/prismaPlugin.mjs +1 -1
- package/dist/vite/redwoodPlugin.d.mts +2 -0
- package/dist/vite/redwoodPlugin.mjs +37 -17
- package/dist/vite/redwoodPlugin.test.mjs +2 -2
- package/dist/vite/resolveForcedPaths.d.mts +4 -0
- package/dist/vite/resolveForcedPaths.mjs +9 -0
- package/dist/vite/runDirectivesScan.d.mts +3 -1
- package/dist/vite/runDirectivesScan.mjs +60 -20
- package/dist/vite/runDirectivesScan.test.mjs +2 -2
- package/dist/vite/ssrBridgePlugin.mjs +10 -3
- package/dist/vite/transformClientComponents.mjs +8 -6
- package/dist/vite/transformClientComponents.test.mjs +117 -59
- package/dist/vite/transformJsxScriptTagsPlugin.mjs +1 -1
- package/dist/vite/transformJsxScriptTagsPlugin.test.mjs +2 -2
- package/dist/vite/transformServerFunctions.d.mts +1 -1
- package/dist/vite/transformServerFunctions.mjs +5 -5
- package/dist/vite/transformServerFunctions.test.mjs +3 -3
- package/package.json +60 -50
- package/dist/runtime/imports/resolveSSRValue.d.ts +0 -1
- package/dist/runtime/imports/resolveSSRValue.js +0 -8
- package/dist/runtime/lib/injectHtmlAtMarker.d.ts +0 -11
- package/dist/runtime/lib/injectHtmlAtMarker.js +0 -90
|
@@ -1,14 +1,57 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
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";
|
|
5
6
|
import { launchBrowser } from "./browser.mjs";
|
|
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;
|
|
6
45
|
// Environment variable flags for skipping tests
|
|
7
46
|
const SKIP_DEV_SERVER_TESTS = process.env.RWSDK_SKIP_DEV === "1";
|
|
8
47
|
const SKIP_DEPLOYMENT_TESTS = process.env.RWSDK_SKIP_DEPLOY === "1";
|
|
9
48
|
// Global test environment state
|
|
10
|
-
let
|
|
11
|
-
|
|
49
|
+
let globalDevPlaygroundEnv = null;
|
|
50
|
+
let globalDeployPlaygroundEnv = null;
|
|
51
|
+
let globalDevInstancePromise = null;
|
|
52
|
+
let globalDeploymentInstancePromise = null;
|
|
53
|
+
let globalDevInstance = null;
|
|
54
|
+
let globalDeploymentInstance = null;
|
|
12
55
|
let hooksRegistered = false;
|
|
13
56
|
/**
|
|
14
57
|
* Registers global cleanup hooks automatically
|
|
@@ -16,49 +59,29 @@ let hooksRegistered = false;
|
|
|
16
59
|
function ensureHooksRegistered() {
|
|
17
60
|
if (hooksRegistered)
|
|
18
61
|
return;
|
|
19
|
-
// Register global afterEach to clean up resources created during tests
|
|
20
|
-
afterEach(async () => {
|
|
21
|
-
const tasksToCleanup = [...cleanupTasks];
|
|
22
|
-
cleanupTasks.length = 0; // Clear the array
|
|
23
|
-
for (const task of tasksToCleanup) {
|
|
24
|
-
try {
|
|
25
|
-
await task.cleanup();
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
console.warn(`Failed to cleanup ${task.type} ${task.id}:`, error);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
62
|
// Register global afterAll to clean up the playground environment
|
|
33
63
|
afterAll(async () => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
globalPlaygroundEnv = null;
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
console.warn("Failed to cleanup playground environment:", error);
|
|
41
|
-
}
|
|
64
|
+
const cleanupPromises = [];
|
|
65
|
+
if (globalDevInstance) {
|
|
66
|
+
cleanupPromises.push(globalDevInstance.stopDev());
|
|
42
67
|
}
|
|
68
|
+
if (globalDeploymentInstance) {
|
|
69
|
+
cleanupPromises.push(globalDeploymentInstance.cleanup());
|
|
70
|
+
}
|
|
71
|
+
if (globalDevPlaygroundEnv) {
|
|
72
|
+
cleanupPromises.push(globalDevPlaygroundEnv.cleanup());
|
|
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;
|
|
43
82
|
});
|
|
44
83
|
hooksRegistered = true;
|
|
45
84
|
}
|
|
46
|
-
/**
|
|
47
|
-
* Registers a cleanup task to be executed automatically
|
|
48
|
-
*/
|
|
49
|
-
function registerCleanupTask(task) {
|
|
50
|
-
ensureHooksRegistered();
|
|
51
|
-
cleanupTasks.push(task);
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Removes a cleanup task from the registry (when manually cleaned up)
|
|
55
|
-
*/
|
|
56
|
-
function unregisterCleanupTask(id) {
|
|
57
|
-
const index = cleanupTasks.findIndex((task) => task.id === id);
|
|
58
|
-
if (index !== -1) {
|
|
59
|
-
cleanupTasks.splice(index, 1);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
85
|
/**
|
|
63
86
|
* Get the project directory for the current test by looking at the call stack
|
|
64
87
|
*/
|
|
@@ -68,28 +91,31 @@ function getProjectDirectory() {
|
|
|
68
91
|
return "../playground/hello-world";
|
|
69
92
|
}
|
|
70
93
|
/**
|
|
71
|
-
* Derive the playground directory from import.meta.url
|
|
94
|
+
* Derive the playground directory from import.meta.url by finding the nearest package.json
|
|
72
95
|
*/
|
|
73
96
|
function getPlaygroundDirFromImportMeta(importMetaUrl) {
|
|
74
97
|
const url = new URL(importMetaUrl);
|
|
75
98
|
const testFilePath = url.pathname;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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);
|
|
83
107
|
}
|
|
84
|
-
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.`);
|
|
85
110
|
}
|
|
86
111
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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.
|
|
91
116
|
*/
|
|
92
|
-
export function setupPlaygroundEnvironment(
|
|
117
|
+
export function setupPlaygroundEnvironment(options = {}) {
|
|
118
|
+
const { sourceProjectDir, monorepoRoot, dev = true, deploy = true, } = typeof options === "string" ? { sourceProjectDir: options } : options;
|
|
93
119
|
ensureHooksRegistered();
|
|
94
120
|
beforeAll(async () => {
|
|
95
121
|
let projectDir;
|
|
@@ -105,289 +131,312 @@ export function setupPlaygroundEnvironment(sourceProjectDir) {
|
|
|
105
131
|
projectDir = sourceProjectDir;
|
|
106
132
|
}
|
|
107
133
|
console.log(`Setting up playground environment from ${projectDir}...`);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
}
|
|
175
|
+
}, SETUP_PLAYGROUND_ENV_TIMEOUT);
|
|
127
176
|
}
|
|
128
177
|
/**
|
|
129
178
|
* Creates a dev server instance using the shared playground environment.
|
|
130
179
|
* Automatically registers cleanup to run after the test.
|
|
131
180
|
*/
|
|
132
|
-
export async function createDevServer() {
|
|
181
|
+
export async function createDevServer(projectDir) {
|
|
133
182
|
if (SKIP_DEV_SERVER_TESTS) {
|
|
134
183
|
throw new Error("Dev server tests are skipped via RWSDK_SKIP_DEV=1");
|
|
135
184
|
}
|
|
136
|
-
const
|
|
137
|
-
const devResult = await runDevServer(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
id: serverId,
|
|
144
|
-
type: "devServer",
|
|
145
|
-
cleanup: devResult.stopDev,
|
|
185
|
+
const packageManager = process.env.PACKAGE_MANAGER || "pnpm";
|
|
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
|
+
},
|
|
146
192
|
});
|
|
147
193
|
return {
|
|
148
194
|
url: devResult.url,
|
|
149
|
-
stopDev:
|
|
150
|
-
await devResult.stopDev();
|
|
151
|
-
unregisterCleanupTask(serverId); // Remove from auto-cleanup since manually cleaned
|
|
152
|
-
},
|
|
195
|
+
stopDev: devResult.stopDev,
|
|
153
196
|
};
|
|
154
197
|
}
|
|
155
198
|
/**
|
|
156
199
|
* Creates a deployment instance using the shared playground environment.
|
|
157
200
|
* Automatically registers cleanup to run after the test.
|
|
158
201
|
*/
|
|
159
|
-
export async function createDeployment() {
|
|
202
|
+
export async function createDeployment(projectDir) {
|
|
160
203
|
if (SKIP_DEPLOYMENT_TESTS) {
|
|
161
204
|
throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
|
|
162
205
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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 () => {
|
|
174
230
|
// Run deployment cleanup in background without blocking
|
|
175
231
|
const performCleanup = async () => {
|
|
176
232
|
if (isRelatedToTest(deployResult.workerName, resourceUniqueKey)) {
|
|
177
|
-
await deleteWorker(deployResult.workerName,
|
|
233
|
+
await deleteWorker(deployResult.workerName, projectDir, resourceUniqueKey);
|
|
178
234
|
}
|
|
179
|
-
await deleteD1Database(resourceUniqueKey,
|
|
235
|
+
await deleteD1Database(resourceUniqueKey, projectDir, resourceUniqueKey);
|
|
180
236
|
};
|
|
181
237
|
// Start cleanup in background and return immediately
|
|
182
238
|
performCleanup().catch((error) => {
|
|
183
239
|
console.warn(`Warning: Background deployment cleanup failed: ${error.message}`);
|
|
184
240
|
});
|
|
185
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}`);
|
|
186
255
|
},
|
|
187
256
|
});
|
|
188
|
-
return {
|
|
189
|
-
url: deployResult.url,
|
|
190
|
-
workerName: deployResult.workerName,
|
|
191
|
-
resourceUniqueKey,
|
|
192
|
-
};
|
|
193
257
|
}
|
|
194
258
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
259
|
+
* Executes a test function with a retry mechanism for specific error codes.
|
|
260
|
+
* @param name - The name of the test, used for logging.
|
|
261
|
+
* @param attemptFn - A function that executes one attempt of the test.
|
|
262
|
+
* It should set up resources, run the test logic, and
|
|
263
|
+
* return a cleanup function. The cleanup function will be
|
|
264
|
+
* called automatically on failure.
|
|
197
265
|
*/
|
|
198
|
-
export async function
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
registerCleanupTask({
|
|
222
|
-
id: browserId,
|
|
223
|
-
type: "browser",
|
|
224
|
-
cleanup: async () => {
|
|
225
|
-
try {
|
|
226
|
-
await browser.close();
|
|
266
|
+
export async function runTestWithRetries(name, attemptFn) {
|
|
267
|
+
const retryCounts = {};
|
|
268
|
+
let attempt = 0;
|
|
269
|
+
let lastError;
|
|
270
|
+
while (attempt < TEST_MAX_RETRIES) {
|
|
271
|
+
attempt++;
|
|
272
|
+
try {
|
|
273
|
+
await attemptFn();
|
|
274
|
+
if (attempt > 1) {
|
|
275
|
+
console.log(`[runTestWithRetries] Test "${name}" succeeded on attempt ${attempt}.`);
|
|
276
|
+
}
|
|
277
|
+
return; // Success
|
|
278
|
+
}
|
|
279
|
+
catch (e) {
|
|
280
|
+
lastError = e;
|
|
281
|
+
const errorCode = e?.code;
|
|
282
|
+
if (typeof errorCode === "string" && errorCode) {
|
|
283
|
+
const count = (retryCounts[errorCode] || 0) + 1;
|
|
284
|
+
retryCounts[errorCode] = count;
|
|
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
|
|
288
|
+
}
|
|
227
289
|
}
|
|
228
|
-
|
|
229
|
-
|
|
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));
|
|
230
293
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
294
|
+
else {
|
|
295
|
+
console.error(`[runTestWithRetries] Test "${name}" failed after ${attempt} attempts.`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
throw lastError;
|
|
300
|
+
}
|
|
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
|
+
});
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
});
|
|
238
371
|
};
|
|
239
|
-
return browser;
|
|
240
372
|
}
|
|
241
373
|
/**
|
|
242
374
|
* High-level test wrapper for dev server tests.
|
|
243
375
|
* Automatically skips if RWSDK_SKIP_DEV=1
|
|
244
376
|
*/
|
|
245
|
-
export function testDev(
|
|
246
|
-
|
|
247
|
-
test.skip(name, () => { });
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
test(name, async () => {
|
|
251
|
-
const devServer = await createDevServer();
|
|
252
|
-
const browser = await createBrowser();
|
|
253
|
-
const page = await browser.newPage();
|
|
254
|
-
await testFn({
|
|
255
|
-
devServer,
|
|
256
|
-
browser,
|
|
257
|
-
page,
|
|
258
|
-
url: devServer.url,
|
|
259
|
-
});
|
|
260
|
-
// Automatic cleanup handled by afterEach hooks
|
|
261
|
-
});
|
|
377
|
+
export function testDev(...args) {
|
|
378
|
+
return createTestRunner(test.concurrent, "dev")(...args);
|
|
262
379
|
}
|
|
263
|
-
/**
|
|
264
|
-
* Skip version of testDev
|
|
265
|
-
*/
|
|
266
380
|
testDev.skip = (name, testFn) => {
|
|
267
381
|
test.skip(name, testFn || (() => { }));
|
|
268
382
|
};
|
|
383
|
+
testDev.only = createTestRunner(test.only, "dev");
|
|
269
384
|
/**
|
|
270
385
|
* High-level test wrapper for deployment tests.
|
|
271
386
|
* Automatically skips if RWSDK_SKIP_DEPLOY=1
|
|
272
387
|
*/
|
|
273
|
-
export function testDeploy(
|
|
274
|
-
|
|
275
|
-
test.skip(name, () => { });
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
test(name, async () => {
|
|
279
|
-
const deployment = await createDeployment();
|
|
280
|
-
const browser = await createBrowser();
|
|
281
|
-
const page = await browser.newPage();
|
|
282
|
-
await testFn({
|
|
283
|
-
deployment,
|
|
284
|
-
browser,
|
|
285
|
-
page,
|
|
286
|
-
url: deployment.url,
|
|
287
|
-
});
|
|
288
|
-
// Automatic cleanup handled by afterEach hooks
|
|
289
|
-
});
|
|
388
|
+
export function testDeploy(...args) {
|
|
389
|
+
return createTestRunner(test.concurrent, "deploy")(...args);
|
|
290
390
|
}
|
|
291
|
-
/**
|
|
292
|
-
* Skip version of testDeploy
|
|
293
|
-
*/
|
|
294
391
|
testDeploy.skip = (name, testFn) => {
|
|
295
392
|
test.skip(name, testFn || (() => { }));
|
|
296
393
|
};
|
|
394
|
+
testDeploy.only = createTestRunner(test.only, "deploy");
|
|
297
395
|
/**
|
|
298
396
|
* Unified test function that runs the same test against both dev server and deployment.
|
|
299
397
|
* Automatically skips based on environment variables.
|
|
300
398
|
*/
|
|
301
399
|
export function testDevAndDeploy(name, testFn) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
else {
|
|
306
|
-
test(`${name} (dev)`, async () => {
|
|
307
|
-
const devServer = await createDevServer();
|
|
308
|
-
const browser = await createBrowser();
|
|
309
|
-
const page = await browser.newPage();
|
|
310
|
-
await testFn({
|
|
311
|
-
devServer,
|
|
312
|
-
browser,
|
|
313
|
-
page,
|
|
314
|
-
url: devServer.url,
|
|
315
|
-
});
|
|
316
|
-
// Automatic cleanup handled by afterEach hooks
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
if (SKIP_DEPLOYMENT_TESTS) {
|
|
320
|
-
test.skip(`${name} (deployment)`, () => { });
|
|
321
|
-
}
|
|
322
|
-
else {
|
|
323
|
-
test(`${name} (deployment)`, async () => {
|
|
324
|
-
const deployment = await createDeployment();
|
|
325
|
-
const browser = await createBrowser();
|
|
326
|
-
const page = await browser.newPage();
|
|
327
|
-
await testFn({
|
|
328
|
-
deployment,
|
|
329
|
-
browser,
|
|
330
|
-
page,
|
|
331
|
-
url: deployment.url,
|
|
332
|
-
});
|
|
333
|
-
// Automatic cleanup handled by afterEach hooks
|
|
334
|
-
});
|
|
335
|
-
}
|
|
400
|
+
testDev(`${name} (dev)`, testFn);
|
|
401
|
+
testDeploy(`${name} (deployment)`, testFn);
|
|
336
402
|
}
|
|
337
403
|
/**
|
|
338
404
|
* Skip version of testDevAndDeploy
|
|
339
405
|
*/
|
|
340
406
|
testDevAndDeploy.skip = (name, testFn) => {
|
|
341
|
-
test.skip(
|
|
342
|
-
test.skip(`${name} (deployment)`, testFn || (() => { }));
|
|
407
|
+
test.skip(name, testFn || (() => { }));
|
|
343
408
|
};
|
|
344
|
-
/**
|
|
345
|
-
* Only version of testDevAndDeploy
|
|
346
|
-
*/
|
|
347
409
|
testDevAndDeploy.only = (name, testFn) => {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const devServer = await createDevServer();
|
|
351
|
-
const browser = await createBrowser();
|
|
352
|
-
const page = await browser.newPage();
|
|
353
|
-
await testFn({
|
|
354
|
-
devServer,
|
|
355
|
-
browser,
|
|
356
|
-
page,
|
|
357
|
-
url: devServer.url,
|
|
358
|
-
});
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
if (!SKIP_DEPLOYMENT_TESTS) {
|
|
362
|
-
test.only(`${name} (deployment)`, async () => {
|
|
363
|
-
const deployment = await createDeployment();
|
|
364
|
-
const browser = await createBrowser();
|
|
365
|
-
const page = await browser.newPage();
|
|
366
|
-
await testFn({
|
|
367
|
-
deployment,
|
|
368
|
-
browser,
|
|
369
|
-
page,
|
|
370
|
-
url: deployment.url,
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
}
|
|
410
|
+
testDev.only(`${name} (dev)`, testFn);
|
|
411
|
+
testDeploy.only(`${name} (deployment)`, testFn);
|
|
374
412
|
};
|
|
375
413
|
/**
|
|
376
|
-
*
|
|
414
|
+
* Waits for the page to be fully loaded and hydrated.
|
|
415
|
+
* This should be used before any user interaction is simulated.
|
|
377
416
|
*/
|
|
378
|
-
export async function
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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());
|
|
389
433
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
+
};
|
|
393
442
|
}
|