rwsdk 1.0.0-alpha.5 → 1.0.0-alpha.7
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/e2e/browser.d.mts +10 -0
- package/dist/lib/e2e/browser.mjs +107 -0
- package/dist/lib/e2e/dev.d.mts +8 -0
- package/dist/lib/e2e/dev.mjs +232 -0
- package/dist/lib/e2e/environment.d.mts +14 -0
- package/dist/lib/e2e/environment.mjs +201 -0
- package/dist/lib/e2e/index.d.mts +7 -0
- package/dist/lib/e2e/index.mjs +7 -0
- package/dist/lib/e2e/release.d.mts +56 -0
- package/dist/lib/e2e/release.mjs +537 -0
- package/dist/lib/e2e/tarball.d.mts +14 -0
- package/dist/lib/e2e/tarball.mjs +189 -0
- package/dist/lib/e2e/testHarness.d.mts +98 -0
- package/dist/lib/e2e/testHarness.mjs +393 -0
- package/dist/lib/e2e/types.d.mts +31 -0
- package/dist/lib/e2e/types.mjs +1 -0
- package/dist/lib/smokeTests/browser.mjs +3 -94
- package/dist/lib/smokeTests/development.mjs +2 -223
- package/dist/lib/smokeTests/environment.d.mts +4 -11
- package/dist/lib/smokeTests/environment.mjs +10 -158
- package/dist/lib/smokeTests/release.d.mts +2 -49
- package/dist/lib/smokeTests/release.mjs +3 -503
- package/dist/runtime/lib/injectHtmlAtMarker.d.ts +11 -0
- package/dist/runtime/lib/injectHtmlAtMarker.js +90 -0
- package/dist/runtime/lib/realtime/worker.d.ts +1 -1
- package/dist/runtime/lib/router.js +32 -20
- package/dist/runtime/lib/router.test.js +506 -1
- package/dist/runtime/lib/rwContext.d.ts +22 -0
- package/dist/runtime/lib/rwContext.js +1 -0
- package/dist/runtime/render/assembleDocument.d.ts +6 -0
- package/dist/runtime/render/assembleDocument.js +22 -0
- package/dist/runtime/render/createThenableFromReadableStream.d.ts +1 -0
- package/dist/runtime/render/createThenableFromReadableStream.js +9 -0
- package/dist/runtime/render/normalizeActionResult.d.ts +1 -0
- package/dist/runtime/render/normalizeActionResult.js +43 -0
- package/dist/runtime/render/preloads.d.ts +2 -2
- package/dist/runtime/render/preloads.js +2 -3
- package/dist/runtime/render/{renderRscThenableToHtmlStream.d.ts → renderDocumentHtmlStream.d.ts} +3 -3
- package/dist/runtime/render/renderDocumentHtmlStream.js +39 -0
- package/dist/runtime/render/renderHtmlStream.d.ts +7 -0
- package/dist/runtime/render/renderHtmlStream.js +31 -0
- package/dist/runtime/render/renderToRscStream.d.ts +2 -3
- package/dist/runtime/render/renderToRscStream.js +2 -41
- package/dist/runtime/render/renderToStream.d.ts +2 -1
- package/dist/runtime/render/renderToStream.js +15 -8
- package/dist/runtime/render/stylesheets.d.ts +2 -2
- package/dist/runtime/render/stylesheets.js +2 -3
- package/dist/runtime/ssrBridge.d.ts +2 -1
- package/dist/runtime/ssrBridge.js +2 -1
- package/dist/runtime/worker.d.ts +1 -0
- package/dist/runtime/worker.js +11 -6
- package/dist/vite/configPlugin.mjs +2 -2
- package/package.json +8 -4
- package/dist/runtime/render/renderRscThenableToHtmlStream.js +0 -54
- package/dist/runtime/render/transformRscToHtmlStream.d.ts +0 -8
- package/dist/runtime/render/transformRscToHtmlStream.js +0 -19
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { $ } from "execa";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
const log = (message) => console.log(message);
|
|
7
|
+
/**
|
|
8
|
+
* Copies wrangler cache from monorepo to temp directory for deployment tests
|
|
9
|
+
*/
|
|
10
|
+
async function copyWranglerCache(targetDir, sdkRoot) {
|
|
11
|
+
try {
|
|
12
|
+
// Find the monorepo root by starting from the SDK root directory
|
|
13
|
+
// and walking up to find the monorepo root
|
|
14
|
+
let currentDir = path.resolve(sdkRoot);
|
|
15
|
+
let monorepoRoot = null;
|
|
16
|
+
// Walk up the directory tree to find the monorepo root
|
|
17
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
18
|
+
const nodeModulesPath = path.join(currentDir, "node_modules");
|
|
19
|
+
const packageJsonPath = path.join(currentDir, "package.json");
|
|
20
|
+
if (fs.existsSync(nodeModulesPath) && fs.existsSync(packageJsonPath)) {
|
|
21
|
+
try {
|
|
22
|
+
const packageJson = JSON.parse(await fs.promises.readFile(packageJsonPath, "utf8"));
|
|
23
|
+
// Check if this looks like our monorepo root
|
|
24
|
+
if (packageJson.name === "rw-sdk-monorepo" ||
|
|
25
|
+
packageJson.private === true) {
|
|
26
|
+
monorepoRoot = currentDir;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Continue searching if we can't read the package.json
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
currentDir = path.dirname(currentDir);
|
|
35
|
+
}
|
|
36
|
+
if (!monorepoRoot) {
|
|
37
|
+
log(` ⚠️ Could not find monorepo root, skipping wrangler cache copy`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const sourceCachePath = path.join(monorepoRoot, "node_modules/.cache/wrangler");
|
|
41
|
+
const targetCachePath = path.join(targetDir, "node_modules/.cache/wrangler");
|
|
42
|
+
if (fs.existsSync(sourceCachePath)) {
|
|
43
|
+
log(` 🔐 Copying wrangler cache from monorepo to temp directory...`);
|
|
44
|
+
// Ensure the target cache directory exists
|
|
45
|
+
await fs.promises.mkdir(path.dirname(targetCachePath), {
|
|
46
|
+
recursive: true,
|
|
47
|
+
});
|
|
48
|
+
// Copy the entire wrangler cache directory
|
|
49
|
+
await $ `cp -r ${sourceCachePath} ${path.dirname(targetCachePath)}`;
|
|
50
|
+
log(` ✅ Wrangler cache copied successfully`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
log(` ⚠️ No wrangler cache found in monorepo, deployment tests may require authentication`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
log(` ⚠️ Failed to copy wrangler cache: ${error.message}`);
|
|
58
|
+
// Don't throw - this is not a fatal error, deployment tests will just need manual auth
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Creates a tarball-based test environment similar to the release script approach
|
|
63
|
+
*/
|
|
64
|
+
export async function setupTarballEnvironment({ projectDir, packageManager = "pnpm", }) {
|
|
65
|
+
// Generate unique temp directory name
|
|
66
|
+
const randomId = crypto.randomBytes(4).toString("hex");
|
|
67
|
+
const projectName = path.basename(projectDir);
|
|
68
|
+
const tempDirName = `${projectName}-e2e-test-${randomId}`;
|
|
69
|
+
const targetDir = path.join(os.tmpdir(), tempDirName);
|
|
70
|
+
log(`📁 Creating temp directory: ${targetDir}`);
|
|
71
|
+
// Create temp directory
|
|
72
|
+
await fs.promises.mkdir(targetDir, { recursive: true });
|
|
73
|
+
try {
|
|
74
|
+
// Copy project to temp directory
|
|
75
|
+
log(`📋 Copying project from ${projectDir} to ${targetDir}`);
|
|
76
|
+
await $ `cp -a ${projectDir}/. ${targetDir}/`;
|
|
77
|
+
// Configure temp project to not use frozen lockfile
|
|
78
|
+
log(`⚙️ Configuring temp project to not use frozen lockfile...`);
|
|
79
|
+
const npmrcPath = path.join(targetDir, ".npmrc");
|
|
80
|
+
await fs.promises.writeFile(npmrcPath, "frozen-lockfile=false\n");
|
|
81
|
+
// Replace workspace:* dependencies with placeholder versions
|
|
82
|
+
log(`🔄 Replacing workspace dependencies...`);
|
|
83
|
+
const packageJsonPath = path.join(targetDir, "package.json");
|
|
84
|
+
const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf8");
|
|
85
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
86
|
+
// Replace workspace:* dependencies with a placeholder version
|
|
87
|
+
const replaceWorkspaceDeps = (deps) => {
|
|
88
|
+
if (!deps)
|
|
89
|
+
return;
|
|
90
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
91
|
+
if (version === "workspace:*") {
|
|
92
|
+
deps[name] = "0.0.80"; // Use a placeholder version that exists on npm
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
replaceWorkspaceDeps(packageJson.dependencies);
|
|
97
|
+
replaceWorkspaceDeps(packageJson.devDependencies);
|
|
98
|
+
replaceWorkspaceDeps(packageJson.peerDependencies);
|
|
99
|
+
await fs.promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
100
|
+
// Find SDK root directory (relative to current working directory)
|
|
101
|
+
const currentDir = process.cwd();
|
|
102
|
+
const sdkRoot = currentDir.includes("/playground")
|
|
103
|
+
? path.join(currentDir, "../sdk")
|
|
104
|
+
: currentDir;
|
|
105
|
+
// Pack the SDK
|
|
106
|
+
log(`📦 Packing SDK from ${sdkRoot}...`);
|
|
107
|
+
const packResult = await $({ cwd: sdkRoot }) `npm pack`;
|
|
108
|
+
const tarballName = packResult.stdout.trim();
|
|
109
|
+
const tarballPath = path.join(sdkRoot, tarballName);
|
|
110
|
+
// Install the tarball in the temp project
|
|
111
|
+
log(`💿 Installing tarball ${tarballName} in ${targetDir}...`);
|
|
112
|
+
if (packageManager === "pnpm") {
|
|
113
|
+
await $({ cwd: targetDir }) `pnpm add ${tarballPath}`;
|
|
114
|
+
}
|
|
115
|
+
else if (packageManager === "npm") {
|
|
116
|
+
await $({ cwd: targetDir }) `npm install ${tarballPath}`;
|
|
117
|
+
}
|
|
118
|
+
else if (packageManager === "yarn") {
|
|
119
|
+
await $({ cwd: targetDir }) `yarn add ${tarballPath}`;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
throw new Error(`Unsupported package manager: ${packageManager}`);
|
|
123
|
+
}
|
|
124
|
+
// Verify installation
|
|
125
|
+
const sdkPackageJson = JSON.parse(await fs.promises.readFile(path.join(sdkRoot, "package.json"), "utf8"));
|
|
126
|
+
const packageName = sdkPackageJson.name;
|
|
127
|
+
const installedDistPath = path.join(targetDir, "node_modules", packageName, "dist");
|
|
128
|
+
log(`🔍 Verifying installed package contents...`);
|
|
129
|
+
if (!fs.existsSync(installedDistPath)) {
|
|
130
|
+
throw new Error(`dist/ directory not found in installed package at ${installedDistPath}`);
|
|
131
|
+
}
|
|
132
|
+
// Compare checksums like the release script does
|
|
133
|
+
const originalDistPath = path.join(sdkRoot, "dist");
|
|
134
|
+
if (fs.existsSync(originalDistPath)) {
|
|
135
|
+
const getDistChecksum = async (distPath) => {
|
|
136
|
+
const findResult = await $({ cwd: distPath }) `find . -type f`;
|
|
137
|
+
const sortedFiles = findResult.stdout
|
|
138
|
+
.trim()
|
|
139
|
+
.split("\n")
|
|
140
|
+
.sort()
|
|
141
|
+
.join("\n");
|
|
142
|
+
return crypto.createHash("md5").update(sortedFiles).digest("hex");
|
|
143
|
+
};
|
|
144
|
+
const originalChecksum = await getDistChecksum(originalDistPath);
|
|
145
|
+
const installedChecksum = await getDistChecksum(installedDistPath);
|
|
146
|
+
log(` - Original dist checksum: ${originalChecksum}`);
|
|
147
|
+
log(` - Installed dist checksum: ${installedChecksum}`);
|
|
148
|
+
if (originalChecksum !== installedChecksum) {
|
|
149
|
+
throw new Error("File list in installed dist/ does not match original dist/");
|
|
150
|
+
}
|
|
151
|
+
log(` ✅ Installed package contents match the local build`);
|
|
152
|
+
}
|
|
153
|
+
// Copy wrangler cache from monorepo to temp directory for deployment tests
|
|
154
|
+
await copyWranglerCache(targetDir, sdkRoot);
|
|
155
|
+
// Cleanup function
|
|
156
|
+
const cleanup = async () => {
|
|
157
|
+
try {
|
|
158
|
+
// Remove tarball
|
|
159
|
+
if (fs.existsSync(tarballPath)) {
|
|
160
|
+
await fs.promises.unlink(tarballPath);
|
|
161
|
+
}
|
|
162
|
+
// Remove temp directory
|
|
163
|
+
if (fs.existsSync(targetDir)) {
|
|
164
|
+
await fs.promises.rm(targetDir, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.warn(`Warning: Failed to cleanup temp files: ${error.message}`);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
targetDir,
|
|
173
|
+
cleanup,
|
|
174
|
+
tarballPath,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
// Cleanup on error
|
|
179
|
+
try {
|
|
180
|
+
if (fs.existsSync(targetDir)) {
|
|
181
|
+
await fs.promises.rm(targetDir, { recursive: true, force: true });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (cleanupError) {
|
|
185
|
+
console.warn(`Warning: Failed to cleanup after error: ${cleanupError.message}`);
|
|
186
|
+
}
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Browser, Page } from "puppeteer-core";
|
|
2
|
+
interface PlaygroundEnvironment {
|
|
3
|
+
projectDir: string;
|
|
4
|
+
cleanup: () => Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
interface DevServerInstance {
|
|
7
|
+
url: string;
|
|
8
|
+
stopDev: () => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
interface DeploymentInstance {
|
|
11
|
+
url: string;
|
|
12
|
+
workerName: string;
|
|
13
|
+
resourceUniqueKey: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Sets up a playground environment for the entire test suite.
|
|
17
|
+
* Automatically registers beforeAll and afterAll hooks.
|
|
18
|
+
*
|
|
19
|
+
* @param sourceProjectDir - Explicit path to playground directory, or import.meta.url to auto-detect
|
|
20
|
+
*/
|
|
21
|
+
export declare function setupPlaygroundEnvironment(sourceProjectDir?: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Gets the current playground environment.
|
|
24
|
+
* Throws if no environment has been set up.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getPlaygroundEnvironment(): PlaygroundEnvironment;
|
|
27
|
+
/**
|
|
28
|
+
* Creates a dev server instance using the shared playground environment.
|
|
29
|
+
* Automatically registers cleanup to run after the test.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createDevServer(): Promise<DevServerInstance>;
|
|
32
|
+
/**
|
|
33
|
+
* Creates a deployment instance using the shared playground environment.
|
|
34
|
+
* Automatically registers cleanup to run after the test.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createDeployment(): Promise<DeploymentInstance>;
|
|
37
|
+
/**
|
|
38
|
+
* Manually cleans up a deployment instance (deletes worker and D1 database).
|
|
39
|
+
* This is optional since cleanup happens automatically after each test.
|
|
40
|
+
*/
|
|
41
|
+
export declare function cleanupDeployment(deployment: DeploymentInstance): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Creates a browser instance for testing.
|
|
44
|
+
* Automatically registers cleanup to run after the test.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createBrowser(): Promise<Browser>;
|
|
47
|
+
/**
|
|
48
|
+
* High-level test wrapper for dev server tests.
|
|
49
|
+
* Automatically skips if RWSDK_SKIP_DEV=1
|
|
50
|
+
*/
|
|
51
|
+
export declare function testDev(name: string, testFn: (context: {
|
|
52
|
+
devServer: DevServerInstance;
|
|
53
|
+
browser: Browser;
|
|
54
|
+
page: Page;
|
|
55
|
+
url: string;
|
|
56
|
+
}) => Promise<void>): void;
|
|
57
|
+
export declare namespace testDev {
|
|
58
|
+
var skip: (name: string, testFn?: any) => void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* High-level test wrapper for deployment tests.
|
|
62
|
+
* Automatically skips if RWSDK_SKIP_DEPLOY=1
|
|
63
|
+
*/
|
|
64
|
+
export declare function testDeploy(name: string, testFn: (context: {
|
|
65
|
+
deployment: DeploymentInstance;
|
|
66
|
+
browser: Browser;
|
|
67
|
+
page: Page;
|
|
68
|
+
url: string;
|
|
69
|
+
}) => Promise<void>): void;
|
|
70
|
+
export declare namespace testDeploy {
|
|
71
|
+
var skip: (name: string, testFn?: any) => void;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Unified test function that runs the same test against both dev server and deployment.
|
|
75
|
+
* Automatically skips based on environment variables.
|
|
76
|
+
*/
|
|
77
|
+
export declare function testDevAndDeploy(name: string, testFn: (context: {
|
|
78
|
+
devServer?: DevServerInstance;
|
|
79
|
+
deployment?: DeploymentInstance;
|
|
80
|
+
browser: Browser;
|
|
81
|
+
page: Page;
|
|
82
|
+
url: string;
|
|
83
|
+
}) => Promise<void>): void;
|
|
84
|
+
export declare namespace testDevAndDeploy {
|
|
85
|
+
var skip: (name: string, testFn?: any) => void;
|
|
86
|
+
var only: (name: string, testFn: (context: {
|
|
87
|
+
devServer?: DevServerInstance;
|
|
88
|
+
deployment?: DeploymentInstance;
|
|
89
|
+
browser: Browser;
|
|
90
|
+
page: Page;
|
|
91
|
+
url: string;
|
|
92
|
+
}) => Promise<void>) => void;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Utility function for polling/retrying assertions
|
|
96
|
+
*/
|
|
97
|
+
export declare function poll(fn: () => Promise<boolean>, timeout?: number, interval?: number): Promise<void>;
|
|
98
|
+
export {};
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { test, beforeAll, afterAll, afterEach, } from "vitest";
|
|
2
|
+
import { setupTarballEnvironment } from "./tarball.mjs";
|
|
3
|
+
import { runDevServer } from "./dev.mjs";
|
|
4
|
+
import { runRelease, deleteWorker, deleteD1Database, isRelatedToTest, } from "./release.mjs";
|
|
5
|
+
import { launchBrowser } from "./browser.mjs";
|
|
6
|
+
// Environment variable flags for skipping tests
|
|
7
|
+
const SKIP_DEV_SERVER_TESTS = process.env.RWSDK_SKIP_DEV === "1";
|
|
8
|
+
const SKIP_DEPLOYMENT_TESTS = process.env.RWSDK_SKIP_DEPLOY === "1";
|
|
9
|
+
// Global test environment state
|
|
10
|
+
let globalPlaygroundEnv = null;
|
|
11
|
+
const cleanupTasks = [];
|
|
12
|
+
let hooksRegistered = false;
|
|
13
|
+
/**
|
|
14
|
+
* Registers global cleanup hooks automatically
|
|
15
|
+
*/
|
|
16
|
+
function ensureHooksRegistered() {
|
|
17
|
+
if (hooksRegistered)
|
|
18
|
+
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
|
+
// Register global afterAll to clean up the playground environment
|
|
33
|
+
afterAll(async () => {
|
|
34
|
+
if (globalPlaygroundEnv) {
|
|
35
|
+
try {
|
|
36
|
+
await globalPlaygroundEnv.cleanup();
|
|
37
|
+
globalPlaygroundEnv = null;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.warn("Failed to cleanup playground environment:", error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
hooksRegistered = true;
|
|
45
|
+
}
|
|
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
|
+
/**
|
|
63
|
+
* Get the project directory for the current test by looking at the call stack
|
|
64
|
+
*/
|
|
65
|
+
function getProjectDirectory() {
|
|
66
|
+
// For now, let's hardcode this to '../playground/hello-world' since we only have one project
|
|
67
|
+
// TODO: Make this more dynamic when we have multiple playground projects
|
|
68
|
+
return "../playground/hello-world";
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Derive the playground directory from import.meta.url
|
|
72
|
+
*/
|
|
73
|
+
function getPlaygroundDirFromImportMeta(importMetaUrl) {
|
|
74
|
+
const url = new URL(importMetaUrl);
|
|
75
|
+
const testFilePath = url.pathname;
|
|
76
|
+
// Extract playground name from path like: /path/to/playground/PLAYGROUND_NAME/__tests__/e2e.test.mts
|
|
77
|
+
const playgroundMatch = testFilePath.match(/\/playground\/([^\/]+)\/__tests__\//);
|
|
78
|
+
if (playgroundMatch) {
|
|
79
|
+
const playgroundName = playgroundMatch[1];
|
|
80
|
+
// Return the absolute path to the playground directory
|
|
81
|
+
const playgroundPath = testFilePath.replace(/\/__tests__\/.*$/, "");
|
|
82
|
+
return playgroundPath;
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`Could not determine playground directory from import.meta.url: ${importMetaUrl}`);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Sets up a playground environment for the entire test suite.
|
|
88
|
+
* Automatically registers beforeAll and afterAll hooks.
|
|
89
|
+
*
|
|
90
|
+
* @param sourceProjectDir - Explicit path to playground directory, or import.meta.url to auto-detect
|
|
91
|
+
*/
|
|
92
|
+
export function setupPlaygroundEnvironment(sourceProjectDir) {
|
|
93
|
+
ensureHooksRegistered();
|
|
94
|
+
beforeAll(async () => {
|
|
95
|
+
let projectDir;
|
|
96
|
+
if (!sourceProjectDir) {
|
|
97
|
+
projectDir = getProjectDirectory();
|
|
98
|
+
}
|
|
99
|
+
else if (sourceProjectDir.startsWith("file://")) {
|
|
100
|
+
// This is import.meta.url, derive the playground directory
|
|
101
|
+
projectDir = getPlaygroundDirFromImportMeta(sourceProjectDir);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// This is an explicit path
|
|
105
|
+
projectDir = sourceProjectDir;
|
|
106
|
+
}
|
|
107
|
+
console.log(`Setting up playground environment from ${projectDir}...`);
|
|
108
|
+
const tarballEnv = await setupTarballEnvironment({
|
|
109
|
+
projectDir,
|
|
110
|
+
packageManager: "pnpm",
|
|
111
|
+
});
|
|
112
|
+
globalPlaygroundEnv = {
|
|
113
|
+
projectDir: tarballEnv.targetDir,
|
|
114
|
+
cleanup: tarballEnv.cleanup,
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Gets the current playground environment.
|
|
120
|
+
* Throws if no environment has been set up.
|
|
121
|
+
*/
|
|
122
|
+
export function getPlaygroundEnvironment() {
|
|
123
|
+
if (!globalPlaygroundEnv) {
|
|
124
|
+
throw new Error("No playground environment set up. Call setupPlaygroundEnvironment() in beforeAll()");
|
|
125
|
+
}
|
|
126
|
+
return globalPlaygroundEnv;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Creates a dev server instance using the shared playground environment.
|
|
130
|
+
* Automatically registers cleanup to run after the test.
|
|
131
|
+
*/
|
|
132
|
+
export async function createDevServer() {
|
|
133
|
+
if (SKIP_DEV_SERVER_TESTS) {
|
|
134
|
+
throw new Error("Dev server tests are skipped via RWSDK_SKIP_DEV=1");
|
|
135
|
+
}
|
|
136
|
+
const env = getPlaygroundEnvironment();
|
|
137
|
+
const devResult = await runDevServer("pnpm", env.projectDir);
|
|
138
|
+
const serverId = `devServer-${Date.now()}-${Math.random()
|
|
139
|
+
.toString(36)
|
|
140
|
+
.substring(2, 9)}`;
|
|
141
|
+
// Register automatic cleanup
|
|
142
|
+
registerCleanupTask({
|
|
143
|
+
id: serverId,
|
|
144
|
+
type: "devServer",
|
|
145
|
+
cleanup: devResult.stopDev,
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
url: devResult.url,
|
|
149
|
+
stopDev: async () => {
|
|
150
|
+
await devResult.stopDev();
|
|
151
|
+
unregisterCleanupTask(serverId); // Remove from auto-cleanup since manually cleaned
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Creates a deployment instance using the shared playground environment.
|
|
157
|
+
* Automatically registers cleanup to run after the test.
|
|
158
|
+
*/
|
|
159
|
+
export async function createDeployment() {
|
|
160
|
+
if (SKIP_DEPLOYMENT_TESTS) {
|
|
161
|
+
throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
|
|
162
|
+
}
|
|
163
|
+
const env = getPlaygroundEnvironment();
|
|
164
|
+
const resourceUniqueKey = Math.random().toString(36).substring(2, 15);
|
|
165
|
+
const deployResult = await runRelease(env.projectDir, env.projectDir, resourceUniqueKey);
|
|
166
|
+
const deploymentId = `deployment-${Date.now()}-${Math.random()
|
|
167
|
+
.toString(36)
|
|
168
|
+
.substring(2, 9)}`;
|
|
169
|
+
// Register automatic cleanup (non-blocking for deployments)
|
|
170
|
+
registerCleanupTask({
|
|
171
|
+
id: deploymentId,
|
|
172
|
+
type: "deployment",
|
|
173
|
+
cleanup: async () => {
|
|
174
|
+
// Run deployment cleanup in background without blocking
|
|
175
|
+
const performCleanup = async () => {
|
|
176
|
+
if (isRelatedToTest(deployResult.workerName, resourceUniqueKey)) {
|
|
177
|
+
await deleteWorker(deployResult.workerName, env.projectDir, resourceUniqueKey);
|
|
178
|
+
}
|
|
179
|
+
await deleteD1Database(resourceUniqueKey, env.projectDir, resourceUniqueKey);
|
|
180
|
+
};
|
|
181
|
+
// Start cleanup in background and return immediately
|
|
182
|
+
performCleanup().catch((error) => {
|
|
183
|
+
console.warn(`Warning: Background deployment cleanup failed: ${error.message}`);
|
|
184
|
+
});
|
|
185
|
+
return Promise.resolve();
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
url: deployResult.url,
|
|
190
|
+
workerName: deployResult.workerName,
|
|
191
|
+
resourceUniqueKey,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Manually cleans up a deployment instance (deletes worker and D1 database).
|
|
196
|
+
* This is optional since cleanup happens automatically after each test.
|
|
197
|
+
*/
|
|
198
|
+
export async function cleanupDeployment(deployment) {
|
|
199
|
+
const env = getPlaygroundEnvironment();
|
|
200
|
+
if (isRelatedToTest(deployment.workerName, deployment.resourceUniqueKey)) {
|
|
201
|
+
await deleteWorker(deployment.workerName, env.projectDir, deployment.resourceUniqueKey);
|
|
202
|
+
}
|
|
203
|
+
await deleteD1Database(deployment.resourceUniqueKey, env.projectDir, deployment.resourceUniqueKey);
|
|
204
|
+
// Remove from auto-cleanup registry since manually cleaned
|
|
205
|
+
const deploymentId = cleanupTasks.find((task) => task.type === "deployment" &&
|
|
206
|
+
task.id.includes(deployment.resourceUniqueKey))?.id;
|
|
207
|
+
if (deploymentId) {
|
|
208
|
+
unregisterCleanupTask(deploymentId);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Creates a browser instance for testing.
|
|
213
|
+
* Automatically registers cleanup to run after the test.
|
|
214
|
+
*/
|
|
215
|
+
export async function createBrowser() {
|
|
216
|
+
// Check if we should run in headed mode for debugging
|
|
217
|
+
const headless = process.env.RWSDK_HEADLESS !== "false";
|
|
218
|
+
const browser = await launchBrowser(undefined, headless);
|
|
219
|
+
const browserId = `browser-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
220
|
+
// Register automatic cleanup
|
|
221
|
+
registerCleanupTask({
|
|
222
|
+
id: browserId,
|
|
223
|
+
type: "browser",
|
|
224
|
+
cleanup: async () => {
|
|
225
|
+
try {
|
|
226
|
+
await browser.close();
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
// Browser might already be closed, ignore the error
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
// Wrap the close method to handle cleanup registration
|
|
234
|
+
const originalClose = browser.close.bind(browser);
|
|
235
|
+
browser.close = async () => {
|
|
236
|
+
await originalClose();
|
|
237
|
+
unregisterCleanupTask(browserId); // Remove from auto-cleanup since manually closed
|
|
238
|
+
};
|
|
239
|
+
return browser;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* High-level test wrapper for dev server tests.
|
|
243
|
+
* Automatically skips if RWSDK_SKIP_DEV=1
|
|
244
|
+
*/
|
|
245
|
+
export function testDev(name, testFn) {
|
|
246
|
+
if (SKIP_DEV_SERVER_TESTS) {
|
|
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
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Skip version of testDev
|
|
265
|
+
*/
|
|
266
|
+
testDev.skip = (name, testFn) => {
|
|
267
|
+
test.skip(name, testFn || (() => { }));
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* High-level test wrapper for deployment tests.
|
|
271
|
+
* Automatically skips if RWSDK_SKIP_DEPLOY=1
|
|
272
|
+
*/
|
|
273
|
+
export function testDeploy(name, testFn) {
|
|
274
|
+
if (SKIP_DEPLOYMENT_TESTS) {
|
|
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
|
+
});
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Skip version of testDeploy
|
|
293
|
+
*/
|
|
294
|
+
testDeploy.skip = (name, testFn) => {
|
|
295
|
+
test.skip(name, testFn || (() => { }));
|
|
296
|
+
};
|
|
297
|
+
/**
|
|
298
|
+
* Unified test function that runs the same test against both dev server and deployment.
|
|
299
|
+
* Automatically skips based on environment variables.
|
|
300
|
+
*/
|
|
301
|
+
export function testDevAndDeploy(name, testFn) {
|
|
302
|
+
if (SKIP_DEV_SERVER_TESTS) {
|
|
303
|
+
test.skip(`${name} (dev)`, () => { });
|
|
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
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Skip version of testDevAndDeploy
|
|
339
|
+
*/
|
|
340
|
+
testDevAndDeploy.skip = (name, testFn) => {
|
|
341
|
+
test.skip(`${name} (dev)`, testFn || (() => { }));
|
|
342
|
+
test.skip(`${name} (deployment)`, testFn || (() => { }));
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* Only version of testDevAndDeploy
|
|
346
|
+
*/
|
|
347
|
+
testDevAndDeploy.only = (name, testFn) => {
|
|
348
|
+
if (!SKIP_DEV_SERVER_TESTS) {
|
|
349
|
+
test.only(`${name} (dev)`, async () => {
|
|
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
|
+
}
|
|
374
|
+
};
|
|
375
|
+
/**
|
|
376
|
+
* Utility function for polling/retrying assertions
|
|
377
|
+
*/
|
|
378
|
+
export async function poll(fn, timeout = 5000, interval = 100) {
|
|
379
|
+
const startTime = Date.now();
|
|
380
|
+
while (Date.now() - startTime < timeout) {
|
|
381
|
+
try {
|
|
382
|
+
const result = await fn();
|
|
383
|
+
if (result) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
// Continue polling on errors
|
|
389
|
+
}
|
|
390
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
391
|
+
}
|
|
392
|
+
throw new Error(`Polling timed out after ${timeout}ms`);
|
|
393
|
+
}
|