F9: Small polish batch
- getYouTubeId also recognizes /embed/<id> and /shorts/<id> URLs (test renders all three variants as thumbnails on the exercise detail view) - useListNavigation JSDoc: itemCount is a Ref<number>, not a number - collectionRepository.getAll: N+1 per-collection count queries folded into one LEFT JOIN ... GROUP BY Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
166cf87e95
commit
0bc20f81d8
|
|
@ -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<number>} 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<number>, onKeydown: Function, focusItem: Function }}
|
||||
* @returns {{ focusedIndex: import('vue').Ref<number>, onKeydown: Function, focusItem: Function }}
|
||||
*/
|
||||
export function useListNavigation(itemCount, onSelect) {
|
||||
const focusedIndex = ref(-1)
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
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`,
|
||||
)
|
||||
c.sessionCount = row ? row.sessionCount : 0
|
||||
}
|
||||
return collections
|
||||
},
|
||||
|
||||
async getAllWithSessions() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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/<id> and /shorts/<id> 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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue