diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f2fb08c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,72 @@ +name: CI + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + frontend: + name: Frontend CI + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Sync SvelteKit + run: npx svelte-kit sync + + - name: Check Types + run: npm run check + + - name: Lint + run: npm run lint + + - name: Unit Tests + run: npm run test:unit + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: E2E Tests + run: npm run test:e2e + + - name: Build + run: npm run build + + worker: + name: Worker CI + runs-on: ubuntu-latest + defaults: + run: + working-directory: worker + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: worker/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build + run: npx wrangler deploy --dry-run --outdir=dist diff --git a/README.md b/README.md index bc7f63e..130da3f 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ This repository now contains two main pieces: - `frontend/` — A SvelteKit (TypeScript) frontend scaffold. The home page (`src/routes/+page.svelte`) contains tiles that link to individual tools (e.g. `/handicap`). - `worker/` — A Cloudflare Worker scaffold (TypeScript) using `Hono` for lightweight API endpoints. It exposes `/api/handicap` and `/api/health` handlers. -Quick start (local) -- +## Quick start (local) + Prereqs: Node 18+, npm, and `wrangler` for Cloudflare Workers. 1. Frontend @@ -32,11 +32,36 @@ npm install npx wrangler dev ``` -Deploy notes +## Running Tests +The frontend includes both end-to-end tests (Playwright) and unit tests (Vitest). + +```bash +cd frontend +``` + +### Unit Tests +```bash +npm run test:unit +``` + +### End-to-End Tests +```bash +# Install Playwright browsers (first time only) +npx playwright install +# Run tests +npm run test:e2e +``` + +### Run all tests +```bash +npm run test +``` + +### Deploy notes - Frontend: build with `npm run build` in `frontend/` and deploy to Cloudflare Pages. SvelteKit supports the Cloudflare adapter for Pages/Workers. - Worker: publish with `npx wrangler publish` (fill `account_id` in `worker/wrangler.toml`). -Next steps +### Next steps - Wire the frontend to the Worker API endpoints health endpoint - Implement availability tracker diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 134b1b2..a95cb58 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -19,8 +19,10 @@ export default defineConfig( globals: { ...globals.browser, ...globals.node } }, rules: { // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. - // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors - "no-undef": 'off' } + // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors + "no-undef": 'off', + "svelte/no-navigation-without-resolve": "off" + } }, { files: [ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 72635e9..62d026e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -29,7 +29,8 @@ "tailwindcss": "^4.1.17", "typescript": "^5.9.3", "typescript-eslint": "^8.47.0", - "vite": "^7.2.2" + "vite": "^7.2.2", + "vitest": "^4.0.15" } }, "node_modules/@alloc/quick-lru": { @@ -2053,6 +2054,17 @@ "tailwindcss": "4.1.17" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -2060,6 +2072,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2343,6 +2362,117 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vitest/expect": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.15.tgz", + "integrity": "sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.15", + "@vitest/utils": "4.0.15", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.15.tgz", + "integrity": "sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.15", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.15.tgz", + "integrity": "sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.15.tgz", + "integrity": "sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.15", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.15.tgz", + "integrity": "sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.15", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.15.tgz", + "integrity": "sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.15.tgz", + "integrity": "sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2426,6 +2556,16 @@ "node": ">= 0.4" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/autoprefixer": { "version": "10.4.22", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", @@ -2574,6 +2714,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2787,6 +2937,13 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -3048,6 +3205,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3070,6 +3237,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3867,6 +4044,17 @@ "node": ">=0.10.0" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4350,6 +4538,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-swizzle": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", @@ -4384,6 +4579,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stoppable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", @@ -4579,6 +4788,23 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -4596,6 +4822,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -4863,6 +5099,84 @@ } } }, + "node_modules/vitest": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.15.tgz", + "integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.15", + "@vitest/mocker": "4.0.15", + "@vitest/pretty-format": "4.0.15", + "@vitest/runner": "4.0.15", + "@vitest/snapshot": "4.0.15", + "@vitest/spy": "4.0.15", + "@vitest/utils": "4.0.15", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.15", + "@vitest/browser-preview": "4.0.15", + "@vitest/browser-webdriverio": "4.0.15", + "@vitest/ui": "4.0.15", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4879,6 +5193,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 4886152..9c88902 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,8 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "eslint .", "test:e2e": "playwright test", - "test": "npm run test:e2e" + "test:unit": "vitest src", + "test": "npm run test:unit && npm run test:e2e" }, "devDependencies": { "@eslint/compat": "^1.4.0", @@ -36,6 +37,7 @@ "tailwindcss": "^4.1.17", "typescript": "^5.9.3", "typescript-eslint": "^8.47.0", - "vite": "^7.2.2" + "vite": "^7.2.2", + "vitest": "^4.0.15" } -} +} \ No newline at end of file diff --git a/frontend/src/lib/handicap/scoreCalculator.test.ts b/frontend/src/lib/handicap/scoreCalculator.test.ts new file mode 100644 index 0000000..d2f7829 --- /dev/null +++ b/frontend/src/lib/handicap/scoreCalculator.test.ts @@ -0,0 +1,96 @@ + +import { describe, it, expect } from 'vitest'; +import { calculateScores } from './scoreCalculator'; + +describe('calculateScores', () => { + it('should return error message for invalid inputs', () => { + let result = calculateScores(null, 5); + expect(result.output).toBe('Please enter valid handicaps for both players.'); + + result = calculateScores(5, null); + expect(result.output).toBe('Please enter valid handicaps for both players.'); + + result = calculateScores(NaN, 5); + expect(result.output).toBe('Please enter valid handicaps for both players.'); + }); + + describe('Minus v Minus', () => { + it('should calculate correctly when h1 < h2 both negative', () => { + // H1 = -10, H2 = -5 + // Difference = 5 + // h1 < h2 -> P1 starts at 0, P2 starts at 5 + // playTo = 11 + 5 = 16 + const result = calculateScores(-10, -5); + expect(result.startingScorePlayer1).toBe(0); + expect(result.startingScorePlayer2).toBe(5); + expect(result.playToScore).toBe(16); + expect(result.output).toContain('Minus v Minus'); + }); + + it('should calculate correctly when h1 > h2 both negative', () => { + // H1 = -5, H2 = -10 + // Difference = 5 + // h1 > h2 -> P1 starts at 5, P2 starts at 0 + // playTo = 11 + 5 = 16 + const result = calculateScores(-5, -10); + expect(result.startingScorePlayer1).toBe(5); + expect(result.startingScorePlayer2).toBe(0); + expect(result.playToScore).toBe(16); + expect(result.output).toContain('Minus v Minus'); + }); + }); + + describe('Plus v Plus', () => { + it('should calculate correctly when h1 < h2 both positive', () => { + // H1 = 5, H2 = 10 + // Difference = 5 + // h1 < h2 -> P1 starts at 0, P2 starts at 5 + // playTo = 11 + const result = calculateScores(5, 10); + expect(result.startingScorePlayer1).toBe(0); + expect(result.startingScorePlayer2).toBe(5); + expect(result.playToScore).toBe(11); + expect(result.output).toContain('Plus v Plus'); + }); + + it('should calculate correctly when h1 > h2 both positive', () => { + // H1 = 10, H2 = 5 + // Difference = 5 + // h1 > h2 -> P1 starts at 5, P2 starts at 0 + // playTo = 11 + const result = calculateScores(10, 5); + expect(result.startingScorePlayer1).toBe(5); + expect(result.startingScorePlayer2).toBe(0); + expect(result.playToScore).toBe(11); + expect(result.output).toContain('Plus v Plus'); + }); + }); + + describe('Minus v Plus', () => { + it('should calculate correctly when h1 is minus and h2 is plus', () => { + // H1 = -5, H2 = 10 + // PlusHandicap = 10, MinusHandicap = 5 + // Total = 15 + // h1 < h2 -> P1 starts at 0, P2 starts at 15 + // playTo = 11 + 5 = 16 + const result = calculateScores(-5, 10); + expect(result.startingScorePlayer1).toBe(0); + expect(result.startingScorePlayer2).toBe(15); // Logic says: startingScorePlayer1 = h1 < h2 ? 0 : total; h1(-5) < h2(10) is true, so P1=0, P2=total(15) + expect(result.playToScore).toBe(16); + expect(result.output).toContain('Minus v Plus'); + }); + + it('should calculate correctly when h1 is plus and h2 is minus', () => { + // H1 = 10, H2 = -5 + // PlusHandicap = 10, MinusHandicap = 5 + // Total = 15 + // h1 > h2 -> P1 starts at 15, P2 starts at 0 + // playTo = 11 + 5 = 16 + const result = calculateScores(10, -5); + expect(result.startingScorePlayer1).toBe(15); + expect(result.startingScorePlayer2).toBe(0); + expect(result.playToScore).toBe(16); + expect(result.output).toContain('Minus v Plus'); + }); + }); +}); diff --git a/frontend/src/lib/handicap/scoreCalculator.ts b/frontend/src/lib/handicap/scoreCalculator.ts new file mode 100644 index 0000000..1ff8ac3 --- /dev/null +++ b/frontend/src/lib/handicap/scoreCalculator.ts @@ -0,0 +1,58 @@ + +export interface ScoreResult { + startingScorePlayer1: number; + startingScorePlayer2: number; + playToScore: number; + output: string; +} + +export function calculateScores(handicap1: number | null, handicap2: number | null): ScoreResult { + if (handicap1 === null || handicap2 === null || isNaN(handicap1) || isNaN(handicap2)) { + return { + output: 'Please enter valid handicaps for both players.', + startingScorePlayer1: 0, + startingScorePlayer2: 0, + playToScore: 11 + }; + } + + const h1 = handicap1; + const h2 = handicap2; + let startingScorePlayer1 = 0; + let startingScorePlayer2 = 0; + let playToScore = 11; + let output = ''; + + if (h1 < 0 && h2 < 0) { + // Minus v Minus: deduct one handicap from the other and play to 11 plus difference. + const difference = Math.abs(h1 - h2); + startingScorePlayer1 = h1 > h2 ? difference : 0; + startingScorePlayer2 = h1 < h2 ? difference : 0; + playToScore = 11 + difference; + output = `Minus v Minus: Start at ${startingScorePlayer1}-${startingScorePlayer2} and play to ${playToScore}`; + } else if (h1 >= 0 && h2 >= 0) { + // Plus v Plus: deduct one handicap from the other and play to 11. + const difference = Math.abs(h1 - h2); + startingScorePlayer1 = h1 < h2 ? 0 : difference; + startingScorePlayer2 = h1 > h2 ? 0 : difference; + playToScore = 11; // Reset playToScore for this case + output = `Plus v Plus: Start at ${startingScorePlayer1}-${startingScorePlayer2} and play to 11`; + } else { + // (h1 < 0 && h2 >= 0) || (h1 >= 0 && h2 < 0) - Minus v Plus + // Minus v Plus: add the plus to the minus handicap and play to 11 plus the minus handicap. + const plusHandicap = Math.max(h1, h2); + const minusHandicap = Math.abs(Math.min(h1, h2)); + const total = minusHandicap + plusHandicap; + startingScorePlayer1 = h1 < h2 ? 0 : total; + startingScorePlayer2 = h1 > h2 ? 0 : total; + playToScore = 11 + minusHandicap; // Use absolute value + output = `Minus v Plus: Start at ${startingScorePlayer1}-${startingScorePlayer2} and play to ${playToScore}`; + } + + return { + startingScorePlayer1, + startingScorePlayer2, + playToScore, + output + }; +} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 1d4d0c2..5d2223e 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,13 +1,15 @@ -
+
{@render children()} diff --git a/frontend/src/routes/handicap/+page.svelte b/frontend/src/routes/handicap/+page.svelte index 538e162..7cddf2c 100644 --- a/frontend/src/routes/handicap/+page.svelte +++ b/frontend/src/routes/handicap/+page.svelte @@ -1,50 +1,14 @@ @@ -64,10 +28,14 @@ class="p-6 border-b border-gray-100 dark:border-slate-700 bg-gray-50 dark:bg-slate-800/50" >
-
+
-

Handicap Calculator

+

+ Handicap Calculator +

@@ -118,19 +86,26 @@

- {@html output} + {output}

{/if} -
-
+
+

- Plus v Plus: Deduct one handicap from the other and play to 11.
- Minus v Minus: Deduct one handicap from the other and play to 11 plus difference.
- Minus v Plus: Add the plus to the minus handicap and play to 11 plus the minus handicap.
+ Plus v Plus: Deduct one handicap from the + other and play to 11.
+ Minus v Minus: Deduct one handicap from the + other and play to 11 plus difference.
+ Minus v Plus: Add the plus to the minus + handicap and play to 11 plus the minus handicap.
The winner must win by at least 2 points.

diff --git a/worker/package-lock.json b/worker/package-lock.json index 3d73aa1..564b15c 100644 --- a/worker/package-lock.json +++ b/worker/package-lock.json @@ -1100,6 +1100,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -1495,6 +1496,7 @@ "integrity": "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.1", @@ -1510,6 +1512,7 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "bin": { "workerd": "bin/workerd" },