From 2693a76192de147b434d0652088a1e10d7977418 Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 20 Feb 2025 11:48:00 +0000 Subject: [PATCH] fix(sanity): prevent unexpected release type picker mutation (#8701) --- .../releases/__fixtures__/release.fixture.ts | 2 +- .../tool/detail/ReleaseTypePicker.tsx | 28 ++++++-- .../__tests__/ReleaseTypePicker.test.tsx | 67 ++++++++++++++++++- 3 files changed, 88 insertions(+), 9 deletions(-) diff --git a/packages/sanity/src/core/releases/__fixtures__/release.fixture.ts b/packages/sanity/src/core/releases/__fixtures__/release.fixture.ts index a207925f7a4..658307bffc6 100644 --- a/packages/sanity/src/core/releases/__fixtures__/release.fixture.ts +++ b/packages/sanity/src/core/releases/__fixtures__/release.fixture.ts @@ -10,7 +10,7 @@ export const activeScheduledRelease: ReleaseDocument = { metadata: { title: 'active Release', releaseType: 'scheduled', - intendedPublishAt: '2023-10-10T10:00:00Z', + intendedPublishAt: '2023-10-10T10:00:00.000Z', description: 'active Release description', }, } diff --git a/packages/sanity/src/core/releases/tool/detail/ReleaseTypePicker.tsx b/packages/sanity/src/core/releases/tool/detail/ReleaseTypePicker.tsx index 323a7aa5da9..e89467136f4 100644 --- a/packages/sanity/src/core/releases/tool/detail/ReleaseTypePicker.tsx +++ b/packages/sanity/src/core/releases/tool/detail/ReleaseTypePicker.tsx @@ -71,7 +71,15 @@ export function ReleaseTypePicker(props: {release: NotArchivedRelease}): React.J if (open && !dialog) { const newRelease = { ...release, - metadata: {...release.metadata, intendedPublishAt: updatedDate, releaseType}, + metadata: { + ...release.metadata, + releaseType, + ...(typeof updatedDate === 'undefined' + ? {} + : { + intendedPublishAt: updatedDate, + }), + }, } if (!isEqual(newRelease, release)) { @@ -127,12 +135,18 @@ export function ReleaseTypePicker(props: {release: NotArchivedRelease}): React.J return }, [getReleaseTime, isPublishDateInPast, publishDate, release, tRelease]) - const handleButtonReleaseTypeChange = useCallback((pickedReleaseType: ReleaseType) => { - setReleaseType(pickedReleaseType) - const nextPublishAt = pickedReleaseType === 'scheduled' ? startOfMinute(new Date()) : undefined - setIntendedPublishAt(nextPublishAt) - setIsIntendedScheduleDateInPast(true) - }, []) + const handleButtonReleaseTypeChange = useCallback( + (pickedReleaseType: ReleaseType) => { + setReleaseType(pickedReleaseType) + const nextPublishAt = + pickedReleaseType === 'scheduled' + ? (publishDate ?? startOfMinute(new Date())) + : (publishDate ?? undefined) + setIntendedPublishAt(nextPublishAt) + setIsIntendedScheduleDateInPast(true) + }, + [publishDate], + ) const handlePublishAtCalendarChange = useCallback( (date: Date | null) => { diff --git a/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseTypePicker.test.tsx b/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseTypePicker.test.tsx index a3a6fa329f3..4200f9f4796 100644 --- a/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseTypePicker.test.tsx +++ b/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseTypePicker.test.tsx @@ -183,13 +183,78 @@ describe('ReleaseTypePicker', () => { ...activeASAPRelease, metadata: expect.objectContaining({ ...activeASAPRelease.metadata, - intendedPublishAt: undefined, releaseType: 'undecided', }), }) }) }) + describe('noops if the release type is unchanged when the picker is closed', () => { + it('after returning to "ASAP" from "undecided"', async () => { + await renderComponent() + + const pickerButton = screen.getByRole('button') + fireEvent.click(pickerButton) + + const undecidedTab = within(screen.getByRole('tablist')).getByText('Undecided') + fireEvent.click(undecidedTab) + + const asapTab = within(screen.getByRole('tablist')).getByText('ASAP') + fireEvent.click(asapTab) + + fireEvent.click(screen.getByTestId('release-type-picker')) + fireEvent.click(pickerButton) + + expect(mockUpdateRelease).not.toHaveBeenCalled() + }) + + it('after returning to "ASAP" from "at time"', async () => { + await renderComponent() + + const pickerButton = screen.getByRole('button') + fireEvent.click(pickerButton) + + const atTimeTab = within(screen.getByRole('tablist')).getByText('At time') + fireEvent.click(atTimeTab) + + const asapTab = within(screen.getByRole('tablist')).getByText('ASAP') + fireEvent.click(asapTab) + + fireEvent.click(screen.getByTestId('release-type-picker')) + fireEvent.click(pickerButton) + + expect(mockUpdateRelease).not.toHaveBeenCalled() + }) + + it('after returning to "at time" from "ASAP" after the system time has incremented', async () => { + vi.useFakeTimers({shouldAdvanceTime: true}) + + const intendedPublishAt = new Date(activeScheduledRelease.metadata.intendedPublishAt ?? 0) + + // 24 hours before `intendedPublishAt`. + vi.setSystemTime(new Date(intendedPublishAt.getTime() - 3_600 * 1_000 * 24)) + + await renderComponent(activeScheduledRelease) + + const pickerButton = screen.getByRole('button') + fireEvent.click(pickerButton) + + const asapTab = within(screen.getByRole('tablist')).getByText('ASAP') + fireEvent.click(asapTab) + + // 23 hours before `intendedPublishAt` (one hour after picker opened). + vi.setSystemTime(new Date(intendedPublishAt.getTime() - 3_600 * 1_000 * 23)) + + const atTimeTab = within(screen.getByRole('tablist')).getByText('At time') + fireEvent.click(atTimeTab) + + fireEvent.click(pickerButton) + + expect(mockUpdateRelease).not.toHaveBeenCalled() + vi.useRealTimers() + }) + }) + describe('picker behavior based on release state', () => { it('does not show button for picker when release is published state', async () => { await renderComponent(publishedASAPRelease)