Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Use an async test and a for...of loop: await browser.url(url), then wait for and check the page before moving to the next URL. This keeps navigation and page work in order in one WebdriverIO session. For independent URLs that do not need to share a session or preserve order, use separate specs or capabilities to run work in parallel.
Loop through URLs in order with for...of
WebdriverIO commands are asynchronous, so await navigation and the browser work that follows it. In a for...of loop, each iteration pauses at await; the next URL is not opened until the current iteration’s awaited work completes.
const urls = [
'https://example.com/',
'https://example.com/products',
'https://example.com/contact'
]
describe('multiple URLs', () => {
it('visits every URL in order', async () => {
for (const url of urls) {
await browser.url(url)
await expect(browser).toHaveUrl(url)
console.log(await browser.getTitle())
}
})
})
This is a test-runner example: the describe and it blocks belong in a WebdriverIO spec, with a runner configuration that discovers the spec and provides the browser session. The assertion checks the URL after navigation; the title is logged only after it has been retrieved.
When the loop should stop on failure
In the example, an assertion or navigation error fails the test and interrupts the loop. This is appropriate when later checks are meaningless after an earlier page fails, or when you want the spec to fail immediately. Avoid catching an error and ignoring it: doing so can make a broken page look like a successful run.
#1 Best Overall
Choose URLs and use baseUrl
If all pages share a host, configure baseUrl in wdio.conf.js and keep the URL list focused on paths:
export const config = {
baseUrl: 'https://example.com',
// specs, capabilities, and framework options...
}
const paths = ['/', '/products', '/contact']
for (const path of paths) {
await browser.url(path)
console.log(path, await browser.getTitle())
}
A path starting with / resolves from the root of the configured base URL. A value without a scheme or leading slash is appended to the base URL, while a fully qualified URL remains absolute. That difference matters for paths such as products versus /products: the first can be appended after any path in the base URL, while the second starts at the host root. Use absolute URLs when the list spans different hosts.
Can you use forEach with await browser.url()?
Not when the enclosing test needs to wait for the visits to finish in sequence. An async callback passed to forEach returns promises, but forEach does not await those promises. The loop can finish before navigation or assertions have completed, and the callbacks do not provide the ordered, one-at-a-time flow most URL checks need.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware match// Avoid when completion or order matters:
urls.forEach(async (url) => {
await browser.url(url)
console.log(await browser.getTitle())
})
Use for...of for readable sequential navigation. A regular indexed for loop also works when you need the index or want to control increments explicitly:
for (let i = 0; i < urls.length; i++) {
const url = urls[i]
await browser.url(url)
console.log(i, url, await browser.getTitle())
}
An explicitly chained sequence of promises is another option, but it is usually harder to scan and maintain than an async loop. Avoid Promise.all(urls.map(...)) against one shared browser session: concurrent navigations in the same session can interfere with one another, and the result is not an ordered series of page checks.
Wait for the page state you actually need
Navigation completing does not mean every application-specific condition is ready. After browser.url, assert a stable, relevant signal: the expected URL, a title, or a selector that indicates the page’s content has loaded. There is no single wait condition that fits every application; choose one that represents the behavior under test.
for (const url of urls) {
await browser.url(url)
await expect(browser).toHaveTitle(expect.stringContaining('Example'))
console.log(url, await browser.getTitle())
}
For pages with different expected results, keep the expectation next to each URL instead of applying one generic title to every page:
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
const pages = [
{ url: 'https://example.com/', title: 'Home' },
{ url: 'https://example.com/products', title: 'Products' },
{ url: 'https://example.com/contact', title: 'Contact' }
]
for (const page of pages) {
await browser.url(page.url)
await expect(browser).toHaveTitle(expect.stringContaining(page.title))
}
Use expectations that tolerate intentional title details such as a brand suffix, while still detecting the wrong page. If content is rendered asynchronously, add a wait for a page-specific element before extracting data or taking action. A URL check alone verifies the address, not that the expected application content rendered.
Record a result for each URL and decide how errors behave
For a report-style check, catch errors around each URL’s work, record the failure with its URL, and continue deliberately. This is useful when the goal is to inspect every page in a list even if some fail. At the end, make the test fail if any result was unsuccessful; otherwise the catch would turn real failures into a passing test.
const results = []
for (const url of urls) {
try {
await browser.url(url)
await expect(browser).toHaveTitle(expect.stringContaining('Example'))
results.push({ url, ok: true, title: await browser.getTitle() })
} catch (error) {
results.push({ url, ok: false, error: String(error) })
}
}
console.log(results)
expect(results.filter((result) => !result.ok)).toHaveLength(0)
This policy has a trade-off: continuing produces a fuller list of outcomes, but the test only reports its aggregate assertion at the end. If immediate failure is more useful, do not catch per-URL errors. For richer reporting, send each result to the reporting mechanism used by your project, retaining the URL and error rather than logging only a generic failure.
Run a URL loop outside the WebdriverIO test runner
A standalone Node.js script must create and close its own browser session. Install and configure WebdriverIO in the project first, then run this as an ES module in that project:
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →import { remote } from 'webdriverio'
const urls = [
'https://example.com/',
'https://example.com/products',
'https://example.com/contact'
]
const browser = await remote({
capabilities: { browserName: 'chrome' }
})
try {
for (const url of urls) {
await browser.url(url)
console.log(url, await browser.getTitle())
}
} finally {
await browser.deleteSession()
}
The session is created before the loop and reused for each navigation. The finally block closes it whether the loop finishes or an awaited command throws. Omitting session cleanup can leave browser resources running, especially when a script fails partway through. This example logs titles; add explicit assertions or error collection if the script is intended to validate pages rather than inspect them.
When to run multiple URLs in parallel
A single sequential loop is the simplest choice when order matters or each URL should use the same session. For independent page checks where total runtime matters more than order, distribute work across separate specs or capabilities. WebdriverIO’s configuration supports a glob or array of spec paths, and its runner can execute specs in workers. Separate work also means separate sessions, so the available browser capacity, concurrency limits, and reporting behavior of your local or cloud environment constrain how much parallelism is practical.
- Keep URLs in one loop when they share state, depend on prior navigation, or must be checked in a known order.
- Split unrelated checks into specs when they can succeed or fail independently.
- Increase worker or capability concurrency only to what the browser environment can support; excessive concurrency can strain local resources or exceed provider limits.
- Use reporting that identifies the spec and URL so parallel failures remain diagnosable.
Or skip the browser setup
If the task is to capture images or PDFs of URLs rather than interact with them as an end-to-end test, ScreenshotNeo offers a screenshot API and MCP server. For a one-off capture, make one GET request:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/ -o shot.webp
See the ScreenshotNeo API documentation for request options. It can return PNG, JPEG, WebP, or PDF. Cookie banners, newsletter popups, and chat widgets can be removed before capture; those cleanup steps can each be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and whether the request was billed. An MCP server exposes screenshot and PDF tools to AI agents. The free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. This is for rendering captures, not a replacement for WebdriverIO interaction, browser assertions, or session-based test flows. Sign up for free to get 1,000 screenshots a month with no card.
Troubleshooting common loop problems
The test ends before every URL is visited
Check whether the navigation is inside forEach(async ...) or another callback whose returned promise is not awaited. Replace it with for...of and await each browser operation inside the test callback.
The wrong page appears in the results
Confirm that each URL is being awaited before the next navigation, and that the loop uses absolute URLs or the intended baseUrl paths. Add an assertion for the resulting URL or a page-specific title or selector. A completed navigation command alone is not proof that the correct application content is displayed.
Relative paths resolve unexpectedly
Inspect the exact string passed to browser.url. A leading slash resolves from the root of baseUrl; a relative value without that slash is appended directly. Use /products for the host-root path or provide an absolute URL if the desired location is on another host.
One failed page prevents checks on later URLs
That is the default effect of an uncaught error in a sequential loop. If the purpose is to collect all outcomes, catch the error per URL, attach it to a result record, continue, and fail the overall test after examining the result list. If immediate failure is intentional, keep the current behavior.
Free tools Windows power users keep installed
One-click scans. No signup required.
The standalone script leaves a browser running
Put await browser.deleteSession() in a finally block around the loop. That ensures cleanup is attempted after both a successful run and an exception.
Performance and reliability considerations
Sequential navigation is predictable but takes at least the cumulative time of each navigation and its awaited checks. Keep the work inside the loop limited to what is needed for the test, and use a meaningful readiness assertion rather than arbitrary delays where possible. If the list is large, divide independent pages into separate specs and scale workers cautiously; parallel execution can reduce elapsed time but consumes more browser sessions and makes results order-independent.
Best Value
For reliable diagnostics, retain the URL alongside each assertion result, title, or error. Consider whether redirects are expected before asserting the URL, and whether the page can legitimately vary by authentication, locale, or application state. Those conditions should be set up explicitly in the test rather than treated as random failures.
Frequently Asked Questions
Can one WebdriverIO browser session visit several URLs?
Yes. Create or use one session and await each navigation in a sequential loop; a standalone script should close the session when finished.
Does the URL loop follow links on each page?
No. It navigates directly to the URL strings in the array. To test link behavior, locate and click links as a separate interaction.
Can I use this approach for a very large URL inventory?
You can, but decide how much work belongs in one spec and whether independent pages should be split across specs or capabilities for manageable runtime and reporting.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

