Using Cursor IDE AI to Write Playwright Tests
Today, we will explore using an AI-centric editor to help design and create test cases for a well-known framework.
Cursor is a code editor designed around AI with features to optimize interactions and make the user "extraordinarily productive." To try it out, let's set up a typical project for Playwright and generate some tests.
1. Set up the framework
Creating an empty directory with the name "playwright-cursor", I gave the editor the prompt: I want to initialize the latest Playwright version in this folder.
It successfully determined what I wanted and started the installation process and walkthrough for setting up a new Playwright project.
All the files look correct, and the config file is the default with 3 projects (Chromium, Firefox, and Webkit) defined.
For this example, let's only use one project (Chromium) for simplicity. I have modified the configuration only to use Chromium.
2. Generate Tests
Now, let's try giving a prompt to generate a test based on a web page. On the cursor.com site, there is a price page with three options. As a test, I want to navigate to this page and verify that there are three price options. I provided the agent with the following prompt: Write a test that goes to @https://www.cursor.com/, navigates to the pricing page, and verifies there are 3 price options.
A new file tests\cursor-pricing.spec.ts
was generated with the following code:
// tests\cursor-pricing.spec.ts
import { test, expect } from '@playwright/test';
test('verify cursor pricing options', async ({ page }) => {
// Navigate to Cursor's homepage
await page.goto('https://www.cursor.com/');
// Click on the pricing link in the navigation
await page.getByRole('link', { name: 'Pricing' }).click();
// Wait for the pricing page to load and verify URL
await expect(page).toHaveURL('https://www.cursor.com/pricing');
// Wait for pricing cards to be visible
await page.waitForSelector('[data-testid="pricing-card"]');
// Get all pricing cards and verify there are exactly 3
const pricingCards = await page.locator('[data-testid="pricing-card"]').all();
expect(pricingCards.length).toBe(3);
// Optional: Verify each pricing tier has a title
const pricingTitles = await page.locator('[data-testid="pricing-card"] h2').allTextContents();
expect(pricingTitles.length).toBe(3);
// Log the pricing tiers for verification
console.log('Found pricing tiers:', pricingTitles);
});
Generated test file
Going over the good first: impressively, it creates a nice test file with an accurate test name and descriptions of tests. It follows best practices as well by waiting for elements to be visible before interacting with them (although with a discouraged method) and using test IDs for locators. The output is very helpful as well, explaining exactly what was done.
What could be improved, however, is the fact that it chose a discouraged method to wait for the selectors (locator.waitFor() is preferred
). Also, the test will fail as is because the page has more than one locator matching the pricing link locator. This creates a great opportunity to use the in-place editor.
3. Editing using AI
After highlighting await page.getByRole('link', { name: 'Pricing' }).click();
and doing the command (Ctr-K) for code edit, I asked to make the locator more unique and unsurprisingly, it appended the .first()
method that addressed the issue. Fixing the completely wrong locator, however, proved challenging for the model to figure out. Since the page lacks test IDs, I proposed using a class locator, but it generated a plausible class name rather than the actual one on the page. At this point, it would be faster to manually look at the element and grab the class name.
4. Refactoring
After fixing up the code to be passable, the next step I would take is to refactor some items into reusable methods and properties. Let's check if the editor can assist.
tests\cursor-pricing.spec.ts
import { test, expect } from '@playwright/test';
test('verify cursor pricing options', async ({ page }) => {
// Navigate to Cursor's homepage
await page.goto('https://www.cursor.com/');
// Click on the pricing link in the navigation
await page.getByRole('link', { name: 'Pricing', exact: true }).first().click();
// Wait for the pricing page to load and verify URL
await expect(page).toHaveURL('https://www.cursor.com/pricing');
// Wait for pricing cards to be visible
await page.locator('li.list-none').first().waitFor();
// Get all pricing cards and verify there are exactly 3
const pricingCards = await page.locator('li.list-none').all();
expect(pricingCards.length).toBe(3);
});
Passing test code
I want to make a Page Object and throw the locators as properties of the page. Since we are dealing with two pages, let's create two pages and move the respective locators.
I gave the agent the highlighted locator as context and the prompt: Please create a new page object named "homepage" and move the locator here as property of the page named "pricingLink".
It delivered exactly what I expected! It even went further and added the goTo as a method of the page.
// tests\pages\homepage.ts
import { Page } from '@playwright/test';
export class HomePage {
readonly page: Page;
readonly pricingLink;
constructor(page: Page) {
this.page = page;
this.pricingLink = this.page.getByRole('link', { name: 'Pricing', exact: true }).first();
}
async goto() {
await this.page.goto('https://www.cursor.com/');
}
}
New page file
// tests\cursor-pricing.spec.ts
import { test, expect } from '@playwright/test';
import { HomePage } from './pages/homepage';
test('verify cursor pricing options', async ({ page }) => {
// Initialize page objects
const homepage = new HomePage(page);
// Navigate to Cursor's homepage
await homepage.goto();
// Click on the pricing link in the navigation
await homepage.pricingLink.click();
// Wait for the pricing page to load and verify URL
await expect(page).toHaveURL('https://www.cursor.com/pricing');
// Wait for pricing cards to be visible
await page.locator('li.list-none').first().waitFor();
// Get all pricing cards and verify there are exactly 3
const pricingCards = await page.locator('li.list-none').all();
expect(pricingCards.length).toBe(3);
});
Updated test file
Now, let's do the same for the price page.
import { test, expect } from '@playwright/test';
import { HomePage } from './pages/homepage';
import { PricingPage } from './pages/pricing-page';
test('verify cursor pricing options', async ({ page }) => {
// Initialize page objects
const homepage = new HomePage(page);
const pricingPage = new PricingPage(page);
// Navigate to Cursor's homepage
await homepage.goto();
// Click on the pricing link in the navigation
await homepage.pricingLink.click();
// Wait for the pricing page to load and verify URL
await expect(page).toHaveURL('https://www.cursor.com/pricing');
// Wait for pricing cards to be visible
await pricingPage.waitForPricingCards();
// Get all pricing cards and verify there are exactly 3
await pricingPage.verifyPricingCardsCount(3);
});
Refactored test file
Finally, let's see if the AI can understand that I want to move the page initializations into Playwright fixtures.
Let's move page initializing into a fixtures file
Once again, it understood exactly what I wanted and easily made the changes.
// tests\fixtures.ts
import { test as base } from '@playwright/test';
import { HomePage } from './pages/homepage';
import { PricingPage } from './pages/pricing-page';
// Define the fixtures type
type Pages = {
homepage: HomePage;
pricingPage: PricingPage;
};
// Extend the base test with our fixtures
export const test = base.extend<Pages>({
homepage: async ({ page }, use) => {
await use(new HomePage(page));
},
pricingPage: async ({ page }, use) => {
await use(new PricingPage(page));
},
});
// Export expect for convenience
export { expect } from '@playwright/test';
New fixtures file
// tests\cursor-pricing.spec.ts
import { test, expect } from './fixtures';
test('verify cursor pricing options', async ({ homepage, pricingPage, page }) => {
// Navigate to Cursor's homepage
await homepage.goto();
// Click on the pricing link in the navigation
await homepage.pricingLink.click();
// Wait for the pricing page to load and verify URL
await expect(page).toHaveURL('https://www.cursor.com/pricing');
// Wait for pricing cards to be visible
await pricingPage.waitForPricingCards();
// Get all pricing cards and verify there are exactly 3
await pricingPage.verifyPricingCardsCount(3);
});
Final test file
This is where I think the editor shines. Imagine if you were refactoring code that did not have page objects implemented and had a ton of potential locator properties. This would save so much time over trying to manually update the code.
Final thoughts and considerations
The Good
I think this editor has great potential to optimize SDETs' time by helping reduce tedious tasks. It made framework setup and code refactoring a breeze. The type-ahead was also very quick and helpful compared to other IDEs.
The Wary
As we saw with the locator generation, the model tried to predict the names rather than pull them off pages. This could lead to confusion if it guesses correctly on some elements but erroneously on others. However, it did create a solid test file that can be used as a template and then fixed by hand.