GitHub Actions Hands-on Practice Projects

1. Hello World Workflow

A minimal workflow to test trigger mechanics, job execution, and basic logging in GitHub Actions.

YAML

name: Hello World

on:
  push:
    branches: [ "main" ]
  workflow_dispatch:

jobs:
  say-hello:
    runs-on: ubuntu-latest
    steps:
      - name: Print Welcome Message
        run: echo "Hello World from GitHub Actions!"
      
      - name: Display Runner Details
        run: |
          echo "Triggered event: ${{ github.event_name }}"
          echo "Running on branch: ${{ github.ref_name }}"
          echo "Executed by: ${{ github.actor }}"

2. Node.js Build Pipeline

Automates dependencies installation, build scripts execution, and unit tests using Node.js with package manager caching.

YAML

name: Node.js CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint --if-present

      - name: Build application
        run: npm run build --if-present

      - name: Execute unit tests
        run: npm test

3. Python CI Pipeline

Runs static analysis, dependency installation, and pytest execution for Python projects.

YAML

name: Python CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
          pip install pytest flake8

      - name: Lint with flake8
        run: |
          # stop the build if there are Python syntax errors or undefined names
          flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
          # exit-zero treats all errors as warnings.
          flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

      - name: Run Pytest
        run: pytest

4. Java Maven Pipeline

Sets up OpenJDK, caches local Maven dependencies, builds the project, and runs integration tests.

YAML

name: Java CI with Maven

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: maven

      - name: Build with Maven
        run: mvn -B package --file pom.xml

      - name: Run Tests
        run: mvn test

5. Playwright Automation Workflow

Installs system dependencies and browser binaries required to execute Playwright end-to-end tests headless in CI.

YAML

name: Playwright Tests

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  playwright-tests:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright Browsers & OS Dependencies
        run: npx playwright install --with-deps

      - name: Run Playwright tests
        run: npx playwright test

      - name: Upload Playwright Report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

6. Selenium Pytest Workflow

Executes Selenium browser automation tests using headless Chrome in a Python environment.

YAML

name: Selenium Pytest Pipeline

on:
  push:
    branches: [ "main" ]

jobs:
  selenium-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'
          cache: 'pip'

      - name: Install Dependencies
        run: |
          pip install --upgrade pip
          pip install selenium pytest pytest-xdist

      - name: Run Selenium Tests (Headless Chrome)
        run: pytest tests/ui/ --headless

7. Parallel Browser Execution with a Matrix Strategy

Spins up isolated, parallel runners for multiple browser engine combinations simultaneously to accelerate test execution.

YAML

name: Cross-Browser Matrix Execution

on:
  push:
    branches: [ "main" ]

jobs:
  matrix-test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        browser: [chromium, firefox, webkit]
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Install Matrix Browser (${{ matrix.browser }})
        run: npx playwright install ${{ matrix.browser }} --with-deps

      - name: Run Tests on ${{ matrix.browser }}
        run: npx playwright test --project=${{ matrix.browser }}

8. Scheduled Nightly Test Execution

Uses cron scheduling syntax to trigger automated regression suites at a designated time without manual intervention.

YAML

name: Nightly Regression Suite

on:
  schedule:
    # Runs at 00:00 UTC every day
    - cron: '0 0 * * *'
  workflow_dispatch: # Allows manual trigger if needed

jobs:
  nightly-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install Dependencies
        run: npm ci

      - name: Execute Nightly Regression
        run: npm run test:regression

9. Manual Workflow with User Inputs

Presents interactive inputs in the GitHub UI for standard testing parameters like target environment, target browser, and debug flags.

YAML

name: Manual Test Trigger

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target Deployment Environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - dev
          - qa
          - staging
          - production
      browser:
        description: 'Browser Choice'
        required: true
        default: 'chrome'
        type: choice
        options:
          - chrome
          - firefox
          - safari
      headless:
        description: 'Run in Headless Mode?'
        required: true
        type: boolean
        default: true

jobs:
  run-custom-test:
    runs-on: ubuntu-latest
    steps:
      - name: Print User Parameters
        run: |
          echo "Running tests against: ${{ inputs.environment }}"
          echo "Selected Browser: ${{ inputs.browser }}"
          echo "Headless mode: ${{ inputs.headless }}"

      - name: Checkout code
        uses: actions/checkout@v4

      - name: Run Suite
        run: |
          echo "Executing test CLI commands with flags..."
          # Example CLI invocation:
          # npm test -- --env=${{ inputs.environment }} --browser=${{ inputs.browser }} --headless=${{ inputs.headless }}

10. Upload HTML and Allure Reports

Executes tests, generates report output, and attaches compiled HTML and Allure report assets to the workflow summary using if: always().

YAML

name: Test Suite with Allure Reporting

on:
  push:
    branches: [ "main" ]

jobs:
  test-and-report:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install Dependencies
        run: |
          pip install pytest allure-pytest

      - name: Run Tests & Generate Allure Results
        run: pytest --alluredir=allure-results
        continue-on-error: true

      - name: Install Allure CLI & Generate HTML Report
        run: |
          sudo wget https://github.com/allure-framework/allure2/releases/download/2.24.0/allure-2.24.0.tgz
          sudo tar -zxvf allure-2.24.0.tgz -C /opt/
          sudo ln -s /opt/allure-2.24.0/bin/allure /usr/bin/allure
          allure generate allure-results --clean -o allure-report

      - name: Upload Allure HTML Report Artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: allure-html-report
          path: allure-report/
          retention-days: 14

11. Reusable Workflow for Common Test Execution

Defines a modular, parameterized reusable workflow (workflow_call) and demonstrates how a caller workflow invokes it across repositories or jobs.

Standard Reusable Workflow (.github/workflows/reusable-test.yml)

YAML

name: Shared Test Runner

on:
  workflow_call:
    inputs:
      test_suite:
        required: true
        type: string
      node_version:
        required: false
        type: string
        default: '20'
    secrets:
      API_TOKEN:
        required: true

jobs:
  execute-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node_version }}
      - run: npm ci
      - name: Run Test Suite
        env:
          API_TOKEN: ${{ secrets.API_TOKEN }}
        run: npm run test:${{ inputs.test_suite }}

Caller Workflow (.github/workflows/main-pipeline.yml)

YAML

name: Main Pipeline

on:
  push:
    branches: [ "main" ]

jobs:
  run-smoke-tests:
    uses: ./.github/workflows/reusable-test.yml
    with:
      test_suite: 'smoke'
      node_version: '20'
    secrets:
      API_TOKEN: ${{ secrets.SMOKE_API_TOKEN }}

12. Docker Build and Publish Pipeline

Authenticates to Docker Hub (or GHCR), configures Docker Buildx, and pushes a multi-arch container image.

YAML

name: Docker Build and Publish

on:
  push:
    tags:
      - 'v*.*.*'
    branches:
      - 'main'

jobs:
  docker-build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Extract Metadata (Tags, Labels)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ secrets.DOCKERHUB_USERNAME }}/my-app

      - name: Build and Push Docker Image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

13. Deploy to AWS EC2

Connects to AWS using OpenID Connect (OIDC) and deploys updated application builds to an EC2 target via SSH.

YAML

name: Deploy to AWS EC2

on:
  push:
    branches: [ "main" ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4

      - name: Configure AWS Credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsEC2DeployRole
          aws-region: us-east-1

      - name: Deploy to EC2 via SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ec2-user
          key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/myapp
            git pull origin main
            npm install --production
            pm2 restart all

14. Trigger One Workflow from Another

Uses the workflow_run event trigger to execute a downstream automation workflow automatically after an upstream workflow finishes.

Downstream Workflow (.github/workflows/deploy-on-ci-success.yml)

YAML

name: Deployment Triggered by CI Success

on:
  workflow_run:
    workflows: ["Node.js CI"]  # Name of upstream workflow
    types:
      - completed

jobs:
  on-success:
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    steps:
      - name: Run Downstream Task
        run: echo "Upstream CI succeeded! Starting deployment process..."
        
  on-failure:
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'failure' }}
    steps:
      - name: Notify Team of Failure
        run: echo "Upstream CI failed. Aborting downstream triggers."

15. Multi-Environment Deployment (Dev → QA → Staging → Production)

Establishes sequential environment promotion chains using needs conditions, protected environments, and manual gating rules.

YAML

name: Multi-Environment Promotion Pipeline

on:
  push:
    branches: [ "main" ]

jobs:
  deploy-dev:
    name: Deploy to Development
    runs-on: ubuntu-latest
    environment: dev
    steps:
      - uses: actions/checkout@v4
      - name: Run Dev Deployment
        run: echo "Deploying build commit ${{ github.sha }} to DEV environment"

  deploy-qa:
    name: Deploy to QA
    needs: deploy-dev
    runs-on: ubuntu-latest
    environment: qa
    steps:
      - uses: actions/checkout@v4
      - name: Run QA Deployment
        run: echo "Deploying build commit ${{ github.sha }} to QA environment"

  deploy-staging:
    name: Deploy to Staging
    needs: deploy-qa
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - name: Run Staging Deployment
        run: echo "Deploying build commit ${{ github.sha }} to STAGING environment"

  deploy-production:
    name: Deploy to Production
    needs: deploy-staging
    runs-on: ubuntu-latest
    # Requires manual reviewer approval set up under GitHub Environment rules
    environment: production
    steps:
      - uses: actions/checkout@v4
      - name: Run Production Deployment
        run: echo "Deploying build commit ${{ github.sha }} to PRODUCTION environment"

Github Fundamentals Intermediate

1. What is Matrix Strategy?

A matrix strategy lets you run a single job multiple times using variations of inputs (like different OSs, runtime versions, or build flags). GitHub Actions automatically creates a parallel job for every possible combination of matrix variables.

YAML

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm test

This example runs 6 parallel jobs ($2 \text{ OSs} \times 3 \text{ Node versions}$).

2. Explain Caching

Caching stores dependencies (such as node_modules, Maven packages, or Docker layers) across workflow runs. Because runner VMs are completely wiped after every job, downloading dependencies repeatedly wastes time and bandwidth.

You can use actions/cache@v4 or built-in caching options inside setup actions (like actions/setup-node).

YAML

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # Built-in caching for NPM dependencies
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci

When a cache key matches, the runner downloads the pre-packaged dependencies instead of reinstalling them from the internet.

3. How Do You Upload Artifacts?

Artifacts are files or directories generated during a job (e.g., compiled binaries, test coverage logs, APKs) that you want to save after the runner VM is destroyed.

You use the actions/upload-artifact@v4 action to attach files to the workflow run.

YAML

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run build
      - name: Save build output
        uses: actions/upload-artifact@v4
        with:
          name: web-dist
          path: dist/

Uploaded artifacts can be downloaded manually from the GitHub UI or programmatically by other jobs.

4. How Do Jobs Communicate?

Because every job executes in a completely isolated virtual machine, they do not share filesystem memory or local variables directly. Jobs communicate in two primary ways:

Method A: Job Outputs (For Small Data / Strings)

Pass scalar values (like commit IDs, version numbers, or status flags) via $GITHUB_OUTPUT.

YAML

jobs:
  generator:
    runs-on: ubuntu-latest
    outputs:
      app_version: ${{ steps.set_version.outputs.version }}
    steps:
      - id: set_version
        run: echo "version=1.2.3" >> $GITHUB_OUTPUT

  receiver:
    needs: generator
    runs-on: ubuntu-latest
    steps:
      - run: echo "The version is ${{ needs.generator.outputs.app_version }}"

Method B: Artifacts (For Files / Directories)

Pass files between jobs using actions/upload-artifact in Job 1 and actions/download-artifact in Job 2.

5. Explain Reusable Workflows

Reusable Workflows allow you to avoid duplicating workflow code across repositories or within the same repository. Instead of copying and pasting pipeline definitions, you create a central workflow file and invoke it from caller workflows.

A reusable workflow must use the workflow_call trigger:

YAML

# .github/workflows/reusable-build.yml (Called Workflow)
name: Reusable Build Component
on:
  workflow_call:
    inputs:
      target-env:
        type: string
        required: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building for ${{ inputs.target-env }}"

Caller workflow usage:

YAML

# .github/workflows/main.yml (Caller Workflow)
jobs:
  run-reusable:
    uses: ./.github/workflows/reusable-build.yml
    with:
      target-env: 'production'

6. Difference Between uses and run

Featurerunuses
PurposeExecutes inline shell commands or scripts.Executes a pre-packaged Action or Reusable Workflow.
SourceRuns directly on the runner shell (Bash, PowerShell, zsh).Fetched from GitHub Marketplace, local files, or public repositories.
Examplerun: npm testuses: actions/checkout@v4

7. What is needs?

By default, jobs in a workflow execute in parallel. The needs keyword defines dependency requirements to force sequential execution or form an execution chain (DAG).

YAML

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm test

  deploy:
    needs: test # Waits for 'test' job to complete successfully
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh

If test fails, deploy is automatically skipped.

8. How Do You Trigger Workflows Conditionally?

You can run workflows, jobs, or individual steps conditionally using if expressions or trigger filters:

Method A: Step or Job Level if Condition

YAML

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to Prod
        # Runs only if the event is a push to main branch
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: ./deploy-prod.sh

Method B: Event Path/Branch Filtering (on)

YAML

on:
  push:
    branches:
      - main
    paths:
      - 'src/**' # Triggers only if files in src/ change

9. What are Environments?

Environments describe continuous deployment targets like production, staging, or development. They allow you to attach security controls and configuration variables directly to a deployment target.

Key features of Environments:

  • Required Reviewers: Pause workflow execution until specified team members manually approve.
  • Wait Timers: Delay deployment after a trigger (e.g., wait 15 minutes before pushing).
  • Environment Secrets/Variables: Restrict deployment keys so they are only accessible when running against that specific environment.

YAML

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://myapp.com
    steps:
      - run: ./deploy.sh

10. What are Concurrency Groups?

Concurrency Groups ensure that only a single job or workflow run within a designated group executes at any given time. If a new run starts while a previous run is in progress, GitHub Actions can automatically cancel the old run or queue the new run.

This prevents deployment race conditions or redundant builds when developers rapidly push multiple commits.

YAML

name: Deploy Pipeline
on: push

# Prevents simultaneous deployments on the same branch
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true # Cancels any previous run still processing

GitHub Actions Advanced Concept

1. What is OIDC Authentication?

OpenID Connect (OIDC) allows GitHub Actions to authenticate directly with cloud providers (AWS, Azure, GCP) using short-lived JWT tokens rather than long-lived, hardcoded credentials or API keys.

  1. The runner requests an OIDC JSON Web Token (JWT) from GitHub’s OIDC provider.
  2. The runner sends this JWT to the cloud provider (e.g., AWS Security Token Service).
  3. The cloud provider validates the signature, evaluates claims (such as repository name, branch, or environment), and exchanges the JWT for short-lived cloud credentials.

2. How Do You Deploy Securely to AWS?

Use OIDC alongside the official aws-actions/configure-aws-credentials action instead of storing AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY inside repository secrets.

YAML

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write  # Grants permission to fetch the OIDC JWT token
      contents: read
    steps:
      - uses: actions/checkout@v4
      
      - name: Authenticate with AWS via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeployRole
          aws-region: us-east-1

      - name: Deploy AWS Infrastructure
        run: aws s3 sync ./dist s3://my-production-bucket

3. How Do You Optimize Large Workflows?

  • Smart Caching: Cache dependency directories (~/.npm, ~/.m2, target folders) using build-tool-native caching options.
  • Path & Branch Filtering: Use paths and paths-ignore so workflows run only when relevant code changes.
  • Job Dependencies (needs): Fail fast by placing cheap checks (linting, static analysis) before heavy build/test jobs.
  • Matrix Filtering / Cancellation: Set fail-fast: true inside matrix strategies so remaining matrix combinations cancel immediately if one fails.
  • Concurrency Cancellation: Set cancel-in-progress: true inside concurrency blocks to terminate stale builds on new commits.

YAML

on:
  push:
    paths:
      - 'src/**'  # Ignore documentation or root README changes

4. How Do You Reuse Workflows Across Repositories?

Reference a reusable workflow stored in an external repository using the {owner}/{repo}/.github/workflows/{filename}.yml@{ref} syntax. The target repository must have its visibility set to public or shared within an organization.

YAML

# In repo: caller-org/app-repo
jobs:
  call-shared-ci:
    # References shared pipeline from security-team/shared-workflows repo
    uses: security-team/shared-workflows/.github/workflows/ci-template.yml@v2.1
    with:
      environment: 'staging'
    secrets: inherit # Automatically passes caller secrets to called workflow

5. What are Composite Actions?

A Composite Action aggregates multiple step commands or actions into a single reusable action. Unlike reusable workflows (which encompass full jobs), composite actions run as a single step within a job.

They are defined in an action.yml file and require runs.using: 'composite' and an explicit shell: directive on every run step.

YAML

# .github/actions/setup-node-and-cache/action.yml
name: 'Setup Node and Cache'
description: 'Custom setup wrapper for project standard initialization'

inputs:
  node-version:
    default: '20'

runs:
  using: 'composite'
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'npm'
    - name: Install dependencies
      shell: bash  # Required in composite actions
      run: npm ci

6. How Do You Secure Secrets?

  • Use Least Privilege: Restrict secret exposure using Environment Secrets gated behind approval environments.
  • Never Print Secrets: Avoid passing secrets directly into shell scripts where set -x or debug flags could print them to standard output.
  • Avoid PR Triggers from Forks: Workflows triggered by pull_request from fork repositories do not receive access to repository secrets by default.
  • Pass Secrets via Environment Variables: Pass secrets to scripts using env blocks rather than raw string replacement inside commands to avoid process listing injection.

YAML

steps:
  - name: Run Secure Script
    env:
      DB_PASSWORD: ${{ secrets.DB_PASSWORD }} # Passed as env var, not inline text
    run: python db_migrate.py

7. What is Least-Privilege Workflow Permission?

By default, GITHUB_TOKEN may inherit read/write permissions depending on organization settings. To enforce least privilege, set global workflow permissions to read-all or contents: read, and grant specific elevated scopes only to the jobs that require them.

YAML

name: Security Standard Pipeline
on: push

# Deny all permissions globally by default
permissions: {}

jobs:
  build_and_test:
    runs-on: ubuntu-latest
    permissions:
      contents: read # Read-only access to code
    steps:
      - uses: actions/checkout@v4
      - run: npm test

  publish_release:
    needs: build_and_test
    runs-on: ubuntu-latest
    permissions:
      contents: write # Granted write access ONLY for release generation
    steps:
      - uses: actions/checkout@v4
      - run: gh release create v1.0.0
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

8. How Do You Handle Rollbacks?

Automate rollbacks using conditional steps (if: failure()) or a dedicated rollback workflow triggered by deployment checks.

YAML

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy Release
        id: deploy_step
        run: ./deploy-app.sh

      - name: Healthcheck Target
        id: healthcheck
        run: ./verify-endpoint.sh

      - name: Trigger Automated Rollback
        if: failure() && steps.deploy_step.outcome == 'success'
        run: ./rollback-to-previous-image.sh

9. How Do You Implement Blue-Green Deployment?

Deploy the new application version to an isolated “Green” environment, perform health checks, and swap live traffic routing (e.g., DNS, Load Balancer, or ingress route) from “Blue” to “Green”.

YAML

jobs:
  blue_green_deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Green Environment
        run: ./deploy-green.sh --version ${{ github.sha }}

      - name: Run Integration Tests Against Green
        run: ./test-target.sh --url https://green.internal.myapp.com

      - name: Switch Traffic (Blue -> Green)
        run: ./switch-load-balancer.sh --target green

10. How Do You Debug Failed Workflows?

  • Enable Runner Diagnostic / Step Debug Logging: Set repository secrets ACTIONS_STEP_DEBUG = true and ACTIONS_RUNNER_DEBUG = true to produce verbose log outputs.
  • Re-run Jobs with Debug Logging Enabled: Click Re-run jobs -> check Enable debug logging from the GitHub UI interface.
  • Inspect Artifacts: Save log files, crash dumps, or screenshots using actions/upload-artifact@v4 inside an if: always() step.

YAML

steps: – name: Run Test Suite run: npm test – name: Capture Failure Logs if: failure() uses: actions/upload-artifact@v4 with: name: test-failure-logs path: ./logs/

GitHub Actions Fundamentals for Beginners

1. What is GitHub Actions?

GitHub Actions is an automated CI/CD (Continuous Integration and Continuous Delivery) platform built directly into GitHub. It allows you to automate software workflows directly from your repository—such as building code, running tests, publishing packages, or deploying applications—whenever specific GitHub events occur.

YAML

# Simple example: Running tests whenever code is pushed
name: CI Pipeline
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm install
      - run: npm test

2. What is a Workflow?

A Workflow is an automated, configurable process defined in a .yml (or .yaml) file stored inside your repository’s .github/workflows/ directory. A repository can have multiple workflows (e.g., one for unit testing, one for code linting, and one for production deployments).

Workflows are triggered by events (like a push, pull_request, or schedule) and consist of one or more jobs.

YAML

# .github/workflows/greeting.yml
name: Daily Greeting
on:
  schedule:
    - cron: '0 9 * * 1-5' # Runs every weekday at 9 AM UTC

jobs:
  greet:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Good morning, team!"

3. What is a Runner?

A Runner is an application or server that executes the jobs inside a GitHub Actions workflow. When a workflow is triggered, GitHub assigns a runner to listen for available jobs, run the steps, and stream the logs and results back to GitHub.

Runners come pre-installed with standard development tooling (Git, Node.js, Python, Docker, Docker Compose, CLI tools, etc.).

4. Difference Between Job and Step

  • Job: A group of steps that execute on the same runner instance. By default, if a workflow has multiple jobs, they run in parallel unless configured to run sequentially using needs.
  • Step: An individual task within a job. Steps run sequentially on the runner and share the same environment and filesystem. A step can either run a shell command (run) or execute a reusable action (uses).
FeatureJobStep
ExecutionRuns in parallel (default) or sequentiallyAlways runs sequentially inside its parent job
EnvironmentRuns on its own separate virtual machine / runnerShares the virtual machine environment with other steps in the job
Data SharingFilesystem is isolated from other jobsShares local filesystem changes directly with subsequent steps

YAML

jobs:
  build_job: # JOB 1
    runs-on: ubuntu-latest
    steps:
      - name: Step 1 # STEP 1.1
        run: echo "Compiling code..."
      - name: Step 2 # STEP 1.2
        run: echo "Creating build artifact..."

  deploy_job: # JOB 2 (Depends on JOB 1)
    needs: build_job
    runs-on: ubuntu-latest
    steps:
      - name: Step 1 # STEP 2.1
        run: echo "Deploying build..."

5. What is YAML?

YAML (“YAML Ain’t Markup Language”) is a human-readable data format used for configuration files. GitHub Actions uses YAML to structure workflows. It relies strictly on indentation (spaces, never tabs) and key-value pairs to establish hierarchy.

YAML

# Basic YAML structure rules:
string_key: "value"
number_key: 100
boolean_key: true

list_example:
  - item1
  - item2

nested_object:
  parent:
    child: "value"

6. What are GitHub-Hosted Runners?

GitHub-hosted runners are fully managed virtual machines hosted by GitHub. Every time a job runs, GitHub spins up a fresh, ephemeral VM running your choice of operating system (Ubuntu Linux, Windows, or macOS), runs the job, and destroys the VM afterward.

  • Pros: Zero maintenance, completely clean environment per run, automatically updated software packages.
  • Syntax: runs-on: ubuntu-latest, runs-on: windows-latest, or runs-on: macos-latest.

YAML

jobs:
  check-os:
    runs-on: macos-latest
    steps:
      - run: sw_vers # Checks macOS version

7. What are Self-Hosted Runners?

Self-hosted runners are machines (physical servers, local VMs, cloud instances on AWS/Azure, or Docker containers) that you set up and manage yourself, then register with GitHub.

  • Pros: Access to private internal networks/databases, custom hardware requirements (e.g., GPUs for AI/ML training), faster builds through persistent caching.
  • Cons: You are responsible for OS updates, security, and scaling.
  • Syntax: runs-on: self-hosted.

YAML

jobs:
  internal-deploy:
    runs-on: self-hosted
    steps:
      - run: ./deploy-to-private-datacenter.sh

8. What are GitHub Secrets?

GitHub Secrets are encrypted environment variables stored securely in repository, organization, or environment settings. They allow you to store sensitive credentials (e.g., API keys, database passwords, deployment tokens) without hardcoding them into your source code.

GitHub automatically masks secrets in workflow logs (replacing secret values with ***).

YAML

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Login to Docker Hub
        run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}

9. What is workflow_dispatch?

workflow_dispatch is an event trigger that allows you to run a workflow manually. Once added to on:, a “Run workflow” button appears in the GitHub Actions tab UI. It can also accept custom input parameters (text, drop-downs, booleans).

YAML

name: Manual Release
on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target deployment environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying to ${{ inputs.environment }}"

10. What is actions/checkout?

actions/checkout is an official, reusable action (actions/checkout@v4) provided by GitHub.

When a job starts, the runner’s workspace is completely empty. actions/checkout clones your repository’s code into the workspace so that subsequent steps (like linter checks, test runs, or build tools) can access and execute against your project files.

YAML

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # Without checkout, npm build will fail because package.json is missing
      - name: Download repository code
        uses: actions/checkout@v4

      - name: Build project
        run: npm run build

GitHub Actions Complete Roadmap (Beginner to Advanced)

Step-by-Step Learning Guide with Practical Examples

This roadmap is designed specifically for QA Engineers, SDET, DevOps Beginners, and Developers who want to master GitHub Actions from scratch.

By the end of this guide, you’ll be able to build production-ready CI/CD pipelines for:

  • Selenium
  • Playwright
  • Cypress
  • API Testing
  • Python
  • Java
  • Node.js
  • Docker
  • AWS Deployment

Learning Path

LevelTopicsOutcome
Level 1GitHub FundamentalsUnderstand Git & GitHub
Level 2GitHub Actions BasicsRun first workflow
Level 3Workflow SyntaxLearn YAML
Level 4Events & TriggersControl execution
Level 5Jobs & StepsMulti-job pipelines
Level 6Variables & SecretsSecure pipelines
Level 7Matrix StrategyMultiple OS/Browsers
Level 8ArtifactsUpload Reports
Level 9CachingFaster execution
Level 10Advanced WorkflowsReusable workflows
Level 11DockerRun inside containers
Level 12DeploymentsAWS/Azure/GCP
Level 13Enterprise ConceptsProduction CI/CD

Module 1 — GitHub Fundamentals

Topics

  • What is Git?
  • What is GitHub?
  • Repository
  • Branch
  • Commit
  • Push
  • Pull
  • Merge
  • Pull Request
  • Fork
  • Clone

Example

git init

git add .

git commit -m "Initial Commit"

git push origin main

Module 2 — What is GitHub Actions?

GitHub Actions is GitHub’s built-in automation platform.

It automatically performs tasks whenever an event occurs.

Examples

  • Run tests
  • Build applications
  • Deploy websites
  • Send email
  • Upload reports
  • Publish packages

Module 3 — GitHub Actions Architecture

Understand these components:

Repository

↓

Workflow

↓

Jobs

↓

Steps

↓

Actions

↓

Runner

Workflow

.github/workflows/main.yml

Job

A workflow may contain multiple jobs.

jobs:
  test:

Step

steps:

Each job contains multiple steps.


Runner

GitHub provides runners.

Examples

ubuntu-latest

windows-latest

macos-latest

Action

Reusable automation.

Example

actions/checkout

Module 4 — First Workflow

name: My First Workflow

on: push

jobs:

  hello:

    runs-on: ubuntu-latest

    steps:

      - name: Print Message
        run: echo "Hello GitHub Actions"

Output

Hello GitHub Actions

Module 5 — YAML Basics

Topics

  • Indentation
  • Keys
  • Values
  • Lists
  • Mapping

Example

name: Demo

on:
  push:

jobs:

  build:

    runs-on: ubuntu-latest

    steps:

      - run: echo Hello

Module 6 — Workflow Triggers

Push

on:
  push:

Pull Request

on:
  pull_request:

Manual Trigger

on:
  workflow_dispatch:

Schedule

on:

  schedule:

    - cron: "0 6 * * *"

Runs every day.


Multiple Events

on:

  push:

  pull_request:

  workflow_dispatch:

Module 7 — Jobs

Single Job

jobs:

  test:

    runs-on: ubuntu-latest

Multiple Jobs

jobs:

  build:

  test:

  deploy:

Dependent Jobs

needs:

  build

Module 8 — Steps

steps:

- uses: actions/checkout@v4

- run: npm install

- run: npm test

Module 9 — GitHub Marketplace Actions

Popular Actions

actions/checkout

actions/setup-node

actions/cache

actions/upload-artifact

actions/download-artifact

actions/setup-python

Module 10 — Setup Programming Languages

NodeJS

- uses: actions/setup-node@v4

  with:

    node-version: 22

Python

- uses: actions/setup-python@v5

  with:

    python-version: 3.13

Java

- uses: actions/setup-java@v4

.NET

actions/setup-dotnet

Module 11 — Running Scripts

steps:

- run: ls

- run: pwd

- run: npm install

- run: npm test

Module 12 — Environment Variables

env:

  URL: https://example.com

Use

echo $URL

Windows

echo %URL%

Module 13 — Secrets

Repository

Settings

↓

Secrets and Variables

↓

Actions

Example

${{ secrets.USERNAME }}

${{ secrets.PASSWORD }}

Module 14 — Expressions

${{ github.actor }}

${{ github.repository }}

${{ github.ref }}

${{ github.sha }}

${{ runner.os }}

Module 15 — Conditional Execution

if:

  github.ref == 'refs/heads/main'

Example

- name: Deploy

  if: github.ref == 'refs/heads/main'

Module 16 — Matrix Strategy

Run on multiple operating systems.

strategy:

  matrix:

    os:

      - ubuntu-latest

      - windows-latest

      - macos-latest

Multiple Node versions

matrix:

  node:

    - 18

    - 20

    - 22

Module 17 — Caching

uses:

actions/cache@v4

Cache

  • npm
  • Maven
  • Gradle
  • Pip

Improves build speed.


Module 18 — Upload Artifacts

Example

- uses:

actions/upload-artifact@v4

with:

  name: TestReport

  path: reports/

Download later.


Module 19 — Download Artifacts

actions/download-artifact

Module 20 — Service Containers

Run databases.

MySQL

PostgreSQL

Redis

MongoDB

Module 21 — Docker

Build Docker Image

docker build .

Push Docker Hub

docker push

Module 22 — Reusable Workflows

workflow_call

Used in enterprise projects.


Module 23 — Composite Actions

Create your own custom action.

action.yml

Module 24 — Inputs

workflow_dispatch:

inputs:

Example

Branch Name

Environment

Browser

Module 25 — Outputs

Pass values between jobs.

outputs:

Module 26 — Self Hosted Runner

Instead of GitHub Runner

Your Server

↓

GitHub Runner Installed

↓

Workflow Executes

Module 27 — Deployments

Deploy to

  • AWS EC2
  • Azure
  • GCP
  • Kubernetes
  • Docker
  • IIS

Module 28 — Notifications

Examples

  • Email
  • Slack
  • Microsoft Teams
  • Discord

Module 29 — Playwright CI

Workflow

Checkout

↓

Setup Node

↓

Install Dependencies

↓

Install Browsers

↓

Execute Tests

↓

Upload HTML Report

Module 30 — Selenium Python CI

Checkout

↓

Setup Python

↓

Install Requirements

↓

Run Pytest

↓

Upload Allure Report

Module 31 — API Automation

Run

  • Postman
  • Newman
  • Rest Assured
  • Pytest API

Module 32 — Advanced Concepts

  • Reusable workflows
  • Composite actions
  • Dependency graph
  • Workflow permissions
  • OIDC authentication
  • Concurrency
  • Environments
  • Required reviewers
  • Branch protection
  • Deployment approvals
  • Environment protection rules

Module 33 — Enterprise CI/CD Pipeline

Developer

↓

Push Code

↓

Build

↓

Static Code Analysis

↓

Unit Test

↓

Integration Test

↓

Automation Test

↓

Package

↓

Docker Build

↓

Security Scan

↓

Deploy Staging

↓

Approval

↓

Deploy Production

↓

Notification

Module 34 — Real-World Project Examples

Project 1

Node.js Application CI

  • Install dependencies
  • Run ESLint
  • Run Unit Tests
  • Upload Coverage

Project 2

Python Project

  • Install Python
  • Install Requirements
  • Execute Pytest
  • Generate HTML Report

Project 3

Playwright Automation

  • Install Node
  • Install Browsers
  • Execute Tests
  • Upload HTML Report
  • Upload Allure Report

Project 4

Java Selenium

  • Setup Java
  • Setup Maven
  • Execute TestNG
  • Publish Report

Project 5

Docker Deployment

  • Build Image
  • Push Docker Hub
  • Deploy EC2

Project 6

AWS Deployment

  • Build
  • SCP Files
  • Restart Service

How to Perform File Download Using Playwright (TypeScript)

File download is one of the most common scenarios in UI automation. Playwright provides built-in support for handling downloads without relying on browser-specific configurations.

This guide explains everything from basic file downloads to advanced scenarios with practical examples.


Prerequisites

Install Playwright:

npm init playwright@latest

Import Playwright:

import { test, expect } from '@playwright/test';

How Playwright Handles Downloads

Whenever a download starts, Playwright creates a Download object.

You can:

  • Wait for the download event
  • Get download information
  • Save the file
  • Verify file name
  • Verify file content
  • Delete downloaded files

The download object provides methods like:

MethodDescription
download.path()Returns downloaded file path
download.saveAs()Save file to custom location
download.suggestedFilename()Returns original filename
download.failure()Returns download failure reason
download.delete()Deletes downloaded file
download.createReadStream()Reads downloaded file

Example Website

Suppose clicking this button downloads a PDF.

<button>Download Report</button>

Example 1: Basic File Download

import { test, expect } from '@playwright/test';

test('Download File', async ({ page }) => {

    await page.goto('https://example.com');

    const downloadPromise = page.waitForEvent('download');

    await page.getByText('Download Report').click();

    const download = await downloadPromise;

    console.log(download.suggestedFilename());

});

What happens?

Click Button
      │
      ▼
Browser Starts Download
      │
      ▼
Playwright Creates Download Object
      │
      ▼
You Can Save or Verify File

Example 2: Save Download to Custom Folder

import path from 'path';

test('Save Download', async ({ page }) => {

    await page.goto('https://example.com');

    const downloadPromise = page.waitForEvent('download');

    await page.locator('#download').click();

    const download = await downloadPromise;

    await download.saveAs(
        path.join('downloads', download.suggestedFilename())
    );

});

Downloaded folder:

Project

│
├── downloads
│      report.pdf
│
├── tests

Example 3: Get File Name

const fileName = download.suggestedFilename();

console.log(fileName);

Output

report.pdf

Example 4: Get Download Path

const path = await download.path();

console.log(path);

Example Output

C:\Users\Deepesh\AppData\Local\Temp\playwright-downloads\12345.pdf

Example 5: Verify Downloaded File Exists

import fs from 'fs';

const filePath = await download.path();

expect(fs.existsSync(filePath!)).toBeTruthy();

Example 6: Verify File Extension

expect(download.suggestedFilename()).toContain('.pdf');

or

expect(download.suggestedFilename()).toMatch(/\.pdf$/);

Example 7: Verify File Size

import fs from 'fs';

const filePath = await download.path();

const stats = fs.statSync(filePath!);

console.log(stats.size);

Assertion

expect(stats.size).toBeGreaterThan(1000);

Example 8: Verify Downloaded CSV Content

import fs from 'fs';

const filePath = await download.path();

const content = fs.readFileSync(filePath!, 'utf-8');

expect(content).toContain('Employee Name');

Example 9: Verify Downloaded Text File

const filePath = await download.path();

const text = fs.readFileSync(filePath!, 'utf-8');

expect(text).toContain('Welcome');

Example 10: Download Multiple Files

const download1 = page.waitForEvent('download');
await page.click('#download1');
const file1 = await download1;

const download2 = page.waitForEvent('download');
await page.click('#download2');
const file2 = await download2;

console.log(file1.suggestedFilename());
console.log(file2.suggestedFilename());

Example 11: Using Promise.all()

This is the recommended approach because it avoids missing the download event.

const [download] = await Promise.all([
    page.waitForEvent('download'),
    page.locator('#download').click()
]);

await download.saveAs(
    `downloads/${download.suggestedFilename()}`
);

Example 12: Verify Download Failure

const failure = await download.failure();

expect(failure).toBeNull();

If download fails

Network Error

or

Cancelled

Example 13: Delete Downloaded File

await download.delete();

Example 14: Read Download Stream

const stream = await download.createReadStream();

stream?.on('data', chunk => {
    console.log(chunk.toString());
});

Example 15: Download After Login

test('Download Invoice', async ({ page }) => {

    await page.goto('https://example.com/login');

    await page.fill('#username', 'admin');
    await page.fill('#password', 'admin123');

    await page.click('#login');

    const [download] = await Promise.all([
        page.waitForEvent('download'),
        page.click('text=Download Invoice')
    ]);

    await download.saveAs(
        `downloads/${download.suggestedFilename()}`
    );

});

Download PDF Example

const [download] = await Promise.all([
    page.waitForEvent('download'),
    page.getByText('Download PDF').click()
]);

expect(download.suggestedFilename()).toContain('.pdf');

Download Excel Example

const [download] = await Promise.all([
    page.waitForEvent('download'),
    page.getByRole('button', { name: 'Export Excel' }).click()
]);

expect(download.suggestedFilename()).toContain('.xlsx');

Download ZIP Example

const [download] = await Promise.all([
    page.waitForEvent('download'),
    page.click('#zip')
]);

await download.saveAs(
    `downloads/${download.suggestedFilename()}`
);

Browser Download Behavior

BrowserSupported
Chromium✅
Firefox✅
WebKit✅

No browser-specific download configuration is required in Playwright.


Best Practices

  • Always use Promise.all() to wait for the download event and trigger the download simultaneously.
  • Save downloads to a dedicated folder (for example, downloads/) to keep test artifacts organized.
  • Verify both the filename and file extension.
  • Validate the file contents whenever possible instead of checking only that a file exists.
  • Clean up downloaded files after the test to avoid consuming unnecessary disk space.
  • Use download.failure() to detect download issues early.
  • Avoid hard-coded delays such as waitForTimeout() when handling downloads.
  • Use download.suggestedFilename() instead of assuming a fixed filename.

Common Interview Questions

1. How do you handle file downloads in Playwright?

Use page.waitForEvent('download') together with the action that triggers the download, preferably inside Promise.all(). Then use the Download object to save or verify the file.


2. Why should you use Promise.all() for downloads?

It ensures Playwright starts listening for the download event before the click occurs, preventing race conditions where the download starts before the listener is attached.


3. How do you save a downloaded file to a custom location?

Use:

await download.saveAs('downloads/report.pdf');

4. How do you verify a file was downloaded successfully?

You can:

  • Check that await download.failure() returns null.
  • Verify the file exists using fs.existsSync().
  • Validate the filename with download.suggestedFilename().
  • Check the file size or inspect its contents.

5. Can Playwright verify the contents of a downloaded file?

Yes. After obtaining the file path with download.path(), use Node.js modules like fs (or libraries such as xlsx for Excel or pdf-parse for PDFs) to read and validate the file contents.


Summary

Playwright’s download API is simple yet powerful. By using the Download object and the Promise.all() pattern, you can reliably automate downloading PDFs, Excel files, ZIP archives, CSVs, and other file types while verifying filenames, sizes, and contents. This approach works consistently across Chromium, Firefox, and WebKit, making it suitable for both functional and end-to-end automation tests.

Integrating Allure Report with Playwright (Step-by-Step Guide)

Allure Report is one of the most popular reporting tools for Playwright automation projects. It provides interactive HTML reports with test execution history, screenshots, attachments, environment information, categories, and detailed execution steps.

This guide explains how to integrate Allure Report into a Playwright project from scratch, based on your project configuration.


What is Allure Report?

Allure Report is an advanced test reporting framework that provides:

  • Beautiful HTML reports
  • Test execution history
  • Test retries
  • Screenshots
  • Videos
  • Trace attachments
  • Execution duration
  • Environment information
  • Categories of failures
  • Test hierarchy
  • Dashboard and statistics

Example Report Structure

Dashboard
│
├── Overview
├── Behaviors
├── Suites
├── Graphs
├── Timeline
├── Categories
└── Packages

Prerequisites

Before integrating Allure, make sure you have:

  • Node.js installed
  • Playwright project already created
  • npm installed

Verify versions

node -v
npm -v
npx playwright --version

Step 1: Install Required Packages

Install the Allure reporter and command-line utility.

npm install --save-dev allure-playwright allure-commandline

These packages serve different purposes:

PackagePurpose
allure-playwrightGenerates Allure result files during test execution
allure-commandlineConverts result files into an interactive HTML report

This is the first step required to enable Allure reporting.


Step 2: Configure Playwright Reporter

Open:

playwright.config.ts

Update the reporter section:

reporter: [
  ['list'],
  ['html', { open: 'never', outputFolder: 'playwright-report' }],
  ['allure-playwright', {
      outputFolder: 'allure-results',
      suiteTitle: false
  }]
]

Explanation:

List Reporter

['list']

Displays test execution in the terminal.

Example

✓ Login Test
✓ Logout Test
✓ Search Product

HTML Reporter

['html', {
    open: 'never',
    outputFolder: 'playwright-report'
}]

Generates Playwright’s built-in HTML report.

Output folder:

playwright-report/

Allure Reporter

['allure-playwright', {
    outputFolder: 'allure-results',
    suiteTitle: false
}]

This reporter creates raw execution data.

Output folder:

allure-results/

These settings match the Playwright configuration used in your project.


Step 3: Add npm Scripts

Open

package.json

Add:

{
  "scripts": {
    "test": "playwright test",
    "test:allure": "playwright test --reporter=list,allure-playwright",
    "allure:generate": "allure generate allure-results -o allure-report --clean",
    "allure:open": "allure open allure-report"
  }
}

Meaning of each script:

Run tests with Allure

npm run test:allure

Generates:

allure-results/

Generate HTML Report

npm run allure:generate

Creates:

allure-report/

Open Report

npm run allure:open

Starts a local web server and opens the report in your default browser.

These scripts are defined in your project’s package.json.


Step 4: Execute Playwright Tests

Run:

npm run test:allure

Example output:

Running 10 tests

✓ Login Test
✓ Logout Test
✓ Add Product
✓ Delete Product

10 passed

After execution, a new folder is created:

allure-results/

This folder contains files such as:

allure-results
│
├── *.json
├── *.txt
├── attachments
└── executor.json

These are the raw files used to build the final report.


Step 5: Generate the HTML Report

Run:

npm run allure:generate

Expected output:

Report successfully generated.

A new directory is created:

allure-report/

Inside:

allure-report
│
├── index.html
├── app.js
├── plugins
├── widgets
└── history

The report generation command produces the allure-report folder.


Step 6: Open the Report

Run:

npm run allure:open

Allure starts a local server:

http://127.0.0.1:5050

The report opens automatically in your default browser, allowing you to explore dashboards, suites, graphs, and test details.


Project Folder Structure

Playwright Project
│
├── tests
├── node_modules
├── playwright.config.ts
├── package.json
│
├── allure-results
│     ├── *.json
│     └── attachments
│
├── allure-report
│     ├── index.html
│     └── widgets
│
└── playwright-report

What Information Does Allure Capture?

By default, Allure records:

  • Test name
  • Execution status
  • Duration
  • Start and end time
  • Test suite
  • Error messages
  • Stack trace
  • Retries
  • Execution timeline

You can also enhance reports with:

  • Screenshots
  • Videos
  • Trace files
  • Environment details
  • Custom labels
  • Custom steps
  • Links to requirements or test cases

Common Commands

CommandPurpose
npm run test:allureExecute tests and generate Allure result files
npm run allure:generateBuild the HTML report
npm run allure:openOpen the generated report
npx playwright testRun Playwright tests only
npx playwright show-reportOpen Playwright’s built-in HTML report

Troubleshooting

Report is Empty

Possible causes:

  • Tests were not executed.
  • allure-results folder is empty.
  • Reporter is not configured correctly.

Solution:

npm run test:allure
npm run allure:generate

allure Command Not Found

Cause: allure-commandline is missing or not installed correctly.

Solution:

npm install --save-dev allure-commandline

Or run via:

npx allure generate allure-results -o allure-report --clean

Report Not Updating

Clean previous reports before regenerating:

rm -rf allure-results
rm -rf allure-report

Run the tests again and regenerate the report.


Best Practices

  • Keep both Playwright HTML and Allure reports enabled for different reporting needs.
  • Exclude allure-results and allure-report from version control to avoid committing generated artifacts.
  • Add screenshots and trace files for failed tests to improve debugging.
  • Generate a fresh report after every test execution.
  • Publish the allure-report folder as a CI/CD artifact in Jenkins or GitHub Actions for easy access.

Conclusion

Integrating Allure with Playwright provides a professional and feature-rich reporting solution that goes far beyond the default Playwright HTML report. By installing the required packages, configuring the Playwright reporter, adding npm scripts, executing tests, generating the report, and opening it locally, you can produce interactive reports that simplify debugging, improve test visibility, and make automation results easier to share with teams and stakeholders.

JavaScript Object Programs (Without Solution)

The following exercises are designed for developers who already understand JavaScript objects and want to practice solving real-world problems. Each program includes a scenario, input, expected output, and requirements without providing the solution.


1. Find the Employee with the Highest Salary

Scenario

A company stores employee details in an object. Find the employee who has the highest salary.

Input

const employees = {
    emp1: { name: "John", salary: 55000 },
    emp2: { name: "Alice", salary: 72000 },
    emp3: { name: "David", salary: 68000 }
};

Expected Output

Highest Salary Employee:
Alice
Salary: 72000

2. Count Product Categories

Scenario

An online shopping website stores products with categories. Count how many products belong to each category.

Input

const products = {
    p1: { category: "Electronics" },
    p2: { category: "Furniture" },
    p3: { category: "Electronics" },
    p4: { category: "Books" },
    p5: { category: "Books" }
};

Expected Output

Electronics : 2
Furniture : 1
Books : 2

3. Merge Student Information

Scenario

Merge two student objects into one object.

Input

const personal = {
    name: "Rahul",
    age: 21
};

const academic = {
    course: "B.Tech",
    marks: 88
};

Expected Output

{
    name: "Rahul",
    age: 21,
    course: "B.Tech",
    marks: 88
}

4. Find Missing Properties

Scenario

Check whether every employee object contains an email property.

Input

const employees = {
    emp1: {
        name: "John",
        email: "john@test.com"
    },
    emp2: {
        name: "Alice"
    },
    emp3: {
        name: "David",
        email: "david@test.com"
    }
};

Expected Output

Employee Missing Email:
Alice

5. Calculate Total Shopping Cart Value

Scenario

Calculate the total bill of all products in the shopping cart.

Input

const cart = {
    item1: {
        name: "Mouse",
        price: 700,
        quantity: 2
    },
    item2: {
        name: "Keyboard",
        price: 1200,
        quantity: 1
    },
    item3: {
        name: "Monitor",
        price: 9500,
        quantity: 1
    }
};

Expected Output

Total Cart Value:
12100

6. Remove Null Values from Object

Scenario

Remove all properties whose value is null.

Input

const user = {
    name: "Deepesh",
    phone: null,
    city: "Bhopal",
    email: null,
    age: 30
};

Expected Output

{
    name: "Deepesh",
    city: "Bhopal",
    age: 30
}

7. Find Duplicate Values

Scenario

Identify duplicate values present in an object.

Input

const students = {
    s1: "A",
    s2: "B",
    s3: "A",
    s4: "C",
    s5: "B"
};

Expected Output

Duplicate Values:
A
B

8. Convert Object into Sorted Array

Scenario

Convert the object values into an array and sort them in ascending order.

Input

const marks = {
    maths: 78,
    science: 91,
    english: 65,
    computer: 99
};

Expected Output

[65, 78, 91, 99]

9. Update Nested Object

Scenario

Update the city of the employee.

Input

const employee = {
    id: 101,
    name: "John",
    address: {
        city: "Delhi",
        state: "Delhi"
    }
};

Task

Update city to Mumbai.

Expected Output

{
    id:101,
    name:"John",
    address:{
        city:"Mumbai",
        state:"Delhi"
    }
}

10. Find Average Salary

Scenario

Calculate the average salary of all employees.

Input

const employees = {
    emp1: { salary: 45000 },
    emp2: { salary: 60000 },
    emp3: { salary: 75000 },
    emp4: { salary: 50000 }
};

Expected Output

Average Salary:
57500

11. Inventory Stock Checker

Scenario

Display all products whose quantity is less than 5.

Input

const inventory = {
    p1: { name: "Laptop", quantity: 3 },
    p2: { name: "Keyboard", quantity: 8 },
    p3: { name: "Mouse", quantity: 2 },
    p4: { name: "Monitor", quantity: 10 }
};

Expected Output

Low Stock Products:
Laptop
Mouse

12. Group Employees by Department

Scenario

Group employees based on department.

Input

const employees = {
    emp1: { name: "John", department: "IT" },
    emp2: { name: "Alice", department: "HR" },
    emp3: { name: "David", department: "IT" },
    emp4: { name: "Emma", department: "Finance" }
};

Expected Output

{
    IT: ["John", "David"],
    HR: ["Alice"],
    Finance: ["Emma"]
}

13. Find the Most Expensive Product

Scenario

Find the product with the highest price.

Input

const products = {
    p1: { name: "Phone", price: 25000 },
    p2: { name: "Laptop", price: 65000 },
    p3: { name: "Watch", price: 12000 }
};

Expected Output

Laptop
65000

14. Count Boolean Values

Scenario

Count how many properties have true and false values.

Input

const permissions = {
    read: true,
    write: false,
    delete: true,
    update: false,
    share: true
};

Expected Output

True : 3
False : 2

15. Reverse Key-Value Pairs

Scenario

Swap the keys and values of an object.

Input

const countryCodes = {
    India: "IN",
    America: "US",
    Japan: "JP"
};

Expected Output

{
    IN: "India",
    US: "America",
    JP: "Japan"
}

Top 50 Playwright Assertions Interview Questions and Answers

Assertions are one of the most frequently asked topics in Playwright interviews. They help verify whether the application behaves as expected after performing user actions. Playwright provides a powerful assertion library with auto-waiting, retry mechanisms, and rich error reporting, making tests more reliable and less flaky.

This guide covers beginner, intermediate, and advanced Playwright assertion interview questions with detailed answers and TypeScript examples.


1. What are Assertions in Playwright?

Answer

Assertions are used to verify that the actual result matches the expected result. If the expected condition is not met, the test fails.

Example

import { test, expect } from '@playwright/test';

test('Verify Page Title', async ({ page }) => {
    await page.goto('https://example.com');

    await expect(page).toHaveTitle('Example Domain');
});

2. Why are Assertions important in Automation Testing?

Answer

Assertions ensure that:

  • The application behaves correctly.
  • Test results are validated.
  • Bugs are detected automatically.
  • Expected UI and business logic are verified.

Without assertions, an automation script only performs actions without validating outcomes.


3. What assertion library does Playwright use?

Answer

Playwright Test provides a built-in expect() assertion library.

expect(value).toBe(expected);

4. What makes Playwright assertions better than traditional assertions?

Answer

Playwright assertions provide:

  • Auto-waiting
  • Automatic retries
  • Better error messages
  • Screenshot capture on failures
  • Trace support
  • Reduced flaky tests

5. What is expect() in Playwright?

Answer

expect() is used to validate expected results.

expect(10).toBe(10);

6. What is auto-waiting in Playwright assertions?

Answer

Playwright automatically waits until the expected condition becomes true or the timeout is reached.

Example:

await expect(page.locator('#login')).toBeVisible();

No explicit wait is required.


7. What happens if an assertion fails?

Answer

  • The current test fails.
  • Playwright captures useful diagnostics (such as screenshots and traces, if configured).
  • Remaining steps in the test are skipped unless soft assertions are used.

8. What is the default timeout for assertions?

Answer

By default, Playwright assertions use the configured expect timeout, which is 5 seconds unless changed in the Playwright configuration or overridden for a specific assertion.

Example:

await expect(locator).toBeVisible({
    timeout: 10000
});

9. Difference between toBe() and toEqual()?

Answer

toBe()toEqual()
Checks primitive values using strict equalityDeep comparison for objects and arrays
Best for numbers, strings, booleansBest for objects and arrays

Example:

expect(5).toBe(5);

expect({
    name: 'John'
}).toEqual({
    name: 'John'
});

10. What is not in Playwright assertions?

Answer

Used to verify negative conditions.

expect(5).not.toBe(10);

11. What is toBeTruthy()?

Answer

Checks whether a value is truthy.

expect(true).toBeTruthy();

12. What is toBeFalsy()?

Answer

Checks whether a value is falsy.

expect(false).toBeFalsy();

13. What is toBeNull()?

Answer

Checks if the value is null.

expect(null).toBeNull();

14. What is toBeDefined()?

Answer

Checks whether a variable is defined.

const username = 'Admin';

expect(username).toBeDefined();

15. What is toBeUndefined()?

Answer

Checks if a variable is undefined.

let city;

expect(city).toBeUndefined();

16. What is toContain()?

Answer

Checks whether an array or string contains a value.

expect('Playwright').toContain('wright');

17. What is toHaveLength()?

Answer

Verifies array or string length.

expect([1,2,3]).toHaveLength(3);

18. What is toBeGreaterThan()?

Answer

Checks if a value is greater than another value.

expect(100).toBeGreaterThan(50);

19. What is toBeLessThan()?

Answer

expect(10).toBeLessThan(20);

20. What is toBeCloseTo()?

Answer

Useful for floating-point numbers.

expect(0.1 + 0.2).toBeCloseTo(0.3);

21. What are Locator Assertions?

Answer

Locator assertions validate the state of UI elements.

Examples:

  • toBeVisible()
  • toBeHidden()
  • toBeEnabled()
  • toHaveText()
  • toHaveValue()

22. What is toBeVisible()?

Answer

Checks whether an element is visible.

await expect(
    page.locator('#login')
).toBeVisible();

23. What is toBeHidden()?

Answer

Checks if an element is hidden.

await expect(
    page.locator('.loader')
).toBeHidden();

24. What is toBeEnabled()?

Answer

Verifies that an element is enabled.

await expect(
    page.locator('#submit')
).toBeEnabled();

25. What is toBeDisabled()?

Answer

Checks if an element is disabled.

await expect(
    page.locator('#submit')
).toBeDisabled();

26. What is toBeChecked()?

Answer

Used for checkboxes and radio buttons.

await expect(
page.locator('#remember')
).toBeChecked();

27. What is toHaveText()?

Answer

Verifies exact text.

await expect(
page.locator('h1')
).toHaveText('Dashboard');

28. What is toContainText()?

Answer

Checks partial text.

await expect(
page.locator('.message')
).toContainText('Success');

29. Difference between toHaveText() and toContainText()?

Answer

toHaveText()toContainText()
Exact matchPartial match
Entire text must matchOnly a portion needs to match

30. What is toHaveValue()?

Answer

Checks the value of input fields.

await expect(
page.locator('#username')
).toHaveValue('Admin');

31. What is toHaveAttribute()?

Answer

Checks an HTML attribute.

await expect(
page.locator('#email')
).toHaveAttribute('type','email');

32. What is toHaveClass()?

Answer

Verifies CSS classes.

await expect(
page.locator('.active')
).toHaveClass('active');

33. What is toHaveCount()?

Answer

Checks the number of matching elements.

await expect(
page.locator('.product')
).toHaveCount(5);

34. What is toBeEditable()?

Answer

Checks if an input can be edited.

await expect(
page.locator('#username')
).toBeEditable();

35. What is toHaveCSS()?

Answer

Verifies CSS property values.

await expect(
page.locator('#title')
).toHaveCSS('color','rgb(255, 0, 0)');

36. What are Page Assertions?

Answer

Assertions used directly on the page.

Examples:

  • toHaveTitle()
  • toHaveURL()

37. What is toHaveTitle()?

Answer

Checks the page title.

await expect(page)
.toHaveTitle('Dashboard');

38. What is toHaveURL()?

Answer

Checks the current URL.

await expect(page)
.toHaveURL(/dashboard/);

39. What are Soft Assertions?

Answer

Soft assertions allow the test to continue even if an assertion fails.

expect.soft(title).toBe('Dashboard');

expect.soft(username).toBe('Admin');

40. When should you use Soft Assertions?

Answer

Use soft assertions when:

  • Validating multiple UI elements.
  • Collecting all failures in a single execution.
  • Creating comprehensive UI verification tests.

41. What is expect.poll()?

Answer

expect.poll() repeatedly executes a function until the expected result is achieved or the timeout expires.

await expect.poll(async () => {
    return await page.locator('.counter').textContent();
}).toBe('10');

42. What is the difference between expect() and expect.poll()?

Answer

expect()expect.poll()
Checks an immediate value or locatorRepeatedly evaluates a callback until the condition is met
Best for UI elements and direct valuesBest for values that change over time

43. What are Screenshot Assertions?

Answer

Used for visual regression testing.

await expect(page)
.toHaveScreenshot();

44. Can assertions be customized with messages?

Answer

Yes.

expect(
    total,
    'Total price should be greater than zero'
).toBeGreaterThan(0);

45. How do you verify API responses in Playwright?

Answer

const response = await page.request.get(
'https://reqres.in/api/users/2'
);

expect(response.status()).toBe(200);

expect(response.ok()).toBeTruthy();

46. What are common mistakes when writing assertions?

Answer

  • Using waitForTimeout() before assertions.
  • Verifying text with textContent() instead of toHaveText().
  • Writing unnecessary manual waits.
  • Using exact matches for dynamic values.
  • Overusing hard-coded timeouts.

47. Why are locator assertions preferred over manual value checks?

Answer

Locator assertions automatically wait for the expected condition and retry until the timeout expires, making tests more stable and less flaky.

Instead of:

const text = await page.locator('h1').textContent();
expect(text).toBe('Dashboard');

Prefer:

await expect(page.locator('h1')).toHaveText('Dashboard');

48. Can Playwright assertions be used with regular JavaScript variables?

Answer

Yes.

const total = 100;

expect(total).toBe(100);

49. How do you verify multiple conditions in one test?

Answer

await expect(page).toHaveTitle('Dashboard');

await expect(page).toHaveURL(/dashboard/);

await expect(page.locator('#logout'))
    .toBeVisible();

await expect(page.locator('.product'))
    .toHaveCount(5);

50. What are the best practices for Playwright Assertions?

Answer

  • Use locator assertions (toBeVisible(), toHaveText(), toHaveValue()) instead of manually reading values.
  • Avoid waitForTimeout(); rely on Playwright’s auto-waiting.
  • Use expect.soft() when multiple independent validations should run in the same test.
  • Use expect.poll() for asynchronous values that change over time.
  • Keep assertions specific and focused on a single expected outcome.
  • Use regular expressions for dynamic URLs and text where appropriate.
  • Write descriptive custom messages for critical business validations.
  • Prefer built-in Playwright assertions over custom validation logic whenever possible.

Bonus Interview Questions

1. What is the difference between expect(locator).toHaveText() and expect(await locator.textContent()).toBe()?

Answer:

toHaveText() automatically waits and retries until the expected text appears, making it more reliable for dynamic web pages. textContent() retrieves the current text immediately and does not retry.


2. Why should waitForTimeout() not be used before assertions?

Answer:

waitForTimeout() introduces unnecessary delays and can make tests flaky. Playwright assertions already include built-in waiting and retry mechanisms, so explicit sleep statements are rarely needed.


3. Which Playwright assertions are most commonly asked in interviews?

Answer:

The most frequently discussed assertions are:

  • toBe()
  • toEqual()
  • toContain()
  • toHaveText()
  • toContainText()
  • toBeVisible()
  • toBeHidden()
  • toBeEnabled()
  • toBeDisabled()
  • toHaveValue()
  • toHaveAttribute()
  • toHaveCount()
  • toHaveURL()
  • toHaveTitle()
  • expect.soft()
  • expect.poll()
  • toHaveScreenshot()

Mastering these assertions and understanding when to use each one will prepare you for most Playwright automation interviews, from beginner to advanced levels.

Top 50 Playwright Locator Interview Questions and Answers

Playwright locators are one of the most important interview topics for QA Automation Engineers, SDETs, and Test Automation Engineers. Interviewers frequently ask questions about locator strategies, best practices, auto-waiting, strict mode, and advanced locator chaining.


1. What is a Locator in Playwright?

Answer:

A locator is an object that identifies and interacts with elements on a web page. Unlike traditional selectors, Playwright locators automatically wait for elements to become available before performing actions.

Example

const loginButton = page.locator('#login');

await loginButton.click();

2. Why are Playwright Locators better than CSS or XPath?

Answer:

Playwright locators provide:

  • Auto-waiting
  • Retry mechanism
  • Better readability
  • Improved stability
  • Built-in strict mode
  • Better handling of dynamic pages

3. What is the difference between page.locator() and page.$()?

Answer:

page.locator()page.$()
Auto waitsNo auto wait
RecommendedDeprecated for most use cases
Supports retriesReturns immediately
Better stabilityMore flaky

Example:

await page.locator('#submit').click();

4. What is Strict Mode in Playwright?

Answer:

Strict mode ensures a locator resolves to exactly one element.

If multiple elements match, Playwright throws an error.

Example

await page.locator('button').click();

Error:

Strict mode violation:
Locator resolved to 5 elements.

5. How do you disable Strict Mode?

Answer:

Use methods like:

.first()

.last()

.nth(index)

Example

await page.locator('button').first().click();

6. Explain page.locator().

Answer:

Creates a reusable locator.

const username = page.locator('#username');

await username.fill('Admin');

7. What is getByRole()?

Answer:

Locates elements using their ARIA role.

Example

await page.getByRole('button', {
    name: 'Login'
}).click();

Recommended because it mimics user interaction.


8. Why is getByRole() preferred?

Answer:

  • Accessibility friendly
  • Stable
  • Less affected by UI changes
  • Easy to understand

9. What is getByText()?

Answer:

Finds elements using visible text.

await page.getByText('Sign In').click();

10. Difference between locator('text=Login') and getByText()?

Answer:

getByText() is more readable and recommended.

page.getByText('Login');

instead of

page.locator('text=Login');

11. What is getByLabel()?

Answer:

Locates form fields using associated labels.

await page.getByLabel('Username')
.fill('Admin');

12. What is getByPlaceholder()?

Answer:

Finds input elements using placeholder text.

await page
.getByPlaceholder('Search')
.fill('Laptop');

13. What is getByAltText()?

Answer:

Used for images.

page.getByAltText('Company Logo');

14. What is getByTitle()?

Answer:

Finds elements using the title attribute.

page.getByTitle('Settings');

15. What is getByTestId()?

Answer:

Locates elements using the data-testid attribute.

<button data-testid="login-btn">
page.getByTestId('login-btn');

16. Why is data-testid recommended?

Answer:

  • Independent of UI
  • Stable
  • Easy to maintain
  • Designed for automation

17. What is Locator Chaining?

Answer:

Locating elements inside another locator.

page.locator('.card')
.locator('button');

18. What is filter()?

Answer:

Filters locator results.

page.locator('.product')
.filter({
    hasText:'Laptop'
});

19. What is hasText?

Answer:

Filters elements by text.

page.locator('li').filter({
    hasText:'Apple'
});

20. What is has()?

Answer:

Filters elements containing another locator.

page.locator('.card').filter({

    has: page.locator('button')

});

21. What is nth()?

Answer:

Selects the element at a given index (zero-based).

page.locator('li').nth(2);

22. What is first()?

Answer:

Returns the first matching element.

page.locator('button').first();

23. What is last()?

Answer:

Returns the last matching element.

page.locator('button').last();

24. Difference between nth(0) and first()?

Answer:

Both return the first element, but first() is more readable and expressive.


25. How do you count matching elements?

Answer:

const count =
await page.locator('.product').count();

26. How do you loop through multiple elements?

Answer:

const items = page.locator('.product');

const count = await items.count();

for (let i = 0; i < count; i++) {
    console.log(await items.nth(i).textContent());
}

27. How do you get the text of an element?

Answer:

const text =
await page.locator('h1').textContent();

Prefer assertions when verifying text:

await expect(page.locator('h1'))
    .toHaveText('Dashboard');

28. What is allTextContents()?

Answer:

Returns the text content of all matching elements.

const texts =
await page.locator('li').allTextContents();

29. What is allInnerTexts()?

Answer:

Returns the rendered inner text of all matching elements, excluding hidden text in many cases.

const texts =
await page.locator('li').allInnerTexts();

30. Difference between textContent() and innerText()?

Answer:

textContent()innerText()
Includes hidden textReturns visible rendered text
FasterSlower due to layout calculations
Reads DOM textReads displayed text

31. How do you locate using CSS selectors?

Answer:

page.locator('#username');

page.locator('.login');

page.locator('input');

32. Can Playwright use XPath?

Answer:

Yes.

page.locator('//button');

However, CSS selectors and user-facing locators like getByRole() are generally preferred for readability and maintainability.


33. How do you locate a parent element?

Answer:

Use locator chaining or XPath when appropriate.

Example using chaining:

const card = page.locator('.card').filter({
  has: page.getByText('Product A')
});

34. How do you locate child elements?

Answer:

page.locator('.card')
.locator('button');

35. What is Auto Waiting?

Answer:

Playwright automatically waits until the target element is actionable (e.g., attached, visible, stable, and enabled where applicable) before performing actions.

Example:

await page.locator('#login').click();

36. Can Locators be reused?

Answer:

Yes.

const loginButton =
page.getByRole('button', { name: 'Login' });

await loginButton.click();
await expect(loginButton).toBeVisible();

37. Are Locators Lazy?

Answer:

Yes.

A locator does not immediately search the DOM. It resolves the element when an action or assertion is performed, allowing it to work with dynamic pages.


38. Difference between Locator and ElementHandle?

Answer:

LocatorElementHandle
Lazy evaluationSnapshot of a DOM element
Auto waitsNo automatic retries
RecommendedUse only for advanced scenarios
More resilientCan become stale

39. How do you wait for a Locator?

Answer:

Usually you don’t need to wait manually.

If needed:

await page.locator('#login').waitFor();

40. How do you verify a Locator exists?

Answer:

await expect(
page.locator('#login')
).toBeVisible();

41. How do you locate an element inside an iframe?

Answer:

Use frameLocator().

await page
.frameLocator('#payment-frame')
.getByRole('button', { name: 'Pay Now' })
.click();

42. How do you locate shadow DOM elements?

Answer:

Playwright automatically pierces open shadow DOMs, so standard locators often work without extra APIs.

await page.getByRole('button', { name: 'Submit' }).click();

43. What are the best locator strategies in Playwright?

Answer:

Preferred order:

  1. getByRole()
  2. getByLabel()
  3. getByPlaceholder()
  4. getByTestId()
  5. getByText()
  6. CSS selectors
  7. XPath (only when necessary)

44. Why should XPath be avoided when possible?

Answer:

  • More fragile
  • Harder to read
  • Breaks easily after DOM changes
  • Longer and more complex expressions

45. How do you handle dynamic locators?

Answer:

Use template literals.

const productName = 'Laptop';

await page.getByText(productName).click();

Or:

await page.locator(
    `[data-id="${productName}"]`
).click();

46. How do you locate a button with specific text?

Answer:

await page.getByRole('button', {
    name: 'Submit'
}).click();

47. How do you locate multiple buttons with the same text?

Answer:

Use nth(), first(), or last().

await page
.getByRole('button', { name: 'Edit' })
.nth(1)
.click();

48. How do you debug a locator?

Answer:

Useful techniques include:

  • Use Playwright Inspector (--debug)
  • Use page.pause()
  • Use the Playwright Codegen tool
  • Inspect the locator in browser DevTools
  • Verify uniqueness with assertions like toHaveCount(1)

Example:

await page.pause();

49. What are common locator mistakes?

Answer:

  • Using overly generic selectors like div or button
  • Relying on dynamic CSS classes
  • Depending on long XPath expressions
  • Using nth() unnecessarily when a unique locator is available
  • Adding manual waits instead of relying on locator auto-waiting

50. What are the best practices for Playwright locators?

Answer:

  • Prefer user-facing locators such as getByRole() and getByLabel().
  • Use data-testid for elements without stable accessible attributes.
  • Create reusable locators in Page Object Model classes.
  • Avoid brittle XPath and deeply nested CSS selectors.
  • Keep locators unique and descriptive.
  • Use Playwright assertions (toBeVisible(), toHaveText(), etc.) instead of manual checks.
  • Take advantage of locator chaining and filter() to make selectors more precise.
  • Write locators that reflect user behavior rather than implementation details.

Bonus Interview Question

What is the recommended locator priority in Playwright?

Answer:

The Playwright team recommends choosing locators in roughly this order:

  1. getByRole() – Best for accessibility and user-centric testing.
  2. getByLabel() – Ideal for form controls.
  3. getByPlaceholder() – Useful for inputs with meaningful placeholders.
  4. getByTestId() – Great for stable automation-specific selectors.
  5. getByText() – Suitable for visible text when it uniquely identifies the element.
  6. CSS selectors – Use when semantic locators are unavailable.
  7. XPath – Reserve for cases where no better locator strategy is feasible.

Following this hierarchy produces tests that are more readable, accessible, and resilient to UI changes.

Playwright Assertions with TypeScript – Complete Guide with Examples

Assertions are one of the most important concepts in Playwright test automation. They verify whether the application behaves as expected after performing an action. Without assertions, your automation script simply performs actions without validating the results.

Playwright provides a powerful assertion library built on top of expect(), which automatically waits for conditions to become true before failing the test. This feature makes Playwright tests much more reliable compared to traditional automation frameworks.

In this article, you’ll learn everything about Playwright assertions, including different assertion types, best practices, and practical examples using TypeScript.


What are Assertions?

Assertions verify the expected state of your application.

For example, after clicking the Login button, you may want to verify:

  • User is redirected to Dashboard
  • Welcome message is displayed
  • Logout button is visible
  • Shopping cart contains 3 products

If the expected condition is not met, the test fails.

Example:

expect(await page.title()).toBe("Dashboard");

Why Playwright Assertions are Powerful

Unlike traditional assertions, Playwright assertions:

  • Auto-wait until the condition becomes true
  • Retry assertions automatically
  • Produce detailed error messages
  • Capture screenshots on failures
  • Generate trace files
  • Work with UI elements directly

Traditional Selenium example:

Thread.sleep(5000);
Assert.assertEquals(driver.getTitle(), "Dashboard");

Playwright:

await expect(page).toHaveTitle("Dashboard");

No manual waits required.


Importing Assertions

Playwright Test automatically provides expect.

import { test, expect } from '@playwright/test';

Assertion Categories

Playwright provides several assertion types.

  • Value Assertions
  • Locator Assertions
  • Page Assertions
  • API Assertions
  • Screenshot Assertions
  • Soft Assertions
  • Polling Assertions
  • Custom Assertions

Let’s explore each one.


1. Value Assertions

Used for JavaScript values.

Example:

test('Number Assertion', async () => {

    const count = 10;

    expect(count).toBe(10);

});

toBe()

Checks exact equality.

expect(5).toBe(5);

Example

const user = "Deepesh";

expect(user).toBe("Deepesh");

not.toBe()

expect(5).not.toBe(10);

toEqual()

Used for arrays and objects.

const user = {
    name: "John",
    age: 30
};

expect(user).toEqual({
    name: "John",
    age: 30
});

toContain()

expect(["Apple","Orange"]).toContain("Apple");

String example

expect("Playwright Automation").toContain("Automation");

toHaveLength()

expect([1,2,3]).toHaveLength(3);

String

expect("Playwright").toHaveLength(10);

toBeTruthy()

expect(true).toBeTruthy();

Example

const isLoggedIn = true;

expect(isLoggedIn).toBeTruthy();

toBeFalsy()

expect(false).toBeFalsy();

toBeNull()

expect(null).toBeNull();

toBeDefined()

const username = "Admin";

expect(username).toBeDefined();

toBeUndefined()

let city;

expect(city).toBeUndefined();

toBeGreaterThan()

expect(20).toBeGreaterThan(10);

toBeLessThan()

expect(5).toBeLessThan(10);

toBeCloseTo()

Useful for decimal values.

expect(0.1 + 0.2).toBeCloseTo(0.3);

2. Locator Assertions

Locator assertions are the most commonly used assertions in Playwright.

Example locator

const loginButton = page.locator("#login");

toBeVisible()

Checks whether an element is visible.

await expect(loginButton).toBeVisible();

toBeHidden()

await expect(page.locator(".loader")).toBeHidden();

toBeEnabled()

await expect(loginButton).toBeEnabled();

toBeDisabled()

await expect(page.locator("#submit")).toBeDisabled();

toBeChecked()

Checkbox example

await page.locator("#remember").check();

await expect(page.locator("#remember")).toBeChecked();

toHaveText()

await expect(page.locator("h1"))
.toHaveText("Dashboard");

toContainText()

await expect(page.locator(".message"))
.toContainText("Success");

toHaveValue()

Textbox example

await page.fill("#username","Admin");

await expect(page.locator("#username"))
.toHaveValue("Admin");

toHaveAttribute()

await expect(page.locator("#email"))
.toHaveAttribute("type","email");

toHaveClass()

await expect(page.locator(".active"))
.toHaveClass("active");

Multiple classes

await expect(page.locator(".btn"))
.toHaveClass(/btn primary/);

toHaveCount()

Useful for lists.

await expect(page.locator(".product"))
.toHaveCount(8);

toBeEditable()

await expect(page.locator("#username"))
.toBeEditable();

toHaveCSS()

await expect(page.locator("#title"))
.toHaveCSS("color","rgb(255, 0, 0)");

toHaveJSProperty()

await expect(page.locator("#checkbox"))
.toHaveJSProperty("checked", true);

toHaveId()

await expect(page.locator("#login"))
.toHaveId("login");

3. Page Assertions


toHaveTitle()

await expect(page)
.toHaveTitle("Dashboard");

toHaveURL()

await expect(page)
.toHaveURL("https://example.com/dashboard");

Using Regex

await expect(page)
.toHaveURL(/dashboard/);

4. Screenshot Assertions

Playwright can compare screenshots automatically.

await expect(page)
.toHaveScreenshot();

Specific filename

await expect(page)
.toHaveScreenshot("homepage.png");

Element screenshot

await expect(page.locator(".logo"))
.toHaveScreenshot();

This is useful for visual regression testing.


5. API Assertions

Example

const response = await page.request.get(
    "https://reqres.in/api/users/2"
);

expect(response.status()).toBe(200);
expect(response.ok()).toBeTruthy();

Verify JSON

const body = await response.json();

expect(body.data.first_name)
.toBe("Janet");

6. Soft Assertions

Normally,

expect(value).toBe(expected);

stops execution when it fails.

Soft assertions allow the test to continue.

expect.soft(title).toBe("Dashboard");

expect.soft(username).toBe("Admin");

Example

test('Soft Assertions', async ({ page }) => {

    await page.goto("https://example.com");

    await expect.soft(page).toHaveTitle("Home");

    await expect.soft(page.locator("h1"))
    .toContainText("Example");

    await expect.soft(page.locator("#login"))
    .toBeVisible();

});

7. Polling Assertions

Sometimes values are updated after several seconds.

Playwright provides polling.

await expect.poll(async () => {

    return await page.locator(".counter").textContent();

}).toBe("10");

Playwright retries until timeout.


8. Custom Message

Provide a helpful failure message.

expect(
    totalPrice,
    "Total price should be greater than zero"
).toBeGreaterThan(0);

9. Array Assertions

const fruits = [
    "Apple",
    "Orange",
    "Mango"
];

expect(fruits).toContain("Orange");
expect(fruits).toHaveLength(3);

10. Object Assertions

const employee = {

    id:1,
    name:"John"

};

expect(employee).toEqual({

    id:1,
    name:"John"

});

Real Login Example

import { test, expect } from '@playwright/test';

test('Verify Login', async ({ page }) => {

    await page.goto("https://example.com");

    await page.fill("#username","Admin");

    await page.fill("#password","admin123");

    await page.click("#login");

    await expect(page)
        .toHaveURL(/dashboard/);

    await expect(page.locator("h1"))
        .toContainText("Dashboard");

    await expect(page.locator("#logout"))
        .toBeVisible();

});

Assertion Timeouts

By default, Playwright retries assertions until the configured timeout.

Example:

await expect(locator).toBeVisible({
    timeout: 10000
});

This waits up to 10 seconds before failing.


Combining Assertions

await expect(page).toHaveTitle("Dashboard");

await expect(page).toHaveURL(/dashboard/);

await expect(page.locator("#welcome"))
    .toContainText("Welcome");

await expect(page.locator("#logout"))
    .toBeVisible();

await expect(page.locator(".product"))
    .toHaveCount(6);

Best Practices

  • Prefer Playwright locator assertions (toBeVisible, toHaveText, etc.) over manual value checks because they automatically wait for the expected condition.
  • Avoid using waitForTimeout() before assertions. Let Playwright’s built-in retry mechanism handle synchronization.
  • Use descriptive locators (getByRole, getByLabel, getByTestId) instead of fragile CSS or XPath selectors whenever possible.
  • Use expect.soft() when you want to collect multiple verification failures in a single test execution.
  • Keep assertions focused on one expected behavior to make failures easier to understand.
  • Add meaningful custom messages for business-critical validations.
  • Use regular expressions with toHaveURL() and toHaveText() when parts of the value are dynamic.
  • For visual testing, use screenshot assertions only for stable UI components to reduce flaky results.
  • Set assertion-specific timeouts only when an element legitimately requires more time to appear.

Common Mistakes

Using Manual Waits

❌

await page.waitForTimeout(5000);

expect(await page.title()).toBe("Dashboard");

✅

await expect(page).toHaveTitle("Dashboard");

Using textContent() Instead of Locator Assertions

❌

const text = await page.locator("h1").textContent();

expect(text).toBe("Dashboard");

✅

await expect(page.locator("h1"))
    .toHaveText("Dashboard");

Hard-Coding Dynamic URLs

❌

await expect(page).toHaveURL(
    "https://example.com/dashboard?id=123"
);

✅

await expect(page).toHaveURL(/dashboard/);

Summary

Playwright assertions are a core part of building stable and maintainable automated tests. Their automatic waiting, intelligent retries, and rich set of locator-specific assertions eliminate much of the flakiness found in traditional UI automation frameworks.

By mastering value assertions, locator assertions, page assertions, API assertions, soft assertions, polling, and visual assertions, you can create robust end-to-end tests that are easier to read, debug, and maintain. Adopting Playwright’s built-in assertion methods instead of manual waits or custom validation logic will result in faster, more reliable, and more professional automation suites.