@oxyhq/bloom 0.68.0 → 0.69.1
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/lib/commonjs/animated-check/AnimatedCheck.js +22 -93
- package/lib/commonjs/animated-check/AnimatedCheck.js.map +1 -1
- package/lib/commonjs/avatar/Avatar.js +6 -24
- package/lib/commonjs/avatar/Avatar.js.map +1 -1
- package/lib/commonjs/avatar/AvatarRing.js +14 -30
- package/lib/commonjs/avatar/AvatarRing.js.map +1 -1
- package/lib/commonjs/bottom-sheet/index.js +9 -4
- package/lib/commonjs/bottom-sheet/index.js.map +1 -1
- package/lib/commonjs/connection-status/index.js +16 -7
- package/lib/commonjs/connection-status/index.js.map +1 -1
- package/lib/commonjs/connection-status/netinfo.js +119 -0
- package/lib/commonjs/connection-status/netinfo.js.map +1 -0
- package/lib/commonjs/hooks/haptics-module.js +109 -0
- package/lib/commonjs/hooks/haptics-module.js.map +1 -0
- package/lib/commonjs/hooks/useHaptics.js +6 -8
- package/lib/commonjs/hooks/useHaptics.js.map +1 -1
- package/lib/commonjs/loading/SpinnerIcon.js +22 -55
- package/lib/commonjs/loading/SpinnerIcon.js.map +1 -1
- package/lib/commonjs/theme/adaptive-colors.js +20 -4
- package/lib/commonjs/theme/adaptive-colors.js.map +1 -1
- package/lib/commonjs/theme/color-scope/style-builder.js +107 -6
- package/lib/commonjs/theme/color-scope/style-builder.js.map +1 -1
- package/lib/commonjs/theme/init-css-interop.js +13 -11
- package/lib/commonjs/theme/init-css-interop.js.map +1 -1
- package/lib/module/animated-check/AnimatedCheck.js +22 -91
- package/lib/module/animated-check/AnimatedCheck.js.map +1 -1
- package/lib/module/avatar/Avatar.js +1 -19
- package/lib/module/avatar/Avatar.js.map +1 -1
- package/lib/module/avatar/AvatarRing.js +7 -23
- package/lib/module/avatar/AvatarRing.js.map +1 -1
- package/lib/module/bottom-sheet/index.js +9 -4
- package/lib/module/bottom-sheet/index.js.map +1 -1
- package/lib/module/connection-status/index.js +16 -6
- package/lib/module/connection-status/index.js.map +1 -1
- package/lib/module/connection-status/netinfo.js +114 -0
- package/lib/module/connection-status/netinfo.js.map +1 -0
- package/lib/module/hooks/haptics-module.js +104 -0
- package/lib/module/hooks/haptics-module.js.map +1 -0
- package/lib/module/hooks/useHaptics.js +7 -11
- package/lib/module/hooks/useHaptics.js.map +1 -1
- package/lib/module/loading/SpinnerIcon.js +13 -46
- package/lib/module/loading/SpinnerIcon.js.map +1 -1
- package/lib/module/loading/index.web.js +2 -2
- package/lib/module/theme/adaptive-colors.js +20 -4
- package/lib/module/theme/adaptive-colors.js.map +1 -1
- package/lib/module/theme/color-scope/style-builder.js +107 -6
- package/lib/module/theme/color-scope/style-builder.js.map +1 -1
- package/lib/module/theme/init-css-interop.js +13 -11
- package/lib/module/theme/init-css-interop.js.map +1 -1
- package/lib/typescript/commonjs/animated-check/AnimatedCheck.d.ts.map +1 -1
- package/lib/typescript/commonjs/avatar/Avatar.d.ts.map +1 -1
- package/lib/typescript/commonjs/avatar/AvatarRing.d.ts.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/connection-status/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/connection-status/netinfo.d.ts +69 -0
- package/lib/typescript/commonjs/connection-status/netinfo.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/haptics-module.d.ts +58 -0
- package/lib/typescript/commonjs/hooks/haptics-module.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/useHaptics.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/SpinnerIcon.d.ts +9 -2
- package/lib/typescript/commonjs/loading/SpinnerIcon.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/adaptive-colors.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-scope/style-builder.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/init-css-interop.d.ts.map +1 -1
- package/lib/typescript/module/animated-check/AnimatedCheck.d.ts.map +1 -1
- package/lib/typescript/module/avatar/Avatar.d.ts.map +1 -1
- package/lib/typescript/module/avatar/AvatarRing.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/module/connection-status/index.d.ts.map +1 -1
- package/lib/typescript/module/connection-status/netinfo.d.ts +69 -0
- package/lib/typescript/module/connection-status/netinfo.d.ts.map +1 -0
- package/lib/typescript/module/hooks/haptics-module.d.ts +58 -0
- package/lib/typescript/module/hooks/haptics-module.d.ts.map +1 -0
- package/lib/typescript/module/hooks/useHaptics.d.ts.map +1 -1
- package/lib/typescript/module/loading/SpinnerIcon.d.ts +9 -2
- package/lib/typescript/module/loading/SpinnerIcon.d.ts.map +1 -1
- package/lib/typescript/module/theme/adaptive-colors.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-scope/style-builder.d.ts.map +1 -1
- package/lib/typescript/module/theme/init-css-interop.d.ts.map +1 -1
- package/package.json +11 -7
- package/src/__tests__/color-scope-nativewind.test.ts +135 -0
- package/src/__tests__/connection-status-netinfo.test.tsx +203 -0
- package/src/__tests__/optional-peer-imports.test.ts +441 -0
- package/src/__tests__/svg-required-peer.test.tsx +75 -0
- package/src/__tests__/useHaptics.test.tsx +203 -16
- package/src/animated-check/AnimatedCheck.tsx +19 -86
- package/src/avatar/Avatar.tsx +1 -11
- package/src/avatar/AvatarRing.tsx +7 -15
- package/src/bottom-sheet/index.tsx +9 -4
- package/src/connection-status/index.tsx +16 -6
- package/src/connection-status/netinfo.ts +131 -0
- package/src/hooks/haptics-module.ts +118 -0
- package/src/hooks/useHaptics.tsx +6 -9
- package/src/loading/SpinnerIcon.tsx +13 -52
- package/src/loading/index.web.ts +2 -2
- package/src/theme/adaptive-colors.ts +20 -4
- package/src/theme/color-scope/style-builder.ts +118 -6
- package/src/theme/init-css-interop.ts +13 -11
- package/lib/commonjs/avatar/svg-module.js +0 -15
- package/lib/commonjs/avatar/svg-module.js.map +0 -1
- package/lib/commonjs/utils/lazy-require.js +0 -37
- package/lib/commonjs/utils/lazy-require.js.map +0 -1
- package/lib/module/avatar/svg-module.js +0 -11
- package/lib/module/avatar/svg-module.js.map +0 -1
- package/lib/module/utils/lazy-require.js +0 -33
- package/lib/module/utils/lazy-require.js.map +0 -1
- package/lib/typescript/commonjs/avatar/svg-module.d.ts +0 -8
- package/lib/typescript/commonjs/avatar/svg-module.d.ts.map +0 -1
- package/lib/typescript/commonjs/utils/lazy-require.d.ts +0 -15
- package/lib/typescript/commonjs/utils/lazy-require.d.ts.map +0 -1
- package/lib/typescript/module/avatar/svg-module.d.ts +0 -8
- package/lib/typescript/module/avatar/svg-module.d.ts.map +0 -1
- package/lib/typescript/module/utils/lazy-require.d.ts +0 -15
- package/lib/typescript/module/utils/lazy-require.d.ts.map +0 -1
- package/src/avatar/svg-module.ts +0 -11
- package/src/utils/lazy-require.ts +0 -31
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SpinnerIcon.d.ts","sourceRoot":"","sources":["../../../../src/loading/SpinnerIcon.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoB,MAAM,OAAO,CAAC;AACzC,OAAO,
|
|
1
|
+
{"version":3,"file":"SpinnerIcon.d.ts","sourceRoot":"","sources":["../../../../src/loading/SpinnerIcon.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoB,MAAM,OAAO,CAAC;AACzC,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAU9C,UAAU,gBAAgB;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAwDlD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adaptive-colors.d.ts","sourceRoot":"","sources":["../../../../src/theme/adaptive-colors.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"adaptive-colors.d.ts","sourceRoot":"","sources":["../../../../src/theme/adaptive-colors.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAgH3C,wBAAgB,iBAAiB,IAAI,WAAW,GAAG,IAAI,CAItD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style-builder.d.ts","sourceRoot":"","sources":["../../../../../src/theme/color-scope/style-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAY,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAGxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"style-builder.d.ts","sourceRoot":"","sources":["../../../../../src/theme/color-scope/style-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAY,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAGxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AA6CtD;;;;;GAKG;AACH,MAAM,MAAM,gCAAgC,GAAG,KAAK,CAAC,aAAa,CAAC;IACjE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC,CAAC;AAkEH;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,YAAY,EACzB,IAAI,EAAE,OAAO,GAAG,MAAM,EACtB,OAAO,CAAC,EAAE,eAAe,GACxB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,IAAI,gCAAgC,GAAG,IAAI,CAepF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,YAAY,EACzB,IAAI,EAAE,OAAO,GAAG,MAAM,GACrB,SAAS,CAAC,SAAS,CAAC,CAQtB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init-css-interop.d.ts","sourceRoot":"","sources":["../../../../src/theme/init-css-interop.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"init-css-interop.d.ts","sourceRoot":"","sources":["../../../../src/theme/init-css-interop.ts"],"names":[],"mappings":"AAyHA;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAS7C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/bloom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.69.1",
|
|
4
4
|
"description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -1490,6 +1490,7 @@
|
|
|
1490
1490
|
"@react-native-community/netinfo": "^12.0.1"
|
|
1491
1491
|
},
|
|
1492
1492
|
"peerDependencies": {
|
|
1493
|
+
"@react-native-community/netinfo": ">=11.1.0",
|
|
1493
1494
|
"expo": "*",
|
|
1494
1495
|
"expo-blur": "*",
|
|
1495
1496
|
"expo-font": "*",
|
|
@@ -1498,18 +1499,21 @@
|
|
|
1498
1499
|
"expo-image": "*",
|
|
1499
1500
|
"expo-router": ">=3.0.0",
|
|
1500
1501
|
"expo-symbols": ">=0.4.5",
|
|
1502
|
+
"nativewind": ">=5.0.0",
|
|
1501
1503
|
"react": ">=18.0.0",
|
|
1502
1504
|
"react-dom": ">=18.0.0",
|
|
1503
1505
|
"react-native": ">=0.73.0",
|
|
1504
1506
|
"react-native-gesture-handler": ">=2.16.1",
|
|
1507
|
+
"react-native-keyboard-controller": ">=1.11.4",
|
|
1505
1508
|
"react-native-reanimated": ">=3.13.0",
|
|
1506
1509
|
"react-native-safe-area-context": ">=5.0.0",
|
|
1507
1510
|
"react-native-screens": ">=3.16.0",
|
|
1508
|
-
"react-native-svg": ">=13.0.0"
|
|
1509
|
-
"nativewind": ">=5.0.0",
|
|
1510
|
-
"@react-native-community/netinfo": ""
|
|
1511
|
+
"react-native-svg": ">=13.0.0"
|
|
1511
1512
|
},
|
|
1512
1513
|
"peerDependenciesMeta": {
|
|
1514
|
+
"@react-native-community/netinfo": {
|
|
1515
|
+
"optional": true
|
|
1516
|
+
},
|
|
1513
1517
|
"expo": {
|
|
1514
1518
|
"optional": true
|
|
1515
1519
|
},
|
|
@@ -1522,13 +1526,13 @@
|
|
|
1522
1526
|
"expo-router": {
|
|
1523
1527
|
"optional": true
|
|
1524
1528
|
},
|
|
1525
|
-
"
|
|
1529
|
+
"nativewind": {
|
|
1526
1530
|
"optional": true
|
|
1527
1531
|
},
|
|
1528
|
-
"
|
|
1532
|
+
"react-dom": {
|
|
1529
1533
|
"optional": true
|
|
1530
1534
|
},
|
|
1531
|
-
"
|
|
1535
|
+
"react-native-keyboard-controller": {
|
|
1532
1536
|
"optional": true
|
|
1533
1537
|
}
|
|
1534
1538
|
},
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import * as ReactNative from 'react-native';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* THE OPTIONAL-PEER BOUNDARY for `nativewind`.
|
|
5
|
+
*
|
|
6
|
+
* `BloomColorScope` publishes a preset's CSS vars into the subtree through the
|
|
7
|
+
* CONSUMER's nativewind — its version decides which mechanism exists
|
|
8
|
+
* (`VariableContextProvider` under NativeWind 5, `vars()` before it), so the
|
|
9
|
+
* module has to be loaded from the app, not from Bloom's own react-native-css.
|
|
10
|
+
* Loaded through a variable specifier it was never loaded at all under Metro,
|
|
11
|
+
* and a scope that does not scope is invisible: the subtree keeps rendering,
|
|
12
|
+
* just in the app-wide palette.
|
|
13
|
+
*
|
|
14
|
+
* `optional-peer-imports.test.ts` guards the static shape (literal specifier,
|
|
15
|
+
* inside a try). This runs it: the module is used when present, the scope
|
|
16
|
+
* degrades rather than crashes when absent, and it says so once.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const NATIVEWIND = 'nativewind';
|
|
20
|
+
|
|
21
|
+
type StyleBuilder = typeof import('../theme/color-scope/style-builder');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load `style-builder` against a given nativewind factory.
|
|
25
|
+
*
|
|
26
|
+
* `{ virtual: true }` IS correct here, unlike in the netinfo suite: nativewind
|
|
27
|
+
* is not installed in this repo at all, so without it `doMock` throws while
|
|
28
|
+
* resolving the name it is being asked to stand in for. The trade is that a
|
|
29
|
+
* virtual mock is keyed by the bare specifier — which is exactly what the
|
|
30
|
+
* loader requires, so the two agree.
|
|
31
|
+
*/
|
|
32
|
+
function loadStyleBuilder(factory?: () => unknown): StyleBuilder {
|
|
33
|
+
let mod: StyleBuilder | undefined;
|
|
34
|
+
|
|
35
|
+
jest.isolateModules(() => {
|
|
36
|
+
if (factory) jest.doMock(NATIVEWIND, factory, { virtual: true });
|
|
37
|
+
// A second copy of react-native carries a second `Platform`, so the OS each
|
|
38
|
+
// case sets would land on an object the module under test never reads.
|
|
39
|
+
jest.doMock('react-native', () => ReactNative);
|
|
40
|
+
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
|
+
mod = require('../theme/color-scope/style-builder') as StyleBuilder;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!mod) throw new Error('module isolation did not run');
|
|
46
|
+
return mod;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const originalOS = ReactNative.Platform.OS;
|
|
50
|
+
|
|
51
|
+
describe('BloomColorScope nativewind loading', () => {
|
|
52
|
+
let warn: jest.SpyInstance<void, Parameters<typeof console.warn>>;
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
56
|
+
ReactNative.Platform.OS = 'ios';
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
ReactNative.Platform.OS = originalOS;
|
|
61
|
+
warn.mockRestore();
|
|
62
|
+
jest.resetModules();
|
|
63
|
+
jest.dontMock(NATIVEWIND);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('uses the consumer’s VariableContextProvider when nativewind provides one', () => {
|
|
67
|
+
const VariableContextProvider = () => null;
|
|
68
|
+
const vars = jest.fn(() => ({ marker: true }));
|
|
69
|
+
const builder = loadStyleBuilder(() => ({ vars, VariableContextProvider }));
|
|
70
|
+
|
|
71
|
+
expect(builder.getVariableContextProvider()).toBe(VariableContextProvider);
|
|
72
|
+
expect(warn).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('passes the resolved scope vars through the consumer’s vars()', () => {
|
|
76
|
+
const vars = jest.fn((_record: Record<string, string>) => ({ marker: true }));
|
|
77
|
+
const builder = loadStyleBuilder(() => ({ vars }));
|
|
78
|
+
|
|
79
|
+
expect(builder.buildNativePresetStyle('blue', 'dark')).toEqual({ marker: true });
|
|
80
|
+
// Not just "called": the record it forwards must be the canonical tokens
|
|
81
|
+
// plus the Tailwind `--color-*` aliases, which is the whole payload of a
|
|
82
|
+
// scope. A `vars({})` would otherwise read as a working scope.
|
|
83
|
+
const [record] = vars.mock.calls[0] ?? [{}];
|
|
84
|
+
expect(record['--primary']).toMatch(/^rgb/);
|
|
85
|
+
expect(record['--color-primary']).toBe(record['--primary']);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('degrades to no scope, and says so once, when the peer is absent', () => {
|
|
89
|
+
const builder = loadStyleBuilder(() => {
|
|
90
|
+
throw new Error(`Cannot find module '${NATIVEWIND}'`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(builder.getVariableContextProvider()).toBeNull();
|
|
94
|
+
expect(builder.buildNativePresetStyle('blue', 'light')).toBeUndefined();
|
|
95
|
+
|
|
96
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
97
|
+
const [message] = warn.mock.calls[0] as [string];
|
|
98
|
+
expect(message).toContain('BloomColorScope');
|
|
99
|
+
expect(message).toContain(NATIVEWIND);
|
|
100
|
+
expect(message).toContain('expo install');
|
|
101
|
+
expect(message).toContain('Cannot find module');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('names the NativeWind 5 API when nativewind resolves without it', () => {
|
|
105
|
+
const builder = loadStyleBuilder(() => ({ vars: jest.fn(() => ({})) }));
|
|
106
|
+
|
|
107
|
+
expect(builder.getVariableContextProvider()).toBeNull();
|
|
108
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
109
|
+
expect((warn.mock.calls[0] as [string])[0]).toContain('VariableContextProvider');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('never loads nativewind on web, where the vars go on documentElement', () => {
|
|
113
|
+
ReactNative.Platform.OS = 'web';
|
|
114
|
+
const vars = jest.fn(() => ({ marker: true }));
|
|
115
|
+
const builder = loadStyleBuilder(() => ({ vars, VariableContextProvider: () => null }));
|
|
116
|
+
|
|
117
|
+
expect(builder.getVariableContextProvider()).toBeNull();
|
|
118
|
+
expect(builder.buildNativePresetStyle('blue', 'light')).toBeUndefined();
|
|
119
|
+
expect(vars).not.toHaveBeenCalled();
|
|
120
|
+
// A warning here would fire on every web app rendering a scoped subtree.
|
|
121
|
+
expect(warn).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('builds the same token map on every platform', () => {
|
|
125
|
+
// `buildScopeVars` is the pure half — it must not depend on nativewind at
|
|
126
|
+
// all, since web consumes it directly as an inline style object.
|
|
127
|
+
const builder = loadStyleBuilder(() => {
|
|
128
|
+
throw new Error('absent');
|
|
129
|
+
});
|
|
130
|
+
const scope = builder.buildScopeVars('green', 'light');
|
|
131
|
+
|
|
132
|
+
expect(scope['--primary']).toMatch(/^rgb/);
|
|
133
|
+
expect(scope['--color-primary']).toBe(scope['--primary']);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react-native';
|
|
3
|
+
import type NetInfo from '@react-native-community/netinfo';
|
|
4
|
+
|
|
5
|
+
import type { NetInfoLike } from '../connection-status/netinfo';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* THE OPTIONAL-PEER BOUNDARY for `@react-native-community/netinfo`.
|
|
9
|
+
*
|
|
10
|
+
* The defect: netinfo was declared an optional peer AND imported statically, so
|
|
11
|
+
* an app that never installed it did not lose the outage toast — its native
|
|
12
|
+
* bundle failed to build, pointing at a package it never mentions. The fix
|
|
13
|
+
* (`try { require('<literal>') } catch`) has to hold two properties at once, and
|
|
14
|
+
* both are asserted here: the real module is still used when it IS installed,
|
|
15
|
+
* and the component degrades to inert — not to a crash, and not silently —
|
|
16
|
+
* when it is not.
|
|
17
|
+
*
|
|
18
|
+
* `optional-peer-imports.test.ts` guards the static shape of the same contract.
|
|
19
|
+
* This one runs it.
|
|
20
|
+
*
|
|
21
|
+
* Every case goes through `jest.isolateModules` + `jest.doMock` because the
|
|
22
|
+
* loader caches its result in module state: without a fresh registry the first
|
|
23
|
+
* case to resolve netinfo would leave every later case asserting against a cache
|
|
24
|
+
* it can no longer influence, passing whatever the implementation did.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const NETINFO = '@react-native-community/netinfo';
|
|
28
|
+
const reactModule: unknown = jest.requireActual('react');
|
|
29
|
+
|
|
30
|
+
/** A netinfo state object with only the two fields the component reads. */
|
|
31
|
+
function state(isConnected: boolean, isInternetReachable: boolean | null = true) {
|
|
32
|
+
return { isConnected, isInternetReachable };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compile-time half of the contract: the real netinfo surface must remain
|
|
37
|
+
* assignable to Bloom's hand-written `NetInfoLike`. The interface is written by
|
|
38
|
+
* hand so netinfo's types never reach Bloom's emitted `.d.ts` (a consumer that
|
|
39
|
+
* skips the optional peer must not inherit a TS7016 from Bloom), which leaves
|
|
40
|
+
* this as the only place a shape change in netinfo can be caught.
|
|
41
|
+
*
|
|
42
|
+
* It is enforced by `bun run typescript`, NOT by jest: ts-jest transpiles
|
|
43
|
+
* without typechecking here, so a drifted type is green under `bunx jest` and
|
|
44
|
+
* red under `tsc` (verified by mutation — widening the unsubscribe to
|
|
45
|
+
* `Promise<void>` produces `TS2322: Type 'true' is not assignable to type
|
|
46
|
+
* 'never'` on the line below).
|
|
47
|
+
*/
|
|
48
|
+
type RealNetInfoIsCompatible = typeof NetInfo extends NetInfoLike ? true : never;
|
|
49
|
+
const NETINFO_TYPES_STILL_MATCH: RealNetInfoIsCompatible = true;
|
|
50
|
+
|
|
51
|
+
type Harness = {
|
|
52
|
+
ConnectionStatusToasts: React.ComponentType<Record<string, never>>;
|
|
53
|
+
toast: { error: jest.Mock; warning: jest.Mock; success: jest.Mock };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load `connection-status` against a given netinfo module factory. Passing a
|
|
58
|
+
* factory that THROWS is how the absent-peer case is expressed: that is exactly
|
|
59
|
+
* what the `catch` sees when Metro stubs an unresolved optional dependency.
|
|
60
|
+
*/
|
|
61
|
+
function loadWithNetInfo(factory: () => unknown): Harness {
|
|
62
|
+
let harness: Harness | undefined;
|
|
63
|
+
|
|
64
|
+
jest.isolateModules(() => {
|
|
65
|
+
const toast = { error: jest.fn(), warning: jest.fn(), success: jest.fn() };
|
|
66
|
+
// NOT `{ virtual: true }`: netinfo is a real devDependency here, and a
|
|
67
|
+
// virtual mock is keyed by the bare name while the component's `require`
|
|
68
|
+
// resolves to the installed path — the mock would never be consulted and
|
|
69
|
+
// every case would quietly run against the real module.
|
|
70
|
+
jest.doMock(NETINFO, factory);
|
|
71
|
+
jest.doMock('../toast', () => ({ toast }));
|
|
72
|
+
// An isolated registry would otherwise build the component against a SECOND
|
|
73
|
+
// copy of React, whose dispatcher is null under the outer renderer — every
|
|
74
|
+
// case would fail on `useState` before reaching what it asserts. Pinning
|
|
75
|
+
// react to the outer instance keeps one hook dispatcher for the whole tree.
|
|
76
|
+
jest.doMock('react', () => reactModule);
|
|
77
|
+
|
|
78
|
+
// A plain `require`, NOT `jest.requireActual`: the latter bypasses the mock
|
|
79
|
+
// registry for the whole dependency subtree, so the doMocks above would be
|
|
80
|
+
// ignored and the suite would silently exercise the REAL netinfo.
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
82
|
+
const { ConnectionStatusToasts } = require('../connection-status') as typeof import('../connection-status');
|
|
83
|
+
|
|
84
|
+
harness = { ConnectionStatusToasts, toast };
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!harness) throw new Error('module isolation did not run');
|
|
88
|
+
return harness;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe('ConnectionStatusToasts netinfo loading', () => {
|
|
92
|
+
let warn: jest.SpyInstance<void, Parameters<typeof console.warn>>;
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
// `__mocks__/setup.ts` already silences console.warn; this re-spies so the
|
|
96
|
+
// calls are recorded and stay scoped to one test.
|
|
97
|
+
warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
warn.mockRestore();
|
|
102
|
+
jest.resetModules();
|
|
103
|
+
jest.dontMock(NETINFO);
|
|
104
|
+
jest.dontMock('../toast');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('keeps its own types in step with the real netinfo surface', () => {
|
|
108
|
+
// The real assertion is the type annotation above, checked by `tsc`. This
|
|
109
|
+
// only keeps the constant referenced so it cannot be dropped as unused.
|
|
110
|
+
expect(NETINFO_TYPES_STILL_MATCH).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('subscribes through the named export when the peer is installed', () => {
|
|
114
|
+
const unsubscribe = jest.fn();
|
|
115
|
+
const addEventListener = jest.fn().mockReturnValue(unsubscribe);
|
|
116
|
+
const { ConnectionStatusToasts, toast } = loadWithNetInfo(() => ({ addEventListener }));
|
|
117
|
+
|
|
118
|
+
const view = render(<ConnectionStatusToasts />);
|
|
119
|
+
expect(addEventListener).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(warn).not.toHaveBeenCalled();
|
|
121
|
+
|
|
122
|
+
const emit = addEventListener.mock.calls[0][0] as (s: ReturnType<typeof state>) => void;
|
|
123
|
+
view.rerender(<ConnectionStatusToasts />);
|
|
124
|
+
emit(state(false));
|
|
125
|
+
view.rerender(<ConnectionStatusToasts />);
|
|
126
|
+
|
|
127
|
+
expect(toast.error).toHaveBeenCalledWith('No internet connection', expect.anything());
|
|
128
|
+
|
|
129
|
+
view.unmount();
|
|
130
|
+
expect(unsubscribe).toHaveBeenCalledTimes(1);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('subscribes through the default export too', () => {
|
|
134
|
+
// Which shape comes back depends on whether the bundler resolved netinfo's
|
|
135
|
+
// CommonJS build or its ESM/source entry; both are live in the fleet.
|
|
136
|
+
const addEventListener = jest.fn().mockReturnValue(jest.fn());
|
|
137
|
+
const { ConnectionStatusToasts } = loadWithNetInfo(() => ({
|
|
138
|
+
__esModule: true,
|
|
139
|
+
default: { addEventListener },
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
render(<ConnectionStatusToasts />);
|
|
143
|
+
expect(addEventListener).toHaveBeenCalledTimes(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('renders inert instead of throwing when the peer is absent', () => {
|
|
147
|
+
const { ConnectionStatusToasts, toast } = loadWithNetInfo(() => {
|
|
148
|
+
throw new Error(`Cannot find module '${NETINFO}'`);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const view = render(<ConnectionStatusToasts />);
|
|
152
|
+
expect(view.toJSON()).toBeNull();
|
|
153
|
+
expect(toast.error).not.toHaveBeenCalled();
|
|
154
|
+
expect(toast.warning).not.toHaveBeenCalled();
|
|
155
|
+
expect(toast.success).not.toHaveBeenCalled();
|
|
156
|
+
expect(() => view.unmount()).not.toThrow();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('names the missing package once, not once per mount', () => {
|
|
160
|
+
const { ConnectionStatusToasts } = loadWithNetInfo(() => {
|
|
161
|
+
throw new Error(`Cannot find module '${NETINFO}'`);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
render(<ConnectionStatusToasts />).unmount();
|
|
165
|
+
render(<ConnectionStatusToasts />).unmount();
|
|
166
|
+
|
|
167
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
168
|
+
const [message] = warn.mock.calls[0] as [string];
|
|
169
|
+
// An actionable warning has to carry all three: what broke, which package,
|
|
170
|
+
// and how to fix it. A bare "netinfo missing" sends the reader to Bloom.
|
|
171
|
+
expect(message).toContain(NETINFO);
|
|
172
|
+
expect(message).toContain('inert');
|
|
173
|
+
expect(message).toContain('expo install');
|
|
174
|
+
// …and the underlying resolution error, so a shape/version failure is not
|
|
175
|
+
// reported as if the package were simply not installed.
|
|
176
|
+
expect(message).toContain('Cannot find module');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('warns when the peer resolves but has no addEventListener', () => {
|
|
180
|
+
const { ConnectionStatusToasts } = loadWithNetInfo(() => ({ fetch: jest.fn() }));
|
|
181
|
+
|
|
182
|
+
render(<ConnectionStatusToasts />);
|
|
183
|
+
|
|
184
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
185
|
+
expect((warn.mock.calls[0] as [string])[0]).toContain('without an `addEventListener` export');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('stays silent in production', () => {
|
|
189
|
+
const previous = process.env.NODE_ENV;
|
|
190
|
+
// Metro and Vite fold this branch away entirely; jest cannot, so the guard
|
|
191
|
+
// is asserted at runtime instead.
|
|
192
|
+
process.env.NODE_ENV = 'production';
|
|
193
|
+
try {
|
|
194
|
+
const { ConnectionStatusToasts } = loadWithNetInfo(() => {
|
|
195
|
+
throw new Error(`Cannot find module '${NETINFO}'`);
|
|
196
|
+
});
|
|
197
|
+
render(<ConnectionStatusToasts />);
|
|
198
|
+
expect(warn).not.toHaveBeenCalled();
|
|
199
|
+
} finally {
|
|
200
|
+
process.env.NODE_ENV = previous;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|