@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.
@@ -1,7 +1,8 @@
1
1
  import type {
2
- NativePluginBridge,
3
- NativePluginCapability,
4
- NativePluginPlatform,
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<NativePluginBridge>;
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: NativePluginPlatform;
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: NativePluginCapability[];
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
- const PLATFORMS: readonly NativePluginPlatform[] = ['ios', 'android'];
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 NativePluginUnavailableError extends Error {
60
+ class NativeBridgeUnavailableError extends Error {
54
61
  readonly code = 'NATIVE_PLUGIN_UNAVAILABLE';
55
62
  }
56
63
 
57
- class NativePluginCapabilityError extends Error {
64
+ class NativeBridgeCapabilityError extends Error {
58
65
  readonly code = 'NATIVE_PLUGIN_CAPABILITY_DENIED';
59
- readonly capability: NativePluginCapability;
66
+ readonly capability: NativeBridgeCapability;
60
67
 
61
- constructor(capability: NativePluginCapability) {
62
- super(`Native plugin capability is not declared: ${capability}`);
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 NativePluginUnsupportedPlatformError extends Error {
74
+ class NativeBridgeUnsupportedPlatformError extends Error {
68
75
  readonly code = 'NATIVE_PLUGIN_PLATFORM_EXCLUDED';
69
- readonly platforms: readonly NativePluginPlatform[];
76
+ readonly platforms: readonly NativeBridgePlatform[];
70
77
 
71
- constructor(platforms: readonly NativePluginPlatform[], platform: string) {
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 NativePluginVersionError extends Error {
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 plugin API ${requiredApi} is required, but ${installedApi} is installed.`);
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 NativePluginDisposedError extends Error {
96
+ class NativeBridgeDisposedError extends Error {
90
97
  readonly code = 'NATIVE_PLUGIN_SCOPE_DISPOSED';
91
98
 
92
99
  constructor() {
93
- super('The native plugin scope has been disposed.');
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 = { platform: 'ios', modules: new Map(), capabilities: [] };
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<NativePluginPlatform, Handler>>): Handler {
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 NativePluginUnsupportedPlatformError(
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<NativePluginPlatform, Handler>>,
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
- NativePluginUnavailableError,
142
- NativePluginCapabilityError,
143
- NativePluginUnsupportedPlatformError,
144
- NativePluginVersionError,
145
- NativePluginDisposedError,
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: NativePluginCapability) => state.capabilities.includes(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: NativePluginBridge = {
189
+ const bridge: NativeBridge = {
176
190
  apiVersion: '1.0.0',
177
191
  abiVersion: '1',
178
192
  capabilities,
@@ -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 entities = new Map<PropertyKey, AddonHandle>();
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
- entities.set(id, handle);
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
- entities.delete(id);
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
- entities,
158
+ addons,
159
159
  errors,
160
160
  startDurations,
161
161
  get: (id: string) => handles.get(id),
@@ -226,7 +226,6 @@ const identity = (value: number) => value;
226
226
 
227
227
  /**
228
228
  * @description Passes an easing curve through, since the fake draws no frames.
229
- * @param curve The curve.
230
229
  * @returns The same curve.
231
230
  */
232
231
  function keep(curve: Curve): Curve {
@@ -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
- // These mirror the client's `utils` line for line: the testing kit ships without the client's private packages.
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
  }
@@ -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';
@@ -4,7 +4,8 @@ import {
4
4
  type IcuNode,
5
5
  type IcuPluralNode,
6
6
  type IcuSelectNode,
7
- } from './icu-parse';
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(message: string, vars: IcuVars = {}, locale: string = 'en-US'): string {
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 * from './install-addon-globals';
19
- export * from './fake-native-bridge';
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)) {
@@ -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, 'i18n');
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, 'assets');
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
- return name.endsWith('@2x') || name.endsWith('@3x') ? name.slice(0, -'@2x'.length) : name;
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 { createFakeLocales, SOURCE_LOCALE } from './fake-locales';
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 namespaces that serve nested modules, as `unbound.metro.components` is `@unbound-app/api/metro/components`.
36
- const NESTED = new Map([
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
- const IMAGE = /\.(png|jpe?g|webp|gif)$/i;
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.get(name)?.includes(nested)) {
179
+ if (NESTED.has(`${name}/${nested}`)) {
179
180
  mock.module(`@unbound-app/api/${name}/${nested}`, () => members);
180
181
  }
181
182
  }
@@ -1,4 +1,8 @@
1
- import type { NativePluginCapability, NativePluginPlatform } from '../src/native';
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 = { register: (module: FakeModule, path?: string) => void };
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: NativePluginPlatform) => void;
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: NativePluginCapability[]) => void;
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
  },
@@ -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('en-US');
76
+ session.locales?.setLocale(SOURCE_LOCALE);
77
77
  }