Skip to content

Commit 1800484

Browse files
committed
Extracted configuration extraction method
1 parent 78e07fc commit 1800484

File tree

2 files changed

+231
-0
lines changed

2 files changed

+231
-0
lines changed

src/extractConfiguration.test.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import fs from "fs";
2+
import extractConfiguration, {
3+
sanitizeConfig,
4+
toEnvVar,
5+
} from "./extractConfiguration";
6+
7+
// Mock the functions used for reading files
8+
jest.mock("fs", () => ({
9+
existsSync: jest.fn(),
10+
readFileSync: jest.fn(),
11+
}));
12+
13+
describe("Config Utilities", () => {
14+
describe("toEnvVar", () => {
15+
it("should convert camelCase to uppercase with underscores and prepend with JEST_HTML_REPORTER_", () => {
16+
expect(toEnvVar("statusIgnoreFilter")).toBe(
17+
"JEST_HTML_REPORTER_STATUS_IGNORE_FILTER"
18+
);
19+
expect(toEnvVar("executionTimeWarningThreshold")).toBe(
20+
"JEST_HTML_REPORTER_EXECUTION_TIME_WARNING_THRESHOLD"
21+
);
22+
});
23+
});
24+
25+
describe("readJsonFile", () => {
26+
it("should return parsed JSON from file", () => {
27+
const mockConfig = { append: true };
28+
(fs.existsSync as jest.Mock).mockReturnValue(true);
29+
(fs.readFileSync as jest.Mock).mockReturnValue(
30+
JSON.stringify(mockConfig)
31+
);
32+
33+
const result = extractConfiguration({});
34+
expect(result.append).toBe(true);
35+
});
36+
37+
it("should return an empty object if file doesn't exist", () => {
38+
(fs.existsSync as jest.Mock).mockReturnValue(false);
39+
(fs.readFileSync as jest.Mock).mockReturnValue(null);
40+
41+
const result = extractConfiguration({});
42+
expect(result).toEqual(expect.objectContaining({}));
43+
});
44+
45+
it("should return an empty object on JSON parse error", () => {
46+
(fs.existsSync as jest.Mock).mockReturnValue(true);
47+
(fs.readFileSync as jest.Mock).mockReturnValue("Invalid JSON");
48+
49+
const result = extractConfiguration({});
50+
expect(result).toEqual(expect.objectContaining({}));
51+
});
52+
});
53+
54+
describe("sanitizeConfig", () => {
55+
it("should sanitize input based on type parsers", () => {
56+
const input = {
57+
append: "true", // Should be parsed as a boolean
58+
dateFormat: "yyyy-MM-dd", // Should be parsed as a string
59+
};
60+
61+
const sanitized = sanitizeConfig(input);
62+
expect(sanitized.append).toBe(true); // True, since 'true' is parsed to boolean
63+
expect(sanitized.dateFormat).toBe("yyyy-MM-dd");
64+
});
65+
66+
it("should skip invalid values", () => {
67+
const input = {
68+
append: "invalid", // Should be ignored due to invalid boolean
69+
dateFormat: "yyyy-MM-dd", // Should be parsed correctly
70+
};
71+
72+
const sanitized = sanitizeConfig(input);
73+
expect(sanitized.append).toBe(false); // Default value
74+
expect(sanitized.dateFormat).toBe("yyyy-MM-dd");
75+
});
76+
77+
it("should handle null and undefined", () => {
78+
const input = null;
79+
const sanitized = sanitizeConfig(input);
80+
expect(sanitized).toEqual({});
81+
});
82+
});
83+
84+
describe("extractConfiguration", () => {
85+
it("should prioritize environment variables over other sources", () => {
86+
process.env.JEST_HTML_REPORTER_PAGE_TITLE = "Environment Title";
87+
const config = extractConfiguration({ pageTitle: "CLI Title" });
88+
89+
expect(config.pageTitle).toBe("Environment Title");
90+
91+
delete process.env.JEST_HTML_REPORTER_PAGE_TITLE; // Clear the env var to avoid side effects
92+
});
93+
94+
it("should use CLI config when no env var is provided", () => {
95+
const config = extractConfiguration({ pageTitle: "CLI Title" });
96+
97+
expect(config.pageTitle).toBe("CLI Title");
98+
});
99+
100+
it("should use default values when no config is provided", () => {
101+
const config = extractConfiguration({});
102+
103+
expect(config.pageTitle).toBe("Test Report");
104+
expect(config.dateFormat).toBe("yyyy-mm-dd HH:MM:ss");
105+
});
106+
});
107+
});

src/extractConfiguration.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import path from "path";
2+
import fs from "fs";
3+
import { JestHTMLReporterConfiguration } from "./types";
4+
import { parseBoolean, parseNumber, parseString } from "./utils";
5+
6+
const defaultValues: JestHTMLReporterConfiguration = {
7+
append: false,
8+
boilerplate: undefined,
9+
collapseSuitesByDefault: false,
10+
customScriptPath: undefined,
11+
dateFormat: "yyyy-mm-dd HH:MM:ss",
12+
executionTimeWarningThreshold: 5,
13+
includeConsoleLog: false,
14+
includeFailureMsg: false,
15+
includeStackTrace: true,
16+
includeSuiteFailure: false,
17+
includeObsoleteSnapshots: false,
18+
logo: undefined,
19+
outputPath: path.join(process.cwd(), "test-report.html"),
20+
pageTitle: "Test Report",
21+
sort: undefined,
22+
statusIgnoreFilter: undefined,
23+
styleOverridePath: undefined,
24+
theme: "defaultTheme",
25+
useCssFile: false,
26+
};
27+
28+
// Convert config key to environment variable format
29+
export function toEnvVar(key: string): string {
30+
return `JEST_HTML_REPORTER_${key
31+
.replace(/([a-z])([A-Z])/g, "$1_$2")
32+
.toUpperCase()}`;
33+
}
34+
35+
// Read JSON file safely
36+
export function readJsonFile(filePath: string) {
37+
try {
38+
if (fs.existsSync(filePath)) {
39+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
40+
}
41+
} catch {
42+
// Ignore errors
43+
return {};
44+
}
45+
return {};
46+
}
47+
48+
// Type conversion functions
49+
const typeParsers: {
50+
[key in keyof JestHTMLReporterConfiguration]: (
51+
value: unknown
52+
) => string | number | boolean | undefined;
53+
} = {
54+
append: parseBoolean,
55+
boilerplate: parseString,
56+
collapseSuitesByDefault: parseBoolean,
57+
customScriptPath: parseString,
58+
dateFormat: parseString,
59+
executionTimeWarningThreshold: parseNumber,
60+
includeConsoleLog: parseBoolean,
61+
includeFailureMsg: parseBoolean,
62+
includeStackTrace: parseBoolean,
63+
includeSuiteFailure: parseBoolean,
64+
includeObsoleteSnapshots: parseBoolean,
65+
logo: parseString,
66+
outputPath: parseString,
67+
pageTitle: parseString,
68+
sort: parseString,
69+
statusIgnoreFilter: parseString,
70+
styleOverridePath: parseString,
71+
theme: parseString,
72+
useCssFile: parseBoolean,
73+
};
74+
75+
// Function to clean & validate configuration input
76+
export function sanitizeConfig(
77+
input: unknown
78+
): Partial<JestHTMLReporterConfiguration> {
79+
if (typeof input !== "object" || input === null) {
80+
return {};
81+
}
82+
83+
return (
84+
Object.keys(defaultValues) as (keyof JestHTMLReporterConfiguration)[]
85+
).reduce((sanitized, key) => {
86+
const parser = typeParsers[key];
87+
if (parser && typeof parser === "function" && key in input) {
88+
const value = parser((input as JestHTMLReporterConfiguration)[key]);
89+
if (value !== undefined) {
90+
return { ...sanitized, [key]: value };
91+
}
92+
}
93+
return sanitized;
94+
}, {});
95+
}
96+
97+
export default function (cliConfig: unknown): JestHTMLReporterConfiguration {
98+
// Read from environment variables
99+
const envConfig: Partial<JestHTMLReporterConfiguration> = (
100+
Object.keys(defaultValues) as (keyof JestHTMLReporterConfiguration)[]
101+
).reduce((config, key) => {
102+
const envKey = toEnvVar(key);
103+
if (process.env[envKey] !== undefined) {
104+
return { ...config, [key]: process.env[envKey] };
105+
}
106+
return config;
107+
}, {});
108+
109+
// Read from JSON files
110+
const customJsonConfig = readJsonFile(
111+
path.join(process.cwd(), "jesthtmlreporter.config.json")
112+
);
113+
const packageJsonConfig =
114+
readJsonFile(path.join(process.cwd(), "package.json"))?.jest || {};
115+
116+
// Merge configurations in priority order (with sanitization)
117+
return {
118+
...defaultValues, // Default values (lowest priority)
119+
...sanitizeConfig(packageJsonConfig), // package.json "jest" section
120+
...sanitizeConfig(customJsonConfig), // Custom JSON config
121+
...sanitizeConfig(cliConfig), // CLI parameters
122+
...sanitizeConfig(envConfig), // Environment variables (highest priority)
123+
};
124+
}

0 commit comments

Comments
 (0)