diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..13afc0b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,78 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + unit-and-integration-tests: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Build and start containers + run: | + cd mock-project + docker compose up --build -d + + - name: Wait for API + run: sleep 10 + + - name: Run unit tests + run: | + cd mock-project + docker compose exec api pytest unit_tests/ -v + + - name: Run integration tests + run: | + cd mock-project + docker compose exec api pytest integration_tests/ -v + + - name: Stop containers + run: | + cd mock-project + docker compose down + + e2e-tests: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: | + cd mock-project + npm ci + + - name: Install Playwright browsers + run: | + cd mock-project + npx playwright install chromium + + - name: Start containers + run: | + cd mock-project + docker compose up --build -d + + - name: Wait for API to be ready + run: | + sleep 10 + curl --retry 5 --retry-delay 2 --retry-connrefused http://localhost:5000/health || true + + - name: Run Playwright tests + run: | + cd mock-project + npx playwright test + + - name: Stop containers + run: | + cd mock-project + docker compose down diff --git a/.gitignore b/.gitignore index b7faf40..eff573a 100644 --- a/.gitignore +++ b/.gitignore @@ -205,3 +205,6 @@ cython_debug/ marimo/_static/ marimo/_lsp/ __marimo__/ +node_modules/ +mock-project/node_modules/ +*.swp diff --git a/assignment-4-e2e-test-report.md b/assignment-4-e2e-test-report.md new file mode 100644 index 0000000..55bf871 --- /dev/null +++ b/assignment-4-e2e-test-report.md @@ -0,0 +1,34 @@ +# E2E Test Report - Assignment 4 + +## Test Environment + +- **Local:** Docker Compose on Ubuntu +- **CI:** GitHub Actions (Ubuntu latest) +- **Browser:** Chromium (headless) +- **Test Framework:** Playwright 1.60.0 + +## Test Results + +| Test | Status | Duration | +|------|--------|----------| +| homepage loads and shows title | PASS | 278ms | +| health endpoint returns ok | PASS | 48ms | +| user can submit answers and complete the test | PASS | 13.4s | +| results page shows cone scores | PASS | 13.5s | +| reset button clears results and restarts test | PASS | 13.7s | + +**Total:** 5 passed, 0 failed (41.8s) + +## AI Generation + +The E2E tests were initially generated by AI and then manually refined to match the Ishihara-style diagnostic application. The AI provided a good structure but assumed a simpler API-based application. I had to rewrite the tests to work with the form-based submission and dynamic plate generation. + +## Limitations + +- The tests use "0" as the default answer for all plates. This does not test the user's actual color vision. +- The tests do not verify the accuracy of the diagnosis, only that the flow completes. +- Full diagnostic accuracy requires manual validation against clinical tests like Enchroma. + +## CI/CD Integration + +The E2E tests run automatically on every push and pull request via GitHub Actions. The workflow installs Playwright browsers, starts the Docker container, runs the tests, and stops the container. diff --git a/assignment-4-golden-paths.md b/assignment-4-golden-paths.md new file mode 100644 index 0000000..e45a2ae --- /dev/null +++ b/assignment-4-golden-paths.md @@ -0,0 +1,31 @@ +# Golden Paths Analysis + +## What users pay for + +Users pay for an accurate color blindness diagnosis. The most valuable scenario is a user completing the full test and receiving a reliable result that matches clinical tests. + +## Golden Path 1: Complete diagnostic test + +A user opens the web application, answers all test plates by typing the numbers they see (or 0 if they see nothing), and receives a diagnosis with cone percentage scores. + +**Why this is valuable:** This is the core value proposition. Without an accurate diagnosis, the test has no value. + +## Golden Path 2: Retaking the test + +A user completes the test, receives a result, and clicks "Take Test Again" to start a fresh session with randomized plates. + +**Why this is valuable:** Users may want to verify their results or test again after the variance disclaimer. This also demonstrates that the test is not deterministic. + +## Golden Path 3: Health check for operations + +A DevOps engineer calls the `/health` endpoint to verify the service is running before routing traffic. + +**Why this is valuable:** For enterprise deployment, reliability monitoring is essential. Users cannot take the test if the service is down. + +## E2E tests covering these paths + +| Golden Path | E2E Test | +|-------------|----------| +| Complete diagnostic test | `user can submit answers and complete the test` | +| Retaking the test | `reset button clears results and restarts test` | +| Health check | `health endpoint returns ok` | diff --git a/assignment-4-manual-testing.md b/assignment-4-manual-testing.md new file mode 100644 index 0000000..89ec357 --- /dev/null +++ b/assignment-4-manual-testing.md @@ -0,0 +1,44 @@ +# Manual Testing Required Beyond Automation + +## What cannot be automated + +1. **Diagnostic accuracy** – An automated test cannot verify that the diagnosis matches a clinical test like Enchroma. Only a human with known color blindness can validate this. + +2. **Perceptual difficulty calibration** – The test needs to be challenging but not impossible. Automated tests only check that the flow completes, not that the colors are properly calibrated. + +3. **User experience** – An automated test cannot judge whether the instructions are clear, the interface is intuitive, or the results are easy to understand. + +4. **Realistic user behavior** – Automated tests follow a script (typing 0 for every plate). Real users may hesitate, change answers, or behave unpredictably. + +## Manual test cases to run + +1. **Diagnostic accuracy** – Have a user with known color blindness (Protan, Deutan, or Tritan) take the test. Compare the results to their Enchroma or clinical diagnosis. + +2. **Calibration check** – A person with normal vision should get "Normal Color Vision" consistently. If they get a false positive, the test is too hard. + +3. **Variance check** – Take the test 3 times. Scores should vary within the +/- 13% disclaimer. Large swings indicate instability. + +4. **Usability** – Ask a first-time user to take the test without instructions. Where do they get stuck? Is the "enter 0 if you see no number" instruction clear? + +5. **Cross-browser testing** – Test on Chrome, Firefox, and Safari. The Canvas rendering should be consistent. + +6. **Mobile testing** – The test is designed for desktop. On mobile, the numbers may be too small. Document this limitation. + +## Manual test results template + +| Test Case | Result | Notes | +|-----------|--------|-------| +| Protan user diagnosis | Pending | Need test subject | +| Deutan user diagnosis | Pending | Need test subject | +| Tritan user diagnosis | Pending | Need test subject | +| Normal vision (3x) | Pending | Should get Normal each time | +| First-time usability | Pending | Observe hesitation points | +| Chrome | Pending | | +| Firefox | Pending | | +| Safari | Pending | | + +## Recommendations for improvement + +- Recruit colorblind users for validation +- Add a calibration mode with known control plates +- Implement a confidence score based on response consistency diff --git a/mock-project/Dockerfile b/mock-project/Dockerfile new file mode 100644 index 0000000..6aa95d1 --- /dev/null +++ b/mock-project/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.11-slim +WORKDIR /app +COPY backend/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY backend/ . +CMD ["python", "app.py"] diff --git a/mock-project/README.md b/mock-project/README.md new file mode 100644 index 0000000..8f9166b --- /dev/null +++ b/mock-project/README.md @@ -0,0 +1,126 @@ +# Color Vision Diagnostic Test + +A web-based Ishihara-style color vision test that diagnoses Protan (red deficiency), Deutan (green deficiency), and Tritan (blue deficiency) color blindness. + +## Prerequisites + +- Docker installed on your machine +- Git (to clone the repository) +- Node.js and npm (for E2E tests only) + +## Getting Started + +Clone the repository and navigate to the mock-project folder: + +```bash +git clone https://github.com/pitekopaga/testing.git +cd testing/mock-project +``` + +## Run the Web UI + +Start the application: + +```bash +docker compose up --build -d +``` + +Open your browser and go to: `http://localhost:5000` + +## How to Take the Test + +1. A circle of colored dots will appear with a hidden number +2. Type the number you see in the input box +3. If you see no number, type **0** +4. Click Submit +5. Repeat for all plates +6. After the final plate, you will see your results with cone percentage scores + +## Functional Requirements + +| ID | Requirement | Status | +|----|-------------|--------| +| FR-1 | Present Ishihara-style dot patterns with hidden numbers | Implemented | +| FR-2 | Accept user input as a number (0-99) | Implemented | +| FR-3 | Accept "0" when user sees no number | Implemented | +| FR-4 | Track answers across all test plates | Implemented | +| FR-5 | Calculate cone response percentages for red, green, and blue | Implemented | +| FR-6 | Diagnose Protan, Deutan, or Tritan color blindness based on lowest cone score | Implemented | +| FR-7 | Provide diagnosis with description | Implemented | +| FR-8 | Allow user to reset and retake the test | Implemented | + +## Non-Functional Requirements + +| ID | Requirement | Status | +|----|-------------|--------| +| NFR-1 | Test completes within 2 minutes for typical users | Implemented | +| NFR-2 | Plates are generated client-side using Canvas API | Implemented | +| NFR-3 | Application runs in Docker container | Implemented | +| NFR-4 | No user data is stored permanently (session-only) | Implemented | +| NFR-5 | Test provides results with +/- 13% variance disclaimer | Implemented | +| NFR-6 | Instructions are displayed on every test page | Implemented | +| NFR-7 | Progress indicator shows current plate number and total | Implemented | +| NFR-8 | Results page shows individual cone scores with progress bars | Implemented | + +## Run Automated Tests + +### Unit Tests + +```bash +docker compose exec api pytest unit_tests/ -v +``` + +### Integration Tests + +```bash +docker compose exec api pytest integration_tests/ -v +``` + +### E2E Tests (Playwright) + +```bash +npm install +npx playwright install chromium +npx playwright test +``` + +## Test the API manually with curl + +Health check: + +```bash +curl http://localhost:5000/health +``` + +Expected output: `{"status":"ok"}` + +## Stop the Application + +```bash +docker compose down +``` + +## Clearing Browser Data (if needed) + +If the test behaves unexpectedly, clear your browser data for localhost: + +**Chrome:** +1. Click the lock icon next to the address bar +2. Click "Cookies and site data" +3. Click "Manage cookies and site data" +4. Click the trash icon next to `localhost` +5. Refresh the page + +**Alternative:** Open an incognito/private browsing window. + +## Troubleshooting + +**Port 5000 is already in use:** Stop the process using port 5000, or change the port mapping in `docker-compose.yml`. + +**The test gives unexpected results:** Clear your browser data as described above. + +**Playwright tests fail:** Run `npx playwright install chromium` to ensure browsers are installed. + +## License + +MIT diff --git a/mock-project/backend/app.py b/mock-project/backend/app.py new file mode 100644 index 0000000..dd4e088 --- /dev/null +++ b/mock-project/backend/app.py @@ -0,0 +1,372 @@ +from flask import Flask, render_template_string, session, redirect, url_for, request +import random + +app = Flask(__name__) +app.secret_key = 'your-secret-key-here' + +# Number patterns (5x5 grid) +PATTERNS = { + 0: [ + [0,1,1,1,0], + [1,0,0,0,1], + [1,0,0,0,1], + [1,0,0,0,1], + [0,1,1,1,0] + ], + 1: [ + [0,0,1,0,0], + [0,1,1,0,0], + [0,0,1,0,0], + [0,0,1,0,0], + [0,1,1,1,0] + ], + 2: [ + [0,1,1,1,0], + [1,0,0,0,1], + [0,0,1,1,0], + [0,1,0,0,0], + [1,1,1,1,1] + ], + 3: [ + [0,1,1,1,0], + [1,0,0,0,1], + [0,0,1,1,0], + [1,0,0,0,1], + [0,1,1,1,0] + ], + 4: [ + [1,0,0,0,1], + [1,0,0,0,1], + [1,1,1,1,1], + [0,0,0,0,1], + [0,0,0,0,1] + ], + 5: [ + [1,1,1,1,1], + [1,0,0,0,0], + [1,1,1,1,0], + [0,0,0,0,1], + [1,1,1,1,0] + ], + 6: [ + [0,1,1,1,0], + [1,0,0,0,0], + [1,1,1,1,0], + [1,0,0,0,1], + [0,1,1,1,0] + ], + 7: [ + [1,1,1,1,1], + [0,0,0,0,1], + [0,0,1,1,0], + [0,1,0,0,0], + [1,0,0,0,0] + ], + 8: [ + [0,1,1,1,0], + [1,0,0,0,1], + [0,1,1,1,0], + [1,0,0,0,1], + [0,1,1,1,0] + ], + 9: [ + [0,1,1,1,0], + [1,0,0,0,1], + [0,1,1,1,1], + [0,0,0,0,1], + [0,1,1,1,0] + ], +} + +def get_pattern(num): + if num < 10: + return PATTERNS.get(num, PATTERNS[0]) + tens = num // 10 + ones = num % 10 + p1 = PATTERNS.get(tens, PATTERNS[0]) + p2 = PATTERNS.get(ones, PATTERNS[0]) + combined = [] + for i in range(5): + row = p1[i] + [0] + p2[i] + combined.append(row) + return combined + +def make_plate(plate_type, number): + if plate_type == 'protan': + # Protan - make it VERY hard for red-deficient + # Almost identical red and green values + base_red = random.randint(80, 110) + base_green = random.randint(80, 110) + bg = [base_red, base_green, random.randint(60, 90)] + fg = [base_red + random.randint(-10, 10), base_green + random.randint(-10, 10), random.randint(60, 90)] + elif plate_type == 'deutan': + # Deutan - medium difficulty + bg = [random.randint(70, 100), random.randint(120, 150), random.randint(70, 100)] + fg = [random.randint(120, 150), random.randint(40, 70), random.randint(70, 100)] + elif plate_type == 'tritan': + # Tritan - VERY easy for normal vision (high contrast) + bg = [120, 120, 120] + fg = [40, 40, 200] + else: + # Control - extremely easy + bg = [60, 60, 60] + fg = [220, 220, 220] + + for i in range(3): + bg[i] = max(30, min(230, bg[i])) + fg[i] = max(30, min(230, fg[i])) + + return { + 'num': number, + 'type': plate_type, + 'bg': bg, + 'fg': fg, + 'pattern': get_pattern(number) + } + +HTML = ''' + + + + Color Vision Test + + + +

Color Vision Diagnostic Test

+ {% if not done %} +
+ Instructions: A number is hidden in the circle of dots. Type the number you see.
+ If you do not see any number, type 0. +
+
+

Plate {{ idx }} of {{ total }}

+ +
+ + +
+ + {% else %} +
+

Your Color Blind Test Result

+

{{ diagnosis }}

+

{{ description }}

+ +
+
Blue Cone (Tritan)
+
{{ blue_score }}%
+ +
+ +
+
Green Cone (Deutan)
+
{{ green_score }}%
+ +
+ +
+
Red Cone (Protan)
+
{{ red_score }}%
+ +
+ +
Note: Scores below 60% indicate a possible deficiency in that cone type.
+ +
+ +
+
+ {% endif %} + + +''' + +PROTAN_QUESTIONS = [12, 8, 5, 74, 29, 6, 3, 15] +DEUTAN_QUESTIONS = [12, 8, 5, 74, 29, 6, 3, 15] +TRITAN_QUESTIONS = [2, 9, 4] +CONTROL_QUESTIONS = [7, 0] + +@app.route('/', methods=['GET', 'POST']) +def index(): + if not session.get('initialized'): + session.clear() + plates = [] + for q in PROTAN_QUESTIONS: + plates.append(make_plate('protan', q)) + for q in DEUTAN_QUESTIONS: + plates.append(make_plate('deutan', q)) + for q in TRITAN_QUESTIONS: + plates.append(make_plate('tritan', q)) + for q in CONTROL_QUESTIONS: + plates.append(make_plate('control', q)) + random.shuffle(plates) + + session['plates'] = plates + session['answers'] = [] + session['step'] = 0 + session['initialized'] = True + session.modified = True + + if request.method == 'POST': + ans = request.form.get('answer', '0') + try: + ans = int(ans) + except: + ans = 0 + + step = session.get('step', 0) + plates = session.get('plates', []) + + if step < len(plates): + answers = session.get('answers', []) + answers.append({ + 'user': ans, + 'correct': plates[step]['num'], + 'type': plates[step]['type'] + }) + session['answers'] = answers + session['step'] = step + 1 + session.modified = True + + if session.get('step', 0) >= len(plates): + return redirect(url_for('result')) + + step = session.get('step', 0) + plates = session.get('plates', []) + + if step >= len(plates) or not plates: + return redirect(url_for('result')) + + p = plates[step] + return render_template_string(HTML, + pattern=p['pattern'], + bg=p['bg'], + fg=p['fg'], + idx=step+1, + total=len(plates), + pct=(step/len(plates))*100, + done=False) + +@app.route('/result') +def result(): + answers = session.get('answers', []) + plates = session.get('plates', []) + + protan_correct = 0 + protan_total = 0 + deutan_correct = 0 + deutan_total = 0 + tritan_correct = 0 + tritan_total = 0 + + for i, a in enumerate(answers): + if i >= len(plates): + continue + plate_type = plates[i]['type'] + is_correct = (a['user'] == a['correct']) + + if plate_type == 'protan': + protan_total += 1 + if is_correct: + protan_correct += 1 + elif plate_type == 'deutan': + deutan_total += 1 + if is_correct: + deutan_correct += 1 + elif plate_type == 'tritan': + tritan_total += 1 + if is_correct: + tritan_correct += 1 + + red_score = round((protan_correct / max(protan_total, 1)) * 100) + green_score = round((deutan_correct / max(deutan_total, 1)) * 100) + blue_score = round((tritan_correct / max(tritan_total, 1)) * 100) + + # Find the lowest score + scores = {'red': red_score, 'green': green_score, 'blue': blue_score} + min_type = min(scores, key=scores.get) + min_score = scores[min_type] + + if min_score < 60: + if min_type == 'red': + diagnosis = "Protan Color Blind" + description = "You have a stronger deficiency in your red color cone, which means you have a type of red-green color blindness called Protan." + elif min_type == 'green': + diagnosis = "Deutan Color Blind" + description = "You have a stronger deficiency in your green color cone, which means you have a type of red-green color blindness called Deutan." + else: + diagnosis = "Tritan Color Blind" + description = "You have a deficiency in your blue color cone, which means you have blue-yellow color blindness called Tritan." + else: + diagnosis = "Normal Color Vision" + description = "Your color vision appears normal within the range of this test." + + return render_template_string(HTML, + done=True, + red_score=red_score, + green_score=green_score, + blue_score=blue_score, + diagnosis=diagnosis, + description=description) + +@app.route('/reset', methods=['POST']) +def reset(): + session.clear() + return redirect(url_for('index')) + +@app.route('/health') +def health(): + return {'status': 'ok'} + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/mock-project/backend/integration_tests/test_api_integration.py b/mock-project/backend/integration_tests/test_api_integration.py new file mode 100644 index 0000000..776aefe --- /dev/null +++ b/mock-project/backend/integration_tests/test_api_integration.py @@ -0,0 +1,38 @@ +import requests + +BASE_URL = "http://localhost:5000" + +def test_health_endpoint(): + """Test that health endpoint returns ok status""" + response = requests.get(f"{BASE_URL}/health") + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + +def test_homepage_loads(): + """Test that the main test page loads correctly""" + response = requests.get(f"{BASE_URL}/") + assert response.status_code == 200 + assert "Color Vision Diagnostic Test" in response.text + assert "Enter number" in response.text + +def test_form_submission(): + """Test that submitting an answer redirects correctly""" + session = requests.Session() + session.get(f"{BASE_URL}/") + response = session.post(f"{BASE_URL}/", data={"answer": "0"}) + assert response.status_code in [200, 302] + +def test_results_page(): + """Test that results page loads and shows scores""" + session = requests.Session() + session.get(f"{BASE_URL}/") + + response = None + for _ in range(25): + response = session.post(f"{BASE_URL}/", data={"answer": "0"}) + if "Your Color Blind Test Result" in response.text: + break + + assert response is not None + assert "Your Color Blind Test Result" in response.text + assert "score" in response.text.lower() or "cone" in response.text.lower() diff --git a/mock-project/backend/requirements.txt b/mock-project/backend/requirements.txt new file mode 100644 index 0000000..7f76110 --- /dev/null +++ b/mock-project/backend/requirements.txt @@ -0,0 +1,3 @@ +flask +pytest +requests diff --git a/mock-project/backend/unit_tests/test_colorblind.py b/mock-project/backend/unit_tests/test_colorblind.py new file mode 100644 index 0000000..ec136cb --- /dev/null +++ b/mock-project/backend/unit_tests/test_colorblind.py @@ -0,0 +1,46 @@ +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import pytest +from app import get_pattern, PATTERNS + +class TestGetPattern: + def test_single_digit_pattern(self): + """Test that single digit returns correct pattern""" + pattern = get_pattern(5) + assert len(pattern) == 5 # 5 rows + assert len(pattern[0]) == 5 # 5 columns + assert pattern == PATTERNS[5] + + def test_two_digit_pattern_has_gap(self): + """Test that two-digit numbers have a column gap between digits""" + pattern = get_pattern(74) + assert len(pattern) == 5 # 5 rows + # Two-digit pattern: 5 + 1 (gap) + 5 = 11 columns + assert len(pattern[0]) == 11 + # Middle column (index 5) should be all zeros (the gap) + for row in pattern: + assert row[5] == 0 + + def test_pattern_returns_valid_for_unknown(self): + """Test that unknown number returns pattern for 0""" + pattern = get_pattern(99) + assert pattern is not None + assert len(pattern) == 5 + +class TestPatternsStructure: + def test_all_digits_have_consistent_size(self): + """Test that all digit patterns are 5x5 grids""" + for digit, pattern in PATTERNS.items(): + assert len(pattern) == 5, f"Digit {digit} has wrong row count" + for row in pattern: + assert len(row) == 5, f"Digit {digit} has wrong column count" + for cell in row: + assert cell in [0, 1], f"Digit {digit} has invalid cell value" + + def test_patterns_are_not_empty(self): + """Test that no pattern is all zeros""" + for digit, pattern in PATTERNS.items(): + total = sum(sum(row) for row in pattern) + assert total > 0, f"Digit {digit} pattern is all zeros" diff --git a/mock-project/docker-compose.yml b/mock-project/docker-compose.yml new file mode 100644 index 0000000..7e8fa42 --- /dev/null +++ b/mock-project/docker-compose.yml @@ -0,0 +1,5 @@ +services: + api: + build: . + ports: + - "5000:5000" diff --git a/mock-project/e2e_tests/colorblind.spec.js b/mock-project/e2e_tests/colorblind.spec.js new file mode 100644 index 0000000..b78a67e --- /dev/null +++ b/mock-project/e2e_tests/colorblind.spec.js @@ -0,0 +1,73 @@ +const { test, expect } = require('@playwright/test'); + +test('homepage loads and shows title', async ({ page }) => { + await page.goto('http://localhost:5000'); + await expect(page).toHaveTitle(/Color Vision Test/); + await expect(page.locator('h1')).toContainText('Color Vision Diagnostic Test'); +}); + +test('health endpoint returns ok', async ({ request }) => { + const response = await request.get('http://localhost:5000/health'); + expect(response.status()).toBe(200); + expect(await response.json()).toEqual({ status: 'ok' }); +}); + +test('user can submit answers and complete the test', async ({ page }) => { + await page.goto('http://localhost:5000'); + + // Get total number of plates from the progress indicator + const totalText = await page.locator('p').first().textContent(); + const totalMatch = totalText.match(/of (\d+)/); + const totalPlates = totalMatch ? parseInt(totalMatch[1]) : 10; + + // Submit answers for all plates + for (let i = 0; i < totalPlates; i++) { + // Enter a guess (using 0 as default) + await page.fill('input[name="answer"]', '0'); + await page.click('button[type="submit"]'); + await page.waitForLoadState('networkidle'); + } + + // Should reach results page + await expect(page.locator('h2')).toContainText('Your Color Blind Test Result'); +}); + +test('results page shows cone scores', async ({ page }) => { + // Complete the test first + await page.goto('http://localhost:5000'); + + const totalText = await page.locator('p').first().textContent(); + const totalMatch = totalText.match(/of (\d+)/); + const totalPlates = totalMatch ? parseInt(totalMatch[1]) : 10; + + for (let i = 0; i < totalPlates; i++) { + await page.fill('input[name="answer"]', '0'); + await page.click('button[type="submit"]'); + await page.waitForLoadState('networkidle'); + } + + // Verify results page has score elements + await expect(page.locator('.score').first()).toBeVisible(); +}); + +test('reset button clears results and restarts test', async ({ page }) => { + await page.goto('http://localhost:5000'); + + // Complete the test + const totalText = await page.locator('p').first().textContent(); + const totalMatch = totalText.match(/of (\d+)/); + const totalPlates = totalMatch ? parseInt(totalMatch[1]) : 10; + + for (let i = 0; i < totalPlates; i++) { + await page.fill('input[name="answer"]', '0'); + await page.click('button[type="submit"]'); + await page.waitForLoadState('networkidle'); + } + + // Click reset button + await page.click('button[type="submit"]'); + + // Should be back to first plate + await expect(page.locator('h1')).toContainText('Color Vision Diagnostic Test'); + await expect(page.locator('input[name="answer"]')).toBeVisible(); +}); diff --git a/mock-project/package-lock.json b/mock-project/package-lock.json new file mode 100644 index 0000000..e3550da --- /dev/null +++ b/mock-project/package-lock.json @@ -0,0 +1,74 @@ +{ + "name": "colorblind-api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "colorblind-api", + "version": "1.0.0", + "devDependencies": { + "@playwright/test": "^1.60.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/mock-project/package.json b/mock-project/package.json new file mode 100644 index 0000000..8dc8775 --- /dev/null +++ b/mock-project/package.json @@ -0,0 +1,10 @@ +{ + "name": "colorblind-api", + "version": "1.0.0", + "scripts": { + "test": "playwright test" + }, + "devDependencies": { + "@playwright/test": "^1.60.0" + } +}