mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-11-01 21:26:08 +08:00
fix: 修复 Codex 审查发现的两处回归
- Bangumi / BangumiUpdate 的 9 个可空字段恢复为必填(default=...),只传部分字段的更新与订阅请求重新返回 422,不再用占位值改写规则和保存路径 - ab-menu 补上 role=menu、打开时把焦点移到菜单、aria-activedescendant 指向高亮项,关闭后焦点回到触发器
This commit is contained in:
@@ -10,32 +10,30 @@ class Bangumi(SQLModel, table=True):
|
||||
official_title: str = Field(
|
||||
default="official_title", alias="official_title", title="番剧中文名"
|
||||
)
|
||||
year: Optional[str] = Field(default=None, alias="year", title="番剧年份")
|
||||
year: Optional[str] = Field(default=..., alias="year", title="番剧年份")
|
||||
title_raw: str = Field(
|
||||
default="title_raw", alias="title_raw", title="番剧原名", index=True
|
||||
)
|
||||
season: int = Field(default=1, alias="season", title="番剧季度")
|
||||
season_raw: Optional[str] = Field(
|
||||
default=None, alias="season_raw", title="番剧季度原名"
|
||||
default=..., alias="season_raw", title="番剧季度原名"
|
||||
)
|
||||
group_name: Optional[str] = Field(default=None, alias="group_name", title="字幕组")
|
||||
dpi: Optional[str] = Field(default=None, alias="dpi", title="分辨率")
|
||||
source: Optional[str] = Field(default=None, alias="source", title="来源")
|
||||
subtitle: Optional[str] = Field(default=None, alias="subtitle", title="字幕")
|
||||
group_name: Optional[str] = Field(default=..., alias="group_name", title="字幕组")
|
||||
dpi: Optional[str] = Field(default=..., alias="dpi", title="分辨率")
|
||||
source: Optional[str] = Field(default=..., alias="source", title="来源")
|
||||
subtitle: Optional[str] = Field(default=..., alias="subtitle", title="字幕")
|
||||
eps_collect: bool = Field(default=False, alias="eps_collect", title="是否已收集")
|
||||
episode_offset: int = Field(default=0, alias="episode_offset", title="集数偏移量")
|
||||
season_offset: int = Field(default=0, alias="season_offset", title="季度偏移量")
|
||||
filter: str = Field(default="720,\\d+-\\d+", alias="filter", title="番剧过滤器")
|
||||
rss_link: str = Field(default="", alias="rss_link", title="番剧RSS链接")
|
||||
poster_link: Optional[str] = Field(
|
||||
default=None, alias="poster_link", title="番剧海报链接"
|
||||
default=..., alias="poster_link", title="番剧海报链接"
|
||||
)
|
||||
added: bool = Field(default=False, alias="added", title="是否已添加")
|
||||
rule_name: Optional[str] = Field(
|
||||
default=None, alias="rule_name", title="番剧规则名"
|
||||
)
|
||||
rule_name: Optional[str] = Field(default=..., alias="rule_name", title="番剧规则名")
|
||||
save_path: Optional[str] = Field(
|
||||
default=None, alias="save_path", title="番剧保存路径"
|
||||
default=..., alias="save_path", title="番剧保存路径"
|
||||
)
|
||||
deleted: bool = Field(
|
||||
default=False, alias="deleted", title="是否已删除", index=True
|
||||
@@ -81,30 +79,28 @@ class BangumiUpdate(SQLModel):
|
||||
official_title: str = Field(
|
||||
default="official_title", alias="official_title", title="番剧中文名"
|
||||
)
|
||||
year: Optional[str] = Field(default=None, alias="year", title="番剧年份")
|
||||
year: Optional[str] = Field(default=..., alias="year", title="番剧年份")
|
||||
title_raw: str = Field(default="title_raw", alias="title_raw", title="番剧原名")
|
||||
season: int = Field(default=1, alias="season", title="番剧季度")
|
||||
season_raw: Optional[str] = Field(
|
||||
default=None, alias="season_raw", title="番剧季度原名"
|
||||
default=..., alias="season_raw", title="番剧季度原名"
|
||||
)
|
||||
group_name: Optional[str] = Field(default=None, alias="group_name", title="字幕组")
|
||||
dpi: Optional[str] = Field(default=None, alias="dpi", title="分辨率")
|
||||
source: Optional[str] = Field(default=None, alias="source", title="来源")
|
||||
subtitle: Optional[str] = Field(default=None, alias="subtitle", title="字幕")
|
||||
group_name: Optional[str] = Field(default=..., alias="group_name", title="字幕组")
|
||||
dpi: Optional[str] = Field(default=..., alias="dpi", title="分辨率")
|
||||
source: Optional[str] = Field(default=..., alias="source", title="来源")
|
||||
subtitle: Optional[str] = Field(default=..., alias="subtitle", title="字幕")
|
||||
eps_collect: bool = Field(default=False, alias="eps_collect", title="是否已收集")
|
||||
episode_offset: int = Field(default=0, alias="episode_offset", title="集数偏移量")
|
||||
season_offset: int = Field(default=0, alias="season_offset", title="季度偏移量")
|
||||
filter: str = Field(default="720,\\d+-\\d+", alias="filter", title="番剧过滤器")
|
||||
rss_link: str = Field(default="", alias="rss_link", title="番剧RSS链接")
|
||||
poster_link: Optional[str] = Field(
|
||||
default=None, alias="poster_link", title="番剧海报链接"
|
||||
default=..., alias="poster_link", title="番剧海报链接"
|
||||
)
|
||||
added: bool = Field(default=False, alias="added", title="是否已添加")
|
||||
rule_name: Optional[str] = Field(
|
||||
default=None, alias="rule_name", title="番剧规则名"
|
||||
)
|
||||
rule_name: Optional[str] = Field(default=..., alias="rule_name", title="番剧规则名")
|
||||
save_path: Optional[str] = Field(
|
||||
default=None, alias="save_path", title="番剧保存路径"
|
||||
default=..., alias="save_path", title="番剧保存路径"
|
||||
)
|
||||
downloader_id: Optional[str] = Field(
|
||||
default=None, alias="downloader_id", title="下载器实例"
|
||||
|
||||
@@ -152,6 +152,18 @@ class TestUpdateBangumi:
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_update_partial_body_rejected(self, authed_client):
|
||||
"""只传部分字段时返回 422,不能用占位值改写规则与保存路径。"""
|
||||
with patch("module.api.bangumi.TorrentManager") as MockManager:
|
||||
mock_mgr = MockManager.return_value
|
||||
mock_mgr.update_rule = AsyncMock()
|
||||
response = authed_client.patch(
|
||||
"/api/v1/bangumi/update/1", json={"year": "2025"}
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
mock_mgr.update_rule.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DELETE endpoints
|
||||
|
||||
@@ -60,6 +60,28 @@ describe('ab-menu', () => {
|
||||
expect(trigger.attributes('aria-expanded')).toBe('true');
|
||||
});
|
||||
|
||||
it('should move focus into the menu and announce the highlighted item when opened', async () => {
|
||||
const wrapper = mount(AbMenu, {
|
||||
props: { items: items() },
|
||||
slots: { trigger: '<button type="button">Actions</button>' },
|
||||
attachTo: document.body,
|
||||
});
|
||||
const trigger = wrapper.find('button');
|
||||
(trigger.element as HTMLElement).focus();
|
||||
await trigger.trigger('click');
|
||||
await flushPromises();
|
||||
const menu = document.querySelector<HTMLElement>('[role="menu"]')!;
|
||||
expect(document.activeElement).toBe(menu);
|
||||
|
||||
menu.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
||||
await flushPromises();
|
||||
expect(menu.getAttribute('aria-activedescendant')).toBe(menuItems()[0].id);
|
||||
|
||||
menu.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
||||
await flushPromises();
|
||||
expect(document.activeElement).toBe(trigger.element);
|
||||
});
|
||||
|
||||
it('should mark danger items with the danger class', async () => {
|
||||
const wrapper = mount(AbMenu, {
|
||||
props: { items: items() },
|
||||
|
||||
@@ -14,8 +14,9 @@ export interface AbMenuItem {
|
||||
}
|
||||
|
||||
// 下拉菜单:naive-ui NDropdown 负责定位与键盘导航(方向键/回车/Esc);
|
||||
// 触发器放在 #trigger 插槽。NDropdown 不输出 aria,menuitem 角色与
|
||||
// 触发器的 aria-haspopup/aria-expanded 在这里补上。
|
||||
// 触发器放在 #trigger 插槽。NDropdown 不输出 aria,也不移动焦点:这里补上
|
||||
// menu/menuitem 角色、触发器的 aria-haspopup/aria-expanded,打开时把焦点
|
||||
// 移到菜单容器,并用 aria-activedescendant 指向高亮项,读屏才能读出当前项。
|
||||
const { items, align = 'left' } = defineProps<{
|
||||
items: AbMenuItem[];
|
||||
/** 菜单相对触发器的对齐边 */
|
||||
@@ -27,6 +28,8 @@ const emit = defineEmits<{ select: [item: AbMenuItem] }>();
|
||||
const slots = defineSlots<{ trigger: () => VNode[] }>();
|
||||
|
||||
const open = ref(false);
|
||||
const listId = useId();
|
||||
const activeId = ref<string>();
|
||||
|
||||
const options = computed<DropdownOption[]>(() =>
|
||||
items.map((item, index) => ({
|
||||
@@ -35,7 +38,9 @@ const options = computed<DropdownOption[]>(() =>
|
||||
disabled: item.disabled,
|
||||
icon: item.icon && (() => h(item.icon!, { size: 15 })),
|
||||
props: {
|
||||
id: `${listId}-${item.key ?? index}`,
|
||||
role: 'menuitem',
|
||||
'aria-disabled': item.disabled || undefined,
|
||||
class: item.danger ? 'ab-menu-item--danger' : undefined,
|
||||
},
|
||||
}))
|
||||
@@ -48,6 +53,38 @@ function Trigger() {
|
||||
});
|
||||
}
|
||||
|
||||
// NDropdown 不暴露高亮项,只能从它的 --pending 类读出
|
||||
let observer: MutationObserver | undefined;
|
||||
let opener: HTMLElement | null = null;
|
||||
|
||||
watch(open, async (isOpen) => {
|
||||
observer?.disconnect();
|
||||
activeId.value = undefined;
|
||||
const list = document.getElementById(listId);
|
||||
if (!isOpen) {
|
||||
// 焦点留在正在关闭的菜单里或已丢到 body 时,还给触发器
|
||||
const focused = document.activeElement;
|
||||
if (focused === document.body || list?.contains(focused)) opener?.focus();
|
||||
return;
|
||||
}
|
||||
opener = document.activeElement as HTMLElement | null;
|
||||
await nextTick();
|
||||
const menu = document.getElementById(listId);
|
||||
if (!menu) return;
|
||||
menu.focus({ preventScroll: true });
|
||||
observer = new MutationObserver(() => {
|
||||
activeId.value =
|
||||
menu.querySelector('.n-dropdown-option-body--pending')?.id || undefined;
|
||||
});
|
||||
observer.observe(menu, {
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => observer?.disconnect());
|
||||
|
||||
function onSelect(key: string | number) {
|
||||
const item = items.find((entry, index) => (entry.key ?? index) === key);
|
||||
if (!item || item.disabled) return;
|
||||
@@ -62,7 +99,15 @@ function onSelect(key: string | number) {
|
||||
:show="open"
|
||||
:options="options"
|
||||
:placement="align === 'right' ? 'bottom-end' : 'bottom-start'"
|
||||
:menu-props="() => ({ class: 'ab-menu-list' })"
|
||||
:menu-props="
|
||||
() => ({
|
||||
'id': listId,
|
||||
'class': 'ab-menu-list',
|
||||
'role': 'menu',
|
||||
'tabindex': -1,
|
||||
'aria-activedescendant': activeId,
|
||||
})
|
||||
"
|
||||
@update:show="open = $event"
|
||||
@select="onSelect"
|
||||
>
|
||||
@@ -82,6 +127,11 @@ function onSelect(key: string | number) {
|
||||
.ab-menu-list {
|
||||
min-width: 180px;
|
||||
|
||||
// 焦点在容器上只为 aria-activedescendant,高亮项已经标出当前项
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// 触屏放大命中区域,与旧菜单一致
|
||||
@include forTouch {
|
||||
.n-dropdown-option-body {
|
||||
|
||||
Reference in New Issue
Block a user