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: 4 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,7 @@
## 2025-06-16 - [Functional Matchers for Complex Buttons and Utility-First Layout]
**Learning:** React Testing Library's `findByText` can fail on buttons containing Materialize icons due to text node fragmentation and the icon's name being part of the `textContent`. Using a functional matcher that checks for partial content and tag name is more robust. Additionally, strictly adhering to utility classes (e.g., 'center-align') instead of inline styles ensures compliance with repository constraints and theme consistency.
**Action:** Always use functional matchers for testing interactive elements with icons and avoid inline styles by leveraging existing CSS utility classes.

## 2025-06-25 - [Copy-to-Clipboard UX and Immediate Feedback]
**Learning:** Adding a copy-to-clipboard feature for key user identifiers (like email or IDs) significantly reduces friction in user workflows. Implementing this with `navigator.clipboard.writeText` and providing immediate visual feedback via a toast notification ('Correo copiado al portapapeles') confirms success. Using icon-only buttons with explicit `aria-label` and `title` ensures the feature is both accessible and intuitive.
**Action:** Always provide a copy-to-clipboard option for static text that users frequently need to reuse, and ensure immediate feedback is provided upon action.
36 changes: 34 additions & 2 deletions src/v2/pages/V2Profile.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ import { useState, useMemo } from 'react';
import { useHistory } from 'react-router-dom';
import Auth from '../../modules/Auth';
import { alertSuccess } from '../../services/AlertService';
import Util from '../../commons/Util';
import '../styles/v2-theme.css';

const V2Profile = () => {

// Mock user with potentially missing email for testing
const [user] = useState(useMemo(() => Auth.getUserInfo() || { name: 'García', email: '' }, []));

const handleCopyEmail = () => {
if (user.email) {
navigator.clipboard.writeText(user.email);
Util.showToast('Correo copiado al portapapeles');
}
};
const [selectedSpecialties, setSelectedSpecialties] = useState(['Pediatría', 'Medicina Interna']);
const allSpecialties = ['Cirugía General', 'Ginecología y Obstetricia', 'Medicina Interna', 'Pediatría', 'Traumatología'];
const [isVerifying, setIsVerifying] = useState(false);
Expand Down Expand Up @@ -50,7 +58,20 @@ const V2Profile = () => {
</div>
<div>
<h2 className='v2-title-large'>Dr. {user.name}</h2>
<p className='v2-label-large v2-opacity-60'>{user.email || 'Correo no asignado'}</p>
<div className='v2-flex-align-center v2-flex-justify-center v2-gap-8'>
<p className='v2-label-large v2-opacity-60'>{user.email || 'Correo no asignado'}</p>
{user.email && (
<button
className='v2-btn-icon v2-btn-icon-sm'
style={{ width: '28px', height: '28px', minWidth: '28px' }}
onClick={handleCopyEmail}
aria-label='Copiar correo'
title='Copiar correo'
>
<i className='material-icons' style={{ fontSize: '16px' }}>content_copy</i>
</button>
)}
</div>
</div>
<div className='v2-card-tonal v2-flex-align-center v2-gap-8' style={{ padding: '12px 24px', borderRadius: '24px' }}>
<i className='material-icons v2-text-primary' style={{ fontSize: '20px' }}>workspace_premium</i>
Expand Down Expand Up @@ -93,7 +114,18 @@ const V2Profile = () => {
<span>Correo Electrónico</span>
</div>
{user.email ? (
<span className='v2-opacity-60'>{user.email}</span>
<div className='v2-flex-align-center v2-gap-8'>
<span className='v2-opacity-60'>{user.email}</span>
<button
className='v2-btn-icon v2-btn-icon-sm'
style={{ width: '28px', height: '28px', minWidth: '28px' }}
onClick={handleCopyEmail}
aria-label='Copiar correo'
title='Copiar correo'
>
<i className='material-icons' style={{ fontSize: '16px' }}>content_copy</i>
</button>
</div>
) : (
<button
className='v2-btn-tonal'
Expand Down
70 changes: 70 additions & 0 deletions src/v2/pages/V2Profile.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import V2Profile from './V2Profile';
import Auth from '../../modules/Auth';
import Util from '../../commons/Util';

vi.mock('../../modules/Auth');
vi.mock('../../commons/Util');

describe('V2Profile', () => {
const mockUser = {
name: 'Test Doctor',
email: 'test@doctor.com',
id: 123
};

beforeEach(() => {
vi.clearAllMocks();
Auth.getUserInfo.mockReturnValue(mockUser);

// Mock navigator.clipboard
Object.assign(navigator, {
clipboard: {
writeText: vi.fn().mockImplementation(() => Promise.resolve()),
},
});
});

it('renders profile information correctly', () => {
render(
<MemoryRouter>
<V2Profile />
</MemoryRouter>
);

expect(screen.getByText('Dr. Test Doctor')).toBeTruthy();
expect(screen.getAllByText('test@doctor.com').length).toBeGreaterThan(0);
});

it('handles copy email to clipboard', async () => {
render(
<MemoryRouter>
<V2Profile />
</MemoryRouter>
);

const copyButtons = screen.getAllByLabelText('Copiar correo');
expect(copyButtons.length).toBe(2);

fireEvent.click(copyButtons[0]);

expect(navigator.clipboard.writeText).toHaveBeenCalledWith('test@doctor.com');
expect(Util.showToast).toHaveBeenCalledWith('Correo copiado al portapapeles');
});

it('does not show copy buttons if email is missing', () => {
Auth.getUserInfo.mockReturnValue({ name: 'No Email Doctor', email: '' });

render(
<MemoryRouter>
<V2Profile />
</MemoryRouter>
);

expect(screen.queryByLabelText('Copiar correo')).toBeNull();
expect(screen.getByText('Correo no asignado')).toBeTruthy();
expect(screen.getByText('Asignar Correo')).toBeTruthy();
});
});
Loading