UNPKG

@sprucelabs/spruce-cli

Version:

Command line interface for building Spruce skills.

698 lines 24.1 kB
"use strict"; 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