feat: release 1.5.1 with human review and batch filters
This commit is contained in:
1 parent
53c5680260
commit
bdd1aa2c14
36 files changed
+672
-57
No files matched your search
+5
-2
@@ -5,6 +5,7 @@
|
||||
@if (error()) { <p class="error" role="alert">{{error()}}</p> }
|
||||
@if (notice()) { <p class="notice" role="status">{{notice()}}</p> }
|
||||
@if (tab() === 'reports') {
|
||||
@if(batch(); as b){<button type="button" class="back-to-batch" (click)="tab.set('batches')">返回批次:{{b.name}}</button>}
|
||||
<section><h3>默认报告格式</h3><p>任务结束后自动生成。批次报告包同时包含汇总和每个任务的报告。</p>
|
||||
<div class="format-options"><label><input type="checkbox" [(ngModel)]="word"> Word 文档</label>
|
||||
<label><input type="checkbox" [(ngModel)]="excel"> Excel 表格</label></div>
|
||||
@@ -15,7 +16,7 @@
|
||||
<div class="report-overview">
|
||||
<strong class="report-title">{{r.name}}</strong>
|
||||
<div class="report-badges"><span [class.failed]="r.status === 'failed'">{{label(r.status)}}</span><app-test-result-badge [result]="r.test_result" [executionStatus]="r.status" /></div>
|
||||
@if(r.test_result; as result){<app-test-result-card [result]="result" />}
|
||||
@if(r.test_result; as result){<app-test-result-card [result]="result" [sessionId]="r.id" [executionStatus]="r.status" (reviewSaved)="reviewSaved($event)" />}
|
||||
@if (r.error) { <div class="report-error" role="status"><strong>异常原因</strong><p>{{r.error}}</p></div> }
|
||||
</div>
|
||||
<div class="report-meta"><div><strong>任务目标与预期</strong><p>{{r.goal || '未记录'}}</p></div>@if(r.expected){<div><strong>预期结果</strong><p>{{r.expected}}</p></div>}</div>
|
||||
@@ -84,6 +85,8 @@
|
||||
@if(batch(); as b){<section><h3>{{b.name}}</h3><p>{{label(b.status)}} · {{b.reason}}</p>
|
||||
<p class="execution-summary">{{b.case_count || b.jobs?.length || 0}} 个用例 · {{b.repeat_count || 1}} 轮 · 共 {{b.total_executions || b.jobs?.length || 0}} 次执行</p>
|
||||
<div class="actions">
|
||||
<button type="button" (click)="showBatchQueue(b.id)">在任务队列查看此批次</button>
|
||||
@if(agent.currentSessionId()){<button type="button" (click)="tab.set('reports')">查看当前任务报告</button>}
|
||||
@if(b.status === 'running'){<button [disabled]="busy()" (click)="batchAction('pause')">暂停后续任务</button>}
|
||||
@if(b.status === 'draft' || b.status === 'paused'){<button [disabled]="busy()" (click)="batchAction('resume')">{{b.status === 'draft' ? '开始执行' : '继续未开始任务'}}</button>}
|
||||
@if(b.status !== 'running' && b.status !== 'deleting'){<button [disabled]="busy()" (click)="batchAction('retry_failed')">复制失败项为新批次</button>}
|
||||
@@ -103,6 +106,6 @@
|
||||
</div>
|
||||
}
|
||||
@if(b.report_error){<p class="error">报告生成失败:{{b.report_error}}</p>}
|
||||
@for(j of b.jobs; track j.session_id){<div class="job"><button (click)="agent.selectSession(j.session_id,true); tab.set('reports')" [title]="j.case.goal">{{j.case.case_id ? j.case.case_id + ' ' : ''}}{{j.case.name}}@if(j.repeat_count > 1){ · 第 {{j.iteration}} / {{j.repeat_count}} 轮}</button><span>{{label(j.status)}}</span><app-test-result-badge [result]="j.test_result" [executionStatus]="j.status" />@if(j.error){<small>{{j.error}}</small>}</div>}
|
||||
@for(j of b.jobs; track j.session_id){<div class="job" [class.selected]="agent.currentSessionId() === j.session_id"><button (click)="openBatchTask(j.session_id)" [title]="j.case.goal" [attr.aria-pressed]="agent.currentSessionId() === j.session_id">{{j.case.case_id ? j.case.case_id + ' ' : ''}}{{j.case.name}}@if(j.repeat_count > 1){ · 第 {{j.iteration}} / {{j.repeat_count}} 轮}</button><span>{{label(j.status)}}</span><app-test-result-badge [result]="j.test_result" [executionStatus]="j.status" [compact]="true" />@if(j.error){<small>{{j.error}}</small>}</div>}
|
||||
</section>}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
:host { display:block;min-width:0;color:#1e293b;font-size:13px;line-height:1.65; }
|
||||
.back-to-batch{margin-bottom:12px;max-width:100%;text-align:left;overflow-wrap:anywhere}
|
||||
.job.selected{background:#eff6ff;border-radius:8px;padding-left:8px;padding-right:8px}.job.selected button{color:#1d4ed8;border-color:#93c5fd;background:#fff}
|
||||
.tabs {display:flex;gap:4px;padding:4px;margin-bottom:18px;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:20px;
|
||||
button {flex:1;padding:7px 8px;border:0;border-radius:16px;color:#475569;background:transparent;white-space:nowrap;
|
||||
&.selected {color:#2563eb;background:#fff;box-shadow:0 1px 4px #0f172a12;} }
|
||||
|
||||
+26
-1
@@ -12,7 +12,9 @@ describe('AutomationPanelComponent batch deletion', () => {
|
||||
currentSessionId: signal<string | null>(null),
|
||||
runningSessionId: signal<string | null>(null),
|
||||
agentStatus: signal('idle'),
|
||||
sessions: signal<any[]>([])
|
||||
sessions: signal<any[]>([]),
|
||||
resultRevision: signal(0), batchFilter: signal(''), activeTab: signal('automation'),
|
||||
selectSession: jasmine.createSpy('selectSession'), applyCaseReview: jasmine.createSpy('applyCaseReview')
|
||||
};
|
||||
const http = jasmine.createSpyObj<HttpClient>('HttpClient', ['get', 'post', 'delete']);
|
||||
|
||||
@@ -23,6 +25,7 @@ describe('AutomationPanelComponent batch deletion', () => {
|
||||
http.post.and.returnValue(of({ status: 'ready', stage: '报告包已就绪', completed: 0, total: 0, elapsed_seconds: 0, eta_seconds: null, error: null }));
|
||||
http.delete.calls.reset();
|
||||
http.post.calls.reset();
|
||||
agent.currentSessionId.set(null); agent.activeTab.set('automation'); agent.batchFilter.set(''); agent.selectSession.calls.reset();
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AutomationPanelComponent],
|
||||
providers: [
|
||||
@@ -117,6 +120,28 @@ describe('AutomationPanelComponent batch deletion', () => {
|
||||
expect((fixture.nativeElement.querySelector('.delete-batch') as HTMLButtonElement).disabled).toBeTrue();
|
||||
});
|
||||
|
||||
it('查看同批次用例时保留批次列表,可直接筛选该批次队列', () => {
|
||||
const fixture = TestBed.createComponent(AutomationPanelComponent);
|
||||
const component = fixture.componentInstance;
|
||||
component.tab.set('batches');
|
||||
component.batch.set({id:'batch-1',name:'冒烟批次',status:'completed',jobs:[
|
||||
{session_id:'task-a',status:'completed',case:{name:'用例 A',goal:'观察设置'}},
|
||||
{session_id:'task-b',status:'completed',case:{name:'用例 B',goal:'观察主页'}}
|
||||
]});
|
||||
fixture.detectChanges();
|
||||
const jobs: HTMLButtonElement[] = [...fixture.nativeElement.querySelectorAll('.job button')];
|
||||
jobs[0].click(); jobs[1].click();
|
||||
expect(agent.selectSession.calls.allArgs()).toEqual([['task-a',true],['task-b',true]]);
|
||||
expect(component.tab()).toBe('batches');
|
||||
expect(fixture.nativeElement.querySelectorAll('.job').length).toBe(2);
|
||||
const queueRequested = jasmine.createSpy('queueRequested');
|
||||
component.queueRequested.subscribe(queueRequested);
|
||||
component.showBatchQueue('batch-1');
|
||||
expect(agent.batchFilter()).toBe('batch-1'); expect(agent.activeTab()).toBe('tasks');
|
||||
expect(queueRequested).toHaveBeenCalled();
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('shows the failure reason and renders Chinese report structure with line breaks', () => {
|
||||
const fixture = TestBed.createComponent(AutomationPanelComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, DestroyRef, computed, effect, inject, signal } from '@angular/core';
|
||||
import { Component, DestroyRef, computed, effect, inject, output, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
@@ -8,6 +8,7 @@ import { SystemService } from '../../services/system.service';
|
||||
import { renderMarkdownToHtml } from '../../utils/markdown-parser.util';
|
||||
import { TestResultBadgeComponent } from '../test-result-badge/test-result-badge.component';
|
||||
import { TestResultCardComponent } from '../test-result-badge/test-result-card.component';
|
||||
import { CaseReviewResponse } from '../../core/models/session.model';
|
||||
|
||||
interface ArchiveProgress {
|
||||
status: 'idle' | 'preparing' | 'ready' | 'failed';
|
||||
@@ -24,6 +25,7 @@ interface ArchiveProgress {
|
||||
templateUrl: './automation-panel.component.html', styleUrl: './automation-panel.component.scss'
|
||||
})
|
||||
export class AutomationPanelComponent {
|
||||
queueRequested = output<void>();
|
||||
agent = inject(AgentService); system = inject(SystemService); http = inject(HttpClient);
|
||||
tab = signal('reports'); error = signal(''); notice = signal(''); busy = signal(false);
|
||||
batches = signal<any[]>([]); preview = signal<any>(null);
|
||||
@@ -50,6 +52,7 @@ export class AutomationPanelComponent {
|
||||
const timer = setInterval(() => void this.refresh(), 5000);
|
||||
destroy.onDestroy(() => { this.destroyed = true; clearInterval(timer); });
|
||||
effect(() => {
|
||||
this.agent.resultRevision();
|
||||
const sid = this.agent.currentSessionId(); this.report.set(null); this.guidance = '';
|
||||
if (sid) this.http.get<any>(`/api/automation/reports/${encodeURIComponent(sid)}`).subscribe({ next: r => { if (this.agent.currentSessionId() === sid) this.report.set(r); }, error: () => {} });
|
||||
});
|
||||
@@ -57,6 +60,9 @@ export class AutomationPanelComponent {
|
||||
formats() { return [ ...(this.word ? ['docx'] : []), ...(this.excel ? ['xlsx'] : []) ]; }
|
||||
label(s: string) { return ({draft:'待开始',running:'执行中',paused:'已暂停',task_paused:'等待指导',resuming:'等待设备恢复',completed:'执行完成',failed:'失败',cancelled:'已取消',pending:'待执行',queued:'排队中',interrupted:'中断',skipped:'跳过',finalizing:'正在生成报告',deleting:'删除中,可重试清理'} as Record<string,string>)[s] || s; }
|
||||
renderReport(content: string) { return renderMarkdownToHtml(content || ''); }
|
||||
reviewSaved(review: CaseReviewResponse) { this.agent.applyCaseReview(review); if (this.report()?.id === review.id) this.report.update(value => ({...value, ...review})); }
|
||||
openBatchTask(id: string) { this.agent.selectSession(id, true); }
|
||||
showBatchQueue(id: string) { this.agent.batchFilter.set(id); this.agent.activeTab.set('tasks'); this.queueRequested.emit(); }
|
||||
duration(seconds: number) { const minutes = Math.floor(seconds / 60); return minutes ? `${minutes} 分 ${seconds % 60} 秒` : `${seconds} 秒`; }
|
||||
downloadBatchArchive(id: string) { void this.prepareBatchArchive(id); }
|
||||
private async prepareBatchArchive(id: string) {
|
||||
|
||||
Reference in new issue
Block a user