or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

authentication.mdclient-runtime.mddatabase.mde2e-testing.mdindex.mdrealtime.mdrouting.mdsynced-state.mdturnstile.mdvite-plugin.mdworker-runtime.md
tile.json

e2e-testing.mddocs/

E2E Testing

Comprehensive end-to-end testing utilities with Puppeteer integration for testing both development and deployed applications.

Capabilities

Test Runners

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');
});

Browser Management

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>;

Development Server

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";

Environment Setup

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>;

Deployment & Release

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;

Interactive CLI Testing

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);

Polling Utilities

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;
}

Page Utilities

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[];
};

Test Retry

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>;

Command Execution

Shell command executors.

/**
 * Execa shell command executor
 */
const $: ExecaFunction;

/**
 * Shell-enabled command executor
 */
const $sh: ExecaFunction;

Tarball Testing

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>;
}>;

Types

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;
}

Constants

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;

Complete Testing Example

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');
  });
});

Testing Patterns

Testing Server Functions

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);
});

Testing Realtime Features

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'
  );
});

Testing Authentication

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');
});