# Browser Contexts, Pages, and Emulation

Course: [Playwright](https://www.devtutorials.ai/learn/playwright) · Section: Browser Control
Source: https://www.devtutorials.ai/learn/playwright/browser-control/browser-contexts-pages-and-emulation

> The browser, context and page hierarchy, how contexts isolate tests, creating contexts for multi-user tests, working with several pages, popups and new windows, setting the viewport and emulating devices, locale, timezone, geolocation and permissions, colour scheme, reduced motion, user agent and disabling JavaScript.

## Browser Contexts, Pages, and Emulation

- So far, each test had one page in one browser context. Real apps need more.
- A chat needs two users at once. A link opens a new tab. A shop must work on a phone and in other countries.
- In this lesson, you will create contexts and pages yourself, and make the browser pretend to be other devices and places.

**Simple lesson:** contexts give you separate users, pages give you tabs, emulation gives you other devices and places.

### A Real-World Example

- The Practice Shop has a support chat. Asha writes, and Ben, the support agent, must see her message.
- One browser context per person, like two different computers. Both open the chat.

- Open the **Browser** tab after the run. Each page has its own tab at the top.

### What You Will Learn

1. The browser, context and page hierarchy.
2. How contexts keep tests apart, and how to make more contexts.
3. Working with several pages, popups and new windows.
4. Viewports and device emulation.
5. Locale, time zone, geolocation and permissions.
6. Colour scheme, reduced motion, user agent and JavaScript.

## Browser Contexts

### Browser, Context and Page Hierarchy

- **Browser**: one running browser program.
- **Context**: a private, separate session inside it, like an incognito window.
- **Page**: one tab inside a context.

### How Playwright Isolates Tests with Contexts

- Each test gets a **new context**. Cookies, local storage and logins from one test never reach another.
- New contexts are cheap. Playwright makes thousands of them while starting the browser only once per worker.

<figure className="plw-fig">
<svg viewBox="0 0 640 200" role="img" aria-labelledby="fig-plw-isolate" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-plw-isolate">Animation: a cookie and a cart item are saved in context A. Context B stays empty, because each context has its own storage</title>
  <rect x="15" y="15" width="610" height="170" rx="14" style={{ fill: "var(--plw-blue-soft)", stroke: "var(--plw-blue)" }} />
  <text x="30" y="38" style={{ fill: "var(--plw-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Browser (Chromium)</text>
  <rect x="40" y="55" width="260" height="115" rx="10" style={{ fill: "var(--plw-purple-soft)", stroke: "var(--plw-purple)" }} />
  <rect x="340" y="55" width="260" height="115" rx="10" style={{ fill: "var(--plw-purple-soft)", stroke: "var(--plw-purple)" }} />
  <g style={{ fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700, fill: "var(--plw-purple)" }}><text x="55" y="75">Context A (Asha)</text><text x="355" y="75">Context B (Ben)</text></g>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11 }}>
    <text x="55" y="100" style={{ fill: "var(--ink2)" }}>cookies:</text><text x="355" y="100" style={{ fill: "var(--ink2)" }}>cookies: (empty)</text>
    <text x="55" y="125" style={{ fill: "var(--ink2)" }}>localStorage:</text><text x="355" y="125" style={{ fill: "var(--ink2)" }}>localStorage: (empty)</text>
    <text x="120" y="100" opacity="0" style={{ fill: "var(--plw-orange)", fontWeight: 700 }}>sid=asha<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.2;0.25;0.95;1" dur="7s" repeatCount="indefinite" /></text>
    <text x="155" y="125" opacity="0" style={{ fill: "var(--plw-orange)", fontWeight: 700 }}>cart=[Yoga Mat]<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.4;0.45;0.95;1" dur="7s" repeatCount="indefinite" /></text>
  </g>
  <line x1="320" y1="60" x2="320" y2="170" style={{ stroke: "var(--plw-red)", strokeWidth: 3, strokeDasharray: "6 5" }} />
  <text x="470" y="155" textAnchor="middle" opacity="0" style={{ fill: "var(--plw-green)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 700 }}>✓ nothing leaks across<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.6;0.65;0.95;1" dur="7s" repeatCount="indefinite" /></text>
</svg>
<figcaption>Two contexts in one browser share nothing: not cookies, not storage, not logins.</figcaption>
</figure>

### Creating Contexts with browser.newContext()

- Ask for the `browser` fixture, then call `browser.newContext()`.
- Pass options to it: `browser.newContext({ locale: 'fr-FR' })`.
- In Playwright Test, new contexts also get the `use` options from your config, such as `baseURL`.
- Close contexts you create yourself with `context.close()`.

### Multiple Contexts for Multi-User Tests

- Use one context per user: a buyer and a seller, an admin and a customer, two chat users.

**Simple lesson:** one context per user. Contexts never share cookies or storage.

## Pages and Tabs

### Creating Pages with context.newPage()

- `context.newPage()` opens a new tab in a context. Tabs in the **same** context share cookies and storage.

### Working with Multiple Pages

- `context.pages()` lists all open tabs. Use each page object to act in that tab.

### Popups and New Windows

- A link with `target="_blank"` opens a **popup**. Wait for it with `page.waitForEvent('popup')`.
- Start waiting **before** the click, then await the promise after it:

```ts
const popupPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Size guide' }).click();
const popup = await popupPromise;
```

- `context.waitForEvent('page')` catches any new tab in the context, not only popups from one page.

### bringToFront() and Closing Pages

- `page.bringToFront()` makes a tab the active one, like clicking it.
- `page.close()` closes a tab. Closing the context closes all its tabs.

Start `waitForEvent()` **before** the click. If you start it after, the popup may already be open, and the wait times out.

**Simple lesson:** wait for a popup with `waitForEvent('popup')`, started before the click.

## Viewport and Devices

### Setting the Viewport

- The **viewport** is the visible area of the page, in pixels.
- Set it in the config or `test.use({ viewport: { width: 390, height: 700 } })`, or change it during a test with `page.setViewportSize()`.

### Device Descriptors and Device Projects

- `devices['iPhone 13']` is a **device descriptor**: viewport, user agent, scale and touch, all set like a real iPhone 13.
- Since Playwright 1.64, a descriptor also sets the `screen` size, so `window.screen` and media queries match the device. Set `screen: undefined` to turn that off.
- Spread it into a project to run every test on that device:

```ts
projects: [
  { name: 'Mobile Chrome', use: { ...devices['Pixel 7'] } },
  { name: 'Mobile Safari', use: { ...devices['iPhone 13'] } },
],
```

### Mobile Emulation

| Option | Effect |
|---|---|
| `isMobile` | Uses the mobile viewport rules (Chromium) |
| `hasTouch` | Turns on touch events. Needed for `tap()` |
| `deviceScaleFactor` | Pixels per CSS pixel (2 or 3 on phones) |

- Emulation changes size, user agent and touch. It is not a real phone. Check important flows on real devices too.

**Simple lesson:** `devices[...]` gives a ready-made phone or tablet setup. `setViewportSize()` resizes on the fly.

## Locale and Region

### Locale and Timezone

- `locale: 'de-DE'` makes the browser report German as its language and format numbers and dates the German way.
- `timezoneId: 'Asia/Kolkata'` sets the time zone. Dates shown on the page change with it.

### Geolocation and Permissions

- `geolocation: { latitude: 19.08, longitude: 72.88 }` sets where the browser thinks it is.
- The page must also be **allowed** to read it: `permissions: ['geolocation']`, or `context.grantPermissions(['geolocation'])`.
- `context.setGeolocation()` moves the location during a test.

**Simple lesson:** `locale`, `timezoneId` and `geolocation` move the browser to another place. Permissions decide what the page may use.

## Display Preferences

### Color Scheme and Reduced Motion

- `colorScheme: 'dark'` makes the page think the user prefers dark mode.
- `reducedMotion: 'reduce'` makes it think the user wants fewer animations.
- `page.emulateMedia({ colorScheme: 'dark' })` changes them during a test.

### User Agent and Disabling JavaScript

- `userAgent` changes the browser's identity string, which some sites read to choose a layout.
- `javaScriptEnabled: false` loads the page with no JavaScript, to test what users with scripts off see.

In the lab, emulation is done by the practice pages' own browser features in your browser. In a real project, Playwright sets these options inside Chromium, Firefox and WebKit directly.

Run your main tests in a dark-mode project too. Many visual bugs only show up in dark mode.

**Simple lesson:** emulate dark mode, reduced motion, user agents and no-JavaScript to test what different users see.

## Try It in Your Project

- These steps take the lesson from the lab to your own app.
- Write one test with two contexts for a flow that involves two users, like chat or approvals.
- Add a mobile project with `devices['Pixel 7']` and see which tests break.
- Test one page in another locale and time zone if your app is used abroad.

**Simple lesson:** use one idea from this lesson on your own app this week.

## TL;DR

- Browser holds contexts. Contexts hold pages. Each test gets its own context.
- `browser.newContext()` makes another isolated user. Close it when done.
- `page.waitForEvent('popup')`, started before the click, catches new tabs.
- `devices[...]`, `viewport` and `setViewportSize()` emulate screens and phones.
- `locale`, `timezoneId`, `geolocation` and `permissions` emulate places.
- `colorScheme`, `reducedMotion`, `userAgent` and `javaScriptEnabled` emulate preferences.

## Common mistakes

| Mistake | What happens | Fix |
|---|---|---|
| Two users in two pages of one context | They share cookies, so both are the same user | Use one context per user |
| Waiting for the popup after the click | The wait can miss the popup and time out | Start `waitForEvent()` before the click |
| Setting `geolocation` without permission | The page gets "permission denied" | Add `permissions: ['geolocation']` |
| Forgetting `context.close()` on extra contexts | Pages keep running until the test ends | Close every context you create |
| Trusting emulation alone for mobile | Some real-device bugs are missed | Also check key flows on real devices |

## Check Yourself

## Exercises

## What to Remember

- One context per user. Contexts share nothing.
- Wait for popups before the click that opens them.
- Devices, viewports, locales, time zones and locations are just options.
- Permissions decide what the page may use.
- Emulation is fast and good, but not the same as a real device.

Next: **Handling Complex Page Elements**
