113 lines
5.6 KiB
JavaScript
113 lines
5.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { createHash } from 'node:crypto';
|
|
import { readFile, stat } from 'node:fs/promises';
|
|
import test from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { createJiti } from 'jiti';
|
|
|
|
globalThis.__APP_VERSION__ = 'test';
|
|
globalThis.window = { addEventListener() {}, removeEventListener() {}, focus() {} };
|
|
globalThis.document = { visibilityState: 'hidden', addEventListener() {}, title: '' };
|
|
globalThis.localStorage = { getItem() { return null; }, setItem() {}, removeItem() {} };
|
|
window.localStorage = localStorage;
|
|
|
|
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
const jiti = createJiti(import.meta.url, { alias: { '@': `${root}src` } });
|
|
const source = (path) => readFile(new URL(`../${path}`, import.meta.url), 'utf8');
|
|
let soundAudio;
|
|
|
|
test('display brand changes while the Tinode client identifier stays MeetingIM', async () => {
|
|
const { BASE_APP_NAME, APP_NAME } = await jiti.import('../src/lib/config.ts');
|
|
assert.equal(BASE_APP_NAME, '迅工捷使');
|
|
assert.equal(APP_NAME, 'MeetingIM/test');
|
|
assert.match(await source('index.html'), /<title>迅工捷使<\/title>/);
|
|
assert.match(await source('src/features/auth/TenantEntryPage.tsx'), />\{BASE_APP_NAME\}<\/strong>/);
|
|
const system = await source('src/features/system/SystemMessagesView.tsx');
|
|
assert.equal((system.match(/迅工捷使 安全团队/g) ?? []).length, 2);
|
|
});
|
|
|
|
test('login uses the official icon and current mobile copy without unsupported links', async () => {
|
|
const login = await source('src/features/auth/LoginPage.tsx');
|
|
const tenant = await source('src/features/auth/TenantEntryPage.tsx');
|
|
assert.equal((login.match(/src="\/app-icon\.png"/g) ?? []).length, 2);
|
|
assert.match(tenant, /src="\/app-icon\.png"/);
|
|
assert.doesNotMatch(login, />\s*M\s*</);
|
|
assert.doesNotMatch(tenant, />\s*M\s*</);
|
|
assert.match(login, /单聊、群聊、通讯录、文件与收藏、音视频通话,消息与回执在手机端保持一致。/);
|
|
for (const stale of ['下载桌面客户端', 'iOS / Android', '帮助中心']) assert.doesNotMatch(login, new RegExp(stale));
|
|
});
|
|
|
|
test('official image and supplied sound are copied byte for byte and used for icons', async () => {
|
|
const digest = async (path) => createHash('sha256').update(await readFile(`${root}public/${path}`)).digest('hex');
|
|
assert.equal(await digest('app-icon.png'), 'e5d1487e61b70b0df1574b3809ab80072c7e6aecb1231c7dadac2f4cc13be7b6');
|
|
assert.equal(await digest('message-notification.mp3'), 'f0d9e1db55d578504b54b752b113b2c2ca91937594edc19247c0f2a3947a9654');
|
|
const html = await source('index.html');
|
|
const tab = await source('src/lib/notifications/tabIndicator.ts');
|
|
const push = await source('src/lib/notifications/push.ts');
|
|
assert.match(html, /rel="icon" type="image\/png" href="\/app-icon\.png"/);
|
|
assert.match(tab, /FAVICON_HREF = '\/app-icon\.png'/);
|
|
assert.match(push, /icon: '\/app-icon\.png'/);
|
|
await assert.rejects(stat(`${root}public/favicon.svg`), { code: 'ENOENT' });
|
|
});
|
|
|
|
test('sound is lazy, rewinds on repeat, and ignores sync and async playback failures', async () => {
|
|
const sound = await jiti.import('../src/lib/notifications/sound.ts');
|
|
assert.equal(typeof sound.playMessageNotificationSound, 'function');
|
|
const played = [];
|
|
globalThis.Audio = class {
|
|
constructor(src) { this.src = src; this.currentTime = 7; played.push(this); }
|
|
play() { return Promise.resolve(); }
|
|
};
|
|
assert.equal(played.length, 0);
|
|
sound.playMessageNotificationSound();
|
|
sound.playMessageNotificationSound();
|
|
assert.equal(played.length, 1);
|
|
soundAudio = played[0];
|
|
assert.equal(played[0].src, '/message-notification.mp3');
|
|
assert.equal(played[0].currentTime, 0);
|
|
played[0].play = () => { throw new Error('blocked'); };
|
|
assert.doesNotThrow(() => sound.playMessageNotificationSound());
|
|
played[0].play = () => Promise.reject(new Error('blocked'));
|
|
assert.doesNotThrow(() => sound.playMessageNotificationSound());
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
});
|
|
|
|
test('incoming alerts gate own, visible and muted messages, but sound needs no notification permission', async () => {
|
|
const { notifyIncomingMessage } = await jiti.import('../src/lib/notifications/push.ts');
|
|
assert.equal(typeof notifyIncomingMessage, 'function');
|
|
const { useSessionStore } = await jiti.import('../src/stores/session.ts');
|
|
const { setActiveConversationTopic } = await jiti.import('../src/lib/tinode/topics.ts');
|
|
useSessionStore.setState({ myUserId: 'me' });
|
|
let plays = 0;
|
|
soundAudio.play = () => { plays++; return Promise.resolve(); };
|
|
let notifications = 0;
|
|
globalThis.Notification = class {
|
|
static permission = 'default';
|
|
constructor() { notifications++; }
|
|
};
|
|
const contact = { topic: 'p2p-test', public: { fn: '朋友' }, private: {} };
|
|
notifyIncomingMessage('p2p-test', { from: 'me' }, contact);
|
|
setActiveConversationTopic('p2p-test');
|
|
document.visibilityState = 'visible';
|
|
notifyIncomingMessage('p2p-test', { from: 'peer' }, contact);
|
|
setActiveConversationTopic(null);
|
|
document.visibilityState = 'hidden';
|
|
notifyIncomingMessage('p2p-test', { from: 'peer' }, { ...contact, private: { mute_notifications: true } });
|
|
assert.equal(plays, 0);
|
|
notifyIncomingMessage('p2p-test', { from: 'peer' }, contact);
|
|
assert.equal(plays, 1);
|
|
assert.equal(notifications, 0);
|
|
delete globalThis.Notification;
|
|
notifyIncomingMessage('p2p-test', { from: 'peer' }, contact);
|
|
assert.equal(plays, 2);
|
|
assert.equal(notifications, 0);
|
|
globalThis.Notification = class {
|
|
static permission = 'granted';
|
|
constructor() { notifications++; }
|
|
};
|
|
notifyIncomingMessage('p2p-test', { from: 'peer' }, contact);
|
|
assert.equal(plays, 3);
|
|
assert.equal(notifications, 1);
|
|
});
|