mirror of
https://github.com/sstent/foodplanner.git
synced 2026-03-17 10:45:30 +00:00
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
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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/)*
|
||||
|
||||
@@ -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)
|
||||
- [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)
|
||||
@@ -13,10 +13,11 @@
|
||||
<input type="hidden" name="date" value="{{ current_date.isoformat() }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Select Meal</label>
|
||||
<select class="form-control" name="meal_id" required>
|
||||
<input type="text" class="form-control mb-2" id="mealSearchInput" placeholder="Search meals..." data-testid="meal-search-input">
|
||||
<select class="form-control" name="meal_id" id="mealSelect" required size="10">
|
||||
<option value="">Choose meal...</option>
|
||||
{% for meal in meals %}
|
||||
<option value="{{ meal.id }}">{{ meal.name }}</option>
|
||||
<option value="{{ meal.id }}" data-testid="meal-option">{{ meal.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
@@ -28,4 +29,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('mealSearchInput').addEventListener('input', function() {
|
||||
const searchText = this.value.toLowerCase();
|
||||
const select = document.getElementById('mealSelect');
|
||||
const options = select.options;
|
||||
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
const option = options[i];
|
||||
if (option.value === "") continue; // Skip "Choose meal..."
|
||||
|
||||
const text = option.text.toLowerCase();
|
||||
if (text.includes(searchText)) {
|
||||
option.style.display = "";
|
||||
} else {
|
||||
option.style.display = "none";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Reset search when modal is shown
|
||||
document.getElementById('addMealModal').addEventListener('show.bs.modal', function () {
|
||||
document.getElementById('mealSearchInput').value = '';
|
||||
const options = document.getElementById('mealSelect').options;
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
options[i].style.display = "";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
<input type="hidden" name="date" value="{{ current_date.isoformat() }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Select Food</label>
|
||||
<select class="form-control" name="food_id" required onchange="updateServingSizeNote(this)">
|
||||
<input type="text" class="form-control mb-2" id="foodSearchInput" placeholder="Search foods by name or brand..." data-testid="food-search-input">
|
||||
<select class="form-control" name="food_id" id="foodSelect" required onchange="updateServingSizeNote(this)" size="10">
|
||||
<option value="">Choose food...</option>
|
||||
{% for food in foods %}
|
||||
<option value="{{ food.id }}" data-serving-size="{{ food.serving_size }}">{{ food.name }}</option>
|
||||
<option value="{{ food.id }}" data-serving-size="{{ food.serving_size }}" data-brand="{{ food.brand }}" data-testid="food-option">{{ food.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
@@ -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 = '';
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
88
tests/meal_food_search.spec.js
Normal file
88
tests/meal_food_search.spec.js
Normal file
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user