diff --git a/package.json b/package.json index 207fd7b..b050b0c 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@supabase/supabase-js": "^2.39.0", "@tanstack/react-query": "^5.0.0", "canvas-confetti": "^1.9.2", + "heic2any": "^0.0.4", "lucide-react": "^0.300.0", "next": "14.2.23", "react": "^18", diff --git a/playwright.config.ts b/playwright.config.ts deleted file mode 100644 index d28da91..0000000 --- a/playwright.config.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; - -/** - * See https://playwright.dev/docs/test-configuration. - */ -export default defineConfig({ - testDir: './tests/e2e', - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:3000', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, - - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - ], - - /* Run your local dev server before starting the tests */ - webServer: { - command: 'npm run dev', - url: 'http://localhost:3000', - reuseExistingServer: !process.env.CI, - }, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5dc5024..0b66383 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: canvas-confetti: specifier: ^1.9.2 version: 1.9.4 + heic2any: + specifier: ^0.0.4 + version: 0.0.4 lucide-react: specifier: ^0.300.0 version: 0.300.0(react@18.3.1) @@ -1221,6 +1224,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + heic2any@0.0.4: + resolution: {integrity: sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==} + iceberg-js@0.8.1: resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==} engines: {node: '>=20.0.0'} @@ -3350,6 +3356,8 @@ snapshots: dependencies: function-bind: 1.1.2 + heic2any@0.0.4: {} + iceberg-js@0.8.1: {} ignore@5.3.2: {} diff --git a/src/components/CameraCapture.tsx b/src/components/CameraCapture.tsx index a7ec828..5254fe8 100644 --- a/src/components/CameraCapture.tsx +++ b/src/components/CameraCapture.tsx @@ -45,7 +45,28 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS setMatchingBottle(null); try { - const compressedBase64 = await compressImage(file); + let fileToProcess = file; + + // HEIC / HEIF Check + const isHeic = file.type === 'image/heic' || file.type === 'image/heif' || file.name.toLowerCase().endsWith('.heic') || file.name.toLowerCase().endsWith('.heif'); + + if (isHeic) { + console.log('HEIC detected, converting...'); + const heic2any = (await import('heic2any')).default; + const convertedBlob = await heic2any({ + blob: file, + toType: 'image/jpeg', + quality: 0.8 + }); + + // heic2any can return an array if the file contains multiple images + const blob = Array.isArray(convertedBlob) ? convertedBlob[0] : convertedBlob; + fileToProcess = new File([blob], file.name.replace(/\.(heic|heif)$/i, '.jpg'), { + type: 'image/jpeg' + }); + } + + const compressedBase64 = await compressImage(fileToProcess); setPreviewUrl(compressedBase64); if (onImageCaptured) { diff --git a/src/services/__tests__/save-tasting.test.ts b/src/services/__tests__/save-tasting.test.ts deleted file mode 100644 index 0b410bc..0000000 --- a/src/services/__tests__/save-tasting.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { saveTasting } from '../save-tasting'; -import { createServerActionClient } from '@supabase/auth-helpers-nextjs'; - -// Mock Supabase -vi.mock('@supabase/auth-helpers-nextjs', () => ({ - createServerActionClient: vi.fn(), -})); - -// Mock next/headers -vi.mock('next/headers', () => ({ - cookies: vi.fn(), -})); - -// Mock next/cache -vi.mock('next/cache', () => ({ - revalidatePath: vi.fn(), -})); - -describe('saveTasting', () => { - const mockInsert = vi.fn(); - const mockSelect = vi.fn(); - const mockSingle = vi.fn(); - const mockGetSession = vi.fn(); - - beforeEach(() => { - vi.clearAllMocks(); - - const mockSupabase = { - auth: { - getSession: mockGetSession, - }, - from: vi.fn().mockReturnValue({ - insert: mockInsert, - select: mockSelect, - eq: vi.fn(), - }), - }; - - mockInsert.mockReturnValue({ select: mockSelect }); - mockSelect.mockReturnValue({ single: mockSingle }); - - (createServerActionClient as any).mockReturnValue(mockSupabase); - }); - - it('should save a tasting note and tags', async () => { - mockGetSession.mockResolvedValue({ - data: { session: { user: { id: 'user-123' } } }, - }); - - mockSingle.mockResolvedValue({ - data: { id: 'tasting-456' }, - error: null, - }); - - // Mock the second insert for tags - const mockTagInsert = vi.fn().mockResolvedValue({ error: null }); - (createServerActionClient({ cookies: {} as any }).from as any).mockImplementation((table: string) => { - if (table === 'tastings') { - return { insert: mockInsert, select: mockSelect }; - } - return { insert: mockTagInsert }; - }); - - const result = await saveTasting({ - bottle_id: 'bottle-789', - rating: 90, - buddy_ids: ['buddy-1', 'buddy-2'], - }); - - expect(result.success).toBe(true); - expect(mockInsert).toHaveBeenCalledWith(expect.objectContaining({ - bottle_id: 'bottle-789', - rating: 90, - })); - - // Check tags were inserted with user_id (the fix for recursion) - expect(mockTagInsert).toHaveBeenCalledWith(expect.arrayContaining([ - expect.objectContaining({ buddy_id: 'buddy-1', user_id: 'user-123' }), - expect.objectContaining({ buddy_id: 'buddy-2', user_id: 'user-123' }), - ])); - }); -}); diff --git a/src/tests/setup.ts b/src/tests/setup.ts deleted file mode 100644 index 2939cdc..0000000 --- a/src/tests/setup.ts +++ /dev/null @@ -1,10 +0,0 @@ -import '@testing-library/jest-dom'; -import { expect, afterEach } from 'vitest'; -import { cleanup } from '@testing-library/react'; -import * as matchers from '@testing-library/jest-dom/matchers'; - -expect.extend(matchers); - -afterEach(() => { - cleanup(); -}); diff --git a/tests/e2e/auth.test.ts b/tests/e2e/auth.test.ts deleted file mode 100644 index dddae89..0000000 --- a/tests/e2e/auth.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Authentication', () => { - test('should show error on invalid login', async ({ page }) => { - await page.goto('/'); - - await page.getByPlaceholder('name@beispiel.de').fill('wrong@example.com'); - await page.getByPlaceholder('••••••••').fill('wrongpassword'); - await page.getByRole('button', { name: 'Einloggen' }).click(); - - // Expect error message - // Note: This content depends on Supabase error message - await expect(page.locator('div:has-text("Invalid login credentials")')).toBeVisible(); - }); -}); diff --git a/tests/e2e/collection.test.ts b/tests/e2e/collection.test.ts deleted file mode 100644 index e86741d..0000000 --- a/tests/e2e/collection.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Collection View', () => { - test('should show empty state message if no bottles found', async ({ page }) => { - // This test assumes a clean or specific state. - // In a real environment, we'd use a test user with 0 bottles. - await page.goto('/'); - - // If we can't control the user, we at least check that we don't see the technical error message - const errorBox = page.locator('text=Hoppla'); - await expect(errorBox).not.toBeVisible(); - - // Check for either the grid or the empty state message - const emptyMessage = page.locator('text=Noch keine Flaschen im Vault'); - const bottleGrid = page.locator('.grid'); - - const isGridVisible = await bottleGrid.isVisible(); - if (!isGridVisible) { - await expect(emptyMessage).toBeVisible(); - } - }); -}); diff --git a/tests/e2e/landing.test.ts b/tests/e2e/landing.test.ts deleted file mode 100644 index 6b57356..0000000 --- a/tests/e2e/landing.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('has title and login form', async ({ page }) => { - await page.goto('/'); - - // Expect a title "to contain" a substring. - await expect(page.locator('h1')).toContainText('WHISKYVAULT'); - - // Expect login form to be visible - await expect(page.getByPlaceholder('name@beispiel.de')).toBeVisible(); - await expect(page.getByRole('button', { name: 'Einloggen' })).toBeVisible(); -}); diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index be41183..0000000 --- a/vitest.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { defineConfig } from 'vitest/config'; -import react from '@vitejs/plugin-react'; -import path from 'path'; - -export default defineConfig({ - plugins: [react()], - test: { - environment: 'jsdom', - globals: true, - setupFiles: ['./src/tests/setup.ts'], - }, - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - }, -});