'use strict'; /** * 文章页点赞/收藏的点击 UX 对齐参考主题: * 计数行内加载点 → 服务端回包 → 选中态 + lovestar 动画 + 提示。 */ const assert = require('node:assert/strict'); const test = require('node:test'); const { runScenario } = require('./article-actions-harness.cjs'); const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); test('点赞成功:请求中显示行内加载点,成功后更新计数并播放动画', async () => { const state = runScenario('like', { responses: [{ success: true, data: { active: true, count: 8 } }] }); const { likeBtn, toasts } = state; likeBtn.click(); // 请求尚未返回时:按钮禁用 + 计数换成加载点。 assert.equal(likeBtn.disabled, true, '请求中应禁用按钮,避免重复提交'); const pending = likeBtn.querySelectorAll('count')[0]; assert.match(pending.innerHTML, /loading zts/, '请求中计数应替换为行内加载点'); await flush(); assert.equal(likeBtn.disabled, false, '请求结束后按钮恢复'); assert.ok(likeBtn.classList.contains('actived'), '成功后应带参考主题的 actived 类'); assert.ok(likeBtn.classList.contains('action-animation'), '成功后应触发 lovestar 动画'); assert.equal(pending.textContent, '8', '计数应更新为服务端返回值'); assert.equal(pending.dataset.baseCount, '8'); assert.deepEqual(toasts, [{ msg: '已赞!感谢您的支持', type: 'success' }]); }); test('重复点赞(服务端状态未变化):不播放动画,给出 warning 提示', async () => { const state = runScenario('like-duplicate', { // 首次成功点亮,第二次服务端返回相同状态与计数。 responses: [ { success: true, data: { active: true, count: 8 } }, { success: true, data: { active: true, count: 8 } } ] }); const { likeBtn, toasts } = state; likeBtn.click(); await flush(); assert.ok(likeBtn.classList.contains('action-animation')); // 清掉第一次的动画类,验证第二次点击不会再被重新加上。 likeBtn.classList.remove('action-animation'); likeBtn.click(); await flush(); assert.equal(likeBtn.classList.contains('action-animation'), false, '状态未变化时不应重放动画'); assert.ok(likeBtn.classList.contains('actived'), '仍保持已赞状态'); assert.deepEqual(toasts[1], { msg: '已赞过此文章了!', type: 'warning' }); }); test('取消点赞:移除 actived 与动画类,计数回退', async () => { const state = runScenario('unlike', { responses: [ { success: true, data: { active: true, count: 8 } }, { success: true, data: { active: false, count: 7 } } ] }); const { likeBtn, toasts } = state; likeBtn.click(); await flush(); likeBtn.click(); await flush(); assert.equal(likeBtn.classList.contains('actived'), false); assert.equal(likeBtn.classList.contains('action-animation'), false); assert.equal(likeBtn.querySelectorAll('count')[0].textContent, '7'); assert.deepEqual(toasts[1], { msg: '点赞已取消', type: 'warning' }); }); test('收藏与点赞共用同一套交互,收藏文案沿用参考主题', async () => { const state = runScenario('favorite', { responses: [{ success: true, data: { active: true, count: 4 } }] }); const { favBtn, toasts } = state; favBtn.click(); assert.equal(favBtn.disabled, true); assert.match(favBtn.querySelectorAll('count')[0].innerHTML, /loading zts/); await flush(); assert.ok(favBtn.classList.contains('actived')); assert.ok(favBtn.classList.contains('action-animation')); assert.equal(favBtn.querySelectorAll('count')[0].textContent, '4'); assert.deepEqual(toasts, [{ msg: '已收藏此文章', type: 'success' }]); }); test('请求失败:还原计数内容并恢复按钮,提示 danger', async () => { const state = runScenario('failure', { responses: [{ success: false, data: { message: '请先登录' } }] }); const { likeBtn, toasts } = state; likeBtn.click(); await flush(); assert.equal(likeBtn.disabled, false); assert.equal(likeBtn.classList.contains('actived'), false); assert.equal(likeBtn.querySelectorAll('count')[0].textContent, '7', '失败后应还原原计数'); assert.deepEqual(toasts, [{ msg: '请先登录', type: 'danger' }]); }); test('服务端计数被并发修正:即使状态未变也更新计数并播放动画', async () => { const state = runScenario('concurrent', { responses: [ { success: true, data: { active: true, count: 8 } }, // 别人也点了赞:状态仍是已赞,但计数变成 12。 { success: true, data: { active: true, count: 12 } } ] }); const { likeBtn, toasts } = state; likeBtn.click(); await flush(); likeBtn.classList.remove('action-animation'); likeBtn.click(); await flush(); assert.equal(likeBtn.querySelectorAll('count')[0].textContent, '12', '应同步服务端的真实计数'); assert.ok(likeBtn.classList.contains('action-animation'), '计数变化时仍应播放动画'); assert.deepEqual(toasts[1], { msg: '已赞!感谢您的支持', type: 'success' }); }); test('游客收藏被拦:还原计数并拉起站点登录弹窗', async () => { const { el } = require('./article-actions-harness.cjs'); let loginClicks = 0; const state = runScenario('guest-favorite', { responses: [{ success: false, data: { message: '请先登录', login: true } }] }, { decorate(state) { const loginTrigger = el('a', { class: 'signin-loader' }); loginTrigger.addEventListener('click', () => { loginClicks += 1; }); state.body.appendChild(loginTrigger); } }); state.favBtn.click(); await flush(); assert.equal(state.favBtn.disabled, false); assert.equal(state.favBtn.classList.contains('actived'), false); assert.equal(state.favBtn.querySelectorAll('count')[0].textContent, '3', '失败后应还原原计数'); assert.deepEqual(state.toasts, [{ msg: '请先登录', type: 'danger' }]); assert.equal(loginClicks, 1, '游客收藏应打开登录弹窗,与参考主题一致'); }); test('页面内多个同款按钮(正文 + 底部栏)保持状态同步', async () => { const { el } = require('./article-actions-harness.cjs'); const state = runScenario('mirror', { responses: [{ success: true, data: { active: true, count: 9 } }] }, { // 绑定前就存在的镜像按钮(正文 + 底部栏),与真实页面一致。 decorate(state) { const mirror = el('button', { class: 'tabbar-item action-like' }); mirror.dataset.postId = '42'; mirror.dataset.initialActive = '0'; mirror.appendChild(el('count', { 'data-stat-count': 'likes', 'data-base-count': '7' }, '7')); state.body.appendChild(mirror); state.mirror = mirror; } }); state.likeBtn.click(); await flush(); assert.ok(state.mirror.classList.contains('actived'), '镜像按钮应同步选中态'); assert.equal(state.mirror.querySelectorAll('count')[0].textContent, '9'); });