diff --git a/app/web-app/__mocks__/@reown-appkit-react.js b/app/web-app/__mocks__/@reown-appkit-react.js new file mode 100644 index 0000000..2bae353 --- /dev/null +++ b/app/web-app/__mocks__/@reown-appkit-react.js @@ -0,0 +1,3 @@ +module.exports = { + useAppKit: () => ({ open: () => {} }), +}; diff --git a/app/web-app/__mocks__/lucide-react.js b/app/web-app/__mocks__/lucide-react.js new file mode 100644 index 0000000..7909570 --- /dev/null +++ b/app/web-app/__mocks__/lucide-react.js @@ -0,0 +1,9 @@ +const React = require('react'); + +// Return a simple SVG React component for any named export +module.exports = new Proxy({}, { + get: (_target, prop) => { + if (prop === '__esModule') return true; + return (props) => React.createElement('svg', { ...props, 'data-icon': String(prop) }); + }, +}); diff --git a/app/web-app/e2e/home.spec.ts b/app/web-app/e2e/home.spec.ts new file mode 100644 index 0000000..4b27e8d --- /dev/null +++ b/app/web-app/e2e/home.spec.ts @@ -0,0 +1,6 @@ +import { test, expect } from '@playwright/test'; + +test('homepage shows brand', async ({ page }) => { + await page.goto('http://localhost:3000'); + await expect(page.locator('text=FairFund')).toBeVisible(); +}); diff --git a/app/web-app/jest.config.cjs b/app/web-app/jest.config.cjs new file mode 100644 index 0000000..8ee8550 --- /dev/null +++ b/app/web-app/jest.config.cjs @@ -0,0 +1,18 @@ +const nextJest = require('next/jest'); + +const createJestConfig = nextJest({ dir: './' }); + +const customJestConfig = { + roots: ['/src'], + setupFiles: ['/jest.mocks.js'], + testEnvironment: 'jsdom', + setupFilesAfterEnv: ['/jest.setup.ts'], + moduleNameMapper: { + '\\.(css|less|scss|sass)$': 'identity-obj-proxy', + '^lucide-react(.*)$': '/__mocks__/lucide-react.js', + '^@reown/appkit/react$': '/__mocks__/@reown-appkit-react.js', + }, +}; + +module.exports = createJestConfig(customJestConfig); + diff --git a/app/web-app/jest.mocks.js b/app/web-app/jest.mocks.js new file mode 100644 index 0000000..942f8c5 --- /dev/null +++ b/app/web-app/jest.mocks.js @@ -0,0 +1,24 @@ +// Mocks to run before modules are imported + +// Mock wagmi hooks used in components +jest.mock('wagmi', () => ({ + useAccount: () => ({ isConnected: false }), + useWalletClient: () => ({ data: null }), + useChainId: () => undefined, +})); + +// Mock @reown/appkit react hook used for wallet modal +jest.mock('@reown/appkit/react', () => ({ + useAppKit: () => ({ open: jest.fn() }), +})); + +// Mock lucide-react icons (ESM) to prevent ESM parsing issues in Jest +jest.mock('lucide-react', () => { + const React = require('react'); + return new Proxy({}, { + get: (_target, prop) => { + if (prop === '__esModule') return true; + return (props) => React.createElement('svg', { ...props, 'data-icon': String(prop) }); + }, + }); +}); diff --git a/app/web-app/jest.setup.js b/app/web-app/jest.setup.js new file mode 100644 index 0000000..01fdad3 --- /dev/null +++ b/app/web-app/jest.setup.js @@ -0,0 +1,25 @@ +// Setup file executed before tests to mock environment and modules +require('@testing-library/jest-dom'); + +// Mock wagmi hooks used in components +jest.mock('wagmi', () => ({ + useAccount: () => ({ isConnected: false }), + useWalletClient: () => ({ data: null }), + useChainId: () => undefined, +})); + +// Mock @reown/appkit react hook used for wallet modal +jest.mock('@reown/appkit/react', () => ({ + useAppKit: () => ({ open: jest.fn() }), +})); + +// Mock lucide-react icons (ESM) to prevent ESM parsing issues in Jest +jest.mock('lucide-react', () => { + const React = require('react'); + return new Proxy({}, { + get: (_target, prop) => { + if (prop === '__esModule') return true; + return (props) => React.createElement('svg', { ...props, 'data-icon': String(prop) }); + }, + }); +}); diff --git a/app/web-app/jest.setup.ts b/app/web-app/jest.setup.ts new file mode 100644 index 0000000..adee3c8 --- /dev/null +++ b/app/web-app/jest.setup.ts @@ -0,0 +1,2 @@ +import '@testing-library/jest-dom'; + diff --git a/app/web-app/package.json b/app/web-app/package.json index 0d40ab9..336fdb1 100644 --- a/app/web-app/package.json +++ b/app/web-app/package.json @@ -8,6 +8,11 @@ "start": "next start", "lint": "next lint", "format": "prettier --write .", + "test": "jest --passWithNoTests", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "e2e": "playwright test", + "e2e:headed": "playwright test --headed", "addMockSpaces": "ts-node --compiler-options '{\"module\":\"CommonJS\"}' src/mocks/add-spaces.ts", "cleanDB": "ts-node --compiler-options '{\"module\":\"CommonJS\"}' src/mocks/remove.ts" }, @@ -52,6 +57,15 @@ "zod": "^3.23.8" }, "devDependencies": { + "@testing-library/jest-dom": "^6.0.1", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.4.3", + "@types/jest": "^29.5.3", + "jest-environment-jsdom": "^28.0.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.0", + "@playwright/test": "^1.43.0", "@faker-js/faker": "^8.4.1", "@types/node": "^20", "@types/react": "^18", diff --git a/app/web-app/playwright.config.ts b/app/web-app/playwright.config.ts new file mode 100644 index 0000000..45a96c9 --- /dev/null +++ b/app/web-app/playwright.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + use: { + headless: true, + viewport: { width: 1280, height: 720 }, + actionTimeout: 0, + navigationTimeout: 30000, + }, + webServer: { + command: 'npm run dev', + port: 3000, + reuseExistingServer: true, + } +}); diff --git a/app/web-app/src/components/__tests__/navbar.test.tsx b/app/web-app/src/components/__tests__/navbar.test.tsx new file mode 100644 index 0000000..396ff3e --- /dev/null +++ b/app/web-app/src/components/__tests__/navbar.test.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +// Mocks for next/navigation and next-auth +jest.mock('next/navigation', () => ({ usePathname: () => '/' })); +jest.mock('next-auth/react', () => ({ useSession: () => ({ data: null }) })); + +import Navbar from '../navbar'; + +describe('Navbar', () => { + it('renders the brand', () => { + render(); + expect(screen.getByText(/FairFund/i)).toBeInTheDocument(); + }); +});