diff --git a/e2e/demo.html b/e2e/demo.html
new file mode 100644
index 0000000..43d4d16
--- /dev/null
+++ b/e2e/demo.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/e2e/smoke.test.ts b/e2e/smoke.test.ts
new file mode 100644
index 0000000..a487f7e
--- /dev/null
+++ b/e2e/smoke.test.ts
@@ -0,0 +1,29 @@
+import { expect, test } from '@playwright/test';
+
+declare global {
+ interface Window {
+ c: { next(): void; readonly index: number };
+ }
+}
+
+const scrollLeft = () => document.getElementById('t')?.scrollLeft ?? 0;
+
+test('next() scrolls the track and updates index', async ({ page }) => {
+ await page.goto('/e2e/demo.html');
+ const before = await page.evaluate(scrollLeft);
+ await page.evaluate(() => window.c.next());
+ await page.waitForTimeout(500); // smooth scroll settle
+ const after = await page.evaluate(scrollLeft);
+ expect(after).toBeGreaterThan(before);
+ await expect.poll(() => page.evaluate(() => window.c.index)).toBe(1);
+});
+
+test('clicking a dot navigates (fallback path on Firefox)', async ({ page, browserName }) => {
+ // dots() self-gates: on Chromium native ::scroll-marker is present, so it no-ops.
+ // Firefox lacks native markers — the JS fallback is the path that must pass.
+ test.skip(browserName !== 'firefox', 'dots() no-ops where native ::scroll-marker exists');
+ await page.goto('/e2e/demo.html');
+ await page.locator('#dots button').nth(2).click();
+ await page.waitForTimeout(500);
+ await expect.poll(() => page.evaluate(() => window.c.index)).toBe(2);
+});
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 0000000..8505a42
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,21 @@
+import { defineConfig, devices } from '@playwright/test';
+
+const isCI = !!process.env.CI;
+
+export default defineConfig({
+ testDir: 'e2e',
+ testMatch: /.*\.test\.ts$/,
+ forbidOnly: isCI,
+ retries: isCI ? 2 : 0,
+ reporter: isCI ? [['html', { open: 'never' }], ['github']] : [['list']],
+ use: { baseURL: 'http://localhost:5173' },
+ projects: [
+ { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
+ { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
+ ],
+ webServer: {
+ command: 'yarn dlx http-server . -p 5173 -s',
+ port: 5173,
+ reuseExistingServer: !isCI,
+ },
+});