fix: adapt tests for last-page-only dismiss and fix editor z-index

- SystemNoticeModal tests: navigate to last page before testing
  X button, ESC, and CTA dismiss (matches new last-page-only behavior)
- EntryEditor: use z-[9999] instead of portal (fixes iOS stacking
  without breaking test DOM queries)
- Pros/cons inputs: remove colored backgrounds in dark mode
This commit is contained in:
Maurice
2026-04-16 23:46:07 +02:00
parent 0f44d7d264
commit 0e5c819f7c
2 changed files with 41 additions and 20 deletions
@@ -112,8 +112,9 @@ describe('ModalRenderer', () => {
});
it('FE-SN-MODAL-005: CTA nav button dismisses all notices (not just current)', async () => {
const noticeA = makeNotice({ id: 'n-a', titleKey: 'Notice A', cta: { kind: 'nav', labelKey: 'Go to trips', href: '/trips' } });
const noticeB = makeNotice({ id: 'n-b', titleKey: 'Notice B' });
// CTA is only shown on the last page; navigate there first
const noticeA = makeNotice({ id: 'n-a', titleKey: 'Notice A' });
const noticeB = makeNotice({ id: 'n-b', titleKey: 'Notice B', cta: { kind: 'nav', labelKey: 'Go to trips', href: '/trips' } });
useSystemNoticeStore.setState({ notices: [noticeA, noticeB], loaded: true });
const dismissSpy = vi.spyOn(useSystemNoticeStore.getState(), 'dismiss');
@@ -121,6 +122,12 @@ describe('ModalRenderer', () => {
await flushGraceDelay();
// Navigate to last page
await act(async () => {
fireEvent.click(screen.getByLabelText('Go to notice 2'));
});
await flushGraceDelay();
const ctaBtn = screen.getByRole('button', { name: 'Go to trips' });
await act(async () => {
fireEvent.click(ctaBtn);
@@ -299,17 +306,22 @@ describe('ModalRenderer', () => {
expect(screen.getByText('Notice A')).toBeTruthy();
expect(screen.getByText('1 / 3')).toBeTruthy();
// Dismiss notice A — store shrinks, parent re-renders with [B, C]
// Navigate to last page where X button is available
await act(async () => {
fireEvent.click(screen.getByLabelText('Dismiss'));
useSystemNoticeStore.setState({ notices: [noticeB, noticeC], loaded: true });
rerender(<ModalRenderer notices={[noticeB, noticeC]} />);
fireEvent.click(screen.getByLabelText('Go to notice 3'));
});
await flushGraceDelay();
// Must show B (idx=0), not C (idx=1 — the old buggy behavior)
expect(screen.getByText('Notice B')).toBeTruthy();
expect(screen.getByText('1 / 2')).toBeTruthy();
// Dismiss all from last page — store shrinks
await act(async () => {
fireEvent.click(screen.getByLabelText('Dismiss'));
useSystemNoticeStore.setState({ notices: [], loaded: true });
rerender(<ModalRenderer notices={[]} />);
});
await flushGraceDelay();
// All dismissed — modal should be gone
expect(screen.queryByRole('dialog')).toBeNull();
});
it('FE-SN-MODAL-017: X button dismisses all notices, not just the current one', async () => {
@@ -321,6 +333,12 @@ describe('ModalRenderer', () => {
render(<ModalRenderer notices={[noticeA, noticeB]} />);
await flushGraceDelay();
// X button only appears on the last page — navigate there
await act(async () => {
fireEvent.click(screen.getByLabelText('Go to notice 2'));
});
await flushGraceDelay();
await act(async () => {
fireEvent.click(screen.getByLabelText('Dismiss'));
});
@@ -330,7 +348,7 @@ describe('ModalRenderer', () => {
expect(dismissSpy).toHaveBeenCalledTimes(2);
});
it('FE-SN-MODAL-018: ESC key dismisses all notices when current is dismissible', async () => {
it('FE-SN-MODAL-018: ESC key dismisses all notices when on last page', async () => {
const noticeA = makeNotice({ id: 'n-a', titleKey: 'Notice A' });
const noticeB = makeNotice({ id: 'n-b', titleKey: 'Notice B' });
useSystemNoticeStore.setState({ notices: [noticeA, noticeB], loaded: true });
@@ -339,6 +357,12 @@ describe('ModalRenderer', () => {
render(<ModalRenderer notices={[noticeA, noticeB]} />);
await flushGraceDelay();
// ESC only works on last page — navigate there first
await act(async () => {
fireEvent.click(screen.getByLabelText('Go to notice 2'));
});
await flushGraceDelay();
await act(async () => {
fireEvent.keyDown(document, { key: 'Escape' });
});
+7 -10
View File
@@ -1,5 +1,4 @@
import { useEffect, useState, useRef, useCallback, useMemo } from 'react'
import { createPortal } from 'react-dom'
import { useParams, useNavigate } from 'react-router-dom'
import { useJourneyStore } from '../store/journeyStore'
import { useAuthStore } from '../store/authStore'
@@ -228,16 +227,15 @@ export default function JourneyDetailPage() {
/>
)}
{/* Fullscreen entry view (mobile) — portal to body for iOS stacking */}
{viewingEntry && createPortal(
{/* Fullscreen entry view (mobile) */}
{viewingEntry && (
<MobileEntryView
entry={viewingEntry}
onClose={() => setViewingEntry(null)}
onEdit={() => { setViewingEntry(null); setEditingEntry(viewingEntry); }}
onDelete={() => { setViewingEntry(null); setDeleteTarget(viewingEntry); }}
onPhotoClick={(photos, idx) => setLightbox({ photos: photos.map(p => ({ id: p.id, src: photoUrl(p, 'original'), caption: p.caption, provider: p.provider, asset_id: p.asset_id, owner_id: p.owner_id })), index: idx })}
/>,
document.body
/>
)}
{/* Floating tab toggle on mobile combined view */}
@@ -580,8 +578,8 @@ export default function JourneyDetailPage() {
</div>
</div>
{/* Entry Editor — portal to body to escape stacking context on iOS */}
{editingEntry && createPortal(
{/* Entry Editor */}
{editingEntry && (
<EntryEditor
entry={editingEntry}
journeyId={current.id}
@@ -605,8 +603,7 @@ export default function JourneyDetailPage() {
setEditingEntry(null)
loadJourney(Number(id))
}}
/>,
document.body
/>
)}
{/* Journey Settings */}
@@ -2093,7 +2090,7 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
}
return (
<div className="fixed inset-0 z-[200] flex items-end sm:items-center sm:justify-center sm:p-5" style={{ background: 'rgba(9,9,11,0.75)' }}>
<div className="fixed inset-0 z-[9999] flex items-end sm:items-center sm:justify-center sm:p-5" style={{ background: 'rgba(9,9,11,0.75)' }}>
<div className="bg-white dark:bg-zinc-900 sm:rounded-2xl shadow-[0_20px_40px_rgba(0,0,0,0.2)] sm:max-w-[640px] w-full flex flex-col overflow-hidden h-full sm:h-auto sm:max-h-[90vh]">