From 0bc20f81d81b1ca0e3fc31e23bcf3b7857148683 Mon Sep 17 00:00:00 2001 From: kaivalya Date: Thu, 9 Jul 2026 15:45:39 +0200 Subject: [PATCH] F9: Small polish batch - getYouTubeId also recognizes /embed/ and /shorts/ URLs (test renders all three variants as thumbnails on the exercise detail view) - useListNavigation JSDoc: itemCount is a Ref, not a number - collectionRepository.getAll: N+1 per-collection count queries folded into one LEFT JOIN ... GROUP BY Co-Authored-By: Claude Fable 5 --- src/composables/useListNavigation.js | 4 +- src/db/repositories/collectionRepository.js | 16 ++++---- src/utils/youtube.js | 2 + tests/test_step3_exercises.py | 41 +++++++++++++++++++++ 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/composables/useListNavigation.js b/src/composables/useListNavigation.js index 13fa876..f822e0e 100644 --- a/src/composables/useListNavigation.js +++ b/src/composables/useListNavigation.js @@ -3,9 +3,9 @@ import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue' /** * Provides keyboard navigation for a list of items using arrow keys. * - * @param {number} itemCount - The number of items in the list + * @param {import('vue').Ref} itemCount - Ref holding the number of items in the list * @param {Function} onSelect - Callback when Enter is pressed on a focused item (receives index) - * @returns {{ focusedIndex: Ref, onKeydown: Function, focusItem: Function }} + * @returns {{ focusedIndex: import('vue').Ref, onKeydown: Function, focusItem: Function }} */ export function useListNavigation(itemCount, onSelect) { const focusedIndex = ref(-1) diff --git a/src/db/repositories/collectionRepository.js b/src/db/repositories/collectionRepository.js index 7ca3d69..8943274 100644 --- a/src/db/repositories/collectionRepository.js +++ b/src/db/repositories/collectionRepository.js @@ -20,15 +20,13 @@ export const collectionRepository = { }, async getAll() { - const collections = await db.selectAll('SELECT * FROM collection ORDER BY label') - for (const c of collections) { - const row = await db.selectOne( - 'SELECT COUNT(*) AS sessionCount FROM collection_session WHERE collection_id = ?', - [c.id], - ) - c.sessionCount = row ? row.sessionCount : 0 - } - return collections + return db.selectAll( + `SELECT c.*, COUNT(cs.session_id) AS sessionCount + FROM collection c + LEFT JOIN collection_session cs ON cs.collection_id = c.id + GROUP BY c.id + ORDER BY c.label`, + ) }, async getAllWithSessions() { diff --git a/src/utils/youtube.js b/src/utils/youtube.js index 244089f..3303bc4 100644 --- a/src/utils/youtube.js +++ b/src/utils/youtube.js @@ -5,6 +5,8 @@ export function getYouTubeId(url) { return u.pathname.slice(1) } if (u.hostname.includes('youtube.com')) { + const pathMatch = u.pathname.match(/^\/(?:embed|shorts)\/([^/?]+)/) + if (pathMatch) return pathMatch[1] return u.searchParams.get('v') } } catch { diff --git a/tests/test_step3_exercises.py b/tests/test_step3_exercises.py index 2933b0f..fce8672 100644 --- a/tests/test_step3_exercises.py +++ b/tests/test_step3_exercises.py @@ -9,6 +9,7 @@ Step 3 — Exercise Management Tests - test_delete_exercise: Delete an exercise with confirmation - test_delete_exercise_cancel: Cancelling delete does not remove exercise - test_delete_exercise_used_in_session: Delete confirm mentions usage; no session_item orphans +- test_exercise_video_embed_and_shorts_urls: embed/shorts YouTube URLs render as thumbnails - test_search_exercises: Search filters exercises by title - test_search_no_results: Search with no matches shows empty message - test_search_clear_button: Clear button empties the search field and restores the full list @@ -172,6 +173,46 @@ def test_create_exercise_full(page: Page, app_url: str): _clean_exercises(page) +def test_exercise_video_embed_and_shorts_urls(page: Page, app_url: str): + """YouTube /embed/ and /shorts/ URLs are recognized like watch + URLs and render as thumbnails on the detail view.""" + _go_to_exercises(page, app_url) + _clean_exercises(page) + + exercise_id = page.evaluate( + """async () => { + const uuid = await window.__repos.settings.get('user_uuid'); + return window.__repos.exercises.create({ + title: 'Video URL Variants', + description: '', + asymmetric: false, + alternate: false, + image_urls: [], + video_urls: [ + 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + 'https://www.youtube.com/embed/dQw4w9WgXcQ', + 'https://www.youtube.com/shorts/dQw4w9WgXcQ', + ], + default_reps: 10, + default_weight: 0, + created_by: uuid + }); + }""" + ) + + page.goto(f"{app_url}/#/exercises/{exercise_id}") + page.wait_for_selector('[data-testid="exercise-detail-video-0"]', timeout=5000) + + for i in range(3): + video = page.locator(f'[data-testid="exercise-detail-video-{i}"]') + expect(video).to_have_class("youtube-thumb") + expect(video.locator("img")).to_have_attribute( + "src", "https://img.youtube.com/vi/dQw4w9WgXcQ/mqdefault.jpg" + ) + + _clean_exercises(page) + + def test_exercise_appears_in_list(page: Page, app_url: str): """Created exercise appears in the list view with correct info.""" _go_to_exercises(page, app_url)