このコミットが含まれているのは:
@@ -0,0 +1,116 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
require 'tempfile'
|
||||||
|
|
||||||
|
RSpec.describe CertifiedTagNameTranslations do
|
||||||
|
let!(:language) { Language.create!(code: 'ain', name: 'ain') }
|
||||||
|
let!(:script) { Script.create!(code: 'Kana', name: 'Kana') }
|
||||||
|
let!(:tag) { create(:tag, primary_name: 'source') }
|
||||||
|
let(:entries) { [{ 'ja' => 'source', 'ain-Kana' => 'レ_ハイタ' }] }
|
||||||
|
let(:document) do
|
||||||
|
{ 'format' => described_class::FORMAT,
|
||||||
|
'source_locale' => 'ja', 'target_locale' => 'ain-Kana', 'entries' => entries }
|
||||||
|
end
|
||||||
|
let(:locale) do
|
||||||
|
Locale.new(code: 'ain-Kana', language_code: 'ain', script_code: 'Kana',
|
||||||
|
region_code: 392, variants: '', name: 'Ainu')
|
||||||
|
end
|
||||||
|
|
||||||
|
around do |example|
|
||||||
|
Tempfile.create(['certified-names', '.json']) do |file|
|
||||||
|
@path = file.path
|
||||||
|
example.run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_document
|
||||||
|
File.write(@path, JSON.generate(document))
|
||||||
|
locale.certified_tag_names_path = @path
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'registers certified names before automatic names and records their provenance' do
|
||||||
|
other = create(:tag, primary_name: 'レ_ハイタ')
|
||||||
|
write_document
|
||||||
|
|
||||||
|
locale.save!
|
||||||
|
|
||||||
|
expect(tag.reload.tag_name('ain')).to have_attributes(
|
||||||
|
name: 'レ_ハイタ', primary_flg: true, auto_generated: false)
|
||||||
|
expect(other.reload.tag_name('ain')).to have_attributes(
|
||||||
|
name: "Tag_##{ other.id }", auto_generated: true)
|
||||||
|
version = tag.tag_versions.order(:version_no).last
|
||||||
|
expect(version.created_by_user_id).to be_nil
|
||||||
|
expect(version.names.fetch('ain')).to include(
|
||||||
|
'name' => 'レ_ハイタ', 'auto_generated' => false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'skips missing sources without rolling back the locale or matching entries' do
|
||||||
|
entries.unshift({ 'ja' => 'missing', 'ain-Kana' => 'unused' })
|
||||||
|
write_document
|
||||||
|
|
||||||
|
locale.save!
|
||||||
|
|
||||||
|
expect(locale).to be_persisted
|
||||||
|
expect(tag.reload.name('ain')).to eq('レ_ハイタ')
|
||||||
|
expect(TagName.exists?(language_code: 'ain', name: 'unused')).to be(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'matches source names exactly rather than using a case-insensitive collation' do
|
||||||
|
entries.first['ja'] = 'SOURCE'
|
||||||
|
write_document
|
||||||
|
|
||||||
|
expect(described_class.load!(@path, locale:)).to eq({})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rejects ambiguous primary matches' do
|
||||||
|
write_document
|
||||||
|
relation = instance_double(ActiveRecord::Relation)
|
||||||
|
allow(TagName).to receive(:where).with(
|
||||||
|
language_code: 'ja', primary_flg: true, name: ['source']).and_return(relation)
|
||||||
|
allow(relation).to receive(:pluck).with(:name, :tag_id)
|
||||||
|
.and_return([['source', tag.id], ['source', tag.id + 1]])
|
||||||
|
|
||||||
|
expect { described_class.load!(@path, locale:) }
|
||||||
|
.to raise_error(described_class::Error, /source.*exactly one tag/)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rejects duplicate sources and identifies the offending name' do
|
||||||
|
entries.push({ 'ja' => 'source', 'ain-Kana' => 'another' })
|
||||||
|
write_document
|
||||||
|
|
||||||
|
expect { locale.save! }.to raise_error(described_class::Error, /duplicate source.*source/)
|
||||||
|
expect(Locale.exists?(code: 'ain-Kana')).to be(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rejects duplicate targets even when a source is missing' do
|
||||||
|
entries.push({ 'ja' => 'missing', 'ain-Kana' => 'レ_ハイタ' })
|
||||||
|
write_document
|
||||||
|
|
||||||
|
expect { locale.save! }.to raise_error(described_class::Error, /duplicate target.*レ_ハイタ/)
|
||||||
|
expect(Locale.exists?(code: 'ain-Kana')).to be(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rolls back names and histories if a later certified name is invalid' do
|
||||||
|
create(:tag, primary_name: 'second')
|
||||||
|
entries.push({ 'ja' => 'second', 'ain-Kana' => 'nico:invalid' })
|
||||||
|
write_document
|
||||||
|
original_versions = TagVersion.order(:id).map(&:attributes)
|
||||||
|
|
||||||
|
expect { locale.save! }.to raise_error(described_class::Error, /nico:invalid/)
|
||||||
|
|
||||||
|
expect(Locale.exists?(code: 'ain-Kana')).to be(false)
|
||||||
|
expect(TagName.where(language_code: 'ain')).to be_empty
|
||||||
|
expect(TagVersion.order(:id).map(&:attributes)).to eq(original_versions)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not add names or versions on repeated completion' do
|
||||||
|
write_document
|
||||||
|
locale.save!
|
||||||
|
original_names = TagName.order(:id).map(&:attributes)
|
||||||
|
original_versions = TagVersion.order(:id).map(&:attributes)
|
||||||
|
|
||||||
|
TagNameCompletion.for_locale!(locale)
|
||||||
|
|
||||||
|
expect(TagName.order(:id).map(&:attributes)).to eq(original_names)
|
||||||
|
expect(TagVersion.order(:id).map(&:attributes)).to eq(original_versions)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import LocalisedText from '@/components/LocalisedText'
|
||||||
|
|
||||||
|
|
||||||
|
describe ('LocalisedText', () => {
|
||||||
|
it.each ([
|
||||||
|
['ネコ チャペ', ['ネコ', 'チャペ']],
|
||||||
|
['ネコ、チャペ', ['ネコ、', 'チャペ']],
|
||||||
|
['著作者人格権、商標権、肖像権、パブリシティー権',
|
||||||
|
['著作者人格権、', '商標権、', '肖像権、', 'パブリシティー権']],
|
||||||
|
['ネコ、チャペ。', ['ネコ、', 'チャペ。']],
|
||||||
|
['ネコ、「チャペ」。', ['ネコ、', '「チャペ」。']],
|
||||||
|
['ネコ,、」チャペ', ['ネコ,、」', 'チャペ']]]) (
|
||||||
|
'adds word boundaries without changing %s', (text, words) => {
|
||||||
|
const { container } = render (<LocalisedText language="ain">{text}</LocalisedText>)
|
||||||
|
const wrapper = container.firstElementChild!
|
||||||
|
expect (wrapper).toHaveAttribute ('lang', 'ain-Kana')
|
||||||
|
expect (wrapper.textContent).toBe (text)
|
||||||
|
expect (Array.from (wrapper.querySelectorAll ('.ainu-ui-word'), node => node.textContent))
|
||||||
|
.toEqual (words)
|
||||||
|
expect (wrapper.querySelectorAll ('wbr')).toHaveLength (words.length - 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('preserves Japanese fallback and links and code inside an Ainu paragraph', () => {
|
||||||
|
const { container } = render (
|
||||||
|
<LocalisedText language="ain">
|
||||||
|
<span lang="ja">日本語、本文</span>
|
||||||
|
<a href="https://example.com">ネコ チャペ</a>
|
||||||
|
<code>ネコ チャペ</code>
|
||||||
|
<strong>ネコ チャペ</strong>
|
||||||
|
</LocalisedText>)
|
||||||
|
expect (container.querySelector ('[lang="ja"] .ainu-ui-word')).toBeNull ()
|
||||||
|
expect (container.querySelector ('a .ainu-ui-word')).toBeNull ()
|
||||||
|
expect (container.querySelector ('code .ainu-ui-word')).toBeNull ()
|
||||||
|
expect (container.querySelectorAll ('strong .ainu-ui-word')).toHaveLength (2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('does not apply Ainu word splitting to Japanese text', () => {
|
||||||
|
const { container } = render (
|
||||||
|
<LocalisedText language="ja">ネコ、チャペ。</LocalisedText>)
|
||||||
|
expect (container.firstElementChild).toHaveAttribute ('lang', 'ja')
|
||||||
|
expect (container.querySelector ('.ainu-ui-word')).toBeNull ()
|
||||||
|
expect (container.textContent).toBe ('ネコ、チャペ。')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -29,7 +29,7 @@ describe ('Help file catalogue', () => {
|
|||||||
const { api } = createHelpFixture ()
|
const { api } = createHelpFixture ()
|
||||||
expect (api.parseHelpEdition (edition)).toEqual ({ date: '2026-10-02', revision })
|
expect (api.parseHelpEdition (edition)).toEqual ({ date: '2026-10-02', revision })
|
||||||
const date = api.helpEditionDate (edition)!
|
const date = api.helpEditionDate (edition)!
|
||||||
expect ([date.getFullYear (), date.getMonth (), date.getDate ()]).toEqual ([2026, 9, 2])
|
expect (date.toISOString ()).toBe ('2026-10-01T15:00:00.000Z')
|
||||||
})
|
})
|
||||||
|
|
||||||
it.each (['missing', '2026-02-30', '2026-10-02.0', '2026-10-02.2.extra',
|
it.each (['missing', '2026-02-30', '2026-10-02.0', '2026-10-02.2.extra',
|
||||||
@@ -51,9 +51,9 @@ describe ('Help file catalogue', () => {
|
|||||||
expect ((await api.getHelpArticle ('guide'))?.languages.ja.currentEdition)
|
expect ((await api.getHelpArticle ('guide'))?.languages.ja.currentEdition)
|
||||||
.toBe ('2026-10-02.10')
|
.toBe ('2026-10-02.10')
|
||||||
expect (await api.getHelpSearchEntries ('ja')).toEqual ([
|
expect (await api.getHelpSearchEntries ('ja')).toEqual ([
|
||||||
{ identifier: 'guide', edition: '2026-10-02.10',
|
{ identifier: 'guide', language: 'ja', edition: '2026-10-02.10',
|
||||||
title: 'ホーム第十版', body: '\nタグを検索する 第十版本文' },
|
title: 'ホーム第十版', body: '\nタグを検索する 第十版本文' },
|
||||||
{ identifier: '設定 手順', edition: '2026-10-03',
|
{ identifier: '設定 手順', language: 'ja', edition: '2026-10-03',
|
||||||
title: '設定', body: '\nタグを設定する' }])
|
title: '設定', body: '\nタグを設定する' }])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -66,6 +66,15 @@ describe ('Help file catalogue', () => {
|
|||||||
.not.toContain ('2026-10-02.11')
|
.not.toContain ('2026-10-02.11')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it ('does not substitute Japanese articles, search or changes for missing Ainu content',
|
||||||
|
async () => {
|
||||||
|
const { api } = createHelpFixture ()
|
||||||
|
const article = await api.getHelpArticle ('guide')
|
||||||
|
expect (api.resolveHelpLanguage (article, 'ain')).toBeNull ()
|
||||||
|
expect (await api.getHelpSearchEntries ('ain')).toEqual ([])
|
||||||
|
expect (await api.getHelpChanges ('ain')).toEqual ([])
|
||||||
|
})
|
||||||
|
|
||||||
it ('uses generated edition titles without reparsing raw sources', async () => {
|
it ('uses generated edition titles without reparsing raw sources', async () => {
|
||||||
const { api, raw } = createHelpFixture ()
|
const { api, raw } = createHelpFixture ()
|
||||||
expect (await api.getHelpEditionText ('guide', 'ja', '2026-10-01'))
|
expect (await api.getHelpEditionText ('guide', 'ja', '2026-10-01'))
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ describe ('posts API functions', () => {
|
|||||||
expect (api.apiGet).toHaveBeenCalledWith (
|
expect (api.apiGet).toHaveBeenCalledWith (
|
||||||
'/posts',
|
'/posts',
|
||||||
{
|
{
|
||||||
|
locale: 'ja',
|
||||||
params: {
|
params: {
|
||||||
title: 'title',
|
title: 'title',
|
||||||
tags: 'a b',
|
tags: 'a b',
|
||||||
@@ -89,6 +90,7 @@ describe ('posts API functions', () => {
|
|||||||
original_created_before: '2026-01-02T00:00:00Z',
|
original_created_before: '2026-01-02T00:00:00Z',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
locale: 'ja',
|
||||||
params: {
|
params: {
|
||||||
base_version_no: '7',
|
base_version_no: '7',
|
||||||
force: '1',
|
force: '1',
|
||||||
@@ -113,7 +115,7 @@ describe ('posts API functions', () => {
|
|||||||
|
|
||||||
expect (api.apiGet).toHaveBeenCalledWith (
|
expect (api.apiGet).toHaveBeenCalledWith (
|
||||||
'/posts/versions',
|
'/posts/versions',
|
||||||
{ params: { page: 2, limit: 50 } },
|
{ locale: 'ja', params: { page: 2, limit: 50 } },
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -129,6 +131,7 @@ describe ('posts API functions', () => {
|
|||||||
expect (api.apiGet).toHaveBeenCalledWith (
|
expect (api.apiGet).toHaveBeenCalledWith (
|
||||||
'/posts/versions',
|
'/posts/versions',
|
||||||
{
|
{
|
||||||
|
locale: 'ja',
|
||||||
params: {
|
params: {
|
||||||
external_tag: '7',
|
external_tag: '7',
|
||||||
page: 2,
|
page: 2,
|
||||||
|
|||||||
@@ -1,14 +1,46 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { QueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
import { postsKeys, tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||||
|
|
||||||
|
import type { FetchNicoTagsParams } from '@/types'
|
||||||
|
|
||||||
describe ('query keys', () => {
|
describe ('query keys', () => {
|
||||||
it ('uses stable namespaces for posts, tags, and wiki', () => {
|
it ('uses stable namespaces for posts, tags, and wiki', () => {
|
||||||
expect (postsKeys.show ('3')).toEqual (['posts', '3'])
|
expect (postsKeys.show ('3')).toEqual (['posts', 'ja', '3'])
|
||||||
expect (postsKeys.related ('3')).toEqual (['related', '3'])
|
expect (postsKeys.related ('3')).toEqual (['related', 'ja', '3'])
|
||||||
expect (tagsKeys.deerjikists ('7')).toEqual (['tags', 'deerjikists', '7'])
|
expect (tagsKeys.deerjikists ('7')).toEqual (['tags', 'ja', 'deerjikists', '7'])
|
||||||
expect (wikiKeys.show ('Title', { version: '2' })).toEqual (
|
expect (wikiKeys.show ('Title', { version: '2' })).toEqual (
|
||||||
['wiki', 'Title', { version: '2' }],
|
['wiki', 'ja', 'Title', { version: '2' }])
|
||||||
)
|
})
|
||||||
|
|
||||||
|
it ('isolates the same resource by viewing locale', () => {
|
||||||
|
const client = new QueryClient ()
|
||||||
|
client.setQueryData (postsKeys.show ('3', 'ja'), { title: '日本語' })
|
||||||
|
client.setQueryData (postsKeys.show ('3', 'ain-Kana'), { title: 'アイヌ語' })
|
||||||
|
|
||||||
|
expect (client.getQueryData (postsKeys.show ('3', 'ja'))).toEqual ({ title: '日本語' })
|
||||||
|
expect (client.getQueryData (postsKeys.show ('3', 'ain-Kana')))
|
||||||
|
.toEqual ({ title: 'アイヌ語' })
|
||||||
|
client.clear ()
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('invalidates both nico locales without invalidating ordinary tags', async () => {
|
||||||
|
const client = new QueryClient ()
|
||||||
|
const params: FetchNicoTagsParams = {
|
||||||
|
name: '', linkedTag: '', linkStatus: 'all', page: 1, limit: 20,
|
||||||
|
order: 'name:asc' }
|
||||||
|
const ja = tagsKeys.nicoIndex (params, 'ja')
|
||||||
|
const ain = tagsKeys.nicoIndex (params, 'ain-Kana')
|
||||||
|
const ordinary = tagsKeys.show ('3', 'ja')
|
||||||
|
for (const key of [ja, ain, ordinary])
|
||||||
|
client.setQueryData (key, [])
|
||||||
|
|
||||||
|
await client.invalidateQueries ({ queryKey: tagsKeys.nicoRoot })
|
||||||
|
|
||||||
|
expect (client.getQueryState (ja)?.isInvalidated).toBe (true)
|
||||||
|
expect (client.getQueryState (ain)?.isInvalidated).toBe (true)
|
||||||
|
expect (client.getQueryState (ordinary)?.isInvalidated).toBe (false)
|
||||||
|
client.clear ()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,65 +4,90 @@ import remarkWikiAutolink from '@/lib/remark-wiki-autolink'
|
|||||||
|
|
||||||
import type { Root } from 'mdast'
|
import type { Root } from 'mdast'
|
||||||
|
|
||||||
|
import type { WikiAutolinkCandidate } from '@/types'
|
||||||
|
|
||||||
|
|
||||||
|
const candidate = (
|
||||||
|
name: string,
|
||||||
|
resourceId: number,
|
||||||
|
overrides: Partial<WikiAutolinkCandidate> = {},
|
||||||
|
): WikiAutolinkCandidate => ({
|
||||||
|
name, resourceId, tagId: resourceId + 100, tagDescriptionId: resourceId,
|
||||||
|
resourceType: 'tag_description', languageCode: 'ja', localeCode: 'ain-Kana',
|
||||||
|
...overrides })
|
||||||
|
|
||||||
|
|
||||||
|
const paragraph = (value: string): Root => ({
|
||||||
|
type: 'root', children: [{
|
||||||
|
type: 'paragraph', children: [{ type: 'text', value }] }] })
|
||||||
|
|
||||||
|
|
||||||
describe ('remarkWikiAutolink', () => {
|
describe ('remarkWikiAutolink', () => {
|
||||||
it ('links matching wiki page names and prefers longer matches', () => {
|
it ('prefers longer names and keeps the viewing locale for Japanese text', () => {
|
||||||
const tree: Root = {
|
const tree = paragraph ('虹夏 and 虹')
|
||||||
type: 'root',
|
|
||||||
children: [{
|
|
||||||
type: 'paragraph',
|
|
||||||
children: [{ type: 'text', value: '虹夏 and 虹' }],
|
|
||||||
}],
|
|
||||||
}
|
|
||||||
|
|
||||||
remarkWikiAutolink (['虹', '虹夏']) (tree)
|
remarkWikiAutolink ([candidate ('虹', 1), candidate ('虹夏', 2)],
|
||||||
|
'ja', 'ain-Kana') (tree)
|
||||||
|
|
||||||
expect (tree.children[0]).toMatchObject ({
|
expect (tree.children[0]).toMatchObject ({
|
||||||
type: 'paragraph',
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{ type: 'link', url: '/ain-Kana/wiki/2',
|
||||||
type: 'link',
|
children: [{ type: 'text', value: '虹夏' }] },
|
||||||
url: '/wiki/%E8%99%B9%E5%A4%8F',
|
|
||||||
children: [{ type: 'text', value: '虹夏' }],
|
|
||||||
},
|
|
||||||
{ type: 'text', value: ' and ' },
|
{ type: 'text', value: ' and ' },
|
||||||
{
|
{ type: 'link', url: '/ain-Kana/wiki/1',
|
||||||
type: 'link',
|
children: [{ type: 'text', value: '虹' }] }] })
|
||||||
url: '/wiki/%E8%99%B9',
|
|
||||||
children: [{ type: 'text', value: '虹' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it ('does not link text inside existing links or code', () => {
|
it ('does not rewrite existing links, code or images', () => {
|
||||||
const tree: Root = {
|
const tree: Root = {
|
||||||
type: 'root',
|
type: 'root', children: [
|
||||||
children: [
|
{ type: 'paragraph', children: [
|
||||||
{
|
{ type: 'link', url: '/existing', children: [{ type: 'text', value: '虹' }] },
|
||||||
type: 'paragraph',
|
{ type: 'inlineCode', value: '虹' },
|
||||||
children: [{
|
{ type: 'image', url: '/image', alt: '虹' }] },
|
||||||
type: 'link',
|
{ type: 'code', value: '虹' }] }
|
||||||
url: '/existing',
|
const original = structuredClone (tree)
|
||||||
children: [{ type: 'text', value: '虹' }],
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'code',
|
|
||||||
value: '虹',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
remarkWikiAutolink (['虹']) (tree)
|
remarkWikiAutolink ([candidate ('虹', 1)], 'ja', 'ain-Kana') (tree)
|
||||||
|
|
||||||
expect (tree.children[0]).toMatchObject ({
|
expect (tree).toEqual (original)
|
||||||
type: 'paragraph',
|
|
||||||
children: [{
|
|
||||||
type: 'link',
|
|
||||||
url: '/existing',
|
|
||||||
children: [{ type: 'text', value: '虹' }],
|
|
||||||
}],
|
|
||||||
})
|
})
|
||||||
expect (tree.children[1]).toMatchObject ({ type: 'code', value: '虹' })
|
|
||||||
|
it ('links an alias to its description rather than its tag ID or matched name', () => {
|
||||||
|
const tree = paragraph ('別名')
|
||||||
|
remarkWikiAutolink ([candidate ('別名', 9, { tagId: 123 })],
|
||||||
|
'ja', 'ain-Kana') (tree)
|
||||||
|
expect (tree.children[0]).toMatchObject ({
|
||||||
|
children: [{ type: 'link', url: '/ain-Kana/wiki/9' }] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('leaves ambiguous names and other locales or languages unlinked', () => {
|
||||||
|
const tree = paragraph ('虹 名前 name')
|
||||||
|
const original = structuredClone (tree)
|
||||||
|
remarkWikiAutolink ([candidate ('虹', 1), candidate ('虹', 2),
|
||||||
|
candidate ('名前', 3, { localeCode: 'ja' }),
|
||||||
|
candidate ('name', 4, { languageCode: 'ain' })], 'ja', 'ain-Kana') (tree)
|
||||||
|
expect (tree).toEqual (original)
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('links Ainu words only at word boundaries', () => {
|
||||||
|
const tree = paragraph ('チャペ チャペ。チャペア')
|
||||||
|
remarkWikiAutolink ([candidate ('チャペ', 5, { languageCode: 'ain' })],
|
||||||
|
'ain', 'ain-Kana') (tree)
|
||||||
|
expect (tree.children[0]).toMatchObject ({
|
||||||
|
children: [
|
||||||
|
{ type: 'link', url: '/ain-Kana/wiki/5' },
|
||||||
|
{ type: 'text', value: ' ' },
|
||||||
|
{ type: 'link', url: '/ain-Kana/wiki/5' },
|
||||||
|
{ type: 'text', value: '。チャペア' }] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('preserves tag-group resource identity', () => {
|
||||||
|
const tree = paragraph ('group')
|
||||||
|
remarkWikiAutolink ([candidate ('group', 8, {
|
||||||
|
resourceType: 'tag_group', tagId: null, tagDescriptionId: null })],
|
||||||
|
'ja', 'ain-Kana') (tree)
|
||||||
|
expect (tree.children[0]).toMatchObject ({
|
||||||
|
children: [{ type: 'link', url: '/ain-Kana/wiki/8' }] })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ describe ('tags API functions', () => {
|
|||||||
expect (api.apiGet).toHaveBeenCalledWith (
|
expect (api.apiGet).toHaveBeenCalledWith (
|
||||||
'/tags',
|
'/tags',
|
||||||
{
|
{
|
||||||
|
locale: 'ja',
|
||||||
params: {
|
params: {
|
||||||
name: '虹',
|
name: '虹',
|
||||||
category: 'character',
|
category: 'character',
|
||||||
@@ -68,7 +69,7 @@ describe ('tags API functions', () => {
|
|||||||
|
|
||||||
expect (api.apiGet).toHaveBeenCalledWith (
|
expect (api.apiGet).toHaveBeenCalledWith (
|
||||||
'/tags',
|
'/tags',
|
||||||
{ params: expect.objectContaining ({ deprecated: expected }) },
|
{ locale: 'ja', params: expect.objectContaining ({ deprecated: expected }) },
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -91,6 +92,6 @@ describe ('tags API functions', () => {
|
|||||||
api.apiGet.mockRejectedValueOnce (error)
|
api.apiGet.mockRejectedValueOnce (error)
|
||||||
await expect (fetchTagByName ('名前/#%')).rejects.toBe (error)
|
await expect (fetchTagByName ('名前/#%')).rejects.toBe (error)
|
||||||
expect (api.apiGet).toHaveBeenLastCalledWith (
|
expect (api.apiGet).toHaveBeenLastCalledWith (
|
||||||
`/tags/name/${ encodeURIComponent ('名前/#%') }`)
|
`/tags/name/${ encodeURIComponent ('名前/#%') }`, { locale: 'ja' })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ describe ('wiki API functions', () => {
|
|||||||
expect (api.apiGet).toHaveBeenNthCalledWith (
|
expect (api.apiGet).toHaveBeenNthCalledWith (
|
||||||
1,
|
1,
|
||||||
'/wiki',
|
'/wiki',
|
||||||
{ params: { title: '虹' } })
|
{ locale: 'ja', params: { title: '虹' } })
|
||||||
expect (api.apiGet).toHaveBeenNthCalledWith (
|
expect (api.apiGet).toHaveBeenNthCalledWith (
|
||||||
2,
|
2,
|
||||||
'/wiki/1',
|
'/wiki/1',
|
||||||
{ params: { version: '3' } })
|
{ locale: 'ja', params: { version: '3' } })
|
||||||
})
|
})
|
||||||
|
|
||||||
it ('encodes title path segments and returns null on misses', async () => {
|
it ('encodes title path segments and returns null on misses', async () => {
|
||||||
@@ -38,7 +38,7 @@ describe ('wiki API functions', () => {
|
|||||||
expect (api.apiGet).toHaveBeenNthCalledWith (
|
expect (api.apiGet).toHaveBeenNthCalledWith (
|
||||||
1,
|
1,
|
||||||
'/wiki/title/a%2Fb%20c',
|
'/wiki/title/a%2Fb%20c',
|
||||||
{ params: { version: undefined } })
|
{ locale: 'ja', params: { version: undefined } })
|
||||||
})
|
})
|
||||||
|
|
||||||
it.each ([403, 500]) ('preserves HTTP %s errors', async status => {
|
it.each ([403, 500]) ('preserves HTTP %s errors', async status => {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { ModuleKind, ScriptTarget, transpileModule } from 'typescript'
|
|||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
import source from '@/lib/help.ts?raw'
|
import source from '@/lib/help.ts?raw'
|
||||||
|
import * as locales from '@/lib/locales'
|
||||||
|
import * as queryKeys from '@/lib/queryKeys'
|
||||||
|
|
||||||
import type { ComponentType } from 'react'
|
import type { ComponentType } from 'react'
|
||||||
|
|
||||||
@@ -92,6 +94,10 @@ export const createHelpFixture = () => {
|
|||||||
loaders: vi.fn (() => loaders) }
|
loaders: vi.fn (() => loaders) }
|
||||||
const api = { } as typeof Help
|
const api = { } as typeof Help
|
||||||
const requireFixture = (name: string) => {
|
const requireFixture = (name: string) => {
|
||||||
|
if (name === '@/lib/locales')
|
||||||
|
return locales
|
||||||
|
if (name === '@/lib/queryKeys')
|
||||||
|
return queryKeys
|
||||||
const key = name.replace ('virtual:help/', '') as keyof typeof loads
|
const key = name.replace ('virtual:help/', '') as keyof typeof loads
|
||||||
if (Object.hasOwn (loads, key))
|
if (Object.hasOwn (loads, key))
|
||||||
return loads[key] ()
|
return loads[key] ()
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ const TestRoute = () => {
|
|||||||
const children = useContext (TestContentContext)
|
const children = useContext (TestContentContext)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<DialogueProvider>
|
||||||
<UnsavedChangesGuardProvider>
|
<UnsavedChangesGuardProvider>
|
||||||
<KeyboardShortcutsProvider>
|
<KeyboardShortcutsProvider>
|
||||||
{children}
|
{children}
|
||||||
</KeyboardShortcutsProvider>
|
</KeyboardShortcutsProvider>
|
||||||
</UnsavedChangesGuardProvider>)
|
</UnsavedChangesGuardProvider>
|
||||||
|
</DialogueProvider>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const renderWithProviders = (
|
export const renderWithProviders = (
|
||||||
@@ -46,11 +48,9 @@ export const renderWithProviders = (
|
|||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
<DialogueProvider>
|
|
||||||
<TestContentContext.Provider value={children}>
|
<TestContentContext.Provider value={children}>
|
||||||
<RouterProvider router={router}/>
|
<RouterProvider router={router}/>
|
||||||
</TestContentContext.Provider>
|
</TestContentContext.Provider>
|
||||||
</DialogueProvider>
|
|
||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
</QueryClientProvider>)
|
</QueryClientProvider>)
|
||||||
}
|
}
|
||||||
|
|||||||
新しいイシューから参照
ユーザーをブロックする