diff --git a/packages/dtslint/test/checks.test.ts b/packages/dtslint/test/checks.test.ts new file mode 100644 index 0000000000..a6241336ee --- /dev/null +++ b/packages/dtslint/test/checks.test.ts @@ -0,0 +1,236 @@ +/// +import { CompilerOptionsRaw, checkTsconfig } from "../src/checks"; + +describe("checkTsconfig additional coverage", () => { + const base: CompilerOptionsRaw = { + module: "commonjs", + lib: ["es6"], + noImplicitAny: true, + noImplicitThis: true, + strictNullChecks: true, + strictFunctionTypes: true, + types: [], + noEmit: true, + forceConsistentCasingInFileNames: true, + }; + + function config(overrides?: Partial, fileOverrides?: object) { + return { compilerOptions: { ...base, ...overrides }, files: ["index.d.ts", "test.ts"], ...fileOverrides }; + } + + describe("mustHave validations", () => { + it("errors when noEmit is false", () => { + const errors = checkTsconfig(config({ noEmit: false })); + expect(errors).toContainEqual(expect.stringContaining("noEmit")); + }); + + it("errors when noEmit is missing", () => { + const opts = { ...base }; + delete (opts as any).noEmit; + expect(checkTsconfig({ compilerOptions: opts, files: ["index.d.ts", "test.ts"] })).toContainEqual( + expect.stringContaining("noEmit"), + ); + }); + + it("errors when forceConsistentCasingInFileNames is false", () => { + const errors = checkTsconfig(config({ forceConsistentCasingInFileNames: false })); + expect(errors).toContainEqual(expect.stringContaining("forceConsistentCasingInFileNames")); + }); + + it("errors when types is not an empty array", () => { + const errors = checkTsconfig(config({ types: ["node"] })); + expect(errors).toContainEqual(expect.stringContaining("types")); + }); + }); + + describe("lib validation", () => { + it("errors when lib is missing", () => { + const opts = { ...base }; + delete (opts as any).lib; + const errors = checkTsconfig({ compilerOptions: opts, files: ["index.d.ts", "test.ts"] }); + expect(errors).toContainEqual(expect.stringContaining('Must specify "lib"')); + }); + }); + + describe("module validation", () => { + it("errors when module is an invalid value", () => { + const errors = checkTsconfig(config({ module: "esnext" })); + expect(errors).toContainEqual( + expect.stringContaining('When "module" is present, it must be set to "commonjs" or "node16"'), + ); + }); + + it("allows module: commonjs", () => { + const errors = checkTsconfig(config({ module: "commonjs" })); + expect(errors).toEqual([]); + }); + + it("allows module: node16", () => { + const errors = checkTsconfig(config({ module: "node16" })); + expect(errors).toEqual([]); + }); + + it("allows module: COMMONJS (case insensitive)", () => { + const errors = checkTsconfig(config({ module: "COMMONJS" })); + expect(errors).toEqual([]); + }); + }); + + describe("strict mode", () => { + it("allows strict: true and removes individual strict options", () => { + const strictOpts: CompilerOptionsRaw = { + module: "commonjs", + lib: ["es6"], + strict: true, + types: [], + noEmit: true, + forceConsistentCasingInFileNames: true, + }; + const errors = checkTsconfig({ compilerOptions: strictOpts, files: ["index.d.ts", "test.ts"] }); + expect(errors).toEqual([]); + }); + + it("errors when strict is false", () => { + const strictOpts: CompilerOptionsRaw = { + module: "commonjs", + lib: ["es6"], + strict: false, + types: [], + noEmit: true, + forceConsistentCasingInFileNames: true, + }; + const errors = checkTsconfig({ compilerOptions: strictOpts, files: ["index.d.ts", "test.ts"] }); + expect(errors).toContainEqual(expect.stringContaining('When "strict" is present, it must be set to `true`')); + }); + + it("throws when strict is true and noImplicitAny is also set", () => { + const strictOpts: CompilerOptionsRaw = { + module: "commonjs", + lib: ["es6"], + strict: true, + noImplicitAny: true, + types: [], + noEmit: true, + forceConsistentCasingInFileNames: true, + }; + expect(() => checkTsconfig({ compilerOptions: strictOpts, files: ["index.d.ts", "test.ts"] })).toThrow( + TypeError, + ); + }); + + it("throws when strict is true and strictNullChecks is also set", () => { + const strictOpts: CompilerOptionsRaw = { + module: "commonjs", + lib: ["es6"], + strict: true, + strictNullChecks: true, + types: [], + noEmit: true, + forceConsistentCasingInFileNames: true, + }; + expect(() => checkTsconfig({ compilerOptions: strictOpts, files: ["index.d.ts", "test.ts"] })).toThrow( + TypeError, + ); + }); + + it("errors when not strict and missing noImplicitAny", () => { + const opts = { ...base }; + delete (opts as any).noImplicitAny; + const errors = checkTsconfig({ compilerOptions: opts, files: ["index.d.ts", "test.ts"] }); + expect(errors).toContainEqual(expect.stringContaining("noImplicitAny")); + }); + + it("errors when not strict and missing noImplicitThis", () => { + const opts = { ...base }; + delete (opts as any).noImplicitThis; + const errors = checkTsconfig({ compilerOptions: opts, files: ["index.d.ts", "test.ts"] }); + expect(errors).toContainEqual(expect.stringContaining("noImplicitThis")); + }); + + it("errors when not strict and missing strictNullChecks", () => { + const opts = { ...base }; + delete (opts as any).strictNullChecks; + const errors = checkTsconfig({ compilerOptions: opts, files: ["index.d.ts", "test.ts"] }); + expect(errors).toContainEqual(expect.stringContaining("strictNullChecks")); + }); + + it("errors when not strict and missing strictFunctionTypes", () => { + const opts = { ...base }; + delete (opts as any).strictFunctionTypes; + const errors = checkTsconfig({ compilerOptions: opts, files: ["index.d.ts", "test.ts"] }); + expect(errors).toContainEqual(expect.stringContaining("strictFunctionTypes")); + }); + }); + + describe("allowed optional compiler options", () => { + it("allows target", () => { + expect(checkTsconfig(config({ target: "es2020" }))).toEqual([]); + }); + + it("allows jsx", () => { + expect(checkTsconfig(config({ jsx: "react" }))).toEqual([]); + }); + + it("allows jsxFactory", () => { + expect(checkTsconfig(config({ jsxFactory: "h" }))).toEqual([]); + }); + + it("allows jsxImportSource", () => { + expect(checkTsconfig(config({ jsxImportSource: "preact" }))).toEqual([]); + }); + + it("allows experimentalDecorators", () => { + expect(checkTsconfig(config({ experimentalDecorators: true }))).toEqual([]); + }); + + it("allows noUnusedLocals", () => { + expect(checkTsconfig(config({ noUnusedLocals: true }))).toEqual([]); + }); + + it("allows noUnusedParameters", () => { + expect(checkTsconfig(config({ noUnusedParameters: true }))).toEqual([]); + }); + + it("allows esModuleInterop", () => { + expect(checkTsconfig(config({ esModuleInterop: true }))).toEqual([]); + }); + + it("allows allowSyntheticDefaultImports", () => { + expect(checkTsconfig(config({ allowSyntheticDefaultImports: true }))).toEqual([]); + }); + + it("allows noUncheckedIndexedAccess", () => { + expect(checkTsconfig(config({ noUncheckedIndexedAccess: true }))).toEqual([]); + }); + }); + + describe("unknown compiler options", () => { + it("reports multiple unknown options", () => { + const errors = checkTsconfig(config({ someOption: true, anotherOption: false } as any)); + expect(errors).toContainEqual(expect.stringContaining("someOption")); + expect(errors).toContainEqual(expect.stringContaining("anotherOption")); + }); + }); + + describe("files validation edge cases", () => { + it("allows index.d.ts with ./index.d.ts prefix", () => { + expect(checkTsconfig({ compilerOptions: base, files: ["./index.d.ts", "./test.ts"] })).toEqual([]); + }); + + it("errors when files has only test files but no index.d.ts", () => { + const errors = checkTsconfig({ compilerOptions: base, files: ["test.ts"] }); + expect(errors).toContainEqual(expect.stringContaining('"files" list must include "index.d.ts"')); + }); + + it("returns error for include even if files is present", () => { + const errors = checkTsconfig({ compilerOptions: base, include: ["**/*.ts"], files: ["index.d.ts"] }); + expect(errors).toContainEqual(expect.stringContaining('Use "files" instead of "include"')); + }); + }); + + describe("valid complete config produces no errors", () => { + it("passes with a well-formed config", () => { + expect(checkTsconfig(config())).toEqual([]); + }); + }); +}); diff --git a/packages/dtslint/test/index.extra.test.ts b/packages/dtslint/test/index.extra.test.ts new file mode 100644 index 0000000000..97407574da --- /dev/null +++ b/packages/dtslint/test/index.extra.test.ts @@ -0,0 +1,30 @@ +/// +import { assertPackageIsNotDeprecated } from "../src/index"; + +describe("assertPackageIsNotDeprecated extended", () => { + it("throws with informative message including package name", () => { + expect(() => assertPackageIsNotDeprecated("moment", '{ "packages": { "moment": {} } }')).toThrow( + "notNeededPackages.json has an entry for moment", + ); + }); + + it("does not throw when notNeededPackages has other packages", () => { + expect(assertPackageIsNotDeprecated("lodash", '{ "packages": { "moment": {} } }')).toBeUndefined(); + }); + + it("does not throw when packages object is empty", () => { + expect(assertPackageIsNotDeprecated("anything", '{ "packages": {} }')).toBeUndefined(); + }); + + it("handles multiple packages in the list", () => { + const json = '{ "packages": { "foo": {}, "bar": {}, "baz": {} } }'; + expect(() => assertPackageIsNotDeprecated("bar", json)).toThrow("notNeededPackages.json has an entry for bar"); + expect(assertPackageIsNotDeprecated("qux", json)).toBeUndefined(); + }); + + it("throw message mentions removing entry from notNeededPackages.json", () => { + expect(() => assertPackageIsNotDeprecated("test-pkg", '{ "packages": { "test-pkg": {} } }')).toThrow( + /remove its entry from notNeededPackages\.json/, + ); + }); +}); diff --git a/packages/dtslint/test/util.test.ts b/packages/dtslint/test/util.test.ts new file mode 100644 index 0000000000..965cf411d1 --- /dev/null +++ b/packages/dtslint/test/util.test.ts @@ -0,0 +1,68 @@ +/// +import { packageNameFromPath, packageDirectoryNameWithVersionFromPath } from "../src/util"; + +describe("util", () => { + describe("packageNameFromPath", () => { + it("returns base name for a simple package path", () => { + expect(packageNameFromPath("/types/jquery")).toBe("jquery"); + }); + + it("returns parent name when path ends with a version directory (v14)", () => { + expect(packageNameFromPath("/types/node/v14")).toBe("node"); + }); + + it("returns parent name when path ends with a minor version directory (v0.69)", () => { + expect(packageNameFromPath("/types/react-native/v0.69")).toBe("react-native"); + }); + + it("returns parent name when path ends with ts version directory (ts5.0)", () => { + expect(packageNameFromPath("/types/some-pkg/ts5.0")).toBe("some-pkg"); + }); + + it("returns base name when path does not end with version pattern", () => { + expect(packageNameFromPath("/types/some-pkg/subdir")).toBe("subdir"); + }); + + it("handles Windows-style paths", () => { + expect(packageNameFromPath("C:\\types\\jquery")).toBe("jquery"); + }); + + it("handles Windows-style paths with version directory", () => { + expect(packageNameFromPath("C:\\types\\node\\v14")).toBe("node"); + }); + + it("returns parent for version pattern v1", () => { + expect(packageNameFromPath("/types/lodash/v1")).toBe("lodash"); + }); + + it("handles scoped-like package names", () => { + expect(packageNameFromPath("/types/bla__foo")).toBe("bla__foo"); + }); + }); + + describe("packageDirectoryNameWithVersionFromPath", () => { + it("returns just the package name when no version directory", () => { + expect(packageDirectoryNameWithVersionFromPath("/types/jquery")).toBe("jquery"); + }); + + it("returns package/version when path ends with version directory", () => { + expect(packageDirectoryNameWithVersionFromPath("/types/node/v14")).toBe("node/v14"); + }); + + it("returns package/version for minor version directories", () => { + expect(packageDirectoryNameWithVersionFromPath("/types/react-native/v0.69")).toBe("react-native/v0.69"); + }); + + it("returns just the subdir name when last segment is not a version", () => { + expect(packageDirectoryNameWithVersionFromPath("/types/pkg/subdir")).toBe("subdir"); + }); + + it("handles Windows-style paths without version", () => { + expect(packageDirectoryNameWithVersionFromPath("C:\\types\\jquery")).toBe("jquery"); + }); + + it("handles Windows-style paths with version", () => { + expect(packageDirectoryNameWithVersionFromPath("C:\\types\\node\\v14")).toBe("node/v14"); + }); + }); +});