mcpField
S
stably-sdk-rules

stably-sdk-rules

github.com/stablyaiTesting & quality2465 installsUpdated 28w ago
Repository

About stably-sdk-rules

stably-sdk-rules is a usage guide for writing tests with the Stably Playwright SDK, aimed at browser automation developers. It explains when to use standard Playwright, AI-based actions and assertions, and covers email verification and test isolation.

Key features

  1. 01Set rules for Playwright locators and assertions
  2. 02Handle dynamic visual assertions
  3. 03Extract structured data from pages
  4. 04Use AI locators and actions
  5. 05Create isolated test inboxes
  6. 06Handle Google authentication flows

Use cases

  1. 01Test unstable canvas interactions
  2. 02Verify dynamic page content
  3. 03Extract data from pages or emails
  4. 04Test OTP and verification email flows

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/stablyai/agent-skills --skill stably-sdk-rules

After installing, type/stably-sdk-rulesin Claude Code to confirm it loaded.

Bundled filesREADME.md

Details

From the project README

Stably SDK Rules

Quick Rules

  1. Prefer raw Playwright for deterministic actions/assertions (faster + cheaper).
  2. Prioritize reliability over cost when Playwright becomes brittle.
  3. Use agent.act() for canvas, coordinate-based drag/click, or unstable multi-step flows.
  4. Use expect(...).aiAssert() for dynamic visual assertions; keep prompts specific.
  5. Use page.extract() / locator.extract() when you need visual-to-data extraction.
  6. Use page.getLocatorsByAI() when semantic selectors are hard with standard locators.
  7. Use Inbox for OTP/magic-link/verification email flows.
  8. All prompts must be self-contained; never rely on implicit previous context.
  9. Keep agent.act() tasks small; do loops/calculations/conditionals in code.
  10. Use fullPage: true only if content outside viewport matters.
  11. Always add .describe("...") to locators for trace readability.
  12. For email isolation, use unique Inbox.build({ suffix }) per test and clean up.

Setup (Single Block)

npm install -D @playwright/test @stablyai/playwright-test @stablyai/email
export STABLY_API_KEY=YOUR_KEY
export STABLY_PROJECT_ID=YOUR_PROJECT_ID
import { test, expect } from "@stablyai/playwright-test";
import { Inbox } from "@stablyai/email";

Optional: set API key programmatically.

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

Core Rules

  • Locator rule: every locator interaction should use .describe("...").
  • Assertion choice:
    • Use Playwright assertions first.
    • Use aiAssert for dynamic/visual-heavy checks.
  • Interaction choice:
    • Use Playwright for deterministic steps.
    • Use agent.act for brittle or semantic tasks (especially canvas/coordinates).
  • Prompt quality:
    • Include explicit target, intent, and constraints.
    • Pass cross-step data through variables, not vague references.

Minimal Usage Patterns

aiAssert
await expect(page).aiAssert("Shows revenue trend chart and spotlight card");
await expect(page.locator(".header").describe("Header")).aiAssert("Has nav, avatar, and bell icon");

Use fullPage: true only when assertion needs off-screen content.

extract
const orderId = await page.extract("Extract the order ID from the first row");

With schema:

import { z } from "zod";
const Schema = z.object({ revenue: z.string(), users: z.number() });
const metrics = await page.extract("Get revenue and active users", { schema: Schema });
getLocatorsByAI

Requires Playwright >= 1.54.1.

const { locator, count } = await page.getLocatorsByAI("the login button");
expect(count).toBe(1);
await locator.describe("Login button located by AI").click();
agent.act
await agent.act("Find the first pending order and mark it as shipped", { page });

Good pattern: compute values in code, then pass concrete values into the prompt.

Inbox (Email Isolation)

Install: npm install -D @stablyai/email. Requires STABLY_API_KEY and STABLY_PROJECT_ID env vars (or pass to Inbox.build()).

const inbox = await Inbox.build({ suffix: `test-${Date.now()}` });
// inbox.address → "[email protected]"

await page.getByLabel("Email").describe("Email input").fill(inbox.address);

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

await inbox.deleteAllEmails();
Inbox.build Options
OptionTypeDescription
suffixstringSuffix for test isolation (e.g., "test-123" → "[email protected]")
apiKeystringDefaults to STABLY_API_KEY env var
projectIdstringDefaults to STABLY_PROJECT_ID env var

Always use a unique suffix per test for parallel isolation. The inbox automatically filters out emails received before it was created.

waitForEmail
const email = await inbox.waitForEmail({
  from: "[email protected]",    // filter by sender
  subject: "verification",         // contains match by default
  subjectMatch: "exact",           // or "contains" (default)
  timeoutMs: 60_000,               // default: 120000 (2 min)
  pollIntervalMs: 5000,            // default: 3000 (3 sec)
});

Throws EmailTimeoutError if no match arrives within the timeout.

extractFromEmail

Returns { data, reason }. Throws EmailExtractionError on failure.

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

// Structured extraction with Zod schema
import { z } from "zod";
const { data } = await inbox.extractFromEmail({
  id: email.id,
  prompt: "Extract the verification URL and expiration time",
  schema: z.object({ url: z.string().url(), expiresIn: z.string() }),
});
Inbox Properties
PropertyTypeDescription
addressstringFull email address (with suffix if provided)
suffixstring | undefinedThe suffix passed to Inbox.build()
createdAtDateInbox creation time; emails before this are auto-filtered
listEmails
const { emails, nextCursor } = await inbox.listEmails(options?);
OptionTypeDefaultDescription
fromstring—Filter by sender address
subjectstring—Filter by subject
subjectMatch'contains' | 'exact''contains'Subject matching mode
limitnumber20Max results (max: 100)
cursorstring—Pagination cursor from previous nextCursor
sinceDate—Override the default creation-time filter
includeOlderbooleanfalseInclude emails received before inbox creation
Other Methods
const email = await inbox.getEmail(id);                          // get by ID
await inbox.deleteEmail(email.id);                               // delete single
await inbox.deleteAllEmails();                                   // delete all (this inbox only)
Email Object Properties
PropertyTypeDescription
idstringUnique identifier
mailboxstringContainer (e.g., "INBOX")
from{ address: string, name?: string }Sender
to{ address: string, name?: string }[]Recipients
subjectstringSubject line
receivedAtDateArrival timestamp
textstring?Plain text body
htmlstring[]?HTML body parts
Playwright Fixture Pattern
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("signup flow", async ({ page, inbox }) => {
  await page.fill("#email", inbox.address);
  await page.click("#signup");
  const email = await inbox.waitForEmail({ subject: "Welcome" });
  // ...
});
Finding Your Organization's Email Address

Your email address is visible in the Stably dashboard:

  • Settings > Email Inbox: Displays the full address with a copy button
  • In code: inbox.address after calling Inbox.build() returns your full address

The pattern is {org-name}@mail.stably.ai. If the user needs to allowlist, they should add mail.stably.ai to their email provider's allowlist.

Direct users to the dashboard Settings > Email Inbox to find their specific address.

Auth Flows (Google)

Use the helper instead of custom popup scripting:

import { authWithGoogle } from "@stablyai/playwright-test/auth";

await authWithGoogle({
  context,
  email: process.env.GOOGLE_AUTH_EMAIL!,
  password: process.env.GOOGLE_AUTH_PASSWORD!,
  otpSecret: process.env.GOOGLE_AUTH_OTP_SECRET!,
});

Required env vars:

  • GOOGLE_AUTH_EMAIL
  • GOOGLE_AUTH_PASSWORD
  • GOOGLE_AUTH_OTP_SECRET

Use a dedicated test Google account only.

Troubleshooting (Short)

  • aiAssert is slow/flaky: scope to a locator, tighten prompt, avoid unnecessary fullPage: true.
  • agent.act fails: split into smaller tasks, pass explicit constraints, raise maxCycles only when needed.
  • Email timeout: verify subject/from filter and use unique inbox suffixes.

Full References

Comments

Markdown supported