This commit is contained in:
498
backend/__tests__/utils/misc.spec.ts
Normal file
498
backend/__tests__/utils/misc.spec.ts
Normal file
@@ -0,0 +1,498 @@
|
||||
import { describe, it, expect, afterAll, vi } from "vitest";
|
||||
import * as Misc from "../../src/utils/misc";
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
describe("Misc Utils", () => {
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("matchesAPattern", () => {
|
||||
const testCases = [
|
||||
{
|
||||
pattern: "eng.*",
|
||||
cases: ["english", "aenglish", "en", "eng"],
|
||||
expected: [true, false, false, true],
|
||||
},
|
||||
|
||||
{
|
||||
pattern: "\\d+",
|
||||
cases: ["b", "2", "331", "1a"],
|
||||
expected: [false, true, true, false],
|
||||
},
|
||||
{
|
||||
pattern: "(hi|hello)",
|
||||
cases: ["hello", "hi", "hillo", "hi hello"],
|
||||
expected: [true, true, false, false],
|
||||
},
|
||||
{
|
||||
pattern: ".+",
|
||||
cases: ["a2", "b2", "c1", ""],
|
||||
expected: [true, true, true, false],
|
||||
},
|
||||
];
|
||||
|
||||
it.each(testCases)(
|
||||
"matchesAPattern with $pattern",
|
||||
({ pattern, cases, expected }) => {
|
||||
cases.forEach((caseValue, index) => {
|
||||
expect(Misc.matchesAPattern(caseValue, pattern)).toBe(
|
||||
expected[index],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("kogascore", () => {
|
||||
const testCases = [
|
||||
{
|
||||
wpm: 214.8,
|
||||
acc: 93.04,
|
||||
timestamp: 1653586489000,
|
||||
expectedScore: 1214800930423111,
|
||||
},
|
||||
{
|
||||
wpm: 214.8,
|
||||
acc: 93.04,
|
||||
timestamp: 1653601763000,
|
||||
expectedScore: 1214800930407837,
|
||||
},
|
||||
{
|
||||
wpm: 199.37,
|
||||
acc: 97.69,
|
||||
timestamp: 1653588809000,
|
||||
expectedScore: 1199370976920791,
|
||||
},
|
||||
{
|
||||
wpm: 196.2,
|
||||
acc: 96.07,
|
||||
timestamp: 1653591901000,
|
||||
expectedScore: 1196200960717699,
|
||||
},
|
||||
{
|
||||
wpm: 196.205,
|
||||
acc: 96.075,
|
||||
timestamp: 1653591901000,
|
||||
expectedScore: 1196210960817699,
|
||||
},
|
||||
{
|
||||
// this one is particularly important - in JS 154.39 * 100 is equal to 15438.999999999998
|
||||
// thanks floating point errors!
|
||||
wpm: 154.39,
|
||||
acc: 96.14,
|
||||
timestamp: 1740333827000,
|
||||
expectedScore: 1154390961421373,
|
||||
},
|
||||
];
|
||||
|
||||
it.each(testCases)(
|
||||
"kogascore with wpm:$wpm, acc:$acc, timestamp:$timestamp = $expectedScore",
|
||||
({ wpm, acc, timestamp, expectedScore }) => {
|
||||
expect(Misc.kogascore(wpm, acc, timestamp)).toBe(expectedScore);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("identity", () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: "",
|
||||
expected: "string",
|
||||
},
|
||||
{
|
||||
input: {},
|
||||
expected: "object",
|
||||
},
|
||||
{
|
||||
input: 0,
|
||||
expected: "number",
|
||||
},
|
||||
{
|
||||
input: null,
|
||||
expected: "null",
|
||||
},
|
||||
{
|
||||
input: undefined,
|
||||
expected: "undefined",
|
||||
},
|
||||
];
|
||||
it.each(testCases)(
|
||||
"identity with $input = $expected",
|
||||
({ input, expected }) => {
|
||||
expect(Misc.identity(input)).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("flattenObjectDeep", () => {
|
||||
const testCases = [
|
||||
{
|
||||
obj: {
|
||||
a: {
|
||||
b: {
|
||||
c: 1,
|
||||
},
|
||||
},
|
||||
d: 2,
|
||||
e: [],
|
||||
},
|
||||
expected: {
|
||||
"a.b.c": 1,
|
||||
d: 2,
|
||||
e: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
obj: {
|
||||
a: {
|
||||
b: {
|
||||
c: 1,
|
||||
},
|
||||
},
|
||||
d: {
|
||||
e: {
|
||||
f: 2,
|
||||
g: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: {
|
||||
"a.b.c": 1,
|
||||
"d.e.f": 2,
|
||||
"d.e.g": 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
obj: {
|
||||
a: {
|
||||
b: {
|
||||
c: 1,
|
||||
d: {
|
||||
e: 2,
|
||||
f: 3,
|
||||
g: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: {
|
||||
"a.b.c": 1,
|
||||
"a.b.d.e": 2,
|
||||
"a.b.d.f": 3,
|
||||
"a.b.d.g": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
obj: {},
|
||||
expected: {},
|
||||
},
|
||||
];
|
||||
|
||||
it.each(testCases)(
|
||||
"flattenObjectDeep with $obj = $expected",
|
||||
({ obj, expected }) => {
|
||||
expect(Misc.flattenObjectDeep(obj)).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("sanitizeString", () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: "h̶̼͔̭͈̏́̀́͋͜ͅe̵̺̞̦̫̫͔̋́̅̅̃̀͝͝ļ̶̬̯͚͇̺͍̞̫̟͖͋̓͛̆̒̓͜ĺ̴̗̘͇̬̆͂͌̈͊͝͝ỡ̴̡̦̩̠̞̐̃͆̚͠͝",
|
||||
expected: "hello",
|
||||
},
|
||||
{
|
||||
input: "hello",
|
||||
expected: "hello",
|
||||
},
|
||||
{
|
||||
input: "hel lo",
|
||||
expected: "hel lo",
|
||||
},
|
||||
{
|
||||
input: " hel lo ",
|
||||
expected: "hel lo",
|
||||
},
|
||||
{
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
input: " \n\n\n",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
input: undefined,
|
||||
expected: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, expected }) => {
|
||||
expect(Misc.sanitizeString(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it("getOrdinalNumberString", () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: 0,
|
||||
output: "0th",
|
||||
},
|
||||
{
|
||||
input: 1,
|
||||
output: "1st",
|
||||
},
|
||||
{
|
||||
input: 2,
|
||||
output: "2nd",
|
||||
},
|
||||
{
|
||||
input: 3,
|
||||
output: "3rd",
|
||||
},
|
||||
{
|
||||
input: 4,
|
||||
output: "4th",
|
||||
},
|
||||
{
|
||||
input: 10,
|
||||
output: "10th",
|
||||
},
|
||||
{
|
||||
input: 11,
|
||||
output: "11th",
|
||||
},
|
||||
{
|
||||
input: 12,
|
||||
output: "12th",
|
||||
},
|
||||
{
|
||||
input: 13,
|
||||
output: "13th",
|
||||
},
|
||||
{
|
||||
input: 100,
|
||||
output: "100th",
|
||||
},
|
||||
{
|
||||
input: 101,
|
||||
output: "101st",
|
||||
},
|
||||
{
|
||||
input: 102,
|
||||
output: "102nd",
|
||||
},
|
||||
{
|
||||
input: 103,
|
||||
output: "103rd",
|
||||
},
|
||||
{
|
||||
input: 104,
|
||||
output: "104th",
|
||||
},
|
||||
{
|
||||
input: 93589423,
|
||||
output: "93589423rd",
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach(({ input, output }) => {
|
||||
expect(Misc.getOrdinalNumberString(input)).toEqual(output);
|
||||
});
|
||||
});
|
||||
it("formatSeconds", () => {
|
||||
const testCases = [
|
||||
{
|
||||
seconds: 5,
|
||||
expected: "5 seconds",
|
||||
},
|
||||
{
|
||||
seconds: 65,
|
||||
expected: "1.08 minutes",
|
||||
},
|
||||
{
|
||||
seconds: Misc.HOUR_IN_SECONDS,
|
||||
expected: "1 hour",
|
||||
},
|
||||
{
|
||||
seconds: Misc.DAY_IN_SECONDS,
|
||||
expected: "1 day",
|
||||
},
|
||||
{
|
||||
seconds: Misc.WEEK_IN_SECONDS,
|
||||
expected: "1 week",
|
||||
},
|
||||
{
|
||||
seconds: Misc.YEAR_IN_SECONDS,
|
||||
expected: "1 year",
|
||||
},
|
||||
{
|
||||
seconds: 2 * Misc.YEAR_IN_SECONDS,
|
||||
expected: "2 years",
|
||||
},
|
||||
{
|
||||
seconds: 4 * Misc.YEAR_IN_SECONDS,
|
||||
expected: "4 years",
|
||||
},
|
||||
{
|
||||
seconds: 3 * Misc.WEEK_IN_SECONDS,
|
||||
expected: "3 weeks",
|
||||
},
|
||||
{
|
||||
seconds: Misc.MONTH_IN_SECONDS * 4,
|
||||
expected: "4 months",
|
||||
},
|
||||
{
|
||||
seconds: Misc.MONTH_IN_SECONDS * 11,
|
||||
expected: "11 months",
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach(({ seconds, expected }) => {
|
||||
expect(Misc.formatSeconds(seconds)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("replaceObjectId", () => {
|
||||
it("replaces objecId with string", () => {
|
||||
const fromDatabase = {
|
||||
_id: new ObjectId(),
|
||||
test: "test",
|
||||
number: 1,
|
||||
};
|
||||
expect(Misc.replaceObjectId(fromDatabase)).toStrictEqual({
|
||||
_id: fromDatabase._id.toHexString(),
|
||||
test: "test",
|
||||
number: 1,
|
||||
});
|
||||
});
|
||||
it("ignores null values", () => {
|
||||
expect(Misc.replaceObjectId(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("replaceObjectIds", () => {
|
||||
it("replaces objecIds with string", () => {
|
||||
const fromDatabase = {
|
||||
_id: new ObjectId(),
|
||||
test: "test",
|
||||
number: 1,
|
||||
};
|
||||
const fromDatabase2 = {
|
||||
_id: new ObjectId(),
|
||||
test: "bob",
|
||||
number: 2,
|
||||
};
|
||||
expect(
|
||||
Misc.replaceObjectIds([fromDatabase, fromDatabase2]),
|
||||
).toStrictEqual([
|
||||
{
|
||||
_id: fromDatabase._id.toHexString(),
|
||||
test: "test",
|
||||
number: 1,
|
||||
},
|
||||
{
|
||||
_id: fromDatabase2._id.toHexString(),
|
||||
test: "bob",
|
||||
number: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("handles undefined", () => {
|
||||
expect(Misc.replaceObjectIds(undefined as any)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("omit()", () => {
|
||||
it("should omit a single key", () => {
|
||||
const input = { a: 1, b: 2, c: 3 };
|
||||
const result = Misc.omit(input, ["b"]);
|
||||
expect(result).toEqual({ a: 1, c: 3 });
|
||||
});
|
||||
|
||||
it("should omit multiple keys", () => {
|
||||
const input = { a: 1, b: 2, c: 3, d: 4 };
|
||||
const result = Misc.omit(input, ["a", "d"]);
|
||||
expect(result).toEqual({ b: 2, c: 3 });
|
||||
});
|
||||
|
||||
it("should return the same object if no keys are omitted", () => {
|
||||
const input = { x: 1, y: 2 };
|
||||
const result = Misc.omit(input, []);
|
||||
expect(result).toEqual({ x: 1, y: 2 });
|
||||
});
|
||||
|
||||
it("should not mutate the original object", () => {
|
||||
const input = { foo: "bar", baz: "qux" };
|
||||
const copy = { ...input };
|
||||
Misc.omit(input, ["baz"]);
|
||||
expect(input).toEqual(copy);
|
||||
});
|
||||
|
||||
it("should ignore keys that do not exist", () => {
|
||||
const input = { a: 1, b: 2 };
|
||||
const result = Misc.omit(input, "c" as any); // allow a non-existing key
|
||||
expect(result).toEqual({ a: 1, b: 2 });
|
||||
});
|
||||
|
||||
it("should work with different value types", () => {
|
||||
const input = {
|
||||
str: "hello",
|
||||
num: 123,
|
||||
bool: true,
|
||||
obj: { x: 1 },
|
||||
arr: [1, 2, 3],
|
||||
};
|
||||
const result = Misc.omit(input, ["bool", "arr"]);
|
||||
expect(result).toEqual({
|
||||
str: "hello",
|
||||
num: 123,
|
||||
obj: { x: 1 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPlainObject", () => {
|
||||
it("should return true for plain objects", () => {
|
||||
expect(Misc.isPlainObject({})).toBe(true);
|
||||
expect(Misc.isPlainObject({ a: 1, b: 2 })).toBe(true);
|
||||
expect(Misc.isPlainObject(Object.create(Object.prototype))).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for arrays", () => {
|
||||
expect(Misc.isPlainObject([])).toBe(false);
|
||||
expect(Misc.isPlainObject([1, 2, 3])).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for null", () => {
|
||||
expect(Misc.isPlainObject(null)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for primitives", () => {
|
||||
expect(Misc.isPlainObject(123)).toBe(false);
|
||||
expect(Misc.isPlainObject("string")).toBe(false);
|
||||
expect(Misc.isPlainObject(true)).toBe(false);
|
||||
expect(Misc.isPlainObject(undefined)).toBe(false);
|
||||
expect(Misc.isPlainObject(Symbol("sym"))).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for objects with different prototypes", () => {
|
||||
// oxlint-disable-next-line no-extraneous-class
|
||||
class MyClass {}
|
||||
expect(Misc.isPlainObject(new MyClass())).toBe(false);
|
||||
expect(Misc.isPlainObject(Object.create(null))).toBe(false);
|
||||
expect(Misc.isPlainObject(new Date())).toBe(false);
|
||||
expect(Misc.isPlainObject(new Map())).toBe(false);
|
||||
expect(Misc.isPlainObject(new Set())).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for functions", () => {
|
||||
// oxlint-disable-next-line no-empty-function
|
||||
expect(Misc.isPlainObject(function () {})).toBe(false);
|
||||
// oxlint-disable-next-line no-empty-function
|
||||
expect(Misc.isPlainObject(() => {})).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
21
backend/__tests__/utils/monkey-mail.spec.ts
Normal file
21
backend/__tests__/utils/monkey-mail.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { buildMonkeyMail } from "../../src/utils/monkey-mail";
|
||||
|
||||
describe("Monkey Mail", () => {
|
||||
it("should properly create a mail object", () => {
|
||||
const mailConfig = {
|
||||
subject: "",
|
||||
body: "",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const mail = buildMonkeyMail(mailConfig) as any;
|
||||
|
||||
expect(mail.id).toBeDefined();
|
||||
expect(mail.subject).toBe("");
|
||||
expect(mail.body).toBe("");
|
||||
expect(mail.timestamp).toBeDefined();
|
||||
expect(mail.read).toBe(false);
|
||||
expect(mail.rewards).toEqual([]);
|
||||
});
|
||||
});
|
||||
213
backend/__tests__/utils/pb.spec.ts
Normal file
213
backend/__tests__/utils/pb.spec.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import * as pb from "../../src/utils/pb";
|
||||
import { Mode, PersonalBests } from "@monkeytype/schemas/shared";
|
||||
import { Result } from "@monkeytype/schemas/results";
|
||||
import { FunboxName } from "@monkeytype/schemas/configs";
|
||||
|
||||
describe("Pb Utils", () => {
|
||||
describe("funboxCatGetPb", () => {
|
||||
const testCases: { funbox: FunboxName[] | undefined; expected: boolean }[] =
|
||||
[
|
||||
{
|
||||
funbox: ["plus_one"],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
funbox: [],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
funbox: undefined,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
funbox: ["nausea", "plus_one"],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
funbox: ["arrows"],
|
||||
expected: false,
|
||||
},
|
||||
];
|
||||
|
||||
it.each(testCases)(
|
||||
"canFunboxGetPb with $funbox = $expected",
|
||||
({ funbox, expected }) => {
|
||||
const result = pb.canFunboxGetPb({ funbox } as any);
|
||||
expect(result).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("checkAndUpdatePb", () => {
|
||||
it("should update personal best", () => {
|
||||
const userPbs: PersonalBests = {
|
||||
time: {},
|
||||
words: {},
|
||||
custom: {},
|
||||
quote: {},
|
||||
zen: {},
|
||||
};
|
||||
const result = {
|
||||
difficulty: "normal",
|
||||
language: "english",
|
||||
punctuation: false,
|
||||
lazyMode: false,
|
||||
acc: 100,
|
||||
consistency: 100,
|
||||
rawWpm: 100,
|
||||
wpm: 110,
|
||||
numbers: false,
|
||||
mode: "time",
|
||||
mode2: "15",
|
||||
} as unknown as Result<Mode>;
|
||||
|
||||
const run = pb.checkAndUpdatePb(
|
||||
userPbs,
|
||||
{} as pb.LbPersonalBests,
|
||||
result,
|
||||
);
|
||||
|
||||
expect(run.isPb).toBe(true);
|
||||
expect(run.personalBests.time?.["15"]?.[0]).not.toBe(undefined);
|
||||
expect(run.lbPersonalBests).not.toBe({});
|
||||
});
|
||||
it("should not override default pb when saving numbers test", () => {
|
||||
const userPbs: PersonalBests = {
|
||||
time: {
|
||||
"15": [
|
||||
{
|
||||
acc: 100,
|
||||
consistency: 100,
|
||||
difficulty: "normal",
|
||||
lazyMode: false,
|
||||
language: "english",
|
||||
numbers: false,
|
||||
punctuation: false,
|
||||
raw: 100,
|
||||
timestamp: 0,
|
||||
wpm: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
words: {},
|
||||
custom: {},
|
||||
quote: {},
|
||||
zen: {},
|
||||
};
|
||||
const result = {
|
||||
difficulty: "normal",
|
||||
language: "english",
|
||||
punctuation: false,
|
||||
lazyMode: false,
|
||||
acc: 100,
|
||||
consistency: 100,
|
||||
rawWpm: 100,
|
||||
wpm: 110,
|
||||
numbers: true,
|
||||
mode: "time",
|
||||
mode2: "15",
|
||||
} as unknown as Result<Mode>;
|
||||
|
||||
const run = pb.checkAndUpdatePb(userPbs, undefined, result);
|
||||
|
||||
expect(run.isPb).toBe(true);
|
||||
|
||||
expect(run.personalBests.time?.["15"]).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ numbers: false, wpm: 100 }),
|
||||
expect.objectContaining({ numbers: true, wpm: 110 }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("updateLeaderboardPersonalBests", () => {
|
||||
const userPbs: PersonalBests = {
|
||||
time: {
|
||||
"15": [
|
||||
{
|
||||
acc: 100,
|
||||
consistency: 100,
|
||||
difficulty: "normal",
|
||||
lazyMode: false,
|
||||
language: "english",
|
||||
numbers: false,
|
||||
punctuation: false,
|
||||
raw: 100,
|
||||
timestamp: 0,
|
||||
wpm: 100,
|
||||
},
|
||||
{
|
||||
acc: 100,
|
||||
consistency: 100,
|
||||
difficulty: "normal",
|
||||
lazyMode: false,
|
||||
language: "spanish",
|
||||
numbers: false,
|
||||
punctuation: false,
|
||||
raw: 100,
|
||||
timestamp: 0,
|
||||
wpm: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
words: {},
|
||||
custom: {},
|
||||
quote: {},
|
||||
zen: {},
|
||||
};
|
||||
it("should update leaderboard personal bests if they dont exist or the structure is incomplete", () => {
|
||||
const lbpbstartingvalues = [
|
||||
undefined,
|
||||
{},
|
||||
{ time: {} },
|
||||
{ time: { "15": {} } },
|
||||
{ time: { "15": { english: {} } } },
|
||||
];
|
||||
|
||||
const result15 = {
|
||||
mode: "time",
|
||||
mode2: "15",
|
||||
} as unknown as Result<Mode>;
|
||||
|
||||
for (const lbPb of lbpbstartingvalues) {
|
||||
const lbPbPb = pb.updateLeaderboardPersonalBests(
|
||||
userPbs,
|
||||
structuredClone(lbPb) as pb.LbPersonalBests,
|
||||
result15,
|
||||
);
|
||||
|
||||
expect(lbPbPb).toEqual({
|
||||
time: {
|
||||
"15": {
|
||||
english: {
|
||||
acc: 100,
|
||||
consistency: 100,
|
||||
difficulty: "normal",
|
||||
lazyMode: false,
|
||||
language: "english",
|
||||
numbers: false,
|
||||
punctuation: false,
|
||||
raw: 100,
|
||||
timestamp: 0,
|
||||
wpm: 100,
|
||||
},
|
||||
spanish: {
|
||||
acc: 100,
|
||||
consistency: 100,
|
||||
difficulty: "normal",
|
||||
lazyMode: false,
|
||||
language: "spanish",
|
||||
numbers: false,
|
||||
punctuation: false,
|
||||
raw: 100,
|
||||
timestamp: 0,
|
||||
wpm: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
220
backend/__tests__/utils/result.spec.ts
Normal file
220
backend/__tests__/utils/result.spec.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { replaceLegacyValues, DBResult } from "../../src/utils/result";
|
||||
|
||||
describe("Result Utils", () => {
|
||||
describe("replaceLegacyValues", () => {
|
||||
describe("legacy charStats conversion", () => {
|
||||
it.each([
|
||||
{
|
||||
description:
|
||||
"should convert correctChars and incorrectChars to charStats",
|
||||
correctChars: 95,
|
||||
incorrectChars: 5,
|
||||
expectedCharStats: [95, 5, 0, 0],
|
||||
},
|
||||
{
|
||||
description: "should handle zero values for legacy chars",
|
||||
correctChars: 0,
|
||||
incorrectChars: 0,
|
||||
expectedCharStats: [0, 0, 0, 0],
|
||||
},
|
||||
{
|
||||
description: "should handle large values for legacy chars",
|
||||
correctChars: 9999,
|
||||
incorrectChars: 1234,
|
||||
expectedCharStats: [9999, 1234, 0, 0],
|
||||
},
|
||||
])(
|
||||
"$description",
|
||||
({ correctChars, incorrectChars, expectedCharStats }) => {
|
||||
const resultWithLegacyChars: DBResult = {
|
||||
correctChars,
|
||||
incorrectChars,
|
||||
} as any;
|
||||
|
||||
const result = replaceLegacyValues(resultWithLegacyChars);
|
||||
|
||||
expect(result.charStats).toEqual(expectedCharStats);
|
||||
expect(result.correctChars).toBeUndefined();
|
||||
expect(result.incorrectChars).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it("should prioritise charStats when legacy data exists", () => {
|
||||
const resultWithBothFormats: DBResult = {
|
||||
charStats: [80, 4, 2, 1],
|
||||
correctChars: 95,
|
||||
incorrectChars: 5,
|
||||
} as any;
|
||||
|
||||
const result = replaceLegacyValues(resultWithBothFormats);
|
||||
|
||||
// Should convert legacy values and overwrite existing charStats
|
||||
expect(result.charStats).toEqual([80, 4, 2, 1]);
|
||||
// Legacy values should be removed after conversion
|
||||
expect(result.correctChars).toBeUndefined();
|
||||
expect(result.incorrectChars).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
description:
|
||||
"should not convert when only one legacy property is present",
|
||||
input: { correctChars: 95 },
|
||||
expectedCharStats: undefined,
|
||||
expectedCorrectChars: 95,
|
||||
expectedIncorrectChars: undefined,
|
||||
},
|
||||
{
|
||||
description: "should not convert when only incorrectChars is present",
|
||||
input: { incorrectChars: 5 },
|
||||
expectedCharStats: undefined,
|
||||
expectedCorrectChars: undefined,
|
||||
expectedIncorrectChars: 5,
|
||||
},
|
||||
])(
|
||||
"$description",
|
||||
({
|
||||
input,
|
||||
expectedCharStats,
|
||||
expectedCorrectChars,
|
||||
expectedIncorrectChars,
|
||||
}) => {
|
||||
const result = replaceLegacyValues(input as any);
|
||||
|
||||
// Should not convert since both properties are required
|
||||
expect(result.charStats).toBe(expectedCharStats);
|
||||
expect(result.correctChars).toBe(expectedCorrectChars);
|
||||
expect(result.incorrectChars).toBe(expectedIncorrectChars);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("legacy funbox conversion", () => {
|
||||
it.each([
|
||||
{
|
||||
description: "should convert string funbox to array",
|
||||
input: "memory#mirror",
|
||||
expected: ["memory", "mirror"],
|
||||
},
|
||||
{
|
||||
description: "should convert single funbox string to array",
|
||||
input: "memory",
|
||||
expected: ["memory"],
|
||||
},
|
||||
{
|
||||
description: "should convert 'none' funbox to empty array",
|
||||
input: "none",
|
||||
expected: [],
|
||||
},
|
||||
{
|
||||
description: "should handle complex funbox combinations",
|
||||
input: "memory#mirror#arrows#58008",
|
||||
expected: ["memory", "mirror", "arrows", "58008"],
|
||||
},
|
||||
])("$description", ({ input, expected }) => {
|
||||
const resultWithStringFunbox: DBResult = {
|
||||
funbox: input as any,
|
||||
} as any;
|
||||
|
||||
const result = replaceLegacyValues(resultWithStringFunbox);
|
||||
|
||||
expect(result.funbox).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("legacy chartData conversion", () => {
|
||||
it("should convert chartData with 'raw' property to 'burst'", () => {
|
||||
const resultWithLegacyChartData: DBResult = {
|
||||
chartData: {
|
||||
wpm: [50, 55, 60],
|
||||
raw: [52, 57, 62],
|
||||
err: [1, 0, 2],
|
||||
} as any,
|
||||
} as any;
|
||||
|
||||
const result = replaceLegacyValues(resultWithLegacyChartData);
|
||||
|
||||
expect(result.chartData).toEqual({
|
||||
wpm: [50, 55, 60],
|
||||
burst: [52, 57, 62],
|
||||
err: [1, 0, 2],
|
||||
});
|
||||
});
|
||||
|
||||
it("should not convert chartData when it's 'toolong'", () => {
|
||||
const resultWithToolongChartData: DBResult = {
|
||||
chartData: "toolong",
|
||||
} as any;
|
||||
|
||||
const result = replaceLegacyValues(resultWithToolongChartData);
|
||||
|
||||
expect(result.chartData).toBe("toolong");
|
||||
});
|
||||
|
||||
it("should not convert chartData when it doesn't have 'raw' property", () => {
|
||||
const resultWithModernChartData: DBResult = {
|
||||
chartData: {
|
||||
wpm: [50, 55, 60],
|
||||
burst: [52, 57, 62],
|
||||
err: [1, 0, 2],
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = replaceLegacyValues(resultWithModernChartData);
|
||||
|
||||
expect(result.chartData).toEqual({
|
||||
wpm: [50, 55, 60],
|
||||
burst: [52, 57, 62],
|
||||
err: [1, 0, 2],
|
||||
});
|
||||
});
|
||||
|
||||
it("should not convert chartData when it's undefined", () => {
|
||||
const resultWithoutChartData: DBResult = {} as any;
|
||||
|
||||
const result = replaceLegacyValues(resultWithoutChartData);
|
||||
|
||||
expect(result.chartData).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should convert all legacy data at once", () => {
|
||||
const resultWithAllLegacy: DBResult = {
|
||||
correctChars: 100,
|
||||
incorrectChars: 8,
|
||||
funbox: "memory#mirror" as any,
|
||||
chartData: {
|
||||
wpm: [50, 55, 60],
|
||||
raw: [52, 57, 62],
|
||||
err: [1, 0, 2],
|
||||
} as any,
|
||||
} as any;
|
||||
|
||||
const result = replaceLegacyValues(resultWithAllLegacy);
|
||||
|
||||
expect(result.charStats).toEqual([100, 8, 0, 0]);
|
||||
expect(result.correctChars).toBeUndefined();
|
||||
expect(result.incorrectChars).toBeUndefined();
|
||||
expect(result.funbox).toEqual(["memory", "mirror"]);
|
||||
expect(result.chartData).toEqual({
|
||||
wpm: [50, 55, 60],
|
||||
burst: [52, 57, 62],
|
||||
err: [1, 0, 2],
|
||||
});
|
||||
});
|
||||
|
||||
describe("no legacy values", () => {
|
||||
it("should return result unchanged when no legacy values present", () => {
|
||||
const modernResult: DBResult = {
|
||||
charStats: [95, 5, 2, 1],
|
||||
funbox: ["memory"],
|
||||
} as any;
|
||||
|
||||
const result = replaceLegacyValues(modernResult);
|
||||
|
||||
expect(result).toEqual(modernResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
55
backend/__tests__/utils/validation.spec.ts
Normal file
55
backend/__tests__/utils/validation.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import * as Validation from "../../src/utils/validation";
|
||||
|
||||
describe("Validation", () => {
|
||||
it("isTestTooShort", () => {
|
||||
const testCases = [
|
||||
{
|
||||
result: {
|
||||
mode: "time",
|
||||
mode2: 10,
|
||||
customText: undefined,
|
||||
testDuration: 10,
|
||||
bailedOut: false,
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
result: {
|
||||
mode: "time",
|
||||
mode2: 15,
|
||||
customText: undefined,
|
||||
testDuration: 15,
|
||||
bailedOut: false,
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
result: {
|
||||
mode: "time",
|
||||
mode2: 0,
|
||||
customText: undefined,
|
||||
testDuration: 20,
|
||||
bailedOut: false,
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
result: {
|
||||
mode: "time",
|
||||
mode2: 0,
|
||||
customText: undefined,
|
||||
testDuration: 2,
|
||||
bailedOut: false,
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
expect(Validation.isTestTooShort(testCase.result as any)).toBe(
|
||||
testCase.expected,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user