Cleanup pass on the conformance_pattern tests.
This change reorders some tests, and uniformizes them in the use of "toHaveFailures", which is far more descriptive when failing than just "1 expected to be 2".
PiperOrigin-RevId: 273949911
diff --git a/internal/tsetse/tests/ban_conformance_pattern/fixer_test.ts b/internal/tsetse/tests/ban_conformance_pattern/fixer_test.ts
index 24e53ad..59843a6 100644
--- a/internal/tsetse/tests/ban_conformance_pattern/fixer_test.ts
+++ b/internal/tsetse/tests/ban_conformance_pattern/fixer_test.ts
@@ -20,7 +20,7 @@
const uppercaseFixerBuilt: Fixer = buildReplacementFixer((node: ts.Node) => {
return {replaceWith: node.getText().toUpperCase()};
-})
+});
// The initial config and source off which we run those checks.
const baseConfig = {
@@ -39,28 +39,26 @@
const rule = new ConformancePatternRule(baseConfig, uppercaseFixer);
const results = compileAndCheck(rule, source);
- expect(results).toHaveNFailures(1, baseConfig);
- expect(results[0]).toBeFailureMatching({
+ expect(results).toHaveFailuresMatching({
matchedCode: `q.cite = 'some example string'`,
- messageText: 'found citation'
+ messageText: 'found citation',
+ fix: [
+ {start: 50, end: 80, replacement: `Q.CITE = 'SOME EXAMPLE STRING'`}
+ ]
});
- expect(results[0]).toHaveFixMatching([
- {start: 50, end: 80, replacement: `Q.CITE = 'SOME EXAMPLE STRING'`}
- ]);
});
it('for a single match (alternate fixer)', () => {
const rule = new ConformancePatternRule(baseConfig, uppercaseFixerBuilt);
const results = compileAndCheck(rule, source);
- expect(results).toHaveNFailures(1, baseConfig);
- expect(results[0]).toBeFailureMatching({
+ expect(results).toHaveFailuresMatching({
matchedCode: `q.cite = 'some example string'`,
- messageText: 'found citation'
+ messageText: 'found citation',
+ fix: [
+ {start: 50, end: 80, replacement: `Q.CITE = 'SOME EXAMPLE STRING'`}
+ ]
});
- expect(results[0]).toHaveFixMatching([
- {start: 50, end: 80, replacement: `Q.CITE = 'SOME EXAMPLE STRING'`}
- ]);
});
it('for several matches', () => {
@@ -69,27 +67,30 @@
source + `q.cite = 'some other example string';\n`;
const results = compileAndCheck(rule, sourceTwoMatches);
- expect(results).toHaveNFailures(2, baseConfig);
- expect(results[0]).toBeFailureMatching({
- matchedCode: `q.cite = 'some example string'`,
- messageText: 'found citation'
- });
- expect(results[1]).toBeFailureMatching({
- matchedCode: `q.cite = 'some other example string'`,
- messageText: 'found citation'
- });
- expect(results[0]).toHaveFixMatching([
- {start: 50, end: 80, replacement: `Q.CITE = 'SOME EXAMPLE STRING'`}
- ]);
+ expect(results).toHaveFailuresMatching(
+ {
+ matchedCode: `q.cite = 'some example string'`,
+ messageText: 'found citation',
+ fix: [{
+ start: 50,
+ end: 80,
+ replacement: `Q.CITE = 'SOME EXAMPLE STRING'`
+ }]
+ },
+ {
+ matchedCode: `q.cite = 'some other example string'`,
+ messageText: 'found citation',
+ fix: [{
+ start: 82,
+ end: 118,
+ replacement: `Q.CITE = 'SOME OTHER EXAMPLE STRING'`
+ }]
+ });
+
expect(results[0].fixToReadableStringInContext())
.toBe(
`Suggested fix:\n` +
`- Replace the full match with: Q.CITE = 'SOME EXAMPLE STRING'`);
- expect(results[1]).toHaveFixMatching([{
- start: 82,
- end: 118,
- replacement: `Q.CITE = 'SOME OTHER EXAMPLE STRING'`
- }]);
expect(results[1].fixToReadableStringInContext())
.toBe(
`Suggested fix:\n` +
diff --git a/internal/tsetse/tests/ban_conformance_pattern/name_call_non_constant_argument_test.ts b/internal/tsetse/tests/ban_conformance_pattern/name_call_non_constant_argument_test.ts
index 44c9a31..dba2664 100644
--- a/internal/tsetse/tests/ban_conformance_pattern/name_call_non_constant_argument_test.ts
+++ b/internal/tsetse/tests/ban_conformance_pattern/name_call_non_constant_argument_test.ts
@@ -3,11 +3,12 @@
import {compileAndCheck, customMatchers} from '../../util/testing/test_support';
describe('BANNED_NAME_CALL_NON_CONSTANT_ARGUMENT', () => {
- const rule = new ConformancePatternRule({
+ const config = {
errorMessage: 'do not call bar.foo with non-literal 1st arg',
kind: PatternKind.BANNED_NAME_CALL_NON_CONSTANT_ARGUMENT,
values: ['bar:0']
- });
+ };
+ const rule = new ConformancePatternRule(config);
it('matches simple examples', () => {
const sources = [
@@ -17,8 +18,7 @@
];
const results = compileAndCheck(rule, ...sources);
- expect(results.length).toBe(1);
- expect(results[0]).toBeFailureMatching({
+ expect(results).toHaveFailuresMatching({
matchedCode: `foo.bar(window.name, 1)`,
messageText: 'do not call bar.foo with non-literal 1st arg'
});
@@ -31,7 +31,7 @@
];
const results = compileAndCheck(rule, ...sources);
- expect(results.length).toBe(0);
+ expect(results).toHaveNoFailures();
});
it('looks at the right position', () => {
@@ -48,13 +48,13 @@
];
const results = compileAndCheck(rule, ...sources);
- expect(results.length).toBe(2);
- expect(results[0]).toBeFailureMatching({
- matchedCode: `foo.aaa(1, window.name)`,
- });
- expect(results[1]).toBeFailureMatching({
- matchedCode: `foo.bbb(window.name)`,
- });
+ expect(results).toHaveFailuresMatching(
+ {
+ matchedCode: `foo.aaa(1, window.name)`,
+ },
+ {
+ matchedCode: `foo.bbb(window.name)`,
+ });
});
it('supports static methods', () => {
@@ -72,8 +72,7 @@
];
const results = compileAndCheck(rule, ...sources);
- expect(results.length).toBe(1);
- expect(results[0]).toBeFailureMatching({
+ expect(results).toHaveFailuresMatching({
matchedCode: `Car.buildFromParts(window.name)`,
});
});
@@ -88,8 +87,7 @@
const sources = [`URL.createObjectURL(window.name);\n`];
const results = compileAndCheck(rule, ...sources);
- expect(results.length).toBe(1);
- expect(results[0]).toBeFailureMatching({
+ expect(results).toHaveFailuresMatching({
matchedCode: `URL.createObjectURL(window.name)`,
});
});
@@ -104,8 +102,7 @@
const sources = [`eval(window.name);\n`];
const results = compileAndCheck(rule, ...sources);
- expect(results.length).toBe(1);
- expect(results[0]).toBeFailureMatching({
+ expect(results).toHaveFailuresMatching({
matchedCode: `eval(window.name)`,
});
});
diff --git a/internal/tsetse/tests/ban_conformance_pattern/name_test.ts b/internal/tsetse/tests/ban_conformance_pattern/name_test.ts
index d3ffdd0..4e8609c 100644
--- a/internal/tsetse/tests/ban_conformance_pattern/name_test.ts
+++ b/internal/tsetse/tests/ban_conformance_pattern/name_test.ts
@@ -4,39 +4,29 @@
describe('BANNED_NAME', () => {
it('matches simple example of globals', () => {
- const rule = new ConformancePatternRule({
+ const config = {
errorMessage: 'no Infinity',
kind: PatternKind.BANNED_NAME,
values: ['Infinity']
- });
- const source = [
- `Infinity; 1+1;`,
- ].join('\n');
- const results = compileAndCheck(rule, source);
+ };
+ const source = `Infinity; 1+1;`;
+ const results = compileAndCheck(new ConformancePatternRule(config), source);
- expect(results.length).toBe(1);
- expect(results[0]).toBeFailureMatching({
- matchedCode: `Infinity`,
- messageText: 'no Infinity'
- });
+ expect(results).toHaveFailuresMatching(
+ {matchedCode: `Infinity`, messageText: 'no Infinity'});
});
it('matches namespaced globals', () => {
- const rule = new ConformancePatternRule({
+ const config = {
errorMessage: 'no blob url',
kind: PatternKind.BANNED_NAME,
values: ['URL.createObjectURL']
- });
- const source = [
- `URL.createObjectURL({});`,
- ].join('\n');
- const results = compileAndCheck(rule, source);
+ };
+ const source = `URL.createObjectURL({});`;
+ const results = compileAndCheck(new ConformancePatternRule(config), source);
- expect(results.length).toBe(1);
- expect(results[0]).toBeFailureMatching({
- matchedCode: `createObjectURL`,
- messageText: 'no blob url'
- });
+ expect(results).toHaveFailuresMatching(
+ {matchedCode: `createObjectURL`, messageText: 'no blob url'});
});
it('does not choke on type aliases', () => {
@@ -46,21 +36,21 @@
// Symbols that verify ts.SymbolFlags.Alias, and ts.SymbolFlags.TypeAlias is
// not acceptable (the typechecker will throw).
+ const config = {
+ errorMessage: 'should not trigger',
+ kind: PatternKind.BANNED_NAME,
+ values: ['whatever']
+ };
const sources = [
`export type Foo = {bar: number, baz: (x:string)=>void}`,
`import {Foo} from './file_0';
export const c: Foo["baz"] = (x:string)=>{};`,
`import {c} from './file_1'; c(window.name);`
];
- const results = compileAndCheck(
- new ConformancePatternRule({
- errorMessage: 'should not trigger',
- kind: PatternKind.BANNED_NAME,
- values: ['whatever']
- }),
- ...sources);
+ const results =
+ compileAndCheck(new ConformancePatternRule(config), ...sources);
- expect(results.length).toBe(0);
+ expect(results).toHaveNoFailures();
});
});
diff --git a/internal/tsetse/tests/ban_conformance_pattern/property_non_constant_write_test.ts b/internal/tsetse/tests/ban_conformance_pattern/property_non_constant_write_test.ts
index b545571..753e08a 100644
--- a/internal/tsetse/tests/ban_conformance_pattern/property_non_constant_write_test.ts
+++ b/internal/tsetse/tests/ban_conformance_pattern/property_non_constant_write_test.ts
@@ -7,17 +7,15 @@
const source = `const q = document.createElement('q');\n` +
`q.cite = 'some example string';\n` + // literal
`q.cite = window.name;\n`; // non-literal
- const rule = new ConformancePatternRule({
+ const config = {
errorMessage: 'do not cite dynamically',
kind: PatternKind.BANNED_PROPERTY_NON_CONSTANT_WRITE,
values: ['HTMLQuoteElement.prototype.cite']
- });
- const results = compileAndCheck(rule, source);
+ };
+ const results = compileAndCheck(new ConformancePatternRule(config), source);
- expect(results.length).toBe(1);
- expect(results[0])
- .toBeFailureMatching(
- {start: 71, end: 91, messageText: 'do not cite dynamically'});
+ expect(results).toHaveFailuresMatching(
+ {start: 71, end: 91, messageText: 'do not cite dynamically'});
});
});
diff --git a/internal/tsetse/tests/ban_conformance_pattern/property_write_test.ts b/internal/tsetse/tests/ban_conformance_pattern/property_write_test.ts
index e0f3254..5aa9f56 100644
--- a/internal/tsetse/tests/ban_conformance_pattern/property_write_test.ts
+++ b/internal/tsetse/tests/ban_conformance_pattern/property_write_test.ts
@@ -3,77 +3,81 @@
import {compileAndCheck, customMatchers} from '../../util/testing/test_support';
describe('BANNED_PROPERTY_WRITE', () => {
- const rule = new ConformancePatternRule({
- errorMessage: 'do not cite',
- kind: PatternKind.BANNED_PROPERTY_WRITE,
- values: ['HTMLQuoteElement.prototype.cite']
- });
+ describe('simpler matcher tests', () => {
+ const config = {
+ errorMessage: 'do not cite',
+ kind: PatternKind.BANNED_PROPERTY_WRITE,
+ values: ['HTMLQuoteElement.prototype.cite']
+ };
+ const rule = new ConformancePatternRule(config);
- it('matches simple examples', () => {
- const source = [
- `const q = document.createElement('q');`,
- `q.cite = 'some example string';`,
- ].join('\n');
- const results = compileAndCheck(rule, source);
+ it('matches simple examples', () => {
+ const source = [
+ `const q = document.createElement('q');`,
+ `q.cite = 'some example string';`,
+ ].join('\n');
+ const results = compileAndCheck(rule, source);
- expect(results.length).toBe(1);
- expect(results[0]).toBeFailureMatching({
- matchedCode: `q.cite = 'some example string'`,
- messageText: 'do not cite'
+ expect(results).toHaveFailuresMatching({
+ matchedCode: `q.cite = 'some example string'`,
+ messageText: 'do not cite'
+ });
});
- });
- it('matches precisely, even with whitespace or comments', () => {
- const source = [
- `const q = document.createElement('q');`,
- ` q.cite = 'exampleA';`,
- `q.cite = 'exampleB' ;`,
- `/* test1 */ q.cite = /* test2 */ 'exampleC' /* test3 */;`,
- ].join('\n');
- const results = compileAndCheck(rule, source);
+ it('matches precisely, even with whitespace or comments', () => {
+ const source = [
+ `const q = document.createElement('q');`,
+ ` q.cite = 'exampleA';`,
+ `q.cite = 'exampleB' ;`,
+ `/* test1 */ q.cite = /* test2 */ 'exampleC' /* test3 */;`,
+ ].join('\n');
+ const results = compileAndCheck(rule, source);
- expect(results.length).toBe(3);
- expect(results[0]).toBeFailureMatching({
- matchedCode: `q.cite = 'exampleA'`,
- messageText: 'do not cite'
+ expect(results).toHaveFailuresMatching(
+ {matchedCode: `q.cite = 'exampleA'`, messageText: 'do not cite'},
+ {matchedCode: `q.cite = 'exampleB'`, messageText: 'do not cite'}, {
+ matchedCode: `q.cite = /* test2 */ 'exampleC'`,
+ messageText: 'do not cite'
+ });
});
- expect(results[1]).toBeFailureMatching({
- matchedCode: `q.cite = 'exampleB'`,
- messageText: 'do not cite'
+
+ it('understands function prototypes', () => {
+ const source = [
+ `function foo(q:HTMLQuoteElement) {`,
+ ` q.cite = 'some example string';`,
+ `}`,
+ ].join('\n');
+ const results = compileAndCheck(rule, source);
+
+ expect(results).toHaveFailuresMatching({
+ matchedCode: `q.cite = 'some example string'`,
+ messageText: 'do not cite'
+ });
});
- expect(results[2]).toBeFailureMatching({
- matchedCode: `q.cite = /* test2 */ 'exampleC'`,
- messageText: 'do not cite'
+
+ it('understands imported symbols', () => {
+ const sources = [
+ `const q = document.createElement('q'); export {q};`,
+ `import {q} from './file_0'; q.cite = window.name;`
+ ];
+ const results = compileAndCheck(rule, ...sources);
+
+ expect(results).toHaveFailuresMatching({
+ matchedCode: 'q.cite = window.name',
+ fileName: 'file_1.ts',
+ messageText: 'do not cite',
+ });
});
- })
- it('understands function prototypes', () => {
- const source = [
- `function foo(q:HTMLQuoteElement) {`,
- ` q.cite = 'some example string';`,
- `}`,
- ].join('\n');
- const results = compileAndCheck(rule, source);
+ it('understands shadowing', () => {
+ const source = [
+ `const q = document.createElement('q');`,
+ `const f1 = (q: {cite: string}) => { q.cite = 'example 1'; };`,
+ ].join('\n');
+ const rule = new ConformancePatternRule(config);
+ const results = compileAndCheck(rule, source);
- expect(results.length).toBe(1);
- expect(results[0]).toBeFailureMatching({
- matchedCode: `q.cite = 'some example string'`,
- messageText: 'do not cite'
- });
- });
-
- it('understands imported symbols', () => {
- const sources = [
- `const q = document.createElement('q'); export {q};`,
- `import {q} from './file_0'; q.cite = window.name;`
- ];
- const results = compileAndCheck(rule, ...sources);
-
- expect(results.length).toBe(1);
- expect(results[0]).toBeFailureMatching({
- matchedCode: 'q.cite = window.name',
- fileName: 'file_1.ts',
- messageText: 'do not cite',
+ expect(results).toHaveNoFailures();
});
});
@@ -94,42 +98,27 @@
};
it('banning Parent.x matches (instance of Child).x', () => {
- const ruleOnParent = new ConformancePatternRule({
+ const configOnParent = {
errorMessage: 'found write to x',
kind: PatternKind.BANNED_PROPERTY_WRITE,
values: ['Parent.prototype.x']
- });
- const r = compileAndCheck(ruleOnParent, source);
- expect(r.length).toBe(1);
- expect(r[0]).toBeFailureMatching(expectedFailure);
+ };
+ const ruleOnParent = new ConformancePatternRule(configOnParent);
+ const results = compileAndCheck(ruleOnParent, source);
+
+ expect(results).toHaveFailuresMatching(expectedFailure);
});
it('banning Child.x matches x defined on Parent', () => {
- const ruleOnChild = new ConformancePatternRule({
+ const configOnChild = {
errorMessage: 'found write to x',
kind: PatternKind.BANNED_PROPERTY_WRITE,
values: ['Child.prototype.x']
- });
- const r = compileAndCheck(ruleOnChild, source);
- expect(r.length).toBe(1);
- expect(r[0]).toBeFailureMatching(expectedFailure);
- });
- });
+ };
+ const ruleOnChild = new ConformancePatternRule(configOnChild);
+ const results = compileAndCheck(ruleOnChild, source);
- describe('with shadowing', () => {
- it('does not over-match', () => {
- const source = [
- `const q = document.createElement('q');`,
- `const f1 = (q: {cite: string}) => { q.cite = 'example 1'; };`,
- ].join('\n');
- const rule = new ConformancePatternRule({
- errorMessage: 'do not cite',
- kind: PatternKind.BANNED_PROPERTY_WRITE,
- values: ['HTMLQuoteElement.prototype.cite']
- });
- const results = compileAndCheck(rule, source);
-
- expect(results.length).toBe(0);
+ expect(results).toHaveFailuresMatching(expectedFailure);
});
});
});
diff --git a/internal/tsetse/tests/ban_conformance_pattern/whitelist_test.ts b/internal/tsetse/tests/ban_conformance_pattern/whitelist_test.ts
index a1c7342..93e17bd 100644
--- a/internal/tsetse/tests/ban_conformance_pattern/whitelist_test.ts
+++ b/internal/tsetse/tests/ban_conformance_pattern/whitelist_test.ts
@@ -26,7 +26,7 @@
const rule = new ConformancePatternRule(config);
const results = compileAndCheck(rule, source);
- expect(results).toHaveNFailures(1, config);
+ expect(results).toHaveNFailures(1);
});
it('matches if there is an empty whitelist group', () => {
@@ -39,7 +39,7 @@
const rule = new ConformancePatternRule(config);
const results = compileAndCheck(rule, source);
- expect(results).toHaveNFailures(1, config);
+ expect(results).toHaveNFailures(1);
});
it('respects prefix-based whitelists (matching test)', () => {
@@ -53,7 +53,7 @@
const rule = new ConformancePatternRule(config);
const results = compileAndCheck(rule, source);
- expect(results).toHaveNFailures(0, config);
+ expect(results).toHaveNoFailures();
});
it('respects prefix-based whitelists (non-matching test)', () => {
@@ -67,7 +67,7 @@
const rule = new ConformancePatternRule(config);
const results = compileAndCheck(rule, source);
- expect(results).toHaveNFailures(1, config);
+ expect(results).toHaveNFailures(1);
});
it('respects regex-based whitelists', () => {
@@ -81,7 +81,7 @@
const rule = new ConformancePatternRule(config);
const results = compileAndCheck(rule, source);
- expect(results).toHaveNFailures(0, config);
+ expect(results).toHaveNoFailures();
});
it('accepts several regex-based whitelists', () => {
@@ -99,7 +99,7 @@
// Testing two times the same file so that both regexps match.
const results = compileAndCheck(rule, source, source);
- expect(results).toHaveNFailures(0, config);
+ expect(results).toHaveNoFailures();
});
it('throws on creation of invalid regexps', () => {
@@ -111,6 +111,7 @@
}]
};
expect(() => {
+ // tslint:disable-next-line:no-unused-expression
new ConformancePatternRule(config);
}).toThrowError(/Invalid regular expression/);
});
diff --git a/internal/tsetse/util/testing/test_support.ts b/internal/tsetse/util/testing/test_support.ts
index fb2460e..e474ec7 100644
--- a/internal/tsetse/util/testing/test_support.ts
+++ b/internal/tsetse/util/testing/test_support.ts
@@ -9,7 +9,6 @@
import {Checker} from '../../checker';
import {Failure, fixToString} from '../../failure';
import {AbstractRule} from '../../rule';
-import {Config} from '../pattern_config';
@@ -74,18 +73,146 @@
return file ? file.fileName : 'unknown';
}
+/**
+ * Returns the location the temp directory for that platform (with forward
+ * slashes).
+ */
export function getTempDirForWhitelist() {
// TS uses forward slashes on Windows ¯\_(ツ)_/¯
- return os.platform() == 'win32' ? os.tmpdir().replace(/\\/g, '/') :
- os.tmpdir();
+ return os.platform() === 'win32' ? os.tmpdir().replace(/\\/g, '/') :
+ os.tmpdir();
}
-// Custom matcher for Jasmine, for a better experience matching fixes.
+interface FailureExpectations {
+ fileName?: string;
+ start?: number;
+ end?: number;
+ matchedCode?: string;
+ messageText?: string;
+ fix?: FixExpectations;
+}
+
+type FixExpectations = [{
+ fileName?: string;
+ start?: number;
+ end?: number;
+ replacement?: string;
+}];
+
+
+function failureMatchesExpectation(
+ f1: Failure, exp: FailureExpectations): {pass: boolean, message: string} {
+ const diagnostic = f1.toDiagnostic();
+ let regrets = '';
+ if (exp === undefined) {
+ regrets += 'The matcher requires two arguments. ';
+ }
+ if (exp.fileName) {
+ if (!diagnostic.file) {
+ regrets += `Expected diagnostic to have a source file, but it had ${
+ diagnostic.file}. `;
+ } else if (!diagnostic.file.fileName.endsWith(exp.fileName)) {
+ regrets +=
+ `Expected ${diagnostic.file.fileName} to end with ${exp.fileName}. `;
+ }
+ }
+ if (exp.messageText !== undefined &&
+ exp.messageText !== diagnostic.messageText) {
+ regrets +=
+ expectation('errorMessage', exp.messageText, diagnostic.messageText);
+ }
+ if (exp.start !== undefined && diagnostic.start !== exp.start) {
+ regrets += expectation('start', exp.start, diagnostic.start);
+ }
+ if (exp.end !== undefined && diagnostic.end !== exp.end) {
+ regrets += expectation('end', exp.end, diagnostic.end);
+ }
+ if (exp.matchedCode) {
+ if (!diagnostic.file) {
+ regrets += `Expected diagnostic to have a source file, but it had ${
+ diagnostic.file}. `;
+ } else if (diagnostic.start === undefined) {
+ // I don't know how this could happen, but typings say so.
+ regrets += `Expected diagnostic to have a starting position. `;
+ } else {
+ const foundMatchedCode = diagnostic.file.getFullText().slice(
+ Number(diagnostic.start), diagnostic.end);
+ if (foundMatchedCode !== exp.matchedCode) {
+ regrets += `Expected diagnostic to match ${exp.matchedCode}, but was ${
+ foundMatchedCode} (from ${Number(diagnostic.start)} to ${
+ diagnostic.end}). `;
+ }
+ }
+ }
+ if (exp.fix) {
+ const {pass, message: fixMessage} = fixMatchesExpectation(f1, exp.fix);
+ if (!pass) {
+ regrets += fixMessage;
+ }
+ }
+ return {pass: regrets === '', message: regrets};
+}
+
+function fixMatchesExpectation(failure: Failure, expected: FixExpectations):
+ {pass: boolean, message: string} {
+ let regrets = '';
+ const actualFix = failure.toDiagnostic().fix;
+ if (!actualFix) {
+ regrets += `Expected ${failure.toString()} to have fix ${
+ JSON.stringify(expected)}. `;
+ } else if (actualFix.changes.length !== expected.length) {
+ regrets += `Expected ${expected.length} individual changes, got ${
+ actualFix.changes.length}. `;
+ if (actualFix.changes.length) {
+ regrets += '\n' + fixToString(actualFix);
+ }
+ } else {
+ for (let i = 0; i < expected.length; i++) {
+ const e = expected[i];
+ const a = actualFix.changes[i];
+ if (e.start !== undefined && e.start !== a.start) {
+ regrets +=
+ expectation(`${i}th individualChange's start`, e.start, a.start);
+ }
+ if (e.end !== undefined && e.end !== a.end) {
+ regrets += expectation(`${i}th individualChange's end`, e.end, a.end);
+ }
+ if (e.replacement !== undefined && e.replacement !== a.replacement) {
+ regrets += expectation(
+ `${i}th individualChange's replacement`, e.replacement,
+ a.replacement);
+ }
+ if (e.fileName !== undefined && e.fileName !== a.sourceFile.fileName) {
+ regrets += expectation(
+ `${i}th individualChange's fileName`, e.fileName,
+ a.sourceFile.fileName);
+ }
+ // TODO: Consider adding matchedCode as for the failure matcher.
+ }
+ }
+
+ return {pass: regrets === '', message: regrets};
+}
+
+
+/**
+ * Custom matcher for Jasmine, for a better experience matching failures and
+ * fixes.
+ */
export const customMatchers: jasmine.CustomMatcherFactories = {
+ toBeFailureMatching(): jasmine.CustomMatcher {
+ return {compare: failureMatchesExpectation};
+ },
+
+ /** Checks that a Failure has the expected Fix field. */
+ toHaveFixMatching(): jasmine.CustomMatcher {
+ return {compare: fixMatchesExpectation};
+ },
+
toHaveNFailures(): jasmine.CustomMatcher {
return {
- compare: (actual: Failure[], expected: Number, config?: Config) => {
+ compare: (actual: Failure[], expected: number) => {
if (actual.length === expected) {
return {pass: true};
} else {
@@ -94,123 +221,33 @@
if (actual.length) {
message += '\n' + actual.map(f => f.toString()).join('\n');
}
- if (config) {
- message += `\nConfig: {kind:${config.kind}, values:${
- JSON.stringify(config.values)}, whitelist:${
- JSON.stringify(config.whitelistEntries)} }`;
- }
return {pass: false, message};
}
}
};
},
- toBeFailureMatching(): jasmine.CustomMatcher {
+ toHaveFailuresMatching(): jasmine.CustomMatcher {
return {
- compare: (actualFailure: Failure, exp: {
- fileName?: string,
- start?: number,
- end?: number,
- matchedCode?: string,
- messageText?: string,
- }) => {
- const actualDiagnostic = actualFailure.toDiagnostic();
- let regrets = '';
- if (exp === undefined) {
- regrets += 'The matcher requires two arguments. ';
- }
- if (exp.fileName) {
- if (!actualDiagnostic.file) {
- regrets += `Expected diagnostic to have a source file, but it had ${
- actualDiagnostic.file}. `;
- } else if (!actualDiagnostic.file.fileName.endsWith(exp.fileName)) {
- regrets += `Expected ${
- actualDiagnostic.file.fileName} to end with ${exp.fileName}. `;
+ compare: (actual: Failure[], ...expected: FailureExpectations[]) => {
+ if (actual.length !== expected.length) {
+ let message =
+ `Expected ${expected} Failures, but found ${actual.length}.`;
+ if (actual.length) {
+ message += '\n' + actual.map(f => f.toString()).join('\n');
}
+ return {pass: false, message};
}
- if (exp.messageText !== undefined &&
- exp.messageText != actualDiagnostic.messageText) {
- regrets += expectation(
- 'errorMessage', exp.messageText, actualDiagnostic.messageText);
- }
- if (exp.start !== undefined && actualDiagnostic.start !== exp.start) {
- regrets += expectation('start', exp.start, actualDiagnostic.start);
- }
- if (exp.end !== undefined && actualDiagnostic.end !== exp.end) {
- regrets += expectation('end', exp.end, actualDiagnostic.end);
- }
- if (exp.matchedCode) {
- if (!actualDiagnostic.file) {
- regrets += `Expected diagnostic to have a source file, but it had ${
- actualDiagnostic.file}. `;
- } else if (actualDiagnostic.start === undefined) {
- // I don't know how this could happen, but typings say so.
- regrets += `Expected diagnostic to have a starting position. `;
- } else {
- const foundMatchedCode = actualDiagnostic.file.getFullText().slice(
- Number(actualDiagnostic.start), actualDiagnostic.end);
- if (foundMatchedCode != exp.matchedCode) {
- regrets += `Expected diagnostic to match ${
- exp.matchedCode}, but was ${foundMatchedCode} (from ${
- Number(
- actualDiagnostic.start)} to ${actualDiagnostic.end}). `;
- }
+ let pass = true, message = '';
+ for (let i = 0; i < actual.length; i++) {
+ const comparison = failureMatchesExpectation(actual[i], expected[i]);
+ pass = pass && comparison.pass;
+ if (comparison.message) {
+ message += `For failure ${i}: ${comparison.message}\n`;
}
+ message += comparison.message;
}
- return {pass: regrets === '', message: regrets};
- }
- };
- },
-
- /** Checks that a Failure has the expected Fix field. */
- toHaveFixMatching(): jasmine.CustomMatcher {
- return {
- compare: (actualFailure: Failure, exp: [{
- fileName?: string,
- start?: number,
- end?: number,
- replacement?: string
- }]) => {
- let regrets = '';
- const actualFix = actualFailure.toDiagnostic().fix;
- if (!actualFix) {
- regrets += `Expected ${actualFailure.toString()} to have fix ${
- JSON.stringify(exp)}. `;
- } else if (actualFix.changes.length != exp.length) {
- regrets += `Expected ${exp.length} individual changes, got ${
- actualFix.changes.length}. `;
- if (actualFix.changes.length) {
- regrets += '\n' + fixToString(actualFix);
- }
- } else {
- for (let i = 0; i < exp.length; i++) {
- const e = exp[i];
- const a = actualFix.changes[i];
- if (e.start !== undefined && e.start !== a.start) {
- regrets += expectation(
- `${i}th individualChange's start`, e.start, a.start);
- }
- if (e.end !== undefined && e.end !== a.end) {
- regrets +=
- expectation(`${i}th individualChange's end`, e.end, a.end);
- }
- if (e.replacement !== undefined &&
- e.replacement !== a.replacement) {
- regrets += expectation(
- `${i}th individualChange's replacement`, e.replacement,
- a.replacement);
- }
- if (e.fileName !== undefined &&
- e.fileName !== a.sourceFile.fileName) {
- regrets += expectation(
- `${i}th individualChange's fileName`, e.fileName,
- a.sourceFile.fileName);
- }
- // TODO: Consider adding matchedCode as for the failure matcher.
- }
- }
-
- return {pass: regrets === '', message: regrets};
+ return {pass, message};
}
};
},
@@ -227,11 +264,23 @@
};
}
};
- }
+ },
+ toHaveNoFailures(): jasmine.CustomMatcher {
+ return {
+ compare: (actual: Failure[]) => {
+ if (actual.length !== 0) {
+ let message = `Expected no Failures, but found ${actual.length}.`;
+ message += '\n' + actual.map(f => f.toString()).join('\n');
+ return {pass: false, message};
+ }
+ return {pass: true, message: ''};
+ }
+ };
+ }
};
-function expectation(fieldname: string, expectation: any, actual: any) {
+function expectation<T>(fieldname: string, expectation: T, actual: T) {
return `Expected .${fieldname} to be ${expectation}, was ${actual}. `;
}
@@ -239,23 +288,21 @@
declare global {
namespace jasmine {
interface Matchers<T> {
- toBeFailureMatching(expected: {
- fileName?: string,
- start?: number,
- end?: number,
- matchedCode?: string,
- messageText?: string,
- }): void;
+ toBeFailureMatching(expected: FailureExpectations): void;
+
/** Checks that a Failure has the expected Fix field. */
- toHaveFixMatching(expected: [
- {fileName?: string, start?: number, end?: number, replacement?: string}
- ]): void;
-
- toHaveNFailures(expected: Number, config?: Config): void;
+ toHaveFixMatching(expected: FixExpectations): void;
/** Asserts that a Failure has no fix. */
toHaveNoFix(): void;
+
+ toHaveNFailures(expected: number): void;
+
+ toHaveFailuresMatching(...expected: FailureExpectations[]): void;
+
+ /** Asserts that the results are empty. */
+ toHaveNoFailures(): void;
}
}
}