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
66 changes: 38 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,71 +2,81 @@

Hello there!

Your task is to implement a new Stepper component
### Task Overview

Implemented a new Stepper component
based on [these specifications in Figma](https://www.figma.com/design/gvf4xjR6f9rvaj94BPFFyZ/Frontend-Coding-Challenge?node-id=0-1&t=tHGuIxWbpBVVsH5B-1).
Make sure your component behaves as expected by
creating tests for it.

## File structure
The relevant files are these

```text
__tests__/
components/
stepper/
stepper.test.tsx
-> Implement your tests here
-> Contains tests for the Stepper component, verifying that it renders correctly and displays the steps.
stepPage.test.tsx
-> Contains tests for the StepPage component, verifying that it renders the correct content based on the route, calls the `setCurrentStep` function with the correct index, and handles invalid routes.

components/
stepper/
stepper.tsx
stepper.tsx
-> The stepper component
steps.ts
-> You might want to use this to
configure the steps
-> Configuring the steps
stepMapping.tsx
-> step mapping for each page
stepPage.ts
-> For creating dynamic routes
hooks/
useStepper.ts
-> Custom hook that handles navigation

context/
stepperContext.tsx
-> Contains React context providers for managing application state- `StepperContext`

pages/
index.tsx
-> The main page (localhost:3000)
termin.tsx, fahrzeug.tsx, kontakt.tsx:
-> Page components that use the StepPage component.
```

## Architecture Diagram

![Project Architecture](public/architectureDiagram.png)

## Setting up everything

Install the dependencies

```bash
npm install
```

Start the dev server to get a preview

```bash
npm run dev
```

Check out the page on [localhost:3000](http://localhost:3000)

To run the tests

```bash
npm run test
```

## Getting started

Get started by having a look at the relevant files
and then start implementing the Stepper component in
`/components/stepper/stepper.tsx`.

## Things to consider

Please use **Tailwind CSS** to style the component.
Please **do not install any additional packages**.

Make sure the component

- aligns **exactly** with the Figma spec (we already
implemented the page layout for you,
you just have to worry about the stepper component)
- is **reusable**
- works with **different sets of steps**
- works well on **all breakpoints**
## Features & Functionality

Good luck! :)
- Stepper Component implementation ✅
- Steps configured ✅
- useStepper hook to encapsulate the stepper logic and manage navigation of steps and routing ✅
- Responsiveness ✅
- StepperContext to manage the stepper state (steps, current step) and functions (handleNextStep, setCurrentStep) ✅
- Dynamic step management ✅
- Basic error handling to display an "Invalid Step" message if the route does not match a valid step. ✅
- StepPage is a reusable component that renders the core layout of each step page ✅
- Tests for Stepper component and StepPage ✅
105 changes: 105 additions & 0 deletions __tests__/components/stepper/stepPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import StepPage from "../../../components/stepper/stepPage";
import { useStepperContext } from "../../../context/stepperContext";
import { useRouter } from "next/router";

jest.mock("next/router", () => ({
useRouter: jest.fn(),
}));

jest.mock("../../../context/stepperContext", () => ({
useStepperContext: jest.fn(),
}));


jest.mock("../../../components/stepper/stepMapping", () => {
const mockStepMapping = { step1: 1, step2: 2, termin: 1 };
return {
stepMapping: mockStepMapping,
};
});

describe("StepPage", () => {
const mockSteps = [
{ title: "Step 1", isCompleted: false, navigateTo: "/step1" },
{ title: "Step 2", isCompleted: false, navigateTo: "/step2" },
];

const mockUseStepperContext = {
steps: mockSteps,
currentStep: 0,
handleNextStep: jest.fn(),
setCurrentStep: jest.fn(),
};

beforeEach(() => {
(useStepperContext as jest.Mock).mockReturnValue(mockUseStepperContext);
(useRouter as jest.Mock).mockReturnValue({
pathname: "/step1",
query: {},
push: jest.fn(),
events: {
emit: jest.fn(),
on: jest.fn(),
off: jest.fn(),
},
beforePopState: jest.fn(() => null),
prefetch: jest.fn(() => Promise.resolve()),
});
});

it("renders the Stepper component", () => {
(useRouter as jest.Mock).mockReturnValue({
pathname: "/step1",
query: {},
push: jest.fn(),
events: {
emit: jest.fn(),
on: jest.fn(),
off: jest.fn(),
},
beforePopState: jest.fn(() => null),
prefetch: jest.fn(() => Promise.resolve()),
});
render(<StepPage />);
expect(screen.getByText("Step 1")).toBeInTheDocument();
});

it("calls setCurrentStep with the correct step index based on pathname", () => {
(useRouter as jest.Mock).mockReturnValue({
pathname: "/termin",
query: {},
push: jest.fn(),
events: {
emit: jest.fn(),
on: jest.fn(),
off: jest.fn(),
},
beforePopState: jest.fn(() => null),
prefetch: jest.fn(() => Promise.resolve()),
});

render(<StepPage />);
expect(mockUseStepperContext.setCurrentStep).toHaveBeenCalledWith(1);
});

it("renders 'Invalid Step' if the pathname does not match any step", () => {
(useRouter as jest.Mock).mockReturnValue({
pathname: "/unknown",
query: {},
push: jest.fn(),
events: {
emit: jest.fn(),
on: jest.fn(),
off: jest.fn(),
},
beforePopState: jest.fn(() => null),
prefetch: jest.fn(() => Promise.resolve()),
});

const { container } = render(<StepPage />);
expect(container).toHaveTextContent("Invalid Step");
});
});
63 changes: 59 additions & 4 deletions __tests__/components/stepper/stepper.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,61 @@
// TODO: Implement your tests here
import React from "react";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import Stepper from "../../../components/stepper/stepper";
import { steps } from "../../../components/stepper/steps";

describe("Stepper", () => {
it("TODO", () => {
expect(true).toBe(false);
});
it("renders all steps", () => {
render(<Stepper steps={steps} currentStep={1} />);

steps.forEach((step, index) => {
expect(screen.getByText(step.title)).toBeInTheDocument();
expect(screen.getByText(String(index + 1))).toBeInTheDocument();
});
});

it("highlights current and previous steps", () => {
const current = 2;
const stepsWithCompleted = steps.map((s, i) => ({ ...s, isCompleted: i <= current }));

render(<Stepper steps={stepsWithCompleted} currentStep={current} />);

for (let i = 0; i <= current; i++) {
const stepCircle = screen.getByText(String(i + 1));
expect(stepCircle).toHaveClass("bg-blue-500");
}
});

it("renders correct number of connection lines", () => {
render(<Stepper steps={steps} currentStep={1} />);

const connectionLines = screen.getAllByTestId("connection-line");
expect(connectionLines).toHaveLength(steps.length - 1);
});

it("applies responsive classes", () => {
render(<Stepper steps={steps} currentStep={1} />);

const stepCircle = screen.getByText("1");
expect(stepCircle).toHaveClass("w-[30px]");
expect(stepCircle).toHaveClass("h-[30px]");
expect(stepCircle).toHaveClass("md:w-10");
expect(stepCircle).toHaveClass("md:h-10");

const stepTitle = screen.getByText(steps[0].title);
expect(stepTitle).toHaveClass("text-[12px]");
expect(stepTitle).toHaveClass("lg:text-[16px]");
});

it("renders correctly with last step as current", () => {
const lastIndex = steps.length - 1;
const stepsAllCompleted = steps.map((s) => ({ ...s, isCompleted: true }));

render(<Stepper steps={stepsAllCompleted} currentStep={lastIndex} />);

steps.forEach((_, index) => {
const stepCircle = screen.getByText(String(index + 1));
expect(stepCircle).toHaveClass("bg-blue-500");
});
});
});
23 changes: 23 additions & 0 deletions components/stepper/stepMapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { steps, Step } from "./steps";

interface StepMapping {
[key: string]: number;
}

export const stepMapping: StepMapping = steps.reduce(
(accStepMapping: StepMapping, step: Step, stepIndex: number) => {
const pageName: string | null = step.navigateTo
? step.navigateTo.replace("/", "")
: null;
// console.log(
// `Mapping step "${step.title}" to page "${pageName}" with index ${index}`
// );

if (pageName) {
accStepMapping[pageName] = stepIndex + 1;
}

return accStepMapping;
},
{}
);
47 changes: 47 additions & 0 deletions components/stepper/stepPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useEffect } from "react";
import Header from "../../components/repareo/header";
import MainWrapper from "../../components/repareo/mainWrapper";
import StepperWrapper from "../../components/repareo/stepperWrapper";
import Stepper from "../../components/stepper/stepper";
import ButtonWrapper from "../../components/repareo/buttonWrapper";
import Button from "../../components/repareo/button";
import { useStepperContext } from "../../context/stepperContext";
import { useRouter } from "next/router";
import { stepMapping } from "./stepMapping";

interface StepPageProps {}

export default function StepPage({}: StepPageProps) {
const { steps, currentStep, handleNextStep, setCurrentStep } =
useStepperContext();

const router = useRouter();
const { pathname } = router;
const pageName = pathname.split("/").pop() || "";
const stepIndex = stepMapping[pageName];
useEffect(() => {
if (stepIndex) {
setCurrentStep(stepIndex);
}
}, [setCurrentStep, stepIndex]);

if (!stepIndex) {
return <p>Invalid Step</p>;
}

return (
<>
<Header />
<MainWrapper>
<StepperWrapper>
<Stepper steps={steps} currentStep={currentStep} />
</StepperWrapper>
<ButtonWrapper>
{currentStep < steps.length - 1 && (
<Button onClick={handleNextStep}>Next</Button>
)}
</ButtonWrapper>
</MainWrapper>
</>
);
}
Loading