Skip to content

Commit e6604a3

Browse files
committed
test(options): un-skip the options suite and cover the revived validator
The suite was skipped wholesale, which is why the validator could sit dead for five years without a single test noticing. One assertion had genuinely rotted: `config` is declared in both commonOptions (Array) and globalOptions (String), and the latter wins, so the array-coercion test was asserting a type `config` no longer has. It now exercises the coercion through a command-specific Array option, which is what it was written to check. The existing assertions describe the hard-fail behavior, so they run with NS_STRICT_OPTIONS=error; new cases cover the staging itself, the vision-ng camel/dashed pair, negated booleans, dot-notation object values, and the skipOptionsValidation bypass.
1 parent 6502f13 commit e6604a3

3 files changed

Lines changed: 186 additions & 8 deletions

File tree

lib/options.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -278,10 +278,9 @@ export class Options {
278278
continue;
279279
}
280280

281-
// yargs emits both the dashed and the camelCase spelling of every flag.
282-
// Unknown options have no declaration to normalize against, so key the
283-
// dedupe off the camelCase form both spellings collapse to.
284-
const dedupeKey = this.getNonDashedOptionName(optionName);
281+
// yargs emits every spelling of a flag: dashed, camelCase and, for an
282+
// aliased option, the alias. Collapse them so one flag is reported once.
283+
const dedupeKey = this.getCanonicalOptionName(optionName);
285284
if (_.includes(validated, dedupeKey)) {
286285
continue;
287286
}
@@ -317,6 +316,21 @@ export class Options {
317316
}
318317
}
319318

319+
// The name every spelling of an option collapses to. Unknown options keep
320+
// their own name; there is no declaration to resolve them against.
321+
private getCanonicalOptionName(optionName: string): string {
322+
const correctName = this.getCorrectOptionName(optionName);
323+
if (this.options[correctName]) {
324+
return this.getNonDashedOptionName(correctName);
325+
}
326+
327+
const aliasedName = _.findKey(
328+
this.options,
329+
(opt) => opt.alias === correctName,
330+
);
331+
return this.getNonDashedOptionName(aliasedName || correctName);
332+
}
333+
320334
// yargs strips the `no-` prefix off a negated flag, so an undeclared
321335
// `--no-foo` surfaces as `foo` and would otherwise be reported under a name
322336
// the user never typed.

test/commands-service.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { assert } from "chai";
2+
import { Yok } from "../lib/common/yok";
3+
import { CommandsService } from "../lib/common/services/commands-service";
4+
import { ICommand } from "../lib/common/definitions/commands";
5+
6+
function createTestInjector(command: ICommand): {
7+
injector: Yok;
8+
validatedWith: { called: boolean };
9+
} {
10+
const injector = new Yok();
11+
const validatedWith = { called: false };
12+
13+
injector.register("errors", {
14+
fail: (message: string): void => {
15+
throw new Error(message);
16+
},
17+
failWithHelp: (message: string): void => {
18+
throw new Error(message);
19+
},
20+
});
21+
injector.register("hooksService", {});
22+
injector.register("logger", { warn: (): void => undefined });
23+
injector.register("options", {
24+
validateOptions: (): void => {
25+
validatedWith.called = true;
26+
},
27+
});
28+
injector.register("staticConfig", {});
29+
injector.register("extensibilityService", {});
30+
injector.register("optionsTracker", {});
31+
32+
injector.resolveCommand = () => command;
33+
34+
return { injector, validatedWith };
35+
}
36+
37+
describe("commands-service", () => {
38+
describe("option validation", () => {
39+
const baseCommand: ICommand = {
40+
execute: async (): Promise<void> => undefined,
41+
allowedParameters: [],
42+
canExecute: async (): Promise<boolean> => true,
43+
};
44+
45+
it("validates the options of an ordinary command", async () => {
46+
const { injector, validatedWith } = createTestInjector(baseCommand);
47+
const service = injector.resolve(CommandsService);
48+
49+
await (<any>service).tryExecuteCommandAction("info", []);
50+
51+
assert.isTrue(validatedWith.called);
52+
});
53+
54+
it("skips validation for a command that forwards its options", async () => {
55+
const { injector, validatedWith } = createTestInjector({
56+
...baseCommand,
57+
skipOptionsValidation: true,
58+
});
59+
const service = injector.resolve(CommandsService);
60+
61+
await (<any>service).tryExecuteCommandAction("preview", []);
62+
63+
assert.isFalse(validatedWith.called);
64+
});
65+
});
66+
});

test/options.ts

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { OptionType } from "../lib/common/enums";
1212
import * as _ from "lodash";
1313

1414
let isExecutionStopped = false;
15+
let warnings: string[] = [];
1516

1617
function createTestInjector(): IInjector {
1718
const testInjector = new Yok();
@@ -23,6 +24,11 @@ function createTestInjector(): IInjector {
2324
setSettings: (settings: IConfigurationSettings): any => undefined,
2425
getProfileDir: () => "profileDir",
2526
});
27+
testInjector.register("logger", {
28+
warn: (message: string): void => {
29+
warnings.push(message);
30+
},
31+
});
2632

2733
return testInjector;
2834
}
@@ -32,8 +38,7 @@ function createOptions(testInjector: IInjector): IOptions {
3238
return options;
3339
}
3440

35-
// TODO: Igor and Nathan will make this work again
36-
describe.skip("options", () => {
41+
describe("options", () => {
3742
let testInjector: IInjector;
3843
beforeEach(() => {
3944
testInjector = createTestInjector();
@@ -48,6 +53,14 @@ describe.skip("options", () => {
4853

4954
testInjector.register("errors", errors);
5055
isExecutionStopped = false;
56+
warnings = [];
57+
// The assertions below describe the hard-fail behavior, which is opt-in
58+
// while validation is staged. The staging itself is covered separately.
59+
process.env.NS_STRICT_OPTIONS = "error";
60+
});
61+
62+
afterEach(() => {
63+
delete process.env.NS_STRICT_OPTIONS;
5164
});
5265

5366
describe("validateOptions", () => {
@@ -180,13 +193,13 @@ describe.skip("options", () => {
180193

181194
it("converts string value to array when option type is array", () => {
182195
const options: any = createOptions(testInjector);
183-
process.argv.push("--config");
196+
process.argv.push("--test1");
184197
process.argv.push("value");
185198
options.validateOptions({ test1: { type: OptionType.Array } });
186199
process.argv.pop();
187200
process.argv.pop();
188201
assert.isFalse(isExecutionStopped);
189-
assert.deepStrictEqual(["value"], <any>options["config"]);
202+
assert.deepStrictEqual(["value"], <any>options.argv.test1);
190203
});
191204

192205
it("does not break execution when valid commandSpecificOptions are passed", () => {
@@ -306,6 +319,91 @@ describe.skip("options", () => {
306319
"Dashed options (special-dashed-v) are added to yargs.argv in two ways: special-dashed-v and specialDashedV",
307320
);
308321
});
322+
323+
// vision-ng and friends are declared with a literal dashed key, so the
324+
// camelCase spelling yargs derives has no declaration of its own.
325+
_.each(["--vision-ng", "--visionNg"], (arg) => {
326+
it(`does not break execution when ${arg} is passed`, () => {
327+
process.argv.push(arg);
328+
const options = createOptions(testInjector);
329+
options.validateOptions();
330+
process.argv.pop();
331+
assert.isFalse(isExecutionStopped);
332+
assert.isEmpty(warnings);
333+
});
334+
});
335+
});
336+
337+
describe("does not report valid options", () => {
338+
it("accepts a negated declared boolean", () => {
339+
process.argv.push("--no-hmr");
340+
const options = createOptions(testInjector);
341+
options.validateOptions();
342+
process.argv.pop();
343+
assert.isFalse(isExecutionStopped);
344+
assert.isEmpty(warnings);
345+
assert.isFalse(<boolean>options.argv.hmr);
346+
});
347+
348+
it("accepts dot-notation values of an object option", () => {
349+
process.argv.push("--env.production");
350+
process.argv.push("--env.sourceMap");
351+
const options = createOptions(testInjector);
352+
options.validateOptions();
353+
process.argv.pop();
354+
process.argv.pop();
355+
assert.isFalse(isExecutionStopped);
356+
assert.isEmpty(warnings);
357+
assert.deepStrictEqual(options.argv.env, {
358+
production: true,
359+
sourceMap: true,
360+
});
361+
});
362+
});
363+
364+
describe("staged reporting", () => {
365+
it("warns instead of failing when an option is not supported", () => {
366+
delete process.env.NS_STRICT_OPTIONS;
367+
process.argv.push("--unknownOption");
368+
const options = createOptions(testInjector);
369+
options.validateOptions();
370+
process.argv.pop();
371+
assert.isFalse(isExecutionStopped);
372+
assert.lengthOf(warnings, 1);
373+
assert.include(warnings[0], "'unknownOption' is not supported");
374+
assert.include(warnings[0], "NS_STRICT_OPTIONS=error");
375+
});
376+
377+
it("fails when an option is not supported and NS_STRICT_OPTIONS=error", () => {
378+
process.env.NS_STRICT_OPTIONS = "error";
379+
process.argv.push("--unknownOption");
380+
const options = createOptions(testInjector);
381+
options.validateOptions();
382+
process.argv.pop();
383+
assert.isTrue(isExecutionStopped);
384+
assert.isEmpty(warnings);
385+
});
386+
387+
it("warns instead of failing when a string option has no value", () => {
388+
delete process.env.NS_STRICT_OPTIONS;
389+
process.argv.push("--path");
390+
const options = createOptions(testInjector);
391+
options.validateOptions();
392+
process.argv.pop();
393+
assert.isFalse(isExecutionStopped);
394+
assert.lengthOf(warnings, 1);
395+
assert.include(warnings[0], "'path' requires non-empty value");
396+
});
397+
398+
it("reports an undeclared negated flag under the spelling that was used", () => {
399+
delete process.env.NS_STRICT_OPTIONS;
400+
process.argv.push("--no-something");
401+
const options = createOptions(testInjector);
402+
options.validateOptions();
403+
process.argv.pop();
404+
assert.lengthOf(warnings, 1);
405+
assert.include(warnings[0], "'no-something' is not supported");
406+
});
309407
});
310408
});
311409

0 commit comments

Comments
 (0)