コミットを比較

..
作成者 SHA1 メッセージ 日付
Codex Night Worker 63ea2727f0 WIP: preserve Issue #479 changes 2026-11-15 15:40:04 +09:00
テツノニジカとCodex Night Worker e13a52db9a AI: #454 広場のタグ検索,スマホ版だとサゼストが途中で見切れる問題 (#456)
Refs #454

## Automated implementation (requires human review)
[SidebarComponent.tsx](frontend/src/components/layout/SidebarComponent.tsx) の1行を修正しました。`overflow-y-auto` を `md` 以上に限定し、スマホでサゼストがサイドバーの領域外で切られる原因を取り除きました。

差分を確認済みです。指定に従い、テスト・lint・ビルドは未実行です。実画面でのスマホ表示とPC表示の確認も未実施です。

## Automated review (not a guarantee of correctness)
```text
The change restricts sidebar scrolling to medium and larger viewports. No actionable regression was identified in the current diff.

```

## Verification
- Tests/builds/migrations were not authorized and were not invoked by the controller.
- Codex was instructed not to run them; its command transcript should be inspected.
- Do not merge before manual acceptance and required checks.

---------

Co-authored-by: Codex Night Worker <codex-night@localhost>
Reviewed-on: #456
Co-authored-by: テツノニジカ <969+codex-bot@noreply.localhost>
2026-11-14 17:09:38 +09:00
41個のファイルの変更、134行の追加、138行の削除
+1 -1
ファイルの表示
@@ -424,7 +424,7 @@ races according to the target constraint's business meaning.
and JSON keys are generally snake_case.
- Preserve existing HTTP status conventions:
`:unauthorized` for no user, `:forbidden` for insufficient role or banned
user, `:not_found` for missing records, and `:unprocessable_entity` for
user, `:not_found` for missing records, and `:unprocessable_content` for
validation failures.
- For diagnostic or internal helper JSON, prefer a deliberately light response
shape over full representation classes when callers only need identifiers,
+1 -1
ファイルの表示
@@ -95,7 +95,7 @@ class ApplicationController < ActionController::API
render_validation_error base: ['すでに存在してゐます.']
end
def render_validation_error record = nil, fields: { }, base: [], status: :unprocessable_entity
def render_validation_error record = nil, fields: { }, base: [], status: :unprocessable_content
errors = { }
if record
+3 -3
ファイルの表示
@@ -18,7 +18,7 @@ class CountsExportsController < ApplicationController
render json: { error: 'private_condition' }, status: :forbidden
rescue CountsTableQuery::TooManyColumns
render json: { error: 'too_many_columns', max_columns: CountsTableQuery::MAX_COLUMNS },
status: :unprocessable_entity
status: :unprocessable_content
rescue ActiveRecord::StatementTimeout
render json: { error: 'counts_query_timeout' }, status: :service_unavailable
rescue ArgumentError, TypeError, KeyError, JSON::ParserError
@@ -63,7 +63,7 @@ class CountsExportsController < ApplicationController
render json: { error: 'private_condition' }, status: :forbidden
rescue CountsTableQuery::TooManyColumns
render json: { error: 'too_many_columns', max_columns: CountsTableQuery::MAX_COLUMNS },
status: :unprocessable_entity
status: :unprocessable_content
rescue ActiveRecord::StatementTimeout
render json: { error: 'counts_query_timeout' }, status: :service_unavailable
rescue ArgumentError, TypeError, KeyError, JSON::ParserError
@@ -107,7 +107,7 @@ class CountsExportsController < ApplicationController
render json: { error: 'private_condition' }, status: :forbidden
rescue CountsTableQuery::TooManyColumns
render json: { error: 'too_many_columns', max_columns: CountsTableQuery::MAX_COLUMNS },
status: :unprocessable_entity
status: :unprocessable_content
rescue ActiveRecord::StatementTimeout
render json: { error: 'counts_query_timeout' }, status: :service_unavailable
rescue ArgumentError, TypeError, KeyError, JSON::ParserError
+1 -1
ファイルの表示
@@ -24,7 +24,7 @@ class CountsTablesController < ApplicationController
render json: { error: 'private_condition' }, status: :forbidden
rescue CountsTableQuery::TooManyColumns
render json: { error: 'too_many_columns', max_columns: CountsTableQuery::MAX_COLUMNS },
status: :unprocessable_entity
status: :unprocessable_content
rescue CountsPivotTotals::TimedOut
Rails.logger.warn('counts_query_timeout: pivot_totals')
render json: { error: 'counts_query_timeout', stage: 'totals',
+2 -2
ファイルの表示
@@ -15,7 +15,7 @@ class GekanatorGamesController < ApplicationController
answers:)
if game.invalid?
render json: { errors: game.errors.full_messages }, status: :unprocessable_entity
render json: { errors: game.errors.full_messages }, status: :unprocessable_content
return
end
@@ -31,7 +31,7 @@ class GekanatorGamesController < ApplicationController
learned_example_count:
}, status: :created
rescue ActiveRecord::RecordInvalid => e
render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
render json: { errors: e.record.errors.full_messages }, status: :unprocessable_content
end
def extra_questions
+1 -1
ファイルの表示
@@ -536,7 +536,7 @@ class MaterialsController < ApplicationController
errors: { },
base_errors: ['ZIP export に必要な素材ファイルが欠損しています.'],
missing_files: },
status: :unprocessable_entity
status: :unprocessable_content
end
def build_uploaded_material_blob! file, file_sha256
+2 -2
ファイルの表示
@@ -429,7 +429,7 @@ class PostsController < ApplicationController
filters = literals.map do |literal|
internal, = source_ids.fetch(literal[:name])
name = internal.empty? ? literal[:name] : names[internal.first]
return head :unprocessable_entity unless name && internal.length <= 1
return head :unprocessable_content unless name && internal.length <= 1
literal[:negative] ? "not:#{ name }" : name
end
@@ -438,7 +438,7 @@ class PostsController < ApplicationController
literals.zip(translated).each do |literal, translation|
unless (source_ids.fetch(literal[:name]).map(&:sort) ==
target_ids.fetch(translation[:name]).map(&:sort))
return head :unprocessable_entity
return head :unprocessable_content
end
end
index = 0
+2 -2
ファイルの表示
@@ -55,7 +55,7 @@ class TheatresController < ApplicationController
theatre = Theatre.find_by(id: params[:id])
return head :not_found unless theatre
requested_post_id = params[:post_id].to_i
return head :unprocessable_entity if requested_post_id <= 0
return head :unprocessable_content if requested_post_id <= 0
skipped = false
conflicted = false
@@ -95,7 +95,7 @@ class TheatresController < ApplicationController
theatre = Theatre.find_by(id: params[:id])
return head :not_found unless theatre
requested_post_id = params[:post_id].to_i
return head :unprocessable_entity if requested_post_id <= 0
return head :unprocessable_content if requested_post_id <= 0
conflicted = false
+1 -1
ファイルの表示
@@ -262,7 +262,7 @@ RSpec.describe 'Counts table API', type: :request do
tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024, 3))
end
get '/counts/query', params: base.merge(mode: 'summary', rows: '', cols: 'title', sort: '')
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(response.parsed_body).to eq('error' => 'too_many_columns', 'max_columns' => 100)
end
+2 -2
ファイルの表示
@@ -20,7 +20,7 @@ RSpec.describe 'error responses', type: :request do
patch "/tags/#{ tag.id }", params: { category: 'nico' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json).to include(
'type' => 'validation_error',
'message' => '入力内容を確認してください.',
@@ -37,7 +37,7 @@ RSpec.describe 'error responses', type: :request do
put "/users/#{ user.id }", params: { name: 'a' * 256 }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json).to include(
'type' => 'validation_error',
'message' => '入力内容を確認してください.')
+1 -1
ファイルの表示
@@ -49,7 +49,7 @@ RSpec.describe 'Gekanator games API', type: :request do
question_count: 4,
answers: [{ question_id: 'tag:1', answer: 'no' }] }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'returns unauthorized without a user' do
+4 -4
ファイルの表示
@@ -126,7 +126,7 @@ RSpec.describe 'Gekanator learning API', type: :request do
}
}.not_to change { GekanatorGame.count }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'stores a game result for a non-admin user' do
@@ -785,7 +785,7 @@ RSpec.describe 'Gekanator learning API', type: :request do
}
}.not_to change { GekanatorQuestionExample.count }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'rejects non-accepted questions' do
@@ -805,7 +805,7 @@ RSpec.describe 'Gekanator learning API', type: :request do
]
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'rejects non-post_similarity questions' do
@@ -829,7 +829,7 @@ RSpec.describe 'Gekanator learning API', type: :request do
]
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'allows a non-admin user to answer extra questions for their own game' do
+2 -2
ファイルの表示
@@ -55,7 +55,7 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
deprecated: false, aliases: 'other_primary', parent_tags: '' }
aggregate_failures do
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(other_primary.reload).to have_attributes(tag_id: other_tag.id, primary_flg: true)
expect(TagName.where(tag: other_tag, language_code: 'ja', primary_flg: true))
.to contain_exactly(other_primary)
@@ -289,7 +289,7 @@ RSpec.describe 'Locale propagation on write paths', type: :request do
aliases: 'other_ainu_primary', parent_tags: '' }
aggregate_failures do
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(other_primary.reload).to have_attributes(
tag_id: other_tag.id, language_code: 'ain', primary_flg: true)
expect(TagName.where(tag: other_tag, language_code: 'ain', primary_flg: true))
+6 -6
ファイルの表示
@@ -289,7 +289,7 @@ RSpec.describe 'Materials API', type: :request do
it 'returns 422 when tag is blank' do
post '/materials', params: { tag: ' ', file: dummy_upload }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'tag' => ['タグは必須です.'])
end
@@ -297,7 +297,7 @@ RSpec.describe 'Materials API', type: :request do
it 'returns 422 when both file and url are blank' do
post '/materials', params: { tag: 'material_create_blank' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'file' => ['ファイルまたは URL は必須です.'],
'url' => ['ファイルまたは URL は必須です.'])
@@ -381,7 +381,7 @@ RSpec.describe 'Materials API', type: :request do
file: dummy_upload
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'persists url-only material' do
@@ -424,7 +424,7 @@ RSpec.describe 'Materials API', type: :request do
}
end.not_to change(Material, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'file' => ['抑止された素材です: copyright_high_risk']
)
@@ -484,7 +484,7 @@ RSpec.describe 'Materials API', type: :request do
file: dummy_upload
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'tag' => ['タグは必須です.'])
end
@@ -695,7 +695,7 @@ RSpec.describe 'Materials API', type: :request do
file: dummy_upload(filename: 'blocked.png', body: 'blocked-update')
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(material.reload.file.blob.filename.to_s).to eq('old.png')
end
end
+2 -2
ファイルの表示
@@ -218,7 +218,7 @@ RSpec.describe 'NicoTags', type: :request do
patch "/tags/nico/#{nico_tag.id}", params: { tags: 'nico:linked_ng' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'tags' => ['ニコニコ・タグ同士は連携できません.'])
end
@@ -239,7 +239,7 @@ RSpec.describe 'NicoTags', type: :request do
expect([Tag.count, TagName.count, TagVersion.count]).to eq(before_counts)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors').fetch('tags')).to include(
a_string_including('タグ名 “invalid”:', '名前に使用できない文字が含まれてゐます.'))
end
+1 -1
ファイルの表示
@@ -48,7 +48,7 @@ RSpec.describe 'Post filter identity across locales', type: :request do
primary_tag_name_for(tag, 'ja').update_columns(name: 'nico:shared')
get '/posts/tag_filters', params: {
locale: 'ja', target_locale: 'ain-Kana', tags: 'nico:shared' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'rejects removed reference parameters instead of overriding the name' do
+24 -24
ファイルの表示
@@ -1042,7 +1042,7 @@ RSpec.describe 'Posts API', type: :request do
get '/posts/metadata', params: { url: 'file:///etc/passwd' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to have_key('url')
end
end
@@ -1180,7 +1180,7 @@ RSpec.describe 'Posts API', type: :request do
)
}.not_to change(Post, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'thumbnail' => ['サムネイル画像の変換に失敗しました.']
)
@@ -1219,7 +1219,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload
)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'tags' => ['廃止済みタグがあります: deprecated_direct_tag']
)
@@ -1265,7 +1265,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload
)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json).to include(
'type' => 'validation_error',
'message' => '入力内容を確認してください.'
@@ -1333,7 +1333,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload
)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'video_ms' => ['タグ区間の終端が動画時間を超えてゐます.']
)
@@ -1444,7 +1444,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload
)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'video_ms' => ['タグ区間の開始が動画時間以上です.']
)
@@ -1465,7 +1465,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload
)
expect(response).to have_http_status(:unprocessable_entity), response.body
expect(response).to have_http_status(:unprocessable_content), response.body
expect(json).to include(
'type' => 'validation_error',
'message' => '入力内容を確認してください.',
@@ -1485,7 +1485,7 @@ RSpec.describe 'Posts API', type: :request do
tags: 'spec_tag', # 既存タグ名を投げる
thumbnail: dummy_upload)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
end
@@ -1499,7 +1499,7 @@ RSpec.describe 'Posts API', type: :request do
tags: 'spec_tag', # 既存タグ名を投げる
thumbnail: dummy_upload)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
end
@@ -1601,7 +1601,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload }
}.not_to change(Post, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to be_present
end
end
@@ -1620,7 +1620,7 @@ RSpec.describe 'Posts API', type: :request do
)
}.not_to change(Post, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to be_present
end
end
@@ -1639,7 +1639,7 @@ RSpec.describe 'Posts API', type: :request do
)
}.not_to change(PostImplication, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to be_present
end
end
@@ -1770,7 +1770,7 @@ RSpec.describe 'Posts API', type: :request do
tags: 'deprecated_update_tag'
)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'tags' => ['廃止済みタグは付与できません.']
)
@@ -1789,7 +1789,7 @@ RSpec.describe 'Posts API', type: :request do
title: 'updated title',
tags: 'nico:nico_tag')
expect(response).to have_http_status(:unprocessable_entity), response.body
expect(response).to have_http_status(:unprocessable_content), response.body
expect(json).to include(
'type' => 'validation_error',
'message' => '入力内容を確認してください.',
@@ -1896,7 +1896,7 @@ RSpec.describe 'Posts API', type: :request do
title: 'updated title',
tags: 'spec_tag' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to be_present
end
end
@@ -1922,7 +1922,7 @@ RSpec.describe 'Posts API', type: :request do
parent_post_ids: 'abc'
)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(post_record.reload.parent_posts.pluck(:id)).to eq([parent_post.id])
end
end
@@ -1948,7 +1948,7 @@ RSpec.describe 'Posts API', type: :request do
parent_post_ids: '999999999'
)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(post_record.reload.parent_posts.pluck(:id)).to eq([parent_post.id])
end
end
@@ -1964,7 +1964,7 @@ RSpec.describe 'Posts API', type: :request do
parent_post_ids: post_record.id.to_s
)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(PostImplication.exists?(
post_id: post_record.id,
@@ -2669,7 +2669,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload)
end.not_to change(PostVersion, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'does not create a version when PUT /posts/:id is invalid' do
@@ -2685,7 +2685,7 @@ RSpec.describe 'Posts API', type: :request do
original_created_before: Time.zone.local(2020, 1, 1, 0, 0, 0).iso8601)
end.not_to change(PostVersion, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'rejects second-bearing original created timestamps on POST /posts' do
@@ -2698,7 +2698,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload,
original_created_from: '2020-01-01T00:00:01Z')
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'original_created_from' => ['オリジナルの作成日時は分単位で入力してください.']
)
@@ -2715,7 +2715,7 @@ RSpec.describe 'Posts API', type: :request do
thumbnail: dummy_upload,
original_created_before: '2020-01-01T00:00:00.123Z')
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'original_created_before' => ['オリジナルの作成日時は分単位で入力してください.']
)
@@ -2733,7 +2733,7 @@ RSpec.describe 'Posts API', type: :request do
original_created_from: '2020-01-01T00:00Z',
original_created_before: '2020-01-01T00:00Z')
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'original_created_at' => ['オリジナルの作成日時の順番がをかしぃです.']
)
@@ -2767,7 +2767,7 @@ RSpec.describe 'Posts API', type: :request do
tags: 'spec_tag',
original_created_from: 'not-a-time')
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'original_created_from' => ['オリジナルの作成日時の形式が不正です.']
)
+1 -1
ファイルの表示
@@ -64,7 +64,7 @@ RSpec.describe "TagChildren", type: :request do
do_request
}.not_to change(TagImplication, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(TagImplication.where(tag: child, parent_tag: parent)).not_to exist
end
end
+1 -1
ファイルの表示
@@ -303,7 +303,7 @@ RSpec.describe 'Tag description native API', type: :request do
headers
before_state = stored_state
native_write(body: '')
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json).to include('type' => 'validation_error',
'errors' => include('body' => a_kind_of(Array)))
expect(stored_state).to eq(before_state)
+2 -2
ファイルの表示
@@ -84,7 +84,7 @@ RSpec.describe 'Tag group locale contracts', type: :request do
put "/wiki/#{ group.id }", params: {
locale: 'ain-Kana', title: 'タググループ:partial rename', body: '- japanese_member' },
headers: headers
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
@@ -117,7 +117,7 @@ RSpec.describe 'Tag group locale contracts', type: :request do
expect(representation.fetch('members').sole.fetch('tag').fetch('name')).to be_nil
expect(representation.fetch('placements').sole.fetch('tag').fetch('name')).to be_nil
get "/wiki/#{ old_group.id }", params: { locale: 'ain-Kana', version: version.id }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(response.body).not_to include('historical_japanese', 'current_ainu')
end
end
+2 -2
ファイルの表示
@@ -133,7 +133,7 @@ RSpec.describe 'Tag group Markdown round trips through the Wiki facade', type: :
get group_title_path('混在構造')
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_rows('tag_groups', id:).sole.fetch('id')).to eq(id)
end
@@ -142,7 +142,7 @@ RSpec.describe 'Tag group Markdown round trips through the Wiki facade', type: :
get group_title_path('未配置構造')
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_rows('tag_groups', id:).sole.fetch('id')).to eq(id)
end
end
+1 -1
ファイルの表示
@@ -21,7 +21,7 @@ RSpec.describe 'Tag group Wiki facade writes', type: :request do
before_state = group_state
yield
aggregate_failures do
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
expect(response.media_type).to eq('application/json')
expect(response.body).not_to be_empty
+1 -1
ファイルの表示
@@ -27,7 +27,7 @@ RSpec.describe 'Tag group write integrity', type: :request do
def expect_no_writes
before_state = group_state
yield
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
+7 -7
ファイルの表示
@@ -148,7 +148,7 @@ RSpec.describe 'Tag group native API', type: :request do
before_state = group_state
native_write(operation, payload.merge(invalid))
aggregate_failures do
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
end
@@ -158,7 +158,7 @@ RSpec.describe 'Tag group native API', type: :request do
before_state = group_state
invalid = [{ tag_id: tag.id, section_name: '不正', children: [] }]
native_write(operation, payload.merge(placements: invalid))
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
@@ -167,7 +167,7 @@ RSpec.describe 'Tag group native API', type: :request do
before_state = group_state
native_write(operation, payload.merge(
members: [{ tag_id: missing }], placements: [{ tag_id: missing, children: [] }]))
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
@@ -177,7 +177,7 @@ RSpec.describe 'Tag group native API', type: :request do
native_write(operation, payload.merge(
members: [{ tag_group_id: missing }],
placements: [{ tag_group_id: missing, children: [] }]))
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
@@ -186,7 +186,7 @@ RSpec.describe 'Tag group native API', type: :request do
6.times { nodes = [{ tag_id: tag.id, children: nodes }] }
before_state = group_state
native_write(operation, payload.merge(placements: nodes))
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
end
@@ -206,7 +206,7 @@ RSpec.describe 'Tag group native API', type: :request do
before_state = group_state
native_write(:update, payload.merge(
members: [{ tag_group_id: parent }], placements: []))
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
@@ -214,7 +214,7 @@ RSpec.describe 'Tag group native API', type: :request do
before_state = group_state
native_write(:update, payload.merge(
members: [{ tag_group_id: group_id }], placements: []))
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
+3 -3
ファイルの表示
@@ -297,7 +297,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
do_request
}.not_to change { Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] } }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json).to include(
'type' => 'validation_error',
'message' => '入力内容を確認してください.',
@@ -321,7 +321,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
do_request
}.not_to change { Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] } }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'deerjikists.0.platform' => [be_present],
)
@@ -352,7 +352,7 @@ RSpec.describe 'Tags deerjikists API', type: :request do
do_request
}.not_to change { Deerjikist.order(:platform, :code).pluck(:platform, :code, :tag_id) }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'deerjikists.1.code' => [include('existing-deerjikist')],
)
+10 -10
ファイルの表示
@@ -696,7 +696,7 @@ RSpec.describe 'Tags API', type: :request do
it 'nico category への変更は 422 を返す' do
patch "/tags/#{tag.id}", params: { name: 'new', category: 'nico' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
expect(tag.category).to eq('general')
end
@@ -740,7 +740,7 @@ RSpec.describe 'Tags API', type: :request do
patch "/tags/#{tag.id}", params: { category: 'nico' }
}.not_to change(TagVersion, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(tag.reload.category).to eq('general')
end
@@ -824,7 +824,7 @@ RSpec.describe 'Tags API', type: :request do
patch "/tags/#{ tag.id }", params: { name: 'unknown' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'name' => ['その名前は既に使はれてゐます.']
)
@@ -840,7 +840,7 @@ RSpec.describe 'Tags API', type: :request do
patch "/tags/#{ system_tag.id }", params: { name: 'patch_system_tag_renamed' }
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(primary_tag_name_for(system_tag.reload, 'ja').name).to eq('bot操作')
end
end
@@ -1144,7 +1144,7 @@ RSpec.describe 'Tags API', type: :request do
parent_tags: '',
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
end
@@ -1156,7 +1156,7 @@ RSpec.describe 'Tags API', type: :request do
parent_tags: '',
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
expect(tag.category).to eq('general')
end
@@ -1169,7 +1169,7 @@ RSpec.describe 'Tags API', type: :request do
parent_tags: '',
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'deprecated' => ['廃止状態は必須です.']
)
@@ -1366,7 +1366,7 @@ RSpec.describe 'Tags API', type: :request do
}
}.not_to change(TagVersion, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(primary_tag_name_for(tag.reload, 'ja').name).to eq('spec_tag')
expect(tag.category).to eq('general')
@@ -1387,7 +1387,7 @@ RSpec.describe 'Tags API', type: :request do
}
}.not_to change(TagVersion, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(primary_tag_name_for(system_tag.reload, 'ja').name).to eq(old_name)
expect(system_tag.category).to eq(old_category)
@@ -1569,7 +1569,7 @@ RSpec.describe 'Tags API', type: :request do
deprecated: '0',
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(TagImplication.where(tag:, parent_tag: child)).not_to exist
end
+1 -1
ファイルの表示
@@ -131,7 +131,7 @@ RSpec.describe 'TheatreComments', type: :request do
post "/theatres/#{theatre.id}/comments", params: { content: ' ' }
}.not_to change(TheatreComment, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'theatre が存在しなければ 404 を返す' do
+1 -1
ファイルの表示
@@ -376,7 +376,7 @@ RSpec.describe 'Theatres API', type: :request do
put "/theatres/#{theatre.id}/skip_vote", params: { post_id: 'invalid' }
}.not_to change(TheatreSkipVote, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'records a vote and returns the current vote status before majority' do
+2 -2
ファイルの表示
@@ -79,7 +79,7 @@ RSpec.describe 'user settings', type: :request do
patch '/users/settings', params: { theme: 1 }, as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json).to include('type' => 'validation_error')
expect(json.fetch('errors')).to include(
'theme' => ['値の型が不正です.'])
@@ -90,7 +90,7 @@ RSpec.describe 'user settings', type: :request do
patch '/users/settings', params: { theme: 'neon' }, as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json).to include('type' => 'validation_error')
expect(json.fetch('errors').fetch('theme')).to be_present
end
+2 -2
ファイルの表示
@@ -84,7 +84,7 @@ RSpec.describe 'user theme slots', type: :request do
params: { tokens: 'not-object' },
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'base_theme' => ['値が不正です.'])
end
@@ -96,7 +96,7 @@ RSpec.describe 'user theme slots', type: :request do
params: { tokens: 'not-object' },
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'tokens' => ['JSON object で指定してください.'])
end
+1 -1
ファイルの表示
@@ -119,7 +119,7 @@ RSpec.describe 'Users', type: :request do
params: { name: ' ' },
headers: auth_headers(user)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(json.fetch('errors')).to include(
'name' => ['名前は必須です.'])
end
+1 -1
ファイルの表示
@@ -39,7 +39,7 @@ RSpec.describe 'Wiki creation freeze', type: :request do
it "rejects invalid description body #{ body.inspect } atomically" do
before_state = group_state
put "/wiki/#{ page.id }", params: { title: tag.name('ja'), body: }, headers: headers
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
end
+3 -3
ファイルの表示
@@ -100,12 +100,12 @@ RSpec.describe 'Wiki API', type: :request do
context 'when params invalid' do
it 'returns 422 when title blank' do
post endpoint, params: { title: '', body: 'Hello' }, headers: auth_headers(member)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
it 'returns 422 when body blank' do
post endpoint, params: { title: 'Test', body: '' }, headers: auth_headers(member)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
end
@@ -160,7 +160,7 @@ RSpec.describe 'Wiki API', type: :request do
put "/wiki/#{page.id}",
params: { title: 'TestPage', body: '' },
headers: auth_headers(member)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
end
end
+1 -1
ファイルの表示
@@ -34,7 +34,7 @@ RSpec.describe 'Wiki title collision', type: :request do
},
headers: auth_headers(user)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
source.reload
+1 -1
ファイルの表示
@@ -18,7 +18,7 @@ RSpec.describe 'Removed standalone Wiki resources', type: :request do
before_state = group_state
post '/wiki', params: { title:, body: 'body' },
headers: cookie_headers(user)
expect(response).to have_http_status(:unprocessable_entity)
expect(response).to have_http_status(:unprocessable_content)
expect(group_state).to eq(before_state)
end
end
+2 -8
ファイルの表示
@@ -36,13 +36,10 @@ const replaceToken = (value: string, start: number, end: number, text: string) =
type Props = Omit<ComponentPropsWithoutRef<'textarea'>, 'value' | 'onChange' | 'onBlur'> & {
tags: string
setTags: (tags: string) => void
label?: ReactNode
errors?: ReactNode[] }
const PostFormTagsArea: FC<Props> = (
{ tags, setTags, label: fieldLabel, errors, ...rest },
) => {
const PostFormTagsArea: FC<Props> = ({ tags, setTags, errors, ...rest }) => {
const { label } = useUITranslation ('navigation')
const locale = useLocale ()
const ref = useRef<HTMLTextAreaElement> (null)
@@ -120,10 +117,7 @@ const PostFormTagsArea: FC<Props> = (
}, [locale])
return (
<FormField
className="relative w-full"
label={fieldLabel ?? label ('tags')}
messages={errors}>
<FormField className="relative w-full" label={label ('tags')} messages={errors}>
{({ describedBy, invalid }) => (
<>
<TextArea
+10
ファイルの表示
@@ -70,6 +70,16 @@ describe ('SidebarComponent', () => {
vi.spyOn (HTMLElement.prototype, 'hasPointerCapture').mockReturnValue (true)
})
it ('limits sidebar scrolling to desktop to avoid clipping mobile suggestions', () => {
renderSidebar ()
// jsdom has no layout; guard the responsive classes responsible for clipping.
const sidebar = screen.getByText ('Sidebar body')
expect (sidebar).not.toHaveClass ('overflow-y-auto')
expect (sidebar).toHaveClass ('md:overflow-y-auto')
})
it ('uses maxWidth without applying the container main-width reserve twice', async () => {
localStorage.setItem ('sidebar:test-sidebar:left:width', '700')
const onWidthChange = vi.fn ()
+1 -1
ファイルの表示
@@ -339,7 +339,7 @@ const SidebarComponent: FC<Props> = ({
</style>
</Helmet>
<div className="h-full overflow-y-auto p-4 sidebar">
<div className="h-full md:overflow-y-auto p-4 sidebar">
{children}
</div>
+3 -22
ファイルの表示
@@ -162,13 +162,12 @@ describe ('NicoTagListPage', () => {
})
})
it ('saves links after completing the tag at the cursor', async () => {
it ('updates links from a tag card', async () => {
api.apiGet
.mockResolvedValueOnce ({
tags: [buildNicoTag ({ id: 7, name: 'nico:source' })],
count: 1,
})
.mockResolvedValueOnce ([buildTag ({ id: 8, name: '連携先', postCount: 3 })])
.mockResolvedValueOnce ({
tags: [
buildNicoTag ({
@@ -184,24 +183,8 @@ describe ('NicoTagListPage', () => {
renderPage ()
fireEvent.click (await screen.findByRole ('button', { name: '編集' }))
const textarea = screen.getByLabelText ('連携する広場タグ')
fireEvent.change (textarea, { target: { value: '既存 連携\n後続' } })
fireEvent.focus (textarea)
fireEvent.select (textarea, {
target: { selectionStart: 4, selectionEnd: 4 } })
await waitFor (() => {
expect (api.apiGet).toHaveBeenCalledWith (
'/tags/autocomplete',
{ locale: 'ja', params: { q: '連携', nico: '0' },
signal: expect.any (AbortSignal) })
})
fireEvent.mouseDown (await screen.findByText ('連携先'))
expect (textarea).toHaveValue ('既存 連携先 \n後続')
await waitFor (() => {
expect ((textarea as HTMLTextAreaElement).selectionStart).toBe (7)
expect ((textarea as HTMLTextAreaElement).selectionEnd).toBe (7)
fireEvent.change (screen.getByLabelText ('連携する広場タグ'), {
target: { value: '連携先' },
})
fireEvent.click (screen.getByRole ('button', { name: '保存' }))
@@ -212,8 +195,6 @@ describe ('NicoTagListPage', () => {
{ locale: 'ja', headers: { 'Content-Type': 'multipart/form-data' } },
)
})
const payload = api.apiPut.mock.calls[0][1] as FormData
expect (payload.get ('tags')).toBe ('既存 連携先 \n後続')
expect (await screen.findByText ('1 件')).toBeInTheDocument ()
})
+12 -8
ファイルの表示
@@ -6,10 +6,11 @@ import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import TagLink from '@/components/TagLink'
import SortHeader from '@/components/SortHeader'
import PostFormTagsArea from '@/components/PostFormTagsArea'
import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle'
import Pagination from '@/components/common/Pagination'
import TextArea from '@/components/common/TextArea'
import useDialogue from '@/lib/dialogues/useDialogue'
import MainArea from '@/components/layout/MainArea'
import { toast } from '@/components/ui/use-toast'
@@ -356,18 +357,21 @@ const NicoTagListPage: FC<Props> = ({ user }) => {
dark:bg-rose-950/30">
<td colSpan={editable ? 5 : 4} className="p-3">
<div className="space-y-2">
<PostFormTagsArea
<label htmlFor={`nico-links-${ tag.id }`}
className="block text-sm font-semibold">
{label ('nico.tagsToLink')}
</label>
<TextArea
id={`nico-links-${ tag.id }`}
label={label ('nico.tagsToLink')}
aria-label={message ('nico.tagsToLink').text}
tags={rawTags[tag.id] ?? ''}
errors={errors}
value={rawTags[tag.id] ?? ''}
invalid={(errorsByTagId[tag.id] ?? []).length > 0}
className="min-h-24 resize-y"
placeholder={message ('nico.tagsPlaceholder').text}
lang={message ('nico.tagsPlaceholder').language}
setTags={value => setRawTags (rawTags => ({
onChange={e => setRawTags (rawTags => ({
...rawTags,
[tag.id]: value}))}/>
[tag.id]: e.target.value}))}/>
<FieldError messages={errors}/>
<div className="flex justify-end gap-2">
<button
type="button"
+9 -2
ファイルの表示
@@ -35,6 +35,10 @@ const renderPage = () =>
describe ('TagDetailPage', () => {
beforeEach (() => {
vi.clearAllMocks ()
tagsApi.fetchTag.mockReset ()
api.apiPut.mockReset ()
api.isApiError.mockReset ()
toastApi.toast.mockReset ()
api.isApiError.mockReturnValue (false)
})
@@ -51,8 +55,11 @@ describe ('TagDetailPage', () => {
})
it ('submits edited tag fields', async () => {
tagsApi.fetchTag.mockResolvedValueOnce (buildTag ({ id: 7, name: 'old' }))
api.apiPut.mockResolvedValueOnce (buildTag ({ id: 7, name: 'new', aliases: ['alias'] }))
const updatedTag = buildTag ({ id: 7, name: 'new', aliases: ['alias'] })
tagsApi.fetchTag
.mockResolvedValue (updatedTag)
.mockResolvedValueOnce (buildTag ({ id: 7, name: 'old' }))
api.apiPut.mockResolvedValueOnce (updatedTag)
renderPage ()