├── .gitignore
├── LICENSE.md
├── README.md
├── examples
└── playwright-demo
│ ├── package.json
│ ├── playwright.config.ts
│ ├── tests
│ ├── test-with-fixture.ts
│ ├── zerostep-demo.spec.ts
│ └── zerostep-regression.spec.ts
│ └── zerostep.config.json
├── package.json
└── packages
└── playwright
├── .npmignore
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── RELEASING.md
├── package-lock.json
├── package.json
├── src
├── cdp.ts
├── config.ts
├── index.ts
├── meta.ts
├── playwright.ts
├── types.ts
└── webSocket.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # misc
2 | .DS_Store
3 |
4 | # playwright
5 | packages/playwright/lib
6 | packages/playwright/node_modules
7 | packages/playwright/tmp.README.md
8 |
9 | # examples
10 | examples/playwright-demo/package-lock.json
11 | examples/playwright-demo/node_modules
12 | examples/playwright-demo/test-results
13 | examples/playwright-demo/playwright-report
14 | examples/playwright-demo/playwright/.cache
15 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Reflect Software Inc
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
13 |
14 |
15 |
16 | # Zerostep
17 |
18 | Supercharge your Playwright tests with AI. Learn more at https://zerostep.com
19 |
20 | ## Setup
21 |
22 | 1. Install the @zerostep/playwright dependency
23 | ```sh
24 | $ npm i @zerostep/playwright -D
25 | ```
26 |
27 | 2. This package relies on an environment variable with your zerostep token being exposed to
28 | the playwright process, or a config file that holds the token. This token can be found
29 | in your account on https://app.zerostep.com. You can expose the environment variable
30 | however you'd like, e.g.
31 | ```sh
32 | $ export ZEROSTEP_TOKEN=""
33 | ```
34 | Alternatively, you can create a `zerostep.config.json` file in the root of your project
35 | and store the token there, e.g.
36 | ```json
37 | {
38 | "TOKEN": ""
39 | }
40 | ```
41 |
42 | 3. Import and use the `ai` function
43 | ```ts
44 | import { test } from '@playwright/test'
45 | import { ai } from '@zerostep/playwright'
46 |
47 | test('zerostep example', async ({ page }) => {
48 | await page.goto('https://zerostep.com/')
49 |
50 | // An object with page and test must be passed into every call
51 | const aiArgs = { page, test }
52 | const headerText = await ai('Get the header text', aiArgs)
53 | await page.goto('https://google.com/')
54 | await ai(`Type "${headerText}" in the search box`, aiArgs)
55 | await ai('Press enter', aiArgs)
56 | })
57 | ```
58 |
59 | ## Usage
60 |
61 | At minimum, the `ai()` function requires a plain text prompt and an argument that contains your
62 | `page` and `test` objects.
63 |
64 | ```ts
65 | ai('', { page, test })
66 | ```
67 |
68 | You can also pass multiple prompts in an array as the first argument. In that
69 | case prompts will be run concurrently in chunks. The number of prompts being run
70 | in a chunk defaults to `10` and can be controlled by `options`, see below. Note
71 | that each prompt passed into the array counts as a single `ai()` call.
72 |
73 | ```ts
74 | ai(['', '', ''])
75 | ```
76 |
77 | ### Playwright Fixture
78 |
79 | The `zerostep/playwright` library ships with a playwright fixture out of the box. This allows
80 | you to call `ai()` steps without passing the `{ test, page }` argument every time. You can
81 | use the [playwright docs](https://playwright.dev/docs/test-fixtures#creating-a-fixture) as a guide
82 | to get setup, but here's some example code
83 |
84 | ```ts
85 | // my-test.ts
86 | import { test as base } from '@playwright/test'
87 | import { aiFixture, type AiFixture } from '@zerostep/playwright'
88 |
89 | export const test = base.extend({
90 | ...aiFixture(base),
91 | })
92 | ```
93 |
94 | ```ts
95 | // my-spec.ts
96 | import { test } from './my-test.ts'
97 |
98 | test('I can foo', async ({ ai }) => {
99 | await ai('click bar')
100 | })
101 | ```
102 |
103 | There is example code in the `/examples/playwright-demo/tests/zerostep-regression.spec.ts` file
104 |
105 |
106 | ### Supported Browsers
107 |
108 | This package only supports executing `ai()` steps in Chromium browsers.
109 |
110 | ### Additional Options
111 |
112 | There are additional options you can pass as a third argument
113 |
114 | ```ts
115 | const options = {
116 | debug?: boolean, // If true, debugging information is returned from the ai() call.
117 | type?: 'action' | 'assert' | 'query', // Forces the ai step to be interpreted as the specified type.
118 | model?: 'GPT_3.5', // The ai model to use, only GPT_3.5 is supported
119 | disableScroll?: boolean, // If true, the ai will not scroll out of view elements into view.
120 | parallelism?: number, // The number of prompts that will be run in a chunk, applies when passing an array of prompts to ai(). Defaults to 10.
121 | failImmediately?: boolean // If true and an array of prompts was provided, the function will throw immediately if any prompt throws. Defaults to false.
122 | }
123 |
124 | ai('', { page, test }, options)
125 | ```
126 |
127 | ### Supported Actions & Return Values
128 |
129 | Depending on the `type` of action (specified above or inferred by the ai function), there
130 | are different behaviors and return types.
131 |
132 | **Action**: An action (e.g. "click") is some simulated user interaction with the page, e.g.
133 | a click on a link. It will scroll to perform the given task if required, but favors elements
134 | within the current viewport. Actions will return undefined if they were successful and will
135 | throw an error if they failed, e.g.
136 |
137 | ```ts
138 | try {
139 | await ai('Click the link', { page, test })
140 | } catch (e) {
141 | console.error('Failed to click the link')
142 | }
143 | ```
144 |
145 | Action prompts will resolve to one or more of the following browser actions:
146 | - Click
147 | - Hover
148 | - Text Input
149 | - 'Enter' keypress
150 | - Scroll
151 | - Navigating to a new URL
152 |
153 | Other browser actions such as drag-and-drops and file uploads are not currently supported.
154 |
155 | **Query**: A query will return requested data from the visible portion of the page as a string, e.g.
156 |
157 | ```ts
158 | const linkText = await ai('Get the text of the first link', { page, test })
159 | console.log('The link text is', linkText)
160 | ```
161 |
162 | **Assert**: An assertion is a question that will return true or false based on the visible portion of the page, e.g.
163 | ```ts
164 | const thereAreThreeLinks = await ai('Are there 3 links on the page?', { page, test })
165 | console.log(`"There are 3 links" is a ${thereAreThreeLinks} statement`)
166 | ```
167 |
168 | ## Examples
169 |
170 | This repository comes with a demo to quickly experiment with the ai() function. In order to
171 | start using it you need to
172 |
173 | 1. Build the local version of the zerostep/playwright package
174 | ```sh
175 | cd packages/playwright
176 | npm install
177 | npm run build
178 | ```
179 | 2. Install the zerostep/playwright dependency in the examples directory
180 | ```sh
181 | cd ../../examples/playwright-demo
182 | npm install
183 | ```
184 | 3. Expose the `ZEROSTEP_TOKEN` environment variable or config value (see the "Setup" section above)
185 | 4. Run the tests, with or without UI mode
186 | ```sh
187 | $ npm run test # or npm run test-ui
188 | ```
189 |
190 | ## Best Practices
191 |
192 | ZeroStep AI prompts need not conform to any predefined syntax. However, we recommend following these best practices to
193 | ensure your prompts work as you intend:
194 |
195 | - Write your prompts in complete English sentences with no spelling or grammatical mistakes.
196 | - Put quotes around any text that should appear exactly as described. e.g. `Click on the "Login" button`
197 | - Don't include CSS/XPath selectors in your prompt, or any other implementation-level details.
198 | - Don't combine two or more instructions in the same prompt. e.g.
199 | `Click on the Settings icon and then click on the "User Profile" link`. Instead, each prompt should contain one
200 | distinct action, query, or assertion.
201 | - Note: The exception here is Action prompts that perform a single logical task which is accomplished by multiple
202 | actions. In other words, a prompt like `Fill out the form with realistic values` is a perfectly fine prompt.
203 | - Write prompts to the level of specificity dictated by your requirements. Some level of ambiguity is fine and even
204 | desirable in a lot of circumstances. A prompt such as `Click on the "Get Started" link` will work even when there are
205 | multiple "Get Started" links on the page, or if the page is completely redesigned.
206 |
207 | ## Community
208 |
209 | Have questions or suggestions? [Join our Discord](https://discord.gg/BcDmfWqSGe)!
210 |
211 |
212 |
213 |
214 |
215 |
220 |
225 |
226 |
227 |
--------------------------------------------------------------------------------
/examples/playwright-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playwright-demo",
3 | "version": "0.0.1",
4 | "license": "MIT",
5 | "scripts": {
6 | "test": "npx playwright test",
7 | "test-ui": "npx playwright test --ui"
8 | },
9 | "devDependencies": {
10 | "@playwright/test": "1.39.0",
11 | "@types/node": "^20.8.6",
12 | "@zerostep/playwright": "../../packages/playwright"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/examples/playwright-demo/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/test';
2 |
3 | /**
4 | * Read environment variables from file.
5 | * https://github.com/motdotla/dotenv
6 | */
7 | // require('dotenv').config();
8 |
9 | /**
10 | * See https://playwright.dev/docs/test-configuration.
11 | */
12 | export default defineConfig({
13 | testDir: './tests',
14 | /* Run tests in files in parallel */
15 | fullyParallel: true,
16 | /* Fail the build on CI if you accidentally left test.only in the source code. */
17 | forbidOnly: !!process.env.CI,
18 | /* Retry on CI only */
19 | retries: process.env.CI ? 2 : 0,
20 | /* Opt out of parallel tests on CI. */
21 | workers: process.env.CI ? 1 : undefined,
22 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
23 | reporter: 'html',
24 | timeout: 120_000,
25 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
26 | use: {
27 | /* Base URL to use in actions like `await page.goto('/')`. */
28 | // baseURL: 'http://127.0.0.1:3000',
29 |
30 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
31 | trace: 'on-first-retry',
32 | },
33 |
34 | /* Configure projects for major browsers */
35 | projects: [
36 | {
37 | name: 'chromium',
38 | use: { ...devices['Desktop Chrome'] },
39 | },
40 |
41 | // {
42 | // name: 'firefox',
43 | // use: { ...devices['Desktop Firefox'] },
44 | // },
45 |
46 | // {
47 | // name: 'webkit',
48 | // use: { ...devices['Desktop Safari'] },
49 | // },
50 |
51 | /* Test against mobile viewports. */
52 | // {
53 | // name: 'Mobile Chrome',
54 | // use: { ...devices['Pixel 5'] },
55 | // },
56 | // {
57 | // name: 'Mobile Safari',
58 | // use: { ...devices['iPhone 12'] },
59 | // },
60 |
61 | /* Test against branded browsers. */
62 | // {
63 | // name: 'Microsoft Edge',
64 | // use: { ...devices['Desktop Edge'], channel: 'msedge' },
65 | // },
66 | // {
67 | // name: 'Google Chrome',
68 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
69 | // },
70 | ],
71 |
72 | /* Run your local dev server before starting the tests */
73 | // webServer: {
74 | // command: 'npm run start',
75 | // url: 'http://127.0.0.1:3000',
76 | // reuseExistingServer: !process.env.CI,
77 | // },
78 | });
79 |
--------------------------------------------------------------------------------
/examples/playwright-demo/tests/test-with-fixture.ts:
--------------------------------------------------------------------------------
1 | import { test as base } from '@playwright/test'
2 | import { aiFixture, type AiFixture } from '@zerostep/playwright'
3 |
4 | export const test = base.extend({
5 | ...aiFixture(base),
6 | })
7 |
8 | export { expect } from '@playwright/test'
--------------------------------------------------------------------------------
/examples/playwright-demo/tests/zerostep-demo.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test'
2 | import { ai } from '@zerostep/playwright'
3 |
4 | test.describe('ZeroStep', () => {
5 | test('throws useful error messages in UI mode when there is no page', async ({ page }) => {
6 | let error: Error | null = null
7 |
8 | await ai('Click on the button (there is no page)', { page, test }).catch((e) => error = e)
9 |
10 | expect(error).toBeDefined()
11 | })
12 |
13 | test('throws useful error messages in UI mode when the task is too long', async ({ page }) => {
14 | let error: Error | null = null
15 | let taskString = '0'.repeat(2_001)
16 |
17 | await ai(taskString, { page, test }).catch((e) => error = e)
18 |
19 | expect(error).toBeDefined()
20 | })
21 | })
22 |
23 | test.describe('Calendly', () => {
24 | test('book the next available timeslot', async ({ page }) => {
25 | await page.goto('https://calendly.com/zerostep-test/test-calendly')
26 |
27 | await page.waitForSelector('[data-testid="calendar"]')
28 | await ai('Dismiss the privacy modal', { page, test })
29 | await ai('Click on the first day in the month with times available', { page, test })
30 | await ai('Click on the first available time in the sidebar', { page, test })
31 | // Alternatively: await ai('Click the "Next" button', { page, test })
32 | await page.locator('[data-container="selected-spot"] button:nth-of-type(2)').click()
33 | await ai('Fill out the form with realistic values', { page, test })
34 | await page.getByText('Schedule Event').click()
35 |
36 | const element = await page.getByText('You are scheduled')
37 | expect(element).toBeDefined()
38 | })
39 | })
40 |
41 | test.describe('GitHub', () => {
42 | test('verify the number of labels in a repo', async ({ page }) => {
43 | await page.goto('https://github.com/zerostep-ai/zerostep')
44 |
45 | await ai(`Click on the Issues tabs`, { page, test })
46 | await page.waitForURL('https://github.com/zerostep-ai/zerostep/issues')
47 |
48 | // Alternatively: await ai('Click on Labels', { page, test })
49 | await page.locator('[role="search"] a[href="/zerostep-ai/zerostep/labels"]').click()
50 | await page.waitForURL('https://github.com/zerostep-ai/zerostep/labels')
51 |
52 | const numLabels = await ai('How many labels are listed?', { page, test }) as string
53 |
54 | expect(parseInt(numLabels)).toEqual(9)
55 | })
56 | })
57 |
58 | test.describe('Google', () => {
59 | const searchTerm = 'software testing'
60 |
61 | test('search and verify the first organic search result', async ({ page }) => {
62 | await page.goto('https://www.google.com')
63 |
64 | await ai(`Search for '${searchTerm}'`, { page, test })
65 | await page.keyboard.press('Enter')
66 |
67 | await page.waitForURL('https://www.google.com/search**')
68 |
69 | const title = await ai(`What is the title of the first organic search result?`, { page, test })
70 |
71 | console.log('First organic search result is: ', title)
72 | })
73 | })
74 |
75 | test.describe('New York Times', () => {
76 | test('go to section and verify ad is displayed', async ({ page }) => {
77 | await page.goto('https://www.nytimes.com')
78 |
79 | await ai(`Hover over the World top nav item`, { page, test })
80 | await ai('Click the "World" section', { page, test })
81 | const cta = await ai('What is the CTA of the ad at the top of the page?', { page, test })
82 |
83 | console.log('Call to action is: ', cta)
84 | })
85 | })
86 |
87 | test.describe('Wikipedia', () => {
88 | test('view article history and verify earliest revision', async ({ page }) => {
89 | await page.goto('https://en.wikipedia.org/wiki/Software_testing')
90 |
91 | // Alternatively: await ai('Click View History', { page, test })
92 | await page.locator('#right-navigation #p-views').getByText('View History').click()
93 | await ai('Sort by "oldest"', { page, test })
94 | const date = await ai('What is the date of the first revision listed on this page?', { page, test })
95 |
96 | expect(date).toEqual('16 April 2004')
97 | })
98 | })
99 |
100 | test.describe('Yahoo Finance', () => {
101 | test('get the latest stock price', async ({ page }) => {
102 | await page.goto('https://finance.yahoo.com')
103 |
104 | const price = await ai('Return the current price for the S&P 500. Strip out all commas.', { page, test }) as string
105 | const formattedPrice = parseFloat(price)
106 |
107 | expect(formattedPrice > 4000).toEqual(true)
108 | })
109 | })
110 |
111 | // Replace these values with your Salesforce credentials
112 | const email = 'test@example.com'
113 | const password = 'passwordhere'
114 | const hostname = 'realhostnamehere.develop.lightning.force.com'
115 |
116 | test.describe('Salesforce', () => {
117 | test('create an opportunity', async ({ page }) => {
118 | test.skip(email === 'test@example.com', 'Replace placeholder values to run this test');
119 |
120 | await page.goto('https://login.salesforce.com')
121 | await ai(`Enter the username ${email}`, { page, test })
122 | await ai(`Enter the password ${password}`, { page, test })
123 | await page.click('text="Log In"')
124 |
125 | // Only reaches here if we are successfully authenticated
126 | await page.waitForSelector('text="Home"')
127 |
128 | // Navigate directly to Sales app
129 | await page.goto(`https://${hostname}/lightning/page/home`)
130 | await page.waitForSelector('text="Quarterly Performance"')
131 |
132 | await ai('Click on Opportunities link', { page, test })
133 | await page.click('text="New"')
134 |
135 | // Wait for 'New Opportunity' form to be displayed
136 | await page.waitForSelector('text="New Opportunity"')
137 |
138 | await ai(`Enter '12000' in the Amount field.`, { page, test })
139 | await ai('Enter Test in the opportunity name input', { page, test })
140 |
141 | const thirtyDaysFromNow = new Date()
142 | thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30)
143 | const closeDate = thirtyDaysFromNow.toLocaleDateString('en-US', {
144 | month: '2-digit',
145 | day: '2-digit',
146 | year: 'numeric',
147 | })
148 |
149 | await ai(`Input ${closeDate} into the Close Date field`, { page, test })
150 | await ai('Click on the Stage dropdown', { page, test })
151 | await ai('Click on the Needs Analysis option', { page, test })
152 | await ai('Click Save', { page, test })
153 |
154 | const result = await ai('What is the current stage of the opportunity?', { page, test })
155 | expect(result).toEqual('Needs Analysis')
156 | })
157 | })
158 |
--------------------------------------------------------------------------------
/examples/playwright-demo/tests/zerostep-regression.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from './test-with-fixture.ts'
2 |
3 | test.describe('SauceDemo', () => {
4 | test('can login and logout', async ({ page, ai }) => {
5 | await page.goto('https://www.saucedemo.com/')
6 | const [username, password] = await ai([
7 | 'Get the first accepted username',
8 | 'Get the accepted password',
9 | ])
10 | await ai([
11 | `Enter ${username} as the username`,
12 | `Enter ${password} as the password`
13 | ])
14 | await ai('Click Login')
15 | await ai('Click the menu button')
16 | await ai('Click the logout link')
17 | })
18 |
19 | test('can login and checkout', async ({ page, ai }) => {
20 | await page.goto('https://www.saucedemo.com/')
21 | const [username, password] = await ai([
22 | 'Get the first accepted username',
23 | 'Get the accepted password',
24 | ])
25 | await ai([
26 | `Enter ${username} as the username`,
27 | `Enter ${password} as the password`
28 | ])
29 | await ai('Click Login')
30 | await ai('Sort by price high to low')
31 | const [priceOne, priceTwo] = await ai([
32 | 'Get the plain number price of the first item',
33 | 'Get the plain number price of the second item',
34 | ])
35 | await ai('Add the first 2 items you can to the cart')
36 | await ai('Go to the cart')
37 | await ai('Go to the checkout page')
38 | await ai('Fill out the form with realistic values')
39 | const [tax, total] = await ai([
40 | 'Get the plain number cost of tax',
41 | 'Get the plain number total cost',
42 | ])
43 |
44 | const parsedPrice = parseFloat((parseFloat(priceOne) + parseFloat(priceTwo) + parseFloat(tax)).toFixed(2))
45 | const parsedTotal = parseFloat((parseFloat(total)).toFixed(2))
46 |
47 | console.log(`total=${parsedTotal}, computed=${parsedPrice}`)
48 | expect(parsedPrice).not.toBeNaN()
49 | expect(parsedPrice).toEqual(parsedTotal)
50 | })
51 | })
52 |
53 | test.describe('OrangeHRM', () => {
54 | test('can login and checkout', async ({ page, ai }) => {
55 | await page.goto('https://opensource-demo.orangehrmlive.com/web/index.php/auth/login', { waitUntil: 'networkidle' })
56 | const [username, password] = await ai([
57 | 'Get the username listed on the page',
58 | 'Get the password listed on the page',
59 | ])
60 | await ai([
61 | `Enter ${username} as the username`,
62 | `Enter ${password} as the password`
63 | ])
64 | await ai('Click Login')
65 | await page.waitForTimeout(5_000)
66 | await ai('Search for "performance"')
67 | await ai('Click the Performance link')
68 | await page.waitForTimeout(2_000)
69 | await ai('Enter "Fiona" in the employee name input')
70 | await page.waitForTimeout(2_000)
71 | await ai('Click "Fiona Grace"')
72 | await ai('Click the "Search" button in the employee reviews section')
73 | const noRecordsFound = await ai('Confirm there are no records found')
74 | expect(noRecordsFound).toEqual(true)
75 | })
76 | })
77 |
78 | test.describe('JSPaint', () => {
79 | test('can fill the canvas', async ({ page, ai }) => {
80 | await page.goto('https://jspaint.app/')
81 |
82 | await ai('Click the paint bucket')
83 | await ai('Click the canvas')
84 | await ai('Click the eye dropper')
85 | await ai('Click the canvas')
86 |
87 | const foregroundColorHandle = await page.locator('.swatch.color-selection.foreground-color').evaluateHandle((e) => e.getAttribute('data-color'))
88 | const foregroundColor = await foregroundColorHandle.jsonValue()
89 | expect(foregroundColor).toEqual('rgba(0,0,0,1)')
90 | })
91 | })
92 |
93 | test.describe('Reflect', () => {
94 | test('can scroll elements to the bottom', async ({ page, ai }) => {
95 | await page.goto('https://reflect.run/docs/')
96 | await ai('Scroll the sidebar navigation to the bottom')
97 |
98 | const scrollTop = await page.evaluate(() => {
99 | return document.querySelector('#docs-left-nav > div')?.scrollTop
100 | })
101 |
102 | console.log('scrollTop', scrollTop)
103 |
104 | expect(scrollTop).toBeTruthy()
105 | })
106 |
107 | test('can scroll elements to the top', async ({ page, ai }) => {
108 | await page.goto('https://reflect.run/docs/')
109 | await ai('Scroll the sidebar navigation to the bottom')
110 | await ai('Scroll the sidebar navigation to the top')
111 |
112 | const scrollTop = await page.evaluate(() => {
113 | return document.querySelector('#docs-left-nav > div')?.scrollTop
114 | })
115 |
116 | console.log('scrollTop', scrollTop)
117 |
118 | expect(scrollTop).toBe(0)
119 | })
120 |
121 | test('can scroll elements down', async ({ page, ai }) => {
122 | await page.goto('https://reflect.run/docs/')
123 | await ai('Scroll the sidebar navigation down')
124 |
125 | const scrollTop = await page.evaluate(() => {
126 | return document.querySelector('#docs-left-nav > div')?.scrollTop
127 | })
128 |
129 | console.log('scrollTop', scrollTop)
130 |
131 | expect(scrollTop).toBeTruthy()
132 | })
133 |
134 | test('can scroll elements up', async ({ page, ai }) => {
135 | await page.goto('https://reflect.run/docs/')
136 | await ai('Scroll the sidebar navigation down')
137 | await ai('Scroll the sidebar navigation up')
138 |
139 | const scrollTop = await page.evaluate(() => {
140 | return document.querySelector('#docs-left-nav > div')?.scrollTop
141 | })
142 |
143 | console.log('scrollTop', scrollTop)
144 |
145 | expect(scrollTop).toBe(0)
146 | })
147 | })
148 |
--------------------------------------------------------------------------------
/examples/playwright-demo/zerostep.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "TOKEN": "",
3 | "LOGS_ENABLED": true
4 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zerostep-monorepo",
3 | "version": "0.0.1",
4 | "description": "Supercharge your Playwright tests with AI",
5 | "author": "zerostep",
6 | "license": "MIT",
7 | "homepage": "https://github.com/zerostep-ai/zerostep#readme",
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/zerostep-ai/zerostep.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/zerostep-ai/zerostep/issues"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/playwright/.npmignore:
--------------------------------------------------------------------------------
1 | # Ignores everything (except for package.json, CHANGELOG, LICENSE and README) https://docs.npmjs.com/misc/developers
2 | **/*
3 |
4 | # Include the lib itself
5 | !lib/**/*.js
6 | !LICENSE.md
7 | !CHANGELOG.md
--------------------------------------------------------------------------------
/packages/playwright/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | Documentation on notable changes to the package and release notes
4 |
5 | ---
6 |
7 | ## [0.1.5](https://www.npmjs.com/package/@zerostep/playwright/v/0.1.5) - 12-08-2023
8 |
9 | **What's New**
10 |
11 | - Can execute scroll commands against specific elements, including those in iframes
12 |
13 | ---
14 |
15 | ## [0.1.4](https://www.npmjs.com/package/@zerostep/playwright/v/0.1.4) - 12-01-2023
16 |
17 | **What's New**
18 |
19 | - Adds support for a `zerostep.config.json` file at the root that can store the TOKEN
20 | - Improves interactions with `ElementHandle` in `ShadowRoot`
21 | - Include package version information in error messages
22 |
23 | ---
24 |
25 | ## [0.1.3](https://www.npmjs.com/package/@zerostep/playwright/v/0.1.3) - 11-28-2023
26 |
27 | **What's New**
28 |
29 | - Improved error messaging, especially in UI mode
30 | - Fixes for actions that target `