Skip to content
Open
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
31 changes: 17 additions & 14 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
node_modules
package-lock.json

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local
.env
cypress/

# IDE
# Editor directories and files
.vscode/*
!.vscode/tasks.js
!.vscode/extensions.json
*.coverage
*.coveragexml
.idea

# Optional npm cache directory
.npm

# OS generated files #
.DS_Store
.DS_Store?
*.local
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
1 change: 1 addition & 0 deletions _mocks_/fileMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = "test-file-stub";
56 changes: 56 additions & 0 deletions _tests_/e2e/Dashboard.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
describe("Dashboard Page Full Flow", () => {
beforeEach(() => {
cy.uiLogin();
});

it("can add, view, edit, search, and delete a client", () => {
const uniqueName = `John Test ${Date.now()}`;

cy.get("button").contains("+ Add Client").click();
cy.get("input[name='name']").type(uniqueName);
cy.get("input[name='company']").type("Test Co");
cy.get("input[name='age']").type("35");
cy.get("select[name='gender']").select("male");
cy.get("select[name='currency']").select("USD");
cy.get("input[name='subscriptionCost']").type("99.99");
cy.get("button[type='submit']").click();
cy.contains("Client added successfully").should("exist");

cy.contains("[data-cy='client-table'] tr", uniqueName).within(() => {
cy.get("[data-cy='view-btn']").click();
});
cy.get("[data-cy='client-view']").should("be.visible");
cy.contains("Client Profile").should("exist");
cy.get("button").contains("×").click();

cy.contains("[data-cy='client-table'] tr", uniqueName).within(() => {
cy.get("[data-cy='edit-btn']").click();
});
cy.get("input[name='company']").clear().type("Updated Co");
cy.get("button[type='submit']").click();
cy.contains("Client updated successfully").should("exist");

cy.wait(3000); // - wait for toast
cy.get("[data-cy='search-input']").clear().type(uniqueName);
cy.contains("[data-cy='client-table'] tr", uniqueName).should("exist");

cy.contains("[data-cy='client-table'] tr", uniqueName).within(() => {
cy.get("[data-cy='delete-btn']").click();
});

cy.contains("Are you sure you want to delete this client?").should("exist");
cy.get("button").contains("Delete").click();
cy.wait(3000); // - wait for toast
cy.contains("Client deleted").should("exist");

cy.get("[data-cy='search-input']").clear().type(uniqueName);
cy.contains("[data-cy='client-table'] tr", uniqueName).should("not.exist");
cy.get("[data-cy='search-input']").clear();
});
it("can logout and go back to home page", () => {
cy.get("[data-cy='logout-btn']").click();

cy.url().should("include", "login");
cy.contains("Login").should("exist");
});
});
27 changes: 27 additions & 0 deletions _tests_/e2e/HomePage.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
describe("HomePage", () => {
beforeEach(() => {
cy.visit("/");
});
it("shows login form on load", () => {
cy.get("form#login-form").should("exist");
cy.get("input[placeholder='Username']").should("exist");
cy.get("input[placeholder='Password']").should("exist");
cy.contains("Login").should("exist");
});

it("switches to register form", () => {
cy.contains(/register/i).click();

cy.get("form#register-form").should("exist");
cy.get("input[placeholder='Confirm Password']").should("exist");
cy.contains("Register").should("exist");
});

it("switches to forgot password form", () => {
cy.contains(/forgot password/i).click();

cy.get("form#forgot-form").should("exist");
cy.get("input[placeholder='Enter your username']").should("exist");
cy.contains("Reset Password").should("exist");
});
});
50 changes: 50 additions & 0 deletions _tests_/e2e/auth/ForgotPassword.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
describe("Forgot Password Flow", () => {
beforeEach(() => {
cy.visit("/?mode=forgot");
});

it("shows validation error when username is empty", () => {
cy.get('button[type="submit"]').click();
cy.contains(/username is required/i).should("exist");
});

it("shows error for non-existent user", () => {
cy.get('input[placeholder="Enter your username"]').type("nonexistentuser");
cy.get('button[type="submit"]').click();

cy.contains("User not found.").should("exist");
});

it("opens password modal when user exists", () => {
cy.get('input[placeholder="Enter your username"]').type("testuser");
cy.get('button[type="submit"]').click();

cy.contains("Enter New Password").should("exist");
});

it("submits new password and closes modal", () => {
cy.get('input[placeholder="Enter your username"]').type("testuser");
cy.get('button[type="submit"]').click();

cy.get('input[placeholder="New password"]').type("newpass123");
cy.get('button[type="submit"]').contains("Submit").click();

cy.contains("Password updated successfully").should("exist");
cy.contains("Enter New Password").should("not.exist");
});

it("cancels password reset modal", () => {
cy.get('input[placeholder="Enter your username"]').type("testuser");
cy.get('button[type="submit"]').click();

cy.contains("Enter New Password").should("exist");
cy.get("button").contains("Cancel").click();
cy.contains("Enter New Password").should("not.exist");
});
it("navigates back to login form", () => {
cy.contains(/back to login/i).click();

cy.url().should("include", "mode=login");
cy.get("form#login-form").should("exist");
});
});
56 changes: 56 additions & 0 deletions _tests_/e2e/auth/Login.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
describe("Login Flow", () => {
beforeEach(() => {
cy.visit("/");
});

it("logs in successfully with valid credentials", () => {
const username = Cypress.env("username");
const password = Cypress.env("password");
console.log({ username, password });
cy.get("input[placeholder='Username']").type(username);
cy.get("input[placeholder='Password']").type(password);

cy.contains("Login").click();

cy.url().should("include", "/dashboard");
});
it("shows error on invalid credentials", () => {
cy.visit("/");

cy.get("input[placeholder='Username']").type("wronguser");
cy.get("input[placeholder='Password']").type("wrongpass");

cy.contains("Login").click();

cy.contains(/invalid username or password/i).should("be.visible");

cy.url().should("eq", Cypress.config().baseUrl + "/");
});
it("shows required field errors", () => {
cy.contains("Login").click();

cy.contains(/username is required/i).should("be.visible");
cy.contains(/password is required/i).should("be.visible");
});
it("shows validation error for short password", () => {
cy.get("input[placeholder='Username']").type("testuser");
cy.get("input[placeholder='Password']").type("123");

cy.contains("Login").click();

cy.contains(/password must be at least/i).should("be.visible");
});
it("navigates to forgot password", () => {
cy.contains("Forgot Password?").click();

cy.url().should("include", "/?mode=forgot");
cy.get("form#forgot-form").should("exist");
});

it("navigates to register", () => {
cy.contains("Register").click();

cy.url().should("include", "/?mode=register");
cy.get("form#register-form").should("exist");
});
});
65 changes: 65 additions & 0 deletions _tests_/e2e/auth/Register.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
describe("Register Flow", () => {
beforeEach(() => {
cy.visit("/?mode=register");
});

it("registers a new user successfully with a random username", () => {
const randomUsername = `user_${Date.now()}`;

cy.get('input[placeholder="Username"]').type(randomUsername);
cy.get('input[placeholder="Password"]').type("securepass");
cy.get('input[placeholder="Confirm Password"]').type("securepass");

cy.get("form#register-form").submit();

cy.url().should("not.include", "mode=register");
});

it("shows error when username already exists", () => {
const existingUsername = Cypress.env("username");

cy.get('input[placeholder="Username"]').type(existingUsername);
cy.get('input[placeholder="Password"]').type("securepass");
cy.get('input[placeholder="Confirm Password"]').type("securepass");

cy.get("form#register-form").submit();

cy.on("window:alert", (text) => {
expect(text).toMatch(/username already taken/i);
});
});

it("shows error for mismatched passwords", () => {
cy.get('input[placeholder="Username"]').type("testuser123");
cy.get('input[placeholder="Password"]').type("securepass");
cy.get('input[placeholder="Confirm Password"]').type("wrongpass");

cy.get("form#register-form").submit();

cy.contains(/passwords do not match/i).should("exist");
});

it("shows error for short password", () => {
cy.get('input[placeholder="Username"]').type("testuser123");
cy.get('input[placeholder="Password"]').type("123");
cy.get('input[placeholder="Confirm Password"]').type("123");

cy.get("form#register-form").submit();

cy.contains(/password must be at least/i).should("exist");
});

it("shows validation error for empty fields", () => {
cy.get("form#register-form").submit();

cy.contains(/username is required/i).should("exist");
cy.contains(/password is required/i).should("exist");
});

it("navigates back to login form", () => {
cy.contains(/back to login/i).click();

cy.url().should("include", "mode=login");
cy.get("form#login-form").should("exist");
});
});
18 changes: 18 additions & 0 deletions _tests_/e2e/support.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/// <reference types="cypress" />

declare global {
namespace Cypress {
interface Chainable {
uiLogin(): Chainable<void>;
}
}
}

Cypress.Commands.add("uiLogin", () => {
cy.visit("/");
cy.get("input[placeholder='Username']").type(Cypress.env("username"));
cy.get("input[placeholder='Password']").type(Cypress.env("password"));
cy.get("button[type='submit']").click();
});

export {};
24 changes: 24 additions & 0 deletions _tests_/unit/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { render, screen } from "@testing-library/react";
import App from "../../src/App";

jest.mock("sonner", () => ({
Toaster: () => <div data-testid="toaster" />,
}));

jest.mock("../../src/providers/ModalProvider", () => () => (
<div data-testid="modal-provider" />
));

jest.mock("../../src/routes/AppRoutes", () => () => (
<div data-testid="app-routes">Mocked Routes</div>
));

describe("App", () => {
it("renders Toaster, ModalProvider, and AppRoutes", () => {
render(<App />);

expect(screen.getByTestId("toaster")).toBeInTheDocument();
expect(screen.getByTestId("modal-provider")).toBeInTheDocument();
expect(screen.getByTestId("app-routes")).toHaveTextContent("Mocked Routes");
});
});
Loading