Files
phaseflow/e2e/garmin.spec.ts
Petru Paler 27f084f950
All checks were successful
Deploy / deploy (push) Successful in 1m38s
Fix Garmin token connection not persisting after save
Root cause: The setup-db script was missing user field definitions
(garminConnected, tokens, etc.). Production PocketBase had no such
fields, so updates silently failed to persist.

Changes:
- Add user custom fields to setup-db.ts (matches e2e harness)
- Fix status route to use strict boolean check (=== true)
- Add verification in tokens route with helpful error message
- Add ENCRYPTION_KEY to playwright config for e2e tests
- Add comprehensive e2e tests for Garmin connection flow

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 13:20:50 +00:00

192 lines
6.8 KiB
TypeScript

// ABOUTME: E2E tests for Garmin token connection flow.
// ABOUTME: Tests saving tokens, verifying connection status, and disconnection.
import { expect, test } from "@playwright/test";
test.describe("garmin connection", () => {
test.describe("unauthenticated", () => {
test("redirects to login when not authenticated", async ({ page }) => {
await page.goto("/settings/garmin");
await expect(page).toHaveURL(/\/login/);
});
});
test.describe("authenticated", () => {
test.beforeEach(async ({ page }) => {
const email = process.env.TEST_USER_EMAIL;
const password = process.env.TEST_USER_PASSWORD;
if (!email || !password) {
test.skip();
return;
}
// Login via the login page
await page.goto("/login");
await page.waitForLoadState("networkidle");
const emailInput = page.getByLabel(/email/i);
const hasEmailForm = await emailInput.isVisible().catch(() => false);
if (!hasEmailForm) {
test.skip();
return;
}
await emailInput.fill(email);
await page.getByLabel(/password/i).fill(password);
await page.getByRole("button", { name: /sign in/i }).click();
// Wait for redirect to dashboard, then navigate to garmin settings
await page.waitForURL("/", { timeout: 10000 });
await page.goto("/settings/garmin");
await page.waitForLoadState("networkidle");
});
test("shows not connected initially for new user", async ({ page }) => {
// Verify initial state shows "Not Connected"
const notConnectedText = page.getByText(/not connected/i);
await expect(notConnectedText).toBeVisible();
// Token input should be visible
const tokenInput = page.locator("#tokenInput");
await expect(tokenInput).toBeVisible();
// Save button should be visible
const saveButton = page.getByRole("button", { name: /save tokens/i });
await expect(saveButton).toBeVisible();
});
test("can save valid tokens and become connected", async ({ page }) => {
// Verify initial state shows "Not Connected"
await expect(page.getByText(/not connected/i)).toBeVisible();
// Create valid token JSON - expires 90 days from now
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 90);
const validTokens = JSON.stringify({
oauth1: { token: "test-oauth1-token", secret: "test-oauth1-secret" },
oauth2: { access_token: "test-oauth2-access-token" },
expires_at: expiresAt.toISOString(),
});
// Enter tokens in textarea
const tokenInput = page.locator("#tokenInput");
await tokenInput.fill(validTokens);
// Click Save Tokens button
const saveButton = page.getByRole("button", { name: /save tokens/i });
await saveButton.click();
// Wait for success toast - sonner renders toasts with role="status"
const successToast = page.getByText(/tokens saved successfully/i);
await expect(successToast).toBeVisible({ timeout: 10000 });
// Verify status changes to "Connected" with green indicator
const connectedText = page.getByText("Connected", { exact: true });
await expect(connectedText).toBeVisible({ timeout: 10000 });
// Green indicator should be visible (the circular badge)
const greenIndicator = page.locator(".bg-green-500").first();
await expect(greenIndicator).toBeVisible();
// Disconnect button should now be visible
const disconnectButton = page.getByRole("button", {
name: /disconnect/i,
});
await expect(disconnectButton).toBeVisible();
// Token input should be hidden when connected
await expect(tokenInput).not.toBeVisible();
});
test("shows error toast for invalid JSON", async ({ page }) => {
const tokenInput = page.locator("#tokenInput");
await tokenInput.fill("not valid json");
const saveButton = page.getByRole("button", { name: /save tokens/i });
await saveButton.click();
// Error toast should appear
const errorToast = page.getByText(/invalid json/i);
await expect(errorToast).toBeVisible({ timeout: 5000 });
});
test("shows error toast for missing required fields", async ({ page }) => {
const tokenInput = page.locator("#tokenInput");
await tokenInput.fill('{"oauth1": {}}'); // Missing oauth2 and expires_at
const saveButton = page.getByRole("button", { name: /save tokens/i });
await saveButton.click();
// Error toast should appear for missing oauth2
const errorToast = page.getByText(/oauth2 is required/i);
await expect(errorToast).toBeVisible({ timeout: 5000 });
});
test("can disconnect after connecting", async ({ page }) => {
// First connect
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 90);
const validTokens = JSON.stringify({
oauth1: { token: "test-token", secret: "test-secret" },
oauth2: { access_token: "test-access-token" },
expires_at: expiresAt.toISOString(),
});
const tokenInput = page.locator("#tokenInput");
await tokenInput.fill(validTokens);
await page.getByRole("button", { name: /save tokens/i }).click();
// Wait for connected state
await expect(page.getByText("Connected", { exact: true })).toBeVisible({
timeout: 10000,
});
// Click disconnect
const disconnectButton = page.getByRole("button", {
name: /disconnect/i,
});
await disconnectButton.click();
// Wait for disconnect success toast
const successToast = page.getByText(/garmin disconnected/i);
await expect(successToast).toBeVisible({ timeout: 10000 });
// Verify status returns to "Not Connected"
await expect(page.getByText(/not connected/i)).toBeVisible({
timeout: 10000,
});
// Token input should be visible again
await expect(page.locator("#tokenInput")).toBeVisible();
});
test("shows days until expiry when connected", async ({ page }) => {
// Connect with tokens expiring in 45 days
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 45);
const validTokens = JSON.stringify({
oauth1: { token: "test-token", secret: "test-secret" },
oauth2: { access_token: "test-access-token" },
expires_at: expiresAt.toISOString(),
});
const tokenInput = page.locator("#tokenInput");
await tokenInput.fill(validTokens);
await page.getByRole("button", { name: /save tokens/i }).click();
// Wait for connected state
await expect(page.getByText("Connected", { exact: true })).toBeVisible({
timeout: 10000,
});
// Should show days until expiry (approximately 45 days)
const expiryText = page.getByText(/\d+ days/i);
await expect(expiryText).toBeVisible();
});
});
});