From 964f8dfd970b24e02d9b7d3bcdae286ebb721886 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 3 Dec 2022 20:47:20 -0900 Subject: [PATCH] setup E2E Testing --- .vscode/settings.json | 15 +- frontend/lib/api/__test__/user/stats.test.ts | 142 +++++++++++++++++++ frontend/lib/api/classes/group.ts | 8 +- frontend/lib/api/classes/items.ts | 2 +- frontend/lib/api/classes/stats.ts | 42 ++++++ frontend/lib/api/user.ts | 3 + frontend/pages/home.vue | 2 +- 7 files changed, 201 insertions(+), 13 deletions(-) create mode 100644 frontend/lib/api/__test__/user/stats.test.ts create mode 100644 frontend/lib/api/classes/stats.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index b330533..f05ebc8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,4 @@ { - "editor.codeActionsOnSave": { - "source.fixAll.eslint": true - }, "yaml.schemas": { "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" }, @@ -13,5 +10,15 @@ }, "cSpell.words": [ "debughandlers" - ] + ], + // use ESLint to format code on save + "editor.formatOnSave": true, + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "[typescript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + } diff --git a/frontend/lib/api/__test__/user/stats.test.ts b/frontend/lib/api/__test__/user/stats.test.ts new file mode 100644 index 0000000..247b024 --- /dev/null +++ b/frontend/lib/api/__test__/user/stats.test.ts @@ -0,0 +1,142 @@ +import { faker } from "@faker-js/faker"; +import { beforeAll, describe, expect, test } from "vitest"; +import { UserClient } from "../../user"; +import { factories } from "../factories"; + +type ImportObj = { + ImportRef: string; + Location: string; + Labels: string; + Quantity: string; + Name: string; + Description: string; + Insured: boolean; + SerialNumber: string; + ModelNumber: string; + Manufacturer: string; + Notes: string; + PurchaseFrom: string; + PurchasedPrice: number; + PurchasedTime: string; + LifetimeWarranty: boolean; + WarrantyExpires: string; + WarrantyDetails: string; + SoldTo: string; + SoldPrice: number; + SoldTime: string; + SoldNotes: string; +}; + +function toCsv(data: ImportObj[]): string { + const headers = Object.keys(data[0]).join("\t"); + const rows = data.map(row => { + return Object.values(row).join("\t"); + }); + return [headers, ...rows].join("\n"); +} + +function importFileGenerator(entries: number): ImportObj[] { + const imports: ImportObj[] = []; + + const pick = (arr: string[]) => arr[Math.floor(Math.random() * arr.length)]; + + const labels = faker.random.words(5).split(" ").join(";"); + const locations = faker.random.words(3).split(" "); + + const half = Math.floor(entries / 2); + + for (let i = 0; i < entries; i++) { + imports.push({ + ImportRef: faker.database.mongodbObjectId(), + Location: pick(locations), + Labels: labels, + Quantity: faker.random.numeric(1), + Name: faker.random.words(3), + Description: "", + Insured: faker.datatype.boolean(), + SerialNumber: faker.random.alphaNumeric(5), + ModelNumber: faker.random.alphaNumeric(5), + Manufacturer: faker.random.alphaNumeric(5), + Notes: "", + PurchaseFrom: faker.name.fullName(), + PurchasedPrice: faker.datatype.number(100), + PurchasedTime: faker.date.past().toDateString(), + LifetimeWarranty: half > i, + WarrantyExpires: faker.date.future().toDateString(), + WarrantyDetails: "", + SoldTo: faker.name.fullName(), + SoldPrice: faker.datatype.number(100), + SoldTime: faker.date.past().toDateString(), + SoldNotes: "", + }); + } + + return imports; +} + +describe("group related statistics tests", () => { + const TOTAL_ITEMS = 30; + + let api: UserClient | undefined; + const imports = importFileGenerator(TOTAL_ITEMS); + + beforeAll(async () => { + // -- Setup -- + const { client } = await factories.client.singleUse(); + api = client; + + const csv = toCsv(imports); + + const setupResp = await client.items.import(new Blob([csv], { type: "text/csv" })); + + expect(setupResp.status).toBe(204); + }); + + // Write to file system for debugging + // fs.writeFileSync("test.csv", csv); + test("Validate Group Statistics", async () => { + const { status, data } = await api.stats.group(); + expect(status).toBe(200); + + expect(data.totalItems).toEqual(TOTAL_ITEMS); + expect(data.totalLabels).toEqual(11); // default + new + expect(data.totalLocations).toEqual(11); // default + new + expect(data.totalUsers).toEqual(1); + expect(data.totalWithWarranty).toEqual(Math.floor(TOTAL_ITEMS / 2)); + }); + + const labelData: Record = {}; + const locationData: Record = {}; + + for (const item of imports) { + for (const label of item.Labels.split(";")) { + labelData[label] = (labelData[label] || 0) + item.PurchasedPrice; + } + + locationData[item.Location] = (locationData[item.Location] || 0) + item.PurchasedPrice; + } + + test("Validate Labels Statistics", async () => { + const { status, data } = await api.stats.labels(); + expect(status).toBe(200); + + for (const label of data) { + expect(label.total).toEqual(labelData[label.name]); + } + }); + + test("Validate Locations Statistics", async () => { + const { status, data } = await api.stats.locations(); + expect(status).toBe(200); + + for (const location of data) { + expect(location.total).toEqual(locationData[location.name]); + } + }); + + test("Validate Purchase Over Time", async () => { + const { status, data } = await api.stats.totalPriceOverTime(); + expect(status).toBe(200); + expect(data.entries.length).toEqual(TOTAL_ITEMS); + }); +}); diff --git a/frontend/lib/api/classes/group.ts b/frontend/lib/api/classes/group.ts index 9c8fefa..7468f09 100644 --- a/frontend/lib/api/classes/group.ts +++ b/frontend/lib/api/classes/group.ts @@ -1,5 +1,5 @@ import { BaseAPI, route } from "../base"; -import { Group, GroupInvitation, GroupInvitationCreate, GroupStatistics, GroupUpdate } from "../types/data-contracts"; +import { Group, GroupInvitation, GroupInvitationCreate, GroupUpdate } from "../types/data-contracts"; export class GroupApi extends BaseAPI { createInvitation(data: GroupInvitationCreate) { @@ -21,10 +21,4 @@ export class GroupApi extends BaseAPI { url: route("/groups"), }); } - - statistics() { - return this.http.get({ - url: route("/groups/statistics"), - }); - } } diff --git a/frontend/lib/api/classes/items.ts b/frontend/lib/api/classes/items.ts index ee18f01..f4fb38d 100644 --- a/frontend/lib/api/classes/items.ts +++ b/frontend/lib/api/classes/items.ts @@ -50,7 +50,7 @@ export class ItemsApi extends BaseAPI { return payload; } - import(file: File) { + import(file: File | Blob) { const formData = new FormData(); formData.append("csv", file); diff --git a/frontend/lib/api/classes/stats.ts b/frontend/lib/api/classes/stats.ts new file mode 100644 index 0000000..b605270 --- /dev/null +++ b/frontend/lib/api/classes/stats.ts @@ -0,0 +1,42 @@ +import { BaseAPI, route } from "../base"; +import { GroupStatistics, TotalsByOrganizer, ValueOverTime } from "../types/data-contracts"; + +function YYYY_DD_MM(date?: Date): string { + if (!date) { + return ""; + } + // with leading zeros + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + return `${year}-${month}-${day}`; +} +export class StatsAPI extends BaseAPI { + totalPriceOverTime(start?: Date, end?: Date) { + return this.http.get({ + url: route("/groups/statistics/purchase-price", { start: YYYY_DD_MM(start), end: YYYY_DD_MM(end) }), + }); + } + + /** + * Returns ths general statistics for the group. This mostly just + * includes the totals for various group properties. + */ + group() { + return this.http.get({ + url: route("/groups/statistics"), + }); + } + + labels() { + return this.http.get({ + url: route("/groups/statistics/labels"), + }); + } + + locations() { + return this.http.get({ + url: route("/groups/statistics/locations"), + }); + } +} diff --git a/frontend/lib/api/user.ts b/frontend/lib/api/user.ts index 31538a8..079f113 100644 --- a/frontend/lib/api/user.ts +++ b/frontend/lib/api/user.ts @@ -5,6 +5,7 @@ import { LocationsApi } from "./classes/locations"; import { GroupApi } from "./classes/group"; import { UserApi } from "./classes/users"; import { ActionsAPI } from "./classes/actions"; +import { StatsAPI } from "./classes/stats"; import { Requests } from "~~/lib/requests"; export class UserClient extends BaseAPI { @@ -14,6 +15,7 @@ export class UserClient extends BaseAPI { group: GroupApi; user: UserApi; actions: ActionsAPI; + stats: StatsAPI; constructor(requests: Requests, attachmentToken: string) { super(requests, attachmentToken); @@ -24,6 +26,7 @@ export class UserClient extends BaseAPI { this.group = new GroupApi(requests); this.user = new UserApi(requests); this.actions = new ActionsAPI(requests); + this.stats = new StatsAPI(requests); Object.freeze(this); } diff --git a/frontend/pages/home.vue b/frontend/pages/home.vue index 7723d95..92edee3 100644 --- a/frontend/pages/home.vue +++ b/frontend/pages/home.vue @@ -22,7 +22,7 @@ const labels = computed(() => labelsStore.labels); const { data: statistics } = useAsyncData(async () => { - const { data } = await api.group.statistics(); + const { data } = await api.stats.group(); return data; });