586 lines
20 KiB
TypeScript
586 lines
20 KiB
TypeScript
// @vitest-environment happy-dom
|
|
|
|
import { act, type ReactNode } from 'react'
|
|
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { useAppStore } from '@/store'
|
|
import type {
|
|
FolderWorkspace,
|
|
LinearIssue,
|
|
Repo,
|
|
Worktree,
|
|
WorktreeMeta
|
|
} from '../../../../shared/types'
|
|
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
|
|
|
|
// Why: Radix tooltips need a provider the dialog does not own, and the menu's
|
|
// portal needs real layout. Stand-ins keep these tests on provider selection.
|
|
vi.mock('@/components/ui/tooltip', () => ({
|
|
Tooltip: ({ children }: { children?: ReactNode }) => <>{children}</>,
|
|
TooltipContent: () => null,
|
|
TooltipTrigger: ({ children }: { children?: ReactNode }) => <>{children}</>
|
|
}))
|
|
|
|
vi.mock('@/components/ui/dropdown-menu', async () => {
|
|
const React = await import('react')
|
|
const SelectContext = React.createContext<(value: string) => void>(() => {})
|
|
const Passthrough = ({ children }: { children?: ReactNode }) => <>{children}</>
|
|
return {
|
|
DropdownMenu: Passthrough,
|
|
DropdownMenuTrigger: Passthrough,
|
|
DropdownMenuContent: Passthrough,
|
|
DropdownMenuRadioGroup: ({
|
|
value,
|
|
onValueChange,
|
|
children
|
|
}: {
|
|
value: string
|
|
onValueChange: (value: string) => void
|
|
children?: ReactNode
|
|
}) => (
|
|
<SelectContext.Provider value={onValueChange}>
|
|
<div data-selected={value}>{children}</div>
|
|
</SelectContext.Provider>
|
|
),
|
|
DropdownMenuRadioItem: ({ value, children }: { value: string; children?: ReactNode }) => {
|
|
const onSelect = React.useContext(SelectContext)
|
|
return (
|
|
<button type="button" role="menuitemradio" onClick={() => onSelect(value)}>
|
|
{children}
|
|
</button>
|
|
)
|
|
}
|
|
}
|
|
})
|
|
|
|
import WorktreeMetaDialog from './WorktreeMetaDialog'
|
|
|
|
const REPO_ID = 'repo-1'
|
|
const WORKTREE_ID = 'repo-1::/repo/worktrees/feature'
|
|
|
|
const initialState = useAppStore.getInitialState()
|
|
const updateWorktreeMeta =
|
|
vi.fn<
|
|
(
|
|
id: string,
|
|
updates: Partial<WorktreeMeta>
|
|
) => Promise<{ ok: true } | { ok: false; error: string }>
|
|
>()
|
|
const fetchLinearIssue = vi.fn<(...args: never[]) => Promise<LinearIssue | null>>()
|
|
const openUrl = vi.fn<(url: string) => void>()
|
|
|
|
/** Only `url` is read by the open-issue path. */
|
|
function makeLinearIssue(url: string): LinearIssue {
|
|
return { url } as LinearIssue
|
|
}
|
|
|
|
function makeRepo(id: string = REPO_ID, path: string = '/repo'): Repo {
|
|
return { id, path, displayName: 'orca', badgeColor: '#999999', addedAt: 1 }
|
|
}
|
|
|
|
function makeWorktree(overrides: Partial<Worktree> = {}): Worktree {
|
|
return {
|
|
id: WORKTREE_ID,
|
|
repoId: REPO_ID,
|
|
path: '/repo/worktrees/feature',
|
|
displayName: 'Feature work',
|
|
branch: 'feature',
|
|
head: 'abc123',
|
|
isBare: false,
|
|
isMainWorktree: false,
|
|
comment: 'existing note',
|
|
linkedIssue: null,
|
|
linkedPR: null,
|
|
linkedLinearIssue: null,
|
|
isArchived: false,
|
|
isUnread: false,
|
|
isPinned: false,
|
|
sortOrder: 0,
|
|
lastActivityAt: 1,
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
function makeFolderWorkspace(overrides: Partial<FolderWorkspace> = {}): FolderWorkspace {
|
|
return {
|
|
id: 'fw-1',
|
|
projectGroupId: 'pg-1',
|
|
name: 'Docs folder',
|
|
folderPath: '/repo/docs',
|
|
linkedTask: {
|
|
provider: 'linear',
|
|
type: 'issue',
|
|
number: 901,
|
|
title: 'Fix auth',
|
|
url: 'https://linear.app/acme/issue/STA-901',
|
|
linearIdentifier: 'STA-901'
|
|
},
|
|
comment: '',
|
|
isArchived: false,
|
|
isUnread: false,
|
|
isPinned: false,
|
|
sortOrder: 0,
|
|
lastActivityAt: 1,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
function openDialog(
|
|
options: {
|
|
worktree?: Partial<Worktree>
|
|
worktreeId?: string
|
|
folderWorkspace?: Partial<FolderWorkspace>
|
|
/** Extra owners of the same workspace ID, which the index reads as ambiguous. */
|
|
otherRepos?: { repoId: string; worktree?: Partial<Worktree> }[]
|
|
modalRepoId?: string
|
|
linearViewerOrganizationUrlKey?: string
|
|
} = {}
|
|
): void {
|
|
const worktree = makeWorktree(options.worktree)
|
|
const otherRepos = options.otherRepos ?? []
|
|
useAppStore.setState({
|
|
repos: [makeRepo(), ...otherRepos.map((other) => makeRepo(other.repoId, `/${other.repoId}`))],
|
|
worktreesByRepo: {
|
|
[REPO_ID]: [worktree],
|
|
...Object.fromEntries(
|
|
otherRepos.map((other) => [
|
|
other.repoId,
|
|
[makeWorktree({ repoId: other.repoId, ...other.worktree })]
|
|
])
|
|
)
|
|
},
|
|
...(options.folderWorkspace
|
|
? { folderWorkspaces: [makeFolderWorkspace(options.folderWorkspace)] }
|
|
: {}),
|
|
...(options.linearViewerOrganizationUrlKey
|
|
? {
|
|
linearStatus: {
|
|
connected: true,
|
|
viewer: {
|
|
displayName: 'Viewer',
|
|
email: null,
|
|
organizationName: 'Active',
|
|
organizationUrlKey: options.linearViewerOrganizationUrlKey
|
|
}
|
|
}
|
|
}
|
|
: {}),
|
|
activeModal: 'edit-meta',
|
|
modalData: {
|
|
worktreeId: options.worktreeId ?? worktree.id,
|
|
...(options.modalRepoId ? { repoId: options.modalRepoId } : {}),
|
|
currentDisplayName: worktree.displayName,
|
|
currentComment: worktree.comment,
|
|
focus: 'comment'
|
|
},
|
|
updateWorktreeMeta,
|
|
fetchLinearIssue: fetchLinearIssue as unknown as ReturnType<
|
|
typeof useAppStore.getState
|
|
>['fetchLinearIssue']
|
|
})
|
|
render(<WorktreeMetaDialog />)
|
|
}
|
|
|
|
function issueInput(): HTMLInputElement {
|
|
return screen.getByPlaceholderText('Issue #, or a GitHub or Linear URL')
|
|
}
|
|
|
|
function providerChip(): HTMLButtonElement {
|
|
return screen.getByRole('button', { name: 'Issue provider' })
|
|
}
|
|
|
|
function saveButton(): HTMLButtonElement {
|
|
return screen.getByRole('button', { name: 'Save' })
|
|
}
|
|
|
|
function openIssueButton(): HTMLButtonElement {
|
|
return screen.getByRole('button', { name: 'Open linked issue' })
|
|
}
|
|
|
|
describe('WorktreeMetaDialog issue link row', () => {
|
|
beforeEach(() => {
|
|
useAppStore.setState(initialState, true)
|
|
updateWorktreeMeta.mockReset()
|
|
updateWorktreeMeta.mockResolvedValue({ ok: true })
|
|
fetchLinearIssue.mockReset()
|
|
fetchLinearIssue.mockResolvedValue(null)
|
|
openUrl.mockReset()
|
|
Object.defineProperty(window, 'api', {
|
|
configurable: true,
|
|
value: { shell: { openUrl } }
|
|
})
|
|
})
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
useAppStore.setState(initialState, true)
|
|
})
|
|
|
|
it('seeds the chip and value from a GitHub link', () => {
|
|
openDialog({ worktree: { linkedIssue: 42 } })
|
|
|
|
expect(providerChip().textContent).toContain('GitHub')
|
|
expect(issueInput().value).toBe('42')
|
|
})
|
|
|
|
it('replaces a completed emoji shortcode in the display name', () => {
|
|
openDialog()
|
|
const displayNameInput = screen.getByRole('textbox', { name: 'Display Name' })
|
|
|
|
fireEvent.change(displayNameInput, {
|
|
target: { value: 'Feature :wink:', selectionStart: 14 }
|
|
})
|
|
|
|
expect((displayNameInput as HTMLInputElement).value).toBe('Feature 😉')
|
|
})
|
|
|
|
it('seeds the chip and value from a Linear link', () => {
|
|
openDialog({ worktree: { linkedLinearIssue: 'STA-335' } })
|
|
|
|
expect(providerChip().textContent).toContain('Linear')
|
|
expect(issueInput().value).toBe('STA-335')
|
|
})
|
|
|
|
it('flips to Linear when a linear.app issue URL is pasted', () => {
|
|
openDialog({ worktree: { linkedIssue: 42 } })
|
|
|
|
fireEvent.change(issueInput(), {
|
|
target: { value: 'https://linear.app/acme/issue/STA-335/fix-the-thing' }
|
|
})
|
|
|
|
expect(providerChip().textContent).toContain('Linear')
|
|
})
|
|
|
|
// Why: Linear and Jira issue keys are the same shape, so only a URL may steer
|
|
// the provider — a bare key must never override the user's explicit choice.
|
|
it('keeps the chip on GitHub when a bare issue key is typed', () => {
|
|
openDialog({ worktree: { linkedIssue: 42 } })
|
|
|
|
fireEvent.change(issueInput(), { target: { value: 'GH-1234' } })
|
|
|
|
expect(providerChip().textContent).toContain('GitHub')
|
|
expect(providerChip().textContent).not.toContain('Linear')
|
|
})
|
|
|
|
it('flips to GitHub when a GitHub issue URL is pasted over a Linear link', () => {
|
|
openDialog({ worktree: { linkedLinearIssue: 'STA-335' } })
|
|
|
|
fireEvent.change(issueInput(), {
|
|
target: { value: 'https://github.com/acme/orca/issues/77' }
|
|
})
|
|
|
|
expect(providerChip().textContent).toContain('GitHub')
|
|
})
|
|
|
|
it('names the Linear issue that switching to GitHub would unlink', () => {
|
|
openDialog({ worktree: { linkedLinearIssue: 'STA-335' } })
|
|
|
|
fireEvent.click(screen.getByRole('menuitemradio', { name: 'GitHub' }))
|
|
fireEvent.change(issueInput(), { target: { value: '99' } })
|
|
|
|
expect(
|
|
screen.getByText('Saving unlinks Linear STA-335 — a workspace tracks one issue.')
|
|
).toBeTruthy()
|
|
})
|
|
|
|
// Both slots can hold a link at once — naming only one understates the save.
|
|
it('names both links when clearing the field would drop both', () => {
|
|
openDialog({ worktree: { linkedIssue: 42, linkedLinearIssue: 'STA-335' } })
|
|
|
|
fireEvent.change(issueInput(), { target: { value: '' } })
|
|
|
|
expect(
|
|
screen.getByText(
|
|
'Saving unlinks Linear STA-335 and GitHub #42 — a workspace tracks one issue.'
|
|
)
|
|
).toBeTruthy()
|
|
})
|
|
|
|
// The warning above only promises the displacement — this asserts the payload
|
|
// that carries it out, which is where the one-issue-per-workspace rule lives.
|
|
it('clears the displaced GitHub link when a Linear value is saved', async () => {
|
|
openDialog({ worktree: { linkedIssue: 42 } })
|
|
|
|
fireEvent.click(screen.getByRole('menuitemradio', { name: 'Linear' }))
|
|
fireEvent.change(issueInput(), { target: { value: 'STA-335' } })
|
|
await act(async () => {
|
|
fireEvent.click(saveButton())
|
|
})
|
|
|
|
await waitFor(() => expect(updateWorktreeMeta).toHaveBeenCalledTimes(1))
|
|
const updates = updateWorktreeMeta.mock.calls[0]?.[1] ?? {}
|
|
expect(updates.linkedLinearIssue).toBe('STA-335')
|
|
expect(updates.linkedIssue).toBeNull()
|
|
})
|
|
|
|
// A GitHub-only save must carry no Linear keys: persistence gates the remote
|
|
// Linear capability on key presence, so a synthetic clear fails the save.
|
|
it('sends no Linear keys when the workspace has no Linear link', async () => {
|
|
openDialog({ worktree: { linkedIssue: 42 } })
|
|
|
|
fireEvent.change(issueInput(), { target: { value: '99' } })
|
|
await act(async () => {
|
|
fireEvent.click(saveButton())
|
|
})
|
|
|
|
await waitFor(() => expect(updateWorktreeMeta).toHaveBeenCalledTimes(1))
|
|
const updates = updateWorktreeMeta.mock.calls[0]?.[1] ?? {}
|
|
expect(updates.linkedIssue).toBe(99)
|
|
expect(updates).not.toHaveProperty('linkedLinearIssue')
|
|
})
|
|
|
|
// updateWorktreeMeta stamps lastActivityAt on any comment write, which would
|
|
// reorder the workspace under the time-decay sidebar sort.
|
|
it('sends no comment when only the issue link changed', async () => {
|
|
openDialog({ worktree: { linkedLinearIssue: 'STA-335' } })
|
|
|
|
fireEvent.change(issueInput(), { target: { value: 'STA-999' } })
|
|
await act(async () => {
|
|
fireEvent.click(saveButton())
|
|
})
|
|
|
|
await waitFor(() => expect(updateWorktreeMeta).toHaveBeenCalledTimes(1))
|
|
expect(updateWorktreeMeta.mock.calls[0]?.[1] ?? {}).not.toHaveProperty('comment')
|
|
})
|
|
|
|
// A failed save refetches and reverts the optimistic write, so closing here
|
|
// would report success for an edit that silently undid itself.
|
|
it('keeps the dialog open and reports why when the save fails', async () => {
|
|
openDialog({ worktree: { linkedLinearIssue: 'STA-335' } })
|
|
updateWorktreeMeta.mockResolvedValue({ ok: false, error: 'Runtime is offline' })
|
|
|
|
fireEvent.change(issueInput(), { target: { value: 'STA-999' } })
|
|
await act(async () => {
|
|
fireEvent.click(saveButton())
|
|
})
|
|
|
|
expect(screen.getByRole('alert').textContent).toBe('Runtime is offline')
|
|
expect(useAppStore.getState().activeModal).toBe('edit-meta')
|
|
})
|
|
|
|
it('leaves the Linear link alone when only the comment is edited', async () => {
|
|
openDialog({ worktree: { linkedLinearIssue: 'STA-335' } })
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('Notes about this worktree...'), {
|
|
target: { value: 'updated note' }
|
|
})
|
|
|
|
expect(screen.queryByText(/Saving unlinks/)).toBeNull()
|
|
|
|
await act(async () => {
|
|
fireEvent.click(saveButton())
|
|
})
|
|
|
|
await waitFor(() => expect(updateWorktreeMeta).toHaveBeenCalledTimes(1))
|
|
const updates = updateWorktreeMeta.mock.calls[0]?.[1] ?? {}
|
|
expect(Object.keys(updates)).not.toContain('linkedLinearIssue')
|
|
expect(Object.keys(updates)).not.toContain('linkedIssue')
|
|
expect(updates.comment).toBe('updated note')
|
|
})
|
|
|
|
it('blocks saving an unparseable Linear value', () => {
|
|
openDialog({ worktree: { linkedLinearIssue: 'STA-335' } })
|
|
|
|
fireEvent.change(issueInput(), { target: { value: 'not an issue' } })
|
|
|
|
expect(screen.getByText('Not a Linear issue key or linear.app issue URL.')).toBeTruthy()
|
|
expect(saveButton().disabled).toBe(true)
|
|
})
|
|
|
|
it('is read-only for a folder workspace', () => {
|
|
openDialog({ worktreeId: folderWorkspaceKey('fw-1') })
|
|
|
|
expect(issueInput().disabled).toBe(true)
|
|
expect(providerChip().disabled).toBe(true)
|
|
expect(
|
|
screen.getByText(
|
|
"Issue links are set when a folder workspace is created and can't be changed here yet."
|
|
)
|
|
).toBeTruthy()
|
|
})
|
|
|
|
// Folder workspaces live outside worktreesByRepo, so the indexed lookup alone
|
|
// leaves the row blank and the link it does hold looks lost.
|
|
it('shows a folder workspace its own linked issue', () => {
|
|
openDialog({ worktreeId: folderWorkspaceKey('fw-1'), folderWorkspace: {} })
|
|
|
|
expect(issueInput().value).toBe('STA-901')
|
|
expect(providerChip().textContent).toContain('Linear')
|
|
})
|
|
|
|
// A background `orca worktree set` must not move the baseline mid-edit: the
|
|
// field would read as dirty and a comment-only save would write the stale seed.
|
|
it('keeps the baseline frozen when the store changes while open', async () => {
|
|
openDialog({ worktree: { linkedLinearIssue: 'STA-335' } })
|
|
|
|
act(() => {
|
|
useAppStore.setState({
|
|
worktreesByRepo: {
|
|
[REPO_ID]: [makeWorktree({ linkedLinearIssue: 'STA-999' })]
|
|
}
|
|
})
|
|
})
|
|
fireEvent.change(screen.getByPlaceholderText('Notes about this worktree...'), {
|
|
target: { value: 'still working' }
|
|
})
|
|
await act(async () => {
|
|
fireEvent.click(saveButton())
|
|
})
|
|
|
|
const updates = updateWorktreeMeta.mock.calls[0]?.[1] ?? {}
|
|
expect(updates.comment).toBe('still working')
|
|
expect(updates).not.toHaveProperty('linkedLinearIssue')
|
|
expect(updates).not.toHaveProperty('linkedIssue')
|
|
})
|
|
|
|
// A bare key names no organization. Building one from the connected viewer
|
|
// opens a not-found page — or a colliding issue — for every other workspace
|
|
// the user belongs to, and skips the lookup that would have said so.
|
|
it('resolves a bare Linear key across workspaces rather than the active organization', async () => {
|
|
fetchLinearIssue.mockResolvedValue(makeLinearIssue('https://linear.app/other/issue/STA-999'))
|
|
openDialog({
|
|
worktree: { linkedLinearIssue: 'STA-335' },
|
|
linearViewerOrganizationUrlKey: 'active-org'
|
|
})
|
|
|
|
fireEvent.change(issueInput(), { target: { value: 'STA-999' } })
|
|
await act(async () => {
|
|
fireEvent.click(openIssueButton())
|
|
})
|
|
|
|
expect(fetchLinearIssue.mock.calls[0]?.slice(0, 2)).toEqual(['STA-999', 'all'])
|
|
expect(openUrl).toHaveBeenCalledWith('https://linear.app/other/issue/STA-999')
|
|
})
|
|
|
|
// The stored key belongs to the persisted identifier, so it stays authoritative
|
|
// for it — no lookup, no round trip.
|
|
it('opens a stored Linear link directly from its organization key', async () => {
|
|
openDialog({
|
|
worktree: {
|
|
linkedLinearIssue: 'STA-335',
|
|
linkedLinearIssueOrganizationUrlKey: 'acme'
|
|
},
|
|
linearViewerOrganizationUrlKey: 'active-org'
|
|
})
|
|
|
|
await act(async () => {
|
|
fireEvent.click(openIssueButton())
|
|
})
|
|
|
|
expect(fetchLinearIssue).not.toHaveBeenCalled()
|
|
expect(openUrl).toHaveBeenCalledWith('https://linear.app/acme/issue/STA-335')
|
|
})
|
|
|
|
// Promise.race cannot cancel the lookup, and the field stays editable while it
|
|
// runs — a late result must not open the issue the user just replaced.
|
|
it('does not open a lookup result after the field moved on', async () => {
|
|
let resolveLookup: ((issue: LinearIssue | null) => void) | undefined
|
|
fetchLinearIssue.mockReturnValue(
|
|
new Promise<LinearIssue | null>((resolve) => {
|
|
resolveLookup = resolve
|
|
})
|
|
)
|
|
openDialog({ worktree: { linkedLinearIssue: 'STA-335' } })
|
|
|
|
fireEvent.change(issueInput(), { target: { value: 'STA-999' } })
|
|
fireEvent.click(openIssueButton())
|
|
fireEvent.change(issueInput(), { target: { value: 'STA-777' } })
|
|
await act(async () => {
|
|
resolveLookup?.(makeLinearIssue('https://linear.app/acme/issue/STA-999'))
|
|
})
|
|
|
|
expect(openUrl).not.toHaveBeenCalled()
|
|
expect(
|
|
screen.queryByText(
|
|
"Couldn't open that issue. Check the identifier and your Linear connection."
|
|
)
|
|
).toBeNull()
|
|
})
|
|
|
|
// Displacement is decided at save time, not at open: a link added by the CLI
|
|
// while the dialog sat open must not outlive the save that warned about it.
|
|
it('clears a Linear link added while the dialog was open', async () => {
|
|
openDialog({ worktree: { linkedIssue: 42 } })
|
|
|
|
act(() => {
|
|
useAppStore.setState({
|
|
worktreesByRepo: {
|
|
[REPO_ID]: [makeWorktree({ linkedIssue: 42, linkedLinearIssue: 'STA-999' })]
|
|
}
|
|
})
|
|
})
|
|
fireEvent.change(issueInput(), { target: { value: '99' } })
|
|
|
|
expect(
|
|
screen.getByText('Saving unlinks Linear STA-999 — a workspace tracks one issue.')
|
|
).toBeTruthy()
|
|
|
|
await act(async () => {
|
|
fireEvent.click(saveButton())
|
|
})
|
|
|
|
await waitFor(() => expect(updateWorktreeMeta).toHaveBeenCalledTimes(1))
|
|
const updates = updateWorktreeMeta.mock.calls[0]?.[1] ?? {}
|
|
expect(updates.linkedIssue).toBe(99)
|
|
expect(updates.linkedLinearIssue).toBeNull()
|
|
})
|
|
|
|
// Same issue, different spelling: the link is unchanged, so its title and
|
|
// SSH/runtime source context must survive the save.
|
|
it('keeps the linked work item when the value is only respelled', async () => {
|
|
openDialog({
|
|
worktree: {
|
|
linkedLinearIssue: 'STA-335',
|
|
linkedWorkItem: {
|
|
provider: 'linear',
|
|
type: 'issue',
|
|
number: 335,
|
|
title: 'Fix auth',
|
|
url: 'https://linear.app/acme/issue/STA-335'
|
|
}
|
|
}
|
|
})
|
|
|
|
fireEvent.change(issueInput(), { target: { value: 'sta-335' } })
|
|
|
|
expect(screen.queryByText(/Saving unlinks/)).toBeNull()
|
|
|
|
await act(async () => {
|
|
fireEvent.click(saveButton())
|
|
})
|
|
|
|
await waitFor(() => expect(updateWorktreeMeta).toHaveBeenCalledTimes(1))
|
|
const updates = updateWorktreeMeta.mock.calls[0]?.[1] ?? {}
|
|
expect(updates).not.toHaveProperty('linkedWorkItem')
|
|
expect(updates).not.toHaveProperty('linkedTaskSourceContext')
|
|
expect(updates).not.toHaveProperty('linkedLinearIssue')
|
|
})
|
|
|
|
// The owner index reports a duplicated workspace ID as ambiguous rather than
|
|
// guessing, so the opening row has to name its own bucket.
|
|
it('shows the clicked row when the same workspace ID exists under two hosts', () => {
|
|
openDialog({
|
|
worktree: { linkedIssue: 42 },
|
|
otherRepos: [{ repoId: 'repo-2', worktree: { linkedIssue: 77 } }],
|
|
modalRepoId: 'repo-2'
|
|
})
|
|
|
|
expect(issueInput().value).toBe('77')
|
|
expect(providerChip().textContent).toContain('GitHub')
|
|
})
|
|
|
|
it('dispatches nothing when the dialog is cancelled', async () => {
|
|
openDialog({ worktree: { linkedLinearIssue: 'STA-335' } })
|
|
|
|
fireEvent.change(issueInput(), { target: { value: '99' } })
|
|
await act(async () => {
|
|
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
|
|
})
|
|
|
|
expect(updateWorktreeMeta).not.toHaveBeenCalled()
|
|
expect(useAppStore.getState().activeModal).toBe('none')
|
|
})
|
|
})
|