cometchat-native-testing-0f1c2b — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited cometchat-native-testing-0f1c2b (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Teaches Claude how to write and run tests against a CometChat React Native integration. Covers:
@cometchat/chat-uikit-react-native and @cometchat/chat-sdk-react-native (both pull in native modules that fail in Node's jest-expo / jest-react-native environments)Ground truth: @cometchat/[email protected]'s example jest config (examples/SampleAppWithPushNotifications/jest.config.js) and the standard RN testing toolkit docs (callstack.github.io/react-native-testing-library, wix.github.io/Detox, maestro.mobile.dev).
Not every test is worth the maintenance cost. A few rules of thumb:
Worth testing:
Skip:
<CometChatConversations> renders a list is testing CometChat's code.cometchat-native-theming edits all churn the snapshots with no real signal.The golden rule: if the test fails because your code changed, it's valuable. If it fails because the UI Kit updated or a network blip happened, it's churn.
| Layer | Tool | Why |
|---|---|---|
| Unit + component tests | Jest + @testing-library/react-native | The RN default. Preset handles metro module resolution. |
| Mocking | Jest moduleNameMapper + manual mocks | UI Kit imports native modules — can't run real components in a Node env. |
| Snapshot | Jest's built-in | Use sparingly — see §7 |
| E2E | Maestro OR Detox | See §10 for tradeoff |
| CI | GitHub Actions / EAS / Bitrise | §11 |
Install:
# Bare RN
npm install --save-dev jest @testing-library/react-native @testing-library/jest-native \
react-test-renderer @types/jest
# Expo
npx expo install --dev jest-expo @testing-library/react-native @testing-library/jest-native \
react-test-rendererjest-expo wraps react-native preset with Expo-specific module resolution (handles expo-modules-core, expo-router, etc.).
Bare RN — jest.config.js:
module.exports = {
preset: "react-native",
setupFilesAfterEach: ["<rootDir>/jest.setup.ts"],
transformIgnorePatterns: [
"node_modules/(?!(?:react-native|@react-native|@react-navigation|" +
"@cometchat/chat-uikit-react-native|@cometchat/chat-sdk-react-native|" +
"react-native-.+|@notifee/react-native)/)",
],
moduleNameMapper: {
"^@cometchat/chat-uikit-react-native$": "<rootDir>/__mocks__/cometchat-uikit.ts",
"^@cometchat/chat-sdk-react-native$": "<rootDir>/__mocks__/cometchat-sdk.ts",
},
};Expo — jest.config.js:
module.exports = {
preset: "jest-expo",
setupFilesAfterEach: ["<rootDir>/jest.setup.ts"],
transformIgnorePatterns: [
"node_modules/(?!(?:(jest-)?react-native|@react-native|expo(nent)?|@expo(nent)?/.*|" +
"@expo-google-fonts/.*|react-navigation|@react-navigation/.*|" +
"@cometchat/chat-uikit-react-native|@cometchat/chat-sdk-react-native|" +
"@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)/)",
],
moduleNameMapper: {
"^@cometchat/chat-uikit-react-native$": "<rootDir>/__mocks__/cometchat-uikit.ts",
"^@cometchat/chat-sdk-react-native$": "<rootDir>/__mocks__/cometchat-sdk.ts",
},
};`transformIgnorePatterns` matters. By default Jest doesn't transform anything under node_modules, but CometChat ships ES module source. Without the pattern, Jest errors with SyntaxError: Unexpected token 'export'. The UI Kit + SDK names must be in the allow list.
jest.setup.tsimport "@testing-library/jest-native/extend-expect";
// Silence RN's "AnimatedValue" warning noise in tests
jest.mock("react-native/Libraries/Animated/NativeAnimatedHelper");
// Mock native modules that the UI Kit pulls in
jest.mock("react-native-gesture-handler", () => {
const View = require("react-native/Libraries/Components/View/View");
return {
GestureHandlerRootView: View,
PanGestureHandler: View,
TapGestureHandler: View,
State: {},
Directions: {},
};
});
jest.mock("react-native-reanimated", () =>
require("react-native-reanimated/mock"),
);
jest.mock("react-native-safe-area-context", () => ({
SafeAreaProvider: ({ children }: { children: React.ReactNode }) => children,
SafeAreaView: ({ children }: { children: React.ReactNode }) => children,
useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 0, left: 0 }),
}));
// Silence console.warn from legacy components in tests — re-enable locally if debugging
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => {
if (
typeof args[0] === "string" &&
/componentWill|Unable to find|act\(\)/i.test(args[0])
) {
return;
}
originalWarn(...args);
};The UI Kit's top-level components (CometChatConversations, CometChatMessageList, etc.) wire socket listeners, call native modules, and render FlatLists with async data. Rendering them in Jest is more effort than value.
Strategy: mock them as transparent Views that forward children. This lets your tests verify your integration (are the right props being passed? does the right component mount in the right screen?) without pulling in the real implementation.
__mocks__/cometchat-uikit.ts:
import React from "react";
import { View } from "react-native";
const passThrough = (name: string) =>
React.forwardRef<unknown, Record<string, unknown>>((props, ref) => {
const { children, ...rest } = props as { children?: React.ReactNode };
return (
<View ref={ref as never} testID={name} {...rest}>
{children}
</View>
);
});
export const CometChatUIKit = {
init: jest.fn(async () => undefined),
login: jest.fn(async () => ({ getUid: () => "cometchat-uid-1" })),
logout: jest.fn(async () => undefined),
getLoggedinUser: jest.fn(async () => ({ getUid: () => "cometchat-uid-1" })),
};
export const UIKitSettingsBuilder = class {
setAppId() { return this; }
setRegion() { return this; }
setAuthKey() { return this; }
subscribePresenceForAllUsers() { return this; }
build() { return {}; }
};
export const CometChatThemeProvider = passThrough("CometChatThemeProvider");
export const CometChatI18nProvider = passThrough("CometChatI18nProvider");
export const CometChatConversations = passThrough("CometChatConversations");
export const CometChatMessageList = passThrough("CometChatMessageList");
export const CometChatMessageComposer = passThrough("CometChatMessageComposer");
export const CometChatMessageHeader = passThrough("CometChatMessageHeader");
export const CometChatUsers = passThrough("CometChatUsers");
export const CometChatGroups = passThrough("CometChatGroups");
export const CometChatIncomingCall = passThrough("CometChatIncomingCall");
export const CometChatOutgoingCall = passThrough("CometChatOutgoingCall");
export const CometChatUIEventHandler = {
addUIListener: jest.fn(),
removeListener: jest.fn(),
};
export const CometChatUIEvents = {};
export const useTheme = () => ({
color: {
primary: "#6852D6",
background1: "#FFFFFF",
textPrimary: "#141414",
},
typography: {
heading1: { fontFamily: "System", fontSize: 28 },
body1: { fontFamily: "System", fontSize: 16 },
},
});__mocks__/cometchat-sdk.ts:
export const CometChat = {
getUser: jest.fn(async (uid: string) => ({ getUid: () => uid, getName: () => "Test User" })),
getGroup: jest.fn(async (guid: string) => ({ getGuid: () => guid, getName: () => "Test Group" })),
addMessageListener: jest.fn(),
removeMessageListener: jest.fn(),
};
export const CometChatNotifications = {
PushPlatforms: {
FCM_REACT_NATIVE_ANDROID: "fcm-android",
FCM_REACT_NATIVE_IOS: "fcm-ios",
APNS_REACT_NATIVE_DEVICE: "apns-device",
APNS_REACT_NATIVE_VOIP: "apns-voip",
},
registerPushToken: jest.fn(async () => ({ success: true })),
unregisterPushToken: jest.fn(async () => ({ success: true })),
};Every real <CometChatMessageList> in your code renders as <View testID="CometChatMessageList"> in tests. You can assert on testID + the props you passed.
Example — a custom chat screen that renders <CometChatMessageList> for a specific user:
// src/screens/MessagesScreen.tsx
import { CometChat } from "@cometchat/chat-sdk-react-native";
import { CometChatMessageList } from "@cometchat/chat-uikit-react-native";
import { useEffect, useState } from "react";
export function MessagesScreen({ uid }: { uid: string }) {
const [user, setUser] = useState<CometChat.User | null>(null);
useEffect(() => {
CometChat.getUser(uid).then(setUser);
}, [uid]);
if (!user) return null;
return <CometChatMessageList user={user} hideReplyInThreadOption />;
}Test:
// src/screens/__tests__/MessagesScreen.test.tsx
import { render, waitFor } from "@testing-library/react-native";
import { CometChat } from "@cometchat/chat-sdk-react-native";
import { MessagesScreen } from "../MessagesScreen";
test("fetches user then renders MessageList", async () => {
const { getByTestId, queryByTestId } = render(<MessagesScreen uid="alice" />);
// Before fetch resolves — nothing rendered
expect(queryByTestId("CometChatMessageList")).toBeNull();
// After fetch resolves — list renders with user prop
await waitFor(() => expect(getByTestId("CometChatMessageList")).toBeTruthy());
expect(CometChat.getUser).toHaveBeenCalledWith("alice");
});
test("passes hideReplyInThreadOption to MessageList", async () => {
const { findByTestId } = render(<MessagesScreen uid="alice" />);
const list = await findByTestId("CometChatMessageList");
// The mocked component stored props on the View — check them
expect(list.props.hideReplyInThreadOption).toBe(true);
});The second test is the valuable one — it guards the mandatory hideReplyInThreadOption flag (hard rule §4 in cometchat-native-core) against a future refactor dropping it.
Do snapshot:
Don't snapshot:
CometChatThemeProvider — a token change churns snapshots with no regression meaning.// Good — isolated, theme-free
test("formatTimestamp(1700000000000) matches snapshot", () => {
expect(formatTimestamp(1_700_000_000_000)).toMatchInlineSnapshot(`"Tue, 14 Nov 2023"`);
});If a snapshot test churns on every UI Kit update, delete it — it's net-negative.
The four-wrapper chain (hard rule §3 in cometchat-native-core) is one of the most common regressions AI edits introduce. Test that all four wrappers render:
// src/App.test.tsx
import { render } from "@testing-library/react-native";
import App from "./App";
test("App mounts all four CometChat wrappers", () => {
const { getByTestId } = render(<App />);
// The mocked wrappers each render a View with testID matching their name
expect(getByTestId("CometChatThemeProvider")).toBeTruthy();
// Note: GestureHandlerRootView and SafeAreaProvider are pass-through Views
// without distinct testIDs in our setup, so assert via presence of children
// OR extend the mock in jest.setup.ts to add testIDs.
});For the GestureHandlerRootView + SafeAreaProvider assertion, extend their mocks in jest.setup.ts to add testID:
jest.mock("react-native-gesture-handler", () => {
const { View } = require("react-native");
return {
GestureHandlerRootView: (props: any) =>
require("react").createElement(View, { ...props, testID: "GestureHandlerRootView" }),
// ...
};
});The ensureLoggedIn helper (hard rule §2 in cometchat-native-core) must handle concurrent calls safely:
// src/providers/__tests__/CometChatProvider.test.tsx
import { CometChatUIKit } from "@cometchat/chat-uikit-react-native";
import { ensureLoggedIn } from "../CometChatProvider";
beforeEach(() => {
jest.clearAllMocks();
});
test("concurrent ensureLoggedIn calls only invoke login once", async () => {
(CometChatUIKit.getLoggedinUser as jest.Mock).mockResolvedValue(null);
const results = await Promise.all([
ensureLoggedIn("alice"),
ensureLoggedIn("alice"),
ensureLoggedIn("alice"),
]);
expect(CometChatUIKit.login).toHaveBeenCalledTimes(1);
});
test("already-logged-in skips login entirely", async () => {
(CometChatUIKit.getLoggedinUser as jest.Mock).mockResolvedValue({
getUid: () => "alice",
});
await ensureLoggedIn("alice");
expect(CometChatUIKit.login).not.toHaveBeenCalled();
});These two tests catch the most common ensureLoggedIn breakages — dropping the module-level promise guard, or forgetting the getLoggedinUser short-circuit.
Two choices for end-to-end. Different philosophies.
| Detox | Maestro | |
|---|---|---|
| Config | Native drivers (iOS + Android). .detoxrc.js. | YAML flows. Single binary. |
| Language | JavaScript / TypeScript | YAML |
| Setup | Heavy — Xcode build, Detox CLI, Jest runner | Light — brew install, run CLI |
| CI | Slow (full native build each run) | Fast (reuses install) |
| Speed | Flaky in CI, reliable locally | Fast, stable |
| iOS + Android parity | Yes | Yes |
| Cloud runs | No native cloud support | Maestro Cloud (paid) |
| Learning curve | Steep if you don't know RN internals | Low |
Recommendation: Maestro for most teams. Flows are readable, runs in seconds, CI-friendly. Detox makes sense if you have existing Jest infrastructure and want E2E to live in the same runner.
.maestro/chat-happy-path.yaml:
appId: com.yourapp.mobile
---
- launchApp
- tapOn: "Login"
- inputText: "cometchat-uid-1"
- tapOn: "Continue"
- assertVisible: "Messages"
- tapOn: "Messages"
- assertVisible: "Conversations"
- tapOn: id: "conversation-cometchat-uid-2"
- inputText: "Hello from Maestro"
- tapOn: id: "send-button"
- assertVisible: "Hello from Maestro"Run:
maestro test .maestro/chat-happy-path.yamlNeeds your RN <CometChatMessageComposer> to expose testID="send-button" — the UI Kit supports this via the sendButtonStyle slot or via a Custom view template.
.detoxrc.js (abbreviated):
module.exports = {
testRunner: { args: { $0: "jest", config: "e2e/jest.config.js" } },
apps: {
"ios.debug": {
type: "ios.app",
binaryPath: "ios/build/Build/Products/Debug-iphonesimulator/YourApp.app",
},
},
devices: { simulator: { type: "ios.simulator", device: { type: "iPhone 15" } } },
configurations: {
"ios.sim.debug": { device: "simulator", app: "ios.debug" },
},
};Test:
// e2e/chat.test.ts
describe("chat flow", () => {
beforeAll(async () => {
await device.launchApp();
});
it("sends a message", async () => {
await element(by.text("Login")).tap();
await element(by.id("uid-input")).typeText("cometchat-uid-1");
await element(by.text("Continue")).tap();
await element(by.text("Messages")).tap();
await element(by.id("conversation-cometchat-uid-2")).tap();
await element(by.id("message-input")).typeText("Hello from Detox");
await element(by.id("send-button")).tap();
await expect(element(by.text("Hello from Detox"))).toBeVisible();
});
});Detox needs a native dev build first (detox build --configuration ios.sim.debug) — slow in CI.
.github/workflows/test.yml:
name: test
on: [push, pull_request]
jobs:
jest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx tsc --noEmit
- run: npm test -- --ci --coverageMaestro runs on macOS runners (iOS) or Linux runners with Android emulators. The mobile-dev-inc/action-maestro-cloud action simplifies it:
e2e:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Build iOS
run: |
cd ios
pod install
xcodebuild -workspace YourApp.xcworkspace -scheme YourApp \
-sdk iphonesimulator -configuration Debug \
-derivedDataPath build
- name: Run Maestro flows
uses: mobile-dev-inc/action-maestro-cloud@v1
with:
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
app-file: ios/build/Build/Products/Debug-iphonesimulator/YourApp.app
workspace: .maestroIf you're on EAS, eas build --profile preview followed by maestro test against the preview build works for CI smoke tests. EAS Test (paid) orchestrates Maestro runs across multiple devices.
| Symptom | Cause | Fix |
|---|---|---|
SyntaxError: Unexpected token 'export' | Jest not transforming a UI Kit or SDK file | Add package name to transformIgnorePatterns allow list |
TypeError: Cannot read properties of undefined (reading 'Directions') | Gesture handler native module missing | Mock in jest.setup.ts (see §4) |
| Tests hang for 30s+ | Real WebSocket or fetch not mocked | Add jest.useFakeTimers() + mock fetch |
| Snapshot fails after no code change | Theme token update churned output | Either delete the snapshot (§7) or run --updateSnapshot |
useInsertionEffect must not schedule updates warning | React Navigation dev warning, harmless in tests | Silence in jest.setup.ts (see §4) |
Could not find React Testing Library matchers | @testing-library/jest-native not extended | import "@testing-library/jest-native/extend-expect" in setup |
| Maestro "app not installed" | Bundle ID mismatch or simulator not booted | xcrun simctl boot "iPhone 15", verify appId in YAML |
| Detox "Cannot find element" | testID not set on UI Kit component | Add via slot view template or custom view; don't rely on text matching |
| This skill | Covers |
|---|---|
cometchat-native-testing (this) | Jest + RNTL setup, mocking UI Kit + SDK, component / provider / login tests, Detox vs Maestro for E2E, CI |
cometchat-native-core | The provider chain + login concurrency patterns you're testing |
cometchat-native-components | Component catalog — what props to assert in tests |
cometchat-native-customization | DataSource decorators + custom views — test per §6 |
cometchat-native-push | Push tests (mock CometChatNotifications); E2E tap-to-deep-link needs a real device |
cometchat-native-troubleshooting | Metro cache / pod install / native module errors (often surface first in a CI run) |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.