Phase 2 Task 41 — Dashboard redesign. Backend: - Extend GET /users/onboarding-status with email_verified and shop_setup_done. - tried_ai_assistant kept in payload for backward-compat during deploy. Frontend: - New NextStepCard: surfaces the highest-priority incomplete onboarding item with a primary CTA. Priority order: verify email > set up shop > run first FlowPilot session > connect PSA > invite teammate > pick a plan (gated on trial stage warning/urgent/expired). Returns null when all done OR onboarding_dismissed. - New SetupChecklist: unified single list (no SOLO/TEAM bifurcation), drops the stale tried_ai_assistant / Script Builder item, surfaces "Pick a plan" when trial stage is warning or later. - Mounted on QuickStartPage below the hero with a "Show all setup steps" toggle. The whole onboarding section auto-hides when there's nothing left to nudge on, so the dashboard goes back to clean once setup is done. - Removed the orphaned OnboardingChecklist component (was defined but never mounted). - New useOnboardingStatus hook so page + components share one fetch contract. Tests: - Backend: test_onboarding_status_includes_email_verified_and_shop_setup_done. - Frontend (Vitest): 13 new tests across NextStepCard, SetupChecklist, and QuickStartPage covering priority ordering, dismissal, the SOLO/TEAM removal, the toggle reveal, and the trial-stage gate on Pick a plan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
114 lines
3.6 KiB
Python
114 lines
3.6 KiB
Python
"""Tests for onboarding status endpoints."""
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
import pytest
|
|
from sqlalchemy import select
|
|
|
|
from app.models.user import User
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_onboarding_status_fresh_user(client, auth_headers):
|
|
"""Fresh user should have all onboarding items false."""
|
|
response = await client.get(
|
|
"/api/v1/users/onboarding-status",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
assert data["created_flow"] is False
|
|
assert data["ran_session"] is False
|
|
assert data["exported_session"] is False
|
|
assert data["tried_ai_assistant"] is False
|
|
assert data["invited_teammate"] is False
|
|
assert data["connected_psa"] is False
|
|
assert data["is_team_user"] is False
|
|
assert data["dismissed"] is False
|
|
# Phase 2 fields default to false on a fresh, unverified user with no wizard progress.
|
|
assert data["email_verified"] is False
|
|
assert data["shop_setup_done"] is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_onboarding_status_includes_email_verified_and_shop_setup_done(
|
|
client, auth_headers, test_user, test_db
|
|
):
|
|
"""email_verified flips when email_verified_at is set; shop_setup_done flips at step >= 1."""
|
|
# Sanity-check baseline.
|
|
response = await client.get(
|
|
"/api/v1/users/onboarding-status",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["email_verified"] is False
|
|
assert data["shop_setup_done"] is False
|
|
|
|
# Mutate the underlying user, then re-fetch.
|
|
user_email = test_user["email"]
|
|
result = await test_db.execute(select(User).where(User.email == user_email))
|
|
user = result.scalar_one()
|
|
user.email_verified_at = datetime.now(tz=timezone.utc)
|
|
user.onboarding_step_completed = 1
|
|
await test_db.commit()
|
|
|
|
response = await client.get(
|
|
"/api/v1/users/onboarding-status",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["email_verified"] is True
|
|
assert data["shop_setup_done"] is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_onboarding_dismiss(client, auth_headers):
|
|
"""Dismiss endpoint should set dismissed to true."""
|
|
# Verify starts as false
|
|
response = await client.get(
|
|
"/api/v1/users/onboarding-status",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["dismissed"] is False
|
|
|
|
# Dismiss
|
|
response = await client.post(
|
|
"/api/v1/users/onboarding-status/dismiss",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["dismissed"] is True
|
|
|
|
# Verify persisted
|
|
response = await client.get(
|
|
"/api/v1/users/onboarding-status",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["dismissed"] is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_onboarding_created_flow_after_tree_creation(client, auth_headers, test_tree):
|
|
"""After creating a tree, created_flow should be true."""
|
|
response = await client.get(
|
|
"/api/v1/users/onboarding-status",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["created_flow"] is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_onboarding_requires_auth(client):
|
|
"""Unauthenticated requests should be rejected."""
|
|
response = await client.get("/api/v1/users/onboarding-status")
|
|
assert response.status_code == 401
|
|
|
|
response = await client.post("/api/v1/users/onboarding-status/dismiss")
|
|
assert response.status_code == 401
|