test(backend): migrate to vitest (#95)

Use `vitest` for backend tests:

- `bench` script now uses [vitest's
`bench`](https://vitest.dev/api/#bench)
- e2e test fails with same error happening now with jest:`data-testid`
attribute not present.
  vitest left, jest right
<img width="1264" height="603" alt="image"
src="https://github.com/user-attachments/assets/56fc43a6-e143-4208-bf14-7e016aae7fa9"
/>
This commit is contained in:
Marco Pasqualetti
2026-02-22 16:02:46 +01:00
committed by GitHub
parent 62207c0b32
commit 238ca1c97f
60 changed files with 649 additions and 2708 deletions
-8
View File
@@ -1,8 +0,0 @@
export default {
clearMocks: true,
transform: {},
testEnvironment: "jsdom",
coverageProvider: "v8",
testMatch: ["<rootDir>/tests/bench/*.bench.{ts,js}"],
setupFiles: ["<rootDir>/tests/setup.jest.js"],
};
-7
View File
@@ -1,7 +0,0 @@
export default {
coverageProvider: "v8",
projects: [
"<rootDir>/jest.public-instance.config.js",
"<rootDir>/jest.private-instance.config.js",
],
};
-7
View File
@@ -1,7 +0,0 @@
export default {
clearMocks: true,
transform: {},
testEnvironment: "node",
coverageProvider: "v8",
testMatch: ["<rootDir>/tests/e2e/*.test.{ts,js}"],
};
@@ -1,13 +0,0 @@
export default {
clearMocks: true,
transform: {},
testEnvironment: "jsdom",
testMatch: [
"<rootDir>/tests/*.test.{ts,js}",
"<rootDir>/tests/private-instance/*.test.{ts,js}",
],
setupFiles: [
"<rootDir>/tests/setup.jest.js",
"<rootDir>/tests/setup.private-instance.jest.js",
],
};
@@ -1,10 +0,0 @@
export default {
clearMocks: true,
transform: {},
testEnvironment: "jsdom",
testMatch: [
"<rootDir>/tests/*.test.{ts,js}",
"<rootDir>/tests/public-instance/*.test.{ts,js}",
],
setupFiles: ["<rootDir>/tests/setup.jest.js"],
};
+8 -10
View File
@@ -19,27 +19,25 @@
"url": "https://github.com/stats-organization/github-stats-extended.git"
},
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
"test:update:snapshot": "node --experimental-vm-modules node_modules/jest/bin/jest.js -u",
"test:e2e": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.e2e.config.js",
"test": "vitest",
"test:update:snapshot": "vitest -u",
"test:e2e": "vitest --config vitest.config.e2e.ts",
"theme-readme-gen": "node scripts/generate-theme-doc",
"generate-langs-json": "node scripts/generate-langs-json",
"lint": "npx eslint --max-warnings 0 \"./src/**/*.js\" \"./scripts/**/*.js\" \"./tests/**/*.js\" \"./api-renamed/**/*.js\" \"./themes/**/*.js\"",
"bench": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.bench.config.js"
"bench": "vitest bench --run --config vitest.config.bench.ts"
},
"author": "Anurag Hazra",
"license": "MIT",
"devDependencies": {
"@jest/globals": "30.2.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@uppercod/css-to-object": "^1.1.1",
"@vitest/coverage-v8": "catalog:default",
"axios-mock-adapter": "^2.1.0",
"express": "^5.2.1",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"js-yaml": "^4.1.1"
"js-yaml": "^4.1.1",
"jsdom": "28.0.0",
"vitest": "catalog:default"
},
"dependencies": {
"axios": "^1.13.5",
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test Render WakaTime Card should render correctly 1`] = `
exports[`Test Render WakaTime Card > should render correctly 1`] = `
"
<svg
width="495"
@@ -158,7 +158,7 @@ exports[`Test Render WakaTime Card should render correctly 1`] = `
"
`;
exports[`Test Render WakaTime Card should render correctly with compact layout 1`] = `
exports[`Test Render WakaTime Card > should render correctly with compact layout 1`] = `
"
<svg
width="495"
@@ -310,7 +310,7 @@ exports[`Test Render WakaTime Card should render correctly with compact layout 1
"
`;
exports[`Test Render WakaTime Card should render correctly with compact layout when langs_count is set 1`] = `
exports[`Test Render WakaTime Card > should render correctly with compact layout when langs_count is set 1`] = `
"
<svg
width="495"
@@ -462,7 +462,7 @@ exports[`Test Render WakaTime Card should render correctly with compact layout w
"
`;
exports[`Test Render WakaTime Card should render correctly with percent display format 1`] = `
exports[`Test Render WakaTime Card > should render correctly with percent display format 1`] = `
"
<svg
width="495"
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test /api/wakatime should render error if user data is not accessible 1`] = `
exports[`Test /api/wakatime > should render error if user data is not accessible 1`] = `
"
<svg
width="495"
+3 -10
View File
@@ -1,15 +1,8 @@
// @ts-check
import {
afterEach,
beforeEach,
describe,
expect,
it,
jest,
} from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import api from "../api-renamed/index.js";
import { calculateRank } from "../src/calculateRank.js";
@@ -52,8 +45,8 @@ const faker = (query, data) => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").replyOnce(200, data);
+12 -10
View File
@@ -1,11 +1,9 @@
import { it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { bench, describe, vi } from "vitest";
import api from "../../api-renamed/index.js";
import { runAndLogStats } from "./utils.js";
const stats = {
name: "Anurag Hazra",
totalStars: 100,
@@ -63,18 +61,22 @@ const faker = (query, data) => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").replyOnce(200, data);
return { req, res };
};
it("test /api", async () => {
await runAndLogStats("test /api", async () => {
const { req, res } = faker({}, data_stats);
describe("/api", () => {
bench(
"base",
async () => {
const { req, res } = faker({}, data_stats);
await api(req, res);
});
await api(req, res);
},
{ warmupIterations: 50 },
);
});
+18 -16
View File
@@ -1,20 +1,22 @@
import { it } from "@jest/globals";
import { bench, describe } from "vitest";
import { calculateRank } from "../../src/calculateRank.js";
import { runAndLogStats } from "./utils.js";
it("calculateRank", async () => {
await runAndLogStats("calculateRank", () => {
calculateRank({
all_commits: false,
commits: 1300,
prs: 1500,
issues: 4500,
reviews: 1000,
repos: 0,
stars: 600000,
followers: 50000,
});
});
describe("calculateRank", () => {
bench(
"base",
async () => {
calculateRank({
all_commits: false,
commits: 1300,
prs: 1500,
issues: 4500,
reviews: 1000,
repos: 0,
stars: 600000,
followers: 50000,
});
},
{ warmupIterations: 50 },
);
});
+18 -16
View File
@@ -1,11 +1,9 @@
import { it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { bench, describe, vi } from "vitest";
import gist from "../../api-renamed/gist.js";
import { runAndLogStats } from "./utils.js";
const gist_data = {
data: {
viewer: {
@@ -36,18 +34,22 @@ const gist_data = {
const mock = new MockAdapter(axios);
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
it("test /api/gist", async () => {
await runAndLogStats("test /api/gist", async () => {
const req = {
query: {
id: "bbfce31e0217a3689c8d961a356cb10d",
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
describe("test /api/gist", () => {
bench(
"base",
async () => {
const req = {
query: {
id: "bbfce31e0217a3689c8d961a356cb10d",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
await gist(req, res);
});
await gist(req, res);
},
{ warmupIterations: 50 },
);
});
+19 -17
View File
@@ -1,11 +1,9 @@
import { it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { bench, describe, vi } from "vitest";
import pin from "../../api-renamed/pin.js";
import { runAndLogStats } from "./utils.js";
const data_repo = {
repository: {
username: "anuraghazra",
@@ -34,19 +32,23 @@ const data_user = {
const mock = new MockAdapter(axios);
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
it("test /api/pin", async () => {
await runAndLogStats("test /api/pin", async () => {
const req = {
query: {
username: "anuraghazra",
repo: "convoychat",
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
};
describe("/api/pin", () => {
bench(
"base",
async () => {
const req = {
query: {
username: "anuraghazra",
repo: "convoychat",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
await pin(req, res);
});
await pin(req, res);
},
{ warmupIterations: 50 },
);
});
-184
View File
@@ -1,184 +0,0 @@
// @ts-check
const DEFAULT_RUNS = 1000;
const DEFAULT_WARMUPS = 50;
/**
* Formats a duration in nanoseconds to a compact human-readable string.
*
* @param {bigint} ns Duration in nanoseconds.
* @returns {string} Formatted time string.
*/
const formatTime = (ns) => {
if (ns < 1_000n) {
return `${ns}ns`;
}
if (ns < 1_000_000n) {
return `${Number(ns) / 1_000}µs`;
}
if (ns < 1_000_000_000n) {
return `${(Number(ns) / 1_000_000).toFixed(3)}ms`;
}
return `${(Number(ns) / 1_000_000_000).toFixed(3)}s`;
};
/**
* Measures synchronous or async function execution time.
*
* @param {Function} fn Function to measure.
* @returns {Promise<bigint>} elapsed nanoseconds
*/
const measurePerformance = async (fn) => {
const start = process.hrtime.bigint();
const ret = fn();
if (ret instanceof Promise) {
await ret;
}
const end = process.hrtime.bigint();
return end - start;
};
/**
* Computes basic & extended statistics.
*
* @param {bigint[]} samples Array of samples in nanoseconds.
* @returns {{
* runs: number
* min: number
* max: number
* average: number
* median: number
* p75: number
* p95: number
* p99: number
* stdev: number
* totalTime: number
* }} Stats
*/
const computeStats = (samples) => {
const sorted = [...samples].sort((a, b) => (a < b ? -1 : 1));
const toNumber = (/** @type {bigint} */ b) => Number(b); // safe for typical short benches
const n = sorted.length;
const sum = sorted.reduce((a, b) => a + b, 0n);
const avg = Number(sum) / n;
const median =
n % 2
? toNumber(sorted[(n - 1) / 2])
: (toNumber(sorted[n / 2 - 1]) + toNumber(sorted[n / 2])) / 2;
const p = (/** @type {number} */ q) => {
const idx = Math.min(n - 1, Math.floor((q / 100) * n));
return toNumber(sorted[idx]);
};
const min = toNumber(sorted[0]);
const max = toNumber(sorted[n - 1]);
const variance =
sorted.reduce((acc, v) => acc + (toNumber(v) - avg) ** 2, 0) / n;
const stdev = Math.sqrt(variance);
return {
runs: n,
min,
max,
average: avg,
median,
p75: p(75),
p95: p(95),
p99: p(99),
stdev,
totalTime: toNumber(sum),
};
};
/**
* Benchmark a function.
*
* @param {string} fnName Name of the function (for logging).
* @param {Function} fn Function to benchmark.
* @param {object} [opts] Options.
* @param {number} [opts.runs] Number of measured runs.
* @param {number} [opts.warmup] Warm-up iterations (not measured).
* @param {boolean} [opts.trimOutliers] Drop top & bottom 1% before stats.
* @returns {Promise<object>} Stats (nanoseconds for core metrics).
*/
export const runAndLogStats = async (
fnName,
fn,
{ runs = DEFAULT_RUNS, warmup = DEFAULT_WARMUPS, trimOutliers = false } = {},
) => {
if (runs <= 0) {
throw new Error("Number of runs must be positive.");
}
// Warm-up
for (let i = 0; i < warmup; i++) {
const ret = fn();
if (ret instanceof Promise) {
await ret;
}
}
const samples = [];
for (let i = 0; i < runs; i++) {
samples.push(await measurePerformance(fn));
}
let processed = samples;
if (trimOutliers && samples.length > 10) {
const sorted = [...samples].sort((a, b) => (a < b ? -1 : 1));
const cut = Math.max(1, Math.floor(sorted.length * 0.01));
processed = sorted.slice(cut, sorted.length - cut);
}
const stats = computeStats(processed);
const fmt = (/** @type {number} */ ns) => formatTime(BigInt(Math.round(ns)));
console.log(
`${fnName} | runs=${stats.runs} avg=${fmt(stats.average)} median=${fmt(
stats.median,
)} p95=${fmt(stats.p95)} min=${fmt(stats.min)} max=${fmt(
stats.max,
)} stdev=${fmt(stats.stdev)}`,
);
return stats;
};
/**
* Creates an asymmetric matcher for approximate numeric equality.
*
* This helper is intended for use in test frameworks (e.g., Jest) where
* values need to be compared within a configurable decimal precision
* instead of strict equality.
*
* The comparison succeeds when:
*
* |actual - expected| < 10^(-precision)
*
* For example, with `precision = 3`, values must be within `0.001`.
*
* @param {number} expected The expected numeric value to compare against.
*
* @param {number} [precision=10]
* The number of decimal places of tolerance. Higher values mean stricter
* comparison. Internally converted to epsilon = 10^-precision.
*
* @returns {{
* asymmetricMatch(actual: unknown): boolean,
* toAsymmetricMatcher(): string
* }} An object implementing Jest-style asymmetric matcher methods.
*
*/
export function approxNumber(expected, precision = 10) {
return {
asymmetricMatch(actual) {
if (typeof actual !== "number" || typeof expected !== "number") {
return false;
}
const epsilon = Math.pow(10, -precision);
return Math.abs(actual - expected) < epsilon;
},
toAsymmetricMatcher() {
return `${expected} (precision ${precision})`;
},
};
}
+4 -3
View File
@@ -1,9 +1,10 @@
import { describe, expect, it } from "@jest/globals";
import { describe, expect, it } from "vitest";
import "@testing-library/jest-dom";
import { calculateRank } from "../src/calculateRank.js";
import { approxNumber } from "./bench/utils.js";
import { approxNumber } from "./utils.js";
import "@testing-library/jest-dom/vitest";
describe("Test calculateRank", () => {
it("new user gets C rank", () => {
+3 -2
View File
@@ -1,12 +1,13 @@
import { describe, expect, it } from "@jest/globals";
import { queryByTestId } from "@testing-library/dom";
import "@testing-library/jest-dom";
import { cssToObject } from "@uppercod/css-to-object";
import { describe, expect, it } from "vitest";
import { Card } from "../src/common/Card.js";
import { getCardColors } from "../src/common/color.js";
import { icons } from "../src/common/icons.js";
import "@testing-library/jest-dom/vitest";
describe("Card", () => {
it("should hide border", () => {
const card = new Card({});
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, it } from "@jest/globals";
import { describe, expect, it } from "vitest";
import { getCardColors } from "../src/common/color";
+1 -1
View File
@@ -2,8 +2,8 @@
* @file Contains end-to-end tests for the Vercel preview instance.
*/
import { beforeAll, describe, expect, test } from "@jest/globals";
import axios from "axios";
import { beforeAll, describe, expect, test } from "vitest";
import { renderGistCard } from "../../src/cards/gist.js";
import { renderRepoCard } from "../../src/cards/repo.js";
+3 -2
View File
@@ -1,10 +1,11 @@
import { afterEach, describe, expect, it } from "@jest/globals";
import "@testing-library/jest-dom";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it } from "vitest";
import { fetchGist } from "../src/fetchers/gist.js";
import "@testing-library/jest-dom/vitest";
const gist_data = {
data: {
viewer: {
+3 -2
View File
@@ -1,10 +1,11 @@
import { afterEach, describe, expect, it } from "@jest/globals";
import "@testing-library/jest-dom";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it } from "vitest";
import { fetchRepo } from "../src/fetchers/repo.js";
import "@testing-library/jest-dom/vitest";
const data_repo = {
repository: {
name: "convoychat",
+7 -13
View File
@@ -1,18 +1,12 @@
/** @jest-environment node */
/*
Jest must use node for these tests because jsdom doesn't support the structuredClone call in stats.js.
https://jestjs.io/docs/configuration#testenvironment-string
https://github.com/jsdom/jsdom/issues/3363
*/
import { afterEach, beforeEach, describe, expect, it } from "@jest/globals";
import "@testing-library/jest-dom";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { calculateRank } from "../src/calculateRank.js";
import { fetchStats } from "../src/fetchers/stats.js";
import "@testing-library/jest-dom/vitest";
// Test parameters.
const data_stats = {
data: {
@@ -260,8 +254,8 @@ describe("Test fetchStats", () => {
});
it("should throw specific error when include_all_commits true and invalid username", async () => {
expect(fetchStats("asdf///---", true)).rejects.toThrow(
new Error("Invalid username provided."),
await expect(fetchStats("asdf///---", true)).rejects.toThrow(
"Invalid username provided.",
);
});
@@ -272,8 +266,8 @@ describe("Test fetchStats", () => {
)
.reply(200, { error: "Some test error message" });
expect(fetchStats("anuraghazra", true)).rejects.toThrow(
new Error("Could not fetch data from GitHub REST API."),
await expect(fetchStats("anuraghazra", true)).rejects.toThrow(
"Could not fetch data from GitHub REST API.",
);
});
+4 -3
View File
@@ -1,11 +1,12 @@
import { afterEach, describe, expect, it } from "@jest/globals";
import "@testing-library/jest-dom";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it } from "vitest";
import { fetchTopLanguages } from "../src/fetchers/top-languages.js";
import { approxNumber } from "./bench/utils.js";
import { approxNumber } from "./utils.js";
import "@testing-library/jest-dom/vitest";
const mock = new MockAdapter(axios);
+3 -2
View File
@@ -1,10 +1,11 @@
import { afterEach, describe, expect, it } from "@jest/globals";
import "@testing-library/jest-dom";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it } from "vitest";
import { fetchWakatimeStats } from "../src/fetchers/wakatime.js";
import "@testing-library/jest-dom/vitest";
const mock = new MockAdapter(axios);
afterEach(() => {
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, it } from "@jest/globals";
import { describe, expect, it } from "vitest";
import { flexLayout } from "../src/common/render.js";
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, it } from "@jest/globals";
import { describe, expect, it } from "vitest";
import {
formatBytes,
+13 -12
View File
@@ -1,9 +1,8 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import "@testing-library/jest-dom";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import gist from "../api-renamed/gist.js";
import { renderGistCard } from "../src/cards/gist.js";
@@ -12,6 +11,8 @@ import { renderError } from "../src/common/render.js";
import { gist_data } from "./test-data/gist-data.js";
import "@testing-library/jest-dom/vitest";
const gist_not_found_data = {
data: {
viewer: {
@@ -34,8 +35,8 @@ describe("Test /api/gist", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
@@ -66,8 +67,8 @@ describe("Test /api/gist", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
@@ -96,8 +97,8 @@ describe("Test /api/gist", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock
.onPost("https://api.github.com/graphql")
@@ -119,8 +120,8 @@ describe("Test /api/gist", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
@@ -142,8 +143,8 @@ describe("Test /api/gist", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, it } from "@jest/globals";
import { describe, expect, it } from "vitest";
import { encodeHTML } from "../src/common/html.js";
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, it } from "@jest/globals";
import { describe, expect, it } from "vitest";
import { I18n } from "../src/common/I18n.js";
import { statCardLocales } from "../src/translations.js";
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, it } from "@jest/globals";
import { describe, expect, it } from "vitest";
import {
chunkArray,
+3 -10
View File
@@ -2,16 +2,9 @@
* @file Tests for the status/pat-info cloud function.
*/
import {
afterEach,
beforeAll,
describe,
expect,
it,
jest,
} from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import patInfo, { RATE_LIMIT_SECONDS } from "../api-renamed/status/pat-info.js";
@@ -30,8 +23,8 @@ const faker = (query) => {
query: { ...query },
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
return { req, res };
+15 -14
View File
@@ -1,9 +1,8 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import "@testing-library/jest-dom";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import pin from "../api-renamed/pin.js";
import { renderRepoCard } from "../src/cards/repo.js";
@@ -12,6 +11,8 @@ import { renderError } from "../src/common/render.js";
import { data_repo, data_user } from "./test-data/pin-data.js";
import "@testing-library/jest-dom/vitest";
const mock = new MockAdapter(axios);
afterEach(() => {
@@ -27,8 +28,8 @@ describe("Test /api/pin", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
@@ -57,8 +58,8 @@ describe("Test /api/pin", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
@@ -85,8 +86,8 @@ describe("Test /api/pin", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock
.onPost("https://api.github.com/graphql")
@@ -108,8 +109,8 @@ describe("Test /api/pin", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock
.onPost("https://api.github.com/graphql")
@@ -132,8 +133,8 @@ describe("Test /api/pin", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
@@ -156,8 +157,8 @@ describe("Test /api/pin", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
@@ -1,8 +1,8 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import api from "../../api-renamed/index.js";
import { renderError } from "../../src/common/render.js";
@@ -22,8 +22,8 @@ describe("Test /api/", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").replyOnce(200, data_stats);
@@ -1,8 +1,8 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import gist from "../../api-renamed/gist.js";
import { renderError } from "../../src/common/render.js";
@@ -22,8 +22,8 @@ describe("Test /api/gist with gist whitelist", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
@@ -1,8 +1,8 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import pin from "../../api-renamed/pin.js";
import { renderError } from "../../src/common/render.js";
@@ -23,8 +23,8 @@ describe("Test /api/pin", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
@@ -45,8 +45,8 @@ describe("Test /api/pin", () => {
query: {},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
@@ -1,8 +1,8 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import topLangs from "../../api-renamed/top-langs.js";
import { renderError } from "../../src/common/render.js";
@@ -22,8 +22,8 @@ describe("Test /api/top-langs", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
@@ -1,8 +1,8 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import api from "../../api-renamed/index.js";
import { renderError } from "../../src/common/render.js";
@@ -22,8 +22,8 @@ describe("Test /api/", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").replyOnce(200, data_stats);
@@ -1,8 +1,8 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import gist from "../../api-renamed/gist.js";
import { renderError } from "../../src/common/render.js";
@@ -20,8 +20,8 @@ describe("Test /api/gist", () => {
query: {},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
@@ -1,8 +1,8 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import pin from "../../api-renamed/pin.js";
import { renderError } from "../../src/common/render.js";
@@ -23,8 +23,8 @@ describe("Test /api/pin", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
@@ -45,8 +45,8 @@ describe("Test /api/pin", () => {
query: {},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
@@ -1,8 +1,8 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import topLangs from "../../api-renamed/top-langs.js";
import { renderError } from "../../src/common/render.js";
@@ -22,8 +22,8 @@ describe("Test /api/top-langs", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
+3 -2
View File
@@ -1,11 +1,12 @@
// @ts-check
import { describe, expect, it } from "@jest/globals";
import { queryByTestId } from "@testing-library/dom";
import { describe, expect, it } from "vitest";
import "@testing-library/jest-dom/jest-globals";
import { renderError } from "../src/common/render.js";
import "@testing-library/jest-dom/vitest";
describe("Test render.js", () => {
it("should test renderError", () => {
document.body.innerHTML = renderError({ message: "Something went wrong" });
+3 -2
View File
@@ -1,11 +1,12 @@
import { describe, expect, it } from "@jest/globals";
import { queryByTestId } from "@testing-library/dom";
import "@testing-library/jest-dom";
import { cssToObject } from "@uppercod/css-to-object";
import { describe, expect, it } from "vitest";
import { renderGistCard } from "../src/cards/gist.js";
import { themes } from "../themes/index.js";
import "@testing-library/jest-dom/vitest";
/**
* @type {import("../src/fetchers/gist").GistData}
*/
+3 -2
View File
@@ -1,11 +1,12 @@
import { describe, expect, it } from "@jest/globals";
import { queryByTestId } from "@testing-library/dom";
import "@testing-library/jest-dom";
import { cssToObject } from "@uppercod/css-to-object";
import { describe, expect, it } from "vitest";
import { renderRepoCard } from "../src/cards/repo.js";
import { themes } from "../themes/index.js";
import "@testing-library/jest-dom/vitest";
const data_repo = {
repository: {
nameWithOwner: "anuraghazra/convoychat",
+3 -2
View File
@@ -1,16 +1,17 @@
import { describe, expect, it } from "@jest/globals";
import {
getByTestId,
queryAllByTestId,
queryByTestId,
} from "@testing-library/dom";
import "@testing-library/jest-dom";
import { cssToObject } from "@uppercod/css-to-object";
import { describe, expect, it } from "vitest";
import { renderStatsCard } from "../src/cards/stats.js";
import { CustomError } from "../src/common/error.js";
import { themes } from "../themes/index.js";
import "@testing-library/jest-dom/vitest";
const stats = {
name: "Anurag Hazra",
totalStars: 100,
@@ -1,7 +1,6 @@
import { describe, expect, it } from "@jest/globals";
import { queryAllByTestId, queryByTestId } from "@testing-library/dom";
import "@testing-library/jest-dom";
import { cssToObject } from "@uppercod/css-to-object";
import { describe, expect, it } from "vitest";
import {
MIN_CARD_WIDTH,
@@ -23,7 +22,9 @@ import {
} from "../src/cards/top-languages.js";
import { themes } from "../themes/index.js";
import { approxNumber } from "./bench/utils.js";
import { approxNumber } from "./utils.js";
import "@testing-library/jest-dom/vitest";
const langs = {
HTML: {
@@ -1,11 +1,11 @@
import { describe, expect, it } from "@jest/globals";
import { queryByTestId } from "@testing-library/dom";
import { describe, expect, it } from "vitest";
import { renderWakatimeCard } from "../src/cards/wakatime.js";
import { wakaTimeData } from "./fetchWakatime.test.js";
import "@testing-library/jest-dom";
import "@testing-library/jest-dom/vitest";
describe("Test Render WakaTime Card", () => {
it("should render correctly", () => {
+7 -6
View File
@@ -1,23 +1,24 @@
// @ts-check
import { describe, expect, it, jest } from "@jest/globals";
import { describe, expect, it, vi } from "vitest";
import "@testing-library/jest-dom/vitest";
import "@testing-library/jest-dom";
import { logger } from "../src/common/log.js";
import { retryer } from "../src/common/retryer.js";
const fetcher = jest.fn((variables, token) => {
const fetcher = vi.fn((variables, token) => {
logger.log(variables, token);
return new Promise((res) => res({ data: "ok" }));
});
const fetcherFail = jest.fn(() => {
const fetcherFail = vi.fn(() => {
return new Promise((res) =>
res({ data: { errors: [{ type: "RATE_LIMITED" }] } }),
);
});
const fetcherFailOnSecondTry = jest.fn((_vars, _token, retries) => {
const fetcherFailOnSecondTry = vi.fn((_vars, _token, retries) => {
return new Promise((res) => {
// faking rate limit
// @ts-ignore
@@ -28,7 +29,7 @@ const fetcherFailOnSecondTry = jest.fn((_vars, _token, retries) => {
});
});
const fetcherFailWithMessageBasedRateLimitErr = jest.fn(
const fetcherFailWithMessageBasedRateLimitErr = vi.fn(
(_vars, _token, retries) => {
return new Promise((res) => {
// faking rate limit
+3 -3
View File
@@ -2,9 +2,9 @@
* @file Tests for the status/up cloud function.
*/
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import up, { RATE_LIMIT_SECONDS } from "../api-renamed/status/up.js";
@@ -21,8 +21,8 @@ const faker = (query) => {
query: { ...query },
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
return { req, res };
+15 -14
View File
@@ -1,9 +1,8 @@
// @ts-check
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import "@testing-library/jest-dom";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import topLangs from "../api-renamed/top-langs.js";
import { renderTopLanguages } from "../src/cards/top-languages.js";
@@ -12,6 +11,8 @@ import { renderError } from "../src/common/render.js";
import { data_langs } from "./test-data/langs-data.js";
import "@testing-library/jest-dom/vitest";
const error = {
errors: [
{
@@ -50,8 +51,8 @@ describe("Test /api/top-langs", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
@@ -74,8 +75,8 @@ describe("Test /api/top-langs", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
@@ -101,8 +102,8 @@ describe("Test /api/top-langs", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, error);
@@ -126,8 +127,8 @@ describe("Test /api/top-langs", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
@@ -150,8 +151,8 @@ describe("Test /api/top-langs", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
@@ -173,8 +174,8 @@ describe("Test /api/top-langs", () => {
},
};
const res = {
setHeader: jest.fn(),
send: jest.fn(),
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
+41
View File
@@ -0,0 +1,41 @@
// @ts-check
/**
* Creates an asymmetric matcher for approximate numeric equality.
*
* This helper is intended for use in test frameworks (e.g., Jest) where
* values need to be compared within a configurable decimal precision
* instead of strict equality.
*
* The comparison succeeds when:
*
* |actual - expected| < 10^(-precision)
*
* For example, with `precision = 3`, values must be within `0.001`.
*
* @param {number} expected The expected numeric value to compare against.
*
* @param {number} [precision=10]
* The number of decimal places of tolerance. Higher values mean stricter
* comparison. Internally converted to epsilon = 10^-precision.
*
* @returns {{
* asymmetricMatch(actual: unknown): boolean,
* toAsymmetricMatcher(): string
* }} An object implementing Jest-style asymmetric matcher methods.
*
*/
export function approxNumber(expected, precision = 10) {
return {
asymmetricMatch(actual) {
if (typeof actual !== "number" || typeof expected !== "number") {
return false;
}
const epsilon = Math.pow(10, -precision);
return Math.abs(actual - expected) < epsilon;
},
toAsymmetricMatcher() {
return `${expected} (precision ${precision})`;
},
};
}
+7 -6
View File
@@ -1,13 +1,14 @@
import { afterEach, describe, expect, it, jest } from "@jest/globals";
import "@testing-library/jest-dom";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import wakatime from "../api-renamed/wakatime.js";
import { renderWakatimeCard } from "../src/cards/wakatime.js";
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
import { renderError } from "../src/index.js";
import "@testing-library/jest-dom/vitest";
const wakaTimeData = {
data: {
categories: [
@@ -118,7 +119,7 @@ describe("Test /api/wakatime", () => {
it("should test the request", async () => {
const username = "anuraghazra";
const req = { query: { username } };
const res = { setHeader: jest.fn(), send: jest.fn() };
const res = { setHeader: vi.fn(), send: vi.fn() };
mock
.onGet(
`https://wakatime.com/api/v1/users/${username}/stats?is_including_today=true`,
@@ -136,7 +137,7 @@ describe("Test /api/wakatime", () => {
it("should render error if wrong locale is provided", async () => {
const username = "anuraghazra";
const req = { query: { username, locale: "asdf" } };
const res = { setHeader: jest.fn(), send: jest.fn() };
const res = { setHeader: vi.fn(), send: vi.fn() };
mock
.onGet(
`https://wakatime.com/api/v1/users/${username}/stats?is_including_today=true`,
@@ -157,7 +158,7 @@ describe("Test /api/wakatime", () => {
it("should render error if user data is not accessible", async () => {
const username = "anuraghazra";
const req = { query: { username } };
const res = { setHeader: jest.fn(), send: jest.fn() };
const res = { setHeader: vi.fn(), send: vi.fn() };
mock
.onGet(
`https://wakatime.com/api/v1/users/${username}/stats?is_including_today=true`,
@@ -174,7 +175,7 @@ describe("Test /api/wakatime", () => {
it("should have proper cache", async () => {
const username = "anuraghazra";
const req = { query: { username } };
const res = { setHeader: jest.fn(), send: jest.fn() };
const res = { setHeader: vi.fn(), send: vi.fn() };
mock
.onGet(
`https://wakatime.com/api/v1/users/${username}/stats?is_including_today=true`,
+14
View File
@@ -0,0 +1,14 @@
{
"extends": ["../../tsconfig.base.json"],
"include": [
"src",
"vitest.config.ts",
"vitest.config.bench.ts",
"vitest.config.e2e.ts"
],
"compilerOptions": {
"composite": true,
"module": "nodenext",
"moduleResolution": "nodenext"
}
}
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
dir: "./tests/bench",
environment: "jsdom",
setupFiles: ["./tests/_setup.js"],
},
});
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
dir: "tests/e2e",
},
});
+32
View File
@@ -0,0 +1,32 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
enabled: true,
},
projects: [
{
test: {
name: "backend/public-instance",
environment: "jsdom",
dir: "tests",
include: ["./*.test.{ts,js}", "./public-instance/*.test.{ts,js}"],
setupFiles: ["./tests/_setup.js"],
},
},
{
test: {
name: "backend/private-instance",
environment: "jsdom",
dir: "tests",
include: ["./*.test.{ts,js}", "./private-instance/*.test.{ts,js}"],
setupFiles: [
"./tests/_setup.js",
"./tests/_setup.private-instance.js",
],
},
},
],
},
});
+1 -18
View File
@@ -2,26 +2,9 @@
"$schema": "./node_modules/knip/schema.json",
"workspaces": {
"apps/backend": {
"jest": {
"config": [
"jest.config.js",
"jest.*.config.js",
"package.json" // included by default, see https://knip.dev/reference/plugins/jest
]
},
"entry": [
"api-renamed/*.js",
"express.js",
"jest.*.js",
"tests/bench/*.bench.js"
],
"entry": ["api-renamed/*.js", "express.js", "tests/bench/*.bench.js"],
"ignoreFiles": [
"_dot_vercel_copy/**/*" // Hopefully by using a npm package this file will be removed
],
"ignoreDependencies": [
"jest" // used by test scripts referencing binary due to lack of ESM support
]
},
"apps/frontend": {
+312 -2235
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -2,6 +2,7 @@ packages:
- apps/*
catalog:
"@vitest/coverage-v8": 4.0.18
vite: 7.3.1
vitest: 4.0.18
+3
View File
@@ -1,6 +1,9 @@
{
"files": [],
"references": [
{
"path": "./apps/backend"
},
{
"path": "./apps/frontend"
}