stably-sdk-rules

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Stably SDK Rules

Stably SDK 规则

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.
  1. 对于确定性操作/断言,优先使用原生Playwright(更快且成本更低)。
  2. 当Playwright变得不稳定时,优先考虑可靠性而非成本。
  3. 对于画布、基于坐标的拖拽/点击或不稳定的多步骤流程,使用
    agent.act()
  4. 对于动态视觉断言,使用
    expect(...).aiAssert()
    ;提示语需具体明确。
  5. 当需要从视觉内容提取数据时,使用
    page.extract()
    /
    locator.extract()
  6. 当标准定位器难以实现语义选择时,使用
    page.getLocatorsByAI()
  7. 对于OTP/魔法链接/验证邮件流程,使用
    Inbox
  8. 所有提示语必须独立完整;切勿依赖隐含的上下文信息。
  9. agent.act()
    的任务需拆分得尽量小;循环、计算、条件判断逻辑放在代码中实现。
  10. 仅当视口外的内容对测试有影响时,才使用
    fullPage: true
  11. 始终为定位器添加
    .describe("...")
    ,以提升追踪日志的可读性。
  12. 为实现邮件隔离,每个测试使用唯一的
    Inbox.build({ suffix })
    ,并在测试后清理。

Setup (Single Block)

配置(单块代码)

bash
npm install -D @playwright/test @stablyai/playwright-test @stablyai/email
export STABLY_API_KEY=YOUR_KEY
export STABLY_PROJECT_ID=YOUR_PROJECT_ID
ts
import { test, expect } from "@stablyai/playwright-test";
import { Inbox } from "@stablyai/email";
Optional: set API key programmatically.
ts
import { setApiKey } from "@stablyai/playwright-test";
setApiKey("YOUR_KEY");
bash
npm install -D @playwright/test @stablyai/playwright-test @stablyai/email
export STABLY_API_KEY=YOUR_KEY
export STABLY_PROJECT_ID=YOUR_PROJECT_ID
ts
import { test, expect } from "@stablyai/playwright-test";
import { Inbox } from "@stablyai/email";
可选:通过代码设置API密钥。
ts
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.
  • 定位器规则:所有定位器交互都应使用
    .describe("...")
  • 断言选择:
    • 优先使用Playwright原生断言。
    • 对于动态/视觉密集型检查,使用
      aiAssert
  • 交互选择:
    • 对于确定性步骤,使用Playwright。
    • 对于不稳定或语义化任务(尤其是画布/坐标操作),使用
      agent.act
  • 提示语质量:
    • 包含明确的目标、意图和约束条件。
    • 通过变量传递跨步骤数据,而非模糊的引用。

Minimal Usage Patterns

极简使用模式

aiAssert

aiAssert

ts
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.
ts
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");
仅当断言需要检查屏幕外内容时,才使用
fullPage: true

extract

extract

ts
const orderId = await page.extract("Extract the order ID from the first row");
With schema:
ts
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 });
ts
const orderId = await page.extract("Extract the order ID from the first row");
结合Schema使用:
ts
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

getLocatorsByAI

Requires Playwright
>= 1.54.1
.
ts
const { locator, count } = await page.getLocatorsByAI("the login button");
expect(count).toBe(1);
await locator.describe("Login button located by AI").click();
要求Playwright版本
>= 1.54.1
ts
const { locator, count } = await page.getLocatorsByAI("the login button");
expect(count).toBe(1);
await locator.describe("Login button located by AI").click();

agent.act

agent.act

ts
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.
ts
await agent.act("Find the first pending order and mark it as shipped", { page });
推荐模式:在代码中计算数值,然后将具体值传入提示语。

Inbox
(Email Isolation)

Inbox
(邮件隔离)

Install:
npm install -D @stablyai/email
. Requires
STABLY_API_KEY
and
STABLY_PROJECT_ID
env vars (or pass to
Inbox.build()
).
ts
const inbox = await Inbox.build({ suffix: `test-${Date.now()}` });
// inbox.address → "my-org+test-1706621234567@mail.stably.ai"

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();
安装:
npm install -D @stablyai/email
。需要配置环境变量
STABLY_API_KEY
STABLY_PROJECT_ID
(也可传入
Inbox.build()
)。
ts
const inbox = await Inbox.build({ suffix: `test-${Date.now()}` });
// inbox.address → "my-org+test-1706621234567@mail.stably.ai"

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

Inbox.build 选项

OptionTypeDescription
suffix
stringSuffix for test isolation (e.g.,
"test-123"
"org+test-123@mail.stably.ai"
)
apiKey
stringDefaults to
STABLY_API_KEY
env var
projectId
stringDefaults 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.
选项类型描述
suffix
string用于测试隔离的后缀(例如:
"test-123"
"org+test-123@mail.stably.ai"
apiKey
string默认使用环境变量
STABLY_API_KEY
projectId
string默认使用环境变量
STABLY_PROJECT_ID
为实现并行隔离,每个测试必须使用唯一的
suffix
。收件箱会自动过滤掉其创建之前收到的邮件。

waitForEmail

waitForEmail

ts
const email = await inbox.waitForEmail({
  from: "noreply@example.com",    // 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.
ts
const email = await inbox.waitForEmail({
  from: "noreply@example.com",    // 按发件人过滤
  subject: "verification",         // 默认包含匹配
  subjectMatch: "exact",           // 可选"contains"(默认值)
  timeoutMs: 60_000,               // 默认值:120000(2分钟)
  pollIntervalMs: 5000,            // 默认值:3000(3秒)
});
如果在超时时间内未找到匹配邮件,会抛出
EmailTimeoutError

extractFromEmail

extractFromEmail

Returns
{ data, reason }
. Throws
EmailExtractionError
on failure.
ts
// 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() }),
});
返回
{ data, reason }
。提取失败时抛出
EmailExtractionError
ts
// 字符串提取
const { data: otp } = await inbox.extractFromEmail({
  id: email.id,
  prompt: "Extract the 6-digit OTP code",
});

// 结合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

Inbox 属性

PropertyTypeDescription
address
stringFull email address (with suffix if provided)
suffix
string | undefinedThe suffix passed to
Inbox.build()
createdAt
DateInbox creation time; emails before this are auto-filtered
属性类型描述
address
string完整邮箱地址(如果提供了后缀则包含后缀)
suffix
string | undefined传入
Inbox.build()
的后缀
createdAt
Date收件箱创建时间;在此时间之前的邮件会被自动过滤

listEmails

listEmails

ts
const { emails, nextCursor } = await inbox.listEmails(options?);
OptionTypeDefaultDescription
from
stringFilter by sender address
subject
stringFilter by subject
subjectMatch
'contains'
|
'exact'
'contains'
Subject matching mode
limit
number20Max results (max: 100)
cursor
stringPagination cursor from previous
nextCursor
since
DateOverride the default creation-time filter
includeOlder
booleanfalseInclude emails received before inbox creation
ts
const { emails, nextCursor } = await inbox.listEmails(options?);
选项类型默认值描述
from
string按发件人地址过滤
subject
string按主题过滤
subjectMatch
'contains'
|
'exact'
'contains'
主题匹配模式
limit
number20最大结果数(上限:100)
cursor
string来自上一次调用
nextCursor
的分页游标
since
Date覆盖默认的创建时间过滤条件
includeOlder
booleanfalse是否包含收件箱创建之前收到的邮件

Other Methods

其他方法

ts
const email = await inbox.getEmail(id);                          // get by ID
await inbox.deleteEmail(email.id);                               // delete single
await inbox.deleteAllEmails();                                   // delete all (this inbox only)
ts
const email = await inbox.getEmail(id);                          // 通过ID获取邮件
await inbox.deleteEmail(email.id);                               // 删除单封邮件
await inbox.deleteAllEmails();                                   // 删除当前收件箱的所有邮件

Email Object Properties

邮件对象属性

PropertyTypeDescription
id
stringUnique identifier
mailbox
stringContainer (e.g.,
"INBOX"
)
from
{ address: string, name?: string }
Sender
to
{ address: string, name?: string }[]
Recipients
subject
stringSubject line
receivedAt
DateArrival timestamp
text
string?Plain text body
html
string[]?HTML body parts
属性类型描述
id
string唯一标识符
mailbox
string邮件容器(例如:
"INBOX"
from
{ address: string, name?: string }
发件人信息
to
{ address: string, name?: string }[]
收件人列表
subject
string邮件主题
receivedAt
Date邮件接收时间戳
text
string?纯文本邮件内容
html
string[]?HTML格式的邮件内容片段

Playwright Fixture Pattern

Playwright Fixture 模式

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("signup flow", async ({ page, inbox }) => {
  await page.fill("#email", inbox.address);
  await page.click("#signup");
  const email = await inbox.waitForEmail({ subject: "Welcome" });
  // ...
});
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("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.
你的邮箱地址可在Stably控制台中查看:
  • 设置 > 邮件收件箱:显示完整地址及复制按钮
  • 代码中:调用
    Inbox.build()
    后,
    inbox.address
    会返回完整地址
地址格式为
{org-name}@mail.stably.ai
。如果需要添加白名单,应将
mail.stably.ai
添加到邮件服务商的白名单中。
引导用户前往控制台的“设置 > 邮件收件箱”页面查找具体地址。

Auth Flows (Google)

认证流程(Google)

Use the helper instead of custom popup scripting:
ts
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.
使用辅助工具而非自定义弹窗脚本:
ts
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!,
});
所需环境变量:
  • GOOGLE_AUTH_EMAIL
  • GOOGLE_AUTH_PASSWORD
  • GOOGLE_AUTH_OTP_SECRET
仅使用专用的测试Google账号。

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.
  • aiAssert
    运行缓慢/不稳定:缩小定位器范围,优化提示语,避免不必要的
    fullPage: true
  • agent.act
    执行失败:拆分为更小的任务,传入明确的约束条件,仅在必要时提高
    maxCycles
  • 邮件超时:验证主题/发件人过滤条件,确保使用唯一的收件箱后缀。

Full References

完整参考