test(e2e): cover GitLab Checks panel job details

Add a headless Electron E2E spec that drives the Checks panel against a
synthetic GitLab MR + failed pipeline job (no live credentials, no private
data). It re-registers the hostedReview/workItemDetails/jobTrace IPC handlers
in the main process with fake data, links the MR to the active worktree, then:

- expands the failed job row and asserts the inline details load (the "View
  full logs" affordance + log-tail hint) instead of "No inline details are
  available for this check";
- opens the full-details tab and asserts the job trace renders.

Doubles as the reproducible screenshot source for the PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
William Quintal
2026-07-20 15:29:36 -07:00
committed by Brennan Benson
co-authored by Claude Opus 4.8
parent b03a2852fb
commit 2d1a6ce22b
2 changed files with 214 additions and 0 deletions
@@ -0,0 +1,83 @@
import type { Page } from '@stablyai/playwright-test'
import { mkdirSync } from 'node:fs'
import path from 'node:path'
import { test, expect } from './helpers/orca-app'
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import { openChecks } from './helpers/source-control-ai-generation'
import {
GITLAB_CHECKS_FIXTURE,
installGitLabChecksBackend,
linkGitLabMRToWorktree
} from './helpers/gitlab-checks-fixture'
async function resolveActiveWorktreeId(page: Page): Promise<string> {
const worktreeId = await page.evaluate(() => window.__store?.getState().activeWorktreeId ?? null)
if (!worktreeId) {
throw new Error('E2E fixture did not expose an active worktree')
}
return worktreeId
}
test.describe('GitLab Checks panel job details', () => {
test('expands a failed pipeline job to show its log excerpt', async ({
orcaPage,
electronApp
}, testInfo) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await installGitLabChecksBackend(electronApp)
const worktreeId = await resolveActiveWorktreeId(orcaPage)
await linkGitLabMRToWorktree(orcaPage, worktreeId, GITLAB_CHECKS_FIXTURE.mrNumber)
await openChecks(orcaPage, worktreeId)
// The pipeline job renders as a check row labelled "<stage>: <name>".
const rowLabel = `${GITLAB_CHECKS_FIXTURE.stage}: ${GITLAB_CHECKS_FIXTURE.jobName}`
const jobRow = orcaPage.getByText(rowLabel, { exact: true })
await expect(jobRow).toBeVisible({ timeout: 10_000 })
const screenshotDir = path.join(
process.cwd(),
'validation-screenshots',
`gitlab-checks-job-details-${Date.now()}`
)
mkdirSync(screenshotDir, { recursive: true })
await testInfo.attach('validation-screenshot-dir', {
body: screenshotDir,
contentType: 'text/plain'
})
await orcaPage.screenshot({
path: path.join(screenshotDir, '01-gitlab-checks-job-collapsed.png')
})
// Expand the job row → the fix loads the trace instead of showing
// "No inline details are available for this check." The inline panel keeps
// the full log behind "View full logs" (matching GitHub), so assert the
// failed-jobs section and the log-tail affordance render.
await jobRow.click()
await expect(orcaPage.getByText('No inline details are available for this check.')).toHaveCount(
0
)
const viewFullLogs = orcaPage.getByRole('button', { name: 'View full logs' })
await expect(viewFullLogs).toBeVisible({ timeout: 10_000 })
await expect(orcaPage.getByText('Log tail available in full details.')).toBeVisible()
await orcaPage.screenshot({
path: path.join(screenshotDir, '02-gitlab-checks-job-expanded.png')
})
// Open the full-details tab → the actual trace renders in the editor.
await viewFullLogs.click()
await expect(orcaPage.getByText('AssertionError: expected refunded amount')).toBeVisible({
timeout: 10_000
})
await expect(orcaPage.getByText('ERROR: Job failed: exit code 1')).toBeVisible()
await orcaPage.screenshot({
path: path.join(screenshotDir, '03-gitlab-checks-job-full-logs.png')
})
})
})
+131
View File
@@ -0,0 +1,131 @@
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
import { expect } from '@stablyai/playwright-test'
/**
* Synthetic GitLab MR + failed pipeline job used to drive the Checks panel
* without a live GitLab project. All values are fake so evidence screenshots
* carry no private data.
*/
export const GITLAB_CHECKS_FIXTURE = {
mrNumber: 4242,
jobId: 987654,
jobName: 'Purchase API Component Tests',
stage: 'Component Tests',
webUrl: 'https://gitlab.example.test/acme/orca/-/jobs/987654',
mrUrl: 'https://gitlab.example.test/acme/orca/-/merge_requests/4242',
headSha: 'e2ee2ee2ee2ee2ee2ee2ee2ee2ee2ee2ee2ee2e',
trace: [
'$ pnpm test:component --project purchase-api',
'Running 128 component tests across 6 shards…',
'PASS src/purchase/cart.spec.ts (42 tests)',
'PASS src/purchase/checkout.spec.ts (37 tests)',
'FAIL src/purchase/refund.spec.ts',
' ● refund flow › issues a partial refund for a split payment',
' AssertionError: expected refunded amount 4200 to equal 4250',
' at Object.<anonymous> (src/purchase/refund.spec.ts:118:32)',
'Tests: 1 failed, 127 passed, 128 total',
'ERROR: Job failed: exit code 1'
].join('\n')
} as const
/**
* Re-registers the GitLab + hosted-review IPC handlers in the main process so
* the real renderer fetch path resolves to synthetic data. Returns nothing; the
* handlers stay installed for the lifetime of the app instance.
*/
export async function installGitLabChecksBackend(
electronApp: ElectronApplication,
fixture: typeof GITLAB_CHECKS_FIXTURE = GITLAB_CHECKS_FIXTURE
): Promise<void> {
await electronApp.evaluate(({ ipcMain }, fx) => {
ipcMain.removeHandler('hostedReview:forBranch')
ipcMain.handle('hostedReview:forBranch', async () => ({
provider: 'gitlab',
number: fx.mrNumber,
title: 'Add purchase API component test coverage',
state: 'open',
url: fx.mrUrl,
status: 'failure',
updatedAt: '2026-07-07T12:00:00.000Z',
mergeable: 'MERGEABLE',
headSha: fx.headSha
}))
ipcMain.removeHandler('gitlab:workItemDetails')
ipcMain.handle('gitlab:workItemDetails', async () => ({
item: {
id: `gitlab-mr-${fx.mrNumber}`,
type: 'mr',
number: fx.mrNumber,
title: 'Add purchase API component test coverage',
state: 'opened',
url: fx.mrUrl,
labels: [],
updatedAt: '2026-07-07T12:00:00.000Z',
author: 'e2e-bot'
},
body: 'Synthetic MR used for Checks panel evidence.',
comments: [],
headSha: fx.headSha,
pipelineJobs: [
{
id: fx.jobId,
pipelineId: 55,
name: fx.jobName,
stage: fx.stage,
status: 'failed',
webUrl: fx.webUrl,
duration: 87
}
],
reviewers: []
}))
ipcMain.removeHandler('gitlab:jobTrace')
ipcMain.handle('gitlab:jobTrace', async () => ({ ok: true, trace: fx.trace }))
}, fixture)
}
/**
* Links the synthetic GitLab MR to the given worktree and clears its git status
* so the Checks panel renders the review header + pipeline jobs.
*/
export async function linkGitLabMRToWorktree(
page: Page,
worktreeId: string,
mrNumber: number
): Promise<void> {
await page.evaluate(
({ worktreeId, mrNumber }) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
store.setState((current) => ({
worktreesByRepo: Object.fromEntries(
Object.entries(current.worktreesByRepo).map(([repoId, worktrees]) => [
repoId,
worktrees.map((worktree) =>
worktree.id === worktreeId ? { ...worktree, linkedGitLabMR: mrNumber } : worktree
)
])
),
gitStatusByWorktree: {
...current.gitStatusByWorktree,
[worktreeId]: []
}
}))
},
{ worktreeId, mrNumber }
)
await expect
.poll(
() =>
page.evaluate((worktreeId) => {
const worktrees = Object.values(window.__store?.getState().worktreesByRepo ?? {}).flat()
return worktrees.find((entry) => entry.id === worktreeId)?.linkedGitLabMR ?? null
}, worktreeId),
{ timeout: 5_000 }
)
.toBe(mrNumber)
}