Bootstrap Testing: How to Test Bootstrap Components (2026)

Bootstrap Testing: How to Test Bootstrap Components (2026)

Testing Bootstrap components requires handling JavaScript-driven behavior — modals use .show/.hide classes, dropdowns toggle .show, tabs use aria-selected. Use explicit waits for animations: wait for .modal.show (not just .modal). For unit tests with React Bootstrap, use React Testing Library — screen.getByRole('dialog') for modals, screen.getByRole('tab') for tabs. Avoid testing Bootstrap internals; test your application's behavior instead.

Key Takeaways

Test Bootstrap behavior, not Bootstrap itself. Bootstrap is tested by the Bootstrap team. Your tests should verify what your application does with Bootstrap components — does clicking "Delete" open the confirmation modal? Does submitting invalid data show the right error state?

Wait for Bootstrap animations. Bootstrap modals animate in/out. driver.find_element(By.CSS_SELECTOR, '.modal') often succeeds before the modal is visible. Wait for .modal.show (both classes), or use EC.visibility_of_element_located.

Bootstrap class changes are state changes. .active, .show, .collapsed, .disabled are testable state indicators. After clicking a tab, verify [data-bs-target].active appears on the correct tab.

data-bs-* attributes are reliable selectors. [data-bs-toggle="modal"], [data-bs-target="#myModal"], [data-bs-dismiss="modal"] are Bootstrap's own attributes — more stable than custom class names.

For React Bootstrap, use React Testing Library with getByRole. getByRole('dialog') for modals, getByRole('tab') for tabs, getByRole('button', { name: 'Close' }) for close buttons.

Why Bootstrap Testing Has Unique Challenges

Bootstrap components use JavaScript to toggle CSS classes, manage ARIA attributes, and handle animations. Standard DOM-presence checks often succeed before components are actually visible or interactive.

Key challenges:

  • Modal animations: .modal exists in DOM but isn't visible until JS adds .show
  • Dropdown state: The dropdown menu appears/disappears via .show class toggling
  • Tab switching: Active tab state tracked via both classes and aria-selected
  • Form validation: Bootstrap adds .is-valid/.is-invalid classes to inputs
  • Carousel timing: Slides transition with CSS animations

Testing Bootstrap Modals (Selenium)

Basic Modal Interaction

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.implicitly_wait(0)
wait = WebDriverWait(driver, 10)

driver.get("https://example.com/page-with-modal")

# Click button that triggers modal
trigger = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-bs-toggle='modal']")))
trigger.click()

# ❌ Wait for .modal to exist — it might already be in DOM, hidden
# wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".modal")))

# ✅ Wait for .modal.show — both classes mean modal is visible
modal = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".modal")))

# Interact with modal content
modal_title = modal.find_element(By.CSS_SELECTOR, ".modal-title")
assert modal_title.text == "Confirm Delete"

confirm_btn = modal.find_element(By.CSS_SELECTOR, ".btn-danger")
confirm_btn.click()

# Wait for modal to close
wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, ".modal")))
# Open modal
wait.until(EC.element_to_be_clickable((By.ID, "edit-user-btn"))).click()

# Wait for modal
modal = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, "#editUserModal.show")))

# Fill form inside modal
name_input = modal.find_element(By.ID, "userName")
name_input.clear()
name_input.send_keys("New Name")

email_input = modal.find_element(By.ID, "userEmail")
email_input.clear()
email_input.send_keys("new@example.com")

# Save
save_btn = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#editUserModal .btn-primary")))
save_btn.click()

# Wait for modal to close and verify update
wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, "#editUserModal")))
user_name_cell = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".user-name")))
assert "New Name" in user_name_cell.text

Close Modal with X Button

# Open modal
trigger.click()
modal = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".modal")))

# Close with X button (data-bs-dismiss="modal")
close_btn = modal.find_element(By.CSS_SELECTOR, "[data-bs-dismiss='modal']")
close_btn.click()

wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, ".modal")))

Testing Bootstrap Dropdowns (Selenium)

driver.get("https://example.com/nav")

# Click to open dropdown
dropdown_toggle = wait.until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-bs-toggle='dropdown']"))
)
dropdown_toggle.click()

# Wait for dropdown menu to appear (Bootstrap adds .show class)
dropdown_menu = wait.until(
    EC.visibility_of_element_located((By.CSS_SELECTOR, ".dropdown-menu.show"))
)

# Click a dropdown item
settings_item = dropdown_menu.find_element(By.LINK_TEXT, "Settings")
settings_item.click()

wait.until(EC.url_contains("/settings"))

Verify Dropdown Items

# Open dropdown
toggle.click()
menu = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".dropdown-menu.show")))

# Get all items
items = menu.find_elements(By.CSS_SELECTOR, ".dropdown-item")
item_texts = [item.text for item in items]
assert "Profile" in item_texts
assert "Settings" in item_texts
assert "Sign Out" in item_texts

# Check disabled items
disabled = menu.find_elements(By.CSS_SELECTOR, ".dropdown-item.disabled")

Testing Bootstrap Tabs (Selenium)

driver.get("https://example.com/dashboard")

# Wait for tabs to load
tabs = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".nav-tabs .nav-link")))

# Verify default active tab
active_tab = driver.find_element(By.CSS_SELECTOR, ".nav-tabs .nav-link.active")
assert active_tab.text == "Overview"

# Click "Details" tab
details_tab = driver.find_element(By.CSS_SELECTOR, ".nav-link[data-bs-target='#details']")
details_tab.click()

# Wait for tab to become active
wait.until(lambda d: "active" in details_tab.get_attribute("class"))

# Verify correct tab content is shown
details_pane = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, "#details.active")))
assert details_pane.is_displayed()

# Verify previous pane is hidden
overview_pane = driver.find_element(By.ID, "overview")
assert not overview_pane.is_displayed()

Testing Bootstrap Form Validation

driver.get("https://example.com/register")

# Submit empty form to trigger validation
submit = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button[type='submit']")))
submit.click()

# Bootstrap adds .is-invalid to invalid fields and .was-validated to form
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "form.was-validated")))

invalid_fields = driver.find_elements(By.CSS_SELECTOR, ".form-control.is-invalid")
assert len(invalid_fields) > 0

# Check Bootstrap feedback messages
feedback = driver.find_elements(By.CSS_SELECTOR, ".invalid-feedback")
feedback_texts = [f.text for f in feedback if f.is_displayed()]
assert any("required" in text.lower() for text in feedback_texts)

# Fill valid data
email_input = driver.find_element(By.ID, "email")
email_input.send_keys("valid@example.com")

# After input, the field should become valid
wait.until(lambda d: "is-invalid" not in email_input.get_attribute("class"))
assert "is-valid" in email_input.get_attribute("class")

Testing Bootstrap Alerts / Toasts

# Trigger an action that shows a toast
save_btn = wait.until(EC.element_to_be_clickable((By.ID, "save-settings")))
save_btn.click()

# Wait for Bootstrap toast/alert to appear
toast = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".toast.show")))
assert "saved" in toast.text.lower()

# Wait for auto-dismiss (Bootstrap toasts auto-hide after 5s by default)
wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, ".toast.show")))

React Bootstrap Testing (React Testing Library)

For React projects using react-bootstrap:

// src/components/DeleteConfirmModal.test.tsx
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { DeleteConfirmModal } from './DeleteConfirmModal'

describe('DeleteConfirmModal', () => {
  it('opens when trigger is clicked', async () => {
    render(<DeleteConfirmModal itemName="Widget #42" onDelete={vi.fn()} />)

    // Modal not in DOM initially
    expect(screen.queryByRole('dialog')).not.toBeInTheDocument()

    // Click trigger
    await userEvent.click(screen.getByRole('button', { name: 'Delete' }))

    // Modal is visible
    expect(screen.getByRole('dialog')).toBeInTheDocument()
    expect(screen.getByText('Delete Widget #42?')).toBeInTheDocument()
  })

  it('calls onDelete when confirmed', async () => {
    const handleDelete = vi.fn()
    render(<DeleteConfirmModal itemName="Widget #42" onDelete={handleDelete} />)

    await userEvent.click(screen.getByRole('button', { name: 'Delete' }))
    await userEvent.click(screen.getByRole('button', { name: 'Confirm Delete' }))

    expect(handleDelete).toHaveBeenCalledOnce()
  })

  it('closes without deleting when cancelled', async () => {
    const handleDelete = vi.fn()
    render(<DeleteConfirmModal itemName="Widget #42" onDelete={handleDelete} />)

    await userEvent.click(screen.getByRole('button', { name: 'Delete' }))
    await userEvent.click(screen.getByRole('button', { name: 'Cancel' }))

    expect(handleDelete).not.toHaveBeenCalled()
  })
})

Tabs

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { SettingsTabs } from './SettingsTabs'

it('switches between tabs', async () => {
  render(<SettingsTabs />)

  // Initial tab active
  expect(screen.getByRole('tab', { name: 'Profile', selected: true })).toBeInTheDocument()
  expect(screen.getByText('Profile settings content')).toBeInTheDocument()

  // Click security tab
  await userEvent.click(screen.getByRole('tab', { name: 'Security' }))

  // Security tab now active
  expect(screen.getByRole('tab', { name: 'Security', selected: true })).toBeInTheDocument()
  expect(screen.getByText('Security settings content')).toBeInTheDocument()
  expect(screen.queryByText('Profile settings content')).not.toBeInTheDocument()
})
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { UserMenu } from './UserMenu'

it('shows dropdown items on click', async () => {
  render(<UserMenu username="Jane" />)

  // Dropdown not visible initially
  expect(screen.queryByText('Sign Out')).not.toBeInTheDocument()

  // Open dropdown
  await userEvent.click(screen.getByRole('button', { name: 'Jane' }))

  // Items visible
  expect(screen.getByText('Profile')).toBeInTheDocument()
  expect(screen.getByText('Settings')).toBeInTheDocument()
  expect(screen.getByText('Sign Out')).toBeInTheDocument()
})

Form Validation

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { RegisterForm } from './RegisterForm'

it('shows validation errors on empty submit', async () => {
  render(<RegisterForm onSubmit={vi.fn()} />)

  await userEvent.click(screen.getByRole('button', { name: 'Register' }))

  // Bootstrap invalid-feedback messages
  expect(screen.getByText('Email is required')).toBeInTheDocument()
  expect(screen.getByText('Password is required')).toBeInTheDocument()
})

it('clears error when valid input entered', async () => {
  render(<RegisterForm onSubmit={vi.fn()} />)

  await userEvent.click(screen.getByRole('button', { name: 'Register' }))
  expect(screen.getByText('Email is required')).toBeInTheDocument()

  await userEvent.type(screen.getByLabelText('Email'), 'valid@example.com')
  expect(screen.queryByText('Email is required')).not.toBeInTheDocument()
})

Testing Bootstrap with Playwright

Playwright has built-in element waiting and more natural Bootstrap interaction:

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

test('Bootstrap modal workflow', async ({ page }) => {
  await page.goto('https://example.com/products')

  // Click delete button
  await page.click('[data-bs-toggle="modal"][data-bs-target="#deleteModal"]')

  // Modal becomes visible
  await expect(page.locator('.modal')).toBeVisible()
  await expect(page.locator('.modal-title')).toHaveText('Confirm Delete')

  // Confirm
  await page.click('.modal .btn-danger')

  // Modal closes
  await expect(page.locator('.modal')).not.toBeVisible()
})

test('Bootstrap tabs', async ({ page }) => {
  await page.goto('https://example.com/dashboard')

  // Click tab
  await page.click('[data-bs-target="#details"]')

  // Verify tab active state
  await expect(page.locator('[data-bs-target="#details"]')).toHaveClass(/active/)

  // Verify correct content shown
  await expect(page.locator('#details')).toBeVisible()
  await expect(page.locator('#overview')).not.toBeVisible()
})

Common Bootstrap CSS Selectors Reference

# Modals
".modal"                    # Modal container
".modal.show"               # Open modal (use for waits)
".modal-dialog"             # Dialog box
".modal-title"              # Title text
"[data-bs-dismiss='modal']" # Close button

# Dropdowns
".dropdown-toggle"              # Toggle button
".dropdown-menu"                # Menu container
".dropdown-menu.show"           # Open menu
".dropdown-item"                # Menu item
".dropdown-item.active"         # Selected item
".dropdown-item.disabled"       # Disabled item

# Tabs/Nav
".nav-link"                     # Tab/nav link
".nav-link.active"              # Active tab
".tab-pane"                     # Tab content
".tab-pane.active"              # Visible tab content

# Forms
".form-control.is-valid"        # Valid input
".form-control.is-invalid"      # Invalid input
".valid-feedback"               # Success message
".invalid-feedback"             # Error message
"form.was-validated"            # Form after validation attempt

# Alerts
".alert.show"                   # Visible alert
".alert-success"                # Success alert
".alert-danger"                 # Error alert
".toast.show"                   # Visible toast

# Accordion
".accordion-collapse.show"      # Open accordion panel
".accordion-button.collapsed"   # Collapsed header
".accordion-button:not(.collapsed)"  # Expanded header

The Alternative: AI-Driven Bootstrap Testing

Bootstrap component testing requires detailed knowledge of Bootstrap's internal state management (which classes indicate which states). HelpMeTest tests Bootstrap apps the way a human would:

Go to https://example.com/products
Click the "Delete" button next to "Product #42"
Verify a confirmation dialog appears asking to confirm deletion
Click "Confirm Delete" in the dialog
Verify the dialog closes
Verify "Product #42" no longer appears in the list

The AI recognizes the Bootstrap modal as a "confirmation dialog" visually — it doesn't need to know about .modal.show or data-bs-dismiss. When Bootstrap 6 changes class names, tests don't break.

Bootstrap-specific selectors matter when:

  • Testing Bootstrap component behavior in isolation
  • Verifying exact CSS state transitions
  • Large Selenium/Playwright test suites with Bootstrap apps
  • Need to verify accessibility attributes (ARIA) on Bootstrap components

When to use AI-driven testing:

  • Full application E2E testing
  • Non-technical team members write tests
  • Bootstrap version upgrades frequently change internals

Summary

Key patterns for Bootstrap testing:

# Modal — wait for .show class (indicates visible state)
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".modal")))
modal = driver.find_element(By.CSS_SELECTOR, ".modal")

# Dropdown — wait for .show on menu
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".dropdown-menu.show")))

# Tab — verify active state after click
tab = driver.find_element(By.CSS_SELECTOR, ".nav-link[data-bs-target='#tab-id']")
tab.click()
wait.until(lambda d: "active" in tab.get_attribute("class"))

# Form validation — check Bootstrap state classes
invalid = driver.find_elements(By.CSS_SELECTOR, ".form-control.is-invalid")
feedback = driver.find_elements(By.CSS_SELECTOR, ".invalid-feedback")

# React Bootstrap (RTL) — use semantic roles
screen.getByRole('dialog')     # Modal
screen.getByRole('tab')        # Tab
screen.getByRole('button', { name: 'Close' })  # Close button

Read more