stably-sdk-rules
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseStably SDK Rules
Stably SDK 规则
Quick Rules
快速规则
- Prefer raw Playwright for deterministic actions/assertions (faster + cheaper).
- Prioritize reliability over cost when Playwright becomes brittle.
- Use for canvas, coordinate-based drag/click, or unstable multi-step flows.
agent.act() - Use for dynamic visual assertions; keep prompts specific.
expect(...).aiAssert() - Use /
page.extract()when you need visual-to-data extraction.locator.extract() - Use when semantic selectors are hard with standard locators.
page.getLocatorsByAI() - Use for OTP/magic-link/verification email flows.
Inbox - All prompts must be self-contained; never rely on implicit previous context.
- Keep tasks small; do loops/calculations/conditionals in code.
agent.act() - Use only if content outside viewport matters.
fullPage: true - Always add to locators for trace readability.
.describe("...") - For email isolation, use unique per test and clean up.
Inbox.build({ suffix })
- 对于确定性操作/断言,优先使用原生Playwright(更快且成本更低)。
- 当Playwright变得不稳定时,优先考虑可靠性而非成本。
- 对于画布、基于坐标的拖拽/点击或不稳定的多步骤流程,使用。
agent.act() - 对于动态视觉断言,使用;提示语需具体明确。
expect(...).aiAssert() - 当需要从视觉内容提取数据时,使用/
page.extract()。locator.extract() - 当标准定位器难以实现语义选择时,使用。
page.getLocatorsByAI() - 对于OTP/魔法链接/验证邮件流程,使用。
Inbox - 所有提示语必须独立完整;切勿依赖隐含的上下文信息。
- 的任务需拆分得尽量小;循环、计算、条件判断逻辑放在代码中实现。
agent.act() - 仅当视口外的内容对测试有影响时,才使用。
fullPage: true - 始终为定位器添加,以提升追踪日志的可读性。
.describe("...") - 为实现邮件隔离,每个测试使用唯一的,并在测试后清理。
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_IDts
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_IDts
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 for dynamic/visual-heavy checks.
aiAssert
- Interaction choice:
- Use Playwright for deterministic steps.
- Use for brittle or semantic tasks (especially canvas/coordinates).
agent.act
- 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
aiAssertaiAssert
aiAssertts
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 only when assertion needs off-screen content.
fullPage: truets
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: trueextract
extractextract
extractts
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
getLocatorsByAIgetLocatorsByAI
getLocatorsByAIRequires Playwright .
>= 1.54.1ts
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.1ts
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.actagent.act
agent.actts
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)
InboxInbox
(邮件隔离)
InboxInstall: . Requires and env vars (or pass to ).
npm install -D @stablyai/emailSTABLY_API_KEYSTABLY_PROJECT_IDInbox.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/emailSTABLY_API_KEYSTABLY_PROJECT_IDInbox.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 选项
| Option | Type | Description |
|---|---|---|
| string | Suffix for test isolation (e.g., |
| string | Defaults to |
| string | Defaults to |
Always use a unique per test for parallel isolation. The inbox automatically filters out emails received before it was created.
suffix| 选项 | 类型 | 描述 |
|---|---|---|
| string | 用于测试隔离的后缀(例如: |
| string | 默认使用环境变量 |
| string | 默认使用环境变量 |
为实现并行隔离,每个测试必须使用唯一的。收件箱会自动过滤掉其创建之前收到的邮件。
suffixwaitForEmail
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 if no match arrives within the timeout.
EmailTimeoutErrorts
const email = await inbox.waitForEmail({
from: "noreply@example.com", // 按发件人过滤
subject: "verification", // 默认包含匹配
subjectMatch: "exact", // 可选"contains"(默认值)
timeoutMs: 60_000, // 默认值:120000(2分钟)
pollIntervalMs: 5000, // 默认值:3000(3秒)
});如果在超时时间内未找到匹配邮件,会抛出。
EmailTimeoutErrorextractFromEmail
extractFromEmail
Returns . Throws on failure.
{ data, reason }EmailExtractionErrorts
// 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 }EmailExtractionErrorts
// 字符串提取
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 属性
| Property | Type | Description |
|---|---|---|
| string | Full email address (with suffix if provided) |
| string | undefined | The suffix passed to |
| Date | Inbox creation time; emails before this are auto-filtered |
| 属性 | 类型 | 描述 |
|---|---|---|
| string | 完整邮箱地址(如果提供了后缀则包含后缀) |
| string | undefined | 传入 |
| Date | 收件箱创建时间;在此时间之前的邮件会被自动过滤 |
listEmails
listEmails
ts
const { emails, nextCursor } = await inbox.listEmails(options?);| Option | Type | Default | Description |
|---|---|---|---|
| string | — | Filter by sender address |
| string | — | Filter by subject |
| | | Subject matching mode |
| number | 20 | Max results (max: 100) |
| string | — | Pagination cursor from previous |
| Date | — | Override the default creation-time filter |
| boolean | false | Include emails received before inbox creation |
ts
const { emails, nextCursor } = await inbox.listEmails(options?);| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| string | — | 按发件人地址过滤 |
| string | — | 按主题过滤 |
| | | 主题匹配模式 |
| number | 20 | 最大结果数(上限:100) |
| string | — | 来自上一次调用 |
| Date | — | 覆盖默认的创建时间过滤条件 |
| boolean | false | 是否包含收件箱创建之前收到的邮件 |
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
邮件对象属性
| Property | Type | Description |
|---|---|---|
| string | Unique identifier |
| string | Container (e.g., |
| | Sender |
| | Recipients |
| string | Subject line |
| Date | Arrival timestamp |
| string? | Plain text body |
| string[]? | HTML body parts |
| 属性 | 类型 | 描述 |
|---|---|---|
| string | 唯一标识符 |
| string | 邮件容器(例如: |
| | 发件人信息 |
| | 收件人列表 |
| string | 邮件主题 |
| Date | 邮件接收时间戳 |
| string? | 纯文本邮件内容 |
| 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: after calling
inbox.addressreturns your full addressInbox.build()
The pattern is . If the user needs to allowlist, they should add to their email provider's allowlist.
{org-name}@mail.stably.aimail.stably.aiDirect users to the dashboard Settings > Email Inbox to find their specific address.
你的邮箱地址可在Stably控制台中查看:
- 设置 > 邮件收件箱:显示完整地址及复制按钮
- 代码中:调用后,
Inbox.build()会返回完整地址inbox.address
地址格式为。如果需要添加白名单,应将添加到邮件服务商的白名单中。
{org-name}@mail.stably.aimail.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_EMAILGOOGLE_AUTH_PASSWORDGOOGLE_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_EMAILGOOGLE_AUTH_PASSWORDGOOGLE_AUTH_OTP_SECRET
仅使用专用的测试Google账号。
Troubleshooting (Short)
快速排查
- is slow/flaky: scope to a locator, tighten prompt, avoid unnecessary
aiAssert.fullPage: true - fails: split into smaller tasks, pass explicit constraints, raise
agent.actonly when needed.maxCycles - Email timeout: verify subject/from filter and use unique inbox suffixes.
- 运行缓慢/不稳定:缩小定位器范围,优化提示语,避免不必要的
aiAssert。fullPage: true - 执行失败:拆分为更小的任务,传入明确的约束条件,仅在必要时提高
agent.act。maxCycles - 邮件超时:验证主题/发件人过滤条件,确保使用唯一的收件箱后缀。
Full References
完整参考
- Stably docs: https://docs.stably.ai
- SDK setup skill:
skills/stably-sdk-setup/SKILL.md - Package docs: https://www.npmjs.com/package/@stablyai/playwright-test
- Email package docs: https://www.npmjs.com/package/@stablyai/email
- Local overview:
skills/stably-sdk-rules/README.md
- Stably 文档:https://docs.stably.ai
- SDK配置技能:
skills/stably-sdk-setup/SKILL.md - 包文档:https://www.npmjs.com/package/@stablyai/playwright-test
- 邮件包文档:https://www.npmjs.com/package/@stablyai/email
- 本地概览:
skills/stably-sdk-rules/README.md