@sprucelabs/spruce-cli
Version:
Command line interface for building Spruce skills.
698 lines • 24.1 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const chalk_1 = __importDefault(require("chalk"));
const duration_utility_1 = __importDefault(require("../../utilities/duration.utility"));
const WidgetFactory_1 = __importDefault(require("../../widgets/WidgetFactory"));
const TestLogItemGenerator_1 = __importDefault(require("./TestLogItemGenerator"));
class TestReporter {
started = false;
table;
bar;
bottomLayout;
testLog;
errorLog;
errorLogItemGenerator;
lastResults = {
totalTestFiles: 0,
customErrors: [],
};
updateInterval;
menu;
statusBar;
window;
widgets;
selectTestPopup;
topLayout;
filterInput;
filterPattern;
clearFilterPatternButton;
isDebugging = false;
watchMode = 'off';
status = 'ready';
countDownTimeInterval;
cwd;
orientation = 'landscape';
handleStartStop;
handleRestart;
handleQuit;
handleRerunTestFile;
handleFilterChange;
handleOpenTestFile;
handleToggleDebug;
handleToggleRpTraining;
handletoggleStandardWatch;
handleToggleSmartWatch;
minWidth = 50;
isRpTraining;
trainingTokenPopup;
// private orientationWhenErrorLogWasShown: TestReporterOrientation =
// 'landscape'
constructor(options) {
this.cwd = options?.cwd;
this.filterPattern = options?.filterPattern;
this.handleRestart = options?.handleRestart;
this.handleStartStop = options?.handleStartStop;
this.handleQuit = options?.handleQuit;
this.handleRerunTestFile = options?.handleRerunTestFile;
this.handleOpenTestFile = options?.handleOpenTestFile;
this.handleFilterChange = options?.handleFilterPatternChange;
this.status = options?.status ?? 'ready';
this.handleToggleDebug = options?.handleToggleDebug;
this.handletoggleStandardWatch = options?.handletoggleStandardWatch;
this.handleToggleRpTraining = options?.handleToggleRpTraining;
this.isDebugging = options?.isDebugging ?? false;
this.watchMode = options?.watchMode ?? 'off';
this.isRpTraining = options?.isRpTraining ?? false;
this.handleToggleSmartWatch = options?.handleToggleSmartWatch;
this.errorLogItemGenerator = new TestLogItemGenerator_1.default();
this.widgets = new WidgetFactory_1.default();
}
setFilterPattern(pattern) {
this.filterPattern = pattern;
this.filterInput.setValue(pattern ?? '');
this.clearFilterPatternButton.setText(buildPatternButtonText(pattern));
}
setIsDebugging(isDebugging) {
this.setLabelStatus('toggleDebug', 'Debug', isDebugging);
this.isDebugging = isDebugging;
}
setIsRpTraining(isRpTraining) {
this.setLabelStatus('rp', 'Train AI', isRpTraining);
this.isRpTraining = isRpTraining;
}
startCountdownTimer(durationSec) {
clearInterval(this.countDownTimeInterval);
this.countDownTimeInterval = undefined;
let remaining = durationSec;
function renderCountdownTime(time) {
return `Starting ${time} `;
}
this.setWatchLabel(renderCountdownTime(remaining));
this.countDownTimeInterval = setInterval(() => {
remaining--;
if (remaining < 0) {
this.stopCountdownTimer();
}
else {
this.setWatchLabel(renderCountdownTime(remaining));
}
}, 1000);
}
stopCountdownTimer() {
clearInterval(this.countDownTimeInterval);
this.countDownTimeInterval = undefined;
this.setWatchMode(this.watchMode);
}
setWatchMode(watchMode) {
this.watchMode = watchMode;
if (!this.countDownTimeInterval) {
let label = watchMode === 'smart' ? 'Smart Watch' : 'Standard Watch';
if (watchMode === 'off') {
label = 'Not Watching';
}
this.setWatchLabel(label);
}
}
setWatchLabel(label) {
const isEnabled = this.watchMode !== 'off';
this.setLabelStatus('watchDropdown', label, isEnabled);
this.menu.setTextForItem('toggleStandardWatch', this.watchMode === 'standard' ? '√ Standard' : 'Standard');
this.menu.setTextForItem('toggleSmartWatch', this.watchMode === 'smart' ? '√ Smart' : 'Smart');
}
setLabelStatus(menuKey, label, isEnabled) {
this.menu.setTextForItem(menuKey, `${label} ^${isEnabled ? 'k' : 'w'}^#^${isEnabled ? 'g' : 'r'}${isEnabled ? ' • ' : ' • '}^`);
}
async start() {
this.started = true;
this.window = this.widgets.Widget('window', {});
this.window.hideCursor();
const { width } = this.window.getFrame();
if (width < this.minWidth) {
throw new Error(`Your screen must be at least ${this.minWidth} characters wide.`);
}
void this.window.on('key', this.handleGlobalKeypress.bind(this));
void this.window.on('kill', this.destroy.bind(this));
void this.window.on('resize', this.handleWindowResize.bind(this));
this.dropInTopLayout();
this.dropInProgressBar();
this.dropInMenu();
this.dropInBottomLayout();
this.dropInStatusBar();
this.dropInTestLog();
this.dropInFilterControls();
this.updateOrientation();
this.setIsDebugging(this.isDebugging);
this.setWatchMode(this.watchMode);
this.setStatus(this.status);
try {
this.setIsRpTraining(this.isRpTraining);
}
catch { }
this.updateInterval = setInterval(this.handleUpdateInterval.bind(this), 1000);
}
handleWindowResize() {
this.updateOrientation();
}
updateOrientation() {
const frame = this.window.getFrame();
if (frame.width * 0.4 > frame.height) {
this.orientation = 'landscape';
}
else {
this.orientation = 'portrait';
}
}
dropInMenu() {
this.menu = this.widgets.Widget('menuBar', {
parent: this.window,
left: 0,
top: 0,
shouldLockWidthWithParent: true,
items: [
{
label: 'Restart ',
value: 'restart',
},
{
label: 'Debug ',
value: 'toggleDebug',
},
{
label: 'Not Watching ',
value: 'watchDropdown',
items: [
{
label: 'Watch all',
value: 'toggleStandardWatch',
},
{
label: 'Smart watch',
value: 'toggleSmartWatch',
},
],
},
{
label: 'Train AI ',
value: 'rp',
},
{
label: 'Quit',
value: 'quit',
},
],
});
void this.menu.on('select', this.handleMenuSelect.bind(this));
}
setStatus(status) {
this.status = status;
this.updateMenuLabels();
this.closeSelectTestPopup();
this.bottomLayout.updateLayout();
if (status === 'ready') {
this.setStatusLabel('Starting...');
}
else if (this.status === 'stopped') {
this.refreshResults();
this.setStatusLabel('');
}
else if (this.status === 'running') {
this.setStatusLabel('Running tests...');
}
}
updateMenuLabels() {
let restartLabel = 'Start ^#^r › ^';
switch (this.status) {
case 'running':
restartLabel = 'Stop ^k^#^g › ^';
break;
case 'stopped':
restartLabel = `Start ^w^#^r › ^`;
break;
case 'ready':
restartLabel = 'Booting ^#^K › ^';
break;
}
this.menu.setTextForItem('restart', restartLabel);
}
handleMenuSelect(payload) {
switch (payload.value) {
case 'quit':
this.handleQuit?.();
break;
case 'restart':
this.handleStartStop?.();
break;
case 'toggleDebug':
this.handleToggleDebug?.();
break;
case 'toggleStandardWatch':
this.handletoggleStandardWatch?.();
break;
case 'toggleSmartWatch':
this.handleToggleSmartWatch?.();
break;
case 'rp':
this.handleToggleRpTraining?.();
break;
}
}
handleUpdateInterval() {
if (this.status !== 'stopped') {
this.refreshResults();
}
}
refreshResults() {
if (this.lastResults) {
this.updateLogs();
}
}
async handleGlobalKeypress(payload) {
if (this.window.getFocusedWidget() === this.filterInput) {
return;
}
switch (payload.key) {
case 'ENTER':
this.handleRestart?.();
break;
case 'CTRL_C':
this.handleQuit?.();
process.exit();
break;
}
}
dropInTestLog() {
const parent = this.bottomLayout.getChildById('results');
if (parent) {
this.testLog = this.widgets.Widget('text', {
parent,
isScrollEnabled: true,
left: 0,
top: 0,
height: '100%',
width: '100%',
shouldLockHeightWithParent: true,
shouldLockWidthWithParent: true,
});
void this.testLog.on('click', this.handleClickTestLog.bind(this));
}
}
async handleClickTestLog(payload) {
const testFile = this.getFileForLine(payload.row);
const { row, column } = payload;
this.closeSelectTestPopup();
if (testFile) {
this.dropInSelectTestPopup({ testFile, column, row });
}
}
async askForTrainingToken() {
if (this.trainingTokenPopup) {
return;
}
this.trainingTokenPopup = this.widgets.Widget('popup', {
parent: this.window,
top: 10,
left: 10,
width: 50,
height: 10,
});
this.widgets.Widget('text', {
parent: this.trainingTokenPopup,
left: 4,
top: 3,
height: 4,
width: this.trainingTokenPopup.getFrame().width - 2,
text: 'Coming soon...',
});
const button = this.widgets.Widget('button', {
parent: this.trainingTokenPopup,
left: 20,
top: 7,
text: ' Ok ',
});
await button.on('click', async () => {
await this.trainingTokenPopup?.destroy();
delete this.trainingTokenPopup;
});
}
closeSelectTestPopup() {
if (this.selectTestPopup) {
void this.selectTestPopup.destroy();
this.selectTestPopup = undefined;
}
}
dropInSelectTestPopup(options) {
const { testFile, row, column } = options;
this.selectTestPopup = this.widgets.Widget('popup', {
parent: this.window,
left: Math.max(1, column - 25),
top: Math.max(4, row - 2),
width: 50,
height: 10,
});
this.widgets.Widget('text', {
parent: this.selectTestPopup,
left: 1,
top: 1,
height: 4,
width: this.selectTestPopup.getFrame().width - 2,
text: `What do you wanna do with:\n\n${testFile}`,
});
const open = this.widgets.Widget('button', {
parent: this.selectTestPopup,
left: 1,
top: 6,
text: 'Open',
});
const rerun = this.widgets.Widget('button', {
parent: this.selectTestPopup,
left: 20,
top: 6,
text: 'Test',
});
const cancel = this.widgets.Widget('button', {
parent: this.selectTestPopup,
left: 37,
top: 6,
text: 'Nevermind',
});
void rerun.on('click', () => {
this.handleRerunTestFile?.(testFile);
this.closeSelectTestPopup();
});
void cancel.on('click', this.closeSelectTestPopup.bind(this));
void open.on('click', () => {
this.openTestFile(testFile);
});
}
openTestFile(testFile) {
this.handleOpenTestFile?.(testFile);
this.closeSelectTestPopup();
}
getFileForLine(row) {
let line = this.testLog.getScrollY();
for (let file of this.lastResults.testFiles ?? []) {
const minRow = line;
const maxRow = line + (file.tests ?? []).length;
if (row >= minRow && row <= maxRow) {
return file.path;
}
line = maxRow;
}
return undefined;
}
dropInProgressBar() {
const parent = this.topLayout.getChildById('progress') ?? this.window;
this.bar = this.widgets.Widget('progressBar', {
parent,
left: 0,
top: 0,
width: parent.getFrame().width,
shouldLockWidthWithParent: true,
label: 'Ready and waiting...',
progress: 0,
});
}
dropInFilterControls() {
const parent = this.topLayout.getChildById('filter') ?? this.window;
const buttonWidth = 3;
this.filterInput = this.widgets.Widget('input', {
parent,
left: 0,
label: 'Pattern',
width: parent.getFrame().width - buttonWidth,
height: 1,
shouldLockWidthWithParent: true,
value: this.filterPattern,
});
void this.filterInput.on('cancel', () => {
this.filterInput.setValue(this.filterPattern ?? '');
});
void this.filterInput.on('submit', (payload) => {
this.handleFilterChange?.(payload.value ?? undefined);
});
this.clearFilterPatternButton = this.widgets.Widget('button', {
parent,
left: this.filterInput.getFrame().width,
width: buttonWidth,
top: 0,
text: buildPatternButtonText(this.filterPattern),
shouldLockRightWithParent: true,
});
void this.clearFilterPatternButton.on('click', () => {
if (this.filterPattern || this.filterPattern?.length === 0) {
this.handleFilterChange?.(undefined);
}
else {
this.filterInput.setValue('');
}
});
}
dropInBottomLayout() {
this.bottomLayout = this.widgets.Widget('layout', {
parent: this.window,
width: '100%',
top: 4,
height: this.window.getFrame().height - 5,
shouldLockWidthWithParent: true,
shouldLockHeightWithParent: true,
rows: [
{
height: '100%',
columns: [
{
id: 'results',
width: '100%',
},
],
},
],
});
}
dropInStatusBar() {
this.statusBar = this.widgets.Widget('text', {
parent: this.window,
top: this.window.getFrame().height - 1,
width: '100%',
shouldLockWidthWithParent: true,
shouldLockBottomWithParent: true,
backgroundColor: 'yellow',
foregroundColor: 'black',
text: '...',
});
}
dropInTopLayout() {
this.topLayout = this.widgets.Widget('layout', {
parent: this.window,
width: '100%',
top: 1,
height: 3,
shouldLockWidthWithParent: true,
shouldLockHeightWithParent: false,
rows: [
{
height: '100%',
columns: [
{
id: 'progress',
width: 50,
},
{
id: 'filter',
},
],
},
],
});
}
updateResults(results) {
if (!this.started) {
throw new Error('You must call start() before anything else.');
}
this.lastResults = {
...this.lastResults,
...results,
};
this.updateProgressBar(results);
const percentPassing = this.generatePercentPassing(results);
const percentComplete = this.generatePercentComplete(results);
this.window.setTitle(`Testing: ${percentComplete}% complete.${percentComplete > 0 ? ` ${percentPassing}% passing.` : ''}`);
this.updateLogs();
}
updateLogs() {
if (this.selectTestPopup) {
return;
}
let { logContent, errorContent } = this.resultsToLogContents(this.lastResults);
this.testLog.setText(logContent);
if (!errorContent) {
// this.errorLog && this.destroyErrorLog()
this.errorLog?.setText(' Nothing to report...');
}
else {
!this.errorLog && this.dropInErrorLog();
const cleanedLog = this.cwd
? errorContent.replace(new RegExp(this.cwd + '/', 'gim'), '')
: errorContent;
this.errorLog?.setText(cleanedLog);
}
}
resultsToLogContents(results) {
let logContent = '';
let errorContent = '';
results.testFiles?.forEach((file) => {
logContent += this.errorLogItemGenerator.generateLogItemForFile(file, this.status);
errorContent +=
this.errorLogItemGenerator.generateErrorLogItemForFile(file);
});
if (this.lastResults.customErrors.length > 0) {
errorContent =
this.lastResults.customErrors
.map((err) => chalk_1.default.red(err))
.join(`\n`) + `\n${errorContent}`;
}
return { logContent, errorContent };
}
dropInErrorLog() {
// this.orientationWhenErrorLogWasShown = this.orientation
if (this.bottomLayout.getRows().length === 1) {
if (this.orientation === 'portrait') {
this.bottomLayout.addRow({
id: 'row_2',
columns: [{ id: 'errors', width: '100%' }],
});
this.bottomLayout.setRowHeight(0, '60%');
this.bottomLayout.setRowHeight(1, '40%');
}
else {
this.bottomLayout.addColumn(0, { id: 'errors', width: '40%' });
this.bottomLayout.setColumnWidth({
rowIdx: 0,
columnIdx: 0,
width: '60%',
});
}
this.bottomLayout.updateLayout();
const cell = this.bottomLayout.getChildById('errors');
if (!cell) {
throw new Error('Pulling child error');
}
this.errorLog = this.widgets.Widget('text', {
parent: cell,
width: '100%',
height: '100%',
isScrollEnabled: true,
shouldAutoScrollWhenAppendingContent: false,
shouldLockHeightWithParent: true,
shouldLockWidthWithParent: true,
padding: { left: 1 },
});
}
}
destroyErrorLog() {
// if (this.errorLog) {
// void this.errorLog?.destroy()
// this.errorLog = undefined
// if (this.orientationWhenErrorLogWasShown === 'landscape') {
// this.bottomLayout.removeColumn(0, 1)
// this.bottomLayout.setColumnWidth({
// rowIdx: 0,
// columnIdx: 0,
// width: '100%',
// })
// } else {
// this.bottomLayout.removeRow(1)
// this.bottomLayout.setRowHeight(0, '100%')
// }
// this.bottomLayout.updateLayout()
// }
}
updateProgressBar(results) {
if (results.totalTestFilesComplete ?? 0 > 0) {
const testsRemaining = results.totalTestFiles - (results.totalTestFilesComplete ?? 0);
if (testsRemaining === 0) {
const { percent, totalTests, totalPassedTests, totalTime } = this.generateProgressStats(results);
this.bar.setLabel(`Finished! ${totalPassedTests} of ${totalTests} (${percent}%) passed in ${duration_utility_1.default.msToFriendly(totalTime)}.${percent < 100 ? ` Don't give up! 💪` : ''}`);
}
else {
this.bar.setLabel(`${results.totalTestFilesComplete} of ${results.totalTestFiles} (${this.generatePercentComplete(results)}%) complete. ${testsRemaining} remaining...`);
}
}
else {
this.bar.setLabel('0%');
}
this.bar.setProgress(this.generatePercentComplete(results) / 100);
}
generateProgressStats(results) {
let totalTests = 0;
let totalPassedTests = 0;
let totalTime = 0;
results.testFiles?.forEach((file) => {
file.tests?.forEach((test) => {
totalTime += test.duration;
if (test.status === 'passed') {
totalPassedTests++;
}
if (test.status === 'passed' || test.status === 'failed') {
totalTests++;
}
});
});
const percent = Math.floor((totalPassedTests / totalTests) * 100);
return {
percent: percent > 0 ? percent : 0,
totalTests,
totalPassedTests,
totalTime,
};
}
generatePercentComplete(results) {
const percent = (results.totalTestFilesComplete ?? 0) / results.totalTestFiles;
if (isNaN(percent)) {
return 0;
}
return Math.round(percent * 100);
}
generatePercentPassing(results) {
const percent = (results.totalPassed ?? 0) / this.getTotalTestFilesRun(results);
if (isNaN(percent)) {
return 0;
}
return Math.floor(percent * 100);
}
getTotalTestFilesRun(results) {
return ((results.totalTests ?? 0) -
(results.totalSkipped ?? 0) -
(results.totalTodo ?? 0));
}
render() {
this.table?.computeCells();
this.table?.draw();
}
async destroy() {
clearInterval(this.updateInterval);
await this.window.destroy();
}
reset() {
this.testLog.setText('');
this.lastResults = {
totalTestFiles: 0,
customErrors: [],
};
this.destroyErrorLog();
this.errorLogItemGenerator.resetStartTimes();
}
setStatusLabel(text) {
this.statusBar.setText(text);
}
appendError(message) {
this.lastResults.customErrors.push(message);
}
}
exports.default = TestReporter;
function buildPatternButtonText(pattern) {
return pattern ? ' x ' : ' - ';
}
//# sourceMappingURL=TestReporter.js.map
;