diff --git a/app/assets/css/main.css b/app/assets/css/main.css index 4b39e60..4167651 100644 --- a/app/assets/css/main.css +++ b/app/assets/css/main.css @@ -273,6 +273,14 @@ p a, blockquote a { min-width: 0; } +/* ---- Nuxt UI placeholder contrast ---- + Default Nuxt UI placeholder uses `text-dimmed` (#a6a09b) which fails WCAG + AA on cream and white backgrounds (≈2.4:1). Override globally to --text-dim + so USelect/USelectMenu placeholders meet the 4.5:1 ratio. */ +[data-slot="placeholder"] { + color: var(--text-dim); +} + /* ---- SHARED USelectMenu STYLES ---- Apply via: Classes are global (not scoped) because Nuxt UI portals the popup content to body. */ diff --git a/app/pages/join.vue b/app/pages/join.vue index 32cc816..5a256e8 100644 --- a/app/pages/join.vue +++ b/app/pages/join.vue @@ -1017,6 +1017,7 @@ onUnmounted(() => { .checkbox-label a, .checkbox-label :deep(a) { color: var(--candle); + text-decoration: underline; } /* ---- ERROR & SUCCESS BOXES ---- */ diff --git a/app/pages/member/profile.vue b/app/pages/member/profile.vue index 0679c48..2f7f54f 100644 --- a/app/pages/member/profile.vue +++ b/app/pages/member/profile.vue @@ -712,11 +712,11 @@ useHead({ .posts-empty-link { color: var(--candle); - text-decoration: none; + text-decoration: underline; } -.posts-empty-link:hover { - text-decoration: underline; +.timezone-select :deep([data-slot="placeholder"]) { + color: var(--text-dim); } .posts-list { diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-desktop-chromium-darwin.png index f1a80e7..994e117 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-desktop-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-mobile-auth-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-mobile-auth-chromium-darwin.png index 0765129..df45026 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-mobile-auth-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-mobile-auth-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-mobile-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-mobile-chromium-darwin.png index 8c4ad03..76301e1 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-mobile-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/about-mobile-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/admin-dashboard-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/admin-dashboard-desktop-chromium-darwin.png index 8aeacc2..b2a406b 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/admin-dashboard-desktop-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/admin-dashboard-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/admin-members-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/admin-members-desktop-chromium-darwin.png index 7838ee3..8da2b9f 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/admin-members-desktop-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/admin-members-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/coming-soon-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/coming-soon-desktop-chromium-darwin.png index 6e601d3..e6e2013 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/coming-soon-desktop-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/coming-soon-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/coming-soon-mobile-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/coming-soon-mobile-chromium-darwin.png index 4cc7b93..d4b7b0e 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/coming-soon-mobile-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/coming-soon-mobile-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/connections-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/connections-desktop-chromium-darwin.png index b7875d9..9aef789 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/connections-desktop-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/connections-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/connections-mobile-auth-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/connections-mobile-auth-chromium-darwin.png index 4fffa70..72e1d0a 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/connections-mobile-auth-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/connections-mobile-auth-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/events-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/events-desktop-chromium-darwin.png index 40d1ee5..3cac92f 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/events-desktop-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/events-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/events-mobile-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/events-mobile-chromium-darwin.png index 2c631f7..babc01e 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/events-mobile-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/events-mobile-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/home-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/home-desktop-chromium-darwin.png index 880a306..264d678 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/home-desktop-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/home-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/home-mobile-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/home-mobile-chromium-darwin.png index 46c53d7..3a29d00 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/home-mobile-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/home-mobile-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/join-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/join-desktop-chromium-darwin.png index 90caa26..cc7d38b 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/join-desktop-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/join-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/join-mobile-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/join-mobile-chromium-darwin.png index c7a4e0e..4ef6dec 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/join-mobile-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/join-mobile-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-account-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-account-desktop-chromium-darwin.png index 8f1a75e..676e384 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-account-desktop-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-account-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-dashboard-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-dashboard-desktop-chromium-darwin.png index 280b1f5..3262023 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-dashboard-desktop-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-dashboard-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-dashboard-mobile-auth-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-dashboard-mobile-auth-chromium-darwin.png index 5af2e0b..3016712 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-dashboard-mobile-auth-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-dashboard-mobile-auth-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-profile-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-profile-desktop-chromium-darwin.png index 44c77f9..a810ac5 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-profile-desktop-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-profile-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-profile-mobile-auth-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-profile-mobile-auth-chromium-darwin.png index b6edc45..da76ae2 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-profile-mobile-auth-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/member-profile-mobile-auth-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-desktop-chromium-darwin.png index 132c9ea..aa7d97d 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-desktop-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-detail-desktop-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-detail-desktop-chromium-darwin.png index 6f05986..af1f86c 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-detail-desktop-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-detail-desktop-chromium-darwin.png differ diff --git a/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-mobile-chromium-darwin.png b/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-mobile-chromium-darwin.png index c8f1848..4ef6dec 100644 Binary files a/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-mobile-chromium-darwin.png and b/e2e/__screenshots__/visual/pages.spec.js-snapshots/members-mobile-chromium-darwin.png differ diff --git a/e2e/admin-board-channels.spec.js b/e2e/admin-board-channels.spec.js index 7a2d1e7..91111cf 100644 --- a/e2e/admin-board-channels.spec.js +++ b/e2e/admin-board-channels.spec.js @@ -11,6 +11,7 @@ test.describe('Admin board channels page', () => { test('create, edit, and delete a channel', async ({ adminPage }) => { await adminPage.goto('/admin/board-channels') + await adminPage.waitForLoadState('networkidle') await expect(adminPage.getByRole('heading', { name: 'Board Channels' })).toBeVisible({ timeout: 15000, }) @@ -18,14 +19,14 @@ test.describe('Admin board channels page', () => { const suffix = Date.now().toString().slice(-6) const channelName = `e2e-channel-${suffix}` const editedName = `e2e-channel-${suffix}-edited` - const slackId = `C${suffix}XYZ` // --- Create --- + // Create flow only takes a name; the Slack channel ID is auto-assigned on + // creation and only becomes editable in the Edit modal. await adminPage.getByRole('button', { name: '+ New Channel' }).click() await expect(adminPage.getByRole('heading', { name: 'New Channel' })).toBeVisible() - await adminPage.locator('input[placeholder="e.g., #coop-formation"]').fill(channelName) - await adminPage.locator('input[placeholder="C0123456789"]').fill(slackId) + await adminPage.locator('input[placeholder="e.g., coop-formation"]').fill(channelName) // Select the first available cooperative tag if any are present const firstTagCheckbox = adminPage.locator('.tag-select input[type="checkbox"]').first() @@ -44,7 +45,7 @@ test.describe('Admin board channels page', () => { await row.getByRole('button', { name: 'Edit' }).click() await expect(adminPage.getByRole('heading', { name: 'Edit Channel' })).toBeVisible() - const nameInput = adminPage.locator('input[placeholder="e.g., #coop-formation"]') + const nameInput = adminPage.locator('input[placeholder="e.g., coop-formation"]') await nameInput.fill(editedName) await adminPage.getByRole('button', { name: 'Save Changes' }).click() diff --git a/e2e/auth.spec.js b/e2e/auth.spec.js index 5b0daca..f7c60f0 100644 --- a/e2e/auth.spec.js +++ b/e2e/auth.spec.js @@ -44,6 +44,7 @@ test.describe('Authentication flows', () => { test('logout clears auth', async ({ page }) => { await loginAsAdmin(page) await page.goto('/admin') + await page.waitForLoadState('networkidle') await expect(page.locator('.admin-tag')).toBeVisible({ timeout: 15000 }) // Set up response listener BEFORE clicking to avoid race diff --git a/e2e/board.spec.js b/e2e/board.spec.js index 6f2fe7e..945cd4d 100644 --- a/e2e/board.spec.js +++ b/e2e/board.spec.js @@ -9,6 +9,7 @@ test.describe('Board page', () => { test('clicking New Post reveals the form', async ({ memberPage }) => { await memberPage.goto('/board') + await memberPage.waitForLoadState('networkidle') await expect(memberPage.getByRole('button', { name: '+ New Post' }).first()).toBeVisible({ timeout: 15000, }) @@ -40,6 +41,7 @@ test.describe('Board page', () => { test('create, edit, and delete own post', async ({ memberPage }) => { await memberPage.goto('/board') + await memberPage.waitForLoadState('networkidle') await expect(memberPage.getByRole('button', { name: '+ New Post' }).first()).toBeVisible({ timeout: 15000, }) @@ -55,7 +57,7 @@ test.describe('Board page', () => { await memberPage.locator('#post-title').fill(originalTitle) await memberPage.locator('#post-seeking').fill('Playwright test seeking text') - await memberPage.getByRole('button', { name: 'Post' }).click() + await memberPage.getByRole('button', { name: 'Post', exact: true }).click() await expect(memberPage.getByRole('heading', { name: originalTitle })).toBeVisible({ timeout: 10000, @@ -75,10 +77,10 @@ test.describe('Board page', () => { timeout: 10000, }) - // --- Delete (confirm dialog) --- - memberPage.once('dialog', (dialog) => dialog.accept()) + // --- Delete (in-card two-step confirm; not a native dialog) --- const editedCard = memberPage.locator('article.board-post', { hasText: editedTitle }) await editedCard.getByRole('button', { name: 'Delete' }).click() + await editedCard.getByRole('button', { name: 'Confirm' }).click() await expect(memberPage.getByRole('heading', { name: editedTitle })).not.toBeVisible({ timeout: 10000, diff --git a/e2e/join-flow.spec.js b/e2e/join-flow.spec.js index 919e9ec..0062b95 100644 --- a/e2e/join-flow.spec.js +++ b/e2e/join-flow.spec.js @@ -68,8 +68,12 @@ test.describe('Join page — member signup flow', () => { await page.locator('#join-name').fill('Test User') await expect(page.locator('.form-submit')).toBeDisabled() - // Fill email too — now all fields are populated and button should be enabled + // Fill email — agreement still unchecked, so still disabled await page.locator('#join-email').fill('incomplete-test@example.com') + await expect(page.locator('.form-submit')).toBeDisabled() + + // Check the Community Guidelines agreement — now all required fields satisfied + await page.getByRole('checkbox', { name: /Community Guidelines/ }).check() await expect(page.locator('.form-submit')).toBeEnabled() }) @@ -83,8 +87,9 @@ test.describe('Join page — member signup flow', () => { await page.locator('#join-name').fill('E2E Test User') await page.locator('#join-email').fill(uniqueEmail) await page.locator('#circle-community').check({ force: true }) - await page.locator('#join-contribution').click() - await page.getByRole('option', { name: '$0/mo' }).click() + // Contribution is now a numeric input with preset chips, not a select + await page.locator('#join-contribution').fill('0') + await page.getByRole('checkbox', { name: /Community Guidelines/ }).check() await expect(page.locator('.form-submit')).toBeEnabled() @@ -93,8 +98,10 @@ test.describe('Join page — member signup flow', () => { await page.locator('.form-submit').click() - // Free tier creates subscription then shows confirmation (step 3) - await expect(page.locator('.success-box')).toBeVisible({ timeout: 15000 }) + // Free tier flips the SignupFlowOverlay into its success state + await expect( + page.getByRole('heading', { name: 'Welcome to Ghost Guild!' }) + ).toBeVisible({ timeout: 15000 }) }) test('duplicate email shows error', async ({ page }) => { @@ -109,12 +116,13 @@ test.describe('Join page — member signup flow', () => { await page.locator('#join-name').fill('Dup Test User') await page.locator('#join-email').fill(duplicateEmail) await page.locator('#circle-community').check({ force: true }) - await page.locator('#join-contribution').click() - await page.getByRole('option', { name: '$0/mo' }).click() + await page.locator('#join-contribution').fill('0') + await page.getByRole('checkbox', { name: /Community Guidelines/ }).check() await page.locator('.form-submit').click() - // Should show an error about the email already existing - await expect(page.locator('.error-box')).toBeVisible({ timeout: 10000 }) - await expect(page.locator('.error-box')).toContainText(/already/i) + // Helcim 409 puts SignupFlowOverlay into its error state + const overlayError = page.locator('.signup-flow-overlay .error-box') + await expect(overlayError).toBeVisible({ timeout: 10000 }) + await expect(overlayError).toContainText(/already/i) }) }) diff --git a/e2e/member-profile.spec.js b/e2e/member-profile.spec.js index 0fa51c8..22af212 100644 --- a/e2e/member-profile.spec.js +++ b/e2e/member-profile.spec.js @@ -3,9 +3,11 @@ import { test, expect } from './helpers/fixtures.js' test.describe('Member profile page', () => { test('profile page loads', async ({ adminPage }) => { await adminPage.goto('/member/profile') + await adminPage.waitForLoadState('networkidle') // Auth is checked client-side in onMounted — wait for profile form to render await expect(adminPage.getByText('Edit Profile')).toBeVisible({ timeout: 15000 }) - await expect(adminPage.getByText('How you appear to other members')).toBeVisible() + // Verify a stable structural section label, not transient marketing copy + await expect(adminPage.getByText('Show in Member Directory')).toBeVisible() }) test('form fields are present', async ({ adminPage }) => { @@ -24,6 +26,7 @@ test.describe('Member profile page', () => { test('bio field accepts input', async ({ adminPage }) => { await adminPage.goto('/member/profile') + await adminPage.waitForLoadState('networkidle') await expect(adminPage.getByText('Edit Profile')).toBeVisible({ timeout: 15000 }) const bio = adminPage.locator('textarea[placeholder*="Share your background"]') diff --git a/server/middleware/03.rate-limit.js b/server/middleware/03.rate-limit.js index ac87ef7..5ee6103 100644 --- a/server/middleware/03.rate-limit.js +++ b/server/middleware/03.rate-limit.js @@ -43,6 +43,11 @@ export default defineEventHandler(async (event) => { const path = getRequestURL(event).pathname if (!path.startsWith('/api/')) return + // Bypass rate limiting in test/dev opt-in mode so parallel E2E runs from a + // single IP (127.0.0.1) do not exhaust the per-IP budget. Mirrors the gate + // used by /api/dev/* endpoints — only set in development and by Playwright. + if (process.env.ALLOW_DEV_TEST_ENDPOINTS === 'true') return + const ip = getClientIp(event) try {