> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stably.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Create Tests with Cursor / Claude Code

> Accelerate test creation using AI coding assistants with Playwright MCP integration

<Tip>While you can use your own IDE, we encourage you to also check out our [CLI](/stably-cli/quickstart) or [Web editor](/stably2/web-quickstart).</Tip>

Write Stably SDK tests faster and more reliably by leveraging AI coding assistants like Cursor or Claude Code with Playwright MCP (Model Context Protocol). These tools can generate complete, production-ready test suites that take full advantage of Stably's AI capabilities.

## Why Use AI Assistants for Test Creation?

<CardGroup cols={2}>
  <Card title="Faster Development" icon="rocket">
    Generate complete test suites in minutes instead of hours
  </Card>

  <Card title="Best Practices Built-in" icon="stars">
    AI automatically applies Stably SDK patterns like `.describe()` and AI assertions
  </Card>

  <Card title="Context-Aware" icon="brain">
    Playwright MCP gives AI direct access to your browser state and page structure
  </Card>

  <Card title="Reduced Errors" icon="shield-check">
    AI writes tests that leverage auto-heal from day one
  </Card>
</CardGroup>

## Prerequisites

<Steps>
  <Step title="Install Stably SDK">
    Follow the [SDK Setup Guide](/getting-started/sdk-setup-guide) guide to install and configure the Stably SDK in your project.
  </Step>

  <Step title="Choose Your AI Assistant">
    Install one of these AI coding assistants:

    * [Cursor](https://cursor.sh/) — AI-first code editor with deep IDE integration
    * [Claude Code](https://www.anthropic.com/claude) — Standalone AI assistant with MCP support
  </Step>

  <Step title="Configure Playwright MCP">
    Playwright MCP allows the AI to interact with browsers directly, inspect page state, and generate accurate selectors.

    <Tabs>
      <Tab title="Cursor">
        Add to your Cursor settings (`~/.cursor/mcp.json` or via Settings → MCP):

        ```json theme={null}
        {
          "mcpServers": {
            "playwright": {
              "command": "npx",
              "args": ["-y", "@playwright/mcp@latest"]
            }
          }
        }
        ```
      </Tab>

      <Tab title="Claude Code">
        Add to your Claude desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):

        ```json theme={null}
        {
          "mcpServers": {
            "playwright": {
              "command": "npx",
               "args": ["-y", "@playwright/mcp@latest"]
            }
          }
        }
        ```
      </Tab>
    </Tabs>
  </Step>
</Steps>

## Setup AI Rules for Stably SDK

AI coding assistants work best when they understand the specific patterns and capabilities of your testing framework. Configure your assistant with Stably SDK rules:

<Tabs>
  <Tab title="Agent Skill (Recommended)">
    Install the Stably SDK rules skill to automatically configure your AI assistant:

    ```bash theme={null}
    npx skills add stablyai/agent-skills --skill stably-sdk-rules
    ```

    This skill automatically creates the appropriate configuration files:

    * `.cursor/rules/stably-sdk-rules.mdc` for Cursor
    * `CLAUDE.md` for Claude Code
    * `AGENTS.md` for other AI agents

    After installation, your AI assistant will understand Stably SDK patterns and capabilities.

    **Verify Configuration**

    Test that your AI assistant understands Stably SDK by asking:

    > "Generate a test that uses Stably SDK's AI assertion to verify a dashboard page"

    The AI should generate code using `aiAssert()` instead of basic Playwright assertions.
  </Tab>

  <Tab title="Manual Setup">
    <Steps>
      <Step title="Copy the Stably SDK AI Rules">
        ````markdown expandable theme={null}
        # Stably SDK — AI Rules (Playwright‑compatible)

        **Assumption:** Full Playwright parity unless noted below.

        ## Import

        ```ts
        import { test, expect } from "@stablyai/playwright-test";

        // Additional exports available:
        import {
          defineConfig,       // Enhanced defineConfig with stably project support
          stablyReporter,     // Reporter for CI/cloud integration
          setApiKey,          // Programmatic API key configuration
          getDirname,         // ESM __dirname equivalent
          getFilename,        // ESM __filename equivalent
        } from "@stablyai/playwright-test";

        // Type imports for model selection
        import type { AIModel } from "@stablyai/playwright-test";

        // Email inbox testing
        import { Inbox } from "@stablyai/email";
        ```

        ## Install & Setup

        ```bash
        # npm
        npm install -D @playwright/test @stablyai/playwright-test

        # pnpm
        pnpm add -D @playwright/test @stablyai/playwright-test

        # yarn
        yarn add -D @playwright/test @stablyai/playwright-test

        export STABLY_API_KEY=YOUR_KEY
        ```

        ```ts
        import { setApiKey } from "@stablyai/playwright-test";
        setApiKey("YOUR_KEY");
        ```

        ## Environment Variables

        | Variable | Description | Default |
        |----------|-------------|---------|
        | `STABLY_API_KEY` | API key for authentication | (required) |
        | `STABLY_PROJECT_ID` | Project ID for reporter | (required for reporter) |
        | `STABLY_API_URL` | Custom API endpoint | `https://api.stably.ai` |

        Both `STABLY_API_KEY` and `STABLY_PROJECT_ID` are shared across `@stablyai/playwright-test` and `@stablyai/email`.

        ## Model Selection

        All AI methods support an optional `model` parameter to specify which AI model to use:

        ```ts
        import type { AIModel } from "@stablyai/playwright-test";

        // Available models:
        // - "openai/o4-mini" - OpenAI's efficient reasoning model
        // - "google/gemini-3.1-pro-preview" - Google's most capable model
        // - "google/gemini-3-flash-preview" - Google's fast, efficient model
        // - Custom model strings are also supported

        // Example usage with different methods:
        await expect(page).aiAssert("Shows dashboard", { model: "openai/o4-mini" });
        const data = await page.extract("Get heading", { model: "google/gemini-3-flash-preview" });
        const { locator } = await page.getLocatorsByAI("the login button", { model: "google/gemini-3.1-pro-preview" });
        ```

        If no model is specified, the backend default is used.

        ## When to Use Stably SDK vs Playwright

        **Prioritization:**
        1. **Test accuracy and stability are the most important factors** - prioritize reliability over cost/speed.
        2. **Otherwise, use Playwright whenever possible** since it's cheaper and faster.
        3. **For interactions:** If the interaction will be hard to express as Playwright or will be too brittle that way (e.g., the scroll amount changes every time), then use `agent.act()`. **Any canvas-related operations, or any drag/click operations that require coordinates, must use `agent.act()`** (more semantic meaning, and less flaky).
        4. **For assertions:** Use Playwright if it fulfills the purpose. But if the assertion is very visual-heavy, use Stably's `aiAssert`.
        5. **For email verification flows:** Use `@stablyai/email` `Inbox` to receive and extract data from emails (OTP codes, magic links, confirmation emails).
        6. **Use Stably SDK methods if it helps your tests pass** - when Playwright methods are insufficient or unreliable.

        ## AI Assertions (intent‑based visuals)

        > **Note:** `toMatchScreenshotPrompt` is deprecated. Use `aiAssert` instead.

        ```ts
        await expect(page).aiAssert(
          "Shows revenue trend chart and spotlight card",
          { timeout: 30_000 }
        );
        await expect(page.locator(".header"))
          .aiAssert("Nav with avatar and bell icon");
        ```

        **Signature:** `expect(page|locator).aiAssert(prompt: string, options?: ScreenshotOptions & { model?: AIModel })`

        * Use for **dynamic** UIs; keep prompts specific; scope with elements (using locators) when possible.
        * **Consider whether you need `fullPage: true`**: Ask yourself if the assertion requires content beyond the visible viewport (e.g., long scrollable lists, full page layout checks). If only viewport content matters, omit `fullPage: true` — it's faster and cheaper. Use it only when you genuinely need to capture content outside the browser window's visible area.

        ## AI Extraction (visual → data)

        ```ts
        // Extract from entire page
        const txt = await page.extract("List revenue, active users, and churn rate");

        // Extract from specific element (more focused, better results)
        const headerText = await page.locator(".header").extract("Get the username displayed");
        ```

        Typed with Zod:

        ```ts
        import { z } from "zod";
        const Metrics = z.object({ revenue: z.string(), activeUsers: z.number(), churnRate: z.number() });
        const m = await page.extract("Return revenue (currency), active users, churn %", { schema: Metrics });

        // Also works on locators
        const UserSchema = z.object({ name: z.string(), role: z.string() });
        const userData = await page.locator(".user-panel").extract("Get user info", { schema: UserSchema });
        ```

        **Signatures:**

        * `page.extract(prompt: string, options?: { model?: AIModel }): Promise<string>`
        * `page.extract<T extends z.AnyZodObject>(prompt, { schema: T, model?: AIModel }): Promise<z.output<T>>`
        * `locator.extract(prompt: string, options?: { model?: AIModel }): Promise<string>`
        * `locator.extract<T extends z.AnyZodObject>(prompt, { schema: T, model?: AIModel }): Promise<z.output<T>>`

        ## AI Locator Finding (accessibility-based)

        Use `getLocatorsByAI` to find elements using natural language based on the page's accessibility tree. Requires Playwright v1.54.1+.

        ```ts
        // Find a single element
        const { locator: loginBtn, count } = await page.getLocatorsByAI("the login button");
        expect(count).toBe(1);
        await loginBtn.click();

        // Find multiple elements
        const { locator: cards, count: cardCount } = await page.getLocatorsByAI("all product cards in the grid");
        console.log(`Found ${cardCount} product cards`);
        await expect(cards.first()).toBeVisible();

        // With model selection
        const { locator } = await page.getLocatorsByAI("the submit button", {
          model: "google/gemini-3-flash-preview"
        });
        ```

        **Signature:** `page.getLocatorsByAI(prompt: string, options?: { model?: AIModel }): Promise<{ locator: Locator, count: number, reason: string }>`

        **Returns:**
        * `locator` - Playwright Locator for the found elements (matches nothing if count is 0)
        * `count` - Number of elements found
        * `reason` - AI's explanation of what it found and why

        **Best Practices:**
        * Describe elements by their accessible properties (labels, roles, text) rather than visual attributes
        * Use for elements that are hard to locate with traditional selectors
        * Check the `count` to verify expected number of matches before interacting

        ## AI Agent (autonomous workflows)

        ### When Test Generation Uses `agent.act()` vs Raw Playwright

        Stably's test generation agent intelligently chooses between raw Playwright code and `agent.act()`:

        - **For repeatable, stable actions**: The test generation agent will try to turn these into raw Playwright code when first generating tests. Raw Playwright is faster and more cost-effective.
        - **For dynamic or unstable pages**: If the agent finds that the page changes frequently and it can't find stable Playwright selectors/code for an action, it will use `agent.act()` instead.

        This means you may see a mix of both in generated tests—this is intentional and optimizes for reliability while keeping costs down where possible.

        ### Using the Agent Fixture

        Use the `agent` fixture to execute complex, human-like workflows:

        ```ts
        test("complex workflow", async ({ agent, page }) => {
          await page.goto("/orders");
          await agent.act("Find the first pending order and mark it as shipped", { page });
        });

        // Or create manually
        const agent = context.newAgent();
        await agent.act("Your task here", { page, maxCycles: 10 }); // split into smaller steps if possible
        ```

        **Signature:** `agent.act(prompt: string, options: { page: Page, maxCycles?: number, model?: Model }): Promise<void>`

        * Throws `Error` if the agent fails to complete the task (check `error.message` for the AI's failure reason)
        * Default maxCycles: 30
        * Supported models: `"anthropic/claude-sonnet-4-5-20250929"` (default), `"google/gemini-2.5-computer-use-preview-10-2025"`, or any custom model string

        ### Passing Variables to Prompts

        You can use template literals to pass variables into your prompts:

        ```ts
        const duration = 24 * 7 * 60;
        await agent.act(`Enter the duration of ${duration} seconds`, { page });

        const username = "john.doe@example.com";
        await agent.act(`Login with username ${username}`, { page });
        ```

        ### Self-Contained Prompts

        All prompts to Stably SDK AI methods (agent.act, aiAssert, extract) must be self-contained with all necessary information:

        1. **No implicit references to outside context** - prompts cannot reference previous actions or state that the AI method doesn't have access to:
           - ❌ Bad: `agent.act("Verify the field you just filled in the form is 4", { page })`
           - ✅ Good: `agent.act("Verify the 'timeout' field in the form has value 4", { page })`
           - ❌ Bad: `agent.act("Pick something that's not in the previous step", { page })`
           - ✅ Good: `const selectedItem = "Option A"; await agent.act(\`Pick an option other than ${selectedItem}\`, { page })`

        2. **Pass information between AI methods using explicit variables:**
           ```ts
           // Extract data, then use it in next action
           const orderId = await page.extract("Get the order ID from the first row");
           await agent.act(`Cancel order with ID ${orderId}`, { page });
           ```

        3. **Include detailed instructions and domain knowledge** to help the AI perform the task successfully:
           - ❌ Bad: `agent.act("Fill in the form", { page })`
           - ✅ Good: `agent.act("Fill in the form with test data. On page 4 you might run into a popup asking for premium features - just click 'Skip' or 'Cancel' to ignore it", { page })`

        ### Optimizing Agent Performance

        **IMPORTANT:** The fewer actions/cycles agent.act() needs to do, the better it performs. Offload work to Playwright code when possible:

        1. If your prompt has work that could be done by Playwright code, use Playwright for that work, and only use agent.act() for actions that are hard for Playwright (canvas operations, dynamic decision making, etc.)
        2. If your prompt has repetition (e.g., do it 5 times), calculations (e.g., type 24*7*60 seconds), or other code-suitable tasks, use code for those, and only have agent.act() perform the agent-suitable part.
        3. If your prompt has an if/else condition that can be expressed in code, use code for the condition, and only have agent.act() perform the agent-suitable part.

        **Examples:**
        - ❌ Bad: `"Click the button 5 times"`
        - ✅ Good: `"Click the button"` (and include this in a loop that runs 5 times)
        - ❌ Bad: `"enter the duration of 24*7*60 seconds"`
        - ✅ Good: Calculate in code (`const sum = 24*7*60`), then use `\`enter the duration of ${sum} seconds\``

        ## Email Inbox (`@stablyai/email`)

        The `@stablyai/email` package provides disposable email inboxes for testing email-dependent flows (OTP codes, verification links, order confirmations, etc.).

        ### Install

        ```bash
        # npm
        npm install -D @stablyai/email

        # pnpm
        pnpm add -D @stablyai/email

        # yarn
        yarn add -D @stablyai/email
        ```

        Requires `STABLY_API_KEY` and `STABLY_PROJECT_ID` environment variables (same as `@stablyai/playwright-test`).

        ### Creating an Inbox

        ```ts
        import { Inbox } from "@stablyai/email";

        const inbox = await Inbox.build({ suffix: `test-${Date.now()}` });
        // Generates address like: "org+test-1706621234567@mail.stably.ai"

        console.log(inbox.address);   // Full email address
        console.log(inbox.suffix);    // Applied suffix
        console.log(inbox.createdAt); // Inbox creation timestamp
        ```

        **`Inbox.build()` Parameters:**

        | Parameter | Type | Description |
        |-----------|------|-------------|
        | `suffix` | string (optional) | Test isolation identifier appended to address |
        | `apiKey` | string (optional) | Overrides `STABLY_API_KEY` env var |
        | `projectId` | string (optional) | Overrides `STABLY_PROJECT_ID` env var |

        ### Waiting for Emails

        ```ts
        const email = await inbox.waitForEmail({
          from: "noreply@example.com",
          subject: "verification",
          timeoutMs: 60_000,
        });
        ```

        **`inbox.waitForEmail()` Parameters:**

        | Parameter | Type | Default | Description |
        |-----------|------|---------|-------------|
        | `from` | string | — | Sender address filter |
        | `subject` | string | — | Subject line filter |
        | `subjectMatch` | `'contains'` \| `'exact'` | `'contains'` | Subject matching mode |
        | `timeoutMs` | number | `120000` | Max wait duration (ms) |
        | `pollIntervalMs` | number | `3000` | Poll frequency (ms) |

        Returns an `Email` object or throws `EmailTimeoutError`. Only checks emails received **after** the inbox was created.

        ### Email Object Properties

        | Property | Type | Description |
        |----------|------|-------------|
        | `id` | string | Unique identifier |
        | `mailbox` | string | Container (e.g., `"INBOX"`) |
        | `from` | `{ address, name }` | Sender info |
        | `to` | array | Recipients |
        | `subject` | string | Subject line |
        | `receivedAt` | Date | Delivery timestamp |
        | `text` | string? | Plain text body |
        | `html` | array? | HTML body parts |

        ### AI Extraction from Emails

        Use `inbox.extractFromEmail()` to extract structured data from emails using AI:

        **String extraction:**
        ```ts
        const { data: otp, reason } = await inbox.extractFromEmail({
          id: email.id,
          prompt: "Extract the 6-digit OTP code",
        });
        ```

        **Structured extraction with Zod:**
        ```ts
        import { z } from "zod";

        const { data } = await inbox.extractFromEmail({
          id: email.id,
          prompt: "Extract verification URL and expiration",
          schema: z.object({
            url: z.string().url(),
            expiresIn: z.string(),
          }),
        });
        ```

        **`inbox.extractFromEmail()` Parameters:**
        - `id` (required): Email identifier
        - `prompt` (required): Extraction instruction
        - `schema` (optional): Zod schema for structured output

        Returns `{ data, reason }` or throws `EmailExtractionError`.

        ### Listing & Retrieving Emails

        ```ts
        // List emails with optional filters
        const { emails, nextCursor } = await inbox.listEmails({
          from: "notifications@example.com",
          limit: 10,
        });

        // Get a specific email by ID
        const email = await inbox.getEmail(emailId);
        ```

        **`inbox.listEmails()` Parameters:**

        | Parameter | Type | Default | Description |
        |-----------|------|---------|-------------|
        | `from` | string | — | Sender filter |
        | `subject` | string | — | Subject filter |
        | `subjectMatch` | `'contains'` \| `'exact'` | `'contains'` | Matching mode |
        | `limit` | number | `20` (max 100) | Result count |
        | `cursor` | string | — | Pagination cursor |
        | `since` | Date | — | Override default time filter |
        | `includeOlder` | boolean | `false` | Include pre-creation emails |

        ### Cleanup

        ```ts
        await inbox.deleteEmail(email.id);  // Delete single email
        await inbox.deleteAllEmails();       // Delete all emails in this inbox
        ```

        Only deletes inbox-scoped emails, not the entire org mailbox.

        ### Playwright Fixture Pattern

        Create a reusable `inbox` fixture for test isolation and automatic cleanup:

        ```ts
        import { test as base } from "@stablyai/playwright-test";
        import { Inbox } from "@stablyai/email";

        const test = base.extend<{ inbox: Inbox }>({
          inbox: async ({}, use, testInfo) => {
            const inbox = await Inbox.build({ suffix: `test-${testInfo.testId}` });
            await use(inbox);
            await inbox.deleteAllEmails();
          },
        });

        test("OTP login flow", async ({ page, inbox }) => {
          await page.goto("/login");
          await page.getByLabel("Email").describe("Email input").fill(inbox.address);
          await page.getByRole("button", { name: "Send OTP" }).describe("Send OTP button").click();

          const email = await inbox.waitForEmail({
            subject: "verification code",
            timeoutMs: 60_000,
          });

          const { data: otp } = await inbox.extractFromEmail({
            id: email.id,
            prompt: "Extract the 6-digit OTP code",
          });

          await page.getByLabel("OTP").describe("OTP input").fill(otp);
          await page.getByRole("button", { name: "Verify" }).describe("Verify button").click();
          await expect(page).toHaveURL("/dashboard");
        });
        ```

        ### Email Testing Best Practices

        * **Always use unique suffixes** (e.g., `testInfo.testId` or `Date.now()`) for parallel test isolation.
        * **Use the fixture pattern** for automatic cleanup after each test.
        * **Keep `timeoutMs` reasonable** — default is 120s, but 60s is usually sufficient.
        * **Prefer `waitForEmail`** over polling with `listEmails` — it handles retry logic automatically.
        * **Use structured extraction with Zod schemas** when you need typed data (URLs, codes, dates).
        * **Common extraction prompts:**
          - `"Extract the OTP or verification code"`
          - `"Extract the sign-in or verification URL"`
          - `"Extract the order number and delivery date"`

        ## CI Reporter / Cloud

        ```bash
        # npm
        npm install -D @stablyai/playwright-test

        # pnpm
        pnpm add -D @stablyai/playwright-test

        # yarn
        yarn add -D @stablyai/playwright-test
        ```

        ```ts
        // playwright.config.ts
        import { defineConfig, stablyReporter } from "@stablyai/playwright-test";

        export default defineConfig({
          reporter: [
            ["list"],
            stablyReporter({
              apiKey: process.env.STABLY_API_KEY,
              projectId: process.env.STABLY_PROJECT_ID,
              // Optional: Scrub sensitive values from traces before upload
              sensitiveValues: ["secret-password", process.env.API_SECRET].filter(Boolean),
            }),
          ],
          use: {
            trace: "on", // Required for trace uploads
          },
        });
        ```

        **Reporter Options:**
        - `apiKey` (required): Your Stably API key
        - `projectId` (required): Your Stably project ID
        - `sensitiveValues` (optional): Array of strings to scrub from trace files
        - `notificationConfigs` (optional): Per-project notification settings for Slack/email

        ## Notifications (Email/Slack)

        Configure notifications per project via `stably` property in defineConfig:

        ```ts
        import { defineConfig, stablyReporter } from "@stablyai/playwright-test";

        export default defineConfig({
          reporter: [stablyReporter({ apiKey: "...", projectId: "..." })],
          projects: [
            {
              name: "smoke",
              stably: {
                notifications: {
                  slack: {
                    channelName: "#test-alerts",
                    notifyOnStart: true,
                    notifyOnResult: "failures-only", // "all" | "failures-only"
                  },
                  email: {
                    to: ["team@example.com"],
                    notifyOnResult: "all",
                  },
                },
              },
            },
          ],
        });
        ```

        ## Commands

        ```bash
        # Recommended for Stably reporter + auto-heal
        # npm
        npx stably test
        # pnpm
        pnpm dlx stably test
        # yarn berry
        yarn dlx stably test

        # Still supported (requires your reporter/config to be set up)
        # npm
        npm exec playwright test
        # pnpm
        pnpm exec playwright test
        # yarn
        yarn playwright test
        # All Playwright CLI flags still work (headed, ui, project, file filters…)

        # When running tests for debugging/getting stacktraces:
        npm exec playwright test --reporter=list  # or pm-equivalent; disable HTML reporter for direct terminal output
        ```

        ## ESM Utilities

        For ESM projects needing `__dirname` or `__filename` equivalents:

        ```ts
        import { getDirname, getFilename } from "@stablyai/playwright-test";

        const __dirname = getDirname(import.meta.url);
        const __filename = getFilename(import.meta.url);

        // Use in tests for file paths
        import path from "path";
        await page.setInputFiles("input", path.join(__dirname, "fixtures", "file.pdf"));
        ```

        ## Best Practices

        * **CRITICAL: All locators must use the `.describe()` method** for readability in trace views and test reports. Example: `page.getByRole('button', { name: 'Submit' }).describe('Submit button')` or `page.locator('table tbody tr').first().describe('First table row')`
        * Scope visual checks with locators; keep prompts specific with labels/units.
        * Use `toHaveScreenshot` for stable pixel‑perfect UIs; `aiAssert` for dynamic UIs.
        * **Be deliberate with `fullPage: true`**: Default to viewport-only screenshots. Only use `fullPage: true` when your assertion genuinely requires content beyond the visible viewport (e.g., verifying footer content on a long page, checking full scrollable lists). Viewport captures are faster and more cost-effective.

        ## Troubleshooting

        * **Slow assertions** → scope visuals; reduce viewport.
        * **Agent stops early** → increase `maxCycles` or break task into smaller steps.

        ## Minimal Template

        ```ts
        import { test, expect } from "@stablyai/playwright-test";

        test("AI‑enhanced dashboard", async ({ page, agent }) => {
          await page.goto("/dashboard");

          // Use agent for complex workflows
          await agent.act("Navigate to settings and enable notifications", { page });

          // Use AI assertions for dynamic content
          await expect(page).aiAssert(
            "Dashboard shows revenue chart (>= 6 months) and account spotlight card"
          );
        });
        ```
        ````
      </Step>

      <Step title="Add Rules to Your AI Assistant">
        <Tabs>
          <Tab title="Cursor">
            Add the AI rules through Cursor's Rules feature:

            1. Open Cursor Settings → Rules (or press `Cmd/Ctrl + Shift + J`)
            2. Create a new `stably-sdk-rules.mdc` rule file
            3. Paste the AI rules content
            4. Configure when to apply the rule:
               * Set appropriate file globs (e.g., `**/*.spec.ts`, `**/tests/**/*.ts`)
               * Choose whether to always apply or apply based on file patterns

            <Note>
              Cursor rules support project-specific and global configurations. You can create multiple rule files and control their scope using globs and the `alwaysApply` setting.
            </Note>

            **Cursor Commands (New Feature)**

            Cursor now supports custom commands that can be triggered with `/` in the chat. Create reusable test generation workflows:

            1. Create a `.cursor/commands` directory in your project root
            2. Add a `create-e2e-test.md` file with your test generation instructions
            3. Use `/create-e2e-test` in Cursor chat to generate tests instantly

            <Tip>
              The automated setup script (in the SDK Setup Guide) automatically creates a test generation command for you. You can also create team-wide commands in the [Cursor Dashboard](https://cursor.com/dashboard?tab=team-content\&section=commands) for your entire organization.
            </Tip>
          </Tab>

          <Tab title="Claude Code">
            **Option 1: Project-level configuration**

            Create a `claude.md` file in your project root:

            ```bash theme={null}
            # In your project root
            touch claude.md
            ```

            Paste the AI rules into this file. Claude will use these as custom instructions when working on your project.

            **Option 2: Conversation instructions**

            Start each testing conversation by pasting the AI rules as context.
          </Tab>
        </Tabs>

        <Note>
          After adding rules to your AI assistant, restart it or start a new conversation to ensure the rules are loaded.
        </Note>
      </Step>

      <Step title="Verify Configuration">
        Test that your AI assistant understands Stably SDK by asking:

        > "Generate a test that uses Stably SDK's AI assertion to verify a dashboard page"

        The AI should generate code using `aiAssert()` instead of basic Playwright assertions.
      </Step>
    </Steps>
  </Tab>
</Tabs>

## Creating Tests with AI

### Basic Workflow

<Steps>
  <Step title="Navigate to the Page">
    Use Playwright MCP to open the page you want to test:

    Ask the AI:

    > "Open a browser and navigate to [https://app.example.com/dashboard](https://app.example.com/dashboard)"

    The AI will use Playwright MCP to launch a browser and navigate to the page.
  </Step>

  <Step title="Explore the Page">
    Ask the AI to inspect elements and understand the page structure:

    > "What elements are visible on this page? Show me the main navigation and action buttons"

    The AI uses MCP to capture page snapshots and identify interactive elements.
  </Step>

  <Step title="Generate Test Code">
    Describe the user flow you want to test:

    > "Generate a Stably SDK test that:
    >
    > 1. Logs into the app with test credentials
    > 2. Clicks the 'Create Project' button
    > 3. Fills in the project form
    > 4. Uses an AI assertion to verify the success message appears"

    The AI generates production-ready code using Stably SDK patterns.
  </Step>

  <Step title="Refine and Iterate">
    Review the generated test and refine as needed:

    > "Add error handling for network timeouts and use .describe() on the submit button locator"

    The AI updates the test with improvements.
  </Step>
</Steps>

### Example: Generated Test

Here's an example of a test generated by an AI assistant with Stably SDK rules:

```typescript theme={null}
import { test, expect } from "@stablyai/playwright-test";

test("create project flow with AI validation", async ({ page }) => {
  // Navigate to the app
  await page.goto("https://app.example.com");
  
  // Login with test credentials
  await page.getByLabel("Email").fill("test@example.com");
  await page.getByLabel("Password").fill("TestPassword123");
  await page.getByRole("button", { name: "Sign In" })
    .describe("Login submit button")
    .click();
  
  // Wait for dashboard to load
  await expect(page).toHaveURL(/.*dashboard/);
  
  // Click create project button
  await page.getByRole("button", { name: "Create Project" })
    .describe("Main CTA to create new project")
    .click();
  
  // Fill project form
  await page.getByLabel("Project Name").fill("E2E Test Project");
  await page.getByLabel("Description").fill("Automated test project");
  await page.getByRole("combobox", { name: "Project Type" })
    .describe("Project type dropdown")
    .selectOption("Web Application");
  
  // Submit form
  await page.getByRole("button", { name: "Create" })
    .describe("Project creation submit button")
    .click();
  
  // Use AI assertion to verify success
  await expect(page).aiAssert(
    "Success message showing 'Project created successfully' with green checkmark icon",
    { timeout: 30_000 }
  );
  
  // Verify project appears in list
  await page.getByRole("link", { name: "Projects" })
    .describe("Navigation link to projects list")
    .click();
  
  await expect(page.getByRole("heading", { name: "E2E Test Project" }))
    .toBeVisible();
});
```

<Note>
  The AI automatically:

  * Uses `.describe()` on critical locators for auto-heal
  * Applies `aiAssert()` for dynamic UI validation
  * Includes proper waits and navigation checks
  * Follows Playwright best practices
</Note>

## Advanced Use Cases

### Multi-Step User Flows

Generate complex, multi-page flows by describing the complete journey:

```
Generate a Stably SDK test for the complete checkout flow:
1. Browse product catalog and add 3 items to cart
2. Proceed to checkout
3. Fill shipping information form
4. Select payment method
5. Use AI assertion to verify order summary shows correct total
6. Complete purchase
7. Use AI extraction to get the order number
8. Verify confirmation page with order number
```

### Data-Driven Tests

Generate tests that use extracted data for validation:

```
Create a test that:
1. Navigates to the analytics dashboard
2. Uses AI extraction to get revenue, active users, and churn rate from the page
3. Validates that revenue is greater than $10,000
4. Validates that churn rate is below 5%
5. Screenshots the trends chart if validation passes
```

Example generated code:

```typescript theme={null}
import { test, expect } from "@stablyai/playwright-test";
import { z } from "zod";

const MetricsSchema = z.object({
  revenue: z.number(),
  activeUsers: z.number(),
  churnRate: z.number()
});

test("validate analytics dashboard metrics", async ({ page }) => {
  await page.goto("/analytics/dashboard");
  
  // Extract metrics using AI
  const metrics = await page.extract(
    "Return revenue (as number), active users (as number), and churn rate (as percentage number)",
    { schema: MetricsSchema }
  );
  
  // Validate metrics
  expect(metrics.revenue).toBeGreaterThan(10000);
  expect(metrics.churnRate).toBeLessThan(5);
  
  console.log(`Validation passed:`, {
    revenue: `$${metrics.revenue.toLocaleString()}`,
    activeUsers: metrics.activeUsers.toLocaleString(),
    churnRate: `${metrics.churnRate}%`
  });
  
  // Verify the trends chart renders correctly
  await expect(page.locator(".trends-chart"))
    .aiAssert("Revenue trend chart showing last 6 months of data");
});
```

### Visual Regression Testing

Generate comprehensive visual tests with AI assertions:

```
Create a visual regression test suite for the marketing landing page that:
1. Checks hero section with CTA button and value proposition
2. Validates features section shows all 6 feature cards
3. Verifies pricing table with 3 tiers
4. Checks footer has social links and newsletter signup
Use AI assertions for all checks to handle dynamic content
```

## Best Practices

<AccordionGroup>
  <Accordion title="Be Specific in Your Prompts">
    The more detailed your description, the better the generated test. Include:

    * Exact labels and button text
    * Expected outcomes and error states
    * Data formats and validation rules
    * Whether to use AI assertions vs. standard assertions
  </Accordion>

  <Accordion title="Leverage MCP for Selector Discovery">
    Instead of manually finding selectors, ask the AI:

    * "What's the best selector for the submit button on this form?"
    * "Show me all clickable elements in the header"
    * "Find the selector for the error message container"

    MCP allows the AI to inspect the live page and suggest robust selectors.
  </Accordion>

  <Accordion title="Use AI Assertions Strategically">
    Guide the AI on when to use different assertion types:

    * **Standard assertions** (`toBeVisible()`, `toHaveText()`) for stable, predictable elements
    * **AI assertions** (`aiAssert()`) for dynamic content, personalized UIs, or complex layouts
    * **AI extraction** (`page.extract()`) when you need to validate computed values or extract data for later use
  </Accordion>

  <Accordion title="Iterate on Generated Tests">
    AI-generated tests are a starting point. Refine them by asking:

    * "Add error handling for network failures"
    * "Make the login reusable as a fixture"
    * "Add .describe() to locators that might break"
    * "Include comments explaining the test logic"
  </Accordion>

  <Accordion title="Review and Validate">
    Always review generated tests:

    * Run the test to verify it works
    * Check that locators are resilient (using `getByRole`, `getByLabel`, etc.)
    * Ensure proper wait conditions
    * Verify timeout values are reasonable
    * Check that API keys and secrets are not hardcoded
  </Accordion>
</AccordionGroup>

## Troubleshooting

<AccordionGroup>
  <Accordion title="AI is generating basic Playwright code without Stably features">
    **Solution:** Ensure AI rules are properly configured. Try:

    1. Verify `stably-sdk-rules.mdc` or `claude.md` file exists in project root
    2. Restart your AI assistant to reload configuration
    3. Explicitly mention in your prompt: "Use Stably SDK patterns with .describe() and aiAssert()"
  </Accordion>

  <Accordion title="Playwright MCP is not working">
    **Solution:**

    1. Verify MCP configuration is correct in settings JSON
    2. Restart your AI assistant
    3. Check that `@playwright/mcp@latest` can be installed:
       ```bash theme={null}
       npx -y @playwright/mcp@latest --help
       ```
    4. Look for MCP connection errors in your assistant's console/logs
  </Accordion>

  <Accordion title="Generated selectors are too fragile">
    **Solution:** Ask the AI to use more semantic selectors:

    * "Add .describe() to all action locators"
    * "Use getByRole instead of CSS selectors"
    * "Prefer getByLabel for form inputs"
    * "Add test-id attributes to critical elements and use getByTestId"
    * "Use viewport screenshots instead of fullPage: true"
  </Accordion>

  <Accordion title="Tests are too slow">
    **Solution:** Optimize generated tests:

    * "Reduce timeout values where possible"
    * "Remove unnecessary waits"
    * "Use viewport screenshots instead of fullPage: true"
    * "Combine multiple aiAssert() in a single one where logical"
  </Accordion>

  <Accordion title="AI is overusing aiAssert()">
    **Solution:** Guide the AI on assertion selection:

    * "Use aiAssert() only for dynamic content that can't be validated with standard assertions"
    * "Prefer toHaveText() and toBeVisible() for stable, predictable elements"
    * "Reserve AI assertions for visually complex validations"
  </Accordion>
</AccordionGroup>

## Example Prompts

### For Complete Test Suites

> "Generate a complete test suite for our e-commerce site covering: product search, add to cart, checkout, and order confirmation. Use Stably SDK with AI assertions for the product grid and checkout summary. Include data extraction for order total validation."

### For Form Testing

> "Create a Stably SDK test for the user registration form. Validate all fields, test error messages, and use an AI assertion to verify the success modal appears after submission. Add .describe() to all form input locators."

### For Visual Testing

> "Generate visual regression tests for our component library. Test button variants, card layouts, and navigation menus. Use aiAssert() for each component and scope with locators."

### For API + UI Testing

> "Write a test that creates a project via API, then verifies it appears correctly in the UI. Extract the project ID from the API response and use it to navigate to the project detail page. Use AI assertion to verify the project details render correctly."

## Next Steps

<CardGroup cols={2}>
  <Card title="AI Assertions Guide" icon="sparkles" href="/stably-sdk/ai-assertions">
    Deep dive into AI-powered visual assertions for dynamic content
  </Card>

  <Card title="AI Extraction" icon="database" href="/stably-sdk/ai-extraction">
    Learn to extract structured data from pages using AI
  </Card>

  <Card title="Run Tests in CI" icon="rocket" href="/run-tests/run-tests-on-github">
    Set up continuous testing with GitHub Actions
  </Card>
</CardGroup>

## Additional Resources

* [Cursor Documentation](https://cursor.sh/docs)
* [Claude Code MCP Guide](https://docs.anthropic.com/claude/docs/model-context-protocol)
* [Playwright MCP Server](https://github.com/microsoft/playwright-mcp)
* [Stably SDK Overview](/stably-sdk/overview)
