feat: release 2.3.0 with workspace plugin extensions
This commit is contained in:
1 parent
8380057548
commit
bb873ec0e0
44 files changed
+1497
-48
No files matched your search
+190
@@ -0,0 +1,190 @@
|
||||
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { TaskExtensionPanelsComponent } from './task-extension-panels.component';
|
||||
import { TaskExtensions } from '../../services/task-extensions.service';
|
||||
|
||||
function snapshot(sessionId = 'task-one'): TaskExtensions {
|
||||
return {
|
||||
session_id: sessionId, initial_prompt: '# 原始需求\n\n| 项目 | 要求 |\n| --- | --- |\n| 页面 | 可见 |', editable: true,
|
||||
plugins: [{
|
||||
plugin_id: 'review', name: '人工评审', version: '1.0.0', revision: 0, values: { accuracy: '', note: '', correctness: '' }, history: [],
|
||||
workspace: {
|
||||
panels: [
|
||||
{ id: 'prompt', title: '任务需求', side: 'left', kind: 'initial_prompt', width: { minimum: 240, preferred: 360, maximum: 480 } },
|
||||
{ id: 'review', title: '人工评审', side: 'right', kind: 'form', width: { minimum: 260, preferred: 380, maximum: 520 } }
|
||||
],
|
||||
fields: [
|
||||
{ id: 'accuracy', label: 'AI执行准确性', description: '人工核对步骤', kind: 'choice', options: ['Pass', 'Failed'], max_length: 100, report: true },
|
||||
{ id: 'note', label: '执行备注', description: '', kind: 'text', options: [], max_length: 2000, report: true },
|
||||
{ id: 'correctness', label: '测试结果正确性', description: '', kind: 'choice', options: ['Pass', 'Failed'], max_length: 100, report: true }
|
||||
]
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
describe('步骤回放工作区扩展', () => {
|
||||
let fixture: ComponentFixture<TaskExtensionPanelsComponent>;
|
||||
let component: TaskExtensionPanelsComponent;
|
||||
let http: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] });
|
||||
fixture = TestBed.createComponent(TaskExtensionPanelsComponent);
|
||||
component = fixture.componentInstance;
|
||||
http = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
afterEach(() => { http.verify(); fixture.destroy(); });
|
||||
|
||||
function open(data = snapshot()) {
|
||||
fixture.componentRef.setInput('sessionId', data.session_id);
|
||||
fixture.componentRef.setInput('enabled', true);
|
||||
fixture.detectChanges();
|
||||
http.expectOne(`/api/task-extensions/${data.session_id}`).flush(data);
|
||||
fixture.detectChanges();
|
||||
}
|
||||
function toggle(side: 'left' | 'right') {
|
||||
const plugin = component.data()!.plugins[0];
|
||||
component.toggle(plugin, plugin.workspace.panels.find(panel => panel.side === side)!);
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
it('does not request extensions in video mode and shows no controls without a plugin', () => {
|
||||
fixture.componentRef.setInput('sessionId', 'task-one');
|
||||
fixture.detectChanges();
|
||||
http.expectNone('/api/task-extensions/task-one');
|
||||
open({ ...snapshot(), plugins: [] });
|
||||
expect(fixture.nativeElement.querySelector('nav')).toBeNull();
|
||||
});
|
||||
|
||||
it('starts collapsed, renders the full initial Markdown prompt and honors declared widths', () => {
|
||||
open();
|
||||
expect(fixture.nativeElement.querySelector('aside')).toBeNull();
|
||||
toggle('left');
|
||||
expect(fixture.nativeElement.querySelector('h1').textContent).toBe('原始需求');
|
||||
expect(fixture.nativeElement.querySelector('table td').textContent).toBe('页面');
|
||||
expect(component.width('left')).toBe(360);
|
||||
expect(fixture.nativeElement.querySelector('.report-prose.compact')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps either single panel beside playback below the former stacking breakpoint', () => {
|
||||
open();
|
||||
for (const width of [740, 600]) {
|
||||
fixture.nativeElement.style.width = `${width}px`;
|
||||
for (const side of ['left', 'right'] as const) {
|
||||
toggle(side);
|
||||
const panel = fixture.nativeElement.querySelector('aside').getBoundingClientRect();
|
||||
const player = fixture.nativeElement.querySelector('.player-content').getBoundingClientRect();
|
||||
const layout = fixture.nativeElement.querySelector('.extension-layout').getBoundingClientRect();
|
||||
expect(panel.top).toBe(player.top);
|
||||
expect(panel.width).toBeGreaterThan(0);
|
||||
expect(player.width).toBeGreaterThan(0);
|
||||
expect(side === 'left' ? panel.right : player.right).toBeCloseTo(side === 'left' ? player.left : panel.left, 0);
|
||||
expect(Math.max(panel.right, player.right)).toBeLessThanOrEqual(layout.right + 1);
|
||||
toggle(side);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('allows partial manual selection and keeps unfilled outcomes blank', () => {
|
||||
open(); toggle('right');
|
||||
expect(fixture.nativeElement.querySelector('select')).toBeNull();
|
||||
const groups: NodeListOf<HTMLElement> = fixture.nativeElement.querySelectorAll('.choice-group');
|
||||
expect(fixture.nativeElement.querySelector('[aria-pressed="true"]')).toBeNull();
|
||||
const pass = groups[0].querySelector<HTMLButtonElement>('.pass')!;
|
||||
const failed = groups[0].querySelector<HTMLButtonElement>('.failed')!;
|
||||
pass.click(); fixture.detectChanges();
|
||||
expect(pass.getAttribute('aria-pressed')).toBe('true');
|
||||
pass.click(); fixture.detectChanges();
|
||||
expect(component.drafts['review'].values['accuracy']).toBe('');
|
||||
failed.click(); pass.click(); fixture.detectChanges();
|
||||
expect(failed.getAttribute('aria-pressed')).toBe('false');
|
||||
expect(groups[1].querySelector('[aria-pressed="true"]')).toBeNull();
|
||||
const note: HTMLTextAreaElement = fixture.nativeElement.querySelector('textarea');
|
||||
note.value = '人工核对'; note.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
fixture.nativeElement.querySelector('button.save').click();
|
||||
const request = http.expectOne('/api/task-extensions/task-one/review');
|
||||
expect(request.request.method).toBe('PUT');
|
||||
expect(request.request.body.values).toEqual({ accuracy: 'Pass', note: '人工核对', correctness: '' });
|
||||
expect(request.request.body.expected_revision).toBe(0);
|
||||
const data = snapshot(); data.plugins[0].revision = 1; data.plugins[0].values = request.request.body.values;
|
||||
request.flush(data); fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toContain('人工评审已保存');
|
||||
});
|
||||
|
||||
it('preserves draft and request receipt after a failed save, and permits clearing a result', () => {
|
||||
open(); toggle('right');
|
||||
const plugin = component.data()!.plugins[0];
|
||||
component.edit('review', 'accuracy', 'Failed'); component.save(plugin);
|
||||
const first = http.expectOne('/api/task-extensions/task-one/review');
|
||||
const requestId = first.request.body.request_id;
|
||||
first.flush({}, { status: 503, statusText: 'Unavailable' });
|
||||
component.save(plugin);
|
||||
const retry = http.expectOne('/api/task-extensions/task-one/review');
|
||||
expect(retry.request.body.request_id).toBe(requestId);
|
||||
retry.flush({}, { status: 409, statusText: 'Conflict' });
|
||||
expect(component.drafts['review'].values['accuracy']).toBe('Failed');
|
||||
component.edit('review', 'accuracy', ''); component.save(plugin);
|
||||
const clear = http.expectOne('/api/task-extensions/task-one/review');
|
||||
expect(clear.request.body.values.accuracy).toBe('');
|
||||
expect(clear.request.body.request_id).not.toBe(requestId);
|
||||
clear.flush(snapshot());
|
||||
});
|
||||
|
||||
it('renders Chinese outcomes as mutually exclusive buttons and saves their declared values', () => {
|
||||
const data = snapshot(); data.plugins[0].workspace.fields[0].options = ['通过', '失败'];
|
||||
open(data); toggle('right');
|
||||
const pass: HTMLButtonElement = fixture.nativeElement.querySelector('.choice-button.pass');
|
||||
const failed: HTMLButtonElement = fixture.nativeElement.querySelector('.choice-button.failed');
|
||||
expect(pass.textContent).toContain('通过'); expect(failed.textContent).toContain('失败');
|
||||
pass.click(); failed.click(); fixture.detectChanges();
|
||||
expect(component.drafts['review'].values['accuracy']).toBe('失败');
|
||||
expect(pass.getAttribute('aria-pressed')).toBe('false');
|
||||
expect(failed.getAttribute('aria-pressed')).toBe('true');
|
||||
});
|
||||
|
||||
it('does not apply a delayed save response to the task now displayed', () => {
|
||||
open(); toggle('right');
|
||||
component.edit('review', 'note', '只属于第一个任务'); component.save(component.data()!.plugins[0]);
|
||||
const oldSave = http.expectOne('/api/task-extensions/task-one/review');
|
||||
fixture.componentRef.setInput('sessionId', 'task-two'); fixture.detectChanges();
|
||||
http.expectOne('/api/task-extensions/task-two').flush(snapshot('task-two')); fixture.detectChanges();
|
||||
oldSave.flush(snapshot('task-one')); fixture.detectChanges();
|
||||
expect(component.data()!.session_id).toBe('task-two');
|
||||
expect(component.drafts['review'].values['note']).toBe('');
|
||||
});
|
||||
|
||||
it('keeps unsaved drafts across task switches and removes panels on plugin disable', () => {
|
||||
open(); toggle('right'); component.edit('review', 'note', '保留草稿');
|
||||
fixture.componentRef.setInput('sessionId', 'task-two'); fixture.detectChanges();
|
||||
http.expectOne('/api/task-extensions/task-two').flush(snapshot('task-two'));
|
||||
fixture.componentRef.setInput('sessionId', 'task-one'); fixture.detectChanges();
|
||||
http.expectOne('/api/task-extensions/task-one').flush(snapshot()); fixture.detectChanges();
|
||||
expect(component.drafts['review'].values['note']).toBe('保留草稿');
|
||||
window.dispatchEvent(new Event('artemis-plugin-change')); fixture.detectChanges();
|
||||
http.expectOne('/api/task-extensions/task-one').flush({ ...snapshot(), plugins: [] }); fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('nav')).toBeNull();
|
||||
expect(fixture.nativeElement.querySelector('aside')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps running tasks read-only then enables editing when the task finishes', () => {
|
||||
open({ ...snapshot(), editable: false }); toggle('right');
|
||||
expect(fixture.nativeElement.querySelector('fieldset').disabled).toBeTrue();
|
||||
component.save(component.data()!.plugins[0]); http.expectNone('/api/task-extensions/task-one/review');
|
||||
fixture.componentRef.setInput('taskStatus', 'completed'); fixture.detectChanges();
|
||||
http.expectOne('/api/task-extensions/task-one').flush(snapshot()); fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('fieldset').disabled).toBeFalse();
|
||||
});
|
||||
|
||||
it('collapses the opposite panel on narrow screens', () => {
|
||||
const original = Object.getOwnPropertyDescriptor(window, 'innerWidth')!;
|
||||
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 600 });
|
||||
try {
|
||||
open(); toggle('left'); toggle('right');
|
||||
expect(component.panels('left').length).toBe(0);
|
||||
expect(component.panels('right').length).toBe(1);
|
||||
} finally { Object.defineProperty(window, 'innerWidth', original); }
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user