@aaronshaf/plane 0.1.3 → 0.1.5
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 +1 -1
- package/scripts/check-coverage.ts +2 -2
- package/src/api.ts +67 -59
- package/src/app.ts +78 -0
- package/src/bin.ts +6 -71
- package/src/commands/cycles.ts +104 -85
- package/src/commands/init.ts +57 -55
- package/src/commands/intake.ts +82 -65
- package/src/commands/issue.ts +418 -314
- package/src/commands/issues.ts +51 -43
- package/src/commands/labels.ts +52 -43
- package/src/commands/members.ts +25 -19
- package/src/commands/modules.ts +136 -99
- package/src/commands/pages.ts +58 -49
- package/src/commands/projects.ts +28 -22
- package/src/commands/states.ts +31 -25
- package/src/config.ts +152 -154
- package/src/format.ts +15 -8
- package/src/output.ts +28 -28
- package/src/resolve.ts +66 -53
- package/tests/api.test.ts +178 -155
- package/tests/cycles-extended.test.ts +205 -162
- package/tests/format.test.ts +72 -54
- package/tests/helpers/mock-api.ts +16 -14
- package/tests/intake.test.ts +173 -139
- package/tests/issue-activity.test.ts +191 -158
- package/tests/issue-commands.test.ts +587 -304
- package/tests/issue-comments-worklogs.test.ts +337 -265
- package/tests/issue-links.test.ts +229 -193
- package/tests/modules.test.ts +283 -239
- package/tests/new-schemas.test.ts +203 -183
- package/tests/new-schemas2.test.ts +195 -183
- package/tests/output.test.ts +66 -64
- package/tests/pages.test.ts +122 -108
- package/tests/resolve.test.ts +186 -156
- package/tests/schemas.test.ts +215 -177
package/src/output.ts
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
const jsonIdx = process.argv.indexOf("--json")
|
|
2
|
-
const xmlIdx = process.argv.indexOf("--xml")
|
|
1
|
+
const jsonIdx = process.argv.indexOf("--json");
|
|
2
|
+
const xmlIdx = process.argv.indexOf("--xml");
|
|
3
3
|
|
|
4
|
-
export const jsonMode = jsonIdx !== -1
|
|
5
|
-
export const xmlMode = xmlIdx !== -1
|
|
4
|
+
export const jsonMode = jsonIdx !== -1;
|
|
5
|
+
export const xmlMode = xmlIdx !== -1;
|
|
6
6
|
|
|
7
|
-
if (jsonIdx !== -1) process.argv.splice(jsonIdx, 1)
|
|
8
|
-
if (xmlIdx !== -1) process.argv.splice(xmlIdx, 1)
|
|
7
|
+
if (jsonIdx !== -1) process.argv.splice(jsonIdx, 1);
|
|
8
|
+
if (xmlIdx !== -1) process.argv.splice(xmlIdx, 1);
|
|
9
9
|
|
|
10
10
|
function escapeXml(val: unknown): string {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
return String(val ?? "")
|
|
12
|
+
.replace(/&/g, "&")
|
|
13
|
+
.replace(/</g, "<")
|
|
14
|
+
.replace(/>/g, ">")
|
|
15
|
+
.replace(/"/g, """);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function toXmlItem(obj: unknown, tag = "item"): string {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
if (obj === null || typeof obj !== "object") {
|
|
20
|
+
return `<${tag}>${escapeXml(obj)}</${tag}>`;
|
|
21
|
+
}
|
|
22
|
+
const attrs = Object.entries(obj as Record<string, unknown>)
|
|
23
|
+
.filter(([, v]) => v === null || typeof v !== "object")
|
|
24
|
+
.map(([k, v]) => `${k}="${escapeXml(v)}"`)
|
|
25
|
+
.join(" ");
|
|
26
|
+
const children = Object.entries(obj as Record<string, unknown>)
|
|
27
|
+
.filter(([, v]) => v !== null && typeof v === "object")
|
|
28
|
+
.map(([k, v]) =>
|
|
29
|
+
Array.isArray(v)
|
|
30
|
+
? `<${k}>${v.map((i) => toXmlItem(i)).join("")}</${k}>`
|
|
31
|
+
: toXmlItem(v, k),
|
|
32
|
+
)
|
|
33
|
+
.join("");
|
|
34
|
+
return `<${tag}${attrs ? " " + attrs : ""}>${children}</${tag}>`;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export function toXml(results: readonly unknown[]): string {
|
|
38
|
-
|
|
38
|
+
return `<results>\n${results.map((r) => " " + toXmlItem(r)).join("\n")}\n</results>`;
|
|
39
39
|
}
|
package/src/resolve.ts
CHANGED
|
@@ -1,76 +1,89 @@
|
|
|
1
|
-
import { Effect } from "effect"
|
|
2
|
-
import { api, decodeOrFail } from "./api.js"
|
|
3
|
-
import {
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { api, decodeOrFail } from "./api.js";
|
|
3
|
+
import {
|
|
4
|
+
IssuesResponseSchema,
|
|
5
|
+
StatesResponseSchema,
|
|
6
|
+
ProjectsResponseSchema,
|
|
7
|
+
} from "./config.js";
|
|
4
8
|
|
|
5
9
|
// Cache project list within a process invocation
|
|
6
|
-
let _projectCache: Record<string, string> | null = null
|
|
10
|
+
let _projectCache: Record<string, string> | null = null;
|
|
7
11
|
|
|
8
12
|
/** Clear the project cache — for use in tests only */
|
|
9
13
|
export function _clearProjectCache() {
|
|
10
|
-
|
|
14
|
+
_projectCache = null;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
function getProjectMap(): Effect.Effect<Record<string, string>, Error> {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
if (_projectCache) return Effect.succeed(_projectCache);
|
|
19
|
+
return Effect.gen(function* () {
|
|
20
|
+
const raw = yield* api.get("projects/");
|
|
21
|
+
const { results } = yield* decodeOrFail(ProjectsResponseSchema, raw);
|
|
22
|
+
_projectCache = Object.fromEntries(
|
|
23
|
+
results.map((p) => [p.identifier.toUpperCase(), p.id]),
|
|
24
|
+
);
|
|
25
|
+
return _projectCache;
|
|
26
|
+
});
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
export function resolveProject(
|
|
24
|
-
|
|
30
|
+
identifier: string,
|
|
25
31
|
): Effect.Effect<{ key: string; id: string }, Error> {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
const key = identifier.toUpperCase();
|
|
33
|
+
return getProjectMap().pipe(
|
|
34
|
+
Effect.flatMap((map) => {
|
|
35
|
+
const id = map[key];
|
|
36
|
+
if (!id) {
|
|
37
|
+
return Effect.fail(
|
|
38
|
+
new Error(
|
|
39
|
+
`Unknown project: ${identifier}. Known: ${Object.keys(map).join(", ")}`,
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return Effect.succeed({ key, id });
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
export function parseIssueRef(
|
|
41
|
-
|
|
49
|
+
ref: string,
|
|
42
50
|
): Effect.Effect<{ projectId: string; projKey: string; seq: number }, Error> {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const parts = ref.toUpperCase().split("-");
|
|
52
|
+
if (parts.length !== 2 || !/^\d+$/.test(parts[1])) {
|
|
53
|
+
return Effect.fail(
|
|
54
|
+
new Error(`Invalid issue ref: ${ref}. Expected format like PROJ-29`),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const [projKey, seqStr] = parts;
|
|
58
|
+
return resolveProject(projKey).pipe(
|
|
59
|
+
Effect.map(({ id }) => ({
|
|
60
|
+
projectId: id,
|
|
61
|
+
projKey,
|
|
62
|
+
seq: parseInt(seqStr, 10),
|
|
63
|
+
})),
|
|
64
|
+
);
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
export function findIssueBySeq(projectId: string, seq: number) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
return Effect.gen(function* () {
|
|
69
|
+
const raw = yield* api.get(`projects/${projectId}/issues/`);
|
|
70
|
+
const { results } = yield* decodeOrFail(IssuesResponseSchema, raw);
|
|
71
|
+
const issue = results.find((i) => i.sequence_id === seq);
|
|
72
|
+
if (!issue) return yield* Effect.fail(new Error(`Issue #${seq} not found`));
|
|
73
|
+
return issue;
|
|
74
|
+
});
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
export function getStateId(projectId: string, nameOrGroup: string) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
return Effect.gen(function* () {
|
|
79
|
+
const raw = yield* api.get(`projects/${projectId}/states/`);
|
|
80
|
+
const { results } = yield* decodeOrFail(StatesResponseSchema, raw);
|
|
81
|
+
const lower = nameOrGroup.toLowerCase();
|
|
82
|
+
const state = results.find(
|
|
83
|
+
(s) => s.group === lower || s.name.toLowerCase() === lower,
|
|
84
|
+
);
|
|
85
|
+
if (!state)
|
|
86
|
+
return yield* Effect.fail(new Error(`State not found: ${nameOrGroup}`));
|
|
87
|
+
return state.id;
|
|
88
|
+
});
|
|
76
89
|
}
|
package/tests/api.test.ts
CHANGED
|
@@ -1,169 +1,192 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import {
|
|
2
|
+
afterAll,
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeAll,
|
|
5
|
+
beforeEach,
|
|
6
|
+
describe,
|
|
7
|
+
expect,
|
|
8
|
+
it,
|
|
9
|
+
} from "bun:test";
|
|
10
|
+
import { Effect } from "effect";
|
|
11
|
+
import { http, HttpResponse } from "msw";
|
|
12
|
+
import { setupServer } from "msw/node";
|
|
13
|
+
import { api, decodeOrFail } from "@/api";
|
|
14
|
+
import { Schema } from "effect";
|
|
15
|
+
|
|
16
|
+
const BASE = "http://api-test.local";
|
|
17
|
+
const WS = "testws";
|
|
18
|
+
|
|
19
|
+
const server = setupServer();
|
|
20
|
+
|
|
21
|
+
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
|
|
22
|
+
afterAll(() => server.close());
|
|
15
23
|
|
|
16
24
|
beforeEach(() => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
})
|
|
25
|
+
process.env["PLANE_HOST"] = BASE;
|
|
26
|
+
process.env["PLANE_WORKSPACE"] = WS;
|
|
27
|
+
process.env["PLANE_API_TOKEN"] = "test-token";
|
|
28
|
+
});
|
|
21
29
|
|
|
22
30
|
afterEach(() => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
})
|
|
31
|
+
server.resetHandlers();
|
|
32
|
+
delete process.env["PLANE_HOST"];
|
|
33
|
+
delete process.env["PLANE_WORKSPACE"];
|
|
34
|
+
delete process.env["PLANE_API_TOKEN"];
|
|
35
|
+
});
|
|
28
36
|
|
|
29
37
|
describe("api.get", () => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
38
|
+
it("makes a GET request and returns parsed JSON", async () => {
|
|
39
|
+
server.use(
|
|
40
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/`, () =>
|
|
41
|
+
HttpResponse.json({
|
|
42
|
+
results: [{ id: "p1", identifier: "ACME", name: "InstUI" }],
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
const result = await Effect.runPromise(api.get("projects/"));
|
|
47
|
+
expect((result as any).results).toHaveLength(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("strips trailing slash from PLANE_HOST", async () => {
|
|
51
|
+
process.env["PLANE_HOST"] = `${BASE}/`;
|
|
52
|
+
server.use(
|
|
53
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/`, () =>
|
|
54
|
+
HttpResponse.json({ results: [] }),
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
const result = await Effect.runPromise(api.get("projects/"));
|
|
58
|
+
expect((result as any).results).toHaveLength(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("appends expand=state for issues/ paths", async () => {
|
|
62
|
+
let capturedUrl = "";
|
|
63
|
+
server.use(
|
|
64
|
+
http.get(
|
|
65
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/p1/issues/`,
|
|
66
|
+
({ request }) => {
|
|
67
|
+
capturedUrl = request.url;
|
|
68
|
+
return HttpResponse.json({ results: [] });
|
|
69
|
+
},
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
await Effect.runPromise(api.get("projects/p1/issues/"));
|
|
73
|
+
expect(capturedUrl).toContain("expand=state");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("fails on HTTP 4xx response", async () => {
|
|
77
|
+
server.use(
|
|
78
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/`, () =>
|
|
79
|
+
HttpResponse.json({ detail: "Not found" }, { status: 404 }),
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
const result = await Effect.runPromise(Effect.either(api.get("projects/")));
|
|
83
|
+
expect(result._tag).toBe("Left");
|
|
84
|
+
if (result._tag === "Left") {
|
|
85
|
+
expect(result.left.message).toContain("HTTP 404");
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("fails on HTTP 401 response", async () => {
|
|
90
|
+
server.use(
|
|
91
|
+
http.get(`${BASE}/api/v1/workspaces/${WS}/projects/`, () =>
|
|
92
|
+
HttpResponse.text("Unauthorized", { status: 401 }),
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
const result = await Effect.runPromise(Effect.either(api.get("projects/")));
|
|
96
|
+
expect(result._tag).toBe("Left");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
86
99
|
|
|
87
100
|
describe("api.post", () => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
101
|
+
it("sends JSON body and returns parsed response", async () => {
|
|
102
|
+
server.use(
|
|
103
|
+
http.post(
|
|
104
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/p1/issues/`,
|
|
105
|
+
async ({ request }) => {
|
|
106
|
+
const body = (await request.json()) as any;
|
|
107
|
+
return HttpResponse.json({
|
|
108
|
+
id: "new-issue",
|
|
109
|
+
sequence_id: 99,
|
|
110
|
+
name: body.name,
|
|
111
|
+
priority: "none",
|
|
112
|
+
state: "s1",
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
const result = (await Effect.runPromise(
|
|
118
|
+
api.post("projects/p1/issues/", { name: "New Issue" }),
|
|
119
|
+
)) as any;
|
|
120
|
+
expect(result.sequence_id).toBe(99);
|
|
121
|
+
expect(result.name).toBe("New Issue");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
108
124
|
|
|
109
125
|
describe("api.patch", () => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
})
|
|
126
|
+
it("sends a PATCH and returns updated resource", async () => {
|
|
127
|
+
server.use(
|
|
128
|
+
http.patch(
|
|
129
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/p1/issues/i1/`,
|
|
130
|
+
async ({ request }) => {
|
|
131
|
+
const body = (await request.json()) as any;
|
|
132
|
+
return HttpResponse.json({
|
|
133
|
+
id: "i1",
|
|
134
|
+
sequence_id: 1,
|
|
135
|
+
name: "Issue",
|
|
136
|
+
priority: body.priority ?? "low",
|
|
137
|
+
state: "s1",
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
const result = (await Effect.runPromise(
|
|
143
|
+
api.patch("projects/p1/issues/i1/", { priority: "high" }),
|
|
144
|
+
)) as any;
|
|
145
|
+
expect(result.priority).toBe("high");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
132
148
|
|
|
133
149
|
describe("api.delete", () => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
it("sends a DELETE request", async () => {
|
|
151
|
+
let called = false;
|
|
152
|
+
server.use(
|
|
153
|
+
http.delete(
|
|
154
|
+
`${BASE}/api/v1/workspaces/${WS}/projects/p1/issues/i1/`,
|
|
155
|
+
() => {
|
|
156
|
+
called = true;
|
|
157
|
+
return new HttpResponse(null, { status: 204 });
|
|
158
|
+
},
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
await Effect.runPromise(api.delete("projects/p1/issues/i1/"));
|
|
162
|
+
expect(called).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
146
165
|
|
|
147
166
|
describe("decodeOrFail", () => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
})
|
|
167
|
+
const NameSchema = Schema.Struct({ name: Schema.String });
|
|
168
|
+
|
|
169
|
+
it("decodes valid data", async () => {
|
|
170
|
+
const result = await Effect.runPromise(
|
|
171
|
+
decodeOrFail(NameSchema, { name: "hello" }),
|
|
172
|
+
);
|
|
173
|
+
expect(result.name).toBe("hello");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("fails with Error for invalid data", async () => {
|
|
177
|
+
const result = await Effect.runPromise(
|
|
178
|
+
Effect.either(decodeOrFail(NameSchema, { name: 42 })),
|
|
179
|
+
);
|
|
180
|
+
expect(result._tag).toBe("Left");
|
|
181
|
+
if (result._tag === "Left") {
|
|
182
|
+
expect(result.left).toBeInstanceOf(Error);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("fails for missing required field", async () => {
|
|
187
|
+
const result = await Effect.runPromise(
|
|
188
|
+
Effect.either(decodeOrFail(NameSchema, {})),
|
|
189
|
+
);
|
|
190
|
+
expect(result._tag).toBe("Left");
|
|
191
|
+
});
|
|
192
|
+
});
|