タグ “廃止” 追加 (#378) #379
@@ -7,7 +7,8 @@ class Post < ApplicationRecord
|
||||
has_many :active_post_tags, -> { kept }, class_name: 'PostTag', inverse_of: :post
|
||||
has_many :post_tags_with_discarded, -> { with_discarded }, class_name: 'PostTag'
|
||||
has_many :tags, through: :active_post_tags
|
||||
has_many :active_tags, -> { where(deprecated_at: nil) }, through: :active_post_tags, source: :tag
|
||||
has_many :active_tags, -> { where(tags: { deprecated_at: nil }) },
|
||||
through: :active_post_tags, source: :tag
|
||||
|
||||
has_many :user_post_views, dependent: :delete_all
|
||||
has_many :post_similarities, dependent: :delete_all
|
||||
|
||||
@@ -897,6 +897,37 @@ RSpec.describe 'Gekanator learning API', type: :request do
|
||||
end
|
||||
|
||||
describe 'GET /gekanator/questions' do
|
||||
it 'omits questions for deprecated tags' do
|
||||
active_tag = Tag.create!(name: 'active_question_tag', category: :general)
|
||||
deprecated_tag = Tag.create!(
|
||||
name: 'deprecated_question_tag',
|
||||
category: :general,
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
|
||||
[active_tag, deprecated_tag].each do |question_tag|
|
||||
GekanatorQuestion.create!(
|
||||
text: "#{ question_tag.name }?",
|
||||
kind: 'tag',
|
||||
source: 'admin_curated',
|
||||
status: 'accepted',
|
||||
priority_weight: 1.0,
|
||||
condition: {
|
||||
type: 'tag',
|
||||
key: "#{ question_tag.category }:#{ question_tag.name }"
|
||||
},
|
||||
created_by: admin
|
||||
)
|
||||
end
|
||||
|
||||
get '/gekanator/questions'
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
question_ids = json.fetch('questions').map { |question| question.fetch('id') }
|
||||
expect(question_ids).to include('tag:general:active_question_tag')
|
||||
expect(question_ids).not_to include('tag:general:deprecated_question_tag')
|
||||
end
|
||||
|
||||
it 'returns accepted questions only and includes example_answers for post_similarity questions' do
|
||||
sign_in_as admin
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
require 'rails_helper'
|
||||
|
||||
|
||||
RSpec.describe 'Gekanator posts API', type: :request do
|
||||
describe 'GET /gekanator/posts' do
|
||||
it 'omits deprecated tags and returns the stored similarity cosine' do
|
||||
active_tag = Tag.create!(name: 'active tag', category: :general)
|
||||
deprecated_tag = Tag.create!(
|
||||
name: 'deprecated tag',
|
||||
category: :general,
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
post_record = Post.create!(title: 'source', url: 'https://example.com/source')
|
||||
target_post = Post.create!(title: 'target', url: 'https://example.com/target')
|
||||
|
||||
PostTag.create!(post: post_record, tag: active_tag)
|
||||
PostTag.create!(post: post_record, tag: deprecated_tag)
|
||||
PostTag.create!(post: target_post, tag: deprecated_tag)
|
||||
PostSimilarity.create!(post: post_record, target_post:, cos: 0.375)
|
||||
|
||||
get '/gekanator/posts'
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
post_json = json.fetch('posts').find { |post| post.fetch('id') == post_record.id }
|
||||
expect(post_json.fetch('tags').map { |tag| tag.fetch('name') }).to eq(['active tag'])
|
||||
expect(post_json.fetch('post_similarity_edges')).to contain_exactly(
|
||||
'target_post_id' => target_post.id,
|
||||
'cos' => 0.375
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -517,6 +517,24 @@ RSpec.describe 'Posts API', type: :request do
|
||||
expect([true, false]).to include(json['viewed'])
|
||||
end
|
||||
|
||||
it 'omits deprecated tags' do
|
||||
deprecated_tag = Tag.create!(
|
||||
name: 'deprecated_post_tag',
|
||||
category: :general,
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
PostTag.create!(post: post_record, tag: deprecated_tag)
|
||||
|
||||
request
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
tag_names = json.fetch('tags').flat_map { |node|
|
||||
[node.fetch('name')] + node.fetch('children').map { |child| child.fetch('name') }
|
||||
}
|
||||
expect(tag_names).to include('spec_tag')
|
||||
expect(tag_names).not_to include('deprecated_post_tag')
|
||||
end
|
||||
|
||||
context 'when post has parent, child, and sibling posts' do
|
||||
let!(:parent_post) do
|
||||
create_parent_post!(
|
||||
@@ -697,6 +715,52 @@ RSpec.describe 'Posts API', type: :request do
|
||||
expect(names).not_to include('manko')
|
||||
end
|
||||
|
||||
it 'rejects a deprecated tag specified directly' do
|
||||
Tag.create!(
|
||||
name: 'deprecated_direct_tag',
|
||||
category: :general,
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
sign_in_as(member)
|
||||
|
||||
post '/posts', params: post_write_params(
|
||||
title: 'new post',
|
||||
url: 'https://example.com/deprecated-direct-tag',
|
||||
tags: 'deprecated_direct_tag',
|
||||
thumbnail: dummy_upload
|
||||
)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'tags' => ['廃止済みタグは付与できません.']
|
||||
)
|
||||
end
|
||||
|
||||
it 'expands through deprecated parent tags and saves active ancestors' do
|
||||
child = Tag.create!(name: 'active_child', category: :general)
|
||||
deprecated_parent = Tag.create!(
|
||||
name: 'deprecated_parent',
|
||||
category: :general,
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
active_grandparent = Tag.create!(name: 'active_grandparent', category: :general)
|
||||
TagImplication.create!(tag: child, parent_tag: deprecated_parent)
|
||||
TagImplication.create!(tag: deprecated_parent, parent_tag: active_grandparent)
|
||||
sign_in_as(member)
|
||||
|
||||
post '/posts', params: post_write_params(
|
||||
title: 'expanded post',
|
||||
url: 'https://example.com/expanded-deprecated-parent',
|
||||
tags: 'active_child',
|
||||
thumbnail: dummy_upload
|
||||
)
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
saved_names = Post.find(json.fetch('id')).tags.map(&:name)
|
||||
expect(saved_names).to include('active_child', 'active_grandparent')
|
||||
expect(saved_names).not_to include('deprecated_parent')
|
||||
end
|
||||
|
||||
context "when nico tag already exists in tags" do
|
||||
before do
|
||||
Tag.find_undiscard_or_create_by!(
|
||||
@@ -930,6 +994,26 @@ RSpec.describe 'Posts API', type: :request do
|
||||
expect(names).to include('spec_tag_2')
|
||||
end
|
||||
|
||||
it 'rejects a deprecated tag specified directly' do
|
||||
Tag.create!(
|
||||
name: 'deprecated_update_tag',
|
||||
category: :general,
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
sign_in_as(member)
|
||||
|
||||
put "/posts/#{ post_record.id }", params: post_update_params(
|
||||
post_record,
|
||||
title: 'updated title',
|
||||
tags: 'deprecated_update_tag'
|
||||
)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'tags' => ['廃止済みタグは付与できません.']
|
||||
)
|
||||
end
|
||||
|
||||
context "when nico tag already exists in tags" do
|
||||
before do
|
||||
Tag.find_undiscard_or_create_by!(
|
||||
|
||||
@@ -89,6 +89,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
|
||||
category: 'general',
|
||||
aliases: '',
|
||||
parent_tags: '',
|
||||
deprecated: '0',
|
||||
}
|
||||
}
|
||||
.to change(TagVersion, :count).by(2)
|
||||
@@ -123,6 +124,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
|
||||
category: 'meme',
|
||||
aliases: '',
|
||||
parent_tags: '',
|
||||
deprecated: '0',
|
||||
}
|
||||
}
|
||||
.to change(TagVersion, :count).by(2)
|
||||
@@ -149,6 +151,7 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
|
||||
category: 'general',
|
||||
aliases: 'put_tag_alias_only_alias',
|
||||
parent_tags: '',
|
||||
deprecated: '0',
|
||||
}
|
||||
}
|
||||
.to change(TagVersion, :count).by(2)
|
||||
|
||||
@@ -76,6 +76,27 @@ RSpec.describe 'Tags API', type: :request do
|
||||
expect(response_tags.first['id']).to eq(meme.id)
|
||||
end
|
||||
|
||||
it 'filters tags by deprecated state' do
|
||||
deprecated_tag = Tag.create!(
|
||||
name: 'deprecated_filter',
|
||||
category: :general,
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
active_tag = Tag.create!(name: 'active_filter', category: :general)
|
||||
|
||||
get '/tags', params: { name: '_filter', deprecated: '1' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response_names).to include(deprecated_tag.name)
|
||||
expect(response_names).not_to include(active_tag.name)
|
||||
|
||||
get '/tags', params: { name: '_filter', deprecated: '0' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response_names).to include(active_tag.name)
|
||||
expect(response_names).not_to include(deprecated_tag.name)
|
||||
end
|
||||
|
||||
it 'filters tags by post_count range' do
|
||||
low = Tag.create!(tag_name: TagName.create!(name: 'pc_low'), category: :general)
|
||||
mid = Tag.create!(tag_name: TagName.create!(name: 'pc_mid'), category: :general)
|
||||
@@ -301,6 +322,21 @@ RSpec.describe 'Tags API', type: :request do
|
||||
expect(t['matched_alias']).to eq('unko')
|
||||
expect(json.map { |x| x['name'] }).not_to include('unknown')
|
||||
end
|
||||
|
||||
it 'omits deprecated tags' do
|
||||
deprecated_tag = Tag.create!(
|
||||
name: 'spec_deprecated',
|
||||
category: :general,
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
deprecated_tag.update_columns(post_count: 1)
|
||||
|
||||
get '/tags/autocomplete', params: { q: 'spec_', present: '0' }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.map { |item| item.fetch('name') }).to include('spec_tag')
|
||||
expect(json.map { |item| item.fetch('name') }).not_to include('spec_deprecated')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /tags/name/:name' do
|
||||
@@ -585,6 +621,26 @@ RSpec.describe 'Tags API', type: :request do
|
||||
expect(row['has_children']).to eq(true)
|
||||
expect(row['children']).to eq([])
|
||||
end
|
||||
|
||||
it 'passes through deprecated tags when finding children' do
|
||||
deprecated_middle = Tag.create!(
|
||||
name: 'depth_deprecated_middle',
|
||||
category: :character,
|
||||
deprecated_at: Time.current
|
||||
)
|
||||
visible_descendant = Tag.create!(
|
||||
name: 'depth_visible_descendant',
|
||||
category: :material
|
||||
)
|
||||
TagImplication.create!(parent_tag: root_material, tag: deprecated_middle)
|
||||
TagImplication.create!(parent_tag: deprecated_middle, tag: visible_descendant)
|
||||
|
||||
get '/tags/with-depth', params: { parent: root_material.id }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(json.map { |item| item.fetch('name') }).to eq(['depth_visible_descendant'])
|
||||
expect(json.map { |item| item.fetch('name') }).not_to include('depth_deprecated_middle')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /tags/name/:name/materials' do
|
||||
@@ -732,6 +788,20 @@ RSpec.describe 'Tags API', type: :request do
|
||||
expect(tag.category).to eq('general')
|
||||
end
|
||||
|
||||
it 'deprecated がなければ 422 を返す' do
|
||||
put "/tags/#{ tag.id }", params: {
|
||||
name: 'new',
|
||||
category: 'general',
|
||||
aliases: '',
|
||||
parent_tags: '',
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(json.fetch('errors')).to include(
|
||||
'deprecated' => ['廃止状態は必須です.']
|
||||
)
|
||||
end
|
||||
|
||||
it 'name, category, aliases, parent tags をまとめて更新できる' do
|
||||
old_parent = Tag.create!(
|
||||
tag_name: TagName.create!(name: 'put_old_parent'),
|
||||
@@ -749,6 +819,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
category: 'meme',
|
||||
aliases: 'put_alias_a put_alias_b put_alias_a',
|
||||
parent_tags: 'put_kept_parent put_new_parent',
|
||||
deprecated: '0',
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
@@ -793,6 +864,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
category: 'general',
|
||||
aliases: 'spec_tag put_alias_self_test',
|
||||
parent_tags: '',
|
||||
deprecated: '0',
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
@@ -810,6 +882,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
category: 'general',
|
||||
aliases: 'unko',
|
||||
parent_tags: 'spec_tag',
|
||||
deprecated: '0',
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
@@ -825,6 +898,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
category: 'meta',
|
||||
aliases: '',
|
||||
parent_tags: '',
|
||||
deprecated: '0',
|
||||
}
|
||||
}.to change(TagVersion, :count).by(2)
|
||||
|
||||
@@ -860,6 +934,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
category: 'general',
|
||||
aliases: 'unko',
|
||||
parent_tags: new_parent.name,
|
||||
deprecated: '0',
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
@@ -875,6 +950,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
category: 'nico',
|
||||
aliases: '',
|
||||
parent_tags: '',
|
||||
deprecated: '0',
|
||||
}
|
||||
}.not_to change(TagVersion, :count)
|
||||
|
||||
@@ -896,6 +972,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
category: 'nico',
|
||||
aliases: '',
|
||||
parent_tags: '',
|
||||
deprecated: '0',
|
||||
}
|
||||
}.not_to change(NicoTagVersion, :count)
|
||||
|
||||
@@ -916,6 +993,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
category: old_category,
|
||||
aliases: '',
|
||||
parent_tags: '',
|
||||
deprecated: '0',
|
||||
}
|
||||
}.not_to change(TagVersion, :count)
|
||||
|
||||
@@ -946,6 +1024,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
category: 'meme',
|
||||
aliases: 'unko',
|
||||
parent_tags: '',
|
||||
deprecated: '0',
|
||||
}
|
||||
}
|
||||
.to change(TagVersion, :count).by(2)
|
||||
@@ -981,6 +1060,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
category: 'general',
|
||||
aliases: 'unko put_stolen_alias',
|
||||
parent_tags: '',
|
||||
deprecated: '0',
|
||||
}
|
||||
}
|
||||
.to change { tag.reload.tag_versions.count }.by(2)
|
||||
@@ -1015,6 +1095,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
category: 'general',
|
||||
aliases: 'unko',
|
||||
parent_tags: child.name,
|
||||
deprecated: '0',
|
||||
}
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
@@ -1036,6 +1117,7 @@ RSpec.describe 'Tags API', type: :request do
|
||||
category: 'general',
|
||||
aliases: 'unko',
|
||||
parent_tags: '',
|
||||
deprecated: '0',
|
||||
}
|
||||
}
|
||||
.to change(TagVersion, :count).by(2)
|
||||
|
||||
@@ -4,11 +4,12 @@ require 'rails_helper'
|
||||
RSpec.describe 'post_similarity:calc' do
|
||||
include RakeTaskHelper
|
||||
|
||||
it 'calls Similarity::Calc with Post and :tags' do
|
||||
it 'calculates similarities from active tags only' do
|
||||
# 必要最低限のデータ
|
||||
t1 = Tag.create!(name: "t1")
|
||||
t2 = Tag.create!(name: "t2")
|
||||
t3 = Tag.create!(name: "t3")
|
||||
deprecated_tag = Tag.create!(name: 'deprecated', deprecated_at: Time.current)
|
||||
|
||||
p1 = Post.create!(url: "https://example.com/1")
|
||||
p2 = Post.create!(url: "https://example.com/2")
|
||||
@@ -22,6 +23,8 @@ RSpec.describe 'post_similarity:calc' do
|
||||
PostTag.create!(post: p2, tag: t3)
|
||||
|
||||
PostTag.create!(post: p3, tag: t3)
|
||||
PostTag.create!(post: p1, tag: deprecated_tag)
|
||||
PostTag.create!(post: p2, tag: deprecated_tag)
|
||||
|
||||
expect { run_rake_task("post_similarity:calc") }
|
||||
.to change { PostSimilarity.count }.from(0)
|
||||
@@ -29,6 +32,6 @@ RSpec.describe 'post_similarity:calc' do
|
||||
ps = PostSimilarity.find_by!(post_id: p1.id, target_post_id: p2.id)
|
||||
ps_rev = PostSimilarity.find_by!(post_id: p2.id, target_post_id: p1.id)
|
||||
expect(ps_rev.cos).to eq(ps.cos)
|
||||
expect(ps.cos).to be_within(0.0001).of(0.5)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ require 'rails_helper'
|
||||
RSpec.describe 'tag_similarity:calc' do
|
||||
include RakeTaskHelper
|
||||
|
||||
it 'calls Similarity::Calc with Tag and :posts' do
|
||||
it 'calculates similarities for active tags only' do
|
||||
# 必要最低限のデータ
|
||||
t1 = Tag.create!(name: "t1")
|
||||
t2 = Tag.create!(name: "t2")
|
||||
t3 = Tag.create!(name: "t3")
|
||||
deprecated_tag = Tag.create!(name: 'deprecated', deprecated_at: Time.current)
|
||||
|
||||
p1 = Post.create!(url: "https://example.com/1")
|
||||
p2 = Post.create!(url: "https://example.com/2")
|
||||
@@ -22,6 +23,7 @@ RSpec.describe 'tag_similarity:calc' do
|
||||
PostTag.create!(post: p2, tag: t3)
|
||||
|
||||
PostTag.create!(post: p3, tag: t3)
|
||||
PostTag.create!(post: p1, tag: deprecated_tag)
|
||||
|
||||
expect { run_rake_task("tag_similarity:calc") }
|
||||
.to change { TagSimilarity.count }.from(0)
|
||||
@@ -29,6 +31,7 @@ RSpec.describe 'tag_similarity:calc' do
|
||||
ps = TagSimilarity.find_by!(tag_id: t1.id, target_tag_id: t2.id)
|
||||
ps_rev = TagSimilarity.find_by!(tag_id: t2.id, target_tag_id: t1.id)
|
||||
expect(ps_rev.cos).to eq(ps.cos)
|
||||
expect(TagSimilarity.where(tag_id: deprecated_tag.id)).to be_empty
|
||||
expect(TagSimilarity.where(target_tag_id: deprecated_tag.id)).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { apiPost } from '@/lib/api'
|
||||
import { apiGet, apiPost } from '@/lib/api'
|
||||
import {
|
||||
buildGekanatorQuestions,
|
||||
expectedAnswerForQuestion,
|
||||
fetchGekanatorPosts,
|
||||
fetchGekanatorQuestions,
|
||||
learnedSemanticSideForPost,
|
||||
questionIdForCondition,
|
||||
restoreGekanatorQuestion,
|
||||
@@ -24,6 +26,7 @@ vi.mock('@/lib/api', () => ({
|
||||
}))
|
||||
|
||||
const mockedApiPost = vi.mocked(apiPost)
|
||||
const mockedApiGet = vi.mocked(apiGet)
|
||||
|
||||
const post = (overrides: Partial<Post> = {}): Post => ({
|
||||
id: 1,
|
||||
@@ -43,6 +46,24 @@ const post = (overrides: Partial<Post> = {}): Post => ({
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('Gekanator API functions', () => {
|
||||
it('returns posts from the Gekanator posts endpoint', async () => {
|
||||
const posts = [post()]
|
||||
mockedApiGet.mockResolvedValueOnce({ posts })
|
||||
|
||||
await expect(fetchGekanatorPosts()).resolves.toEqual(posts)
|
||||
expect(mockedApiGet).toHaveBeenCalledWith('/gekanator/posts')
|
||||
})
|
||||
|
||||
it('returns questions from the Gekanator questions endpoint', async () => {
|
||||
const questions: StoredGekanatorQuestion[] = []
|
||||
mockedApiGet.mockResolvedValueOnce({ questions })
|
||||
|
||||
await expect(fetchGekanatorQuestions()).resolves.toEqual(questions)
|
||||
expect(mockedApiGet).toHaveBeenCalledWith('/gekanator/questions')
|
||||
})
|
||||
})
|
||||
|
||||
describe('expectedAnswerForQuestion', () => {
|
||||
it('returns a direct example answer when present', () => {
|
||||
const question: StoredGekanatorQuestion = {
|
||||
@@ -126,6 +147,7 @@ describe('expectedAnswerForQuestion', () => {
|
||||
postCount: 1,
|
||||
createdAt: '2026-06-10T00:00:00.000Z',
|
||||
updatedAt: '2026-06-10T00:00:00.000Z',
|
||||
deprecatedAt: null,
|
||||
hasWiki: false,
|
||||
hasDeerjikists: false,
|
||||
materialId: null,
|
||||
|
||||
@@ -58,6 +58,20 @@ describe ('tags API functions', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it.each ([
|
||||
[true, '1'],
|
||||
[false, '0'],
|
||||
] as const) ('maps deprecated=%s to %s', async (deprecated, expected) => {
|
||||
api.apiGet.mockResolvedValueOnce ({ tags: [], count: 0 })
|
||||
|
||||
await fetchTags ({ ...baseParams, deprecated })
|
||||
|
||||
expect (api.apiGet).toHaveBeenCalledWith (
|
||||
'/tags',
|
||||
{ params: expect.objectContaining ({ deprecated: expected }) },
|
||||
)
|
||||
})
|
||||
|
||||
it ('returns null when tag fetches fail', async () => {
|
||||
api.apiGet.mockRejectedValueOnce (new Error ('missing'))
|
||||
api.apiGet.mockRejectedValueOnce (new Error ('missing'))
|
||||
|
||||
@@ -14,13 +14,22 @@ vi.mock ('@/lib/tags', () => tagsApi)
|
||||
describe ('TagListPage', () => {
|
||||
it ('loads tags from URL filters and renders the results table', async () => {
|
||||
tagsApi.fetchTags.mockResolvedValueOnce ({
|
||||
tags: [buildTag ({ id: 7, name: '虹夏', category: 'character', postCount: 99 })],
|
||||
tags: [buildTag ({
|
||||
id: 7,
|
||||
name: '虹夏',
|
||||
category: 'character',
|
||||
postCount: 99,
|
||||
deprecatedAt: '2026-06-01T00:00:00.000Z',
|
||||
})],
|
||||
count: 1,
|
||||
})
|
||||
|
||||
renderWithProviders (
|
||||
<TagListPage/>,
|
||||
{ route: '/tags?name=%E8%99%B9&category=character&page=3&post_count_gte=5' },
|
||||
{
|
||||
route:
|
||||
'/tags?name=%E8%99%B9&category=character&page=3&post_count_gte=5&deprecated=1',
|
||||
},
|
||||
)
|
||||
|
||||
await waitFor (() => {
|
||||
@@ -30,6 +39,7 @@ describe ('TagListPage', () => {
|
||||
category: 'character',
|
||||
page: 3,
|
||||
postCountGTE: 5,
|
||||
deprecated: true,
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -38,6 +48,8 @@ describe ('TagListPage', () => {
|
||||
'/tags/7',
|
||||
)
|
||||
expect (screen.getAllByText ('キャラクター').length).toBeGreaterThan (0)
|
||||
expect (screen.getAllByRole ('combobox')[1]).toHaveValue ('1')
|
||||
expect (screen.getAllByText ('廃止')).toHaveLength (2)
|
||||
})
|
||||
|
||||
it ('navigates to a normalized search URL on submit', async () => {
|
||||
@@ -46,7 +58,9 @@ describe ('TagListPage', () => {
|
||||
renderWithProviders (<TagListPage/>, { route: '/tags' })
|
||||
|
||||
fireEvent.change (screen.getByRole ('textbox'), { target: { value: '虹夏' } })
|
||||
fireEvent.change (screen.getByRole ('combobox'), { target: { value: 'character' } })
|
||||
fireEvent.change (screen.getAllByRole ('combobox')[0], {
|
||||
target: { value: 'character' },
|
||||
})
|
||||
fireEvent.submit (screen.getByRole ('button', { name: '検索' }).closest ('form')!)
|
||||
|
||||
await waitFor (() => {
|
||||
|
||||
@@ -27,5 +27,6 @@
|
||||
"@/*": ["*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/test"]
|
||||
}
|
||||
|
||||
新しい課題から参照
ユーザをブロックする