Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Binary file not shown.
1 change: 1 addition & 0 deletions .codevalid/ui/mock/fixtures/receipt-invalid.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
plain text receipt content
Binary file added .codevalid/ui/mock/fixtures/receipt-oversize.pdf
Binary file not shown.
1 change: 1 addition & 0 deletions .codevalid/ui/mock/fixtures/receipt-valid.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions .codevalid/ui/mock/fixtures/receipt-valid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions .codevalid/ui/mock/mock-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export const authUser = {
id: "1",
username: "johndoe",
email: "john@example.com",
fullName: "John Doe",
phone: "+1 (555) 000-0000",
organization: "Acme Corp",
};

export const authSession = {
user: authUser,
token: "mock-valid-token",
};

export const emptyEvents = [];

export const morningCommutePreset = {
presetId: 101,
name: "Morning Commute",
primaryDirection: "North",
periodTag: "morning",
exactStartTimeLocal: "07:30",
durationMinutes: 55,
miles: 12,
lastUsedAtUtc: "2026-08-17T12:00:00Z",
updatedAtUtc: "2026-08-17T12:00:00Z",
};

export const presetWeatherResponse = {
rideDateTimeLocal: "2026-08-18T07:30",
temperature: 68,
windSpeedMph: 8,
windDirectionDeg: 180,
relativeHumidityPercent: 52,
cloudCoverPercent: 25,
precipitationType: "None",
isAvailable: true,
};

export const loadWeatherResponse = {
rideDateTimeLocal: "2024-06-10T14:25",
temperature: 66,
windSpeedMph: 11,
windDirectionDeg: 315,
relativeHumidityPercent: 58,
cloudCoverPercent: 35,
precipitationType: "Rain",
isAvailable: true,
};
70 changes: 70 additions & 0 deletions .codevalid/ui/mock/mock-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { authSession, emptyEvents } from "./mock-data.js";

function json(route, status, body) {
return route.fulfill({
status,
contentType: "application/json",
body: JSON.stringify(body),
});
}

export async function setupMockRoutes(page, options = {}) {
const events = options.events ?? emptyEvents;
const session = options.session ?? authSession;
const gasPriceResponse = options.gasPriceResponse ?? {
date: "2026-08-18",
pricePerGallon: null,
isAvailable: false,
dataSource: null,
};
const weatherResponse = options.weatherResponse ?? {
rideDateTimeLocal: "2026-08-18T08:00",
isAvailable: false,
};
const presets = options.presets ?? [];
const recordRideResponse = options.recordRideResponse ?? {
rideId: 101,
riderId: 1,
savedAtUtc: "2026-08-18T12:00:00Z",
eventStatus: "recorded",
};

await page.route("**/api/auth/signin", async (route) =>
json(route, 200, session)
);
await page.route("**/api/auth/signup", async (route) =>
json(route, 201, session)
);
await page.route("**/api/events", async (route) =>
json(route, 200, events)
);
await page.route("**/health", async (route) =>
json(route, 200, { status: "ok" })
);
await page.route("**/api/rides/gas-price**", async (route) =>
json(route, 200, gasPriceResponse)
);
await page.route("**/api/rides/weather**", async (route) => {
const url = new URL(route.request().url());
const rideDateTimeLocal =
url.searchParams.get("rideDateTimeLocal") ?? weatherResponse.rideDateTimeLocal;
return json(route, 200, {
...weatherResponse,
rideDateTimeLocal,
});
});
await page.route("**/api/rides/presets", async (route) =>
json(route, 200, {
presets,
generatedAtUtc: "2026-08-18T12:00:00Z",
})
);
await page.route("**/api/rides", async (route) => {
if (route.request().method() === "POST") {
return json(route, 201, recordRideResponse);
}
return route.fallback();
});
}

export { json };
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { test, expect } from "@playwright/test";
import { ExecutionRecorder } from "../../helpers/execution-recorder.js";
import {
setupImportPageSession,
mockImportPageShell,
mockSampleCsvDownload,
} from "../../helpers/mock-api.js";

test("download_sample_csv_provides_correct_structure", async ({ page }, testInfo) => {
const recorder = new ExecutionRecorder({
testId: "download_sample_csv_provides_correct_structure",
testTitle: testInfo.title,
});

await recorder.step("seed authenticated monthly import session and shell mocks", async () => {
await setupImportPageSession(page);
await mockImportPageShell(page);
await mockSampleCsvDownload(page);
});

let download;
await recorder.step("open monthly import page", async () => {
await page.goto("/import/monthly");
await expect(
page.getByRole("heading", { name: "Monthly Summary Import" })
).toBeVisible();
});

await recorder.step("download sample csv", async () => {
const downloadPromise = page.waitForDownload();
await page.getByRole("link", { name: "Download Sample CSV" }).click();
download = await downloadPromise;
await expect(download.suggestedFilename()).toMatch(/\.csv$/i);
});

await recorder.step("inspect downloaded csv content", async () => {
const filePath = await download.path();
const content = await page.evaluate(async (path) => {
const fs = await import("fs/promises");
return fs.readFile(path, "utf-8");
}, filePath);

const lines = content.trim().split(/\r?\n/);
expect(lines[0].startsWith("#")).toBeTruthy();
expect(content).toContain("Difficulty");
expect(content).toContain("PrimaryTravelDirection");
expect(content).toContain("Notes");
expect(content).toContain("1-5");
expect(content).toContain("North");
expect(content).toContain("NE");
expect(content).toContain("max 500 characters");
expect(content).toContain("2026-08-01,12.5,3,North,Morning commute");
expect(content).toContain("2026-08-02,8.1,5,SE,Strong headwind but manageable");
});

console.log("CODEVALID_TEST_ASSERTION_OK:download_sample_csv_provides_correct_structure");
await recorder.save(testInfo);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { test, expect } from "@playwright/test";
import fs from "fs/promises";
import { ExecutionRecorder } from "../../helpers/execution-recorder.js";
import {
setupImportPageSession,
mockImportPageShell,
mockSampleCsvDownload,
} from "../../helpers/mock-api.js";

test("Download Sample CSV returns file with correct columns and legend", async ({ page }, testInfo) => {
const recorder = new ExecutionRecorder({
testId: "download_sample_csv_returns_correct_format",
testTitle: "Download Sample CSV returns file with correct columns and legend",
});

await recorder.step("Arrange authenticated import page and sample CSV download mock", async () => {
await setupImportPageSession(page);
await mockImportPageShell(page);
await mockSampleCsvDownload(page);
});

let download;
await recorder.step("Open the import rides page", async () => {
await page.goto("/rides/import");
await expect(page.getByRole("heading", { name: "Import Rides" })).toBeVisible();
await expect(page.getByText("No file selected.")).toBeVisible();
});

await recorder.step("Click Download sample CSV and wait for download", async () => {
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Download sample CSV" }).click();
download = await downloadPromise;
});

await recorder.step("Read downloaded CSV and verify columns, legend, and sample rows", async () => {
expect(download.suggestedFilename()).toBe("ride-import-sample.csv");
const filePath = await download.path();
const csvText = await fs.readFile(filePath, "utf-8");

expect(csvText).toContain("Difficulty: 1-5");
expect(csvText).toContain("PrimaryTravelDirection/Direction: valid values are N, NE, E, SE, S, SW, W, NW or North, Northeast, East, Southeast, South, Southwest, West, Northwest");
expect(csvText).toContain("Notes: max 500 characters");
expect(csvText).toContain("Difficulty");
expect(csvText).toContain("PrimaryTravelDirection");
expect(csvText).toContain("Notes");
expect(csvText).toContain("Morning commute");
expect(csvText.split("\n")[0].trim().startsWith("#")).toBeTruthy();
});

console.log("CODEVALID_TEST_ASSERTION_OK:download_sample_csv_returns_correct_format");
await recorder.save(testInfo);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { test, expect } from "@playwright/test";
import { ExecutionRecorder } from "../../helpers/execution-recorder.js";
import {
setupImportPageSession,
mockImportPageShell,
setupRideImportScenario,
} from "../../helpers/mock-api.js";

test("import_csv_allows_blank_or_missing_optional_fields", async ({ page }, testInfo) => {
const recorder = new ExecutionRecorder({
testId: "import_csv_allows_blank_or_missing_optional_fields",
testTitle: testInfo.title,
});

await recorder.step("seed authenticated session and blank optional fields scenario", async () => {
await setupImportPageSession(page);
await mockImportPageShell(page);
await setupRideImportScenario(page, { scenario: "blank_optional_fields" });
});

await recorder.step("open page and preview file with blank optional values", async () => {
await page.goto("/import/monthly");
await expect(
page.getByRole("heading", { name: "Monthly Summary Import" })
).toBeVisible();

await page.locator("#monthly-import-file").setInputFiles({
name: "blank-optional-fields.csv",
mimeType: "text/csv",
buffer: Buffer.from("Month,Miles,Days\nAugust,15,2\nSeptember,10,1\n"),
});
await page.getByRole("spinbutton").fill("2026");
await page.getByRole("button", { name: "Preview import" }).click();
});

await recorder.step("verify no validation errors appear in preview", async () => {
await expect(page.getByText("Valid rows: 2 / Invalid rows: 0")).toBeVisible();
await expect(page.getByText("Total generated rides: 2")).toBeVisible();
await expect(page.getByText("Duplicate rides: 0")).toBeVisible();
});

await recorder.step("start import and verify all rows import", async () => {
await page.getByRole("button", { name: "Start import" }).click();

await expect(
page.getByRole("heading", { name: "Monthly import summary" })
).toBeVisible();
await expect(page.getByText("Rides created: 2")).toBeVisible();
await expect(page.getByText("Rows rejected: 0")).toBeVisible();
});

console.log("CODEVALID_TEST_ASSERTION_OK:import_csv_allows_blank_or_missing_optional_fields");
await recorder.save(testInfo);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { test, expect } from "@playwright/test";
import { ExecutionRecorder } from "../../helpers/execution-recorder.js";
import {
setupImportPageSession,
mockImportPageShell,
setupRideImportScenario,
} from "../../helpers/mock-api.js";

test("Import CSV with blank values in optional fields", async ({ page }, testInfo) => {
const recorder = new ExecutionRecorder({
testId: "import_csv_blank_optional_fields",
testTitle: "Import CSV with blank values in optional fields",
});

await recorder.step("Arrange blank optional fields scenario", async () => {
await setupImportPageSession(page);
await mockImportPageShell(page);
await setupRideImportScenario(page, {
scenario: "blank_optional_fields",
});
});

await recorder.step("Navigate to import page and upload CSV", async () => {
await page.goto("/rides/import");
await expect(page.getByRole("heading", { name: "Import Rides" })).toBeVisible();
await page.locator("#csv-upload-input").setInputFiles({
name: "blank-optionals.csv",
mimeType: "text/csv",
buffer: Buffer.from("Date,Miles,Difficulty,PrimaryTravelDirection,Notes\n2026-08-10,7.0,,,\n2026-08-11,7.5,,,\n"),
});
});

await recorder.step("Preview then start import", async () => {
await page.getByRole("button", { name: "Preview Import" }).click();
await expect(page.getByText("Total rows: 2 | Valid rows: 2 | Invalid rows: 0")).toBeVisible();
await page.getByRole("button", { name: "Start Import" }).click();
});

await recorder.step("Verify all rows imported successfully", async () => {
await expect(page.getByRole("heading", { name: "Import Complete" })).toBeVisible();
await expect(page.getByText("2 rides were imported successfully.")).toBeVisible();
});

console.log("CODEVALID_TEST_ASSERTION_OK:import_csv_blank_optional_fields");
await recorder.save(testInfo);
});
Loading
Loading