diff --git a/lib/utils/display.js b/lib/utils/display.js index 76303644daacd..06ab3d61c8760 100644 --- a/lib/utils/display.js +++ b/lib/utils/display.js @@ -441,6 +441,7 @@ class Progress { #lastUpdate = 0 #interval #timeout + #rendered = false // We are rendering is enabled option is set and we are not waiting for the render timeout get #rendering () { @@ -527,12 +528,17 @@ class Progress { } this.#clearSpinner() this.#stream.write(this.#spinner.frames[this.#frameIndex]) + this.#rendered = true } #clearSpinner () { + if (!this.#rendered) { + return + } // Move to the start of the line and clear the rest of the line this.#stream.cursorTo(0) this.#stream.clearLine(1) + this.#rendered = false } } diff --git a/test/lib/utils/display.js b/test/lib/utils/display.js index bc4e23485fa3e..837e156a86cb3 100644 --- a/test/lib/utils/display.js +++ b/test/lib/utils/display.js @@ -29,6 +29,7 @@ const mockDisplay = async (t, { mocks, load } = {}) => { ...procLog, display, displayLoad, + streams: logs.streams, ...logs.logs, } } @@ -101,6 +102,50 @@ t.test('can do progress', async (t) => { t.strictSame(outputs, ['before input', 'during input', 'after input']) }) +t.test('progress resume does not clear output when spinner inactive', async (t) => { + const { input, output, outputs, streams } = await mockDisplay(t, { + load: { + progress: true, + }, + }) + + const origClearLine = streams.stderr.clearLine + const origCursorTo = streams.stderr.cursorTo + let clearCalls = 0 + let cursorCalls = 0 + streams.stderr.clearLine = (...args) => { + clearCalls++ + return origClearLine.call(streams.stderr, ...args) + } + streams.stderr.cursorTo = (...args) => { + cursorCalls++ + return origCursorTo.call(streams.stderr, ...args) + } + + t.teardown(() => { + streams.stderr.clearLine = origClearLine + streams.stderr.cursorTo = origCursorTo + }) + + // ensure the spinner has rendered at least once so progress.off clears it + await timers.setTimeout(300) + + const endInput = input.start() + await timers.setTimeout(0) + + clearCalls = 0 + cursorCalls = 0 + + output.standard('no trailing newline') + + endInput() + await timers.setTimeout(0) + + t.equal(clearCalls, 0, 'resume does not clear the line when spinner inactive') + t.equal(cursorCalls, 0, 'resume does not reposition cursor when spinner inactive') + t.equal(outputs.at(-1), 'no trailing newline', 'output remains visible') +}) + t.test('handles log throwing', async (t) => { class ThrowInspect { #crashes = 0;