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:
.modalexists in DOM but isn't visible until JS adds.show - Dropdown state: The dropdown menu appears/disappears via
.showclass toggling - Tab switching: Active tab state tracked via both classes and
aria-selected - Form validation: Bootstrap adds
.is-valid/.is-invalidclasses 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")))
Modal with Form
# 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:
Modal
// 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()
})
Dropdown
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