Skip to content

Commit 1d1867f

Browse files
authored
feat: Add Jest mock + integration docs (#1269)
1 parent 079c767 commit 1d1867f

7 files changed

Lines changed: 193 additions & 52 deletions

File tree

.changeset/loud-adults-juggle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-native-async-storage/async-storage": patch
3+
---
4+
5+
include jest mock

docs/api/brownfield.md

Lines changed: 1 addition & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1 @@
1-
!!! info
2-
3-
Brownfield integration is supported on **Android**, **iOS**, and **macOS**.
4-
5-
`AsyncStorage` is built on a shared storage layer (`SharedStorage`) that can also be accessed directly from native
6-
code.
7-
This is especially useful in brownfield scenarios, where your app combines React Native and native code, allowing both
8-
layers to read from and write to the same storage consistently.
9-
10-
All platforms provide a thread-safe singleton registry called `StorageRegistry` to manage storage instances.
11-
12-
### Android
13-
14-
On Android, `StorageRegistry` is a public singleton, which is used to share `SharedStorage` instances with the native module.
15-
Multiple calls with the same name return the same singleton instance, ensuring consistent access.
16-
17-
```kotlin
18-
import android.content.Context
19-
import kotlinx.coroutines.runBlocking
20-
import org.asyncstorage.shared_storage.Entry
21-
import org.asyncstorage.shared_storage.SharedStorage
22-
import org.asyncstorage.storage.StorageRegistry
23-
24-
// access shared storage via StorageRegistry
25-
val storage: SharedStorage = StorageRegistry.getStorage(ctx, "my-users")
26-
27-
// runBlocking only for a demonstration
28-
runBlocking {
29-
storage.setValues(listOf(Entry("email", "john@example.com")))
30-
val values = storage.getValues(listOf("email"))
31-
println("Stored email: ${values.firstOrNull()?.value}")
32-
}
33-
```
34-
35-
### iOS / macOS
36-
37-
On iOS and macOS, the `StorageRegistry` singleton provides the same functionality in Swift and Objective-C.
38-
39-
```swift
40-
import AsyncStorage
41-
import SharedAsyncStorage
42-
43-
// access shared storage via StorageRegistry
44-
let storage: SharedStorage = StorageRegistry.shared.getStorage(dbName: "my-users")
45-
46-
Task {
47-
storage.setValues([Entry(key: "email", value: "john@example.com")])
48-
let values = storage.getValues(keys: ["email"])
49-
print("Stored email: \(values.first?.value ?? "none")")
50-
}
51-
```
1+
Documentation moved to [integrations/brownfield](../integrations/brownfield.md)

docs/integrations/brownfield.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
title: Brownfield integration
3+
---
4+
5+
# Brownfield integration
6+
7+
!!! info
8+
9+
Brownfield integration is supported on **Android**, **iOS**, and **macOS**.
10+
11+
`AsyncStorage` is built on a shared storage layer (`SharedStorage`) that can also be accessed directly from native
12+
code.
13+
This is especially useful in brownfield scenarios, where your app combines React Native and native code, allowing both
14+
layers to read from and write to the same storage consistently.
15+
16+
All platforms provide a thread-safe singleton registry called `StorageRegistry` to manage storage instances.
17+
18+
### Android
19+
20+
On Android, `StorageRegistry` is a public singleton, which is used to share `SharedStorage` instances with the native module.
21+
Multiple calls with the same name return the same singleton instance, ensuring consistent access.
22+
23+
```kotlin
24+
import android.content.Context
25+
import kotlinx.coroutines.runBlocking
26+
import org.asyncstorage.shared_storage.Entry
27+
import org.asyncstorage.shared_storage.SharedStorage
28+
import org.asyncstorage.storage.StorageRegistry
29+
30+
// access shared storage via StorageRegistry
31+
val storage: SharedStorage = StorageRegistry.getStorage(ctx, "my-users")
32+
33+
// runBlocking only for a demonstration
34+
runBlocking {
35+
storage.setValues(listOf(Entry("email", "john@example.com")))
36+
val values = storage.getValues(listOf("email"))
37+
println("Stored email: ${values.firstOrNull()?.value}")
38+
}
39+
```
40+
41+
### iOS / macOS
42+
43+
On iOS and macOS, the `StorageRegistry` singleton provides the same functionality in Swift and Objective-C.
44+
45+
```swift
46+
import AsyncStorage
47+
import SharedAsyncStorage
48+
49+
// access shared storage via StorageRegistry
50+
let storage: SharedStorage = StorageRegistry.shared.getStorage(dbName: "my-users")
51+
52+
Task {
53+
storage.setValues([Entry(key: "email", value: "john@example.com")])
54+
let values = storage.getValues(keys: ["email"])
55+
print("Stored email: \(values.first?.value ?? "none")")
56+
}
57+
```

docs/integrations/jest.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
title: Jest integration
3+
---
4+
5+
# Jest integration
6+
7+
AsyncStorage requires a native module that is not available in a Jest environment. The package ships a built-in
8+
in-memory mock at `@react-native-async-storage/async-storage/jest` that replaces the native implementation during
9+
tests.
10+
11+
## Setup
12+
13+
### Transform configuration
14+
15+
The package ships as ESM source, so Jest must be configured to transform it. Add it to the `transformIgnorePatterns` in
16+
your Jest config:
17+
18+
```js
19+
transformIgnorePatterns: [
20+
'node_modules/(?!@react-native-async-storage/)',
21+
],
22+
```
23+
24+
### Automatic mock file
25+
26+
Create a manual mock file that Jest will pick up automatically for every test:
27+
28+
```
29+
__mocks__/@react-native-async-storage/async-storage.js
30+
```
31+
32+
With content:
33+
34+
```js
35+
module.exports = require("@react-native-async-storage/async-storage/jest");
36+
```
37+
38+
Jest resolves files under `__mocks__/` automatically when the module is imported, so no additional configuration is
39+
needed.
40+
41+
### Inline mock
42+
43+
To mock the module for a specific test file, call `jest.mock` at the top of the file:
44+
45+
```js
46+
jest.mock("@react-native-async-storage/async-storage", () =>
47+
require("@react-native-async-storage/async-storage/jest")
48+
);
49+
```
50+
51+
## What the mock provides
52+
53+
The mock is a full in-memory implementation of the `AsyncStorage` interface. Additionally, it exports
54+
`clearAllMockStorages` to clear all in-memory storages.
55+
Example usage:
56+
57+
```ts
58+
import { clearAllMockStorages } from "@react-native-async-storage/async-storage/jest";
59+
60+
beforeEach(() => {
61+
clearAllMockStorages();
62+
});
63+
```

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ nav:
1212
- "Brownfield integration": api/brownfield.md
1313
- Integrations:
1414
- Expo: integrations/expo.md
15+
- Jest: integrations/jest.md
16+
- Brownfield: integrations/brownfield.md
1517
- "Migration to v3": migration-to-3.md
1618
- FAQ: faq.md
1719
- Contributing: contributing.md

packages/async-storage/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
"types": "./lib/typescript/index.d.ts",
1111
"default": "./lib/module/index.js"
1212
},
13-
"./package.json": "./package.json"
13+
"./package.json": "./package.json",
14+
"./jest": {
15+
"source": "./src/jest/AsyncStorageMock.ts",
16+
"types": "./lib/typescript/jest/AsyncStorageMock.d.ts",
17+
"default": "./lib/module/jest/AsyncStorageMock.js"
18+
}
1419
},
1520
"scripts": {
1621
"prepare": "yarn build",
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { AsyncStorage } from "@react-native-async-storage/async-storage";
2+
3+
class AsyncStorageMemoryImpl implements AsyncStorage {
4+
private store = new Map<string, string>();
5+
6+
getItem = async (key: string): Promise<string | null> => {
7+
return this.store.get(key) ?? null;
8+
};
9+
10+
setItem = async (key: string, value: string): Promise<void> => {
11+
this.store.set(key, value);
12+
};
13+
14+
removeItem = async (key: string): Promise<void> => {
15+
this.store.delete(key);
16+
};
17+
18+
getMany = async (keys: string[]): Promise<Record<string, string | null>> => {
19+
return keys.reduce<Record<string, string | null>>((result, key) => {
20+
result[key] = this.store.get(key) ?? null;
21+
return result;
22+
}, {});
23+
};
24+
25+
setMany = async (entries: Record<string, string>): Promise<void> => {
26+
for (const [key, value] of Object.entries(entries)) {
27+
this.store.set(key, value);
28+
}
29+
};
30+
31+
removeMany = async (keys: string[]): Promise<void> => {
32+
for (const key of keys) {
33+
this.store.delete(key);
34+
}
35+
};
36+
37+
getAllKeys = async (): Promise<string[]> => {
38+
return Array.from(this.store.keys());
39+
};
40+
41+
clear = async (): Promise<void> => {
42+
this.store.clear();
43+
};
44+
}
45+
46+
const inMemoryDbRegistry = new Map<string, AsyncStorageMemoryImpl>();
47+
48+
export function createAsyncStorage(databaseName: string): AsyncStorage {
49+
if (!inMemoryDbRegistry.has(databaseName)) {
50+
inMemoryDbRegistry.set(databaseName, new AsyncStorageMemoryImpl());
51+
}
52+
return inMemoryDbRegistry.get(databaseName)!;
53+
}
54+
55+
export function clearAllMockStorages(): void {
56+
inMemoryDbRegistry.clear();
57+
}
58+
59+
export default createAsyncStorage("legacy");

0 commit comments

Comments
 (0)