215 lines
12 KiB
TypeScript
215 lines
12 KiB
TypeScript
import { signal } from '@angular/core';
|
|
import { HttpClient } from '@angular/common/http';
|
|
import { TestBed } from '@angular/core/testing';
|
|
import { of } from 'rxjs';
|
|
|
|
import { AgentService } from '../../services/agent.service';
|
|
import { SystemService } from '../../services/system.service';
|
|
import { AutomationPanelComponent } from './automation-panel.component';
|
|
|
|
describe('AutomationPanelComponent batch deletion', () => {
|
|
const agent = {
|
|
currentSessionId: signal<string | null>(null),
|
|
runningSessionId: signal<string | null>(null),
|
|
agentStatus: signal('idle'),
|
|
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']);
|
|
|
|
beforeEach(async () => {
|
|
(http.get.and as any).callFake((url: string) => of(url.endsWith('/preferences')
|
|
? { formats: ['docx', 'xlsx'], batch_timeout_seconds: 1800 } : []));
|
|
http.delete.and.returnValue(of({ status: 'deleted' }));
|
|
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: [
|
|
{ provide: AgentService, useValue: agent },
|
|
{ provide: SystemService, useValue: { connectedDevices: signal([]) } },
|
|
{ provide: HttpClient, useValue: http }
|
|
]
|
|
}).compileComponents();
|
|
});
|
|
|
|
it('selects all imported cases by default, toggles all and submits only checked cases with rounds', async () => {
|
|
const tasks = [
|
|
{ row: 2, case_id: '1.1', name: '打开设置', goal: '观察设置', task_config: { model: 'mimo-v2.6-pro' } },
|
|
{ row: 3, case_id: '1.2', name: '观察首页', goal: '观察首页', task_config: {} }
|
|
];
|
|
(http.post.and as any).callFake((url: string) => of(url.endsWith('/preview')
|
|
? { valid: true, tasks, errors: [] } : url.endsWith('/batches')
|
|
? { id: 'selected-batch', status: 'draft' } : { id: 'selected-batch', status: 'running' }));
|
|
const fixture = TestBed.createComponent(AutomationPanelComponent);
|
|
const component = fixture.componentInstance;
|
|
component.tab.set('batches');
|
|
component.upload({ target: { files: [new File(['test'], '用例.xlsx')] } } as unknown as Event);
|
|
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
fixture.detectChanges();
|
|
const checkboxes = () => [...fixture.nativeElement.querySelectorAll('.selection-column input')] as HTMLInputElement[];
|
|
expect(checkboxes().every(c => c.checked)).toBeTrue();
|
|
const toggle = fixture.nativeElement.querySelector('.selection-toolbar button') as HTMLButtonElement;
|
|
expect(toggle.textContent).toContain('全不选');
|
|
toggle.click(); fixture.detectChanges();
|
|
expect(component.selectedCount()).toBe(0);
|
|
expect((fixture.nativeElement.querySelector('.primary') as HTMLButtonElement).disabled).toBeTrue();
|
|
expect(toggle.textContent).toContain('全选');
|
|
toggle.click(); fixture.detectChanges();
|
|
expect(component.selectedCount()).toBe(2);
|
|
checkboxes()[1].click();
|
|
component.repeatCount = 3; component.device = 'test-device';
|
|
fixture.detectChanges();
|
|
expect(fixture.nativeElement.querySelector('.execution-summary').textContent).toContain('共 3 次执行');
|
|
component.startBatch();
|
|
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
const request = http.post.calls.allArgs().find(args => args[0] === '/api/automation/batches');
|
|
expect(request?.[1]).toEqual(jasmine.objectContaining({
|
|
cases: [{ case_id: '1.1', name: '打开设置', goal: '观察设置', task_config: { model: 'mimo-v2.6-pro' } }],
|
|
repeat_count: 3, device_serial: 'test-device'
|
|
}));
|
|
});
|
|
|
|
it('rejects empty selections and invalid or excessive loop counts without creating a batch', async () => {
|
|
const fixture = TestBed.createComponent(AutomationPanelComponent);
|
|
const component = fixture.componentInstance;
|
|
component.preview.set({ valid: true, tasks: [{ row: 2, name: '检查', goal: '观察' }], errors: [] });
|
|
component.device = 'test-device';
|
|
component.startBatch();
|
|
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
expect(component.error()).toContain('至少勾选');
|
|
component.selectedRows.set(new Set([2]));
|
|
for (const count of [0, 1.5, 10001]) {
|
|
component.repeatCount = count; component.startBatch();
|
|
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
expect(component.error()).toContain('循环次数');
|
|
}
|
|
expect(http.post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('confirms deletion and removes the batch from the visible list', async () => {
|
|
const fixture = TestBed.createComponent(AutomationPanelComponent);
|
|
fixture.componentInstance.tab.set('batches');
|
|
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
fixture.componentInstance.batches.set([
|
|
{ id: 'batch-1', name: '批量冒烟', status: 'completed', created_at: 100 }
|
|
]);
|
|
fixture.detectChanges();
|
|
expect(fixture.nativeElement.querySelector('[aria-label="删除批次:批量冒烟"]')).not.toBeNull();
|
|
|
|
spyOn(window, 'confirm').and.returnValue(true);
|
|
const button = fixture.nativeElement.querySelector('.delete-batch') as HTMLButtonElement;
|
|
button.click();
|
|
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
fixture.detectChanges();
|
|
expect(http.delete).toHaveBeenCalledWith('/api/automation/batches/batch-1');
|
|
expect(fixture.componentInstance.batches()).toEqual([]);
|
|
});
|
|
|
|
it('keeps a running batch protected from the delete control', async () => {
|
|
const fixture = TestBed.createComponent(AutomationPanelComponent);
|
|
fixture.componentInstance.tab.set('batches');
|
|
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
fixture.componentInstance.batches.set([
|
|
{ id: 'batch-2', name: '进行中', status: 'running', created_at: 100 }
|
|
]);
|
|
fixture.detectChanges();
|
|
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();
|
|
fixture.componentInstance.report.set({
|
|
id: 'task-1', name: '系统设置检查', status: 'failed', verdict: '未验证',
|
|
error: '模型连续 3 次未提供可执行工具调用', goal: '查看设置', expected: '',
|
|
content: '## 结果\n第一行\n第二行\n\n| 项目 | 状态 |\n| --- | --- |\n| 设置 | 未验证 |'
|
|
});
|
|
fixture.detectChanges();
|
|
const root = fixture.nativeElement as HTMLElement;
|
|
expect(root.querySelector('.report-error')?.textContent).toContain('连续 3 次');
|
|
expect(root.querySelector('.report-body h2')?.textContent).toBe('结果');
|
|
expect(root.querySelector('.report-body')?.textContent).toContain('第一行');
|
|
expect(root.querySelector('.report-body')?.textContent).toContain('第二行');
|
|
expect(root.querySelector('.report-body table td')?.textContent).toBe('设置');
|
|
});
|
|
|
|
it('prepares a batch archive before starting the browser download', async () => {
|
|
const fixture = TestBed.createComponent(AutomationPanelComponent);
|
|
const component = fixture.componentInstance;
|
|
component.tab.set('batches');
|
|
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
component.batch.set({ id: 'batch-3', name: '批量测试', status: 'completed', jobs: [] });
|
|
const click = spyOn(HTMLAnchorElement.prototype, 'click');
|
|
fixture.detectChanges();
|
|
(fixture.nativeElement.querySelector('.actions button:last-child') as HTMLButtonElement).click();
|
|
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
fixture.detectChanges();
|
|
expect(http.post).toHaveBeenCalledWith('/api/automation/batches/batch-3/archive/prepare', {});
|
|
expect(click).toHaveBeenCalled();
|
|
expect(fixture.nativeElement.querySelector('.archive-progress')?.textContent).toContain('下载已开始');
|
|
});
|
|
|
|
it('shows archive errors and lets the user retry', async () => {
|
|
http.post.and.returnValue(of({ status: 'failed', stage: '打包失败', completed: 1, total: 2, elapsed_seconds: 3, eta_seconds: null, error: '截图文件无法读取' }));
|
|
const fixture = TestBed.createComponent(AutomationPanelComponent);
|
|
const component = fixture.componentInstance;
|
|
component.tab.set('batches');
|
|
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
component.batch.set({ id: 'batch-4', name: '批量测试', status: 'completed', jobs: [] });
|
|
fixture.detectChanges();
|
|
(fixture.nativeElement.querySelector('.actions button:last-child') as HTMLButtonElement).click();
|
|
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
fixture.detectChanges();
|
|
expect(fixture.nativeElement.querySelector('.archive-progress')?.textContent).toContain('截图文件无法读取');
|
|
expect((fixture.nativeElement.querySelector('.actions button:last-child') as HTMLButtonElement).disabled).toBeFalse();
|
|
});
|
|
|
|
it('shows live archive progress and estimated remaining time while preparing', async () => {
|
|
http.post.and.returnValue(of({ status: 'preparing', stage: '正在打包任务 2/4', completed: 1, total: 4, elapsed_seconds: 12, eta_seconds: 36, error: null }));
|
|
(http.get.and as any).callFake((url: string) => of(url.endsWith('/status')
|
|
? { status: 'ready', stage: '报告包已就绪', completed: 4, total: 4, elapsed_seconds: 13, eta_seconds: null, error: null }
|
|
: url.endsWith('/preferences') ? { formats: ['docx', 'xlsx'], batch_timeout_seconds: 1800 } : []));
|
|
const fixture = TestBed.createComponent(AutomationPanelComponent);
|
|
const component = fixture.componentInstance;
|
|
component.tab.set('batches');
|
|
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
component.batch.set({ id: 'batch-5', name: '批量测试', status: 'completed', jobs: [] });
|
|
spyOn(HTMLAnchorElement.prototype, 'click');
|
|
fixture.detectChanges();
|
|
(fixture.nativeElement.querySelector('.actions button:last-child') as HTMLButtonElement).click();
|
|
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
fixture.detectChanges();
|
|
const progress = fixture.nativeElement.querySelector('.archive-progress') as HTMLElement;
|
|
expect(progress.textContent).toContain('已处理 1 / 4 个任务');
|
|
expect(progress.textContent).toContain('预计还需约 36 秒');
|
|
expect(progress.querySelector('.archive-spinner')).not.toBeNull();
|
|
fixture.destroy();
|
|
});
|
|
});
|