Alex Spinov Cypress runs tests in the browser. Playwright controls the browser from outside. This fundamental...
Cypress runs tests in the browser. Playwright controls the browser from outside. This fundamental difference makes Playwright faster, more reliable, and more capable.
| Feature | Cypress | Playwright |
|---|---|---|
| Multi-browser | Chrome only (real) | Chromium, Firefox, WebKit |
| Multi-tab | No | Yes |
| iframes | Limited | Full support |
| File download | Workarounds | Native |
| Network interception | Yes | Yes (more powerful) |
| Parallel tests | Paid (Dashboard) | Free (built-in) |
| Auto-waiting | Yes | Yes (smarter) |
| Speed | Medium | Fast |
| Mobile emulation | Limited | Full (devices, geolocation) |
npm init playwright@latest
# Installs browsers, creates config, example tests
import { test, expect } from "@playwright/test";
test("user can sign up", async ({ page }) => {
await page.goto("/signup");
await page.getByLabel("Email").fill("test@example.com");
await page.getByLabel("Password").fill("SecurePass123!");
await page.getByRole("button", { name: "Create Account" }).click();
await expect(page.getByText("Welcome!")).toBeVisible();
await expect(page).toHaveURL("/dashboard");
});
Playwright auto-waits for elements to be:
// Cypress — needs explicit waits
cy.get("button").should("be.visible").click();
// Playwright — auto-waits, just click
await page.getByRole("button").click();
// By role (most reliable)
page.getByRole("button", { name: "Submit" });
page.getByRole("heading", { level: 1 });
page.getByRole("link", { name: "Dashboard" });
// By label (forms)
page.getByLabel("Email address");
page.getByLabel("Password");
// By placeholder
page.getByPlaceholder("Search...");
// By text
page.getByText("Welcome back");
page.getByText(/total: \$\d+/);
// By test ID (last resort)
page.getByTestId("submit-btn");
test("API: create user", async ({ request }) => {
const response = await request.post("/api/users", {
data: { name: "Alice", email: "alice@test.com" },
});
expect(response.ok()).toBeTruthy();
const body = await response.json();
expect(body.id).toBeDefined();
expect(body.name).toBe("Alice");
});
test("homepage looks correct", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveScreenshot("homepage.png");
});
test("button states", async ({ page }) => {
await page.goto("/components");
const button = page.getByRole("button", { name: "Submit" });
await expect(button).toHaveScreenshot("button-default.png");
await button.hover();
await expect(button).toHaveScreenshot("button-hover.png");
});
Built-in pixel diffing — no extra tools needed.
test("shows error on API failure", async ({ page }) => {
await page.route("/api/users", (route) =>
route.fulfill({ status: 500, body: "Server Error" })
);
await page.goto("/users");
await expect(page.getByText("Failed to load")).toBeVisible();
});
// playwright.config.ts
export default defineConfig({
workers: process.env.CI ? 4 : undefined,
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
});
Run tests across multiple workers for free. No paid dashboard needed.
Need test automation or web scraping? I build browser automation and data tools. Email spinov001@gmail.com or check my Apify tools.