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

feat: Install count #81

Merged
merged 8 commits into from
Jun 7, 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
6 changes: 6 additions & 0 deletions assets/install.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 24 additions & 8 deletions src/components/installButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import * as hooks from "@polarexpress/dataAccess/broker/hooks";
import { shortAddonList } from "@polarexpress/mockData/addons";
import {
addInstalled,
deleteInstalledList,
getInstalled,
initializeInstalled
} from "@polarexpress/test/mockingUtils";
import { setupPageWithId } from "@polarexpress/test/utils";
import { waitFor } from "@testing-library/react";

describe("InstallButton", () => {
const testAddon = shortAddonList[0];
Expand All @@ -23,10 +23,6 @@ describe("InstallButton", () => {
initializeInstalled();
});

afterEach(() => {
deleteInstalledList();
});

/**
* Sets up the InstallButton component for testing.
*
Expand All @@ -44,7 +40,7 @@ describe("InstallButton", () => {
await expect(findByTestId("button-loading")).rejects.toThrow();

const button = getByTestId("install") as HTMLButtonElement;
return { button, user };
return { button, getByTestId, user };
};

it('shows "Install" when addon is NOT installed', async () => {
Expand All @@ -68,11 +64,15 @@ describe("InstallButton", () => {

await user.click(button);

expect(getInstalled()).toContainEqual(testAddon);
expect(getInstalled().map(addon => addon._id)).toStrictEqual([
testAddon._id
]);

await user.click(button);

expect(getInstalled()).not.toContainEqual(testAddon);
expect(getInstalled().map(addon => addon._id)).not.toStrictEqual([
testAddon._id
]);
});

it('shows "Installing..." when installation is in progress', async () => {
Expand Down Expand Up @@ -103,6 +103,22 @@ describe("InstallButton", () => {
expect(button.disabled).toBe(true);
});

it("properly increments and decrements install count", async () => {
const { button, getByTestId, user } = await setupButton();

await user.click(button);

waitFor(() => {
expect(getByTestId("install-count")).toHaveTextContent("1 installs");
});

await user.click(button);

waitFor(() => {
expect(getByTestId("install-count")).toHaveTextContent("0 installs");
});
});

it('shows "Login to install" when user is not authorized', async () => {
const { button } = await setupButton(false);

Expand Down
23 changes: 13 additions & 10 deletions src/dataAccess/broker/broker.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,23 @@ export class MockBroker extends BrokerBase {
}

case "install": {
"addonID" in message.body
? addInstalled(generateAddon(Number(message.body.addonID)))
: console.warn(
`Invalid message body: ${JSON.stringify(message.body)}`
);
if ("addonID" in message.body) {
const addonID = message.body.addonID as string;
const addon = generateAddon(Number(addonID));
addInstalled(addon);
} else {
console.warn(`Invalid message body: ${JSON.stringify(message.body)}`);
}
break;
}

case "uninstall": {
"addonID" in message.body
? removeInstalled(message.body.addonID as string)
: console.warn(
`Invalid message body: ${JSON.stringify(message.body)}`
);
if ("addonID" in message.body) {
const addonID = message.body.addonID as string;
removeInstalled(addonID);
} else {
console.warn(`Invalid message body: ${JSON.stringify(message.body)}`);
}
break;
}

Expand Down
8 changes: 4 additions & 4 deletions src/dataAccess/broker/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ export const useInstallAddon = () => {
const { error, isPending, manageAddon } = useAddon();

/* eslint-disable -- dependency cannot change */
const installAddon = useCallback((addonId: string) => {
manageAddon({ action: "install", addonId });
const installAddon = useCallback(async (addonId: string) => {
await manageAddon({ action: "install", addonId });
}, []);
/* eslint-enable */

Expand All @@ -90,8 +90,8 @@ export const useUninstallAddon = () => {
const { error, isPending, manageAddon } = useAddon();

/* eslint-disable -- dependency cannot change */
const uninstallAddon = useCallback((addonId: string) => {
manageAddon({ action: "uninstall", addonId });
const uninstallAddon = useCallback(async (addonId: string) => {
await manageAddon({ action: "uninstall", addonId });
}, []);
/* eslint-enable */

Expand Down
1 change: 1 addition & 0 deletions src/mockData/addons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const generateAddon = (index: number): Addon => ({
authorId: authorList[(index - 1) % authorList.length].userId,
category: categories[(index - 1) % categories.length],
icon: "icon.png",
installCount: 0,
name: `Addon${index}`,
summary: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Addon ${index}.`
});
Expand Down
28 changes: 19 additions & 9 deletions src/pages/addonPage/addonList/addonCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import type { Addon } from "@polarexpress/types/addon";

import { Link } from "react-router-dom";
import InstallIcon from "../../../../assets/install.svg";

/**
* Defines the properties for the AddonCard component.
Expand All @@ -27,19 +28,28 @@ const AddonCard = ({ addOn }: AddonCardProperties) => {
return (
<div className="size-64 flex-none gap-4 text-center font-sans font-bold leading-7">
<Link
className="block h-full rounded-lg border border-gray-200 bg-white p-4 hover:shadow-md"
className="flex h-full flex-col rounded-lg border border-gray-200 bg-white p-4 hover:shadow-md"
data-testid="addon-card"
to={`/addons/${addOn._id}`}>
<h1 className="mt-2 text-2xl font-semibold">{addOn.name}</h1>
<div className="grow">
<h1 className="mt-2 truncate text-2xl font-semibold">{addOn.name}</h1>

{/* TODO: Fetch author name instead of id */}
<p className="mt-2 text-xs font-thin text-gray-400">
Author: {addOn.authorId}
</p>
{/* TODO: Fetch author name instead of id */}
<p className="mt-2 truncate text-xs font-thin text-gray-400">
Author: {addOn.authorId}
</p>

<p className="mt-2 overflow-x-hidden text-lg font-normal text-gray-700">
{addOn.summary.split(" ").slice(0, 15).join(" ")}
</p>
<p className="mt-2 line-clamp-3 text-lg font-normal text-gray-700">
{addOn.summary}
</p>
</div>

<div className="mt-4 flex items-center justify-center">
<img className="mr-2 h-6 text-gray-600" src={InstallIcon} />
<div className="text-lg font-semibold text-gray-800">
{addOn.installCount}
</div>
</div>
</Link>
</div>
);
Expand Down
21 changes: 16 additions & 5 deletions src/pages/addonPage/addonPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
LoadingSpinner,
RTKError
} from "@polarexpress/components";
import InstallIcon from "../../../assets/install.svg";
import {
useGetAddonsByUserId,
useInstallAddon,
Expand Down Expand Up @@ -39,7 +40,8 @@ const AddonPage = () => {
const {
data: addon,
error: addonError,
isLoading: isAddonLoading
isLoading: isAddonLoading,
refetch
} = useGetAddonByIdQuery(thisId ?? "");

const {
Expand Down Expand Up @@ -83,15 +85,16 @@ const AddonPage = () => {
* Handles the installation or uninstallation of the add-on. Changes the
* internal installed state as well.
*/
const handleInstall = () => {
const handleInstall = async () => {
if (auth.authorized) {
if (installed) {
uninstallAddon(thisId ?? "");
await uninstallAddon(thisId ?? "");
setInstalled(false);
} else {
installAddon(thisId ?? "");
await installAddon(thisId ?? "");
setInstalled(true);
}
refetch();
} else {
// TODO: Should redirect to the login page when it exists
console.warn("User is not logged in. Redirecting to login page.");
Expand All @@ -114,13 +117,21 @@ const AddonPage = () => {
</div>
);

if (addon !== undefined) {
if (addon) {
return (
<div className="m-8 font-sans leading-10" data-testid="addon-page">
<div className="mb-2 border-b-2 pb-2 text-center">
<h1 className="text-4xl font-bold">{addon.name}</h1>
<p className="text-sm font-light">{addon.authorId}</p>
<p>{addon.summary}</p>
<div className="mt-2 flex items-center justify-center">
<img className="mr-2 h-6 text-gray-600" src={InstallIcon}></img>
<div
className="text-lg font-semibold text-gray-800"
data-testid="install-count">
{addon.installCount} installs
</div>
</div>
<div className="my-4 flex justify-center">
<InstallButton
authorized={auth.authorized ?? false}
Expand Down
36 changes: 30 additions & 6 deletions src/test/mockingUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function createBroker(): BrokerBase {
*/
export const initializeInstalled = () => {
sessionStorage.setItem("installed", JSON.stringify([]));
sessionStorage.setItem("installCounts", JSON.stringify({}));
};

/**
Expand All @@ -41,6 +42,14 @@ export const removeInstalled = (addonID: string): void => {
addon => addon._id !== addonID
);
sessionStorage.setItem("installed", JSON.stringify(updatedAddons));

const installCounts = JSON.parse(
sessionStorage.getItem("installCounts") || "{}"
);
if (installCounts[addonID]) {
installCounts[addonID]--;
}
sessionStorage.setItem("installCounts", JSON.stringify(installCounts));
};

/**
Expand All @@ -54,13 +63,16 @@ export const addInstalled = (addon: Addon): void => {
);
installedAddons.push(addon);
sessionStorage.setItem("installed", JSON.stringify(installedAddons));
};

/**
* Deletes the list of installed addons from sessionStorage.
*/
export const deleteInstalledList = () => {
sessionStorage.removeItem("installed");
const installCounts = JSON.parse(
sessionStorage.getItem("installCounts") || "{}"
);
if (installCounts[addon._id]) {
installCounts[addon._id]++;
} else {
installCounts[addon._id] = 1;
}
sessionStorage.setItem("installCounts", JSON.stringify(installCounts));
};

/**
Expand All @@ -76,3 +88,15 @@ export const getInstalled = (): Addon[] => {
);
return installedAddons;
};

/**
* Retrieves the install counts from sessionStorage.
*
* @returns A record of addon IDs and their install counts.
*/
export const getInstallCounts = (): Record<string, number> => {
const installCounts = JSON.parse(
sessionStorage.getItem("installCounts") || "{}"
);
return installCounts;
};
23 changes: 17 additions & 6 deletions src/test/mswHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { shortAddonList } from "@polarexpress/mockData/addons";
import { AddonCategory } from "@polarexpress/types/addon";
import { HttpResponse, http, passthrough } from "msw";
import { getInstallCounts } from "./mockingUtils";

const baseUrl = import.meta.env.VITE_API_BASE;

Expand All @@ -34,25 +35,35 @@ export const handlers = [
);
}

const installCounts = getInstallCounts();
const updatedAddons = filteredAddons.map(addon => ({
...addon,
installCount: installCounts[addon._id] || 0
}));

const pageSize = 20;
const startIndex = page * pageSize;
const endIndex = startIndex + pageSize;

const paginatedAddons = filteredAddons.slice(startIndex, endIndex);
const totalPages = Math.ceil(filteredAddons.length / pageSize);
const paginatedAddons = updatedAddons.slice(startIndex, endIndex);
const totalPages = Math.ceil(updatedAddons.length / pageSize);

return HttpResponse.json({ addons: paginatedAddons, totalPages });
}),

http.post(`${baseUrl}/addons/get-by-id`, async ({ request }) => {
const body = (await request.json()) as {
id: string;
};
const body = (await request.json()) as { id: string };
const addonId = body.id;

const addon = shortAddonList.find(addon => addon._id === addonId);
const installCounts = getInstallCounts();
const addonWithInstallCount = addon
? { ...addon, installCount: installCounts[addon._id] || 0 }
: undefined;

return addon ? HttpResponse.json({ addon: addon }) : HttpResponse.json();
return addonWithInstallCount
? HttpResponse.json({ addon: addonWithInstallCount })
: HttpResponse.json();
}),

http.post(`${baseUrl}/addons/get-readme`, async ({ request }) => {
Expand Down
2 changes: 2 additions & 0 deletions src/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import "@testing-library/jest-dom/vitest";
import { setupServer } from "msw/node";

import { handlers } from "./mswHandlers";
import { initializeInstalled } from "./mockingUtils";

// Set up http handlers during testing
export const server = setupServer(...handlers);
Expand All @@ -19,4 +20,5 @@ afterAll(() => server.close());
beforeEach(() => {
vi.restoreAllMocks();
server.resetHandlers();
initializeInstalled();
});
4 changes: 4 additions & 0 deletions src/types/addon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export interface Addon {
* Link to the icon of the addon.
*/
icon: string;
/**
* Number of times this addon was installed.
*/
installCount: number;
/**
* Display name of the add-on.
*/
Expand Down
Loading