第八轮最小粒度测试:修复CSS选择器/JSX结构不匹配的一整类bug,清理Tinode品牌残留

第八轮改用"读CSS选择器+核对JSX结构"的方式复测全部11章,命中了7轮纯接口测试从未发现的一类
问题——选择器写对了但目标元素的实际DOM结构/className对不上,样式静默失效:

- 语音通话最小化胶囊按钮尺寸被同特异度规则覆盖;移动端悬浮通话窗口left/right/width三值同时
  生效导致过度约束;来电全屏页认证徽章/姓名标题缺少专属样式覆盖
- 我的收藏-语音卡片三列grid对应实际只有1个子元素,播放器被压缩到卡片左侧一角;链接/记录头部
  完全没有基础样式,退化成浏览器默认外观
- 多选模式下"收到的"消息气泡被flex auto-margin一起拖到最右侧;.avatar-box/#send-message-panel
  两处逗号断句错误导致后半句选择器丢失祖先限定
- 按住说话悬浮反馈渲染成内联胶囊,而参考图要求的居中悬浮卡片样式其实已经存在(挂在了另一个非
  生产demo组件上);批量转发确认框沿用了80%视口高度;audio-player.jsx的playback类点在canvas
  上而非包裹层,波形和计时器可能纵向堆叠
- "消息已撤回"占位符误拼了日期分隔条的class而非专属的deleted

品牌清理:浏览器标签页标题、未读数标题更新均硬编码"Tinode";LINK_CONTACT_US等三个链接常量
指向开源Tinode项目自己的域名(联系我们改为跟用户条款/隐私协议一致的toast,不再发起外部mailto)。

以及:空会话丢失.chat-box样式类、青少年模式PIN定位、若干收藏/文件类型展示细节。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-05 17:09:42 +08:00
co-authored by Claude Sonnet 5
parent 5e173c55e8
commit d759f67b11
24 changed files with 375 additions and 250 deletions
+154 -85
View File
@@ -1258,7 +1258,12 @@
.report-author {
display: grid;
grid-template-columns: 3rem minmax(0, 1fr) auto;
/* Same dead-avatar-column issue .report-cards article > header had (see its comment) and was
fixed for -- DailyReportDetail/MonthlyReportDetail (the only two renderers of this class)
never render an <img> here either, just the name+badge span and the status <em>. The unused
leading 3rem column squeezed the name into a column narrow enough to wrap "员工" onto its own
line, while the status badge landed in what should've been the name's share of the row. */
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 0.7rem;
}
@@ -2117,6 +2122,8 @@ input[type="email"],
input[type="password"],
input[type="search"],
input[type="tel"],
input[type="date"],
input[type="time"],
textarea {
height: 2.5rem;
border-bottom: 1px solid var(--clr-input-border);
@@ -2128,6 +2135,8 @@ input[type="text"],
input[type="email"],
input[type="password"],
input[type="tel"],
input[type="date"],
input[type="time"],
textarea {
width: 100%;
}
@@ -2137,7 +2146,9 @@ input[type="text"]:focus,
input[type="email"]:focus,
input[type="password"]:focus,
input[type="search"]:focus,
input[type="tel"]:focus {
input[type="tel"]:focus,
input[type="date"]:focus,
input[type="time"]:focus {
border-bottom: 1px solid var(--clr-input-border-focus);
box-shadow: 0 1px 0 var(--clr-input-shadow-focus);
}
@@ -3114,7 +3125,7 @@ img.channel {
}
/* Online-offline indicator positioned inside avatar-box */
.avatar-box .online, .offline {
.avatar-box .online, .avatar-box .offline {
position: absolute;
width: 0.75rem;
min-width: 0.75rem;
@@ -3582,9 +3593,15 @@ img.channel {
.select-checkbox.blue {
color: var(--clr-primary-accent);
}
/* Flexbox auto-margin trick: pins the checkbox to the far left of the row regardless of
whether the message itself is left- or right-aligned (docs/images/多选-选中对话框.png). */
.chat-box li .select-checkbox {
/* Flexbox auto-margin trick: pins the checkbox to the far left of the row for sent (.right)
messages, where it would otherwise cluster next to the bubble at the row's right edge along with
everything else justify-content:flex-end pulls there. Scoped to .right only -- applying it
unconditionally (as this rule originally did) claims 100% of the row's remaining space for the
auto margin regardless of alignment, which on a received (.left) message drags the avatar+bubble
that follow the checkbox all the way to the row's right edge instead of leaving them where
justify-content's default flex-start already puts them: immediately after the checkbox, at the
row's left edge (docs/images/多选-选中对话框.png). */
.chat-box li.right .select-checkbox {
margin-right: auto;
}
@@ -3637,12 +3654,29 @@ img.channel {
.batch-forward-confirm {
max-width: 24rem;
}
/* Also carries .forward-dialog for its base sheet chrome, but that class's own height:80% (right
for forward-dialog.jsx's long contact list) leaves this content-sized confirm sheet mostly empty
space below its buttons. Same (single-class) specificity as .forward-dialog itself, so a bare
.batch-forward-confirm{height:auto} would depend on which rule happens to come later in this
file to win -- the compound selector here beats it unconditionally instead. */
.forward-dialog.batch-forward-confirm {
height: auto;
}
.batch-forward-target {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.6rem 0;
}
/* .contact-box > li (2969 above) is more specific than the bare .batch-forward-target class above,
so its padding and its avatar-reserving divider (the gradient's gap starts at 3.5rem, assuming an
avatar occupies that space) win by default -- this row has no avatar, so the divider ends up
looking like it starts partway across the row. Reasserted here at matching specificity with a
full-width divider instead. */
.contact-box > li.batch-forward-target {
padding: 0.6rem 0;
background: linear-gradient(to right, var(--clr-background-extra-light-gray), var(--clr-background-extra-light-gray)) 0 100% / 100% 1px no-repeat, var(--clr-bright);
}
/* 从只读 div 改成可输入的留言框后,要显式清掉 input 的原生边框/宽度,否则会缩成一个小方框。 */
.batch-forward-comment {
box-sizing: border-box;
@@ -3900,7 +3934,18 @@ img.channel {
font-size: 1.6rem;
}
#send-message-panel .material-icons.secondary, .material-icons.disabled {
#send-message-panel .material-icons.secondary {
color: var(--clr-text-tertiary)!important;
}
/* Split out from the rule above -- it was originally a second, unscoped clause on it (a copy-paste
slip, matching the same .avatar-box .online/.offline mistake elsewhere in this file), which in
practice is the only rule greying out letter-tile.jsx's generic (no-letter) deleted-topic icon
(`<i class="material-icons {color} disabled">`, no wrapper class of its own to hang a scoped rule
off of) -- letter-tile.jsx's *other* deleted-avatar case (.lettertile.disabled) has its own real
rule and was never affected. Kept as an explicit, separately-scoped rule instead of silently
dropping a behavior nothing else provides. */
i.material-icons.disabled {
color: var(--clr-text-tertiary)!important;
}
@@ -4221,7 +4266,7 @@ button.full-screen {
min-width: unset;
}
.minimized button.full-screen {
.minimized:not(.audio-minimized) button.full-screen {
top: 1rem;
left: 1rem;
width: 2rem;
@@ -4236,7 +4281,7 @@ button.full-screen .material-icons {
font-size: x-large;
}
.minimized button.full-screen .material-icons {
.minimized:not(.audio-minimized) button.full-screen .material-icons {
font-size: large;
}
@@ -4362,7 +4407,7 @@ video {
margin-top: 0.5rem;
}
.caller-card .trusted-badge {
.caller-card .trusted-badge, .call-peer-card h1 .trusted-badge {
display: inline-block;
}
@@ -5497,29 +5542,12 @@ ul.tabbar li.active > a {
margin-right: 0;
}
.audio-hold {
flex: 1;
justify-content: center;
gap: 0.6rem;
}
.audio-hold canvas {
/* .compose-record-overlay (8588 below) replaced this holdMode presentation -- see
audio-recorder.jsx's holdMode render for why. */
.compose-record-overlay canvas {
width: 6rem;
margin-right: 0;
}
.audio-hold-hint {
color: var(--clr-text-tertiary);
font-size: 0.85rem;
}
.audio-hold.cancelling {
background-color: var(--clr-danger-text, #d93025);
}
.audio-hold.cancelling .duration,
.audio-hold.cancelling .audio-hold-hint {
color: #fff;
height: 1.8rem;
margin-top: 0.4rem;
}
.audio .duration {
@@ -5550,7 +5578,11 @@ ul.tabbar li.active > a {
display: block;
}
.audio-player div.playback {
/* Targets the wrapper div around canvas.playback + .timer (audio-player.jsx's non-short render) --
was accidentally written as div.playback, but "playback" is the canvas's own class, not this
wrapper's (which had no class at all), so this never matched anything; canvas+timer fell back to
block-level stacking (timer on its own line below the waveform) instead of sitting side by side. */
.audio-player .playback-row {
display: flex;
align-items: center;
justify-content: center;
@@ -6786,6 +6818,17 @@ div.toast.show {
opacity: 0;
}
/* .auth-pin-input's shared top:11rem was measured against .auth-pin-content's layout (6rem
top padding + an <h1> pushing .auth-pin-boxes further down) -- .youth-password-gate has no
title and only 3rem of top padding, so .auth-pin-boxes sits ~9.25rem higher here and the
invisible real input ended up entirely below the visible boxes (confirmed live: boxes at
86.7-140px, input at 210-243px, a real click on the boxes hit nothing and never refocused
the input). Re-anchored to this container's own shorter layout instead of reusing the other
page's absolute value. */
.youth-password-gate .auth-pin-input {
top: 1.75rem;
}
.auth-pin-content .auth-primary {
width: min(100%, 24rem);
margin-top: 3rem;
@@ -7056,6 +7099,7 @@ div.toast.show {
margin-top: 0.4rem;
overflow: hidden;
font-size: 0.78rem;
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -7552,22 +7596,6 @@ div.toast.show {
line-height: 1.55;
}
.system-notification-card p button {
display: inline;
width: auto;
min-width: 0;
height: auto;
min-height: 0;
margin: 0;
padding: 0;
border: 0;
box-shadow: none;
color: #1683f8;
background: transparent;
font: inherit;
text-transform: none;
}
.system-notification-card dl {
margin: 0;
}
@@ -9162,7 +9190,8 @@ div.toast.show {
.group-list button > span:nth-child(2),
.group-request-list article > div,
.group-admin-page article > span:nth-child(2),
.group-muted-list article > span:nth-child(2) {
.group-muted-list article > span:nth-child(2),
.group-member-picker > button > span:nth-child(2) {
display: flex;
min-width: 0;
flex-direction: column;
@@ -9353,15 +9382,6 @@ div.toast.show {
text-transform: none;
}
.people-search-result em {
padding: 0.4rem 0.8rem;
border-radius: 0.3rem;
color: #fff;
background: #398afa;
font-size: 0.78rem;
font-style: normal;
}
.people-qr-page main,
.group-qr-page main {
display: flex;
@@ -9684,8 +9704,12 @@ div.toast.show {
.people-settings section small,
.group-settings section small {
min-width: 0;
margin-left: auto;
overflow: hidden;
color: #949ba4;
text-overflow: ellipsis;
white-space: nowrap;
}
.people-switch,
@@ -10212,20 +10236,6 @@ div.toast.show {
line-height: 1.8;
}
.group-announcement-detail footer {
display: flex;
align-items: center;
gap: 0.7rem;
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #eceef1;
}
.group-announcement-detail footer > span {
display: flex;
flex-direction: column;
}
.group-announcement-detail > small {
display: block;
margin-top: 1rem;
@@ -10309,12 +10319,15 @@ div.toast.show {
flex: 0 0 3.6rem;
}
/* "添加"格:跟成员头像同样尺寸的浅色方块,视觉上与真实成员头像区分开 */
/* "添加"格:跟成员头像同样尺寸的浅色方块,视觉上与真实成员头像区分开。宽高本该跟上面那条规则
一致(3.6rem)——之前这里写成了3rem,两条规则特异性相同、这条排在后面,字面覆盖了 width/
height,但没盖到上面那条的 flex:0 0 3.6rem(这条完全没提 flex),容器又是 flex-direction:
column,主轴尺寸吃 flex-basis 不吃 height,实际渲成了 3rem 宽、3.6rem 高的长方形。 */
.group-member-tile-action {
display: grid;
place-items: center;
width: 3rem;
height: 3rem;
width: 3.6rem;
height: 3.6rem;
border-radius: 0.45rem;
color: #98a0aa;
background: #f0f2f5;
@@ -10569,11 +10582,6 @@ div.toast.show {
text-transform: none;
}
.group-member-picker > button > span:nth-child(2) {
display: flex;
flex-direction: column;
}
.group-check {
display: grid;
place-items: center;
@@ -10866,7 +10874,7 @@ div.toast.show {
.library-favorite-content > footer {
display: grid;
grid-template-columns: minmax(0, 1fr) auto 2rem;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 0.5rem;
margin-top: 0.65rem;
@@ -10959,6 +10967,32 @@ div.toast.show {
font-style: normal;
}
.library-link-text {
display: flex;
align-items: center;
gap: 0.6rem;
width: 100%;
min-width: 0;
height: auto;
min-height: 2.8rem;
margin: 0;
padding: 0.6rem 0.8rem;
border: 1px solid #eceef1;
border-radius: 0.4rem;
box-shadow: none;
color: var(--clr-primary-accent);
background: #fafbfc;
text-align: left;
text-transform: none;
line-height: 1.3;
}
.library-link-text span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Shared by VoiceCard/LinkText/MediaThumb's "removed" state (content-library-view.jsx) -- same
opacity + red label language as .library-file-card.removed/em above, but these three don't share
its grid layout (voice/media assume an icon+waveform or an image backdrop that isn't there once
@@ -10982,8 +11016,7 @@ div.toast.show {
}
.library-voice-card {
display: grid;
grid-template-columns: 2rem minmax(4rem, 12rem) auto;
display: flex;
align-items: center;
gap: 0.6rem;
width: min(100%, 20rem);
@@ -11501,8 +11534,39 @@ div.toast.show {
background: #fff;
}
.library-record-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
width: 100%;
min-width: 0;
height: auto;
margin: 0 0 0.4rem;
padding: 0;
border: 0;
box-shadow: none;
color: inherit;
background: transparent;
text-align: left;
text-transform: none;
}
.library-record-header strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 600;
}
.library-record-header small {
flex: none;
color: #9299a2;
font-size: 0.74rem;
}
.library-record-files .library-file-card,
.library-record-links .library-link-card {
.library-record-links .library-link-text {
margin-bottom: 0.7rem;
}
@@ -11851,6 +11915,10 @@ div.toast.show {
margin: 1.3rem 0 0.45rem;
color: #fff;
font-size: 1.55rem;
max-width: 100%;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.call-peer-card p {
@@ -12606,6 +12674,7 @@ div.toast.show {
#video-container.minimized {
flex-direction: row;
left: 0;
right: auto;
top: var(--caption-panel-height);
width: 100%;
height: 11rem;
+1 -1
View File
File diff suppressed because one or more lines are too long