diff --git a/frontend/src/lib/help.test.ts b/frontend/src/lib/help.test.ts index a4b246b..79ee9b0 100644 --- a/frontend/src/lib/help.test.ts +++ b/frontend/src/lib/help.test.ts @@ -4,6 +4,18 @@ import { createHelpFixture } from '@/test/helpFixtures' describe ('Help file catalogue', () => { + it.each ([['Éclair', 'eclair'], ['home', 'HOME'], ['e\u0301clair', 'Éclair']]) ( + 'compares title %s and %s equally at primary strength', (a, b) => { + const { api } = createHelpFixture () + expect (api.compareHelpTitles (a, b)).toBe (0) + }) + + it ('orders distinct Latin titles in both directions', () => { + const { api } = createHelpFixture () + expect (api.compareHelpTitles ('Apple', 'Zebra')).toBeLessThan (0) + expect (api.compareHelpTitles ('Zebra', 'Apple')).toBeGreaterThan (0) + }) + it.each ([['ほーむ', 'ホーム'], ['ホーム', 'ほーむ'], ['ホーム', 'ホーム'], ['ABC', 'abc'], ['ガイド', 'がいど']]) ( 'normalises search text %s and %s identically', (query, text) => { diff --git a/frontend/src/lib/help.ts b/frontend/src/lib/help.ts index de61501..36b6207 100644 --- a/frontend/src/lib/help.ts +++ b/frontend/src/lib/help.ts @@ -33,6 +33,14 @@ export type HelpDiff = { export type HelpEditionIdentifier = { date: string; revision: number | null } +// Fix the locale and primary strength; ICU versions still differ from MySQL UCA 9.0. +const helpTitleCollator = new Intl.Collator ('en', { + usage: 'sort', sensitivity: 'base', ignorePunctuation: false, numeric: false }) + + +export const compareHelpTitles = (a: string, b: string): number => + helpTitleCollator.compare (a, b) + export const normaliseHelpSearchText = (value: string): string => value.normalize ('NFKC').toLowerCase ().replace (/[ァ-ヶヽヾ]/g, diff --git a/frontend/src/pages/help/HelpListPage.test.tsx b/frontend/src/pages/help/HelpListPage.test.tsx index 385ad79..218ccd1 100644 --- a/frontend/src/pages/help/HelpListPage.test.tsx +++ b/frontend/src/pages/help/HelpListPage.test.tsx @@ -90,6 +90,20 @@ describe ('Help search', () => { }) // ai_ci equality is documented by MySQL; these are not captured DB sort results. + it.each (['asc', 'desc']) ('orders different titles before identifiers in order %s', + async direction => { + helpFixture.assets.search.default.ja = [ + { identifier: 'alpha', edition: '2026-10-02', title: 'Zebra', body: '' }, + { identifier: 'beta', edition: '2026-10-02', title: 'Apple', body: '' }] + const { container } = renderWithProviders (, { + route: `/help?order=title:${ direction }` }) + await screen.findByText ('2 件') + const expected = direction === 'asc' + ? ['/help/beta', '/help/alpha'] : ['/help/alpha', '/help/beta'] + expect (Array.from (container.querySelectorAll ('tbody a'), link => + link.getAttribute ('href'))).toEqual (expected) + }) + it.each (['asc', 'desc']) ('uses identifier ties for MySQL ai_ci title order %s', async direction => { helpFixture.assets.search.default.ja = [ diff --git a/frontend/src/pages/help/HelpListPage.tsx b/frontend/src/pages/help/HelpListPage.tsx index 0b4976d..bf0341c 100644 --- a/frontend/src/pages/help/HelpListPage.tsx +++ b/frontend/src/pages/help/HelpListPage.tsx @@ -11,6 +11,7 @@ import Pagination from '@/components/common/Pagination' import MainArea from '@/components/layout/MainArea' import { SITE_TITLE } from '@/config' import { compareHelpEditions, + compareHelpTitles, getHelpSearchEntries, helpEditionDate, normaliseHelpSearchText } from '@/lib/help' @@ -70,7 +71,7 @@ const HelpSearchPage: FC = () => { || normaliseHelpSearchText (entry.body).includes (textQuery)))) .sort ((a, b) => { const comparison = field === 'title' - ? a.title.localeCompare (b.title, 'ja') + ? compareHelpTitles (a.title, b.title) : compareHelpEditions (a.edition, b.edition) return (direction === 'asc' ? comparison : -comparison) diff --git a/frontend/src/pages/wiki/WikiDetailPage.test.tsx b/frontend/src/pages/wiki/WikiDetailPage.test.tsx index fac93da..84268c9 100644 --- a/frontend/src/pages/wiki/WikiDetailPage.test.tsx +++ b/frontend/src/pages/wiki/WikiDetailPage.test.tsx @@ -1,9 +1,10 @@ -import { act, fireEvent, screen, waitFor } from '@testing-library/react' -import { Route, Routes } from 'react-router-dom' +import { act, fireEvent, screen, waitFor, within } from '@testing-library/react' +import { Route, Routes, useLocation } from 'react-router-dom' import { beforeEach, describe, expect, it, vi } from 'vitest' import { apiPost } from '@/lib/api' import { getHelpTitleIndex } from '@/lib/help' +import { tagsKeys, wikiKeys } from '@/lib/queryKeys' import WikiDetailPage from '@/pages/wiki/WikiDetailPage' import WikiNewPage from '@/pages/wiki/WikiNewPage' import { buildTag, buildUser, buildWikiPage } from '@/test/factories' @@ -27,7 +28,8 @@ vi.mock ('@/lib/wiki', () => wikiApi) vi.mock ('@/lib/tags', () => tagsApi) vi.mock ('@/lib/posts', () => postsApi) vi.mock ('@/lib/help', () => ({ getHelpTitleIndex: vi.fn () })) -vi.mock ('@/lib/api', () => ({ apiPost: vi.fn () })) +vi.mock ('@/lib/api', async importOriginal => ({ + ...await importOriginal (), apiPost: vi.fn () })) vi.mock ('@/components/ui/use-toast', () => ({ toast: vi.fn () })) vi.mock ('react-markdown-editor-lite', () => ({ default: ({ value, onChange }: { @@ -36,6 +38,13 @@ vi.mock ('react-markdown-editor-lite', () => ({