This commit is contained in:
19
tests/mock-h5-browser-init.js
Normal file
19
tests/mock-h5-browser-init.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// Load with agent-browser --init-script when checking the local preview.
|
||||
if (location.hostname === '127.0.0.1' && location.port === '48138') {
|
||||
localStorage.setItem('token', 'mock-session');
|
||||
localStorage.setItem('identifier', 'A');
|
||||
localStorage.setItem('customerAssetPhoneState', JSON.stringify({
|
||||
type: 'object',
|
||||
data: {
|
||||
token: 'mock-session', identifier: 'A', customerPhone: '13800000001',
|
||||
assetPhoneBound: false, assetPhoneIsOwn: false, needBindPhone: false
|
||||
}
|
||||
}));
|
||||
const originalOpen = XMLHttpRequest.prototype.open;
|
||||
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
|
||||
if (typeof url === 'string' && url.includes('/api/c/v1/')) {
|
||||
url = location.origin + '/__mock/' + url.split('/api/c/v1/')[1];
|
||||
}
|
||||
return originalOpen.call(this, method, url, ...rest);
|
||||
};
|
||||
}
|
||||
29
tests/mock-h5-preview.mjs
Normal file
29
tests/mock-h5-preview.mjs
Normal file
@@ -0,0 +1,29 @@
|
||||
// Local-only visual QA: no request reaches the real backend.
|
||||
import { preview } from 'vite';
|
||||
|
||||
const server = await preview({
|
||||
configFile: false,
|
||||
build: { outDir: 'dist/build/h5' },
|
||||
preview: { host: '127.0.0.1', port: 48138, strictPort: true },
|
||||
plugins: [{
|
||||
name: 'mock-h5-api',
|
||||
configurePreviewServer(server) {
|
||||
server.middlewares.use((req, res, next) => {
|
||||
if (!req.url.startsWith('/__mock/')) return next();
|
||||
let data = {};
|
||||
if (req.url.startsWith('/__mock/asset/info')) data = {
|
||||
identifier: 'A', asset_type: 'card', bound_phone: '13800000001',
|
||||
current_package: '测试套餐', current_package_id: 1, current_package_usage_id: 1,
|
||||
real_total_mb: 1024, real_used_mb: 256,
|
||||
estimated_final_expires_at: '2026-10-13T16:00:00Z', days_until_final_expiry: 0, is_expiring: true,
|
||||
expiry_reminder: { remaining_days: 0, expires_at: '2026-10-13T16:00:00Z', level: 'red', level_name: '即将到期' }
|
||||
};
|
||||
if (req.url.startsWith('/__mock/notifications')) data = { count: 0, items: [], total: 0 };
|
||||
const conflict = req.url.startsWith('/__mock/auth/bind-phone');
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.end(JSON.stringify({ code: conflict ? 1192 : 0, msg: conflict ? '该资产已绑定手机号,如需更换请联系管理员' : 'success', data }));
|
||||
});
|
||||
}
|
||||
}]
|
||||
});
|
||||
server.printUrls();
|
||||
397
tests/october-h5.test.mjs
Normal file
397
tests/october-h5.test.mjs
Normal file
@@ -0,0 +1,397 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import vm from 'node:vm';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const require = createRequire(import.meta.url);
|
||||
const vue = require('vue');
|
||||
const plain = (value) => JSON.parse(JSON.stringify(value));
|
||||
const deferred = () => {
|
||||
let resolvePromise;
|
||||
const promise = new Promise(resolve => { resolvePromise = resolve; });
|
||||
return { promise, resolve: resolvePromise };
|
||||
};
|
||||
|
||||
// Execute actual store/API modules and page setup scripts with real Vue
|
||||
// reactivity. Only native uni APIs and lifecycle scheduling are substituted.
|
||||
function runtime(initial = {}) {
|
||||
const storage = new Map(Object.entries({ token: 'session-A', identifier: 'A', ...initial }));
|
||||
const calls = [];
|
||||
const requests = [];
|
||||
const mounted = [];
|
||||
const shown = [];
|
||||
const hidden = [];
|
||||
const unmounted = [];
|
||||
const timers = [];
|
||||
const sessionStorage = new Map();
|
||||
const api = {};
|
||||
let options = {};
|
||||
const uni = {
|
||||
getStorageSync: key => storage.get(key) ?? '',
|
||||
setStorageSync: (key, value) => storage.set(key, plain(value)),
|
||||
removeStorageSync: key => storage.delete(key),
|
||||
clearStorageSync: () => storage.clear(),
|
||||
request: request => { requests.push(request); },
|
||||
showToast: value => calls.push({ kind: 'toast', ...value }),
|
||||
showModal: value => calls.push({ kind: 'modal', ...value }),
|
||||
reLaunch: value => calls.push({ kind: 'reLaunch', ...value }),
|
||||
redirectTo: value => calls.push({ kind: 'redirectTo', ...value }),
|
||||
navigateTo: value => calls.push({ kind: 'navigateTo', ...value }),
|
||||
navigateBack: () => calls.push({ kind: 'navigateBack' })
|
||||
};
|
||||
const context = vm.createContext({
|
||||
uni, Intl, Date, URLSearchParams,
|
||||
console: { log() {}, error() {}, warn() {} },
|
||||
setTimeout: callback => { timers.push(callback); return timers.length; },
|
||||
setInterval: () => 1, clearInterval() {},
|
||||
getCurrentPages: () => [{ options }],
|
||||
sessionStorage: { getItem: key => sessionStorage.get(key) || '', removeItem: key => sessionStorage.delete(key), setItem: (key, value) => sessionStorage.set(key, value) },
|
||||
window: { location: { search: '', pathname: '/', hash: '' }, history: { replaceState() {} } },
|
||||
defineProps: () => ({}), defineEmits: () => () => {}
|
||||
});
|
||||
const cache = new Map();
|
||||
const synthetic = (key, values) => {
|
||||
if (!cache.has(key)) {
|
||||
cache.set(key, new vm.SyntheticModule(Object.keys(values), function () {
|
||||
for (const [name, value] of Object.entries(values)) this.setExport(name, value);
|
||||
}, { context, identifier: key }));
|
||||
}
|
||||
return cache.get(key);
|
||||
};
|
||||
function moduleFor(path, expose = []) {
|
||||
if (cache.has(path)) return cache.get(path);
|
||||
let source = readFileSync(path, 'utf8');
|
||||
if (path.endsWith('.vue')) {
|
||||
source = source.match(/<script setup>([\s\S]*?)<\/script>/)[1];
|
||||
if (expose.length) source += `\nexport { ${expose.join(', ')} };`;
|
||||
}
|
||||
const module = new vm.SourceTextModule(source, { context, identifier: path });
|
||||
cache.set(path, module);
|
||||
return module;
|
||||
}
|
||||
async function linker(specifier, parent) {
|
||||
if (specifier === 'vue') return synthetic('vue', {
|
||||
...vue,
|
||||
onMounted: cb => mounted.push(cb), onUnmounted: cb => unmounted.push(cb)
|
||||
});
|
||||
if (specifier === '@dcloudio/uni-app') return synthetic('uni-app', {
|
||||
onShow: cb => shown.push(cb), onHide: cb => hidden.push(cb), onUnload: cb => unmounted.push(cb)
|
||||
});
|
||||
if (specifier === '@/api/index.js') return synthetic('api-index', {
|
||||
...api, deviceApi: {}, hasActiveOrPendingPackage: async () => true
|
||||
});
|
||||
if (specifier === '@/utils/payment.js') return synthetic('payment', {
|
||||
consumePendingPaymentRefresh: () => false, PAYMENT_REFRESH_TARGETS: { HOME: 'home' }
|
||||
});
|
||||
if (specifier === '@/utils/wxsdk.js') return synthetic('wxsdk', {
|
||||
initWxConfig: async () => {}, wxScan: async () => {}, isInWechat: () => false
|
||||
});
|
||||
if (specifier === './env.js') return synthetic('env', { BASE_URL: 'https://example.invalid' });
|
||||
if (specifier.endsWith('.vue')) return synthetic(specifier, { default: {} });
|
||||
const path = specifier.startsWith('@/') ? resolve(root, specifier.slice(2)) : resolve(dirname(parent.identifier), specifier);
|
||||
return moduleFor(path);
|
||||
}
|
||||
async function load(path, expose) {
|
||||
const module = moduleFor(resolve(root, path), expose);
|
||||
if (module.status === 'unlinked') await module.link(linker);
|
||||
if (module.status === 'linked') await module.evaluate();
|
||||
return module.namespace;
|
||||
}
|
||||
const reply = (request, data = {}, code = 0, statusCode = 200, msg = 'success') => {
|
||||
request.success({ statusCode, data: { code, msg, data } });
|
||||
};
|
||||
return { storage, sessionStorage, context, calls, requests, mounted, shown, hidden, unmounted, timers, api, load, reply,
|
||||
setOptions: value => { options = value; }, tick: () => vue.nextTick() };
|
||||
}
|
||||
|
||||
test('phone binding modes and success updates distinguish all four cases', async () => {
|
||||
const r = runtime();
|
||||
const { getPhoneBindingMode: mode, getBindingSuccessState: success, optionalBoolean } = await r.load('utils/phone-bind.js');
|
||||
assert.equal(mode('', false), 'customer-asset');
|
||||
assert.equal(mode('13800000001', false), 'asset');
|
||||
assert.equal(mode('', true), 'customer');
|
||||
assert.equal(mode('13800000001', true), 'none');
|
||||
assert.equal(mode(null, false), 'unknown');
|
||||
assert.equal(optionalBoolean(undefined), null);
|
||||
assert.equal(optionalBoolean('false'), null);
|
||||
assert.equal(success({ customerPhone: '13800000001' }, '13900000002', 'asset').customerPhone, undefined);
|
||||
assert.equal(success({ customerPhone: '13800000001' }, '13900000002', 'asset').assetPhoneIsOwn, false);
|
||||
assert.equal(success({}, '13900000002', 'customer').assetPhoneBound, undefined);
|
||||
assert.equal(success({}, '13900000002', 'customer-asset').assetPhoneBound, true);
|
||||
});
|
||||
|
||||
test('login facts are persisted per token/asset; customer phone changes never migrate assets', async () => {
|
||||
const r = runtime({ phoneBindStatus: { A: { bound: true, phone: '13900000002' } } });
|
||||
const user = (await r.load('store/user.js')).useUserStore();
|
||||
assert.equal(user.state.assetPhoneBound, null);
|
||||
user.setLoginPhoneState({ asset_phone_bound: false, asset_phone_is_own: false, need_bind_phone: true });
|
||||
user.setCustomerPhone('13800000001');
|
||||
user.completePhoneBinding('13900000002', 'asset');
|
||||
assert.equal(user.state.customerPhone, '13800000001');
|
||||
assert.equal(user.state.assetPhoneBound, true);
|
||||
assert.equal(user.state.assetPhoneIsOwn, false);
|
||||
user.completeCustomerPhoneChange('13700000003');
|
||||
assert.equal(user.state.customerPhone, '13700000003');
|
||||
assert.equal(user.state.assetPhoneBound, true);
|
||||
assert.equal(user.state.assetPhoneIsOwn, null);
|
||||
const reloaded = runtime(Object.fromEntries(r.storage));
|
||||
assert.equal((await reloaded.load('store/user.js')).useUserStore().state.assetPhoneBound, true);
|
||||
user.setIdentifier('B');
|
||||
assert.equal(user.state.assetPhoneBound, null);
|
||||
assert.equal(user.state.customerPhone, null);
|
||||
assert.equal(r.storage.has('phoneBindStatus'), false);
|
||||
user.setToken('session-B');
|
||||
assert.equal(user.state.needBindPhone, null);
|
||||
user.clearUser();
|
||||
assert.equal(r.storage.has('customerAssetPhoneState'), false);
|
||||
});
|
||||
|
||||
test('login routing honors server guidance even with a phone bound to someone else', async () => {
|
||||
for (const required of [false, true]) {
|
||||
const r = runtime({ token: '' });
|
||||
r.sessionStorage.set('assetToken', 'mock-asset-token');
|
||||
r.context.window.location.search = '?code=mock-code';
|
||||
r.api.wechatApi = {};
|
||||
r.api.authApi = { wechatLogin: async () => ({ token: 'new-session', asset_phone_bound: true, asset_phone_is_own: false, need_bind_phone: required }) };
|
||||
const page = await r.load('pages/login/login.vue', ['handleWechatCallback']);
|
||||
await page.handleWechatCallback();
|
||||
assert.equal(r.calls.find(call => call.kind === 'redirectTo').url, required ? '/pages/bind/bind?fromLogin=true' : '/pages/index/index');
|
||||
const user = (await r.load('store/user.js')).useUserStore();
|
||||
assert.equal(user.state.assetPhoneBound, true);
|
||||
assert.equal(user.state.assetPhoneIsOwn, false);
|
||||
assert.equal(user.state.needBindPhone, required);
|
||||
}
|
||||
});
|
||||
|
||||
test('expiry boundaries and Shanghai rendering preserve server values, including zero', async () => {
|
||||
const r = runtime();
|
||||
const { normalizeExpiryReminder, getShanghaiDay, formatShanghaiExpiry } = await r.load('utils/expiry-reminder.js');
|
||||
for (const days of [0, 3, 4, 7, 8, 15]) {
|
||||
const level = days <= 3 ? 'red' : days <= 7 ? 'purple' : 'pink';
|
||||
assert.equal(normalizeExpiryReminder({ remaining_days: days, level }).remaining_days, days);
|
||||
}
|
||||
for (const value of [undefined, null, {}, { remaining_days: -1, level: 'red' }, { remaining_days: 16, level: 'pink' }]) {
|
||||
assert.equal(normalizeExpiryReminder(value), null);
|
||||
}
|
||||
assert.equal(getShanghaiDay(new Date('2026-10-12T15:59:59Z')), '2026-10-12');
|
||||
assert.equal(getShanghaiDay(new Date('2026-10-12T16:00:00Z')), '2026-10-13');
|
||||
assert.match(formatShanghaiExpiry('2026-10-12T16:00:00Z'), /2026.*10.*13.*00:00/);
|
||||
assert.equal(formatShanghaiExpiry('2026-10-13T12:30:00'), '2026-10-13 12:30:00');
|
||||
});
|
||||
|
||||
test('shared asset-info receiver keeps a child-first reminder and rejects stale sessions/phones', async () => {
|
||||
const r = runtime();
|
||||
const { assetApi } = await r.load('api/modules/asset.js');
|
||||
const user = (await r.load('store/user.js')).useUserStore();
|
||||
const asset = (await r.load('store/asset.js')).useAssetStore();
|
||||
const first = assetApi.getInfo('A');
|
||||
const second = assetApi.getInfo('A');
|
||||
r.reply(r.requests[0], { bound_phone: '13800000001', expiry_reminder: { remaining_days: 0, level: 'red', expires_at: '2026-10-13T16:00:00Z' } });
|
||||
await first;
|
||||
r.reply(r.requests[1], { bound_phone: '13800000001' });
|
||||
await second;
|
||||
assert.equal(asset.state.expiryReminder.reminder.remaining_days, 0);
|
||||
asset.markExpiryReminderShown();
|
||||
asset.dismissExpiryReminder();
|
||||
const repeated = assetApi.getInfo('A');
|
||||
r.reply(r.requests[2], { expiry_reminder: { remaining_days: 0, level: 'red' } });
|
||||
await repeated;
|
||||
assert.equal(asset.state.expiryReminder, null);
|
||||
const oldPhoneResponse = assetApi.getInfo('A');
|
||||
user.completeCustomerPhoneChange('13700000003');
|
||||
r.reply(r.requests[3], { bound_phone: '13800000001' });
|
||||
await oldPhoneResponse;
|
||||
assert.equal(user.state.customerPhone, '13700000003');
|
||||
const late = assetApi.getInfo('A');
|
||||
user.setIdentifier('B');
|
||||
r.reply(r.requests[4], { bound_phone: '13800000001', expiry_reminder: { remaining_days: 3, level: 'red' } });
|
||||
await late;
|
||||
assert.equal(asset.state.expiryReminder, null);
|
||||
assert.equal(user.state.customerPhone, null);
|
||||
const b = assetApi.getInfo('B');
|
||||
r.reply(r.requests[5], { expiry_reminder: { remaining_days: 8, level: 'pink' } });
|
||||
await b;
|
||||
assert.equal(asset.state.expiryReminder.identifier, 'B');
|
||||
});
|
||||
|
||||
test('notification APIs use POST read-all and retain PUT single-read without new parameters', async () => {
|
||||
const r = runtime();
|
||||
const { notificationApi } = await r.load('api/modules/notification.js');
|
||||
const all = notificationApi.markAllRead();
|
||||
assert.equal(r.requests[0].method, 'POST');
|
||||
assert.match(r.requests[0].url, /\/notifications\/read-all$/);
|
||||
r.reply(r.requests[0]); await all;
|
||||
const one = notificationApi.markRead(12);
|
||||
assert.equal(r.requests[1].method, 'PUT');
|
||||
assert.deepEqual(plain(r.requests[1].data), { id: 12 });
|
||||
r.reply(r.requests[1]); await one;
|
||||
const list = notificationApi.getList(1, 20);
|
||||
assert.deepEqual(plain(r.requests[2].data), { page: 1, page_size: 20 });
|
||||
r.reply(r.requests[2]); await list;
|
||||
});
|
||||
|
||||
test('1192 disables repeated submission, while 1184 keeps the backend error', async () => {
|
||||
for (const code of [1192, 1184]) {
|
||||
const r = runtime();
|
||||
const user = (await r.load('store/user.js')).useUserStore();
|
||||
user.setLoginPhoneState({ asset_phone_bound: false, asset_phone_is_own: false, need_bind_phone: false });
|
||||
user.setCustomerPhone('13800000001');
|
||||
let submits = 0;
|
||||
r.api.authApi = { bindPhone: async () => { submits++; throw { code, msg: '手机号已被其他客户绑定' }; } };
|
||||
r.api.assetApi = { getInfo: async () => ({ bound_phone: '13800000001' }) };
|
||||
const page = await r.load('pages/bind/bind.vue', ['bind', 'bindPhone', 'assetConflict']);
|
||||
await r.mounted[0]();
|
||||
page.bind.phone = '13900000002'; page.bind.code = '123456';
|
||||
await page.bindPhone();
|
||||
if (code === 1192) {
|
||||
await page.bindPhone();
|
||||
assert.equal(submits, 1);
|
||||
assert.equal(page.assetConflict.value, true);
|
||||
assert.match(r.calls.find(call => call.kind === 'modal').content, /联系管理员/);
|
||||
} else {
|
||||
assert.equal(page.assetConflict.value, false);
|
||||
assert.equal(r.calls.find(call => call.kind === 'toast').title, '手机号已被其他客户绑定');
|
||||
}
|
||||
assert.equal(user.state.assetPhoneBound, false);
|
||||
assert.equal(user.state.customerPhone, '13800000001');
|
||||
}
|
||||
});
|
||||
|
||||
test('asset-only binding accepts a different phone; mandatory binding does not consume asset info', async () => {
|
||||
const r = runtime();
|
||||
const user = (await r.load('store/user.js')).useUserStore();
|
||||
user.setLoginPhoneState({ asset_phone_bound: false, asset_phone_is_own: false, need_bind_phone: false });
|
||||
user.setCustomerPhone('13800000001');
|
||||
let submitted;
|
||||
r.api.authApi = { bindPhone: async (phone, code) => { submitted = { phone, code }; } };
|
||||
r.api.assetApi = { getInfo: async () => ({ bound_phone: '13800000001' }) };
|
||||
const page = await r.load('pages/bind/bind.vue', ['bind', 'bindPhone', 'bindingMode']);
|
||||
await r.mounted[0]();
|
||||
assert.equal(page.bindingMode.value, 'asset');
|
||||
page.bind.phone = '13900000002'; page.bind.code = '123456';
|
||||
await page.bindPhone();
|
||||
assert.deepEqual(submitted, { phone: '13900000002', code: '123456' });
|
||||
assert.equal(user.state.customerPhone, '13800000001');
|
||||
assert.equal(user.state.assetPhoneBound, true);
|
||||
for (const assetBound of [true, false]) {
|
||||
const mandatory = runtime();
|
||||
mandatory.setOptions({ fromLogin: 'true' });
|
||||
const mandatoryUser = (await mandatory.load('store/user.js')).useUserStore();
|
||||
mandatoryUser.setLoginPhoneState({ asset_phone_bound: assetBound, asset_phone_is_own: false, need_bind_phone: true });
|
||||
mandatory.api.authApi = { bindPhone: async () => {} };
|
||||
mandatory.api.assetApi = { getInfo: async () => { throw new Error('Mandatory page must not consume expiry reminder'); } };
|
||||
const mandatoryPage = await mandatory.load('pages/bind/bind.vue', ['bind', 'bindPhone']);
|
||||
await mandatory.mounted[0]();
|
||||
mandatoryPage.bind.phone = '13900000002'; mandatoryPage.bind.code = '123456';
|
||||
await mandatoryPage.bindPhone();
|
||||
assert.equal(mandatory.calls.find(call => call.kind === 'reLaunch').url, '/pages/login/login');
|
||||
assert.equal(mandatory.storage.get('identifier'), 'A');
|
||||
assert.equal(mandatory.storage.has('token'), false);
|
||||
}
|
||||
});
|
||||
|
||||
test('change-phone page updates only the confirmed customer phone', async () => {
|
||||
const r = runtime();
|
||||
const user = (await r.load('store/user.js')).useUserStore();
|
||||
user.setLoginPhoneState({ asset_phone_bound: true, asset_phone_is_own: true, need_bind_phone: false });
|
||||
user.setCustomerPhone('13800000001');
|
||||
r.api.assetApi = { getInfo: async () => ({ bound_phone: '13800000001' }) };
|
||||
r.api.authApi = { changePhone: async () => ({ phone: '13900000002' }) };
|
||||
const page = await r.load('pages/change-phone/change-phone.vue', ['form', 'submit']);
|
||||
await r.mounted[0]();
|
||||
page.form.oldCode = '123456'; page.form.newPhone = '13900000002'; page.form.newCode = '654321';
|
||||
await page.submit();
|
||||
assert.equal(user.state.customerPhone, '13900000002');
|
||||
assert.equal(user.state.assetPhoneBound, true);
|
||||
assert.equal(user.state.assetPhoneIsOwn, null);
|
||||
assert.equal(page.form.oldPhone, '13900000002');
|
||||
assert.equal(r.storage.has('phoneBindStatus'), false);
|
||||
});
|
||||
|
||||
test('home shows shared reminder once and excludes only expiry from old notification popups', async () => {
|
||||
const r = runtime();
|
||||
r.api.assetApi = { getInfo: async () => ({ identifier: 'A' }) };
|
||||
r.api.notificationApi = {
|
||||
getList: async () => ({ items: [{ id: 1, category: 'expiry', is_read: false }, { id: 2, category: 'approval', is_read: false }] }),
|
||||
getUnreadCount: async () => ({ count: 2 }), markRead: async () => {}
|
||||
};
|
||||
const asset = (await r.load('store/asset.js')).useAssetStore();
|
||||
asset.receiveExpiryReminder({ token: 'session-A', identifier: 'A' }, { remaining_days: 0, level: 'red' });
|
||||
const page = await r.load('pages/index/index.vue', ['visibleExpiryReminder', 'loadUnreadNotifications', 'notificationItems', 'closeExpiryReminder']);
|
||||
r.shown[0](); await r.tick();
|
||||
assert.equal(page.visibleExpiryReminder.value.remaining_days, 0);
|
||||
await page.loadUnreadNotifications();
|
||||
assert.deepEqual(plain(page.notificationItems.value).map(item => item.id), [2]);
|
||||
page.closeExpiryReminder();
|
||||
r.hidden[0](); r.shown[0](); await r.tick();
|
||||
assert.equal(page.visibleExpiryReminder.value, null);
|
||||
});
|
||||
|
||||
test('notification center refreshes server scope after read-all; failures keep unread items', async () => {
|
||||
for (const fail of [false, true]) {
|
||||
const r = runtime();
|
||||
let readAll = false;
|
||||
r.api.notificationApi = {
|
||||
getList: async () => ({ items: [{ id: 1, is_read: readAll, category: 'expiry' }], total: 1 }),
|
||||
getUnreadCount: async () => ({ count: readAll ? 0 : 1 }),
|
||||
markAllRead: async () => { if (fail) throw new Error('offline'); readAll = true; }
|
||||
};
|
||||
const page = await r.load('pages/notifications/notifications.vue', ['loadNotifications', 'loadUnreadCount', 'markAllRead', 'notifications', 'unreadCount']);
|
||||
await page.loadNotifications(); await page.loadUnreadCount();
|
||||
await page.markAllRead();
|
||||
assert.equal(page.notifications[0].is_read, !fail);
|
||||
assert.equal(page.unreadCount.value, fail ? 1 : 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('notification center rejects an old asset response after switching context', async () => {
|
||||
const r = runtime();
|
||||
const oldList = deferred();
|
||||
r.api.notificationApi = { getList: () => oldList.promise, getUnreadCount: async () => ({ count: 0 }) };
|
||||
const page = await r.load('pages/notifications/notifications.vue', ['loadNotifications', 'notifications']);
|
||||
const pending = page.loadNotifications();
|
||||
r.storage.set('identifier', 'B');
|
||||
oldList.resolve({ items: [{ id: 1, category: 'expiry' }], total: 1 });
|
||||
await pending;
|
||||
assert.equal(page.notifications.length, 0);
|
||||
});
|
||||
|
||||
test('asset A read-all does not locally mark asset B expiry notifications read', async () => {
|
||||
const r = runtime();
|
||||
const read = new Map([['A', false], ['B', false]]);
|
||||
r.api.notificationApi = {
|
||||
getList: async () => ({ items: [{ id: r.storage.get('identifier'), category: 'expiry', is_read: read.get(r.storage.get('identifier')) }], total: 1 }),
|
||||
getUnreadCount: async () => ({ count: read.get(r.storage.get('identifier')) ? 0 : 1 }),
|
||||
markAllRead: async () => read.set(r.storage.get('identifier'), true)
|
||||
};
|
||||
const page = await r.load('pages/notifications/notifications.vue', ['loadNotifications', 'loadUnreadCount', 'markAllRead', 'notifications', 'unreadCount']);
|
||||
await page.loadNotifications(); await page.loadUnreadCount(); await page.markAllRead();
|
||||
assert.equal(page.notifications[0].is_read, true);
|
||||
r.storage.set('identifier', 'B');
|
||||
await r.shown[0]();
|
||||
assert.equal(page.notifications[0].id, 'B');
|
||||
assert.equal(page.notifications[0].is_read, false);
|
||||
assert.equal(page.unreadCount.value, 1);
|
||||
});
|
||||
|
||||
test('401 clears phone/reminder state, but late 401 cannot log out a new session', async () => {
|
||||
const r = runtime();
|
||||
const request = (await r.load('utils/request.js')).default;
|
||||
const user = (await r.load('store/user.js')).useUserStore();
|
||||
const stale = request({ url: '/old' });
|
||||
user.setToken('session-B');
|
||||
r.reply(r.requests[0], {}, 1, 401);
|
||||
await assert.rejects(stale);
|
||||
assert.equal(user.state.token, 'session-B');
|
||||
user.setLoginPhoneState({ asset_phone_bound: true, need_bind_phone: false });
|
||||
const expired = request({ url: '/current' });
|
||||
r.reply(r.requests[1], {}, 1, 401);
|
||||
await assert.rejects(expired);
|
||||
assert.equal(user.state.token, '');
|
||||
assert.equal(user.state.assetPhoneBound, null);
|
||||
assert.equal(r.storage.has('customerAssetPhoneState'), false);
|
||||
});
|
||||
Reference in New Issue
Block a user