Skip to content
Merged
2 changes: 2 additions & 0 deletions .changeset/wild-carpets-make.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { UserProfileFormError } from '@clerk/ui/mosaic/user-profile/user-profile-account-section/user-profile-account-section.types';
import { UserProfileSaveError } from '@clerk/ui/mosaic/user-profile/user-profile-account-section/user-profile-account-section.types';
import type { UserProfileEditNameValue } from '@clerk/ui/mosaic/user-profile/user-profile-account-section/user-profile-edit-name.view';
import type { UserProfileEditNameValue } from '@clerk/ui/mosaic/user-profile/user-profile-account-section/user-profile-edit-name.dialog';
import { useState } from 'react';

export interface UserProfileEditNameFixtureOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { describe, expect, it, vi } from 'vitest';

import { Button } from '../../components/button';
import { MosaicProvider } from '../../MosaicProvider';
import type { UserProfileEditNameViewProps } from '../user-profile-account-section/user-profile-edit-name.view';
import { UserProfileEditNameView } from '../user-profile-account-section/user-profile-edit-name.view';
import type { UserProfileEditNameDialogProps } from '../user-profile-account-section/user-profile-edit-name.dialog';
import { UserProfileEditNameDialog } from '../user-profile-account-section/user-profile-edit-name.dialog';

function renderView(overrides: Partial<UserProfileEditNameViewProps> = {}) {
const props: UserProfileEditNameViewProps = {
function renderView(overrides: Partial<UserProfileEditNameDialogProps> = {}) {
const props: UserProfileEditNameDialogProps = {
open: true,
onOpenChange: vi.fn(),
firstName: 'Preston',
Expand All @@ -22,7 +22,7 @@ function renderView(overrides: Partial<UserProfileEditNameViewProps> = {}) {
props,
...render(
<MosaicProvider>
<UserProfileEditNameView {...props} />
<UserProfileEditNameDialog {...props} />
</MosaicProvider>,
),
};
Expand All @@ -32,7 +32,7 @@ const firstNameField = () => screen.getByLabelText('First name');
const lastNameField = () => screen.getByLabelText('Last name');
const saveButton = () => screen.getByRole('button', { name: 'Save changes' });

describe('UserProfileEditNameView', () => {
describe('UserProfileEditNameDialog', () => {
it('renders nothing until the caller opens it', () => {
renderView({ open: false });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { describe, expect, it, vi } from 'vitest';

import { Button } from '../../components/button';
import { MosaicProvider } from '../../MosaicProvider';
import type { UserProfileEditUsernameViewProps } from '../user-profile-account-section/user-profile-edit-username.view';
import { UserProfileEditUsernameView } from '../user-profile-account-section/user-profile-edit-username.view';
import type { UserProfileEditUsernameDialogProps } from '../user-profile-account-section/user-profile-edit-username.dialog';
import { UserProfileEditUsernameDialog } from '../user-profile-account-section/user-profile-edit-username.dialog';

function renderView(overrides: Partial<UserProfileEditUsernameViewProps> = {}) {
const props: UserProfileEditUsernameViewProps = {
function renderView(overrides: Partial<UserProfileEditUsernameDialogProps> = {}) {
const props: UserProfileEditUsernameDialogProps = {
open: true,
onOpenChange: vi.fn(),
username: 'prestonxyz',
Expand All @@ -20,7 +20,7 @@ function renderView(overrides: Partial<UserProfileEditUsernameViewProps> = {}) {
props,
...render(
<MosaicProvider>
<UserProfileEditUsernameView {...props} />
<UserProfileEditUsernameDialog {...props} />
</MosaicProvider>,
),
};
Expand All @@ -29,7 +29,7 @@ function renderView(overrides: Partial<UserProfileEditUsernameViewProps> = {}) {
const usernameField = () => screen.getByLabelText('Username');
const saveButton = () => screen.getByRole('button', { name: 'Save changes' });

describe('UserProfileEditUsernameView', () => {
describe('UserProfileEditUsernameDialog', () => {
it('renders nothing until the caller opens it', () => {
renderView({ open: false });

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';

import { MosaicProvider } from '../../MosaicProvider';
import type { UserProfilePictureRowViewProps } from '../user-profile-account-section/user-profile-picture-row.view';
import { UserProfilePictureRowView } from '../user-profile-account-section/user-profile-picture-row.view';

function renderView(overrides: Partial<UserProfilePictureRowViewProps> = {}) {
return render(
<MosaicProvider>
<UserProfilePictureRowView
name='Preston Booth'
{...overrides}
/>
</MosaicProvider>,
);
}

describe('UserProfilePictureRowView', () => {
it('renders and clears the error supplied by the section', () => {
const { rerender } = renderView({ errorMessage: 'File size exceeds the maximum limit of 10MB.' });

expect(screen.getByRole('alert')).toHaveTextContent('File size exceeds the maximum limit of 10MB.');

rerender(
<MosaicProvider>
<UserProfilePictureRowView name='Preston Booth' />
</MosaicProvider>,
);

expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});

it('offers Upload while the avatar is only a generated default', () => {
renderView({
hasImage: false,
imageUrl: 'https://img.clerk.com/generated-default.png',
onChange: vi.fn(),
onRemove: vi.fn(),
});

expect(screen.getByRole('button', { name: 'Upload' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Manage profile picture' })).toBeNull();
});

it('offers change and remove in a menu once a profile picture is set', async () => {
const onChange = vi.fn();
const onRemove = vi.fn();
const user = userEvent.setup();
renderView({
hasImage: true,
imageUrl: 'https://example.com/avatar.png',
onChange,
onRemove,
});

expect(screen.queryByRole('button', { name: 'Upload' })).toBeNull();
await user.click(screen.getByRole('button', { name: 'Manage profile picture' }));

expect(screen.getByRole('menuitem', { name: 'Change avatar' })).toBeInTheDocument();
await user.click(screen.getByRole('menuitem', { name: 'Remove avatar' }));

expect(onRemove).toHaveBeenCalledOnce();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,11 @@ describe('UserProfileProfilePanelView', () => {
expect(screen.queryByRole('button', { name: 'Manage profile picture' })).toBeNull();

const input = container.querySelector<HTMLInputElement>('input[type="file"]');
expect(input).not.toBeNull();
if (!input) {
throw new Error('File picker not found');
}
const file = new File(['avatar'], 'avatar.png', { type: 'image/png' });
await user.upload(input as HTMLInputElement, file);
await user.upload(input, file);

expect(onProfilePictureChange).toHaveBeenCalledWith(file);
});
Expand All @@ -85,7 +87,11 @@ describe('UserProfileProfilePanelView', () => {
const { container } = renderView({ onProfilePictureChange, onProfilePictureReject });

const oversized = new File([new Uint8Array(10 * 1000 * 1000 + 1)], 'big.png', { type: 'image/png' });
await user.upload(container.querySelector<HTMLInputElement>('input[type="file"]') as HTMLInputElement, oversized);
const input = container.querySelector<HTMLInputElement>('input[type="file"]');
if (!input) {
throw new Error('File picker not found');
}
await user.upload(input, oversized);

expect(onProfilePictureChange).not.toHaveBeenCalled();
expect(onProfilePictureReject).toHaveBeenCalledWith([{ file: oversized, reason: 'size' }]);
Expand All @@ -96,7 +102,10 @@ describe('UserProfileProfilePanelView', () => {
it('clears the rejection once an acceptable file is picked', async () => {
const user = userEvent.setup();
const { container } = renderView({ onProfilePictureChange: vi.fn() });
const input = container.querySelector<HTMLInputElement>('input[type="file"]') as HTMLInputElement;
const input = container.querySelector<HTMLInputElement>('input[type="file"]');
if (!input) {
throw new Error('File picker not found');
}

await user.upload(input, new File([new Uint8Array(10 * 1000 * 1000 + 1)], 'big.png', { type: 'image/png' }));
expect(screen.getByRole('alert')).toBeInTheDocument();
Expand All @@ -105,38 +114,6 @@ describe('UserProfileProfilePanelView', () => {
expect(screen.queryByRole('alert')).toBeNull();
});

it('offers Upload while the avatar is only a generated default', () => {
renderView({
hasImage: false,
imageUrl: 'https://img.clerk.com/generated-default.png',
onProfilePictureChange: vi.fn(),
onRemoveProfilePicture: vi.fn(),
});

expect(screen.getByRole('button', { name: 'Upload' })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Manage profile picture' })).toBeNull();
});

it('offers change and remove in a menu once a profile picture is set', async () => {
const onProfilePictureChange = vi.fn();
const onRemoveProfilePicture = vi.fn();
const user = userEvent.setup();
renderView({
hasImage: true,
imageUrl: 'https://example.com/avatar.png',
onProfilePictureChange,
onRemoveProfilePicture,
});

expect(screen.queryByRole('button', { name: 'Upload' })).toBeNull();
await user.click(screen.getByRole('button', { name: 'Manage profile picture' }));

expect(screen.getByRole('menuitem', { name: 'Change avatar' })).toBeInTheDocument();
await user.click(screen.getByRole('menuitem', { name: 'Remove avatar' }));

expect(onRemoveProfilePicture).toHaveBeenCalledOnce();
});

it('breaks out both contact types when multiple accounts are allowed', () => {
renderView({
emails: [{ id: 'email_1', value: 'item1@clerk.dev', isDefault: true }],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as stylex from '@stylexjs/stylex';

import { space } from '../../tokens.stylex';

export const styles = stylex.create({
sections: {
gap: space['8'],
display: 'flex',
flexDirection: 'column',
width: '100%',
},
});
Loading
Loading