From de060b381e8fc291555239673835c01bd0d408a6 Mon Sep 17 00:00:00 2001 From: Johannes Loher Date: Sun, 13 Feb 2022 18:34:03 +0100 Subject: [PATCH 1/3] refactor: improve testing setup --- .eslintrc.cjs | 8 ++++++ jest.config.js | 8 ++++-- spec/localization/localization.spec.ts | 8 ++---- spec/rolls/check-evaluation.spec.ts | 24 +++-------------- spec/setup.ts | 27 +++++++++++++++++++ spec/{tsconfig.spec.json => tsconfig.json} | 0 ...pec.json.license => tsconfig.json.license} | 0 tsconfig.json | 3 ++- 8 files changed, 48 insertions(+), 30 deletions(-) create mode 100644 spec/setup.ts rename spec/{tsconfig.spec.json => tsconfig.json} (100%) rename spec/{tsconfig.spec.json.license => tsconfig.json.license} (100%) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 2802a161..a0729fae 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -29,5 +29,13 @@ module.exports = { "@typescript-eslint/no-var-requires": "off", }, }, + { + files: ["./spec/**/*"], + env: { + browser: false, + }, + extends: ["plugin:jest/recommended"], + plugins: ["jest"], + }, ], }; diff --git a/jest.config.js b/jest.config.js index 3e74f815..7d3bde02 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,11 +2,15 @@ // // SPDX-License-Identifier: MIT -export default { +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +const config = { preset: "ts-jest", globals: { "ts-jest": { - tsconfig: "/spec/tsconfig.spec.json", + tsconfig: "/spec/tsconfig.json", }, }, + setupFiles: ["/spec/setup.ts"], }; + +export default config; diff --git a/spec/localization/localization.spec.ts b/spec/localization/localization.spec.ts index 4c62e45a..a050cf71 100644 --- a/spec/localization/localization.spec.ts +++ b/spec/localization/localization.spec.ts @@ -2,14 +2,10 @@ // // SPDX-License-Identifier: MIT -import * as fs from "fs-extra"; -import * as path from "path"; +import en from "../../lang/en.json"; +import de from "../../lang/de.json"; describe("English and german localization files", () => { - const localizationPath = "./lang/"; - const en: Record = fs.readJSONSync(path.join(localizationPath, "en.json")); - const de: Record = fs.readJSONSync(path.join(localizationPath, "de.json")); - it("should have the same keys.", () => { const deKeys = Object.keys(de); const enKeys = Object.keys(en); diff --git a/spec/rolls/check-evaluation.spec.ts b/spec/rolls/check-evaluation.spec.ts index bde1ddbf..5e5e954c 100644 --- a/spec/rolls/check-evaluation.spec.ts +++ b/spec/rolls/check-evaluation.spec.ts @@ -5,39 +5,21 @@ import evaluateCheck from "../../src/rolls/check-evaluation"; -class StubGame { - constructor() { - this.i18n = { - localize: (key: string) => key, - }; - } - i18n: { - localize: (key: string) => string; - }; -} - -const game = new StubGame(); - -Object.defineProperties(globalThis, { - game: { value: game }, - Game: { value: StubGame }, -}); - describe("evaluateCheck with no dice", () => { it("should throw an error.", () => { - expect(() => evaluateCheck([], 10)).toThrow("DS4.ErrorInvalidNumberOfDice"); + expect(() => evaluateCheck([], 10)).toThrow("Invalid number of dice."); }); }); describe("evaluateCheck with more dice than required by the checkTargetNumber", () => { it("should throw an error.", () => { - expect(() => evaluateCheck([10, 10], 10)).toThrow("DS4.ErrorInvalidNumberOfDice"); + expect(() => evaluateCheck([10, 10], 10)).toThrow("Invalid number of dice."); }); }); describe("evaluateCheck with less dice than required by the checkTargetNumber", () => { it("should throw an error.", () => { - expect(() => evaluateCheck([10], 21)).toThrow("DS4.ErrorInvalidNumberOfDice"); + expect(() => evaluateCheck([10], 21)).toThrow("Invalid number of dice."); }); }); diff --git a/spec/setup.ts b/spec/setup.ts new file mode 100644 index 00000000..9b10aba6 --- /dev/null +++ b/spec/setup.ts @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2022 Johannes Loher +// +// SPDX-License-Identifier: MIT + +import en from "../lang/en.json"; + +function setupStubs() { + class StubGame { + constructor() { + this.i18n = { + localize: (key: string) => (key in en ? en[key as keyof typeof en] : key), + }; + } + i18n: { + localize: (key: string) => string; + }; + } + + const game = new StubGame(); + + Object.defineProperties(globalThis, { + game: { value: game }, + Game: { value: StubGame }, + }); +} + +setupStubs(); diff --git a/spec/tsconfig.spec.json b/spec/tsconfig.json similarity index 100% rename from spec/tsconfig.spec.json rename to spec/tsconfig.json diff --git a/spec/tsconfig.spec.json.license b/spec/tsconfig.json.license similarity index 100% rename from spec/tsconfig.spec.json.license rename to spec/tsconfig.json.license diff --git a/tsconfig.json b/tsconfig.json index b8fbff11..7a7a51ce 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,8 @@ "moduleResolution": "node", "forceConsistentCasingInFileNames": true, "strict": true, - "noUncheckedIndexedAccess": true + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true }, "include": ["src"] } From 666b61ec09aed73efb01c4ceb79b76b07ca05a81 Mon Sep 17 00:00:00 2001 From: Johannes Loher Date: Sun, 13 Feb 2022 18:35:15 +0100 Subject: [PATCH 2/3] test: add tests for calculating the spell price --- spec/item/type-specific-helpers/spell.spec.ts | 231 ++++++++++++++++++ spec/setup.ts | 30 +++ 2 files changed, 261 insertions(+) create mode 100644 spec/item/type-specific-helpers/spell.spec.ts diff --git a/spec/item/type-specific-helpers/spell.spec.ts b/spec/item/type-specific-helpers/spell.spec.ts new file mode 100644 index 00000000..9b520e5f --- /dev/null +++ b/spec/item/type-specific-helpers/spell.spec.ts @@ -0,0 +1,231 @@ +// SPDX-FileCopyrightText: 2022 Johannes Loher +// +// SPDX-License-Identifier: MIT + +import { DS4SpellDataSourceData, TemporalUnit, UnitData } from "../../../src/item/item-data-source"; +import { calculateSpellPrice } from "../../../src/item/type-specific-helpers/spell"; + +const defaultData: DS4SpellDataSourceData = { + description: "", + equipped: false, + spellType: "spellcasting", + bonus: "", + spellCategory: "unset", + maxDistance: { + value: "", + unit: "meter", + }, + effectRadius: { + value: "", + unit: "meter", + }, + duration: { + value: "", + unit: "custom", + }, + cooldownDuration: { + value: "", + unit: "rounds", + }, + minimumLevels: { + healer: null, + wizard: null, + sorcerer: null, + }, +}; + +type TestCase = { + minimumLevel: number | null; + expected: number | null; +}; + +type CombinedTestCase = { + minimumLevels: DS4SpellDataSourceData["minimumLevels"]; + expected: number | null; +}; + +const testCases: Record = { + healer: [ + { minimumLevel: null, expected: null }, + { minimumLevel: 1, expected: 10 }, + { minimumLevel: 2, expected: 45 }, + { minimumLevel: 3, expected: 80 }, + { minimumLevel: 4, expected: 115 }, + { minimumLevel: 5, expected: 150 }, + { minimumLevel: 6, expected: 185 }, + { minimumLevel: 7, expected: 220 }, + { minimumLevel: 8, expected: 255 }, + { minimumLevel: 9, expected: 290 }, + { minimumLevel: 10, expected: 325 }, + { minimumLevel: 11, expected: 360 }, + { minimumLevel: 12, expected: 395 }, + { minimumLevel: 13, expected: 430 }, + { minimumLevel: 14, expected: 465 }, + { minimumLevel: 15, expected: 500 }, + { minimumLevel: 16, expected: 535 }, + { minimumLevel: 17, expected: 570 }, + { minimumLevel: 18, expected: 605 }, + { minimumLevel: 19, expected: 640 }, + { minimumLevel: 20, expected: 675 }, + ], + sorcerer: [ + { minimumLevel: null, expected: null }, + { minimumLevel: 1, expected: 10 }, + { minimumLevel: 2, expected: 75 }, + { minimumLevel: 3, expected: 140 }, + { minimumLevel: 4, expected: 205 }, + { minimumLevel: 5, expected: 270 }, + { minimumLevel: 6, expected: 335 }, + { minimumLevel: 7, expected: 400 }, + { minimumLevel: 8, expected: 465 }, + { minimumLevel: 9, expected: 530 }, + { minimumLevel: 10, expected: 595 }, + { minimumLevel: 11, expected: 660 }, + { minimumLevel: 12, expected: 725 }, + { minimumLevel: 13, expected: 790 }, + { minimumLevel: 14, expected: 855 }, + { minimumLevel: 15, expected: 920 }, + { minimumLevel: 16, expected: 985 }, + { minimumLevel: 17, expected: 1050 }, + { minimumLevel: 18, expected: 1115 }, + { minimumLevel: 19, expected: 1180 }, + { minimumLevel: 20, expected: 1245 }, + ], + wizard: [ + { minimumLevel: null, expected: null }, + { minimumLevel: 1, expected: 10 }, + { minimumLevel: 2, expected: 60 }, + { minimumLevel: 3, expected: 110 }, + { minimumLevel: 4, expected: 160 }, + { minimumLevel: 5, expected: 210 }, + { minimumLevel: 6, expected: 260 }, + { minimumLevel: 7, expected: 310 }, + { minimumLevel: 8, expected: 360 }, + { minimumLevel: 9, expected: 410 }, + { minimumLevel: 10, expected: 460 }, + { minimumLevel: 11, expected: 510 }, + { minimumLevel: 12, expected: 560 }, + { minimumLevel: 13, expected: 610 }, + { minimumLevel: 14, expected: 660 }, + { minimumLevel: 15, expected: 710 }, + { minimumLevel: 16, expected: 760 }, + { minimumLevel: 17, expected: 810 }, + { minimumLevel: 18, expected: 860 }, + { minimumLevel: 19, expected: 910 }, + { minimumLevel: 20, expected: 960 }, + ], +}; + +function buildCombinedTestCases(): CombinedTestCase[] { + const combinedTestCases: CombinedTestCase[] = []; + + // permutation test cases + const isRelevantPermutationTestCase = (t: TestCase) => + ([null, 1, 10, 20] as (number | null)[]).includes(t.minimumLevel); + + for (const healerTestCase of testCases.healer.filter(isRelevantPermutationTestCase)) { + for (const sorcererTestCase of testCases.sorcerer.filter(isRelevantPermutationTestCase)) { + for (const wizardTestCase of testCases.wizard.filter(isRelevantPermutationTestCase)) { + const expected = + healerTestCase.expected !== null || + sorcererTestCase.expected !== null || + wizardTestCase.expected !== null + ? Math.min( + healerTestCase.expected ?? Infinity, + sorcererTestCase.expected ?? Infinity, + wizardTestCase.expected ?? Infinity, + ) + : null; + combinedTestCases.push({ + minimumLevels: { + healer: healerTestCase.minimumLevel, + sorcerer: sorcererTestCase.minimumLevel, + wizard: wizardTestCase.minimumLevel, + }, + expected, + }); + } + } + } + + // single test cases + const isRelevantSingleTestCase = (t: TestCase) => t.minimumLevel !== null; + + for (const spellCasterClass of ["healer", "sorcerer", "wizard"] as const) { + for (const testCase of testCases[spellCasterClass].filter(isRelevantSingleTestCase)) { + combinedTestCases.push({ + minimumLevels: { + ...defaultData.minimumLevels, + [spellCasterClass]: testCase.minimumLevel, + }, + expected: testCase.expected, + }); + } + } + + return combinedTestCases; +} + +describe("calculateSpellPrice", () => { + const cooldownDurations: (UnitData & { factor: number })[] = [ + { value: "", unit: "rounds", factor: 1 }, + { value: "foo", unit: "rounds", factor: 1 }, + { value: "0", unit: "rounds", factor: 1 }, + { value: "1", unit: "rounds", factor: 1 }, + { value: "17279", unit: "rounds", factor: 1 }, + { value: "17280", unit: "rounds", factor: 2 }, + { value: "34559", unit: "rounds", factor: 2 }, + { value: "34560", unit: "rounds", factor: 3 }, + + { value: "", unit: "minutes", factor: 1 }, + { value: "foo", unit: "minutes", factor: 1 }, + { value: "0", unit: "minutes", factor: 1 }, + { value: "1", unit: "minutes", factor: 1 }, + { value: "1439", unit: "minutes", factor: 1 }, + { value: "1440", unit: "minutes", factor: 2 }, + { value: "2879", unit: "minutes", factor: 2 }, + { value: "2880", unit: "minutes", factor: 3 }, + + { value: "", unit: "hours", factor: 1 }, + { value: "foo", unit: "hours", factor: 1 }, + { value: "0", unit: "hours", factor: 1 }, + { value: "1", unit: "hours", factor: 1 }, + { value: "23", unit: "hours", factor: 1 }, + { value: "24", unit: "hours", factor: 2 }, + { value: "47", unit: "hours", factor: 2 }, + { value: "48", unit: "hours", factor: 3 }, + + { value: "", unit: "days", factor: 3 }, + { value: "foo", unit: "days", factor: 3 }, + { value: "0", unit: "days", factor: 1 }, + { value: "1", unit: "days", factor: 2 }, + { value: "2", unit: "days", factor: 3 }, + ]; + + describe.each(cooldownDurations)("with cooldown duration set to $value $unit", ({ value, unit, factor }) => { + const dataWithCooldownDuration = { + ...defaultData, + cooldownDuration: { + value, + unit, + }, + }; + + it.each(buildCombinedTestCases())( + `returns ${factor} × $expected if the minimum leves are $minimumLevels`, + ({ minimumLevels, expected }) => { + // given + const data: DS4SpellDataSourceData = { + ...dataWithCooldownDuration, + minimumLevels, + }; + + // when + const spellPrice = calculateSpellPrice(data); + + // then + expect(spellPrice).toBe(expected !== null ? expected * factor : expected); + }, + ); + }); +}); diff --git a/spec/setup.ts b/spec/setup.ts index 9b10aba6..d96ad725 100644 --- a/spec/setup.ts +++ b/spec/setup.ts @@ -4,6 +4,35 @@ import en from "../lang/en.json"; +function setupPrimitives() { + Object.defineProperties(Number, { + isNumeric: { + value: function (n: unknown) { + if (n instanceof Array) return false; + else if (([null, ""] as unknown[]).includes(n)) return false; + // @ts-expect-error Abusing JavaScript a bit here, but it's the implementation from foundry + return +n === +n; + }, + }, + fromString: { + value: function (str: unknown) { + if (typeof str !== "string" || !str.length) return NaN; + // Remove whitespace. + str = str.replace(/\s+/g, ""); + return Number(str); + }, + }, + }); + + Object.defineProperties(Math, { + clamped: { + value: function (num: number, min: number, max: number) { + return Math.min(max, Math.max(num, min)); + }, + }, + }); +} + function setupStubs() { class StubGame { constructor() { @@ -24,4 +53,5 @@ function setupStubs() { }); } +setupPrimitives(); setupStubs(); From 76f578a3fa8ed65ea13904eebd9076190ae493c3 Mon Sep 17 00:00:00 2001 From: Johannes Loher Date: Sun, 13 Feb 2022 18:37:25 +0100 Subject: [PATCH 3/3] fix: don't use an input to display the spell price as it's not editable --- templates/sheets/item/components/properties/spell.hbs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/templates/sheets/item/components/properties/spell.hbs b/templates/sheets/item/components/properties/spell.hbs index bfeba19e..ea788cd9 100644 --- a/templates/sheets/item/components/properties/spell.hbs +++ b/templates/sheets/item/components/properties/spell.hbs @@ -106,7 +106,6 @@ SPDX-License-Identifier: MIT
- + {{data.data.price}}