Repository: oliverschwendener/ueli
Pull request #1215 (2024-09-23 13:27:33)

test: currency conversion
This PR adds tests for the CurrencyConversion extension


Patch:
diff --git a/src/main/Extensions/CurrencyConversion/CurrencyConversion.ts b/src/main/Extensions/CurrencyConversion/CurrencyConversion.ts
index 71627863..9bc25d23 100644
--- a/src/main/Extensions/CurrencyConversion/CurrencyConversion.ts
+++ b/src/main/Extensions/CurrencyConversion/CurrencyConversion.ts
@@ -1,10 +1,12 @@
-import type { AssetPathResolver } from "@Core/AssetPathResolver";
-import type { Extension } from "@Core/Extension";
-import type { SettingsManager } from "@Core/SettingsManager";
 import { SearchResultItemActionUtility, type SearchResultItem } from "@common/Core";
 import { getExtensionSettingKey } from "@common/Core/Extension";
 import type { Image } from "@common/Core/Image";
+import type { AssetPathResolver } from "@Core/AssetPathResolver";
+import type { Extension } from "@Core/Extension";
+import type { SettingsManager } from "@Core/SettingsManager";
 import type { Net } from "electron";
+import { convert } from "./convert";
+import type { Rates } from "./Rates";
 
 export class CurrencyConversion implements Extension {
     private static readonly translationNamespace = "extension[CurrencyConversion]";
@@ -26,7 +28,7 @@ export class CurrencyConversion implements Extension {
         currencies: ["usd", "chf", "eur"],
     };
 
-    private readonly rates: Record<string, Record<string, number>>;
+    private rates: Rates;
 
     public constructor(
         private readonly settingsManager: SettingsManager,
@@ -53,16 +55,16 @@ export class CurrencyConversion implements Extension {
             }
         }
 
-        const conversionResult = this.convert({
-            value: Number(parts[0]),
-            base: parts[1],
-            target: parts[3],
-        });
+        const value = Number(parts[0]);
+        const base = parts[1];
+        const target = parts[3];
+
+        const conversionResult = convert({ value, base, target, rates: this.rates });
 
         return [
             {
                 defaultAction: SearchResultItemActionUtility.createCopyToClipboardAction({
-                    textToCopy: conversionResult.toFixed(2),
+                    textToCopy: conversionResult.result.toFixed(2),
                     description: "Currency Conversion",
                     descriptionTranslation: {
                         key: "copyToClipboard",
@@ -76,7 +78,7 @@ export class CurrencyConversion implements Extension {
                 },
                 id: `currency-conversion:instant-result`,
                 image: this.getImage(),
-                name: `${conversionResult.toFixed(2)} ${parts[3].toUpperCase()}`,
+                name: `${conversionResult.result.toFixed(2)} ${parts[3].toUpperCase()}`,
             },
         ];
     }
@@ -123,10 +125,6 @@ export class CurrencyConversion implements Extension {
         return [getExtensionSettingKey(this.id, "currencies")];
     }
 
-    private convert({ value, base, target }: { value: number; base: string; target: string }): number {
-        return value * this.rates[base.toLowerCase()][target.toLowerCase()];
-    }
-
     private async setRates(): Promise<void> {
         const currencies = this.settingsManager.getValue(
             getExtensionSettingKey(this.id, "currencies"),
diff --git a/src/main/Extensions/CurrencyConversion/Rates.ts b/src/main/Extensions/CurrencyConversion/Rates.ts
new file mode 100644
index 00000000..33823d6a
--- /dev/null
+++ b/src/main/Extensions/CurrencyConversion/Rates.ts
@@ -0,0 +1,1 @@
+export type Rates = Record<string, Record<string, number>>;
diff --git a/src/main/Extensions/CurrencyConversion/convert.ts b/src/main/Extensions/CurrencyConversion/convert.ts
new file mode 100644
index 00000000..77823449
--- /dev/null
+++ b/src/main/Extensions/CurrencyConversion/convert.ts
@@ -0,0 +1,26 @@
+import type { Rates } from "./Rates";
+
+export const convert = ({
+    value,
+    base,
+    target,
+    rates,
+}: {
+    value: number;
+    base: string;
+    target: string;
+    rates: Rates;
+}): { result: number; target: string } => {
+    if (!Object.keys(rates).includes(base.toLowerCase())) {
+        throw new Error("Base currency not found");
+    }
+
+    if (!Object.keys(rates[base.toLowerCase()]).includes(target.toLowerCase())) {
+        throw new Error("Target currency not found");
+    }
+
+    return {
+        result: value * rates[base.toLowerCase()][target.toLowerCase()],
+        target,
+    };
+};


Test patch:
diff --git a/src/main/Extensions/CurrencyConversion/CurrencyConversion.test.ts b/src/main/Extensions/CurrencyConversion/CurrencyConversion.test.ts
new file mode 100644
index 00000000..00003489
--- /dev/null
+++ b/src/main/Extensions/CurrencyConversion/CurrencyConversion.test.ts
@@ -0,0 +1,84 @@
+import type { AssetPathResolver } from "@Core/AssetPathResolver";
+import { describe, expect, it, vi } from "vitest";
+import { CurrencyConversion } from "./CurrencyConversion";
+import type { Rates } from "./Rates";
+
+describe(CurrencyConversion, () => {
+    describe(CurrencyConversion.prototype.getInstantSearchResultItems, () => {
+        const testSuccessfulConversion = ({
+            expectedResult,
+            userInput,
+            rates,
+        }: {
+            expectedResult: string;
+            userInput: string;
+            rates: Rates;
+        }) => {
+            const imageFilePath = "/path/to/image";
+            const getExtensionAssetPathMock = vi.fn().mockReturnValue(imageFilePath);
+
+            const assetPathResolver = <AssetPathResolver>{
+                getExtensionAssetPath: (i, f) => getExtensionAssetPathMock(i, f),
+                getModuleAssetPath: () => null,
+            };
+
+            const currencyConversion = new CurrencyConversion(null, null, assetPathResolver);
+
+            currencyConversion["rates"] = rates;
+
+            const actual = currencyConversion.getInstantSearchResultItems(userInput);
+
+            expect(actual[0].name).toEqual(expectedResult);
+            expect(actual[0].image).toEqual({ url: `file://${imageFilePath}` });
+            expect(getExtensionAssetPathMock).toHaveBeenCalledWith(currencyConversion.id, "currency-conversion.png");
+        };
+
+        it("should return empty array when user input does not contain 4 parts", () => {
+            const currencyConversion = new CurrencyConversion(null, null, null);
+
+            currencyConversion["rates"] = { chf: { usd: 2 } };
+
+            expect(currencyConversion.getInstantSearchResultItems("1 CHF to")).toEqual([]);
+        });
+
+        it("should return empty array when first part in user input is not numerical", () => {
+            const currencyConversion = new CurrencyConversion(null, null, null);
+
+            currencyConversion["rates"] = { chf: { usd: 2 } };
+
+            expect(currencyConversion.getInstantSearchResultItems("abc CHF to USD")).toEqual([]);
+        });
+
+        it("should return empty array when base currency does not exist", () => {
+            const currencyConversion = new CurrencyConversion(null, null, null);
+
+            currencyConversion["rates"] = { chf: { usd: 2 } };
+
+            expect(currencyConversion.getInstantSearchResultItems("1 EUR to USD")).toEqual([]);
+        });
+
+        it("should return empty array when second part of user input is not 'in' or 'to'", () => {
+            const currencyConversion = new CurrencyConversion(null, null, null);
+
+            currencyConversion["rates"] = { chf: { usd: 2 } };
+
+            expect(currencyConversion.getInstantSearchResultItems("1 CHF inn USD")).toEqual([]);
+            expect(currencyConversion.getInstantSearchResultItems("1 CHF t USD")).toEqual([]);
+        });
+
+        it("should return empty array when target currency is not found", () => {
+            const currencyConversion = new CurrencyConversion(null, null, null);
+
+            currencyConversion["rates"] = { chf: { usd: 2 } };
+
+            expect(currencyConversion.getInstantSearchResultItems("1 CHF to EUR")).toEqual([]);
+        });
+
+        it("should convert currencies based on the rates when user input matches expected pattern", () => {
+            const rates: Rates = { chf: { usd: 2, eur: 0.5 } };
+
+            testSuccessfulConversion({ expectedResult: "2.00 USD", userInput: "1 CHF to USD", rates });
+            testSuccessfulConversion({ expectedResult: "0.50 EUR", userInput: "1 CHF in EUR", rates });
+        });
+    });
+});
diff --git a/src/main/Extensions/CurrencyConversion/convert.test.ts b/src/main/Extensions/CurrencyConversion/convert.test.ts
new file mode 100644
index 00000000..9098c724
--- /dev/null
+++ b/src/main/Extensions/CurrencyConversion/convert.test.ts
@@ -0,0 +1,33 @@
+import { describe, expect, it } from "vitest";
+import { convert } from "./convert";
+import type { Rates } from "./Rates";
+
+describe(convert, () => {
+    it("should convert the base currency to the target according to the rates", () => {
+        const rates: Rates = {
+            chf: {
+                usd: 2,
+                eur: 0.5,
+            },
+            usd: {
+                chf: 0.5,
+                eur: 0.25,
+            },
+        };
+
+        expect(convert({ base: "CHF", rates, target: "USD", value: 1 })).toEqual({ result: 2, target: "USD" });
+        expect(convert({ base: "CHF", rates, target: "EUR", value: 10 })).toEqual({ result: 5, target: "EUR" });
+    });
+
+    it("should throw an error if the base currency is not found", () => {
+        expect(() => convert({ base: "CHF", rates: { usd: { chf: 0.5 } }, target: "USD", value: 1 })).toThrowError(
+            "Base currency not found",
+        );
+    });
+
+    it("should throw an error if the target currency is not found", () => {
+        expect(() => convert({ base: "CHF", rates: { chf: { eur: 0.5 } }, target: "USD", value: 1 })).toThrowError(
+            "Target currency not found",
+        );
+    });
+});