|
|
@@ -6,7 +6,7 @@ import { describe, it, expect, beforeEach } from 'vitest';
|
|
|
import { screen, waitFor } from '@testing-library/react';
|
|
|
import userEvent from '@testing-library/user-event';
|
|
|
import { render } from '../utils';
|
|
|
-import { ProjectsPage } from '../../pages/ProjectsPage';
|
|
|
+import { ProjectsPage, ProjectModal } from '../../pages/ProjectsPage';
|
|
|
import { http, HttpResponse } from 'msw';
|
|
|
import { server } from '../mocks/server';
|
|
|
|
|
|
@@ -305,4 +305,78 @@ describe('ProjectsPage', () => {
|
|
|
expect(document.querySelectorAll('[aria-hidden="true"] img').length).toBe(0);
|
|
|
});
|
|
|
});
|
|
|
+
|
|
|
+ describe('modal scrolls on short viewports (#1642)', () => {
|
|
|
+ /**
|
|
|
+ * Reporter on a Pi screen couldn't reach the Save button when editing a
|
|
|
+ * project because the modal had no max-h / overflow. The structural fix
|
|
|
+ * puts a max-h on the card, the form fields in a `flex-1 overflow-y-auto`
|
|
|
+ * wrapper, and the Save/Cancel buttons in a `flex-shrink-0` sibling so
|
|
|
+ * they're always visible regardless of scroll position.
|
|
|
+ *
|
|
|
+ * jsdom doesn't compute layout heights so we can't simulate the actual
|
|
|
+ * overflow. We pin the structure instead: the scrollable wrapper exists,
|
|
|
+ * the Save button is NOT a descendant of it, and the card has a max-h.
|
|
|
+ * A future refactor that removes any of these would re-introduce the bug.
|
|
|
+ */
|
|
|
+ const editableProject = {
|
|
|
+ id: 7,
|
|
|
+ name: 'Spool holder',
|
|
|
+ description: null,
|
|
|
+ color: '#00ae42',
|
|
|
+ url: null,
|
|
|
+ cover_image_filename: null,
|
|
|
+ archive_count: 0,
|
|
|
+ total_print_time_seconds: 0,
|
|
|
+ total_filament_grams: 0,
|
|
|
+ target_plates_count: null,
|
|
|
+ target_parts_count: null,
|
|
|
+ tags: null,
|
|
|
+ due_date: null,
|
|
|
+ priority: null,
|
|
|
+ budget: null,
|
|
|
+ status: 'active' as const,
|
|
|
+ created_at: '2024-01-01T00:00:00Z',
|
|
|
+ updated_at: '2024-01-01T00:00:00Z',
|
|
|
+ };
|
|
|
+
|
|
|
+ it('renders the action footer outside the scrollable fields wrapper', () => {
|
|
|
+ render(
|
|
|
+ <ProjectModal
|
|
|
+ project={editableProject}
|
|
|
+ onClose={() => {}}
|
|
|
+ onSave={() => {}}
|
|
|
+ isLoading={false}
|
|
|
+ currencySymbol="€"
|
|
|
+ t={((k: string) => k) as never}
|
|
|
+ />,
|
|
|
+ );
|
|
|
+
|
|
|
+ const saveButton = screen.getByRole('button', { name: 'common.save' });
|
|
|
+ const scrollable = document.querySelector('.overflow-y-auto');
|
|
|
+ expect(scrollable).not.toBeNull();
|
|
|
+ // The save button must live OUTSIDE the scrollable region — otherwise
|
|
|
+ // a long form pushes it below the fold on short viewports (#1642).
|
|
|
+ expect(scrollable!.contains(saveButton)).toBe(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('caps the modal card height so it cannot exceed the viewport', () => {
|
|
|
+ render(
|
|
|
+ <ProjectModal
|
|
|
+ project={editableProject}
|
|
|
+ onClose={() => {}}
|
|
|
+ onSave={() => {}}
|
|
|
+ isLoading={false}
|
|
|
+ currencySymbol="€"
|
|
|
+ t={((k: string) => k) as never}
|
|
|
+ />,
|
|
|
+ );
|
|
|
+
|
|
|
+ // Card has max-h set so it never extends past the viewport — without
|
|
|
+ // this, vertical-center alignment pushes the bottom of the modal
|
|
|
+ // (including the action footer) off-screen.
|
|
|
+ const card = document.querySelector('.max-h-\\[calc\\(100vh-2rem\\)\\]');
|
|
|
+ expect(card).not.toBeNull();
|
|
|
+ });
|
|
|
+ });
|
|
|
});
|