@unbound-app/api 2.2.0 → 3.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/package.json +2 -2
- package/src/_conventions/addon-assets.ts +59 -0
- package/src/_conventions/addon-locales.ts +50 -0
- package/{testing → src/_conventions}/icu-parse.ts +2 -0
- package/src/_conventions/metro.ts +37 -0
- package/src/_conventions/walk-tree.ts +88 -0
- package/src/_internal.d.ts +210 -126
- package/src/builtins.d.ts +4 -2
- package/src/global.d.ts +24 -19
- package/src/marketplace.d.ts +1 -1
- package/src/metro/index.d.ts +18 -16
- package/src/native.d.ts +34 -22
- package/src/plugins.d.ts +9 -3
- package/src/storage.d.ts +7 -3
- package/src/sync.d.ts +3 -3
- package/testing/fake-feedback.ts +1 -1
- package/testing/fake-java.ts +41 -2
- package/testing/fake-locales.ts +1 -3
- package/testing/fake-metro.ts +53 -45
- package/testing/fake-native.ts +44 -30
- package/testing/fake-plugins.ts +4 -4
- package/testing/fake-react-native-modules.ts +0 -1
- package/testing/fake-utils.ts +3 -59
- package/testing/fakes.ts +8 -0
- package/testing/icu-format.ts +7 -2
- package/testing/index.ts +13 -26
- package/testing/install-addon-globals.ts +12 -2
- package/testing/plugin-files.ts +11 -3
- package/testing/preset.ts +9 -8
- package/testing/recorders.ts +17 -4
- package/testing/session.ts +2 -2
package/testing/fake-native.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
NativeBridge,
|
|
3
|
+
NativeBridgeCapability,
|
|
4
|
+
NativeBridgeMethod,
|
|
5
|
+
NativeBridgePlatform,
|
|
5
6
|
PluginContext,
|
|
6
7
|
} from '../src/native';
|
|
7
8
|
import type { AddonManifest } from '../src/global';
|
|
@@ -14,17 +15,19 @@ import { createFakeObjc, type FakeObjc } from './fake-objc';
|
|
|
14
15
|
/** What `startPlugin` takes to build the `PluginContext` its `start` receives; the rest is filled in as the client would. */
|
|
15
16
|
export type StartContext = Partial<Omit<PluginContext, 'native'>> & {
|
|
16
17
|
/** The bridge members the plugin calls; the others are empty objects. */
|
|
17
|
-
native?: Partial<
|
|
18
|
+
native?: Partial<NativeBridge>;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
/** The fake `native` namespace's state, which a test sets through the `native` recorder. */
|
|
21
22
|
export type NativeState = {
|
|
22
23
|
/** The host platform `definePlatform` selects by and `Platform.OS` reports; `ios` until a test sets it. */
|
|
23
|
-
platform:
|
|
24
|
+
platform: NativeBridgePlatform;
|
|
24
25
|
/** The native modules `getNativeModule` finds, by name. */
|
|
25
26
|
modules: Map<string, unknown>;
|
|
26
27
|
/** The capabilities `native.has` reports. */
|
|
27
|
-
capabilities:
|
|
28
|
+
capabilities: NativeBridgeCapability[];
|
|
29
|
+
/** The bridge methods `native.supports` denies; every other method is supported. */
|
|
30
|
+
unsupportedMethods: NativeBridgeMethod[];
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
/** The fake `native`, its state, and the `PluginContext` builder. */
|
|
@@ -48,49 +51,53 @@ function emptyMember<T>(): T {
|
|
|
48
51
|
return Object.create(null);
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
// Keyed by every platform the SDK names, so a new one fails to typecheck until the fake serves it.
|
|
55
|
+
const PLATFORMS: NativeBridgePlatform[] = Object.values({
|
|
56
|
+
ios: 'ios',
|
|
57
|
+
android: 'android',
|
|
58
|
+
} as const satisfies { [P in NativeBridgePlatform]: P });
|
|
52
59
|
|
|
53
|
-
class
|
|
60
|
+
class NativeBridgeUnavailableError extends Error {
|
|
54
61
|
readonly code = 'NATIVE_PLUGIN_UNAVAILABLE';
|
|
55
62
|
}
|
|
56
63
|
|
|
57
|
-
class
|
|
64
|
+
class NativeBridgeCapabilityError extends Error {
|
|
58
65
|
readonly code = 'NATIVE_PLUGIN_CAPABILITY_DENIED';
|
|
59
|
-
readonly capability:
|
|
66
|
+
readonly capability: NativeBridgeCapability;
|
|
60
67
|
|
|
61
|
-
constructor(capability:
|
|
62
|
-
super(`Native
|
|
68
|
+
constructor(capability: NativeBridgeCapability) {
|
|
69
|
+
super(`Native bridge capability is not declared: ${capability}`);
|
|
63
70
|
this.capability = capability;
|
|
64
71
|
}
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
class
|
|
74
|
+
class NativeBridgeUnsupportedPlatformError extends Error {
|
|
68
75
|
readonly code = 'NATIVE_PLUGIN_PLATFORM_EXCLUDED';
|
|
69
|
-
readonly platforms: readonly
|
|
76
|
+
readonly platforms: readonly NativeBridgePlatform[];
|
|
70
77
|
|
|
71
|
-
constructor(platforms: readonly
|
|
78
|
+
constructor(platforms: readonly NativeBridgePlatform[], platform: string) {
|
|
72
79
|
super(`This plugin supports ${platforms.join(', ')}; the host platform is ${platform}.`);
|
|
73
80
|
this.platforms = platforms;
|
|
74
81
|
}
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
class
|
|
84
|
+
class NativeBridgeVersionError extends Error {
|
|
78
85
|
readonly code = 'NATIVE_PLUGIN_API_VERSION_UNSUPPORTED';
|
|
79
86
|
readonly installedApi: string;
|
|
80
87
|
readonly requiredApi: string;
|
|
81
88
|
|
|
82
89
|
constructor(requiredApi: string, installedApi: string) {
|
|
83
|
-
super(`Native
|
|
90
|
+
super(`Native bridge API ${requiredApi} is required, but ${installedApi} is installed.`);
|
|
84
91
|
this.installedApi = installedApi;
|
|
85
92
|
this.requiredApi = requiredApi;
|
|
86
93
|
}
|
|
87
94
|
}
|
|
88
95
|
|
|
89
|
-
class
|
|
96
|
+
class NativeBridgeDisposedError extends Error {
|
|
90
97
|
readonly code = 'NATIVE_PLUGIN_SCOPE_DISPOSED';
|
|
91
98
|
|
|
92
99
|
constructor() {
|
|
93
|
-
super('The native
|
|
100
|
+
super('The native bridge scope has been disposed.');
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
|
|
@@ -100,16 +107,21 @@ class NativePluginDisposedError extends Error {
|
|
|
100
107
|
* @returns The namespace, its state, and the context builder.
|
|
101
108
|
*/
|
|
102
109
|
export function createFakeNative(): FakeNative {
|
|
103
|
-
const state: NativeState = {
|
|
110
|
+
const state: NativeState = {
|
|
111
|
+
platform: 'ios',
|
|
112
|
+
modules: new Map(),
|
|
113
|
+
capabilities: [],
|
|
114
|
+
unsupportedMethods: [],
|
|
115
|
+
};
|
|
104
116
|
const log = createCallLog();
|
|
105
117
|
const objc = createFakeObjc(log);
|
|
106
118
|
const java = createFakeJava(log);
|
|
107
119
|
|
|
108
|
-
function select<Handler>(handlers: Partial<Record<
|
|
120
|
+
function select<Handler>(handlers: Partial<Record<NativeBridgePlatform, Handler>>): Handler {
|
|
109
121
|
const handler = handlers[state.platform];
|
|
110
122
|
|
|
111
123
|
if (handler === undefined) {
|
|
112
|
-
throw new
|
|
124
|
+
throw new NativeBridgeUnsupportedPlatformError(
|
|
113
125
|
PLATFORMS.filter((name) => handlers[name] !== undefined),
|
|
114
126
|
state.platform,
|
|
115
127
|
);
|
|
@@ -121,7 +133,7 @@ export function createFakeNative(): FakeNative {
|
|
|
121
133
|
// A plugin selects as it imports platform.ts, before a test sets the platform, so a function handler
|
|
122
134
|
// is a forwarder to the current platform's handler. Any other value is read now.
|
|
123
135
|
function definePlatform<Handler>(
|
|
124
|
-
handlers: Partial<Record<
|
|
136
|
+
handlers: Partial<Record<NativeBridgePlatform, Handler>>,
|
|
125
137
|
): Handler {
|
|
126
138
|
const selected = select(handlers);
|
|
127
139
|
|
|
@@ -138,11 +150,11 @@ export function createFakeNative(): FakeNative {
|
|
|
138
150
|
}
|
|
139
151
|
|
|
140
152
|
const namespace: FakeModule = {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
153
|
+
NativeBridgeUnavailableError,
|
|
154
|
+
NativeBridgeCapabilityError,
|
|
155
|
+
NativeBridgeUnsupportedPlatformError,
|
|
156
|
+
NativeBridgeVersionError,
|
|
157
|
+
NativeBridgeDisposedError,
|
|
146
158
|
BundleInfo: {},
|
|
147
159
|
BundleManager: {},
|
|
148
160
|
DeviceInfo: {},
|
|
@@ -151,8 +163,9 @@ export function createFakeNative(): FakeNative {
|
|
|
151
163
|
names.map((name) => state.modules.get(name)).find(Boolean),
|
|
152
164
|
getNativeResources: () => undefined,
|
|
153
165
|
getRuntimeProperties: () => ({}),
|
|
154
|
-
has: (capability:
|
|
166
|
+
has: (capability: NativeBridgeCapability) => state.capabilities.includes(capability),
|
|
155
167
|
reload: async () => {},
|
|
168
|
+
supports: (method: NativeBridgeMethod) => !state.unsupportedMethods.includes(method),
|
|
156
169
|
};
|
|
157
170
|
|
|
158
171
|
return {
|
|
@@ -168,11 +181,12 @@ export function createFakeNative(): FakeNative {
|
|
|
168
181
|
state.platform = 'ios';
|
|
169
182
|
state.modules.clear();
|
|
170
183
|
state.capabilities = [];
|
|
184
|
+
state.unsupportedMethods = [];
|
|
171
185
|
},
|
|
172
186
|
context(manifest, overrides = {}) {
|
|
173
187
|
const { native, ...rest } = overrides;
|
|
174
188
|
const capabilities = rest.capabilities ?? state.capabilities;
|
|
175
|
-
const bridge:
|
|
189
|
+
const bridge: NativeBridge = {
|
|
176
190
|
apiVersion: '1.0.0',
|
|
177
191
|
abiVersion: '1',
|
|
178
192
|
capabilities,
|
package/testing/fake-plugins.ts
CHANGED
|
@@ -38,7 +38,7 @@ export function createFakePlugins(own: AddonManifest): FakePlugins {
|
|
|
38
38
|
const states = new Map<string, State>();
|
|
39
39
|
const handles = new Map<string, AddonHandle>();
|
|
40
40
|
const errors = new Map<PropertyKey, Error>();
|
|
41
|
-
const
|
|
41
|
+
const addons = new Map<PropertyKey, AddonHandle>();
|
|
42
42
|
const startDurations = new Map<string, number>();
|
|
43
43
|
|
|
44
44
|
function stateOf(id: string): State | undefined {
|
|
@@ -110,7 +110,7 @@ export function createFakePlugins(own: AddonManifest): FakePlugins {
|
|
|
110
110
|
const handle = handleOf(id, id === own.id ? own : undefined);
|
|
111
111
|
|
|
112
112
|
handles.set(id, handle);
|
|
113
|
-
|
|
113
|
+
addons.set(id, handle);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
const error = stateOf(id)?.error;
|
|
@@ -125,7 +125,7 @@ export function createFakePlugins(own: AddonManifest): FakePlugins {
|
|
|
125
125
|
function remove(id: string) {
|
|
126
126
|
states.delete(id);
|
|
127
127
|
handles.delete(id);
|
|
128
|
-
|
|
128
|
+
addons.delete(id);
|
|
129
129
|
errors.delete(id);
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -155,7 +155,7 @@ export function createFakePlugins(own: AddonManifest): FakePlugins {
|
|
|
155
155
|
reset();
|
|
156
156
|
|
|
157
157
|
const namespace: FakeModule = {
|
|
158
|
-
|
|
158
|
+
addons,
|
|
159
159
|
errors,
|
|
160
160
|
startDurations,
|
|
161
161
|
get: (id: string) => handles.get(id),
|
package/testing/fake-utils.ts
CHANGED
|
@@ -3,7 +3,9 @@ import { useRef } from 'react';
|
|
|
3
3
|
import type { TreeNode, TreePredicate, TreeSearchOptions } from '../src/utils/index';
|
|
4
4
|
import type { PredicateResult } from '../src/_types';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import walkTree from '../src/_conventions/walk-tree';
|
|
7
|
+
|
|
8
|
+
// The tree walk is the client's own; the React finders below mirror the client's `utils/react`.
|
|
7
9
|
|
|
8
10
|
/** What `findComponent` and `filterComponents` match by: a component name, or a predicate on each element. */
|
|
9
11
|
type ComponentMatch = string | ((element: TreeNode) => PredicateResult);
|
|
@@ -28,64 +30,6 @@ const REACT_KEYS = [
|
|
|
28
30
|
// Elements nest through their props, so only `props` and `children` lead to more elements.
|
|
29
31
|
const ELEMENT_OPTIONS: TreeSearchOptions = { walkable: ['props', 'children'] };
|
|
30
32
|
|
|
31
|
-
function walkTree(
|
|
32
|
-
root: TreeNode,
|
|
33
|
-
predicate: TreePredicate,
|
|
34
|
-
options: TreeSearchOptions,
|
|
35
|
-
all: boolean,
|
|
36
|
-
): TreeNode[] {
|
|
37
|
-
const { walkable, ignore = [], maxDepth = 100 } = options;
|
|
38
|
-
const seen = new WeakSet<object>();
|
|
39
|
-
const found: TreeNode[] = [];
|
|
40
|
-
|
|
41
|
-
// Returns `true` once the search should stop.
|
|
42
|
-
function visit(node: TreeNode, depth: number): boolean {
|
|
43
|
-
if (node === null || node === undefined) {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const isObject = typeof node === 'object';
|
|
48
|
-
|
|
49
|
-
if (isObject) {
|
|
50
|
-
if (seen.has(node)) {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
seen.add(node);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (predicate(node)) {
|
|
58
|
-
found.push(node);
|
|
59
|
-
|
|
60
|
-
if (!all) {
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (!isObject || depth >= maxDepth) {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const keys = Array.isArray(node) ? node.keys() : (walkable ?? Object.keys(node));
|
|
70
|
-
|
|
71
|
-
for (const key of keys) {
|
|
72
|
-
if (typeof key === 'string' && ignore.includes(key)) {
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (visit(node[key], depth + 1)) {
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
visit(root, 0);
|
|
85
|
-
|
|
86
|
-
return found;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
33
|
function reactOptions({ walkable = [], ...rest }: TreeSearchOptions): TreeSearchOptions {
|
|
90
34
|
return { ...rest, walkable: [...REACT_KEYS, ...walkable] };
|
|
91
35
|
}
|
package/testing/fakes.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createFakeSettings, defineSettings, setting, type FakeSettings } from './fake-settings';
|
|
2
|
+
// The fakes the preset is built from, for testing shared code that runs inside plugins but is no plugin itself.
|
|
3
|
+
export { installAddonGlobals, type InstalledAddonGlobals } from './install-addon-globals';
|
|
4
|
+
export { createFakeJava, type FakeJava, type JavaPlain } from './fake-java';
|
|
5
|
+
export { createMemoryStorage, type MemoryStorage } from './memory-storage';
|
|
6
|
+
export { brand, settled, withMissingMembers } from './fake-native-bridge';
|
|
7
|
+
export { createFakeNative, type FakeNative } from './fake-native';
|
|
8
|
+
export { createRecordingPatcher } from './recording-patcher';
|
package/testing/icu-format.ts
CHANGED
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
type IcuNode,
|
|
5
5
|
type IcuPluralNode,
|
|
6
6
|
type IcuSelectNode,
|
|
7
|
-
} from '
|
|
7
|
+
} from '../src/_conventions/icu-parse';
|
|
8
|
+
import { SOURCE_LOCALE } from '../src/_conventions/addon-locales';
|
|
8
9
|
|
|
9
10
|
/** A value `format` puts into a placeholder, as the client's `FormatVars` types it. */
|
|
10
11
|
export type IcuValue = string | number | boolean | Date;
|
|
@@ -127,6 +128,10 @@ function formatNodes(nodes: IcuNode[], vars: IcuVars, locale: string, pound?: nu
|
|
|
127
128
|
* @param locale The locale plural rules, numbers and dates follow, such as `de`.
|
|
128
129
|
* @returns The formatted message.
|
|
129
130
|
*/
|
|
130
|
-
export function formatIcu(
|
|
131
|
+
export function formatIcu(
|
|
132
|
+
message: string,
|
|
133
|
+
vars: IcuVars = {},
|
|
134
|
+
locale: string = SOURCE_LOCALE,
|
|
135
|
+
): string {
|
|
131
136
|
return formatNodes(parseIcu(message), vars, locale);
|
|
132
137
|
}
|
package/testing/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// What a plugin's tests import, beside the preset in their bunfig.toml: the lifecycle, `render` and the recorders.
|
|
1
2
|
export {
|
|
2
3
|
toasts,
|
|
3
4
|
dialogs,
|
|
@@ -11,32 +12,18 @@ export {
|
|
|
11
12
|
logs,
|
|
12
13
|
setLocale,
|
|
13
14
|
extendApi,
|
|
15
|
+
type ToastRecorder,
|
|
16
|
+
type DialogRecorder,
|
|
17
|
+
type CommandRecorder,
|
|
18
|
+
type PatchRecorder,
|
|
19
|
+
type FluxRecorder,
|
|
20
|
+
type MetroRecorder,
|
|
21
|
+
type NativeRecorder,
|
|
22
|
+
type PluginRecorder,
|
|
23
|
+
type FsRecorder,
|
|
24
|
+
type LogRecorder,
|
|
14
25
|
} from './recorders';
|
|
15
|
-
export { act, createTestRenderer, type TestRenderer } from './test-renderer';
|
|
16
26
|
export { startPlugin, stopPlugin, pluginHolds } from './plugin-lifecycle';
|
|
17
27
|
export { render, type RenderOptions, type RenderResult } from './render';
|
|
18
|
-
export
|
|
19
|
-
export
|
|
20
|
-
export * from './fake-react-native';
|
|
21
|
-
export * from './listener-registry';
|
|
22
|
-
export * from './recording-patcher';
|
|
23
|
-
export * from './fake-components';
|
|
24
|
-
export * from './memory-storage';
|
|
25
|
-
export * from './fake-feedback';
|
|
26
|
-
export * from './fake-settings';
|
|
27
|
-
export * from './fake-locales';
|
|
28
|
-
export * from './fake-plugins';
|
|
29
|
-
export * from './plugin-types';
|
|
30
|
-
export * from './fake-native';
|
|
31
|
-
export * from './fake-timers';
|
|
32
|
-
export * from './react-value';
|
|
33
|
-
export * from './fake-metro';
|
|
34
|
-
export * from './fake-react';
|
|
35
|
-
export * from './fake-utils';
|
|
36
|
-
export * from './icu-format';
|
|
37
|
-
export * from './fake-java';
|
|
38
|
-
export * from './fake-objc';
|
|
39
|
-
export * from './icu-parse';
|
|
40
|
-
export * from './test-node';
|
|
41
|
-
export * from './fake-api';
|
|
42
|
-
export * from './fake-fs';
|
|
28
|
+
export { fakeAssetId, type StoreSeeds } from './fake-api';
|
|
29
|
+
export type { PluginTypes } from './plugin-types';
|
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
OptionValue,
|
|
13
13
|
Reply,
|
|
14
14
|
} from '../src/commands';
|
|
15
|
-
import type { AddonHandle, AddonLogger, AddonManifest } from '../src/global';
|
|
15
|
+
import type { AddonHandle, AddonLogger, AddonManifest, AddonScope } from '../src/global';
|
|
16
16
|
|
|
17
17
|
import { createRecordingPatcher, type RecordingPatcher } from './recording-patcher';
|
|
18
18
|
import { createFakeMetro, type FakeMetro, type FakeModule } from './fake-metro';
|
|
@@ -82,6 +82,16 @@ export type InstalledAddonGlobals = {
|
|
|
82
82
|
uninstall: () => void;
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
+
// The fake each Addon scope global is; keying the installed globals by `AddonScope` fails typecheck on a new one.
|
|
86
|
+
type ScopeFake =
|
|
87
|
+
| FakeReact
|
|
88
|
+
| FakeUnbound
|
|
89
|
+
| AddonHandle
|
|
90
|
+
| RecordingPatcher
|
|
91
|
+
| AddonLogger
|
|
92
|
+
| FakeCommands
|
|
93
|
+
| FakeTimers[keyof FakeTimers];
|
|
94
|
+
|
|
85
95
|
const LOG_LEVELS: LogLevel[] = ['log', 'info', 'success', 'warn', 'error', 'debug'];
|
|
86
96
|
|
|
87
97
|
function createLogger(logs: LogEntry[]): AddonLogger {
|
|
@@ -238,7 +248,7 @@ export function installAddonGlobals(options: InstallOptions = {}): InstalledAddo
|
|
|
238
248
|
setInterval: timers.setInterval,
|
|
239
249
|
clearTimeout: timers.clearTimeout,
|
|
240
250
|
clearInterval: timers.clearInterval,
|
|
241
|
-
}
|
|
251
|
+
} satisfies Record<keyof AddonScope, ScopeFake>;
|
|
242
252
|
const previous = new Map<string, PropertyDescriptor | undefined>();
|
|
243
253
|
|
|
244
254
|
for (const [name, value] of Object.entries(globals)) {
|
package/testing/plugin-files.ts
CHANGED
|
@@ -5,8 +5,14 @@ import type { AddonAuthor, AddonManifest } from '../src/global';
|
|
|
5
5
|
import type { LocaleTable } from './fake-locales';
|
|
6
6
|
import type { AnyObject } from '../src/patcher';
|
|
7
7
|
|
|
8
|
+
import { ASSETS_DIRECTORY, ASSET_SCALES } from '../src/_conventions/addon-assets';
|
|
9
|
+
import { LOCALES_DIRECTORY } from '../src/_conventions/addon-locales';
|
|
10
|
+
|
|
8
11
|
const ENTRY_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js'];
|
|
9
12
|
|
|
13
|
+
// The `@2x` and `@3x` an image's file name ends in at each scale above 1.
|
|
14
|
+
const SCALE_SUFFIXES = ASSET_SCALES.filter((scale) => scale !== '1').map((scale) => `@${scale}x`);
|
|
15
|
+
|
|
10
16
|
function isRecord(value: unknown): value is AnyObject {
|
|
11
17
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
18
|
}
|
|
@@ -97,7 +103,7 @@ export function readManifest(root: string): Partial<AddonManifest> {
|
|
|
97
103
|
* @returns Each table by locale code; empty when the plugin has no `i18n/`. Throws naming an invalid file.
|
|
98
104
|
*/
|
|
99
105
|
export function readLocaleTables(root: string): Map<string, LocaleTable> {
|
|
100
|
-
const directory = join(root,
|
|
106
|
+
const directory = join(root, LOCALES_DIRECTORY);
|
|
101
107
|
const tables = new Map<string, LocaleTable>();
|
|
102
108
|
|
|
103
109
|
if (!existsSync(directory)) {
|
|
@@ -147,9 +153,11 @@ export function findEntry(root: string): string | undefined {
|
|
|
147
153
|
* @returns The asset name, such as `icons/star`.
|
|
148
154
|
*/
|
|
149
155
|
export function assetNameOf(root: string, path: string): string {
|
|
150
|
-
const assets = join(root,
|
|
156
|
+
const assets = join(root, ASSETS_DIRECTORY);
|
|
151
157
|
const base = path.startsWith(assets + sep) ? assets : root;
|
|
152
158
|
const name = relative(base, path).slice(0, -extname(path).length).split(sep).join('/');
|
|
153
159
|
|
|
154
|
-
|
|
160
|
+
const suffix = SCALE_SUFFIXES.find((each) => name.endsWith(each));
|
|
161
|
+
|
|
162
|
+
return suffix ? name.slice(0, -suffix.length) : name;
|
|
155
163
|
}
|
package/testing/preset.ts
CHANGED
|
@@ -18,10 +18,12 @@ import { createFakeSettings, defineSettings, setting, type SettingsSchema } from
|
|
|
18
18
|
import { createListenerRegistry, type ListenerRegistry } from './listener-registry';
|
|
19
19
|
import { createFakeReactNative, type HostPropsByName } from './fake-react-native';
|
|
20
20
|
import { installAddonGlobals, type FakeUnbound } from './install-addon-globals';
|
|
21
|
-
import {
|
|
21
|
+
import { SOURCE_LOCALE } from '../src/_conventions/addon-locales';
|
|
22
|
+
import { ASSET_TYPES } from '../src/_conventions/addon-assets';
|
|
22
23
|
import { createFakeAssets, createFakeFlux } from './fake-api';
|
|
23
24
|
import { createJsxRuntimes } from './unfrozen-elements';
|
|
24
25
|
import { resetSession, startSession } from './session';
|
|
26
|
+
import { createFakeLocales } from './fake-locales';
|
|
25
27
|
import { createFakePlugins } from './fake-plugins';
|
|
26
28
|
import { forgetPlugin } from './plugin-lifecycle';
|
|
27
29
|
import { createFakeUi } from './fake-components';
|
|
@@ -29,16 +31,15 @@ import { createFakeNative } from './fake-native';
|
|
|
29
31
|
import { createFakeUtils } from './fake-utils';
|
|
30
32
|
import { unmountAll } from './test-renderer';
|
|
31
33
|
import { createFakeFs } from './fake-fs';
|
|
34
|
+
import sdk from '../package.json';
|
|
32
35
|
|
|
33
36
|
type SvgProps = Omit<HostPropsByName['SvgXml'], 'xml' | 'asset'>;
|
|
34
37
|
|
|
35
|
-
// The
|
|
36
|
-
const NESTED = new
|
|
37
|
-
['metro', ['api', 'common', 'components', 'filters', 'stores']],
|
|
38
|
-
['utils', ['react']],
|
|
39
|
-
]);
|
|
38
|
+
// The nested modules the SDK publishes, such as `metro/components` for `unbound.metro.components`.
|
|
39
|
+
const NESTED = new Set(Object.keys(sdk.exports).map((subpath) => subpath.slice(2)));
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
// The build also takes `.jpeg` as a jpg.
|
|
42
|
+
const IMAGE = new RegExp(`\\.(${[...ASSET_TYPES, 'jpeg'].join('|')})$`, 'i');
|
|
42
43
|
const SVG = /\.svg$/i;
|
|
43
44
|
const SVG_ASSET = /\.svg\?asset$/i;
|
|
44
45
|
const SVG_ASSET_NAMESPACE = 'unbound-svg-asset';
|
|
@@ -175,7 +176,7 @@ for (const [name, namespace] of Object.entries(scope.unbound)) {
|
|
|
175
176
|
|
|
176
177
|
// A namespace that nests others serves each one at its own path, such as `@unbound-app/api/metro/components`.
|
|
177
178
|
for (const [nested, members] of Object.entries(namespace)) {
|
|
178
|
-
if (NESTED.
|
|
179
|
+
if (NESTED.has(`${name}/${nested}`)) {
|
|
179
180
|
mock.module(`@unbound-app/api/${name}/${nested}`, () => members);
|
|
180
181
|
}
|
|
181
182
|
}
|
package/testing/recorders.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
NativeBridgeCapability,
|
|
3
|
+
NativeBridgeMethod,
|
|
4
|
+
NativeBridgePlatform,
|
|
5
|
+
} from '../src/native';
|
|
2
6
|
import type { CommandArgs, LogEntry } from './install-addon-globals';
|
|
3
7
|
import type { OpenedDialog, ShownToast } from './fake-feedback';
|
|
4
8
|
import type { CommandContext, Reply } from '../src/commands';
|
|
@@ -42,17 +46,23 @@ export type FluxRecorder = {
|
|
|
42
46
|
};
|
|
43
47
|
|
|
44
48
|
/** The fake metro's modules: `register` one for the plugin to find, such as `{ sendMessage }`. */
|
|
45
|
-
export type MetroRecorder = {
|
|
49
|
+
export type MetroRecorder = {
|
|
50
|
+
register: (module: FakeModule, path?: string) => void;
|
|
51
|
+
/** Marks a prop name, file path or export name as missing from this build: a lazy lookup by it returns `undefined`, and `onModuleLoaded` calls `onMissing`. */
|
|
52
|
+
markMissing: (term: string) => void;
|
|
53
|
+
};
|
|
46
54
|
|
|
47
55
|
/** The fake `native`: the platform `definePlatform` selects by, and the modules and capabilities it reports. */
|
|
48
56
|
export type NativeRecorder = {
|
|
49
57
|
/** Switches the host platform, `ios` until set; `definePlatform` and `Platform.OS` follow it. */
|
|
50
|
-
setPlatform: (platform:
|
|
58
|
+
setPlatform: (platform: NativeBridgePlatform) => void;
|
|
51
59
|
/** Gives `getNativeModule(name)` a module to return. */
|
|
52
60
|
// UNKNOWN: a native module has whatever shape the plugin reads.
|
|
53
61
|
registerModule: (name: string, module: unknown) => void;
|
|
54
62
|
/** Sets what `native.has` reports. */
|
|
55
|
-
setCapabilities: (capabilities:
|
|
63
|
+
setCapabilities: (capabilities: NativeBridgeCapability[]) => void;
|
|
64
|
+
/** Sets the bridge methods `native.supports` denies, as on an older loader; none until set. */
|
|
65
|
+
setUnsupportedMethods: (methods: NativeBridgeMethod[]) => void;
|
|
56
66
|
/** The Objective-C bridge a plugin gets on iOS: register classes and objects for its calls to reach. */
|
|
57
67
|
readonly objc: FakeObjc;
|
|
58
68
|
/** The Java bridge a plugin gets on Android: register classes and objects for its calls to reach. */
|
|
@@ -108,12 +118,15 @@ function createRecorders() {
|
|
|
108
118
|
};
|
|
109
119
|
const metro: MetroRecorder = {
|
|
110
120
|
register: (module, path) => getSession().scope.metro.register(module, path),
|
|
121
|
+
markMissing: (term) => getSession().scope.metro.markMissing(term),
|
|
111
122
|
};
|
|
112
123
|
const native: NativeRecorder = {
|
|
113
124
|
setPlatform: (platform) => void (getSession().native.state.platform = platform),
|
|
114
125
|
registerModule: (name, module) => void getSession().native.state.modules.set(name, module),
|
|
115
126
|
setCapabilities: (capabilities) =>
|
|
116
127
|
void (getSession().native.state.capabilities = capabilities),
|
|
128
|
+
setUnsupportedMethods: (methods) =>
|
|
129
|
+
void (getSession().native.state.unsupportedMethods = methods),
|
|
117
130
|
get objc() {
|
|
118
131
|
return getSession().native.objc;
|
|
119
132
|
},
|
package/testing/session.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { FakePlugins } from './fake-plugins';
|
|
|
8
8
|
import type { FakeNative } from './fake-native';
|
|
9
9
|
import type { FakeFs } from './fake-fs';
|
|
10
10
|
|
|
11
|
+
import { SOURCE_LOCALE } from '../src/_conventions/addon-locales';
|
|
11
12
|
import { unmountAll } from './test-renderer';
|
|
12
13
|
|
|
13
14
|
/** The plugin under test and every fake the preset installed for it. */
|
|
@@ -31,7 +32,6 @@ let active: TestSession | undefined;
|
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* @description Makes a session the one the recorders, `render` and `startPlugin` read. The preset calls it once.
|
|
34
|
-
* @param session The session.
|
|
35
35
|
*/
|
|
36
36
|
export function startSession(session: TestSession): void {
|
|
37
37
|
active = session;
|
|
@@ -73,5 +73,5 @@ export function resetSession(): void {
|
|
|
73
73
|
session.flux.reset();
|
|
74
74
|
session.toasts.shown.length = 0;
|
|
75
75
|
session.dialogs.opened.length = 0;
|
|
76
|
-
session.locales?.setLocale(
|
|
76
|
+
session.locales?.setLocale(SOURCE_LOCALE);
|
|
77
77
|
}
|