98 lines
5.3 KiB
JavaScript
98 lines
5.3 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { spawnSync } = require('node:child_process');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
|
|
const root = path.resolve(__dirname, '..', '..');
|
|
const harness = path.join(__dirname, 'user-badges-modal-harness.php');
|
|
|
|
function render() {
|
|
const result = spawnSync('php', [harness], { encoding: 'utf8', cwd: root });
|
|
assert.equal(result.status, 0, `PHP modal harness failed:\n${result.stderr || result.stdout}`);
|
|
return JSON.parse(result.stdout);
|
|
}
|
|
|
|
function assertBefore(html, first, second, message, from = 0) {
|
|
const firstIndex = html.indexOf(first, from);
|
|
const secondIndex = html.indexOf(second, from);
|
|
assert.notEqual(firstIndex, -1, `missing ${first}`);
|
|
assert.notEqual(secondIndex, -1, `missing ${second}`);
|
|
assert.ok(firstIndex < secondIndex, message);
|
|
}
|
|
|
|
test('overview matches the reference header, safely renders labels and places owned badges first', () => {
|
|
const cases = render();
|
|
const html = cases.self.html;
|
|
|
|
assert.match(html, /modal-colorful-header colorful-bg c-blue/);
|
|
assert.match(html, /class="avatar-img"[^>]*>[\s\S]*?width="38"/);
|
|
assert.doesNotMatch(html, /<a[^>]*>\s*<img[^>]*avatar-img/);
|
|
assert.match(html, /medal-list-box[^>]*flex[^>]*at[^>]*hh|medal-list-box[^>]*hh[^>]*flex/);
|
|
const listStart = html.indexOf('mini-scrollbar scroll-y max-vh5');
|
|
assertBefore(html, '自动已得', '手动已得', 'owned medals keep the reference category order', listStart);
|
|
assertBefore(html, '手动已得', '手动未得', 'owned medals render before unearned medals', listStart);
|
|
assert.match(html, /类别 <svg onload=alert\(1\)>/);
|
|
assert.match(html, /手动未得 <b>危险<\/b>/);
|
|
assert.match(cases.grant_admin.html, /说明 <script>bad\(\)<\/script>/);
|
|
assert.doesNotMatch(html, /<script>bad\(\)<\/script>|<svg onload=alert\(1\)>|<b>危险<\/b>/);
|
|
assert.match(html, /data-aksrt-modal-height="201"|data-height="201"/);
|
|
assert.match(html, /data-aksrt-modal-new="true"|new="new"/);
|
|
assert.match(html, /data-aksrt-modal-mobile-bottom="true"|mobile-bottom="true"/);
|
|
});
|
|
|
|
test('own and other-user overviews apply the ownership visibility rule for ordinary users and administrators', () => {
|
|
const cases = render();
|
|
for (const key of ['other', 'admin_other']) {
|
|
const html = cases[key].html;
|
|
assert.match(html, /自动已得/);
|
|
assert.match(html, /手动已得/);
|
|
assert.doesNotMatch(html, /手动未得|自动未得/);
|
|
}
|
|
assert.match(cases.other.html, /Other <img src=x onerror=alert\(1\)>/);
|
|
assert.doesNotMatch(cases.other.html, /<img src=x onerror=alert\(1\)>/);
|
|
assert.match(cases.admin_self.html, /授予徽章/);
|
|
assert.match(cases.admin_self.html, /data-aksrt-modal-height="240"|data-height="240"/);
|
|
assert.match(cases.admin_self.html, /data-aksrt-modal-mobile-bottom="true"|mobile-bottom="true"/);
|
|
});
|
|
|
|
test('single-badge details show ownership state and only the permitted action', () => {
|
|
const cases = render();
|
|
assert.match(cases.single_owned_self.html, /2026-02-03 04:05:06 获得/);
|
|
assert.match(cases.single_owned_self.html, /data-aksrt-badge-action="wear"/);
|
|
assert.match(cases.single_unowned_self.html, /暂未获得/);
|
|
assert.doesNotMatch(cases.single_unowned_self.html, /data-aksrt-badge-action=/);
|
|
assert.match(cases.single_wearing_self.html, /当前佩戴此徽章/);
|
|
assert.doesNotMatch(cases.single_wearing_self.html, /data-aksrt-badge-action="wear"/);
|
|
|
|
assert.match(cases.single_manual_admin.html, /收回此徽章/);
|
|
assert.match(cases.single_manual_admin.html, /data-aksrt-badge-action="revoke"/);
|
|
assert.doesNotMatch(cases.single_manual_admin.html, /2026-03-04 05:06:07 获得|暂未获得/);
|
|
assert.doesNotMatch(cases.single_auto_admin.html, /data-aksrt-badge-action="revoke"|收回此徽章/);
|
|
assert.match(cases.single_auto_unowned_admin.html, /参数传入错误或没有此徽章/);
|
|
assert.doesNotMatch(cases.single_auto_unowned_admin.html, /data-aksrt-badge-action=/);
|
|
assert.match(cases.single_owned_self.html, /data-aksrt-modal-close/);
|
|
});
|
|
|
|
test('grant modal preserves configured manual-badge order, actions, mobile sizing, and illustrated empty state', () => {
|
|
const cases = render();
|
|
const html = cases.grant_admin.html;
|
|
|
|
assert.match(html, /手动未得/);
|
|
assert.match(html, /手动已得/);
|
|
assert.doesNotMatch(html, /自动已得|自动未得/);
|
|
assertBefore(html, '手动未得', '手动已得', 'grant catalog follows the configured order');
|
|
assert.match(html, /data-aksrt-badge-action="grant"/);
|
|
assert.match(html, /data-aksrt-badge-action="revoke"/);
|
|
const css = fs.readFileSync(path.join(root, 'assets/css/main.css'), 'utf8');
|
|
assert.match(css, /\.medal-list\s*\{[^}]*width:\s*calc\(20%\s*-\s*14px\)[^}]*margin:\s*10px\s+7px/s);
|
|
assert.match(css, /@media\s*\(max-width:\s*767px\)[\s\S]*?\.medal-list\s*\{[^}]*width:\s*calc\(25%\s*-\s*14px\)/);
|
|
assert.match(css, /\.avatar-img\s*\{[^}]*--this-size:\s*38px/s);
|
|
const badgeCss = fs.readFileSync(path.join(root, 'assets/css/frontend/medal.css'), 'utf8');
|
|
assert.doesNotMatch(badgeCss, /\.medal-list-box\s*\{|\.medal-card__name|\.medal-card \.medal-icon/);
|
|
|
|
const empty = cases.illustrated_empty;
|
|
assert.match(empty.html, /aksrt-test-null/);
|
|
assert.ok(empty.null_calls.some((call) => call[1] === 30 && call[2] === 'null-2.svg'));
|
|
});
|