Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5d18c77
feat: add TypeScript types for telemetry data and WebSocket messages
Sep 16, 2025
2657480
feat: implement Facility class for API key management
Sep 16, 2025
356206c
feat: implement Device class for WebSocket connection management
Sep 16, 2025
09f6389
feat: create useLiveTelemetry React hook for real-time data consumption
Sep 16, 2025
657c8c7
feat: set up library exports and main entry point
Sep 16, 2025
1d5169e
test: add comprehensive test suite with WebSocket mocks
Sep 16, 2025
2ea510e
feat: create example implementation demonstrating library usage
Sep 16, 2025
41472f7
chore: update project configuration and dependencies
Sep 16, 2025
6e8a430
docs: add comprehensive documentation and usage examples
Sep 16, 2025
2953e7a
refactor: reorganize project structure and update test setup
azariak Sep 16, 2025
6b698ea
style: fixed linter errors
azariak Sep 16, 2025
bc60a27
style: fixed last lint error hopefully :)
azariak Sep 16, 2025
3065a74
chore: updated depreciating packages
crypto-a Sep 17, 2025
5934473
refactor: modified Faiclity object
crypto-a Sep 17, 2025
568bf25
refactor: fixed the device class
crypto-a Sep 17, 2025
af1ced6
del: removed doublicates and unnecesary files
crypto-a Sep 17, 2025
ed34218
refactor: modofied the hook code
crypto-a Sep 17, 2025
0199a7f
feat: added storybook setup
crypto-a Sep 17, 2025
aa2ea5f
fix: foixed linting issue
crypto-a Sep 17, 2025
8b699d5
tests: fixed testcases
crypto-a Sep 17, 2025
bb63893
docs: updated readme
crypto-a Sep 17, 2025
6a8cf39
fix: fixed ci issue
crypto-a Sep 17, 2025
95c74ac
fix: fixed the setupTests
crypto-a Sep 17, 2025
099ba25
docs: removed unnecessary comment
danielrafailov1 Sep 17, 2025
f4b8714
docs: removed unnecessary comment
danielrafailov1 Sep 17, 2025
8c56955
docs: removed unnecessary trailing line
danielrafailov1 Sep 17, 2025
447c711
docs: removed unnecessary trailing line
danielrafailov1 Sep 17, 2025
9766863
docs: removed unnecessary comment
danielrafailov1 Sep 17, 2025
3bd9880
docs: removed unnecessary trailing line
danielrafailov1 Sep 17, 2025
fd84081
docs: removed unnecessary comment
danielrafailov1 Sep 17, 2025
abd66ec
docs: removed unnecessary trailing line
danielrafailov1 Sep 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions .github/workflows/ci-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
- name: Run ESLint
run: npm run lint

tests:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -49,13 +49,9 @@ jobs:
cache: npm
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Start Storybook
run: |
npm run storybook &
npx wait-on http://127.0.0.1:6006
- name: Run Storybook tests (CI)
run: npm run test:stories:ci
- name: Run ESLint
run: npm run lint
- name: Run tests
run: npm run test


98 changes: 97 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,97 @@
# library-react
# scadable

A React library for consuming real-time telemetry data through WebSocket connections.

## Features

- 🔌 **WebSocket Integration**: Seamless connection to telemetry data streams.
- ⚛️ **React Hooks**: Simple `useLiveTelemetry` hook for real-time data consumption.
- 🛡️ **TypeScript Support**: Full type safety with comprehensive TypeScript definitions.
- 🧪 **Comprehensive Tests**: Unit and integration tests with WebSocket mocks.
- 🔧 **Easy Setup**: Simple API with minimal configuration required.

## Installation

```bash
npm install scadable
````

## Quick Start

```tsx
import React from "react";
import { Facility, Device } from "scadable";
import { useLiveTelemetry } from "scadable/hooks";
import { TelemetryDisplay } from "scadable/components";

// Initialize facility and device
const facility = new Facility("your-api-key");
const device = new Device(facility, "your-device-id");

function App() {
return <TelemetryDisplay device={device} />;
}
```

## API Reference

### `Facility`

Manages the API key for WebSocket authentication.

```tsx
const facility = new Facility("your-api-key");
```

#### Methods

- `connect(deviceId: string)`: Creates and returns a WebSocket instance.

### `Device`

Manages the device ID and WebSocket connection.

```tsx
const device = new Device(facility, "device-id");
```

#### Methods

- `connect()`: Establishes the WebSocket connection.
- `disconnect()`: Closes the WebSocket connection.
- `onMessage(handler)`: Subscribes to telemetry messages.
- `onError(handler)`: Subscribes to connection errors.
- `onStatusChange(handler)`: Subscribes to connection status changes.

### `useLiveTelemetry` Hook

A React hook for consuming real-time telemetry data.

```tsx
const { telemetry, isConnected, error } = useLiveTelemetry(device);
```

## Development

### Running Storybook

To see a live example of the library in action, run Storybook:

```bash
npm run storybook
```

This will open a new browser window with the `TelemetryDisplay` component, showcasing the `useLiveTelemetry` hook with a mock data stream.

### Building

```bash
npm run build
```

### Testing

```bash
npm run test
```

92 changes: 92 additions & 0 deletions __tests__/Device.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { Facility, Device, ConnectionStatus } from '../src/index';

// WebSocket is mocked in setupTests.ts

describe('Device', () => {
let facility: Facility;
let device: Device;

beforeEach(() => {
facility = new Facility('test-api-key');
device = new Device(facility, 'test-device-id');
});

it('should create a device with valid facility and device ID', () => {
expect(device.getDeviceId()).toBe('test-device-id');
expect(device.getConnectionStatus()).toBe(ConnectionStatus.DISCONNECTED);
});

it('should throw error for invalid facility', () => {
expect(() => new Device(null as any, 'device-id')).toThrow('Facility instance is required');
expect(() => new Device(undefined as any, 'device-id')).toThrow('Facility instance is required');
});

it('should throw error for invalid device ID', () => {
expect(() => new Device(facility, '')).toThrow('Device ID must be a non-empty string');
expect(() => new Device(facility, null as any)).toThrow('Device ID must be a non-empty string');
});

it('should build correct WebSocket URL', () => {
device.connect();
// The URL should contain token and deviceid parameters
expect(device.getConnectionStatus()).toBe(ConnectionStatus.CONNECTING);
});

it('should handle message subscription and unsubscription', () => {
const messageHandler = vi.fn();
const unsubscribe = device.onMessage(messageHandler);
expect(typeof unsubscribe).toBe('function');
unsubscribe();
});

it('should handle error subscription and unsubscription', () => {
const errorHandler = vi.fn();
const unsubscribe = device.onError(errorHandler);
expect(typeof unsubscribe).toBe('function');
unsubscribe();
});

it('should handle status change subscription and unsubscription', () => {
const statusHandler = vi.fn();
const unsubscribe = device.onStatusChange(statusHandler);
expect(typeof unsubscribe).toBe('function');
unsubscribe();
});

it('should connect and disconnect properly', () => {
device.connect();
expect(device.getConnectionStatus()).toBe(ConnectionStatus.CONNECTING);
device.disconnect();
expect(device.getConnectionStatus()).toBe(ConnectionStatus.DISCONNECTED);
});

it('should not connect if already connected', () => {
device.connect();
const initialStatus = device.getConnectionStatus();
device.connect();
expect(device.getConnectionStatus()).toBe(initialStatus);
});

it('should parse JSON messages correctly', () => {
const messageHandler = vi.fn();
device.onMessage(messageHandler);
const jsonMessage = { temperature: 25.5, humidity: 60 };
(device as any).handleMessage(JSON.stringify(jsonMessage));
expect(messageHandler).toHaveBeenCalledWith(jsonMessage);
});

it('should handle non-JSON messages as raw strings', () => {
const messageHandler = vi.fn();
device.onMessage(messageHandler);
const rawMessage = 'raw telemetry data';
(device as any).handleMessage(rawMessage);
expect(messageHandler).toHaveBeenCalledWith(rawMessage);
});

it('should check connection status correctly', () => {
expect(device.isConnected()).toBe(false);
device.connect();
expect(typeof device.isConnected).toBe('function');
});
});
25 changes: 25 additions & 0 deletions __tests__/Facility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, it, expect } from 'vitest';
import { Facility } from '../src';

describe('Facility', () => {
it('should create a facility with a valid API key', () => {
const facility = new Facility('test-api-key');
expect(facility.getApiKey()).toBe('test-api-key');
expect(facility.isValid()).toBe(true);
});

it('should throw error for empty API key', () => {
expect(() => new Facility('')).toThrow('API key must be a non-empty string');
});

it('should throw error for non-string API key', () => {
expect(() => new Facility(null as any)).toThrow('API key must be a non-empty string');
expect(() => new Facility(undefined as any)).toThrow('API key must be a non-empty string');
expect(() => new Facility(123 as any)).toThrow('API key must be a non-empty string');
});

it('should return true for valid facility', () => {
const facility = new Facility('valid-key');
expect(facility.isValid()).toBe(true);
});
});
40 changes: 40 additions & 0 deletions __tests__/integration.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, it, expect } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { TelemetryDisplay } from '../src/components/TelemetryDisplay';
import {Device, Facility} from "../src";


// WebSocket is mocked in setupTests.ts

describe('Integration Tests', () => {
it('should render the TelemetryDisplay component with Scadable Stream title', () => {
const facility = new Facility('test-key');
const device = new Device(facility, 'test-device');
render(<TelemetryDisplay device={device} />);
expect(screen.getByText('Scadable Stream')).toBeInTheDocument();
});

it('should show connection status', async () => {
const facility = new Facility('test-key');
const device = new Device(facility, 'test-device');
render(<TelemetryDisplay device={device} />);
expect(screen.getByText('Disconnected')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Connected')).toBeInTheDocument();
}, { timeout: 1000 });
});

it('should show "--" for temperature when no temperature data is available', () => {
const facility = new Facility('test-key');
const device = new Device(facility, 'test-device');
render(<TelemetryDisplay device={device} />);
expect(screen.getByText('--')).toBeInTheDocument();
});

it('should show "No data received" when no telemetry data is available', () => {
const facility = new Facility('test-key');
const device = new Device(facility, 'test-device');
render(<TelemetryDisplay device={device} />);
expect(screen.getByText('No data received')).toBeInTheDocument();
});
});
52 changes: 52 additions & 0 deletions __tests__/setupTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import '@testing-library/jest-dom';

// Mock WebSocket globally
class MockWebSocket {
// Define static properties to avoid relying on the global WebSocket object
static CONNECTING = 0;
static OPEN = 1;
static CLOSING = 2;
static CLOSED = 3;

public readyState: number = MockWebSocket.CONNECTING;
public onopen: ((_event: Event) => void) | null = null;
public onmessage: ((_event: MessageEvent) => void) | null = null;
public onerror: ((_event: Event) => void) | null = null;
public onclose: ((_event: CloseEvent) => void) | null = null;
public url: string;

constructor(url: string) {
this.url = url;
// Simulate connection after a short delay
setTimeout(() => {
this.readyState = MockWebSocket.OPEN; // Use the static property
if (this.onopen) {
this.onopen(new Event('open'));
}
}, 10);
}

close() {
this.readyState = MockWebSocket.CLOSED; // Use the static property
if (this.onclose) {
this.onclose(new CloseEvent('close'));
}
}

send() {
// No-op for this mock
}
}

// Mock global WebSocket
global.WebSocket = MockWebSocket as any;

// Mock Event classes
global.Event = class Event {
constructor(type: string) {
// @ts-ignore
this.type = type;
}
} as any;
global.MessageEvent = class MessageEvent extends Event {} as any;
global.CloseEvent = class CloseEvent extends Event {} as any;
Loading