Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add modal unit tests #57

Merged
merged 1 commit into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading