Skip to content

Commit

Permalink
test: add a few more unit tests (#262)
Browse files Browse the repository at this point in the history
## PR Checklist

- [x] Addresses an existing open issue: continues on #7, #8
- [x] That issue was marked as [`status: accepting
prs`](https://github.com/JoshuaKGoldberg/ts-api-utils/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22)
- [x] Steps in
[CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/ts-api-utils/blob/main/.github/CONTRIBUTING.md)
were taken

## Overview

It's been bugging me that this package now has >2.5 million downloads
but only has 46% coverage. Which Codecov shows as a red color in the
badge. 😡

No end user impacts here. Just added test coverage.
  • Loading branch information
JoshuaKGoldberg authored Aug 18, 2023
1 parent 9157e31 commit 116f976
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 31 deletions.
10 changes: 5 additions & 5 deletions src/comments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ describe("forEachComment", () => {
if (isTsVersionAtLeast(4, 3)) {
it("calls the callback when the source has a leading comment", () => {
const { node, sourceFile } = createNodeAndSourceFile(`
// hello world
let value;
`);
// hello world
let value;
`);
const callback = vitest.fn();

forEachComment(node, callback, sourceFile);
Expand All @@ -35,8 +35,8 @@ describe("forEachComment", () => {

it("calls the callback when the source has a trailing comment", () => {
const { node, sourceFile } = createNodeAndSourceFile(`
let value; // hello world
`);
let value; // hello world
`);
const callback = vitest.fn();

forEachComment(node, callback, sourceFile);
Expand Down
44 changes: 44 additions & 0 deletions src/nodes/typeGuards/union.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import ts from "typescript";
import { describe, expect, it } from "vitest";

import { createNode } from "../../test/utils";
import { isTsVersionAtLeast } from "../../utils";
import {
isAccessExpression,
isAccessibilityModifier,
isAccessorDeclaration,
isArrayBindingElement,
isArrayBindingOrAssignmentPattern,
isAssignmentPattern,
isBooleanLiteral,
Expand Down Expand Up @@ -34,6 +37,36 @@ describe("isAccessibilityModifier", () => {
});
});

if (isTsVersionAtLeast(4, 9)) {
describe("isAccessorDeclaration", () => {
it.each([
[false, `abc`],
[
true,
ts.factory.createGetAccessorDeclaration(
undefined,
"property",
[],
undefined,
undefined,
),
],
[
true,
ts.factory.createSetAccessorDeclaration(
undefined,
"property",
[],
undefined,
),
],
])("returns %j when given %s", (expected, sourceText) => {
// eslint-disable-next-line deprecation/deprecation
expect(isAccessorDeclaration(createNode(sourceText))).toBe(expected);
});
});
}

describe("isArrayBindingOrAssignmentPattern", () => {
it.each([
[false, `"[a]"`],
Expand All @@ -47,6 +80,17 @@ describe("isArrayBindingOrAssignmentPattern", () => {
});
});

describe("isArrayBindingElement", () => {
it.each([
[false, `abc`],
[true, ts.factory.createBindingElement(undefined, "property", "name")],
[true, ts.factory.createOmittedExpression()],
])("returns %j when given %s", (expected, sourceText) => {
// eslint-disable-next-line deprecation/deprecation
expect(isArrayBindingElement(createNode(sourceText))).toBe(expected);
});
});

describe("isAssignmentPattern", () => {
it.each([
[false, `"[a]"`],
Expand Down
48 changes: 48 additions & 0 deletions src/types/getters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,54 @@ describe("getCallSignaturesOfType", () => {

expect(getCallSignaturesOfType(type)).toHaveLength(2);
});

it("returns the call signature when one exists across two objects in an intersection", () => {
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(`
declare const x:
& { (): void; }
& { }
;
`);

const node = (sourceFile.statements[0] as ts.VariableStatement)
.declarationList.declarations[0].name;

const type = typeChecker.getTypeAtLocation(node);

expect(getCallSignaturesOfType(type)).toHaveLength(1);
});

it("returns the call signatures when two exist in one object across two objects in an intersection", () => {
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(`
declare const x:
& { (): void; (value: string): void; }
& { }
;
`);

const node = (sourceFile.statements[0] as ts.VariableStatement)
.declarationList.declarations[0].name;

const type = typeChecker.getTypeAtLocation(node);

expect(getCallSignaturesOfType(type)).toHaveLength(2);
});

it("returns zero call signatures when two exist across two objects in an intersection", () => {
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(`
declare const x:
& { (): void; }
& { (value: string): void; }
;
`);

const node = (sourceFile.statements[0] as ts.VariableStatement)
.declarationList.declarations[0].name;

const type = typeChecker.getTypeAtLocation(node);

expect(getCallSignaturesOfType(type)).toHaveLength(0);
});
});

describe("getWellKnownSymbolPropertyOfType", () => {
Expand Down
41 changes: 41 additions & 0 deletions src/types/typeGuards/literal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import ts from "typescript";
import { describe, expect, it } from "vitest";

import { createSourceFileAndTypeChecker } from "../../test/utils";
import {
isBigIntLiteralType,
isFalseLiteralType,
isLiteralType,
isNumberLiteralType,
isStringLiteralType,
isTemplateLiteralType,
isTrueLiteralType,
} from "./literal";

describe.each([
["isBigIntLiteralType", isBigIntLiteralType, "0", "0n"],
["isLiteralType", isLiteralType, "number", "0"],
["isNumberLiteralType", isNumberLiteralType, "number", "0"],
["isStringLiteralType", isStringLiteralType, "string", "''"],
["isTemplateLiteralType", isTemplateLiteralType, "''", "`abc${string}`"],
["isTrueLiteralType", isTrueLiteralType, "boolean", "true"],
["isFalseLiteralType", isFalseLiteralType, "boolean", "false"],
])(`%s`, (name, typeGuard, falseCase, trueCase) => {
describe(name, () => {
it.each([
[false, falseCase],
[true, trueCase],
])('returns %j when given "%s"', (expected, source) => {
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(`
declare const x: ${source};
`);

const node = (sourceFile.statements[0] as ts.VariableStatement)
.declarationList.declarations[0].name;

const type = typeChecker.getTypeAtLocation(node);

expect(typeGuard(type)).toEqual(expected);
});
});
});
12 changes: 12 additions & 0 deletions src/types/typeGuards/single.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
import { createSourceFileAndTypeChecker } from "../../test/utils";
import {
isConditionalType,
isEnumType,
isIntersectionType,
isObjectType,
isUnionOrIntersectionType,
Expand Down Expand Up @@ -40,6 +41,17 @@ describe("isConditionalType", () => {
});
});

describe("isEnumType", () => {
it.each([
[false, "class Box {} type _ = Box;"],
[true, "enum Values {} type _ = Values;"],
])("returns %j when given %s", (expected, sourceText) => {
const type = getTypeForTypeNode(sourceText);

expect(isEnumType(type)).toBe(expected);
});
});

describe("isIntersectionType", () => {
it.each([
[false, "type Test = 1;"],
Expand Down
134 changes: 108 additions & 26 deletions src/types/utilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,49 @@ import {
} from "./utilities";

describe("isPropertyReadonlyInType", () => {
it("returns false when the property is not readonly", () => {
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(`
interface Box {
value: string;
};
`);
const node = sourceFile.statements[0] as ts.InterfaceDeclaration;
const type = typeChecker.getTypeAtLocation(node);

expect(
isPropertyReadonlyInType(
type,
ts.escapeLeadingUnderscores("value"),
typeChecker,
),
).toBe(false);
});

it("returns true when the property is readonly", () => {
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(`
interface Box {
readonly value: string;
};
`);
const node = sourceFile.statements[0] as ts.InterfaceDeclaration;
const type = typeChecker.getTypeAtLocation(node);

expect(
isPropertyReadonlyInType(
type,
ts.escapeLeadingUnderscores("value"),
typeChecker,
),
).toBe(true);
});

it("does not crash when the type is a mapped type parameter extending any", () => {
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(`
type MyType<T> = {
[K in keyof T]: 'cat' | 'dog' | T[K];
};
type Test<A extends any[]> = MyType<A>;
`);
type MyType<T> = {
[K in keyof T]: 'cat' | 'dog' | T[K];
};
type Test<A extends any[]> = MyType<A>;
`);
const node = sourceFile.statements.at(-1) as ts.TypeAliasDeclaration;
const type = typeChecker.getTypeAtLocation(node);

Expand All @@ -30,6 +66,73 @@ describe("isPropertyReadonlyInType", () => {
});
});

describe("symbolHasReadonlyDeclaration", () => {
it("returns false when the symbol is not readonly", () => {
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(`
interface Box {
value: string;
};
let box = { value: "" };
box;
`);
const node = sourceFile.statements.at(-1) as ts.ExpressionStatement;
const symbol = typeChecker.getSymbolAtLocation(node.expression)!;

expect(symbolHasReadonlyDeclaration(symbol, typeChecker)).toBe(false);
});

it("returns false when the symbol is not readonly", () => {
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(`
interface Box {
value: string;
};
const box = { value: "" };
box;
`);
const node = sourceFile.statements.at(-1) as ts.ExpressionStatement;
const symbol = typeChecker.getSymbolAtLocation(node.expression)!;

expect(symbolHasReadonlyDeclaration(symbol, typeChecker)).toBe(true);
});

it.each([
[false, "[]"],
[
true,
`
enum Values { a };
const value = Values.a;
value;
`,
],
[
false,
`
const Values = { a: ['a'] };
Values.a = Values.a;
`,
],
[
false,
`
class Box {}
new Box();
`,
],
])("returns %j when given %s", (expected, source) => {
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(source);
const node = sourceFile.statements.at(-1) as ts.ExpressionStatement;
const type = typeChecker.getTypeAtLocation(node.expression);
const symbol = type.getSymbol()!;

expect(symbolHasReadonlyDeclaration(symbol, typeChecker)).toBe(expected);
});
});

describe("isFalsyType", () => {
it.each([
[false, "{}"],
Expand Down Expand Up @@ -67,24 +170,3 @@ describe("isThenableType", () => {
expect(isThenableType(typeChecker, node, type)).toBe(expected);
});
});

describe("symbolHasReadonlyDeclaration", () => {
it.each([
[false, "[]"],
[
true,
`
enum Values { a };
const value = Values.a;
value;
`,
],
])("returns %j when given %s", (expected, source) => {
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(source);
const node = sourceFile.statements.at(-1) as ts.ExpressionStatement;
const type = typeChecker.getTypeAtLocation(node.expression);
const symbol = type.getSymbol()!;

expect(symbolHasReadonlyDeclaration(symbol, typeChecker)).toBe(expected);
});
});

0 comments on commit 116f976

Please sign in to comment.