feat: add stock database with prisma for portfolio persistence

- Initialize Prisma with SQLite and Stock model
- Create database service layer with singleton client
- Add API routes for stock CRUD operations
- Integrate database with analyze page to persist ticker entries
- Add Playwright tests for stock database functionality
This commit is contained in:
2026-05-14 10:23:56 +02:00
parent f40eec1420
commit 3340fd11ca
29 changed files with 2530 additions and 221 deletions
+61
View File
@@ -0,0 +1,61 @@
import { test, expect } from "@playwright/test";
test.describe("Portfolio Analysis Page", () => {
test("should load and display portfolio analysis form", async ({ page }) => {
await page.goto("/analyze");
await expect(page.locator("h1")).toHaveText("Portfolio Analysis");
await expect(page.locator('input[placeholder*="Add ticker"]')).toBeVisible();
await expect(page.locator("button:has-text('Add Stock')")).toBeVisible();
});
test("should load Alpaca portfolio on page load", async ({ page }) => {
await page.goto("/analyze");
// Wait for potential positions to load from Alpaca
await page.waitForTimeout(3000);
// If user has positions, they should appear; otherwise check empty state
const hasPositions = await page.locator("td.font-bold").first().isVisible();
if (hasPositions) {
const tickerCell = page.locator("td.font-bold").first();
await expect(tickerCell).not.toBeEmpty();
} else {
await expect(page.locator("text=No stocks added")).toBeVisible();
}
});
test("should add a ticker and display it in the table", async ({ page }) => {
await page.goto("/analyze");
await page.fill('input[placeholder*="Add ticker"]', "AAPL");
await page.click("button:has-text('Add Stock')");
await expect(page.locator("td.font-bold")).toContainText("AAPL");
});
test("should display position quantity from Alpaca", async ({ page }) => {
await page.goto("/analyze");
// Wait for any Alpaca portfolio to load first
await page.waitForTimeout(3000);
// Wait for the table row to appear - if already stocks exist, verify position column works
await page.waitForSelector("tr td.font-bold");
// Get first ticker row and verify it has position column (third column)
const firstRow = page.locator("tbody tr").first();
const tickerCell = firstRow.locator("td.font-bold");
const positionCell = firstRow.locator("td:nth-child(3)");
// Verify ticker is displayed
await expect(tickerCell).toBeVisible();
// Position column should have some content (number or empty if no position)
const positionText = await positionCell.textContent();
// Position should be a number if present
if (positionText && positionText.trim()) {
expect(positionText.trim()).toMatch(/^\d+$/);
}
});
});
-5
View File
@@ -1,5 +0,0 @@
import { test, expect } from "vitest";
test("placeholder test", () => {
expect(true).toBe(true);
});
+31
View File
@@ -0,0 +1,31 @@
import { test, expect } from "@playwright/test";
test.describe("Stock Database", () => {
test("should add and list stocks", async ({ request }) => {
const uniqueTicker = `TEST${Date.now()}`;
const createRes = await request.post("/api/stocks", {
form: { ticker: uniqueTicker },
});
expect(createRes.ok()).toBeTruthy();
const listRes = await request.get("/api/stocks");
const stocks = await listRes.json();
expect(stocks).toContainEqual(expect.objectContaining({ ticker: uniqueTicker }));
});
test("should persist tickers after page reload", async ({ request, page }) => {
const uniqueTicker = `PERSIST${Date.now()}`;
await request.post("/api/stocks", {
form: { ticker: uniqueTicker },
});
await page.goto("/stocks");
await page.waitForLoadState("networkidle");
const listRes = await request.get("/api/stocks");
const stocks = await listRes.json();
expect(stocks).toContainEqual(expect.objectContaining({ ticker: uniqueTicker }));
});
});