diff --git a/backend/spec/models/locale_spec.rb b/backend/spec/models/locale_spec.rb
index 9e559a4..59dcba2 100644
--- a/backend/spec/models/locale_spec.rb
+++ b/backend/spec/models/locale_spec.rb
@@ -67,20 +67,18 @@ RSpec.describe Locale, type: :model do
primary_flg: true, auto_generated: false)
end
- it 'creates a primary name when the language has only an alias' do
- prepare_ainu_reference!
- tag = create(:tag, primary_name: 'alias_only_tag')
- alias_name = create(:tag_name, :alias, name: 'alias_en',
- tag:,
- language_code: 'ain', script_code: 'Kana')
+ it 'adds the new language primary while preserving existing names and aliases' do
+ tag = create(:tag, primary_name: 'named_tag')
+ create(:tag_name, :alias, name: 'existing_alias', tag:,
+ language_code: 'ja', script_code: 'Jpan')
+ original_names = tag.tag_names.order(:id).map(&:attributes)
create_ainu_locale!
- expect(alias_name.reload).to have_attributes(
- tag_id: tag.id,
- language_code: 'ain', script_code: 'Kana', primary_flg: false)
+ expect(tag.tag_names.where(language_code: 'ja').order(:id).map(&:attributes))
+ .to eq(original_names)
generated = TagName.find_by!(tag_id: tag.id, language_code: 'ain', primary_flg: true)
- expect(generated).to have_attributes(tag_id: tag.id, language_code: 'ain',
+ expect(generated).to have_attributes(name: 'named_tag', tag_id: tag.id, language_code: 'ain',
script_code: 'Kana', primary_flg: true,
auto_generated: true)
expect(TagName.where(tag_id: tag.id, language_code: 'ain', primary_flg: true).count).to eq(1)
diff --git a/backend/spec/models/tag_name_sanitisation_rule_spec.rb b/backend/spec/models/tag_name_sanitisation_rule_spec.rb
index 3d88b96..ab77c84 100644
--- a/backend/spec/models/tag_name_sanitisation_rule_spec.rb
+++ b/backend/spec/models/tag_name_sanitisation_rule_spec.rb
@@ -113,6 +113,8 @@ RSpec.describe TagNameSanitisationRule, type: :model do
it 'renames without merging tags or deleting either language identity' do
locale
nipponese = create(:tag, primary_name: 'foobar')
+ primary_tag_name_for(nipponese, 'ain').update!(
+ name: 'different_ainu_name', auto_generated: false)
ainu_name = create(:tag_name, name: 'temporary',
language_code: 'ain', script_code: 'Kana')
ainu = create(:tag, primary_tag_name: ainu_name)
diff --git a/backend/spec/models/tag_spec.rb b/backend/spec/models/tag_spec.rb
index 5645680..a247f93 100644
--- a/backend/spec/models/tag_spec.rb
+++ b/backend/spec/models/tag_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe Tag, type: :model do
it 'canonicalises aliases in the supplied language only' do
prepare_ainu_reference!
nipponese = create(:tag, primary_name: 'nipponese_canonical')
- ainu_name = create(:tag_name, name: 'ainu_canonical',
+ ainu_name = build(:tag_name, tag: nil, name: 'ainu_canonical',
language_code: 'ain', script_code: 'Kana')
ainu = create(:tag, primary_tag_name: ainu_name)
primary_tag_name_for(ainu, 'ja').update!(
@@ -265,7 +265,7 @@ RSpec.describe Tag, type: :model do
expect(tag.category).to eq('character')
end
- it 'uses a Japanese fallback for an Ainu-origin tag without reverse transcription' do
+ it 'copies an Ainu-origin name to Japanese without reverse transcription' do
locale
allow(TagName).to receive(:generate_name).and_call_original
@@ -277,9 +277,9 @@ RSpec.describe Tag, type: :model do
name: 'ainu_origin', auto_generated: false))
expect(TagName.where(tag:, language_code: 'ja', primary_flg: true))
.to contain_exactly(have_attributes(
- name: "Tag_##{ tag.id }", auto_generated: true))
+ name: 'ainu_origin', auto_generated: true))
expect(TagName).to have_received(:generate_name)
- .with(Locale.nipponese, tag, nil)
+ .with(Locale.nipponese, tag, tag.tag_name('ain'))
end
it 'gives a new tag one primary name for every active language' do
diff --git a/backend/spec/requests/locale_write_paths_spec.rb b/backend/spec/requests/locale_write_paths_spec.rb
index c5ecfcc..51577d4 100644
--- a/backend/spec/requests/locale_write_paths_spec.rb
+++ b/backend/spec/requests/locale_write_paths_spec.rb
@@ -153,11 +153,14 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
let(:ainu_name) do
create(:tag_name, name: 'shared_name', language_code: 'ain', script_code: 'Kana')
end
- let(:ainu_tag) { create(:tag, primary_tag_name: ainu_name, category: :material) }
+ let(:ainu_tag) do
+ create(:tag, primary_tag_name: ainu_name, category: :material).tap do |tag|
+ primary_tag_name_for(tag, 'ja').update!(name: 'ainu_in_japanese', auto_generated: false)
+ end
+ end
let(:nipponese_tag) do
create(:tag, primary_name: 'temporary_nipponese', category: :general).tap do |tag|
- # Isolate lookup from the separately tested uniqueness validation.
- primary_tag_name_for(tag, 'ja').update_columns(name: 'shared_name')
+ primary_tag_name_for(tag, 'ja').update!(name: 'shared_name')
end
end
@@ -291,6 +294,8 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
it 'allows a rename to a name already used in Nipponese' do
locale
nipponese = create(:tag, primary_name: 'rename_target')
+ primary_tag_name_for(nipponese, 'ain').update!(
+ name: 'different_ainu_target', auto_generated: false)
patch "/tags/#{ tag.id }", params: { locale: 'ain-Kana', name: 'rename_target' }
diff --git a/backend/spec/requests/posts_spec.rb b/backend/spec/requests/posts_spec.rb
index 38bc115..06148f4 100644
--- a/backend/spec/requests/posts_spec.rb
+++ b/backend/spec/requests/posts_spec.rb
@@ -2323,8 +2323,10 @@ RSpec.describe 'Posts API', type: :request do
'prev' => nil
)
expect(latest.fetch('tags')).to include(
- { 'name' => 'spec_tag_2', 'type' => 'added' },
- { 'name' => 'spec_tag', 'type' => 'removed' }
+ { 'name' => 'spec_tag_2', 'restore_name' => 'spec_tag_2',
+ 'external' => false, 'type' => 'added' },
+ { 'name' => 'spec_tag', 'restore_name' => 'spec_tag',
+ 'external' => false, 'type' => 'removed' }
)
expect(latest.fetch('original_created_from')).to eq(
'current' => oc_from.iso8601,
@@ -2351,7 +2353,8 @@ RSpec.describe 'Posts API', type: :request do
'prev' => nil
)
expect(first.fetch('tags')).to include(
- { 'name' => 'spec_tag', 'type' => 'added' }
+ { 'name' => 'spec_tag', 'restore_name' => 'spec_tag',
+ 'external' => false, 'type' => 'added' }
)
expect(first.fetch('created_at')).to eq(t_v1.iso8601)
end
@@ -2435,7 +2438,9 @@ RSpec.describe 'Posts API', type: :request do
expect(response).to have_http_status(:ok)
expect(json.fetch('count')).to eq(3)
expect(json.fetch('versions').first.fetch('tags')).to include(
- 'name' => primary_tag_name_for(tag2, 'ja').name, 'type' => 'context')
+ 'name' => primary_tag_name_for(tag2, 'ja').name,
+ 'restore_name' => primary_tag_name_for(tag2, 'ja').name,
+ 'external' => false, 'type' => 'context')
end
it 'filters versions by tag when the current snapshot includes the tag' do
@@ -2449,7 +2454,8 @@ RSpec.describe 'Posts API', type: :request do
expect(versions[0]['post_id']).to eq(post_record.id)
expect(versions[0]['version_no']).to eq(2)
expect(versions[0]['tags']).to include(
- { 'name' => 'spec_tag_2', 'type' => 'added' }
+ { 'name' => 'spec_tag_2', 'restore_name' => 'spec_tag_2',
+ 'external' => false, 'type' => 'added' }
)
end
@@ -2467,10 +2473,12 @@ RSpec.describe 'Posts API', type: :request do
first = versions[1]
expect(latest['tags']).to include(
- { 'name' => 'spec_tag', 'type' => 'removed' }
+ { 'name' => 'spec_tag', 'restore_name' => 'spec_tag',
+ 'external' => false, 'type' => 'removed' }
)
expect(first['tags']).to include(
- { 'name' => 'spec_tag', 'type' => 'added' }
+ { 'name' => 'spec_tag', 'restore_name' => 'spec_tag',
+ 'external' => false, 'type' => 'added' }
)
end
diff --git a/backend/spec/services/post_create_plan_spec.rb b/backend/spec/services/post_create_plan_spec.rb
index 511346c..0efbaee 100644
--- a/backend/spec/services/post_create_plan_spec.rb
+++ b/backend/spec/services/post_create_plan_spec.rb
@@ -18,9 +18,9 @@ RSpec.describe PostCreatePlan do
ainu_name = create(:tag_name, name: 'shared_name',
language_code: 'ain', script_code: 'Kana')
ainu = create(:tag, primary_tag_name: ainu_name, category: :character)
+ primary_tag_name_for(ainu, 'ja').update!(name: 'ainu_in_japanese', auto_generated: false)
nipponese = create(:tag, primary_name: 'temporary_nipponese', category: :general)
- # The DB permits this identity; validation has its own contract spec.
- primary_tag_name_for(nipponese, 'ja').update_columns(name: 'shared_name')
+ primary_tag_name_for(nipponese, 'ja').update!(name: 'shared_name')
ainu_parent_name = create(:tag_name, name: 'ainu_parent',
language_code: 'ain', script_code: 'Kana')
ainu_parent = create(:tag, primary_tag_name: ainu_parent_name,
diff --git a/backend/spec/services/shared_resource_id_allocator_mysql_spec.rb b/backend/spec/services/shared_resource_id_allocator_mysql_spec.rb
index a6d908f..e6b2aa4 100644
--- a/backend/spec/services/shared_resource_id_allocator_mysql_spec.rb
+++ b/backend/spec/services/shared_resource_id_allocator_mysql_spec.rb
@@ -141,6 +141,40 @@ RSpec.describe 'Shared resource IDs across MySQL connections', type: :model do
result(first)
end
+ [false, true].each do |inner_rollback|
+ [false, true].each do |outer_rollback|
+ it "keeps savepoint=#{ inner_rollback } lock through outer rollback=#{ outer_rollback }" do
+ connection = ApplicationRecord.connection
+ database = connection.pool.db_config.database
+ lock_name = "btrc:ids:#{ Digest::SHA256.hexdigest(database)[0, 16] }:tags"
+ quoted_lock = connection.quote(lock_name)
+ owner = connection.select_value('SELECT CONNECTION_ID()').to_i
+
+ connection.transaction do
+ connection.transaction(requires_new: true) do
+ create_resource(:external)
+ raise ActiveRecord::Rollback if inner_rollback
+ end
+
+ expect(connection.select_value("SELECT IS_USED_LOCK(#{ quoted_lock })").to_i)
+ .to eq(owner)
+ contender = worker do |other|
+ other.select_value("SELECT GET_LOCK(#{ quoted_lock }, 0)").to_i
+ end
+ expect(result(contender)).to eq(0)
+
+ SharedResourceIdAllocator.lock_space!(ExternalTag, 'tags')
+ raise ActiveRecord::Rollback if outer_rollback
+ end
+
+ expect(connection.select_value("SELECT IS_FREE_LOCK(#{ quoted_lock })").to_i)
+ .to eq(1)
+ expect(ExternalTag.exists?(name: "#{ @prefix }_external"))
+ .to eq(!inner_rollback && !outer_rollback)
+ end
+ end
+ end
+
it 'raises on a real lock acquisition failure without inserting a record' do
first, (_id, _connection_id, lock_name) = hold_creation(:external, rollback: true)
stub_const('SharedResourceIdAllocator::LOCK_TIMEOUT', 0)
diff --git a/backend/spec/services/shared_resource_id_allocator_spec.rb b/backend/spec/services/shared_resource_id_allocator_spec.rb
index 8ec2114..1fa3f18 100644
--- a/backend/spec/services/shared_resource_id_allocator_spec.rb
+++ b/backend/spec/services/shared_resource_id_allocator_spec.rb
@@ -101,7 +101,7 @@ RSpec.describe SharedResourceIdAllocator, 'lock lifecycle' do
let(:record) { build(:external_tag) }
let(:connection) { double('connection') }
let(:transaction) { double('transaction', open?: true) }
- let(:callbacks) { { } }
+ let(:callbacks) { [] }
let(:sql) { [] }
before do
@@ -128,19 +128,24 @@ RSpec.describe SharedResourceIdAllocator, 'lock lifecycle' do
end
end
allow(ExternalTag).to receive(:connection).and_return(connection)
- allow(ExternalTag).to receive(:current_transaction).and_return(transaction)
+ allow(connection).to receive(:current_transaction).and_return(transaction)
+ allow(connection).to receive(:add_transaction_record) { callbacks << _1 }
+ allow(connection).to receive(:disconnect!)
allow(ExternalTag).to receive(:uncached).and_yield
- [:after_commit, :after_rollback].each do |event|
- allow(transaction).to receive(event) { |&callback| callbacks[event] = callback }
- end
end
- [:after_commit, :after_rollback].each do |event|
+ def finish_transaction event
+ allow(transaction).to receive(:open?).and_return(false)
+ callbacks.shift.public_send(event)
+ allow(transaction).to receive(:open?).and_return(true)
+ end
+
+ [:committed!, :rolledback!].each do |event|
it "holds the shared lock until #{ event }" do
described_class.assign!(record)
expect(record.id).to eq(9)
expect(sql.grep(/RELEASE_LOCK/)).to be_empty
- callbacks.fetch(event).call
+ finish_transaction(event)
expect(sql.grep(/RELEASE_LOCK/).size).to eq(1)
end
end
@@ -155,7 +160,7 @@ RSpec.describe SharedResourceIdAllocator, 'lock lifecycle' do
it 'releases a successfully acquired lock when allocation fails and rolls back' do
allow(connection).to receive(:select_all).and_raise('query failed')
expect { described_class.assign!(record) }.to raise_error(RuntimeError, 'query failed')
- callbacks.fetch(:after_rollback).call
+ finish_transaction(:rolledback!)
expect(sql.grep(/RELEASE_LOCK/).size).to eq(1)
end
@@ -164,18 +169,17 @@ RSpec.describe SharedResourceIdAllocator, 'lock lifecycle' do
described_class.assign!(record)
expect(record.id).to eq(50)
expect(sql.grep(/FOR UPDATE/)).to be_empty
- callbacks.fetch(:after_commit).call
+ finish_transaction(:committed!)
end
it 'uses the same named lock within each space and different locks between spaces' do
locks = [Tag, ExternalTag, TagDescription, TagGroup, TagDescriptionVersion, TagGroupVersion].map do |model|
allow(model).to receive(:connection).and_return(connection)
- allow(model).to receive(:current_transaction).and_return(transaction)
allow(model).to receive(:uncached).and_yield
record = model.new(id: 50)
described_class.assign!(record)
statement = sql.grep(/GET_LOCK/).last
- callbacks.fetch(:after_commit).call
+ finish_transaction(:committed!)
statement
end
expect(locks[0]).to eq(locks[1])
@@ -187,7 +191,21 @@ RSpec.describe SharedResourceIdAllocator, 'lock lifecycle' do
it 'reports lock release failures' do
described_class.assign!(record)
allow(connection).to receive(:select_value).with(/RELEASE_LOCK/).and_return(nil)
- expect { callbacks.fetch(:after_commit).call }
+ expect { finish_transaction(:committed!) }
.to raise_error(ActiveRecord::ActiveRecordError, /lock release failed/)
+ expect(connection).to have_received(:disconnect!)
+ end
+
+ it 'keeps the lock and transfers cleanup when a savepoint rolls back' do
+ described_class.assign!(record)
+ callbacks.shift.rolledback!
+
+ expect(callbacks.size).to eq(1)
+ expect(sql.grep(/RELEASE_LOCK/)).to be_empty
+ described_class.assign!(record)
+ expect(sql.grep(/GET_LOCK/).size).to eq(1)
+
+ finish_transaction(:committed!)
+ expect(sql.grep(/RELEASE_LOCK/).size).to eq(1)
end
end
diff --git a/backend/spec/services/tag_name_transcription_spec.rb b/backend/spec/services/tag_name_transcription_spec.rb
index 419d1cf..2036b96 100644
--- a/backend/spec/services/tag_name_transcription_spec.rb
+++ b/backend/spec/services/tag_name_transcription_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe TagNameTranscription do
'ガギグゲゴダヂヅデド' => 'カキクケコタチチュテト',
'バビブベボパピプペポ' => 'パピプペポパピプペポ',
'ザジズゼゾ' => 'チャチチュチェチョ',
- 'ジャジュジョ' => 'チヤチユチヨ',
+ 'ジャジュジョ' => 'チャチュチョ',
'アㇰアㇱアッアンアㇺアㇷ゚' => 'アㇰアㇱアッアンアㇺアㇷ゚',
'アㇻイㇼウㇽエㇾオㇿ' => 'アㇻイㇼウㇽエㇾオㇿ',
'漢Aあ' => 'カンAア' }.each do |source, expected|
@@ -26,9 +26,12 @@ RSpec.describe TagNameTranscription do
end
end
- ['😀', 'ヴ', 'ティ', 'ㇵ', '魑', '𠮷', 'ー', '々'].each do |source|
- it "rejects unsupported input #{ source }" do
- expect { described_class.ainu(source) }.to raise_error(described_class::Unconvertible)
+ { '😀' => '', 'ヴ' => '', 'ティ' => 'テ', 'ㇵ' => '',
+ '魑' => '', '𠮷' => '', 'ー' => '', '々' => '',
+ 'ア😀😀イ' => 'ア_イ', '😀アイ😀' => 'アイ',
+ '_😀_' => '__', 'ア_😀_イ' => 'ア___イ', '漢魑字' => 'カン_チ' }.each do |source, expected|
+ it "preserves convertible portions of #{ source }" do
+ expect(described_class.ainu(source)).to eq(expected)
end
end
end
diff --git a/backend/spec/services/tag_version_recorder_spec.rb b/backend/spec/services/tag_version_recorder_spec.rb
index 34b6c67..f2073c1 100644
--- a/backend/spec/services/tag_version_recorder_spec.rb
+++ b/backend/spec/services/tag_version_recorder_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe TagVersionRecorder do
it 'snapshots future languages together without a request locale argument' do
prepare_ainu_names!
- version = described_class.record!(tag:, event_type: :create, created_by_user: user)
+ version = described_class.record!(tag:, event_type: :update, created_by_user: user)
expect(version.names).to eq(
'ja' => nipponese_snapshot,
@@ -75,12 +75,12 @@ RSpec.describe TagVersionRecorder do
it 'creates a version for an Ainu-only change and preserves prior names' do
prepare_ainu_names!
- original = described_class.record!(tag:, event_type: :create, created_by_user: user)
+ original = described_class.record!(tag:, event_type: :update, created_by_user: user)
primary_tag_name_for(tag, 'ain').update!(name: 'Renamed')
updated = described_class.record!(tag:, event_type: :update, created_by_user: user)
- expect(updated.version_no).to eq(2)
+ expect(updated.version_no).to eq(original.version_no + 1)
expect(original.reload.names.fetch('ain')).to include('name' => 'Ainu name')
expect(updated.names.fetch('ain')).to include('name' => 'Renamed')
expect(updated.names.fetch('ja')).to eq(nipponese_snapshot)
@@ -88,11 +88,11 @@ RSpec.describe TagVersionRecorder do
it 'does not create a version for an unchanged multilingual snapshot' do
prepare_ainu_names!
- described_class.record!(tag:, event_type: :create, created_by_user: user)
+ original = described_class.record!(tag:, event_type: :update, created_by_user: user)
unchanged = described_class.record!(tag:, event_type: :update, created_by_user: user)
- expect(unchanged.version_no).to eq(1)
- expect(tag.reload.tag_versions.count).to eq(1)
+ expect(unchanged.version_no).to eq(original.version_no)
+ expect(tag.reload.tag_versions.count).to eq(original.version_no)
end
end
diff --git a/frontend/src/components/ErrorScreen.test.tsx b/frontend/src/components/ErrorScreen.test.tsx
index 2f3bca3..b032bad 100644
--- a/frontend/src/components/ErrorScreen.test.tsx
+++ b/frontend/src/components/ErrorScreen.test.tsx
@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react'
import { HelmetProvider } from 'react-helmet-async'
+import { MemoryRouter } from 'react-router-dom'
import { describe, expect, it } from 'vitest'
import ErrorScreen from '@/components/ErrorScreen'
@@ -12,9 +13,9 @@ describe ('ErrorScreen', () => {
[503, '鯖死んでるよ(泣)'],
]) ('renders status %s', (status, message) => {
render (
-
+
- ,
+ ,
)
expect (screen.getByText (String (status))).toBeInTheDocument ()
@@ -24,9 +25,9 @@ describe ('ErrorScreen', () => {
it ('throws for unsupported statuses', () => {
expect (() => render (
-
+
- ,
+ ,
)).toThrow ()
})
})
diff --git a/frontend/src/components/PostEditForm.test.tsx b/frontend/src/components/PostEditForm.test.tsx
index 349c753..60c4170 100644
--- a/frontend/src/components/PostEditForm.test.tsx
+++ b/frontend/src/components/PostEditForm.test.tsx
@@ -1,8 +1,10 @@
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { fireEvent, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
+import { textContentOf } from '@/test/text'
import PostEditForm from '@/components/PostEditForm'
import { buildPost, buildTag } from '@/test/factories'
+import { renderWithProviders as render } from '@/test/render'
const postsApi = vi.hoisted (() => ({
updatePost: vi.fn (),
@@ -19,7 +21,8 @@ const toastApi = vi.hoisted (() => ({
vi.mock ('@/lib/posts', () => postsApi)
vi.mock ('@/lib/api', () => api)
vi.mock ('@/components/ui/use-toast', () => toastApi)
-vi.mock ('@/lib/dialogues/useDialogue', () => ({
+vi.mock ('@/lib/dialogues/useDialogue', async importOriginal => ({
+ ...await importOriginal (),
default: () => ({
choice: vi.fn (),
}),
@@ -60,11 +63,12 @@ describe ('PostEditForm', () => {
parentPostIds: '3 4',
tags: 'general-tag',
}),
- { baseVersionNo: 4 },
+ { baseVersionNo: 4 }, 'ja',
)
})
expect (onSave).toHaveBeenCalledWith (expect.objectContaining ({ versionNo: 5 }))
- expect (toastApi.toast).toHaveBeenCalledWith ({ description: '更新しました.' })
+ expect (toastApi.toast).toHaveBeenCalledTimes (1)
+ expect (textContentOf (toastApi.toast.mock.calls[0][0].description)).toBe ('更新しました.')
})
it ('preserves duration while the video tag is temporarily removed', () => {
@@ -78,7 +82,7 @@ describe ('PostEditForm', () => {
render ()
- expect (screen.getByText ('動画時間').parentElement?.querySelector ('input'))
+ expect (screen.getByDisplayValue ('180.5'))
.toHaveValue ('180.5')
const tags = screen.getAllByRole ('textbox')[2]
@@ -88,7 +92,7 @@ describe ('PostEditForm', () => {
fireEvent.change (tags, {
target: { value: '動画 general-tag' },
})
- expect (screen.getByText ('動画時間').parentElement?.querySelector ('input'))
+ expect (screen.getByDisplayValue ('180.5'))
.toHaveValue ('180.5')
})
diff --git a/frontend/src/components/PostEmbed.test.tsx b/frontend/src/components/PostEmbed.test.tsx
index 15cd42c..d123068 100644
--- a/frontend/src/components/PostEmbed.test.tsx
+++ b/frontend/src/components/PostEmbed.test.tsx
@@ -1,8 +1,9 @@
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { fireEvent, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import PostEmbed from '@/components/PostEmbed'
import { buildPost } from '@/test/factories'
+import { renderWithProviders as render } from '@/test/render'
const dialogue = vi.hoisted (() => ({
confirm: vi.fn (),
@@ -12,8 +13,9 @@ const nicoViewer = vi.hoisted (() => ({
props: vi.fn (),
}))
-vi.mock ('@/components/dialogues/DialogueProvider', () => ({
- useDialogue: () => dialogue,
+vi.mock ('@/lib/dialogues/useDialogue', async importOriginal => ({
+ ...await importOriginal (),
+ default: () => dialogue,
}))
vi.mock ('@/components/NicoViewer', () => ({
diff --git a/frontend/src/components/PostFormTagsArea.test.tsx b/frontend/src/components/PostFormTagsArea.test.tsx
index b9586db..5687cd2 100644
--- a/frontend/src/components/PostFormTagsArea.test.tsx
+++ b/frontend/src/components/PostFormTagsArea.test.tsx
@@ -26,7 +26,8 @@ describe ('PostFormTagsArea', () => {
await waitFor (() => {
expect (api.apiGet).toHaveBeenCalledWith (
'/tags/autocomplete',
- { params: { q: '虹', nico: '0' } },
+ { locale: 'ja', params: { q: '虹', nico: '0' },
+ signal: expect.any (AbortSignal) },
)
})
expect (setTags).toHaveBeenCalledWith ('虹夏')
diff --git a/frontend/src/components/PostList.test.tsx b/frontend/src/components/PostList.test.tsx
index 8180a6a..78db6ac 100644
--- a/frontend/src/components/PostList.test.tsx
+++ b/frontend/src/components/PostList.test.tsx
@@ -26,11 +26,11 @@ describe ('PostList', () => {
expect (screen.getByRole ('link', { name: 'First' })).toHaveAttribute (
'href',
- '/posts/1',
+ '/ja/posts/1',
)
expect (
screen.getByRole ('link', { name: 'https://example.com/second' }),
- ).toHaveAttribute ('href', '/posts/2')
+ ).toHaveAttribute ('href', '/ja/posts/2')
})
it ('calls the optional click handler', () => {
diff --git a/frontend/src/components/PostOriginalCreatedTimeField.test.tsx b/frontend/src/components/PostOriginalCreatedTimeField.test.tsx
index 37186e2..5c64f10 100644
--- a/frontend/src/components/PostOriginalCreatedTimeField.test.tsx
+++ b/frontend/src/components/PostOriginalCreatedTimeField.test.tsx
@@ -1,7 +1,8 @@
-import { fireEvent, render, screen } from '@testing-library/react'
+import { fireEvent, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
+import { renderWithProviders as render } from '@/test/render'
describe ('PostOriginalCreatedTimeField', () => {
it ('updates from and before values', () => {
diff --git a/frontend/src/components/SortHeader.test.tsx b/frontend/src/components/SortHeader.test.tsx
index f11b93e..d797735 100644
--- a/frontend/src/components/SortHeader.test.tsx
+++ b/frontend/src/components/SortHeader.test.tsx
@@ -17,7 +17,7 @@ describe ('SortHeader', () => {
expect (screen.getByRole ('link', { name: 'タイトル ▲' })).toHaveAttribute (
'href',
- '/posts?tags=x&page=1&order=title%3Adesc',
+ '/ja/posts?tags=x&page=1&order=title%3Adesc',
)
})
@@ -33,7 +33,7 @@ describe ('SortHeader', () => {
expect (screen.getByRole ('link', { name: '更新' })).toHaveAttribute (
'href',
- '/posts?page=1&order=updated_at%3Adesc',
+ '/ja/posts?page=1&order=updated_at%3Adesc',
)
})
})
diff --git a/frontend/src/components/TagLink.test.tsx b/frontend/src/components/TagLink.test.tsx
index 8922265..4aba0d6 100644
--- a/frontend/src/components/TagLink.test.tsx
+++ b/frontend/src/components/TagLink.test.tsx
@@ -13,7 +13,7 @@ describe ('TagLink', () => {
expect (screen.getByRole ('link', { name: '虹 夏' })).toHaveAttribute (
'href',
- '/posts?tags=%E8%99%B9+%E5%A4%8F',
+ '/ja/posts?tags=%E8%99%B9+%E5%A4%8F',
)
expect (screen.getByText ('4')).toBeInTheDocument ()
})
@@ -40,7 +40,7 @@ describe ('TagLink', () => {
expect (screen.getByRole ('link', { name: '?' })).toHaveAttribute (
'href',
- '/wiki/a%2Fb',
+ '/ja/wiki/a%2Fb',
)
})
diff --git a/frontend/src/components/TopNavUser.test.tsx b/frontend/src/components/TopNavUser.test.tsx
index 49e1ee2..85ac99d 100644
--- a/frontend/src/components/TopNavUser.test.tsx
+++ b/frontend/src/components/TopNavUser.test.tsx
@@ -17,7 +17,7 @@ describe ('TopNavUser', () => {
expect (screen.getByRole ('link', { name: '山田' })).toHaveAttribute (
'href',
- '/users/settings',
+ '/ja/users/settings',
)
})
diff --git a/frontend/src/components/common/Pagination.test.tsx b/frontend/src/components/common/Pagination.test.tsx
index d5a659b..162d1b0 100644
--- a/frontend/src/components/common/Pagination.test.tsx
+++ b/frontend/src/components/common/Pagination.test.tsx
@@ -13,11 +13,11 @@ describe ('Pagination', () => {
expect (screen.getByLabelText ('前のページ')).toHaveAttribute (
'href',
- '/posts?tags=abc&page=2',
+ '/ja/posts?tags=abc&page=2',
)
expect (screen.getByLabelText ('次のページ')).toHaveAttribute (
'href',
- '/posts?tags=abc&page=4',
+ '/ja/posts?tags=abc&page=4',
)
expect (screen.getByText ('3')).toHaveAttribute ('aria-current', 'page')
})
@@ -33,6 +33,6 @@ describe ('Pagination', () => {
rerender ()
- expect (screen.getByLabelText ('次のページ')).toHaveAttribute ('href', '/tags?page=2')
+ expect (screen.getByLabelText ('次のページ')).toHaveAttribute ('href', '/ja/tags?page=2')
})
})
diff --git a/frontend/src/components/common/TagInput.test.tsx b/frontend/src/components/common/TagInput.test.tsx
index 96eabf8..af8cb9a 100644
--- a/frontend/src/components/common/TagInput.test.tsx
+++ b/frontend/src/components/common/TagInput.test.tsx
@@ -1,8 +1,9 @@
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { fireEvent, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import TagInput from '@/components/common/TagInput'
import { buildTag } from '@/test/factories'
+import { renderWithProviders as render } from '@/test/render'
const api = vi.hoisted (() => ({
apiGet: vi.fn (),
@@ -26,7 +27,8 @@ describe ('TagInput', () => {
await waitFor (() => {
expect (api.apiGet).toHaveBeenCalledWith (
'/tags/autocomplete',
- { params: { q: '虹夏', nico: '0' } },
+ { locale: 'ja', params: { q: '虹夏', nico: '0' },
+ signal: expect.any (AbortSignal) },
)
})
expect (setValue).toHaveBeenCalledWith ('ぼっち 虹夏')
@@ -53,7 +55,8 @@ describe ('TagInput', () => {
await waitFor (() => {
expect (api.apiGet).toHaveBeenCalledWith (
'/tags/autocomplete',
- { params: { q: '虹夏', nico: '1' } },
+ { locale: 'ja', params: { q: '虹夏', nico: '1' },
+ signal: expect.any (AbortSignal) },
)
})
})
diff --git a/frontend/src/components/dialogues/DialogueProvider.test.tsx b/frontend/src/components/dialogues/DialogueProvider.test.tsx
index 171631f..3caf266 100644
--- a/frontend/src/components/dialogues/DialogueProvider.test.tsx
+++ b/frontend/src/components/dialogues/DialogueProvider.test.tsx
@@ -1,6 +1,7 @@
import { useCallback, useEffect } from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
+import { MemoryRouter } from 'react-router-dom'
import DialogueProvider from '@/components/dialogues/DialogueProvider'
import useDialogue from '@/lib/dialogues/useDialogue'
@@ -49,7 +50,7 @@ describe ('DialogueProvider', () => {
)
}
- render ()
+ render ()
fireEvent.click (screen.getByRole ('button', { name: '開く' }))
expect (screen.getByText ('一件目')).toBeInTheDocument ()
@@ -73,7 +74,7 @@ describe ('DialogueProvider', () => {
)
}
- render ()
+ render ()
fireEvent.click (screen.getByRole ('button', { name: '開く' }))
const dialogue = screen.getByRole ('dialog')
@@ -101,7 +102,7 @@ describe ('DialogueProvider', () => {
)
}
- render ()
+ render ()
fireEvent.click (screen.getByRole ('button', { name: '開く' }))
const action = await screen.findByRole ('button', { name: '左操作' })
fireEvent.click (action)
diff --git a/frontend/src/components/layout/SidebarComponent.test.tsx b/frontend/src/components/layout/SidebarComponent.test.tsx
index c618446..8c7879a 100644
--- a/frontend/src/components/layout/SidebarComponent.test.tsx
+++ b/frontend/src/components/layout/SidebarComponent.test.tsx
@@ -1,8 +1,9 @@
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { fireEvent, screen, waitFor } from '@testing-library/react'
import { HelmetProvider } from 'react-helmet-async'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import SidebarComponent from '@/components/layout/SidebarComponent'
+import { renderWithProviders as render } from '@/test/render'
const sidebarWidth = (element: Element | null): string =>
diff --git a/frontend/src/components/posts/PostCreationDataFieldsUsage.test.tsx b/frontend/src/components/posts/PostCreationDataFieldsUsage.test.tsx
index d617eb9..2d2b2ea 100644
--- a/frontend/src/components/posts/PostCreationDataFieldsUsage.test.tsx
+++ b/frontend/src/components/posts/PostCreationDataFieldsUsage.test.tsx
@@ -1,7 +1,8 @@
-import { render, screen } from '@testing-library/react'
+import { screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { buildPostImportRow } from '@/test/postImportFactories'
+import { renderWithProviders as render } from '@/test/render'
import type { DialogueFormControls } from '@/lib/dialogues/useDialogue'
diff --git a/frontend/src/components/posts/PostThumbnailPreview.test.tsx b/frontend/src/components/posts/PostThumbnailPreview.test.tsx
index bb9a17e..656cc01 100644
--- a/frontend/src/components/posts/PostThumbnailPreview.test.tsx
+++ b/frontend/src/components/posts/PostThumbnailPreview.test.tsx
@@ -1,7 +1,8 @@
-import { fireEvent, render, screen } from '@testing-library/react'
+import { fireEvent, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import PostThumbnailPreview from '@/components/posts/PostThumbnailPreview'
+import { renderWithProviders as render } from '@/test/render'
describe ('PostThumbnailPreview', () => {
it ('keeps an existing blob preview URL unchanged for normal post forms', () => {
diff --git a/frontend/src/components/posts/import/PostImportRowForm.test.tsx b/frontend/src/components/posts/import/PostImportRowForm.test.tsx
index ad33957..bde56a9 100644
--- a/frontend/src/components/posts/import/PostImportRowForm.test.tsx
+++ b/frontend/src/components/posts/import/PostImportRowForm.test.tsx
@@ -1,8 +1,10 @@
-import { act, render, screen, waitFor } from '@testing-library/react'
+import { act, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import PostImportRowForm from '@/components/posts/import/PostImportRowForm'
import { buildPostImportRow } from '@/test/postImportFactories'
+import { renderWithProviders as render } from '@/test/render'
+import { textContentOf } from '@/test/text'
import type { DialogueFormAction, DialogueFormControls } from '@/lib/dialogues/useDialogue'
@@ -37,7 +39,7 @@ describe ('PostImportRowForm', () => {
const titleInput = screen.getByDisplayValue ('manual title')
await waitFor (() => expect (actions.at (-1)?.length).toBe (2))
- const reset = actions.at (-1)?.find (action => action.label === '変更をリセット')
+ const reset = actions.at (-1)?.find (action => textContentOf (action.label) === '変更をリセット')
expect (reset).toMatchObject ({ placement: 'start', variant: 'danger', disabled: false })
await act (async () => {
await reset?.onSelect ()
@@ -47,7 +49,7 @@ describe ('PostImportRowForm', () => {
expect (titleInput).toHaveValue ('')
expect (onSave).not.toHaveBeenCalled ()
- const save = actions.at (-1)?.find (action => action.label === '編輯内容を保存')
+ const save = actions.at (-1)?.find (action => textContentOf (action.label) === '編輯内容を保存')
await act (async () => {
await save?.onSelect ()
})
@@ -76,7 +78,7 @@ describe ('PostImportRowForm', () => {
await waitFor (() => expect (actions.length).toBe (2))
await act (async () => {
- await actions.find (action => action.label === '変更をリセット')?.onSelect ()
+ await actions.find (action => textContentOf (action.label) === '変更をリセット')?.onSelect ()
})
expect (screen.getByDisplayValue ('manual title')).toBeInTheDocument ()
@@ -124,7 +126,7 @@ describe ('PostImportRowForm', () => {
await waitFor (() => expect (actions.length).toBe (2))
await act (async () => {
- void actions.find (action => action.label === '編輯内容を保存')?.onSelect ()
+ void actions.find (action => textContentOf (action.label) === '編輯内容を保存')?.onSelect ()
})
expect (onSave).toHaveBeenCalledWith ({
@@ -171,7 +173,7 @@ describe ('PostImportRowForm', () => {
expect (screen.getByDisplayValue ('tag1')).toBeInTheDocument ()
await act (async () => {
- await actions.find (action => action.label === '編輯内容を保存')?.onSelect ()
+ await actions.find (action => textContentOf (action.label) === '編輯内容を保存')?.onSelect ()
})
expect (onSave).toHaveBeenCalledWith ({
@@ -218,7 +220,8 @@ describe ('PostImportRowForm', () => {
onSave={vi.fn ()}/>)
await waitFor (() => expect (actions.length).toBe (2))
- expect (actions.find (action => action.label === '変更をリセット')?.disabled).toBe (false)
+ const reset = actions.find (action => textContentOf (action.label) === '変更をリセット')
+ expect (reset?.disabled).toBe (false)
})
it (
@@ -249,7 +252,8 @@ describe ('PostImportRowForm', () => {
let savePromise: Promise | undefined
await act (async () => {
- savePromise = actions.find (action => action.label === '編輯内容を保存')?.onSelect ()
+ const save = actions.find (action => textContentOf (action.label) === '編輯内容を保存')
+ savePromise = save?.onSelect ()
})
await waitFor (() => {
diff --git a/frontend/src/components/posts/import/PostImportThumbnailPreview.test.tsx b/frontend/src/components/posts/import/PostImportThumbnailPreview.test.tsx
index 63879a7..41b9edd 100644
--- a/frontend/src/components/posts/import/PostImportThumbnailPreview.test.tsx
+++ b/frontend/src/components/posts/import/PostImportThumbnailPreview.test.tsx
@@ -1,7 +1,8 @@
-import { fireEvent, render, screen } from '@testing-library/react'
+import { fireEvent, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import PostImportThumbnailPreview from '@/components/posts/import/PostImportThumbnailPreview'
+import { renderWithProviders as render } from '@/test/render'
describe ('PostImportThumbnailPreview', () => {
beforeEach (() => {
diff --git a/frontend/src/lib/gekanator.test.ts b/frontend/src/lib/gekanator.test.ts
index bf8947f..a7ad271 100644
--- a/frontend/src/lib/gekanator.test.ts
+++ b/frontend/src/lib/gekanator.test.ts
@@ -52,7 +52,7 @@ describe('Gekanator API functions', () => {
mockedApiGet.mockResolvedValueOnce({ posts })
await expect(fetchGekanatorPosts()).resolves.toEqual(posts)
- expect(mockedApiGet).toHaveBeenCalledWith('/gekanator/posts')
+ expect(mockedApiGet).toHaveBeenCalledWith('/gekanator/posts', { locale: 'ja' })
})
it('returns questions from the Gekanator questions endpoint', async () => {
@@ -60,7 +60,7 @@ describe('Gekanator API functions', () => {
mockedApiGet.mockResolvedValueOnce({ questions })
await expect(fetchGekanatorQuestions()).resolves.toEqual(questions)
- expect(mockedApiGet).toHaveBeenCalledWith('/gekanator/questions')
+ expect(mockedApiGet).toHaveBeenCalledWith('/gekanator/questions', { locale: 'ja' })
})
})
@@ -458,7 +458,7 @@ describe('Gekanator API writers', () => {
original_answer: 'partial',
},
],
- })
+ }, { locale: 'ja' })
})
it('sends question suggestions using snake_case request keys', async () => {
@@ -482,7 +482,7 @@ describe('Gekanator API writers', () => {
gekanator_game_id: 100,
question_text: '喜多ちゃんが泣いてる?',
answer: 'yes',
- })
+ }, { locale: 'ja' })
})
it('sends extra question answers using snake_case request keys', async () => {
@@ -517,7 +517,7 @@ describe('Gekanator API writers', () => {
answer: 'probably_no',
},
],
- },
+ }, { locale: 'ja' },
)
})
})
diff --git a/frontend/src/lib/helpPrefetch.test.tsx b/frontend/src/lib/helpPrefetch.test.tsx
index 788b8e3..60969be 100644
--- a/frontend/src/lib/helpPrefetch.test.tsx
+++ b/frontend/src/lib/helpPrefetch.test.tsx
@@ -43,7 +43,7 @@ describe ('Help prefetch integration', () => {
Help
- }/>
+ }/>
>)
const loader = helpFixture.moduleLoads['../../help/guide/editions/ja/2026-10-02.10.mdx']
@@ -84,14 +84,14 @@ describe ('Help prefetch integration', () => {
diff
- }/>
+ }/>
>)
await Promise.all ([prefetchers.prefetchForURL (queryClient, path),
prefetchers.prefetchForURL (queryClient, path)])
fireEvent.click (screen.getByRole ('link', { name: 'diff' }))
expect (await screen.findByRole ('link', { name: '旧ホーム (2026-10-01)' }))
- .toHaveAttribute ('href', '/help/guide?edition=2026-10-01')
+ .toHaveAttribute ('href', '/ja/help/guide?edition=2026-10-01')
expect (screen.getByText ('+ # ホーム第十版')).toBeInTheDocument ()
Object.entries (helpFixture.raw).forEach (([name, load]) => {
const expected = /\/ja\/(2026-10-01|2026-10-02\.10)\.mdx$/.test (name) ? 1 : 0
diff --git a/frontend/src/lib/prefetchers.test.ts b/frontend/src/lib/prefetchers.test.ts
index b147c79..c79fce0 100644
--- a/frontend/src/lib/prefetchers.test.ts
+++ b/frontend/src/lib/prefetchers.test.ts
@@ -21,6 +21,8 @@ const wikiApi = vi.hoisted (() => ({
fetchWikiPage: vi.fn (),
fetchWikiPageByTitle: vi.fn (),
fetchWikiPages: vi.fn (),
+ fetchWikiPageByReference: vi.fn (),
+ fetchWikiAutolinkCandidates: vi.fn (),
}))
vi.mock ('@/lib/posts', () => postsApi)
@@ -45,12 +47,14 @@ describe ('prefetchForURL', () => {
wikiApi.fetchWikiPages.mockResolvedValue ([])
wikiApi.fetchWikiPage.mockResolvedValue ({ id: 1 })
wikiApi.fetchWikiPageByTitle.mockResolvedValue (null)
+ wikiApi.fetchWikiPageByReference.mockResolvedValue (null)
+ wikiApi.fetchWikiAutolinkCandidates.mockResolvedValue ([])
})
it ('prefetches post indexes from query parameters', async () => {
await prefetchForURL (
qc (),
- 'http://localhost/posts?tags=a+b&match=any&page=2&limit=5&order=title%3Aasc',
+ '/ja/posts?tags=a+b&match=any&page=2&limit=5&order=title%3Aasc',
)
expect (postsApi.fetchPosts).toHaveBeenCalledWith (
@@ -60,20 +64,20 @@ describe ('prefetchForURL', () => {
page: 2,
limit: 5,
order: 'title:asc',
- }),
+ }), 'ja',
)
})
it ('prefetches post detail pages', async () => {
- await prefetchForURL (qc (), 'http://localhost/posts/12')
+ await prefetchForURL (qc (), '/ja/posts/12')
- expect (postsApi.fetchPost).toHaveBeenCalledWith ('12')
+ expect (postsApi.fetchPost).toHaveBeenCalledWith ('12', 'ja')
})
it ('prefetches tag indexes from query parameters', async () => {
await prefetchForURL (
qc (),
- 'http://localhost/tags?post=9&name=x&category=general&page=4&post_count_lte=10',
+ '/ja/tags?post=9&name=x&category=general&page=4&post_count_lte=10',
)
expect (tagsApi.fetchTags).toHaveBeenCalledWith (
@@ -83,17 +87,17 @@ describe ('prefetchForURL', () => {
category: 'general',
page: 4,
postCountLTE: 10,
- }),
+ }), 'ja',
)
})
it ('prefetches nico tag indexes and their alias from query parameters', async () => {
await prefetchForURL (
qc (),
- 'http://localhost/tags/nico?name=source&linked_tag=destination'
+ '/ja/tags/nico?name=source&linked_tag=destination'
+ '&link_status=linked&page=3&limit=10',
)
- await prefetchForURL (qc (), 'http://localhost/nico/tags?page=2')
+ await prefetchForURL (qc (), '/ja/nico/tags?page=2')
expect (tagsApi.fetchNicoTags).toHaveBeenNthCalledWith (1, {
name: 'source',
@@ -102,7 +106,7 @@ describe ('prefetchForURL', () => {
page: 3,
limit: 10,
order: 'updated_at:desc',
- })
+ }, 'ja')
expect (tagsApi.fetchNicoTags).toHaveBeenNthCalledWith (2, {
name: '',
linkedTag: '',
@@ -110,28 +114,29 @@ describe ('prefetchForURL', () => {
page: 2,
limit: 20,
order: 'updated_at:desc',
- })
+ }, 'ja')
})
it ('prefetches wiki show pages and related tag/post data', async () => {
- wikiApi.fetchWikiPageByTitle.mockResolvedValueOnce ({
+ wikiApi.fetchWikiPageByReference.mockResolvedValueOnce ({
id: 3,
title: 'Actual',
body: 'body',
})
- await prefetchForURL (qc (), 'http://localhost/wiki/Alias')
+ await prefetchForURL (qc (), '/ja/wiki/Alias')
- expect (wikiApi.fetchWikiPageByTitle).toHaveBeenCalledWith ('Alias', { version: undefined })
- expect (wikiApi.fetchWikiPage).toHaveBeenCalledWith ('3', {})
- expect (tagsApi.fetchTagByName).toHaveBeenCalledWith ('Actual')
+ expect (wikiApi.fetchWikiPageByReference).toHaveBeenCalledWith (
+ 'Alias', { version: undefined }, 'ja')
+ expect (wikiApi.fetchWikiPage).not.toHaveBeenCalled ()
+ expect (tagsApi.fetchTagByName).toHaveBeenCalledWith ('Actual', 'ja')
expect (postsApi.fetchPosts).toHaveBeenCalledWith (
- expect.objectContaining ({ tags: 'Actual', limit: 8 }),
+ expect.objectContaining ({ tags: 'Actual', limit: 8 }), 'ja',
)
})
it ('ignores routes without a prefetcher', async () => {
- await prefetchForURL (qc (), 'http://localhost/unknown')
+ await prefetchForURL (qc (), '/ja/unknown')
expect (postsApi.fetchPosts).not.toHaveBeenCalled ()
expect (tagsApi.fetchTags).not.toHaveBeenCalled ()
@@ -141,14 +146,14 @@ describe ('prefetchForURL', () => {
it ('prefetches external tag post history without treating it as an internal tag', async () => {
await prefetchForURL (
qc (),
- 'http://localhost/posts/changes?external_tag=12&page=2&limit=50',
+ '/ja/posts/changes?external_tag=12&page=2&limit=50',
)
expect (postsApi.fetchPostChanges).toHaveBeenCalledWith ({
externalTag: '12',
page: 2,
limit: 50,
- })
+ }, 'ja')
expect (tagsApi.fetchTag).not.toHaveBeenCalled ()
})
diff --git a/frontend/src/mdx-components.test.tsx b/frontend/src/mdx-components.test.tsx
index a4ff0b6..d630175 100644
--- a/frontend/src/mdx-components.test.tsx
+++ b/frontend/src/mdx-components.test.tsx
@@ -32,7 +32,7 @@ describe ('Help MDX with the application compiler', () => {
expect (within (table).getAllByRole ('columnheader')).toHaveLength (2)
expect (within (table).getAllByRole ('cell')).toHaveLength (4)
expect (within (table).getByRole ('link', { name: 'ヘルプ' }))
- .toHaveAttribute ('href', '/help/guide')
+ .toHaveAttribute ('href', '/ja/help/guide')
expect (table.querySelector ('strong')).toHaveTextContent ('強調')
})
@@ -72,6 +72,6 @@ describe ('Help MDX with the application compiler', () => {
renderWithProviders ()
fireEvent.mouseEnter (screen.getByRole ('link', { name: '内部リンク' }))
expect (prefetch).toHaveBeenCalledWith (expect.anything (),
- new URL ('/help/guide', window.location.origin).href)
+ new URL ('/ja/help/guide', window.location.origin).href)
})
})
diff --git a/frontend/src/pages/deerjikists/DeerjikistDetailPage.test.tsx b/frontend/src/pages/deerjikists/DeerjikistDetailPage.test.tsx
index ae60500..3f7ac1c 100644
--- a/frontend/src/pages/deerjikists/DeerjikistDetailPage.test.tsx
+++ b/frontend/src/pages/deerjikists/DeerjikistDetailPage.test.tsx
@@ -63,7 +63,7 @@ describe ('DeerjikistDetailPage', () => {
await waitFor (() => {
expect (api.apiPut).toHaveBeenCalledWith (
'/tags/7/deerjikists',
- [{ platform: null, code: 'abc' }],
+ [{ platform: null, code: 'abc' }], { locale: 'ja' },
)
})
expect (await screen.findByText ('プラットフォームを入力してください.')).toBeInTheDocument ()
diff --git a/frontend/src/pages/help/HelpChangesPage.test.tsx b/frontend/src/pages/help/HelpChangesPage.test.tsx
index a0f8177..30a08f9 100644
--- a/frontend/src/pages/help/HelpChangesPage.test.tsx
+++ b/frontend/src/pages/help/HelpChangesPage.test.tsx
@@ -8,7 +8,7 @@ import App from '@/App'
import { helpFixture } from '@/test/helpFixtures'
vi.hoisted (() => {
- window.history.replaceState (null, '', '/help/changes')
+ window.history.replaceState (null, '', '/ja/help/changes')
})
vi.mock ('@/lib/help', async () => (await import ('@/test/helpFixtures')).helpFixture.api)
@@ -21,7 +21,10 @@ vi.mock ('@/lib/useClientBehaviourSettings', () => ({
describe ('Help global edition history', () => {
- beforeEach (() => helpFixture.reset ())
+ beforeEach (() => {
+ helpFixture.reset ()
+ vi.mocked (apiGet).mockResolvedValue ({ locales: [{ code: 'ja', name: '日本語' }] })
+ })
it ('routes /help/changes to published editions in date and numeric order', async () => {
const client = new QueryClient ({ defaultOptions: { queries: { retry: false } } })
@@ -31,12 +34,12 @@ describe ('Help global edition history', () => {
)
const links = await screen.findAllByRole ('link')
const editions = links.map (link => link.getAttribute ('href'))
- .filter (href => href?.startsWith ('/help/') && href.includes ('edition='))
+ .filter (href => href?.startsWith ('/ja/help/') && href.includes ('edition='))
expect (editions).toEqual ([
- `/help/${encodeURIComponent ('設定 手順')}?edition=2026-10-03`,
- '/help/guide?edition=2026-10-02.10', '/help/guide?edition=2026-10-02.3',
- '/help/guide?edition=2026-10-02.2', '/help/guide?edition=2026-10-02',
- '/help/guide?edition=2026-10-01'])
+ `/ja/help/${encodeURIComponent ('設定 手順')}?edition=2026-10-03`,
+ '/ja/help/guide?edition=2026-10-02.10', '/ja/help/guide?edition=2026-10-02.3',
+ '/ja/help/guide?edition=2026-10-02.2', '/ja/help/guide?edition=2026-10-02',
+ '/ja/help/guide?edition=2026-10-01'])
expect (container).toHaveTextContent ('旧ホーム')
expect (screen.getAllByRole ('columnheader').map (cell => cell.textContent))
.toEqual (['差分', 'タイトル', '有効開始年月日'])
@@ -45,11 +48,11 @@ describe ('Help global edition history', () => {
expect (container.textContent).not.toMatch (/\d{1,2}:\d{2}/)
expect (screen.getAllByRole ('link', { name: '差分' }).map (link =>
link.getAttribute ('href'))).toEqual ([
- '/help/guide/diff?from=2026-10-02.3&to=2026-10-02.10',
- '/help/guide/diff?from=2026-10-02.2&to=2026-10-02.3',
- '/help/guide/diff?from=2026-10-02&to=2026-10-02.2',
- '/help/guide/diff?from=2026-10-01&to=2026-10-02'])
- expect (apiGet).not.toHaveBeenCalled ()
- expect (window.location.pathname).toBe ('/help/changes')
+ '/ja/help/guide/diff?from=2026-10-02.3&to=2026-10-02.10',
+ '/ja/help/guide/diff?from=2026-10-02.2&to=2026-10-02.3',
+ '/ja/help/guide/diff?from=2026-10-02&to=2026-10-02.2',
+ '/ja/help/guide/diff?from=2026-10-01&to=2026-10-02'])
+ expect (apiGet).toHaveBeenCalledWith ('/locales')
+ expect (window.location.pathname).toBe ('/ja/help/changes')
})
})
diff --git a/frontend/src/pages/help/HelpDetailPage.test.tsx b/frontend/src/pages/help/HelpDetailPage.test.tsx
index 6bc3489..6f8b30a 100644
--- a/frontend/src/pages/help/HelpDetailPage.test.tsx
+++ b/frontend/src/pages/help/HelpDetailPage.test.tsx
@@ -17,19 +17,19 @@ const Navigation = () => {
return (
<>
-
-