Skip to content

Commit 6b1c6db

Browse files
authored
Feat: API refinement and API validation support (#114)
Addresses Issue: https://github.com/anti-work/iffy/issues/225 This PR ships v0.0.8 and completes migrating Iffy from playwright test to shortest. ### Added - Added support for playwright's browser and playwright object model - Rename test namespace to shortest - Added new lifecycle method called .after() that will only run after the specific test case - Updated shortest.yml
1 parent 9848af7 commit 6b1c6db

File tree

17 files changed

+769
-557
lines changed

17 files changed

+769
-557
lines changed

.github/workflows/shortest.yml

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ jobs:
2222
- name: Install dependencies
2323
run: pnpm install
2424

25+
- name: Install Playwright browsers
26+
run: pnpm exec playwright install
27+
2528
- name: Get Vercel preview URL
2629
id: vercel_url
2730
run: |

README.md

+15-15
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ AI-powered natural language end-to-end testing framework.
1111

1212
## Installation
1313
```bash
14-
npm install -g @antiwork/shortest
14+
npm install @antiwork/shortest
1515
# or
16-
pnpm add -g @antiwork/shortest
16+
pnpm add @antiwork/shortest
1717
# or
18-
yarn add -g @antiwork/shortest
18+
yarn add @antiwork/shortest
1919
```
2020

2121
### If you installed shortest without `-g` flag, you can run tests as follows:
@@ -43,25 +43,25 @@ export default {
4343
2. Write your test in your test directory: `app/__tests__/login.test.ts`
4444

4545
```typescript
46-
import { test } from '@antiwork/shortest'
46+
import { shortest } from '@antiwork/shortest'
4747

48-
test('Login to the app using email and password', { username: process.env.GITHUB_USERNAME, password: process.env.GITHUB_PASSWORD })
48+
shortest('Login to the app using email and password', { username: process.env.GITHUB_USERNAME, password: process.env.GITHUB_PASSWORD })
4949
```
5050

5151
## Using callback functions
5252
You can also use callback functions to add additoinal assertions and other logic. AI will execute the callback function after the test
5353
execution in browser is completed.
5454

5555
```typescript
56-
import { test } from '@antiwork/shortest';
56+
import { shortest } from '@antiwork/shortest';
5757
import { db } from '@/lib/db/drizzle';
5858
import { users } from '@/lib/db/schema';
5959
import { eq } from 'drizzle-orm';
6060

61-
test('Login to the app using Github login', {
62-
username: process.env.GITHUB_USERNAME,
63-
password: process.env.GITHUB_PASSWORD
64-
}, async ({ page }) => {
61+
shortest('Login to the app using username and password', {
62+
username: process.env.USERNAME,
63+
password: process.env.PASSWORD
64+
}).after(async ({ page }) => {
6565
// Get current user's clerk ID from the page
6666
const clerkId = await page.evaluate(() => {
6767
return window.localStorage.getItem('clerk-user');
@@ -86,15 +86,15 @@ test('Login to the app using Github login', {
8686
You can use lifecycle hooks to run code before and after the test.
8787

8888
```typescript
89-
import { test } from '@antiwork/shortest';
89+
import { shortest } from '@antiwork/shortest';
9090

91-
test.beforeAll(async ({ page }) => {
91+
shortest.beforeAll(async ({ page }) => {
9292
await clerkSetup({
9393
frontendApiUrl: process.env.PLAYWRIGHT_TEST_BASE_URL ?? "http://localhost:3000",
9494
});
9595
});
9696

97-
test.beforeEach(async ({ page }) => {
97+
shortest.beforeEach(async ({ page }) => {
9898
await clerk.signIn({
9999
page,
100100
signInParams: {
@@ -104,11 +104,11 @@ test.beforeEach(async ({ page }) => {
104104
});
105105
});
106106

107-
test.afterEach(async ({ page }) => {
107+
shortest.afterEach(async ({ page }) => {
108108
await page.close();
109109
});
110110

111-
test.afterAll(async ({ page }) => {
111+
shortest.afterAll(async ({ page }) => {
112112
await clerk.signOut({ page });
113113
});
114114
```

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
"prepare": "cd packages/shortest && pnpm build"
1919
},
2020
"dependencies": {
21+
"@playwright/test": "^1.48.1",
22+
"@faker-js/faker": "^8.4.1",
2123
"@ai-sdk/anthropic": "^0.0.50",
2224
"@ai-sdk/openai": "^0.0.61",
2325
"@clerk/nextjs": "^5.6.0",

packages/shortest/CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.0.8] - 2024-12-16
9+
10+
### Added
11+
- Added support for playwright's browser and playwright object model
12+
- Rename test namespace to shortest
13+
- Added new lifecycle method called .after() that will only run after the specific test case
14+
- Improve system prompt to be more robust and structured
15+
- Added Windows support for playwright install command
16+
817
## [0.0.7] - 2024-12-12
918

1019
### Fixed

packages/shortest/README.md

+15-15
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ AI-powered natural language end-to-end testing framework.
1010

1111
## Installation
1212
```bash
13-
npm install -g @antiwork/shortest
13+
npm install @antiwork/shortest
1414
# or
15-
pnpm add -g @antiwork/shortest
15+
pnpm add @antiwork/shortest
1616
# or
17-
yarn add -g @antiwork/shortest
17+
yarn add @antiwork/shortest
1818
```
1919

2020
## Quick Start
@@ -34,25 +34,25 @@ export default {
3434

3535
2. Write your test in the test directory: `app/__tests__/login.test.ts`
3636
```typescript
37-
import { test } from '@antiwork/shortest'
37+
import { shortest } from '@antiwork/shortest'
3838

39-
test('Login to the app using email and password', { username: process.env.GITHUB_USERNAME, password: process.env.GITHUB_PASSWORD })
39+
shortest('Login to the app using email and password', { username: process.env.GITHUB_USERNAME, password: process.env.GITHUB_PASSWORD })
4040
```
4141

4242
## Using callback functions
4343
You can also use callback functions to add additoinal assertions and other logic. AI will execute the callback function after the test
4444
execution in browser is completed.
4545

4646
```typescript
47-
import { test } from '@antiwork/shortest';
47+
import { shortest } from '@antiwork/shortest';
4848
import { db } from '@/lib/db/drizzle';
4949
import { users } from '@/lib/db/schema';
5050
import { eq } from 'drizzle-orm';
5151

52-
test('Login to the app using Github login', {
53-
username: process.env.GITHUB_USERNAME,
54-
password: process.env.GITHUB_PASSWORD
55-
}, async ({ page }) => {
52+
shortest('Login to the app using username and password', {
53+
username: process.env.USERNAME,
54+
password: process.env.PASSWORD
55+
}).after(async ({ page }) => {
5656
// Get current user's clerk ID from the page
5757
const clerkId = await page.evaluate(() => {
5858
return window.localStorage.getItem('clerk-user');
@@ -77,15 +77,15 @@ test('Login to the app using Github login', {
7777
You can use lifecycle hooks to run code before and after the test.
7878

7979
```typescript
80-
import { test } from '@antiwork/shortest';
80+
import { shrotest } from '@antiwork/shortest';
8181

82-
test.beforeAll(async ({ page }) => {
82+
shortest.beforeAll(async ({ page }) => {
8383
await clerkSetup({
8484
frontendApiUrl: process.env.PLAYWRIGHT_TEST_BASE_URL ?? "http://localhost:3000",
8585
});
8686
});
8787

88-
test.beforeEach(async ({ page }) => {
88+
shortest.beforeEach(async ({ page }) => {
8989
await clerk.signIn({
9090
page,
9191
signInParams: {
@@ -95,11 +95,11 @@ test.beforeEach(async ({ page }) => {
9595
});
9696
});
9797

98-
test.afterEach(async ({ page }) => {
98+
shortest.afterEach(async ({ page }) => {
9999
await page.close();
100100
});
101101

102-
test.afterAll(async ({ page }) => {
102+
shortest.afterAll(async ({ page }) => {
103103
await clerk.signOut({ page });
104104
});
105105
```

packages/shortest/index.d.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Expect } from 'expect';
2-
import type { Page } from 'playwright';
2+
import type { Page, Browser, APIRequest, APIRequestContext } from 'playwright';
3+
import type * as playwright from 'playwright';
34
import type { TestAPI, TestContext } from './dist/types/test';
45
import type { ShortestConfig } from './dist/types/config';
56

@@ -10,15 +11,24 @@ declare global {
1011
declare module '@antiwork/shortest' {
1112
export type TestContextProps = {
1213
page: Page;
14+
browser: Browser;
15+
playwright: typeof playwright & {
16+
request: APIRequest & {
17+
newContext: (options?: { extraHTTPHeaders?: Record<string, string> }) => Promise<APIRequestContext>;
18+
};
19+
};
1320
};
1421

1522
export type TestChain = {
23+
expect(fn: (context: TestContextProps) => Promise<void>): TestChain;
1624
expect(description: string): TestChain;
1725
expect(description: string, fn?: (context: TestContextProps) => Promise<void>): TestChain;
1826
expect(description: string, payload?: any, fn?: (context: TestContextProps) => Promise<void>): TestChain;
27+
after(fn: (context: TestContextProps) => void | Promise<void>): TestChain;
1928
};
2029

2130
export type TestAPI = {
31+
(fn: (context: TestContextProps) => Promise<void>): void;
2232
(name: string): TestChain;
2333
(name: string, fn?: (context: TestContextProps) => Promise<void>): TestChain;
2434
(name: string, payload?: any, fn?: (context: TestContextProps) => Promise<void>): TestChain;
@@ -36,6 +46,6 @@ declare module '@antiwork/shortest' {
3646
afterEach(name: string, fn: (context: TestContextProps) => Promise<void>): void;
3747
};
3848

39-
export const test: TestAPI;
49+
export const shortest: TestAPI;
4050
export type { TestContext, ShortestConfig };
4151
}

packages/shortest/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@antiwork/shortest",
3-
"version": "0.0.7",
3+
"version": "0.0.8",
44
"description": "AI-powered natural language end-to-end testing framework",
55
"type": "module",
66
"main": "./dist/index.js",
+35-43
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,50 @@
1-
export const SYSTEM_PROMPT = `
2-
You are a test automation expert with access to Chrome browser. When you are given a test case,
3-
you will need to execute the browser actions to validate the test cases.
4-
You are already in Chrome browser in the web page of the application in test case instructions,
5-
so you don't need to load the browser yourself.
6-
7-
This is an example of a test case that you might recieve:
1+
export const SYSTEM_PROMPT = `You are a test automation expert working with a Chrome browser. You will be given test instructions, and your task is to execute specified browser actions to validate the provided test cases. You are already in the Chrome browser and on the relevant application page, so there is no need to open or initialize the browser yourself.
82
3+
EXAMPLE TEST CASE:
4+
------------------
95
Test: "Login to the app using Github login"
106
Context: {"username":"[email protected]","password":"password1234"}
11-
Callback function: [NO_CALLBACK]
12-
13-
Expect:
14-
1. Test case to be generated within at least 20 seconds [HAS_CALLBACK]
15-
16-
IMPORTANT RULES THAT YOU MUST ALWAYS FOLLOW WHEN EXECUTING TEST CASES:
7+
Callback function: [NO_CALLBACK]
8+
Expect: 1. Test case to be generated within at least 20 seconds [HAS_CALLBACK]
9+
------------------
1710
18-
1. Sometimes you may be instructed to wait for a certain condition to be met before you can continue with the next step.
19-
That condition might be time in seconds, or minutes. Or it can be for a certain element to be visible,
20-
or a certain element to be clickable. Make sure you wait for the condition to be met before you continue with the next step. If the
21-
condition is not met after the specified time, you should fail the test case.
11+
IMPORTANT GLOBAL RULES:
2212
23-
2. You might need to use tools api to do some actions. If that's the case, wait until the
24-
tool has finished its execution before you continue with the next action. Once the tool
25-
has finished its execution, you will recieve the result of the tool execution wether it failed or not. You can decide
26-
to continue based on the result. Sometimes you might not understand the result of the tool based on screenshots, therefore you will
27-
always recieve metadata about the tool execution which will help you understand the result.
13+
1. **Waiting for Conditions**:
14+
- Some steps will require waiting before proceeding to the next action.
15+
- This waiting can be based on a time delay (e.g., seconds or minutes) or waiting for an element to become visible or clickable.
16+
- If the specified condition is not met after the allotted time, the test should be considered failed.
2817
29-
3. IMPORTANT! DO NOT ask for screenshot until the tool has finished its execution. Once the tool has finished its execution,
30-
you will recieve the result of the tool execution wether it failed or not.
31-
Then you can ask for a screenshot to determine for your next action if anything else is needed.
18+
2. **Tool Usage**:
19+
- You may need to use provided tools to perform certain actions (e.g., clicking, navigating, or running callbacks).
20+
- After invoking a tool, wait until the tool finishes its execution and you receive a success/failure result.
21+
- You will also receive metadata about the tool's execution to help you interpret its outcome.
22+
- Only after the tool finishes and you know the result should you request any screenshots or proceed to the next action.
3223
33-
4. If you need to test a login flow with Github 2fa, you need to call the "github_login" tool only after you have
34-
seen the github login page. If you call the tool before, it will not work as expected.
24+
3. **Screenshot Rule**:
25+
- Do not request screenshots until after a tool has completely finished its execution.
26+
- Once the tool execution result is received, you may then request a screenshot to determine subsequent actions if needed.
3527
36-
5.IMPORTANT! There is a feature provided to you by tools api called "run_callback" that allows you to run callback functions for a test step.
37-
Whenever you see [HAS_CALLBACK] after the step description, you must call "run_callback" tool. Remember, only
38-
call "run_callback" tool after you have completed the browser actions for that step otherwise the callback will not work as expected.
39-
When done, you can continue with the next step. If result of the callback is failed, you must fail the test case.
28+
4. **Github Login Flow with 2FA**:
29+
- If you need to test a Github login flow that involves 2FA, only call the "github_login" tool after you have confirmed that the Github login page is displayed.
30+
- Calling the "github_login" tool prematurely (before the Github login page is visible) will lead to incorrect test behavior.
4031
41-
6. IMPORTANT! ONLY USE THIS TOOL IF YOU ARE SPECIFIED TO NAVIGATE TO A NEW PAGE IN THE TEST CASE INSTRUCTIONS.
42-
DO NOT USE THIS TOOL BASED ON YOUR INTUITION! If you need to navigate to a new page, you must use the "navigate" tool.
43-
Although you are already in a browser, you do not have access to the browser search bar, therefore,
44-
you must use the "navigate" tool to navigate to the new page. After navigating to the new page is done,
45-
you will recieve the result of the navigation and you can see if the the requested page is loaded or not from the
46-
url field in the metadata.
32+
5. **Callbacks**:
33+
- Steps may include a notation like [HAS_CALLBACK], which means after completing the browser actions for that step, you must call the "run_callback" tool.
4734
48-
7. IMPORTANT! If there is a "Expect" present in the test intruction, you must make sure it is fulfilled. If not, you must fail the test case.
35+
6. **Navigation Rule**:
36+
- Only use the "navigate" tool when explicitly specified in the test case instructions.
37+
- Do not use navigation based on intuition - follow test instructions exactly.
38+
- You must use the "navigate" tool as you don't have direct access to the browser search bar.
39+
- After navigation, verify the requested page is loaded by checking the URL in the metadata.
4940
50-
MUST FOLLOW THIS RULE: perform exactly as instructed in the test case instructions.
41+
7. **Test Expectations**:
42+
- All expectations listed in the test instructions must be fulfilled.
43+
- If any expectation is not met, the test case must be marked as failed.
5144
5245
Your task is to:
5346
1. Execute browser actions to validate test cases
5447
2. Use provided browser tools to interact with the page
55-
3. You must return the result of test execution in strict JSON format: { result: "pass" | "fail", reason: string }.
56-
for the failure reason, provide a maximum of 1 sentence.
57-
4. For any click actions, you will need to provide the x,y coordinates of the element to click.
58-
`;
48+
3. Return test execution results in strict JSON format: { result: "pass" | "fail", reason: string }
49+
For failures, provide a maximum 1-sentence reason.
50+
4. For click actions, provide x,y coordinates of the element to click.`;

packages/shortest/src/browser/core/browser-tool.ts

+7-13
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ declare global {
66
}
77
}
88

9-
import { Page } from 'playwright';
9+
import { Page, Browser } from 'playwright';
1010
import { BaseBrowserTool, ToolError } from './index';
1111
import { ActionInput, ToolResult, BetaToolType } from '../../types/browser';
1212
import { writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'fs';
@@ -228,27 +228,21 @@ export class BrowserTool extends BaseBrowserTool {
228228

229229
const testContext = this.testContext;
230230
const currentTest = testContext.currentTest as TestFunction;
231+
231232
const currentStepIndex = testContext.currentStepIndex ?? 0;
232233

233234
try {
234-
if (currentStepIndex === 0) {
235-
if (currentTest.fn) {
236-
await currentTest.fn({ page: this.page });
237-
testContext.currentStepIndex = 1;
238-
return {
239-
output: 'Test function executed successfully'
240-
};
241-
}
242-
return {
243-
output: 'Skipping callback execution: No callback function defined for this test'
244-
};
235+
if (currentStepIndex === 0 && currentTest.fn) {
236+
await currentTest.fn(testContext);
237+
testContext.currentStepIndex = 1;
238+
return { output: 'Test function executed successfully' };
245239
} else {
246240
// Handle expectations
247241
const expectationIndex = currentStepIndex - 1;
248242
const expectation = currentTest.expectations?.[expectationIndex];
249243

250244
if (expectation?.fn) {
251-
await expectation.fn({ page: this.page });
245+
await expectation.fn(this.testContext);
252246
testContext.currentStepIndex = currentStepIndex + 1;
253247
return {
254248
output: `Callback function for "${expectation.description}" passed successfully`

0 commit comments

Comments
 (0)