Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 44 additions & 10 deletions packages/typo3-docs-theme/assets/js/versions.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,14 @@ document.addEventListener('DOMContentLoaded', () => {
versionSelect.innerHTML = '';
const seen = new Set();

const currentVersion = versionSelect.getAttribute('data-current-version');

const sortedData = versionData.sort((a, b) => {
const priority = (v) => {
if (v === 'main') return Infinity;
const num = parseFloat(v);
return isNaN(num) ? -1 : num;
};
return priority(b.version) - priority(a.version);
});
// The current page URL is the authoritative source of the active version
// (e.g. ".../0.10/en-us/..."). Prefer it over the rendered
// data-current-version attribute, which can be wrong for versions like
// "0.10" (server-side numeric coercion turns it into "0.1").
const currentVersion = getVersionFromUrl(currentURL)
|| versionSelect.getAttribute('data-current-version');

const sortedData = [...versionData].sort((a, b) => compareVersionsDescending(a.version, b.version));

sortedData.forEach(item => {
if (!seen.has(item.version)) {
Expand Down Expand Up @@ -131,6 +129,42 @@ document.addEventListener('DOMContentLoaded', () => {
return match ? match[1].toLowerCase() : '';
}

// The version is the path segment right before the language code,
// e.g. "/p/vendor/pkg/0.10/en-us/Index.html" -> "0.10".
function getVersionFromUrl(url) {
const match = url.match(/\/([^/]+)\/[a-z]{2}-[a-z]{2}(?:\/|$)/i);
return match ? match[1] : '';
}

// Compares two version strings for descending order ("main" first, then
// highest version). Each dotted component is compared numerically so that
// "0.10" correctly sorts above "0.9" (parseFloat would treat both as 0.1/0.9).
// A missing or non-numeric component counts as 0, and equal numeric versions
// fall back to a single string tie-break, so the comparator is a consistent
// total order for any input.
function compareVersionsDescending(a, b) {
if (a === 'main' || b === 'main') {
return a === b ? 0 : (a === 'main' ? -1 : 1);
}

const partsA = a.split('.');
const partsB = b.split('.');
const length = Math.max(partsA.length, partsB.length);

for (let i = 0; i < length; i++) {
const numA = parseInt(partsA[i], 10);
const numB = parseInt(partsB[i], 10);
const valueA = Number.isNaN(numA) ? 0 : numA;
const valueB = Number.isNaN(numB) ? 0 : numB;

if (valueA !== valueB) {
return valueB - valueA;
}
}

return b.localeCompare(a);
}

function toAbsoluteUrl(url) {
try {
const link = document.createElement('a');
Expand Down
2 changes: 1 addition & 1 deletion packages/typo3-docs-theme/resources/public/js/theme.min.js

Large diffs are not rendered by default.

98 changes: 98 additions & 0 deletions packages/typo3-docs-theme/tests/js/versions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* Regression test for the version switcher (versions.js).
*
* Two bugs on packages that have both "0.1" and "0.10" (e.g. netresearch/nr-vault):
* 1. Sorting used parseFloat(), so parseFloat("0.10") === 0.1 and "0.10"
* sorted together with the 0.x group at the bottom instead of first.
* 2. Pre-selection used the rendered data-current-version attribute, which is
* "0.1" on the 0.10 page (server-side numeric coercion of "0.10"), so the
* dropdown pre-selected the wrong entry.
*
* The fix sorts each dotted component numerically and derives the active
* version from the page URL.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';

// Importing the script registers its DOMContentLoaded handler once.
import '../../assets/js/versions.js';

function versionList(versions) {
return versions.map(version => ({
version,
language: 'en-us',
url: `/p/netresearch/pkg/${version}/en-us/Index.html`,
}));
}

// netresearch/nr-vault: has both 0.1 and 0.10.
const NR_VAULT_VERSIONS = versionList(['0.1', '0.10', '0.2', '0.9', 'main']);

// netresearch/nr-llm: multi-digit minors 0.10, 0.11, 0.12.
const NR_LLM_VERSIONS = versionList(
['0.9', '0.8', '0.7', '0.6', '0.5', '0.4', '0.3', '0.2', '0.12', '0.11', '0.1', '0.10', 'main']);

function setup(currentUrl, dataCurrentVersion, versions) {
document.body.replaceChildren();

const languageSelect = document.createElement('select');
languageSelect.id = 'languageSelect';

const versionSelect = document.createElement('select');
versionSelect.id = 'versionSelect';
versionSelect.setAttribute('data-current-version', dataCurrentVersion);
versionSelect.setAttribute('data-override-url-self', currentUrl);

document.body.append(languageSelect, versionSelect);

global.fetch = vi.fn(() =>
Promise.resolve({ ok: true, json: () => Promise.resolve(versions) }));

return versions.length;
}

async function runAndWait(expectedCount) {
document.dispatchEvent(new Event('DOMContentLoaded'));
await vi.waitFor(() => {
const options = document.querySelectorAll('#versionSelect option');
expect(options.length).toBe(expectedCount);
});
return [...document.querySelectorAll('#versionSelect option')];
}

describe('version switcher', () => {
it('sorts 0.10 above 0.9 and the 0.x group (not last)', async () => {
// On the 0.10 page the server renders data-current-version="0.1" (coercion).
const count = setup(
'https://docs.typo3.org/p/netresearch/nr-vault/0.10/en-us/Index.html', '0.1', NR_VAULT_VERSIONS);
const order = (await runAndWait(count)).map(option => option.textContent);

expect(order).toEqual(['main', '0.10', '0.9', '0.2', '0.1']);
});

it('pre-selects the version from the URL (0.10), not the coerced attribute (0.1)', async () => {
const count = setup(
'https://docs.typo3.org/p/netresearch/nr-vault/0.10/en-us/Index.html', '0.1', NR_VAULT_VERSIONS);
const selected = (await runAndWait(count)).find(option => option.selected);

expect(selected.textContent).toBe('0.10');
});

it('orders multi-digit minors correctly (nr-llm: 0.12, 0.11, 0.10, 0.9, …)', async () => {
const count = setup(
'https://docs.typo3.org/p/netresearch/nr-llm/0.12/en-us/Index.html', '0.12', NR_LLM_VERSIONS);
const options = await runAndWait(count);

expect(options.map(option => option.textContent)).toEqual(
['main', '0.12', '0.11', '0.10', '0.9', '0.8', '0.7', '0.6', '0.5', '0.4', '0.3', '0.2', '0.1']);
expect(options.find(option => option.selected).textContent).toBe('0.12');
});

it('orders versions with a differing number of components (13.4.21 > 13.4 > 13.3)', async () => {
const versions = versionList(['13.3', '13.4', '13.4.21', '14.0', 'main']);
const count = setup(
'https://docs.typo3.org/m/typo3/manual/13.4/en-us/Index.html', '13.4', versions);
const order = (await runAndWait(count)).map(option => option.textContent);

expect(order).toEqual(['main', '14.0', '13.4.21', '13.4', '13.3']);
});
});