|
1 | 1 | /** @vitest-environment jsdom */ |
2 | 2 |
|
3 | | -import { act, useState } from 'react' |
| 3 | +import { act, useEffect, useState } from 'react' |
4 | 4 | import { nextNavigationMockFns } from '@sim/testing/mocks/next-navigation.mock' |
5 | 5 | import { createRoot, type Root } from 'react-dom/client' |
6 | 6 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' |
7 | 7 | import { SettingsIntentLink } from '@/components/settings/settings-intent-link' |
8 | 8 | import { SettingsNavigationGuard } from '@/components/settings/settings-navigation-guard' |
9 | 9 | import { useSettingsBrowserNavigation } from '@/components/settings/use-settings-browser-navigation' |
10 | 10 | import { useSettingsUnsavedGuard } from '@/components/settings/use-settings-unsaved-guard' |
| 11 | +import { |
| 12 | + FileNavigationProvider, |
| 13 | + useFileNavigation, |
| 14 | +} from '@/app/workspace/[workspaceId]/files/components/file-detail/navigation' |
11 | 15 | import { useSettingsDirtyStore } from '@/stores/settings/dirty/store' |
12 | 16 |
|
13 | 17 | vi.mock( |
@@ -587,3 +591,63 @@ describe('native settings navigation', () => { |
587 | 591 | expect(unload.defaultPrevented).toBe(false) |
588 | 592 | }) |
589 | 593 | }) |
| 594 | + |
| 595 | +function FileDraft() { |
| 596 | + const [draft, setDraft] = useState('') |
| 597 | + const { setIsDirty, discardRef } = useFileNavigation({ |
| 598 | + owner: { entityType: 'project', entityId: 'project' }, |
| 599 | + }) |
| 600 | + useEffect(() => { |
| 601 | + discardRef.current = () => setDraft('') |
| 602 | + return () => { |
| 603 | + discardRef.current = null |
| 604 | + } |
| 605 | + }, [discardRef]) |
| 606 | + return ( |
| 607 | + <input |
| 608 | + value={draft} |
| 609 | + onChange={(event) => { |
| 610 | + setDraft(event.target.value) |
| 611 | + setIsDirty(Boolean(event.target.value)) |
| 612 | + }} |
| 613 | + /> |
| 614 | + ) |
| 615 | +} |
| 616 | + |
| 617 | +it.each([-1, 1])( |
| 618 | + 'keeps a file draft through native history cancellation and discards only on confirmed traversal %s', |
| 619 | + async (delta) => { |
| 620 | + window.history.pushState({ router: 'future' }, '', '/future') |
| 621 | + nativeGo.call(window.history, -1) |
| 622 | + await settle() |
| 623 | + act(() => |
| 624 | + root.render( |
| 625 | + <FileNavigationProvider |
| 626 | + owner={{ entityType: 'project', entityId: 'project' }} |
| 627 | + fileId='file' |
| 628 | + > |
| 629 | + <FileDraft /> |
| 630 | + </FileNavigationProvider> |
| 631 | + ) |
| 632 | + ) |
| 633 | + const field = container.querySelector('input') |
| 634 | + const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set |
| 635 | + if (!field || !setter) throw new Error('Missing file draft') |
| 636 | + act(() => { |
| 637 | + setter.call(field, 'unsaved file draft') |
| 638 | + field.dispatchEvent(new Event('input', { bubbles: true })) |
| 639 | + }) |
| 640 | + nativeGo.call(window.history, delta) |
| 641 | + await settle() |
| 642 | + expect(window.location.pathname).toBe('/editor') |
| 643 | + expect(field.value).toBe('unsaved file draft') |
| 644 | + act(() => useSettingsDirtyStore.getState().cancelLeave()) |
| 645 | + expect(field.value).toBe('unsaved file draft') |
| 646 | + nativeGo.call(window.history, delta) |
| 647 | + await settle() |
| 648 | + act(() => useSettingsDirtyStore.getState().confirmLeave()) |
| 649 | + await settle() |
| 650 | + expect(window.location.pathname).toBe(delta === -1 ? '/prior' : '/future') |
| 651 | + expect(field.value).toBe('') |
| 652 | + } |
| 653 | +) |
0 commit comments