Skip to content

Commit

Permalink
[frontend] feat: Logout logic
Browse files Browse the repository at this point in the history
  • Loading branch information
helloitsdave committed Mar 24, 2024
1 parent 9857fb9 commit 29074fb
Show file tree
Hide file tree
Showing 12 changed files with 145 additions and 24 deletions.
6 changes: 3 additions & 3 deletions backend/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ export const seed = [
"userID": "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272"
},
{
"title": "Shopping List",
"content": "Milk, eggs, bread, and fruits.",
"title": "Different User - scoping check",
"content": "Should not see this note with Test User",
"createdAt": "2024-02-05T23:33:42.253Z",
"updatedAt": "2024-02-05T23:33:42.253Z",
"userID": "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272"
"userID": "dcf89a7e-b941-4f17-bbe0-4e0c8b2cd272"
},
{
"title": "Recipe",
Expand Down
16 changes: 11 additions & 5 deletions backend/tests/service/notes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,15 @@ describe("Authenticated Flows", () => {
token = response.body.token;
});

test("Get the list of Notes", async () => {
test("Should only see notes for the given user ", async () => {
getNoteResponse = await request(NOTES_URL)
.get("/")
.set("Authorization", `Bearer ${token}`);

expect(getNoteResponse.status).toBe(200);
expect(getNoteResponse.body).toHaveLength(9);

/** Should see 8 notes instead of all 9 in the database */
expect(getNoteResponse.body).toHaveLength(8);

expect(getNoteResponse.body[getNoteResponse.body.length - 1]).toStrictEqual(
{
Expand All @@ -71,6 +74,9 @@ describe("Authenticated Flows", () => {
userID: "ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272",
}
);

/** Should not see notes for a different user */
expect(getNoteResponse.body.find((note: any) => note.title === "Different User - scoping check")).toBeUndefined();
});

test("Create a new Note", async () => {
Expand All @@ -96,7 +102,7 @@ describe("Authenticated Flows", () => {
.get("/")
.set("Authorization", `Bearer ${token}`);
expect(getNoteResponse.status).toBe(200);
expect(getNoteResponse.body).toHaveLength(10);
expect(getNoteResponse.body).toHaveLength(9);
});

test("Update a Note", async () => {
Expand All @@ -115,7 +121,7 @@ describe("Authenticated Flows", () => {
.get("/")
.set("Authorization", `Bearer ${token}`);
expect(getNoteResponse.status).toBe(200);
expect(getNoteResponse.body).toHaveLength(10);
expect(getNoteResponse.body).toHaveLength(9);

// Updated note should appear first in the list
expect(getNoteResponse.body[0].id).toBe(createdID);
Expand All @@ -131,7 +137,7 @@ describe("Authenticated Flows", () => {
.get("/")
.set("Authorization", `Bearer ${token}`);
expect(getNoteResponse.status).toBe(200);
expect(getNoteResponse.body).toHaveLength(9);
expect(getNoteResponse.body).toHaveLength(8);
});

test("Error handling: Attempt to Delete a Note with invalid ID", async () => {
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/service/users.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe("Authenticated Flows", () => {
expect(getUsersResponse.body.length).toBeGreaterThan(0);
});

test("Usernames should be unique", async () => {
test("Username's should be unique", async () => {
const response = await request(USERS_URL)
.post("/")
.set("Authorization", `Bearer ${token}`)
Expand Down
15 changes: 14 additions & 1 deletion frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ body {
flex-direction: column;
width: 100vw;
}

.action-header {
width: 40%;
}
}

.login-page {
Expand Down Expand Up @@ -110,13 +114,22 @@ h2 {

.app-header {
display: flex;
flex-direction: column;
flex-direction: row;
justify-content: center;
align-items: center;
margin-bottom: 20px;
width: 100%;
}

.action-header {
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: center;
margin-bottom: 5px;
width: 100% vw;
}

.note-form {
display: flex;
flex-direction: column;
Expand Down
60 changes: 60 additions & 0 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import App from "./App";

describe("App", () => {
it("renders correctly", () => {
render(<App />);

expect(screen.getByTestId("app-logo")).toBeInTheDocument();
expect(screen.getByText("Login")).toBeInTheDocument();

expect(localStorage.getItem("token")).toBeNull();

expect(screen.queryByTestId("note-app")).not.toBeInTheDocument();
});

it("renders correctly when logged in", async () => {
render(<App />);

const username = screen.getByTestId("username");
const password = screen.getByTestId("password");
const loginButton = screen.getByText("Login");

userEvent.type(username, "test");
userEvent.type(password, "pass");
userEvent.click(loginButton);

const noteApp = await screen.findByTestId("note-app");

expect(noteApp).toBeInTheDocument();

expect(localStorage.getItem("token")).not.toBeNull();
});

it("renders correctly when logged out", async () => {
render(<App />);

const username = screen.getByTestId("username");
const password = screen.getByTestId("password");
const loginButton = screen.getByText("Login");

userEvent.type(username, "test");
userEvent.type(password, "pass");
userEvent.click(loginButton);

const noteApp = await screen.findByTestId("note-app");

expect(noteApp).toBeInTheDocument();

const logoutButton = screen.getByText("Logout");

userEvent.click(logoutButton);

expect(localStorage.getItem("token")).toBeNull();

expect(screen.queryByTestId("note-app")).not.toBeInTheDocument();

expect(screen.getByText("Login")).toBeInTheDocument();
});
});
7 changes: 6 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@ function App() {
setLoggedIn(true);
};

const handleLogout = () => {
setLoggedIn(false);
localStorage.removeItem("token");
};

return (
<Router>
<Header />
<Routes>
<Route path="/" element={loggedIn ? <Navigate to="/notes" /> : <Login onLogin={handleLogin} />} />
<Route path="/notes" element={loggedIn ? <NoteApp /> : <Navigate to="/" replace />} />
<Route path="/notes" element={loggedIn ? <NoteApp onLogout={handleLogout}/> : <Navigate to="/" replace />} />
</Routes>
</Router>
);
Expand Down
24 changes: 17 additions & 7 deletions frontend/src/NoteApp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { errorHandlers } from "./mocks/handlers";


test("Notes NoteApp loads with notes", async () => {
render(<NoteApp />);
render(<NoteApp onLogout={() => {}} />);

expect(await screen.findByTestId('spinner-container')).not.toBeInTheDocument();

Expand All @@ -17,7 +17,7 @@ test("Notes NoteApp loads with notes", async () => {
});

test('User can select and update note', async () => {
render(<NoteApp />);
render(<NoteApp onLogout={() => {}} />);

expect(await screen.findByText("Test Title Note 1")).toBeInTheDocument();
// Click on the first note
Expand All @@ -42,7 +42,7 @@ test('User can select and update note', async () => {
});

test('User can add a Add a note', async () => {
render(<NoteApp />);
render(<NoteApp onLogout={() => {}} />);

const addButton = await screen.findByRole("button", { name: "Add a note" });
userEvent.click(addButton);
Expand All @@ -65,7 +65,7 @@ test('User can add a Add a note', async () => {
});

test('User can delete a note', async () => {
render(<NoteApp />);
render(<NoteApp onLogout={() => {}} />);

// Click on the first note
const noteTitle = await screen.findByText("Test Title Note 2");
Expand All @@ -90,14 +90,14 @@ test('User can delete a note', async () => {
test('Connection Error is displayed on Notes fetch', async () => {
mswServer.use(...errorHandlers);

render(<NoteApp />);
render(<NoteApp onLogout={() => {}} />);

expect(await screen.findByRole("heading", { name: "Warning: API Connection Issue"})).toBeInTheDocument();
});

test('Connection Error is displayed on Create Note', async () => {
// Render the NoteApp component
render(<NoteApp />);
render(<NoteApp onLogout={() => {}}/>);

const addButton = await screen.findByRole("button", { name: "Add a note" });
userEvent.click(addButton);
Expand Down Expand Up @@ -127,7 +127,7 @@ test('Connection Error is displayed on Create Note', async () => {
// });

test('Connection Error is displayed on Update Note', async () => {
render(<NoteApp />);
render(<NoteApp onLogout={() => {}}/>);

// Click on the first note
const notes = await screen.findAllByTestId("note");
Expand All @@ -143,4 +143,14 @@ test('Connection Error is displayed on Update Note', async () => {
userEvent.click(saveButton);

expect(await screen.findByRole("heading", { name: "Warning: API Connection Issue"})).toBeInTheDocument();
});

test('User can logout', async () => {
const onLogout = vitest.fn();
render(<NoteApp onLogout={onLogout} />);

const logoutButton = await screen.findByRole("button", { name: "Logout" });
userEvent.click(logoutButton);

expect(onLogout).toHaveBeenCalledTimes(1);
});
18 changes: 15 additions & 3 deletions frontend/src/NoteApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import Spinner from "./components/Spinner";
import type NoteType from "./types/note";
import { postNote, patchNote, getNotes, removeNote } from "./api/apiService";

function NoteApp() {
export interface LogoutProps {
onLogout: () => void;
}

const NoteApp: React.FC<LogoutProps> = ({ onLogout }) => {
const [notes, setNotes] = useState<NoteType[]>([]);
const [selectedNote, setSelectedNote] = useState<NoteType | null>(null);
const [connectionIssue, setConnectionIssue] = useState<boolean>(false);
Expand Down Expand Up @@ -81,8 +85,8 @@ function NoteApp() {
};

return (
<div className="app-container">
<div className="app-header">
<div className="app-container" data-testid="note-app">
<div className="action-header">
<Button
className="add-note-button"
type="primary"
Expand All @@ -91,6 +95,14 @@ function NoteApp() {
>
Add a note
</Button>
<Button
className="logout-button"
type="primary"
style={{ marginBottom: "20px" }}
onClick={() => onLogout()}
>
Logout
</Button>
</div>

{connectionIssue && (
Expand Down
1 change: 0 additions & 1 deletion frontend/src/api/apiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,5 @@ export const login = async (username: string, password: string) => {
},
}
);
console.log(response);
return response;
};
9 changes: 9 additions & 0 deletions frontend/src/components/Header.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { render, screen } from '@testing-library/react';
import Header from './Header';

describe('Header', () => {
it('renders correctly', () => {
render(<Header />);
expect(screen.getByTestId('app-logo')).toBeInTheDocument();
});
});
2 changes: 1 addition & 1 deletion frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const Header = () => {
return (
<header className="app-header">
<img className="app-logo" src="notes.png" alt="note icon" />
<img className="app-logo" src="notes.png" alt="note icon" data-testid="app-logo"/>
</header>
);
};
Expand Down
9 changes: 8 additions & 1 deletion playwright/tests/note.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ test.beforeAll(async ({ browser }, { timeout }) => {
});

test('Notes App e2e flow', async () => {
await test.step('Should see the login form', async () => {
await test.step('Should be able to login with valid user credentials', async () => {
await expect(page.getByPlaceholder('Username')).toBeVisible();
await expect(page.getByPlaceholder('Password')).toBeVisible();

Expand Down Expand Up @@ -96,5 +96,12 @@ test('Notes App e2e flow', async () => {
await expect(page.getByTestId('note-content').first()).not.toHaveText(
NOTE_CONTENT
);
// Close the modal
await page.locator('button[class*="modal-close"]').click();
});

await test.step('Should be able to logout', async () => {
await page.getByRole('button', { name: 'Logout' }).click();
await expect(page.getByPlaceholder('Username')).toBeVisible();
});
});

0 comments on commit 29074fb

Please sign in to comment.