Skip to content

Commit

Permalink
testing(ui): banner (#715)
Browse files Browse the repository at this point in the history
* chore: `newuserPrompt` -> `newUserPrompt`

* feat: create initial `banner` component

* chore: all `alert` with `banner`

* chore: add `dialog-footer` and tests for button labels

* chore: remove `alertContext`

* chore: abstract the observer logic into its separate file

* chore: add conditional pathname logic for banner displaying

* chore: new banner tests

* chore: one more test

* chore: remove `pathname` for `useEffect` and rename private variable

* fix: move banner and navigate under polling; improve logic

* fix: move `banner` after poller
  • Loading branch information
asharonbaltazar authored Aug 16, 2024
1 parent 54be6e2 commit 073cbd4
Show file tree
Hide file tree
Showing 26 changed files with 405 additions and 390 deletions.
81 changes: 81 additions & 0 deletions react-app/src/components/Banner/banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useEffect, useState } from "react";
import { Alert, AlertVariant } from "../Alert";
import { Check, X } from "lucide-react";
import { useLocation } from "react-router-dom";
import { Observer } from "@/utils/basic-observable";

export type Banner = {
header: string;
body: string;
variant?: AlertVariant;
pathnameToDisplayOn: string;
};

class BannerObserver extends Observer<Banner> {
create = (data: Banner) => {
this.publish(data);
this.observed = { ...data };
};

dismiss = () => {
this.publish(null);
this.observed = null;
};
}

const bannerState = new BannerObserver();

export const banner = (newBanner: Banner) => {
return bannerState.create(newBanner);
};

export const Banner = () => {
const [activeBanner, setActiveBanner] = useState<Banner | null>(null);
const { pathname } = useLocation();

const onClose = () => {
bannerState.dismiss();
};

useEffect(() => {
const unsubscribe = bannerState.subscribe((banner) => {
if (banner) {
setActiveBanner(banner);
} else {
setActiveBanner(null);
}
});

return unsubscribe;
}, []);

useEffect(() => {
if (activeBanner && activeBanner.pathnameToDisplayOn !== pathname) {
onClose();
}
}, [pathname, activeBanner]);

if (activeBanner && activeBanner.pathnameToDisplayOn === pathname) {
return (
<Alert
variant={activeBanner.variant}
className="mt-4 mb-8 flex-row text-sm"
>
<div className="flex items-start justify-between">
<Check />
<div className="ml-2 w-full">
<h3 className="text-lg font-bold" data-testid="banner-header">
{activeBanner.header}
</h3>
<p data-testid="banner-body">{activeBanner.body}</p>
</div>
<button onClick={onClose} data-testid="banner-close">
<X size={20} />
</button>
</div>
</Alert>
);
}

return null;
};
1 change: 1 addition & 0 deletions react-app/src/components/Banner/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./banner";
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export function ConfirmationDialog({
className={cn({
"flex-col sm:flex-row-reverse sm:justify-start": areButtonsReversed,
})}
data-testid="dialog-footer"
>
{acceptButtonVisible && (
<Button
Expand Down
19 changes: 19 additions & 0 deletions react-app/src/components/ConfirmationDialog/userPrompt.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,23 @@ describe("userPrompt", () => {

expect(mockOnCancel).toHaveBeenCalled();
});

test("Custom Accept and Cancel button texts are applied", async () => {
const { getByTestId } = render(<UserPrompt />);

act(() => {
userPrompt({
header: "Testing",
body: "testing body",
onAccept: vi.fn(),
acceptButtonText: "Custom Accept",
cancelButtonText: "Custom Cancel",
});
});

const { children: dialogFooterChildren } = getByTestId("dialog-footer");

expect(dialogFooterChildren.item(0).textContent).toEqual("Custom Accept");
expect(dialogFooterChildren.item(1).textContent).toEqual("Custom Cancel");
});
});
34 changes: 7 additions & 27 deletions react-app/src/components/ConfirmationDialog/userPrompt.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { ConfirmationDialog } from "@/components/ConfirmationDialog";
import { Observer } from "@/utils/basic-observable";

export type UserPrompt = {
header: string;
Expand All @@ -11,43 +12,22 @@ export type UserPrompt = {
onCancel?: () => void;
};

class Observer {
subscribers: Array<(userPrompt: UserPrompt) => void>;
userPrompt: UserPrompt | null;

constructor() {
this.subscribers = [];
this.userPrompt = null;
}

subscribe = (subscriber: (userPrompt: UserPrompt | null) => void) => {
this.subscribers.push(subscriber);

return () => {
const index = this.subscribers.indexOf(subscriber);
this.subscribers.splice(index, 1);
};
};

private publish = (data: UserPrompt | null) => {
this.subscribers.forEach((subscriber) => subscriber(data));
};

class UserPromptObserver extends Observer<UserPrompt> {
create = (data: UserPrompt) => {
this.publish(data);
this.userPrompt = { ...data };
this.observed = { ...data };
};

dismiss = () => {
this.publish(null);
this.userPrompt = null;
this.observed = null;
};
}

const userPromptState = new Observer();
const userPromptState = new UserPromptObserver();

export const userPrompt = (newuserPrompt: UserPrompt) => {
return userPromptState.create(newuserPrompt);
export const userPrompt = (newUserPrompt: UserPrompt) => {
return userPromptState.create(newUserPrompt);
};

export const UserPrompt = () => {
Expand Down
111 changes: 111 additions & 0 deletions react-app/src/components/Container/banner.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { ReactNode } from "react";
import { Link, MemoryRouter, Route, Routes } from "react-router-dom";
import { describe, expect, test } from "vitest";
import { act, render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { banner, Banner } from "../Banner";

const wrapper = ({ children }: { children: ReactNode }) => (
<MemoryRouter initialEntries={["/dashboard"]}>
<Routes>
<Route
path="/dashboard"
element={<Link to="/example" id="dashboard-link" />}
/>
<Route
path="/example"
element={<Link to="/dashboard" id="example-link" />}
/>
</Routes>
{children}
</MemoryRouter>
);

describe("banner", () => {
test("Hidden on initial render", () => {
const { queryByTestId } = render(<Banner />, { wrapper });

expect(queryByTestId("banner-header")).not.toBeInTheDocument();
});

test("Check if banner is not rendered on wrong pathnameToDisplayOn", () => {
const { queryByTestId } = render(<Banner />, { wrapper });

act(() => {
banner({
header: "Test header",
body: "Test body",
pathnameToDisplayOn: "/",
});
});

expect(queryByTestId("banner-header")).not.toBeInTheDocument();
});

test("Check if banner is visible on correct pathnameToDisplayOn", () => {
const { getByTestId } = render(<Banner />, { wrapper });

act(() => {
banner({
header: "Test header",
body: "Test body",
pathnameToDisplayOn: "/dashboard",
});
});

expect(getByTestId("banner-header")).toHaveTextContent("Test header");
});

test("Check if banner header and body text match", () => {
const { getByText } = render(<Banner />, { wrapper });

act(() => {
banner({
header: "Test header",
body: "Test body",
pathnameToDisplayOn: "/dashboard",
});
});

expect(getByText("Test header")).toBeInTheDocument();
expect(getByText("Test body")).toBeInTheDocument();
});

test("Check if banner is closed when clicking the Close button", async () => {
const { getByTestId, queryByTestId } = render(<Banner />, {
wrapper,
});
const user = userEvent.setup();

act(() => {
banner({
header: "Test header",
body: "Test body",
pathnameToDisplayOn: "/dashboard",
});
});

await user.click(getByTestId("banner-close"));

expect(queryByTestId("banner-header")).not.toBeInTheDocument();
});

test("Check if banner is closed when navigating away", async () => {
const { container, queryByTestId } = render(<Banner />, {
wrapper,
});
const user = userEvent.setup();

act(() => {
banner({
header: "Test header",
body: "Test body",
pathnameToDisplayOn: "/dashboard",
});
});

await user.click(container.querySelector("#dashboard-link"));

expect(queryByTestId("banner-header")).not.toBeInTheDocument();
});
});
64 changes: 0 additions & 64 deletions react-app/src/components/Context/alertContext.test.tsx

This file was deleted.

Loading

0 comments on commit 073cbd4

Please sign in to comment.