Skip to content
ExponaBLOG
PLAYWRIGHT & BROWSER AUTOMATION · DAY 043 / 100

Testing Angular and Vue apps: what developers should know

Same principles, any framework.

Expona Admin7 min read

Framework differences shouldn't matter for browser automation testing

You've built solid Playwright tests for your React application, but now your team is shipping a new Angular dashboard or migrating a Vue component library to production. The question hits immediately: do we start from scratch? The answer is no. Testing Angular and Vue apps: what developers should know boils down to one principle—Playwright works the same way regardless of the framework powering your UI. Whether you're clicking buttons in React, Angular, Vue, or Svelte, the browser sees the same DOM, fires the same events, and renders the same pixels. This series explores the practical implications of that framework-agnostic nature and how to leverage it to write tests that are maintainable, fast, and genuinely useful in production.

Playwright is framework-agnostic by design

Playwright operates at the browser level, not the framework level. It doesn't parse React hooks, Angular dependency injection, or Vue reactivity—it controls a real browser instance and inspects the DOM that gets rendered to users. This is both a strength and something that requires a shift in mindset compared to framework-specific testing tools.

When you write a test in Playwright, you're asserting on what the user sees and interacts with. A button rendered by React, Angular, or Vue looks identical to Playwright. The click() method works the same way. The selector logic doesn't change. This means your team can move between frameworks with confidence that the fundamentals of browser automation remain stable.

Consider a login form. In React, you might use a hook to manage state. In Angular, you'd reach for a form control. In Vue, you'd use a ref or reactive data. But to Playwright, it's just a form with an email input, a password input, and a submit button:

typescript

// Works identically regardless of framework
await page.fill('input[type="email"]', 'test@example.com');
await page.fill('input[type="password"]', 'secure-password');
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');

The framework underneath is irrelevant. Playwright speaks the browser's language, not the framework's.

Wait on UI state, not framework internals

The most common mistake when testing modern frameworks is waiting for framework-specific conditions instead of waiting for what the user actually sees. You might feel tempted to wait for Angular's zone to settle, Vue's reactivity to update, or React's render cycle to complete. Resist this temptation. It couples your tests to implementation details that change over time, makes tests harder to debug, and often doesn't even work reliably.

Instead, wait for UI state changes that matter to users: elements appearing, text changing, network requests completing, URLs shifting. Playwright gives you robust methods for this exact scenario.

Here's a concrete example. You have a data table that loads user records. An Angular version might look like this in the app code:

typescript

// Angular component (implementation detail - don't test this)
export class UserTableComponent {
  users$ = this.userService.getUsers().pipe(
    tap(() => this.loading = false),
    catchError(err => this.handleError(err))
  );
}

A Vue version might handle it differently:

typescript

// Vue component (different implementation)
const users = ref([]);
const loading = ref(true);

onMounted(async () => {
  users.value = await fetchUsers();
  loading.value = false;
});

Both render the same result. Your Playwright test should wait for the table rows to appear, not for loading flags to change:

typescript

// Good: wait for UI that the user cares about
await page.waitForSelector('table tbody tr');
const rowCount = await page.locator('table tbody tr').count();
expect(rowCount).toBeGreaterThan(0);

// Bad: wait for framework internals
await page.waitForFunction(() => {
  // Can't reliably access component state from the browser
  return window.__ngZone?.hasPendingTimers === false;
});

The first approach works across all frameworks. The second is fragile, framework-specific, and breaks when the component's internal structure changes.

Use test IDs for complex widgets

CSS selectors and XPath work fine for simple, stable elements. But when you're testing a complex widget—a dropdown with async search, a date picker, a tree view with dynamic nesting—selectors become brittle. A designer updates the DOM structure, and suddenly your test breaks. This is where test IDs shine.

A test ID is a stable attribute you add to HTML elements specifically for automation. It doesn't change when styling changes. It doesn't break when the DOM hierarchy shifts. It's a contract between your test code and your component code that says "this element is important for testing."

In Angular, you'd add it to your template:

typescript

// Angular template
<select data-testid="user-role-dropdown">
  <option value="admin">Admin</option>
  <option value="user">User</option>
</select>

In Vue:

typescript

// Vue template
<select data-testid="user-role-dropdown">
  <option value="admin">Admin</option>
  <option value="user">User</option>
</select>

In your Playwright test, you use the same selector syntax:

typescript

// Same syntax, any framework
await page.selectOption('[data-testid="user-role-dropdown"]', 'admin');
const selected = await page.inputValue('[data-testid="user-role-dropdown"]');
expect(selected).toBe('admin');

Test IDs are framework-agnostic. They're just HTML attributes. When you use them consistently across Angular, Vue, and React apps, your test code becomes remarkably portable and stable.

Start the dev server via webServer

Playwright's webServer configuration handles dev server lifecycle for you. Instead of manually starting your Angular CLI, Vue dev server, or React dev server before running tests, you let Playwright manage it. This ensures your tests always run against a clean, fresh dev environment and that the process shuts down cleanly afterward.

Your Playwright config file stays consistent across frameworks. Here's an Angular setup:

typescript

// playwright.config.ts for Angular
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  webServer: {
    command: 'ng serve',
    url: 'http://localhost:4200',
    reuseExistingServer: process.env.CI ? false : true,
  },
  use: { baseURL: 'http://localhost:4200' },
  // ... rest of config
});

For Vue, it's nearly identical:

typescript

// playwright.config.ts for Vue
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:5173',
    reuseExistingServer: process.env.CI ? false : true,
  },
  use: { baseURL: 'http://localhost:5173' },
  // ... rest of config
});

The pattern is identical. You declare the command to start the dev server, the URL it will be available at, and Playwright waits for that URL to respond before running your tests. When your test suite finishes, Playwright terminates the dev server process. No manual setup, no stray processes left behind.

In CI environments, set reuseExistingServer to false to ensure a fresh start. Locally, set it to true so you can keep your dev server running and tests hit it repeatedly without restart overhead.

Common mistakes

  • Testing framework internals: Waiting for Angular's change detection, Vue's reactivity flush, or React state updates instead of waiting for DOM changes. This couples your tests to implementation details and makes them fragile.
  • Over-relying on implicit waits: Using waitForTimeout(1000) instead of waitForSelector() or waitForURL(). Timeouts are flaky—they pass when things are slow and fail when they're fast.
  • Mixing framework-specific tools into Playwright: Trying to use Angular's TestBed or Vue's mount API from within Playwright tests. Playwright runs in the browser; it can't access server-side or build-time APIs. Stay in the browser's domain.
  • Forgetting to add test IDs to new components: When a developer adds a new complex widget to an Angular or Vue app, they often forget that QA will need to test it. Make test ID addition part of your code review process.
  • Running tests against production URLs in dev:**Hardcoding production URLs in tests means your tests can fail when production is down, and they don't run in isolation from other test suites. Always use baseURL and local dev environments.

Key takeaways

  • Playwright is genuinely framework-agnostic—the same test code and patterns work for Angular, Vue, React, and beyond because Playwright operates at the browser level, not the framework level.
  • Wait for DOM changes and user-visible state, not framework internals like change detection cycles or reactivity flushes.
  • Add data-testid attributes to complex components so selectors remain stable as designs and DOM structures evolve.
  • Use Playwright's webServer configuration to let Playwright manage dev server lifecycle, keeping your test setup clean and portable across projects.
  • When your team moves between frameworks or maintains multiple apps, the core Playwright patterns stay the same—your testing knowledge transfers directly.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles