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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions resources/js/react/pages/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export default function Login() {
{route().has('password.request') && (
<Link
href={forgotUrl}
className="text-primary ml-auto inline-block text-sm font-medium underline-offset-4 hover:underline"
className="text-primary ml-auto inline-block text-sm font-medium whitespace-nowrap underline-offset-4 hover:underline"
data-testid="forgot-password-link"
>
{t('Forgot your password?')}
Expand All @@ -145,7 +145,7 @@ export default function Login() {
{(page.props.auth as any)?.magic_link_enabled && (
<Link
href={route('magic-link.create')}
className="text-primary/70 font-medium underline-offset-4 hover:underline"
className="text-primary font-medium underline-offset-4 hover:underline"
data-testid="magic-link-login-link"
>
{t('Login with magic link')}
Expand Down
57 changes: 54 additions & 3 deletions resources/js/react/pages/Register.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Button } from '@/components/ui/button';
import { Field, FieldError } from '@/components/ui/field';
import { Checkbox } from '@/components/ui/checkbox';
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useT } from '@/i18n';
Expand All @@ -17,13 +18,20 @@ export default function Register() {
email: '',
password: '',
password_confirmation: '',
terms: false,
});

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
post(route('register'), { preserveScroll: true });
};

const canSubmit =
data.name.trim() !== '' &&
data.email.trim() !== '' &&
data.password !== '' &&
data.terms;

return (
<AuthCardLayout
title={t('Create your account')}
Expand Down Expand Up @@ -115,10 +123,53 @@ export default function Register() {
)}
</Field>

<Field
orientation="horizontal"
className="mt-6 items-start"
data-invalid={!!errors.terms}
>
<Checkbox
id="terms"
name="terms"
data-testid="terms-checkbox"
checked={data.terms}
onCheckedChange={(checked) =>
setData('terms', !!checked)
}
aria-invalid={!!errors.terms}
/>
<FieldLabel
htmlFor="terms"
className="text-sm font-normal leading-snug"
>
{t('I agree to the')}{' '}
<Link
href={route('terms')}
className="text-primary font-medium underline-offset-4 hover:underline"
data-testid="terms-link"
>
{t('Terms of Service')}
</Link>{' '}
{t('and the')}{' '}
<Link
href={route('privacy')}
className="text-primary font-medium underline-offset-4 hover:underline"
data-testid="privacy-link"
>
{t('Privacy Policy')}
</Link>
</FieldLabel>
</Field>
{errors.terms && (
<FieldError data-testid="terms-error">
{errors.terms}
</FieldError>
)}

<Button
type="submit"
className="mt-3 w-full"
disabled={processing}
disabled={processing || !canSubmit}
data-testid="register-button"
>
{t('Register')}
Expand All @@ -128,7 +179,7 @@ export default function Register() {
{t('Already registered?')}{' '}
<Link
href={route('login')}
className="text-primary/70 font-medium underline-offset-4 hover:underline"
className="text-primary font-medium underline-offset-4 hover:underline"
data-testid="login-link"
>
{t('Log in')}
Expand Down
4 changes: 2 additions & 2 deletions resources/js/vue/pages/Login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const forgotUrl = computed(() =>
<Link
v-if="route().has('password.request')"
:href="forgotUrl"
class="text-primary ml-auto inline-block text-sm font-medium underline-offset-4 hover:underline"
class="text-primary ml-auto inline-block text-sm font-medium whitespace-nowrap underline-offset-4 hover:underline"
data-testid="forgot-password-link"
:data-invalid="false"
>
Expand All @@ -93,7 +93,7 @@ const forgotUrl = computed(() =>
<Link
v-if="$page.props.auth.magic_link_enabled"
:href="route('magic-link.create')"
class="text-primary/70 font-medium underline-offset-4 hover:underline"
class="text-primary font-medium underline-offset-4 hover:underline"
data-testid="magic-link-login-link"
>
{{ $t('Login with magic link') }}
Expand Down
63 changes: 61 additions & 2 deletions resources/js/vue/pages/Register.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
<script setup lang="ts">
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
import InputField from '@/components/ui/input/InputField.vue';
import { Form, Link } from '@inertiajs/vue3';
import { Form, Link, usePage } from '@inertiajs/vue3';
import { computed, ref } from 'vue';
import SocialiteProviders from '../components/SocialiteProviders.vue';
import AuthCardLayout from '../layouts/AuthCardLayout.vue';

const page = usePage();
const termsError = computed(() => page.props.errors?.terms);

const nameRef = ref('');
const emailRef = ref('');
const passwordRef = ref('');
const termsRef = ref(false);

const canSubmit = computed(
() =>
!!nameRef.value.trim() &&
!!emailRef.value.trim() &&
!!passwordRef.value &&
termsRef.value,
);
</script>

<template>
Expand All @@ -28,6 +47,7 @@ import AuthCardLayout from '../layouts/AuthCardLayout.vue';
:label="$t('Name')"
:placeholder="$t('Enter your full name')"
autocomplete="name"
v-model="nameRef"
/>

<!-- Email -->
Expand All @@ -37,6 +57,7 @@ import AuthCardLayout from '../layouts/AuthCardLayout.vue';
:label="$t('Email')"
:placeholder="$t('Enter your email')"
autocomplete="email"
v-model="emailRef"
/>

<!-- Password -->
Expand All @@ -47,12 +68,50 @@ import AuthCardLayout from '../layouts/AuthCardLayout.vue';
:placeholder="$t('Enter your password')"
autocomplete="new-password"
required
v-model="passwordRef"
/>

<!-- Terms & Privacy -->
<Field
orientation="horizontal"
class="mt-6 items-start"
:data-invalid="!!termsError"
>
<Checkbox
id="terms"
name="terms"
data-testid="terms-checkbox"
:aria-invalid="!!termsError"
v-model="termsRef"
/>
<FieldLabel for="terms" class="text-sm font-normal leading-snug">
{{ $t('I agree to the') }}
<Link
:href="route('terms')"
class="text-primary font-medium underline-offset-4 hover:underline"
data-testid="terms-link"
>
{{ $t('Terms of Service') }}
</Link>
{{ $t('and the') }}
<Link
:href="route('privacy')"
class="text-primary font-medium underline-offset-4 hover:underline"
data-testid="privacy-link"
>
{{ $t('Privacy Policy') }}
</Link>
</FieldLabel>
</Field>
<FieldError v-if="termsError" data-testid="terms-error">
{{ termsError }}
</FieldError>

<Button
type="submit"
class="mt-3 w-full"
data-testid="register-button"
:disabled="!canSubmit"
>
{{ $t('Register') }}
</Button>
Expand All @@ -63,7 +122,7 @@ import AuthCardLayout from '../layouts/AuthCardLayout.vue';
{{ $t('Already registered?') }}
<Link
:href="route('login')"
class="text-primary/70 font-medium underline-offset-4 hover:underline"
class="text-primary font-medium underline-offset-4 hover:underline"
data-testid="login-link"
>
{{ $t('Log in') }}
Expand Down
13 changes: 13 additions & 0 deletions src/Http/Requests/RegisterRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ public function rules(): array
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:'.User::class,
'password' => ['required', Rules\Password::defaults()],
'terms' => ['accepted'],
];
}

/**
* Get the error messages for the defined validation rules.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'terms.accepted' => __('You must accept the Terms of Service and Privacy Policy to register.'),
];
}

Expand Down
15 changes: 15 additions & 0 deletions tests/Feature/RegisterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public function test_user_can_register_with_valid_data(): void
'email' => 'test@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
'terms' => true,
]);

$this->assertAuthenticated();
Expand All @@ -45,6 +46,7 @@ public function test_registered_user_is_assigned_user_role(): void
'email' => 'test@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
'terms' => true,
]);

$user = User::where('email', 'test@example.com')->firstOrFail();
Expand All @@ -60,6 +62,7 @@ public function test_welcome_notification_is_sent_on_registration(): void
'email' => 'test@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
'terms' => true,
]);

$user = User::where('email', 'test@example.com')->firstOrFail();
Expand All @@ -75,6 +78,7 @@ public function test_password_is_hashed_on_registration(): void
'email' => 'test@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
'terms' => true,
]);

$user = User::where('email', 'test@example.com')->firstOrFail();
Expand Down Expand Up @@ -135,4 +139,15 @@ public function test_register_validates_password_is_required(): void

$response->assertInvalid('password');
}

public function test_register_validates_terms_must_be_accepted(): void
{
$response = $this->post(route('register'), [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password123',
]);

$response->assertInvalid('terms');
}
}
4 changes: 4 additions & 0 deletions tests/e2e/pages/RegisterPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export class RegisterPage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly passwordToggle: Locator;
readonly termsCheckbox: Locator;
readonly registerButton: Locator;
readonly signupEndpoint: string;
readonly redirectEndpoint: string;
Expand All @@ -27,6 +28,7 @@ export class RegisterPage {
this.emailInput = page.getByTestId('email');
this.passwordInput = page.getByTestId('password');
this.passwordToggle = page.getByTestId('password-toggle');
this.termsCheckbox = page.getByTestId('terms-checkbox');
this.registerButton = page.getByTestId('register-button');
}

Expand All @@ -38,6 +40,7 @@ export class RegisterPage {
await this.nameInput.fill(name);
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.termsCheckbox.check();

await this.registerButton.click();
}
Expand All @@ -47,6 +50,7 @@ export class RegisterPage {
await expect(this.nameInput).toBeVisible();
await expect(this.emailInput).toBeVisible();
await expect(this.passwordInput).toBeVisible();
await expect(this.termsCheckbox).toBeVisible();
await expect(this.registerButton).toBeVisible();
}

Expand Down
15 changes: 15 additions & 0 deletions tests/e2e/tests/register/register.basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,24 @@ test.describe.parallel('Register Basics', () => {
await registerPage.nameInput.fill(user.name);
await registerPage.emailInput.fill(user.email);
await registerPage.passwordInput.fill(user.password);
await registerPage.termsCheckbox.check();

await registerPage.passwordInput.press('Enter');

await expectSuccessfulRegistration();
});

test('disables register button until all fields are filled and terms are accepted', async () => {
const user = newUser();

await expect(registerPage.registerButton).toBeDisabled();

await registerPage.nameInput.fill(user.name);
await registerPage.emailInput.fill(user.email);
await registerPage.passwordInput.fill(user.password);
await expect(registerPage.registerButton).toBeDisabled();

await registerPage.termsCheckbox.check();
await expect(registerPage.registerButton).toBeEnabled();
});
});