Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/components/SkillSearchPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,8 @@ function retryRepositoryIndex(): void {
@mousemove="hover(index)"
>
<!--
The box closes the panel when focus leaves it. Task search answers
inside the panel, so a press on its row keeps focus in the box.
The box closes the panel when focus leaves it, and this cell takes
no focus. A press keeps focus in the box, so the click still lands.
-->
<div
:id="searchCellId(index, 0)"
Expand All @@ -296,7 +296,7 @@ function retryRepositoryIndex(): void {
:aria-busy="row._tag === 'task' && row.status._tag === 'running' ? true : undefined"
class="search-row__open"
:class="{ 'search-row__open--static': row._tag === 'task' && !taskRowCopy(row.status).actionable }"
@mousedown="(event) => { if (row._tag === 'task') event.preventDefault() }"
@mousedown.prevent
@click="emit('select', row)"
>
<template v-if="row._tag === 'skill'">
Expand Down
10 changes: 9 additions & 1 deletion app/composables/useSkillSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
23 changes: 23 additions & 0 deletions test/unit/home-search.nuxt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 })
Expand Down
16 changes: 16 additions & 0 deletions test/unit/skill-search-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading