Skip to content

Commit 09d3d7b

Browse files
authored
Full coverage (#563)
1 parent 8cfc6a6 commit 09d3d7b

File tree

14 files changed

+165
-34
lines changed

14 files changed

+165
-34
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ jobs:
5656
run: pnpm install && pnpm add --global concurrently
5757

5858
- name: Build & Test
59-
run: concurrently --prefix none --group "pnpm:build" "pnpm:test" "pnpm:test:smoke"
59+
run: concurrently --prefix none --group "pnpm:build" "pnpm:test --coverage" "pnpm:test:smoke"
6060

6161
- name: Submit coverage
6262
uses: coverallsapp/github-action@master

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"lint:fix": "pnpm run lint --fix",
3737
"prepublishOnly": "safe-publish-latest && pnpm run build",
3838
"report-coverage": "cat coverage/lcov.info | coveralls",
39-
"test": "vitest --project unit --coverage",
39+
"test": "vitest --project unit",
4040
"test:smoke": "vitest run --project smoke",
4141
"prepare": "husky"
4242
},

src/assert.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
3+
import { assertDeprecated } from './assert';
4+
5+
describe('#assertDeprecated()', () => {
6+
const consoleMock = vi.spyOn(console, 'warn').mockImplementation(() => {});
7+
8+
afterEach(() => {
9+
vi.clearAllMocks();
10+
});
11+
12+
it('prints warning with name and message when condition is false', () => {
13+
assertDeprecated(false, 'example-flag', 'This is an example message.');
14+
15+
expect(consoleMock).toHaveBeenLastCalledWith(
16+
'[concurrently] example-flag is deprecated. This is an example message.',
17+
);
18+
});
19+
20+
it('prints same warning only once', () => {
21+
assertDeprecated(false, 'example-flag', 'This is an example message.');
22+
assertDeprecated(false, 'different-flag', 'This is another message.');
23+
24+
expect(consoleMock).toBeCalledTimes(1);
25+
expect(consoleMock).toHaveBeenLastCalledWith(
26+
'[concurrently] different-flag is deprecated. This is another message.',
27+
);
28+
});
29+
30+
it('prints nothing if condition is true', () => {
31+
assertDeprecated(true, 'example-flag', 'This is an example message.');
32+
33+
expect(consoleMock).not.toHaveBeenCalled();
34+
});
35+
});

src/assert.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const deprecations = new Set<string>();
55
* The message is printed only once.
66
*/
77
export function assertDeprecated(check: boolean, name: string, message: string) {
8-
if (!check) {
8+
if (!check && !deprecations.has(name)) {
99
// eslint-disable-next-line no-console
1010
console.warn(`[concurrently] ${name} is deprecated. ${message}`);
1111
deprecations.add(name);

src/command-parser/expand-wildcard.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ describe.each(['npm run', 'yarn run', 'pnpm run', 'bun run', 'node --run'])(
182182

183183
expect(
184184
parser.parse({
185-
name: '',
185+
name: 'watch-*',
186186
command: `${command} watch-*`,
187187
}),
188188
).toEqual([

src/command-parser/expand-wildcard.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ export class ExpandWildcard implements CommandParser {
102102
return;
103103
}
104104

105-
const [, match] = wildcardRegex.exec(script) || [];
105+
const result = wildcardRegex.exec(script);
106+
const match = result?.[1];
106107
if (match !== undefined) {
107108
return {
108109
...commandInfo,

src/command.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ describe('#start()', () => {
107107
expect(command.stdin).toBe(process.stdin);
108108
});
109109

110+
it('handles process with no stdin', () => {
111+
process.stdin = null;
112+
const { command } = createCommand();
113+
command.start();
114+
115+
expect(command.stdin).toBe(undefined);
116+
});
117+
110118
it('changes state to started', () => {
111119
const { command } = createCommand();
112120
const spy = subscribeSpyTo(command.stateChange);

src/date-format.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,11 +458,16 @@ describe('tokens', () => {
458458
]);
459459

460460
describe('hour', () => {
461-
makeTests('1-12 format', 'h', [
461+
makeTests('1-12 format (1 PM)', 'h', [
462462
[{ expected: '1', input: withTime('13:00:00') }],
463463
[{ expected: '01', input: withTime('13:00:00') }],
464464
]);
465465

466+
makeTests('1-12 format (12 PM)', 'h', [
467+
[{ expected: '12', input: withTime('00:00:00') }],
468+
[{ expected: '12', input: withTime('00:00:00') }],
469+
]);
470+
466471
makeTests('0-23 format', 'H', [
467472
[
468473
{ expected: '0', input: withTime('00:00:00') },

src/date-format.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ let locale: Intl.Locale;
257257
function getLocale(options: FormatterOptions): Intl.Locale {
258258
if (!locale || locale.baseName !== options.locale) {
259259
locale = new Intl.Locale(
260+
/* v8 ignore next - fallback value only for safety */
260261
options.locale || new Intl.DateTimeFormat().resolvedOptions().locale,
261262
);
262263
}
@@ -292,6 +293,7 @@ function makeTokenFn(
292293

293294
const parts = formatter.formatToParts(date);
294295
const part = parts.find((p) => p.type === type);
296+
/* v8 ignore next - fallback value '' only for safety */
295297
return part?.value ?? (fallback ? fallback(date, formatterOptions) : '');
296298
};
297299
}

src/flow-control/teardown.spec.ts

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
1-
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
1+
import { ChildProcess } from 'node:child_process';
22

3+
import { afterEach, describe, expect, it, Mock, vi } from 'vitest';
4+
5+
import { SpawnCommand } from '../command';
36
import { createMockInstance } from '../fixtures/create-mock-instance';
47
import { createFakeProcess, FakeCommand } from '../fixtures/fake-command';
58
import { Logger } from '../logger';
6-
import { getSpawnOpts } from '../spawn';
9+
import * as spawn from '../spawn';
710
import { Teardown } from './teardown';
811

9-
let spawn: Mock;
10-
let logger: Logger;
12+
const spySpawn = vi
13+
.spyOn(spawn, 'spawn')
14+
.mockImplementation(() => createFakeProcess(1) as ChildProcess) as Mock;
15+
const logger = createMockInstance(Logger);
1116
const commands = [new FakeCommand()];
1217
const teardown = 'cowsay bye';
1318

14-
beforeEach(() => {
15-
logger = createMockInstance(Logger);
16-
spawn = vi.fn(() => createFakeProcess(1));
19+
afterEach(() => {
20+
vi.clearAllMocks();
1721
});
1822

19-
const create = (teardown: string[]) =>
23+
const create = (teardown: string[], spawn?: SpawnCommand) =>
2024
new Teardown({
2125
spawn,
2226
logger,
@@ -31,61 +35,94 @@ it('returns commands unchanged', () => {
3135
describe('onFinish callback', () => {
3236
it('does not spawn nothing if there are no teardown commands', () => {
3337
create([]).handle(commands).onFinish();
34-
expect(spawn).not.toHaveBeenCalled();
38+
expect(spySpawn).not.toHaveBeenCalled();
3539
});
3640

3741
it('runs teardown command', () => {
3842
create([teardown]).handle(commands).onFinish();
39-
expect(spawn).toHaveBeenCalledWith(teardown, getSpawnOpts({ stdio: 'raw' }));
43+
expect(spySpawn).toHaveBeenCalledWith(teardown, spawn.getSpawnOpts({ stdio: 'raw' }));
44+
});
45+
46+
it('runs teardown command with custom spawn function', () => {
47+
const customSpawn = vi.fn(() => createFakeProcess(1));
48+
create([teardown], customSpawn).handle(commands).onFinish();
49+
expect(customSpawn).toHaveBeenCalledWith(teardown, spawn.getSpawnOpts({ stdio: 'raw' }));
4050
});
4151

4252
it('waits for teardown command to close', async () => {
4353
const child = createFakeProcess(1);
44-
spawn.mockReturnValue(child);
54+
spySpawn.mockReturnValue(child);
4555

4656
const result = create([teardown]).handle(commands).onFinish();
4757
child.emit('close', 1, null);
4858
await expect(result).resolves.toBeUndefined();
4959
});
5060

51-
it('rejects if teardown command errors', async () => {
61+
it('rejects if teardown command errors (string)', async () => {
62+
const child = createFakeProcess(1);
63+
spySpawn.mockReturnValue(child);
64+
65+
const result = create([teardown]).handle(commands).onFinish();
66+
const error = 'fail';
67+
child.emit('error', error);
68+
await expect(result).rejects.toBeUndefined();
69+
expect(logger.logGlobalEvent).toHaveBeenLastCalledWith('fail');
70+
});
71+
72+
it('rejects if teardown command errors (error)', async () => {
73+
const child = createFakeProcess(1);
74+
spySpawn.mockReturnValue(child);
75+
76+
const result = create([teardown]).handle(commands).onFinish();
77+
const error = new Error('fail');
78+
child.emit('error', error);
79+
await expect(result).rejects.toBeUndefined();
80+
expect(logger.logGlobalEvent).toHaveBeenLastCalledWith(
81+
expect.stringMatching(/Error: fail/),
82+
);
83+
});
84+
85+
it('rejects if teardown command errors (error, no stack)', async () => {
5286
const child = createFakeProcess(1);
53-
spawn.mockReturnValue(child);
87+
spySpawn.mockReturnValue(child);
5488

5589
const result = create([teardown]).handle(commands).onFinish();
56-
child.emit('error', 'fail');
90+
const error = new Error('fail');
91+
delete error.stack;
92+
child.emit('error', error);
5793
await expect(result).rejects.toBeUndefined();
94+
expect(logger.logGlobalEvent).toHaveBeenLastCalledWith('Error: fail');
5895
});
5996

6097
it('runs multiple teardown commands in sequence', async () => {
6198
const child1 = createFakeProcess(1);
6299
const child2 = createFakeProcess(2);
63-
spawn.mockReturnValueOnce(child1).mockReturnValueOnce(child2);
100+
spySpawn.mockReturnValueOnce(child1).mockReturnValueOnce(child2);
64101

65102
const result = create(['foo', 'bar']).handle(commands).onFinish();
66103

67-
expect(spawn).toHaveBeenCalledTimes(1);
68-
expect(spawn).toHaveBeenLastCalledWith('foo', getSpawnOpts({ stdio: 'raw' }));
104+
expect(spySpawn).toHaveBeenCalledTimes(1);
105+
expect(spySpawn).toHaveBeenLastCalledWith('foo', spawn.getSpawnOpts({ stdio: 'raw' }));
69106

70107
child1.emit('close', 1, null);
71108
await new Promise((resolve) => setTimeout(resolve));
72109

73-
expect(spawn).toHaveBeenCalledTimes(2);
74-
expect(spawn).toHaveBeenLastCalledWith('bar', getSpawnOpts({ stdio: 'raw' }));
110+
expect(spySpawn).toHaveBeenCalledTimes(2);
111+
expect(spySpawn).toHaveBeenLastCalledWith('bar', spawn.getSpawnOpts({ stdio: 'raw' }));
75112

76113
child2.emit('close', 0, null);
77114
await expect(result).resolves.toBeUndefined();
78115
});
79116

80117
it('stops running teardown commands on SIGINT', async () => {
81118
const child = createFakeProcess(1);
82-
spawn.mockReturnValue(child);
119+
spySpawn.mockReturnValue(child);
83120

84121
const result = create(['foo', 'bar']).handle(commands).onFinish();
85122
child.emit('close', null, 'SIGINT');
86123
await result;
87124

88-
expect(spawn).toHaveBeenCalledTimes(1);
89-
expect(spawn).toHaveBeenLastCalledWith('foo', expect.anything());
125+
expect(spySpawn).toHaveBeenCalledTimes(1);
126+
expect(spySpawn).toHaveBeenLastCalledWith('foo', expect.anything());
90127
});
91128
});

0 commit comments

Comments
 (0)