Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
396 changes: 396 additions & 0 deletions apps/site/docs/en/integrate-with-rstest.mdx

Large diffs are not rendered by default.

396 changes: 396 additions & 0 deletions apps/site/docs/zh/integrate-with-rstest.mdx

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions apps/site/rspress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ export default defineConfig({
text: 'Integrate with Puppeteer',
link: '/integrate-with-puppeteer',
},
{
text: 'Integrate with Rstest',
link: '/integrate-with-rstest',
},
{
text: 'Bridge to the desktop Chrome',
link: '/bridge-mode',
Expand Down Expand Up @@ -281,6 +285,10 @@ export default defineConfig({
text: '集成到 Puppeteer',
link: '/zh/integrate-with-puppeteer',
},
{
text: '集成到 Rstest',
link: '/zh/integrate-with-rstest',
},
{
text: '桥接到桌面 Chrome',
link: '/zh/bridge-mode',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"build": "nx run-many --target=build --exclude=doc --verbose",
"build:packages": "pnpm -r --filter './packages/**' --if-present run build",
"build:skip-cache": "npm run clean && nx run-many --target=build --exclude=doc --verbose --skip-nx-cache",
"test": "nx run-many --target=test --projects=@midscene/core,@midscene/shared,@midscene/visualizer,@midscene/web,@midscene/cli,@midscene/android,@midscene/ios,@midscene/computer,@midscene/android-mcp,@midscene/ios-mcp,@midscene/web-bridge-mcp,@midscene/playground,studio --verbose",
"test": "nx run-many --target=test --projects=@midscene/core,@midscene/shared,@midscene/visualizer,@midscene/web,@midscene/cli,@midscene/android,@midscene/ios,@midscene/computer,@midscene/android-mcp,@midscene/ios-mcp,@midscene/web-bridge-mcp,@midscene/playground,@midscene/rstest,studio --verbose",
"test:ai": "nx run-many --target=test:ai --projects=@midscene/core,@midscene/web,@midscene/cli,@midscene/computer --verbose",
"e2e": "nx run @midscene/web:e2e --verbose --exclude-task-dependencies",
"e2e:cache": "nx run @midscene/web:e2e:cache --verbose --exclude-task-dependencies",
Expand Down
5 changes: 5 additions & 0 deletions packages/rstest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @midscene/rstest

Run Midscene AI browser agents as [Rstest](https://rstest.rsbuild.dev/) tests. Provides Playwright and Puppeteer providers plus an Rstest reporter that surfaces Midscene reports.

- **Integration guide**: <https://midscenejs.com/integrate-with-rstest.html>
91 changes: 91 additions & 0 deletions packages/rstest/demo/playground.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { test as base } from '@midscene/rstest/playwright';
import { describe, expect } from '@rstest/core';

const PAGE_URL =
'https://lf3-static.bytednsdoc.com/obj/eden-cn/nupipfups/Midscene/contacts3.html';

// Per-file URL override. The `page` fixture navigates here for every test.
const test = base.extend({ url: PAGE_URL });

describe('Contacts page', () => {
// Pattern: semantic UI check via `aiAssert`.
test('renders the smart contacts header and grid', async ({ agent }) => {
await agent.aiAssert(
'the page header reads "Smart Contacts" with a grid of contact cards below it, each card showing an avatar, name, position, and contact details',
);
});

// Pattern: structured data extraction via `aiQuery<T>` + deterministic
// comparison via rstest `expect`.
test('lists every contact with the expected fields', async ({ agent }) => {
const contacts = await agent.aiQuery<
{ name: string; position: string; email: string }[]
>(
'Array<{name: string, position: string, email: string}>, the name (heading), position (line under the name) and email address shown on every contact card',
);

expect(contacts).toHaveLength(5);
const byName = Object.fromEntries(contacts.map((c) => [c.name, c]));
expect(byName['Alice Johnson']).toEqual({
name: 'Alice Johnson',
position: 'Senior Software Engineer',
email: '[email protected]',
});
});

// Pattern: AI interaction (`aiRightClick`) + AI synchronization (`aiWaitFor`)
// + AI extraction (`aiQuery`) chained together.
test('opens the custom context menu on right-click', async ({ agent }) => {
await agent.aiRightClick("Alice Johnson's contact card");
await agent.aiWaitFor(
'a context menu is visible with the items "Call Contact", "Send Email", "Send Message", "Edit Contact", "Copy Info" and "Delete Contact"',
{ timeoutMs: 10_000 },
);

const items = await agent.aiQuery<string[]>(
'string[], the visible text of every item inside the open context menu, in order',
);
expect(items).toEqual([
'Call Contact',
'Send Email',
'Send Message',
'Edit Contact',
'Copy Info',
'Delete Contact',
]);
});

// Escape hatch: raw Playwright `Page` for browser-primitive checks that
// don't need the AI. Destructure only `page` — the `agent` fixture is not
// created, but `page` still pulls in `browser`/`context`/auto-navigation.
test('inspects raw page state via the Playwright page escape hatch', async ({
page,
}) => {
expect(page.url()).toBe(PAGE_URL);

const viewport = page.viewportSize();
expect(viewport?.width).toBeGreaterThan(0);
expect(viewport?.height).toBeGreaterThan(0);

const title = await page.title();
expect(title.length).toBeGreaterThan(0);
});

// Multi-session: open a second isolated browser context (think "another
// user") and drive it with a separate midscene agent via `agentForPage`.
// The secondary's report is merged alongside the primary's at fixture
// teardown; destroy is automatic.
test('drives a second isolated session via browser + agentForPage', async ({
browser,
agentForPage,
}) => {
const sessionB = await browser.newContext();
const pageB = await sessionB.newPage();
await pageB.goto(PAGE_URL);

const agentB = await agentForPage(pageB);
await agentB.aiAssert(
'the contact grid is visible and contains "Alice Johnson"',
);
});
});
12 changes: 12 additions & 0 deletions packages/rstest/demo/rstest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import MidsceneReporter from '@midscene/rstest/reporter';
import { defineConfig } from '@rstest/core';
import dotenv from 'dotenv';

dotenv.config({ path: '../../.env' });

export default defineConfig({
include: ['*.test.ts'],
testTimeout: 1_800_000,
hookTimeout: 60_000,
reporters: ['default', new MidsceneReporter()],
});
91 changes: 91 additions & 0 deletions packages/rstest/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{
"name": "@midscene/rstest",
"description": "Run Midscene AI browser agents as Rstest tests. Provides Playwright and Puppeteer providers plus a Rstest reporter that surfaces Midscene reports.",
"keywords": [
"rstest",
"midscene",
"AI testing",
"AI UI automation",
"Browser use",
"playwright",
"puppeteer"
],
"version": "1.7.9",
"repository": "https://github.com/web-infra-dev/midscene",
"homepage": "https://midscenejs.com/",
"type": "module",
"sideEffects": false,
"files": ["dist", "README.md"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./reporter": {
"types": "./dist/reporter.d.ts",
"import": "./dist/reporter.js"
},
"./playwright": {
"types": "./dist/playwright.d.ts",
"import": "./dist/playwright.js"
},
"./puppeteer": {
"types": "./dist/puppeteer.d.ts",
"import": "./dist/puppeteer.js"
}
},
"scripts": {
"dev": "npm run build:watch",
"build": "rslib build",
"build:watch": "rslib build --watch --no-clean",
"test": "rstest run",
"test:demo": "rstest run --config rstest.config.ts --root demo",
"test:watch": "rstest watch",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@midscene/shared": "workspace:*"
},
"peerDependencies": {
"@midscene/core": "workspace:*",
"@midscene/web": "workspace:*",
"@playwright/test": "^1.45.0",
"@rstest/core": ">=0.9.0",
"playwright": "^1.45.0",
"puppeteer": ">=20.0.0"
},
"peerDependenciesMeta": {
"@playwright/test": {
"optional": true
},
"playwright": {
"optional": true
},
"puppeteer": {
"optional": true
}
},
"devDependencies": {
"@midscene/core": "workspace:*",
"@midscene/web": "workspace:*",
"@playwright/test": "^1.45.0",
"@rslib/core": "^0.18.3",
"@rstest/core": "^0.9.9",
"@types/node": "^18.0.0",
"deepmerge": "^4.3.1",
"dotenv": "^16.4.5",
"playwright": "^1.45.0",
"puppeteer": "24.6.0",
"reduce-configs": "^1.1.2",
"std-env": "^4.1.0",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=18.19.0"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
},
"license": "MIT"
}
23 changes: 23 additions & 0 deletions packages/rstest/rslib.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { defineConfig } from '@rslib/core';

export default defineConfig({
lib: [
{
source: {
entry: {
index: './src/index.ts',
reporter: './src/reporter.ts',
playwright: './src/playwright.ts',
puppeteer: './src/puppeteer.ts',
},
},
format: 'esm',
syntax: 'es2022',
dts: { bundle: false },
},
],
output: {
target: 'node',
sourceMap: true,
},
});
5 changes: 5 additions & 0 deletions packages/rstest/rstest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineConfig } from '@rstest/core';

export default defineConfig({
include: ['tests/unit-test/**/*.test.ts'],
});
1 change: 1 addition & 0 deletions packages/rstest/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { Resolver } from './resolve';
Loading
Loading