Files
Friendship-and-chat-uniapp/src/common/__tests__/jpush-permission.test.mjs
T
2026-11-11 19:38:29 +08:00

156 lines
5.8 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import vm from 'node:vm';
const GUIDE_SHOWN_KEY = 'chat-jpush-notification-guide-shown';
const settle = () => new Promise((resolve) => setImmediate(resolve));
/** 与现有推送测试相同,使用隔离 VM 注入 uni 和原生接口。 */
async function fixture(options = {}) {
const storage = options.storage || new Map([['token', 'test-token']]);
const state = {
status: options.status ?? 0,
checks: 0,
settings: 0,
modals: [],
};
const nativePush = {
isNotificationEnabled() {
state.checks++;
if (options.checkFails) throw new Error('Native unavailable');
return state.status;
},
goToAppNotificationSettings() {
state.settings++;
if (options.settingsFail) throw new Error('Settings unavailable');
},
};
const context = vm.createContext({
...(options.native === false ? {} : { plus: { runtime: {} } }),
uni: {
getDeviceInfo: () => ({ platform: options.platform || 'android' }),
getStorageSync: (key) => storage.get(key),
setStorageSync: (key, value) => storage.set(key, value),
showModal(modal) {
state.modals.push(modal);
if (options.manualModal) return;
if (options.modalFails) modal.fail();
else modal.success({ confirm: options.confirm ?? false });
},
},
});
let source = await readFile(new URL('../jpush-permission.js', import.meta.url), 'utf8');
if (options.h5Build) {
source = source.replace(/\/\/ #ifdef APP-PLUS[\s\S]*?\/\/ #endif/g, '');
}
source = source.replace(/^export const /gm, 'const ');
vm.runInContext(`${source}\nglobalThis.check = checkPushNotificationPermission;`, context);
return { check: () => context.check(nativePush), nativePush, state, storage };
}
test('enabled notifications continue without a guide or installation marker', async () => {
const { check, state, storage } = await fixture({ status: 1 });
assert.equal(await check(), true);
assert.equal(state.checks, 1);
assert.equal(state.modals.length, 0);
assert.equal(storage.has(GUIDE_SHOWN_KEY), false);
});
test('confirmation opens official notification settings once and foreground return rechecks', async () => {
const { check, state, storage } = await fixture({ confirm: true });
assert.equal(await check(), false);
assert.equal(state.modals.length, 1);
assert.equal(state.modals[0].title, '开启消息通知');
assert.equal(state.modals[0].confirmText, '去开启');
assert.equal(state.modals[0].cancelText, '暂不开启');
assert.equal(state.settings, 1);
assert.equal(storage.get(GUIDE_SHOWN_KEY), true);
state.status = 1;
assert.equal(await check(), true);
state.status = 0;
assert.equal(await check(), false);
assert.equal(state.checks, 3);
assert.equal(state.modals.length, 1);
assert.equal(state.settings, 1);
});
test('declining the guide persists across reload and account changes', async () => {
const first = await fixture();
assert.equal(await first.check(), false);
first.storage.set('token', 'second-account-token');
assert.equal(await first.check(), false);
assert.equal(first.state.modals.length, 1);
assert.equal(first.state.settings, 0);
const reloaded = await fixture({ storage: first.storage });
assert.equal(await reloaded.check(), false);
assert.equal(reloaded.state.modals.length, 0);
});
test('overlapping login and foreground checks share a single modal', async () => {
const { check, state } = await fixture({ manualModal: true });
const first = check();
const second = check();
assert.equal(first, second);
await settle();
assert.equal(state.checks, 1);
assert.equal(state.modals.length, 1);
state.modals[0].success({ confirm: false });
assert.equal(await first, false);
});
test('login completion can continue while the user has not answered', async () => {
const { check, state } = await fixture({ manualModal: true });
void check();
await settle();
assert.equal(state.modals.length, 1);
state.modals[0].success({ confirm: false });
await settle();
});
test('logged-out applications and iOS do not query Android notification APIs', async () => {
for (const options of [{ storage: new Map() }, { platform: 'ios' }]) {
const { check, state } = await fixture(options);
assert.equal(await check(), null);
assert.equal(state.checks, 0);
assert.equal(state.modals.length, 0);
}
});
test('Android browsers and preprocessed H5 builds do not access the native module', async () => {
for (const options of [{ native: false }, { h5Build: true }]) {
const { check, state } = await fixture(options);
assert.equal(await check(), null);
assert.equal(state.checks, 0);
assert.equal(state.modals.length, 0);
}
});
test('unknown native status and query errors do not consume the one-time guide', async () => {
for (const options of [{ status: -1 }, { checkFails: true }]) {
const { check, state, storage } = await fixture(options);
assert.equal(await check(), null);
assert.equal(state.modals.length, 0);
assert.equal(storage.has(GUIDE_SHOWN_KEY), false);
}
});
test('modal and settings failures resolve safely without repeating the guide', async () => {
for (const options of [{ modalFails: true }, { confirm: true, settingsFail: true }]) {
const { check, state } = await fixture(options);
await check();
await check();
assert.equal(state.modals.length, 1);
}
});
test('logout while the guide is open prevents a later settings launch', async () => {
const { check, state, storage } = await fixture({ manualModal: true });
const pending = check();
await settle();
storage.delete('token');
state.modals[0].success({ confirm: true });
assert.equal(await pending, false);
assert.equal(state.settings, 0);
});