@tencentcloud/web-push 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +100 -92
  3. package/{dist/index.esm.js → index.esm.js} +1 -1
  4. package/{dist/index.umd.js → index.umd.js} +1 -1
  5. package/package.json +9 -47
  6. package/sw.js +1 -0
  7. package/dist/sw.js +0 -1
  8. package/src/__tests__/index.test.ts +0 -120
  9. package/src/__tests__/integration.test.ts +0 -285
  10. package/src/__tests__/setup.ts +0 -210
  11. package/src/__tests__/types.test.ts +0 -303
  12. package/src/__tests__/web-push-sdk.test.ts +0 -257
  13. package/src/components/message-popup.ts +0 -1007
  14. package/src/core/event-emitter.ts +0 -61
  15. package/src/core/service-worker-manager.ts +0 -614
  16. package/src/core/web-push-sdk.ts +0 -690
  17. package/src/debug/GenerateTestUserSig.js +0 -37
  18. package/src/debug/index.d.ts +0 -6
  19. package/src/debug/index.js +0 -1
  20. package/src/debug/lib-generate-test-usersig-es.min.js +0 -2
  21. package/src/index.ts +0 -9
  22. package/src/service-worker/sw.ts +0 -494
  23. package/src/types/index.ts +0 -2
  24. package/src/types/inner.ts +0 -44
  25. package/src/types/outer.ts +0 -142
  26. package/src/utils/browser-support.ts +0 -412
  27. package/src/utils/logger.ts +0 -66
  28. package/src/utils/storage.ts +0 -51
  29. package/src/utils/validator.ts +0 -267
  30. /package/{dist/index.d.ts → index.d.ts} +0 -0
  31. /package/{dist/src → src}/components/message-popup.d.ts +0 -0
  32. /package/{dist/src → src}/core/event-emitter.d.ts +0 -0
  33. /package/{dist/src → src}/core/service-worker-manager.d.ts +0 -0
  34. /package/{dist/src → src}/core/web-push-sdk.d.ts +0 -0
  35. /package/{dist/src → src}/index.d.ts +0 -0
  36. /package/{dist/src → src}/service-worker/sw.d.ts +0 -0
  37. /package/{dist/src → src}/types/index.d.ts +0 -0
  38. /package/{dist/src → src}/types/inner.d.ts +0 -0
  39. /package/{dist/src → src}/types/outer.d.ts +0 -0
  40. /package/{dist/src → src}/utils/browser-support.d.ts +0 -0
  41. /package/{dist/src → src}/utils/logger.d.ts +0 -0
  42. /package/{dist/src → src}/utils/storage.d.ts +0 -0
  43. /package/{dist/src → src}/utils/validator.d.ts +0 -0
package/package.json CHANGED
@@ -1,41 +1,21 @@
1
1
  {
2
2
  "name": "@tencentcloud/web-push",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Tencent Cloud Web Push SDK - Service Worker-based web push notification service",
5
- "main": "dist/index.umd.js",
6
- "module": "dist/index.esm.js",
7
- "types": "dist/index.d.ts",
5
+ "main": "index.umd.js",
6
+ "module": "index.esm.js",
7
+ "types": "index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.esm.js",
12
- "require": "./dist/index.umd.js"
10
+ "types": "./index.d.ts",
11
+ "import": "./index.esm.js",
12
+ "require": "./index.umd.js"
13
13
  },
14
- "./sw": "./dist/sw.js"
14
+ "./sw": "./sw.js"
15
15
  },
16
16
  "files": [
17
- "dist",
18
- "src"
17
+ "**/*"
19
18
  ],
20
- "scripts": {
21
- "dev": "vite",
22
- "build": "rm -rf dist && vite build && npm run build:sw && npm run build:types",
23
- "build:sw": "vite build --config vite.sw.config.ts",
24
- "build:types": "tsc --project tsconfig.build.json",
25
- "build:release": "node scripts/build-release.js",
26
- "preview": "vite preview",
27
- "test": "jest --verbose",
28
- "test:coverage": "jest --coverage",
29
- "lint": "eslint src --ext .ts,.js",
30
- "lint:fix": "eslint src --ext .ts,.js --fix",
31
- "format": "prettier --write \"src/**/*.{ts,js,json,md}\"",
32
- "format:check": "prettier --check \"src/**/*.{ts,js,json,md}\"",
33
- "type-check": "tsc --noEmit",
34
- "pre-commit": "lint-staged",
35
- "prepare": "husky install",
36
- "prepublish": "npm run build && npm run lint && npm run build:release",
37
- "publish": "cd dist && npm publish --access public"
38
- },
39
19
  "keywords": [
40
20
  "web-push",
41
21
  "service-worker",
@@ -44,24 +24,6 @@
44
24
  ],
45
25
  "author": "Tencent Cloud",
46
26
  "license": "MIT",
47
- "devDependencies": {
48
- "@babel/preset-env": "^7.28.3",
49
- "@types/jest": "^29.5.5",
50
- "@typescript-eslint/eslint-plugin": "^8.46.1",
51
- "@typescript-eslint/parser": "^8.46.1",
52
- "chalk": "^5.6.2",
53
- "eslint": "^8.50.0",
54
- "husky": "^9.1.7",
55
- "jest": "^29.7.0",
56
- "jest-environment-jsdom": "^29.7.0",
57
- "lint-staged": "^16.2.4",
58
- "prettier": "^3.6.2",
59
- "terser": "^5.44.0",
60
- "ts-jest": "^29.1.1",
61
- "typescript": "^5.2.2",
62
- "vite": "^4.4.9",
63
- "vite-plugin-dts": "^3.6.0"
64
- },
65
27
  "peerDependencies": {
66
28
  "@tencentcloud/lite-chat": "^4.2.0"
67
29
  }
package/sw.js ADDED
@@ -0,0 +1 @@
1
+ !function(){"use strict";let t=1;function e(e){return 4!==t&&e>=t}function a(t,...a){e(1)&&console.log(`[WebPush SW] ${t}`,...a)}function i(t,...a){e(2)&&console.warn(`[WebPush SW] ${t}`,...a)}function n(t,...a){e(3)&&console.error(`[WebPush SW] ${t}`,...a)}async function o(t,e={}){const{shouldReport:a=!0,clickEventType:i=2}=e,n={messageID:t.id||Date.now().toString(36)+Math.random().toString(36).substring(2),title:t.title||"New Message",body:t.desc||"You have a new message",icon:t.icon||"",tag:t.tag||t?.id?.slice(-100)||"push-"+Date.now()+"-"+Math.random().toString(36).substring(2,7),data:{...t,clickEventType:i},timestamp:Date.now()},o=self.registration.showNotification(n.title,{body:n.body,icon:n.icon,image:t.image,tag:n.tag,data:n,requireInteraction:!1}),c=s({type:"MESSAGE_RECEIVED",data:n}),l=a&&t.rptURL&&t.rptExt?r({id:t.id,rptURL:t.rptURL,rptExt:t.rptExt,eventType:1}):Promise.resolve();await Promise.all([o,c,l])}async function s(t){try{(await self.clients.matchAll({includeUncontrolled:!0,type:"window"})).forEach(e=>{e.postMessage(t)})}catch(e){n("Failed to send message to clients",e)}}async function r(t){try{if(!t.rptURL||!t.rptExt)return void i("Missing rptURL or rptExt, skipping report");const e={webpushEvents:[{id:t.id,EventType:t.eventType||1,EventTime:Math.floor(Date.now()/1e3),rptExt:t.rptExt}]};a("Reporting WebPush event",{rptURL:t.rptURL,reportData:e});const n=await fetch(t.rptURL,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});n.ok?a("WebPush event reported successfully"):i("Failed to report WebPush event",n.status,n.statusText)}catch(e){n("Error reporting WebPush event",e)}}self.addEventListener("install",t=>{a("Service Worker installing..."),self.skipWaiting()}),self.addEventListener("activate",t=>{a("Service Worker activating..."),t.waitUntil(self.clients.claim())}),self.addEventListener("push",t=>{if(a("Push message received",t),t.data)try{const e=function(t){if(!t)throw new Error("No push data available");try{const e=t.json();return a("Push message data (JSON):",e),e}catch{const e=t.text();return a("Push message data (Text):",e),{id:"",title:"WebPush Notification",desc:e||"You have a new message",url:"/",icon:"",tag:"",image:"",ext:"",rptExt:"",rptURL:""}}}(t.data);t.waitUntil(o(e))}catch(e){n("Failed to process push message",e),t.waitUntil(self.registration.showNotification("WebPush Notification",{body:"You have a new message",tag:"fallback"}))}else i("Push message has no data")}),self.addEventListener("notificationclick",t=>{a("Notification clicked",t);const e=t.notification,o=t.action,c=e.data;if(e.close(),"close"===o)return;const l=s({type:"NOTIFICATION_CLICKED",data:{notification:c,action:o}}),d=async function(t="/"){try{a("Attempting to open or focus window with URL:",t);const n=await self.clients.matchAll({type:"window",includeUncontrolled:!0});let o;a("Found clients:",n.length);try{o=t.startsWith("http://")||t.startsWith("https://")?t:new URL(t,self.location.origin).href}catch(e){i("Invalid URL, using origin:",t,e),o=self.location.origin}a("Full target URL:",o);for(const t of n)if(t.url===o&&"focus"in t)return void(await t.focus());self.clients.openWindow&&await self.clients.openWindow(o)}catch(e){n("Failed to open or focus window",e)}}(c?.data?.url||"/"),f=c?.data?.clickEventType||2,u=c?.data?.rptURL&&c?.data?.rptExt?r({id:c.messageID,rptURL:c.data.rptURL,rptExt:c.data.rptExt,eventType:f}):Promise.resolve();t.waitUntil(Promise.all([l,d,u]))}),self.addEventListener("notificationclose",t=>{a("Notification closed",t)}),self.addEventListener("message",e=>{a("Message received from main thread",e.data);const{type:i,payload:c}=e.data;switch(i){case"SHOW_NOTIFICATION":!async function(t){try{const{eventType:e,data:i,options:n}=t;a("Handling show notification request",{eventType:e,data:i,options:n}),await self.registration.showNotification(n.title,{body:n.body,icon:n.icon,badge:n.badge,tag:n.tag,requireInteraction:n.requireInteraction,silent:n.silent,data:n.data}),a("Notification shown successfully",n.title)}catch(e){n("Failed to handle show notification",e)}}(c);break;case"PROCESS_WEBPUSH_DATA":!async function(t){try{a("Handling webpush data from main thread",t),await o(t,{shouldReport:!1,clickEventType:3}),a("WebPush notification processed successfully")}catch(e){n("Failed to handle webpush data",e);try{await self.registration.showNotification("WebPush Notification",{body:"You have a new message",tag:"fallback"})}catch(i){n("Failed to show fallback notification",i)}}}(c);break;case"REVOKE_MESSAGE":!async function(t){try{(await self.registration.getNotifications()).forEach(e=>{e.data&&e.data.messageID===t&&e.close()}),await s({type:"MESSAGE_REVOKED",data:{messageID:t}})}catch(e){n("Failed to handle message revocation",e)}}(c.messageID);break;case"SET_LOG_LEVEL":l=c.logLevel,t=l,a("Log level updated to:",l);break;case"REPORT_WEBPUSH_EVENT":r(c)}var l}),self.addEventListener("error",t=>{n("Service Worker error",t.error)}),self.addEventListener("unhandledrejection",t=>{n("Service Worker unhandled promise rejection",t.reason)})}();
package/dist/sw.js DELETED
@@ -1 +0,0 @@
1
- !function(){"use strict";let t=1;function e(e){return 4!==t&&e>=t}function a(t,...a){e(1)&&console.log(`[WebPush SW] ${t}`,...a)}function i(t,...a){e(2)&&console.warn(`[WebPush SW] ${t}`,...a)}function n(t,...a){e(3)&&console.error(`[WebPush SW] ${t}`,...a)}async function o(t,e={}){const{shouldReport:a=!0,clickEventType:i=2}=e,n={messageID:t.id||Date.now().toString(36)+Math.random().toString(36).substring(2),title:t.title||"New Message",body:t.desc||"You have a new message",icon:t.icon||"",tag:t.tag||t?.id?.slice(-100)||"push-"+Date.now()+"-"+Math.random().toString(36).substring(2,7),data:{url:t.url,image:t.image,rptExt:t.rptExt,rptURL:t.rptURL,clickEventType:i},timestamp:Date.now()},o=self.registration.showNotification(n.title,{body:n.body,icon:n.icon,image:t.image,tag:n.tag,data:n,requireInteraction:!1}),l=r({type:"MESSAGE_RECEIVED",data:s(n)}),d=a&&t.rptURL&&t.rptExt?c({id:t.id,rptURL:t.rptURL,rptExt:t.rptExt,eventType:1}):Promise.resolve();await Promise.all([o,l,d])}function s(t){return{messageID:t.messageID,title:t.title,body:t.body,icon:t.icon,tag:t.tag,data:{url:t.data?.url,image:t.data?.image},timestamp:t.timestamp}}async function r(t){try{(await self.clients.matchAll({includeUncontrolled:!0,type:"window"})).forEach(e=>{e.postMessage(t)})}catch(e){n("Failed to send message to clients",e)}}async function c(t){try{if(!t.rptURL||!t.rptExt)return void i("Missing rptURL or rptExt, skipping report");const e={webpushEvents:[{id:t.id,EventType:t.eventType||1,EventTime:Math.floor(Date.now()/1e3),rptExt:t.rptExt}]};a("Reporting WebPush event",{rptURL:t.rptURL,reportData:e});const n=await fetch(t.rptURL,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});n.ok?a("WebPush event reported successfully"):i("Failed to report WebPush event",n.status,n.statusText)}catch(e){n("Error reporting WebPush event",e)}}self.addEventListener("install",t=>{a("Service Worker installing..."),self.skipWaiting()}),self.addEventListener("activate",t=>{a("Service Worker activating..."),t.waitUntil(self.clients.claim())}),self.addEventListener("push",t=>{if(a("Push message received",t),t.data)try{const e=function(t){if(!t)throw new Error("No push data available");try{const e=t.json();return a("Push message data (JSON):",e),e}catch{const e=t.text();return a("Push message data (Text):",e),{id:"",title:"WebPush Notification",desc:e||"You have a new message",url:"/",icon:"",tag:"",image:"",rptExt:"",rptURL:""}}}(t.data);t.waitUntil(o(e))}catch(e){n("Failed to process push message",e),t.waitUntil(self.registration.showNotification("WebPush Notification",{body:"You have a new message",tag:"fallback"}))}else i("Push message has no data")}),self.addEventListener("notificationclick",t=>{a("Notification clicked",t);const e=t.notification,o=t.action,l=e.data;if(e.close(),"close"===o)return;const d=r({type:"NOTIFICATION_CLICKED",data:{notification:s(l),action:o}}),u=async function(t="/"){try{a("Attempting to open or focus window with URL:",t);const n=await self.clients.matchAll({type:"window",includeUncontrolled:!0});let o;a("Found clients:",n.length);try{o=t.startsWith("http://")||t.startsWith("https://")?t:new URL(t,self.location.origin).href}catch(e){i("Invalid URL, using origin:",t,e),o=self.location.origin}a("Full target URL:",o);for(const t of n)if(t.url===o&&"focus"in t)return void(await t.focus());self.clients.openWindow&&await self.clients.openWindow(o)}catch(e){n("Failed to open or focus window",e)}}(l?.data?.url||"/"),p=l?.data?.clickEventType||2,f=l?.data?.rptURL&&l?.data?.rptExt?c({id:l.messageID,rptURL:l.data.rptURL,rptExt:l.data.rptExt,eventType:p}):Promise.resolve();t.waitUntil(Promise.all([d,u,f]))}),self.addEventListener("notificationclose",t=>{a("Notification closed",t)}),self.addEventListener("message",e=>{a("Message received from main thread",e.data);const{type:i,payload:s}=e.data;switch(i){case"SHOW_NOTIFICATION":!async function(t){try{const{eventType:e,data:i,options:n}=t;a("Handling show notification request",{eventType:e,data:i,options:n}),await self.registration.showNotification(n.title,{body:n.body,icon:n.icon,badge:n.badge,tag:n.tag,requireInteraction:n.requireInteraction,silent:n.silent,data:n.data}),a("Notification shown successfully",n.title)}catch(e){n("Failed to handle show notification",e)}}(s);break;case"PROCESS_WEBPUSH_DATA":!async function(t){try{a("Handling webpush data from main thread",t),await o(t,{shouldReport:!1,clickEventType:3}),a("WebPush notification processed successfully")}catch(e){n("Failed to handle webpush data",e);try{await self.registration.showNotification("WebPush Notification",{body:"You have a new message",tag:"fallback"})}catch(i){n("Failed to show fallback notification",i)}}}(s);break;case"REVOKE_MESSAGE":!async function(t){try{(await self.registration.getNotifications()).forEach(e=>{e.data&&e.data.messageID===t&&e.close()}),await r({type:"MESSAGE_REVOKED",data:{messageID:t}})}catch(e){n("Failed to handle message revocation",e)}}(s.messageID);break;case"SET_LOG_LEVEL":l=s.logLevel,t=l,a("Log level updated to:",l);break;case"REPORT_WEBPUSH_EVENT":c(s)}var l}),self.addEventListener("error",t=>{n("Service Worker error",t.error)}),self.addEventListener("unhandledrejection",t=>{n("Service Worker unhandled promise rejection",t.reason)})}();
@@ -1,120 +0,0 @@
1
- /**
2
- * 主入口文件测试
3
- */
4
- import webPush, { webPush as namedExport, EVENT } from '../index';
5
- import type { RegisterPushOptions } from '../index';
6
-
7
- describe('主入口文件测试', () => {
8
- describe('导出测试', () => {
9
- it('应该导出默认的 webPush 实例', () => {
10
- expect(webPush).toBeDefined();
11
- expect(typeof webPush.registerPush).toBe('function');
12
- expect(typeof webPush.unRegisterPush).toBe('function');
13
- expect(typeof webPush.addPushListener).toBe('function');
14
- expect(typeof webPush.removePushListener).toBe('function');
15
- });
16
-
17
- it('应该导出命名的 webPush 实例', () => {
18
- expect(namedExport).toBeDefined();
19
- expect(namedExport).toBe(webPush);
20
- });
21
-
22
- it('应该导出 EVENT 常量', () => {
23
- expect(EVENT).toBeDefined();
24
- expect(EVENT.MESSAGE_RECEIVED).toBe('message_received');
25
- expect(EVENT.MESSAGE_REVOKED).toBe('message_revoked');
26
- expect(EVENT.NOTIFICATION_CLICKED).toBe('notification_clicked');
27
- });
28
-
29
- it('应该通过实例获取版本号', () => {
30
- expect(webPush.VERSION).toBeDefined();
31
- expect(typeof webPush.VERSION).toBe('string');
32
- expect(webPush.VERSION).toBe('0.1.0');
33
- });
34
-
35
- it('应该导出正确的 TypeScript 类型', () => {
36
- // 这些测试主要是为了确保类型导出正确,在编译时会检查
37
- const options: RegisterPushOptions = {
38
- SDKAppID: 123456,
39
- appKey: 'test-key',
40
- userID: 'test-user',
41
- };
42
-
43
- const eventType = EVENT.MESSAGE_RECEIVED;
44
-
45
- const message = {
46
- messageID: 'test-id',
47
- title: 'Test Title',
48
- body: 'Test Body',
49
- timestamp: Date.now(),
50
- };
51
-
52
- const clickData = {
53
- notification: message,
54
- action: 'default',
55
- };
56
-
57
- // 类型检查通过即可
58
- expect(options).toBeDefined();
59
- expect(eventType).toBeDefined();
60
- expect(message).toBeDefined();
61
- expect(clickData).toBeDefined();
62
- });
63
- });
64
-
65
- describe('单例一致性测试', () => {
66
- it('默认导出和命名导出应该是同一个实例', () => {
67
- expect(webPush).toBe(namedExport);
68
- });
69
-
70
- it('多次导入应该返回同一个实例', () => {
71
- // 重新导入模块
72
- jest.resetModules();
73
- const { default: webPush1, webPush: webPush2 } = require('../index');
74
-
75
- expect(webPush1).toBe(webPush2);
76
- });
77
- });
78
-
79
- describe('接口一致性测试', () => {
80
- it('实例应该实现 IWebPushSDK 接口', () => {
81
- // 检查所有必需的方法是否存在
82
- expect(typeof webPush.registerPush).toBe('function');
83
- expect(typeof webPush.unRegisterPush).toBe('function');
84
- expect(typeof webPush.addPushListener).toBe('function');
85
- expect(typeof webPush.removePushListener).toBe('function');
86
- expect(webPush.EVENT).toBeDefined();
87
- });
88
-
89
- it('EVENT 属性应该与导出的 EVENT 常量一致', () => {
90
- expect(webPush.EVENT).toEqual(EVENT);
91
- expect(webPush.EVENT.MESSAGE_RECEIVED).toBe(EVENT.MESSAGE_RECEIVED);
92
- expect(webPush.EVENT.MESSAGE_REVOKED).toBe(EVENT.MESSAGE_REVOKED);
93
- expect(webPush.EVENT.NOTIFICATION_CLICKED).toBe(
94
- EVENT.NOTIFICATION_CLICKED
95
- );
96
- });
97
- });
98
-
99
- describe('模块稳定性测试', () => {
100
- it('应该能够安全地多次导入', () => {
101
- expect(() => {
102
- require('../index');
103
- require('../index');
104
- require('../index');
105
- }).not.toThrow();
106
- });
107
-
108
- it('导出的对象应该是不可变的关键属性', () => {
109
- // 验证 EVENT 对象的基本属性
110
- expect(EVENT.MESSAGE_RECEIVED).toBe('message_received');
111
- expect(EVENT.MESSAGE_REVOKED).toBe('message_revoked');
112
- expect(EVENT.NOTIFICATION_CLICKED).toBe('notification_clicked');
113
-
114
- // 验证 EVENT 对象包含所有必需的属性
115
- expect(Object.keys(EVENT)).toContain('MESSAGE_RECEIVED');
116
- expect(Object.keys(EVENT)).toContain('MESSAGE_REVOKED');
117
- expect(Object.keys(EVENT)).toContain('NOTIFICATION_CLICKED');
118
- });
119
- });
120
- });
@@ -1,285 +0,0 @@
1
- /**
2
- * 集成测试 - 测试完整的工作流程
3
- */
4
- import { WebPushSDK } from '../core/web-push-sdk';
5
- import { EVENT, RegisterPushOptions } from '../types';
6
-
7
- describe('WebPushSDK 集成测试', () => {
8
- let webPushSDK: WebPushSDK;
9
-
10
- beforeEach(() => {
11
- // 重置单例
12
- (WebPushSDK as unknown as { instance: undefined }).instance = undefined;
13
- webPushSDK = WebPushSDK.getInstance();
14
-
15
- jest.clearAllMocks();
16
- });
17
-
18
- describe('完整注册流程测试', () => {
19
- const validOptions: RegisterPushOptions = {
20
- SDKAppID: 123456,
21
- appKey: 'test-app-key',
22
- userID: 'test-user-id',
23
- };
24
-
25
- it('应该完成完整的注册流程', async () => {
26
- const registrationID = await webPushSDK.registerPush(validOptions);
27
-
28
- expect(registrationID).toBe(validOptions.userID);
29
-
30
- // 验证注册成功
31
- expect(registrationID).toBe(validOptions.userID);
32
- });
33
- });
34
-
35
- describe('完整反注册流程测试', () => {
36
- beforeEach(async () => {
37
- // 先注册
38
- await webPushSDK.registerPush({
39
- SDKAppID: 123456,
40
- appKey: 'test-app-key',
41
- userID: 'test-user-id',
42
- });
43
- jest.clearAllMocks();
44
- });
45
-
46
- it('应该完成完整的反注册流程', async () => {
47
- const result = await webPushSDK.unRegisterPush();
48
-
49
- expect(result).toBe(true);
50
- });
51
- });
52
-
53
- describe('事件监听器完整流程测试', () => {
54
- it('应该正确处理消息接收事件', async () => {
55
- const messageListener = jest.fn();
56
-
57
- // 添加监听器
58
- const listenerId = webPushSDK.addPushListener(
59
- EVENT.MESSAGE_RECEIVED,
60
- messageListener
61
- );
62
- expect(listenerId).toBeTruthy();
63
-
64
- // 模拟消息接收(通过内部事件发射器)
65
- const mockMessage = {
66
- messageID: 'msg-123',
67
- title: 'Test Message',
68
- body: 'Test Body',
69
- timestamp: Date.now(),
70
- };
71
-
72
- // 直接触发内部事件(模拟 Service Worker 消息)
73
- (
74
- webPushSDK as unknown as {
75
- eventEmitter: { emit: (event: string, data: unknown) => void };
76
- }
77
- ).eventEmitter.emit(EVENT.MESSAGE_RECEIVED, mockMessage);
78
-
79
- expect(messageListener).toHaveBeenCalledWith(mockMessage);
80
-
81
- // 移除监听器
82
- const removed = webPushSDK.removePushListener(
83
- EVENT.MESSAGE_RECEIVED,
84
- messageListener
85
- );
86
- expect(removed).toBe(true);
87
-
88
- // 再次触发事件,监听器不应该被调用
89
- (
90
- webPushSDK as unknown as {
91
- eventEmitter: { emit: (event: string, data: unknown) => void };
92
- }
93
- ).eventEmitter.emit(EVENT.MESSAGE_RECEIVED, mockMessage);
94
- expect(messageListener).toHaveBeenCalledTimes(1);
95
- });
96
-
97
- it('应该正确处理通知点击事件', async () => {
98
- const clickListener = jest.fn();
99
-
100
- webPushSDK.addPushListener(EVENT.NOTIFICATION_CLICKED, clickListener);
101
-
102
- const mockClickData = {
103
- notification: {
104
- messageID: 'msg-123',
105
- title: 'Test Message',
106
- body: 'Test Body',
107
- timestamp: Date.now(),
108
- },
109
- action: 'default',
110
- };
111
-
112
- (
113
- webPushSDK as unknown as {
114
- eventEmitter: { emit: (event: string, data: unknown) => void };
115
- }
116
- ).eventEmitter.emit(EVENT.NOTIFICATION_CLICKED, mockClickData);
117
-
118
- expect(clickListener).toHaveBeenCalledWith(mockClickData);
119
- });
120
-
121
- it('应该正确处理消息撤回事件', async () => {
122
- const revokeListener = jest.fn();
123
-
124
- webPushSDK.addPushListener(EVENT.MESSAGE_REVOKED, revokeListener);
125
-
126
- const mockRevokeData = {
127
- messageID: 'msg-123',
128
- };
129
-
130
- (
131
- webPushSDK as unknown as {
132
- eventEmitter: { emit: (event: string, data: unknown) => void };
133
- }
134
- ).eventEmitter.emit(EVENT.MESSAGE_REVOKED, mockRevokeData);
135
-
136
- expect(revokeListener).toHaveBeenCalledWith(mockRevokeData);
137
- });
138
- });
139
-
140
- describe('统计数据记录测试', () => {
141
- let mockChatInstance: any;
142
-
143
- beforeEach(async () => {
144
- // 创建 mock chat 实例
145
- mockChatInstance = {
146
- callExperimentalAPI: jest.fn().mockResolvedValue({ code: 0 }),
147
- destroy: jest.fn(),
148
- };
149
-
150
- // 先注册以设置必要的状态,传入 chat 实例
151
- await webPushSDK.registerPush({
152
- SDKAppID: 123456,
153
- appKey: 'test-app-key',
154
- userID: 'test-user-id',
155
- chat: mockChatInstance, // 传入 mock chat 实例
156
- });
157
- jest.clearAllMocks();
158
- });
159
-
160
- it('应该在消息接收时记录统计数据', async () => {
161
- const mockMessage = {
162
- messageID: 'msg-123',
163
- title: 'Test Message',
164
- body: 'Test Body',
165
- timestamp: Date.now(),
166
- };
167
-
168
- // Mock logger 模块
169
- const { logger } = require('../utils/logger');
170
- const logSpy = jest.spyOn(logger, 'log').mockImplementation();
171
-
172
- // 触发消息接收事件
173
- (
174
- webPushSDK as unknown as {
175
- eventEmitter: { emit: (event: string, data: unknown) => void };
176
- }
177
- ).eventEmitter.emit(EVENT.MESSAGE_RECEIVED, mockMessage);
178
-
179
- // 等待异步操作完成
180
- await new Promise((resolve) => setTimeout(resolve, 0));
181
-
182
- // 验证统计数据被记录到日志中
183
- expect(logSpy).toHaveBeenCalledWith(
184
- 'Message reach statistics recorded',
185
- expect.objectContaining({
186
- messageID: 'msg-123',
187
- type: 'reach',
188
- timestamp: expect.any(Number),
189
- SDKAppID: 123456,
190
- registrationID: 'test-user-id',
191
- })
192
- );
193
-
194
- logSpy.mockRestore();
195
- });
196
-
197
- it('应该在通知点击时记录统计数据', async () => {
198
- const mockClickData = {
199
- notification: {
200
- messageID: 'msg-123',
201
- title: 'Test Message',
202
- body: 'Test Body',
203
- timestamp: Date.now(),
204
- },
205
- };
206
-
207
- // Mock logger 模块
208
- const { logger } = require('../utils/logger');
209
- const logSpy = jest.spyOn(logger, 'log').mockImplementation();
210
-
211
- // 触发通知点击事件
212
- (
213
- webPushSDK as unknown as {
214
- eventEmitter: { emit: (event: string, data: unknown) => void };
215
- }
216
- ).eventEmitter.emit(EVENT.NOTIFICATION_CLICKED, mockClickData);
217
-
218
- // 等待异步操作完成
219
- await new Promise((resolve) => setTimeout(resolve, 0));
220
-
221
- // 验证统计数据被记录到日志中
222
- expect(logSpy).toHaveBeenCalledWith(
223
- 'Message click statistics recorded',
224
- expect.objectContaining({
225
- messageID: 'msg-123',
226
- type: 'click',
227
- timestamp: expect.any(Number),
228
- SDKAppID: 123456,
229
- registrationID: 'test-user-id',
230
- })
231
- );
232
-
233
- logSpy.mockRestore();
234
- });
235
- });
236
-
237
- describe('状态持久化测试', () => {
238
- it('应该在注册后保存状态', async () => {
239
- await webPushSDK.registerPush({
240
- SDKAppID: 123456,
241
- appKey: 'test-app-key',
242
- userID: 'test-user-id',
243
- });
244
-
245
- const { Storage } = require('../utils/storage');
246
- expect(Storage.set).toHaveBeenCalledWith('sdk_state', {
247
- isRegistered: true,
248
- registrationID: 'test-user-id',
249
- SDKAppID: 123456,
250
- appKey: 'test-app-key',
251
- vapidPublicKey: 'test-app-key',
252
- });
253
- });
254
-
255
- it('应该在初始化时恢复状态', () => {
256
- const { Storage } = require('../utils/storage');
257
- Storage.get.mockReturnValue({
258
- isRegistered: true,
259
- registrationID: 'restored-user',
260
- SDKAppID: 654321,
261
- appKey: 'restored-key',
262
- vapidPublicKey: 'restored-vapid',
263
- });
264
-
265
- // 创建新实例
266
- (WebPushSDK as unknown as { instance: undefined }).instance = undefined;
267
- WebPushSDK.getInstance();
268
-
269
- expect(Storage.get).toHaveBeenCalledWith('sdk_state');
270
- });
271
-
272
- it('应该在反注册后清理状态', async () => {
273
- await webPushSDK.registerPush({
274
- SDKAppID: 123456,
275
- appKey: 'test-app-key',
276
- userID: 'test-user-id',
277
- });
278
-
279
- await webPushSDK.unRegisterPush();
280
-
281
- const { Storage } = require('../utils/storage');
282
- expect(Storage.remove).toHaveBeenCalledWith('sdk_state');
283
- });
284
- });
285
- });