@forthtilliath/expo-test-kit 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,9 @@ Test helpers for Expo/Drizzle apps: a real in-memory SQLite database (via libsql
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install --save-dev @forthtilliath/expo-test-kit drizzle-orm @libsql/client
8
+ npm install --save-dev @forthtilliath/expo-test-kit
9
+ # only for the sqlite/ helpers:
10
+ npm install --save-dev drizzle-orm @libsql/client
9
11
  ```
10
12
 
11
13
  Or, from within this monorepo, as a workspace dependency:
@@ -18,7 +20,7 @@ Or, from within this monorepo, as a workspace dependency:
18
20
  }
19
21
  ```
20
22
 
21
- `drizzle-orm` and `@libsql/client` are peer dependencies — install them if not already present. Both are dev-only: `expo-test-kit` doesn't touch your app's production `expo-sqlite` driver, it's a separate in-memory database used only while testing.
23
+ `drizzle-orm` and `@libsql/client` are **optional** peer dependencies: only the `sqlite/` modules need them, so an app that only takes `router/createRenderApp` or the file-system fake doesn't install them. Both are dev-only: `expo-test-kit` doesn't touch your app's production `expo-sqlite` driver, it's a separate in-memory database used only while testing.
22
24
 
23
25
  ## Usage
24
26
 
@@ -77,7 +79,7 @@ beforeAll(async () => {
77
79
 
78
80
  ### `createFakeExpoFileSystem()` / `getFakeExpoFileSystem()`
79
81
 
80
- A minimal in-memory `File`/`Paths` fake — enough to test code that reads/writes files via `expo-file-system`, without the native filesystem (unavailable under Jest/Vitest).
82
+ A minimal in-memory `File`/`Directory`/`Paths` fake — enough to test code that reads/writes files via `expo-file-system`, without the native filesystem (unavailable under Jest/Vitest).
81
83
 
82
84
  ```ts
83
85
  import {
@@ -96,6 +98,10 @@ beforeAll(() => {
96
98
  });
97
99
  ```
98
100
 
101
+ - `File`: `exists`, `create()`, `write(content)`, `delete()`, `text()`, `textSync()`; `Directory`: `exists` (created, a `Paths` root, or holding a file), `create()`, `delete()` (with its files). `store` maps each URI to its content.
102
+ - `fakeFs.failWrites = true` simulates a full disk: `create()`/`write()` throw `"Fake disk full"` and change nothing. `fakeFs.reset()` (e.g. in `beforeEach`) empties the store and turns it back off.
103
+ - `Paths.document` / `Paths.cache` are `"document"` / `"cache"` by default; pass `{ documentUri: "file:///documents", cacheUri: "file:///cache" }` for code that checks the scheme. Parts are joined with a single `/`, as expo-file-system does.
104
+
99
105
  ### `createRenderApp(deps, options?)`
100
106
 
101
107
  Builds a `renderApp(initialUrl)` that renders the whole app — the real screens of `app/`, the real Expo Router navigation — from a URL, and returns `{ getPathname, getSearchParams, navigate }`. It settles the two traps of `renderRouter` under Testing Library v14: fake timers are advanced _before_ awaiting the render (`renderRouter` turns them on, and the first async render never ends otherwise), and a plain object is returned rather than the enriched promise. `navigate` runs inside `act` and waits for the render.
@@ -1,3 +1,6 @@
1
+ type PathPart = string | {
2
+ uri: string;
3
+ };
1
4
  export interface FakeFile {
2
5
  uri: string;
3
6
  exists: boolean;
@@ -5,21 +8,42 @@ export interface FakeFile {
5
8
  write: (content: string) => void;
6
9
  delete: () => void;
7
10
  text: () => Promise<string>;
11
+ textSync: () => string;
12
+ }
13
+ export interface FakeDirectory {
14
+ uri: string;
15
+ /** Created, one of the `Paths` roots, or holding a file. */
16
+ exists: boolean;
17
+ create: () => void;
18
+ /** Removes the directory and everything under it. */
19
+ delete: () => void;
8
20
  }
9
21
  export interface FakeExpoFileSystem {
10
22
  store: Map<string, string>;
11
- File: new (...parts: (string | {
12
- uri: string;
13
- })[]) => FakeFile;
23
+ File: new (...parts: PathPart[]) => FakeFile;
24
+ Directory: new (...parts: PathPart[]) => FakeDirectory;
14
25
  Paths: {
15
- cache: {
16
- uri: string;
17
- };
18
- document: {
19
- uri: string;
20
- };
26
+ cache: FakeDirectory;
27
+ document: FakeDirectory;
21
28
  };
29
+ /**
30
+ * Set to `true` to simulate a full disk: `File#create`/`write` then throw
31
+ * ("Fake disk full") and leave the store untouched.
32
+ */
33
+ failWrites: boolean;
34
+ /** Empties the store, forgets created directories, resets `failWrites`. */
35
+ reset: () => void;
36
+ }
37
+ export interface FakeExpoFileSystemOptions {
38
+ /**
39
+ * URI of `Paths.document`, e.g. `"file:///documents"` for code that checks
40
+ * the scheme. @defaultValue "document"
41
+ */
42
+ documentUri?: string;
43
+ /** URI of `Paths.cache`. @defaultValue "cache" */
44
+ cacheUri?: string;
22
45
  }
23
- export declare function createFakeExpoFileSystem(): FakeExpoFileSystem;
46
+ export declare function createFakeExpoFileSystem({ documentUri, cacheUri, }?: FakeExpoFileSystemOptions): FakeExpoFileSystem;
24
47
  export declare function getFakeExpoFileSystem(): FakeExpoFileSystem;
48
+ export {};
25
49
  //# sourceMappingURL=createFakeExpoFileSystem.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createFakeExpoFileSystem.d.ts","sourceRoot":"","sources":["../../src/file-system/createFakeExpoFileSystem.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,IAAI,EAAE,KAAK,GAAG,KAAK,EAAE,CAAC,MAAM,GAAG;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,KAAK,QAAQ,CAAC;IAC/D,KAAK,EAAE;QAAE,KAAK,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,QAAQ,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;CAC9D;AAWD,wBAAgB,wBAAwB,IAAI,kBAAkB,CAsC7D;AAED,wBAAgB,qBAAqB,IAAI,kBAAkB,CAS1D"}
1
+ {"version":3,"file":"createFakeExpoFileSystem.d.ts","sourceRoot":"","sources":["../../src/file-system/createFakeExpoFileSystem.ts"],"names":[],"mappings":"AAAA,KAAK,QAAQ,GAAG,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5B,QAAQ,EAAE,MAAM,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,4DAA4D;IAC5D,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,qDAAqD;IACrD,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,IAAI,EAAE,KAAK,GAAG,KAAK,EAAE,QAAQ,EAAE,KAAK,QAAQ,CAAC;IAC7C,SAAS,EAAE,KAAK,GAAG,KAAK,EAAE,QAAQ,EAAE,KAAK,aAAa,CAAC;IACvD,KAAK,EAAE;QAAE,KAAK,EAAE,aAAa,CAAC;QAAC,QAAQ,EAAE,aAAa,CAAA;KAAE,CAAC;IACzD;;;OAGG;IACH,UAAU,EAAE,OAAO,CAAC;IACpB,2EAA2E;IAC3E,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAmBD,wBAAgB,wBAAwB,CAAC,EACvC,WAAwB,EACxB,QAAkB,GACnB,GAAE,yBAA8B,GAAG,kBAAkB,CAoFrD;AAED,wBAAgB,qBAAqB,IAAI,kBAAkB,CAS1D"}
@@ -1,44 +1,98 @@
1
1
  const GLOBAL_KEY = "__forthtilliathFakeExpoFileSystem";
2
+ /** Joins like expo-file-system: one `/` between parts, none doubled. */
3
+ function joinUri(parts) {
4
+ return parts
5
+ .map((part) => (typeof part === "string" ? part : part.uri))
6
+ .reduce((uri, part) => (uri.endsWith("/") ? uri + part : `${uri}/${part}`));
7
+ }
2
8
  // Double léger d'expo-file-system : un simple magasin clé/valeur en mémoire,
3
9
  // suffisant pour tester du code lisant/écrivant des fichiers via `File`/
4
- // `Paths`, sans dépendre du système de fichiers natif (indisponible en
5
- // Jest/Vitest). Stashé sur `globalThis` plutôt que capturé par une closure,
6
- // car `jest.mock("expo-file-system", () => ...)` interdit de référencer une
7
- // variable hors-scope dans sa factory — `getFakeExpoFileSystem()` récupère
8
- // l'instance après coup, une fois le mock enregistré.
9
- export function createFakeExpoFileSystem() {
10
+ // `Directory`/`Paths`, sans dépendre du système de fichiers natif
11
+ // (indisponible en Jest/Vitest). Stashé sur `globalThis` plutôt que capturé
12
+ // par une closure, car `jest.mock("expo-file-system", () => ...)` interdit de
13
+ // référencer une variable hors-scope dans sa factory —
14
+ // `getFakeExpoFileSystem()` récupère l'instance après coup, une fois le mock
15
+ // enregistré.
16
+ export function createFakeExpoFileSystem({ documentUri = "document", cacheUri = "cache", } = {}) {
10
17
  const store = new Map();
18
+ const directories = new Set();
19
+ const roots = new Set([documentUri, cacheUri]);
20
+ function assertWritable() {
21
+ if (fakeFs.failWrites)
22
+ throw new Error("Fake disk full");
23
+ }
11
24
  class File {
12
25
  uri;
13
26
  constructor(...parts) {
14
- this.uri = parts
15
- .map((p) => (typeof p === "string" ? p : p.uri))
16
- .join("/");
27
+ this.uri = joinUri(parts);
17
28
  }
18
29
  get exists() {
19
30
  return store.has(this.uri);
20
31
  }
21
32
  create() {
33
+ assertWritable();
22
34
  store.set(this.uri, "");
23
35
  }
24
36
  write(content) {
37
+ assertWritable();
25
38
  store.set(this.uri, content);
26
39
  }
27
40
  delete() {
28
41
  store.delete(this.uri);
29
42
  }
30
43
  text() {
44
+ return Promise.resolve().then(() => this.textSync());
45
+ }
46
+ textSync() {
31
47
  const content = store.get(this.uri);
32
48
  if (content === undefined) {
33
- return Promise.reject(new Error(`Fake file not found: ${this.uri}`));
49
+ throw new Error(`Fake file not found: ${this.uri}`);
50
+ }
51
+ return content;
52
+ }
53
+ }
54
+ class Directory {
55
+ uri;
56
+ constructor(...parts) {
57
+ this.uri = joinUri(parts);
58
+ }
59
+ get prefix() {
60
+ return this.uri.endsWith("/") ? this.uri : `${this.uri}/`;
61
+ }
62
+ get exists() {
63
+ if (roots.has(this.uri) || directories.has(this.uri))
64
+ return true;
65
+ return [...store.keys()].some((uri) => uri.startsWith(this.prefix));
66
+ }
67
+ create() {
68
+ directories.add(this.uri);
69
+ }
70
+ delete() {
71
+ for (const uri of [...store.keys()]) {
72
+ if (uri.startsWith(this.prefix))
73
+ store.delete(uri);
74
+ }
75
+ for (const uri of [...directories]) {
76
+ if (uri === this.uri || uri.startsWith(this.prefix)) {
77
+ directories.delete(uri);
78
+ }
34
79
  }
35
- return Promise.resolve(content);
36
80
  }
37
81
  }
38
82
  const fakeFs = {
39
83
  store,
40
84
  File,
41
- Paths: { cache: { uri: "cache" }, document: { uri: "document" } },
85
+ Directory,
86
+ Paths: {
87
+ cache: new Directory(cacheUri),
88
+ document: new Directory(documentUri),
89
+ },
90
+ failWrites: false,
91
+ reset() {
92
+ store.clear();
93
+ directories.clear();
94
+ fakeFs.failWrites = false;
95
+ },
42
96
  };
43
97
  globalThis[GLOBAL_KEY] = fakeFs;
44
98
  return fakeFs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forthtilliath/expo-test-kit",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -23,15 +23,23 @@
23
23
  "@libsql/client": ">=0.14.0",
24
24
  "drizzle-orm": ">=0.44.0"
25
25
  },
26
+ "peerDependenciesMeta": {
27
+ "@libsql/client": {
28
+ "optional": true
29
+ },
30
+ "drizzle-orm": {
31
+ "optional": true
32
+ }
33
+ },
26
34
  "devDependencies": {
27
35
  "@libsql/client": "^0.17.4",
28
36
  "@types/node": "^26.2.0",
29
37
  "drizzle-orm": "^0.45.2",
30
38
  "eslint": "10.7.0",
31
39
  "typescript": "^5.9.3",
32
- "vitest": "^5.0.2",
33
- "@forthtilliath/eslint-config": "0.9.0",
34
- "@forthtilliath/typescript-config": "0.1.1"
40
+ "vitest": "^5.0.5",
41
+ "@forthtilliath/typescript-config": "0.1.1",
42
+ "@forthtilliath/eslint-config": "0.10.0"
35
43
  },
36
44
  "scripts": {
37
45
  "lint": "eslint . --max-warnings 0",