feat: explore tab, React Query data layer, programs, sync, analytics, testing infrastructure

- Replace browse tab with Supabase-connected explore tab with filters
- Add React Query for data fetching with loading states
- Add 3 structured programs with weekly progression
- Add Supabase anonymous auth sync service
- Add PostHog analytics with screen tracking and events
- Add comprehensive test strategy (Vitest + Maestro E2E)
- Add RevenueCat subscription system with DEV simulation
- Add i18n translations for new screens (EN/FR/DE/ES)
- Add data deletion modal, sync consent modal
- Add assessment screen and program routes
- Add GitHub Actions CI workflow
- Update activity store with sync integration
This commit is contained in:
Millian Lamiaux
2026-03-24 12:04:48 +01:00
parent 8703c484e8
commit cd065d07c3
138 changed files with 26819 additions and 1043 deletions

View File

@@ -0,0 +1,130 @@
import { test, expect } from '@playwright/test'
test.describe('Collections List Page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/collections')
})
test('should display collections page header', async ({ page }) => {
const heading = page.getByRole('heading', { name: /collections|tabatafit admin/i })
await expect(heading).toBeVisible()
})
test('should have Add Collection button', async ({ page }) => {
const url = page.url()
if (!url.includes('/collections')) return
const addButton = page.getByRole('button', { name: /add collection/i })
await expect(addButton).toBeVisible()
})
test('should display subtitle text', async ({ page }) => {
const url = page.url()
if (!url.includes('/collections')) return
await expect(page.getByText(/organize workouts into collections/i)).toBeVisible()
})
test('should display collection cards after loading', async ({ page }) => {
const url = page.url()
if (!url.includes('/collections')) return
// Wait for loading to finish
await page.waitForSelector('[class*="animate-spin"]', { state: 'detached', timeout: 10000 }).catch(() => {})
// Should show collection cards in a grid layout
const grid = page.locator('[class*="grid"]')
await expect(grid).toBeVisible()
})
test('should display collection title and description on cards', async ({ page }) => {
const url = page.url()
if (!url.includes('/collections')) return
await page.waitForSelector('[class*="animate-spin"]', { state: 'detached', timeout: 10000 }).catch(() => {})
// Find collection cards
const cards = page.locator('[class*="bg-neutral-900"]').filter({ has: page.locator('h3') })
const count = await cards.count()
if (count > 0) {
const firstCard = cards.first()
// Card should have a title (h3)
await expect(firstCard.locator('h3')).toBeVisible()
// Card should have description text (p element)
const description = firstCard.locator('p').first()
await expect(description).toBeVisible()
}
})
test('should display collection icon', async ({ page }) => {
const url = page.url()
if (!url.includes('/collections')) return
await page.waitForSelector('[class*="animate-spin"]', { state: 'detached', timeout: 10000 }).catch(() => {})
// Icon containers have specific styling
const iconContainers = page.locator('[class*="w-12"][class*="h-12"][class*="rounded-xl"]')
const count = await iconContainers.count()
if (count > 0) {
await expect(iconContainers.first()).toBeVisible()
}
})
test('should display gradient bars for collections that have them', async ({ page }) => {
const url = page.url()
if (!url.includes('/collections')) return
await page.waitForSelector('[class*="animate-spin"]', { state: 'detached', timeout: 10000 }).catch(() => {})
// Gradient bars have inline background style with linear-gradient
const gradientBars = page.locator('[class*="h-2"][class*="rounded-full"]')
const count = await gradientBars.count()
// Gradient bars are optional (only shown if collection has gradient property)
if (count > 0) {
const firstBar = gradientBars.first()
const style = await firstBar.getAttribute('style')
expect(style).toContain('linear-gradient')
}
})
test('should have edit and delete buttons on cards', async ({ page }) => {
const url = page.url()
if (!url.includes('/collections')) return
await page.waitForSelector('[class*="animate-spin"]', { state: 'detached', timeout: 10000 }).catch(() => {})
const editButtons = page.locator('button').filter({ has: page.locator('svg.lucide-edit') })
const deleteButtons = page.locator('button').filter({ has: page.locator('svg.lucide-trash-2') })
const editCount = await editButtons.count()
const deleteCount = await deleteButtons.count()
// If collections are displayed, they should have action buttons
if (editCount > 0) {
expect(editCount).toBeGreaterThan(0)
expect(deleteCount).toBeGreaterThan(0)
// Each collection should have both edit and delete
expect(editCount).toBe(deleteCount)
}
})
})
test.describe('Collections Page Loading State', () => {
test('should show loading spinner initially', async ({ page }) => {
// Navigate and check for spinner before data loads
await page.goto('/collections')
const url = page.url()
if (!url.includes('/collections')) return
// The spinner might be very brief, so we just verify the page loads
await page.waitForLoadState('networkidle')
// After loading, spinner should be gone
const spinner = page.locator('[class*="animate-spin"]')
await expect(spinner).not.toBeVisible({ timeout: 10000 })
})
})