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
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,9 @@
{#snippet trigger(toggle)}
{@const open = () => {
closeForm()
if (knownTags.size === 0) {
openCreate('')
}
toggle()
}}
<div class="flex flex-nowrap items-center gap-1">
Expand Down Expand Up @@ -331,9 +334,11 @@
</div>

{#snippet listPage()}
<div class="p-2">
<input class="input h-9 w-full" type="search" placeholder="Search" bind:value={search} />
</div>
{#if knownTags.size > 0}
<div class="p-2">
<input class="input h-9 w-full" type="search" placeholder="Search" bind:value={search} />
</div>
{/if}
<div class="scrollbar flex max-h-[280px] flex-col overflow-y-auto pb-1">
{#each selectedTags as tag (tag)}
{@render tagRow(tag, true)}
Expand Down Expand Up @@ -470,7 +475,15 @@
</button>
{/if}
</div>
<div class="flex flex-col gap-3 px-3 pb-3">
<form
class="flex flex-col gap-3 px-3 pb-3"
onsubmit={(e) => {
e.preventDefault()
if (!opts.submitDisabled) {
opts.onSubmit()
}
}}
>
<label class="flex flex-col gap-1 text-sm font-medium">
Tag name
<input
Expand Down Expand Up @@ -509,9 +522,9 @@
</div>
</div>
<button
type="submit"
class="btn h-9! w-full preset-filled-primary-500"
disabled={opts.submitDisabled}
onclick={opts.onSubmit}
>
{opts.submitLabel}
</button>
Expand All @@ -520,5 +533,5 @@
The tag “{newTagName.trim()}” already exists.
</p>
{/if}
</div>
</form>
{/snippet}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { page } from 'vitest/browser'
import { page, userEvent } from 'vitest/browser'
import { render } from 'vitest-browser-svelte'
import type { PipelineManagerApi } from '$lib/compositions/usePipelineManager.svelte'
import Tags from './Tags.svelte'
Expand Down Expand Up @@ -90,13 +90,30 @@ describe('Tags.svelte', () => {
tags: ['prod|ef4444']
})
})

it('opens the create form directly when no known tags exist yet', async () => {
renderTags({ tags: [], knownTags: [] })
await page.getByRole('button', { name: 'Tag' }).click()

// Empty pool skips the search list and lands on create immediately.
await expect.element(page.getByPlaceholder('Tag name')).toBeVisible()
await expect.element(page.getByRole('button', { name: 'Create' })).toBeVisible()
expect(page.getByPlaceholder('Search').elements()).toHaveLength(0)
})

it('shows search when known tags exist', async () => {
renderTags({ tags: [], knownTags: ['dev'] })
await page.getByRole('button', { name: 'Tag' }).click()

await expect.element(page.getByPlaceholder('Search')).toBeVisible()
})
})

describe('create form validation', () => {
describe('create form', () => {
it('blocks an invalid name and surfaces the reason; a valid name is color-encoded', async () => {
const { patchPipeline } = renderTags({ tags: [] })
const { patchPipeline } = renderTags({ tags: [], knownTags: [] })
// Empty knownTags opens create directly — no "Create a new tag" list click.
await page.getByRole('button', { name: 'Tag' }).click()
await page.getByRole('button', { name: 'Create a new tag' }).click()

const nameInput = page.getByPlaceholder('Tag name')
const createButton = page.getByRole('button', { name: 'Create' })
Expand All @@ -117,5 +134,61 @@ describe('Tags.svelte', () => {
tags: ['qa|ef4444']
})
})

it('submits the create form when Enter is pressed in the name field', async () => {
const { patchPipeline } = renderTags({ tags: [], knownTags: [] })
await page.getByRole('button', { name: 'Tag' }).click()

const nameInput = page.getByPlaceholder('Tag name')
await nameInput.fill('qa')
// `fill` leaves the input focused; Enter submits the surrounding <form>.
await userEvent.keyboard('{Enter}')

expect(patchPipeline).toHaveBeenCalledWith('test-pipeline', {
tags: ['qa|ef4444']
})
})

it('does not submit on Enter when the create button is disabled', async () => {
const { patchPipeline } = renderTags({ tags: [], knownTags: [] })
await page.getByRole('button', { name: 'Tag' }).click()

const nameInput = page.getByPlaceholder('Tag name')
await expect.element(page.getByRole('button', { name: 'Create' })).toBeDisabled()
// Focus the empty name field and press Enter — submit must stay a no-op.
await nameInput.click()
await userEvent.keyboard('{Enter}')

expect(patchPipeline).not.toHaveBeenCalled()
})

it('selects a color with Tab and Space before submitting', async () => {
const { patchPipeline } = renderTags({ tags: [], knownTags: [] })
await page.getByRole('button', { name: 'Tag' }).click()

const nameInput = page.getByPlaceholder('Tag name')
await nameInput.fill('qa')
// Tab moves from the name field onto the first color swatch (Red), then
// Purple; Space activates that focused swatch like a mouse click.
await userEvent.keyboard('{Tab}')
await userEvent.keyboard('{Tab}')
await expect.element(page.getByRole('button', { name: 'Purple' })).toHaveFocus()
await userEvent.keyboard(' ')

await expect
.element(page.getByRole('button', { name: 'Purple' }))
.toHaveAttribute('aria-pressed', 'true')
// Enter on a color swatch only re-activates the button (expected). Shift+Tab
// the same number of times returns focus to the name field, then Enter submits.
await userEvent.keyboard('{Shift>}{Tab}{/Shift}')
await userEvent.keyboard('{Shift>}{Tab}{/Shift}')
await expect.element(nameInput).toHaveFocus()
await userEvent.keyboard('{Enter}')

// Purple is #a855f7 in the palette → stored as |a855f7.
expect(patchPipeline).toHaveBeenCalledWith('test-pipeline', {
tags: ['qa|a855f7']
})
})
})
})