Comprehensive end-to-end testing utilities with Puppeteer integration for testing both development and deployed applications.
High-level test runners for different environments.
/**
* Tests in development environment only
* @param name - Test name
* @param testLogic - Test function receiving test resources
*/
function testDev(
name: string,
testLogic: (resources: TestResources) => Promise<void>
): void;
/**
* Tests in deployment environment only
* @param name - Test name
* @param testLogic - Test function receiving test resources
*/
function testDeploy(
name: string,
testLogic: (resources: TestResources) => Promise<void>
): void;
/**
* Tests in both development and deployment environments
* @param name - Test name
* @param testLogic - Test function receiving test resources
*/
function testDevAndDeploy(
name: string,
testLogic: (resources: TestResources) => Promise<void>
): void;
/**
* Low-level SDK test runner
* @param name - Test name
* @param testLogic - Test function
*/
function testSDK(
name: string,
testLogic: (resources: TestResources) => Promise<void>
): void;Usage Example:
import { testDevAndDeploy } from 'rwsdk/e2e';
testDevAndDeploy('homepage renders correctly', async ({ page, url }) => {
await page.goto(url);
await page.waitForSelector('h1');
const title = await page.$eval('h1', (el) => el.textContent);
expect(title).toBe('Welcome');
});Functions for launching and managing browser instances.
/**
* Launches a Puppeteer browser instance
* @param options - Browser launch options
* @returns Browser instance
*/
function launchBrowser(options?: {
headless?: boolean;
}): Promise<Browser>;
/**
* Gets the Chrome executable path
* @returns Path to Chrome executable
*/
function getBrowserPath(): Promise<string>;Functions for managing the development server.
/**
* Starts the development server
* @param packageManager - Package manager to use (default: "pnpm")
* @param cwd - Working directory (default: process.cwd())
* @returns Dev server info with URL and stop function
*/
function runDevServer(
packageManager?: PackageManager,
cwd?: string
): Promise<{
url: string;
stopDev: () => Promise<void>;
}>;
/**
* Creates a dev server instance
* @returns Dev server controller
*/
function createDevServer(): {
start: () => Promise<string>;
stop: () => Promise<void>;
};
type PackageManager = "pnpm" | "npm" | "yarn" | "yarn-classic";Functions for setting up test environments.
/**
* Sets up a playground environment for testing
* @param options - Test configuration options
* @returns Test resources
*/
function setupPlaygroundEnvironment(
options?: SetupPlaygroundEnvironmentOptions
): ReturnType;
interface SetupPlaygroundEnvironmentOptions {
projectDir?: string;
packageManager?: PackageManager;
skipDev?: boolean;
skipDeploy?: boolean;
}
type ReturnType = Promise<TestResources>;
/**
* Copies project to temporary directory
* @param projectDir - Source project directory
* @param resourceUniqueKey - Unique key for resources
* @param packageManager - Package manager to use
* @param monorepoRoot - Monorepo root if applicable
* @param installDependenciesRetries - Number of retry attempts
* @returns Temporary directory info with target directory and worker name
*/
function copyProjectToTempDir(
projectDir: string,
resourceUniqueKey: string,
packageManager?: PackageManager,
monorepoRoot?: string,
installDependenciesRetries?: number
): Promise<{
tempDir: tmp.DirectoryResult;
targetDir: string;
workerName: string;
}>;
/**
* Gets all files recursively in a directory
* @param directory - Directory path
* @returns Array of file paths
*/
function getFilesRecursively(directory: string): Promise<string[]>;
/**
* Computes a hash of directory contents
* @param directory - Directory path
* @returns Hash string
*/
function getDirectoryHash(directory: string): Promise<string>;Functions for deploying and managing Cloudflare resources.
/**
* Ensures Cloudflare account ID is configured
* @returns Account ID
*/
function ensureCloudflareAccountId(): Promise<string>;
/**
* Deploys to Cloudflare
* @param options - Release options
* @returns Deployment URL
*/
function runRelease(options: {
workerName: string;
cwd: string;
packageManager?: PackageManager;
}): Promise<string>;
/**
* Creates a deployment instance
* @returns Deployment controller
*/
function createDeployment(): {
deploy: () => Promise<string>;
cleanup: () => Promise<void>;
};
/**
* Deletes a Cloudflare Worker
* @param workerName - Worker name
*/
function deleteWorker(workerName: string): Promise<void>;
/**
* Lists D1 databases
* @returns Array of database objects
*/
function listD1Databases(): Promise<any[]>;
/**
* Deletes a D1 database
* @param databaseId - Database ID
*/
function deleteD1Database(databaseId: string): Promise<void>;
/**
* Checks if a resource belongs to a test
* @param workerName - Worker name
* @param uniqueKey - Unique key
* @returns True if related
*/
function isRelatedToTest(workerName: string, uniqueKey: string): boolean;Function for testing interactive CLI commands.
/**
* Executes interactive CLI commands with expectations
* @param command - Command to execute
* @param args - Command arguments
* @returns Execution result
*/
function $expect(command: string, args: string[]): Promise<{
stdout: string;
stderr: string;
}>;Usage Example:
import { $expect } from 'rwsdk/e2e';
const result = await $expect('npx', ['create-rwsdk', 'my-app']);
console.log(result.stdout);Functions for polling conditions.
/**
* Polls until condition is met
* @param fn - Function that returns true when condition is met
* @param options - Polling options
*/
function poll(
fn: () => boolean | Promise<boolean>,
options?: PollOptions
): Promise<void>;
/**
* Polls and returns value when available
* @param fn - Function that returns value or undefined
* @param options - Polling options
* @returns The value when available
*/
function pollValue<T>(
fn: () => T | undefined | Promise<T | undefined>,
options?: PollOptions
): Promise<T>;
interface PollOptions {
/** Timeout in milliseconds */
timeout?: number;
/** Interval between attempts in milliseconds */
interval?: number;
/** Description for error messages */
description?: string;
}Utilities for working with Puppeteer pages.
/**
* Waits for page hydration to complete
* @param page - Puppeteer page
*/
function waitForHydration(page: Page): Promise<void>;
/**
* Tracks page errors and failed requests
* @param page - Puppeteer page
* @returns Object with error arrays
*/
function trackPageErrors(page: Page): {
errors: Error[];
failedRequests: string[];
};Function for running tests with retry logic.
/**
* Runs a test with automatic retry on failure
* @param testFn - Test function
* @param options - Retry options
*/
function runTestWithRetries(
testFn: () => Promise<void>,
options?: { maxRetries?: number }
): Promise<void>;Shell command executors.
/**
* Execa shell command executor
*/
const $: ExecaFunction;
/**
* Shell-enabled command executor
*/
const $sh: ExecaFunction;Function for testing npm tarballs.
/**
* Sets up environment for tarball testing
* @param options - Setup options
* @returns Test environment info
*/
function setupTarballEnvironment(options: SmokeTestOptions & {
resourceUniqueKey: string;
}): Promise<{
targetDir: string;
workerName: string;
cleanup: () => Promise<void>;
}>;interface TestResources {
/** Puppeteer browser instance */
browser: Browser;
/** Puppeteer page instance */
page: Page;
/** Application URL (dev server or deployment) */
url: string;
/** Test working directory */
cwd: string;
/** Unique key for test resources */
resourceUniqueKey: string;
}
interface SmokeTestOptions {
projectDir: string;
packageManager?: PackageManager;
skipDeploy?: boolean;
skipDev?: boolean;
}const IS_CI: boolean;
const RWSDK_SKIP_DEV: boolean;
const RWSDK_SKIP_DEPLOY: boolean;
const IS_DEBUG_MODE: boolean;
const SETUP_PLAYGROUND_ENV_TIMEOUT: number;
const DEPLOYMENT_TIMEOUT: number;
const DEPLOYMENT_MIN_TRIES: number;
const DEPLOYMENT_CHECK_TIMEOUT: number;
const PUPPETEER_TIMEOUT: number;
const HYDRATION_TIMEOUT: number;
const DEV_SERVER_TIMEOUT: number;
const DEV_SERVER_MIN_TRIES: number;
const SETUP_WAIT_TIMEOUT: number;
const TEST_MAX_RETRIES: number;
const TEST_MAX_RETRIES_PER_CODE: Record<string, number>;
const INSTALL_DEPENDENCIES_RETRIES: number;import {
testDevAndDeploy,
waitForHydration,
trackPageErrors,
} from 'rwsdk/e2e';
describe('RedwoodSDK Application', () => {
testDevAndDeploy('homepage loads and hydrates', async ({ page, url }) => {
const { errors, failedRequests } = trackPageErrors(page);
await page.goto(url);
await waitForHydration(page);
expect(errors).toHaveLength(0);
expect(failedRequests).toHaveLength(0);
const title = await page.title();
expect(title).toBe('My App');
});
testDevAndDeploy('navigation works', async ({ page, url }) => {
await page.goto(url);
await waitForHydration(page);
await page.click('a[href="/about"]');
await page.waitForSelector('h1');
const heading = await page.$eval('h1', (el) => el.textContent);
expect(heading).toBe('About Us');
});
testDevAndDeploy('form submission works', async ({ page, url }) => {
await page.goto(url + '/contact');
await waitForHydration(page);
await page.type('input[name="email"]', 'test@example.com');
await page.type('textarea[name="message"]', 'Hello!');
await page.click('button[type="submit"]');
await page.waitForSelector('.success-message');
const message = await page.$eval(
'.success-message',
(el) => el.textContent
);
expect(message).toContain('Message sent');
});
testDeploy('deployment has correct headers', async ({ page, url }) => {
const response = await page.goto(url);
expect(response?.headers()['x-powered-by']).toBeUndefined();
expect(response?.headers()['server']).toBe('cloudflare');
});
});testDevAndDeploy('server function works', async ({ page, url }) => {
await page.goto(url);
// Call server function from client
const result = await page.evaluate(async () => {
const { getUsers } = await import('./users');
return await getUsers();
});
expect(result).toHaveLength(3);
});testDevAndDeploy('realtime updates work', async ({ page, url, browser }) => {
// Open two pages
const page1 = await browser.newPage();
const page2 = await browser.newPage();
await page1.goto(url);
await page2.goto(url);
// Make change on page1
await page1.click('button.increment');
// Verify update on page2
await page2.waitForFunction(
() => document.querySelector('.count')?.textContent === '1'
);
});testDevAndDeploy('login flow works', async ({ page, url }) => {
await page.goto(url + '/login');
await page.type('input[name="email"]', 'user@example.com');
await page.type('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await page.waitForNavigation();
const cookies = await page.cookies();
const sessionCookie = cookies.find((c) => c.name === 'session_id');
expect(sessionCookie).toBeDefined();
expect(page.url()).toContain('/dashboard');
});