diff --git a/apps/site/components/Downloads/Release/VersionDropdown.tsx b/apps/site/components/Downloads/Release/VersionDropdown.tsx index c966d0bee6be0..69039bf7cbc44 100644 --- a/apps/site/components/Downloads/Release/VersionDropdown.tsx +++ b/apps/site/components/Downloads/Release/VersionDropdown.tsx @@ -2,7 +2,7 @@ import WithNoScriptSelect from '@node-core/ui-components/Common/Select/NoScriptSelect'; import { useLocale, useTranslations } from 'next-intl'; -import { use } from 'react'; +import { use, useEffect } from 'react'; import { redirect, usePathname } from '#site/navigation.mjs'; import { STATUS_KIND_MAP } from '#site/next.constants.mjs'; @@ -11,6 +11,7 @@ import { ReleasesContext, } from '#site/providers/releaseProvider'; +import type { NodeReleaseStatus } from '#site/types'; import type { FC } from 'react'; const VersionDropdown: FC = () => { @@ -22,27 +23,41 @@ const VersionDropdown: FC = () => { // Allows us to keep the route semantically correct to what the user should expect // from the /current and non /current routes. - const setVersionOrNavigate = (version: string) => { - const release = releases.find( - ({ versionWithPrefix }) => versionWithPrefix === version - ); + const getRedirectPath = (status: NodeReleaseStatus) => { + if (['LTS', 'EOL'].includes(status) && !pathname.endsWith('/download')) { + return '/download'; + } - if ( - (release?.status === 'LTS' || release?.status === 'EOL') && - (pathname.includes('current') || pathname.includes('alpha')) - ) { - redirect({ href: '/download', locale }); - return; + if (status === 'Current' && !pathname.includes('current')) { + return '/download/current'; } - if (release?.status === 'Current' && !pathname.includes('current')) { - redirect({ href: '/download/current', locale }); - return; + if (status === 'Alpha' && !pathname.includes('alpha')) { + return '/download/alpha'; } + }; + + // Ensure the initial route is correct based on the release status. + useEffect(() => { + const redirectPath = getRedirectPath(release.status); + if (redirectPath) { + redirect({ href: redirectPath, locale }); + } + // eslint-disable-next-line @eslint-react/exhaustive-deps -- deps are correct on first render, and this should not run again. + }, []); + + // Handle setting the new version or navigating to the correct route based on status. + const setVersionOrNavigate = (version: string) => { + const selectedRelease = releases.find( + ({ versionWithPrefix }) => versionWithPrefix === version + ); - if (release?.status === 'Alpha' && !pathname.includes('alpha')) { - redirect({ href: '/download/alpha', locale }); - return; + if (selectedRelease) { + const href = getRedirectPath(selectedRelease.status); + if (href) { + redirect({ href, locale }); + return; + } } setVersion(version); diff --git a/apps/site/components/Downloads/Release/__tests__/VersionDropdown.test.jsx b/apps/site/components/Downloads/Release/__tests__/VersionDropdown.test.jsx index 94c1b10f36554..bc01bf8a7198e 100644 --- a/apps/site/components/Downloads/Release/__tests__/VersionDropdown.test.jsx +++ b/apps/site/components/Downloads/Release/__tests__/VersionDropdown.test.jsx @@ -1,7 +1,8 @@ import assert from 'node:assert/strict'; import { beforeEach, describe, it, mock } from 'node:test'; -import { render } from '@testing-library/react'; +import { act, render } from '@testing-library/react'; +import { useState } from 'react'; import { ReleaseContext, @@ -58,11 +59,27 @@ const releases = statuses.flatMap(({ status, versions }) => ); const renderDropdown = (release = releases[0]) => { - render( - - + const Dropdown = () => { + const [version, updateVersion] = useState(release.versionWithPrefix); + setVersion.mock.mockImplementation(updateVersion); + + return ( + candidate.versionWithPrefix === version + ), + setVersion, + }} + > + ); + }; + + render( + + ); @@ -90,6 +107,7 @@ describe('VersionDropdown', () => { assert.equal(defaultValue, release.versionWithPrefix); assert.equal(ariaLabel, 'layouts.download.dropdown.version'); + assert.equal(redirect.mock.callCount(), 0); }); it(`provides the ${status} badge for each version`, () => { @@ -109,16 +127,34 @@ describe('VersionDropdown', () => { }); for (const target of statuses) { + it(`normalizes ${target.path} on load for ${status}`, () => { + pathname = target.path; + + renderDropdown(release); + + if (target.path !== path) { + assert.deepEqual( + redirect.mock.calls.map(call => call.arguments), + [[{ href: path, locale: 'en' }]] + ); + } else { + assert.equal(redirect.mock.callCount(), 0); + } + assert.equal(setVersion.mock.callCount(), 0); + }); + const description = target.status === status ? `another ${status}` : target.status; it(`switches to ${description}`, () => { const { onChange } = renderDropdown(release); + redirect.mock.resetCalls(); + setVersion.mock.resetCalls(); const version = target.versions.find( candidate => candidate !== release.versionWithPrefix ); - onChange(version); + act(() => onChange(version)); if (target.path !== path) { assert.equal(redirect.mock.callCount(), 1); @@ -126,10 +162,19 @@ describe('VersionDropdown', () => { { href: target.path, locale: 'en' }, ]); assert.equal(setVersion.mock.callCount(), 0); + assert.equal(Select.mock.callCount(), 1); + assert.equal( + Select.mock.calls.at(-1).arguments[0].defaultValue, + release.versionWithPrefix + ); } else { assert.equal(redirect.mock.callCount(), 0); assert.equal(setVersion.mock.callCount(), 1); assert.deepEqual(setVersion.mock.calls[0].arguments, [version]); + assert.equal( + Select.mock.calls.at(-1).arguments[0].defaultValue, + version + ); } }); }