diff --git a/README.md b/README.md index 2d01aaa..16e854f 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,57 @@ You will have access to the core component file, which means you can edit the wh > โš ๏ธ **Note:** Avoid reinstalling existing components when prompted, as this will reset any custom styling in the component files. +## ๐Ÿงช Testing + +The project includes a comprehensive test suite using Bun's native test runner for utility functions and business logic. + +### Running Tests + +```bash +# Run all tests +bun test + +# Run tests in watch mode +bun test --watch + +# Run specific test file +bun test tests/lib/utils/erg-converter.test.ts + +# Run with coverage +bun test --coverage +``` + +### Test Coverage + +**Current Status**: โœ… 168/168 tests passing (100% success rate) + +- **Utility Functions** (`src/lib/utils`): + - `utils.ts` - className merging (8 tests) + - `erg-converter.ts` - ERG conversion & formatting (60 tests) + - `error-handler.ts` - Error classification (36 tests) + - `node-service.ts` - Blockchain API service (15 tests) + - `transaction-listener.ts` - Transaction monitoring (12 tests) + +- **Business Logic** (`src/lib/functions/reactor`): + - `utils.ts` - Token validation & swap actions (37 tests) + +### Writing Tests + +Test files are located in the `/tests` directory, mirroring the source structure. See [tests/README.md](tests/README.md) for detailed documentation on: +- Test structure and patterns +- Writing new tests +- Mocking strategies +- Best practices + +### What's Not Tested Yet + +- React components (UI, layout, blockchain components) +- User interaction flows +- Integration tests +- Visual regression tests + +Contributions for component tests are welcome! + ## ๐Ÿ“ Folder Structure The project follows a modular approach. Components or functions that will be used multiple times should be designed to accept various props or be moved to separate files. @@ -102,24 +153,34 @@ The project follows a modular approach. Components or functions that will be use ### Directory Overview ``` -src/ -โ”œโ”€โ”€ lib/ -โ”‚ โ”œโ”€โ”€ components/ # All React components -โ”‚ โ”‚ โ”œโ”€โ”€ ui/ # Shadcn UI components -โ”‚ โ”‚ โ”œโ”€โ”€ blocks/ # Page-specific sections (dashboard, home) -โ”‚ โ”‚ โ”œโ”€โ”€ layout/ # Layout components (navbar, sidebar, SEO) -โ”‚ โ”‚ โ”œโ”€โ”€ blockchain/ # Wallet and protocol integrations -โ”‚ โ”‚ โ””โ”€โ”€ icons/ # Custom icon components -โ”‚ โ”œโ”€โ”€ constants/ # Application constants and addresses -โ”‚ โ”œโ”€โ”€ functions/ # Business logic and protocol functions -โ”‚ โ”œโ”€โ”€ hooks/ # Custom React hooks -โ”‚ โ”œโ”€โ”€ providers/ # Context providers (Ergo, theme) -โ”‚ โ”œโ”€โ”€ services/ # External service integrations -โ”‚ โ”œโ”€โ”€ stores/ # State management (Zustand) -โ”‚ โ”œโ”€โ”€ types/ # TypeScript type definitions -โ”‚ โ””โ”€โ”€ utils/ # Utility functions and helpers -โ”œโ”€โ”€ pages/ # Next.js pages and API routes -โ””โ”€โ”€ styles/ # Global styles and CSS + +โ”œโ”€โ”€ public/ # Static assets (images, logos, fonts) +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ lib/ +โ”‚ โ”‚ โ”œโ”€โ”€ components/ # All React components +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ui/ # Shadcn UI components +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ blocks/ # Page-specific sections (dashboard, home) +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ layout/ # Layout components (navbar, sidebar, SEO) +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ blockchain/ # Wallet and protocol integrations +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ icons/ # Custom icon components +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ toggle/ # Theme toggle components +โ”‚ โ”‚ โ”œโ”€โ”€ constants/ # Application constants and token addresses +โ”‚ โ”‚ โ”œโ”€โ”€ functions/ # Business logic and protocol functions +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ reactor/ # Reactor swap logic (fission, fusion, transmutation) +โ”‚ โ”‚ โ”œโ”€โ”€ providers/ # Context providers (Ergo, theme) +โ”‚ โ”‚ โ””โ”€โ”€ utils/ # Utility functions and helpers +โ”‚ โ”œโ”€โ”€ pages/ # Next.js pages and API routes +โ”‚ โ”‚ โ”œโ”€โ”€ _app.tsx # App wrapper with providers +โ”‚ โ”‚ โ”œโ”€โ”€ _document.tsx # HTML document structure +โ”‚ โ”‚ โ”œโ”€โ”€ index.tsx # Home page +โ”‚ โ”‚ โ””โ”€โ”€ dashboard.tsx # Dashboard page +โ”‚ โ””โ”€โ”€ styles/ # Global styles and CSS +โ””โ”€โ”€ tests/ # Test files (mirrors src structure) + โ”œโ”€โ”€ setup.ts # Global test configuration & mocks + โ””โ”€โ”€ lib/ + โ”œโ”€โ”€ utils/ # Utility function tests (168 tests) + โ””โ”€โ”€ functions/ # Business logic tests + โ””โ”€โ”€ reactor/ # Reactor function tests ``` ### Component Organization Guidelines diff --git a/bun.lockb b/bun.lockb index a7ffd1e..0d61ab7 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 5c416a9..a09004f 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,15 @@ "start": "next start", "lint": "next lint", "build:sdk": "cd lib/gluon-gold-sdk && bun install && bun run build", - "test": "vitest", + "test": "bun test", + "test:watch": "bun test --watch", + "test:coverage": "bun test --coverage", "format": "prettier --write .", "prepare": "husky" }, - "workspaces": ["lib/*"], + "workspaces": [ + "lib/*" + ], "dependencies": { "@fleet-sdk/core": "^0.8.1", "@radix-ui/react-dialog": "^1.1.4", @@ -58,6 +62,7 @@ "@nautilus-js/eip12-types": "^0.1.11", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.3.0", + "@types/bun": "^1.3.3", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -80,4 +85,4 @@ "lint-staged": { "*.{js,jsx,ts,tsx,json,css,md}": "prettier --write" } -} +} \ No newline at end of file diff --git a/tests/lib/functions/reactor/utils.test.ts b/tests/lib/functions/reactor/utils.test.ts new file mode 100644 index 0000000..04ef52e --- /dev/null +++ b/tests/lib/functions/reactor/utils.test.ts @@ -0,0 +1,268 @@ +import { describe, expect, test } from "bun:test"; +import BigNumber from "bignumber.js"; +import { + getValidToTokens, + getActionType, + getDescription, + getTitle, + validateAmount, + formatValue, + defaultTokens, + VALID_PAIRS, +} from "@/lib/functions/reactor/utils"; +import type { Token, TokenSymbol } from "@/lib/functions/reactor/types"; + +describe("VALID_PAIRS", () => { + test("should have correct valid pairs", () => { + expect(VALID_PAIRS).toHaveLength(4); + expect(VALID_PAIRS).toContainEqual({ from: "ERG", to: "GAU-GAUC" }); + expect(VALID_PAIRS).toContainEqual({ from: "GAU-GAUC", to: "ERG" }); + expect(VALID_PAIRS).toContainEqual({ from: "GAU", to: "GAUC" }); + expect(VALID_PAIRS).toContainEqual({ from: "GAUC", to: "GAU" }); + }); +}); + +describe("defaultTokens", () => { + test("should have 4 default tokens", () => { + expect(defaultTokens).toHaveLength(4); + }); + + test("should include ERG token", () => { + const erg = defaultTokens.find(t => t.symbol === "ERG"); + expect(erg).toBeDefined(); + expect(erg?.name).toBe("Ergo"); + expect(erg?.decimals).toBe(9); + }); + + test("should include GAU token", () => { + const gau = defaultTokens.find(t => t.symbol === "GAU"); + expect(gau).toBeDefined(); + expect(gau?.name).toBe("Gluon Gold"); + expect(gau?.decimals).toBe(9); + }); + + test("should include GAUC token", () => { + const gauc = defaultTokens.find(t => t.symbol === "GAUC"); + expect(gauc).toBeDefined(); + expect(gauc?.name).toBe("Gluon Gold Certificate"); + expect(gauc?.decimals).toBe(9); + }); + + test("should include GAU-GAUC pair token", () => { + const pair = defaultTokens.find(t => t.symbol === "GAU-GAUC"); + expect(pair).toBeDefined(); + expect(pair?.name).toBe("Gluon Pair"); + expect(pair?.decimals).toBe(9); + }); +}); + +describe("getValidToTokens", () => { + test("should return GAU-GAUC for ERG", () => { + const result = getValidToTokens("ERG", defaultTokens); + expect(result).toHaveLength(1); + expect(result[0].symbol).toBe("GAU-GAUC"); + }); + + test("should return ERG for GAU-GAUC", () => { + const result = getValidToTokens("GAU-GAUC", defaultTokens); + expect(result).toHaveLength(1); + expect(result[0].symbol).toBe("ERG"); + }); + + test("should return GAUC for GAU", () => { + const result = getValidToTokens("GAU", defaultTokens); + expect(result).toHaveLength(1); + expect(result[0].symbol).toBe("GAUC"); + }); + + test("should return GAU for GAUC", () => { + const result = getValidToTokens("GAUC", defaultTokens); + expect(result).toHaveLength(1); + expect(result[0].symbol).toBe("GAU"); + }); + + test("should return empty array for invalid symbol", () => { + const result = getValidToTokens("INVALID" as TokenSymbol, defaultTokens); + expect(result).toHaveLength(0); + }); +}); + +describe("getActionType", () => { + test("should return erg-to-gau-gauc for ERG to GAU-GAUC", () => { + const result = getActionType("ERG", "GAU-GAUC"); + expect(result).toBe("erg-to-gau-gauc"); + }); + + test("should return gau-gauc-to-erg for GAU-GAUC to ERG", () => { + const result = getActionType("GAU-GAUC", "ERG"); + expect(result).toBe("gau-gauc-to-erg"); + }); + + test("should return gauc-to-gau for GAUC to GAU", () => { + const result = getActionType("GAUC", "GAU"); + expect(result).toBe("gauc-to-gau"); + }); + + test("should return gau-to-gauc for GAU to GAUC", () => { + const result = getActionType("GAU", "GAUC"); + expect(result).toBe("gau-to-gauc"); + }); + + test("should return null for invalid pair", () => { + const result = getActionType("ERG", "GAU"); + expect(result).toBeNull(); + }); + + test("should return null for same token", () => { + const result = getActionType("ERG", "ERG"); + expect(result).toBeNull(); + }); +}); + +describe("getDescription", () => { + test("should return fission description for erg-to-gau-gauc", () => { + const result = getDescription("erg-to-gau-gauc"); + expect(result).toContain("fission"); + expect(result).toContain("ERG"); + expect(result).toContain("GAU"); + expect(result).toContain("GAUC"); + }); + + test("should return fusion description for gau-gauc-to-erg", () => { + const result = getDescription("gau-gauc-to-erg"); + expect(result).toContain("fusion"); + expect(result).toContain("GAU"); + expect(result).toContain("GAUC"); + expect(result).toContain("ERG"); + }); + + test("should return transmutation description for gauc-to-gau", () => { + const result = getDescription("gauc-to-gau"); + expect(result).toContain("transmutation"); + expect(result).toContain("GAUC"); + expect(result).toContain("GAU"); + }); + + test("should return transmutation description for gau-to-gauc", () => { + const result = getDescription("gau-to-gauc"); + expect(result).toContain("transmutation"); + expect(result).toContain("GAU"); + expect(result).toContain("GAUC"); + }); + + test("should return default description for null", () => { + const result = getDescription(null); + expect(result).toBe("Select tokens to swap."); + }); +}); + +describe("getTitle", () => { + test("should return FISSION for erg-to-gau-gauc", () => { + const result = getTitle("erg-to-gau-gauc"); + expect(result).toBe("FISSION"); + }); + + test("should return FUSION for gau-gauc-to-erg", () => { + const result = getTitle("gau-gauc-to-erg"); + expect(result).toBe("FUSION"); + }); + + test("should return TRANSMUTATION for gauc-to-gau", () => { + const result = getTitle("gauc-to-gau"); + expect(result).toBe("TRANSMUTATION"); + }); + + test("should return TRANSMUTATION for gau-to-gauc", () => { + const result = getTitle("gau-to-gauc"); + expect(result).toBe("TRANSMUTATION"); + }); + + test("should return REACTOR for null", () => { + const result = getTitle(null); + expect(result).toBe("REACTOR"); + }); +}); + +describe("validateAmount", () => { + test("should return error for empty string", () => { + const result = validateAmount(""); + expect(result).toBe("Amount cannot be empty"); + }); + + test("should return error for invalid number", () => { + const result = validateAmount("abc"); + expect(result).toBe("Invalid number"); + }); + + test("should return error for zero", () => { + const result = validateAmount("0"); + expect(result).toBe("Amount must be greater than 0"); + }); + + test("should return error for negative number", () => { + const result = validateAmount("-5"); + expect(result).toBe("Amount must be greater than 0"); + }); + + test("should return null for valid positive number", () => { + const result = validateAmount("10"); + expect(result).toBeNull(); + }); + + test("should return null for valid decimal number", () => { + const result = validateAmount("10.5"); + expect(result).toBeNull(); + }); + + test("should return null for very small positive number", () => { + const result = validateAmount("0.000001"); + expect(result).toBeNull(); + }); + + test("should return null for very large number", () => { + const result = validateAmount("1000000000"); + expect(result).toBeNull(); + }); +}); + +describe("formatValue", () => { + test("should return 0 for undefined", () => { + const result = formatValue(undefined); + expect(result).toBe("0"); + }); + + test("should return 0 for NaN", () => { + const result = formatValue(NaN); + expect(result).toBe("0"); + }); + + test("should format number correctly", () => { + const result = formatValue(123.456); + expect(result).toBe("123.456"); + }); + + test("should format BigNumber correctly", () => { + const result = formatValue(new BigNumber("123.456")); + expect(result).toBe("123.456"); + }); + + test("should format zero", () => { + const result = formatValue(0); + expect(result).toBe("0"); + }); + + test("should format negative number", () => { + const result = formatValue(-123.456); + expect(result).toBe("-123.456"); + }); + + test("should format very large number", () => { + const result = formatValue(1000000000); + expect(result).toBe("1000000000"); + }); + + test("should format very small number", () => { + const result = formatValue(0.000000001); + expect(result).toBe("1e-9"); + }); +}); diff --git a/tests/lib/utils/erg-converter.test.ts b/tests/lib/utils/erg-converter.test.ts new file mode 100644 index 0000000..1b47658 --- /dev/null +++ b/tests/lib/utils/erg-converter.test.ts @@ -0,0 +1,291 @@ +import { describe, expect, test } from "bun:test"; +import BigNumber from "bignumber.js"; +import { + convertFromDecimals, + convertToDecimals, + formatMacroNumber, + formatMicroNumber, + formatApprox, + format, + formatNumber, + nanoErgsToErgs, + ergsToNanoErgs, + UIFriendlyValue, + APIFriendlyValue, +} from "@/lib/utils/erg-converter"; + +describe("convertFromDecimals", () => { + test("should convert from decimals with default 9 decimals", () => { + const result = convertFromDecimals(1000000000); + expect(result.toString()).toBe("1"); + }); + + test("should convert from decimals with custom decimals", () => { + const result = convertFromDecimals(1000, 3); + expect(result.toString()).toBe("1"); + }); + + test("should handle bigint input", () => { + const result = convertFromDecimals(BigInt(5000000000)); + expect(result.toString()).toBe("5"); + }); + + test("should handle string input", () => { + const result = convertFromDecimals("2000000000"); + expect(result.toString()).toBe("2"); + }); + + test("should handle zero", () => { + const result = convertFromDecimals(0); + expect(result.toString()).toBe("0"); + }); + + test("should handle very large numbers", () => { + const result = convertFromDecimals("1000000000000000000"); // 1 billion ERG + expect(result.toString()).toBe("1000000000"); + }); +}); + +describe("convertToDecimals", () => { + test("should convert to decimals with default 9 decimals", () => { + const result = convertToDecimals(1); + expect(result.toString()).toBe("1000000000"); + }); + + test("should convert to decimals with custom decimals", () => { + const result = convertToDecimals(1, 3); + expect(result.toString()).toBe("1000"); + }); + + test("should handle string input", () => { + const result = convertToDecimals("2.5"); + expect(result.toString()).toBe("2500000000"); + }); + + test("should handle zero", () => { + const result = convertToDecimals(0); + expect(result.toString()).toBe("0"); + }); + + test("should handle empty string as zero", () => { + const result = convertToDecimals(""); + expect(result.toString()).toBe("0"); + }); + + test("should handle null/undefined as zero", () => { + const result1 = convertToDecimals(null as any); + const result2 = convertToDecimals(undefined as any); + expect(result1.toString()).toBe("0"); + expect(result2.toString()).toBe("0"); + }); + + test("should handle NaN as zero", () => { + const result = convertToDecimals(NaN); + expect(result.toString()).toBe("0"); + }); + + test("should round down decimal values", () => { + const result = convertToDecimals("1.123456789123"); + expect(result.toString()).toBe("1123456789"); + }); +}); + +describe("formatMacroNumber", () => { + test("should format zero", () => { + const result = formatMacroNumber(0); + expect(result.display).toBe("0"); + expect(result.tooltip).toBe("0"); + expect(result.raw).toBe("0"); + }); + + test("should format numbers less than 1000", () => { + const result = formatMacroNumber(500); + expect(result.display).toBe("500"); + }); + + test("should format thousands with K suffix", () => { + const result = formatMacroNumber(5000); + expect(result.display).toBe("5K"); + }); + + test("should format millions with M suffix", () => { + const result = formatMacroNumber(5000000); + expect(result.display).toBe("5M"); + }); + + test("should format billions with B suffix", () => { + const result = formatMacroNumber(5000000000); + expect(result.display).toBe("5B"); + }); + + test("should format trillions with T suffix", () => { + const result = formatMacroNumber(5000000000000); + expect(result.display).toBe("5T"); + }); + + test("should round down to 2 decimal places", () => { + const result = formatMacroNumber(1234567); + expect(result.display).toBe("1.23M"); + }); + + test("should handle BigNumber input", () => { + const result = formatMacroNumber(new BigNumber(1500000)); + expect(result.display).toBe("1.5M"); + }); + + test("should handle string input", () => { + const result = formatMacroNumber("2500000"); + expect(result.display).toBe("2.5M"); + }); + + test("should provide tooltip with formatted number", () => { + const result = formatMacroNumber(1234567); + expect(result.tooltip).toBe("1,234,567"); + }); +}); + +describe("formatMicroNumber", () => { + test("should format zero", () => { + const result = formatMicroNumber(0); + expect(result.display).toBe("0"); + }); + + test("should format whole numbers", () => { + const result = formatMicroNumber(100); + expect(result.display).toBe("100"); + }); + + test("should format decimal numbers", () => { + const result = formatMicroNumber(1.5); + expect(result.display).toBe("1.5"); + }); + + test("should remove trailing zeros", () => { + const result = formatMicroNumber(1.5000); + expect(result.display).toBe("1.5"); + }); + + test("should handle up to 9 decimal places", () => { + const result = formatMicroNumber(1.123456789); + expect(result.display).toBe("1.123456789"); + }); + + test("should truncate beyond 9 decimal places", () => { + const result = formatMicroNumber(1.123456789123); + expect(result.display).toBe("1.123456789"); + }); + + test("should handle very small numbers", () => { + const result = formatMicroNumber(0.000000001); + expect(result.display).toBe("0.000000001"); + }); + + test("should handle BigNumber input", () => { + const result = formatMicroNumber(new BigNumber("1.23456")); + expect(result.display).toBe("1.23456"); + }); +}); + +describe("formatApprox", () => { + test("should return macro formatted display value", () => { + const result = formatApprox(5000000); + expect(result).toBe("5M"); + }); +}); + +describe("format", () => { + test("should return micro formatted display value", () => { + const result = format(1.5); + expect(result).toBe("1.5"); + }); +}); + +describe("formatNumber", () => { + test("should use micro formatting by default", () => { + const result = formatNumber(1.5); + expect(result.display).toBe("1.5"); + }); + + test("should use macro formatting when isMacro is true", () => { + const result = formatNumber(5000000, true); + expect(result.display).toBe("5M"); + }); +}); + +describe("nanoErgsToErgs", () => { + test("should convert nanoErgs to Ergs", () => { + const result = nanoErgsToErgs(1000000000); + expect(result.toString()).toBe("1"); + }); + + test("should handle bigint input", () => { + const result = nanoErgsToErgs(BigInt(5000000000)); + expect(result.toString()).toBe("5"); + }); + + test("should handle string input", () => { + const result = nanoErgsToErgs("2000000000"); + expect(result.toString()).toBe("2"); + }); + + test("should handle zero", () => { + const result = nanoErgsToErgs(0); + expect(result.toString()).toBe("0"); + }); +}); + +describe("ergsToNanoErgs", () => { + test("should convert Ergs to nanoErgs", () => { + const result = ergsToNanoErgs(1); + expect(result.toString()).toBe("1000000000"); + }); + + test("should handle string input", () => { + const result = ergsToNanoErgs("2.5"); + expect(result.toString()).toBe("2500000000"); + }); + + test("should handle zero", () => { + const result = ergsToNanoErgs(0); + expect(result.toString()).toBe("0"); + }); + + test("should round down decimal values", () => { + const result = ergsToNanoErgs("1.123456789123"); + expect(result.toString()).toBe("1123456789"); + }); +}); + +describe("UIFriendlyValue", () => { + test("should convert with default 9 decimals", () => { + const result = UIFriendlyValue(1000000000); + expect(result.toString()).toBe("1"); + }); + + test("should convert with custom divisor", () => { + const result = UIFriendlyValue(1000, 3); + expect(result.toString()).toBe("1"); + }); + + test("should handle bigint input", () => { + const result = UIFriendlyValue(BigInt(5000000000)); + expect(result.toString()).toBe("5"); + }); +}); + +describe("APIFriendlyValue", () => { + test("should convert with default 9 decimals", () => { + const result = APIFriendlyValue(1); + expect(result.toString()).toBe("1000000000"); + }); + + test("should convert with custom divisor", () => { + const result = APIFriendlyValue(1, 3); + expect(result.toString()).toBe("1000"); + }); + + test("should handle string input", () => { + const result = APIFriendlyValue("2.5"); + expect(result.toString()).toBe("2500000000"); + }); +}); diff --git a/tests/lib/utils/error-handler.test.ts b/tests/lib/utils/error-handler.test.ts new file mode 100644 index 0000000..56396f3 --- /dev/null +++ b/tests/lib/utils/error-handler.test.ts @@ -0,0 +1,236 @@ +import { describe, expect, test } from "bun:test"; +import { + ErrorType, + handleTransactionError, + handleCalculationError, + handleInitializationError, +} from "@/lib/utils/error-handler"; + +describe("Error Classification", () => { + test("should classify network errors", () => { + const error = new Error("Network connection failed"); + const result = handleTransactionError(error, "test", false); + expect(result.type).toBe(ErrorType.NETWORK); + }); + + test("should classify insufficient balance errors", () => { + const error = new Error("Insufficient balance to complete transaction"); + const result = handleTransactionError(error, "test", false); + expect(result.type).toBe(ErrorType.INSUFFICIENT_BALANCE); + }); + + test("should classify wallet connection errors", () => { + const error = new Error("Wallet not connected"); + const result = handleTransactionError(error, "test", false); + expect(result.type).toBe(ErrorType.WALLET_CONNECTION); + }); + + test("should classify wallet signing errors", () => { + const error = new Error("User rejected the transaction"); + const result = handleTransactionError(error, "test", false); + expect(result.type).toBe(ErrorType.WALLET_SIGNING); + }); + + test("should classify transaction creation errors", () => { + const error = new Error("Failed to create transaction"); + const result = handleTransactionError(error, "test", false); + expect(result.type).toBe(ErrorType.TRANSACTION_CREATION); + }); + + test("should classify UTXO validation errors", () => { + const error = new Error("Malformed transaction: every input should be in UTXO"); + const result = handleTransactionError(error, "test", false); + expect(result.type).toBe(ErrorType.UTXO_VALIDATION); + }); + + test("should classify oracle errors", () => { + const error = new Error("Oracle price feed unavailable"); + const result = handleTransactionError(error, "test", false); + expect(result.type).toBe(ErrorType.ORACLE_ERROR); + }); + + test("should classify invalid amount errors", () => { + const error = new Error("Amount must be greater than zero"); + const result = handleTransactionError(error, "test", false); + expect(result.type).toBe(ErrorType.INVALID_AMOUNT); + }); + + test("should classify SDK errors", () => { + const error = new Error("Fusion will need more tokens"); + const result = handleTransactionError(error, "test", false); + expect(result.type).toBe(ErrorType.SDK_ERROR); + }); + + test("should default to UNKNOWN for unrecognized errors", () => { + const error = new Error("Some random error"); + const result = handleTransactionError(error, "test", false); + expect(result.type).toBe(ErrorType.UNKNOWN); + }); +}); + +describe("Wallet Error Code Handling", () => { + test("should handle wallet error code 2 (user rejected)", () => { + const error = { code: 2, info: "User rejected." }; + const result = handleTransactionError(error, "test", false); + expect(result.type).toBe(ErrorType.WALLET_SIGNING); + expect(result.message).toBe("User rejected."); + }); + + test("should handle wallet error code 1 (connection error)", () => { + const error = { code: 1, info: "Wallet connection failed" }; + const result = handleTransactionError(error, "test", false); + expect(result.type).toBe(ErrorType.WALLET_CONNECTION); + }); + + test("should handle wallet error code 3 (insufficient funds)", () => { + const error = { code: 3, info: "Insufficient funds" }; + const result = handleTransactionError(error, "test", false); + expect(result.type).toBe(ErrorType.INSUFFICIENT_BALANCE); + }); + + test("should handle wallet error code 4 (transaction creation)", () => { + const error = { code: 4, info: "Transaction creation failed" }; + const result = handleTransactionError(error, "test", false); + expect(result.type).toBe(ErrorType.TRANSACTION_CREATION); + }); + + test("should handle wallet error code 5 (network error)", () => { + const error = { code: 5, info: "Network error" }; + const result = handleTransactionError(error, "test", false); + expect(result.type).toBe(ErrorType.NETWORK); + }); + + test("should handle unknown wallet error codes", () => { + const error = { code: 999, info: "Unknown error" }; + const result = handleTransactionError(error, "test", false); + expect(result.type).toBe(ErrorType.UNKNOWN); + }); +}); + +describe("handleTransactionError", () => { + test("should handle Error objects", () => { + const error = new Error("Test error message"); + const result = handleTransactionError(error, "test", false); + expect(result.message).toBe("Test error message"); + expect(result.technicalMessage).toContain("Test error message"); + }); + + test("should handle string errors", () => { + const error = "String error message"; + const result = handleTransactionError(error, "test", false); + expect(result.message).toBe("String error message"); + expect(result.technicalMessage).toBe("String error message"); + }); + + test("should handle wallet error objects", () => { + const error = { code: 2, info: "User cancelled" }; + const result = handleTransactionError(error, "test", false); + expect(result.message).toBe("User cancelled"); + expect(result.technicalMessage).toContain("Code: 2"); + }); + + test("should customize message for fission insufficient balance", () => { + const error = new Error("Insufficient balance"); + const result = handleTransactionError(error, "fission", false); + expect(result.userMessage).toContain("fission"); + expect(result.userMessage).toContain("ERG"); + }); + + test("should customize message for fusion insufficient balance", () => { + const error = new Error("Insufficient balance"); + const result = handleTransactionError(error, "fusion", false); + expect(result.userMessage).toContain("fusion"); + expect(result.userMessage).toContain("GAU/GAUC"); + }); + + test("should customize message for transmutation insufficient balance", () => { + const error = new Error("Insufficient balance"); + const result = handleTransactionError(error, "transmutation", false); + expect(result.userMessage).toContain("transmutation"); + }); + + test("should include action type in error details", () => { + const error = new Error("Test error"); + const result = handleTransactionError(error, "custom-action", false); + expect(result.actionType).toBe("custom-action"); + }); + + test("should return user-friendly messages", () => { + const error = new Error("Network connection failed"); + const result = handleTransactionError(error, "test", false); + expect(result.userMessage).toBe("Network connection error. Please check your internet connection and try again."); + }); +}); + +describe("handleCalculationError", () => { + test("should handle calculation errors", () => { + const error = new Error("Division by zero"); + const result = handleCalculationError(error, "fusion", false); + expect(result.type).toBe(ErrorType.CALCULATION_ERROR); + expect(result.actionType).toBe("fusion"); + }); + + test("should provide calculation-specific user message", () => { + const error = new Error("Invalid calculation"); + const result = handleCalculationError(error, "fission", false); + expect(result.userMessage).toContain("calculate"); + expect(result.userMessage).toContain("fission"); + }); + + test("should handle wallet error objects in calculations", () => { + const error = { code: 2, info: "User rejected" }; + const result = handleCalculationError(error, "test", false); + expect(result.message).toBe("User rejected"); + }); + + test("should handle string errors in calculations", () => { + const error = "Calculation failed"; + const result = handleCalculationError(error, "test", false); + expect(result.message).toBe("Calculation failed"); + }); +}); + +describe("handleInitializationError", () => { + test("should handle initialization errors", () => { + const error = new Error("Failed to initialize oracle"); + const result = handleInitializationError(error, "oracle", false); + expect(result.actionType).toBe("oracle initialization"); + }); + + test("should provide component-specific context", () => { + const error = new Error("Init failed"); + const result = handleInitializationError(error, "wallet", false); + expect(result.actionType).toContain("wallet"); + }); + + test("should handle network errors during initialization", () => { + const error = new Error("Network timeout during initialization"); + const result = handleInitializationError(error, "system", false); + expect(result.type).toBe(ErrorType.NETWORK); + }); +}); + +describe("Error Message Handling", () => { + test("should handle errors with message property", () => { + const error = { message: "Custom error message" }; + const result = handleTransactionError(error, "test", false); + expect(result.message).toBe("Custom error message"); + }); + + test("should handle errors with info property", () => { + const error = { info: "Error info message" }; + const result = handleTransactionError(error, "test", false); + expect(result.message).toBe("Error info message"); + }); + + test("should handle unknown error objects", () => { + const error = { someProperty: "value" }; + const result = handleTransactionError(error, "test", false); + expect(result.message).toBe("Wallet error occurred"); + }); + + test("should handle null/undefined errors", () => { + const result = handleTransactionError(null, "test", false); + expect(result.message).toBe("An unknown error occurred"); + }); +}); diff --git a/tests/lib/utils/node-service.test.ts b/tests/lib/utils/node-service.test.ts new file mode 100644 index 0000000..c9ca269 --- /dev/null +++ b/tests/lib/utils/node-service.test.ts @@ -0,0 +1,259 @@ +import { describe, expect, test, mock, beforeEach } from "bun:test"; +import { NodeService } from "@/lib/utils/node-service"; + +// Mock axios +const mockAxios = { + create: mock(() => mockAxiosInstance), + get: mock(), + post: mock(), + head: mock(), +}; + +const mockAxiosInstance = { + get: mock((url: string) => Promise.resolve({ data: { success: true } })), + post: mock((url: string, data: any) => Promise.resolve({ data: { success: true } })), + head: mock((url: string) => Promise.resolve({ status: 200 })), + defaults: { + headers: {}, + timeout: 2000, + }, +}; + +mock.module("axios", () => ({ + default: mockAxios, +})); + +describe("NodeService", () => { + let nodeService: NodeService; + const testUrl = "https://test-node.ergoplatform.com"; + + beforeEach(() => { + // Reset mocks before each test + mockAxiosInstance.get.mockClear(); + mockAxiosInstance.post.mockClear(); + mockAxiosInstance.head.mockClear(); + + nodeService = new NodeService(testUrl); + }); + + describe("Constructor", () => { + test("should create NodeService instance with URL", () => { + expect(nodeService).toBeDefined(); + }); + }); + + describe("get method", () => { + test("should make GET request", async () => { + const mockData = { height: 1000000 }; + mockAxiosInstance.get.mockResolvedValueOnce({ data: mockData }); + + const result = await nodeService.get("/info"); + + expect(mockAxiosInstance.get).toHaveBeenCalled(); + expect(result).toEqual(mockData); + }); + + test("should pass headers to GET request", async () => { + const headers = { "Custom-Header": "value" }; + mockAxiosInstance.get.mockResolvedValueOnce({ data: {} }); + + await nodeService.get("/info", headers); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + "/info", + expect.objectContaining({ + headers: expect.objectContaining(headers), + }) + ); + }); + + test("should pass params to GET request", async () => { + const params = { limit: 10 }; + mockAxiosInstance.get.mockResolvedValueOnce({ data: {} }); + + await nodeService.get("/blocks", {}, params); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + "/blocks", + expect.objectContaining({ params }) + ); + }); + }); + + describe("post method", () => { + test("should make POST request", async () => { + const mockData = { txId: "abc123" }; + mockAxiosInstance.post.mockResolvedValueOnce({ data: mockData }); + + const result = await nodeService.post("/transactions", {}, { tx: "data" }); + + expect(mockAxiosInstance.post).toHaveBeenCalled(); + expect(result.data).toEqual(mockData); + }); + + test("should pass headers to POST request", async () => { + const headers = { "Custom-Header": "value" }; + mockAxiosInstance.post.mockResolvedValueOnce({ data: {} }); + + await nodeService.post("/transactions", headers, {}); + + expect(mockAxiosInstance.defaults.headers).toEqual( + expect.objectContaining(headers) + ); + }); + }); + + describe("head method", () => { + test("should make HEAD request and return status", async () => { + mockAxiosInstance.head.mockResolvedValueOnce({ status: 200 }); + + const status = await nodeService.head("/transactions/unconfirmed/abc123"); + + expect(mockAxiosInstance.head).toHaveBeenCalled(); + expect(status).toBe(200); + }); + + test("should return error status on failure", async () => { + mockAxiosInstance.head.mockRejectedValueOnce({ + response: { status: 404 }, + }); + + const status = await nodeService.head("/transactions/unconfirmed/notfound"); + + expect(status).toBe(404); + }); + + test("should return 500 on network error", async () => { + mockAxiosInstance.head.mockRejectedValueOnce(new Error("Network error")); + + const status = await nodeService.head("/test"); + + expect(status).toBe(500); + }); + }); + + describe("Blockchain API methods", () => { + test("getInfo should fetch node info", async () => { + const mockInfo = { fullHeight: 1000000, headersHeight: 1000001 }; + mockAxiosInstance.get.mockResolvedValueOnce({ data: mockInfo }); + + const result = await nodeService.getInfo(); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + "/info", + expect.any(Object) + ); + expect(result).toEqual(mockInfo); + }); + + test("getNetworkHeight should return full height", async () => { + const mockInfo = { fullHeight: 1000000 }; + mockAxiosInstance.get.mockResolvedValueOnce({ data: mockInfo }); + + const height = await nodeService.getNetworkHeight(); + + expect(height).toBe(1000000); + }); + + test("getUnspentBoxByTokenId should fetch boxes", async () => { + const tokenId = "abc123"; + const mockBoxes = [{ boxId: "box1" }]; + mockAxiosInstance.get.mockResolvedValueOnce({ data: mockBoxes }); + + const result = await nodeService.getUnspentBoxByTokenId(tokenId); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + `/blockchain/box/unspent/byTokenId/${tokenId}`, + expect.any(Object) + ); + expect(result).toEqual(mockBoxes); + }); + + test("getTokenInfo should fetch token information", async () => { + const tokenId = "token123"; + const mockToken = { id: tokenId, name: "Test Token" }; + mockAxiosInstance.get.mockResolvedValueOnce({ data: mockToken }); + + const result = await nodeService.getTokenInfo(tokenId); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith( + `/blockchain/token/byId/${tokenId}`, + expect.any(Object) + ); + expect(result).toEqual(mockToken); + }); + + test("checkUnconfirmedTx should check transaction status", async () => { + const txId = "tx123"; + mockAxiosInstance.head.mockResolvedValueOnce({ status: 200 }); + + const status = await nodeService.checkUnconfirmedTx(txId); + + expect(mockAxiosInstance.head).toHaveBeenCalledWith( + `/transactions/unconfirmed/${txId}`, + expect.any(Object) + ); + expect(status).toBe(200); + }); + + test("postTransaction should submit transaction", async () => { + const tx = { inputs: [], outputs: [] }; + const mockResponse = { data: "txId123" }; + mockAxiosInstance.post.mockResolvedValueOnce(mockResponse); + + const result = await nodeService.postTransaction(tx); + + expect(mockAxiosInstance.post).toHaveBeenCalledWith( + "transactions", + tx + ); + expect(result).toBe("txId123"); + }); + }); + + describe("Box and Transaction methods", () => { + test("getBoxById should fetch box by ID", async () => { + const boxId = "box123"; + const mockBox = { boxId, value: 1000000 }; + mockAxiosInstance.get.mockResolvedValueOnce({ data: mockBox }); + + const result = await nodeService.getBoxById(boxId); + + expect(result).toEqual(mockBox); + }); + + test("getTxsById should fetch transaction by ID", async () => { + const txId = "tx123"; + const mockTx = { id: txId, inclusionHeight: 1000000 }; + mockAxiosInstance.get.mockResolvedValueOnce({ data: mockTx }); + + const result = await nodeService.getTxsById(txId); + + expect(result).toEqual(mockTx); + }); + + test("getUnconfirmedTransactionById should fetch unconfirmed tx", async () => { + const txId = "tx123"; + const mockTx = { id: txId }; + mockAxiosInstance.get.mockResolvedValueOnce({ data: mockTx }); + + const result = await nodeService.getUnconfirmedTransactionById(txId); + + expect(result).toEqual(mockTx); + }); + }); + + describe("Error handling", () => { + test("should handle network errors in get", async () => { + mockAxiosInstance.get.mockRejectedValueOnce(new Error("Network error")); + + await expect(nodeService.get("/info")).rejects.toThrow("Network error"); + }); + + test("should handle network errors in post", async () => { + mockAxiosInstance.post.mockRejectedValueOnce(new Error("Network error")); + + await expect(nodeService.post("/transactions", {}, {})).rejects.toThrow("Network error"); + }); + }); +}); diff --git a/tests/lib/utils/transaction-listener.test.ts b/tests/lib/utils/transaction-listener.test.ts new file mode 100644 index 0000000..ccbc806 --- /dev/null +++ b/tests/lib/utils/transaction-listener.test.ts @@ -0,0 +1,196 @@ +import { describe, expect, test, beforeEach } from "bun:test"; +import "../../../tests/setup"; // Import setup to ensure localStorage mock is available +import { TransactionListener } from "@/lib/utils/transaction-listener"; +import type { WalletState, ExpectedChanges } from "@/lib/utils/transaction-listener"; +import { NodeService } from "@/lib/utils/node-service"; + +describe("TransactionListener", () => { + let listener: TransactionListener; + let mockNodeService: any; + + beforeEach(() => { + // Clear localStorage before each test + if (typeof localStorage !== 'undefined') { + localStorage.clear(); + } + + // Create mock NodeService + mockNodeService = { + getUnconfirmedTransactionById: () => Promise.resolve(null), + getTxsById: () => Promise.resolve(null), + }; + + listener = new TransactionListener(mockNodeService as NodeService); + }); + + describe("saveUpTransaction", () => { + test("should save transaction to localStorage", () => { + const txHash = "abc123"; + const actionType = "fission"; + const preTransactionState: WalletState = { + erg: "1000000000", + gau: "0", + gauc: "0", + timestamp: Date.now(), + }; + const expectedChanges: ExpectedChanges = { + erg: "-500000000", + gau: "250000000", + gauc: "250000000", + fees: "-1000000", + }; + + listener.saveUpTransaction(txHash, actionType, preTransactionState, expectedChanges); + + const stored = localStorage.getItem("gluon_pending_transactions"); + expect(stored).not.toBeNull(); + + if (stored) { + const parsed = JSON.parse(stored); + expect(parsed[txHash]).toBeDefined(); + expect(parsed[txHash].actionType).toBe(actionType); + expect(parsed[txHash].isConfirmed).toBe(false); + expect(parsed[txHash].isWalletUpdated).toBe(false); + } + }); + + test("should store transaction with correct structure", () => { + const txHash = "test123"; + const actionType = "fusion"; + const preTransactionState: WalletState = { + erg: "1000000000", + gau: "500000000", + gauc: "500000000", + timestamp: Date.now(), + }; + const expectedChanges: ExpectedChanges = { + erg: "1000000000", + gau: "-500000000", + gauc: "-500000000", + fees: "-1000000", + }; + + listener.saveUpTransaction(txHash, actionType, preTransactionState, expectedChanges); + + const stored = localStorage.getItem("gluon_pending_transactions"); + if (stored) { + const parsed = JSON.parse(stored); + const tx = parsed[txHash]; + + expect(tx.txHash).toBe(txHash); + expect(tx.actionType).toBe(actionType); + expect(tx.preTransactionState).toEqual(preTransactionState); + expect(tx.expectedChanges).toEqual(expectedChanges); + expect(tx.retryCount).toBe(0); + } + }); + + test("should handle multiple transactions", () => { + listener.saveUpTransaction("tx1", "fission", { erg: "1000000000", gau: "0", gauc: "0", timestamp: Date.now() }, { erg: "-500000000", gau: "250000000", gauc: "250000000", fees: "-1000000" }); + listener.saveUpTransaction("tx2", "fusion", { erg: "1000000000", gau: "500000000", gauc: "500000000", timestamp: Date.now() }, { erg: "1000000000", gau: "-500000000", gauc: "-500000000", fees: "-1000000" }); + + const stored = localStorage.getItem("gluon_pending_transactions"); + if (stored) { + const parsed = JSON.parse(stored); + expect(Object.keys(parsed).length).toBe(2); + expect(parsed["tx1"]).toBeDefined(); + expect(parsed["tx2"]).toBeDefined(); + } + }); + }); + + describe("cleanUpTransaction", () => { + beforeEach(() => { + // Add some test transactions + listener.saveUpTransaction("tx1", "fission", { erg: "1000000000", gau: "0", gauc: "0", timestamp: Date.now() }, { erg: "-500000000", gau: "250000000", gauc: "250000000", fees: "-1000000" }); + listener.saveUpTransaction("tx2", "fusion", { erg: "1000000000", gau: "500000000", gauc: "500000000", timestamp: Date.now() }, { erg: "1000000000", gau: "-500000000", gauc: "-500000000", fees: "-1000000" }); + }); + + test("should remove specific transaction", () => { + listener.cleanUpTransaction("tx1"); + + const stored = localStorage.getItem("gluon_pending_transactions"); + if (stored) { + const parsed = JSON.parse(stored); + expect(parsed["tx1"]).toBeUndefined(); + expect(parsed["tx2"]).toBeDefined(); + } + }); + + test("should remove all old completed transactions", () => { + // Manually update localStorage with old transaction + const stored = localStorage.getItem("gluon_pending_transactions"); + if (stored) { + const pending = JSON.parse(stored); + pending["tx1"].timestamp = Date.now() - (2 * 60 * 60 * 1000); // 2 hours ago + pending["tx1"].isWalletUpdated = true; + localStorage.setItem("gluon_pending_transactions", JSON.stringify(pending)); + + listener.cleanUpTransaction(); + + const updatedStored = localStorage.getItem("gluon_pending_transactions"); + if (updatedStored) { + const parsed = JSON.parse(updatedStored); + expect(parsed["tx1"]).toBeUndefined(); + expect(parsed["tx2"]).toBeDefined(); + } + } + }); + + test("should clear localStorage when no transactions remain", () => { + listener.cleanUpTransaction("tx1"); + listener.cleanUpTransaction("tx2"); + + const stored = localStorage.getItem("gluon_pending_transactions"); + if (stored) { + const parsed = JSON.parse(stored); + expect(Object.keys(parsed).length).toBe(0); + } + }); + }); + + describe("hasPendingTransactions", () => { + test("should return false when no pending transactions", () => { + expect(listener.hasPendingTransactions()).toBe(false); + }); + + test("should return true when pending transactions exist", () => { + listener.saveUpTransaction("tx1", "fission", { erg: "1000000000", gau: "0", gauc: "0", timestamp: Date.now() }, { erg: "-500000000", gau: "250000000", gauc: "250000000", fees: "-1000000" }); + expect(listener.hasPendingTransactions()).toBe(true); + }); + }); + + describe("getPendingTransactionsList", () => { + test("should return empty array when no transactions", () => { + const list = listener.getPendingTransactionsList(); + expect(list).toEqual([]); + }); + + test("should return array of pending transactions", () => { + listener.saveUpTransaction("tx1", "fission", { erg: "1000000000", gau: "0", gauc: "0", timestamp: Date.now() }, { erg: "-500000000", gau: "250000000", gauc: "250000000", fees: "-1000000" }); + listener.saveUpTransaction("tx2", "fusion", { erg: "1000000000", gau: "500000000", gauc: "500000000", timestamp: Date.now() }, { erg: "1000000000", gau: "-500000000", gauc: "-500000000", fees: "-1000000" }); + + const list = listener.getPendingTransactionsList(); + expect(list.length).toBe(2); + expect(list[0].txHash).toBeDefined(); + expect(list[1].txHash).toBeDefined(); + }); + }); + + describe("initialize", () => { + test("should not start listening when no pending transactions", () => { + listener.initialize(); + expect(listener.hasPendingTransactions()).toBe(false); + }); + + test("should start listening when pending transactions exist", () => { + listener.saveUpTransaction("tx1", "fission", { erg: "1000000000", gau: "0", gauc: "0", timestamp: Date.now() }, { erg: "-500000000", gau: "250000000", gauc: "250000000", fees: "-1000000" }); + + // Create new listener to test initialization + const newListener = new TransactionListener(mockNodeService as NodeService); + newListener.initialize(); + + expect(newListener.hasPendingTransactions()).toBe(true); + }); + }); +}); diff --git a/tests/lib/utils/utils.test.ts b/tests/lib/utils/utils.test.ts new file mode 100644 index 0000000..da74794 --- /dev/null +++ b/tests/lib/utils/utils.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from "bun:test"; +import { cn } from "@/lib/utils/utils"; + +describe("cn (className utility)", () => { + test("should merge class names correctly", () => { + const result = cn("foo", "bar"); + expect(result).toBe("foo bar"); + }); + + test("should handle conditional classes", () => { + const result = cn("foo", false && "bar", "baz"); + expect(result).toBe("foo baz"); + }); + + test("should merge Tailwind classes correctly", () => { + // tailwind-merge should handle conflicting classes + const result = cn("px-2 py-1", "px-4"); + expect(result).toBe("py-1 px-4"); + }); + + test("should handle arrays of classes", () => { + const result = cn(["foo", "bar"], "baz"); + expect(result).toBe("foo bar baz"); + }); + + test("should handle objects with boolean values", () => { + const result = cn({ + foo: true, + bar: false, + baz: true, + }); + expect(result).toBe("foo baz"); + }); + + test("should handle empty input", () => { + const result = cn(); + expect(result).toBe(""); + }); + + test("should handle undefined and null values", () => { + const result = cn("foo", undefined, null, "bar"); + expect(result).toBe("foo bar"); + }); + + test("should handle complex Tailwind class conflicts", () => { + const result = cn( + "bg-red-500 text-white", + "bg-blue-500" // Should override bg-red-500 + ); + expect(result).toBe("text-white bg-blue-500"); + }); +}); diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..a40278f --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,53 @@ +/** + * Global test setup for Bun test runner + * This file configures the test environment and provides global utilities + */ + +// Mock browser APIs that might not be available in test environment +if (typeof window === 'undefined') { + global.window = {} as any; +} + +// Mock localStorage for tests +class LocalStorageMock { + private store: Record = {}; + + clear() { + this.store = {}; + } + + getItem(key: string) { + return this.store[key] || null; + } + + setItem(key: string, value: string) { + this.store[key] = value.toString(); + } + + removeItem(key: string) { + delete this.store[key]; + } + + get length() { + return Object.keys(this.store).length; + } + + key(index: number) { + const keys = Object.keys(this.store); + return keys[index] || null; + } +} + +global.localStorage = new LocalStorageMock() as any; + +// Mock console methods to reduce noise in tests (optional) +// Uncomment if you want to suppress console output during tests +// global.console = { +// ...console, +// log: () => {}, +// debug: () => {}, +// info: () => {}, +// warn: () => {}, +// }; + +export { }; diff --git a/tsconfig.json b/tsconfig.json index 572b7ad..037d93b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,14 @@ { "compilerOptions": { "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "types": [ + "@types/bun" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -14,9 +21,17 @@ "jsx": "preserve", "incremental": true, "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] -} + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file