156 lines
5.8 KiB
JavaScript
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);
|
|
});
|