fix(auth,bank): guard switch-auth nav and surface banking retry
Frontend verify / commits (pull_request) Successful in 39s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Failing after 5m50s

AuthForm disables the switch-auth Link while submit is pending so pointer
click and Enter can no longer leave the auth route and lose the draft/error
context. The form restores a true Link after the request settles.

CardsPage/CardPage distinguish cold vs cached banking failures:
- cold card/section errors expose accessible alert + retry
- cached failures keep the previous data and offer a section GET retry
- summary-only/history-only outages do not hide successful sections
- notFound still skips the retry CTA

Tests cover the pending link guard, cold/cached cards error with retry,
summary failure with retry, and the prerequisite card list recovery.

Closes #56, #57
This commit is contained in:
2026-10-14 10:43:34 +03:00
parent e5f8bbe33c
commit e38ecd87d6
10 changed files with 251 additions and 32 deletions
@@ -30,4 +30,44 @@ describe("AuthForm", () => {
await waitFor(() => expect(onSubmit).not.toHaveBeenCalled());
expect(await screen.findByText("Пароли не совпадают")).toBeInTheDocument();
});
it.each([
["register", "Регистрация", "Уже есть аккаунт? Войти"],
["login", "Вход", "Нет аккаунта? Зарегистрироваться"],
])("blocks the switch-auth link while %s is submitting", async (_mode, submitLabel, linkLabel) => {
const { promise, resolve } = Promise.withResolvers<void>();
const onSubmit = vi.fn(() => promise);
render(
<MantineProvider>
<MemoryRouter>
<AuthForm
onSubmit={onSubmit}
register={_mode === "register"}
/>
</MemoryRouter>
</MantineProvider>,
);
fireEvent.change(screen.getByLabelText("Email"), {
target: { value: "user@example.com" },
});
fireEvent.change(screen.getByLabelText("Пароль"), {
target: { value: "a secure password" },
});
if (_mode === "register") {
fireEvent.change(screen.getByLabelText("Подтвердите пароль"), {
target: { value: "a secure password" },
});
}
fireEvent.click(screen.getByRole("button", { name: submitLabel }));
await waitFor(() => expect(onSubmit).toHaveBeenCalled());
expect(screen.queryByRole("link", { name: linkLabel })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: linkLabel })).toBeDisabled();
resolve();
await screen.findByRole("link", { name: linkLabel });
});
});
+21 -5
View File
@@ -1,4 +1,4 @@
import type { FC } from "react";
import type { FC, ReactNode } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { Alert, Button, Fieldset, PasswordInput, Stack, TextInput } from "@mantine/core";
@@ -15,7 +15,7 @@ import {
type AuthFormProps = {
onSubmit: (email: string, password: string) => Promise<void>;
register?: true;
register?: boolean;
};
const AuthForm: FC<AuthFormProps> = (props) => {
@@ -45,6 +45,24 @@ const AuthForm: FC<AuthFormProps> = (props) => {
});
const to = register ? "/login" : "/register";
const switchLabel = t(register ? "auth.haveAccount" : "auth.noAccount");
let switchLink: ReactNode;
if (isSubmitting) {
switchLink = (
<Button aria-disabled disabled variant="subtle">
{switchLabel}
</Button>
);
}
else {
switchLink = (
<Button component={Link} to={to} variant="subtle">
{switchLabel}
</Button>
);
}
return (
<FormProvider {...methods}>
@@ -77,9 +95,7 @@ const AuthForm: FC<AuthFormProps> = (props) => {
{t(register ? "auth.register" : "auth.login")}
</Button>
<Button component={Link} to={to} variant="subtle">
{t(register ? "auth.haveAccount" : "auth.noAccount")}
</Button>
{switchLink}
</Stack>
</Fieldset>
</form>