magnitude-test
Version:
A TypeScript client for running automated UI tests through the Magnitude testing platform
157 lines (156 loc) • 7.84 kB
JavaScript
import * as uiState from './uiState';
import { scheduleRedraw, redraw } from './uiRenderer';
import { describeModel } from '@/util'; // Import describeModel
// import { MAX_APP_WIDTH } from "./constants"; // No longer used
// Functions that might be moved or adapted from term-app/index.ts
// For now, keep them here or ensure they are imported if they remain in index.ts
// and are exported.
// We'll need to handle SIGINT and resize eventually.
export class TermAppRenderer {
magnitudeConfig;
initialTests;
firstModelReportedInUI = false; // New flag
// To manage SIGINT listener
sigintListener = null;
constructor(config, initialTests) {
this.magnitudeConfig = config;
this.initialTests = [...initialTests]; // Store a copy
// Initial setup based on config, if needed immediately
if (this.magnitudeConfig.display?.showActions !== undefined) {
uiState.setRenderSettings({ showActions: this.magnitudeConfig.display.showActions });
}
uiState.setCurrentModel(""); // Set to blank
// uiState.setAllRegisteredTests will be called in start() after resetState()
}
start() {
process.stdout.write('\n'); // Ensure output starts on a new line
uiState.resetState(); // Reset all UI state
// Re-apply initial settings after reset
if (this.magnitudeConfig.display?.showActions !== undefined) {
uiState.setRenderSettings({ showActions: this.magnitudeConfig.display.showActions });
}
// uiState.setCurrentModel(""); // No longer needed here, resetState handles it.
this.firstModelReportedInUI = false; // Reset flag on start
uiState.setAllRegisteredTests(this.initialTests); // Set the tests
// Initialize currentTestStates for all tests to 'pending'
const initialTestStates = {}; // Use direct import
for (const test of this.initialTests) {
initialTestStates[test.id] = {
status: 'pending', // Add initial status
stepsAndChecks: [],
modelUsage: [],
// macroUsage: { provider: '', model: '', inputTokens: 0, outputTokens: 0, numCalls: 0 },
// microUsage: { provider: '', numCalls: 0 },
};
}
uiState.setCurrentTestStates(initialTestStates);
uiState.setElapsedTimes({}); // Clear elapsed times
// process.stdout.write('\n'); // Removed unnecessary newline
// logUpdate.clear(); // Removed screen clearing
// process.stdout.write('\x1b[2J\x1b[H'); // Removed screen clearing
// Setup event listeners
this.sigintListener = this.handleExitKeyPress.bind(this);
process.on('SIGINT', this.sigintListener);
// Start the timer interval (adapted from original initializeUI)
if (!uiState.timerInterval) {
const interval = setInterval(() => {
if (uiState.isFinished) {
clearInterval(uiState.timerInterval);
uiState.setTimerInterval(null);
return;
}
let runningTestsExist = false;
uiState.setSpinnerFrame((uiState.spinnerFrame + 1) % uiState.spinnerChars.length);
Object.entries(uiState.currentTestStates).forEach(([testId, state]) => {
// Assuming TestState from runner will have a 'status' field
// For now, we need to check if the state itself implies running
// This part will be more robust once TestState includes status directly
// Now we can use the explicit status
const liveState = state; // Cast to full TestState from runner/state
if (liveState.status === 'running') {
runningTestsExist = true;
// Ensure startedAt is set if running, though TestStateTracker should handle this
if (liveState.startedAt) {
uiState.updateElapsedTime(testId, Date.now() - liveState.startedAt);
}
else {
// This case should ideally not happen if TestState is correctly managed
// uiState.updateElapsedTime(testId, 0);
}
}
});
if (runningTestsExist && !uiState.redrawScheduled) { // Only schedule if not already scheduled
scheduleRedraw(); // Direct call
}
}, 100);
uiState.setTimerInterval(interval);
}
scheduleRedraw(); // Initial draw - Direct call
}
stop() {
if (uiState.isFinished)
return; // Prevent double cleanup
uiState.setIsFinished(true);
if (uiState.timerInterval) {
clearInterval(uiState.timerInterval);
uiState.setTimerInterval(null);
}
// Remove event listeners
if (this.sigintListener) {
process.removeListener('SIGINT', this.sigintListener);
this.sigintListener = null;
}
redraw(); // Last draw to reflect final state
// logUpdate.done(); // Responsibility moved to redraw() when isFinished is true
// process.stderr.write('\n'); // Also moved to redraw()
// DO NOT call process.exit() here
}
onTestStateUpdated(test, newState) {
const currentStates = { ...uiState.currentTestStates };
const testId = test.id;
// Merge new state into existing state for the test
// Ensure startedAt is preserved if already set and newState doesn't have it
const existingState = currentStates[testId] || {};
const updatedTestState = {
...existingState,
...newState,
startedAt: newState.startedAt || existingState.startedAt,
};
currentStates[testId] = updatedTestState;
uiState.setCurrentTestStates(currentStates);
// New logic to detect and set the first model for the UI
if (!this.firstModelReportedInUI &&
newState.modelUsage &&
newState.modelUsage.length > 0) {
const firstModelEntry = newState.modelUsage[0];
let modelNameToReport = undefined;
if (firstModelEntry && firstModelEntry.llm) {
modelNameToReport = describeModel(firstModelEntry.llm);
}
if (modelNameToReport) {
uiState.setCurrentModel(modelNameToReport);
this.firstModelReportedInUI = true;
}
}
// Handle startedAt and elapsedTimes
if (updatedTestState.startedAt && !updatedTestState.doneAt) { // Test is running or just started
if (!uiState.elapsedTimes[testId] || uiState.elapsedTimes[testId] === 0) {
// If it just started, set elapsed time to 0 or based on current time
uiState.updateElapsedTime(testId, Date.now() - updatedTestState.startedAt);
}
}
else if (updatedTestState.startedAt && updatedTestState.doneAt) { // Test finished
uiState.updateElapsedTime(testId, updatedTestState.doneAt - updatedTestState.startedAt);
}
scheduleRedraw(); // Direct call
}
// onResize method removed as per user request.
// Adapted from term-app/index.ts
handleExitKeyPress() {
// No longer distinguish between isFinished, just trigger stop
this.stop();
// The TestSuiteRunner or CLI will handle actual process exit if needed after stop() completes.
// Forcing an exit here might preempt cleanup or final reporting.
// A second SIGINT will terminate if stop() doesn't lead to exit.
}
}