diff --git a/packages/sanity/src/core/releases/__fixtures__/release.fixture.ts b/packages/sanity/src/core/releases/__fixtures__/release.fixture.ts index 335805e5c57..2ee4374dff4 100644 --- a/packages/sanity/src/core/releases/__fixtures__/release.fixture.ts +++ b/packages/sanity/src/core/releases/__fixtures__/release.fixture.ts @@ -4,14 +4,46 @@ export const activeScheduledRelease: ReleaseDocument = { _id: '_.releases.activeRelease', _type: 'system.release', createdBy: '', - _createdAt: '2023-10-01T08:00:00Z', - _updatedAt: '2023-10-01T09:00:00Z', + _createdAt: '2023-10-10T08:00:00Z', + _updatedAt: '2023-10-10T09:00:00Z', state: 'active', name: 'activeRelease', metadata: { title: 'active Release', releaseType: 'scheduled', - intendedPublishAt: '2023-10-01T10:00:00Z', + intendedPublishAt: '2023-10-10T10:00:00Z', + description: 'active Release description', + }, +} + +export const scheduledRelease: ReleaseDocument = { + _id: '_.releases.scheduledRelease', + _type: 'system.release', + createdBy: '', + _createdAt: '2023-10-10T08:00:00Z', + _updatedAt: '2023-10-10T09:00:00Z', + state: 'scheduled', + name: 'scheduledRelease', + publishAt: '2023-10-10T10:00:00Z', + metadata: { + title: 'scheduled Release', + releaseType: 'scheduled', + intendedPublishAt: '2023-10-10T10:00:00Z', + description: 'scheduled Release description', + }, +} + +export const activeASAPRelease: ReleaseDocument = { + _id: '_.releases.activeRelease', + _type: 'system.release', + createdBy: '', + _createdAt: '2023-10-01T08:00:00Z', + _updatedAt: '2023-10-01T09:00:00Z', + state: 'active', + name: 'activeRelease', + metadata: { + title: 'active asap Release', + releaseType: 'asap', description: 'active Release description', }, } diff --git a/packages/sanity/src/core/releases/hooks/__tests__/__mocks__/usePerspective.mock.ts b/packages/sanity/src/core/releases/hooks/__tests__/__mocks__/usePerspective.mock.ts new file mode 100644 index 00000000000..01953f2b538 --- /dev/null +++ b/packages/sanity/src/core/releases/hooks/__tests__/__mocks__/usePerspective.mock.ts @@ -0,0 +1,17 @@ +import {type Mocked, vi} from 'vitest' + +import {LATEST} from '../../../util/const' +import {type PerspectiveValue} from '../../usePerspective' + +export const usePerspectiveMock: Mocked = { + perspective: undefined, + excludedPerspectives: [], + setPerspective: vi.fn(), + currentGlobalBundle: LATEST, + setPerspectiveFromReleaseId: vi.fn(), + setPerspectiveFromReleaseDocumentId: vi.fn(), + toggleExcludedPerspective: vi.fn(), + isPerspectiveExcluded: vi.fn(), + currentGlobalBundleId: 'drafts', + bundlesPerspective: [], +} diff --git a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx index 2e26abd8cc5..90ce706b43f 100644 --- a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx +++ b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenu.tsx @@ -7,7 +7,7 @@ import {css, styled} from 'styled-components' import {MenuButton} from '../../../ui-components' import {useTranslation} from '../../i18n' import {CreateReleaseDialog} from '../components/dialog/CreateReleaseDialog' -import {usePerspective} from '../hooks' +import {usePerspective} from '../hooks/usePerspective' import {type ReleaseDocument, type ReleaseType} from '../store/types' import {useReleases} from '../store/useReleases' import { @@ -75,17 +75,8 @@ export function GlobalPerspectiveMenu(): JSX.Element { ) const range: LayerRange = useMemo(() => { - let firstIndex = -1 let lastIndex = 0 - // if (!releases.published.hidden) { - firstIndex = 0 - // } - - if (currentGlobalBundleId === 'published') { - lastIndex = 0 - } - const {asap, scheduled} = sortedReleaseTypeReleases const countAsapReleases = asap.length const countScheduledReleases = scheduled.length @@ -103,12 +94,6 @@ export function GlobalPerspectiveMenu(): JSX.Element { groupSubsetReleases.forEach(({_id}, groupReleaseIndex) => { const index = offset + groupReleaseIndex - if (firstIndex === -1) { - // if (!item.hidden) { - firstIndex = index - // } - } - if (_id === currentGlobalBundleId) { lastIndex = index } @@ -118,7 +103,6 @@ export function GlobalPerspectiveMenu(): JSX.Element { orderedReleaseTypes.forEach(adjustIndexForReleaseType) return { - firstIndex, lastIndex, offsets, } diff --git a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx index 51863539b3f..58f3b8d92be 100644 --- a/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx +++ b/packages/sanity/src/core/releases/navbar/GlobalPerspectiveMenuItem.tsx @@ -20,7 +20,6 @@ import { import {GlobalPerspectiveMenuItemIndicator} from './PerspectiveLayerIndicator' export interface LayerRange { - firstIndex: number lastIndex: number offsets: { asap: number @@ -67,12 +66,12 @@ const ExcludedLayerDot = () => ( type rangePosition = 'first' | 'within' | 'last' | undefined export function getRangePosition(range: LayerRange, index: number): rangePosition { - const {firstIndex, lastIndex} = range + const {lastIndex} = range - if (firstIndex === lastIndex) return undefined - if (index === firstIndex) return 'first' + if (lastIndex === 0) return undefined + if (index === 0) return 'first' if (index === lastIndex) return 'last' - if (index > firstIndex && index < lastIndex) return 'within' + if (index > 0 && index < lastIndex) return 'within' return undefined } @@ -191,12 +190,13 @@ export const GlobalPerspectiveMenuItem = forwardRef< mode="bleed" onClick={handleToggleReleaseVisibility} padding={2} + data-testid="release-toggle-visibility" /> )} {!isPublishedPerspective(release) && isReleaseScheduledOrScheduling(release) && ( - + diff --git a/packages/sanity/src/core/releases/navbar/ReleaseTypeMenuSection.tsx b/packages/sanity/src/core/releases/navbar/ReleaseTypeMenuSection.tsx index 4d632305e58..8ca16282292 100644 --- a/packages/sanity/src/core/releases/navbar/ReleaseTypeMenuSection.tsx +++ b/packages/sanity/src/core/releases/navbar/ReleaseTypeMenuSection.tsx @@ -42,13 +42,13 @@ export function ReleaseTypeMenuSection({ if (releases.length === 0) return null - const {firstIndex, lastIndex, offsets} = range + const {lastIndex, offsets} = range const releaseTypeOffset = offsets[releaseType] return ( <> = releaseTypeOffset} + $withinRange={releaseTypeOffset > 0 && lastIndex >= releaseTypeOffset} paddingRight={2} paddingTop={releaseType === 'asap' ? 1 : 4} paddingBottom={2} diff --git a/packages/sanity/src/core/releases/navbar/__tests__/ReleasesNav.test.tsx b/packages/sanity/src/core/releases/navbar/__tests__/ReleasesNav.test.tsx index b66c9df3301..d28e5597a19 100644 --- a/packages/sanity/src/core/releases/navbar/__tests__/ReleasesNav.test.tsx +++ b/packages/sanity/src/core/releases/navbar/__tests__/ReleasesNav.test.tsx @@ -1,13 +1,23 @@ -import {fireEvent, render, screen} from '@testing-library/react' -import {beforeEach, describe, expect, it, type Mock, vi} from 'vitest' +import {fireEvent, render, type RenderResult, screen, waitFor, within} from '@testing-library/react' +import {beforeEach, describe, expect, it, vi} from 'vitest' import {createTestProvider} from '../../../../../test/testUtils/TestProvider' -import {usePerspective} from '../../hooks/usePerspective' +import { + activeASAPRelease, + activeScheduledRelease, + scheduledRelease, +} from '../../__fixtures__/release.fixture' +import {usePerspectiveMock} from '../../hooks/__tests__/__mocks__/usePerspective.mock' +import {useReleasesMock} from '../../store/__tests__/__mocks/useReleases.mock' import {LATEST} from '../../util/const' import {ReleasesNav} from '../ReleasesNav' vi.mock('../../hooks/usePerspective', () => ({ - usePerspective: vi.fn(), + usePerspective: vi.fn(() => usePerspectiveMock), +})) + +vi.mock('../../store/useReleases', () => ({ + useReleases: vi.fn(() => useReleasesMock), })) vi.mock('sanity/router', async (importOriginal) => ({ @@ -16,33 +26,21 @@ vi.mock('sanity/router', async (importOriginal) => ({ useRouterState: vi.fn().mockReturnValue(undefined), })) -vi.mock('../../../store/releases/useBundles', () => ({ - useBundles: vi.fn().mockReturnValue({ - deletedBundles: {}, - loading: false, - data: [{_id: 'a-release', title: 'Test Bundle'}], - }), -})) - -const mockUsePerspective = usePerspective as Mock +let currentRenderedInstance: RenderResult | undefined const renderTest = async () => { const wrapper = await createTestProvider({ resources: [], }) - return render(, {wrapper}) -} + currentRenderedInstance = render(, {wrapper}) -const mockSetPerspective = vi.fn() + return currentRenderedInstance +} describe('ReleasesNav', () => { beforeEach(() => { - mockUsePerspective.mockReturnValue({ - currentGlobalBundle: LATEST, - setPerspective: mockSetPerspective, - }) + vi.clearAllMocks() }) - it('should have link to releases tool', async () => { await renderTest() @@ -65,48 +63,215 @@ describe('ReleasesNav', () => { }) it('should have clear button to unset perspective when a perspective is chosen', async () => { - mockUsePerspective.mockReturnValue({ - currentGlobalBundle: { - _id: '_.releases.a-release', - metadata: {title: 'Test Release'}, - }, - setPerspective: mockSetPerspective, - }) + usePerspectiveMock.currentGlobalBundle = activeScheduledRelease await renderTest() fireEvent.click(screen.getByTestId('clear-perspective-button')) - expect(mockSetPerspective).toHaveBeenCalledWith(LATEST._id) + expect(usePerspectiveMock.setPerspective).toHaveBeenCalledWith(LATEST._id) }) it('should list the title of the chosen perspective', async () => { - mockUsePerspective.mockReturnValue({ - currentGlobalBundle: { - _id: '_.releases.a-release', - metadata: { - title: 'Test Bundle', - }, - }, - setPerspective: mockSetPerspective, - }) + usePerspectiveMock.currentGlobalBundle = activeScheduledRelease await renderTest() - screen.getByText('Test Bundle') + screen.getByText('active Release') }) it('should show release avatar for chosen perspective', async () => { - mockUsePerspective.mockReturnValue({ - currentGlobalBundle: { - _id: '_.releases.a-release', - metadata: {title: 'Test Bundle', releaseType: 'asap'}, - }, - setPerspective: mockSetPerspective, - }) + usePerspectiveMock.currentGlobalBundle = activeASAPRelease await renderTest() screen.getByTestId('release-avatar-critical') }) + + describe('global perspective menu', () => { + const renderAndWaitForStableMenu = async () => { + await renderTest() + + fireEvent.click(screen.getByTestId('global-perspective-menu-button')) + + await waitFor(() => { + expect(screen.queryByTestId('spinner')).toBeNull() + }) + } + + beforeEach(async () => { + useReleasesMock.data = [ + activeScheduledRelease, + { + ...activeScheduledRelease, + _id: '_.releases.active-scheduled-2', + name: 'activeScheduled2', + metadata: {...activeScheduledRelease.metadata, title: 'active Scheduled 2'}, + }, + activeASAPRelease, + + {...scheduledRelease, publishAt: '2023-10-10T09:00:00Z'}, + ] + }) + + describe('when menu is ready', () => { + beforeEach(renderAndWaitForStableMenu) + + it('should show published perspective item', async () => { + within(screen.getByTestId('release-menu')).getByText('Published') + + fireEvent.click(screen.getByText('Published')) + + expect(usePerspectiveMock.setPerspective).toHaveBeenCalledWith('published') + }) + + it('should list all the releases', async () => { + const releaseMenu = within(screen.getByTestId('release-menu')) + + // section titles + releaseMenu.getByText('ASAP') + releaseMenu.getByText('At time') + expect(releaseMenu.queryByText('Undecided')).toBeNull() + + // releases + releaseMenu.getByText('active Release') + releaseMenu.getByText('active Scheduled 2') + releaseMenu.getByText('active asap Release') + releaseMenu.getByText('scheduled Release') + }) + + it('should show the intended release date for intended schedule releases', async () => { + const scheduledMenuItem = within(screen.getByTestId('release-menu')) + .getByText('active Scheduled 2') + .closest('button')! + + within(scheduledMenuItem).getByText(/\b\d{1,2}\/\d{1,2}\/\d{4}\b/) + within(scheduledMenuItem).getByTestId('release-avatar-primary') + }) + + it('should show the actual release date for a scheduled release', async () => { + const scheduledMenuItem = within(screen.getByTestId('release-menu')) + .getByText('scheduled Release') + .closest('button')! + + within(scheduledMenuItem).getByText(/\b\d{1,2}\/\d{1,2}\/\d{4}\b/) + within(scheduledMenuItem).getByTestId('release-lock-icon') + within(scheduledMenuItem).getByTestId('release-avatar-primary') + }) + + it('allows for new release to be created', async () => { + fireEvent.click(screen.getByText('New release')) + + expect(screen.getByRole('dialog')).toHaveAttribute('id', 'create-release-dialog') + }) + }) + + describe('release layering', () => { + beforeEach(() => { + // since usePerspective is mocked, and the layering exclude toggle is + // controlled by currentGlobalBundleId, we need to manually set it + // to the release that will be selected in below tests + usePerspectiveMock.currentGlobalBundleId = '_.releases.active-scheduled-2' + // add an undecided release to expand testing + useReleasesMock.data = [ + ...useReleasesMock.data, + { + ...activeASAPRelease, + _id: '_.releases.undecidedRelease', + metadata: { + ...activeASAPRelease.metadata, + title: 'undecided Release', + releaseType: 'undecided', + }, + }, + ] + }) + + describe('when a release is clicked', () => { + beforeEach(async () => { + await renderAndWaitForStableMenu() + + // select a release that has some other nested layer releases + fireEvent.click(screen.getByText('active Scheduled 2')) + }) + + it('should set a given perspective from the menu', async () => { + expect(usePerspectiveMock.setPerspectiveFromReleaseDocumentId).toHaveBeenCalledWith( + '_.releases.active-scheduled-2', + ) + expect(usePerspectiveMock.setPerspective).not.toHaveBeenCalled() + }) + + it('should allow for hiding of any deeper layered releases', async () => { + const deepLayerRelease = within(screen.getByTestId('release-menu')) + .getByText('active Release') + .closest('button')! + + // toggle to hide + fireEvent.click(within(deepLayerRelease).getByTestId('release-toggle-visibility')) + expect(usePerspectiveMock.toggleExcludedPerspective).toHaveBeenCalledWith('activeRelease') + + // toggle to include + fireEvent.click(within(deepLayerRelease).getByTestId('release-toggle-visibility')) + expect(usePerspectiveMock.toggleExcludedPerspective).toHaveBeenCalledWith('activeRelease') + }) + + it('should not allow for hiding of published perspective', async () => { + const publishedRelease = within(screen.getByTestId('release-menu')) + .getByText('Published') + .closest('button')! + + expect( + within(publishedRelease).queryByTestId('release-toggle-visibility'), + ).not.toBeInTheDocument() + }) + + it('should not allow hiding of the current perspective', async () => { + const currentRelease = within(screen.getByTestId('release-menu')) + .getByText('active Scheduled 2') + .closest('button')! + + expect( + within(currentRelease).queryByTestId('release-toggle-visibility'), + ).not.toBeInTheDocument() + }) + + it('should not allow hiding of un-nested releases', async () => { + const unNestedRelease = within(screen.getByTestId('release-menu')) + .getByText('undecided Release') + .closest('button')! + + expect( + within(unNestedRelease).queryByTestId('release-toggle-visibility'), + ).not.toBeInTheDocument() + }) + + it('should not allow hiding of locked in scheduled releases', async () => { + const scheduledReleaseMenuItem = within(screen.getByTestId('release-menu')) + .getByText('scheduled Release') + .closest('button')! + + expect( + within(scheduledReleaseMenuItem).queryByTestId('release-toggle-visibility'), + ).not.toBeInTheDocument() + }) + }) + + it('applies existing layering when opened', async () => { + usePerspectiveMock.isPerspectiveExcluded.mockImplementation((id) => { + return id === 'activeRelease' + }) + + await renderAndWaitForStableMenu() + + const activeReleaseMenuItem = within(screen.getByTestId('release-menu')) + .getByText('active Release') + .closest('button')! + + expect( + within(activeReleaseMenuItem).queryByTestId('release-avatar-primary'), + ).not.toBeInTheDocument() + }) + }) + }) }) diff --git a/packages/sanity/src/core/releases/store/__tests__/__mocks/useReleases.mock.ts b/packages/sanity/src/core/releases/store/__tests__/__mocks/useReleases.mock.ts new file mode 100644 index 00000000000..95fff0cc77b --- /dev/null +++ b/packages/sanity/src/core/releases/store/__tests__/__mocks/useReleases.mock.ts @@ -0,0 +1,12 @@ +import {type Mocked, vi} from 'vitest' + +import {type useReleases} from '../../useReleases' + +export const useReleasesMock: Mocked> = { + archivedReleases: [], + data: [], + dispatch: vi.fn(), + error: undefined, + loading: false, + releasesIds: [], +} diff --git a/packages/sanity/src/core/releases/tool/detail/ReleaseDetailsEditor.test.tsx b/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseDetailsEditor.test.tsx similarity index 84% rename from packages/sanity/src/core/releases/tool/detail/ReleaseDetailsEditor.test.tsx rename to packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseDetailsEditor.test.tsx index 241563ff9d9..46982123cbd 100644 --- a/packages/sanity/src/core/releases/tool/detail/ReleaseDetailsEditor.test.tsx +++ b/packages/sanity/src/core/releases/tool/detail/__tests__/ReleaseDetailsEditor.test.tsx @@ -1,12 +1,12 @@ import {fireEvent, render, screen, waitFor} from '@testing-library/react' import {beforeEach, describe, expect, it, vi} from 'vitest' -import {createTestProvider} from '../../../../../test/testUtils/TestProvider' -import {type ReleaseDocument} from '../../index' -import {useReleaseOperations} from '../../store/useReleaseOperations' -import {ReleaseDetailsEditor} from './ReleaseDetailsEditor' +import {createTestProvider} from '../../../../../../test/testUtils/TestProvider' +import {type ReleaseDocument} from '../../../index' +import {useReleaseOperations} from '../../../store/useReleaseOperations' +import {ReleaseDetailsEditor} from '../ReleaseDetailsEditor' // Mock the dependencies -vi.mock('../../store/useReleaseOperations', () => ({ +vi.mock('../../../store/useReleaseOperations', () => ({ useReleaseOperations: vi.fn().mockReturnValue({ updateRelease: vi.fn(), }), diff --git a/packages/sanity/src/core/releases/util/getBundleIdFromReleaseDocumentId.ts b/packages/sanity/src/core/releases/util/getBundleIdFromReleaseDocumentId.ts index eec23e57f9f..dafc831972c 100644 --- a/packages/sanity/src/core/releases/util/getBundleIdFromReleaseDocumentId.ts +++ b/packages/sanity/src/core/releases/util/getBundleIdFromReleaseDocumentId.ts @@ -8,7 +8,7 @@ const PATH_ID_PREFIX = `${RELEASE_DOCUMENTS_PATH}.` */ export function getBundleIdFromReleaseDocumentId(releaseId: string) { if (!releaseId.startsWith(PATH_ID_PREFIX)) { - throw new Error(`Release ID should start with ${RELEASE_DOCUMENTS_PATH}`) + throw new Error(`Release ID was ${releaseId} but should start with ${RELEASE_DOCUMENTS_PATH}`) } return releaseId.slice(PATH_ID_PREFIX.length) }