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__",