Fix garmin-sync upsert and add Settings UI for intensity goals
All checks were successful
Deploy / deploy (push) Successful in 1m39s
All checks were successful
Deploy / deploy (push) Successful in 1m39s
- Fix dailyLog upsert to use range query (matches today route pattern) - Properly distinguish 404 errors from other failures in upsert logic - Add logging for dailyLog create/update operations - Add Settings UI section for weekly intensity goals per phase - Add unit tests for upsert behavior and intensity goals UI - Add E2E tests for intensity goals settings flow This fixes the issue where Garmin sync was creating new dailyLog records instead of updating existing ones (322 vs 222 intensity minutes bug, Unknown HRV bug). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,11 @@ describe("SettingsPage", () => {
|
||||
garminConnected: false,
|
||||
activeOverrides: [],
|
||||
lastPeriodDate: "2024-01-01",
|
||||
intensityGoalMenstrual: 75,
|
||||
intensityGoalFollicular: 150,
|
||||
intensityGoalOvulation: 100,
|
||||
intensityGoalEarlyLuteal: 120,
|
||||
intensityGoalLateLuteal: 50,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -240,6 +245,11 @@ describe("SettingsPage", () => {
|
||||
cycleLength: 30,
|
||||
notificationTime: "08:00",
|
||||
timezone: "America/New_York",
|
||||
intensityGoalMenstrual: 75,
|
||||
intensityGoalFollicular: 150,
|
||||
intensityGoalOvulation: 100,
|
||||
intensityGoalEarlyLuteal: 120,
|
||||
intensityGoalLateLuteal: 50,
|
||||
}),
|
||||
});
|
||||
});
|
||||
@@ -639,4 +649,172 @@ describe("SettingsPage", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("intensity goals section", () => {
|
||||
it("renders Weekly Intensity Goals section heading", async () => {
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("heading", { name: /weekly intensity goals/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders input for menstrual phase goal", async () => {
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/menstrual/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders input for follicular phase goal", async () => {
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/follicular/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders input for ovulation phase goal", async () => {
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/ovulation/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders input for early luteal phase goal", async () => {
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/early luteal/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders input for late luteal phase goal", async () => {
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/late luteal/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("pre-fills intensity goal inputs with current user values", async () => {
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/menstrual/i)).toHaveValue(75);
|
||||
expect(screen.getByLabelText(/follicular/i)).toHaveValue(150);
|
||||
expect(screen.getByLabelText(/ovulation/i)).toHaveValue(100);
|
||||
expect(screen.getByLabelText(/early luteal/i)).toHaveValue(120);
|
||||
expect(screen.getByLabelText(/late luteal/i)).toHaveValue(50);
|
||||
});
|
||||
});
|
||||
|
||||
it("includes intensity goals in PATCH request when saving", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockUser),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({ ...mockUser, intensityGoalMenstrual: 80 }),
|
||||
});
|
||||
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/menstrual/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const menstrualInput = screen.getByLabelText(/menstrual/i);
|
||||
fireEvent.change(menstrualInput, { target: { value: "80" } });
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save/i });
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalledWith("/api/user", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: expect.stringContaining('"intensityGoalMenstrual":80'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("has number type for all intensity goal inputs", async () => {
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/menstrual/i)).toHaveAttribute(
|
||||
"type",
|
||||
"number",
|
||||
);
|
||||
expect(screen.getByLabelText(/follicular/i)).toHaveAttribute(
|
||||
"type",
|
||||
"number",
|
||||
);
|
||||
expect(screen.getByLabelText(/ovulation/i)).toHaveAttribute(
|
||||
"type",
|
||||
"number",
|
||||
);
|
||||
expect(screen.getByLabelText(/early luteal/i)).toHaveAttribute(
|
||||
"type",
|
||||
"number",
|
||||
);
|
||||
expect(screen.getByLabelText(/late luteal/i)).toHaveAttribute(
|
||||
"type",
|
||||
"number",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("validates minimum value of 0 for intensity goals", async () => {
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/menstrual/i)).toHaveAttribute("min", "0");
|
||||
});
|
||||
});
|
||||
|
||||
it("disables intensity goal inputs while saving", async () => {
|
||||
let resolveSave: (value: unknown) => void = () => {};
|
||||
const savePromise = new Promise((resolve) => {
|
||||
resolveSave = resolve;
|
||||
});
|
||||
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockUser),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
ok: true,
|
||||
json: () => savePromise,
|
||||
});
|
||||
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/menstrual/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save/i });
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/menstrual/i)).toBeDisabled();
|
||||
expect(screen.getByLabelText(/follicular/i)).toBeDisabled();
|
||||
expect(screen.getByLabelText(/ovulation/i)).toBeDisabled();
|
||||
expect(screen.getByLabelText(/early luteal/i)).toBeDisabled();
|
||||
expect(screen.getByLabelText(/late luteal/i)).toBeDisabled();
|
||||
});
|
||||
|
||||
resolveSave(mockUser);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user