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
12 changes: 12 additions & 0 deletions packages/server/src/beear/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ class WishlistAdd(BaseModel):
frame_id: str


class WishlistSet(BaseModel):
frame_ids: list[str]


CATALOG_CACHE_CONTROL = "public, max-age=300, stale-while-revalidate=3600"


Expand Down Expand Up @@ -243,6 +247,14 @@ def api_wishlist_add(session_id: str, body: WishlistAdd) -> dict:
return row


@app.put("/api/sessions/{session_id}/wishlist")
def api_wishlist_set(session_id: str, body: WishlistSet) -> dict:
row = sess.set_wishlist(session_id, body.frame_ids)
if not row:
raise HTTPException(404, "session not found")
return row


if SVG_DIR.is_dir():
app.mount("/catalog/svg", StaticFiles(directory=str(SVG_DIR)), name="svg")

Expand Down
9 changes: 9 additions & 0 deletions packages/server/src/beear/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ def add_wishlist(session_id: str, frame_id: str) -> dict[str, Any] | None:
return dict(row)


def set_wishlist(session_id: str, frame_ids: list[str]) -> dict[str, Any] | None:
row = _sessions.get(session_id)
if not row:
return None
row["wishlist"] = list(dict.fromkeys(frame_ids))
row["updated_at"] = time.time()
return dict(row)


def list_sessions(limit: int = 50) -> list[dict[str, Any]]:
rows = sorted(_sessions.values(), key=lambda r: r.get("updated_at", 0), reverse=True)
return [dict(r) for r in rows[: max(1, min(limit, 200))]]
187 changes: 187 additions & 0 deletions packages/server/tests/test_sessions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import pytest

pytest.importorskip("fastapi")
from fastapi.testclient import TestClient

from beear.api import app

client = TestClient(app)


def test_create_session():
r = client.post("/api/sessions", json={"frame_ids": ["aviator_gold"], "note": "test"})
assert r.status_code == 200
body = r.json()
assert "id" in body
assert body["frame_ids"] == ["aviator_gold"]
assert body["note"] == "test"
assert body["wishlist"] == []
assert "created_at" in body
assert "updated_at" in body


def test_create_session_empty():
r = client.post("/api/sessions", json={})
assert r.status_code == 200
body = r.json()
assert body["frame_ids"] == []
assert body["wishlist"] == []


def test_get_session():
r = client.post("/api/sessions", json={"frame_ids": ["wayfarer_black"]})
sid = r.json()["id"]
r = client.get(f"/api/sessions/{sid}")
assert r.status_code == 200
assert r.json()["id"] == sid
assert r.json()["frame_ids"] == ["wayfarer_black"]


def test_get_session_not_found():
r = client.get("/api/sessions/nonexistent")
assert r.status_code == 404


def test_list_sessions():
# Create a few sessions
client.post("/api/sessions", json={"frame_ids": ["aviator_gold"]})
client.post("/api/sessions", json={"frame_ids": ["round_tortoise"]})
r = client.get("/api/sessions")
assert r.status_code == 200
assert "sessions" in r.json()
assert len(r.json()["sessions"]) >= 2


def test_patch_session():
r = client.post("/api/sessions", json={"frame_ids": ["aviator_gold"]})
sid = r.json()["id"]
r = client.patch(
f"/api/sessions/{sid}",
json={"frame_ids": ["wayfarer_black", "round_tortoise"], "note": "updated"},
)
assert r.status_code == 200
body = r.json()
assert body["frame_ids"] == ["wayfarer_black", "round_tortoise"]
assert body["note"] == "updated"


def test_patch_session_not_found():
r = client.patch("/api/sessions/nonexistent", json={"note": "hi"})
assert r.status_code == 404


def test_add_wishlist():
r = client.post("/api/sessions", json={"frame_ids": []})
sid = r.json()["id"]
r = client.post(f"/api/sessions/{sid}/wishlist", json={"frame_id": "aviator_gold"})
assert r.status_code == 200
assert "aviator_gold" in r.json()["wishlist"]


def test_add_wishlist_duplicate():
r = client.post("/api/sessions", json={"frame_ids": []})
sid = r.json()["id"]
client.post(f"/api/sessions/{sid}/wishlist", json={"frame_id": "sport_blue"})
r = client.post(f"/api/sessions/{sid}/wishlist", json={"frame_id": "sport_blue"})
assert r.status_code == 200
# Duplicates should be prevented
assert r.json()["wishlist"].count("sport_blue") == 1


def test_add_wishlist_session_not_found():
r = client.post("/api/sessions/nonexistent/wishlist", json={"frame_id": "aviator_gold"})
assert r.status_code == 404


def test_add_wishlist_bad_frame():
r = client.post("/api/sessions", json={"frame_ids": []})
sid = r.json()["id"]
r = client.post(f"/api/sessions/{sid}/wishlist", json={"frame_id": "nonexistent_frame"})
assert r.status_code == 400


def test_put_wishlist():
"""PUT replaces the entire wishlist."""
r = client.post("/api/sessions", json={"frame_ids": []})
sid = r.json()["id"]
# Add initial wishlist items
client.post(f"/api/sessions/{sid}/wishlist", json={"frame_id": "aviator_gold"})
client.post(f"/api/sessions/{sid}/wishlist", json={"frame_id": "sport_blue"})
# PUT replaces the entire wishlist
r = client.put(
f"/api/sessions/{sid}/wishlist",
json={"frame_ids": ["wayfarer_black", "round_tortoise"]},
)
assert r.status_code == 200
body = r.json()
assert body["wishlist"] == ["wayfarer_black", "round_tortoise"]
# Verify old items are gone
r = client.get(f"/api/sessions/{sid}")
assert r.status_code == 200
assert r.json()["wishlist"] == ["wayfarer_black", "round_tortoise"]


def test_put_wishlist_empty():
"""PUT with empty list clears the wishlist."""
r = client.post("/api/sessions", json={"frame_ids": []})
sid = r.json()["id"]
client.post(f"/api/sessions/{sid}/wishlist", json={"frame_id": "aviator_gold"})
r = client.put(f"/api/sessions/{sid}/wishlist", json={"frame_ids": []})
assert r.status_code == 200
assert r.json()["wishlist"] == []


def test_put_wishlist_dedup():
"""PUT wishlist should deduplicate frame_ids."""
r = client.post("/api/sessions", json={"frame_ids": []})
sid = r.json()["id"]
r = client.put(
f"/api/sessions/{sid}/wishlist",
json={"frame_ids": ["aviator_gold", "aviator_gold", "sport_blue"]},
)
assert r.status_code == 200
body = r.json()
assert body["wishlist"] == ["aviator_gold", "sport_blue"]


def test_put_wishlist_session_not_found():
r = client.put(
"/api/sessions/nonexistent/wishlist",
json={"frame_ids": ["aviator_gold"]},
)
assert r.status_code == 404


def test_full_session_lifecycle():
"""End-to-end: create, update, add wishlist, set wishlist, retrieve."""
# Create
r = client.post("/api/sessions", json={"frame_ids": ["aviator_gold"], "note": "demo"})
assert r.status_code == 200
sid = r.json()["id"]

# Add to wishlist
r = client.post(f"/api/sessions/{sid}/wishlist", json={"frame_id": "sport_blue"})
assert r.status_code == 200
assert "sport_blue" in r.json()["wishlist"]

# PATCH update
r = client.patch(f"/api/sessions/{sid}", json={"note": "updated demo"})
assert r.status_code == 200
assert r.json()["note"] == "updated demo"

# PUT wishlist (full replace)
r = client.put(
f"/api/sessions/{sid}/wishlist",
json={"frame_ids": ["wayfarer_black"]},
)
assert r.status_code == 200
assert r.json()["wishlist"] == ["wayfarer_black"]

# GET verify
r = client.get(f"/api/sessions/{sid}")
assert r.status_code == 200
body = r.json()
assert body["id"] == sid
assert body["frame_ids"] == ["aviator_gold"]
assert body["wishlist"] == ["wayfarer_black"]
assert body["note"] == "updated demo"
96 changes: 96 additions & 0 deletions packages/tryon-js/tests/fit.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,99 @@ test("compareFrames delta", () => {
assert.equal(c.ok, true);
assert.ok(c.width_delta_px > 0);
});

// --- PD-to-fit-scale calculation tests (bounty #7) ---
test("pdToFitScale: correct scale for 140mm frame at 64mm PD, 120px PD", () => {
const s = overlaySize(frame, 120, 64);
const expected = (140 / 64) * 120 * 1.15;
assert.ok(Math.abs(s.overlayW - expected) < 0.5);
assert.ok(s.overlayH > 0);
});

test("pdToFitScale: larger PD means smaller overlay", () => {
const a = overlaySize(frame, 120, 74);
const b = overlaySize(frame, 120, 54);
assert.ok(b.overlayW > a.overlayW);
});

test("pdToFitScale: wider frame produces wider overlay", () => {
const wideFrame = { fit: { width_mm: 160 } };
const narrowFrame = { fit: { width_mm: 120 } };
const w = overlaySize(wideFrame, 120, 64);
const n = overlaySize(narrowFrame, 120, 64);
assert.ok(w.overlayW > n.overlayW);
});

test("pdToFitScale: proportional to pupil pixel distance", () => {
const a = overlaySize(frame, 80, 64);
const b = overlaySize(frame, 160, 64);
assert.ok(Math.abs(b.overlayW / a.overlayW - 2.0) < 0.1);
});

test("pdToFitScale: edge case — min PD (54mm)", () => {
const s = overlaySize({ fit: { width_mm: 140 } }, 100, 54);
assert.ok(s.overlayW > 0);
assert.ok(s.overlayW > 250);
});

test("pdToFitScale: edge case — max PD (74mm)", () => {
const s = overlaySize({ fit: { width_mm: 140 } }, 100, 74);
assert.ok(s.overlayW > 0);
assert.ok(s.overlayW < 250);
});

test("pdToFitScale: missing fit defaults to 140mm", () => {
const s = overlaySize({}, 120, 64);
const expected = (140 / 64) * 120 * 1.15;
assert.ok(Math.abs(s.overlayW - expected) < 0.5);
});

// --- Credit-card calibration math tests (bounty #7) ---
const CARD_W = 85.6;

test("cardCal: pxPerMm from known object", () => {
const cardPx = 300;
const pxPerMm = cardPx / CARD_W;
assert.ok(Math.abs(pxPerMm - (300 / 85.6)) < 0.001);
});

test("cardCal: compute PD from card and pupil pixels", () => {
const cardPx = 300;
const pupilPx = 224;
const pdMm = pupilPx / (cardPx / CARD_W);
assert.ok(pdMm > 60 && pdMm < 68);
});

test("cardCal: larger card px means smaller estimated PD", () => {
const pupilPx = 200;
const pdSmallCard = pupilPx / (200 / CARD_W);
const pdLargeCard = pupilPx / (400 / CARD_W);
assert.ok(pdLargeCard < pdSmallCard);
});

test("cardCal: larger pupil px means larger estimated PD", () => {
const cardPx = 300;
const pxPerMm = cardPx / CARD_W;
assert.ok((300 / pxPerMm) > (100 / pxPerMm));
});

test("cardCal: real-world PD ~64mm scenario", () => {
const cardPx = 250;
const pupilPx = 187;
const pdMm = pupilPx / (cardPx / CARD_W);
assert.ok(Math.abs(pdMm - 64) < 1.5);
});

test("cardCal: boundary — min PD through card", () => {
const cardPx = 500;
const pupilPx = 200;
const pdMm = pupilPx / (cardPx / CARD_W);
assert.ok(pdMm < 50);
});

test("cardCal: boundary — max PD through card", () => {
const cardPx = 100;
const pupilPx = 200;
const pdMm = pupilPx / (cardPx / CARD_W);
assert.ok(pdMm > 100);
});
Loading