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
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@
"ext": "^1.7.0",
"fast-glob": "^3.3.3",
"fs-extra": "^10.1.0",
"is-interactive": "^1",
"is-unicode-supported": "^0.1",
"js-yaml": "^4.1.0",
"log": "^6.3.1",
"log-node": "^8.0.3",
Expand Down
7 changes: 4 additions & 3 deletions src/cli/Output.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const symbols = require('./symbols');
const fs = require('fs');
const { stripVTControlCharacters: stripAnsi } = require('node:util');
const path = require('path');
const isInteractiveTerminal = require('is-interactive');
const { PassThrough } = require('stream');

/**
Expand All @@ -29,15 +28,17 @@ class Output {
this.stdout = disableIO ? new PassThrough() : process.stdout;
this.stderr = disableIO ? new PassThrough() : process.stderr;
this.logsFileStream = disableIO ? new PassThrough() : this.openLogsFile();
if (!disableIO && isInteractiveTerminal()) {
const isInteractive = process.stdin.isTTY && process.stdout.isTTY && !process.env.CI;

if (!disableIO && isInteractive) {
this.interactiveStdout = process.stdout;
this.interactiveStderr = process.stderr;
this.interactiveStdin = process.stdin;
}

// We want to apply it only in non-test environment so we check this only if
// disableIO is not explicitly set to true
if (!isInteractiveTerminal() && disableIO === false) {
if (!isInteractive && disableIO === false) {
// We also want to enable verbose by default for non-interactive environments
this.verboseMode = true;
}
Expand Down
2 changes: 1 addition & 1 deletion src/cli/Progresses.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
const cliCursor = require('cli-cursor');
const { stderrCliColors: colors } = require('./colors');
const symbols = require('./symbols');
const isUnicodeSupported = require('is-unicode-supported');
const isUnicodeSupported = require('./is-unicode-supported');
const { stripVTControlCharacters: stripAnsi } = require('node:util');
const { createRegistry, hasOwn } = require('../utils/safe-object');

Expand Down
15 changes: 15 additions & 0 deletions src/cli/is-unicode-supported.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';

module.exports = () => {
if (process.platform !== 'win32') {
return true;
}

return (
Boolean(process.env.CI) ||
Boolean(process.env.WT_SESSION) ||
process.env.TERM_PROGRAM === 'vscode' ||
process.env.TERM === 'xterm-256color' ||
process.env.TERM === 'alacritty'
);
};
2 changes: 1 addition & 1 deletion src/cli/symbols.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const isUnicodeSupported = require('is-unicode-supported');
const isUnicodeSupported = require('./is-unicode-supported');

const main = {
success: '✔',
Expand Down
114 changes: 114 additions & 0 deletions test/unit/src/cli/Output.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,28 @@

const expect = require('chai').expect;
const proxyquire = require('proxyquire');
const sinon = require('sinon');
const { PassThrough } = require('stream');
const Output = require('../../../../src/cli/Output');
const colors = require('../../../../src/cli/colors');
const readStream = require('../../read-stream');

const setProperty = (object, property, value) => {
Object.defineProperty(object, property, {
configurable: true,
writable: true,
value,
});
};

const restoreProperty = (object, property, descriptor) => {
if (descriptor) {
Object.defineProperty(object, property, descriptor);
} else {
delete object[property];
}
};

describe('test/unit/lib/cli/Output.test.js', () => {
/** @type {Output} */
let output;
Expand Down Expand Up @@ -144,4 +162,100 @@ describe('test/unit/lib/cli/Output.test.js', () => {
'Error: TypeError: An error\n at Context.<anonymous>'
);
});

describe('interactivity detection', () => {
let originalStdinIsTTYDescriptor;
let originalStdoutIsTTYDescriptor;
let originalCI;
let hadCI;
let openLogsFileStub;

beforeEach(() => {
originalStdinIsTTYDescriptor = Object.getOwnPropertyDescriptor(process.stdin, 'isTTY');
originalStdoutIsTTYDescriptor = Object.getOwnPropertyDescriptor(process.stdout, 'isTTY');
hadCI = Object.prototype.hasOwnProperty.call(process.env, 'CI');
originalCI = process.env.CI;
openLogsFileStub = sinon.stub(Output.prototype, 'openLogsFile').returns(new PassThrough());
});

afterEach(() => {
openLogsFileStub.restore();
restoreProperty(process.stdin, 'isTTY', originalStdinIsTTYDescriptor);
restoreProperty(process.stdout, 'isTTY', originalStdoutIsTTYDescriptor);
if (hadCI) {
process.env.CI = originalCI;
} else {
delete process.env.CI;
}
});

const createOutput = ({ stdinIsTTY, stdoutIsTTY, ci } = {}) => {
setProperty(process.stdin, 'isTTY', stdinIsTTY);
setProperty(process.stdout, 'isTTY', stdoutIsTTY);
if (ci === undefined) {
delete process.env.CI;
} else {
process.env.CI = ci;
}

return new Output(false);
};

it('enables interactive streams when stdin and stdout are TTY outside CI', () => {
const localOutput = createOutput({ stdinIsTTY: true, stdoutIsTTY: true });

expect(localOutput.interactiveStdout).to.equal(process.stdout);
expect(localOutput.interactiveStderr).to.equal(process.stderr);
expect(localOutput.interactiveStdin).to.equal(process.stdin);
expect(localOutput.verboseMode).to.equal(false);
});

it('disables interactivity when stdin is not TTY', () => {
const localOutput = createOutput({ stdinIsTTY: false, stdoutIsTTY: true });

expect(localOutput.interactiveStdout).to.equal(undefined);
expect(localOutput.interactiveStderr).to.equal(undefined);
expect(localOutput.interactiveStdin).to.equal(undefined);
expect(localOutput.verboseMode).to.equal(true);
});

it('disables interactivity when stdout is not TTY', () => {
const localOutput = createOutput({ stdinIsTTY: true, stdoutIsTTY: false });

expect(localOutput.interactiveStdout).to.equal(undefined);
expect(localOutput.interactiveStderr).to.equal(undefined);
expect(localOutput.interactiveStdin).to.equal(undefined);
expect(localOutput.verboseMode).to.equal(true);
});

it('disables interactivity when CI is truthy', () => {
const localOutput = createOutput({ stdinIsTTY: true, stdoutIsTTY: true, ci: '1' });

expect(localOutput.interactiveStdout).to.equal(undefined);
expect(localOutput.interactiveStderr).to.equal(undefined);
expect(localOutput.interactiveStdin).to.equal(undefined);
expect(localOutput.verboseMode).to.equal(true);
});

it('matches Serverless by allowing interactivity when CI is empty', () => {
const localOutput = createOutput({ stdinIsTTY: true, stdoutIsTTY: true, ci: '' });

expect(localOutput.interactiveStdout).to.equal(process.stdout);
expect(localOutput.interactiveStdin).to.equal(process.stdin);
expect(localOutput.verboseMode).to.equal(false);
});

it('does not enable interactive streams when IO is disabled', () => {
setProperty(process.stdin, 'isTTY', true);
setProperty(process.stdout, 'isTTY', true);
delete process.env.CI;

const localOutput = new Output(false, true);

expect(localOutput.interactiveStdout).to.equal(undefined);
expect(localOutput.interactiveStderr).to.equal(undefined);
expect(localOutput.interactiveStdin).to.equal(undefined);
expect(localOutput.verboseMode).to.equal(false);
});
});
});
43 changes: 43 additions & 0 deletions test/unit/src/cli/Progresses.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
'use strict';

const expect = require('chai').expect;
const proxyquire = require('proxyquire');
const sinon = require('sinon');

const Progresses = require('../../../../src/cli/Progresses');

describe('test/unit/src/cli/Progresses.test.js', () => {
const loadProgresses = (isUnicodeSupported) =>
proxyquire.noCallThru().load('../../../../src/cli/Progresses', {
'./is-unicode-supported': () => isUnicodeSupported,
});

const createProgresses = (columns = 5, rows = 3) => {
const progresses = Object.create(Progresses.prototype);
progresses.output = {
Expand Down Expand Up @@ -51,4 +57,41 @@ describe('test/unit/src/cli/Progresses.test.js', () => {
expect(progresses.exists('service')).to.deep.include({ status: 'success', text: 'done' });
expect(progresses.exists('constructor')).to.equal(undefined);
});

it('uses dots spinner when unicode is supported', () => {
const LocalProgresses = loadProgresses(true);
const bindSigintStub = sinon.stub(LocalProgresses.prototype, 'bindSigint');

try {
const progresses = new LocalProgresses({});

expect(progresses.options.spinner.frames).to.deep.equal([
'⠋',
'⠙',
'⠹',
'⠸',
'⠼',
'⠴',
'⠦',
'⠧',
'⠇',
'⠏',
]);
} finally {
bindSigintStub.restore();
}
});

it('uses dashes spinner when unicode is not supported', () => {
const LocalProgresses = loadProgresses(false);
const bindSigintStub = sinon.stub(LocalProgresses.prototype, 'bindSigint');

try {
const progresses = new LocalProgresses({});

expect(progresses.options.spinner.frames).to.deep.equal(['-', '_']);
} finally {
bindSigintStub.restore();
}
});
});
91 changes: 91 additions & 0 deletions test/unit/src/cli/is-unicode-supported.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use strict';

const expect = require('chai').expect;

const isUnicodeSupported = require('../../../../src/cli/is-unicode-supported');

describe('test/unit/src/cli/is-unicode-supported.test.js', () => {
let originalPlatformDescriptor;
let originalEnv;

const setPlatform = (platform) => {
Object.defineProperty(process, 'platform', {
configurable: true,
value: platform,
});
};

beforeEach(() => {
originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
originalEnv = {
CI: process.env.CI,
WT_SESSION: process.env.WT_SESSION,
TERM_PROGRAM: process.env.TERM_PROGRAM,
TERM: process.env.TERM,
};

delete process.env.CI;
delete process.env.WT_SESSION;
delete process.env.TERM_PROGRAM;
delete process.env.TERM;
});

afterEach(() => {
Object.defineProperty(process, 'platform', originalPlatformDescriptor);

for (const [key, value] of Object.entries(originalEnv)) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
});

it('supports unicode outside Windows', () => {
setPlatform('darwin');

expect(isUnicodeSupported()).to.equal(true);
});

it('does not support unicode on unknown Windows terminals', () => {
setPlatform('win32');

expect(isUnicodeSupported()).to.equal(false);
});

it('supports unicode on Windows in CI', () => {
setPlatform('win32');
process.env.CI = '1';

expect(isUnicodeSupported()).to.equal(true);
});

it('supports unicode in Windows Terminal', () => {
setPlatform('win32');
process.env.WT_SESSION = 'session-id';

expect(isUnicodeSupported()).to.equal(true);
});

it('supports unicode in VS Code terminal on Windows', () => {
setPlatform('win32');
process.env.TERM_PROGRAM = 'vscode';

expect(isUnicodeSupported()).to.equal(true);
});

it('supports unicode in xterm-compatible Windows terminals', () => {
setPlatform('win32');
process.env.TERM = 'xterm-256color';

expect(isUnicodeSupported()).to.equal(true);
});

it('supports unicode in Alacritty on Windows', () => {
setPlatform('win32');
process.env.TERM = 'alacritty';

expect(isUnicodeSupported()).to.equal(true);
});
});
27 changes: 27 additions & 0 deletions test/unit/src/cli/symbols.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use strict';

const expect = require('chai').expect;
const proxyquire = require('proxyquire');

describe('test/unit/src/cli/symbols.test.js', () => {
const loadSymbols = (isUnicodeSupported) =>
proxyquire.noCallThru().load('../../../../src/cli/symbols', {
'./is-unicode-supported': () => isUnicodeSupported,
});

it('exports unicode symbols when unicode is supported', () => {
expect(loadSymbols(true)).to.deep.equal({
success: '✔',
error: '✖',
separator: '›',
});
});

it('exports fallback symbols when unicode is not supported', () => {
expect(loadSymbols(false)).to.deep.equal({
success: '√',
error: '×',
separator: '>',
});
});
});
9 changes: 9 additions & 0 deletions test/unit/src/utils/colors.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ describe('test/unit/src/utils/colors.test.js', () => {
).to.equal(0);
});

it('disables colors for dumb terminals', () => {
expect(
getColorLevel({
stream: { isTTY: true, getColorDepth: () => 24 },
env: { TERM: 'dumb' },
})
).to.equal(0);
});

it('treats COLORTERM=truecolor case-insensitively', () => {
expect(
getColorLevel({
Expand Down
Loading