dotintent / react-native-ble-plx

React Native BLE library
Apache License 2.0
3.08k stars 515 forks source link

test fails using jest. #1239

Closed benevolentsocialplanner closed 2 months ago

benevolentsocialplanner commented 2 months ago

Prerequisites

Question

Invariant Violation: new NativeEventEmitter() requires a non-null argument.

I'm trying to test if the behavior is as expected. Some libraries have docs for mock integration but as far as i discovered react-native-ble-plx doesn't.

Here are the results after running npm run test.

● renders correctly

Invariant Violation: `new NativeEventEmitter()` requires a non-null argument.

  16 |
  17 | const App = () => {
> 18 |   const [manager] = useState(new BleManager());
     |                              ^

jest.config.js

module.exports = {
  preset: 'react-native',
  transform: {
    '^.+\\.jsx?$': 'babel-jest',
    '^.+\\.tsx?$': 'babel-jest',
  },
  transformIgnorePatterns: [
    'node_modules/(?!(jest-)?react-native|@react-native|react-clone-referenced-element)',
  ],
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  setupFiles: ['<rootDir>/jest-setup.js'],
};

app.test.jsx

import 'react-native';
import React from 'react';
import App from '../App';
import { NativeModules, NativeEventEmitter } from 'react-native';
import { it, beforeEach, expect, jest } from '@jest/globals';
import renderer from 'react-test-renderer';
// Mock NativeModules.BleManager

it('renders correctly', () => {
  renderer.create(<App />);
});

in tests i have a mocks folder. under there i have a native-event-emmiter.js

native-event-emitter.js


jest.mock('react-native', () => ({
  NativeEventEmitter: jest.fn().mockImplementation(() => ({
    addListener: jest.fn(),
    emoveListener: jest.fn(),
  removeAllListeners: jest.fn(),
  emit: jest.fn(),
  })),
}));

test('Dummy test to pass Jest validation', () => {
  expect(true).toBe(true);
});

this does pass the test. how can i achive the expected results?

let me know what you guys think

Question related code

App.jsx: 

import React, { useEffect, useState, useRef } from 'react';
import {
  Alert,
  SafeAreaView,
  StatusBar,
  StyleSheet,
  Text,
  View,
  TouchableOpacity
} from 'react-native';
import { BleManager } from 'react-native-ble-plx';
import AsyncStorage from '@react-native-async-storage/async-storage';

const DEVICE_PREFIX = "AMF-Bruns";
const IN_OUT_PRESS_DURATION = 3000; // 3 seconds

const App = () => {
  const [manager] = useState(new BleManager());
  const [connectedDevice, setConnectedDevice] = useState(null);
  const [scanning, setScanning] = useState(false);
  const [status, setStatus] = useState('Bluetooth Module not initialized');
  const inPressStart = useRef(null);
  const outPressStart = useRef(null);

  useEffect(() => {
    initializeBluetooth();
    retrieveSavedDevice();

    return () => {
      manager.destroy();
    };
  }, []);

  const initializeBluetooth = async () => {
    try {
      setStatus('Bluetooth Module initialized');
    } catch (error) {
      console.error("Error initializing Bluetooth Module", error);
    }
  };

  const retrieveSavedDevice = async () => {
    const savedDeviceId = await AsyncStorage.getItem('savedDeviceId');
    if (savedDeviceId) {
      connectToDevice(savedDeviceId);
    }
  };

  const startAdvertising = () => {
    setStatus('Scanning...');
    setScanning(true);
    manager.startDeviceScan(null, null, (error, device) => {
      if (error) {
        console.error(error);
        setStatus('Error: ' + error.message);
        return;
      }

      if (device.name && device.name.startsWith(DEVICE_PREFIX)) {
        connectToDevice(device.id);
      }
    });

    setTimeout(() => {
      manager.stopDeviceScan();
      setScanning(false);
      setStatus('Scan stopped');
    }, 120000); // 2 minutes
  };

  const connectToDevice = async (deviceId) => {
    try {
      const device = await manager.connectToDevice(deviceId);
      setConnectedDevice(device);
      await AsyncStorage.setItem('savedDeviceId', device.id);
      setStatus("Connected to: " + device.name);
    } catch (error) {
      console.error("Connection error:", error);
      setStatus("Connection error: " + error.message);
    }
  };

  const handleButtonPress = (button) => {
    const now = Date.now();
    if (button === 'IN') {
      inPressStart.current = now;
    } else if (button === 'OUT') {
      outPressStart.current = now;
    }

    if (inPressStart.current && outPressStart.current &&
        (now - inPressStart.current >= IN_OUT_PRESS_DURATION) &&
        (now - outPressStart.current >= IN_OUT_PRESS_DURATION)) {
      if (!connectedDevice) {
        startAdvertising();
      } else {
        disconnectDevice();
      }
      inPressStart.current = null;
      outPressStart.current = null;
    }
  };

  const disconnectDevice = async () => {
    if (connectedDevice) {
      await manager.cancelDeviceConnection(connectedDevice.id);
      await AsyncStorage.removeItem('savedDeviceId');
      setConnectedDevice(null);
      setStatus("Disconnected");
      startAdvertising();
    }
  };

  return (
    <SafeAreaView style={styles.safeArea}>
      <StatusBar barStyle="dark-content" />
      <View style={styles.container}>
        <Text style={styles.statusText}>{status}</Text>
        <TouchableOpacity onPress={() => handleButtonPress('IN')} style={styles.button}>
          <Text style={styles.buttonText}>IN</Text>
        </TouchableOpacity>
        <TouchableOpacity onPress={() => handleButtonPress('OUT')} style={styles.button}>
          <Text style={styles.buttonText}>OUT</Text>
        </TouchableOpacity>
        <Text>{status}</Text>
        {scanning && <Text style={styles.scanningText}>Scanning...</Text>}
      </View>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  safeArea: {
    flex: 1,
  },
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  statusText: {
    marginBottom: 20,
    fontSize: 16,
    color: '#333',
  },
  button: {
    backgroundColor: '#007AFF',
    padding: 15,
    borderRadius: 8,
    alignItems: 'center',
    marginBottom: 20,
    width: 100,
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: 'bold',
  },
  scanningText: {
    marginTop: 20,
    fontSize: 16,
    color: '#333',
  }
});

export default App;
benevolentsocialplanner commented 2 months ago

closing