Skip to content
Closed
176 changes: 176 additions & 0 deletions components/dashboard/ResumePreviewForm.mouse-interactivity.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import React from 'react';
import type { HTMLAttributes, ReactNode } from 'react';
import ResumePreviewForm from './ResumePreviewForm';
import '@testing-library/jest-dom';

const toastMocks = vi.hoisted(() => ({
error: vi.fn(),
success: vi.fn(),
}));

vi.mock('sonner', () => ({
toast: {
error: toastMocks.error,
success: toastMocks.success,
},
}));

vi.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: HTMLAttributes<HTMLDivElement> & { children?: ReactNode }) => (
<div {...props}>{children}</div>
),
},
}));

const parsed = {
name: 'John Doe',
email: 'john@example.com',
phone: '1234567890',
skills: ['React'],
education: [],
experience: [],
};

describe('ResumePreviewForm - Mouse Interactivity & Touch Propagation', () => {
const onBack = vi.fn();
const onComplete = vi.fn();

beforeEach(() => {
vi.clearAllMocks();
});

it('keeps hover affordances intact while add actions still update the skill list', () => {
render(
<ResumePreviewForm
githubUsername="john"
parsed={parsed}
fileName="resume.pdf"
onBack={onBack}
onComplete={onComplete}
/>
);

const addButtons = screen.getAllByRole('button', { name: /^Add$/i });
const addSkillButton = addButtons[0];

fireEvent.mouseEnter(addSkillButton);
fireEvent.mouseLeave(addSkillButton);
fireEvent.click(addSkillButton);

expect(addSkillButton).toHaveClass('hover:text-emerald-500');
expect(screen.getAllByRole('textbox')).toHaveLength(4);
});

it('exposes hover tooltips on remove controls and removes nested sections on click', () => {
render(
<ResumePreviewForm
githubUsername="john"
parsed={parsed}
fileName="resume.pdf"
onBack={onBack}
onComplete={onComplete}
/>
);

const addButtons = screen.getAllByRole('button', { name: /^Add$/i });
fireEvent.click(addButtons[1]);
fireEvent.click(addButtons[2]);

const removeSkillButton = screen.getByRole('button', {

Check failure on line 82 in components/dashboard/ResumePreviewForm.mouse-interactivity.test.tsx

View workflow job for this annotation

GitHub Actions / Format · Lint · Typecheck · Test

components/dashboard/ResumePreviewForm.mouse-interactivity.test.tsx > ResumePreviewForm - Mouse Interactivity & Touch Propagation > exposes hover tooltips on remove controls and removes nested sections on click

TestingLibraryElementError: Unable to find an accessible element with the role "button" and name `/^Remove skill \d+$/i` Here are the accessible roles: heading: Name "Review Parsed Data": <h3 class="text-lg font-bold text-gray-900 dark:text-white" /> -------------------------------------------------- paragraph: Name "": <p class="mt-1 text-xs text-gray-500 dark:text-white/50" /> -------------------------------------------------- textbox: Name "": <input class="w-full rounded-lg border border-black/10 bg-gray-50 px-3 py-2 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-emerald-500 dark:border-[rgba(255,255,255,0.1)] dark:bg-[#111] dark:text-white" type="text" value="John Doe" /> Name "": <input class="w-full rounded-lg border border-black/10 bg-gray-50 px-3 py-2 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-emerald-500 dark:border-[rgba(255,255,255,0.1)] dark:bg-[#111] dark:text-white" type="email" value="john@example.com" /> Name "": <input class="w-24 bg-transparent text-sm text-gray-900 outline-none dark:text-white" type="text" value="React" /> Name "": <input class="rounded-lg border border-black/10 bg-white px-2.5 py-1.5 text-sm outline-none focus:ring-2 focus:ring-emerald-500 dark:border-[rgba(255,255,255,0.1)] dark:bg-[#1a1a1a] dark:text-white" placeholder="Institution" type="text" value="" /> Name "": <input class="rounded-lg border border-black/10 bg-white px-2.5 py-1.5 text-sm outline-none focus:ring-2 focus:ring-emerald-500 dark:border-[rgba(255,255,255,0.1)] dark:bg-[#1a1a1a] dark:text-white" placeholder="Degree" type="text" value="" /> Name "": <input class="rounded-lg border border-black/10 bg-white px-2.5 py-1.5 text-sm outline-none focus:ring-2 focus:ring-emerald-500 dark:border-[rgba(255,255,255,0.1)] dark:bg-[#1a1a1a] dark:text-white" placeholder="Field of Study" type="text" value="" /> Name "": <input class="rounded-lg border border-black/10 bg-white px-2.5 py-1.5 text-sm outline-none focus:ring-2 focus:ring-emerald-500 dark:border-[rgba(255,255,255,0.1)] dark:bg-[#1a1a1a] dark:text-white" placeholder="Start Year" type="text" value="" /> Name "": <input class="rounded-lg border border-black/10 bg-white px-2.5 py-1.5 text-sm outline-none focus:ring-2 focus:ring-emerald-500 dark:border-[rgba(255,255,255,0.1)] dark:bg-[#1a1a1a] dark:text-white" placeholder="End Year" type="text" value="" /> Name "": <input class="rounded-lg border border-black/10 bg-white px-2.5 py-1.5 text-sm outline-none focus:ring-2 focus:ring-emerald-500 dark:border-[rgba(255,255,255,0.1)] dark:bg-[#1a1a1a] dark:text-white" placeholder="Company" type="text" value="" /> Name "": <input class="rounded-lg border border-black/10 bg-white px-2.5 py-1.5 text-sm outline-none focus:ring-2 focus:ring-emerald-500 dark:border-[rgba(255,255,255,0.1)] dark:bg-[#1a1a1a] dark:text-white" placeholder="Role" type="text" value="" /> Name "": <input class="rounded-lg border border-black/10 bg-white px-2.5 py-1.5 text-sm outline-none focus:ring-2 focus:ring-emerald-500 dark:border-[rgba(255,255,255,0.1)] dark:bg-[#1a1a1a] dark:text-white" placeholder="Start Year" type="text" value="" /> Name "": <input class="rounded-lg border border-black/10 bg-white px-2.5 py-1.5 text-sm outline-none focus:ring-2 focus:ring-emerald-500 dark:border-[rgba(255,255,255,0.1)] dark:bg-[#1a1a1a] dark:text-white" placeholder="End Year" type="text" value="" /> Name "": <textarea class="w-full rounded-lg border border-black/10 bg-white px-2.5 py-1.5 text-sm outline-none focus:ring-2 focus:ring-emerald-500 dark:border-[rgba(255,255,255,0.1)] dark:bg-[#1a1a1a] dark:text-white" placeholder="Description" rows="2" /> -------------------------------------------------- button: Name "Add": <button class="flex items-center gap-1 text-xs f
name: /^Remove skill \d+$/i,
});
const removeEducationButton = screen.getByRole('button', {
name: /Remove education entry 1/i,
});
const removeExperienceButton = screen.getByRole('button', {
name: /Remove experience entry 1/i,
});

expect(removeSkillButton).toHaveAttribute('title', 'Remove skill 1');
expect(removeEducationButton).toHaveAttribute('title', 'Remove education entry 1');
expect(removeExperienceButton).toHaveAttribute('title', 'Remove experience entry 1');

fireEvent.mouseEnter(removeSkillButton);
fireEvent.click(removeSkillButton);
fireEvent.click(removeEducationButton);
fireEvent.click(removeExperienceButton);

expect(screen.getAllByRole('textbox')).toHaveLength(2);
expect(screen.queryByPlaceholderText('Institution')).not.toBeInTheDocument();
expect(screen.queryByPlaceholderText('Company')).not.toBeInTheDocument();
});

it('propagates touch events from interactive controls up to parent listeners', () => {
const parentTouchStart = vi.fn();
const parentTouchEnd = vi.fn();

render(
<div onTouchStart={parentTouchStart} onTouchEnd={parentTouchEnd}>
<ResumePreviewForm
githubUsername="john"
parsed={parsed}
fileName="resume.pdf"
onBack={onBack}
onComplete={onComplete}
/>
</div>
);

const addExperienceButton = screen.getAllByRole('button', { name: /^Add$/i })[2];

fireEvent.touchStart(addExperienceButton, {
touches: [{ identifier: 1, clientX: 24, clientY: 64 }],
});
fireEvent.touchEnd(addExperienceButton, {
changedTouches: [{ identifier: 1, clientX: 24, clientY: 64 }],
});
fireEvent.click(addExperienceButton);

expect(parentTouchStart).toHaveBeenCalled();
expect(parentTouchEnd).toHaveBeenCalled();
expect(screen.getByPlaceholderText('Company')).toBeInTheDocument();
});

it('transitions the save button into a pending state and completes after the request resolves', async () => {
let resolveFetch:
| ((value: { ok: boolean; json: () => Promise<{ success: boolean }> }) => void)
| undefined;

const pendingFetch = new Promise<{ ok: boolean; json: () => Promise<{ success: boolean }> }>(
(resolve) => {
resolveFetch = resolve;
}
);

vi.stubGlobal('fetch', vi.fn().mockReturnValue(pendingFetch));

render(
<ResumePreviewForm
githubUsername="john"
parsed={parsed}
fileName="resume.pdf"
onBack={onBack}
onComplete={onComplete}
/>
);

const saveButton = screen.getByRole('button', { name: /Save Profile/i });
fireEvent.click(saveButton);

expect(screen.getByRole('button', { name: /Saving.../i })).toBeDisabled();

resolveFetch?.({
ok: true,
json: async () => ({ success: true }),
});

await waitFor(() => {
expect(onComplete).toHaveBeenCalledTimes(1);
});

expect(toastMocks.success).toHaveBeenCalledWith('Profile saved successfully!');
});
});
Loading