自动化测试
本页给自动化测试提供可视化用例:对照页面上的组件,检查相关属性是否正确。
覆盖范围
| 组件 | aria-label | aria-required | 说明 |
|---|---|---|---|
w-form-item | 已覆盖 | 下发给内部控件 | 根节点会保留自身 aria-label |
w-input | 已覆盖 | 已覆盖 | |
w-textarea | 已覆盖 | 已覆盖 | 与 Input 同一套逻辑 |
w-radio-group | 已覆盖 | 已覆盖 | 写在组的根节点 |
w-checkbox-group | 已覆盖 | 已覆盖 | 写在组的根节点 |
w-color-picker | 已覆盖 | 已覆盖 | 触发器 role=button |
w-rate | 已覆盖 | 已覆盖 | 根节点 role=slider |
w-segmented | 已覆盖 | 已覆盖 | 根节点 role=radiogroup |
w-slider | 已覆盖 | 已覆盖 | 范围根 + 两个手柄显式转发 |
w-input-otp | 已覆盖 | 已覆盖 | 根节点 role=group |
w-tabs | 已覆盖(页签) | 不适用 | tab-pane 的标记落在 role=tab 节点 |
w-popover | 已覆盖(回归锁定) | 不适用 | 不改代码,用例锁住「用户值优先」 |
w-date-picker | 已覆盖 | 已覆盖 | 范围拆分 ::开始 / ::结束 |
w-time-picker | 已覆盖 | 已覆盖 | 范围拆分;aria-required 为 String |
w-upload | 已覆盖 | 不涉及 | 控件落 .w3-upload;文件项落 li |
w-message-box | 已覆盖 | 不涉及 | 面板根 <值>::二次确认框,dialog 不变 |
w-message | 已覆盖 | 不涉及 | 根节点 <值>::消息提示 |
w-notification | 已覆盖 | 不涉及 | 根节点 <值>::通知 |
w-tree | 已覆盖 | 已覆盖 | 根节点回退 form-item;节点标记固定为 树节点::<node-key 值> |
w-cascader | 已覆盖 | 已覆盖 | 区域取控件值;节点标记固定为 级联节点::<value> |
w-table | 已覆盖 | — | 列 → th;行固定 表格行::<row-key>;筛选/列设置浮层有标记 |
w-table-pro | 已覆盖 | — | 透传 w-table,列/行标记自动继承 |
w-table-v2 | 已覆盖 | — | 列 → div[role=columnheader];行固定 表格行::<row-key> |
w-select | 已覆盖 | 已覆盖 | 区域取控件值 + 选项 下拉选项::<value>;input 唯一命中 |
w-select-v2 | 已覆盖 | 已覆盖 | 同 select |
w-dropdown | 已覆盖 | — | 菜单 ul 经 tooltip 落值 |
通用规则
控件自己写了就用自己的;没写才用外面 form-item 的;两边都没写,就不输出这个属性。
| 属性 | 谁说了算 | 出现在哪 |
|---|---|---|
aria-label | 控件自己的 aria-label > form-item 的 aria-label > 不输出 | 控件根节点 |
aria-required | 控件自己的 aria-required(含 "false")> form-item 是否必填 > 不输出 | 控件根节点 |
aria-label
| 组件 | 场景 | 控件上的结果 |
|---|---|---|
w-form-item | 写了 aria-label | 根节点保留 aria-label="表单-患者姓名" |
w-input | 内部没写,外面 form-item 写了 | aria-label="表单-患者姓名" |
w-input | 自己也写了 | 用自己的:aria-label="控件自身" |
w-input | 两边都不写 | 没有 aria-label |
w-textarea | 内部没写,外面 form-item 写了 | aria-label="表单-备注" |
w-radio-group | 在 form-item 内 | aria-label="表单-性别" |
w-radio-group | 不包在 form-item 里 | 仍是默认值 aria-label="radio-group" |
w-checkbox-group | 在 form-item 内 | aria-label="表单-爱好" |
w-tree | 配置了 node-key | div[role=treeitem] 为 树节点::<node-key 值> |
w-tree | 根节点没写,外面 form-item 写了 | div[role=tree] 用 form-item 的值 |
w-cascader | 组件写了 aria-label | 内部 input 拿到该值 |
w-cascader | 选项配置了 value | li[role=menuitem] 为 级联节点::<value> |
颜色选择器(color-picker)
触发器 [role=button]:控件自身 aria-label > form-item 的 aria-label > 默认「颜色选择器」;form-item 用 aria-label 标记时丢弃悬挂的 aria-labelledby(规则 B),用可见 label 文本时保留。
| 编号 | 场景 | 期望 |
|---|---|---|
| L01 | form-item 仅 aria-label | 触发器 aria-label="表单-主题色",无 labelledby |
| L02 | 控件自身 + form-item 都写 | 触发器用自身值 "控件自身" |
| L03 | form-item 用可见 label 文本 | 触发器 aria-labelledby 有值 |
| L04 | 不包 form-item | 保持默认 "颜色选择器" |
| 控制台断言脚本: |
const q = (sel) => document.querySelector(sel)
const t = (sel) => q(sel)?.querySelector('[role="button"]')
console.assert(
t('.case-l-color-01')?.getAttribute('aria-label') === '表单-主题色',
'L01'
)
console.assert(
!t('.case-l-color-01')?.hasAttribute('aria-labelledby'),
'L01 labelledby'
)
console.assert(
t('.case-l-color-02')?.getAttribute('aria-label') === '控件自身',
'L02'
)
console.assert(!!t('.case-l-color-03')?.getAttribute('aria-labelledby'), 'L03')
console.assert(
t('.case-l-color-04')?.getAttribute('aria-label') === '颜色选择器',
'L04'
)评分(rate)
根节点 [role=slider]:自身 > form-item aria-label > 默认「rating」;规则 B 同上。
| 编号 | 场景 | 期望 |
|---|---|---|
| L01 | form-item 仅 aria-label | 根 aria-label="表单-评分",无 labelledby |
| L02 | 控件自身优先 | 根 aria-label="控件自身" |
| L03 | 可见 label 文本 | 根 aria-labelledby 有值 |
| L04 | 不包 form-item | 根 aria-label="rating" |
| 控制台断言脚本: |
const root = (sel) => document.querySelector(sel)?.querySelector('.w3-rate')
console.assert(
root('.case-l-rate-01')?.getAttribute('aria-label') === '表单-评分',
'L01'
)
console.assert(
!root('.case-l-rate-01')?.hasAttribute('aria-labelledby'),
'L01 labelledby'
)
console.assert(
root('.case-l-rate-02')?.getAttribute('aria-label') === '控件自身',
'L02'
)
console.assert(
!!root('.case-l-rate-03')?.getAttribute('aria-labelledby'),
'L03'
)
console.assert(
root('.case-l-rate-04')?.getAttribute('aria-label') === 'rating',
'L04'
)分段控制器(segmented)
根节点 [role=radiogroup]:自身 > form-item aria-label > 默认「segmented」;规则 B 同上。
| 编号 | 场景 | 期望 |
|---|---|---|
| L01 | form-item 仅 aria-label | 根 aria-label="表单-分段选择",无 labelledby |
| L02 | 控件自身优先 | 根 aria-label="控件自身" |
| L03 | 可见 label 文本 | 根 aria-labelledby 有值 |
| L04 | 不包 form-item | 根 aria-label="segmented" |
| 控制台断言脚本: |
const root = (sel) =>
document.querySelector(sel)?.querySelector('.w3-segmented')
console.assert(
root('.case-l-seg-01')?.getAttribute('aria-label') === '表单-分段选择',
'L01'
)
console.assert(
!root('.case-l-seg-01')?.hasAttribute('aria-labelledby'),
'L01 labelledby'
)
console.assert(
root('.case-l-seg-02')?.getAttribute('aria-label') === '控件自身',
'L02'
)
console.assert(!!root('.case-l-seg-03')?.getAttribute('aria-labelledby'), 'L03')
console.assert(
root('.case-l-seg-04')?.getAttribute('aria-label') === 'segmented',
'L04'
)滑块(slider)
范围模式的根 [role=group] 与两个 [role=slider] 手柄是独立节点:根取控件自身 > form-item aria-label > i18n 默认;手柄在有可拆分的基础值时输出 <值>::开始 / <值>::结束,rangeStartLabel / rangeEndLabel 优先;form-item 用可见 label 文本时根走 aria-labelledby,手柄保留自身 i18n 默认名。
| 编号 | 场景 | 期望 |
|---|---|---|
| L01 | 单值 + form-item aria-label | 手柄 aria-label="表单-音量" |
| L02 | 范围 + 控件自身 aria-label | 手柄 控件自身::开始 / 控件自身::结束 |
| L03 | 范围 + form-item 可见 label 文本 | 根 aria-labelledby 有值;手柄保持 选择起始值/选择结束值 |
| L04 | 范围 + form-item 仅 aria-label | 根 "价格";手柄 价格::开始 / 价格::结束 |
| L05 | 裸用 | 根 滑块介于 0 至 100;手柄 选择起始值/选择结束值 |
| 控制台断言脚本: |
const handles = (sel) => [
...(document.querySelector(sel)?.querySelectorAll('[role="slider"]') || []),
]
const group = (sel) =>
document.querySelector(sel)?.querySelector('[role="group"]')
console.assert(
handles('.case-l-slider-01')[0]?.getAttribute('aria-label') === '表单-音量',
'L01'
)
const h2 = handles('.case-l-slider-02')
console.assert(
h2[0]?.getAttribute('aria-label') === '控件自身::开始',
'L02 start'
)
console.assert(
h2[1]?.getAttribute('aria-label') === '控件自身::结束',
'L02 end'
)
console.assert(
!!group('.case-l-slider-03')?.getAttribute('aria-labelledby'),
'L03 root'
)
const h3 = handles('.case-l-slider-03')
console.assert(
h3[0]?.getAttribute('aria-label') === '选择起始值',
'L03 start default'
)
console.assert(
h3[1]?.getAttribute('aria-label') === '选择结束值',
'L03 end default'
)
console.assert(
group('.case-l-slider-04')?.getAttribute('aria-label') === '价格',
'L04 root'
)
const h4 = handles('.case-l-slider-04')
console.assert(h4[0]?.getAttribute('aria-label') === '价格::开始', 'L04 start')
console.assert(h4[1]?.getAttribute('aria-label') === '价格::结束', 'L04 end')
const h5 = handles('.case-l-slider-05')
console.assert(h5[0]?.getAttribute('aria-label') === '选择起始值', 'L05 start')
console.assert(h5[1]?.getAttribute('aria-label') === '选择结束值', 'L05 end')一次性密码框(input-otp)
根节点 [role=group]:自身 > form-item aria-label > 默认「一次性密码输入框」;规则 B 同上。
| 编号 | 场景 | 期望 |
|---|---|---|
| L01 | form-item 仅 aria-label | 根 aria-label="表单-验证码",无 labelledby |
| L02 | 控件自身优先 | 根 aria-label="控件自身" |
| L03 | 可见 label 文本 | 根 aria-labelledby 有值 |
| L04 | 不包 form-item | 根 aria-label="一次性密码输入框" |
| 控制台断言脚本: |
const root = (sel) =>
document.querySelector(sel)?.querySelector('.w3-input-otp')
console.assert(
root('.case-l-otp-01')?.getAttribute('aria-label') === '表单-验证码',
'L01'
)
console.assert(
!root('.case-l-otp-01')?.hasAttribute('aria-labelledby'),
'L01 labelledby'
)
console.assert(
root('.case-l-otp-02')?.getAttribute('aria-label') === '控件自身',
'L02'
)
console.assert(!!root('.case-l-otp-03')?.getAttribute('aria-labelledby'), 'L03')
console.assert(
root('.case-l-otp-04')?.getAttribute('aria-label') === '一次性密码输入框',
'L04'
)页签(tabs)
w-tab-pane 支持 aria-label:配置后渲染到对应 div[role=tab] 节点;多页签各自独立、互不覆盖;未配置时不输出该属性(与改造前一致)。取值规则为外部传值优先,组件不推导、不校验标记值。
页签打标:`w-tab-pane` 的 `aria-label` 渲染到对应 `div[role=tab]`, 多页签各自独立、互不覆盖;未配置则不输出该属性(与改造前一致)。
以上示例:前两个页签打了标记,第三个未打标。控制台断言脚本见本页「tabs」小节。
| 编号 | 场景 | 期望 | 判定 |
|---|---|---|---|
| T-01 | 单个页签写 aria-label | role=tab 节点 aria-label 等于配置值 | attributes("aria-label") === "页签-门诊患者" |
| T-02 | 多页签各自写 aria-label | 各页签独立取值、互不覆盖 | 两个 role=tab 节点分别等于各自配置值 |
| T-03 | 页签未写 aria-label | 不输出该属性(与改造前一致) | aria-label 为 undefined(属性不存在) |
| T-04 | 打标后既有属性 | aria-controls / aria-selected 不受影响 | 值与改造前一致 |
| 控制台断言脚本: |
const tabs = [...document.querySelectorAll('.case-t01 [role="tab"]')]
console.assert(
tabs[0]?.getAttribute('aria-label') === '页签-门诊患者',
'FAIL T-01'
)
console.assert(
tabs[1]?.getAttribute('aria-label') === '页签-住院患者',
'FAIL T-02'
)
console.assert(tabs[2] && !tabs[2].hasAttribute('aria-label'), 'FAIL T-03')
console.assert(
tabs[0]?.getAttribute('aria-controls') === 'pane-outpatient',
'FAIL T-04'
)
console.log(
'tabs 断言完成:',
tabs.map((t) => t.getAttribute('aria-label'))
)气泡卡(popover)
浮层根节点([role=tooltip])的 aria-label 取值规则:外部传值优先,未传保持现状。title 与用户传入的 aria-label 并存时用户值优先(运行时实测,方案原判断「title 覆盖用户值」已被证伪)。popover 不改模板,本节用例仅锁住既有行为、防止回归。
气泡卡取值规则:外部传值优先,未传保持现状。popover 未声明 `aria-label` prop,用户传值经 attrs 落到浮层根节点,优先于 `title`; 只有 `title` 时用 `title`;都不传则不输出。
本示例统一带 `teleported="false"`,浮层渲染在各 case 容器内,便于按容器定位断言(打标行为与默认 teleport 一致)。
| 编号 | 场景 | 期望 | 判定 |
|---|---|---|---|
| P-01 | 仅 title | 浮层 aria-label 等于 title | click 后 querySelector("[role=tooltip]") 读取 |
| P-02 | 仅用户传 aria-label | 浮层 aria-label 等于用户值 | 同上 |
| P-03 | title + 用户值并存 | 用户值优先 | 浮层 aria-label === 用户值(非 title) |
| P-04 | 都不传 | 浮层不输出 aria-label(与改造前一致) | hasAttribute("aria-label") === false |
控制台断言脚本(示例将浮层置于容器内 teleported=false,persistent 下节点常驻 DOM,点击前后均可读取): |
const q = (sel) => document.querySelector(sel)
console.assert(
q('.case-p01 [role="tooltip"]')?.getAttribute('aria-label') === '提示-标题',
'FAIL P-01'
)
console.assert(
q('.case-p02 [role="tooltip"]')?.getAttribute('aria-label') ===
'提示-用户标记',
'FAIL P-02'
)
console.assert(
q('.case-p03 [role="tooltip"]')?.getAttribute('aria-label') ===
'提示-用户标记',
'FAIL P-03'
)
console.assert(
q('.case-p04 [role="tooltip"]') &&
!q('.case-p04 [role="tooltip"]').hasAttribute('aria-label'),
'FAIL P-04'
)
console.log('popover 断言完成')DatePicker 日期范围
范围形态有两个原生 input,标签按 <值>::开始 / <值>::结束 拆分,保证同一标签全页唯一;单日期不拆分。取值:控件自身 aria-label > form-item > 不输出。
| 编号 | 场景 | 期望 | 判定 |
|---|---|---|---|
| L-DP-01 | 范围,业务写 aria-label | 开始框 <值>::开始,结束框 <值>::结束 | 两个框后缀各一,且无同名重复命中 |
| L-DP-02 | 范围,仅 form-item 写 aria-label | 两个框继承 form-item 值并拆分 | [0] 以 ::开始 结尾,[1] 以 ::结束 结尾 |
| L-DP-03 | 范围,form-item 与组件都写 | 取组件自身的值 | 两个框均以组件值开头 |
| L-DP-04 | 单日期(非 range)写 aria-label | 不拆分,原样绑定(w-input 既有行为) | input 的 aria-label 不含 :: |
| L-DP-05 | 范围,两边都不写 | 属性省略,页面不出现字符串 "undefined" | 无 aria-label 属性 |
| 控制台断言脚本: |
const q = (sel) => document.querySelectorAll(sel)
const label = (inp) => inp.getAttribute('aria-label')
const a = q('.case-l-dp-01 input')
console.assert(
a.length === 2 &&
label(a[0]) === '就诊日期::开始' &&
label(a[1]) === '就诊日期::结束',
'L-DP-01'
)
console.assert(q('[aria-label="就诊日期"]').length === 0, 'L-DP-01 唯一命中')
const b = q('.case-l-dp-02 input')
console.assert(
label(b[0]) === '表单-就诊日期::开始' &&
label(b[1]) === '表单-就诊日期::结束',
'L-DP-02'
)
const c = q('.case-l-dp-03 input')
console.assert(
label(c[0]) === '控件自身::开始' && label(c[1]) === '控件自身::结束',
'L-DP-03'
)
const d = q('.case-l-dp-04 input')
console.assert(d.length === 1 && label(d[0]) === '单日期', 'L-DP-04')
const e = q('.case-l-dp-05 input')
console.assert(e.length === 2 && !label(e[0]) && !label(e[1]), 'L-DP-05')TimePicker 时间范围
与 DatePicker 共用范围触发器(timerange),拆分与回退规则一致。
| 编号 | 场景 | 期望 | 判定 |
|---|---|---|---|
| L-TP-01 | 范围,业务写 aria-label | ::开始 / ::结束 | 同 L-DP-01 |
| L-TP-02 | 范围,仅 form-item 写 | 继承并拆分 | 同 L-DP-02 |
| L-TP-03 | 范围,自身值优先 | 用组件值拆分 | 同 L-DP-03 |
| 控制台断言脚本: |
const q = (sel) => document.querySelectorAll(sel)
const label = (inp) => inp.getAttribute('aria-label')
const a = q('.case-l-tp-01 input')
console.assert(
label(a[0]) === '就诊时间::开始' && label(a[1]) === '就诊时间::结束',
'L-TP-01'
)
const b = q('.case-l-tp-02 input')
console.assert(
label(b[0]) === '表单-就诊时间::开始' &&
label(b[1]) === '表单-就诊时间::结束',
'L-TP-02'
)
const c = q('.case-l-tp-03 input')
console.assert(
label(c[0]) === '控件自身::开始' && label(c[1]) === '控件自身::结束',
'L-TP-03'
)Select
w-select 的标记链路:区域标记前缀取业务传入值 —— 选择区根节点为 <值>::选择区,下拉选项区 ul[role=listbox] 为 <值>::下拉选项区(未传 aria-label 时不输出区域标记);选项节点按 value 固定渲染为 下拉选项::<value>,数据里的 'aria-label' 不生效;控件内部 input 的 aria-label 支持 form-item 回退并收敛到唯一命中。
| 编号 | 场景 | 期望 | 判定 |
|---|---|---|---|
| L01 | 选项数据有 value | li[role=option] 为 下拉选项::<value> | 展开后首项 aria-label === '下拉选项::1' |
| L02 | form-item 写 aria-label | 内部 input 输出 form-item 的值 | document.querySelector('.case-d02 input') 带值 |
| L03 | form-item 与控件都写 | 取控件自身值,且全页唯一命中 | [aria-label="控件-科室"] 命中数 === 1 |
| L04 | 模板式 <w-option aria-label> | 外部值不生效,仍按 value 固定 | 展开后首项 aria-label === '下拉选项::1' |
| L05 | 分组数据 | 组内/散项均按 value 固定 | 命中 aria-label="下拉选项::1" |
| L-R1 | 区域标记(取控件值) | 选择区根节点 <值>::选择区 | .case-d01 .w3-select === 药物过敏史::选择区 |
| L-R2 | 区域标记(取控件值) | 展开后 listbox 为 <值>::下拉选项区 | listbox === 药物过敏史::下拉选项区 |
| L-NEG | 数据里写 'aria-label' | 不生效(节点只按 value 生成) | [aria-label="过敏史-青霉素"] 不存在 |
| L-UNI | 控件传 aria-label 并展开 | 命中唯一为内部 input(修复前为 input+listbox 2 个) | findAll('[aria-label=…]').length === 1 |
| 控制台断言脚本(手动展开对应下拉后再执行): |
const assert = (name, cond) => console.log(`${cond ? 'PASS' : 'FAIL'} ${name}`)
// 展开「药物过敏史」后执行:
assert(
'L01 选项按 value 固定打标',
document
.querySelector('.w3-select-dropdown__item')
?.getAttribute('aria-label') === '下拉选项::1'
)
assert(
'L02 form-item 回退',
document.querySelector('.case-d02 input')?.getAttribute('aria-label') ===
'表单-城市'
)
assert(
'L03 控件自身优先',
document.querySelector('.case-d03 input')?.getAttribute('aria-label') ===
'控件-科室'
)
assert(
'L-R1 选择区标记取控件值',
document.querySelector('.case-d01 .w3-select')?.getAttribute('aria-label') ===
'药物过敏史::选择区'
)
assert(
'L-R2 下拉选项区标记取控件值',
document.querySelector('[role=listbox]')?.getAttribute('aria-label') ===
'药物过敏史::下拉选项区'
)
assert(
'L-UNI 唯一命中(含展开后)',
document.querySelectorAll('[aria-label="控件-科室"]').length === 1
)
// 展开「选择方案」「搜索药品」后执行:
assert(
'L04 模板式选项按 value 固定',
[...document.querySelectorAll('.w3-select-dropdown__item')].some(
(li) =>
li.textContent.trim() === '门诊' &&
li.getAttribute('aria-label') === '下拉选项::1'
)
)
assert(
'L05 分组数据按 value 固定',
[...document.querySelectorAll('.w3-select-dropdown__item')].some(
(li) =>
li.textContent.trim() === '阿司匹林' &&
li.getAttribute('aria-label') === '下拉选项::1'
)
)
assert(
'L-NEG 数据 aria-label 不生效',
!document.querySelector('[aria-label="过敏史-青霉素"]')
)SelectV2
w-select-v2 与 select 同构:区域标记前缀取业务传入值 —— 选择区根节点为 <值>::选择区,下拉选项区 listbox 为 <值>::下拉选项区(未传 aria-label 时不输出);选项按 value 固定为 下拉选项::<value>,数据里的 'aria-label' 不生效;内部 input 的 aria-label 支持 form-item 回退且唯一命中。
| 编号 | 场景 | 期望 | 判定 |
|---|---|---|---|
| V01 | 选项数据有 value | li[role=option] 为 下拉选项::<value> | 展开后首项 aria-label === '下拉选项::1' |
| V02 | form-item 写 aria-label,控件未写 | 内部 input 输出 form-item 的值 | .case-v02 input 带 表单-V2城市 |
| V03 | form-item 与控件都写 | 取控件自身值,且展开后全页唯一命中 | [aria-label="控件-V2科室"] 命中数 === 1 |
| V04 | 数据里写 'aria-label' | 不生效(节点只按 value 生成) | [aria-label="过敏史V2-青霉素"] 不存在 |
| V05 | 分组数据 | 组内/散项均按 value 固定 | 命中 aria-label="下拉选项::1" |
| V-R1 | 区域标记(取控件值) | 选择区根节点 <值>::选择区 | .case-v01 .w3-select === 药物过敏史V2::选择区 |
| V-R2 | 区域标记(取控件值) | 展开后 listbox 为 <值>::下拉选项区 | listbox === 药物过敏史V2::下拉选项区 |
| V-UNI | 控件传 aria-label 并展开 | 唯一命中为内部 input(修复前为 input + listbox 2 个) | findAll('[aria-label=…]').length === 1 |
| 控制台断言脚本(手动展开对应下拉后再执行): |
const assert = (name, cond) => console.log(`${cond ? 'PASS' : 'FAIL'} ${name}`)
// 展开「药物过敏史」后执行:
assert(
'V01 选项按 value 固定打标',
document
.querySelector('.w3-select-dropdown__item')
?.getAttribute('aria-label') === '下拉选项::1'
)
assert(
'V02 form-item 回退',
document.querySelector('.case-v02 input')?.getAttribute('aria-label') ===
'表单-V2城市'
)
assert(
'V03 控件自身优先',
document.querySelector('.case-v03 input')?.getAttribute('aria-label') ===
'控件-V2科室'
)
assert(
'V-R1 选择区标记取控件值',
document.querySelector('.case-v01 .w3-select')?.getAttribute('aria-label') ===
'药物过敏史V2::选择区'
)
assert(
'V-R2 下拉选项区标记取控件值',
document.querySelector('[role=listbox]')?.getAttribute('aria-label') ===
'药物过敏史V2::下拉选项区'
)
assert(
'V-UNI 唯一命中(含展开后)',
document.querySelectorAll('[aria-label="控件-V2科室"]').length === 1
)
// 展开「选择药品」「选择病区」后执行:
assert(
'V05 分组数据按 value 固定',
[...document.querySelectorAll('.w3-select-dropdown__item')].some(
(li) =>
li.textContent.trim() === '阿司匹林' &&
li.getAttribute('aria-label') === '下拉选项::1'
)
)
assert(
'V04 数据 aria-label 不生效',
!document.querySelector('[aria-label="过敏史V2-青霉素"]')
)Dropdown
w-dropdown 的标记链路:区域标记前缀取业务传入值 —— 触发区根节点为 <dropdown 值>::触发区,菜单区 ul[role=menu] 为 <值>::下拉菜单区(菜单自身值 > dropdown 值;无值不输出);菜单项按 command 固定为 下拉菜单项::<command>,业务写的 aria-label 不生效。菜单 ul 的 aria 属性由 popper 的 triggerTargetEl watcher 接管,因此统一经 w-tooltip 的 ariaLabel 落值。
① 只给 w-dropdown 传 aria-label:展开后 ul[role=menu] 带该值
② dropdown 与 dropdown-menu 同时传:菜单自身值优先
③ 都不传:菜单 ul 无 aria-label(与改造前一致)
④ 回归:触发器自身 aria-label 不变,ul 的 aria-labelledby 保留
| 编号 | 场景 | 期望 | 判定 |
|---|---|---|---|
| D01 | 只给 w-dropdown 传 aria-label | 菜单区为 操作菜单::下拉菜单区 | ul[aria-label="操作菜单::下拉菜单区"] 存在 |
| D02 | dropdown 与 dropdown-menu 都传 | 菜单区取菜单自身值 | 命中 菜单自身标记::下拉菜单区 |
| D03 | 都不传 | 两个区域都不输出 | 根节点与 ul 均无 aria-label |
| D04 | 回归:触发器自身打标 | 触发器 aria-label 不变 | 触发按钮属性保持业务原值 |
| D05 | 回归:ul 的 aria-labelledby | 保留并指向触发器 id | aria-labelledby === 触发器 id |
| D-R1 | 触发区(取 dropdown 值) | 根节点 <值>::触发区 | .case-d01 根 === 操作菜单::触发区 |
| D06 | 菜单项(取 command) | li[role=menuitem] 为 下拉菜单项::<command> | 首项 === 下拉菜单项::a |
| 控制台断言脚本(手动展开对应菜单后再执行): |
const assert = (name, cond) => console.log(`${cond ? 'PASS' : 'FAIL'} ${name}`)
// 展开「更多操作」后执行:
assert(
'D01 菜单区取 dropdown 值',
!!document.querySelector('ul[role=menu][aria-label="操作菜单::下拉菜单区"]')
)
assert(
'D-R1 触发区取 dropdown 值',
document.querySelector('.case-d01')?.getAttribute('aria-label') ===
'操作菜单::触发区'
)
assert(
'D06 菜单项按 command 固定',
document.querySelector('[role=menuitem]')?.getAttribute('aria-label') ===
'下拉菜单项::a'
)
// 展开「批量处理」后执行:
assert(
'D02 菜单区取菜单自身值',
!!document.querySelector(
'ul[role=menu][aria-label="菜单自身标记::下拉菜单区"]'
)
)
// 展开「随访操作」后执行:
const trigger = document.querySelector('.case-d04 button')
const menu = document.querySelector('ul[role=menu]')
assert(
'D04 触发器 aria-label 不变',
trigger?.getAttribute('aria-label') === '触发器-展开随访菜单'
)
assert(
'D05 aria-labelledby 保留',
menu?.getAttribute('aria-labelledby') === trigger?.id
)tree
节点标记固定为 树节点::<node-key 值>(未配置 node-key 时不输出);树根 div[role=tree] 依次回退组件自身与外层 form-item(控件自身值优先)。数据里 自带的 aria-label 不生效。
| 编号 | 场景 | 期望 | 判定 |
|---|---|---|---|
| T-L01 | 根没写,外层 form-item 写 表单-地区 | 根 div[role=tree] 输出 aria-label="表单-地区" | 控制台脚本 [T-L01] PASS |
| T-R01 | form-item 必填 | 根节点输出 aria-required="true" | 控制台脚本 [T-R01] PASS |
| T-L02 | 配置了 node-key | 节点标记为 树节点::<node-key 值> | 控制台脚本 [T-L02] PASS |
| T-L03 | 未配置 node-key | 该节点不输出 aria-label 属性 | 控制台脚本 [T-L03] PASS |
| T-L04 | form-item 与树都写 | 取树自己的 aria-label="树自身" | 控制台脚本 [T-L04] PASS |
| T-L05 | 两边都不写 | 属性省略,页面上没有 "undefined" 字符串 | 控制台脚本 [T-L05] PASS |
| T-L06 | 全页唯一 | aria-label="树自身" 只命中 1 个节点 | 控制台脚本 [T-L06] PASS |
| 控制台断言脚本(浏览器打开本页,按 F12 粘贴运行): |
;(() => {
const log = (id, desc, pass) =>
console[pass ? 'log' : 'error'](
`[${id}] ${desc}: ${pass ? 'PASS' : 'FAIL'}`
)
const root1 = document.querySelector('.case-t01 [role=tree]')
log(
'T-L01',
'根节点回退 form-item aria-label',
root1?.getAttribute('aria-label') === '表单-地区'
)
log(
'T-R01',
'根节点回退 form-item 必填状态',
root1?.getAttribute('aria-required') === 'true'
)
log(
'T-L02',
'节点标记为 树节点::<node-key 值>',
[...document.querySelectorAll('.case-t01 [role=treeitem]')].some(
(el) => el.getAttribute('aria-label') === '树节点::zj'
)
)
log(
'T-L03',
'未配置 node-key 时节点不输出 aria-label',
[...document.querySelectorAll('.case-t03 [role=treeitem]')].every(
(el) => !el.hasAttribute('aria-label')
)
)
const root2 = document.querySelector('.case-t02 [role=tree]')
log(
'T-L04',
'控件自身值优先于 form-item 回退',
root2?.getAttribute('aria-label') === '树自身'
)
const root3 = document.querySelector('.case-t03 [role=tree]')
log(
'T-L05',
'两边不写时属性省略(无 undefined 字符串)',
!root3?.hasAttribute('aria-label') &&
!document.body.innerHTML.includes('aria-label="undefined"')
)
log(
'T-L06',
'全页唯一:值只落 1 个节点',
document.querySelectorAll('[aria-label="树自身"]').length === 1
)
})()cascader
组件声明 ariaLabel prop 后业务值落到内部 input(form-item 回退由 ARIA-01 在 w-input 内部实现);区域标记前缀取传入值 —— 触发区根节点 <值>::选择区, 下拉浮层 <值>::下拉选项区(无值不输出);业务直写的 aria-required 经 $attrs 转发给 w-input;节点标记固定为 级联节点::<选项 value 值>,选项数据里的 aria-label 不生效。
| 编号 | 场景 | 期望 | 判定 |
|---|---|---|---|
| C-L01 | 组件没写,外层 form-item 写 表单-地区 | 内部 input 输出该值 | 控制台脚本 [C-L01] PASS |
| C-R01 | form-item 必填 | 内部 input 输出 aria-required="true" | 控制台脚本 [C-R01] PASS |
| C-L02 | form-item 与 cascader 都写 | 取 cascader 自己的 aria-label="级联自身" | 控制台脚本 [C-L02] PASS |
| C-R02 | 裸用直写 aria-required="true" | 内部 input 输出 aria-required="true" | 控制台脚本 [C-R02] PASS |
| C-L03 | 裸用不写 aria-label | 内部 input 不输出 aria-label | 控制台脚本 [C-L03] PASS |
| C-L04 | 选项配置了 value | li[role=menuitem] 为 级联节点::<value> | 控制台脚本 [C-L04] PASS |
| C-L05 | 组件和 form-item 都没写 aria-label | 节点仍按 value 打标,仅 input 不输出 | 控制台脚本 [C-L05] PASS |
| C-L06 | 自身值落点 | aria-label="级联自身" 恰好 1 处(内部 input) | 控制台脚本 [C-L06] PASS |
| C-L07 | 打开下拉浮层 | .w3-cascader__dropdown 为 <值>::下拉选项区 | 控制台脚本 [C-L07] PASS |
| C-L08 | 触发区根节点 | .w3-cascader 为 <值>::选择区 | 控制台脚本 [C-L08] PASS |
| 控制台断言脚本(打开本页,点开展开面板的用例前后各跑一次即可;脚本会自动展开面板): |
;(async () => {
const log = (id, desc, pass) =>
console[pass ? 'log' : 'error'](
`[${id}] ${desc}: ${pass ? 'PASS' : 'FAIL'}`
)
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const input1 = document.querySelector('.case-c01 input')
log(
'C-L01',
'input 回退 form-item aria-label',
input1?.getAttribute('aria-label') === '表单-地区'
)
log(
'C-R01',
'input 回退 form-item 必填状态',
input1?.getAttribute('aria-required') === 'true'
)
const input2 = document.querySelector('.case-c02 input')
log(
'C-L02',
'cascader 自身值优先',
input2?.getAttribute('aria-label') === '级联自身'
)
const input3 = document.querySelector('.case-c03 input')
log(
'C-R02',
'裸用直写 aria-required 转发到内部 input',
input3?.getAttribute('aria-required') === 'true'
)
log(
'C-L03',
'裸用不写时 input 无 aria-label',
!input3?.hasAttribute('aria-label')
)
document.querySelector('.case-c01 .w3-cascader')?.click()
await sleep(300)
log(
'C-L04',
'节点标记等于 级联节点::<value>',
[...document.querySelectorAll('.w3-cascader-node')].some(
(el) => el.getAttribute('aria-label') === '级联节点::zj'
)
)
log(
'C-L07',
'浮层为 <值>::下拉选项区',
[...document.querySelectorAll('.w3-cascader__dropdown')].some(
(el) => el.getAttribute('aria-label') === '表单-地区::下拉选项区'
)
)
log(
'C-L08',
'触发区为 <值>::选择区',
document
.querySelector('.case-c01 .w3-cascader')
?.getAttribute('aria-label') === '表单-地区::选择区'
)
document.querySelector('.case-c04 .w3-cascader')?.click()
await sleep(300)
log(
'C-L05',
'未写 aria-label 时节点仍按 value 打标',
[...document.querySelectorAll('.w3-cascader-node')].some(
(el) =>
el.textContent.includes('江苏省') &&
el.getAttribute('aria-label') === '级联节点::js'
)
)
log(
'C-L06',
'自身值只落内部 input(1 处)',
document.querySelectorAll('[aria-label="级联自身"]').length === 1
)
})()表格族(table / table-pro / table-v2)
列配置写了 aria-label 就落到表头节点;行节点固定为 表格行::<row-key 值>(未配置 row-key 时不输出),不读取行数据的 aria-label;筛选浮层固定 筛选::<列标识>(列标识取列 aria-label,未配置回退列字段名),列设置浮层固定 列设置。组件不从 label 或单元格内容自动生成标记。
w-table:列 aria-label → th,行固定 表格行::<row-key> → tr
w-table-v2:列标记 → div[role=columnheader],行固定 表格行::<row-key> → div[role=row]
w-table-pro:透传基础表格打标(不改代码)
浮层:筛选 → 筛选::<列标识>,列设置 → 列设置
| 编号 | 组件 | 场景 | 期望 | 判定 |
|---|---|---|---|---|
| T-01 | w-table | 列写了 aria-label | th[aria-label] 等于配置值 | find('th').attributes('aria-label') |
| T-02 | w-table | 列未写 | th 没有 aria-label | 属性为 undefined |
| T-03 | w-table | 配置了 row-key | tbody tr[aria-label] 为 表格行::<row-key 值> | find('tbody tr').attributes('aria-label') |
| T-04 | w-table-v2 | 列写了 ariaLabel | div[role=columnheader][aria-label] 命中 | document.querySelector |
| T-05 | w-table-v2 | 配置了 row-key | div[role=row][aria-label] 为 表格行::<row-key 值> | document.querySelector |
| T-06 | w-table-v2 | 虚拟滚动后 | 目标行滚动后仍可按 aria-label 定位 | 先滚动再 querySelector |
| T-07 | w-table-pro | 列/行打标 | 内层 w-table 自动继承(透传,不改代码) | 同 T-01/T-03 |
| T-08 | w-table | 默认渲染 | 表头与内容两个 table 固定带 aria-label="表头" / "表格内容" | table[aria-label] |
| T-09 | w-table-v2 | 默认渲染 | 表头与内容两个 div[role=rowgroup] 固定带 aria-label="表头" / "表格内容" | [role=rowgroup][aria-label] |
| T-10 | w-table | 筛选浮层 | [role=dialog] 带 aria-label="筛选::列-病历号" | document.querySelector |
| T-11 | w-table | 列设置浮层 | [role=dialog] 带 aria-label="列设置" | document.querySelector |
| 控制台断言脚本(页面加载后执行,全部输出 PASS 即通过;vitest 侧已有对应用例且全绿): |
const r = (name, ok) => console.log(ok ? 'PASS' : 'FAIL', name)
r(
'T-01',
document.querySelector('.case-t01 thead th')?.getAttribute('aria-label') ===
'列-病历号'
)
r(
'T-02',
!document
.querySelector('.case-t01 thead th:nth-child(2)')
?.hasAttribute('aria-label')
)
r(
'T-03',
document.querySelector('.case-t01 tbody tr')?.getAttribute('aria-label') ===
'表格行::P-001'
)
r(
'T-04',
!!document.querySelector(
'.case-t02 [role=columnheader][aria-label="列-V2病历号"]'
)
)
r(
'T-05',
!!document.querySelector('.case-t02 [role=row][aria-label="表格行::P-001"]')
)
r(
'T-07',
!!document.querySelector('.case-t03 thead th[aria-label="列-病历号Pro"]')
)
r(
'T-08',
!!document.querySelector('.case-t01 table[aria-label="表头"]') &&
!!document.querySelector('.case-t01 table[aria-label="表格内容"]')
)
r(
'T-09',
!!document.querySelector('.case-t02 [role=rowgroup][aria-label="表头"]') &&
!!document.querySelector('.case-t02 [role=rowgroup][aria-label="表格内容"]')
)
// 浮层 teleport 到 body,需全局查询
r(
'T-10',
!!document.querySelector('[role=dialog][aria-label="筛选::列-病历号"]')
)
r('T-11', !!document.querySelector('[role=dialog][aria-label="列设置"]'))上传(upload)
组件标签上的 aria-label 作为前缀:上传触发区 .w3-upload(role="button")标记为 <值>::上传操作,删除节点标记为 <值>::删除操作(文案/图片列表的关闭图标、picture-card 的删除按钮);未配置时触发区不输出,删除图标保留自身 i18n「删除」。UploadFile 的 'aria-label' 字段仍渲染到对应文件列表项 li.w3-upload-list__item,两者互不影响。
文件项配置了 'aria-label',列表项 li 会输出该属性
- 按 delete 键可删除
未配置 'aria-label':li 上没有该属性(与改造前一致)
- 按 delete 键可删除
picture-card 模式:li 带标记,删除按钮自身不受影响
按 delete 键可删除
标签上写 aria-label:上传触发区为 <值>::上传操作,删除节点为 <值>::删除操作
- 按 delete 键可删除
| 编号 | 场景 | 期望 | 判定 |
|---|---|---|---|
| U-01 | 文件配置了 'aria-label' | li 上输出该值 | document.querySelector('li.w3-upload-list__item').getAttribute('aria-label') |
| U-02 | 文件未配置 | li 上没有 aria-label | getAttribute('aria-label') 返回 null |
| U-03 | listType text / picture / picture-card | 三种模式 li 均按配置输出 | 切换示例中三种列表类型分别断言 |
| U-04 | picture-card 删除节点(已配置标签) | 为 <值>::删除操作 | .case-u03 .w3-upload-list__item-delete 的 aria-label |
| U-05 | 文案列表关闭图标(已配置标签) | 为 <值>::删除操作 | .case-u04 .w3-icon--close 的 aria-label |
| U-06 | 标签上写 aria-label | .w3-upload 为 <值>::上传操作,外层包裹 div 不输出 | document.querySelector('.case-u04 .w3-upload').getAttribute('aria-label') |
| U-07 | 标签未写 | .w3-upload 上没有 aria-label | hasAttribute('aria-label') === false |
| U-08 | 标签未写时的删除节点 | 关闭图标保留 i18n「删除」 | .case-u01 .w3-icon--close 的 aria-label |
| 控制台断言脚本: |
const li = document.querySelector('li.w3-upload-list__item')
console.assert(li?.getAttribute('aria-label') === '上传文件标记', 'U-01 失败')
const items = document.querySelectorAll('li.w3-upload-list__item')
const plain = [...items].find((el) => el !== li)
console.assert(plain && !plain.hasAttribute('aria-label'), 'U-02 失败')
const labeled = document.querySelector('.case-u04 .w3-upload')
console.assert(
labeled?.getAttribute('aria-label') === '上传附件::上传操作',
'U-06 失败'
)
console.assert(
labeled?.parentElement?.getAttribute('aria-label') === null,
'U-06 落点失败(外层包裹 div 不应带标记)'
)
console.assert(
!document.querySelector('.case-u01 .w3-upload')?.hasAttribute('aria-label'),
'U-07 失败'
)
console.assert(
document
.querySelector('.case-u03 .w3-upload-list__item-delete')
?.getAttribute('aria-label') === '照片墙::删除操作',
'U-04 失败'
)
console.assert(
document
.querySelector('.case-u04 .w3-icon--close')
?.getAttribute('aria-label') === '上传附件::删除操作',
'U-05 失败'
)
console.assert(
document
.querySelector('.case-u01 .w3-icon--close')
?.getAttribute('aria-label') === '删除',
'U-08 失败'
)
console.log('U-01/02/04/05/06/07/08 断言通过')二次确认框(message-box)
MessageBoxOptions 支持 ariaLabel 与 'aria-label',面板根节点 .w3-message-box 输出 <值>::二次确认框;未传时不输出。外层 [role=dialog] 仍以 title 兜底,行为不变。
打开 alert 与「未传」用例后,控制台按值查找 [...document.querySelectorAll('.w3-message-box')] 断言;外层 .w3-overlay-message-box[role=dialog] 仍以 title 兜底。
| 编号 | 场景 | 期望 | 判定 |
|---|---|---|---|
| M-01 | alert 传 ariaLabel | 面板为 <值>::二次确认框 | [...querySelectorAll('.w3-message-box')].some(带该值) |
| M-02 | confirm 传 'aria-label' | 同上(kebab 键等价) | 同 M-01 |
| M-03 | prompt 传 ariaLabel | 同上 | 同 M-01 |
| M-04 | 未传 | 面板无 aria-label | hasAttribute('aria-label') === false |
| M-05 | 外层 dialog | aria-label 仍等于 title | .w3-overlay-message-box[role=dialog] 的 aria-label |
| 控制台断言脚本(先点开 alert 与「未传」两个按钮;多个面板会同时留在页面上,按值查找而不是取第一个): |
const panels = [...document.querySelectorAll('.w3-message-box')]
console.assert(
panels.some((el) => el.getAttribute('aria-label') === '面板标记::二次确认框'),
'M-01 失败'
)
console.assert(
panels.some((el) => !el.hasAttribute('aria-label')),
'M-04 失败'
)
const dialog = document.querySelector('.w3-overlay-message-box[role=dialog]')
console.assert(dialog?.getAttribute('aria-label') === '标题文本', 'M-05 失败')
console.log('M-01/M-04/M-05 断言通过')消息提示(message)
组件声明 ariaLabel prop(useAriaProps(['ariaLabel'])),根节点 .w3-message 输出 <值>::消息提示;组件式 attrs 透传与方法式 options 均生效,「声明后仍可用」为回归重点。
控制台执行 document.querySelector('.w3-message').getAttribute('aria-label') 断言;未传时该属性不输出。组件式 attrs 透传(S-03)用下方脚本验证。
| 编号 | 场景 | 期望 | 判定 |
|---|---|---|---|
| S-01 | 方法式 options 传 ariaLabel | 根节点输出 <值>::消息提示 | document.querySelector('.w3-message').getAttribute('aria-label') |
| S-02 | 未传 | 不输出该属性 | hasAttribute('aria-label') === false |
| S-03 | 组件式 attrs 'aria-label' | 仍生效(透传) | mount(WMessage, { attrs: { 'aria-label' } }) 单测(message-aria.test.tsx) |
| S-04 | role=alert | 不变 | getAttribute('role') === 'alert' |
| 控制台断言脚本: |
const msg = [...document.querySelectorAll('.w3-message')].find(
(el) => el.getAttribute('aria-label') === '消息标记::消息提示'
)
console.assert(msg, 'S-01 失败')
console.assert(msg?.getAttribute('role') === 'alert', 'S-04 失败')
console.log('S-01/S-04 断言通过')通知(notification)
组件声明 ariaLabel prop,根节点 .w3-notification 输出 <值>::通知;$notify options 与组件式均生效,未传不输出。
控制台执行 document.querySelector('.w3-notification').getAttribute('aria-label') 断言;未传时该属性不输出,role="alert" 行为不变。
| 编号 | 场景 | 预期 | 判定 |
|---|---|---|---|
| N-01 | $notify options 传 ariaLabel | 根节点输出 <值>::通知 | document.querySelector('.w3-notification').getAttribute('aria-label') |
| N-02 | 未传 | 不输出该属性 | hasAttribute('aria-label') === false |
| N-03 | 组件式 attrs 'aria-label' | 仍生效 | mount(WNotification, { attrs: { 'aria-label' } }) 单测(notification-aria.test.tsx) |
| N-04 | role=alert | 不变 | getAttribute('role') === 'alert' |
| 控制台断言脚本: |
const n = [...document.querySelectorAll('.w3-notification')].find(
(el) => el.getAttribute('aria-label') === '通知标记::通知'
)
console.assert(n, 'N-01 失败')
console.assert(n?.getAttribute('role') === 'alert', 'N-04 失败')
console.log('N-01/N-04 断言通过')aria-required
form-item 加了 required(或校验判定为必填),内部控件会带上 aria-required="true"。
| 组件 | 场景 | 控件上的结果 |
|---|---|---|
w-input | form-item 标了 required | aria-required="true" |
w-input | 非必填 | 没有 aria-required |
w-input | 必填,但自己写了 aria-required="false" | 用自己的:false |
w-input | 点击按钮切换 required | aria-required 跟着变 |
w-input | 单独使用并自己写了 | 保留用户写的值 |
w-textarea | form-item 必填 | aria-required="true" |
w-radio-group | form-item 必填 | 组根节点 aria-required="true" |
w-checkbox-group | form-item 必填 | 组根节点 aria-required="true" |
w-tree | form-item 必填 | 根节点 div[role=tree] 输出 aria-required="true" |
w-cascader | form-item 必填 | 内部 input 输出 aria-required="true"(ARIA-01 回退) |
w-cascader | 裸用并直写 aria-required="true" | 转发给内部 input,输出 aria-required="true" |
| 以上组件 | 任意必填场景 | 都没有原生 required |
扩展输入(color-picker / rate / segmented / slider / input-otp)
用户直写 aria-required(含 "false")优先,组件不转真值、不新增 ariaRequired prop;未传时 form-item 必填才输出 "true",非必填不输出,且不使用原生 required。
| 组件 | 承载节点 | 必填时输出 |
|---|---|---|
w-color-picker | 触发器 [role=button] | aria-required="true" |
w-rate | 根 [role=slider] | aria-required="true" |
w-segmented | 根 [role=radiogroup] | aria-required="true" |
w-slider | 范围根 + 两个手柄(共 3 个) | 三处均 aria-required="true" |
w-input-otp | 根 [role=group] | aria-required="true" |
| 编号 | 场景 | 期望 |
| ---- | ---------------------------- | -------------------------------------- |
| R01 | form-item 必填 | 上表承载节点输出 "true" |
| R02 | form-item 非必填 | 不输出 aria-required |
| R03 | 裸用 aria-required="false" | 用户值原样保留 "false";必填时也优先 |
以上四个单节点组件的取值由 packages/components/*/__tests__/*-aria.test.tsx 锁定;slider 另有可视化用例: |
滑块(slider)
slider 的两个手柄是嵌套节点,根节点的透传属性到不了,必须显式转发;范围模式必填时 [aria-required] 命中根 + 两个手柄(共 3 个)。
| 编号 | 场景 | 期望 |
|---|---|---|
| R01 | form-item 必填 + 范围 | 根与两个手柄共 3 个 aria-required="true" |
| R02 | form-item 必填 + 单值 | 手柄 aria-required="true"(根非承载节点,不带 role) |
| R03 | form-item 非必填 | 该用例内无 aria-required |
| R04 | 裸用 aria-required="false" | 用户值原样保留 "false" |
| 控制台断言脚本: |
const d = document.querySelector('.case-r-slider-01')?.closest('.example')
console.assert(
d?.querySelectorAll('.case-r-slider-01 [aria-required]').length === 3,
'R01 count'
)
;[...(d?.querySelectorAll('.case-r-slider-01 [aria-required]') || [])].forEach(
(n) => console.assert(n.getAttribute('aria-required') === 'true', 'R01 value')
)
console.assert(
d?.querySelectorAll('.case-r-slider-03 [aria-required]').length === 0,
'R03'
)
console.assert(
d
?.querySelector('.case-r-slider-04 .w3-rate')
?.getAttribute('aria-required') === 'false',
'R04'
)DatePicker 日期范围
aria-required 为 String 型:业务直写(含 "false")原样输出,未写时回退 form-item 必填状态;单日期 input 与范围两个 input 都会带上,未配置时属性省略且不使用原生 required。
| 编号 | 场景 | 期望 | 判定 |
|---|---|---|---|
| R-DP-01 | form-item 标 required,范围形态 | 两个框均 aria-required="true" | [0]/[1] 均为 "true" |
| R-DP-02 | 业务直写 aria-required="true" | 单日期 input 与范围两个框均 "true" | 命中数 1 / 2 |
| R-DP-03 | 业务直写 aria-required="false" | "false" 原样保留(String 型,不被布尔化) | 值为 "false" |
| R-DP-04 | form-item 必填但业务写 "false" | 自身值优先,仍为 "false" | 两个框均 "false" |
| R-DP-05 | 未配置(非必填、未写) | 属性省略,无原生 required,不出现 "undefined" | 属性为空 |
| 控制台断言脚本: |
const q = (sel) => document.querySelectorAll(sel)
const req = (inp) => inp.getAttribute('aria-required')
const a = q('.case-r-dp-01 input')
console.assert(req(a[0]) === 'true' && req(a[1]) === 'true', 'R-DP-01')
const b1 = q('.case-r-dp-02-single input')
const b2 = q('.case-r-dp-02-range input')
console.assert(req(b1[0]) === 'true', 'R-DP-02 单日期')
console.assert(req(b2[0]) === 'true' && req(b2[1]) === 'true', 'R-DP-02 范围')
const c = q('.case-r-dp-03 input')
console.assert(req(c[0]) === 'false', 'R-DP-03')
const d = q('.case-r-dp-04 input')
console.assert(req(d[0]) === 'false' && req(d[1]) === 'false', 'R-DP-04')
const e = q('.case-r-dp-05 input')
console.assert(
e.every((i) => req(i) === null && i.getAttribute('required') === null),
'R-DP-05'
)TimePicker 时间范围
与 DatePicker 共用同一套取值与下发逻辑(timerange)。
| 编号 | 场景 | 期望 | 判定 |
|---|---|---|---|
| R-TP-01 | form-item 标 required,范围 | 两个框均 "true" | 同 R-DP-01 |
| R-TP-02 | 业务直写 aria-required="true" | 两个框均 "true" | 同 R-DP-02 |
| 控制台断言脚本: |
const q = (sel) => document.querySelectorAll(sel)
const req = (inp) => inp.getAttribute('aria-required')
const a = q('.case-r-tp-01 input')
console.assert(req(a[0]) === 'true' && req(a[1]) === 'true', 'R-TP-01')
const b = q('.case-r-tp-02 input')
console.assert(req(b[0]) === 'true' && req(b[1]) === 'true', 'R-TP-02')Copyright 2025 Winning Health 版权所有

