From f31526b31c975ffa0f052fb24c21e7635af03126 Mon Sep 17 00:00:00 2001 From: joyzoursky Date: Mon, 30 Mar 2026 15:44:43 +0800 Subject: [PATCH] Add Run by column on Test History and details --- apps/web/prisma/schema.prisma | 1 + .../app/api/test-cases/[id]/history/route.ts | 1 + apps/web/src/app/api/test-runs/[id]/route.ts | 1 + .../app/api/test-runs/dispatch/route.test.ts | 7 +++++ .../src/app/api/test-runs/dispatch/route.ts | 11 +++++++ .../test-cases/[id]/history/[runId]/page.tsx | 7 ++++- .../src/app/test-cases/[id]/history/page.tsx | 31 ++++++++++++------- apps/web/src/i18n/locales/en.ts | 2 ++ apps/web/src/i18n/locales/zh-hans.ts | 2 ++ apps/web/src/i18n/locales/zh-hant.ts | 2 ++ .../lib/mcp/__tests__/run-execution.test.ts | 7 +++++ apps/web/src/lib/mcp/run-execution.ts | 7 +++++ apps/web/src/types/database.ts | 1 + 13 files changed, 68 insertions(+), 12 deletions(-) diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index d3e25a07..ebfe6939 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -143,6 +143,7 @@ model TestRun { id String @id @default(cuid()) testCaseId String testCase TestCase @relation(fields: [testCaseId], references: [id], onDelete: Cascade) + triggeredByEmail String? requiredCapability String? requiredRunnerKind String? requestedDeviceId String? diff --git a/apps/web/src/app/api/test-cases/[id]/history/route.ts b/apps/web/src/app/api/test-cases/[id]/history/route.ts index 0e8a694a..f3ba90d8 100644 --- a/apps/web/src/app/api/test-cases/[id]/history/route.ts +++ b/apps/web/src/app/api/test-cases/[id]/history/route.ts @@ -75,6 +75,7 @@ export async function GET( status: true, createdAt: true, error: true, + triggeredByEmail: true, }, skip, take: limit diff --git a/apps/web/src/app/api/test-runs/[id]/route.ts b/apps/web/src/app/api/test-runs/[id]/route.ts index 1a7d6ab8..1e8cd0ec 100644 --- a/apps/web/src/app/api/test-runs/[id]/route.ts +++ b/apps/web/src/app/api/test-runs/[id]/route.ts @@ -165,6 +165,7 @@ export async function GET( completedAt: testRun.completedAt, createdAt: testRun.createdAt, testCaseId: testRun.testCaseId, + triggeredByEmail: testRun.triggeredByEmail, files, events, }); diff --git a/apps/web/src/app/api/test-runs/dispatch/route.test.ts b/apps/web/src/app/api/test-runs/dispatch/route.test.ts index 7861ac3f..a98d853a 100644 --- a/apps/web/src/app/api/test-runs/dispatch/route.test.ts +++ b/apps/web/src/app/api/test-runs/dispatch/route.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const mocks = vi.hoisted(() => ({ verifyAuth: vi.fn(), resolveUserId: vi.fn(), + userFindUnique: vi.fn(), resolveConfigs: vi.fn(), validateTargetUrl: vi.fn(), getTeamDevicesAvailability: vi.fn(), @@ -33,6 +34,9 @@ vi.mock('@/lib/core/prisma', () => ({ testCase: { findUnique: mocks.testCaseFindUnique, }, + user: { + findUnique: mocks.userFindUnique, + }, testCaseFile: { findMany: mocks.testCaseFileFindMany, }, @@ -51,6 +55,7 @@ describe('POST /api/test-runs/dispatch', () => { beforeEach(() => { mocks.verifyAuth.mockReset(); mocks.resolveUserId.mockReset(); + mocks.userFindUnique.mockReset(); mocks.resolveConfigs.mockReset(); mocks.validateTargetUrl.mockReset(); mocks.getTeamDevicesAvailability.mockReset(); @@ -60,6 +65,7 @@ describe('POST /api/test-runs/dispatch', () => { mocks.verifyAuth.mockResolvedValue({ sub: 'auth-user' }); mocks.resolveUserId.mockResolvedValue('user-1'); + mocks.userFindUnique.mockResolvedValue({ email: 'runner@example.com' }); mocks.resolveConfigs.mockResolvedValue({ variables: { CMS: 'https://example.com' }, files: {}, @@ -132,6 +138,7 @@ describe('POST /api/test-runs/dispatch', () => { status: 'QUEUED', requiredCapability: 'BROWSER', requiredRunnerKind: null, + triggeredByEmail: 'runner@example.com', }, }); expect(payload).toMatchObject({ diff --git a/apps/web/src/app/api/test-runs/dispatch/route.ts b/apps/web/src/app/api/test-runs/dispatch/route.ts index ff72995f..0d258cc3 100644 --- a/apps/web/src/app/api/test-runs/dispatch/route.ts +++ b/apps/web/src/app/api/test-runs/dispatch/route.ts @@ -45,6 +45,15 @@ function createConfigurationSnapshot(config: RunTestRequest) { return sanitized; } +async function resolveTriggeredByEmail(userId: string): Promise { + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { email: true }, + }); + + return typeof user?.email === 'string' ? user.email : null; +} + async function validateAndroidTargets( browserConfig: RunTestRequest['browserConfig'] @@ -132,6 +141,7 @@ export async function POST(request: Request) { if (!userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } + const triggeredByEmail = await resolveTriggeredByEmail(userId); const requestHasAndroidTargets = hasAndroidTargets(browserConfig); const testCase = await prisma.testCase.findUnique({ @@ -302,6 +312,7 @@ export async function POST(request: Request) { : null, requestedDeviceId, requestedRunnerId, + triggeredByEmail, } }); diff --git a/apps/web/src/app/test-cases/[id]/history/[runId]/page.tsx b/apps/web/src/app/test-cases/[id]/history/[runId]/page.tsx index 05081961..8f53a192 100644 --- a/apps/web/src/app/test-cases/[id]/history/[runId]/page.tsx +++ b/apps/web/src/app/test-cases/[id]/history/[runId]/page.tsx @@ -21,6 +21,7 @@ interface TestRun { logs: string | null; error: string | null; configurationSnapshot: string | null; + triggeredByEmail?: string | null; events?: TestEvent[]; files?: Array<{ id: string; filename: string; storedName: string; mimeType: string; size: number; createdAt: string }>; } @@ -241,6 +242,7 @@ export default function RunDetailPage({ params }: { params: Promise<{ id: string const runPageHref = projectId ? `/run?testCaseId=${id}&projectId=${projectId}` : `/run?testCaseId=${id}`; + const runByEmail = testRun.triggeredByEmail || '-'; return (
@@ -251,8 +253,11 @@ export default function RunDetailPage({ params }: { params: Promise<{ id: string { label: t('runDetail.breadcrumb.runPrefix', { time: formatDateTime(testRun.createdAt) }) } ]} /> -
+

{t('runDetail.title')}

+

+ {t('runDetail.runBy', { email: runByEmail })} +

diff --git a/apps/web/src/app/test-cases/[id]/history/page.tsx b/apps/web/src/app/test-cases/[id]/history/page.tsx index cfcc5fef..fc8b5bae 100644 --- a/apps/web/src/app/test-cases/[id]/history/page.tsx +++ b/apps/web/src/app/test-cases/[id]/history/page.tsx @@ -19,6 +19,7 @@ interface TestRun { createdAt: string; result: string; error: string | null; + triggeredByEmail?: string | null; } interface HistoryResponse { @@ -226,22 +227,26 @@ export default function HistoryPage({ params }: { params: Promise<{ id: string }
+
+
-
-
+
{Array.from({ length: 8 }, (_, index) => (
-
+
-
+
-
+
+
+
+
@@ -291,9 +296,10 @@ export default function HistoryPage({ params }: { params: Promise<{ id: string }
-
{t('history.table.status')}
-
{t('history.table.date')}
-
{t('history.table.actions')}
+
{t('history.table.status')}
+
{t('history.table.date')}
+
{t('history.table.runBy')}
+
{t('history.table.actions')}
{testRuns.length === 0 ? ( @@ -313,15 +319,18 @@ export default function HistoryPage({ params }: { params: Promise<{ id: string } const isRunRunningOrQueued = isRunActiveStatus(run.status); return (
-
+
{run.status}
-
+
{formatDateTime(run.createdAt)}
-
+
+ {run.triggeredByEmail || '-'} +
+
{isRunRunningOrQueued ? ( ({ + userFindUnique: vi.fn(), testCaseFindUnique: vi.fn(), testRunCreate: vi.fn(), testRunFileCreateMany: vi.fn(), @@ -11,6 +12,9 @@ const mocks = vi.hoisted(() => ({ vi.mock('@/lib/core/prisma', () => ({ prisma: { + user: { + findUnique: mocks.userFindUnique, + }, testCase: { findUnique: mocks.testCaseFindUnique, }, @@ -40,6 +44,7 @@ const { queueTestCaseRun } = await import('@/lib/mcp/run-execution'); describe('queueTestCaseRun', () => { beforeEach(() => { mocks.testCaseFindUnique.mockReset(); + mocks.userFindUnique.mockReset(); mocks.testRunCreate.mockReset(); mocks.testRunFileCreateMany.mockReset(); mocks.resolveConfigs.mockReset(); @@ -51,6 +56,7 @@ describe('queueTestCaseRun', () => { files: {}, allConfigs: [], }); + mocks.userFindUnique.mockResolvedValue({ email: 'runner@example.com' }); mocks.validateTargetUrl.mockReturnValue({ valid: true }); mocks.testRunCreate.mockResolvedValue({ id: 'run-1', @@ -100,6 +106,7 @@ describe('queueTestCaseRun', () => { status: 'QUEUED', requiredCapability: 'BROWSER', requiredRunnerKind: null, + triggeredByEmail: 'runner@example.com', }), }); expect(mocks.testRunFileCreateMany).toHaveBeenCalledTimes(1); diff --git a/apps/web/src/lib/mcp/run-execution.ts b/apps/web/src/lib/mcp/run-execution.ts index c609c4f2..8d2a0a0f 100644 --- a/apps/web/src/lib/mcp/run-execution.ts +++ b/apps/web/src/lib/mcp/run-execution.ts @@ -93,6 +93,12 @@ export async function queueTestCaseRun( testCaseId: string, overrides?: RunTestOverrides ): Promise<{ ok: true; data: QueueTestCaseRunResult } | { ok: false; failure: QueueTestCaseRunFailure }> { + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { email: true }, + }); + const triggeredByEmail = typeof user?.email === 'string' ? user.email : null; + const testCase = await prisma.testCase.findUnique({ where: { id: testCaseId }, include: { @@ -283,6 +289,7 @@ export async function queueTestCaseRun( : null, requestedDeviceId, requestedRunnerId, + triggeredByEmail, } }); diff --git a/apps/web/src/types/database.ts b/apps/web/src/types/database.ts index c5de055d..831484cd 100644 --- a/apps/web/src/types/database.ts +++ b/apps/web/src/types/database.ts @@ -63,6 +63,7 @@ export interface TestCase { export interface TestRun { id: string; testCaseId: string; + triggeredByEmail?: string | null; requiredCapability?: string | null; requiredRunnerKind?: string | null; requestedDeviceId?: string | null;