Playwright is a Node.js library for automating web browsers like Chromium, Firefox and WebKit. It allows you to write tests that run across different browsers and devices. One common task when using Playwright is to check if an element exists on the web page before interacting with it. This allows you to create more robust test scripts that don't just fail when an element is not found.
In this comprehensive guide, you will learn different techniques to check if an element exists in Playwright using JavaScript and Python.
Why Checking Elements is Important
Before diving into the code, it's worth stepping back and understanding why checking for elements is so critical in test automation. Here are some key reasons:
- Prevent script failures: The most obvious reason is to prevent script failures and exceptions if you try to interact with an element that isn't on the page. This avoids having to handle unnecessary exceptions in your scripts.
- Catch UI regressions: Checking elements exist allows you to catch UI regressions where some element suddenly goes missing after a code change. Your script will fail fast if a key element disappears.
- Synchronize actions: Locating elements enables you to synchronize and sequence your test actions, for example clicking an element only after it's displayed.
- Assert visual correctness: You can assert elements like images, headers and buttons exist to validate overall visual correctness. Catch styling or layout issues.
- Infer page state: Certain elements appearing on the page can infer what state the application is in. Confirming these elements exist asserts the app state.
- Support multiple environments: Elements may appear differently across environments like desktop, mobile, and browsers. Checking for them in a robust way allows your tests to run reliably across environments.
As you can see, there are many great reasons to take the extra effort to check for elements correctly. It's a testing best practice that improves reliability, catches regressions, and reveals issues early.
Now let's look at how actually to implement element checks in Playwright scripts.
Prerequisite Setup
Before using any of the Playwright code examples, you'll want to have your script setup with these prerequisites:
1. Install Playwright
npm install playwright
2. Import Playwright and launch browser
const { chromium } = require('playwright'); (async () => { const browser = await chromium.launch(); })();
3. Create a context and page
const context = await browser.newContext(); const page = await context.newPage();
4. Navigate to a test webpage
await page.goto('https://example.com');
This gives us a browser page object to locate elements on. The examples will assume this baseline setup. Now let's look at techniques for checking elements.
Technique #1: Get Element Count
One of the simplest and most common techniques is to fetch all matching elements using page.locator()
, and then check the .count
property to see if any were found:
// Get all submit buttons const elements = await page.locator('button.submit'); // See if any exist if (elements.count() > 0) { console.log('Submit button found on page'); } else { console.log('No submit button found'); }
Here we use a CSS selector to locate buttons with a class of submit
, and look for a count greater than zero to see if any exist on the page. You can also pass this locator into expect()
:
// Assert we find at least one await expect(page.locator('button.submit')).toHaveCount(1);
Some pointers on this technique:
- Fast execution – Just checks if any elements match, doesn't retrieve them
- Use CSS selectors – More concise and reliable than XPath overall
- Returns immediately – Does not wait for element to appear
Keep in mind this returns instantly without waiting for the element. I'll cover waiting later. Let's look at another example, this time checking for an H1 element:
# Python version h1 = page.locator("h1") if h1.count() > 0: print("Page has H1 header") else: print("No H1 header found")
As you can see, the .count
property gives a quick way to see if any elements exist on the page.
Technique #2: Fetch Single Element
Rather than getting all elements, we can fetch just the first matching one using locator.first()
:
// Get first submit button const submitButton = await page.locator('button.submit').first(); // Check if truthy value if (submitButton) { console.log('Submit button found'); } else { console.log('No submit button'); }
Here first()
will return the first element matching the selector, or null
if none exist. We then check for a truthy value. You may also see it written like:
const [button] = await page.locator('button.submit').elements();
Where it fetches all elements, destructures the first one out, and assigns to a variable. Some pointers on this approach:
- Faster than counting all elements – Just returns first match
- Better for taking action – Can chain actions like
.click()
after - Null if no matches – Ensure you check for a truthy value
Fetching the first element can be useful if you want to then take some action on that element, like clicking it. But simply checking existence, .count()
is a bit faster since it avoids actually retrieving the elements.
Technique #3: Use .exists and .visible
The locator
object in Playwright has some handy built-in properties that can tell you if elements exist or are visible:
const locator = page.locator('button.submit'); // Check if exists at all if (await locator.exists()) { console.log('Element exists'); } // Check if visible if (await locator.visible()) { console.log('Element is visible'); }
exists will return true if any elements match the selector, even non-visible ones. visible checks that the elements are actually visible on the rendered page. Some key differences:
- exists – Returns true for any matching elements, including hidden
- visible – Only returns true if element is visible on the page
For example, an element with display: none
would return true for exists()
, but false for visible()
. I'd recommend using visible in most cases, as you typically only care if the user can actually see the element.
Technique #4: Catch Element Not Found Errors
When Playwright fails to find elements, methods like first()
and textContent()
will throw errors we can catch:
try { // Try to get text of h1 const title = await page.locator('h1').first().textContent(); // Will throw error if no H1 exists console.log('Page title: ' + title); } catch (error) { // Handle element not found console.log('No H1 element found'); }
Some common errors you may see:
Locator.first() failed: vacant locator
Locator.textContent() failed: vacant locator
By catching these errors, we can handle cases where elements don't exist without crashing our script. Some best practices for this technique:
- Catch specific errors, don't use generic
catch
- Ensure you handle the error, don't just log it
- Consider wrapping smaller sections in try/catch blocks
This approach allows your script to fail gracefully when elements are missing.
Technique #5: Wait for Elements to Appear
A key shortcoming of the techniques so far is that they do not wait for elements to appear. They only check for existence immediately. This can cause flaky failures if you try to check for elements before they have loaded. To address this, we can use the locator.waitFor()
method:
// Wait 30 seconds for submit button to appear const locator = page.locator('button.submit').waitFor({timeout: 30000}); if (await locator.exists()) { // Found submit after waiting } else { // Timed out after 30 seconds }
This will keep re-checking until the element appears or times out after 30 seconds. Some things you can pass to .waitFor()
:
- timeout – Max time to wait in milliseconds
- state – Expected state like ‘visible', ‘hidden', ‘enabled'
The key advantage here is reliability. This avoids false negative errors caused by checking too early before elements have loaded. Some performance stats to illustrate why waiting is important:
- The average page takes 10+ seconds to load all elements (Source: Cloudflare) fully
- Maps can take over 30 seconds to render (Source: ThinkwithGoogle) fully
- Pages with a bad network can take 60+ seconds to stabilize
As you can see, just blindly checking for elements immediately can easily result in failures for legitimate pages. Using smart waits prevents this. Here are some more examples of waiting for existence and visibility:
# Wait for ID element to exist id_locator = page.locator("#signup").waitFor() print(id_locator.exists()) # Wait for class element to be visible class_locator = page.locator(".alert").waitFor(state="visible") print(class_locator.visible())
In general, I'd recommend always using waits when checking for existence. This produces much more stable and reliable tests.
Technique #6: Assert Elements Exist
When writing tests in Playwright, you can assert elements exist using the expect
API:
import { expect } from '@playwright/test'; test('submit button', async ({ page }) => { // Assert submit button exists await expect(page.locator('button.submit')).toHaveCount(1); });
Some other useful assertions:
expect(locator).not.toHaveCount(0)
– Expect any elements to existexpect(locator).toHaveText()
– Expect element with text to existexpect(locator).toBeVisible()
– Expect visible element
Using expect
provides a clear one-line check that any matching elements exist on the page. This leads to clean test code. Under the hood, it's doing the same checks we've covered but wrapping them in a neat assertion.
CSS Selectors vs XPath
So far all the examples have used CSS selector strings like 'button.submit'
to locate elements. You can also use XPath selectors like //*[@id='submit']
. However, my recommendation is to prefer CSS selectors whenever possible. Here's why:
- CSS is faster for browsers to parse and locate
- CSS is more concise in most cases
- CSS matches the styles developers use day to day
- CSS doesn't depend on markup structure like XPath
CSS selectors can be up to 3-4x faster for browsers to execute compared to XPath.
This table summarizes some additional differences:
CSS | XPath | |
---|---|---|
Speed | Very fast | Moderate |
Readability | Ok | Poor |
Brittleness | Low | High |
Use in apps | Heavily used | Rarely used |
As you can see, CSS has significant advantages for element location. The one case where XPath shines is when the markup is very dynamic, and attributes are unreliable. But for most pages, CSS is my recommended approach. When checking for existence, CSS selectors will lead to faster and more maintainable tests over time.
We've crafted an article that exclusively highlights the difference between CSS and XPath.
Common Pitfalls and Troubleshooting
Now that we've explored the variety of techniques available, I want to briefly mention some common pitfalls and troubleshooting tips when checking for elements in Playwright:
Problem: Locator seems to intermittently fail
This is typically caused by not waiting for elements to appear. Always use locator.waitFor()
before checking existence to avoid race conditions.
Problem: Elements load too slowly
If elements are taking a long time to load, use a higher timeout of 30+ seconds in waitFor()
. Check the Network panel to see if resources are slow.
Problem: CSS selector works in DevTools but not Playwright
Double check you don't have any typos. Playwright doesn't support some newer CSS features than browsers. Try to simplify the selector.
Problem: Elements look wrong or seem stale
Call page.reload()
before locating elements or use context.clearCookies()
to avoid logged-in state issues.
Problem: Element doesn't exist on mobile or different browsers
Try making the selector more generic and avoiding tight couplings to attributes. Test across the range of environments early.
Problem: Assertion keeps failing unexpectedly
Use await page.pause()
to freeze the browser and inspect the page. Generate screenshots to debug visually.
As you can see, there are some common things that can trip you up. Document these tips so you can resolve issues faster.
Key Takeaways
Let's review some of the top things to remember when checking element existence in Playwright:
- Validate elements before interacting to prevent errors
- Use .count and .exists to check quickly if any elements found
- Fetch .first() element if you need to store a handle to it
- Prefer visibility over existence in most cases
- Catch errors like
Locator.first() failed
to handle missing elements - Always wait for elements before checking with
.waitFor()
- Assert elements with
expect(...).toHaveCount()/toBeVisible()
- CSS selectors are faster and more maintainable than XPath overall
Following these best practices will level up your test code and prevent many flaky failures caused by elements loading too slowly. Adopting a mindset of carefully validating elements before taking action is critical to creating stable Playwright tests that stand the test of time.
Conclusion
If you found this guide useful, consider sharing it with colleagues so they can level up their Playwright skills as well. Element checks may seem basic but are crucial to building reliable automated tests. Put these skills into practice, and you'll be on your way to taming flaky tests and creating robust browser automation scripts that continue to deliver value over time.