Skip to content

Commit

Permalink
Merge pull request #57 from Robert-M-Lucas/dev-tests
Browse files Browse the repository at this point in the history
add modal unit tests
  • Loading branch information
DylanRoskilly authored May 3, 2024
2 parents 994912e + a2c82f2 commit 68257a4
Show file tree
Hide file tree
Showing 9 changed files with 950 additions and 15 deletions.
622 changes: 619 additions & 3 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,24 @@
},
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^15.0.5",
"@testing-library/user-event": "^14.5.2",
"@types/lodash": "^4.17.0",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@firebase/rules-unit-testing": "^3.0.2",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/coverage-istanbul": "^1.5.1",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"firebase-admin": "^12.1.0",
"jsdom": "^24.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.2.2",
"vite": "^5.2.0",
"vitest": "^1.5.1"
"vitest": "^1.5.2"
}
}
127 changes: 127 additions & 0 deletions src/components/transactions/CSVUpload.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { beforeEach, describe, expect, test as defaultTest, vi } from 'vitest';
import { CSVUpload } from './CSVUpload';
import { useState } from 'react';
import { Button } from 'react-bootstrap';
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import "@testing-library/jest-dom";

function TestCSVUpload() {
const [show, setShow] = useState(false);

return <>
<Button variant="primary" onClick={() => setShow(true)}>Open</Button>
<CSVUpload show={show} closeModal={() => setShow(false)} />
</>
}

const test = defaultTest.extend({ user: userEvent.setup() })

// mock the "writeNewTransactionsBatched" function to cirumvent firebase auth and rules
// its already been unit tested so we know it works
vi.mock("../../utils/transaction", async (importOriginal) => {
const mod: {} = await importOriginal();
return {
...mod,
writeNewTransactionsBatched: vi.fn(),
}
})

// mock auth.currentUser to simulate a user being signed into website
vi.mock("../../utils/firebase", async (importOriginal) => {
const mod: {} = await importOriginal();
return {
...mod,
auth: { currentUser: { uid: "100" } }
}
})

describe("CSVUpload Tests", () => {
beforeEach(() => {
render(<TestCSVUpload />)
});

test("modal opens", async ({ user }) => {
expect(screen.queryByText("Upload CSV Transactions")).toBeNull();

await user.click(screen.getByText("Open"));

expect(screen.getByText("Upload CSV Transactions")).toBeInTheDocument();
})

test("modal closes with close button", async ({ user }) => {
await user.click(screen.getByText("Open"));

await user.click(screen.getByText("Close"));

expect(screen.getByText("Open")).toBeVisible()
})

test("stops no uploaded CSV", async ({ user }) => {
await user.click(screen.getByText("Open"));

await user.click(screen.getByText("Upload CSV"));

expect(screen.getByText("You haven't uploaded a CSV file")).toBeVisible()
})

test("non-CSV results in zero uploaded transactions", async ({ user }) => {
await user.click(screen.getByText("Open"));

const txt = new File(["testing"], "text.txt", { type: "text/plaintext", lastModified: Date.now() })

await user.upload(screen.getByTestId("csvUpload"), txt)

await user.click(screen.getByText("Upload CSV"));

expect(screen.getByText("You haven't uploaded a CSV file")).toBeVisible()
})

test("empty CSV results in zero uploaded transactions", async ({ user }) => {
await user.click(screen.getByText("Open"));

const csv = new File([""], "emptyCSV.csv", { type: "text/csv", lastModified: Date.now() })

await user.upload(screen.getByTestId("csvUpload"), csv)

await user.click(screen.getByText("Upload CSV"));

expect(screen.getByText("Uploaded CSV is empty")).toBeVisible()
})

test("non-Monzo CSV results in zero uploaded transactions", async ({ user }) => {
await user.click(screen.getByText("Open"));

const csv = new File(
["testing1, 123, testing2, testing,, testing3, 5-5, testing!"],
"invalidCSV.csv",
{ type: "text/csv", lastModified: Date.now() }
)

await user.upload(screen.getByTestId("csvUpload"), csv)

await user.click(screen.getByText("Upload CSV"));

expect(screen.getByText("The uploaded CSV file has no valid transactions")).toBeVisible()
})

test("Monzo CSV file uploads transactions", async ({ user }) => {
await user.click(screen.getByText("Open"));

const csv = new File(
[
"Transaction ID,Date,Time,Type,Name,Emoji,Category,Amount,Currency,Local amount,Local currency,Notes and #tags,Address,Receipt,Description,Category split,Money Out,Money In\n",
"tx_0000AbA3Oasz9hIuCRy3RT,26/10/2023,11:55:41,Card payment,Boots,💊,Personal care,-21.97,GBP,-21.97,GBP,,1 Newark Street,,BOOTS 2011 BATH GBR,,-21.97,\n",
"tx_0000Ab8DxJ14CFwgh9JVh4,25/10/2023,14:44:35,Card payment,Aesop Bath,💄,Shopping,-25.00,GBP,-25.00,GBP,,16 New Bond Street,,Aesop Bath Bath GBR,,-25.00,\n"
],
"validCSV.csv",
{ type: "text/csv", lastModified: Date.now() }
)

await user.upload(screen.getByTestId("csvUpload"), csv)

await user.click(screen.getByText("Upload CSV"));

expect(screen.getByText("2 transactions have been imported")).toBeVisible()
})
})
6 changes: 3 additions & 3 deletions src/components/transactions/CSVUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ export function CSVUpload({ show, closeModal }: { show: boolean, closeModal: (ad

const file = fileElement.files![0];
if (!file) return setError("You haven't uploaded a CSV file");

reader.onload = async (event) => {
if (!auth.currentUser) return setError("You are not signed in");

const csvContent = event.target?.result;
if (!csvContent || csvContent instanceof ArrayBuffer) return setError("Unable to read uploaded CSV file");
if (!csvContent || csvContent instanceof ArrayBuffer) return setError("Uploaded CSV is empty");

const transactionDocuments = csvContent
.split("\n")
Expand Down Expand Up @@ -62,7 +62,7 @@ export function CSVUpload({ show, closeModal }: { show: boolean, closeModal: (ad
</Modal.Header>
<Modal.Body>
<Form>
<Form.Control type="file" id="file" accept=".csv" />
<Form.Control data-testid="csvUpload" type="file" id="file" accept=".csv" />
</Form>

{successMsg && <Alert variant="success">{successMsg}</Alert>}
Expand Down
102 changes: 102 additions & 0 deletions src/components/transactions/InputTransaction.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { beforeEach, describe, expect, vi, test as defaultTest } from 'vitest';
import { InputTransaction } from './InputTransaction';
import { useState } from 'react';
import { Button } from 'react-bootstrap';
import { render, screen } from '@testing-library/react';
import { writeNewTransaction } from '../../utils/transaction';
import { userEvent } from "@testing-library/user-event"
import "@testing-library/jest-dom";

function TestInputTransaction() {
const [show, setShow] = useState(false);

return <>
<Button variant="primary" onClick={() => setShow(true)}>Open</Button>
<InputTransaction show={show} closeModal={() => setShow(false)} />
</>
}

const test = defaultTest.extend({ user: userEvent.setup() })

// mock the "writeNewTransaction" function to cirumvent firebase auth and rules
// its already been unit tested so we know it works
vi.mock("../../utils/transaction", async (importOriginal) => {
const mod: {} = await importOriginal();
return {
...mod,
writeNewTransaction: vi.fn(),
}
})

// mock auth.currentUser to simulate a user being signed into website
vi.mock("../../utils/firebase", async (importOriginal) => {
const mod: {} = await importOriginal();
return {
...mod,
auth: { currentUser: { uid: "100" } }
}
})

describe("InputTransaction Tests", () => {
beforeEach(() => {
render(<TestInputTransaction />)
})

test("modal opens", async ({ user }) => {
expect(screen.queryByText("Add Transaction")).toBeNull();

await user.click(screen.getByText("Open"));

// there are two elements with the text "Add Transaction" in this modal: the modal title and the submit button
expect(screen.getAllByText("Add Transaction")).toHaveLength(2)
})

test("modal closes with close button", async ({ user }) => {
await user.click(screen.getByText("Open"));

await user.click(screen.getByText("Close"));

expect(screen.getByText("Open")).toBeVisible()
})

test("empty required textboxes results in zero uploaded transactions", async ({ user }) => {
await user.click(screen.getByText("Open"));

await user.click(screen.getAllByRole("button").find((element) => element.textContent === "Add Transaction")!);

expect(screen.getByText("The inputted amount is invalid")).toBeVisible()
})

test("allow optional textboxes to be empty", async ({ user }) => {
await user.click(screen.getByText("Open"));

await user.type(screen.getByPlaceholderText("Enter name"), "Sainsburys");
await user.type(screen.getByPlaceholderText("Enter amount (use '-' for expenses, do not include currency)"), "-100");
// use default category -- income

await user.click(screen.getAllByRole("button").find((element) => element.textContent === "Add Transaction")!)

expect(await screen.findByText("Transaction has been successfully added")).toBeVisible()
expect(writeNewTransaction).toHaveBeenCalled();
})

test("transaction uploaded when all textboxes are valid", async ({ user }) => {
await user.click(screen.getByText("Open"));

await user.type(screen.getByPlaceholderText("Enter name"), "Sainsburys");
await user.type(screen.getByPlaceholderText("Enter amount (use '-' for expenses, do not include currency)"), "-100");
// use default category -- income
await user.type(screen.getByPlaceholderText("Enter date (DD/MM/YYYY) (optional)"), "12/12/2001");
await user.type(screen.getByPlaceholderText("Enter time (HH:MM:SS) (optional)"), "00:00:00");
await user.type(screen.getByPlaceholderText("Enter Description (optional)"), "testing description");
await user.type(screen.getByPlaceholderText("Enter Notes (optional)"), "testing notes");
await user.type(screen.getByPlaceholderText("Enter Address (optional)"), "testing address");

await user.click(screen.getAllByRole("button").find((element) => element.textContent === "Add Transaction")!)

expect(await screen.findByText("Transaction has been successfully added")).toBeVisible()
expect(writeNewTransaction).toHaveBeenCalled();
})
})


2 changes: 1 addition & 1 deletion src/components/transactions/InputTransaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function InputTransaction({ show, closeModal }: { show: boolean, closeMod
if (date.trim() !== "" && curTime.trim() === "") curTime = "00:00:00"
else if (curTime.trim() === "") curTime = formatTime(new Date())

const transaction = new Transaction()
const transaction = new Transaction()
.setDate(date.trim() === "" ? formatDate(new Date()) : date)
.setTime(curTime)
.setName(name)
Expand Down
91 changes: 87 additions & 4 deletions src/components/transactions/Tranasction.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,93 @@
import {describe, expect, test} from "vitest";
import {emojis} from "./Transaction.ts";
import {TransactionCategories} from "../../utils/transaction.ts";
import { describe, expect, test } from "vitest";
import { Transaction, emojis } from "./Transaction.ts";
import { TransactionCategories } from "../../utils/transaction.ts";
import _ from "lodash";

describe("Input Transaction Test", () => {
test("Category Integrity Test", () => {
expect(_.isEqual(new Set(Object.keys(emojis)), TransactionCategories), "Emoji keys do not match transaction categories").toBeTruthy();
});
});
});

const validationTest = test.extend({ transaction: new Transaction() })

function doesValidate(transaction: Transaction, field: string) {
expect(!transaction.isValid && transaction.invalidField == field).toBeTruthy()
}

describe("Transaction Validator Tests", () => {
validationTest("stops undefined date", ({ transaction }) => {
transaction.setDate();
doesValidate(transaction, "date");
})

validationTest("stops empty string as date", ({ transaction }) => {
transaction.setDate("");
doesValidate(transaction, "date");
})

validationTest("stops 00/00/00 as date", ({ transaction }) => {
transaction.setDate("00/00/00");
doesValidate(transaction, "date");
})

validationTest("stops 32/21/00 as date", ({ transaction }) => {
transaction.setDate("32/21/00");
doesValidate(transaction, "date");
})

validationTest("stops / as date", ({ transaction }) => {
transaction.setDate("/");
doesValidate(transaction, "date");
})

validationTest("stops undefined time", ({ transaction }) => {
transaction.setTime();
doesValidate(transaction, "time");
})

validationTest("stops empty string as time", ({ transaction }) => {
transaction.setTime("");
doesValidate(transaction, "time");
})

validationTest("stops 25:61:61 as time", ({ transaction }) => {
transaction.setTime("25:61:61");
doesValidate(transaction, "time");
})

validationTest("stops : as time", ({ transaction }) => {
transaction.setTime(":");
doesValidate(transaction, "time");
})

validationTest("stops undefined names", ({ transaction }) => {
transaction.setName();
doesValidate(transaction, "name");
})

validationTest("stops unknown categories", ({ transaction }) => {
transaction.setCategory("testing");
doesValidate(transaction, "category");
})

validationTest("stops undefined category", ({ transaction }) => {
transaction.setCategory();
doesValidate(transaction, "category");
})

validationTest("stops non-number amounts", ({ transaction }) => {
transaction.setAmount("testing");
doesValidate(transaction, "amount");
})

validationTest("stops undefined amount", ({ transaction }) => {
transaction.setAmount();
doesValidate(transaction, "amount");
})

validationTest("stops undefined currencies", ({ transaction }) => {
transaction.setCurrency();
doesValidate(transaction, "currency");
})
})
2 changes: 1 addition & 1 deletion src/components/transactions/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export class Transaction {
setCurrency(currency?: string) {
if (!this.isNotEmpty(currency)) {
this.isValid = false;
this.invalidField = "name";
this.invalidField = "currency";
} else {
this.currency = currency;
}
Expand Down
Loading

0 comments on commit 68257a4

Please sign in to comment.