nalloc 0.4.0 → 0.5.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/README.md +300 -180
- package/build/codemod-cli.cjs +153 -0
- package/build/codemod-cli.cjs.map +1 -0
- package/build/codemod-cli.d.ts +2 -0
- package/build/codemod-cli.js +103 -0
- package/build/codemod-cli.js.map +1 -0
- package/build/codemod.cjs +652 -0
- package/build/codemod.cjs.map +1 -0
- package/build/codemod.d.ts +29 -0
- package/build/codemod.js +634 -0
- package/build/codemod.js.map +1 -0
- package/build/eslint.cjs +221 -0
- package/build/eslint.cjs.map +1 -0
- package/build/eslint.d.ts +36 -0
- package/build/eslint.js +198 -0
- package/build/eslint.js.map +1 -0
- package/build/http.cjs +31 -0
- package/build/http.cjs.map +1 -0
- package/build/http.d.ts +31 -0
- package/build/http.js +13 -0
- package/build/http.js.map +1 -0
- package/build/result.cjs +0 -11
- package/build/result.cjs.map +1 -1
- package/build/result.d.ts +0 -32
- package/build/result.js +0 -8
- package/build/result.js.map +1 -1
- package/build/safe.cjs +4 -0
- package/build/safe.cjs.map +1 -1
- package/build/safe.d.ts +1 -0
- package/build/safe.js +1 -0
- package/build/safe.js.map +1 -1
- package/build/schema.cjs +32 -0
- package/build/schema.cjs.map +1 -0
- package/build/schema.d.ts +44 -0
- package/build/schema.js +14 -0
- package/build/schema.js.map +1 -0
- package/package.json +55 -6
- package/src/__tests__/codemod.ts +211 -0
- package/src/__tests__/eslint.ts +99 -0
- package/src/__tests__/fixtures/tsconfig.json +10 -0
- package/src/__tests__/http.ts +64 -0
- package/src/__tests__/result.ts +74 -125
- package/src/__tests__/result.types.ts +2 -0
- package/src/__tests__/schema.ts +58 -0
- package/src/codemod-cli.ts +108 -0
- package/src/codemod.ts +623 -0
- package/src/eslint.ts +145 -0
- package/src/http.ts +42 -0
- package/src/result.ts +0 -37
- package/src/safe.ts +1 -0
- package/src/schema.ts +52 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { migrateSource, renderReport } from '../codemod.js';
|
|
3
|
+
|
|
4
|
+
function migrate(source: string): ReturnType<typeof migrateSource> {
|
|
5
|
+
return migrateSource(source, 'fixture.ts');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe('codemod', () => {
|
|
9
|
+
it('leaves files without neverthrow imports untouched', () => {
|
|
10
|
+
const source = `const xs = [1, 2].map(x => x * 2);\n`;
|
|
11
|
+
const result = migrate(source);
|
|
12
|
+
expect(result.changed).toBe(false);
|
|
13
|
+
expect(result.output).toBe(source);
|
|
14
|
+
expect(result.skipped).toEqual([]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('rewrites the import and converts provenant single-method calls', () => {
|
|
18
|
+
const source = [
|
|
19
|
+
`import { ok, err } from 'neverthrow';`,
|
|
20
|
+
`const r = ok(1);`,
|
|
21
|
+
`const d = r.map(x => x + 1);`,
|
|
22
|
+
`const e = d.andThen(x => x > 0 ? ok(x) : err('neg'));`,
|
|
23
|
+
].join('\n');
|
|
24
|
+
const result = migrate(source);
|
|
25
|
+
expect(result.changed).toBe(true);
|
|
26
|
+
expect(result.output).toContain(`import { ok, err, Result } from 'nalloc';`);
|
|
27
|
+
expect(result.output).toContain(`const d = Result.map(r, x => x + 1);`);
|
|
28
|
+
expect(result.output).toContain(`const e = Result.flatMap(d, x => x > 0 ? ok(x) : err('neg'));`);
|
|
29
|
+
expect(result.output).not.toContain('neverthrow');
|
|
30
|
+
expect(result.skipped).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('preserves import aliases', () => {
|
|
34
|
+
const source = [`import { ok as okay } from 'neverthrow';`, `const r = okay(1);`, `const d = r.map(x => x + 1);`].join('\n');
|
|
35
|
+
const result = migrate(source);
|
|
36
|
+
expect(result.output).toContain(`import { ok as okay, Result } from 'nalloc';`);
|
|
37
|
+
expect(result.output).toContain(`Result.map(r, x => x + 1)`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('folds chains of two or more methods into pipe', () => {
|
|
41
|
+
const source = [`import { ok } from 'neverthrow';`, `const out = ok(1).map(a => a + 1).mapErr(String).unwrapOr(0);`].join('\n');
|
|
42
|
+
const result = migrate(source);
|
|
43
|
+
expect(result.output).toContain(`import { ok, Result, pipe } from 'nalloc';`);
|
|
44
|
+
expect(result.output).toContain(
|
|
45
|
+
`const out = pipe(ok(1), ($r) => Result.map($r, a => a + 1), ($r) => Result.mapErr($r, String), ($r) => Result.unwrapOr($r, 0));`,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('does not touch bare .map calls without provenance', () => {
|
|
50
|
+
const source = [
|
|
51
|
+
`import { ok } from 'neverthrow';`,
|
|
52
|
+
`const xs = [1, 2].map(x => x * 2);`,
|
|
53
|
+
`declare const unknownValue: { map(fn: (x: number) => number): number[] };`,
|
|
54
|
+
`unknownValue.map(x => x);`,
|
|
55
|
+
].join('\n');
|
|
56
|
+
const result = migrate(source);
|
|
57
|
+
expect(result.output).toContain(`[1, 2].map(x => x * 2)`);
|
|
58
|
+
expect(result.output).toContain(`unknownValue.map(x => x)`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('converts neverthrow-exclusive methods without provenance', () => {
|
|
62
|
+
const source = [`import { ok } from 'neverthrow';`, `declare const foreign: any;`, `const r = foreign.andThen((x: number) => ok(x));`].join('\n');
|
|
63
|
+
const result = migrate(source);
|
|
64
|
+
expect(result.output).toContain(`Result.flatMap(foreign, (x: number) => ok(x))`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('uses type annotations from neverthrow imports as provenance and renames the Result type', () => {
|
|
68
|
+
const source = [
|
|
69
|
+
`import { ok, Result } from 'neverthrow';`,
|
|
70
|
+
`const t: Result<number, string> = ok(2);`,
|
|
71
|
+
`const u = t.map(x => x * 2);`,
|
|
72
|
+
`function f(input: Result<number, string>): Result<number, string> { return input.mapErr(e => e); }`,
|
|
73
|
+
].join('\n');
|
|
74
|
+
const result = migrate(source);
|
|
75
|
+
expect(result.output).toContain(`import { ok, Result, type ResultType } from 'nalloc';`);
|
|
76
|
+
expect(result.output).toContain(`const t: ResultType<number, string> = ok(2);`);
|
|
77
|
+
expect(result.output).toContain(`Result.map(t, x => x * 2)`);
|
|
78
|
+
expect(result.output).toContain(`function f(input: ResultType<number, string>): ResultType<number, string> { return Result.mapErr(input, e => e); }`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('converts fromThrowable to Result.wrap and tracks wrapped functions as provenance', () => {
|
|
82
|
+
const source = [
|
|
83
|
+
`import { fromThrowable } from 'neverthrow';`,
|
|
84
|
+
`const safeParse = fromThrowable(JSON.parse);`,
|
|
85
|
+
`const p = safeParse('{}');`,
|
|
86
|
+
`const q = p.map(v => v);`,
|
|
87
|
+
].join('\n');
|
|
88
|
+
const result = migrate(source);
|
|
89
|
+
expect(result.output).toContain(`const safeParse = Result.wrap(JSON.parse);`);
|
|
90
|
+
expect(result.output).toContain(`Result.map(p, v => v)`);
|
|
91
|
+
expect(result.output).not.toContain('neverthrow');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('converts combine and combineWithAllErrors including Result statics', () => {
|
|
95
|
+
const source = [
|
|
96
|
+
`import { ok, combine, Result } from 'neverthrow';`,
|
|
97
|
+
`const a = combine([ok(1), ok(2)]);`,
|
|
98
|
+
`const b = Result.combineWithAllErrors([ok(1)]);`,
|
|
99
|
+
`const c = Result.fromThrowable(JSON.parse);`,
|
|
100
|
+
].join('\n');
|
|
101
|
+
const result = migrate(source);
|
|
102
|
+
expect(result.output).toContain(`const a = Result.all([ok(1), ok(2)]);`);
|
|
103
|
+
expect(result.output).toContain(`const b = Result.collectAll([ok(1)]);`);
|
|
104
|
+
expect(result.output).toContain(`const c = Result.wrap(JSON.parse);`);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('converts directly-awaited fromPromise and treats its result as migrated', () => {
|
|
108
|
+
const source = [
|
|
109
|
+
`import { fromPromise } from 'neverthrow';`,
|
|
110
|
+
`async function run(p: Promise<number>) {`,
|
|
111
|
+
` const r = await fromPromise(p, e => String(e));`,
|
|
112
|
+
` return r.map(x => x + 1);`,
|
|
113
|
+
`}`,
|
|
114
|
+
].join('\n');
|
|
115
|
+
const result = migrate(source);
|
|
116
|
+
expect(result.output).toContain(`await Result.fromPromise(p, e => String(e))`);
|
|
117
|
+
expect(result.output).toContain(`Result.map(r, x => x + 1)`);
|
|
118
|
+
expect(result.skipped).toEqual([]);
|
|
119
|
+
expect(result.output).not.toContain('neverthrow');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('reports chained fromPromise as result-async and keeps the neverthrow import', () => {
|
|
123
|
+
const source = [`import { fromPromise } from 'neverthrow';`, `const ra = fromPromise(Promise.resolve(1), e => e).map(x => x);`].join('\n');
|
|
124
|
+
const result = migrate(source);
|
|
125
|
+
expect(result.skipped.map((s) => s.reason)).toContain('result-async');
|
|
126
|
+
expect(result.output).toContain(`import { fromPromise } from 'neverthrow';`);
|
|
127
|
+
expect(result.output).toContain(`fromPromise(Promise.resolve(1), e => e).map(x => x)`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('reports ResultAsync values and never converts methods on them', () => {
|
|
131
|
+
const source = [
|
|
132
|
+
`import { okAsync } from 'neverthrow';`,
|
|
133
|
+
`const ra = okAsync(1);`,
|
|
134
|
+
`const rb = ra.map(x => x + 1);`,
|
|
135
|
+
`async function f() { const r = await ra; return r.mapErr(e => e); }`,
|
|
136
|
+
].join('\n');
|
|
137
|
+
const result = migrate(source);
|
|
138
|
+
expect(result.output).toContain(`const rb = ra.map(x => x + 1);`);
|
|
139
|
+
expect(result.output).toContain(`return r.mapErr(e => e);`);
|
|
140
|
+
expect(result.output).toContain(`import { okAsync } from 'neverthrow';`);
|
|
141
|
+
expect(result.skipped.some((s) => s.reason === 'result-async')).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('reports safeTry for manual gen migration', () => {
|
|
145
|
+
const source = [`import { ok, safeTry } from 'neverthrow';`, `const r = safeTry(function* () { return ok(1); });`].join('\n');
|
|
146
|
+
const result = migrate(source);
|
|
147
|
+
expect(result.skipped.map((s) => s.reason)).toContain('safe-try');
|
|
148
|
+
expect(result.output).toContain(`import { safeTry } from 'neverthrow';`);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('refuses whole chains containing unsupported methods', () => {
|
|
152
|
+
const source = [`import { ok } from 'neverthrow';`, `const r = ok(1).andThrough(x => ok(x)).map(x => x + 1);`].join('\n');
|
|
153
|
+
const result = migrate(source);
|
|
154
|
+
expect(result.skipped.map((s) => s.reason)).toContain('unsupported');
|
|
155
|
+
expect(result.output).toContain(`ok(1).andThrough(x => ok(x)).map(x => x + 1)`);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('skips namespace imports entirely', () => {
|
|
159
|
+
const source = [`import * as nt from 'neverthrow';`, `const r = nt.ok(1);`].join('\n');
|
|
160
|
+
const result = migrate(source);
|
|
161
|
+
expect(result.changed).toBe(false);
|
|
162
|
+
expect(result.skipped.map((s) => s.reason)).toEqual(['namespace-import']);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('converts nested chains inside chain arguments', () => {
|
|
166
|
+
const source = [`import { ok, err } from 'neverthrow';`, `const inner = ok(2);`, `const r = ok(1).andThen(x => inner.mapErr(e => e)).map(x => x);`].join(
|
|
167
|
+
'\n',
|
|
168
|
+
);
|
|
169
|
+
const result = migrate(source);
|
|
170
|
+
expect(result.output).toContain(`pipe(ok(1), ($r) => Result.flatMap($r, x => Result.mapErr(inner, e => e)), ($r) => Result.map($r, x => x))`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('converts match and unwrap accessors', () => {
|
|
174
|
+
const source = [
|
|
175
|
+
`import { ok } from 'neverthrow';`,
|
|
176
|
+
`const r = ok(1);`,
|
|
177
|
+
`const m = r.match(x => x, e => 0);`,
|
|
178
|
+
`const v = r._unsafeUnwrap();`,
|
|
179
|
+
`const o = r.isOk();`,
|
|
180
|
+
].join('\n');
|
|
181
|
+
const result = migrate(source);
|
|
182
|
+
expect(result.output).toContain(`const m = Result.match(r, x => x, e => 0);`);
|
|
183
|
+
expect(result.output).toContain(`const v = Result.unwrap(r);`);
|
|
184
|
+
expect(result.output).toContain(`const o = Result.isOk(r);`);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('drops provenance for conflicting bindings', () => {
|
|
188
|
+
const source = [
|
|
189
|
+
`import { ok } from 'neverthrow';`,
|
|
190
|
+
`function a() { const v = ok(1); return v; }`,
|
|
191
|
+
`function b() { const v = [1, 2]; return v.map(x => x); }`,
|
|
192
|
+
].join('\n');
|
|
193
|
+
const result = migrate(source);
|
|
194
|
+
expect(result.output).toContain(`return v.map(x => x);`);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('renders a grouped markdown report', () => {
|
|
198
|
+
const report = renderReport({
|
|
199
|
+
filesChanged: 1,
|
|
200
|
+
converted: 3,
|
|
201
|
+
skipped: [
|
|
202
|
+
{ file: 'a.ts', line: 2, reason: 'result-async', text: 'okAsync(1)' },
|
|
203
|
+
{ file: 'a.ts', line: 5, reason: 'safe-try', text: 'safeTry(fn)' },
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
expect(report).toContain('# nalloc migration report');
|
|
207
|
+
expect(report).toContain('Files changed: 1. Sites converted: 3. Sites needing manual review: 2.');
|
|
208
|
+
expect(report).toContain('## a.ts');
|
|
209
|
+
expect(report).toContain('- line 2 [result-async]: `okAsync(1)`');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
4
|
+
import plugin, { rules, mustUse, noUnwrap } from '../eslint.js';
|
|
5
|
+
|
|
6
|
+
describe('eslint plugin', () => {
|
|
7
|
+
it('exposes both rules with create + meta', () => {
|
|
8
|
+
for (const name of ['must-use', 'no-unwrap'] as const) {
|
|
9
|
+
expect(typeof rules[name].create).toBe('function');
|
|
10
|
+
expect(rules[name].meta.messages).toBeTypeOf('object');
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('wires the recommended config', () => {
|
|
15
|
+
expect(plugin.meta.name).toBe('nalloc');
|
|
16
|
+
const recommended = plugin.configs.recommended as { plugins: Record<string, unknown>; rules: Record<string, string> };
|
|
17
|
+
expect(recommended.plugins.nalloc).toBe(plugin);
|
|
18
|
+
expect(recommended.rules['nalloc/must-use']).toBe('error');
|
|
19
|
+
expect(recommended.rules['nalloc/no-unwrap']).toBe('error');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const typedTester = new RuleTester({
|
|
24
|
+
languageOptions: {
|
|
25
|
+
parserOptions: {
|
|
26
|
+
projectService: { allowDefaultProject: ['*.ts*'] },
|
|
27
|
+
tsconfigRootDir: path.join(import.meta.dirname, 'fixtures'),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
typedTester.run('must-use', mustUse, {
|
|
33
|
+
valid: [
|
|
34
|
+
`type Result<T, E> = readonly [T] | { error: E }; declare function f(): Result<number, string>; const r = f();`,
|
|
35
|
+
`declare function g(): number; g();`,
|
|
36
|
+
`type Foo<T> = readonly [T]; declare function f(): Foo<number>; f();`,
|
|
37
|
+
{
|
|
38
|
+
code: `type Result<T, E> = readonly [T] | { error: E }; declare function f(): Result<number, string>; f();`,
|
|
39
|
+
options: [{ typeNames: ['Option'] }],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
invalid: [
|
|
43
|
+
{
|
|
44
|
+
code: `type Result<T, E> = readonly [T] | { error: E }; declare function f(): Result<number, string>; f();`,
|
|
45
|
+
errors: [{ messageId: 'mustUse', data: { name: 'Result' } }],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
code: `type Option<T> = T | undefined; declare function f(): Option<number>; f();`,
|
|
49
|
+
errors: [{ messageId: 'mustUse', data: { name: 'Option' } }],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
code: `type Result<T, E> = readonly [T] | { error: E }; declare function f(): Promise<Result<number, string>>; async function run() { await f(); }`,
|
|
53
|
+
errors: [{ messageId: 'mustUse', data: { name: 'Result' } }],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
code: `type Mine<T> = readonly [T]; declare function f(): Mine<number>; f();`,
|
|
57
|
+
options: [{ typeNames: ['Mine'] }],
|
|
58
|
+
errors: [{ messageId: 'mustUse', data: { name: 'Mine' } }],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const tester = new RuleTester({ languageOptions: { parserOptions: { sourceType: 'module' } } });
|
|
64
|
+
|
|
65
|
+
tester.run('no-unwrap', noUnwrap, {
|
|
66
|
+
valid: [
|
|
67
|
+
`import { unwrap } from 'other-lib'; declare const r: unknown; unwrap(r);`,
|
|
68
|
+
`import { Result } from 'nalloc'; declare const r: unknown; Result.unwrapOr(r, 0);`,
|
|
69
|
+
`const Result = { unwrap(x: unknown) { return x; } }; Result.unwrap(1);`,
|
|
70
|
+
`function unwrap(x: unknown) { return x; } unwrap(1);`,
|
|
71
|
+
],
|
|
72
|
+
invalid: [
|
|
73
|
+
{
|
|
74
|
+
code: `import { unwrap } from 'nalloc'; declare const r: unknown; unwrap(r);`,
|
|
75
|
+
errors: [{ messageId: 'noUnwrap', data: { name: 'unwrap' } }],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
code: `import { Result } from 'nalloc'; declare const r: unknown; Result.unwrap(r);`,
|
|
79
|
+
errors: [{ messageId: 'noUnwrap', data: { name: 'unwrap' } }],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
code: `import { Option } from 'nalloc'; declare const o: unknown; Option.expect(o, 'm');`,
|
|
83
|
+
errors: [{ messageId: 'noUnwrap', data: { name: 'expect' } }],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
code: `import { Result } from 'nalloc'; declare const r: unknown; Result.unwrapErr(r);`,
|
|
87
|
+
errors: [{ messageId: 'noUnwrap', data: { name: 'unwrapErr' } }],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
code: `import { unwrap as uw } from 'nalloc'; declare const r: unknown; uw(r);`,
|
|
91
|
+
errors: [{ messageId: 'noUnwrap', data: { name: 'unwrap' } }],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
code: `import { unwrap } from '@me/x'; declare const r: unknown; unwrap(r);`,
|
|
95
|
+
options: [{ modules: ['@me/x'] }],
|
|
96
|
+
errors: [{ messageId: 'noUnwrap', data: { name: 'unwrap' } }],
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { fromResponse, fromFetch } from '../http.js';
|
|
3
|
+
import { isOk, isErr } from '../types.js';
|
|
4
|
+
|
|
5
|
+
describe('http', () => {
|
|
6
|
+
describe('fromResponse', () => {
|
|
7
|
+
it('returns Ok carrying the response for a 2xx status', () => {
|
|
8
|
+
const response = new Response('ok', { status: 200 });
|
|
9
|
+
const result = fromResponse(response);
|
|
10
|
+
expect(isOk(result)).toBe(true);
|
|
11
|
+
expect(result).toBe(response);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('returns Err carrying the response for a non-2xx status', () => {
|
|
15
|
+
const response = new Response('nope', { status: 404 });
|
|
16
|
+
const result = fromResponse(response);
|
|
17
|
+
expect(isErr(result)).toBe(true);
|
|
18
|
+
expect((result as { error: Response }).error).toBe(response);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('fromFetch', () => {
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.unstubAllGlobals();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns Ok carrying the response for a 2xx status', async () => {
|
|
28
|
+
const response = new Response('ok', { status: 200 });
|
|
29
|
+
const fetchMock = vi.fn().mockResolvedValue(response);
|
|
30
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
31
|
+
const init = { method: 'POST' };
|
|
32
|
+
const result = await fromFetch('https://example.test', init);
|
|
33
|
+
expect(isOk(result)).toBe(true);
|
|
34
|
+
expect(result).toBe(response);
|
|
35
|
+
expect(fetchMock).toHaveBeenCalledWith('https://example.test', init);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns Err carrying the response for a non-2xx status', async () => {
|
|
39
|
+
const response = new Response('nope', { status: 500 });
|
|
40
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(response));
|
|
41
|
+
const result = await fromFetch('https://example.test');
|
|
42
|
+
expect(isErr(result)).toBe(true);
|
|
43
|
+
expect((result as { error: Response }).error).toBe(response);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns Err carrying the thrown value on transport failure', async () => {
|
|
47
|
+
const failure = new TypeError('fetch failed');
|
|
48
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(failure));
|
|
49
|
+
const result = await fromFetch('https://example.test');
|
|
50
|
+
expect(isErr(result)).toBe(true);
|
|
51
|
+
expect((result as { error: unknown }).error).toBe(failure);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns Err with an AbortError DOMException for a pre-aborted signal', async () => {
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
controller.abort();
|
|
57
|
+
const result = await fromFetch('http://127.0.0.1:1', { signal: controller.signal });
|
|
58
|
+
expect(isErr(result)).toBe(true);
|
|
59
|
+
const error = (result as { error: unknown }).error;
|
|
60
|
+
expect(error).toBeInstanceOf(DOMException);
|
|
61
|
+
expect((error as DOMException).name).toBe('AbortError');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|