@symbiote-native/network 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,8 +40,8 @@ with zero further changes:
40
40
 
41
41
  Full mechanics — the Podfile pieces that normally ship inside the `expo` package, the `expo`
42
42
  peer-dependency exclusion list — live in the `symbiote-expo-native-module` skill. Reference
43
- implementation: `.examples/expo-react/ios/Podfile` and
44
- `.examples/expo-react/android/app/src/main/java/com/canary/MainApplication.kt`.
43
+ implementation: `examples/expo-react/ios/Podfile` and
44
+ `examples/expo-react/android/app/src/main/java/com/canaryexpo/MainApplication.kt`.
45
45
 
46
46
  No platform permission string is needed for network's surface — connection state, IP address, and
47
47
  airplane-mode detection all read system state with no runtime permission prompt on either
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/network",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "expo-network wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, and Angular adapters. Network state (type/connected/internet-reachable) with a change listener, device IP address, and airplane-mode detection.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,7 +40,10 @@
40
40
  "src",
41
41
  "build",
42
42
  "build-ngc",
43
- "native-link.json"
43
+ "native-link.json",
44
+ "!src/**/*.test.*",
45
+ "!src/**/*.spec.*",
46
+ "!src/**/*.detox.*"
44
47
  ],
45
48
  "publishConfig": {
46
49
  "access": "public"
@@ -56,7 +59,7 @@
56
59
  "react": ">=19.0.0",
57
60
  "react-native": ">=0.86",
58
61
  "vue": ">=3.5.0",
59
- "@symbiote-native/angular": "0.6.1",
62
+ "@symbiote-native/angular": "0.6.2",
60
63
  "@symbiote-native/react": "0.2.8",
61
64
  "@symbiote-native/vue": "0.3.8"
62
65
  },
@@ -92,11 +95,11 @@
92
95
  "@vue/runtime-core": "^3.5.13",
93
96
  "react": "19.2.3",
94
97
  "typescript": "~6.0.0",
95
- "@symbiote-native/angular": "0.6.1",
96
- "@symbiote-native/react": "0.2.8",
98
+ "@symbiote-native/angular": "0.6.2",
97
99
  "@symbiote-native/engine": "0.1.7",
98
- "@symbiote-native/vue": "0.3.8",
99
- "@symbiote-native/test-utils": "0.1.6"
100
+ "@symbiote-native/react": "0.2.8",
101
+ "@symbiote-native/test-utils": "0.1.6",
102
+ "@symbiote-native/vue": "0.3.8"
100
103
  },
101
104
  "scripts": {
102
105
  "typecheck": "tsc --build",
@@ -1,103 +0,0 @@
1
- // Co-located Angular-driven test (ADR 0025) for NetworkStateService. See battery's
2
- // battery-state.service.test.ts for the shared rationale.
3
-
4
- import '@angular/compiler';
5
- import { Component, inject, type Signal } from '@angular/core';
6
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
- import { mount, unmount } from '@symbiote-native/angular';
8
- import { installFabric } from '@symbiote-native/test-utils';
9
- import { NetworkStateService } from './index';
10
-
11
- type INetworkState = { type?: string; isConnected?: boolean; isInternetReachable?: boolean };
12
-
13
- const addListenerMock = vi.fn();
14
- const removeMock = vi.fn();
15
- const getNetworkStateAsyncMock = vi.fn(async () => ({
16
- type: 'WIFI',
17
- isConnected: true,
18
- isInternetReachable: true,
19
- }));
20
-
21
- vi.mock('../../../core', () => ({
22
- addNetworkStateListener: (listener: (event: INetworkState) => void) => addListenerMock(listener),
23
- getNetworkStateAsync: () => getNetworkStateAsyncMock(),
24
- }));
25
-
26
- const ROOT_TAG = 973;
27
- const fabric = installFabric();
28
- const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
29
-
30
- let capturedResult: Signal<INetworkState> | undefined;
31
- let capturedListener: ((event: INetworkState) => void) | undefined;
32
-
33
- @Component({
34
- selector: 'symbiote-network-state-host',
35
- standalone: true,
36
- template: '',
37
- })
38
- class NetworkStateHost {
39
- readonly networkState = inject(NetworkStateService).connect();
40
-
41
- constructor() {
42
- capturedResult = this.networkState;
43
- }
44
- }
45
-
46
- beforeEach(() => {
47
- capturedResult = undefined;
48
- capturedListener = undefined;
49
- getNetworkStateAsyncMock.mockResolvedValue({
50
- type: 'WIFI',
51
- isConnected: true,
52
- isInternetReachable: true,
53
- });
54
- addListenerMock.mockImplementation(listener => {
55
- capturedListener = listener;
56
- return { remove: removeMock };
57
- });
58
- });
59
-
60
- afterEach(() => {
61
- unmount(ROOT_TAG);
62
- fabric.reset();
63
- vi.clearAllMocks();
64
- });
65
-
66
- describe('NetworkStateService.connect', () => {
67
- it('reports an empty object before the initial fetch resolves', async () => {
68
- mount(ROOT_TAG, NetworkStateHost);
69
-
70
- expect(capturedResult?.()).toEqual({});
71
- });
72
-
73
- it('reports the fetched state once getNetworkStateAsync() resolves', async () => {
74
- mount(ROOT_TAG, NetworkStateHost);
75
- await tick();
76
-
77
- expect(capturedResult?.()).toEqual({
78
- type: 'WIFI',
79
- isConnected: true,
80
- isInternetReachable: true,
81
- });
82
- });
83
-
84
- it('updates the signal when the registered listener fires', async () => {
85
- mount(ROOT_TAG, NetworkStateHost);
86
- await tick();
87
-
88
- if (capturedListener === undefined) throw new Error('addListener callback was not captured');
89
- capturedListener({ type: 'CELLULAR', isConnected: true, isInternetReachable: false });
90
-
91
- expect(capturedResult?.()?.type).toBe('CELLULAR');
92
- });
93
-
94
- it('removes the subscription when the host component is unmounted', async () => {
95
- mount(ROOT_TAG, NetworkStateHost);
96
- await tick();
97
-
98
- unmount(ROOT_TAG);
99
- await tick();
100
-
101
- expect(removeMock).toHaveBeenCalledOnce();
102
- });
103
- });
@@ -1,103 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const FAKE_NATIVE_NETWORK = {
4
- getNetworkStateAsync: vi.fn(async () => ({
5
- type: 'WIFI',
6
- isConnected: true,
7
- isInternetReachable: true,
8
- })),
9
- getIpAddressAsync: vi.fn(async () => '192.168.1.1'),
10
- isAirplaneModeEnabledAsync: vi.fn(async () => false),
11
- addListener: vi.fn(() => ({ remove: vi.fn() })),
12
- };
13
-
14
- // The real ExpoNetwork native module only exists on device — resolving it via
15
- // requireNativeModule() at import time would throw in this headless test run, so the
16
- // module-lookup file is faked in place of expo-modules-core's runtime resolution, same pattern
17
- // packages/battery/src/core/battery.test.ts uses for its native-module.
18
- vi.mock('./native-module', () => ({
19
- expoNetwork: FAKE_NATIVE_NETWORK,
20
- }));
21
-
22
- // expo-modules-core's real entry transitively imports 'react-native' for Platform/
23
- // TurboModuleRegistry, whose Flow-typed source Vitest's Oxc transform can't parse — same fake
24
- // packages/haptics/src/core/haptics.test.ts uses.
25
- vi.mock('expo-modules-core', () => ({
26
- UnavailabilityError: class UnavailabilityError extends Error {
27
- constructor(moduleName: string, propertyName: string) {
28
- super(`${propertyName} is not available on ${moduleName}`);
29
- }
30
- },
31
- }));
32
-
33
- const {
34
- getNetworkStateAsync,
35
- getIpAddressAsync,
36
- isAirplaneModeEnabledAsync,
37
- addNetworkStateListener,
38
- } = await import('./network');
39
-
40
- afterEach(() => {
41
- vi.clearAllMocks();
42
- });
43
-
44
- describe('getNetworkStateAsync', () => {
45
- it('delegates to the native module', async () => {
46
- await expect(getNetworkStateAsync()).resolves.toEqual({
47
- type: 'WIFI',
48
- isConnected: true,
49
- isInternetReachable: true,
50
- });
51
- });
52
-
53
- it('throws UnavailabilityError when the native method is absent', async () => {
54
- const { getNetworkStateAsync: native } = FAKE_NATIVE_NETWORK;
55
- // @ts-expect-error -- simulating a platform where the native module has no such method
56
- FAKE_NATIVE_NETWORK.getNetworkStateAsync = undefined;
57
-
58
- await expect(getNetworkStateAsync()).rejects.toThrow();
59
-
60
- FAKE_NATIVE_NETWORK.getNetworkStateAsync = native;
61
- });
62
- });
63
-
64
- describe('getIpAddressAsync', () => {
65
- it('delegates to the native module', async () => {
66
- await expect(getIpAddressAsync()).resolves.toBe('192.168.1.1');
67
- });
68
-
69
- it('throws UnavailabilityError when the native method is absent', async () => {
70
- const { getIpAddressAsync: native } = FAKE_NATIVE_NETWORK;
71
- // @ts-expect-error -- simulating a platform where the native module has no such method
72
- FAKE_NATIVE_NETWORK.getIpAddressAsync = undefined;
73
-
74
- await expect(getIpAddressAsync()).rejects.toThrow();
75
-
76
- FAKE_NATIVE_NETWORK.getIpAddressAsync = native;
77
- });
78
- });
79
-
80
- describe('isAirplaneModeEnabledAsync', () => {
81
- it('delegates to the native module', async () => {
82
- await expect(isAirplaneModeEnabledAsync()).resolves.toBe(false);
83
- });
84
-
85
- it('throws UnavailabilityError when the native method is absent', async () => {
86
- const { isAirplaneModeEnabledAsync: native } = FAKE_NATIVE_NETWORK;
87
- // @ts-expect-error -- simulating a platform where the native module has no such method
88
- FAKE_NATIVE_NETWORK.isAirplaneModeEnabledAsync = undefined;
89
-
90
- await expect(isAirplaneModeEnabledAsync()).rejects.toThrow();
91
-
92
- FAKE_NATIVE_NETWORK.isAirplaneModeEnabledAsync = native;
93
- });
94
- });
95
-
96
- describe('addNetworkStateListener', () => {
97
- it('subscribes through the onNetworkStateChanged event name', () => {
98
- const listener = vi.fn();
99
- addNetworkStateListener(listener);
100
-
101
- expect(FAKE_NATIVE_NETWORK.addListener).toHaveBeenCalledWith('onNetworkStateChanged', listener);
102
- });
103
- });
@@ -1,91 +0,0 @@
1
- // Co-located React-driven test (ADR 0025) for useNetworkState. See battery's
2
- // use-battery-state.test.tsx for the shared rationale (mocks `core`, not expo-modules-core
3
- // internals).
4
-
5
- import { createElement, type ReactElement } from 'react';
6
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
- import { mount, unmount, View } from '@symbiote-native/react';
8
- import { installFabric } from '@symbiote-native/test-utils';
9
- import { useNetworkState } from './index';
10
-
11
- type INetworkState = { type?: string; isConnected?: boolean; isInternetReachable?: boolean };
12
-
13
- const { addListener, getNetworkStateAsync, remove } = vi.hoisted(() => {
14
- const remove = vi.fn();
15
- return {
16
- addListener: vi.fn((_listener: (event: INetworkState) => void) => ({ remove })),
17
- getNetworkStateAsync: vi.fn(async () => ({
18
- type: 'WIFI',
19
- isConnected: true,
20
- isInternetReachable: true,
21
- })),
22
- remove,
23
- };
24
- });
25
-
26
- vi.mock('../../../core', () => ({
27
- addNetworkStateListener: addListener,
28
- getNetworkStateAsync,
29
- }));
30
-
31
- const ROOT_TAG = 953;
32
-
33
- const results: INetworkState[] = [];
34
-
35
- function Probe(): ReactElement {
36
- results.push(useNetworkState());
37
- return createElement(View);
38
- }
39
-
40
- const fabric = installFabric();
41
-
42
- beforeEach(() => {
43
- fabric.reset();
44
- results.length = 0;
45
- vi.clearAllMocks();
46
- getNetworkStateAsync.mockResolvedValue({
47
- type: 'WIFI',
48
- isConnected: true,
49
- isInternetReachable: true,
50
- });
51
- });
52
-
53
- afterEach(() => unmount(ROOT_TAG));
54
-
55
- describe('useNetworkState', () => {
56
- it('reports an empty object before the initial fetch resolves', () => {
57
- mount(ROOT_TAG, createElement(Probe));
58
-
59
- expect(results[results.length - 1]).toEqual({});
60
- });
61
-
62
- it('reports the fetched state once the initial getNetworkStateAsync() resolves', async () => {
63
- mount(ROOT_TAG, createElement(Probe));
64
-
65
- await vi.waitFor(() =>
66
- expect(results[results.length - 1]).toEqual({
67
- type: 'WIFI',
68
- isConnected: true,
69
- isInternetReachable: true,
70
- }),
71
- );
72
- });
73
-
74
- it('updates when the native listener fires', async () => {
75
- mount(ROOT_TAG, createElement(Probe));
76
- await vi.waitFor(() => expect(results[results.length - 1].type).toBe('WIFI'));
77
-
78
- const listener = addListener.mock.calls[0][0];
79
- listener({ type: 'CELLULAR', isConnected: true, isInternetReachable: false });
80
-
81
- await vi.waitFor(() => expect(results[results.length - 1].type).toBe('CELLULAR'));
82
- });
83
-
84
- it('unsubscribes from the native listener on unmount', () => {
85
- mount(ROOT_TAG, createElement(Probe));
86
-
87
- unmount(ROOT_TAG);
88
-
89
- expect(remove).toHaveBeenCalledTimes(1);
90
- });
91
- });
@@ -1,100 +0,0 @@
1
- // Co-located Vue-driven test (ADR 0025) for useNetworkState. See battery's
2
- // use-battery-state.test.ts for the shared rationale.
3
-
4
- import { defineComponent, h, type Ref } from '@vue/runtime-core';
5
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
- import { mount, unmount } from '@symbiote-native/vue';
7
- import { installFabric } from '@symbiote-native/test-utils';
8
- import { useNetworkState } from './index';
9
-
10
- const ROOT_TAG = 9953;
11
-
12
- type INetworkState = { type?: string; isConnected?: boolean; isInternetReachable?: boolean };
13
- type IListener = (event: INetworkState) => void;
14
-
15
- let registeredListener: IListener | undefined;
16
- const removeMock = vi.fn();
17
- const addListenerMock = vi.fn((listener: IListener) => {
18
- registeredListener = listener;
19
- return { remove: removeMock };
20
- });
21
- const getNetworkStateAsyncMock = vi.fn(async () => ({
22
- type: 'WIFI',
23
- isConnected: true,
24
- isInternetReachable: true,
25
- }));
26
-
27
- vi.mock('../../../core', () => ({
28
- addNetworkStateListener: (listener: IListener) => addListenerMock(listener),
29
- getNetworkStateAsync: () => getNetworkStateAsyncMock(),
30
- }));
31
-
32
- const fabric = installFabric();
33
-
34
- beforeEach(() => {
35
- fabric.reset();
36
- registeredListener = undefined;
37
- addListenerMock.mockClear();
38
- removeMock.mockClear();
39
- getNetworkStateAsyncMock.mockClear();
40
- getNetworkStateAsyncMock.mockResolvedValue({
41
- type: 'WIFI',
42
- isConnected: true,
43
- isInternetReachable: true,
44
- });
45
- });
46
-
47
- afterEach(() => unmount(ROOT_TAG));
48
-
49
- function mountNetworkState(): Ref<INetworkState> {
50
- let networkState: Ref<INetworkState> | undefined;
51
- mount(
52
- ROOT_TAG,
53
- defineComponent({
54
- setup: () => {
55
- networkState = useNetworkState();
56
- return () => h('symbiote-text', {}, 'network');
57
- },
58
- }),
59
- );
60
- if (networkState === undefined) {
61
- throw new Error('setup() did not run');
62
- }
63
- return networkState;
64
- }
65
-
66
- describe('useNetworkState (Vue)', () => {
67
- it('starts at an empty object before the initial fetch resolves', () => {
68
- const networkState = mountNetworkState();
69
-
70
- expect(networkState.value).toEqual({});
71
- });
72
-
73
- it('updates to the fetched value once getNetworkStateAsync() resolves', async () => {
74
- const networkState = mountNetworkState();
75
-
76
- await vi.waitFor(() =>
77
- expect(networkState.value).toEqual({
78
- type: 'WIFI',
79
- isConnected: true,
80
- isInternetReachable: true,
81
- }),
82
- );
83
- });
84
-
85
- it('updates the ref when the native listener fires', async () => {
86
- const networkState = mountNetworkState();
87
- await vi.waitFor(() => expect(networkState.value.type).toBe('WIFI'));
88
-
89
- registeredListener?.({ type: 'CELLULAR', isConnected: true, isInternetReachable: false });
90
-
91
- expect(networkState.value.type).toBe('CELLULAR');
92
- });
93
-
94
- it('removes the subscription on unmount', () => {
95
- mountNetworkState();
96
- unmount(ROOT_TAG);
97
-
98
- expect(removeMock).toHaveBeenCalledTimes(1);
99
- });
100
- });