Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d4fb7d3

Browse files
authored
fix: complete sensing server API, WebSocket connectivity, and mobile tests (ruvnet#125)
The web UI had persistent 404 errors on model, recording, and training endpoints, and the sensing WebSocket never connected on Dashboard/Live Demo tabs because sensingService.start() was only called lazily on Sensing tab visit. Server (main.rs): - Add 14 fully-functional Axum handlers: model CRUD (7), recording lifecycle (4), training control (3) - Scan data/models/ and data/recordings/ at startup - Recording writes CSI frames to .jsonl via tokio background task - Model load/unload lifecycle with state tracking Web UI (app.js): - Import and start sensingService early in initializeServices() so Dashboard and Live Demo tabs connect to /ws/sensing immediately Mobile (ws.service.ts): - Fix WebSocket URL builder to use same-origin port instead of hardcoded port 3001 Mobile (jest.config.js): - Fix testPathIgnorePatterns that was ignoring the entire test directory Mobile (25 test files): - Replace all it.todo() placeholder tests with real implementations covering components, services, stores, hooks, screens, and utils ADR-043 documents all changes.
1 parent 977da0f commit d4fb7d3

34 files changed

Lines changed: 2975 additions & 87 deletions

docs/adr/ADR-043-sensing-server-ui-api-completion.md

Lines changed: 334 additions & 0 deletions
Large diffs are not rendered by default.

rust-port/wifi-densepose-rs/crates/wifi-densepose-sensing-server/src/main.rs

Lines changed: 488 additions & 1 deletion
Large diffs are not rendered by default.

ui/app.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { SensingTab } from './components/SensingTab.js';
88
import { apiService } from './services/api.service.js';
99
import { wsService } from './services/websocket.service.js';
1010
import { healthService } from './services/health.service.js';
11+
import { sensingService } from './services/sensing.service.js';
1112
import { backendDetector } from './utils/backend-detector.js';
1213

1314
class WiFiDensePoseApp {
@@ -75,6 +76,10 @@ class WiFiDensePoseApp {
7576
console.warn('⚠️ Backend not available:', error.message);
7677
this.showBackendStatus('Backend unavailable — start sensing-server', 'warning');
7778
}
79+
80+
// Start the sensing WebSocket service early so the dashboard and
81+
// live-demo tabs can show the correct data-source status immediately.
82+
sensingService.start();
7883
}
7984
}
8085

ui/mobile/jest.config.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
const expoPreset = require('jest-expo/jest-preset');
2+
13
module.exports = {
24
preset: 'jest-expo',
5+
setupFiles: [
6+
'<rootDir>/jest.setup.pre.js',
7+
...(expoPreset.setupFiles || []),
8+
],
39
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
4-
testPathIgnorePatterns: ['/src/__tests__/'],
10+
testPathIgnorePatterns: ['/node_modules/', '/__mocks__/'],
511
transformIgnorePatterns: [
6-
'node_modules/(?!(expo|expo-.+|react-native|@react-native|react-native-webview|react-native-reanimated|react-native-svg|react-native-safe-area-context|react-native-screens|@react-navigation|@expo|@unimodules|expo-modules-core)/)',
12+
'node_modules/(?!(expo|expo-.+|react-native|@react-native|react-native-webview|react-native-reanimated|react-native-svg|react-native-safe-area-context|react-native-screens|@react-navigation|@expo|@unimodules|expo-modules-core|react-native-worklets)/)',
713
],
814
};

ui/mobile/jest.setup.pre.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Pre-define globals that expo/src/winter/runtime.native.ts would lazily
2+
// install via require()-with-ESM-import, which jest 30 rejects.
3+
// By defining them upfront as non-configurable, the `install()` function
4+
// in installGlobal.ts will skip them with a console.error (which is harmless).
5+
const globalsToProtect = [
6+
'TextDecoder',
7+
'TextDecoderStream',
8+
'TextEncoderStream',
9+
'URL',
10+
'URLSearchParams',
11+
'__ExpoImportMetaRegistry',
12+
'structuredClone',
13+
];
14+
15+
for (const name of globalsToProtect) {
16+
if (globalThis[name] !== undefined) {
17+
// Already defined (e.g. Node provides URL, TextDecoder, structuredClone).
18+
// Make it non-configurable so expo's install() skips it.
19+
try {
20+
Object.defineProperty(globalThis, name, {
21+
value: globalThis[name],
22+
configurable: false,
23+
enumerable: true,
24+
writable: true,
25+
});
26+
} catch {
27+
// Already non-configurable, fine.
28+
}
29+
} else {
30+
// Not yet defined, set a stub value and make non-configurable.
31+
Object.defineProperty(globalThis, name, {
32+
value: name === '__ExpoImportMetaRegistry' ? { url: 'http://localhost:8081' } : undefined,
33+
configurable: false,
34+
enumerable: false,
35+
writable: true,
36+
});
37+
}
38+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
getBundleUrl: () => 'http://localhost:8081',
3+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = {
2+
ImportMetaRegistry: {
3+
get url() {
4+
return 'http://localhost:8081';
5+
},
6+
},
7+
};
Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,36 @@
1-
describe('placeholder', () => {
2-
it('passes', () => {
3-
expect(true).toBe(true);
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react-native';
3+
import { ConnectionBanner } from '@/components/ConnectionBanner';
4+
import { ThemeProvider } from '@/theme/ThemeContext';
5+
6+
const renderWithTheme = (ui: React.ReactElement) =>
7+
render(<ThemeProvider>{ui}</ThemeProvider>);
8+
9+
describe('ConnectionBanner', () => {
10+
it('renders LIVE STREAM text when connected', () => {
11+
renderWithTheme(<ConnectionBanner status="connected" />);
12+
expect(screen.getByText('LIVE STREAM')).toBeTruthy();
13+
});
14+
15+
it('renders DISCONNECTED text when disconnected', () => {
16+
renderWithTheme(<ConnectionBanner status="disconnected" />);
17+
expect(screen.getByText('DISCONNECTED')).toBeTruthy();
18+
});
19+
20+
it('renders SIMULATED DATA text when simulated', () => {
21+
renderWithTheme(<ConnectionBanner status="simulated" />);
22+
expect(screen.getByText('SIMULATED DATA')).toBeTruthy();
23+
});
24+
25+
it('renders without crashing for each status', () => {
26+
const statuses: Array<'connected' | 'simulated' | 'disconnected'> = [
27+
'connected',
28+
'simulated',
29+
'disconnected',
30+
];
31+
for (const status of statuses) {
32+
const { unmount } = renderWithTheme(<ConnectionBanner status={status} />);
33+
unmount();
34+
}
435
});
536
});
Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,63 @@
1-
describe('placeholder', () => {
2-
it('passes', () => {
3-
expect(true).toBe(true);
1+
import React from 'react';
2+
import { render } from '@testing-library/react-native';
3+
import { ThemeProvider } from '@/theme/ThemeContext';
4+
5+
jest.mock('react-native-svg', () => {
6+
const { View } = require('react-native');
7+
return {
8+
__esModule: true,
9+
default: View, // Svg
10+
Svg: View,
11+
Circle: View,
12+
G: View,
13+
Text: View,
14+
Rect: View,
15+
Line: View,
16+
Path: View,
17+
};
18+
});
19+
20+
// GaugeArc uses Animated.createAnimatedComponent(Circle), so we need
21+
// the reanimated mock (already in jest.setup.ts) and SVG mock above.
22+
import { GaugeArc } from '@/components/GaugeArc';
23+
24+
const renderWithTheme = (ui: React.ReactElement) =>
25+
render(<ThemeProvider>{ui}</ThemeProvider>);
26+
27+
describe('GaugeArc', () => {
28+
it('renders without crashing', () => {
29+
const { toJSON } = renderWithTheme(
30+
<GaugeArc value={50} max={100} label="BPM" unit="bpm" color="#00FF00" />,
31+
);
32+
expect(toJSON()).not.toBeNull();
33+
});
34+
35+
it('renders with min and max values', () => {
36+
const { toJSON } = renderWithTheme(
37+
<GaugeArc value={0} min={0} max={200} label="Test" unit="x" color="#FF0000" />,
38+
);
39+
expect(toJSON()).not.toBeNull();
40+
});
41+
42+
it('renders with colorTo gradient', () => {
43+
const { toJSON } = renderWithTheme(
44+
<GaugeArc
45+
value={75}
46+
max={100}
47+
label="HR"
48+
unit="bpm"
49+
color="#00FF00"
50+
colorTo="#FF0000"
51+
size={200}
52+
/>,
53+
);
54+
expect(toJSON()).not.toBeNull();
55+
});
56+
57+
it('renders with custom size', () => {
58+
const { toJSON } = renderWithTheme(
59+
<GaugeArc value={30} max={60} label="BR" unit="brpm" color="#0088FF" size={80} />,
60+
);
61+
expect(toJSON()).not.toBeNull();
462
});
563
});
Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
1-
describe('placeholder', () => {
2-
it('passes', () => {
3-
expect(true).toBe(true);
1+
// HudOverlay.tsx is an empty file (0 bytes). This test verifies that importing
2+
// it does not throw and that the module exists.
3+
4+
describe('HudOverlay', () => {
5+
it('module can be imported without error', () => {
6+
expect(() => {
7+
require('@/components/HudOverlay');
8+
}).not.toThrow();
9+
});
10+
11+
it('module exports are defined (may be empty)', () => {
12+
const mod = require('@/components/HudOverlay');
13+
// The module is empty, so it should be an object (possibly with no exports)
14+
expect(typeof mod).toBe('object');
415
});
516
});

0 commit comments

Comments
 (0)