diff --git a/.oxlintrc.json b/.oxlintrc.json index a416de2c..8bf2109a 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -4,6 +4,7 @@ "categories": {}, "rules": { "for-direction": "warn", + "jest/no-focused-tests": "error", "no-async-promise-executor": "warn", "no-caller": "warn", "no-class-assign": "warn", diff --git a/__mocks__/react-native.ts b/__mocks__/react-native.ts new file mode 100644 index 00000000..617e9411 --- /dev/null +++ b/__mocks__/react-native.ts @@ -0,0 +1,94 @@ +import { vi } from 'vitest'; + +const mockRNOneSignal = { + initialize: vi.fn(), + login: vi.fn(), + logout: vi.fn(), + setPrivacyConsentRequired: vi.fn(), + setPrivacyConsentGiven: vi.fn(), + setLogLevel: vi.fn(), + setAlertLevel: vi.fn(), + enterLiveActivity: vi.fn(), + exitLiveActivity: vi.fn(), + setPushToStartToken: vi.fn(), + removePushToStartToken: vi.fn(), + setupDefaultLiveActivity: vi.fn(), + startDefaultLiveActivity: vi.fn(), + addPushSubscriptionObserver: vi.fn(), + getPushSubscriptionId: vi.fn(), + getPushSubscriptionToken: vi.fn(), + getOptedIn: vi.fn(), + optOut: vi.fn(), + optIn: vi.fn(), + addUserStateObserver: vi.fn(), + getOnesignalId: vi.fn(), + getExternalId: vi.fn(), + setLanguage: vi.fn(), + addAlias: vi.fn(), + addAliases: vi.fn(), + removeAlias: vi.fn(), + removeAliases: vi.fn(), + addEmail: vi.fn(), + removeEmail: vi.fn(), + addSms: vi.fn(), + removeSms: vi.fn(), + addTag: vi.fn(), + addTags: vi.fn(), + removeTags: vi.fn(), + getTags: vi.fn(), + hasNotificationPermission: vi.fn(), + requestNotificationPermission: vi.fn(), + canRequestNotificationPermission: vi.fn(), + registerForProvisionalAuthorization: vi.fn(), + permissionNative: vi.fn(), + addNotificationClickListener: vi.fn(), + addNotificationForegroundLifecycleListener: vi.fn(), + addPermissionObserver: vi.fn(), + clearAllNotifications: vi.fn(), + removeNotification: vi.fn(), + removeGroupedNotifications: vi.fn(), + addInAppMessageClickListener: vi.fn(), + addInAppMessagesLifecycleListener: vi.fn(), + addTriggers: vi.fn(), + removeTrigger: vi.fn(), + removeTriggers: vi.fn(), + clearTriggers: vi.fn(), + paused: vi.fn(), + getPaused: vi.fn(), + requestLocationPermission: vi.fn(), + setLocationShared: vi.fn(), + isLocationShared: vi.fn(), + addOutcome: vi.fn(), + addUniqueOutcome: vi.fn(), + addOutcomeWithValue: vi.fn(), +}; + +const mockPlatform = { + OS: 'ios', +}; + +export const NativeModules = { + OneSignal: mockRNOneSignal, +}; + +export const Platform = mockPlatform; + +export { mockPlatform, mockRNOneSignal }; + +export class NativeEventEmitter { + constructor(_nativeModule: typeof mockRNOneSignal) {} + + addListener(_eventName: string, _callback: (payload: unknown) => void) { + return { + remove: vi.fn(), + }; + } + + removeListener(_eventName: string, _callback: (payload: unknown) => void) { + // Mock implementation + } + + removeAllListeners(_eventName: string) { + // Mock implementation + } +} diff --git a/bun.lock b/bun.lock index c9113f07..157c3ac9 100644 --- a/bun.lock +++ b/bun.lock @@ -9,11 +9,13 @@ "devDependencies": { "@types/invariant": "^2.2.37", "@types/react-native": "^0.73.0", + "@vitest/coverage-v8": "4.0.8", "oxlint": "^1.26.0", "prettier": "^3.6.2", "typescript": "^5.9.3", "vite": "^7.2.0", "vite-plugin-dts": "^4.5.4", + "vitest": "^4.0.8", }, }, }, @@ -86,6 +88,8 @@ "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], @@ -278,6 +282,8 @@ "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@types/argparse": ["@types/argparse@1.0.38", "", {}, "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -288,6 +294,10 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="], @@ -310,6 +320,22 @@ "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], + "@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.8", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.8", "ast-v8-to-istanbul": "^0.3.8", "debug": "^4.4.3", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.8", "vitest": "4.0.8" }, "optionalPeers": ["@vitest/browser"] }, "sha512-wQgmtW6FtPNn4lWUXi8ZSYLpOIb92j3QCujxX3sQ81NTfQ/ORnE0HtK7Kqf2+7J9jeveMGyGyc4NWc5qy3rC4A=="], + + "@vitest/expect": ["@vitest/expect@4.0.8", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.8", "@vitest/utils": "4.0.8", "chai": "^6.2.0", "tinyrainbow": "^3.0.3" } }, "sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA=="], + + "@vitest/mocker": ["@vitest/mocker@4.0.8", "", { "dependencies": { "@vitest/spy": "4.0.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.8", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg=="], + + "@vitest/runner": ["@vitest/runner@4.0.8", "", { "dependencies": { "@vitest/utils": "4.0.8", "pathe": "^2.0.3" } }, "sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.0.8", "", { "dependencies": { "@vitest/pretty-format": "4.0.8", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw=="], + + "@vitest/spy": ["@vitest/spy@4.0.8", "", {}, "sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA=="], + + "@vitest/utils": ["@vitest/utils@4.0.8", "", { "dependencies": { "@vitest/pretty-format": "4.0.8", "tinyrainbow": "^3.0.3" } }, "sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow=="], + "@volar/language-core": ["@volar/language-core@2.4.23", "", { "dependencies": { "@volar/source-map": "2.4.23" } }, "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ=="], "@volar/source-map": ["@volar/source-map@2.4.23", "", {}, "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q=="], @@ -354,6 +380,10 @@ "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.8", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } }, "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ=="], + "async-limiter": ["async-limiter@1.0.1", "", {}, "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="], "babel-jest": ["babel-jest@29.7.0", "", { "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" } }, "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg=="], @@ -388,6 +418,8 @@ "caniuse-lite": ["caniuse-lite@1.0.30001753", "", {}, "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw=="], + "chai": ["chai@6.2.0", "", {}, "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "chrome-launcher": ["chrome-launcher@0.15.2", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0" }, "bin": { "print-chrome-path": "bin/print-chrome-path.js" } }, "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ=="], @@ -438,6 +470,8 @@ "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -448,12 +482,14 @@ "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + "expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="], + "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], "exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="], @@ -508,6 +544,8 @@ "hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="], + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], @@ -540,6 +578,12 @@ "istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="], + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-lib-source-maps": ["istanbul-lib-source-maps@5.0.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0" } }, "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + "jest-environment-node": ["jest-environment-node@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw=="], "jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], @@ -560,7 +604,7 @@ "jju": ["jju@1.4.0", "", {}, "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="], - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], "js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], @@ -594,6 +638,10 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "magicast": ["magicast@0.5.1", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "source-map-js": "^1.2.1" } }, "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw=="], + + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], "marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="], @@ -756,6 +804,8 @@ "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], @@ -770,12 +820,16 @@ "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], "stacktrace-parser": ["stacktrace-parser@0.1.11", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg=="], "statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -784,7 +838,7 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], @@ -794,8 +848,14 @@ "throat": ["throat@5.0.0", "", {}, "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -826,6 +886,8 @@ "vite-plugin-dts": ["vite-plugin-dts@4.5.4", "", { "dependencies": { "@microsoft/api-extractor": "^7.50.1", "@rollup/pluginutils": "^5.1.4", "@volar/typescript": "^2.4.11", "@vue/language-core": "2.2.0", "compare-versions": "^6.1.1", "debug": "^4.4.0", "kolorist": "^1.8.0", "local-pkg": "^1.0.0", "magic-string": "^0.30.17" }, "peerDependencies": { "typescript": "*", "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg=="], + "vitest": ["vitest@4.0.8", "", { "dependencies": { "@vitest/expect": "4.0.8", "@vitest/mocker": "4.0.8", "@vitest/pretty-format": "4.0.8", "@vitest/runner": "4.0.8", "@vitest/snapshot": "4.0.8", "@vitest/spy": "4.0.8", "@vitest/utils": "4.0.8", "debug": "^4.4.3", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.8", "@vitest/browser-preview": "4.0.8", "@vitest/browser-webdriverio": "4.0.8", "@vitest/ui": "4.0.8", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg=="], + "vlq": ["vlq@1.0.1", "", {}, "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w=="], "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], @@ -836,6 +898,8 @@ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -854,6 +918,8 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -866,10 +932,16 @@ "@microsoft/api-extractor/typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@rushstack/node-core-library/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA=="], "@rushstack/node-core-library/semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="], + "@rushstack/terminal/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + + "@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@vue/language-core/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "ajv-formats/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA=="], @@ -878,8 +950,6 @@ "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "connect/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -896,8 +966,12 @@ "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], + "lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "metro/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], "metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], diff --git a/package.json b/package.json index 49fa746d..f372600f 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "prepare": "bun run build", "build": "tsc --noEmit && vite build", "lint": "oxlint src examples && prettier --check src examples", - "lint:fix": "oxlint src examples --fix && prettier --write src examples" + "lint:fix": "oxlint src examples --fix && prettier --write src examples", + "test": "vitest", + "test:coverage": "vitest run --coverage" }, "dependencies": { "invariant": "^2.2.4" @@ -16,11 +18,13 @@ "devDependencies": { "@types/invariant": "^2.2.37", "@types/react-native": "^0.73.0", + "@vitest/coverage-v8": "4.0.8", "oxlint": "^1.26.0", "prettier": "^3.6.2", "typescript": "^5.9.3", "vite": "^7.2.0", - "vite-plugin-dts": "^4.5.4" + "vite-plugin-dts": "^4.5.4", + "vitest": "^4.0.8" }, "keywords": [ "react-component", diff --git a/src/events/events.ts b/src/constants/events.ts similarity index 100% rename from src/events/events.ts rename to src/constants/events.ts diff --git a/src/constants/subscription.ts b/src/constants/subscription.ts new file mode 100644 index 00000000..fe907fd2 --- /dev/null +++ b/src/constants/subscription.ts @@ -0,0 +1,7 @@ +export enum OSNotificationPermission { + NotDetermined = 0, + Denied, + Authorized, + Provisional, // only available in iOS 12 + Ephemeral, // only available in iOS 14 +} diff --git a/src/events/EventManager.ts b/src/events/EventManager.ts index 0f58d312..1a94e579 100644 --- a/src/events/EventManager.ts +++ b/src/events/EventManager.ts @@ -3,8 +3,6 @@ import { NativeEventEmitter, type NativeModule, } from 'react-native'; -import OSNotification from '../OSNotification'; -import NotificationWillDisplayEvent from './NotificationWillDisplayEvent'; import { IN_APP_MESSAGE_CLICKED, IN_APP_MESSAGE_DID_DISMISS, @@ -16,7 +14,9 @@ import { PERMISSION_CHANGED, SUBSCRIPTION_CHANGED, USER_STATE_CHANGED, -} from './events'; +} from '../constants/events'; +import OSNotification from '../OSNotification'; +import NotificationWillDisplayEvent from './NotificationWillDisplayEvent'; const eventList = [ PERMISSION_CHANGED, diff --git a/src/index.test.ts b/src/index.test.ts new file mode 100644 index 00000000..a5d83227 --- /dev/null +++ b/src/index.test.ts @@ -0,0 +1,1476 @@ +import { NativeModules, Platform } from 'react-native'; +import type { MockInstance } from 'vitest'; +import { + IN_APP_MESSAGE_CLICKED, + IN_APP_MESSAGE_DID_DISMISS, + IN_APP_MESSAGE_DID_DISPLAY, + IN_APP_MESSAGE_WILL_DISMISS, + IN_APP_MESSAGE_WILL_DISPLAY, + NOTIFICATION_CLICKED, + NOTIFICATION_WILL_DISPLAY, + PERMISSION_CHANGED, + SUBSCRIPTION_CHANGED, + USER_STATE_CHANGED, +} from './constants/events'; +import EventManager from './events/EventManager'; +import * as helpers from './helpers'; +import { LogLevel, OneSignal, OSNotificationPermission } from './index'; + +const mockRNOneSignal = NativeModules.OneSignal; +const mockPlatform = Platform; + +const APP_ID = 'test-app-id'; +const PUSH_ID = 'subscription-id'; +const PUSH_TOKEN = 'push-token'; + +// spies +const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); +let warnSpy: MockInstance; + +const isNativeLoadedSpy = vi.spyOn(helpers, 'isNativeModuleLoaded'); +const isValidCallbackSpy = vi.spyOn(helpers, 'isValidCallback'); +const addEventManagerListenerSpy = vi.spyOn( + EventManager.prototype, + 'addEventListener', +); +const removeEventManagerListenerSpy = vi.spyOn( + EventManager.prototype, + 'removeEventListener', +); + +describe('OneSignal', () => { + beforeEach(() => { + mockPlatform.OS = 'ios'; + isNativeLoadedSpy.mockReturnValue(true); + isValidCallbackSpy.mockImplementation(() => {}); + warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + describe('LogLevel enum', () => { + test.each([ + [LogLevel.None, 0], + [LogLevel.Fatal, 1], + [LogLevel.Error, 2], + [LogLevel.Warn, 3], + [LogLevel.Info, 4], + [LogLevel.Debug, 5], + [LogLevel.Verbose, 6], + ])('should have correct enum values', (logLevel, expected) => { + expect(logLevel).toBe(expected); + }); + }); + + describe('initialize', () => { + test('should initialize OneSignal with appId', () => { + OneSignal.initialize(APP_ID); + expect(mockRNOneSignal.initialize).toHaveBeenCalledWith(APP_ID); + }); + + test('should not initialize if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.initialize(APP_ID); + expect(mockRNOneSignal.initialize).not.toHaveBeenCalled(); + }); + }); + + describe('login', () => { + test('should login with externalId', () => { + OneSignal.login('external-123'); + expect(mockRNOneSignal.login).toHaveBeenCalledWith('external-123'); + }); + + test('should not login if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.login('external-123'); + expect(mockRNOneSignal.login).not.toHaveBeenCalled(); + }); + }); + + describe('logout', () => { + test('should logout', () => { + OneSignal.logout(); + expect(mockRNOneSignal.logout).toHaveBeenCalled(); + }); + + test('should not logout if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.logout(); + expect(mockRNOneSignal.logout).not.toHaveBeenCalled(); + }); + }); + + describe('setConsentRequired', () => { + test('should set consent required', () => { + OneSignal.setConsentRequired(true); + expect(mockRNOneSignal.setPrivacyConsentRequired).toHaveBeenCalledWith( + true, + ); + }); + + test('should not set consent if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.setConsentRequired(true); + expect(mockRNOneSignal.setPrivacyConsentRequired).not.toHaveBeenCalled(); + }); + }); + + describe('setConsentGiven', () => { + test('should set consent given', () => { + OneSignal.setConsentGiven(true); + expect(mockRNOneSignal.setPrivacyConsentGiven).toHaveBeenCalledWith(true); + }); + + test('should not set consent if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.setConsentGiven(true); + expect(mockRNOneSignal.setPrivacyConsentGiven).not.toHaveBeenCalled(); + }); + }); + + describe('Debug', () => { + describe('setLogLevel', () => { + test('should set log level', () => { + OneSignal.Debug.setLogLevel(LogLevel.Info); + expect(mockRNOneSignal.setLogLevel).toHaveBeenCalledWith(LogLevel.Info); + }); + + test('should not set log level if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.Debug.setLogLevel(LogLevel.Info); + expect(mockRNOneSignal.setLogLevel).not.toHaveBeenCalled(); + }); + }); + + describe('setAlertLevel', () => { + test('should set alert level', () => { + OneSignal.Debug.setAlertLevel(LogLevel.Warn); + expect(mockRNOneSignal.setAlertLevel).toHaveBeenCalledWith( + LogLevel.Warn, + ); + }); + + test('should not set alert level if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.Debug.setAlertLevel(LogLevel.Warn); + expect(mockRNOneSignal.setAlertLevel).not.toHaveBeenCalled(); + }); + }); + }); + + describe('LiveActivities', () => { + describe('enter', () => { + test('should enter live activity on iOS', () => { + const handler = vi.fn(); + OneSignal.LiveActivities.enter('activity-id', 'token', handler); + expect(mockRNOneSignal.enterLiveActivity).toHaveBeenCalledWith( + 'activity-id', + 'token', + handler, + ); + }); + + test('should use default handler if not provided', () => { + OneSignal.LiveActivities.enter('activity-id', 'token'); + expect(mockRNOneSignal.enterLiveActivity).toHaveBeenCalledWith( + 'activity-id', + 'token', + expect.any(Function), + ); + }); + + test('should not enter if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.LiveActivities.enter('activity-id', 'token'); + expect(mockRNOneSignal.enterLiveActivity).not.toHaveBeenCalled(); + }); + + test('should not enter on Android', () => { + mockPlatform.OS = 'android'; + OneSignal.LiveActivities.enter('activity-id', 'token'); + expect(mockRNOneSignal.enterLiveActivity).not.toHaveBeenCalled(); + }); + }); + + describe('exit', () => { + test('should exit live activity on iOS', () => { + const handler = vi.fn(); + OneSignal.LiveActivities.exit('activity-id', handler); + expect(mockRNOneSignal.exitLiveActivity).toHaveBeenCalledWith( + 'activity-id', + handler, + ); + }); + + test('should use default handler if not provided', () => { + OneSignal.LiveActivities.exit('activity-id'); + expect(mockRNOneSignal.exitLiveActivity).toHaveBeenCalledWith( + 'activity-id', + expect.any(Function), + ); + }); + + test('should not exit if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.LiveActivities.exit('activity-id'); + expect(mockRNOneSignal.exitLiveActivity).not.toHaveBeenCalled(); + }); + + test('should not exit on Android', () => { + mockPlatform.OS = 'android'; + OneSignal.LiveActivities.exit('activity-id'); + expect(mockRNOneSignal.exitLiveActivity).not.toHaveBeenCalled(); + mockPlatform.OS = 'ios'; + }); + }); + + describe('setPushToStartToken', () => { + test('should set push to start token on iOS', () => { + OneSignal.LiveActivities.setPushToStartToken('activity-type', 'token'); + expect(mockRNOneSignal.setPushToStartToken).toHaveBeenCalledWith( + 'activity-type', + 'token', + ); + }); + + test('should not set token if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.LiveActivities.setPushToStartToken('activity-type', 'token'); + expect(mockRNOneSignal.setPushToStartToken).not.toHaveBeenCalled(); + }); + + test('should not set token on Android', () => { + mockPlatform.OS = 'android'; + OneSignal.LiveActivities.setPushToStartToken('activity-type', 'token'); + expect(mockRNOneSignal.setPushToStartToken).not.toHaveBeenCalled(); + mockPlatform.OS = 'ios'; + }); + }); + + describe('removePushToStartToken', () => { + test('should remove push to start token on iOS', () => { + OneSignal.LiveActivities.removePushToStartToken('activity-type'); + expect(mockRNOneSignal.removePushToStartToken).toHaveBeenCalledWith( + 'activity-type', + ); + }); + + test('should not remove token if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.LiveActivities.removePushToStartToken('activity-type'); + expect(mockRNOneSignal.removePushToStartToken).not.toHaveBeenCalled(); + }); + + test('should not remove token on Android', () => { + mockPlatform.OS = 'android'; + OneSignal.LiveActivities.removePushToStartToken('activity-type'); + expect(mockRNOneSignal.removePushToStartToken).not.toHaveBeenCalled(); + mockPlatform.OS = 'ios'; + }); + }); + + describe('setupDefault', () => { + test('should setup default live activity on iOS', () => { + const options = { enablePushToStart: true, enablePushToUpdate: false }; + OneSignal.LiveActivities.setupDefault(options); + expect(mockRNOneSignal.setupDefaultLiveActivity).toHaveBeenCalledWith( + options, + ); + }); + + test('should setup default without options', () => { + OneSignal.LiveActivities.setupDefault(); + expect(mockRNOneSignal.setupDefaultLiveActivity).toHaveBeenCalledWith( + undefined, + ); + }); + + test('should not setup if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.LiveActivities.setupDefault(); + expect(mockRNOneSignal.setupDefaultLiveActivity).not.toHaveBeenCalled(); + }); + + test('should not setup on Android', () => { + mockPlatform.OS = 'android'; + OneSignal.LiveActivities.setupDefault(); + expect(mockRNOneSignal.setupDefaultLiveActivity).not.toHaveBeenCalled(); + mockPlatform.OS = 'ios'; + }); + }); + + describe('startDefault', () => { + test('should start default live activity on iOS', () => { + const attributes = { key: 'value' }; + const content = { text: 'content' }; + OneSignal.LiveActivities.startDefault( + 'activity-id', + attributes, + content, + ); + expect(mockRNOneSignal.startDefaultLiveActivity).toHaveBeenCalledWith( + 'activity-id', + attributes, + content, + ); + }); + + test('should not start if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.LiveActivities.startDefault('activity-id', {}, {}); + expect(mockRNOneSignal.startDefaultLiveActivity).not.toHaveBeenCalled(); + }); + + test('should not start on Android', () => { + mockPlatform.OS = 'android'; + OneSignal.LiveActivities.startDefault('activity-id', {}, {}); + expect(mockRNOneSignal.startDefaultLiveActivity).not.toHaveBeenCalled(); + mockPlatform.OS = 'ios'; + }); + }); + }); + + describe('User.pushSubscription', () => { + describe('addEventListener', () => { + test('should validate callback', () => { + const listener = vi.fn(); + OneSignal.User.pushSubscription.addEventListener('change', listener); + expect(helpers.isValidCallback).toHaveBeenCalledWith(listener); + }); + + test('should add push subscription change listener', () => { + const listener = vi.fn(); + OneSignal.User.pushSubscription.addEventListener('change', listener); + expect(mockRNOneSignal.addPushSubscriptionObserver).toHaveBeenCalled(); + expect(addEventManagerListenerSpy).toHaveBeenCalledWith( + SUBSCRIPTION_CHANGED, + listener, + ); + }); + + test('should not add listener if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + const listener = vi.fn(); + OneSignal.User.pushSubscription.addEventListener('change', listener); + expect( + mockRNOneSignal.addPushSubscriptionObserver, + ).not.toHaveBeenCalled(); + }); + }); + + describe('removeEventListener', () => { + test('should not remove listener if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + const listener = vi.fn(); + OneSignal.User.pushSubscription.removeEventListener('change', listener); + expect(removeEventManagerListenerSpy).not.toHaveBeenCalled(); + }); + + test('should remove push subscription change listener', () => { + const listener = vi.fn(); + OneSignal.User.pushSubscription.removeEventListener('change', listener); + expect(removeEventManagerListenerSpy).toHaveBeenCalledWith( + SUBSCRIPTION_CHANGED, + listener, + ); + }); + }); + + describe('getPushSubscriptionId (deprecated)', () => { + test('should return empty string if native module not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + const result = OneSignal.User.pushSubscription.getPushSubscriptionId(); + expect(result).toBe(''); + }); + + test('should log deprecation warning and return the push id', async () => { + // with no push id + const result = OneSignal.User.pushSubscription.getPushSubscriptionId(); + expect(result).toBe(''); + expect(console.warn).toHaveBeenCalledWith( + 'OneSignal: This method has been deprecated. Use getIdAsync instead for getting push subscription id.', + ); + + // with a push id + mockRNOneSignal.getPushSubscriptionId.mockResolvedValue(PUSH_ID); + OneSignal.initialize(APP_ID); + await vi.waitFor(() => { + const result2 = + OneSignal.User.pushSubscription.getPushSubscriptionId(); + expect(result2).toBe(PUSH_ID); + }); + }); + }); + + describe('getIdAsync', () => { + test('should get push subscription id', async () => { + mockRNOneSignal.getPushSubscriptionId.mockResolvedValue(PUSH_ID); + const result = await OneSignal.User.pushSubscription.getIdAsync(); + expect(result).toBe(PUSH_ID); + expect(mockRNOneSignal.getPushSubscriptionId).toHaveBeenCalled(); + }); + + test('should reject if native module is not loaded', async () => { + isNativeLoadedSpy.mockReturnValue(false); + await expect( + OneSignal.User.pushSubscription.getIdAsync(), + ).rejects.toThrow('OneSignal native module not loaded'); + }); + }); + + describe('getPushSubscriptionToken (deprecated)', () => { + test('should return empty string if native module not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + const result = + OneSignal.User.pushSubscription.getPushSubscriptionToken(); + expect(result).toBe(''); + }); + + test('should log deprecation warning and return the push token', async () => { + // with no push token + const result = + OneSignal.User.pushSubscription.getPushSubscriptionToken(); + expect(result).toBe(''); + expect(console.warn).toHaveBeenCalledWith( + 'OneSignal: This method has been deprecated. Use getTokenAsync instead for getting push subscription token.', + ); + + // with a push token + vi.mocked(mockRNOneSignal.getPushSubscriptionToken).mockResolvedValue( + PUSH_TOKEN, + ); + OneSignal.initialize(APP_ID); + await vi.waitFor(() => { + const result2 = + OneSignal.User.pushSubscription.getPushSubscriptionToken(); + expect(result2).toBe(PUSH_TOKEN); + }); + }); + }); + + describe('getTokenAsync', () => { + test('should get push subscription token', async () => { + vi.mocked(mockRNOneSignal.getPushSubscriptionToken).mockResolvedValue( + PUSH_TOKEN, + ); + const result = await OneSignal.User.pushSubscription.getTokenAsync(); + expect(result).toBe(PUSH_TOKEN); + expect(mockRNOneSignal.getPushSubscriptionToken).toHaveBeenCalled(); + }); + + test('should reject if native module is not loaded', async () => { + isNativeLoadedSpy.mockReturnValue(false); + await expect( + OneSignal.User.pushSubscription.getTokenAsync(), + ).rejects.toThrow('OneSignal native module not loaded'); + }); + }); + + describe('getOptedIn (deprecated)', () => { + test('should return false if native module not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + const result = OneSignal.User.pushSubscription.getOptedIn(); + expect(result).toBe(false); + }); + + test('should log deprecation warning and return the opted in status', async () => { + // with no opted in status + const result = OneSignal.User.pushSubscription.getOptedIn(); + expect(result).toBe(false); + expect(console.warn).toHaveBeenCalledWith( + 'OneSignal: This method has been deprecated. Use getOptedInAsync instead for getting push subscription opted in status.', + ); + + // with a opted in status + vi.mocked(mockRNOneSignal.getOptedIn).mockResolvedValue(true); + OneSignal.initialize(APP_ID); + await vi.waitFor(() => { + const result2 = OneSignal.User.pushSubscription.getOptedIn(); + expect(result2).toBe(true); + }); + }); + }); + + describe('getOptedInAsync', () => { + test('should get opted in status', async () => { + vi.mocked(mockRNOneSignal.getOptedIn).mockResolvedValue(true); + const result = await OneSignal.User.pushSubscription.getOptedInAsync(); + expect(result).toBe(true); + expect(mockRNOneSignal.getOptedIn).toHaveBeenCalled(); + }); + + test('should reject if native module is not loaded', async () => { + isNativeLoadedSpy.mockReturnValue(false); + await expect( + OneSignal.User.pushSubscription.getOptedInAsync(), + ).rejects.toThrow('OneSignal native module not loaded'); + }); + }); + + describe('optOut', () => { + test('should opt out', () => { + OneSignal.User.pushSubscription.optOut(); + expect(mockRNOneSignal.optOut).toHaveBeenCalled(); + }); + + test('should not opt out if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.User.pushSubscription.optOut(); + expect(mockRNOneSignal.optOut).not.toHaveBeenCalled(); + }); + }); + + describe('optIn', () => { + test('should opt in', () => { + OneSignal.User.pushSubscription.optIn(); + expect(mockRNOneSignal.optIn).toHaveBeenCalled(); + }); + + test('should not opt in if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.User.pushSubscription.optIn(); + expect(mockRNOneSignal.optIn).not.toHaveBeenCalled(); + }); + }); + }); + + describe('User', () => { + const EMAIL = 'test@example.com'; + const SMS_NUMBER = '+1234567890'; + + describe('addEventListener', () => { + test('should add user state change listener', () => { + const listener = vi.fn(); + OneSignal.User.addEventListener('change', listener); + expect(mockRNOneSignal.addUserStateObserver).toHaveBeenCalled(); + expect(addEventManagerListenerSpy).toHaveBeenCalledWith( + USER_STATE_CHANGED, + listener, + ); + }); + + test('should validate callback', () => { + const listener = vi.fn(); + OneSignal.User.addEventListener('change', listener); + expect(helpers.isValidCallback).toHaveBeenCalledWith(listener); + }); + + test('should not add listener if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + const listener = vi.fn(); + OneSignal.User.addEventListener('change', listener); + expect(mockRNOneSignal.addUserStateObserver).not.toHaveBeenCalled(); + }); + }); + + describe('removeEventListener', () => { + test('should remove user state change listener', () => { + const listener = vi.fn(); + OneSignal.User.removeEventListener('change', listener); + expect(removeEventManagerListenerSpy).toHaveBeenCalledWith( + USER_STATE_CHANGED, + listener, + ); + }); + + test('should not remove listener if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + const listener = vi.fn(); + OneSignal.User.removeEventListener('change', listener); + expect(removeEventManagerListenerSpy).not.toHaveBeenCalled(); + }); + }); + + describe('getOnesignalId', () => { + test('should get onesignal id', async () => { + const mockId = 'onesignal-id'; + vi.mocked(mockRNOneSignal.getOnesignalId).mockResolvedValue(mockId); + + const result = await OneSignal.User.getOnesignalId(); + expect(result).toBe(mockId); + expect(mockRNOneSignal.getOnesignalId).toHaveBeenCalled(); + }); + + test('should reject if native module is not loaded', async () => { + isNativeLoadedSpy.mockReturnValue(false); + await expect(OneSignal.User.getOnesignalId()).rejects.toThrow( + 'OneSignal native module not loaded', + ); + }); + }); + + describe('getExternalId', () => { + test('should get external id', async () => { + const mockId = 'external-id'; + vi.mocked(mockRNOneSignal.getExternalId).mockResolvedValue(mockId); + + const result = await OneSignal.User.getExternalId(); + expect(result).toBe(mockId); + expect(mockRNOneSignal.getExternalId).toHaveBeenCalled(); + }); + + test('should reject if native module is not loaded', async () => { + isNativeLoadedSpy.mockReturnValue(false); + await expect(OneSignal.User.getExternalId()).rejects.toThrow( + 'OneSignal native module not loaded', + ); + }); + }); + + describe('setLanguage', () => { + test('should set language', () => { + OneSignal.User.setLanguage('en'); + expect(mockRNOneSignal.setLanguage).toHaveBeenCalledWith('en'); + }); + + test('should not set language if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.User.setLanguage('en'); + expect(mockRNOneSignal.setLanguage).not.toHaveBeenCalled(); + }); + }); + + describe('addAlias', () => { + test('should add alias', () => { + OneSignal.User.addAlias('label', 'id'); + expect(mockRNOneSignal.addAlias).toHaveBeenCalledWith('label', 'id'); + }); + + test('should not add alias if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.User.addAlias('label', 'id'); + expect(mockRNOneSignal.addAlias).not.toHaveBeenCalled(); + }); + }); + + describe('addAliases', () => { + test('should add aliases', () => { + const aliases = { label1: 'id1', label2: 'id2' }; + OneSignal.User.addAliases(aliases); + expect(mockRNOneSignal.addAliases).toHaveBeenCalledWith(aliases); + }); + + test('should not add aliases if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.User.addAliases({}); + expect(mockRNOneSignal.addAliases).not.toHaveBeenCalled(); + }); + }); + + describe('removeAlias', () => { + test('should remove alias', () => { + OneSignal.User.removeAlias('label'); + expect(mockRNOneSignal.removeAlias).toHaveBeenCalledWith('label'); + }); + + test('should not remove alias if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.User.removeAlias('label'); + expect(mockRNOneSignal.removeAlias).not.toHaveBeenCalled(); + }); + }); + + describe('removeAliases', () => { + test('should remove aliases', () => { + const labels = ['label1', 'label2']; + OneSignal.User.removeAliases(labels); + expect(mockRNOneSignal.removeAliases).toHaveBeenCalledWith(labels); + }); + + test('should not remove aliases if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.User.removeAliases(['label']); + expect(mockRNOneSignal.removeAliases).not.toHaveBeenCalled(); + }); + }); + + describe('addEmail', () => { + test('should add email', () => { + OneSignal.User.addEmail(EMAIL); + expect(mockRNOneSignal.addEmail).toHaveBeenCalledWith(EMAIL); + }); + + test('should not add email if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.User.addEmail(EMAIL); + expect(mockRNOneSignal.addEmail).not.toHaveBeenCalled(); + }); + }); + + describe('removeEmail', () => { + test('should remove email', () => { + OneSignal.User.removeEmail(EMAIL); + expect(mockRNOneSignal.removeEmail).toHaveBeenCalledWith(EMAIL); + }); + + test('should not remove email if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.User.removeEmail('test@example.com'); + expect(mockRNOneSignal.removeEmail).not.toHaveBeenCalled(); + }); + }); + + describe('addSms', () => { + test('should add SMS', () => { + OneSignal.User.addSms(SMS_NUMBER); + expect(mockRNOneSignal.addSms).toHaveBeenCalledWith(SMS_NUMBER); + }); + + test('should not add SMS if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.User.addSms(SMS_NUMBER); + expect(mockRNOneSignal.addSms).not.toHaveBeenCalled(); + }); + }); + + describe('removeSms', () => { + test('should remove SMS', () => { + OneSignal.User.removeSms(SMS_NUMBER); + expect(mockRNOneSignal.removeSms).toHaveBeenCalledWith(SMS_NUMBER); + }); + + test('should not remove SMS if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.User.removeSms(SMS_NUMBER); + expect(mockRNOneSignal.removeSms).not.toHaveBeenCalled(); + }); + }); + + describe('addTag', () => { + test('should add tag', () => { + OneSignal.User.addTag('key', 'value'); + expect(mockRNOneSignal.addTag).toHaveBeenCalledWith('key', 'value'); + }); + + test('should convert non-string values to string', () => { + OneSignal.User.addTag('key', 123 as unknown as string); + expect(console.warn).toHaveBeenCalledWith( + 'OneSignal: addTag: tag value must be of type string; attempting to convert', + ); + expect(mockRNOneSignal.addTag).toHaveBeenCalledWith('key', '123'); + }); + + test('should not add tag if key is missing', () => { + OneSignal.User.addTag('', 'value'); + expect(errorSpy).toHaveBeenCalled(); + expect(mockRNOneSignal.addTag).not.toHaveBeenCalled(); + }); + + test('should not add tag if value is null', () => { + OneSignal.User.addTag('key', null as unknown as string); + expect(errorSpy).toHaveBeenCalled(); + expect(mockRNOneSignal.addTag).not.toHaveBeenCalled(); + }); + + test('should not add tag if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.User.addTag('key', 'value'); + expect(mockRNOneSignal.addTag).not.toHaveBeenCalled(); + }); + }); + + describe('addTags', () => { + test('should add tags', () => { + const tags = { key1: 'value1', key2: 'value2' }; + OneSignal.User.addTags(tags); + expect(mockRNOneSignal.addTags).toHaveBeenCalledWith(tags); + }); + + test('should convert non-string values to string', () => { + const tags = { key1: 'value1', key2: 123 }; + OneSignal.User.addTags(tags); + expect(warnSpy).toHaveBeenCalled(); + expect(mockRNOneSignal.addTags).toHaveBeenCalledWith({ + key1: 'value1', + key2: '123', + }); + }); + + test('should not add tags if tags object is empty', () => { + OneSignal.User.addTags({}); + expect(errorSpy).toHaveBeenCalled(); + expect(mockRNOneSignal.addTags).not.toHaveBeenCalled(); + }); + + test('should not add tags if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.User.addTags({ key: 'value' }); + expect(mockRNOneSignal.addTags).not.toHaveBeenCalled(); + }); + }); + + describe('removeTag', () => { + test('should remove tag', () => { + OneSignal.User.removeTag('key'); + expect(mockRNOneSignal.removeTags).toHaveBeenCalledWith(['key']); + }); + + test('should not remove tag if key is not a string', () => { + OneSignal.User.removeTag(123 as unknown as string); + expect(errorSpy).toHaveBeenCalled(); + expect(mockRNOneSignal.removeTags).not.toHaveBeenCalled(); + }); + + test('should not remove tag if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.User.removeTag('key'); + expect(mockRNOneSignal.removeTags).not.toHaveBeenCalled(); + }); + }); + + describe('removeTags', () => { + test('should remove tags', () => { + const keys = ['key1', 'key2']; + OneSignal.User.removeTags(keys); + expect(mockRNOneSignal.removeTags).toHaveBeenCalledWith(keys); + }); + + test('should not remove tags if keys is not an array', () => { + OneSignal.User.removeTags('key' as unknown as string[]); + expect(errorSpy).toHaveBeenCalled(); + expect(mockRNOneSignal.removeTags).not.toHaveBeenCalled(); + }); + + test('should not remove tags if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.User.removeTags(['key']); + expect(mockRNOneSignal.removeTags).not.toHaveBeenCalled(); + }); + }); + + describe('getTags', () => { + test('should get tags', async () => { + const mockTags = { key1: 'value1', key2: 'value2' }; + vi.mocked(mockRNOneSignal.getTags).mockResolvedValue(mockTags); + + const result = await OneSignal.User.getTags(); + expect(result).toEqual(mockTags); + expect(mockRNOneSignal.getTags).toHaveBeenCalled(); + }); + + test('should reject if native module is not loaded', async () => { + isNativeLoadedSpy.mockReturnValue(false); + await expect(OneSignal.User.getTags()).rejects.toThrow( + 'OneSignal native module not loaded', + ); + }); + }); + + describe('Notifications', () => { + describe('hasPermission (deprecated)', () => { + test('should log deprecation warning', () => { + const consoleSpy = vi + .spyOn(console, 'warn') + .mockImplementation(() => {}); + OneSignal.Notifications.hasPermission(); + expect(consoleSpy).toHaveBeenCalled(); + consoleSpy.mockRestore(); + }); + }); + + describe('getPermissionAsync', () => { + test('should get permission status', async () => { + vi.mocked( + mockRNOneSignal.hasNotificationPermission, + ).mockResolvedValue(true); + const result = await OneSignal.Notifications.getPermissionAsync(); + expect(result).toBe(true); + expect(mockRNOneSignal.hasNotificationPermission).toHaveBeenCalled(); + }); + }); + + describe('requestPermission', () => { + test('should request permission', async () => { + vi.mocked( + mockRNOneSignal.requestNotificationPermission, + ).mockResolvedValue(true); + const result = await OneSignal.Notifications.requestPermission(true); + expect(result).toBe(true); + expect( + mockRNOneSignal.requestNotificationPermission, + ).toHaveBeenCalledWith(true); + }); + + test('should reject if native module is not loaded', async () => { + isNativeLoadedSpy.mockReturnValue(false); + await expect( + OneSignal.Notifications.requestPermission(true), + ).rejects.toThrow('OneSignal native module not loaded'); + }); + }); + + describe('canRequestPermission', () => { + test('should check if can request permission', async () => { + vi.mocked( + mockRNOneSignal.canRequestNotificationPermission, + ).mockResolvedValue(true); + const result = await OneSignal.Notifications.canRequestPermission(); + expect(result).toBe(true); + expect( + mockRNOneSignal.canRequestNotificationPermission, + ).toHaveBeenCalled(); + }); + + test('should reject if native module is not loaded', async () => { + isNativeLoadedSpy.mockReturnValue(false); + await expect( + OneSignal.Notifications.canRequestPermission(), + ).rejects.toThrow('OneSignal native module not loaded'); + }); + }); + + describe('registerForProvisionalAuthorization', () => { + beforeEach(() => { + mockPlatform.OS = 'ios'; + }); + + test('should register for provisional authorization on iOS', () => { + const handler = vi.fn(); + OneSignal.Notifications.registerForProvisionalAuthorization(handler); + + expect( + mockRNOneSignal.registerForProvisionalAuthorization, + ).toHaveBeenCalledWith(handler); + expect(helpers.isValidCallback).toHaveBeenCalledWith(handler); + }); + + test('should not register if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + const handler = vi.fn(); + + OneSignal.Notifications.registerForProvisionalAuthorization(handler); + expect( + mockRNOneSignal.registerForProvisionalAuthorization, + ).not.toHaveBeenCalled(); + }); + + test('should log message on Android', () => { + mockPlatform.OS = 'android'; + const handler = vi.fn(); + + OneSignal.Notifications.registerForProvisionalAuthorization(handler); + expect(console.warn).toHaveBeenCalledWith( + 'registerForProvisionalAuthorization: this function is not supported on Android', + ); + expect( + mockRNOneSignal.registerForProvisionalAuthorization, + ).not.toHaveBeenCalled(); + }); + }); + + describe('permissionNative', () => { + test('should get native permission', async () => { + vi.mocked(mockRNOneSignal.permissionNative).mockResolvedValue( + OSNotificationPermission.Authorized, + ); + const result = await OneSignal.Notifications.permissionNative(); + + expect(result).toBe(OSNotificationPermission.Authorized); + expect(mockRNOneSignal.permissionNative).toHaveBeenCalled(); + }); + + test('should reject if native module is not loaded', async () => { + isNativeLoadedSpy.mockReturnValue(false); + await expect( + OneSignal.Notifications.permissionNative(), + ).rejects.toThrow('OneSignal native module not loaded'); + }); + }); + + describe('addEventListener', () => { + test('should add click listener', () => { + const listener = vi.fn(); + OneSignal.Notifications.addEventListener('click', listener); + + expect( + mockRNOneSignal.addNotificationClickListener, + ).toHaveBeenCalled(); + expect(addEventManagerListenerSpy).toHaveBeenCalledWith( + NOTIFICATION_CLICKED, + listener, + ); + expect(helpers.isValidCallback).toHaveBeenCalledWith(listener); + }); + + test('should add foregroundWillDisplay listener', () => { + const listener = vi.fn(); + OneSignal.Notifications.addEventListener( + 'foregroundWillDisplay', + listener, + ); + + expect( + mockRNOneSignal.addNotificationForegroundLifecycleListener, + ).toHaveBeenCalled(); + expect(addEventManagerListenerSpy).toHaveBeenCalledWith( + NOTIFICATION_WILL_DISPLAY, + listener, + ); + expect(helpers.isValidCallback).toHaveBeenCalledWith(listener); + }); + + test('should add permissionChange listener', () => { + const listener = vi.fn(); + OneSignal.Notifications.addEventListener( + 'permissionChange', + listener, + ); + + expect(mockRNOneSignal.addPermissionObserver).toHaveBeenCalled(); + expect(addEventManagerListenerSpy).toHaveBeenCalledWith( + PERMISSION_CHANGED, + listener, + ); + expect(helpers.isValidCallback).toHaveBeenCalledWith(listener); + }); + + test('should not add listener if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + const listener = vi.fn(); + OneSignal.Notifications.addEventListener('click', listener); + expect( + mockRNOneSignal.addNotificationClickListener, + ).not.toHaveBeenCalled(); + }); + }); + + describe('removeEventListener', () => { + test('should remove click listener', () => { + const listener = vi.fn(); + OneSignal.Notifications.removeEventListener('click', listener); + + expect(removeEventManagerListenerSpy).toHaveBeenCalledWith( + NOTIFICATION_CLICKED, + listener, + ); + }); + + test('should remove foregroundWillDisplay listener', () => { + const listener = vi.fn(); + OneSignal.Notifications.removeEventListener( + 'foregroundWillDisplay', + listener, + ); + + expect(removeEventManagerListenerSpy).toHaveBeenCalledWith( + NOTIFICATION_WILL_DISPLAY, + listener, + ); + }); + + test('should remove permissionChange listener', () => { + const listener = vi.fn(); + OneSignal.Notifications.removeEventListener( + 'permissionChange', + listener, + ); + + expect(removeEventManagerListenerSpy).toHaveBeenCalledWith( + PERMISSION_CHANGED, + listener, + ); + }); + }); + + describe('clearAll', () => { + test('should clear all notifications', () => { + OneSignal.Notifications.clearAll(); + expect(mockRNOneSignal.clearAllNotifications).toHaveBeenCalled(); + }); + + test('should not clear if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.Notifications.clearAll(); + expect(mockRNOneSignal.clearAllNotifications).not.toHaveBeenCalled(); + }); + }); + + describe('removeNotification', () => { + const NOTIFICATION_ID = 123; + beforeEach(() => { + mockPlatform.OS = 'android'; + }); + + test('should remove notification on Android', () => { + OneSignal.Notifications.removeNotification(NOTIFICATION_ID); + expect(mockRNOneSignal.removeNotification).toHaveBeenCalledWith( + NOTIFICATION_ID, + ); + }); + + test('should not remove if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.Notifications.removeNotification(NOTIFICATION_ID); + expect(mockRNOneSignal.removeNotification).not.toHaveBeenCalled(); + }); + + test('should log message on iOS', () => { + mockPlatform.OS = 'ios'; + OneSignal.Notifications.removeNotification(NOTIFICATION_ID); + expect(console.warn).toHaveBeenCalled(); + expect(mockRNOneSignal.removeNotification).not.toHaveBeenCalled(); + }); + }); + + describe('removeGroupedNotifications', () => { + const GROUP_ID = 'group-id'; + + beforeEach(() => { + mockPlatform.OS = 'android'; + }); + + test('should remove grouped notifications on Android', () => { + OneSignal.Notifications.removeGroupedNotifications(GROUP_ID); + expect( + mockRNOneSignal.removeGroupedNotifications, + ).toHaveBeenCalledWith(GROUP_ID); + }); + + test('should not remove if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.Notifications.removeGroupedNotifications(GROUP_ID); + + expect( + mockRNOneSignal.removeGroupedNotifications, + ).not.toHaveBeenCalled(); + }); + + test('should log message on iOS', () => { + mockPlatform.OS = 'ios'; + OneSignal.Notifications.removeGroupedNotifications(GROUP_ID); + + expect(console.warn).toHaveBeenCalledWith( + 'removeGroupedNotifications: this function is not supported on iOS', + ); + expect( + mockRNOneSignal.removeGroupedNotifications, + ).not.toHaveBeenCalled(); + }); + }); + }); + + describe('InAppMessages', () => { + describe('addEventListener', () => { + test('should add click listener', () => { + const listener = vi.fn(); + OneSignal.InAppMessages.addEventListener('click', listener); + + expect( + mockRNOneSignal.addInAppMessageClickListener, + ).toHaveBeenCalled(); + expect(addEventManagerListenerSpy).toHaveBeenCalledWith( + IN_APP_MESSAGE_CLICKED, + listener, + ); + expect(helpers.isValidCallback).toHaveBeenCalledWith(listener); + }); + + test('should add willDisplay listener', () => { + const listener = vi.fn(); + OneSignal.InAppMessages.addEventListener('willDisplay', listener); + + expect( + mockRNOneSignal.addInAppMessagesLifecycleListener, + ).toHaveBeenCalled(); + expect(addEventManagerListenerSpy).toHaveBeenCalledWith( + IN_APP_MESSAGE_WILL_DISPLAY, + listener, + ); + expect(helpers.isValidCallback).toHaveBeenCalledWith(listener); + }); + + test('should add didDisplay listener', () => { + const listener = vi.fn(); + OneSignal.InAppMessages.addEventListener('didDisplay', listener); + + expect( + mockRNOneSignal.addInAppMessagesLifecycleListener, + ).toHaveBeenCalled(); + expect(addEventManagerListenerSpy).toHaveBeenCalledWith( + IN_APP_MESSAGE_DID_DISPLAY, + listener, + ); + expect(helpers.isValidCallback).toHaveBeenCalledWith(listener); + }); + + test('should add willDismiss listener', () => { + const listener = vi.fn(); + OneSignal.InAppMessages.addEventListener('willDismiss', listener); + + expect( + mockRNOneSignal.addInAppMessagesLifecycleListener, + ).toHaveBeenCalled(); + expect(addEventManagerListenerSpy).toHaveBeenCalledWith( + IN_APP_MESSAGE_WILL_DISMISS, + listener, + ); + expect(helpers.isValidCallback).toHaveBeenCalledWith(listener); + }); + + test('should add didDismiss listener', () => { + const listener = vi.fn(); + OneSignal.InAppMessages.addEventListener('didDismiss', listener); + + expect( + mockRNOneSignal.addInAppMessagesLifecycleListener, + ).toHaveBeenCalled(); + expect(addEventManagerListenerSpy).toHaveBeenCalledWith( + IN_APP_MESSAGE_DID_DISMISS, + listener, + ); + expect(helpers.isValidCallback).toHaveBeenCalledWith(listener); + }); + + test('should not add listener if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + const listener = vi.fn(); + OneSignal.InAppMessages.addEventListener('click', listener); + expect( + mockRNOneSignal.addInAppMessageClickListener, + ).not.toHaveBeenCalled(); + }); + }); + + describe('removeEventListener', () => { + test.each([ + ['click', IN_APP_MESSAGE_CLICKED], + ['willDisplay', IN_APP_MESSAGE_WILL_DISPLAY], + ['didDisplay', IN_APP_MESSAGE_DID_DISPLAY], + ['willDismiss', IN_APP_MESSAGE_WILL_DISMISS], + ['didDismiss', IN_APP_MESSAGE_DID_DISMISS], + ])('should remove %s listener', (eventName, eventConstant) => { + const listener = vi.fn(); + OneSignal.InAppMessages.removeEventListener( + eventName as any, + listener, + ); + expect(removeEventManagerListenerSpy).toHaveBeenCalledWith( + eventConstant, + listener, + ); + }); + }); + + describe('addTrigger', () => { + test('should add trigger', () => { + OneSignal.InAppMessages.addTrigger('key', 'value'); + expect(mockRNOneSignal.addTriggers).toHaveBeenCalledWith({ + key: 'value', + }); + }); + + test('should log error but still call native method if key is missing', () => { + OneSignal.InAppMessages.addTrigger('', 'value'); + expect(errorSpy).toHaveBeenCalled(); + expect(mockRNOneSignal.addTriggers).toHaveBeenCalledWith({ + '': 'value', + }); + }); + + test('should log error but still call native method if value is null', () => { + OneSignal.InAppMessages.addTrigger('key', null as unknown as string); + expect(errorSpy).toHaveBeenCalled(); + expect(mockRNOneSignal.addTriggers).toHaveBeenCalledWith({ + key: null, + }); + }); + + test('should not add trigger if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.InAppMessages.addTrigger('key', 'value'); + expect(mockRNOneSignal.addTriggers).not.toHaveBeenCalled(); + }); + }); + + describe('addTriggers', () => { + test('should add triggers', () => { + const triggers = { key1: 'value1', key2: 'value2' }; + OneSignal.InAppMessages.addTriggers(triggers); + expect(mockRNOneSignal.addTriggers).toHaveBeenCalledWith(triggers); + }); + + test('should log error but still call native method if empty', () => { + OneSignal.InAppMessages.addTriggers({}); + expect(errorSpy).toHaveBeenCalled(); + expect(mockRNOneSignal.addTriggers).toHaveBeenCalled(); + }); + + test('should not add triggers if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.InAppMessages.addTriggers({ key: 'value' }); + expect(mockRNOneSignal.addTriggers).not.toHaveBeenCalled(); + }); + }); + + describe('removeTrigger', () => { + test('should remove trigger', () => { + OneSignal.InAppMessages.removeTrigger('key'); + expect(mockRNOneSignal.removeTrigger).toHaveBeenCalledWith('key'); + }); + + test('should not remove trigger if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.InAppMessages.removeTrigger('key'); + expect(mockRNOneSignal.removeTrigger).not.toHaveBeenCalled(); + }); + }); + + describe('removeTriggers', () => { + test('should remove triggers', () => { + const keys = ['key1', 'key2']; + OneSignal.InAppMessages.removeTriggers(keys); + expect(mockRNOneSignal.removeTriggers).toHaveBeenCalledWith(keys); + }); + + test('should not remove triggers if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.InAppMessages.removeTriggers(['key']); + expect(mockRNOneSignal.removeTriggers).not.toHaveBeenCalled(); + }); + }); + + describe('clearTriggers', () => { + test('should clear triggers', () => { + OneSignal.InAppMessages.clearTriggers(); + expect(mockRNOneSignal.clearTriggers).toHaveBeenCalled(); + }); + + test('should not clear triggers if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.InAppMessages.clearTriggers(); + expect(mockRNOneSignal.clearTriggers).not.toHaveBeenCalled(); + }); + }); + + describe('setPaused', () => { + test('should set paused', () => { + OneSignal.InAppMessages.setPaused(true); + expect(mockRNOneSignal.paused).toHaveBeenCalledWith(true); + }); + + test('should not set paused if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.InAppMessages.setPaused(true); + expect(mockRNOneSignal.paused).not.toHaveBeenCalled(); + }); + }); + + describe('getPaused', () => { + test('should get paused status', async () => { + vi.mocked(mockRNOneSignal.getPaused).mockResolvedValue(true); + const result = await OneSignal.InAppMessages.getPaused(); + expect(result).toBe(true); + expect(mockRNOneSignal.getPaused).toHaveBeenCalled(); + }); + + test('should reject if native module is not loaded', async () => { + isNativeLoadedSpy.mockReturnValue(false); + await expect(OneSignal.InAppMessages.getPaused()).rejects.toThrow( + 'OneSignal native module not loaded', + ); + }); + }); + }); + + describe('Location', () => { + describe('requestPermission', () => { + test('should request location permission', () => { + OneSignal.Location.requestPermission(); + expect(mockRNOneSignal.requestLocationPermission).toHaveBeenCalled(); + }); + + test('should not request permission if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.Location.requestPermission(); + expect( + mockRNOneSignal.requestLocationPermission, + ).not.toHaveBeenCalled(); + }); + }); + + describe('setShared', () => { + test('should set location shared', () => { + OneSignal.Location.setShared(true); + expect(mockRNOneSignal.setLocationShared).toHaveBeenCalledWith(true); + }); + + test('should not set shared if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.Location.setShared(true); + expect(mockRNOneSignal.setLocationShared).not.toHaveBeenCalled(); + }); + }); + + describe('isShared', () => { + test('should check if location is shared', async () => { + vi.mocked(mockRNOneSignal.isLocationShared).mockResolvedValue(true); + const result = await OneSignal.Location.isShared(); + expect(result).toBe(true); + expect(mockRNOneSignal.isLocationShared).toHaveBeenCalled(); + }); + + test('should reject if native module is not loaded', async () => { + isNativeLoadedSpy.mockReturnValue(false); + await expect(OneSignal.Location.isShared()).rejects.toThrow( + 'OneSignal native module not loaded', + ); + }); + }); + }); + + describe('Session', () => { + const OUTCOME_NAME = 'outcome-name'; + + describe('addOutcome', () => { + test('should add outcome', () => { + OneSignal.Session.addOutcome(OUTCOME_NAME); + expect(mockRNOneSignal.addOutcome).toHaveBeenCalledWith(OUTCOME_NAME); + }); + + test('should not add outcome if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.Session.addOutcome(OUTCOME_NAME); + expect(mockRNOneSignal.addOutcome).not.toHaveBeenCalled(); + }); + }); + + describe('addUniqueOutcome', () => { + test('should add unique outcome', () => { + OneSignal.Session.addUniqueOutcome(OUTCOME_NAME); + expect(mockRNOneSignal.addUniqueOutcome).toHaveBeenCalledWith( + OUTCOME_NAME, + ); + }); + + test('should not add unique outcome if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.Session.addUniqueOutcome(OUTCOME_NAME); + expect(mockRNOneSignal.addUniqueOutcome).not.toHaveBeenCalled(); + }); + }); + + describe('addOutcomeWithValue', () => { + test('should add outcome with string value', () => { + OneSignal.Session.addOutcomeWithValue(OUTCOME_NAME, '100'); + expect(mockRNOneSignal.addOutcomeWithValue).toHaveBeenCalledWith( + OUTCOME_NAME, + 100, + ); + }); + + test('should add outcome with number value', () => { + OneSignal.Session.addOutcomeWithValue(OUTCOME_NAME, 100); + expect(mockRNOneSignal.addOutcomeWithValue).toHaveBeenCalledWith( + OUTCOME_NAME, + 100, + ); + }); + + test('should not add outcome if native module is not loaded', () => { + isNativeLoadedSpy.mockReturnValue(false); + OneSignal.Session.addOutcomeWithValue(OUTCOME_NAME, 100); + expect(mockRNOneSignal.addOutcomeWithValue).not.toHaveBeenCalled(); + }); + }); + }); + }); +}); diff --git a/src/index.ts b/src/index.ts index bf5bb470..a6fb7ce5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,4 @@ -'use strict'; - import { NativeModules, Platform } from 'react-native'; -import EventManager from './events/EventManager'; import { IN_APP_MESSAGE_CLICKED, IN_APP_MESSAGE_DID_DISMISS, @@ -13,7 +10,9 @@ import { PERMISSION_CHANGED, SUBSCRIPTION_CHANGED, USER_STATE_CHANGED, -} from './events/events'; +} from './constants/events'; +import type { OSNotificationPermission } from './constants/subscription'; +import EventManager from './events/EventManager'; import NotificationWillDisplayEvent from './events/NotificationWillDisplayEvent'; import { isNativeModuleLoaded, isValidCallback } from './helpers'; import type { @@ -25,19 +24,18 @@ import type { InAppMessageEventTypeMap, InAppMessageWillDismissEvent, InAppMessageWillDisplayEvent, -} from './models/InAppMessage'; -import type { LiveActivitySetupOptions } from './models/LiveActivities'; +} from './types/inAppMessage'; +import type { LiveActivitySetupOptions } from './types/liveActivities'; import type { NotificationClickEvent, NotificationEventName, NotificationEventTypeMap, -} from './models/NotificationEvents'; -import { - OSNotificationPermission, - type PushSubscriptionChangedState, - type PushSubscriptionState, -} from './models/Subscription'; -import type { UserChangedState, UserState } from './models/User'; +} from './types/notificationEvents'; +import type { + PushSubscriptionChangedState, + PushSubscriptionState, +} from './types/subscription'; +import type { UserChangedState, UserState } from './types/user'; const RNOneSignal = NativeModules.OneSignal; const eventManager = new EventManager(RNOneSignal); @@ -371,7 +369,7 @@ export namespace OneSignal { 'OneSignal: This method has been deprecated. Use getOptedInAsync instead for getting push subscription opted in status.', ); - return pushSub.optedIn; + return pushSub.optedIn ?? false; } /** @@ -668,7 +666,7 @@ export namespace OneSignal { isValidCallback(handler); RNOneSignal.registerForProvisionalAuthorization(handler); } else { - console.log( + console.warn( 'registerForProvisionalAuthorization: this function is not supported on Android', ); } @@ -757,7 +755,7 @@ export namespace OneSignal { if (Platform.OS === 'android') { RNOneSignal.removeNotification(id); } else { - console.log( + console.warn( 'removeNotification: this function is not supported on iOS', ); } @@ -774,7 +772,7 @@ export namespace OneSignal { if (Platform.OS === 'android') { RNOneSignal.removeGroupedNotifications(id); } else { - console.log( + console.warn( 'removeGroupedNotifications: this function is not supported on iOS', ); } @@ -1000,9 +998,9 @@ export namespace OneSignal { } } +export { OSNotificationPermission } from './constants/subscription'; export { NotificationWillDisplayEvent, - OSNotificationPermission, type InAppMessage, type InAppMessageClickEvent, type InAppMessageDidDismissEvent, @@ -1016,6 +1014,6 @@ export { type UserState, }; -export type { InAppMessageClickResult } from './models/InAppMessage'; -export type { NotificationClickResult } from './models/NotificationEvents'; export { default as OSNotification } from './OSNotification'; +export type { InAppMessageClickResult } from './types/inAppMessage'; +export type { NotificationClickResult } from './types/notificationEvents'; diff --git a/src/models/InAppMessage.ts b/src/types/inAppMessage.ts similarity index 100% rename from src/models/InAppMessage.ts rename to src/types/inAppMessage.ts diff --git a/src/models/LiveActivities.ts b/src/types/liveActivities.ts similarity index 100% rename from src/models/LiveActivities.ts rename to src/types/liveActivities.ts diff --git a/src/models/NotificationEvents.ts b/src/types/notificationEvents.ts similarity index 100% rename from src/models/NotificationEvents.ts rename to src/types/notificationEvents.ts diff --git a/src/models/Subscription.ts b/src/types/subscription.ts similarity index 55% rename from src/models/Subscription.ts rename to src/types/subscription.ts index d4771f55..cffc9485 100644 --- a/src/models/Subscription.ts +++ b/src/types/subscription.ts @@ -1,11 +1,3 @@ -export enum OSNotificationPermission { - NotDetermined = 0, - Denied, - Authorized, - Provisional, // only available in iOS 12 - Ephemeral, // only available in iOS 14 -} - export interface PushSubscriptionState { id?: string; token?: string; diff --git a/src/models/User.ts b/src/types/user.ts similarity index 100% rename from src/models/User.ts rename to src/types/user.ts diff --git a/tsconfig.json b/tsconfig.json index 984f29db..5e1228c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,15 @@ /** * Should use import type syntax instead of regular import when getting types/interfaces from a module */ - "verbatimModuleSyntax": true + "verbatimModuleSyntax": true, + "types": ["vitest/globals"] }, - "exclude": ["node_modules", "dist", "android", "examples", "ios"] + "exclude": [ + "node_modules", + "dist", + "android", + "examples", + "ios", + "vitest.config.ts" + ] } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..26641578 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,23 @@ +import path from 'path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [], + test: { + globals: true, + clearMocks: true, + include: ['**/*.test.ts', '**/*.test.tsx'], + coverage: { + exclude: ['__mocks__'], + enabled: true, + reporter: ['text-summary', 'lcov'], + reportOnFailure: true, + reportsDirectory: 'coverage', + }, + }, + resolve: { + alias: { + 'react-native': path.resolve(__dirname, './__mocks__/react-native.ts'), + }, + }, +});