From f4bbbfd767388642b856b53814187b9c85e4ac22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Mon, 8 Jun 2026 09:03:15 +0200 Subject: [PATCH] fix(auth): prevent FormData crash on successful login (#503) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Removes `resetOnSuccess={['password']}` from the login form. ## Sentry - Fixes PHP-LARAVEL-2Y — `TypeError: Failed to construct 'FormData': parameter 1 is not of type 'HTMLFormElement'.` (6 users, on `/login`) ## Root cause On a successful login the server redirects to the dashboard, which **unmounts the login form**. `resetOnSuccess` then fires a form reset, and Inertia's reset handler constructs `new FormData(ref)` from the form ref — now stale/null — which throws. The reset serves no purpose on login (the user navigates away regardless), so removing it eliminates the crash. ## Tests - Added `resources/js/pages/auth/login.test.tsx`: renders the login page and asserts the `
` is not configured to reset on success. --- resources/js/pages/auth/login.test.tsx | 66 ++++++++++++++++++++++++++ resources/js/pages/auth/login.tsx | 1 - 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 resources/js/pages/auth/login.test.tsx diff --git a/resources/js/pages/auth/login.test.tsx b/resources/js/pages/auth/login.test.tsx new file mode 100644 index 00000000..6563d3ec --- /dev/null +++ b/resources/js/pages/auth/login.test.tsx @@ -0,0 +1,66 @@ +import { render, screen } from '@testing-library/react'; +import { type ReactNode } from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import Login from './login'; + +globalThis.ResizeObserver ??= class { + observe() {} + unobserve() {} + disconnect() {} +}; + +const capturedFormProps: Record = {}; + +vi.mock('@inertiajs/react', () => ({ + Form: ({ + children, + ...props + }: { + children: (renderProps: { + processing: boolean; + errors: Record; + }) => ReactNode; + } & Record) => { + Object.assign(capturedFormProps, props); + + return {children({ processing: false, errors: {} })}; + }, + Head: () => null, + Link: ({ children }: { children: ReactNode }) => {children}, + usePage: () => ({ props: { demoCredentials: undefined } }), +})); + +vi.mock('@/layouts/auth-layout', () => ({ + default: ({ children }: { children: ReactNode }) =>
{children}
, +})); + +vi.mock('@/lib/key-storage', () => ({ + clearKey: vi.fn(), +})); + +vi.mock('@/utils/i18n', () => ({ + __: (key: string) => key, +})); + +vi.mock('@/routes', () => ({ + register: () => ({ url: '/register' }), +})); + +vi.mock('@/routes/login', () => ({ + store: { form: () => ({ action: '/login', method: 'post' }) }, +})); + +vi.mock('@/routes/password', () => ({ + request: () => ({ url: '/forgot-password' }), +})); + +describe('Login', () => { + it('does not reset the form on success', () => { + // The success redirect unmounts the form; resetting it afterwards makes + // Inertia construct a FormData from a stale ref and crash (PHP-LARAVEL-2Y). + render(); + + expect(screen.getByRole('button', { name: /log in/i })).toBeTruthy(); + expect(capturedFormProps.resetOnSuccess).toBeUndefined(); + }); +}); diff --git a/resources/js/pages/auth/login.tsx b/resources/js/pages/auth/login.tsx index fddd8aef..ac0f6ba1 100644 --- a/resources/js/pages/auth/login.tsx +++ b/resources/js/pages/auth/login.tsx @@ -54,7 +54,6 @@ export default function Login({