diff --git a/app/components/SkillSearchPanel.vue b/app/components/SkillSearchPanel.vue
index cdbb85ac..52859c35 100644
--- a/app/components/SkillSearchPanel.vue
+++ b/app/components/SkillSearchPanel.vue
@@ -285,8 +285,8 @@ function retryRepositoryIndex(): void {
@mousemove="hover(index)"
>
{ if (row._tag === 'task') event.preventDefault() }"
+ @mousedown.prevent
@click="emit('select', row)"
>
diff --git a/app/composables/useSkillSearch.ts b/app/composables/useSkillSearch.ts
index 04c41090..3298529c 100644
--- a/app/composables/useSkillSearch.ts
+++ b/app/composables/useSkillSearch.ts
@@ -397,9 +397,17 @@ function useSkillSearchInternal() {
if (server.repository?._tag === 'indexed') {
const { owner, repo, stars, skillCount, registryPath } = server.repository
const repositoryRow: SearchRepository = { owner, repo, stars, skillCount, registryPath }
+ // A Repository with one Skill is that Skill, and only the Skill row
+ // carries the run command. Enter then opens the Skill.
+ const isSkill = (row: SearchRow) => row._tag === 'skill'
+ && row.skill.owner.toLowerCase() === owner.toLowerCase()
+ && row.skill.repo.toLowerCase() === repo.toLowerCase()
+ const leadRows: SearchRow[] = skillCount === 1 && skillRows.some(isSkill)
+ ? []
+ : [{ _tag: 'repository', repository: repositoryRow }]
return {
_tag: 'ready',
- rows: [{ _tag: 'repository', repository: repositoryRow }, ...skillRows],
+ rows: [...leadRows, ...skillRows],
total: skillCount,
repository: repositoryRow,
}
diff --git a/test/unit/home-search.nuxt.test.ts b/test/unit/home-search.nuxt.test.ts
index e6ea5c85..c3e61d19 100644
--- a/test/unit/home-search.nuxt.test.ts
+++ b/test/unit/home-search.nuxt.test.ts
@@ -43,6 +43,9 @@ mockNuxtImport('useSkillSearch', () => () => ({
rememberQuery: vi.fn(),
loadTypeaheadIndex: vi.fn(),
submitRepository,
+ recentSearches: ref([]),
+ retry: vi.fn(),
+ taskSearch: ref({ _tag: 'idle' }),
}))
// The real panel draws the same ids; this stub keeps the test on the box.
@@ -176,6 +179,26 @@ describe('homepage search interactions', () => {
expect(navigate).not.toHaveBeenCalled()
})
+ it('opens a Skill row on a pointer click', async () => {
+ const wrapper = await mountSuspended(HomeSearch, {
+ attachTo: document.body,
+ global: { stubs: { SkillSearchRepositoryModal: true } },
+ })
+ wrappers.push(wrapper)
+ const input = wrapper.get('input').element
+ input.focus()
+ await flushPromises()
+ const press = new MouseEvent('mousedown', { bubbles: true, cancelable: true })
+ wrapper.get('#skill-search-row-0').element.dispatchEvent(press)
+ // A browser moves focus off the input on a press it does not cancel.
+ if (!press.defaultPrevented)
+ input.blur()
+ await flushPromises()
+ await wrapper.get('#skill-search-row-0').trigger('click')
+ await flushPromises()
+ expect(navigate).toHaveBeenCalledWith('/gh/antfu/skills/vue-testing')
+ })
+
it('closes when focus moves outside the search', async () => {
const wrapper = await mountSearch()
await wrapper.get('input').trigger('focusout', { relatedTarget: document.body })
diff --git a/test/unit/skill-search-state.test.ts b/test/unit/skill-search-state.test.ts
index dc2c71cd..e64d4a89 100644
--- a/test/unit/skill-search-state.test.ts
+++ b/test/unit/skill-search-state.test.ts
@@ -109,6 +109,22 @@ describe('search results belong to the current query', () => {
expect(search.rows.value.map(row => row._tag)).toEqual(['repository', 'skill'])
})
+ it('answers a one-Skill Repository with its Skill row alone', async () => {
+ fetchMock.mockResolvedValue({
+ kind: 'repository',
+ repository: { _tag: 'indexed', owner: 'owner', repo: 'skills', stars: 2, skillCount: 1, registryPath: '/gh/owner/skills' },
+ owner: 'owner',
+ understood: null,
+ items: response('shipreel').items,
+ total: 1,
+ mode: null,
+ })
+ await type('Owner/skills')
+ await debounce()
+ expect(search.state.value).toMatchObject({ _tag: 'ready', repository: { owner: 'owner', repo: 'skills' } })
+ expect(search.rows.value).toMatchObject([{ _tag: 'skill', skill: { name: 'shipreel' } }])
+ })
+
it('offers the index action for a Repository the registry does not hold', async () => {
fetchMock.mockResolvedValue({
kind: 'repository',