diff --git a/app/pages/search.vue b/app/pages/search.vue index 1de97475b5..9deaea70ae 100644 --- a/app/pages/search.vue +++ b/app/pages/search.vue @@ -50,7 +50,7 @@ const { committedModel: committedQuery, provider: searchProvider, } = useGlobalSearch() -const query = computed(() => searchQuery.value) +const query = computed(() => searchQuery.value.trim().replace(/!$/, '')) // Track if page just loaded (for hiding "Searching..." during view transition) const hasInteracted = shallowRef(false) @@ -452,28 +452,32 @@ async function navigateToPackage(packageName: string) { // Track the input value when user pressed Enter (for navigating when results arrive) const pendingEnterQuery = shallowRef(null) -// Watch for results to navigate when Enter was pressed before results arrived -watch(displayResults, newResults => { - if (!pendingEnterQuery.value) return - - // Check if input is still focused (user hasn't started navigating or clicked elsewhere) - if (document.activeElement?.tagName !== 'INPUT') { - pendingEnterQuery.value = null - return - } - - // Navigate if first result matches the query that was entered - const firstResult = newResults[0] - // eslint-disable-next-line no-console - console.log('[search] watcher fired', { - pending: pendingEnterQuery.value, - firstResult: firstResult?.package.name, - }) - if (firstResult?.package.name === pendingEnterQuery.value) { - pendingEnterQuery.value = null - navigateToPackage(firstResult.package.name) - } -}) +// Watch for results to navigate when Enter was pressed before results arrived, +// or for "I'm feeling lucky" redirection when the query ends with "!" and there is the exact match. +watch( + displayResults, + newResults => { + const rawQuery = normalizeSearchParam(route.query.q) + const isFeelingLucky = rawQuery.endsWith('!') + if (!pendingEnterQuery.value && !isFeelingLucky) return + + const target = pendingEnterQuery.value || rawQuery.replace(/!$/, '') + if (!target) return + + // Navigate if first result matches the query that was entered + const firstResult = newResults[0] + // eslint-disable-next-line no-console + console.log('[search] watcher fired', { + pending: pendingEnterQuery.value, + firstResult: firstResult?.package.name, + }) + if (firstResult?.package.name === target) { + pendingEnterQuery.value = null + navigateToPackage(firstResult.package.name) + } + }, + { immediate: true }, +) /** * Focus the header search input @@ -501,14 +505,15 @@ function handleResultsKeydown(e: KeyboardEvent) { committedQuery.value = inputValue // Check if first result matches the input value exactly + const cleanedInputValue = inputValue.replace(/!$/, '') const firstResult = displayResults.value[0] - if (firstResult?.package.name === inputValue) { + if (firstResult?.package.name === cleanedInputValue) { pendingEnterQuery.value = null return navigateToPackage(firstResult.package.name) } - // No match yet - store input value, watcher will handle navigation when results arrive - pendingEnterQuery.value = inputValue + // No match yet - store cleaned input value, watcher will handle navigation when results arrive + pendingEnterQuery.value = cleanedInputValue return } diff --git a/test/e2e/search-feeling-lucky.spec.ts b/test/e2e/search-feeling-lucky.spec.ts new file mode 100644 index 0000000000..3c0ecf80e0 --- /dev/null +++ b/test/e2e/search-feeling-lucky.spec.ts @@ -0,0 +1,14 @@ +import { expect, test } from './test-utils' + +test.describe('Search "I\'m Feeling Lucky" Redirect', () => { + test('direct URL access with "!" should redirect to package', async ({ page, goto }) => { + await goto('/search?q=nuxt!', { waitUntil: 'hydration' }) + await expect(page).toHaveURL(/\/package\/nuxt$/, { timeout: 15000 }) + }) + + test('normal search query (without "!") should not redirect', async ({ page, goto }) => { + await goto('/search?q=nuxt', { waitUntil: 'hydration' }) + await expect(page.locator('[data-result-index="0"]').first()).toBeVisible({ timeout: 15000 }) + await expect(page).toHaveURL(/\/search\?q=nuxt/) + }) +})