mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
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:
committed by
Brennan Benson
co-authored by
Claude Opus 4.8
parent
b03a2852fb
commit
2d1a6ce22b
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user