Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
83 changes: 83 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: CI

on:
push:
branches: [main, master, feature/*]
pull_request:
branches: [main, master]

jobs:
server-tests:
name: Server tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: server/market-trading-service
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Cache server node modules
uses: actions/cache@v4
with:
path: server/market-trading-service/node_modules
key: ${{ runner.os }}-server-${{ hashFiles('server/market-trading-service/package-lock.json') }}
restore-keys: |
${{ runner.os }}-server-

- name: Install server dependencies
run: npm ci

- name: Run server tests
run: npm test --silent

client-tests:
name: Client tests (conditional)
runs-on: ubuntu-latest
needs: server-tests
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Detect client test script
id: detect_client_tests
run: |
set -e
PKG=client/trading-dashboard/package.json
if [ -f "$PKG" ] && grep -q '"test"' "$PKG"; then
echo "has_tests=true" >> $GITHUB_OUTPUT
else
echo "has_tests=false" >> $GITHUB_OUTPUT
fi

- name: Use Node.js 20
if: steps.detect_client_tests.outputs.has_tests == 'true'
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Cache client node modules
if: steps.detect_client_tests.outputs.has_tests == 'true'
uses: actions/cache@v4
with:
path: client/trading-dashboard/node_modules
key: ${{ runner.os }}-client-${{ hashFiles('client/trading-dashboard/package-lock.json') }}
restore-keys: |
${{ runner.os }}-client-

- name: Install client dependencies
if: steps.detect_client_tests.outputs.has_tests == 'true'
run: |
cd client/trading-dashboard
npm ci

- name: Run client tests
if: steps.detect_client_tests.outputs.has_tests == 'true'
run: |
cd client/trading-dashboard
npm test --silent
56 changes: 56 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: E2E (Cypress)

on:
push:
branches: [main, master, feature/*]
pull_request:
branches: [main, master]

jobs:
cypress-run:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node 20
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Cache client deps
uses: actions/cache@v4
with:
path: client/trading-dashboard/node_modules
key: ${{ runner.os }}-client-e2e-${{ hashFiles('client/trading-dashboard/package-lock.json') }}
restore-keys: |
${{ runner.os }}-client-e2e-

- name: Install client deps & build
working-directory: client/trading-dashboard
run: |
npm ci
npm run build
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Next.js env vars must be set during build.

Next.js bakes NEXT_PUBLIC_* environment variables at build time, not runtime. Setting NEXT_PUBLIC_MARKET_TRADING_URL in the Cypress step (Line 46) won't affect the already-built application.

Apply this diff to set the environment variable during the build:

       - name: Install client deps & build
         working-directory: client/trading-dashboard
+        env:
+          NEXT_PUBLIC_MARKET_TRADING_URL: http://127.0.0.1:59999
         run: |
           npm ci
           npm run build

Then update the Cypress step to remove the redundant env var from there since it's now baked into the build.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Install client deps & build
working-directory: client/trading-dashboard
run: |
npm ci
npm run build
- name: Install client deps & build
working-directory: client/trading-dashboard
env:
NEXT_PUBLIC_MARKET_TRADING_URL: http://127.0.0.1:59999
run: |
npm ci
npm run build
🤖 Prompt for AI Agents
In .github/workflows/e2e.yml around lines 30 to 34, the workflow builds the
Next.js client without exporting NEXT_PUBLIC_MARKET_TRADING_URL, so the public
env var set later in the Cypress step is not baked into the build; update the
"Install client deps & build" step to export NEXT_PUBLIC_MARKET_TRADING_URL
(e.g., set it in the run block or use env: for that job/step) so the value is
available at build time, and then remove the redundant
NEXT_PUBLIC_MARKET_TRADING_URL assignment from the Cypress step (around line 46)
since the built app will already have the correct value.


- name: Start client & run Cypress (no backend)
uses: cypress-io/github-action@v6
with:
start: npm --prefix client/trading-dashboard start
wait-on: "http://localhost:3000"
wait-on-timeout: 120
browser: chrome
spec: client/trading-dashboard/cypress/e2e/**/*.cy.ts
env:
# Point to an unreachable host so fetch fails and client falls back to MOCK_TICKERS
NEXT_PUBLIC_MARKET_TRADING_URL: http://127.0.0.1:59999
CYPRESS_BASE_URL: http://localhost:3000

- name: Upload Cypress videos/screenshots (if failed)
if: failure()
uses: actions/upload-artifact@v4
with:
name: cypress-artifacts
path: |
client/trading-dashboard/cypress/videos/**
client/trading-dashboard/cypress/screenshots/**
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,56 @@
# multi-bank-coding-challenge
Full Stack real-time trading dashboard

Full-stack real-time trading dashboard used for the Multi Bank coding challenge. This repository contains two main projects and supporting documentation.

Project layout

- `client/trading-dashboard` — Next.js frontend (UI, E2E + unit tests). See: `client/trading-dashboard/README.md` and `client/trading-dashboard/README.md` for developer instructions.
- `server/market-trading-service` — Node + TypeScript microservice that simulates market data and exposes a small REST API. See: `server/market-trading-service/README.md`.
- `docs/` — architecture and project-scope documentation.
- `docker-compose.yml` & `README.DOCKER.md` — Docker setup for running both services together.

Quick start (recommended: Docker)

1. From the repository root, build and start both services with docker-compose:

```bash
docker-compose build
docker-compose up
```

This will start:

- Frontend: http://localhost:3000
- Backend API: http://localhost:3005 (health endpoint: `/health`)

Local development (without Docker)

- Frontend:

```bash
cd client/trading-dashboard
npm install
npm run dev
```

- Backend:

```bash
cd server/market-trading-service
npm install
npm run dev
```

Tests

- Frontend unit tests: run in `client/trading-dashboard` with `npm run test` (Jest)
- Frontend E2E: run Cypress after starting the frontend (see `client/trading-dashboard/cypress.config.ts`)

Notes

- Each subproject includes its own README with more detailed developer and testing instructions. If you plan to contribute, please follow the subproject README before opening a PR.
- If you want CI/CD integration, I can add GitHub Actions workflows that run unit tests and optional Cypress E2E tests.

License

This project is available under the repository LICENSE file.
32 changes: 32 additions & 0 deletions client/trading-dashboard/app/__tests__/page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { render, screen } from "@testing-library/react";
import Index from "../page";
// IMPORTANT: mock child components BEFORE importing the page so the page uses the mocked versions.
jest.mock("@/components/Hero", () => ({
__esModule: true,
default: () => <div data-testid="hero">Hero Component</div>,
}));

jest.mock("@/components/TickerGrid", () => ({
__esModule: true,
default: () => <div data-testid="ticker-grid">TickerGrid Component</div>,
}));

jest.mock("@/components/NewsFeed", () => ({
__esModule: true,
default: () => <div data-testid="news-feed">NewsFeed Component</div>,
}));

describe("Homepage (Index)", () => {
it("renders all main sections: Hero, TickerGrid, and NewsFeed", () => {
render(<Index />);
expect(screen.getByTestId("hero")).toBeInTheDocument();
expect(screen.getByTestId("ticker-grid")).toBeInTheDocument();
expect(screen.getByTestId("news-feed")).toBeInTheDocument();
});

it("has correct layout structure with min-h-screen and bg-background", () => {
const { container } = render(<Index />);
const mainDiv = container.querySelector(".min-h-screen.bg-background");
expect(mainDiv).toBeInTheDocument();
});
});
7 changes: 3 additions & 4 deletions client/trading-dashboard/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import Hero from "@/components/Hero";
import NewsFeed from "@/components/NewsFeed";
import TickerGrid from "@/components/TickerGrid";

const Index = async () => {
// Converted to a synchronous Server Component (no data fetching here) so Jest can render it.
export default function Index() {
return (
<div className="min-h-screen bg-background">
<Hero />
<TickerGrid />
<NewsFeed />
</div>
);
};

export default Index;
}
5 changes: 4 additions & 1 deletion client/trading-dashboard/components/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { TrendingUp, Activity, BarChart3 } from "lucide-react";

const Hero = () => {
return (
<section className="relative overflow-hidden border-b bg-gradient-to-br from-background via-background to-secondary py-20 px-4">
<section
data-testid="hero"
className="relative overflow-hidden border-b bg-gradient-to-br from-background via-background to-secondary py-20 px-4"
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_20%,hsl(210_100%_56%/0.1),transparent_50%)]" />

<div className="container mx-auto max-w-7xl relative z-10">
Expand Down
2 changes: 1 addition & 1 deletion client/trading-dashboard/components/NewsFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const NewsFeed = () => {
},
{
title: "Energy Sector Stocks Rise on Supply Chain Improvements",
source: "Financial Times",
source: "The Guardian",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Test will fail due to inconsistent data.

The news source was changed from "Financial Times" to "The Guardian", but the test file at client/trading-dashboard/components/__tests__/NewsFeed.test.tsx line 45 still expects "Financial Times". This will cause the test to fail.

Apply this fix to the test file:

-    expect(screen.getByText("Financial Times")).toBeInTheDocument();
+    expect(screen.getByText("The Guardian")).toBeInTheDocument();

Alternatively, revert this change to keep "Financial Times" if there was no intentional reason for the change.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In client/trading-dashboard/components/NewsFeed.tsx around line 52 the news
source was changed to "The Guardian", but the unit test at
client/trading-dashboard/components/__tests__/NewsFeed.test.tsx line 45 still
expects "Financial Times", causing a test failure; update the test expectation
to "The Guardian" to match the component (or revert the component change back to
"Financial Times" if that was unintended) and run tests to confirm the
inconsistency is resolved.

time: "10 hours ago",
category: "Energy",
url: "#",
Expand Down
9 changes: 7 additions & 2 deletions client/trading-dashboard/components/PriceChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,18 @@ export const PriceChart: FC<PriceChartProps> = ({
</div>

{/* Chart Container */}
<div className="flex-grow w-full h-72 md:h-96">
<div className="flex-grow w-full min-w-0 h-72 md:h-96">
{isLoading ? (
<div className="flex justify-center items-center h-full">
<Loader2 className="animate-spin text-blue-600 w-10 h-10" />
</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer
minWidth={0}
minHeight={undefined}
width="100%"
height="100%"
>
<AreaChart
data={data}
margin={{ top: 5, right: 20, left: -10, bottom: 5 }}
Expand Down
42 changes: 42 additions & 0 deletions client/trading-dashboard/components/__tests__/Hero.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import Hero from "../Hero";

describe("Hero", () => {
it('renders the main heading with "Real-Time Market Intelligence"', () => {
render(<Hero />);

expect(screen.getByText("Real-Time Market")).toBeInTheDocument();
expect(screen.getByText("Intelligence")).toBeInTheDocument();
});

it("displays the Live Market Data badge", () => {
render(<Hero />);

expect(screen.getByText("Live Market Data")).toBeInTheDocument();
});

it("shows the tagline description", () => {
render(<Hero />);

expect(
screen.getByText(/Stay ahead with live ticker updates/i)
).toBeInTheDocument();
});

it("displays feature cards with Live and 24/7 stats", () => {
render(<Hero />);

expect(screen.getByText("Live")).toBeInTheDocument();
expect(screen.getByText("Price Updates")).toBeInTheDocument();
expect(screen.getByText("24/7")).toBeInTheDocument();
expect(screen.getByText("Market Coverage")).toBeInTheDocument();
});

it("has proper section structure with gradient background", () => {
const { container } = render(<Hero />);

const section = container.querySelector("section");
expect(section).toHaveClass("bg-gradient-to-br");
});
});
Loading
Loading