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
56 changes: 51 additions & 5 deletions frontend/src/app/create/create-page-client.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from "react";
import { act, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { LanguageProvider } from "@/i18n/provider";

import CreatePolicyPageClient from "./create-page-client";

vi.mock("@/components/wallet-provider", () => ({
Expand Down Expand Up @@ -61,9 +63,17 @@ vi.mock("@/components/oracle-source-selector", () => ({
</label>
) : (
<p>Loading oracle providers...</p>
),
),
}));

function renderCreatePolicyPageClient() {
return render(
<LanguageProvider>
<CreatePolicyPageClient />
</LanguageProvider>,
);
}

describe("CreatePolicyPageClient", () => {
beforeEach(() => {
vi.useFakeTimers();
Expand All @@ -77,14 +87,14 @@ describe("CreatePolicyPageClient", () => {
});

it("moves from policy type selection into configure step", () => {
render(<CreatePolicyPageClient />);
renderCreatePolicyPageClient();
fireEvent.click(screen.getByRole("radio", { name: /weather protection/i }));

expect(screen.getByRole("heading", { name: /configure your policy/i })).toBeInTheDocument();
});

it("applies form validation for invalid coverage values", () => {
render(<CreatePolicyPageClient />);
renderCreatePolicyPageClient();
fireEvent.click(screen.getByRole("radio", { name: /weather protection/i }));

const coverageInput = screen.getByLabelText(/coverage amount/i);
Expand All @@ -110,7 +120,7 @@ describe("CreatePolicyPageClient", () => {
updatedAt: Date.now(),
}),
);
render(<CreatePolicyPageClient />);
renderCreatePolicyPageClient();

expect(screen.getByRole("heading", { name: /review your policy/i })).toBeInTheDocument();

Expand Down Expand Up @@ -144,7 +154,7 @@ describe("CreatePolicyPageClient", () => {
updatedAt: Date.now(),
}),
);
render(<CreatePolicyPageClient />);
renderCreatePolicyPageClient();

expect(screen.getByRole("heading", { name: /review your policy/i })).toBeInTheDocument();

Expand All @@ -155,4 +165,40 @@ describe("CreatePolicyPageClient", () => {
const triggerInput = screen.getByPlaceholderText("Trigger mock input") as HTMLInputElement;
expect(triggerInput.value).toBe("temperature > 25 AND rainfall > 50");
});

it("lets users undo an explicit draft discard", () => {
const savedDraft = {
policyType: "weather",
coverageAmount: "5000",
premium: "120",
triggerCondition: "rainfall > 50",
duration: "90",
oracleProvider: "weatherlink-prime",
};
localStorage.setItem(
"stellarinsure-policy-draft",
JSON.stringify({ data: savedDraft, updatedAt: 1000 }),
);

renderCreatePolicyPageClient();

expect(screen.getByRole("heading", { name: /review your policy/i })).toBeInTheDocument();

fireEvent.click(screen.getByRole("button", { name: /discard/i }));

expect(screen.getByText(/draft discarded/i)).toBeInTheDocument();
expect(screen.getByRole("heading", { name: /choose a policy type/i })).toBeInTheDocument();
expect(localStorage.getItem("stellarinsure-policy-draft")).toBeNull();

fireEvent.click(screen.getByRole("button", { name: /undo discard/i }));

expect(screen.getByRole("heading", { name: /review your policy/i })).toBeInTheDocument();
expect(screen.getByText(/rainfall > 50/i)).toBeInTheDocument();

act(() => {
vi.advanceTimersByTime(600);
});

expect(JSON.parse(localStorage.getItem("stellarinsure-policy-draft") ?? "{}").data).toEqual(savedDraft);
});
});
32 changes: 15 additions & 17 deletions frontend/src/app/create/create-page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ const INITIAL_DRAFT: PolicyDraft = {
oracleProvider: "",
};


function getDraftStep(draft: PolicyDraft): CreateStep {
if (draft.policyType && draft.coverageAmount && draft.triggerCondition) return 2;
if (draft.policyType) return 1;
return 0;
}

function parseConditionString(condition: string): ConditionRule[] | undefined {
if (!condition || condition.trim() === "") return undefined;
Expand Down Expand Up @@ -340,11 +344,7 @@ export default function CreatePolicyPageClient() {
hasClearedBackup,
isSaved,
} = usePolicyDraftAutosave<PolicyDraft>("stellarinsure-policy-draft", INITIAL_DRAFT);
const [step, setStep] = useState<CreateStep>(() => {
if (draft.policyType && draft.coverageAmount && draft.triggerCondition) return 2;
if (draft.policyType) return 1;
return 0;
});
const [step, setStep] = useState<CreateStep>(() => getDraftStep(draft));
const [txSteps, setTxSteps] = useState<TimelineStep[]>(DEFAULT_TX_STEPS);
const [coverageTouched, setCoverageTouched] = useState(false);
const [premiumTouched, setPremiumTouched] = useState(false);
Expand All @@ -369,6 +369,13 @@ export default function CreatePolicyPageClient() {
setReceipt(null);
}

function handleDiscardDraft() {
clearDraft();
setStep(0);
setReceipt(null);
setValidationErrors([]);
}

useEffect(() => {
const selectedPolicyType = draft.policyType;

Expand Down Expand Up @@ -576,13 +583,7 @@ export default function CreatePolicyPageClient() {
if (!restored) {
return;
}
if (restored.policyType && restored.coverageAmount && restored.triggerCondition) {
setStep(2);
} else if (restored.policyType) {
setStep(1);
} else {
setStep(0);
}
setStep(getDraftStep(restored));
}}
>
Undo discard
Expand Down Expand Up @@ -800,10 +801,7 @@ export default function CreatePolicyPageClient() {
<button
className="cta-secondary"
type="button"
onClick={() => {
clearDraft();
setStep(0);
}}
onClick={handleDiscardDraft}
>
{t("createPolicy.actions.discard")}
</button>
Expand Down
12 changes: 10 additions & 2 deletions frontend/src/hooks/use-autosave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export function useAutosave<T>(key: string, initial: T): [T, (next: T) => void,
export function usePolicyDraftAutosave<T>(key: string, initial: T): AutosaveControls<T> {
const backupKey = `${key}-backup`;
const editedAtRef = useRef(0);
const clearedAtRef = useRef<number | null>(null);

const [state, setStateInternal] = useState<T>(() => {
const stored = readStoredDraft<T>(key);
Expand Down Expand Up @@ -139,6 +140,7 @@ export function usePolicyDraftAutosave<T>(key: string, initial: T): AutosaveCont
}, [key]);

const clear = useCallback(() => {
const clearedAt = Date.now();
try {
const current = readStoredDraft<T>(key);
if (current) {
Expand All @@ -149,7 +151,8 @@ export function usePolicyDraftAutosave<T>(key: string, initial: T): AutosaveCont
// Ignore backup failures.
}

editedAtRef.current = Date.now();
clearedAtRef.current = clearedAt;
editedAtRef.current = clearedAt;
setStateInternal(initial);
setIsSaved(true);

Expand All @@ -174,11 +177,16 @@ export function usePolicyDraftAutosave<T>(key: string, initial: T): AutosaveCont
const backup = JSON.parse(backupRaw) as StoredDraft<T>;
const restoredAt = backup.updatedAt ?? Date.now();

if (restoredAt <= editedAtRef.current) {
if (clearedAtRef.current === null && restoredAt <= editedAtRef.current) {
return null;
}

if (clearedAtRef.current !== null && editedAtRef.current > clearedAtRef.current) {
return null;
}

editedAtRef.current = restoredAt;
clearedAtRef.current = null;
setStateInternal(backup.data);
writeStoredDraft(key, backup.data, restoredAt);
localStorage.removeItem(backupKey);
Expand Down