fix(signin): align check-in feedback with reference theme
This commit is contained in:
1 parent
1df2822f01
commit
8e9d3a7e49
7 files changed
+334
-65
No files matched your search
@@ -115,11 +115,13 @@ test('sign-in waits for main before binding and invokes the signin captcha', asy
|
||||
const button = {
|
||||
dataset: {},
|
||||
disabled: false,
|
||||
attrs: {},
|
||||
classList: { toggle() {} },
|
||||
setAttribute(name, value) { this.attrs[name] = value; },
|
||||
removeAttribute(name) { delete this.attrs[name]; },
|
||||
addEventListener(name, handler) { listeners[name] = handler; },
|
||||
querySelector(selector) { return selector === '[data-signin-label]' ? label : null; },
|
||||
getAttribute(name) {
|
||||
if (name === 'data-signin-idle-label') return '签到';
|
||||
if (name === 'data-signin-done-label') return '已签到';
|
||||
return '';
|
||||
},
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const source = fs.readFileSync(path.join(root, 'assets/js/signin.js'), 'utf8');
|
||||
|
||||
class Element {
|
||||
constructor(tag, html = '', className = '', labels = {}) {
|
||||
this.tagName = tag.toUpperCase();
|
||||
this._html = html;
|
||||
this.className = className;
|
||||
this.dataset = {};
|
||||
this.attrs = {};
|
||||
this.listeners = {};
|
||||
this.replaced = null;
|
||||
this.disabled = false;
|
||||
this.title = '';
|
||||
this._label = null;
|
||||
this.labels = labels;
|
||||
this.classList = {
|
||||
contains: (name) => this.className.split(/\s+/).includes(name),
|
||||
add: (name) => { if (!this.classList.contains(name)) this.className = (this.className + ' ' + name).trim(); },
|
||||
};
|
||||
this.parseLabel();
|
||||
}
|
||||
get innerHTML() { return this._html; }
|
||||
set innerHTML(value) { this._html = value; this.parseLabel(); }
|
||||
parseLabel() {
|
||||
const match = this._html.match(/<span data-signin-label>(.*?)<\/span>/);
|
||||
if (!match) { this._label = null; return; }
|
||||
const parent = this;
|
||||
let value = match[1];
|
||||
let hasHook = true;
|
||||
this._label = {
|
||||
get textContent() { return value; },
|
||||
set textContent(text) { value = text; parent._html = parent._html.replace(/<span data-signin-label>.*?<\/span>/, '<span' + (hasHook ? ' data-signin-label' : '') + '>' + text + '</span>'); },
|
||||
removeAttribute(name) { if (name === 'data-signin-label') { hasHook = false; parent._html = parent._html.replace(' data-signin-label', ''); } },
|
||||
};
|
||||
}
|
||||
querySelector(selector) { return selector === '[data-signin-label]' ? this._label : null; }
|
||||
addEventListener(name, callback) { (this.listeners[name] ||= []).push(callback); }
|
||||
getAttribute(name) { return this.attrs[name] ?? this.labels[name] ?? ''; }
|
||||
setAttribute(name, value) { this.attrs[name] = String(value); }
|
||||
removeAttribute(name) { delete this.attrs[name]; }
|
||||
replaceWith(node) { this.replaced = node; }
|
||||
click() { (this.listeners.click || []).forEach((callback) => callback({ preventDefault() {} })); }
|
||||
}
|
||||
|
||||
function makeButton(className, label, icon = '<i class="fa fa-calendar-check"></i>', doneLabel = '已签到') {
|
||||
return new Element('button', icon + '<span data-signin-label>' + label + '</span>', className, {
|
||||
'data-signin-done-label': doneLabel,
|
||||
});
|
||||
}
|
||||
|
||||
function deferred() {
|
||||
let resolve;
|
||||
let reject;
|
||||
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
function setup({ buttons = [makeButton('btn uc-header__signin-btn', '签到')], captchaSignin = false, today = Promise.resolve({ ok: true, status: 200, json: { enabled: true, signed: false, streak: 1, reward_if_signin: 2 } }), signin = Promise.resolve({ ok: true, status: 200, json: { message: '签到成功,积分 +2', balance: 12 } }), captchaRun } = {}) {
|
||||
const calls = [];
|
||||
const notices = [];
|
||||
const balance = { textContent: '10', title: '10' };
|
||||
const status = { innerHTML: '', textContent: '' };
|
||||
const modals = [];
|
||||
const listeners = {};
|
||||
const mount = { nodeType: 1 };
|
||||
const doc = {
|
||||
querySelectorAll(selector) {
|
||||
if (selector === '[data-signin-btn]') return buttons.filter((button) => !button.replaced);
|
||||
if (selector === '[data-credits-value]') return [balance];
|
||||
if (selector === '[data-signin-status]') return [status];
|
||||
return [];
|
||||
},
|
||||
querySelector(selector) { return selector === '[data-signin-captcha]' ? mount : null; },
|
||||
createElement(tag) { return new Element(tag); },
|
||||
};
|
||||
const jq = { off() { return this; }, on(event, callback) { listeners[event] = callback; return this; } };
|
||||
const window = {
|
||||
aksrtData: { captchaSignin, signinDetailsUrl: '/wp-json/aksrt/v1/signin/details' },
|
||||
AksrtModalRuntime: { open: (...args) => modals.push(args), openRemote: (...args) => modals.push(args) },
|
||||
AksrtRequest: { request(endpoint, options) { calls.push({ endpoint, options }); return endpoint === '/signin/today' ? today : signin; } },
|
||||
};
|
||||
const captchaCalls = [];
|
||||
if (captchaRun) window.AKSRTCaptcha = {
|
||||
run(scene, scope) { captchaCalls.push(['run', scene, scope]); return captchaRun; },
|
||||
reset(scene, scope) { captchaCalls.push(['reset', scene, scope]); },
|
||||
};
|
||||
vm.runInNewContext(source, { window, document: doc, jQuery: () => jq, notyf: (...args) => notices.push(args), Promise, JSON, Number, tbquire: (_deps, ready) => { ready(); } });
|
||||
return { buttons, calls, notices, listeners, window, captchaCalls, mount, balance, status, modals };
|
||||
}
|
||||
|
||||
const settle = () => new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
test('uses reference loading, success toast, and replaces all entry points with detail links', async () => {
|
||||
const first = makeButton('img-badge jb-yellow', '签到', '<i class="fa far fa-calendar-check ml3 mr6"></i>');
|
||||
const second = makeButton('btn uc-header__signin-btn', '签到', '<i class="fa fa-calendar-check"></i>', '今日已签到');
|
||||
const success = deferred();
|
||||
const h = setup({ buttons: [first, second], signin: success.promise });
|
||||
await settle();
|
||||
const originalOther = second.innerHTML;
|
||||
first.click();
|
||||
assert.equal(first.disabled, true);
|
||||
assert.equal(first.innerHTML, '<i class="loading mr6" aria-hidden="true"></i><text>请稍后</text>');
|
||||
assert.equal(first.getAttribute('aria-busy'), 'true');
|
||||
assert.ok(Object.hasOwn(first.attrs, 'disabled'));
|
||||
assert.equal(second.innerHTML, originalOther, 'non-clicked entries keep their content while the request runs');
|
||||
assert.equal(second.disabled, false);
|
||||
first.click();
|
||||
second.click();
|
||||
await settle();
|
||||
assert.equal(h.calls.filter((call) => call.endpoint === '/signin').length, 1);
|
||||
success.resolve({ ok: true, status: 200, json: { message: '签到成功,积分 +2', balance: 12, streak: 2, reward: 2 } });
|
||||
await settle();
|
||||
assert.deepEqual(h.notices.find((notice) => notice[1] === 'success')?.slice(0, 2), ['签到成功,积分 +2', 'success']);
|
||||
for (const original of [first, second]) {
|
||||
assert.equal(original.replaced?.getAttribute('data-aksrt-modal'), 'remote');
|
||||
assert.equal(original.replaced?.getAttribute('data-aksrt-modal-url'), '/wp-json/aksrt/v1/signin/details');
|
||||
assert.match(original.replaced.innerHTML, /已签到/);
|
||||
assert.doesNotMatch(original.replaced.innerHTML, /data-signin-label/);
|
||||
assert.match(original.replaced.className, /checkin-details-link/);
|
||||
assert.match(original.replaced.innerHTML, /fa-calendar-check/);
|
||||
assert.equal(Object.hasOwn(original.replaced.attrs, 'disabled'), false);
|
||||
assert.equal(original.replaced.getAttribute('aria-busy'), '');
|
||||
}
|
||||
assert.doesNotMatch(first.replaced.className, /is-done/);
|
||||
assert.match(second.replaced.innerHTML, /今日已签到/);
|
||||
assert.equal(second.replaced.className.includes('uc-header__signin-btn'), true);
|
||||
assert.equal(h.calls.filter((call) => call.endpoint === '/signin/today').length, 1, 'success does not issue another status request');
|
||||
assert.equal(h.modals.length, 0, 'success does not open a modal');
|
||||
assert.equal(h.balance.textContent, '12');
|
||||
assert.equal(h.status.innerHTML, '已签到 <strong>2</strong> 天 +2');
|
||||
});
|
||||
|
||||
test('keeps the profile done style on its replacement link', async () => {
|
||||
const button = makeButton('btn uc-header__signin-btn', '签到');
|
||||
const success = deferred();
|
||||
const h = setup({ buttons: [button], signin: success.promise });
|
||||
await settle();
|
||||
button.click();
|
||||
success.resolve({ ok: true, status: 200, json: { message: '签到成功', balance: 1 } });
|
||||
await settle();
|
||||
assert.match(button.replaced.className, /is-done/);
|
||||
});
|
||||
|
||||
test('restores complete button markup after a rejected sign-in response', async () => {
|
||||
const button = makeButton('img-badge jb-yellow', '签到', '<i class="fa far fa-calendar-check ml3 mr6"></i>');
|
||||
const initial = button.innerHTML;
|
||||
const h = setup({ buttons: [button], signin: Promise.resolve({ ok: false, status: 500, json: { message: '签到失败' } }) });
|
||||
await settle();
|
||||
button.click();
|
||||
await settle();
|
||||
assert.equal(button.innerHTML, initial);
|
||||
assert.equal(button.disabled, false);
|
||||
assert.equal(button.getAttribute('aria-busy'), '');
|
||||
assert.equal(button.replaced, null);
|
||||
});
|
||||
|
||||
test('preserves the existing main/captcha ordering and resets captcha after submission', async () => {
|
||||
const button = makeButton('btn', '签到');
|
||||
const success = deferred();
|
||||
const captcha = deferred();
|
||||
const h = setup({ buttons: [button], captchaSignin: true, signin: success.promise, captchaRun: captcha.promise });
|
||||
await settle();
|
||||
button.click();
|
||||
await settle();
|
||||
assert.deepEqual(h.captchaCalls[0]?.slice(0, 2), ['run', 'signin']);
|
||||
assert.equal(h.calls.some((call) => call.endpoint === '/signin'), false);
|
||||
captcha.resolve({ proof: 'ok' });
|
||||
await settle();
|
||||
assert.equal(h.calls.filter((call) => call.endpoint === '/signin').length, 1);
|
||||
success.resolve({ ok: true, status: 200, json: { message: '签到成功', balance: 1 } });
|
||||
await settle();
|
||||
assert.deepEqual(h.captchaCalls.map((call) => call[0]), ['run', 'reset']);
|
||||
});
|
||||
|
||||
test('allows retry after a rejected request', async () => {
|
||||
const button = makeButton('btn', '签到');
|
||||
const today = deferred();
|
||||
const h = setup({ buttons: [button], today: today.promise, signin: Promise.reject(Object.assign(new Error('network down'), { status: 0 })) });
|
||||
button.click();
|
||||
await settle();
|
||||
today.resolve({ ok: true, status: 200, json: { enabled: true, signed: false, streak: 2, reward_if_signin: 3 } });
|
||||
await settle();
|
||||
assert.equal(button.innerHTML.includes('请稍后'), false);
|
||||
assert.equal(button.disabled, false);
|
||||
assert.equal(button.replaced, null);
|
||||
button.click();
|
||||
await settle();
|
||||
assert.equal(h.calls.filter((call) => call.endpoint === '/signin').length, 2);
|
||||
});
|
||||
|
||||
test('keeps the loading button when a previous status read completes during submission', async () => {
|
||||
const button = makeButton('img-badge jb-yellow', '签到');
|
||||
const today = deferred();
|
||||
const signin = deferred();
|
||||
const h = setup({ buttons: [button], today: today.promise, signin: signin.promise });
|
||||
button.click();
|
||||
await settle();
|
||||
today.resolve({ ok: true, status: 200, json: { enabled: true, signed: true, streak: 2, reward_today: 3 } });
|
||||
await settle();
|
||||
assert.equal(button.replaced, null);
|
||||
assert.match(button.innerHTML, /loading mr6/);
|
||||
assert.equal(button.disabled, true);
|
||||
assert.equal(h.status.innerHTML, '');
|
||||
signin.resolve({ ok: true, status: 200, json: { message: '签到成功', balance: 1, streak: 2, reward: 3 } });
|
||||
await settle();
|
||||
assert.ok(button.replaced);
|
||||
});
|
||||
|
||||
test('ignores an old today response after successful sign-in', async () => {
|
||||
const button = makeButton('btn', '签到');
|
||||
const today = deferred();
|
||||
const signin = deferred();
|
||||
const h = setup({ buttons: [button], today: today.promise, signin: signin.promise });
|
||||
button.click();
|
||||
await settle();
|
||||
signin.resolve({ ok: true, status: 200, json: { message: '签到成功', balance: 1, streak: 3, reward: 2 } });
|
||||
await settle();
|
||||
today.resolve({ ok: true, status: 200, json: { enabled: true, signed: false, streak: 2, reward_if_signin: 3 } });
|
||||
await settle();
|
||||
assert.ok(button.replaced, 'success replaced the action with the details link');
|
||||
assert.match(button.replaced.innerHTML, /已签到/);
|
||||
assert.equal(h.status.innerHTML, '已签到 <strong>3</strong> 天 +2');
|
||||
});
|
||||
|
||||
test('leaves a disabled sign-in feature disabled without turning it into a signed state', async () => {
|
||||
const button = makeButton('btn', '签到');
|
||||
const h = setup({ buttons: [button], today: Promise.resolve({ ok: true, status: 200, json: { enabled: false, signed: false } }) });
|
||||
await settle();
|
||||
assert.equal(button.disabled, true);
|
||||
assert.equal(button.replaced, null);
|
||||
assert.doesNotMatch(button.className, /is-done/);
|
||||
assert.equal(button.innerHTML.includes('已签到'), false);
|
||||
});
|
||||
|
||||
test('does not send the request when captcha is absent or cancelled and keeps original markup', async () => {
|
||||
const missingButton = makeButton('btn', '签到');
|
||||
const missing = setup({ buttons: [missingButton], captchaSignin: true });
|
||||
missing.window.AKSRTCaptcha = undefined;
|
||||
// No mount is needed in the harness to verify the missing provider path.
|
||||
missingButton.click();
|
||||
await settle();
|
||||
assert.equal(missingButton.disabled, false);
|
||||
assert.equal(missing.calls.some((call) => call.endpoint === '/signin'), false);
|
||||
|
||||
const cancelledButton = makeButton('btn', '签到');
|
||||
const cancelled = setup({ buttons: [cancelledButton], captchaSignin: true, captchaRun: Promise.resolve(null) });
|
||||
const original = cancelledButton.innerHTML;
|
||||
cancelledButton.click();
|
||||
await settle();
|
||||
assert.equal(cancelledButton.innerHTML, original);
|
||||
assert.equal(cancelledButton.disabled, false);
|
||||
assert.equal(cancelled.calls.some((call) => call.endpoint === '/signin'), false);
|
||||
});
|
||||
|
||||
test('does not bind the same button twice through repeated auto_fun scans', async () => {
|
||||
const button = makeButton('btn', '签到');
|
||||
const h = setup({ buttons: [button] });
|
||||
await settle();
|
||||
h.listeners['auto_fun.aksrt-signin']();
|
||||
assert.equal(button.listeners.click.length, 1);
|
||||
button.click();
|
||||
await settle();
|
||||
assert.equal(h.calls.filter((call) => call.endpoint === '/signin').length, 1);
|
||||
});
|
||||
Reference in new issue
Block a user