Skip to content

Commit 1a92e40

Browse files
feat(examples): enhance vitest example (#89)
* feat(exmaple): upadte puppeteer-with-vitest-demo * feat(exmaple): update puppeteer-with-vitest-demo * refactor web test agent usage and expand demo README (#90) --------- Co-authored-by: yuyutaotao <167746126+yuyutaotao@users.noreply.github.com>
1 parent 372e32f commit 1a92e40

File tree

15 files changed

+535
-83
lines changed

15 files changed

+535
-83
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Playwright with Vitest Demo
2+
3+
This demo shows how to combine **Playwright + Vitest + Midscene** to write AI-driven E2E tests for web pages.
4+
5+
> This demo project was scaffolded using the [vitest-midscene](https://github.com/web-infra-dev/midscene-skills) skill.
6+
7+
## Preparation
8+
9+
Create a `.env` file:
10+
11+
```env
12+
# Replace with your own API key
13+
MIDSCENE_MODEL_BASE_URL="YOUR_MODEL_BASE_URL"
14+
MIDSCENE_MODEL_API_KEY="YOUR_MODEL_API_KEY"
15+
MIDSCENE_MODEL_NAME="YOUR_MODEL_NAME"
16+
MIDSCENE_MODEL_FAMILY="YOUR_MODEL_FAMILY"
17+
```
18+
19+
Install dependencies:
20+
21+
```bash
22+
npm install
23+
```
24+
25+
Run tests:
26+
27+
```bash
28+
# Run all tests
29+
npm run test
30+
31+
# Run web tests only
32+
npm run test:web
33+
```
34+
35+
## What modules are included
36+
37+
```text
38+
├── e2e/
39+
│ └── web/
40+
│ └── todo-list.test.ts
41+
├── src/
42+
│ ├── context/
43+
│ │ ├── base.ts
44+
│ │ ├── index.ts
45+
│ │ └── web.ts
46+
│ ├── report-helper.ts
47+
│ ├── reporter.ts
48+
│ └── utils.ts
49+
├── vitest.config.ts
50+
└── package.json
51+
```
52+
53+
### `e2e/web/todo-list.test.ts`
54+
55+
- Demo web E2E case on TodoMVC.
56+
- Uses Midscene `agent` methods (`aiAct`, `aiQuery`, `aiAssert`, `aiString`, etc.) to drive and validate behavior in natural language.
57+
- Good as a template for writing new scenario tests.
58+
59+
### `src/context/*`
60+
61+
- `web.ts`: `WebTest` context that manages Playwright browser/page lifecycle and creates Midscene `PlaywrightAgent` per test.
62+
- `base.ts`: shared base context abstraction (common lifecycle and report integration hooks).
63+
- `index.ts`: exports the public context API.
64+
65+
### `src/report-helper.ts`
66+
67+
- Collects per-test report data.
68+
- Merges all test reports into one final report at suite end.
69+
- Marks skipped/failed/timedOut/passed status.
70+
71+
### `src/reporter.ts`
72+
73+
- Custom Vitest reporter.
74+
- Prints Midscene merged report path directly in terminal output, so developers can quickly open the report.
75+
76+
### `src/utils.ts`
77+
78+
- Shared helper: timestamp generation for report filenames.
79+
80+
### `vitest.config.ts`
81+
82+
- Test include pattern, timeout settings, and custom reporter setup.
83+
- Loads `.env` via `dotenv/config` for model configuration.
84+
85+
## What the web test does
86+
87+
`e2e/web/todo-list.test.ts` demonstrates an end-to-end AI workflow:
88+
89+
1. Open TodoMVC page.
90+
2. Add 3 todo items with `aiAct`.
91+
3. Delete one specific item and complete another item.
92+
4. Switch to `completed` filter.
93+
5. Query the current list and assert expected count.
94+
6. Assert page text (`1 item left`).
95+
7. Extract structured data from page with `aiString`, `aiNumber`, `aiBoolean`, `aiLocate`.
96+
97+
This demonstrates both:
98+
99+
- **AI action**: controlling UI through natural-language instructions.
100+
- **AI assertion/query**: validating UI state and reading structured information without hand-written locators.
101+
102+
## References
103+
104+
- https://midscenejs.com/integrate-with-playwright.html
105+
- https://midscenejs.com/api.html

puppeteer-with-vitest-demo/tests/demo.test.ts renamed to playwright-with-vitest-demo/e2e/web/todo-list.test.ts

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,14 @@
1-
import { describe, it, expect, vi, beforeAll } from 'vitest';
2-
import puppeteer from 'puppeteer';
3-
import { PuppeteerAgent } from '@midscene/web/puppeteer';
4-
import 'dotenv/config'; // read environment variables from .env file
5-
6-
vi.setConfig({
7-
testTimeout: 240 * 1000,
8-
});
1+
import { describe, it, expect } from 'vitest';
2+
import { WebTest } from '../../src/context';
93

104
const pageUrl = 'https://todomvc.com/examples/react/dist/';
11-
describe('Test todo list', () => {
12-
let agent: PuppeteerAgent;
13-
14-
beforeAll(async () => {
15-
const browser = await puppeteer.launch({
16-
headless: true,
17-
args: ['--no-sandbox', '--disable-setuid-sandbox'],
18-
});
19-
const page = await browser.newPage();
20-
await page.goto(pageUrl);
21-
await page.waitForNetworkIdle();
22-
agent = new PuppeteerAgent(page);
235

24-
return () => {
25-
browser.close();
26-
};
27-
});
6+
describe('Test todo list', () => {
7+
const ctx = WebTest.setup(pageUrl);
288

299
it('ai todo', async () => {
10+
const { agent } = ctx;
11+
3012
await agent.aiAct(
3113
"type 'Study JS today' in the task box input and press the Enter key"
3214
);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "playwright-with-vitest-demo",
3+
"private": true,
4+
"version": "1.0.0",
5+
"main": "index.js",
6+
"type": "module",
7+
"scripts": {
8+
"test": "vitest run",
9+
"test:web": "vitest run e2e/web/"
10+
},
11+
"author": "",
12+
"license": "MIT",
13+
"dependencies": {
14+
"@midscene/core": "latest",
15+
"@midscene/web": "latest",
16+
"playwright": "^1.56.1",
17+
"@playwright/test": "^1.56.1"
18+
},
19+
"devDependencies": {
20+
"dotenv": "^16.4.5",
21+
"typescript": "^5.7.0",
22+
"vitest": "3.0.5"
23+
}
24+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type {
2+
RunnerTestSuite,
3+
TestContext as VitestTestContext,
4+
} from 'vitest';
5+
import { ReportHelper } from '../report-helper';
6+
7+
/**
8+
* Minimal agent interface — all Midscene agents (Playwright / Android / iOS)
9+
* satisfy this via `@midscene/core/agent`.
10+
*/
11+
interface AgentLike {
12+
reportFile?: string | null;
13+
destroy(): Promise<void>;
14+
}
15+
16+
/**
17+
* Base class for all platform test contexts.
18+
*
19+
* Handles the instance-level lifecycle (agent, reportFile, destroy)
20+
* so each platform only needs to implement setup/create/teardown.
21+
*/
22+
export abstract class BaseTestContext<TAgent extends AgentLike> {
23+
agent: TAgent;
24+
startTime: number;
25+
private _reportFile: string | null | undefined;
26+
27+
protected constructor(agent: TAgent) {
28+
this.agent = agent;
29+
this.startTime = performance.now();
30+
}
31+
32+
get reportFile(): string | null | undefined {
33+
return this._reportFile ?? this.agent.reportFile;
34+
}
35+
36+
async destroy(): Promise<void> {
37+
await this.agent.destroy();
38+
this._reportFile = this.agent.reportFile;
39+
await this.onDestroy();
40+
}
41+
42+
/** Override in subclass for extra cleanup (e.g. close page). */
43+
protected async onDestroy(): Promise<void> {}
44+
45+
// ── Static helpers for subclasses ──────────────────────────
46+
47+
protected static _collectReport(
48+
reportHelper: ReportHelper,
49+
ctx: BaseTestContext<AgentLike> | undefined,
50+
testCtx: VitestTestContext,
51+
): Promise<void> {
52+
return reportHelper.collectReport(ctx, testCtx);
53+
}
54+
55+
protected static _mergeAndTeardown(
56+
reportHelper: ReportHelper,
57+
teardownFn: () => Promise<void>,
58+
suite: RunnerTestSuite,
59+
reportName?: string,
60+
): Promise<string | null> {
61+
const merged = reportHelper.mergeReports(suite, reportName);
62+
return teardownFn().then(() => merged);
63+
}
64+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { WebTest } from './web';
2+
export type { WebTestOptions } from './web';
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { PlaywrightAgent, type WebPageAgentOpt } from '@midscene/web/playwright';
2+
import { type Browser, type Page, chromium } from 'playwright';
3+
import { ReportHelper, buildReportMeta } from '../report-helper';
4+
import { afterAll, afterEach, beforeAll, beforeEach } from 'vitest';
5+
import type { RunnerTestSuite, TestContext as VitestTestContext } from 'vitest';
6+
import { BaseTestContext } from './base';
7+
8+
const DEFAULT_ARGS = ['--no-sandbox', '--ignore-certificate-errors'];
9+
10+
export interface WebTestOptions {
11+
viewport?: { width: number; height: number };
12+
headless?: boolean;
13+
/** Options passed to PlaywrightAgent (e.g. aiActionContext, modelConfig). */
14+
agentOptions?: Omit<WebPageAgentOpt, 'groupName' | 'reportFileName'>;
15+
}
16+
17+
export class WebTest extends BaseTestContext<PlaywrightAgent> {
18+
private static sharedBrowser: Browser | null = null;
19+
private static sharedOptions: WebTestOptions = {};
20+
private static reportHelper = new ReportHelper();
21+
22+
page: Page;
23+
24+
private constructor(page: Page, agent: PlaywrightAgent) {
25+
super(agent);
26+
this.page = page;
27+
}
28+
29+
protected async onDestroy(): Promise<void> {
30+
await this.page.close();
31+
}
32+
33+
/**
34+
* Launch the shared Chromium browser. Called once in `beforeAll`.
35+
*/
36+
static async launchBrowser(options?: WebTestOptions): Promise<void> {
37+
WebTest.sharedOptions = options ?? {};
38+
WebTest.sharedBrowser = await chromium.launch({
39+
headless: options?.headless ?? true,
40+
args: DEFAULT_ARGS,
41+
});
42+
WebTest.reportHelper.reset();
43+
}
44+
45+
/**
46+
* Create a new page + agent on the shared browser.
47+
*/
48+
static async create(
49+
targetUrl: string,
50+
testCtx: VitestTestContext,
51+
options?: WebTestOptions,
52+
): Promise<WebTest> {
53+
if (!WebTest.sharedBrowser) {
54+
await WebTest.launchBrowser(options);
55+
}
56+
const opts = { ...WebTest.sharedOptions, ...options };
57+
const page = await WebTest.sharedBrowser!.newPage({
58+
viewport: opts.viewport ?? { width: 1920, height: 1080 },
59+
});
60+
await page.goto(targetUrl);
61+
62+
const { groupName, reportFileName } = buildReportMeta(testCtx);
63+
const agent = new PlaywrightAgent(page, {
64+
...opts.agentOptions,
65+
groupName,
66+
reportFileName,
67+
});
68+
return new WebTest(page, agent);
69+
}
70+
71+
static async collectReport(
72+
ctx: WebTest | undefined,
73+
testCtx: VitestTestContext,
74+
): Promise<void> {
75+
return BaseTestContext._collectReport(
76+
WebTest.reportHelper,
77+
ctx,
78+
testCtx,
79+
);
80+
}
81+
82+
static async mergeAndTeardown(
83+
suite: RunnerTestSuite,
84+
reportName?: string,
85+
): Promise<string | null> {
86+
return BaseTestContext._mergeAndTeardown(
87+
WebTest.reportHelper,
88+
WebTest.teardown,
89+
suite,
90+
reportName,
91+
);
92+
}
93+
94+
static async teardown(): Promise<void> {
95+
await WebTest.sharedBrowser?.close();
96+
WebTest.sharedBrowser = null;
97+
}
98+
99+
/**
100+
* Register lifecycle hooks and return a context object whose `page` and
101+
* `agent` properties point to the current test's instances.
102+
*/
103+
static setup(targetUrl: string, options?: WebTestOptions) {
104+
let currentCtx: WebTest | undefined;
105+
106+
beforeAll(() => WebTest.launchBrowser(options));
107+
beforeEach(async (testCtx) => {
108+
currentCtx = await WebTest.create(targetUrl, testCtx, options);
109+
});
110+
afterEach((testCtx) => {
111+
const ctx = currentCtx;
112+
currentCtx = undefined;
113+
return WebTest.collectReport(ctx, testCtx);
114+
});
115+
afterAll((suite) => WebTest.mergeAndTeardown(suite));
116+
117+
return {
118+
get page() { return currentCtx!.page; },
119+
get agent() { return currentCtx!.agent; },
120+
} as { page: Page; agent: PlaywrightAgent };
121+
}
122+
}

0 commit comments

Comments
 (0)