Skip to content

Commit 27479b0

Browse files
committed
chore(test): improve coverage
1 parent 7b2b36d commit 27479b0

5 files changed

Lines changed: 838 additions & 33 deletions

File tree

test/opt.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,100 @@ describe('legacy opt positional builders', () => {
257257
});
258258
});
259259
});
260+
261+
describe('callable parser merging', () => {
262+
it('opt.options() as callable merges with existing parser', () => {
263+
// First create a parser with positionals
264+
const posParser = pos.positionals(
265+
pos.string({ name: 'input', required: true }),
266+
);
267+
268+
// Use opt.options as callable to merge in options
269+
const merged = opt.options({ verbose: opt.boolean() })(posParser);
270+
271+
expect(merged.__brand, 'to be', 'Parser');
272+
expect(merged.__optionsSchema, 'to satisfy', {
273+
verbose: { type: 'boolean' },
274+
});
275+
expect(merged.__positionalsSchema, 'to satisfy', [
276+
{ name: 'input', type: 'string' },
277+
]);
278+
});
279+
280+
it('pos.positionals() as callable merges with existing parser', () => {
281+
// First create a parser with options
282+
const optParser = opt.options({ verbose: opt.boolean() });
283+
284+
// Use pos.positionals as callable to merge in positionals
285+
const merged = pos.positionals(
286+
pos.string({ name: 'file', required: true }),
287+
)(optParser);
288+
289+
expect(merged.__brand, 'to be', 'Parser');
290+
expect(merged.__optionsSchema, 'to satisfy', {
291+
verbose: { type: 'boolean' },
292+
});
293+
expect(merged.__positionalsSchema, 'to satisfy', [
294+
{ name: 'file', type: 'string' },
295+
]);
296+
});
297+
298+
it('opt.options() preserves transforms from incoming parser', async () => {
299+
const { map } = await import('../src/bargs.js');
300+
301+
// Create a parser with a transform
302+
const parserWithTransform = map(
303+
pos.positionals(pos.string({ name: 'input', required: true })),
304+
({ positionals, values }) => ({
305+
positionals: [positionals[0].toUpperCase()] as const,
306+
values,
307+
}),
308+
);
309+
310+
// Merge with options - should preserve the transform
311+
const merged = opt.options({ verbose: opt.boolean() })(parserWithTransform);
312+
313+
// The __transform should be preserved
314+
const withTransform = merged as typeof merged & {
315+
__transform?: (r: unknown) => unknown;
316+
};
317+
expect(withTransform.__transform, 'to be a', 'function');
318+
});
319+
320+
it('pos.positionals() preserves transforms from incoming parser', async () => {
321+
const { map } = await import('../src/bargs.js');
322+
323+
// Create a parser with a transform
324+
const parserWithTransform = map(
325+
opt.options({ count: opt.number({ default: 1 }) }),
326+
({ positionals, values }) => ({
327+
positionals,
328+
values: { ...values, doubled: values.count * 2 },
329+
}),
330+
);
331+
332+
// Merge with positionals - should preserve the transform
333+
const merged = pos.positionals(pos.string({ name: 'file' }))(
334+
parserWithTransform,
335+
);
336+
337+
// The __transform should be preserved
338+
const withTransform = merged as typeof merged & {
339+
__transform?: (r: unknown) => unknown;
340+
};
341+
expect(withTransform.__transform, 'to be a', 'function');
342+
});
343+
344+
it('validates alias conflicts when merging', () => {
345+
const p1 = opt.options({ verbose: opt.boolean({ aliases: ['v'] }) });
346+
347+
expect(
348+
() =>
349+
opt.options({ version: opt.string({ aliases: ['v'] }) })(
350+
p1 as ReturnType<typeof opt.options>,
351+
),
352+
'to throw',
353+
/Alias conflict.*-v/,
354+
);
355+
});
356+
});

test/parser-commands.test.ts

Lines changed: 187 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,195 @@
11
/**
22
* Tests for command parsing.
33
*
4-
* TODO: Rewrite for new combinator API
4+
* These tests focus on command dispatching and parsing behaviors complementary
5+
* to the tests in bargs.test.ts.
56
*/
7+
import { expect, expectAsync } from 'bupkis';
68
import { describe, it } from 'node:test';
79

10+
import { bargs, handle } from '../src/bargs.js';
11+
import { opt, pos } from '../src/opt.js';
12+
813
describe('command parsing', () => {
9-
it.todo('parses a command with options');
10-
it.todo('parses command positionals');
11-
it.todo('calls command handler');
12-
it.todo('uses defaultCommand when no command given');
13-
it.todo('throws on unknown command');
14-
it.todo('merges global and command options');
14+
it('parses a command with options', async () => {
15+
let result: unknown;
16+
17+
const cli = bargs('test-cli').command(
18+
'greet',
19+
opt.options({
20+
loud: opt.boolean({ default: false }),
21+
name: opt.string({ default: 'world' }),
22+
}),
23+
({ values }) => {
24+
result = values;
25+
},
26+
);
27+
28+
await cli.parseAsync(['greet', '--name', 'Alice', '--loud']);
29+
30+
expect(result, 'to satisfy', {
31+
loud: true,
32+
name: 'Alice',
33+
});
34+
});
35+
36+
it('parses command positionals', async () => {
37+
let result: unknown;
38+
39+
const cli = bargs('test-cli').command(
40+
'greet',
41+
pos.positionals(
42+
pos.string({ name: 'name', required: true }),
43+
pos.number({ default: 1, name: 'times' }),
44+
),
45+
({ positionals }) => {
46+
result = positionals;
47+
},
48+
);
49+
50+
await cli.parseAsync(['greet', 'Alice', '3']);
51+
52+
expect(result, 'to satisfy', ['Alice', 3]);
53+
});
54+
55+
it('calls command handler', async () => {
56+
let handlerCalled = false;
57+
let receivedValues: unknown;
58+
let receivedPositionals: unknown;
59+
60+
const cli = bargs('test-cli').command(
61+
'echo',
62+
handle(
63+
pos.positionals(pos.string({ name: 'message', required: true })),
64+
({ positionals, values }) => {
65+
handlerCalled = true;
66+
receivedValues = values;
67+
receivedPositionals = positionals;
68+
},
69+
),
70+
);
71+
72+
await cli.parseAsync(['echo', 'Hello!']);
73+
74+
expect(handlerCalled, 'to be', true);
75+
expect(receivedPositionals, 'to satisfy', ['Hello!']);
76+
expect(receivedValues, 'to satisfy', {});
77+
});
78+
79+
it('uses defaultCommand when no command given', async () => {
80+
let result: unknown;
81+
82+
const cli = bargs('test-cli')
83+
.command('run', opt.options({ verbose: opt.boolean() }), ({ values }) => {
84+
result = { command: 'run', values };
85+
})
86+
.command('build', opt.options({}), () => {
87+
result = { command: 'build' };
88+
})
89+
.defaultCommand('run');
90+
91+
await cli.parseAsync(['--verbose']);
92+
93+
expect(result, 'to satisfy', {
94+
command: 'run',
95+
values: { verbose: true },
96+
});
97+
});
98+
99+
it('throws on unknown command', async () => {
100+
const cli = bargs('test-cli')
101+
.command(
102+
'add',
103+
handle(opt.options({}), () => {}),
104+
)
105+
.command(
106+
'remove',
107+
handle(opt.options({}), () => {}),
108+
);
109+
110+
await expectAsync(
111+
cli.parseAsync(['unknown']),
112+
'to reject with error satisfying',
113+
/Unknown command: unknown/,
114+
);
115+
});
116+
117+
it('merges global and command options', async () => {
118+
let result: unknown;
119+
120+
const cli = bargs('test-cli')
121+
.globals(
122+
opt.options({
123+
config: opt.string({ default: './config.json' }),
124+
verbose: opt.boolean({ default: false }),
125+
}),
126+
)
127+
.command(
128+
'deploy',
129+
opt.options({
130+
env: opt.enum(['dev', 'staging', 'prod'], { default: 'dev' }),
131+
force: opt.boolean({ default: false }),
132+
}),
133+
({ values }) => {
134+
result = values;
135+
},
136+
);
137+
138+
await cli.parseAsync(['deploy', '--verbose', '--env', 'prod', '--force']);
139+
140+
expect(result, 'to satisfy', {
141+
config: './config.json',
142+
env: 'prod',
143+
force: true,
144+
verbose: true,
145+
});
146+
});
147+
148+
it('parses command with mixed options and positionals', async () => {
149+
let result: unknown;
150+
151+
const cli = bargs('test-cli').command(
152+
'copy',
153+
handle(
154+
pos.positionals(
155+
pos.string({ name: 'source', required: true }),
156+
pos.string({ name: 'dest', required: true }),
157+
)(
158+
opt.options({
159+
force: opt.boolean({ aliases: ['f'] }),
160+
recursive: opt.boolean({ aliases: ['r'] }),
161+
}),
162+
),
163+
({ positionals, values }) => {
164+
result = { positionals, values };
165+
},
166+
),
167+
);
168+
169+
await cli.parseAsync(['copy', '-rf', 'src/', 'dst/']);
170+
171+
expect(result, 'to satisfy', {
172+
positionals: ['src/', 'dst/'],
173+
values: { force: true, recursive: true },
174+
});
175+
});
176+
177+
it('passes positionals through to default command', async () => {
178+
let result: unknown;
179+
180+
const cli = bargs('test-cli')
181+
.command(
182+
'run',
183+
pos.positionals(pos.variadic('string', { name: 'files' })),
184+
({ positionals }) => {
185+
result = positionals;
186+
},
187+
)
188+
.defaultCommand('run');
189+
190+
// When no command matches, positionals go to default command
191+
await cli.parseAsync(['file1.js', 'file2.js', 'file3.js']);
192+
193+
expect(result, 'to satisfy', [['file1.js', 'file2.js', 'file3.js']]);
194+
});
15195
});

0 commit comments

Comments
 (0)