CI Testing with Playwright
BugDrop can be tested in your CI/CD pipeline using Playwright. This guide covers everything from a quick verification check to a comprehensive test suite that validates functionality, accessibility, and configuration.
Quick Check
The fastest way to verify BugDrop is loading correctly is to check for its console message. When the widget initializes successfully, it logs:
[BugDrop] Widget initialized
You can check for this in a simple Playwright test:
import { test, expect } from '@playwright/test';
test('BugDrop loads', async ({ page }) => {
const messages: string[] = [];
page.on('console', (msg) => messages.push(msg.text()));
await page.goto('https://your-site.com');
await page.waitForTimeout(2000);
expect(messages.some((m) => m.includes('[BugDrop]'))).toBe(true);
});
Full Playwright Test Suite
Here is a comprehensive test file that validates BugDrop's functionality, accessibility (WCAG AA compliance), and configuration. You can copy this directly into your project:
import { test, expect } from "@playwright/test";
// ── EXPECTED configuration ──────────────────────────────────────
// Update these values to match YOUR script tag's data-* attributes.
const EXPECTED = {
theme: "light",
position: "bottom-right",
color: "#7c3aed",
showName: false,
requireName: false,
showEmail: false,
requireEmail: false,
buttonDismissible: false,
showRestore: true,
welcomeMessage: "Report a bug or request a feature",
};
const URL = "https://your-site.com"; // Replace with your site URL
// ── Helper: reach into the Shadow DOM ───────────────────────────
async function shadowEl(page, hostSel: string, innerSel: string) {
return page.evaluateHandle(
([h, s]) => {
const host = document.querySelector(h);
if (!host?.shadowRoot) throw new Error(`No shadow root on ${h}`);
const el = host.shadowRoot.querySelector(s);
if (!el) throw new Error(`${s} not found in shadow DOM`);
return el;
},
[hostSel, innerSel]
);
}
// ── Tests ───────────────────────────────────────────────────────
test.describe("BugDrop Widget", () => {
test.beforeEach(async ({ page }) => {
await page.goto(URL);
await page.waitForSelector("bug-drop-widget");
});
test("renders the floating button", async ({ page }) => {
const btn = await shadowEl(page, "bug-drop-widget", ".floating-btn");
expect(btn).toBeTruthy();
});
test("opens and closes the form", async ({ page }) => {
const btn = await shadowEl(page, "bug-drop-widget", ".floating-btn");
await (btn as any).click();
const form = await shadowEl(page, "bug-drop-widget", ".feedback-form");
expect(form).toBeTruthy();
const closeBtn = await shadowEl(page, "bug-drop-widget", ".close-btn");
await (closeBtn as any).click();
});
test("has correct position", async ({ page }) => {
const pos = await page.evaluate(() => {
const host = document.querySelector("bug-drop-widget");
const btn = host?.shadowRoot?.querySelector(".floating-btn");
if (!btn) throw new Error("Button not found");
const style = window.getComputedStyle(btn);
return { right: style.right, left: style.left, bottom: style.bottom };
});
if (EXPECTED.position === "bottom-right") {
expect(parseInt(pos.right)).toBeLessThan(100);
} else {
expect(parseInt(pos.left)).toBeLessThan(100);
}
});
test("uses correct accent color", async ({ page }) => {
const bgColor = await page.evaluate(() => {
const host = document.querySelector("bug-drop-widget");
const btn = host?.shadowRoot?.querySelector(".floating-btn");
if (!btn) throw new Error("Button not found");
return window.getComputedStyle(btn).backgroundColor;
});
expect(bgColor).toBeTruthy();
});
test("displays correct welcome message", async ({ page }) => {
const btn = await shadowEl(page, "bug-drop-widget", ".floating-btn");
await (btn as any).click();
const welcomeText = await page.evaluate(() => {
const host = document.querySelector("bug-drop-widget");
const welcome = host?.shadowRoot?.querySelector(".welcome-message, h2");
return welcome?.textContent?.trim();
});
expect(welcomeText).toContain(EXPECTED.welcomeMessage);
});
test("shows/hides name field based on config", async ({ page }) => {
const btn = await shadowEl(page, "bug-drop-widget", ".floating-btn");
await (btn as any).click();
const nameFieldVisible = await page.evaluate(() => {
const host = document.querySelector("bug-drop-widget");
const nameField = host?.shadowRoot?.querySelector(
'[name="name"], #name, .name-field'
);
if (!nameField) return false;
return window.getComputedStyle(nameField).display !== "none";
});
expect(nameFieldVisible).toBe(EXPECTED.showName);
});
test("shows/hides email field based on config", async ({ page }) => {
const btn = await shadowEl(page, "bug-drop-widget", ".floating-btn");
await (btn as any).click();
const emailFieldVisible = await page.evaluate(() => {
const host = document.querySelector("bug-drop-widget");
const emailField = host?.shadowRoot?.querySelector(
'[name="email"], #email, .email-field'
);
if (!emailField) return false;
return window.getComputedStyle(emailField).display !== "none";
});
expect(emailFieldVisible).toBe(EXPECTED.showEmail);
});
// ── Accessibility (WCAG AA) ─────────────────────────────────
test("button meets WCAG AA contrast ratio", async ({ page }) => {
const contrast = await page.evaluate(() => {
function luminance(r: number, g: number, b: number) {
const [rs, gs, bs] = [r, g, b].map((c) => {
c /= 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
function parseRgb(color: string) {
const match = color.match(/(\d+)/g);
return match ? match.map(Number) : [0, 0, 0];
}
const host = document.querySelector("bug-drop-widget");
const btn = host?.shadowRoot?.querySelector(".floating-btn");
if (!btn) return 0;
const style = window.getComputedStyle(btn);
const [r1, g1, b1] = parseRgb(style.backgroundColor);
const bgLum = luminance(r1, g1, b1);
const whiteLum = luminance(255, 255, 255);
const ratio =
(Math.max(bgLum, whiteLum) + 0.05) /
(Math.min(bgLum, whiteLum) + 0.05);
return ratio;
});
// WCAG AA requires 4.5:1 for normal text, 3:1 for large text
expect(contrast).toBeGreaterThanOrEqual(3);
});
test("form elements are keyboard accessible", async ({ page }) => {
// Open the form
await page.keyboard.press("Tab");
await page.keyboard.press("Enter");
// Verify form opened and is focusable
const formExists = await page.evaluate(() => {
const host = document.querySelector("bug-drop-widget");
return !!host?.shadowRoot?.querySelector(".feedback-form, form");
});
expect(formExists).toBe(true);
});
});
Installation and Setup
To run the tests, you need Playwright installed in your project:
# Install Playwright
npm install --save-dev @playwright/test
# Install browsers
npx playwright install
Create or update your playwright.config.ts:
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
timeout: 30000,
use: {
baseURL: 'https://your-site.com',
},
});
Then run the tests:
npx playwright test
What the Tests Check
The test suite covers three areas:
Functional Tests
- Renders the floating button -- Verifies the widget loads and the button appears in the Shadow DOM
- Opens and closes the form -- Clicks the button, verifies the form appears, then closes it
- Correct position -- Validates the button is in the expected corner (bottom-right or bottom-left)
- Correct accent color -- Checks the button's background color
- Welcome message -- Opens the form and verifies the correct welcome text is displayed
- Name/email field visibility -- Confirms fields show or hide based on your configuration
Accessibility Tests (WCAG AA)
- Contrast ratio -- Calculates the luminance contrast ratio between the button's background color and white text, ensuring it meets WCAG AA standards (3:1 for large text)
- Keyboard accessibility -- Verifies the form can be opened and navigated using only the keyboard
Configuration Verification
The EXPECTED object at the top of the test file defines your expected configuration. Update it to match your actual data-* attributes:
const EXPECTED = {
theme: "light", // data-theme
position: "bottom-right", // data-position
color: "#7c3aed", // data-color
showName: false, // data-show-name
requireName: false, // data-require-name
showEmail: false, // data-show-email
requireEmail: false, // data-require-email
buttonDismissible: false, // data-button-dismissible
showRestore: true, // data-show-restore
welcomeMessage: "Report a bug or request a feature", // data-welcome
};
If any of these values do not match what the widget actually renders, the corresponding test will fail -- catching configuration drift between your script tag and your expected behavior.
Running in CI
Add the test to your CI workflow (GitHub Actions example):
- name: Install Playwright
run: npx playwright install --with-deps chromium
- name: Run BugDrop tests
run: npx playwright test e2e/bugdrop.spec.ts
Next Steps
- Configure the widget with data attributes
- Review security and rate limiting
- Pin your widget version for stable CI runs