接入 Ticket12 人工结束与顺序接入
控制台调用独立人工结束接口,补充区间类型、Ticket12 浏览器接缝和测试脚本。
| | |
| | | "test:e2e:ticket08": "playwright test tests/e2e/running-start-and-reception-boundaries.spec.ts", |
| | | "test:e2e:ticket10": "playwright test tests/e2e/pause-resume-reception.spec.ts", |
| | | "test:e2e:ticket11": "playwright test tests/e2e/collector-disconnect-recovery.spec.ts", |
| | | "test:e2e:ticket12": "playwright test tests/e2e/manual-end-sequential-join.spec.ts", |
| | | "test:contract:ticket09": "node tests/test-console-live.contract.test.mjs" |
| | | }, |
| | | "dependencies": { |
| | |
| | | fieldValue: string | null |
| | | }> |
| | | runtimeAttach?: boolean |
| | | receptionIntervals?: Array<{ |
| | | intervalId: number |
| | | bindingId: number | null |
| | | sourceType: 'CABINET_CHANNEL' | 'COLLECTOR_CHANNEL' | string |
| | | sourceId: number |
| | | sourceChannelCode: string | null |
| | | actionAtUtc: string |
| | | startBoundaryAtUtc: string |
| | | endBoundaryAtUtc: string | null |
| | | boundaryNote: string | null |
| | | intervalStatus: 'OPEN' | 'CLOSED' | string |
| | | }> |
| | | } |
| | | |
| | | export interface TestConfigurationOptions { |
| | |
| | | return (await request<TestDetailResponse>(`/system/test/${testId}/stop`, { method: 'POST' })).data |
| | | } |
| | | |
| | | /** 人工结束接收的独立动作接口;保留 stopTest 兼容旧页面调用。 */ |
| | | export async function endTestReception(testId: number): Promise<TestConfigurationDetail> { |
| | | return (await request<TestDetailResponse>(`/system/test/${testId}/reception/end`, { method: 'POST' })).data |
| | | } |
| | | |
| | | export interface LiveSource { |
| | | sourceType: 'CABINET_CHANNEL' | 'COLLECTOR' | string |
| | | sourceId: number |
| | |
| | | import ConsoleProjectChartPanel from '@/components/ConsoleProjectChartPanel.vue' |
| | | import ConsoleDataListPanel from '@/components/ConsoleDataListPanel.vue' |
| | | import { useTests } from '@/stores/tests' |
| | | import { getTestDetail, getTestLiveState, openTestLiveStream, pauseTestReception, resumeTestReception, startTestReception, stopTest, type TestConfigurationDetail, type TestLiveState } from '@/api/tests' |
| | | import { endTestReception, getTestDetail, getTestLiveState, openTestLiveStream, pauseTestReception, resumeTestReception, startTestReception, type TestConfigurationDetail, type TestLiveState } from '@/api/tests' |
| | | |
| | | type ConsoleState = '未开始接收' | '检测中' | '暂停' | '已结束' |
| | | type ReceptionState = '未开始接收' | '接收中' | '暂停接收' | '已结束接收' |
| | |
| | | if (state.value === '已结束') return |
| | | try { |
| | | await ElMessageBox.confirm('停止测试后将不可恢复,系统会停止请求本测试的数据,但不会向设备发送停止命令,是否继续?', '确认停止测试', { confirmButtonText: '停止测试', cancelButtonText: '取消', type: 'warning', confirmButtonClass: 'danger-confirm' }) |
| | | backendDetail.value = await stopTest(testId.value) |
| | | backendDetail.value = await endTestReception(testId.value) |
| | | state.value = '已结束' |
| | | receptionState.value = '已结束接收' |
| | | testStatusLabel.value = '检测完成' |
| New file |
| | |
| | | import { expect, test } from '@playwright/test' |
| | | |
| | | test('人工结束立即释放来源,第二项测试可从后续边界运行中接入', async ({ page }) => { |
| | | const actions: string[] = [] |
| | | const details: Record<number, any> = { |
| | | 42: { testId: 42, testName: '测试 A', testStatus: 'RUNNING', receptionStatus: 'RECEIVING', runtimeAttach: false, |
| | | batteries: [], planSteps: [], thresholds: [], fieldSnapshots: [], receptionIntervals: [ |
| | | { intervalId: 1, sourceType: 'CABINET_CHANNEL', sourceId: 100, actionAtUtc: '2026-08-30T06:00:00Z', startBoundaryAtUtc: '2026-08-30T06:00:00Z', endBoundaryAtUtc: null, intervalStatus: 'OPEN' }, |
| | | ] }, |
| | | 43: { testId: 43, testName: '测试 B', testStatus: 'WAITING', receptionStatus: 'NOT_STARTED', runtimeAttach: false, |
| | | batteries: [], planSteps: [], thresholds: [], fieldSnapshots: [], receptionIntervals: [] }, |
| | | } |
| | | await page.addInitScript(() => localStorage.setItem('battery_detection_auth_token', 'ticket12-test-token')) |
| | | await page.route('**/system/test/**', async route => { |
| | | const match = route.request().url().match(/\/system\/test\/(\d+)/) |
| | | const testId = match ? Number(match[1]) : 42 |
| | | const detail = details[testId] |
| | | if (!detail) return route.continue() |
| | | if (route.request().method() === 'POST' && route.request().url().endsWith('/reception/end')) { |
| | | actions.push(`stop-${testId}`) |
| | | detail.testStatus = 'COMPLETED' |
| | | detail.receptionStatus = 'ENDED' |
| | | detail.receptionIntervals = detail.receptionIntervals.map((interval: any) => ({ ...interval, endBoundaryAtUtc: '2026-08-30T06:05:00Z', intervalStatus: 'CLOSED', boundaryNote: 'MANUAL_END' })) |
| | | } else if (route.request().method() === 'POST' && route.request().url().endsWith('/reception/start')) { |
| | | actions.push(`start-${testId}`) |
| | | detail.testStatus = 'RUNNING' |
| | | detail.receptionStatus = 'RECEIVING' |
| | | detail.runtimeAttach = true |
| | | detail.receptionIntervals = [{ intervalId: 2, sourceType: 'CABINET_CHANNEL', sourceId: 100, actionAtUtc: '2026-08-30T06:05:01Z', startBoundaryAtUtc: '2026-08-30T06:05:01Z', endBoundaryAtUtc: null, intervalStatus: 'OPEN' }] |
| | | } |
| | | if (route.request().url().endsWith('/live-state')) { |
| | | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: { testId, testStatus: detail.testStatus, receptionStatus: detail.receptionStatus, updatedAt: '2026-08-30T06:05:01Z', sources: [], exceptions: [] } }) }) |
| | | return |
| | | } |
| | | if (route.request().url().endsWith('/live')) { |
| | | await route.fulfill({ status: 200, contentType: 'text/event-stream', body: 'event: heartbeat\ndata: {"at":"2026-08-30T06:05:01Z"}\n\n' }) |
| | | return |
| | | } |
| | | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: detail }) }) |
| | | }) |
| | | |
| | | await page.goto('/test-list/42/console') |
| | | await page.getByRole('button', { name: '停止测试', exact: true }).click() |
| | | await page.getByRole('button', { name: '停止测试', exact: true }).last().click() |
| | | await expect(page.locator('.test-state')).toHaveText('测试状态:检测完成') |
| | | await expect(page.locator('.reception-state')).toHaveText('已结束接收') |
| | | |
| | | await page.goto('/test-list/43/console') |
| | | await page.getByRole('button', { name: '开始接收', exact: true }).click() |
| | | await expect(page.getByText('运行中接入', { exact: true })).toBeVisible() |
| | | await expect(page.locator('.reception-state')).toHaveText('接收中') |
| | | expect(actions).toEqual(['stop-42', 'start-43']) |
| | | expect(details[42].receptionIntervals[0].endBoundaryAtUtc <= details[43].receptionIntervals[0].startBoundaryAtUtc).toBeTruthy() |
| | | }) |
| | |
| | | const request = route.request() |
| | | if (request.method() === 'POST' && request.url().endsWith('/reception/pause')) { status = 'PAUSED'; actionRequests.push('pause') } |
| | | if (request.method() === 'POST' && request.url().endsWith('/reception/resume')) { status = 'RECEIVING'; actionRequests.push('resume') } |
| | | if (request.method() === 'POST' && request.url().endsWith('/stop')) { status = 'ENDED'; testStatus = 'COMPLETED'; actionRequests.push('stop') } |
| | | if (request.method() === 'POST' && (request.url().endsWith('/stop') || request.url().endsWith('/reception/end'))) { status = 'ENDED'; testStatus = 'COMPLETED'; actionRequests.push('stop') } |
| | | if (request.url().endsWith('/live-state')) { |
| | | await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, data: { testId: 42, testStatus, receptionStatus: status, updatedAt: '2026-08-30T06:00:00Z', sources: [], exceptions: [] } }) }) |
| | | return |