├── .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 | ZeroStep Logo 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 | ZeroStep Logo 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 `