diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 08ab610..f8b5209 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -55,7 +55,14 @@ jobs:
- run: npm ci
- name: Install Playwright browsers
- run: npx playwright install --with-deps chromium firefox webkit
+ # PRs only need chromium for the chromium-desktop project; pushes to
+ # main run the full cross-browser matrix.
+ run: |
+ if [ "${{ github.event_name }}" = "pull_request" ]; then
+ npx playwright install --with-deps chromium
+ else
+ npx playwright install --with-deps chromium firefox webkit
+ fi
- name: Build app in mock mode
run: npm run build
@@ -68,12 +75,20 @@ jobs:
# workflow (workflow_dispatch). Until they are committed, run the
# functional e2e tests and skip screenshot assertions instead of
# silently regenerating (and so never actually comparing) them.
+ # PRs only run chromium-desktop for fast feedback; pushes to main
+ # keep running the full browser/device project matrix.
run: |
+ if [ "${{ github.event_name }}" = "pull_request" ]; then
+ PROJECT_ARGS="--project=chromium-desktop"
+ else
+ PROJECT_ARGS=""
+ fi
+
if find e2e/__screenshots__ -name '*.png' | grep -q .; then
- npx playwright test
+ npx playwright test $PROJECT_ARGS
else
echo "::warning::No visual baselines committed; skipping screenshot assertions."
- npx playwright test --ignore-snapshots
+ npx playwright test --ignore-snapshots $PROJECT_ARGS
fi
env:
NEXT_PUBLIC_STREAM_CONTRACT_ID_TESTNET: ''
diff --git a/__tests__/components/clear-local-data.test.tsx b/__tests__/components/clear-local-data.test.tsx
new file mode 100644
index 0000000..031b4b1
--- /dev/null
+++ b/__tests__/components/clear-local-data.test.tsx
@@ -0,0 +1,84 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { ClearLocalData } from '@/components/settings/clear-local-data'
+
+const mockToastSuccess = vi.fn()
+
+vi.mock('sonner', () => ({
+ toast: {
+ success: (...args: any[]) => mockToastSuccess(...args),
+ },
+}))
+
+describe('ClearLocalData', () => {
+ beforeEach(() => {
+ vi.useFakeTimers()
+ window.localStorage.clear()
+ mockToastSuccess.mockClear()
+ })
+
+ afterEach(() => {
+ vi.useRealTimers()
+ vi.restoreAllMocks()
+ })
+
+ it('requires a confirmation step before clearing', () => {
+ window.localStorage.setItem('flowstar:streams', '[]')
+ render()
+
+ expect(screen.queryByText("Are you sure? This can't be undone.")).not.toBeInTheDocument()
+
+ screen.getByText('Clear local data').click()
+
+ expect(screen.getByText("Are you sure? This can't be undone.")).toBeInTheDocument()
+ // Clicking the initial button only arms the confirmation; nothing is removed yet.
+ expect(window.localStorage.getItem('flowstar:streams')).toBe('[]')
+ })
+
+ it('only removes keys prefixed with flowstar, keeping other keys like walletId', () => {
+ window.localStorage.setItem('flowstar:streams', '[]')
+ window.localStorage.setItem('flowstar_notifications', '{}')
+ window.localStorage.setItem('flowstar-network', 'testnet')
+ window.localStorage.setItem('walletId', 'abc123')
+ window.localStorage.setItem('unrelatedKey', 'keep-me')
+
+ render()
+
+ screen.getByText('Clear local data').click()
+ screen.getByText('Confirm clear').click()
+
+ expect(window.localStorage.getItem('flowstar:streams')).toBeNull()
+ expect(window.localStorage.getItem('flowstar_notifications')).toBeNull()
+ expect(window.localStorage.getItem('flowstar-network')).toBeNull()
+ expect(window.localStorage.getItem('walletId')).toBe('abc123')
+ expect(window.localStorage.getItem('unrelatedKey')).toBe('keep-me')
+ })
+
+ it('shows a success toast reporting how many items were cleared', () => {
+ window.localStorage.setItem('flowstar:streams', '[]')
+ window.localStorage.setItem('flowstar_notifications', '{}')
+ window.localStorage.setItem('walletId', 'abc123')
+
+ render()
+
+ screen.getByText('Clear local data').click()
+ screen.getByText('Confirm clear').click()
+
+ expect(mockToastSuccess).toHaveBeenCalledWith(
+ 'Cleared 2 items of local data',
+ expect.objectContaining({ description: 'Reloading…' }),
+ )
+ })
+
+ it('cancel button dismisses the confirmation without clearing anything', () => {
+ window.localStorage.setItem('flowstar:streams', '[]')
+ render()
+
+ screen.getByText('Clear local data').click()
+ screen.getByText('Cancel').click()
+
+ expect(screen.queryByText("Are you sure? This can't be undone.")).not.toBeInTheDocument()
+ expect(window.localStorage.getItem('flowstar:streams')).toBe('[]')
+ expect(mockToastSuccess).not.toHaveBeenCalled()
+ })
+})
diff --git a/e2e/contract-integration.spec.ts b/e2e/contract-integration.spec.ts
index 438ae5f..1cdf42e 100644
--- a/e2e/contract-integration.spec.ts
+++ b/e2e/contract-integration.spec.ts
@@ -1,5 +1,6 @@
import { test, expect, type Page } from '@playwright/test'
+// ─── Shared helper: inject xBull mock so the wallet-gated stream detail page renders ─
// The create form is wrapped in `RequireWallet`, so without a connected
// wallet it only renders a connect prompt instead of the real form fields.
// Mirrors the helper used in e2e/accessibility.spec.ts.
@@ -54,9 +55,9 @@ test.describe('Contract Integration Tests', () => {
})
test('should display share buttons on stream detail', async ({ page }) => {
- await page.goto('/app/stream/test-id')
- const shareBtn = page.locator('button:has-text("Share")')
- // Should be clickable even if stream doesn't exist
+ await withWallet(page)
+ await page.goto('/app/stream/1')
+ const shareBtn = page.getByRole('button', { name: 'Share stream' })
await expect(shareBtn).toBeVisible()
})
})
diff --git a/playwright.config.ts b/playwright.config.ts
index a3d41a6..92607a5 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -5,7 +5,7 @@ export default defineConfig({
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
- workers: process.env.CI ? 1 : undefined,
+ workers: process.env.CI ? 2 : undefined,
reporter: "html",
outputDir: "test-results",
snapshotDir: "./e2e/__screenshots__",