このコミットが含まれているのは:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { columnsForCounts, countColumns, countsCSV, countsDefinitionParams,
|
||||
import { columnsForCounts, countColumns, countsCSV, countsDefinitionParams, countsResourcePath,
|
||||
countsRequestParams, normaliseCountsParams, selectedCountChildren } from '@/lib/counts'
|
||||
import { countsErrorMessage, countsConditionValue, countsNestedIsScalar, countsNestedScalarText }
|
||||
from '@/lib/countsDisplay'
|
||||
@@ -14,6 +14,30 @@ import type { CountsCondition } from '@/lib/countsDefinitions'
|
||||
|
||||
|
||||
describe ('Counts URL contract', () => {
|
||||
it ('distinguishes HTTP, network and internal failures without server details', () => {
|
||||
const http = { isAxiosError: true, response: { status: 503, data: '<private SQL>' } }
|
||||
expect (countsErrorMessage (http, 'ja').text).toContain ('HTTP 503')
|
||||
expect (countsErrorMessage (http, 'ja').text).not.toContain ('private SQL')
|
||||
expect (countsErrorMessage ({ isAxiosError: true }, 'ja').text)
|
||||
.toBe ('APIサーバーとの通信に失敗しました。')
|
||||
expect (countsErrorMessage (new Error ('private stack'), 'ja').text)
|
||||
.toBe ('内部処理で問題が発生しました。')
|
||||
})
|
||||
it ('explains a totals timeout using the server time limit', () => {
|
||||
const failure = { isAxiosError: true, response: { status: 503,
|
||||
data: { error: 'counts_query_timeout', stage: 'totals', timeout_seconds: 30 } } }
|
||||
const text = countsErrorMessage (failure, 'ja').text
|
||||
expect (text).toContain ('集計処理が制限時間を超えました。')
|
||||
expect (text).toContain ('小計・総計の算出が30秒以内に完了しませんでした。')
|
||||
expect (text).toContain ('集計期間や行・列の項目を減らして')
|
||||
expect (countsErrorMessage (failure, 'ain-Kana').language).toBe ('ja')
|
||||
})
|
||||
it ('uses hyphenated page paths for internal resource identifiers', () => {
|
||||
expect (countsResourcePath ('nico_tags')).toBe ('/counts/nico-tags')
|
||||
expect (countsResourcePath ('tag_descriptions')).toBe ('/counts/tag-descriptions')
|
||||
expect (countsResourcePath ('tag_groups')).toBe ('/counts/tag-groups')
|
||||
expect (countsResourcePath ('posts')).toBe ('/counts/posts')
|
||||
})
|
||||
it ('defaults to latest summary, limit 100, and no axes', () => {
|
||||
const params = normaliseCountsParams ('posts', new URLSearchParams (), postDefinition)
|
||||
expect (params.get ('definition')).toBe ('latest')
|
||||
|
||||
@@ -12,6 +12,10 @@ export const countsMaskedValue = '__counts_private__'
|
||||
|
||||
export type CountsResource = 'posts' | 'tags' | 'nico_tags'
|
||||
| 'tag_descriptions' | 'tag_groups' | 'materials'
|
||||
|
||||
export const countsResourcePath = (resource: CountsResource): string =>
|
||||
`/counts/${resource.replaceAll ('_', '-')}`
|
||||
|
||||
export type CountsName = TagNameSnapshot & {
|
||||
languageCode: string; languageName?: string | null; scriptName?: string | null }
|
||||
export type CountsTag = {
|
||||
|
||||
@@ -17,7 +17,8 @@ import type { LocaleCode } from '@/lib/locales'
|
||||
export const countsErrorMessage = (failure: unknown, locale: LocaleCode) => {
|
||||
const response = isApiError (failure) ? failure.response : null
|
||||
const body = (response?.data ?? failure) as {
|
||||
error?: unknown; max_axes?: unknown; maxAxes?: unknown
|
||||
error?: unknown; stage?: unknown; timeout_seconds?: unknown; timeoutSeconds?: unknown
|
||||
max_axes?: unknown; maxAxes?: unknown
|
||||
max_columns?: unknown; maxColumns?: unknown } | null
|
||||
const code = body != null && typeof body === 'object' ? body.error : null
|
||||
const message = (key: string, values?: Record<string, string | number>) =>
|
||||
@@ -31,6 +32,11 @@ export const countsErrorMessage = (failure: unknown, locale: LocaleCode) => {
|
||||
max: maximum })
|
||||
}
|
||||
if (code === 'private_condition') return message ('privateCondition')
|
||||
if (code === 'counts_query_timeout' && body?.stage === 'totals')
|
||||
{
|
||||
const seconds = body.timeoutSeconds ?? body.timeout_seconds
|
||||
if (typeof seconds === 'number') return message ('errors.totalsTimeout', { seconds })
|
||||
}
|
||||
if (code === 'counts_query_timeout' || code === 'generation_timeout')
|
||||
return message ('errors.queryTimeout')
|
||||
if (code === 'export_worker_unavailable' || code === 'worker_unavailable')
|
||||
@@ -40,7 +46,9 @@ export const countsErrorMessage = (failure: unknown, locale: LocaleCode) => {
|
||||
if (response?.status === 401) return message ('errors.authentication')
|
||||
if (response?.status === 403) return message ('errors.permission')
|
||||
if (response?.status === 400 || response?.status === 422) return message ('errors.input')
|
||||
return message ('common:fetchError')
|
||||
if (response != null) return message ('errors.http', { status: response.status })
|
||||
if (isApiError (failure)) return message ('errors.network')
|
||||
return message ('errors.internal')
|
||||
}
|
||||
|
||||
|
||||
@@ -158,7 +166,7 @@ export const countsLogicalName = (
|
||||
if (value == null) return { text: '' }
|
||||
if (field.startsWith ('aliases_') && Array.isArray (value))
|
||||
return { text: value.map (entry => typeof entry === 'string' ? entry
|
||||
: String (entry?.name ?? '')).join ('\n') }
|
||||
: String (entry?.name ?? '')).join (' ') }
|
||||
if (field === 'sections')
|
||||
return { text: (countsSectionRanges (value, locale) ?? []).join (
|
||||
translateUIMessage ('sections.separator', 'counts', locale).text) }
|
||||
@@ -528,6 +536,8 @@ export const countsSummaryValue = (
|
||||
const logical = data.axisLabels?.[`a${index}`]?.[String (value)]
|
||||
if (axis.field === 'user') return message ('actorWithId', {
|
||||
name: logical || message ('notRegistered'), id: value })
|
||||
if (axis.field === 'parent_tag_ids')
|
||||
return String (value) === '' ? '' : logical || message ('notRegistered')
|
||||
if (logical != null) return logical
|
||||
if (axis.field === 'tags')
|
||||
return message ('notRegistered')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isCountsState, selectedCountFields } from '@/lib/counts'
|
||||
import { countsResourcePath, isCountsState, selectedCountFields } from '@/lib/counts'
|
||||
import { translateUIMessage } from '@/lib/i18n'
|
||||
|
||||
import type { CountField, CountsResource, CountsResult } from '@/lib/counts'
|
||||
@@ -90,5 +90,5 @@ export const countsSummaryDrill = (
|
||||
next.set ('before',
|
||||
(before != null && Date.parse (before) < Date.parse (last)) ? before : last)
|
||||
}
|
||||
return `/counts/${resource}?${next}`
|
||||
return `${countsResourcePath (resource)}?${next}`
|
||||
}
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする