Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions packages/cli/src/reporters/__tests__/abstract-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ function countOccurrences (text: string, substring: string): number {
return text.split(substring).length - 1
}

function withStdoutTty<T> (isTTY: boolean, callback: () => T): T {
const stdoutDescriptor = Object.getOwnPropertyDescriptor(process.stdout, 'isTTY')
Object.defineProperty(process.stdout, 'isTTY', { configurable: true, value: isTTY })

try {
return callback()
} finally {
if (stdoutDescriptor) {
Object.defineProperty(process.stdout, 'isTTY', stdoutDescriptor)
} else {
delete (process.stdout as { isTTY?: boolean }).isTTY
}
}
}

describe('AbstractListReporter', () => {
beforeEach(() => {
printLnMock.mockClear()
Expand Down Expand Up @@ -99,11 +114,13 @@ describe('AbstractListReporter', () => {
})

it('should populate _clearString after _printSummary runs', () => {
const { reporter } = makeReporterWithOneCheck()
withStdoutTty(true, () => {
const { reporter } = makeReporterWithOneCheck()

reporter._printSummary()
reporter._printSummary()

expect(reporter._clearString).not.toBe('')
expect(reporter._clearString).not.toBe('')
})
})

it('should include cancellation message with --detach hint after onCancel', () => {
Expand Down Expand Up @@ -139,6 +156,20 @@ describe('AbstractListReporter', () => {
expect(countOccurrences(summary, `Open session: ${TEST_SESSION_URL}`)).toBe(1)
})

it('does not redraw the check list for detached non-TTY output', () => {
withStdoutTty(false, () => {
const { reporter } = makeReporterWithOneCheck(TEST_SESSION_ID)
reporter.onDetach()

const summary = printLnMock.mock.calls.map(([text]: [string]) => text).join('\n')
expect(countOccurrences(summary, SOURCE_FILE)).toBe(1)
expect(countOccurrences(summary, 'My API Check')).toBe(1)
expect(summary).toContain('Checks will continue running in the cloud.')
expect(summary).toContain('1 scheduling, 1 total')
expect(summary).not.toContain('\x1B[K')
})
})

it('should render existing detailed session summary link output', async () => {
vi.mocked(api.testSessions.getShortLink).mockResolvedValue({
data: {
Expand Down
43 changes: 31 additions & 12 deletions packages/cli/src/reporters/abstract-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,26 @@ export default abstract class AbstractListReporter implements Reporter {

onCancel (): void {
this._isCancelling = true
this._clearSummary()
this._printSummary()
if (this._shouldRenderLiveSummary()) {
this._clearSummary()
this._printSummary()
return
}
this._printSummary({ includeCheckTitles: false })
}

onDetach (): void {
this._isDetaching = true
this._clearSummary()
this._printSummary()
if (this._shouldRenderLiveSummary()) {
this._clearSummary()
this._printSummary()
return
}
this._printSummary({ includeCheckTitles: false })
}

private _shouldRenderLiveSummary (): boolean {
return process.stdout.isTTY === true
}

// Clear the summary which was printed by _printStatus from stdout
Expand All @@ -169,17 +181,18 @@ export default abstract class AbstractListReporter implements Reporter {
}
}

_printSummary (opts: { skipCheckCount?: boolean } = {}) {
_printSummary (opts: { skipCheckCount?: boolean, includeCheckTitles?: boolean } = {}) {
const counts = {
numFailed: 0, numPassed: 0, numDegraded: 0,
numRunning: 0, numRetrying: 0, scheduling: 0, numCancelled: 0,
}
const status = []
if (this.checkFilesMap!.size === 1 && this.checkFilesMap!.has(undefined)) {
const includeCheckTitles = opts.includeCheckTitles ?? true
if (includeCheckTitles && this.checkFilesMap!.size === 1 && this.checkFilesMap!.has(undefined)) {
status.push(chalk.bold('Summary:'))
}
for (const [sourceFile, checkMap] of this.checkFilesMap!.entries()) {
if (sourceFile) status.push(sourceFile)
if (includeCheckTitles && sourceFile) status.push(sourceFile)
for (const [, { titleString, result, checkStatus }] of checkMap.entries()) {
if (checkStatus === CheckStatus.SCHEDULING) {
counts.scheduling++
Expand All @@ -196,7 +209,9 @@ export default abstract class AbstractListReporter implements Reporter {
} else {
counts.numPassed++
}
status.push(sourceFile ? indentString(titleString, 2) : titleString)
if (includeCheckTitles) {
status.push(sourceFile ? indentString(titleString, 2) : titleString)
}
}
}

Expand Down Expand Up @@ -241,8 +256,10 @@ export default abstract class AbstractListReporter implements Reporter {

const statusString = status.join('\n')
printLn(statusString)
// Ansi escape code for erasing the line and moving the cursor up
this._clearString = '\r\x1B[K\r\x1B[1A'.repeat(statusString.split('\n').length + 1)
if (this._shouldRenderLiveSummary()) {
// Ansi escape code for erasing the line and moving the cursor up.
this._clearString = '\r\x1B[K\r\x1B[1A'.repeat(statusString.split('\n').length + 1)
}
}

_printBriefSummary () {
Expand Down Expand Up @@ -275,8 +292,10 @@ export default abstract class AbstractListReporter implements Reporter {
status.push('')
const statusString = status.join('\n')
printLn(statusString)
// Ansi escape code for erasing the line and moving the cursor up
this._clearString = '\r\x1B[K\r\x1B[1A'.repeat(statusString.split('\n').length + 1)
if (this._shouldRenderLiveSummary()) {
// Ansi escape code for erasing the line and moving the cursor up.
this._clearString = '\r\x1B[K\r\x1B[1A'.repeat(statusString.split('\n').length + 1)
}
}

// Checks that fail with a run error (e.g. the CLI timing out while waiting for a result)
Expand Down
Loading