Your First Playwright Python Script with sync_playwright (Browser, Context and Page)

Before pytest takes over the browser for you, it is worth writing one complete Playwright script by hand. You will see exactly what happens when a test starts: Playwright launches a browser, creates an isolated context, opens a page, performs actions and closes everything down. Once you understand these four objects, every fixture and error message later in the course will make sense.

📖 What You’ll Learn: the Playwright object hierarchy (Playwright, Browser, BrowserContext, Page), launch options like headless and slow_mo, interacting with a real form, reading text and attributes, taking screenshots, proving that contexts are isolated, and closing resources safely with with and try/finally.


🧱 The Four Objects You Always Use

Every Playwright program, including every pytest test you will write, uses the same chain of objects:

ObjectCreated byWhat it represents
Playwrightsync_playwright()The running Playwright driver. Gives you chromium, firefox, webkit and devices.
Browserp.chromium.launch()One browser process (expensive to start, so reuse it).
BrowserContextbrowser.new_context()An isolated, incognito-like session with its own cookies, storage and cache. Cheap to create.
Pagecontext.new_page()One tab inside a context. Almost all actions happen here.

Think of the browser as a building, each context as a separate flat with its own key, and each page as a room in a flat. Two contexts never share cookies or logins, which is why Playwright tests do not leak state into each other.


✍️ A Complete First Script

This script opens the SQA Tools Automation Practice Page, fills in some fields, reads values back and saves a screenshot. Save it as first_script.py:

📄 first_script.py

from playwright.sync_api import sync_playwright


with sync_playwright() as p:
    # 1. Launch a browser and create an isolated context with one page
    browser = p.chromium.launch()
    context = browser.new_context()
    page = context.new_page()

    # 2. Open the practice page
    page.goto("https://sqatools.in/automation-practice-page/")
    print("Page title:", page.title())

    # 3. Type into text fields, located by their placeholder text
    page.get_by_placeholder("Enter username").fill("qa_learner")
    page.get_by_placeholder("Enter password").fill("Secret@123")

    # 4. Choose a radio button, tick a checkbox and pick a dropdown option
    page.get_by_role("radio", name="Female").check()
    page.get_by_role("checkbox", name="Python").check()
    page.locator("select").first.select_option("India")

    # 5. Read the values back from the page
    print("Username field:", page.get_by_placeholder("Enter username").input_value())
    print("Python checked:", page.get_by_role("checkbox", name="Python").is_checked())
    print("Second table row:", page.get_by_role("row").nth(2).inner_text())

    # 6. Save a screenshot of the visible part of the page
    page.screenshot(path="practice-page.png")

    # 7. Close everything
    context.close()
    browser.close()

Run it:

⌨️ Terminal

python first_script.py

🖥️ Output

Page title: Automation Practice Page - SQA Tools
Username field: qa_learner
Python checked: True
Second table row: 2	Rahul	Tester

A file named practice-page.png appears next to your script.

What each part does

  1. Launch and open. launch() starts the browser, new_context() creates a clean session, and new_page() opens a tab in it.
  2. **page.goto(url)** navigates and waits for the page’s load event before the next line runs.
  3. **get_by_placeholder("Enter username") creates a locator**: a description of how to find an element. Nothing is searched yet. fill() then finds the input, waits until it is editable, clears it and types the text.
  4. **get_by_role("radio", name="Female")** finds the element the way a screen reader would: by its role (radio) and accessible name (Female). check() ticks radios and checkboxes. select_option("India") picks the option whose value or label is India from the first <select> on the page.
  5. Reading values. input_value() returns what is typed in a field, is_checked() returns True or False, and inner_text() returns the visible text. nth(2) picks the third match (counting starts at 0, and row 0 is the header row). The cells in the printed row are separated by tab characters.
  6. **page.screenshot(path=...)** saves a PNG. Add full_page=True to capture the whole scrolling page.
  7. Closing. Closing the context closes its pages; closing the browser ends the process. Leaving the with block then stops the Playwright driver.

💡 Tip: Notice there is no time.sleep() anywhere. Every action waits automatically until its element is ready. This auto-waiting is the main reason Playwright tests are stable.


👀 Watch the Browser: headless and slow_mo

By default the browser is headless: it runs without a window, which is faster and works on servers. While learning, it helps to watch. Save this as launch_options.py:

📄 launch_options.py

from playwright.sync_api import sync_playwright


with sync_playwright() as p:
    browser = p.chromium.launch(
        headless=False,   # show the browser window
        slow_mo=500,      # wait 500 ms after each Playwright action
    )
    page = browser.new_page()

    page.goto("https://sqatools.in/automation-practice-page/")
    page.get_by_role("button", name="Hover Over Me").hover()
    print("Hovered on Chromium version", browser.version)

    browser.close()
  • headless=False opens a real window so you can follow along.
  • slow_mo=500 adds a 500 millisecond pause after every action. Use it only for watching or demos, never in real test suites.
  • browser.new_page() is a shortcut that creates a context and a page in one call. It is handy in quick scripts; when you need several pages in the same session, create the context yourself.
  • browser.version returns the browser’s version string.

🖥️ Output

Hovered on Chromium version 156.0.8078.4

To try another engine, change one word: p.firefox.launch(...) or p.webkit.launch(...). The rest of the script stays the same.


🔒 Contexts Are Isolated

This is one of Playwright’s most useful ideas. Two contexts in the same browser behave like two different people on two different computers. Save this as contexts.py:

📄 contexts.py

from playwright.sync_api import sync_playwright


with sync_playwright() as p:
    browser = p.chromium.launch()

    # Two independent sessions in ONE browser process
    alice = browser.new_context()
    bob = browser.new_context()

    alice_page = alice.new_page()
    bob_page = bob.new_page()

    alice_page.goto("https://sqatools.in/automation-practice-page/")
    bob_page.goto("https://sqatools.in/automation-practice-page/")

    # Alice stores a value in her browser's localStorage
    alice_page.evaluate("localStorage.setItem('user', 'alice')")

    # Bob cannot see it: each context has its own storage
    print("Alice sees:", alice_page.evaluate("localStorage.getItem('user')"))
    print("Bob sees:  ", bob_page.evaluate("localStorage.getItem('user')"))

    print("Contexts open:", len(browser.contexts))
    browser.close()

🖥️ Output

Alice sees: alice
Bob sees:   None
Contexts open: 2
  • page.evaluate("...") runs JavaScript inside the page and returns the result to Python. JavaScript null arrives in Python as None.
  • Bob’s context has no idea what Alice stored, even though both pages are in the same browser.

This is exactly what pytest-playwright does for you: one browser for the whole test session, and a brand-new context and page for every test. Tests start fast, and no cookies or logins leak from one test into the next.


🧹 Closing Safely When Something Fails

If a line throws an error, the code after it never runs. The with sync_playwright() block already guarantees that the driver stops. To make sure the browser itself is closed too, wrap your steps in try/finally. Save this as try_finally.py:

📄 try_finally.py

from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError


with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    try:
        page.goto("https://sqatools.in/automation-practice-page/")

        # This button does not exist, so the click times out after 2 seconds
        page.get_by_role("button", name="Does Not Exist").click(timeout=2000)

    except PlaywrightTimeoutError as error:
        print("Step failed:", error.message.splitlines()[0])

    finally:
        browser.close()     # always runs, even after an error
        print("Browser closed")

🖥️ Output

Step failed: Locator.click: Timeout 2000ms exceeded.
Browser closed
  • TimeoutError from playwright.sync_api is Playwright’s own timeout error. Importing it with an alias avoids confusing it with Python’s built-in TimeoutError.
  • timeout=2000 overrides the default action timeout of 30 seconds for this one click.
  • error.message holds the full message; the first line names the action and the timeout. The lines after it (the call log) show what Playwright was waiting for.
  • The finally block always runs, so the browser never stays open.

📝 Note: You will rarely write try/finally around tests yourself. pytest fixtures handle cleanup automatically, even when a test fails. You will see this in the next lesson.


❓ Frequently Asked Questions

🤔 What is the difference between browser.new_page() and context.new_page()?

browser.new_page() creates a new context and a page in it in one step, and closing that page also closes its context. context.new_page() adds a page to an existing context, so several pages can share the same cookies and login.

🤔 Why does my script finish but the browser window closes immediately?

The script reached browser.close(). To pause and look around, add page.pause() (it opens the Playwright Inspector) or run with slow_mo. You will learn debugging in Module 11.

🤔 Can I connect to a browser that is already open?

Yes, with p.chromium.connect_over_cdp("http://localhost:9222") for Chromium browsers started with remote debugging enabled. Most test suites launch their own browser instead, because it is more reliable.

🤔 What is the default timeout for actions?

30 seconds for actions such as click() and fill(), and 30 seconds for navigation. You can change it per call with timeout= or for a whole context with context.set_default_timeout().


🏁 Summary

  • sync_playwright() gives you a Playwright object; it launches a Browser, which creates BrowserContexts, which open Pages.
  • Locators such as get_by_placeholder() and get_by_role() describe elements; actions such as fill() and check() wait automatically.
  • headless=False and slow_mo help you watch a script; contexts keep sessions fully isolated.
  • Close what you open, or let with blocks and pytest fixtures do it for you.

Next, you will turn this knowledge into your first real pytest test, where the page fixture does all of this setup and cleanup for you.

🎬 Watch the Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on the SQA Tools Automation Practice Page

Install Playwright for Python and pytest (Virtual Environment and VS Code Setup)

Before you write a single test you need a clean, repeatable setup. In this lesson you will install Python, create a virtual environment for your test project, install Playwright and pytest with one command, download the browsers, and set up VS Code so it can discover and run your tests.

📖 What You’ll Learn: checking your Python version, creating and activating a virtual environment on Windows, macOS and Linux, installing pytest-playwright, downloading browsers with playwright install, freezing versions in requirements.txt, and running tests from VS Code.


🧰 What You Need

  • Python 3.10 or newer. Playwright 1.64 and pytest-playwright 0.10 both require it.
  • About 1 GB of free disk space for the three browsers.
  • A code editor. This course uses VS Code, which is free.
  • An internet connection for the first install.

You do not need Node.js, Java or a separate WebDriver download.


🐍 Step 1: Install and Check Python

Download Python from python.org/downloads. On Windows, tick “Add python.exe to PATH” on the first installer screen. Then open a new terminal and check the version:

⌨️ Terminal

python --version

On macOS and Linux the command is usually python3:

⌨️ Terminal

python3 --version

You should see Python 3.10 or higher. If you see an older version, or “command not found”, install a current version before continuing.

💡 Tip: In this course, commands use python. If your system only has python3, use that instead everywhere.


📁 Step 2: Create a Project Folder and a Virtual Environment

A virtual environment is a private folder of Python packages for one project. It keeps this project’s Playwright and pytest versions separate from other projects and from the system Python, so upgrading one project never breaks another. You will learn more in Python Virtual Environments and pip.

Create a folder and a virtual environment named .venv inside it:

⌨️ Terminal

mkdir playwright-python-demo
cd playwright-python-demo
python -m venv .venv
  • mkdir creates the project folder and cd moves into it.
  • python -m venv .venv runs Python’s built-in venv module and creates a .venv folder containing a private copy of Python and pip.

Activate the virtual environment

Activation points your terminal’s python and pip at the .venv folder. Use the command for your operating system:

Operating system / shellActivate command
Windows PowerShell.venv\Scripts\Activate.ps1
Windows Command Prompt.venv\Scripts\activate.bat
macOS / Linux (bash, zsh)source .venv/bin/activate

When it works, your prompt starts with (.venv). Run deactivate to leave it.

⚠️ Watch out: If PowerShell says that running scripts is disabled, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned once, then activate again. This changes the policy only for your user account.


📦 Step 3: Install pytest-playwright

With the virtual environment active, install the official plugin:

⌨️ Terminal

pip install pytest-playwright

This one command installs three things:

  1. pytest-playwright, the plugin that adds the page fixture and Playwright command-line options.
  2. playwright, the browser automation library (installed automatically as a dependency).
  3. pytest, the test runner (also a dependency).

Check the installed versions:

⌨️ Terminal

playwright --version
pytest --version

🖥️ Output

Version 1.64.1
pytest 9.2.0

Your numbers may be newer. That is fine: everything in this course works with Playwright 1.64 and later.


🌐 Step 4: Download the Browsers

The Python package does not contain the browsers themselves. Download the versions that match your Playwright release:

⌨️ Terminal

playwright install

This downloads Chromium, Firefox and WebKit into a shared cache folder (on Windows %USERPROFILE%\AppData\Local\ms-playwright, on macOS ~/Library/Caches/ms-playwright, on Linux ~/.cache/ms-playwright). Every project on your machine that uses the same Playwright version reuses them.

Useful variations:

  • playwright install chromium downloads only Chromium (faster for a first try).
  • playwright install --with-deps also installs the operating-system libraries the browsers need. Use it on Linux and in CI containers; it may ask for administrator (sudo) permission.
  • playwright install --dry-run shows what would be downloaded and where, without downloading.

📝 Note: Each Playwright version needs its own browser builds. After pip install --upgrade playwright, run playwright install again.


✅ Step 5: Verify the Setup with a Test

Create a folder named tests and a file tests/test_setup.py inside it:

📄 tests/test_setup.py

from playwright.sync_api import Page, expect


def test_setup_works(page: Page):
    # Open the SQA Tools practice page
    page.goto("https://sqatools.in/automation-practice-page/")

    # The test passes if the page title matches
    expect(page).to_have_title("Automation Practice Page - SQA Tools")

Run it from the project folder:

⌨️ Terminal

pytest

🖥️ Output

.                                                                        [100%]
1 passed, 1 warning in 1.98s

The single dot means one test passed. pytest found the test because the file name starts with test_ and the function name starts with test_. You will learn these discovery rules in Lesson 4.

📝 Note: With pytest 9.2 and pytest-playwright 0.10.0 you may also see a PytestRemovedIn10Warning about “Session-scoped fixtures defined as instance methods” for the base_url fixture. It comes from inside the plugin, not from your code, and it does not affect your results.


📌 Step 6: Pin Your Versions in requirements.txt

So that teammates and CI install exactly the same versions, save them to a file:

⌨️ Terminal

pip freeze > requirements.txt

pip freeze lists every installed package with its exact version. On a fresh Windows install in November 2026 the file contained these lines (your versions may be newer, and colorama only appears on Windows):

🖥️ Output

colorama==0.4.6
greenlet==3.5.6
iniconfig==2.3.1
packaging==26.3
playwright==1.64.1
pluggy==1.7.0
pyee==14.0.0
Pygments==2.21.0
pytest==9.2.0
pytest-playwright==0.10.0
python-slugify==9.1.3
text-unidecode==1.3
typing_extensions==4.16.0

Anyone can now recreate your environment with:

⌨️ Terminal

pip install -r requirements.txt
playwright install

💡 Tip: Add .venv/ to your .gitignore file. Commit requirements.txt, never the virtual environment folder itself. See our guide to .gitignore.


💻 Step 7: Set Up VS Code

  1. Open the project folder in VS Code (File > Open Folder).
  2. Install the Python extension by Microsoft from the Extensions view (Ctrl+Shift+X).
  3. Press Ctrl+Shift+P, run Python: Select Interpreter, and choose the interpreter inside .venv.
  4. Open the Testing view (the flask icon), click Configure Python Tests, choose pytest, then choose the tests folder.
  5. Your test appears in the tree. Click the ▶ button next to it to run it, or the debug icon to debug it with breakpoints.

📝 Note: The “Playwright Test for VS Code” extension is built for the Node.js test runner (Playwright Test). For Python projects, run and debug tests through the Python extension’s Testing view as shown above.


🗂️ Your Project So Far

🖥️ Output

playwright-python-demo/
├── .venv/                  # virtual environment (not committed)
├── tests/
│   └── test_setup.py       # your first test
└── requirements.txt        # pinned versions

❓ Frequently Asked Questions

🤔 pip says “command not found”. What should I do?

Use python -m pip install pytest-playwright. Running pip through python -m always uses the pip that belongs to the active Python.

🤔 Do I have to install all three browsers?

No. If you only test in Chromium, playwright install chromium is enough. You can add the others later.

🤔 Can I use Google Chrome or Microsoft Edge instead of Playwright’s Chromium?

Yes. Install the browser normally (or with playwright install chrome / playwright install msedge) and run tests with pytest --browser-channel chrome. You will learn this in Lesson 5.

🤔 Is pytest-playwright different from playwright?

Yes. playwright is the automation library. pytest-playwright is the plugin that connects it to pytest. Installing the plugin installs the library for you.


🏁 Summary

  • Use Python 3.10+, and give every test project its own virtual environment.
  • pip install pytest-playwright installs the plugin, Playwright and pytest together.
  • playwright install downloads the matching browsers; add --with-deps on Linux and CI.
  • Freeze versions with pip freeze > requirements.txt, and run tests from VS Code’s Testing view.

Next, you will write a complete Playwright script with sync_playwright to understand browsers, contexts and pages before pytest takes over.

🎬 Watch the Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on the SQA Tools Automation Practice Page

What Is Playwright for Python? Sync API, Async API and pytest Explained

Playwright is a free, open-source browser automation library from Microsoft. Its Python package lets you drive real Chromium, Firefox and WebKit browsers from Python code, and the pytest-playwright plugin turns it into a complete test framework. This first lesson explains what Playwright for Python is, how it works under the hood, and which of its two APIs you will use in this course.

📖 What You’ll Learn: what Playwright for Python is, the browsers it supports, how the Python package talks to the browser, the sync API vs the async API, why tests use pytest with the pytest-playwright plugin, and how the Python version differs from Playwright with TypeScript.


🎭 What Is Playwright?

Playwright is a library for automating web browsers. With a few lines of code you can open a page, click buttons, type into fields, read text and check that the page behaves the way users expect. Testers use it for end-to-end (E2E) UI testing, API testing, and browser tasks like taking screenshots or saving PDFs.

Microsoft maintains Playwright and publishes it for four languages: JavaScript/TypeScript, Python, Java and .NET. All four share the same browser engine and the same concepts (pages, locators, auto-waiting, assertions), so what you learn here carries over to the other languages.

The Python package is called playwright on PyPI. At the time of writing, the current version is 1.64.1 (released October 2026), and it needs Python 3.10 or newer.


🌐 Which Browsers Does It Support?

One Python API controls all three major browser engines:

  • Chromium, the engine behind Google Chrome and Microsoft Edge. You can also run the installed branded Chrome or Edge through a *channel*.
  • Firefox (Gecko engine).
  • WebKit, the engine behind Safari. This lets you test Safari behaviour even on Windows and Linux.

Playwright works on Windows, macOS and Linux, in headless mode (no visible window, the default) or headed mode (you watch the browser).


⚙️ How Playwright for Python Works

When you run a Playwright script, three pieces work together:

  1. Your Python code calls methods such as page.goto() and locator.click().
  2. The Playwright driver, which ships inside the Python package (it includes its own Node.js runtime, so you do not install Node yourself), receives those calls.
  3. The browser is controlled by the driver over a fast, persistent connection, not over separate HTTP requests per command like classic WebDriver.

Because of this design, Playwright can listen to browser events (network requests, dialogs, console messages, new tabs) as they happen and wait for elements intelligently. This is where its famous auto-waiting comes from: before clicking, Playwright checks that the element is attached, visible, stable, enabled and able to receive the click.

📝 Note: The browsers are separate downloads. After installing the Python package you run playwright install once to download the browser builds that match your Playwright version. You will do this in the next lesson.


🔀 Sync API vs Async API

The Python package offers two complete, equivalent APIs. They have the same methods; the difference is how you call them.

The sync API

The sync API (playwright.sync_api) reads like normal Python: each line runs and finishes before the next one starts. Save this as sync_title.py:

📄 sync_title.py

from playwright.sync_api import sync_playwright


with sync_playwright() as p:                     # 1. start Playwright
    browser = p.chromium.launch()                # 2. launch headless Chromium
    page = browser.new_page()                    # 3. open a new tab

    page.goto("https://sqatools.in/automation-practice-page/")   # 4. navigate
    print("Title:", page.title())                # 5. read the page title

    browser.close()                              # 6. close the browser

Line by line:

  1. sync_playwright() starts the Playwright driver. Using it in a with block makes sure it stops cleanly when the block ends, even if an error happens.
  2. p.chromium.launch() starts a Chromium browser. Swap chromium for firefox or webkit to change browser.
  3. browser.new_page() opens a fresh tab and returns a Page object, the object you use for almost everything.
  4. page.goto(url) loads the URL and waits for the page’s load event.
  5. page.title() returns the text of the <title> tag.
  6. browser.close() closes the browser and all its tabs.

Run it with python sync_title.py:

🖥️ Output

Title: Automation Practice Page - SQA Tools

The async API

The async API (playwright.async_api) is built on Python’s asyncio. Every Playwright call is awaited. Save this as async_title.py:

📄 async_title.py

import asyncio

from playwright.async_api import async_playwright


async def main():
    async with async_playwright() as p:          # start Playwright (async version)
        browser = await p.chromium.launch()
        page = await browser.new_page()

        await page.goto("https://sqatools.in/automation-practice-page/")
        print("Title:", await page.title())

        await browser.close()


asyncio.run(main())                              # run the async function

What changed compared to the sync version:

  • The code lives inside an async def function, and asyncio.run(main()) runs it.
  • with becomes async with, and every Playwright call gets await in front of it.
  • The result is the same: Title: Automation Practice Page - SQA Tools.

Which one should you learn?

Sync APIAsync API
Importplaywright.sync_apiplaywright.async_api
StylePlain Python, no awaitasync def + await everywhere
Best forTest automation with pytest, scriptsApps already built on asyncio, running many pages concurrently
Used in this courseModules 1 to 12Module 13

For UI testing, the sync API is the standard choice. The pytest-playwright plugin is built on it, the official docs show it first, and the code is easier to read. You will learn the async API later, once the core skills are solid.

⚠️ Watch out: Do not mix the two APIs in one script. Calling the sync API inside a running asyncio event loop raises an error that tells you to use the async API instead.


🧪 Playwright + pytest: The Test Framework

The playwright package is a library, not a test runner. It does not discover tests, produce pass/fail results or create reports by itself. In Python, that job belongs to pytest, the most popular Python testing framework.

Microsoft publishes an official pytest plugin, pytest-playwright (current version 0.10.0). Installing it gives you:

  • Ready-made fixtures such as page, context and browser. Each test receives a fresh, isolated page without writing any launch or close code.
  • Command-line options like --browser firefox, --headed, --slowmo, --tracing, --video and --screenshot.
  • Web-first assertions through expect(), which wait and retry until the condition is true.

Here is a preview of what a test looks like. You will write and run it yourself in Lesson 4:

📄 test_preview.py

from playwright.sync_api import Page, expect


def test_practice_page_title(page: Page):
    page.goto("https://sqatools.in/automation-practice-page/")
    expect(page).to_have_title("Automation Practice Page - SQA Tools")

Notice what is missing: no sync_playwright(), no launch(), no close(). The page fixture handles all of that.


🆚 Playwright Python vs Playwright TypeScript

If you have seen our Playwright TypeScript tutorial, the ideas are identical but some names and tools differ:

TopicTypeScriptPython
Test runnerPlaywright Test (npx playwright test)pytest with pytest-playwright (pytest)
Configplaywright.config.tspytest.ini / pyproject.toml + conftest.py fixtures
Method namescamelCase: getByRole, toHaveTextsnake_case: get_by_role, to_have_text
Asyncawait on every callNot needed with the sync API
Retries and parallel runsBuilt inPlugins: pytest-rerunfailures, pytest-xdist
Screenshot comparisonBuilt-in toHaveScreenshotNo built-in matcher (covered in Module 5)
AI test agents (planner, generator, healer)AvailableNot available for Python yet

✅ Why Testers Choose Playwright for Python

  • Auto-waiting removes most time.sleep() calls and flaky waits.
  • One API for three engines, including WebKit on any operating system.
  • Isolated browser contexts give every test a fresh, cookie-free session in milliseconds.
  • Powerful tooling: Codegen records tests for you, the Inspector debugs them, and the Trace Viewer replays failures step by step.
  • Network control to mock APIs, block resources or record traffic.
  • Python’s ecosystem: pytest, data libraries, and easy reading of JSON, CSV and Excel test data.

❓ Frequently Asked Questions

🤔 Is Playwright for Python free?

Yes. Playwright and pytest-playwright are open source under the Apache 2.0 license.

🤔 Do I need Node.js to use Playwright with Python?

No. The Python package bundles the driver and the runtime it needs. You only install Python packages and then the browsers with playwright install.

🤔 Is Playwright better than Selenium for Python?

Both are good tools. Playwright gives you auto-waiting, browser contexts, network mocking and tracing out of the box, while Selenium has a longer history and wider grid support. Read Playwright vs Selenium vs Cypress for a detailed comparison.

🤔 Should I learn the sync or async API first?

Start with the sync API. It is what pytest-playwright uses and what you will use for day-to-day testing. The async API uses the same methods with await, so switching later is easy.


🏁 Summary

  • Playwright for Python automates Chromium, Firefox and WebKit from Python 3.10+.
  • The package bundles its own driver; browsers are installed separately with playwright install.
  • It has a sync API (used for testing in this course) and an equivalent async API.
  • pytest plus the official pytest-playwright plugin provides fixtures, CLI options and the test runner.

Next, you will set up Python, a virtual environment, Playwright, pytest and VS Code on your machine.

🎬 Watch the Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on the SQA Tools Automation Practice Page

JavaScript Async/Await Practice Programs with Solutions (31 Programs)

Practise JavaScript promises and async/await with 31 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning JavaScript for Playwright.

How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with node program.js.

New to this topic? Read the JavaScript Async/Await lesson first.

JavaScript Beginner Programs

1. Synchronous vs asynchronous orderBeginner

Predict the order of logs when one of them is inside setTimeout.

Show solution
console.log('1. start');
setTimeout(() => console.log('3. timeout callback'), 0);
console.log('2. end');
await new Promise(resolve => setTimeout(resolve, 10));

Output

1. start
2. end
3. timeout callback

Explanation: setTimeout callbacks run later, after the current code finishes, even with a 0 ms delay.

2. Create and resolve a promiseBeginner

Create a promise that resolves with 'done' and print the result with then.

Show solution
const task = new Promise(resolve => resolve('done'));
task.then(value => console.log('Result:', value));
await task;

Output

Result: done

Explanation: a promise represents a future value. then runs when it resolves.

3. Reject a promise and catch itBeginner

Create a promise that rejects and handle it with catch.

Show solution
const failing = new Promise((resolve, reject) => reject(new Error('Network error')));
await failing.catch(error => console.log('Caught:', error.message));

Output

Caught: Network error

Explanation: reject signals failure, and .catch handles it.

4. A sleep helperBeginner

Write sleep(ms) that returns a promise, and await it between two logs.

Show solution
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
console.log('Waiting...');
await sleep(50);
console.log('Done after 50 ms');

Output

Waiting...
Done after 50 ms

Explanation: wrapping setTimeout in a promise makes delays awaitable. In Playwright tests, prefer auto-waiting over fixed sleeps.

5. Async function returns a promiseBeginner

Show that an async function always returns a promise.

Show solution
async function getTitle() {
  return 'Playwright';
}
const result = getTitle();
console.log(result instanceof Promise);
console.log(await result);

Output

true
Playwright

Explanation: async wraps the return value in a resolved promise. Use await to read the value.

6. await inside an async functionBeginner

Await a simulated API call inside an async function.

Show solution
const fetchUser = () => new Promise(resolve => setTimeout(() => resolve({ name: 'Asha' }), 20));
async function showUser() {
  const user = await fetchUser();
  console.log('User:', user.name);
}
await showUser();

Output

User: Asha

Explanation: await pauses the async function until the promise settles, without blocking other work.

7. try/catch with awaitBeginner

Catch a rejected promise using try/catch and await.

Show solution
const login = () => Promise.reject(new Error('Invalid credentials'));
try {
  await login();
} catch (error) {
  console.log('Login failed:', error.message);
}

Output

Login failed: Invalid credentials

Explanation: with await, rejected promises throw, so normal try/catch handles them.

8. Chain then callsBeginner

Chain three then calls that transform a value step by step.

Show solution
const total = await Promise.resolve(10)
  .then(n => n * 2)
  .then(n => n + 5)
  .then(n => `Total: ${n}`);
console.log(total);

Output

Total: 25

Explanation: each then receives the previous result and returns a new value to the next step.

9. finally on a promiseBeginner

Use finally to log cleanup after success and after failure.

Show solution
await Promise.resolve('ok').then(v => console.log('Success:', v)).finally(() => console.log('cleanup 1'));
await Promise.reject(new Error('fail')).catch(e => console.log('Error:', e.message)).finally(() => console.log('cleanup 2'));

Output

Success: ok
cleanup 1
Error: fail
cleanup 2

Explanation: finally runs whether the promise resolved or rejected, just like in try/catch/finally.

10. Promise.resolve and Promise.rejectBeginner

Create already-settled promises and read their values.

Show solution
const ok = await Promise.resolve(200);
console.log('Status:', ok);
const err = await Promise.reject(new Error('timeout')).catch(e => e.message);
console.log('Error:', err);

Output

Status: 200
Error: timeout

Explanation: Promise.resolve and Promise.reject create settled promises, which is useful in tests and mocks.

JavaScript Intermediate Programs

11. Sequential vs parallel timingIntermediate

Run two 50 ms tasks one after another, then in parallel, and compare the elapsed time.

Show solution
const task = (name, ms) => new Promise(r => setTimeout(() => r(name), ms));
let start = Date.now();
await task('A', 50);
await task('B', 50);
const sequential = Date.now() - start;
start = Date.now();
await Promise.all([task('A', 50), task('B', 50)]);
const parallel = Date.now() - start;
console.log('Parallel faster:', parallel < sequential);

Output

Parallel faster: true

Explanation: awaiting one by one adds up the delays. Promise.all runs independent tasks at the same time.

12. Promise.all results orderIntermediate

Run three tasks with different delays in parallel and print the results.

Show solution
const task = (name, ms) => new Promise(r => setTimeout(() => r(name), ms));
const results = await Promise.all([task('slow', 60), task('fast', 10), task('medium', 30)]);
console.log(results);

Output

[ 'slow', 'fast', 'medium' ]

Explanation: Promise.all returns results in the order of the input array, not in completion order.

13. Promise.all fails fastIntermediate

Show that Promise.all rejects as soon as any promise rejects.

Show solution
const ok = ms => new Promise(r => setTimeout(() => r('ok'), ms));
const fail = ms => new Promise((_, reject) => setTimeout(() => reject(new Error('API 500')), ms));
try {
  await Promise.all([ok(30), fail(10), ok(20)]);
} catch (error) {
  console.log('Promise.all rejected:', error.message);
}

Output

Promise.all rejected: API 500

Explanation: one rejection rejects the whole Promise.all. Use allSettled when you need every result.

14. Promise.allSettledIntermediate

Get the status of every promise, even when some fail.

Show solution
const results = await Promise.allSettled([
  Promise.resolve('page loaded'),
  Promise.reject(new Error('image failed')),
  Promise.resolve('fonts loaded')
]);
for (const r of results) console.log(r.status, r.status === 'fulfilled' ? r.value : r.reason.message);

Output

fulfilled page loaded
rejected image failed
fulfilled fonts loaded

Explanation: allSettled never rejects. It reports fulfilled or rejected for each promise.

15. Promise.race for timeoutsIntermediate

Use Promise.race to fail a slow request after 30 ms.

Show solution
const request = new Promise(r => setTimeout(() => r('response'), 100));
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Request timed out')), 30));
try {
  console.log(await Promise.race([request, timeout]));
} catch (error) {
  console.log(error.message);
}

Output

Request timed out

Explanation: Promise.race settles with whichever promise settles first, which makes it a simple timeout tool.

16. Promise.anyIntermediate

Use Promise.any to get the first successful mirror, ignoring failures.

Show solution
const mirror = (name, ms, ok) => new Promise((res, rej) => setTimeout(() => (ok ? res(name) : rej(new Error(name))), ms));
const fastest = await Promise.any([mirror('mirror-a', 10, false), mirror('mirror-b', 30, true), mirror('mirror-c', 50, true)]);
console.log('First success:', fastest);

Output

First success: mirror-b

Explanation: Promise.any resolves with the first fulfilled promise and only rejects if all of them fail.

17. Convert a callback to a promiseIntermediate

Wrap a callback-style function in a promise.

Show solution
function readConfig(callback) {
  setTimeout(() => callback(null, { retries: 2 }), 10);
}
const readConfigAsync = () =>
  new Promise((resolve, reject) => readConfig((err, data) => (err ? reject(err) : resolve(data))));
const config = await readConfigAsync();
console.log('Retries:', config.retries);

Output

Retries: 2

Explanation: wrapping callbacks in promises lets old APIs work with async/await.

18. Microtasks before timersIntermediate

Compare the order of Promise.then and setTimeout callbacks.

Show solution
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('sync');
await new Promise(r => setTimeout(r, 10));

Output

sync
promise
timeout

Explanation: promise callbacks are microtasks and run before timer callbacks, even with a 0 ms delay.

19. forEach does not waitIntermediate

Show why forEach with async callbacks does not wait, and fix it with for...of.

Show solution
const delay = ms => new Promise(r => setTimeout(r, ms));
const steps = ['one', 'two'];
steps.forEach(async step => { await delay(20); console.log('forEach finished', step); });
console.log('after forEach');
await delay(50);
for (const step of steps) { await delay(5); console.log('for...of finished', step); }
console.log('after for...of');

Output

after forEach
forEach finished one
forEach finished two
for...of finished one
for...of finished two
after for...of

Explanation: forEach ignores returned promises, so the code after it runs first. for...of with await waits for each step.

20. Async iteration with map and Promise.allIntermediate

Fetch details for several IDs in parallel using map and Promise.all.

Show solution
const getProduct = id => new Promise(r => setTimeout(() => r({ id, price: id * 100 }), 10));
const ids = [1, 2, 3];
const products = await Promise.all(ids.map(getProduct));
console.log(products.map(p => `#${p.id}=${p.price}`).join(', '));

Output

#1=100, #2=200, #3=300

Explanation: map creates one promise per item, and Promise.all waits for all of them.

JavaScript Real-World QA Scenario Programs

21. Retry an async actionReal-world

Retry a flaky async call up to 3 times with a short delay between attempts.

Show solution
const sleep = ms => new Promise(r => setTimeout(r, ms));
let calls = 0;
async function flakyApi() {
  calls++;
  if (calls < 3) throw new Error(`503 on call ${calls}`);
  return 'data';
}
async function retry(fn, attempts, delayMs) {
  for (let i = 1; i <= attempts; i++) {
    try { return await fn(); }
    catch (error) { console.log(`Attempt ${i} failed: ${error.message}`); if (i < attempts) await sleep(delayMs); }
  }
  throw new Error('All retries failed');
}
console.log('Result:', await retry(flakyApi, 3, 10));

Output

Attempt 1 failed: 503 on call 1
Attempt 2 failed: 503 on call 2
Result: data

Explanation: return await inside try makes rejected promises reach the catch, so the loop can retry.

22. Poll until a job completesReal-world

Poll a job status every 10 ms until it is 'done', with a maximum number of polls.

Show solution
const sleep = ms => new Promise(r => setTimeout(r, ms));
const statuses = ['queued', 'running', 'running', 'done'];
let call = 0;
const getStatus = async () => statuses[Math.min(call++, statuses.length - 1)];
async function waitForDone(maxPolls) {
  for (let i = 1; i <= maxPolls; i++) {
    const status = await getStatus();
    console.log(`Poll ${i}: ${status}`);
    if (status === 'done') return true;
    await sleep(10);
  }
  return false;
}
console.log('Completed:', await waitForDone(10));

Output

Poll 1: queued
Poll 2: running
Poll 3: running
Poll 4: done
Completed: true

Explanation: polling with a limit is how tests wait for background jobs. Playwright’s expect.poll does this for you.

23. Timeout wrapperReal-world

Write withTimeout(promise, ms) that rejects when a promise takes too long.

Show solution
function withTimeout(promise, ms) {
  let timer;
  const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`Timed out after ${ms} ms`)), ms); });
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}
const slow = new Promise(r => setTimeout(() => r('slow result'), 80));
const fast = new Promise(r => setTimeout(() => r('fast result'), 10));
console.log(await withTimeout(fast, 50));
try { await withTimeout(slow, 30); } catch (e) { console.log(e.message); }

Output

fast result
Timed out after 30 ms

Explanation: clearing the timer in finally avoids leaving stray timers running after the race settles.

24. Run test steps sequentiallyReal-world

Run login, search and checkout steps one after another and log each step.

Show solution
const sleep = ms => new Promise(r => setTimeout(r, ms));
const steps = [
  { name: 'login', run: async () => { await sleep(10); return 'logged in'; } },
  { name: 'search', run: async () => { await sleep(5); return '3 results'; } },
  { name: 'checkout', run: async () => { await sleep(8); return 'order placed'; } }
];
for (const step of steps) console.log(`${step.name}: ${await step.run()}`);

Output

login: logged in
search: 3 results
checkout: order placed

Explanation: dependent steps must run in order, so await each one before starting the next.

25. Parallel API setupReal-world

Create test data through three independent API calls in parallel.

Show solution
const api = (name, ms) => new Promise(r => setTimeout(() => r(`${name} created`), ms));
const [user, product, coupon] = await Promise.all([api('user', 30), api('product', 20), api('coupon', 10)]);
console.log(user);
console.log(product);
console.log(coupon);

Output

user created
product created
coupon created

Explanation: independent setup steps can run together, which makes test setup faster.

26. Collect results from flaky servicesReal-world

Check three health endpoints and report which are down, without stopping at the first failure.

Show solution
const check = (name, up, ms) => new Promise((res, rej) => setTimeout(() => (up ? res(name) : rej(new Error(name))), ms));
const results = await Promise.allSettled([check('auth', true, 10), check('search', false, 20), check('payments', true, 15)]);
const down = results.filter(r => r.status === 'rejected').map(r => r.reason.message);
console.log('Up:', results.length - down.length, '| Down:', down.join(', ') || 'none');

Output

Up: 2 | Down: search

Explanation: allSettled collects every outcome, so one failed service does not hide the others.

27. Limit concurrencyReal-world

Run 5 tasks with at most 2 running at the same time.

Show solution
const sleep = ms => new Promise(r => setTimeout(r, ms));
let running = 0, maxRunning = 0;
async function job(id) {
  running++; maxRunning = Math.max(maxRunning, running);
  await sleep(10);
  running--;
  return id;
}
async function runWithLimit(ids, limit) {
  const results = [];
  for (let i = 0; i < ids.length; i += limit) {
    results.push(...(await Promise.all(ids.slice(i, i + limit).map(job))));
  }
  return results;
}
console.log(await runWithLimit([1, 2, 3, 4, 5], 2));
console.log('Max running at once:', maxRunning);

Output

[ 1, 2, 3, 4, 5 ]
Max running at once: 2

Explanation: processing in batches protects APIs from too many simultaneous requests, similar to limiting workers.

28. Measure an async step’s durationReal-world

Measure how long an async step takes and flag it as slow when it exceeds 40 ms.

Show solution
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function timed(name, fn, limitMs) {
  const start = Date.now();
  await fn();
  const ms = Date.now() - start;
  console.log(`${name}: ${ms >= limitMs ? 'SLOW' : 'ok'}`);
}
await timed('load dashboard', () => sleep(10), 40);
await timed('export report', () => sleep(60), 40);

Output

load dashboard: ok
export report: SLOW

Explanation: timing async steps helps spot performance regressions in end-to-end flows.

29. Async error in one of several stepsReal-world

Stop the flow when a step fails and report which step failed.

Show solution
const steps = [
  ['open page', async () => 'ok'],
  ['submit form', async () => { throw new Error('button disabled'); }],
  ['verify message', async () => 'ok']
];
try {
  for (const [name, run] of steps) {
    await run();
    console.log('✔', name);
  }
} catch (error) {
  console.log('✘ Flow stopped:', error.message);
}

Output

✔ open page
✘ Flow stopped: button disabled

Explanation: with await in a loop, the first rejection jumps to catch and the remaining steps are skipped.

30. Wait for a conditionReal-world

Write waitFor(condition, timeout, interval) that resolves when a condition becomes true.

Show solution
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function waitFor(condition, timeout = 200, interval = 10) {
  const end = Date.now() + timeout;
  while (Date.now() < end) {
    if (condition()) return true;
    await sleep(interval);
  }
  throw new Error('Condition not met in time');
}
let ready = false;
setTimeout(() => { ready = true; }, 40);
console.log('Ready:', await waitFor(() => ready));
try { await waitFor(() => false, 30); } catch (e) { console.log(e.message); }

Output

Ready: true
Condition not met in time

Explanation: this is the idea behind Playwright’s auto-waiting: check repeatedly until it passes or times out.

31. Async generator for paginated dataReal-world

Use an async generator to read pages until there are no more.

Show solution
const pages = [[1, 2], [3, 4], [5]];
async function* fetchAll() {
  for (let page = 0; page < pages.length; page++) {
    await new Promise(r => setTimeout(r, 5));
    yield* pages[page];
  }
}
const items = [];
for await (const item of fetchAll()) items.push(item);
console.log(items);

Output

[ 1, 2, 3, 4, 5 ]

Explanation: for await...of consumes async iterators, which suits paginated APIs and streams.

Keep Practising

JavaScript Exception Handling Practice Programs with Solutions (30 Programs)

Practise JavaScript exception handling with 30 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning JavaScript for Playwright.

How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with node program.js.

New to this topic? Read the JavaScript Exception Handling lesson first.

JavaScript Beginner Programs

1. Basic try/catchBeginner

Parse invalid JSON inside try/catch and print the error name.

Show solution
try {
  JSON.parse('{ invalid json }');
} catch (error) {
  console.log('Caught:', error.name);
}
console.log('Program continues');

Output

Caught: SyntaxError
Program continues

Explanation: catch receives the thrown error, so the program can continue instead of crashing.

2. finally always runsBeginner

Show that finally runs both when an error happens and when it does not.

Show solution
for (const input of ['{"ok":true}', 'oops']) {
  try {
    JSON.parse(input);
    console.log('Parsed', input);
  } catch (error) {
    console.log('Failed to parse', input);
  } finally {
    console.log('Cleanup done');
  }
}

Output

Parsed {"ok":true}
Cleanup done
Failed to parse oops
Cleanup done

Explanation: finally is the place for cleanup, such as closing files or browsers, because it always runs.

3. Throw a custom errorBeginner

Write divide(a, b) that throws an error when b is 0.

Show solution
function divide(a, b) {
  if (b === 0) throw new Error('Cannot divide by zero');
  return a / b;
}
try {
  console.log(divide(10, 2));
  console.log(divide(5, 0));
} catch (error) {
  console.log('Error:', error.message);
}

Output

5
Error: Cannot divide by zero

Explanation: throw stops the function and passes the error to the nearest catch.

4. Error propertiesBeginner

Print the name and message of a caught error.

Show solution
try {
  throw new Error('Login button not found');
} catch (error) {
  console.log(error.name);
  console.log(error.message);
  console.log(error instanceof Error);
}

Output

Error
Login button not found
true

Explanation: every Error has a name, a message and a stack trace.

5. ReferenceErrorBeginner

Use an undefined variable inside try and print the error type.

Show solution
try {
  console.log(undefinedVariable);
} catch (error) {
  console.log(error.name);
}

Output

ReferenceError

Explanation: using a name that was never declared throws a ReferenceError.

6. TypeErrorBeginner

Call a method on undefined and print the error type.

Show solution
const response = {};
try {
  response.data.items.length;
} catch (error) {
  console.log(error.name);
}

Output

TypeError

Explanation: reading a property of undefined throws a TypeError, one of the most common test-script errors.

7. RangeErrorBeginner

Create an array with a negative length and catch the error.

Show solution
try {
  new Array(-1);
} catch (error) {
  console.log(error.name, '-', error.message);
}

Output

RangeError - Invalid array length

Explanation: a RangeError means a value is outside the allowed range.

8. Validate input and throwBeginner

Throw a TypeError when age is not a number.

Show solution
function setAge(age) {
  if (typeof age !== 'number') throw new TypeError('age must be a number');
  return `Age set to ${age}`;
}
for (const value of [30, 'thirty']) {
  try {
    console.log(setAge(value));
  } catch (error) {
    console.log(`${error.name}: ${error.message}`);
  }
}

Output

Age set to 30
TypeError: age must be a number

Explanation: throwing the most specific built-in error type tells callers what went wrong.

9. catch without a bindingBeginner

Use catch without naming the error variable when you do not need it.

Show solution
function isValidJson(text) {
  try {
    JSON.parse(text);
    return true;
  } catch {
    return false;
  }
}
console.log(isValidJson('[1, 2]'));
console.log(isValidJson('[1, 2'));

Output

true
false

Explanation: modern JavaScript allows catch { } without (error) when the error itself is not used.

10. Return a default on failureBeginner

Read a number from text and fall back to 0 when it is invalid.

Show solution
function toNumberOrZero(text) {
  try {
    const n = Number(text);
    if (Number.isNaN(n)) throw new Error('not a number');
    return n;
  } catch {
    return 0;
  }
}
console.log(toNumberOrZero('42'), toNumberOrZero('abc'));

Output

42 0

Explanation: catching and returning a default keeps the program running with a safe value.

JavaScript Intermediate Programs

11. Custom error classIntermediate

Create ValidationError with a field property and catch it.

Show solution
class ValidationError extends Error {
  constructor(field, message) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
  }
}
try {
  throw new ValidationError('email', 'Email is required');
} catch (error) {
  console.log(error.name, error.field, error.message);
}

Output

ValidationError email Email is required

Explanation: custom error classes carry extra details and can be identified by type.

12. Handle different error typesIntermediate

Handle ValidationError and other errors differently with instanceof.

Show solution
class ValidationError extends Error {}
function run(action) {
  try {
    action();
  } catch (error) {
    if (error instanceof ValidationError) console.log('Fix your input:', error.message);
    else if (error instanceof TypeError) console.log('Bug in code:', error.message);
    else console.log('Unknown error:', error.message);
  }
}
run(() => { throw new ValidationError('password too short'); });
run(() => { null.click(); });
run(() => { throw new Error('network down'); });

Output

Fix your input: password too short
Bug in code: Cannot read properties of null (reading 'click')
Unknown error: network down

Explanation: instanceof lets one catch block react differently to each kind of error.

13. Rethrow unexpected errorsIntermediate

Handle only SyntaxError while parsing and rethrow anything else.

Show solution
function parseConfig(text) {
  try {
    return JSON.parse(text);
  } catch (error) {
    if (error instanceof SyntaxError) return {};
    throw error;
  }
}
console.log(parseConfig('{"retries":2}'));
console.log(parseConfig('not json'));

Output

{ retries: 2 }
{}

Explanation: catch only what you can handle. Rethrowing other errors keeps real bugs visible.

14. Error cause chainingIntermediate

Wrap a low-level error in a higher-level one using cause.

Show solution
function loadUser() {
  try {
    JSON.parse('{bad');
  } catch (error) {
    throw new Error('Could not load user', { cause: error });
  }
}
try {
  loadUser();
} catch (error) {
  console.log(error.message);
  console.log('Caused by:', error.cause.name);
}

Output

Could not load user
Caused by: SyntaxError

Explanation: cause preserves the original error, so you get a clear message without losing the root cause.

15. Nested try/catchIntermediate

Use an inner try/catch for an optional step and an outer one for critical steps.

Show solution
try {
  console.log('Step 1: open page');
  try {
    throw new Error('cookie banner not found');
  } catch (inner) {
    console.log('Optional step skipped:', inner.message);
  }
  console.log('Step 2: login');
  throw new Error('login failed');
} catch (outer) {
  console.log('Test failed:', outer.message);
}

Output

Step 1: open page
Optional step skipped: cookie banner not found
Step 2: login
Test failed: login failed

Explanation: the inner catch handles a recoverable problem, while the outer one still catches fatal errors.

16. finally with returnIntermediate

Show that finally runs even when try returns a value.

Show solution
function getStatus() {
  try {
    return 'from try';
  } finally {
    console.log('finally runs before the value is returned');
  }
}
console.log(getStatus());

Output

finally runs before the value is returned
from try

Explanation: finally runs before the function actually returns, which guarantees cleanup.

17. Collect errors instead of stoppingIntermediate

Validate several fields and collect all errors.

Show solution
const checks = [
  () => { if (!'qa@test.com'.includes('@')) throw new Error('bad email'); },
  () => { if ('123'.length < 8) throw new Error('password too short'); },
  () => { if (Number('abc') !== Number('abc')) throw new Error('age is not a number'); }
];
const errors = [];
for (const check of checks) {
  try { check(); } catch (error) { errors.push(error.message); }
}
console.log(errors.join(' | '));

Output

password too short | age is not a number

Explanation: catching inside the loop lets every check run, like Playwright’s soft assertions.

18. Error-first result objectsIntermediate

Return { ok, value, error } instead of throwing.

Show solution
function safeParse(text) {
  try {
    return { ok: true, value: JSON.parse(text) };
  } catch (error) {
    return { ok: false, error: error.message.split(' in JSON')[0] };
  }
}
console.log(safeParse('[1]').ok);
console.log(safeParse('{oops').ok);

Output

true
false

Explanation: result objects make failures part of the normal return value, which is handy in data pipelines.

19. Assertion functionIntermediate

Write assert(condition, message) that throws when the condition is false.

Show solution
function assert(condition, message) {
  if (!condition) throw new Error(`Assertion failed: ${message}`);
}
try {
  assert(2 + 2 === 4, 'maths works');
  console.log('First assertion passed');
  assert('Login'.length === 4, 'title length');
} catch (error) {
  console.log(error.message);
}

Output

First assertion passed
Assertion failed: title length

Explanation: tests are built on assertions that throw when expectations are not met.

JavaScript Real-World QA Scenario Programs

20. Retry on failureReal-world

Retry a flaky action up to 3 times before giving up.

Show solution
let calls = 0;
function clickButton() {
  calls++;
  if (calls < 3) throw new Error(`element detached (call ${calls})`);
  return 'clicked';
}
let result;
for (let attempt = 1; attempt <= 3; attempt++) {
  try {
    result = clickButton();
    break;
  } catch (error) {
    console.log(`Attempt ${attempt} failed: ${error.message}`);
  }
}
console.log('Result:', result);

Output

Attempt 1 failed: element detached (call 1)
Attempt 2 failed: element detached (call 2)
Result: clicked

Explanation: catch, log and retry. break leaves the loop as soon as an attempt succeeds.

21. Safe JSON test-data loaderReal-world

Load JSON test data and report a clear message for invalid content.

Show solution
function loadTestData(name, text) {
  try {
    const data = JSON.parse(text);
    if (!Array.isArray(data)) throw new Error('expected an array');
    return data;
  } catch (error) {
    console.log(`${name}: ${error instanceof SyntaxError ? 'invalid JSON' : error.message}`);
    return [];
  }
}
console.log(loadTestData('users.json', '[{"id":1}]').length);
console.log(loadTestData('broken.json', '[{"id":1}').length);
console.log(loadTestData('object.json', '{"id":1}').length);

Output

1
broken.json: invalid JSON
0
object.json: expected an array
0

Explanation: clear error messages save time when test data files are malformed.

22. Validate a signup formReal-world

Throw a ValidationError for the first invalid field and show it to the user.

Show solution
class ValidationError extends Error {
  constructor(field, message) { super(message); this.name = 'ValidationError'; this.field = field; }
}
function validate({ email, password }) {
  if (!email?.includes('@')) throw new ValidationError('email', 'Enter a valid email');
  if (!password || password.length < 8) throw new ValidationError('password', 'Use at least 8 characters');
  return 'Form is valid';
}
for (const form of [{ email: 'qa', password: '12345678' }, { email: 'qa@test.com', password: '123' }, { email: 'qa@test.com', password: 'Secret123' }]) {
  try { console.log(validate(form)); }
  catch (e) { console.log(`[${e.field}] ${e.message}`); }
}

Output

[email] Enter a valid email
[password] Use at least 8 characters
Form is valid

Explanation: a field name on the error lets the UI show the message next to the right input.

23. Map HTTP status codes to errorsReal-world

Throw a specific error for each failing status code.

Show solution
class HttpError extends Error {
  constructor(status) { super(`HTTP ${status}`); this.status = status; }
}
function checkResponse(status) {
  if (status >= 200 && status < 300) return 'OK';
  throw new HttpError(status);
}
for (const status of [200, 404, 503]) {
  try {
    console.log(status, checkResponse(status));
  } catch (error) {
    const hint = error.status >= 500 ? 'server problem, retry later' : 'check the request';
    console.log(status, error.message, '-', hint);
  }
}

Output

200 OK
404 HTTP 404 - check the request
503 HTTP 503 - server problem, retry later

Explanation: a status code on the error lets callers decide whether to retry or fail fast.

24. Always release resourcesReal-world

Simulate opening and closing a browser, closing it even when the test fails.

Show solution
function runTest(shouldFail) {
  console.log('Browser opened');
  try {
    if (shouldFail) throw new Error('assertion failed');
    console.log('Test passed');
  } catch (error) {
    console.log('Test failed:', error.message);
  } finally {
    console.log('Browser closed');
  }
}
runTest(false);
runTest(true);

Output

Browser opened
Test passed
Browser closed
Browser opened
Test failed: assertion failed
Browser closed

Explanation: finally guarantees cleanup, which is what Playwright fixtures do after use().

25. Soft assertionsReal-world

Check several page values and report all failures at the end.

Show solution
const page = { title: 'Shop', cartCount: 2, user: 'guest' };
const expected = { title: 'Shop', cartCount: 3, user: 'asha' };
const failures = [];
for (const key of Object.keys(expected)) {
  try {
    if (page[key] !== expected[key]) throw new Error(`${key}: expected ${expected[key]}, got ${page[key]}`);
  } catch (error) {
    failures.push(error.message);
  }
}
console.log(failures.length ? `${failures.length} failures\n- ${failures.join('\n- ')}` : 'All checks passed');

Output

2 failures
- cartCount: expected 3, got 2
- user: expected asha, got guest

Explanation: collecting failures shows every problem in one run, like expect.soft in Playwright.

26. Timeout errorReal-world

Throw a TimeoutError when a simulated wait exceeds its limit.

Show solution
class TimeoutError extends Error {
  constructor(ms) { super(`Timed out after ${ms} ms`); this.name = 'TimeoutError'; }
}
function waitForElement(appearsAfterMs, timeoutMs) {
  if (appearsAfterMs > timeoutMs) throw new TimeoutError(timeoutMs);
  return `Element visible after ${appearsAfterMs} ms`;
}
for (const appears of [800, 7000]) {
  try { console.log(waitForElement(appears, 5000)); }
  catch (e) { console.log(`${e.name}: ${e.message}`); }
}

Output

Element visible after 800 ms
TimeoutError: Timed out after 5000 ms

Explanation: a named error type makes timeouts easy to recognise in reports and retry logic.

27. Error message for a missing elementReal-world

Build a helpful error message that lists the available options when a menu item is missing.

Show solution
function clickMenuItem(menu, name) {
  if (!menu.includes(name)) {
    throw new Error(`Menu item '${name}' not found. Available: ${menu.join(', ')}`);
  }
  return `Clicked ${name}`;
}
const menu = ['Home', 'Shop', 'Contact'];
try {
  console.log(clickMenuItem(menu, 'Shop'));
  console.log(clickMenuItem(menu, 'Blog'));
} catch (error) {
  console.log(error.message);
}

Output

Clicked Shop
Menu item 'Blog' not found. Available: Home, Shop, Contact

Explanation: including context in error messages turns a vague failure into an obvious fix.

28. Fail fast on missing configReal-world

Throw at start-up when required environment variables are missing.

Show solution
function requireEnv(env, names) {
  const missing = names.filter(name => !env[name]);
  if (missing.length) throw new Error(`Missing environment variables: ${missing.join(', ')}`);
  return 'Config OK';
}
try {
  console.log(requireEnv({ BASE_URL: 'https://qa.app.test', USERNAME: 'qa' }, ['BASE_URL', 'USERNAME']));
  console.log(requireEnv({ BASE_URL: 'https://qa.app.test' }, ['BASE_URL', 'USERNAME', 'PASSWORD']));
} catch (error) {
  console.log(error.message);
}

Output

Config OK
Missing environment variables: USERNAME, PASSWORD

Explanation: failing immediately with a clear message beats confusing failures halfway through a test run.

29. Log errors with contextReal-world

Wrap a step so errors include the step name.

Show solution
function step(name, fn) {
  try {
    fn();
    console.log(`✔ ${name}`);
  } catch (error) {
    throw new Error(`Step "${name}" failed: ${error.message}`);
  }
}
try {
  step('open home page', () => {});
  step('add item to cart', () => { throw new Error('button disabled'); });
  step('checkout', () => {});
} catch (error) {
  console.log(error.message);
}

Output

✔ open home page
Step "add item to cart" failed: button disabled

Explanation: adding the step name to the error is what Playwright’s test.step does in reports.

30. Count error types in a logReal-world

Count how many errors of each type appear in a list of caught errors.

Show solution
const caught = [new TypeError('a'), new Error('b'), new TypeError('c'), new RangeError('d')];
const counts = {};
for (const e of caught) counts[e.name] = (counts[e.name] || 0) + 1;
console.log(counts);

Output

{ TypeError: 2, Error: 1, RangeError: 1 }

Explanation: grouping by error.name quickly shows which kind of failure dominates a test run.

Keep Practising

JavaScript Object Practice Programs with Solutions (32 Programs)

Practise JavaScript objects with 32 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning JavaScript for Playwright.

How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with node program.js.

New to this topic? Read the JavaScript Objects lesson first.

JavaScript Beginner Programs

1. Create and read an objectBeginner

Create a user object with name, role and active, then print each property.

Show solution
const user = { name: 'Asha', role: 'tester', active: true };
console.log(user.name);
console.log(user['role']);
console.log(user.active);

Output

Asha
tester
true

Explanation: dot notation is the usual way to read properties. Bracket notation also works, and is needed for dynamic keys.

2. Add, update and delete propertiesBeginner

Add an email, change the role and delete active from a user object.

Show solution
const user = { name: 'Asha', role: 'tester', active: true };
user.email = 'asha@test.com';
user.role = 'lead';
delete user.active;
console.log(user);

Output

{ name: 'Asha', role: 'lead', email: 'asha@test.com' }

Explanation: objects are mutable, so properties can be added, changed or removed after creation.

3. Dynamic property namesBeginner

Read a property whose name is stored in a variable.

Show solution
const settings = { browser: 'chromium', retries: 2 };
const key = 'retries';
console.log(settings[key]);
console.log(settings.key);

Output

2
undefined

Explanation: settings[key] uses the variable’s value. settings.key looks for a property literally called key.

4. Check whether a property existsBeginner

Check for retries and proxy with in and Object.hasOwn.

Show solution
const config = { retries: 0 };
console.log('retries' in config);
console.log(Object.hasOwn(config, 'proxy'));
console.log(config.retries !== undefined);

Output

true
false
true

Explanation: in and Object.hasOwn check existence even when the value is falsy, such as 0.

5. Keys, values and entriesBeginner

Print the keys, values and entries of a test-case object.

Show solution
const testCase = { id: 'TC-1', priority: 'high', automated: true };
console.log(Object.keys(testCase));
console.log(Object.values(testCase));
console.log(Object.entries(testCase).length);

Output

[ 'id', 'priority', 'automated' ]
[ 'TC-1', 'high', true ]
3

Explanation: these methods turn an object into arrays that you can loop over or transform.

6. Loop over an objectBeginner

Print each property of a config object as key: value.

Show solution
const config = { browser: 'firefox', headless: false, workers: 2 };
for (const [key, value] of Object.entries(config)) {
  console.log(`${key}: ${value}`);
}

Output

browser: firefox
headless: false
workers: 2

Explanation: Object.entries with for...of and destructuring gives both key and value on each pass.

7. Object with a methodBeginner

Create a calculator object with add and subtract methods.

Show solution
const calculator = {
  add(a, b) { return a + b; },
  subtract(a, b) { return a - b; }
};
console.log(calculator.add(10, 5));
console.log(calculator.subtract(10, 5));

Output

15
5

Explanation: functions stored on an object are called methods.

8. Using this in a methodBeginner

Create a user object whose describe method uses this.

Show solution
const user = {
  name: 'Ravi',
  role: 'admin',
  describe() {
    return `${this.name} is an ${this.role}`;
  }
};
console.log(user.describe());

Output

Ravi is an admin

Explanation: inside a regular method, this refers to the object the method was called on.

9. Shorthand propertiesBeginner

Build an object from variables using shorthand property names.

Show solution
const browser = 'webkit';
const viewport = '1280x720';
const project = { browser, viewport };
console.log(project);

Output

{ browser: 'webkit', viewport: '1280x720' }

Explanation: { browser } is shorthand for { browser: browser } when the variable and property share a name.

10. Nested objectsBeginner

Read nested properties from an order object.

Show solution
const order = { id: 501, customer: { name: 'Meera', address: { city: 'Pune' } } };
console.log(order.customer.name);
console.log(order.customer.address.city);

Output

Meera
Pune

Explanation: chain property access to reach nested values.

JavaScript Intermediate Programs

11. Object destructuring with defaultsIntermediate

Destructure an options object with defaults and a renamed property.

Show solution
const options = { timeout: 5000, headless: false };
const { timeout, headless = true, retries = 1, timeout: waitMs } = options;
console.log(timeout, headless, retries, waitMs);

Output

5000 false 1 5000

Explanation: defaults apply only when a property is undefined. timeout: waitMs copies the same value into a new name.

12. Merge objects with spreadIntermediate

Merge default settings with overrides, keeping the overrides.

Show solution
const defaults = { browser: 'chromium', retries: 0, video: 'off' };
const overrides = { retries: 2, video: 'on' };
const merged = { ...defaults, ...overrides };
console.log(merged);

Output

{ browser: 'chromium', retries: 2, video: 'on' }

Explanation: later spreads overwrite earlier properties with the same name.

13. Shallow vs deep copyIntermediate

Show that a spread copy shares nested objects, and fix it with structuredClone.

Show solution
const original = { name: 'cart', items: ['pen'] };
const shallow = { ...original };
const deep = structuredClone(original);
shallow.items.push('book');
deep.items.push('bag');
console.log(original.items);
console.log(deep.items);

Output

[ 'pen', 'book' ]
[ 'pen', 'bag' ]

Explanation: spread copies only the top level. structuredClone copies nested data too.

14. Freeze and sealIntermediate

Compare Object.freeze and Object.seal.

Show solution
const frozen = Object.freeze({ env: 'qa' });
const sealed = Object.seal({ env: 'qa' });
frozen.env = 'prod';
sealed.env = 'prod';
sealed.extra = true;
console.log(frozen.env, sealed.env, 'extra' in sealed);

Output

qa prod false

Explanation: frozen objects cannot change at all. Sealed objects allow value changes but no new or deleted properties.

15. Count propertiesIntermediate

Count how many properties an object has.

Show solution
const response = { id: 1, name: 'Item', price: 99, inStock: true };
console.log(Object.keys(response).length);

Output

4

Explanation: objects have no length, so count their keys instead.

16. Transform an objectIntermediate

Apply a 10% discount to every price in a price list object.

Show solution
const prices = { pen: 20, book: 350, bag: 900 };
const discounted = Object.fromEntries(
  Object.entries(prices).map(([item, price]) => [item, price * 0.9])
);
console.log(discounted);

Output

{ pen: 18, book: 315, bag: 810 }

Explanation: convert to entries, map them, and convert back with Object.fromEntries.

17. Filter object propertiesIntermediate

Keep only the settings whose values are not null.

Show solution
const settings = { browser: 'chromium', proxy: null, retries: 2, baseURL: null };
const cleaned = Object.fromEntries(Object.entries(settings).filter(([, value]) => value !== null));
console.log(cleaned);

Output

{ browser: 'chromium', retries: 2 }

Explanation: filtering entries is the object equivalent of array.filter.

18. Compare two objectsIntermediate

Write a function that checks whether two flat objects have the same keys and values.

Show solution
function shallowEqual(a, b) {
  const keysA = Object.keys(a);
  const keysB = Object.keys(b);
  if (keysA.length !== keysB.length) return false;
  return keysA.every(key => a[key] === b[key]);
}
console.log(shallowEqual({ x: 1, y: 2 }, { y: 2, x: 1 }));
console.log(shallowEqual({ x: 1 }, { x: '1' }));

Output

true
false

Explanation: === compares object references, so content equality needs a key-by-key check.

19. Getters and settersIntermediate

Create an object with a fullName getter and setter.

Show solution
const person = {
  first: 'Asha',
  last: 'Sharma',
  get fullName() { return `${this.first} ${this.last}`; },
  set fullName(value) { [this.first, this.last] = value.split(' '); }
};
console.log(person.fullName);
person.fullName = 'Neha Verma';
console.log(person.first, person.last);

Output

Asha Sharma
Neha Verma

Explanation: getters and setters run code when a property is read or written, while looking like normal properties.

20. Optional chaining on missing dataIntermediate

Read a deeply nested value that may not exist.

Show solution
const responses = [
  { data: { user: { profile: { country: 'IN' } } } },
  { data: { user: null } },
  {}
];
for (const r of responses) console.log(r.data?.user?.profile?.country ?? 'unknown');

Output

IN
unknown
unknown

Explanation: ?. stops at the first null or undefined, so missing data never throws.

JavaScript Real-World QA Scenario Programs

21. Validate an API responseReal-world

Check that an API user object has all required fields with the right types.

Show solution
const required = { id: 'number', email: 'string', active: 'boolean' };
const user = { id: 12, email: 'qa@test.com', active: 'true' };
const problems = Object.entries(required)
  .filter(([field, type]) => typeof user[field] !== type)
  .map(([field, type]) => `${field} should be ${type}`);
console.log(problems.length ? problems.join('; ') : 'Response is valid');

Output

active should be boolean

Explanation: a small schema object lets one loop check every field, a lightweight contract test.

22. Build test data with defaultsReal-world

Write buildUser(overrides) that returns a default user merged with overrides.

Show solution
function buildUser(overrides = {}) {
  return { name: 'QA User', email: 'qa.user@example.com', role: 'viewer', active: true, ...overrides };
}
console.log(buildUser().role);
const admin = buildUser({ role: 'admin', name: 'Admin' });
console.log(admin.name, admin.role, admin.active);

Output

viewer
Admin admin true

Explanation: a data builder gives sensible defaults, and each test overrides only what it needs.

23. Count tests by statusReal-world

Use an object to count statuses in a results array.

Show solution
const statuses = ['passed', 'failed', 'passed', 'flaky', 'passed', 'failed'];
const counts = {};
for (const s of statuses) counts[s] = (counts[s] ?? 0) + 1;
console.log(counts);

Output

{ passed: 3, failed: 2, flaky: 1 }

Explanation: object keys work as counters, a pattern used in every test report.

24. Environment config lookupReal-world

Store base URLs per environment in an object and pick one by name.

Show solution
const environments = {
  qa: { baseURL: 'https://qa.shop.test', retries: 1 },
  staging: { baseURL: 'https://staging.shop.test', retries: 2 }
};
const envName = 'staging';
const env = environments[envName] ?? environments.qa;
console.log(env.baseURL, env.retries);

Output

https://staging.shop.test 2

Explanation: an object lookup replaces a chain of if statements and falls back to a default.

25. Map API fields to UI labelsReal-world

Rename snake_case API keys to readable labels.

Show solution
const apiUser = { first_name: 'Ravi', last_name: 'Kumar', phone_no: '9876543210' };
const labels = { first_name: 'First name', last_name: 'Last name', phone_no: 'Phone' };
for (const [key, value] of Object.entries(apiUser)) {
  console.log(`${labels[key]}: ${value}`);
}

Output

First name: Ravi
Last name: Kumar
Phone: 9876543210

Explanation: a mapping object keeps label translations in one place.

26. Diff expected vs actual objectsReal-world

Report which fields differ between expected and actual user details.

Show solution
const expected = { name: 'Asha', city: 'Delhi', plan: 'pro' };
const actual = { name: 'Asha', city: 'Mumbai', plan: 'free' };
for (const key of Object.keys(expected)) {
  if (expected[key] !== actual[key]) {
    console.log(`${key}: expected '${expected[key]}', got '${actual[key]}'`);
  }
}

Output

city: expected 'Delhi', got 'Mumbai'
plan: expected 'pro', got 'free'

Explanation: field-level differences make failed assertions much easier to understand than a plain false.

27. Group products by categoryReal-world

Group product names under their category.

Show solution
const products = [
  { name: 'Mouse', category: 'electronics' },
  { name: 'Pen', category: 'stationery' },
  { name: 'Keyboard', category: 'electronics' }
];
const groups = {};
for (const p of products) (groups[p.category] ??= []).push(p.name);
console.log(groups);

Output

{ electronics: [ 'Mouse', 'Keyboard' ], stationery: [ 'Pen' ] }

Explanation: ??= creates the array the first time a category appears, then each product is pushed into it.

28. Read headers case-insensitivelyReal-world

Find the Content-Type header regardless of letter case.

Show solution
const headers = { 'content-type': 'application/json', 'X-Request-Id': 'abc-1' };
function getHeader(all, name) {
  const key = Object.keys(all).find(k => k.toLowerCase() === name.toLowerCase());
  return key ? all[key] : undefined;
}
console.log(getHeader(headers, 'Content-Type'));
console.log(getHeader(headers, 'x-request-id'));

Output

application/json
abc-1

Explanation: HTTP header names are case-insensitive, so compare them in lower case.

29. Remove sensitive fields before loggingReal-world

Copy a user object without its password and token before printing it.

Show solution
const user = { id: 3, email: 'qa@test.com', password: 'Secret!23', token: 'abc.def' };
const { password, token, ...safeUser } = user;
console.log(safeUser);

Output

{ id: 3, email: 'qa@test.com' }

Explanation: rest destructuring collects every property except the named ones, a quick way to strip secrets.

30. Convert form data to a query stringReal-world

Turn a filter object into a URL query string, skipping empty values.

Show solution
const filters = { q: 'laptop', minPrice: 30000, brand: '', sort: 'price_asc' };
const params = new URLSearchParams(
  Object.entries(filters).filter(([, v]) => v !== '').map(([k, v]) => [k, String(v)])
);
console.log(params.toString());

Output

q=laptop&minPrice=30000&sort=price_asc

Explanation: filter out empty values, then URLSearchParams handles encoding.

31. Track a page object’s stateReal-world

Create a cart object with methods that add items and calculate the total.

Show solution
const cart = {
  items: [],
  add(name, price, qty = 1) { this.items.push({ name, price, qty }); return this; },
  total() { return this.items.reduce((sum, i) => sum + i.price * i.qty, 0); }
};
cart.add('Mouse', 599).add('Cable', 199, 2);
console.log(cart.items.length, 'items, total', cart.total());

Output

2 items, total 997

Explanation: methods share state through this, and returning this allows chaining calls.

32. Convert an array of rows into objectsReal-world

Convert CSV rows into objects using the header row as keys.

Show solution
const rows = [['id', 'user', 'status'], ['1', 'asha', 'active'], ['2', 'ravi', 'locked']];
const [header, ...data] = rows;
const records = data.map(row => Object.fromEntries(header.map((h, i) => [h, row[i]])));
console.log(records[1]);
console.log(records.length);

Output

{ id: '2', user: 'ravi', status: 'locked' }
2

Explanation: pairing each header with the value at the same index builds one object per row.

Keep Practising

JavaScript Array Practice Programs with Solutions (33 Programs)

Practise JavaScript arrays with 33 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning JavaScript for Playwright.

How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with node program.js.

New to this topic? Read the JavaScript Array Methods lesson first.

JavaScript Beginner Programs

1. Create and read an arrayBeginner

Create an array of three browsers and print the first, last and length.

Show solution
const browsers = ['chromium', 'firefox', 'webkit'];
console.log(browsers[0]);
console.log(browsers[browsers.length - 1]);
console.log(browsers.length);

Output

chromium
webkit
3

Explanation: array indexes start at 0, so the last item is at length - 1.

2. Add and remove at the endBeginner

Use push and pop on a list of test names.

Show solution
const tests = ['login', 'search'];
tests.push('checkout');
console.log(tests);
const last = tests.pop();
console.log('Removed:', last, '| Now:', tests);

Output

[ 'login', 'search', 'checkout' ]
Removed: checkout | Now: [ 'login', 'search' ]

Explanation: push adds to the end and pop removes and returns the last item.

3. Add and remove at the startBeginner

Use unshift and shift on a queue of jobs.

Show solution
const queue = ['build', 'test'];
queue.unshift('install');
console.log(queue);
const first = queue.shift();
console.log('Processing:', first, '| Remaining:', queue);

Output

[ 'install', 'build', 'test' ]
Processing: install | Remaining: [ 'build', 'test' ]

Explanation: unshift adds to the front and shift removes the first item, which is how a queue works.

4. Check if an item existsBeginner

Check whether 'firefox' and 'edge' are in the browser list.

Show solution
const browsers = ['chromium', 'firefox', 'webkit'];
console.log(browsers.includes('firefox'));
console.log(browsers.includes('edge'));
console.log(browsers.indexOf('webkit'));

Output

true
false
2

Explanation: includes returns a boolean. indexOf returns the position, or -1 if missing.

5. Double every number with mapBeginner

Double each value in [1, 2, 3, 4].

Show solution
const values = [1, 2, 3, 4];
const doubled = values.map(v => v * 2);
console.log(doubled);
console.log(values);

Output

[ 2, 4, 6, 8 ]
[ 1, 2, 3, 4 ]

Explanation: map returns a new array with each item transformed. The original array is unchanged.

6. Filter even numbersBeginner

Keep only the even numbers from [3, 8, 12, 5, 6].

Show solution
const evens = [3, 8, 12, 5, 6].filter(n => n % 2 === 0);
console.log(evens);

Output

[ 8, 12, 6 ]

Explanation: filter keeps items for which the callback returns true.

7. Sum with reduceBeginner

Add up [120, 80, 45, 55].

Show solution
const total = [120, 80, 45, 55].reduce((sum, n) => sum + n, 0);
console.log(total);

Output

300

Explanation: reduce combines all items into one value, starting from the initial value 0.

8. Join an array into a stringBeginner

Join tags with ', ' and with ' | '.

Show solution
const tags = ['smoke', 'regression', 'api'];
console.log(tags.join(', '));
console.log(tags.join(' | '));

Output

smoke, regression, api
smoke | regression | api

Explanation: join turns an array into a string with your chosen separator.

9. Slice part of an arrayBeginner

Take the first two and the last two items from a list.

Show solution
const steps = ['open', 'login', 'search', 'add to cart', 'checkout'];
console.log(steps.slice(0, 2));
console.log(steps.slice(-2));

Output

[ 'open', 'login' ]
[ 'add to cart', 'checkout' ]

Explanation: slice returns a copy of part of the array without changing the original.

10. Loop with forEachBeginner

Print each item with its index using forEach.

Show solution
const steps = ['open page', 'fill form', 'submit'];
steps.forEach((step, index) => console.log(`${index + 1}. ${step}`));

Output

1. open page
2. fill form
3. submit

Explanation: forEach runs a callback for every item. Use for...of instead when you need await or break.

JavaScript Intermediate Programs

11. Find an itemIntermediate

Find the first user with the role 'admin'.

Show solution
const users = [
  { name: 'Asha', role: 'viewer' },
  { name: 'Ravi', role: 'admin' },
  { name: 'Meera', role: 'admin' }
];
const admin = users.find(u => u.role === 'admin');
console.log(admin.name);
console.log(users.findIndex(u => u.role === 'admin'));

Output

Ravi
1

Explanation: find returns the first matching item and findIndex returns its position.

12. some and everyIntermediate

Check whether some tests failed and whether every test passed.

Show solution
const results = ['passed', 'passed', 'failed'];
console.log('Any failed:', results.some(r => r === 'failed'));
console.log('All passed:', results.every(r => r === 'passed'));

Output

Any failed: true
All passed: false

Explanation: some needs one match, every needs all items to match. Both stop early when the answer is known.

13. Sort numbers correctlyIntermediate

Sort [10, 1, 25, 3] ascending and descending.

Show solution
const values = [10, 1, 25, 3];
console.log([...values].sort());
console.log([...values].sort((a, b) => a - b));
console.log([...values].sort((a, b) => b - a));

Output

[ 1, 10, 25, 3 ]
[ 1, 3, 10, 25 ]
[ 25, 10, 3, 1 ]

Explanation: the default sort compares as strings, so 10 comes before 3. Pass a compare function for numbers. Spreading copies first, because sort changes the array in place.

14. Sort objects by a propertyIntermediate

Sort products by price, then print their names.

Show solution
const products = [
  { name: 'Mouse', price: 599 },
  { name: 'Keyboard', price: 1499 },
  { name: 'Cable', price: 199 }
];
const byPrice = [...products].sort((a, b) => a.price - b.price);
console.log(byPrice.map(p => p.name).join(' < '));

Output

Cable < Mouse < Keyboard

Explanation: the compare function reads the property you want to sort by.

15. Remove duplicatesIntermediate

Remove duplicate tags from ['ui', 'api', 'ui', 'smoke', 'api'].

Show solution
const tags = ['ui', 'api', 'ui', 'smoke', 'api'];
const unique = [...new Set(tags)];
console.log(unique);

Output

[ 'ui', 'api', 'smoke' ]

Explanation: a Set keeps only unique values. Spreading it back gives an array.

16. Flatten nested arraysIntermediate

Flatten [[1, 2], [3, [4, 5]]] one level and fully.

Show solution
const nested = [[1, 2], [3, [4, 5]]];
console.log(nested.flat());
console.log(nested.flat(Infinity));

Output

[ 1, 2, 3, [ 4, 5 ] ]
[ 1, 2, 3, 4, 5 ]

Explanation: flat(depth) removes nesting. Infinity flattens every level.

17. Chain map, filter and reduceIntermediate

Calculate the total price of in-stock items only.

Show solution
const items = [
  { name: 'Pen', price: 20, inStock: true },
  { name: 'Book', price: 350, inStock: false },
  { name: 'Bag', price: 899, inStock: true }
];
const total = items
  .filter(i => i.inStock)
  .map(i => i.price)
  .reduce((sum, p) => sum + p, 0);
console.log(total);

Output

919

Explanation: chaining small steps keeps each transformation simple and readable.

18. splice to insert and removeIntermediate

Insert 'verify email' at position 2 and remove 'logout'.

Show solution
const steps = ['register', 'login', 'logout', 'delete account'];
steps.splice(2, 0, 'verify email');
console.log(steps);
steps.splice(steps.indexOf('logout'), 1);
console.log(steps);

Output

[ 'register', 'login', 'verify email', 'logout', 'delete account' ]
[ 'register', 'login', 'verify email', 'delete account' ]

Explanation: splice(start, deleteCount, ...items) changes the array in place. It can remove and insert in one call.

19. Array destructuringIntermediate

Unpack the first item, the second item and the rest from a results array.

Show solution
const scores = [98, 87, 75, 60, 42];
const [top, second, ...others] = scores;
console.log(top, second);
console.log(others);

Output

98 87
[ 75, 60, 42 ]

Explanation: destructuring assigns array items to variables by position, and ...rest collects the remainder.

20. Group items by a keyIntermediate

Group tests by their status.

Show solution
const tests = [
  { id: 1, status: 'passed' }, { id: 2, status: 'failed' },
  { id: 3, status: 'passed' }, { id: 4, status: 'skipped' }
];
const groups = tests.reduce((acc, t) => {
  (acc[t.status] ||= []).push(t.id);
  return acc;
}, {});
console.log(groups);

Output

{ passed: [ 1, 3 ], failed: [ 2 ], skipped: [ 4 ] }

Explanation: reduce builds an object whose keys are statuses and whose values are arrays of IDs.

JavaScript Real-World QA Scenario Programs

21. Verify a list is sortedReal-world

Check whether prices read from the UI are sorted from low to high.

Show solution
const uiPrices = [199, 599, 599, 1499];
const isSorted = uiPrices.every((p, i) => i === 0 || uiPrices[i - 1] <= p);
console.log('Sorted ascending:', isSorted);
const expected = [...uiPrices].sort((a, b) => a - b);
console.log(JSON.stringify(uiPrices) === JSON.stringify(expected));

Output

Sorted ascending: true
true

Explanation: two common checks are comparing neighbours with every, or comparing with a sorted copy.

22. Compare expected and actual menu itemsReal-world

Find items that are missing from the UI and unexpected extras.

Show solution
const expected = ['Home', 'Shop', 'Blog', 'Contact'];
const actual = ['Home', 'Shop', 'Offers', 'Contact'];
const missing = expected.filter(x => !actual.includes(x));
const extra = actual.filter(x => !expected.includes(x));
console.log('Missing:', missing);
console.log('Extra:', extra);

Output

Missing: [ 'Blog' ]
Extra: [ 'Offers' ]

Explanation: two filters give a clear diff, which is easier to debug than a single equality check.

23. Count failed tests per browserReal-world

Count failures for each browser from a list of results.

Show solution
const results = [
  { browser: 'chromium', status: 'failed' },
  { browser: 'firefox', status: 'passed' },
  { browser: 'webkit', status: 'failed' },
  { browser: 'chromium', status: 'failed' }
];
const failures = {};
for (const r of results.filter(r => r.status === 'failed')) {
  failures[r.browser] = (failures[r.browser] || 0) + 1;
}
console.log(failures);

Output

{ chromium: 2, webkit: 1 }

Explanation: filter first, then count, keeping each step simple.

24. Top 3 slowest testsReal-world

Find the three slowest tests by duration.

Show solution
const durations = [
  { name: 'login', ms: 1200 }, { name: 'search', ms: 4300 },
  { name: 'checkout', ms: 8700 }, { name: 'profile', ms: 900 }, { name: 'cart', ms: 5100 }
];
const slowest = [...durations].sort((a, b) => b.ms - a.ms).slice(0, 3);
for (const t of slowest) console.log(`${t.name}: ${t.ms} ms`);

Output

checkout: 8700 ms
cart: 5100 ms
search: 4300 ms

Explanation: sort a copy in descending order, then slice the first three.

25. Paginate an arrayReal-world

Split 7 items into pages of 3 and print page 3.

Show solution
const items = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
const pageSize = 3;
const page = n => items.slice((n - 1) * pageSize, n * pageSize);
console.log(page(1));
console.log(page(3));
console.log('Pages:', Math.ceil(items.length / pageSize));

Output

[ 'a', 'b', 'c' ]
[ 'g' ]
Pages: 3

Explanation: slice with start and end indexes calculated from the page number gives each page.

26. Chunk test cases for parallel workersReal-world

Split 10 test IDs into chunks for 3 workers.

Show solution
const tests = Array.from({ length: 10 }, (_, i) => `TC${i + 1}`);
const workers = 3;
const chunks = Array.from({ length: workers }, () => []);
tests.forEach((t, i) => chunks[i % workers].push(t));
chunks.forEach((c, i) => console.log(`Worker ${i + 1}: ${c.join(', ')}`));

Output

Worker 1: TC1, TC4, TC7, TC10
Worker 2: TC2, TC5, TC8
Worker 3: TC3, TC6, TC9

Explanation: distributing with i % workers balances the work, similar to how test runners spread tests.

27. Build a lookup from an arrayReal-world

Turn an array of users into an object keyed by ID.

Show solution
const users = [{ id: 7, name: 'Asha' }, { id: 9, name: 'Ravi' }];
const byId = Object.fromEntries(users.map(u => [u.id, u.name]));
console.log(byId);
console.log(byId[9]);

Output

{ '7': 'Asha', '9': 'Ravi' }
Ravi

Explanation: a lookup object gives fast access by key instead of searching the array each time.

28. Table data: average of a columnReal-world

Calculate the average price from table rows read as text.

Show solution
const rows = [
  ['Mouse', '₹599'],
  ['Keyboard', '₹1,499'],
  ['Cable', '₹199']
];
const prices = rows.map(([, price]) => Number(price.replace(/[^\d.]/g, '')));
const avg = prices.reduce((a, b) => a + b, 0) / prices.length;
console.log(prices);
console.log('Average:', avg.toFixed(2));

Output

[ 599, 1499, 199 ]
Average: 765.67

Explanation: map each row to a clean number, then reduce to the average. The leading comma in [, price] skips the first column.

29. Merge two result lists without duplicatesReal-world

Combine test IDs from two runs and keep each ID once, sorted.

Show solution
const run1 = ['TC3', 'TC1', 'TC5'];
const run2 = ['TC2', 'TC5', 'TC1'];
const all = [...new Set([...run1, ...run2])].sort();
console.log(all);

Output

[ 'TC1', 'TC2', 'TC3', 'TC5' ]

Explanation: spread both arrays into a Set to remove duplicates, then sort the result.

30. Find newly failing testsReal-world

Compare yesterday’s and today’s failures to find new ones.

Show solution
const yesterday = ['TC2', 'TC8'];
const today = ['TC2', 'TC5', 'TC9'];
const newFailures = today.filter(t => !yesterday.includes(t));
const fixed = yesterday.filter(t => !today.includes(t));
console.log('New failures:', newFailures.join(', '));
console.log('Fixed:', fixed.join(', '));

Output

New failures: TC5, TC9
Fixed: TC8

Explanation: set differences highlight regressions and fixes between runs.

31. Check all form fields are filledReal-world

Report which required fields are empty.

Show solution
const form = [
  { field: 'name', value: 'Asha' },
  { field: 'email', value: '' },
  { field: 'phone', value: '9876543210' },
  { field: 'city', value: '  ' }
];
const empty = form.filter(f => f.value.trim() === '').map(f => f.field);
console.log(empty.length ? `Empty: ${empty.join(', ')}` : 'All fields filled');

Output

Empty: email, city

Explanation: trimming first treats spaces-only values as empty, which matches most form validation rules.

32. Run async steps in orderReal-world

Run three async steps one after another and collect their results in order.

Show solution
const step = (name, ms) => new Promise(resolve => setTimeout(() => resolve(`${name} done`), ms));
const plan = [['login', 30], ['search', 10], ['logout', 20]];
const log = [];
for (const [name, ms] of plan) log.push(await step(name, ms));
console.log(log);

Output

[ 'login done', 'search done', 'logout done' ]

Explanation: for...of with await keeps the order, even though search is the fastest step. forEach would not wait.

33. Run independent requests in parallelReal-world

Run three independent async lookups in parallel with Promise.all and keep the input order.

Show solution
const fetchPrice = (sku, ms) => new Promise(resolve => setTimeout(() => resolve({ sku, price: ms * 10 }), ms));
const results = await Promise.all([fetchPrice('A1', 30), fetchPrice('B2', 10), fetchPrice('C3', 20)]);
console.log(results.map(r => `${r.sku}:${r.price}`).join(' '));

Output

A1:300 B2:100 C3:200

Explanation: Promise.all runs the work at the same time but returns results in the same order as the input array.

Keep Practising

JavaScript String Practice Programs with Solutions (32 Programs)

Practise JavaScript strings with 32 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning JavaScript for Playwright.

How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with node program.js.

New to this topic? Read the JavaScript Strings lesson first.

JavaScript Beginner Programs

1. String lengthBeginner

Print the length of 'Playwright Testing'.

Show solution
const title = 'Playwright Testing';
console.log(title.length);

Output

18

Explanation: length counts every character, including spaces.

2. Upper and lower caseBeginner

Convert 'Login Page' to upper case and lower case.

Show solution
const text = 'Login Page';
console.log(text.toUpperCase());
console.log(text.toLowerCase());

Output

LOGIN PAGE
login page

Explanation: case methods return new strings. The original string is unchanged.

3. First and last characterBeginner

Print the first and last character of 'automation'.

Show solution
const word = 'automation';
console.log(word[0]);
console.log(word.at(-1));

Output

a
n

Explanation: index 0 is the first character, and at(-1) reads from the end.

4. Trim whitespaceBeginner

Remove spaces around ' qa@example.com '.

Show solution
const raw = '   qa@example.com  ';
console.log(`[${raw.trim()}]`);
console.log(`[${raw.trimStart()}]`);

Output

[qa@example.com]
[qa@example.com  ]

Explanation: trim removes whitespace from both ends. trimStart and trimEnd remove one side only.

5. Check if a string contains textBeginner

Check whether 'Order placed successfully' contains 'success' and 'failed'.

Show solution
const message = 'Order placed successfully';
console.log(message.includes('success'));
console.log(message.includes('failed'));

Output

true
false

Explanation: includes returns true or false. It is case-sensitive.

6. startsWith and endsWithBeginner

Check whether a URL starts with https:// and ends with /login.

Show solution
const url = 'https://qa.shop.test/login';
console.log(url.startsWith('https://'));
console.log(url.endsWith('/login'));

Output

true
true

Explanation: these methods check the beginning or end of a string without regular expressions.

7. Extract part of a stringBeginner

Extract 'Play' and 'wright' from 'Playwright' with slice.

Show solution
const tool = 'Playwright';
console.log(tool.slice(0, 4));
console.log(tool.slice(4));
console.log(tool.slice(-6));

Output

Play
wright
wright

Explanation: slice(start, end) stops before end. Negative numbers count from the end.

8. Replace textBeginner

Replace 'staging' with 'qa' in a URL, then replace every space with a dash in a title.

Show solution
const url = 'https://staging.shop.test/staging/home';
console.log(url.replace('staging', 'qa'));
console.log(url.replaceAll('staging', 'qa'));
console.log('My First Test'.replaceAll(' ', '-'));

Output

https://qa.shop.test/staging/home
https://qa.shop.test/qa/home
My-First-Test

Explanation: replace changes only the first match. replaceAll changes every match.

9. Split a sentence into wordsBeginner

Split 'run tests in parallel' into words and count them.

Show solution
const sentence = 'run tests in parallel';
const words = sentence.split(' ');
console.log(words);
console.log('Word count:', words.length);

Output

[ 'run', 'tests', 'in', 'parallel' ]
Word count: 4

Explanation: split turns a string into an array using a separator.

10. Repeat and padBeginner

Print a divider of 20 dashes and pad test IDs to 4 digits.

Show solution
console.log('-'.repeat(20));
for (const id of [7, 42, 315]) console.log('TC-' + String(id).padStart(4, '0'));

Output

--------------------
TC-0007
TC-0042
TC-0315

Explanation: repeat duplicates a string, and padStart fills it to a fixed length.

JavaScript Intermediate Programs

11. Reverse a stringIntermediate

Reverse 'Playwright'.

Show solution
const reversed = 'Playwright'.split('').reverse().join('');
console.log(reversed);

Output

thgirwyalP

Explanation: strings have no reverse, so split into characters, reverse the array and join it again.

12. Palindrome check ignoring case and spacesIntermediate

Check whether 'Never odd or even' is a palindrome.

Show solution
const text = 'Never odd or even';
const clean = text.toLowerCase().replace(/[^a-z0-9]/g, '');
const isPalindrome = clean === [...clean].reverse().join('');
console.log(clean, isPalindrome);

Output

neveroddoreven true

Explanation: normalise the text first (lower case, letters only), then compare it with its reverse.

13. Count occurrences of a characterIntermediate

Count how many times 't' appears in 'test automation tutorial'.

Show solution
const text = 'test automation tutorial';
let count = 0;
for (const ch of text) if (ch === 't') count++;
console.log(count);
console.log(text.split('t').length - 1);

Output

6
6

Explanation: you can loop over characters, or split on the character and count the pieces minus one.

14. Capitalise each wordIntermediate

Convert 'checkout with saved card' to title case.

Show solution
const title = 'checkout with saved card'
  .split(' ')
  .map(word => word[0].toUpperCase() + word.slice(1))
  .join(' ');
console.log(title);

Output

Checkout With Saved Card

Explanation: split into words, capitalise the first letter of each, and join them back.

15. Character frequency mapIntermediate

Count each character in 'banana'.

Show solution
const freq = {};
for (const ch of 'banana') freq[ch] = (freq[ch] || 0) + 1;
console.log(freq);

Output

{ b: 1, a: 3, n: 2 }

Explanation: an object works as a counter, with each character as a key.

16. Anagram checkIntermediate

Check whether 'listen' and 'silent' are anagrams.

Show solution
const normalise = s => [...s.toLowerCase()].sort().join('');
console.log(normalise('listen') === normalise('silent'));
console.log(normalise('test') === normalise('sets'));

Output

true
false

Explanation: anagrams contain the same letters, so their sorted characters match.

17. camelCase to kebab-caseIntermediate

Convert 'loginButtonText' to 'login-button-text'.

Show solution
const camel = 'loginButtonText';
const kebab = camel.replace(/([A-Z])/g, '-$1').toLowerCase();
console.log(kebab);

Output

login-button-text

Explanation: a regular expression inserts a dash before each capital letter, and lower-casing finishes the job.

18. Longest wordIntermediate

Find the longest word in 'Playwright makes cross browser testing reliable'.

Show solution
const words = 'Playwright makes cross browser testing reliable'.split(' ');
let longest = '';
for (const w of words) if (w.length > longest.length) longest = w;
console.log(longest, longest.length);

Output

Playwright 10

Explanation: track the best word while looping, replacing it only when a longer one appears.

19. Truncate long textIntermediate

Write truncate(text, max) that adds ... when text is longer than max.

Show solution
const truncate = (text, max) => (text.length > max ? text.slice(0, max - 3) + '...' : text);
console.log(truncate('Short title', 20));
console.log(truncate('A very long product name that overflows', 20));

Output

Short title
A very long produ...

Explanation: UI tests often check that long names are cut off, so knowing the exact rule helps.

20. Compare strings ignoring caseIntermediate

Compare 'Submit' and 'SUBMIT' case-insensitively in two ways.

Show solution
const a = 'Submit', b = 'SUBMIT';
console.log(a === b);
console.log(a.toLowerCase() === b.toLowerCase());
console.log(a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0);

Output

false
true
true

Explanation: normalising case, or localeCompare with a sensitivity option, ignores letter case.

JavaScript Real-World QA Scenario Programs

21. Validate an email formatReal-world

Check a list of emails against a simple pattern.

Show solution
const pattern = /^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i;
for (const email of ['qa@example.com', 'qa@example', 'qa example@test.com', 'QA@Shop.IN']) {
  console.log(email.padEnd(20), pattern.test(email) ? 'valid' : 'invalid');
}

Output

qa@example.com       valid
qa@example           invalid
qa example@test.com  invalid
QA@Shop.IN           valid

Explanation: a regular expression describes the allowed format. padEnd aligns the output in columns.

22. Extract the order numberReal-world

Extract ORD-58213 from the confirmation text.

Show solution
const confirmation = 'Thank you! Your order ORD-58213 has been placed.';
const match = confirmation.match(/ORD-\d+/);
console.log(match ? match[0] : 'Not found');

Output

ORD-58213

Explanation: match returns the matched text, so tests can reuse dynamic values shown in the UI.

23. Mask a card numberReal-world

Show only the last 4 digits of '4111 1111 1111 1234'.

Show solution
const card = '4111 1111 1111 1234';
const digits = card.replace(/\s/g, '');
const masked = '**** **** **** ' + digits.slice(-4);
console.log(masked);

Output

**** **** **** 1234

Explanation: remove spaces first so the last four characters are always digits.

24. Build a slug from a titleReal-world

Turn ' Playwright: Getting Started (2026)! ' into 'playwright-getting-started-2026'.

Show solution
const title = '  Playwright: Getting Started (2026)!  ';
const slug = title.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
console.log(slug);

Output

playwright-getting-started-2026

Explanation: replace every run of non-alphanumeric characters with one dash, then trim dashes from the ends.

25. Parse a key=value cookie stringReal-world

Turn 'session=abc123; theme=dark; lang=en' into an object.

Show solution
const cookieText = 'session=abc123; theme=dark; lang=en';
const cookies = {};
for (const part of cookieText.split('; ')) {
  const [key, value] = part.split('=');
  cookies[key] = value;
}
console.log(cookies);

Output

{ session: 'abc123', theme: 'dark', lang: 'en' }

Explanation: split into pairs, then split each pair at =.

26. Normalise whitespace in UI textReal-world

Collapse extra spaces and line breaks in ' Total:\n ₹1,299 '.

Show solution
const uiText = '  Total:\n   ₹1,299  ';
const normalised = uiText.replace(/\s+/g, ' ').trim();
console.log(normalised);

Output

Total: ₹1,299

Explanation: page text often contains line breaks and indentation. Playwright’s toHaveText normalises whitespace the same way.

27. Check the URL path and queryReal-world

Read the path and the q parameter from a search URL.

Show solution
const url = new URL('https://shop.test/search?q=wireless%20mouse&page=2');
console.log(url.pathname);
console.log(url.searchParams.get('q'));
console.log(url.searchParams.get('page'));

Output

/search
wireless mouse
2

Explanation: the URL class parses URLs and decodes values such as %20, which is safer than slicing strings.

28. Template literal reportReal-world

Build a multi-line test report with a template literal.

Show solution
const name = 'Checkout';
const passed = 18, failed = 2;
const report = `Suite: ${name}
Passed: ${passed}
Failed: ${failed}
Status: ${failed === 0 ? 'GREEN' : 'RED'}`;
console.log(report);

Output

Suite: Checkout
Passed: 18
Failed: 2
Status: RED

Explanation: template literals keep line breaks and can include expressions such as ternaries.

29. Compare expected and actual textReal-world

Report the first position where two strings differ.

Show solution
function firstDiff(a, b) {
  const len = Math.max(a.length, b.length);
  for (let i = 0; i < len; i++) if (a[i] !== b[i]) return i;
  return -1;
}
const expected = 'Welcome back, Asha';
const actual = 'Welcome back, Asha!';
const i = firstDiff(expected, actual);
console.log(i === -1 ? 'Texts match' : `Differs at index ${i}: '${expected[i] ?? ''}' vs '${actual[i] ?? ''}'`);

Output

Differs at index 18: '' vs '!'

Explanation: pinpointing the first difference makes text-assertion failures much quicker to debug.

30. Extract all hashtagsReal-world

Find all hashtags in a social post.

Show solution
const post = 'Learning #Playwright with #TypeScript for #QA automation';
const tags = post.match(/#\w+/g) || [];
console.log(tags.join(', '));
console.log(tags.length);

Output

#Playwright, #TypeScript, #QA
3

Explanation: the g flag returns every match. || [] avoids errors when nothing matches.

31. Format a name for displayReal-world

Convert ' ASHA sharma ' into 'Asha Sharma'.

Show solution
const raw = '  ASHA   sharma ';
const display = raw
  .trim()
  .split(/\s+/)
  .map(part => part[0].toUpperCase() + part.slice(1).toLowerCase())
  .join(' ');
console.log(display);

Output

Asha Sharma

Explanation: splitting on \s+ handles any number of spaces between words.

32. Count words in a reviewReal-world

Count the words in a review, ignoring extra spaces, and check a 10-word minimum.

Show solution
const review = '  Great product,   fast delivery and   very good packaging  ';
const wordCount = review.trim().split(/\s+/).length;
console.log('Words:', wordCount);
console.log(wordCount >= 10 ? 'Accepted' : 'Too short');

Output

Words: 8
Too short

Explanation: trimming first avoids empty strings at the ends being counted as words.

Keep Practising

JavaScript Function Practice Programs with Solutions (32 Programs)

Practise JavaScript functions with 32 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning JavaScript for Playwright.

How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with node program.js.

New to this topic? Read the JavaScript Functions lesson first.

JavaScript Beginner Programs

1. Function that greets a userBeginner

Write a function greet(name) that returns Hello, &lt;name&gt;! and print it for two names.

Show solution
function greet(name) {
  return `Hello, ${name}!`;
}
console.log(greet('Asha'));
console.log(greet('Ravi'));

Output

Hello, Asha!
Hello, Ravi!

Explanation: a function declaration takes parameters and uses return to send a value back to the caller.

2. Add two numbersBeginner

Write add(a, b) and print the sum of 12 and 30.

Show solution
function add(a, b) {
  return a + b;
}
const result = add(12, 30);
console.log('Result:', result);

Output

Result: 42

Explanation: the returned value can be stored in a variable or used directly.

3. Arrow functionBeginner

Rewrite a square function as an arrow function and print the squares of 4 and 9.

Show solution
const square = n => n * n;
console.log(square(4));
console.log(square(9));

Output

16
81

Explanation: arrow functions with one expression return it automatically, without return or braces.

4. Default parametersBeginner

Write openPage(url, timeout = 30000) that prints the URL and timeout, and call it with and without a timeout.

Show solution
function openPage(url, timeout = 30000) {
  console.log(`Opening ${url} (timeout ${timeout} ms)`);
}
openPage('/login');
openPage('/reports', 60000);

Output

Opening /login (timeout 30000 ms)
Opening /reports (timeout 60000 ms)

Explanation: default parameters are used when the argument is missing or undefined.

5. Function without returnBeginner

Show what a function without a return statement gives back.

Show solution
function logStep(step) {
  console.log('Step:', step);
}
const value = logStep('Click login');
console.log('Returned:', value);

Output

Step: Click login
Returned: undefined

Explanation: a function with no return returns undefined.

6. Even checkerBeginner

Write isEven(n) that returns true or false, and test it with 4 and 7.

Show solution
const isEven = n => n % 2 === 0;
console.log(isEven(4));
console.log(isEven(7));

Output

true
false

Explanation: functions that return booleans read naturally in conditions, for example if (isEven(x)).

7. Celsius to FahrenheitBeginner

Convert 0, 37 and 100 °C to Fahrenheit with a function.

Show solution
function toFahrenheit(celsius) {
  return (celsius * 9) / 5 + 32;
}
for (const c of [0, 37, 100]) console.log(`${c}°C = ${toFahrenheit(c)}°F`);

Output

0°C = 32°F
37°C = 98.6°F
100°C = 212°F

Explanation: wrapping a formula in a function makes it reusable and easy to test.

8. Function expressionBeginner

Store an anonymous function in a constant and call it.

Show solution
const multiply = function (a, b) {
  return a * b;
};
console.log(multiply(6, 7));
console.log(typeof multiply);

Output

42
function

Explanation: functions are values in JavaScript, so they can be stored in variables.

9. Rest parametersBeginner

Write sum(...numbers) that adds any number of arguments.

Show solution
function sum(...numbers) {
  let total = 0;
  for (const n of numbers) total += n;
  return total;
}
console.log(sum(1, 2));
console.log(sum(5, 10, 15, 20));

Output

3
50

Explanation: ...numbers collects all arguments into a real array.

10. Return multiple valuesBeginner

Return the min and max of a list as an object and destructure the result.

Show solution
function minMax(values) {
  return { min: Math.min(...values), max: Math.max(...values) };
}
const { min, max } = minMax([42, 7, 19, 88]);
console.log('min:', min, 'max:', max);

Output

min: 7 max: 88

Explanation: return an object or array when a function produces more than one value.

JavaScript Intermediate Programs

11. Hoisting of function declarationsIntermediate

Call a function declaration before it is defined, and compare with a function expression.

Show solution
console.log(double(5));
function double(n) { return n * 2; }
try {
  triple(5);
} catch (error) {
  console.log(error.name);
}
const triple = n => n * 3;

Output

10
ReferenceError

Explanation: function declarations are hoisted with their body. A const function is in the temporal dead zone until its line runs.

12. Callback functionIntermediate

Write runTest(name, callback) that prints the name and calls the callback with a result.

Show solution
function runTest(name, callback) {
  console.log('Running', name);
  callback('passed');
}
runTest('login', result => console.log('Result:', result));

Output

Running login
Result: passed

Explanation: a callback is a function passed to another function to be called later.

13. Higher-order function that returns a functionIntermediate

Write makeMultiplier(factor) that returns a function, and create double and triple.

Show solution
function makeMultiplier(factor) {
  return n => n * factor;
}
const double = makeMultiplier(2);
const triple = makeMultiplier(3);
console.log(double(10), triple(10));

Output

20 30

Explanation: functions can return functions. The returned function remembers factor through a closure.

14. Closure counterIntermediate

Create a counter function that keeps its own private count.

Show solution
function createCounter() {
  let count = 0;
  return () => ++count;
}
const next = createCounter();
console.log(next(), next(), next());
const other = createCounter();
console.log(other());

Output

1 2 3
1

Explanation: each call to createCounter creates a new count that only its returned function can access.

15. Recursion: factorialIntermediate

Calculate 6! with a recursive function.

Show solution
function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}
console.log(factorial(6));

Output

720

Explanation: a recursive function calls itself with a smaller input until it reaches a base case.

16. Recursion: sum nested arraysIntermediate

Sum all numbers in [1, [2, 3], [4, [5]]] with recursion.

Show solution
function deepSum(items) {
  let total = 0;
  for (const item of items) {
    total += Array.isArray(item) ? deepSum(item) : item;
  }
  return total;
}
console.log(deepSum([1, [2, 3], [4, [5]]]));

Output

15

Explanation: recursion handles data whose nesting depth is not known in advance.

17. Pure vs impure functionsIntermediate

Compare a pure function with one that changes outside state.

Show solution
let calls = 0;
const impureAdd = (a, b) => { calls++; return a + b; };
const pureAdd = (a, b) => a + b;
console.log(pureAdd(2, 3), pureAdd(2, 3));
console.log(impureAdd(2, 3), impureAdd(2, 3), 'calls:', calls);

Output

5 5
5 5 calls: 2

Explanation: a pure function always returns the same output for the same input and changes nothing outside itself, which makes it easy to test.

18. Immediately invoked functionIntermediate

Use an IIFE to compute a value without leaking variables.

Show solution
const config = (() => {
  const env = 'qa';
  const port = 8080;
  return `${env}:${port}`;
})();
console.log(config);
console.log(typeof env);

Output

qa:8080
undefined

Explanation: an IIFE runs immediately and keeps its variables private.

19. Memoize a slow functionIntermediate

Cache results of a function so repeated calls are not recalculated.

Show solution
function memoize(fn) {
  const cache = new Map();
  return n => {
    if (cache.has(n)) return `${cache.get(n)} (cached)`;
    const value = fn(n);
    cache.set(n, value);
    return value;
  };
}
const slowSquare = n => n * n;
const fastSquare = memoize(slowSquare);
console.log(fastSquare(12));
console.log(fastSquare(12));

Output

144
144 (cached)

Explanation: a closure keeps the cache between calls. Memoization trades memory for speed.

20. Arguments with destructured optionsIntermediate

Write createUser({ name, role = 'viewer', active = true }) and call it with partial options.

Show solution
function createUser({ name, role = 'viewer', active = true }) {
  return `${name} | ${role} | ${active ? 'active' : 'inactive'}`;
}
console.log(createUser({ name: 'Asha' }));
console.log(createUser({ name: 'Ravi', role: 'admin', active: false }));

Output

Asha | viewer | active
Ravi | admin | inactive

Explanation: an options object with destructuring and defaults keeps calls readable when a function has many settings.

JavaScript Real-World QA Scenario Programs

21. Reusable assertion helperReal-world

Write assertEqual(actual, expected, message) that prints PASS or FAIL.

Show solution
function assertEqual(actual, expected, message) {
  const ok = actual === expected;
  console.log(`${ok ? 'PASS' : 'FAIL'}: ${message}${ok ? '' : ` (expected ${expected}, got ${actual})`}`);
  return ok;
}
assertEqual(2 + 2, 4, 'adds numbers');
assertEqual('Login'.toLowerCase(), 'login', 'lower-cases text');
assertEqual(10 / 4, 2, 'divides numbers');

Output

PASS: adds numbers
PASS: lower-cases text
FAIL: divides numbers (expected 2, got 2.5)

Explanation: small helpers make checks consistent. Real tests use Playwright’s expect, which works the same way.

22. Build a URL helperReal-world

Write buildUrl(base, path, params) that joins a base URL, a path and query parameters.

Show solution
function buildUrl(base, path, params = {}) {
  const url = new URL(path, base);
  for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
  return url.toString();
}
console.log(buildUrl('https://shop.test', '/search', { q: 'laptop', page: 2 }));
console.log(buildUrl('https://shop.test', '/cart'));

Output

https://shop.test/search?q=laptop&page=2
https://shop.test/cart

Explanation: a helper avoids hand-building URLs, and URL takes care of encoding query values.

23. Generate unique test emailsReal-world

Write uniqueEmail(prefix, id) that returns a unique email address.

Show solution
const uniqueEmail = (prefix, id) => `${prefix}.${id}@example.com`;
for (let i = 1; i <= 3; i++) console.log(uniqueEmail('qa.signup', 1000 + i));

Output

qa.signup.1001@example.com
qa.signup.1002@example.com
qa.signup.1003@example.com

Explanation: a tiny pure function keeps test-data rules in one place.

24. Retry wrapperReal-world

Write retry(fn, attempts) that calls a function until it succeeds or attempts run out.

Show solution
function retry(fn, attempts) {
  for (let i = 1; i <= attempts; i++) {
    try {
      return fn(i);
    } catch (error) {
      console.log(`Attempt ${i} failed: ${error.message}`);
    }
  }
  throw new Error('All attempts failed');
}
const flaky = attempt => {
  if (attempt < 3) throw new Error('element not ready');
  return 'clicked';
};
console.log(retry(flaky, 5));

Output

Attempt 1 failed: element not ready
Attempt 2 failed: element not ready
clicked

Explanation: wrapping retry logic in a function means every flaky step can reuse the same behaviour.

25. Format test durationReal-world

Write formatDuration(ms) that returns values like 850 ms, 2.5 s or 1m 5s.

Show solution
function formatDuration(ms) {
  if (ms < 1000) return `${ms} ms`;
  if (ms < 60000) return `${(ms / 1000).toFixed(1)} s`;
  const minutes = Math.floor(ms / 60000);
  const seconds = Math.round((ms % 60000) / 1000);
  return `${minutes}m ${seconds}s`;
}
for (const ms of [850, 2500, 65000]) console.log(formatDuration(ms));

Output

850 ms
2.5 s
1m 5s

Explanation: one formatting function keeps reports consistent.

26. Validate form inputReal-world

Write validateSignup(data) that returns a list of error messages.

Show solution
function validateSignup({ email = '', password = '', age }) {
  const errors = [];
  if (!email.includes('@')) errors.push('Email is invalid');
  if (password.length < 8) errors.push('Password is too short');
  if (!(age >= 18)) errors.push('Must be 18 or older');
  return errors;
}
const show = errors => console.log(errors.length ? errors.join('; ') : 'No errors');
show(validateSignup({ email: 'qa@test.com', password: 'Secret123', age: 25 }));
show(validateSignup({ email: 'qa', password: '123', age: 16 }));

Output

No errors
Email is invalid; Password is too short; Must be 18 or older

Explanation: returning all errors at once lets a test assert every validation message on the form.

27. Debounce a searchReal-world

Write a debounce function so that only the last of several quick calls runs.

Show solution
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}
const search = debounce(term => console.log('Searching for:', term), 30);
search('p');
search('pl');
search('play');
await new Promise(resolve => setTimeout(resolve, 60));

Output

Searching for: play

Explanation: debouncing waits until calls stop, which is how search boxes avoid one request per keystroke.

28. Compose small functionsReal-world

Combine trim, lowercase and slugify steps into one pipeline function.

Show solution
const pipe = (...fns) => value => fns.reduce((acc, fn) => fn(acc), value);
const trim = s => s.trim();
const lower = s => s.toLowerCase();
const slugify = s => s.replace(/\s+/g, '-');
const toSlug = pipe(trim, lower, slugify);
console.log(toSlug('  Playwright Mock Test  '));

Output

playwright-mock-test

Explanation: composing small single-purpose functions builds bigger behaviour that is easy to test piece by piece.

29. Price calculator with optionsReal-world

Write calculateTotal(items, { taxRate, coupon }) and test two carts.

Show solution
function calculateTotal(items, { taxRate = 0.18, coupon = 0 } = {}) {
  const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
  const discounted = Math.max(subtotal - coupon, 0);
  return Math.round(discounted * (1 + taxRate));
}
const cart = [{ price: 500, qty: 2 }, { price: 250, qty: 1 }];
console.log(calculateTotal(cart));
console.log(calculateTotal(cart, { coupon: 250, taxRate: 0.05 }));

Output

1475
1050

Explanation: an options object with defaults keeps the function easy to call for common cases.

30. Callback for each test resultReal-world

Write runSuite(tests, onResult) that runs simple checks and calls onResult for each.

Show solution
function runSuite(tests, onResult) {
  for (const test of tests) {
    const passed = test.check();
    onResult(test.name, passed);
  }
}
const tests = [
  { name: 'title is set', check: () => 'Shop'.length > 0 },
  { name: 'cart is empty', check: () => [1].length === 0 }
];
runSuite(tests, (name, passed) => console.log(`${passed ? '✔' : '✘'} ${name}`));

Output

✔ title is set
✘ cart is empty

Explanation: passing a callback lets the caller decide how results are reported, such as in the console, a file or a dashboard.

31. Run a function only onceReal-world

Write a once function that lets a warning print only the first time.

Show solution
function once(fn) {
  let called = false;
  return (...args) => {
    if (called) return;
    called = true;
    fn(...args);
  };
}
const warnDeprecated = once(() => console.log('Warning: page.$() is discouraged, use locators'));
warnDeprecated();
warnDeprecated();
warnDeprecated();

Output

Warning: page.$() is discouraged, use locators

Explanation: a closure flag prevents the same message from flooding the logs.

32. Page-object style functionReal-world

Write a factory createLoginPage() that returns an object with methods that record actions.

Show solution
function createLoginPage() {
  const actions = [];
  return {
    fill(field, value) { actions.push(`fill ${field}=${field === 'password' ? '***' : value}`); return this; },
    submit() { actions.push('click submit'); return this; },
    history() { return actions.join(' -> '); }
  };
}
const page = createLoginPage().fill('email', 'qa@test.com').fill('password', 'secret').submit();
console.log(page.history());

Output

fill email=qa@test.com -> fill password=*** -> click submit

Explanation: returning this from each method allows chaining. This is the same idea as a page object with readable steps.

Keep Practising

JavaScript Loop Practice Programs with Solutions (32 Programs)

Practise JavaScript loops with 32 programs, each with a full solution, the exact output and a short explanation. The programs start with the basics, move to intermediate problems and finish with real-world scenarios from test automation, such as test data, prices, form inputs and test results. They are written for testers learning JavaScript for Playwright.

How to practise: read the task, write your own solution first, then open Show solution to compare. Run any program with node program.js.

New to this topic? Read the JavaScript Loops lesson first.

JavaScript Beginner Programs

1. Print 1 to 5 with a for loopBeginner

Print the numbers 1 to 5 on one line.

Show solution
let line = '';
for (let i = 1; i <= 5; i++) {
  line += i + ' ';
}
console.log(line.trim());

Output

1 2 3 4 5

Explanation: a for loop has a start (let i = 1), a condition (i &lt;= 5) and an update (i++).

2. Countdown with a while loopBeginner

Print a countdown from 5 to 1 followed by 'Go!'.

Show solution
let n = 5;
while (n > 0) {
  console.log(n);
  n--;
}
console.log('Go!');

Output

5
4
3
2
1
Go!

Explanation: while repeats as long as its condition is true. Update the variable inside, or the loop never ends.

3. Sum of 1 to 100Beginner

Add up all numbers from 1 to 100.

Show solution
let sum = 0;
for (let i = 1; i <= 100; i++) {
  sum += i;
}
console.log('Sum:', sum);

Output

Sum: 5050

Explanation: an accumulator variable collects the running total on each iteration.

4. Multiplication tableBeginner

Print the multiplication table of 7 from 1 to 10.

Show solution
for (let i = 1; i <= 10; i++) {
  console.log(`7 x ${i} = ${7 * i}`);
}

Output

7 x 1 = 7
7 x 2 = 14
7 x 3 = 21
7 x 4 = 28
7 x 5 = 35
7 x 6 = 42
7 x 7 = 49
7 x 8 = 56
7 x 9 = 63
7 x 10 = 70

Explanation: the loop counter supplies the multiplier on each pass.

5. Even numbers up to 20Beginner

Print all even numbers from 2 to 20 by stepping 2 at a time.

Show solution
const evens = [];
for (let i = 2; i <= 20; i += 2) {
  evens.push(i);
}
console.log(evens.join(', '));

Output

2, 4, 6, 8, 10, 12, 14, 16, 18, 20

Explanation: the update step can be any expression, such as i += 2.

6. Loop over an array with for…ofBeginner

Print each browser name from an array.

Show solution
const browsers = ['chromium', 'firefox', 'webkit'];
for (const browser of browsers) {
  console.log('Running on', browser);
}

Output

Running on chromium
Running on firefox
Running on webkit

Explanation: for...of gives each value directly, which is the cleanest way to read an array.

7. Index and value with entries()Beginner

Print each test name with its position, starting at 1.

Show solution
const tests = ['login', 'search', 'checkout'];
for (const [index, name] of tests.entries()) {
  console.log(`${index + 1}. ${name}`);
}

Output

1. login
2. search
3. checkout

Explanation: entries() yields [index, value] pairs that you can destructure.

8. Loop over object keys with for…inBeginner

Print every key and value of a config object.

Show solution
const config = { browser: 'chromium', retries: 2, headless: true };
for (const key in config) {
  console.log(`${key} = ${config[key]}`);
}

Output

browser = chromium
retries = 2
headless = true

Explanation: for...in iterates over an object’s keys. Use bracket notation to read each value.

9. Characters of a stringBeginner

Print each character of 'QA' and count the letters in 'Playwright'.

Show solution
for (const ch of 'QA') console.log(ch);
let count = 0;
for (const ch of 'Playwright') count++;
console.log('Letters:', count);

Output

Q
A
Letters: 10

Explanation: strings are iterable, so for...of visits each character.

10. do…while runs at least onceBeginner

Show that do...while runs its body once even when the condition is false.

Show solution
let attempts = 10;
do {
  console.log('Attempt', attempts);
  attempts++;
} while (attempts < 3);
console.log('Finished');

Output

Attempt 10
Finished

Explanation: do...while checks the condition after the body, so the body always runs at least once.

JavaScript Intermediate Programs

11. FactorialIntermediate

Calculate 5! and 10! with a loop.

Show solution
function factorial(n) {
  let result = 1;
  for (let i = 2; i <= n; i++) result *= i;
  return result;
}
console.log(factorial(5));
console.log(factorial(10));

Output

120
3628800

Explanation: multiply an accumulator by each number from 2 to n.

12. Fibonacci seriesIntermediate

Print the first 10 Fibonacci numbers.

Show solution
const fib = [0, 1];
for (let i = 2; i < 10; i++) {
  fib.push(fib[i - 1] + fib[i - 2]);
}
console.log(fib.join(' '));

Output

0 1 1 2 3 5 8 13 21 34

Explanation: each new term is the sum of the previous two.

13. Reverse a numberIntermediate

Reverse the digits of 12345 using a while loop and %.

Show solution
let num = 12345;
let reversed = 0;
while (num > 0) {
  reversed = reversed * 10 + (num % 10);
  num = Math.floor(num / 10);
}
console.log(reversed);

Output

54321

Explanation: % 10 takes the last digit and Math.floor(num / 10) removes it.

14. Prime check with breakIntermediate

Check whether 29 and 33 are prime, stopping as soon as a divisor is found.

Show solution
for (const n of [29, 33]) {
  let isPrime = n > 1;
  for (let d = 2; d * d <= n; d++) {
    if (n % d === 0) {
      isPrime = false;
      break;
    }
  }
  console.log(n, isPrime ? 'is prime' : 'is not prime');
}

Output

29 is prime
33 is not prime

Explanation: break exits the loop early. Checking divisors up to √n is enough.

15. Skip values with continueIntermediate

Print numbers 1 to 10, skipping multiples of 3.

Show solution
const result = [];
for (let i = 1; i <= 10; i++) {
  if (i % 3 === 0) continue;
  result.push(i);
}
console.log(result.join(' '));

Output

1 2 4 5 7 8 10

Explanation: continue skips the rest of the current iteration and moves to the next one.

16. Nested loops: star patternIntermediate

Print a right-angled triangle of stars with 4 rows.

Show solution
for (let row = 1; row <= 4; row++) {
  let line = '';
  for (let col = 1; col <= row; col++) line += '*';
  console.log(line);
}

Output

*
**
***
****

Explanation: the inner loop runs once per row and its length depends on the outer counter.

17. Count vowelsIntermediate

Count the vowels in 'Automation Testing'.

Show solution
const text = 'Automation Testing';
let vowels = 0;
for (const ch of text.toLowerCase()) {
  if ('aeiou'.includes(ch)) vowels++;
}
console.log('Vowels:', vowels);

Output

Vowels: 8

Explanation: loop over each character and count the ones that match a condition.

18. Find the maximum without Math.maxIntermediate

Find the largest value in [34, 78, 12, 99, 45] with a loop.

Show solution
const values = [34, 78, 12, 99, 45];
let max = values[0];
for (const v of values) {
  if (v > max) max = v;
}
console.log('Max:', max);

Output

Max: 99

Explanation: keep the best value seen so far and replace it whenever a larger one appears.

19. Labeled break out of nested loopsIntermediate

Search a grid for the value 7 and stop both loops when it is found.

Show solution
const grid = [[1, 2, 3], [4, 7, 6], [7, 8, 9]];
outer: for (let r = 0; r < grid.length; r++) {
  for (let c = 0; c < grid[r].length; c++) {
    if (grid[r][c] === 7) {
      console.log(`Found 7 at row ${r}, col ${c}`);
      break outer;
    }
  }
}

Output

Found 7 at row 1, col 1

Explanation: a label lets break exit the outer loop, not just the inner one.

20. Palindrome checkIntermediate

Check whether 'level' and 'test' are palindromes using two pointers.

Show solution
for (const word of ['level', 'test']) {
  let isPalindrome = true;
  for (let i = 0, j = word.length - 1; i < j; i++, j--) {
    if (word[i] !== word[j]) {
      isPalindrome = false;
      break;
    }
  }
  console.log(word, isPalindrome);
}

Output

level true
test false

Explanation: compare characters from both ends moving inwards. A for loop can update two variables at once.

JavaScript Real-World QA Scenario Programs

21. Run tests in every browserReal-world

Simulate running three tests in three browsers and print each combination.

Show solution
const browsers = ['chromium', 'firefox', 'webkit'];
const tests = ['login', 'search', 'checkout'];
let runs = 0;
for (const browser of browsers) {
  for (const test of tests) {
    runs++;
  }
}
console.log(`${tests.length} tests x ${browsers.length} browsers = ${runs} runs`);

Output

3 tests x 3 browsers = 9 runs

Explanation: nested loops visit every combination, just like Playwright projects multiply test runs.

22. Retry until successReal-world

Retry a flaky action up to 3 times, where it succeeds on the 3rd attempt.

Show solution
const MAX = 3;
let attempt = 0;
let success = false;
while (attempt < MAX && !success) {
  attempt++;
  success = attempt === 3;
  console.log(`Attempt ${attempt}: ${success ? 'passed' : 'failed'}`);
}
console.log(success ? 'Action succeeded' : 'Gave up');

Output

Attempt 1: failed
Attempt 2: failed
Attempt 3: passed
Action succeeded

Explanation: the loop stops when it succeeds or runs out of attempts, whichever comes first.

23. Poll for a status changeReal-world

Poll a job status list until it reports 'done', with a maximum of 5 polls.

Show solution
const statuses = ['queued', 'running', 'running', 'done', 'done'];
let polls = 0;
let status;
do {
  status = statuses[polls];
  polls++;
  console.log(`Poll ${polls}: ${status}`);
} while (status !== 'done' && polls < 5);

Output

Poll 1: queued
Poll 2: running
Poll 3: running
Poll 4: done

Explanation: polling loops need both a success condition and a limit, so tests never hang forever.

24. Summarise test resultsReal-world

Count passed, failed and skipped results in one loop.

Show solution
const results = ['passed', 'failed', 'passed', 'skipped', 'passed', 'failed'];
const summary = { passed: 0, failed: 0, skipped: 0 };
for (const r of results) summary[r]++;
console.log(summary);

Output

{ passed: 3, failed: 2, skipped: 1 }

Explanation: use each status as a key in a counter object and increment it inside the loop.

25. Find the first failed testReal-world

Stop at the first failed test and print its name and position.

Show solution
const runs = [
  { name: 'login', status: 'passed' },
  { name: 'search', status: 'failed' },
  { name: 'checkout', status: 'failed' }
];
for (let i = 0; i < runs.length; i++) {
  if (runs[i].status === 'failed') {
    console.log(`First failure: ${runs[i].name} (#${i + 1})`);
    break;
  }
}

Output

First failure: search (#2)

Explanation: break stops as soon as the first match is found, so later items are not checked.

26. Total a shopping cartReal-world

Loop over cart items and calculate the total price.

Show solution
const cart = [
  { item: 'Keyboard', price: 1499, qty: 1 },
  { item: 'Mouse', price: 599, qty: 2 },
  { item: 'Cable', price: 199, qty: 3 }
];
let total = 0;
for (const { price, qty } of cart) total += price * qty;
console.log('Cart total:', total);

Output

Cart total: 3294

Explanation: destructuring in the for...of header reads just the fields the calculation needs.

27. Generate test usersReal-world

Generate 3 test users with numbered usernames and emails.

Show solution
const users = [];
for (let i = 1; i <= 3; i++) {
  users.push({ username: `qa_user${i}`, email: `qa_user${i}@example.com` });
}
for (const u of users) console.log(u.username, u.email);

Output

qa_user1 qa_user1@example.com
qa_user2 qa_user2@example.com
qa_user3 qa_user3@example.com

Explanation: a counter loop is a simple way to build numbered test data.

28. Validate every emailReal-world

Check each email in a list and print the invalid ones.

Show solution
const emails = ['a@test.com', 'bad-email', 'qa@shop.in', 'no@domain'];
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
for (const email of emails) {
  if (!pattern.test(email)) console.log('Invalid:', email);
}
console.log('Checked', emails.length, 'emails');

Output

Invalid: bad-email
Invalid: no@domain
Checked 4 emails

Explanation: a loop applies the same validation to every item and reports only the failures.

29. Paginate through resultsReal-world

Read pages of results until a page is empty, counting the total items.

Show solution
const pages = [[1, 2, 3], [4, 5, 6], [7], []];
let page = 0;
let total = 0;
while (pages[page].length > 0) {
  total += pages[page].length;
  page++;
}
console.log(`Read ${page} pages, ${total} items`);

Output

Read 3 pages, 7 items

Explanation: pagination loops continue until the server returns an empty page.

30. Compare two listsReal-world

Find expected menu items missing from the actual menu.

Show solution
const expected = ['Home', 'Products', 'About', 'Contact'];
const actual = ['Home', 'About', 'Blog'];
const missing = [];
for (const item of expected) {
  if (!actual.includes(item)) missing.push(item);
}
console.log('Missing:', missing.join(', '));

Output

Missing: Products, Contact

Explanation: loop over one list and check membership in the other to find differences.

31. Table rows to objectsReal-world

Convert table rows (arrays) into objects using a header row.

Show solution
const header = ['id', 'name', 'status'];
const rows = [['1', 'login', 'pass'], ['2', 'search', 'fail']];
for (const row of rows) {
  const record = {};
  for (let i = 0; i < header.length; i++) record[header[i]] = row[i];
  console.log(record);
}

Output

{ id: '1', name: 'login', status: 'pass' }
{ id: '2', name: 'search', status: 'fail' }

Explanation: the inner loop pairs each header with the value at the same position, which is how web tables are usually read.

32. Wait with async delays in a loopReal-world

Simulate three steps that each wait 20 ms, running one after another with for...of and await.

Show solution
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const steps = ['open page', 'fill form', 'submit'];
for (const step of steps) {
  await sleep(20);
  console.log('Done:', step);
}
console.log('All steps finished in order');

Output

Done: open page
Done: fill form
Done: submit
All steps finished in order

Explanation: for...of with await runs async steps sequentially, which is how Playwright actions run. forEach would not wait. (Top-level await works in ES modules, such as .mjs files or "type": "module".)

Keep Practising