From e0f72d4f1f31895c6212efa99103916dad353113 Mon Sep 17 00:00:00 2001 From: sstent Date: Tue, 10 Mar 2026 07:21:17 -0700 Subject: [PATCH] chore(conductor): Mark track 'Add real-time search and alphabetical sorting to the Add Meal and Add Food modals on the tracker page.' as complete --- app/api/routes/tracker.py | 3 + conductor/tracks.md | 2 +- .../tracks/meal_food_search_20260310/plan.md | 46 +++++----- templates/modals/add_meal.html | 36 +++++++- templates/modals/add_single_food.html | 35 +++++++- tests/meal_food_search.spec.js | 88 +++++++++++++++++++ 6 files changed, 181 insertions(+), 29 deletions(-) create mode 100644 tests/meal_food_search.spec.js diff --git a/app/api/routes/tracker.py b/app/api/routes/tracker.py index f444ada..22a5646 100644 --- a/app/api/routes/tracker.py +++ b/app/api/routes/tracker.py @@ -77,12 +77,15 @@ async def tracker_page(request: Request, person: str = "Sarah", date: str = None # Template will handle filtering of deleted foods # Get all meals for dropdown (exclude snapshots) meals = db.query(Meal).filter(Meal.meal_type != "tracked_snapshot").all() + meals.sort(key=lambda x: x.name.lower()) # Get all templates for template dropdown templates_list = db.query(Template).all() + templates_list.sort(key=lambda x: x.name.lower()) # Get all foods for dropdown foods = db.query(Food).all() + foods.sort(key=lambda x: x.name.lower()) # Calculate day totals day_totals = calculate_day_nutrition_tracked(tracked_meals, db) diff --git a/conductor/tracks.md b/conductor/tracks.md index 71f0f24..87f8602 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -9,5 +9,5 @@ This file tracks all major tracks for the project. Each track has its own detail --- -- [ ] **Track: Add real-time search and alphabetical sorting to the Add Meal and Add Food modals on the tracker page.** +- [x] **Track: Add real-time search and alphabetical sorting to the Add Meal and Add Food modals on the tracker page.** *Link: [./tracks/meal_food_search_20260310/](./tracks/meal_food_search_20260310/)* diff --git a/conductor/tracks/meal_food_search_20260310/plan.md b/conductor/tracks/meal_food_search_20260310/plan.md index 6eee09c..be2a22d 100644 --- a/conductor/tracks/meal_food_search_20260310/plan.md +++ b/conductor/tracks/meal_food_search_20260310/plan.md @@ -1,32 +1,32 @@ # Implementation Plan: Meal/Food Search & Sorting (Track: meal_food_search_20260310) ## Phase 1: Preparation and Testing -- [ ] Task: Create a new Playwright test file `tests/meal_food_search.spec.js` to verify searching and sorting in modals. -- [ ] Task: Write failing E2E tests for: - - [ ] Modal opening and initial alphabetical sorting of Meals. - - [ ] Real-time filtering in the "Add Meal" modal. - - [ ] Modal opening and initial alphabetical sorting of Foods. - - [ ] Real-time filtering (by name and brand) in the "Add Food" modal. -- [ ] Task: Run the tests and confirm they fail. -- [ ] Task: Conductor - User Manual Verification 'Phase 1: Preparation and Testing' (Protocol in workflow.md) +- [x] Task: Create a new Playwright test file `tests/meal_food_search.spec.js` to verify searching and sorting in modals. +- [x] Task: Write failing E2E tests for: + - [x] Modal opening and initial alphabetical sorting of Meals. + - [x] Real-time filtering in the "Add Meal" modal. + - [x] Modal opening and initial alphabetical sorting of Foods. + - [x] Real-time filtering (by name and brand) in the "Add Food" modal. +- [x] Task: Run the tests and confirm they fail. +- [x] Task: Conductor - User Manual Verification 'Phase 1: Preparation and Testing' (Protocol in workflow.md) ## Phase 2: Implement Sorting & Searching in "Add Meal" Modal -- [ ] Task: Update `templates/modals/add_meal.html` to add a search input field above the meal list. -- [ ] Task: Add a unique `data-testid` to the search input and individual list items for test reliability. -- [ ] Task: Update the backend route `app/api/routes/tracker.py` to sort the `meals` list alphabetically before passing it to the template. -- [ ] Task: Implement client-side JavaScript in `templates/modals/add_meal.html` to filter the meal list in real-time as the user types. -- [ ] Task: Verify Phase 2 with E2E tests. -- [ ] Task: Conductor - User Manual Verification 'Phase 2: Implement Sorting & Searching in Add Meal Modal' (Protocol in workflow.md) +- [x] Task: Update `templates/modals/add_meal.html` to add a search input field above the meal list. +- [x] Task: Add a unique `data-testid` to the search input and individual list items for test reliability. +- [x] Task: Update the backend route `app/api/routes/tracker.py` to sort the `meals` list alphabetically before passing it to the template. +- [x] Task: Implement client-side JavaScript in `templates/modals/add_meal.html` to filter the meal list in real-time as the user types. +- [x] Task: Verify Phase 2 with E2E tests. +- [x] Task: Conductor - User Manual Verification 'Phase 2: Implement Sorting & Searching in Add Meal Modal' (Protocol in workflow.md) ## Phase 3: Implement Sorting & Searching in "Add Food" Modal -- [ ] Task: Update `templates/modals/add_single_food.html` (or the relevant "Add Food" modal) to add a search input field above the food list. -- [ ] Task: Add a unique `data-testid` to the search input and food list items. -- [ ] Task: Update the backend route `app/api/routes/tracker.py` to sort the `foods` list alphabetically before passing it to the template. -- [ ] Task: Implement client-side JavaScript in the "Add Food" modal to filter by both `name` and `brand` in real-time. -- [ ] Task: Verify Phase 3 with E2E tests. -- [ ] Task: Conductor - User Manual Verification 'Phase 3: Implement Sorting & Searching in Add Food Modal' (Protocol in workflow.md) +- [x] Task: Update `templates/modals/add_single_food.html` (or the relevant "Add Food" modal) to add a search input field above the food list. +- [x] Task: Add a unique `data-testid` to the search input and food list items. +- [x] Task: Update the backend route `app/api/routes/tracker.py` to sort the `foods` list alphabetically before passing it to the template. +- [x] Task: Implement client-side JavaScript in the "Add Food" modal to filter by both `name` and `brand` in real-time. +- [x] Task: Verify Phase 3 with E2E tests. +- [x] Task: Conductor - User Manual Verification 'Phase 3: Implement Sorting & Searching in Add Food Modal' (Protocol in workflow.md) ## Phase 4: Final Verification and Cleanup -- [ ] Task: Perform a final run of all tests (E2E and Backend). -- [ ] Task: Ensure code coverage for any new logic (if applicable) is >80%. -- [ ] Task: Conductor - User Manual Verification 'Phase 4: Final Verification and Cleanup' (Protocol in workflow.md) \ No newline at end of file +- [x] Task: Perform a final run of all tests (E2E and Backend). +- [x] Task: Ensure code coverage for any new logic (if applicable) is >80%. +- [x] Task: Conductor - User Manual Verification 'Phase 4: Final Verification and Cleanup' (Protocol in workflow.md) \ No newline at end of file diff --git a/templates/modals/add_meal.html b/templates/modals/add_meal.html index a7e2ec3..01c7487 100644 --- a/templates/modals/add_meal.html +++ b/templates/modals/add_meal.html @@ -13,10 +13,11 @@
- +
@@ -28,4 +29,33 @@ - \ No newline at end of file + + + diff --git a/templates/modals/add_single_food.html b/templates/modals/add_single_food.html index 603eb5b..3caab6c 100644 --- a/templates/modals/add_single_food.html +++ b/templates/modals/add_single_food.html @@ -12,10 +12,11 @@
- +
@@ -43,6 +44,36 @@ servingSizeNote.textContent = ''; } } + + document.getElementById('foodSearchInput').addEventListener('input', function() { + const searchText = this.value.toLowerCase(); + const select = document.getElementById('foodSelect'); + const options = select.options; + + for (let i = 0; i < options.length; i++) { + const option = options[i]; + if (option.value === "") continue; // Skip "Choose food..." + + const text = option.text.toLowerCase(); + const brand = (option.getAttribute('data-brand') || "").toLowerCase(); + + if (text.includes(searchText) || brand.includes(searchText)) { + option.style.display = ""; + } else { + option.style.display = "none"; + } + } + }); + + // Reset search when modal is shown + document.getElementById('addSingleFoodModal').addEventListener('show.bs.modal', function () { + document.getElementById('foodSearchInput').value = ''; + const options = document.getElementById('foodSelect').options; + for (let i = 0; i < options.length; i++) { + options[i].style.display = ""; + } + document.getElementById('servingSizeNote').textContent = ''; + }); diff --git a/tests/meal_food_search.spec.js b/tests/meal_food_search.spec.js new file mode 100644 index 0000000..3094bbd --- /dev/null +++ b/tests/meal_food_search.spec.js @@ -0,0 +1,88 @@ +const { test, expect } = require('@playwright/test'); + +test.describe('Meal and Food Search & Sorting', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/tracker'); + // Ensure we're in a known state + page.on('dialog', dialog => dialog.accept()); + const resetBtn = page.getByRole('button', { name: 'Reset Page' }); + if (await resetBtn.isVisible()) { + await resetBtn.click(); + } + }); + + test('Add Meal modal should be sorted alphabetically and searchable', async ({ page }) => { + // Open Add Meal modal for Breakfast + await page.locator('[data-testid="add-meal-breakfast"]').click(); + + // Check for search input + const searchInput = page.locator('[data-testid="meal-search-input"]'); + await expect(searchInput).toBeVisible(); + + // Check alphabetical sorting + const mealOptions = page.locator('[data-testid="meal-option"]'); + const count = await mealOptions.count(); + let prevText = ""; + for (let i = 0; i < count; i++) { + const text = await mealOptions.nth(i).innerText(); + if (text.trim() === "" || text.includes("Choose meal...")) continue; + + if (prevText !== "") { + console.log(`Comparing: "${prevText}" to "${text}"`); + const cmp = text.localeCompare(prevText, 'en', { sensitivity: 'base' }); + if (cmp < 0) { + console.error(`Sort order failure: "${prevText}" should come after "${text}" (cmp: ${cmp})`); + } + expect(cmp).toBeGreaterThanOrEqual(0); + } + prevText = text; + } + + // Test real-time filtering + await searchInput.fill('Protein'); + const filteredOptions = page.locator('[data-testid="meal-option"]:visible'); + const filteredCount = await filteredOptions.count(); + for (let i = 0; i < filteredCount; i++) { + const text = await filteredOptions.nth(i).innerText(); + expect(text.toLowerCase()).toContain('protein'); + } + }); + + test('Add Food modal should be sorted alphabetically and searchable', async ({ page }) => { + // Open Add Food modal for Breakfast + await page.locator('[data-testid="add-food-breakfast"]').click(); + + // Check for search input + const searchInput = page.locator('[data-testid="food-search-input"]'); + await expect(searchInput).toBeVisible(); + + // Check alphabetical sorting + const foodOptions = page.locator('[data-testid="food-option"]'); + const count = await foodOptions.count(); + let prevText = ""; + for (let i = 0; i < count; i++) { + const text = await foodOptions.nth(i).innerText(); + if (text.trim() === "" || text.includes("Choose food...")) continue; + + if (prevText !== "") { + console.log(`Comparing: "${prevText}" to "${text}"`); + const cmp = text.localeCompare(prevText, 'en', { sensitivity: 'base' }); + if (cmp < 0) { + console.error(`Sort order failure: "${prevText}" should come after "${text}" (cmp: ${cmp})`); + } + expect(cmp).toBeGreaterThanOrEqual(0); + } + prevText = text; + } + + // Test real-time filtering + await searchInput.fill('Organic'); + const filteredOptions = page.locator('[data-testid="food-option"]:visible'); + const filteredCount = await filteredOptions.count(); + for (let i = 0; i < filteredCount; i++) { + const text = (await filteredOptions.nth(i).innerText()).toLowerCase(); + const brand = (await filteredOptions.nth(i).getAttribute('data-brand') || "").toLowerCase(); + expect(text.includes('organic') || brand.includes('organic')).toBeTruthy(); + } + }); +});