mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 16:03:47 +00:00
* Extract GitHub task search commit debouncing into a hook Move debounce logic from TaskPage into useGitHubTaskSearchCommit to prevent excessive GitHub API calls on every keystroke. Uses a 750ms idle window before committing search values. Add tests for the new hook. * Keep task rows visible while typing search query Removed premature row-hiding logic from the search input handler that was triggering before debounced queries fire. The handler now only updates the input state; debouncing and query timing are handled by a dedicated hook. Added e2e test verifying search idles before fetching and Enter doesn't double-fetch. * Test that GitHub task search commits cancel on disable and unmount Verify the useGitHubTaskSearchCommit hook properly cleans up pending commits when disabled or when the component unmounts. This prevents unnecessary GitHub API calls during normal user interaction. Also fix e2e test instrumentation to find the active repo through the worktree relationship rather than assuming the first repo with a path.
279 lines
9.4 KiB
TypeScript
279 lines
9.4 KiB
TypeScript
/**
|
|
* E2E tests for the Tasks page.
|
|
*
|
|
* Verifies that opening the tasks view renders correctly and that the
|
|
* source controls and close affordance are present.
|
|
*/
|
|
|
|
import { test, expect } from './helpers/orca-app'
|
|
import { waitForSessionReady, waitForActiveWorktree, getStoreState } from './helpers/store'
|
|
|
|
type RenderedTaskSource = {
|
|
source: string
|
|
active: boolean
|
|
}
|
|
|
|
type TaskSearchRequestProbe = {
|
|
countQueries: string[]
|
|
fetchQueries: string[]
|
|
}
|
|
|
|
const TASK_SOURCE_BY_LABEL: Record<string, string> = {
|
|
GitHub: 'github',
|
|
GitLab: 'gitlab',
|
|
Linear: 'linear',
|
|
Jira: 'jira'
|
|
}
|
|
|
|
async function openTasksPage(page: Parameters<typeof getStoreState>[0]): Promise<void> {
|
|
await page.evaluate(() => {
|
|
const store = window.__store
|
|
if (!store) {
|
|
throw new Error('window.__store is not available')
|
|
}
|
|
store.getState().openTaskPage()
|
|
})
|
|
}
|
|
|
|
async function getRenderedTaskSources(
|
|
page: Parameters<typeof getStoreState>[0]
|
|
): Promise<RenderedTaskSource[]> {
|
|
return page
|
|
.locator('[data-contextual-tour-target="tasks-source-filters"] button')
|
|
.evaluateAll((buttons, sourceByLabel) => {
|
|
return buttons.flatMap((button) => {
|
|
const source =
|
|
button.getAttribute('data-task-source') ??
|
|
sourceByLabel[button.getAttribute('aria-label')?.trim() ?? '']
|
|
if (!source) {
|
|
return []
|
|
}
|
|
const active = button.getAttribute('aria-pressed') === 'true'
|
|
return [{ source, active }]
|
|
})
|
|
}, TASK_SOURCE_BY_LABEL)
|
|
}
|
|
|
|
async function openInstrumentedGitHubTasksPage(
|
|
page: Parameters<typeof getStoreState>[0]
|
|
): Promise<void> {
|
|
await page.evaluate(() => {
|
|
const store = window.__store
|
|
if (!store) {
|
|
throw new Error('window.__store is not available')
|
|
}
|
|
const state = store.getState()
|
|
const activeWorktree = Object.values(state.worktreesByRepo)
|
|
.flat()
|
|
.find((worktree) => worktree.id === state.activeWorktreeId)
|
|
const repo = state.repos.find((candidate) => candidate.id === activeWorktree?.repoId)
|
|
if (!repo || !state.settings) {
|
|
throw new Error('GitHub Tasks probe requires a ready repository and settings')
|
|
}
|
|
const probe: TaskSearchRequestProbe = { countQueries: [], fetchQueries: [] }
|
|
;(
|
|
window as typeof window & { __taskSearchRequestProbe?: TaskSearchRequestProbe }
|
|
).__taskSearchRequestProbe = probe
|
|
const existingIssue = {
|
|
id: 'issue:999',
|
|
type: 'issue' as const,
|
|
number: 999,
|
|
title: 'Existing GitHub issue',
|
|
state: 'open' as const,
|
|
url: 'https://github.com/orca/e2e/issues/999',
|
|
labels: [],
|
|
updatedAt: '2026-08-08T00:00:00Z',
|
|
author: 'orca-e2e',
|
|
repoId: repo.id,
|
|
assignees: [],
|
|
reviewRequests: []
|
|
}
|
|
|
|
store.setState({
|
|
repos: state.repos.map((candidate) =>
|
|
candidate.id === repo.id
|
|
? { ...candidate, upstream: { owner: 'orca', repo: 'e2e' } }
|
|
: candidate
|
|
),
|
|
settings: {
|
|
...state.settings,
|
|
defaultTaskSource: 'github',
|
|
defaultTaskViewPreset: 'issues',
|
|
visibleTaskProviders: ['github']
|
|
},
|
|
taskResumeState: {
|
|
...state.taskResumeState,
|
|
githubItemsPreset: 'issues',
|
|
githubItemsQuery: 'is:issue is:open',
|
|
githubMode: 'items'
|
|
},
|
|
prefetchWorkItems: () => undefined,
|
|
fetchWorkItemsAcrossRepos: async (_repos, _perRepoLimit, _displayLimit, query) => {
|
|
probe.fetchQueries.push(query)
|
|
return { items: [existingIssue], failedCount: 0, githubUnavailable: false }
|
|
},
|
|
countWorkItemsAcrossRepos: async (_repos, query) => {
|
|
probe.countQueries.push(query)
|
|
return { totalCount: 1, totalPages: 1 }
|
|
}
|
|
})
|
|
store
|
|
.getState()
|
|
.openTaskPage(
|
|
{ taskSource: 'github', preselectedRepoId: repo.id },
|
|
{ recordTasksInteraction: false }
|
|
)
|
|
})
|
|
}
|
|
|
|
async function readTaskSearchRequestProbe(
|
|
page: Parameters<typeof getStoreState>[0]
|
|
): Promise<TaskSearchRequestProbe> {
|
|
return page.evaluate(() => {
|
|
const probe = (window as typeof window & { __taskSearchRequestProbe?: TaskSearchRequestProbe })
|
|
.__taskSearchRequestProbe
|
|
if (!probe) {
|
|
throw new Error('Task search request probe is not installed')
|
|
}
|
|
return { countQueries: [...probe.countQueries], fetchQueries: [...probe.fetchQueries] }
|
|
})
|
|
}
|
|
|
|
async function resetTaskSearchRequestProbe(
|
|
page: Parameters<typeof getStoreState>[0]
|
|
): Promise<void> {
|
|
await page.evaluate(() => {
|
|
const probe = (window as typeof window & { __taskSearchRequestProbe?: TaskSearchRequestProbe })
|
|
.__taskSearchRequestProbe
|
|
if (!probe) {
|
|
throw new Error('Task search request probe is not installed')
|
|
}
|
|
probe.countQueries.length = 0
|
|
probe.fetchQueries.length = 0
|
|
})
|
|
}
|
|
|
|
test.describe('Tasks page', () => {
|
|
test.beforeEach(async ({ orcaPage }) => {
|
|
await waitForSessionReady(orcaPage)
|
|
await waitForActiveWorktree(orcaPage)
|
|
})
|
|
|
|
test('opening the tasks view renders the tasks UI', async ({ orcaPage }) => {
|
|
await openTasksPage(orcaPage)
|
|
|
|
await expect
|
|
.poll(async () => getStoreState<string>(orcaPage, 'activeView'), { timeout: 5_000 })
|
|
.toBe('tasks')
|
|
|
|
await expect(orcaPage.getByRole('button', { name: 'Close tasks' })).toBeVisible({
|
|
timeout: 10_000
|
|
})
|
|
|
|
// Why: source buttons are provider-availability aware in CI; assert the
|
|
// stable Tasks chrome instead of a GitHub-only tab set.
|
|
let renderedSources: RenderedTaskSource[] = []
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
renderedSources = await getRenderedTaskSources(orcaPage)
|
|
return renderedSources.length
|
|
},
|
|
{
|
|
timeout: 10_000,
|
|
message: 'Tasks source controls did not render'
|
|
}
|
|
)
|
|
.toBeGreaterThan(1)
|
|
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
renderedSources = await getRenderedTaskSources(orcaPage)
|
|
return renderedSources.some((source) => source.active)
|
|
},
|
|
{
|
|
timeout: 5_000,
|
|
message: 'Active task source did not render'
|
|
}
|
|
)
|
|
.toBe(true)
|
|
if (renderedSources.some((source) => source.source === 'github' && source.active)) {
|
|
await expect(orcaPage.getByRole('button', { name: 'Issues', exact: true })).toBeVisible()
|
|
await expect(orcaPage.getByRole('button', { name: 'PRs', exact: true })).toBeVisible()
|
|
await expect(orcaPage.getByRole('button', { name: 'Projects', exact: true })).toBeVisible()
|
|
await expect(orcaPage.getByPlaceholder(/Search GitHub (issues|PRs)/i)).toBeVisible()
|
|
}
|
|
})
|
|
|
|
test('closing the tasks page returns to the previous view', async ({ orcaPage }) => {
|
|
const previousView = await getStoreState<string>(orcaPage, 'activeView')
|
|
|
|
await openTasksPage(orcaPage)
|
|
await expect
|
|
.poll(async () => getStoreState<string>(orcaPage, 'activeView'), { timeout: 5_000 })
|
|
.toBe('tasks')
|
|
// Sanity: the tasks UI actually painted before we close it.
|
|
await expect(orcaPage.getByRole('button', { name: 'Close tasks' })).toBeVisible()
|
|
|
|
await orcaPage.getByRole('button', { name: 'Close tasks' }).click()
|
|
|
|
await expect
|
|
.poll(async () => getStoreState<string>(orcaPage, 'activeView'), { timeout: 5_000 })
|
|
.toBe(previousView)
|
|
// Why: the load-bearing check is that the previous view's DOM actually
|
|
// re-rendered — a store-only `activeView` assertion would pass even if the
|
|
// terminal/editor surface had silently stopped mounting. `.xterm` is the
|
|
// stable class xterm.js emits on every live terminal pane; if the
|
|
// previous view was terminal (by far the common case in E2E setup), that
|
|
// element must be visible. Tasks-close also hides the "Close tasks"
|
|
// button regardless of previous view, so we assert that too.
|
|
await expect(orcaPage.getByRole('button', { name: 'Close tasks' })).toHaveCount(0)
|
|
if (previousView === 'terminal') {
|
|
await expect(orcaPage.locator('.xterm').first()).toBeVisible({ timeout: 5_000 })
|
|
}
|
|
})
|
|
|
|
test('GitHub search waits for idle, keeps rows visible, and Enter does not double-fetch', async ({
|
|
orcaPage
|
|
}) => {
|
|
await openInstrumentedGitHubTasksPage(orcaPage)
|
|
|
|
const input = orcaPage.getByPlaceholder('Search GitHub issues...')
|
|
const existingIssue = orcaPage.getByText('Existing GitHub issue', { exact: true })
|
|
await expect(input).toBeVisible()
|
|
await expect(existingIssue).toBeVisible()
|
|
|
|
await input.fill('')
|
|
await orcaPage.waitForTimeout(800)
|
|
await resetTaskSearchRequestProbe(orcaPage)
|
|
|
|
await input.pressSequentially('rate', { delay: 400 })
|
|
|
|
expect(await readTaskSearchRequestProbe(orcaPage)).toEqual({
|
|
countQueries: [],
|
|
fetchQueries: []
|
|
})
|
|
await expect(existingIssue).toBeVisible()
|
|
|
|
await expect
|
|
.poll(async () => readTaskSearchRequestProbe(orcaPage), { timeout: 2_000 })
|
|
.toEqual({ countQueries: ['is:issue rate'], fetchQueries: ['is:issue rate'] })
|
|
|
|
await resetTaskSearchRequestProbe(orcaPage)
|
|
await input.pressSequentially('x')
|
|
await input.press('Enter')
|
|
|
|
await expect
|
|
.poll(async () => readTaskSearchRequestProbe(orcaPage), { timeout: 2_000 })
|
|
.toEqual({ countQueries: ['is:issue ratex'], fetchQueries: ['is:issue ratex'] })
|
|
await orcaPage.waitForTimeout(800)
|
|
expect(await readTaskSearchRequestProbe(orcaPage)).toEqual({
|
|
countQueries: ['is:issue ratex'],
|
|
fetchQueries: ['is:issue ratex']
|
|
})
|
|
await expect(input).toHaveValue('is:issue ratex')
|
|
await expect(existingIssue).toBeVisible()
|
|
})
|
|
})
|