@freqhole/haruspex 0.0.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/dist/flows/add-peer/machine.d.ts +36 -0
- package/dist/flows/add-peer/machine.d.ts.map +1 -0
- package/dist/flows/add-peer/machine.js +500 -0
- package/dist/flows/add-peer/machine.test.d.ts +2 -0
- package/dist/flows/add-peer/machine.test.d.ts.map +1 -0
- package/dist/flows/add-peer/machine.test.js +421 -0
- package/dist/flows/add-peer/runner.d.ts +16 -0
- package/dist/flows/add-peer/runner.d.ts.map +1 -0
- package/dist/flows/add-peer/runner.js +349 -0
- package/dist/flows/add-peer/runner.test.d.ts +2 -0
- package/dist/flows/add-peer/runner.test.d.ts.map +1 -0
- package/dist/flows/add-peer/runner.test.js +205 -0
- package/dist/flows/add-peer/types.d.ts +271 -0
- package/dist/flows/add-peer/types.d.ts.map +1 -0
- package/dist/flows/add-peer/types.js +10 -0
- package/dist/flows/index.d.ts +6 -0
- package/dist/flows/index.d.ts.map +1 -0
- package/dist/flows/index.js +6 -0
- package/dist/identity/idbStore.d.ts +51 -0
- package/dist/identity/idbStore.d.ts.map +1 -0
- package/dist/identity/idbStore.js +183 -0
- package/dist/identity/idbStore.test.d.ts +2 -0
- package/dist/identity/idbStore.test.d.ts.map +1 -0
- package/dist/identity/idbStore.test.js +103 -0
- package/dist/identity/index.d.ts +8 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +3 -0
- package/dist/identity/resolve.d.ts +31 -0
- package/dist/identity/resolve.d.ts.map +1 -0
- package/dist/identity/resolve.js +37 -0
- package/dist/identity/resolve.test.d.ts +2 -0
- package/dist/identity/resolve.test.d.ts.map +1 -0
- package/dist/identity/resolve.test.js +105 -0
- package/dist/identity/types.d.ts +21 -0
- package/dist/identity/types.d.ts.map +1 -0
- package/dist/identity/types.js +1 -0
- package/dist/identity/webLocks.d.ts +22 -0
- package/dist/identity/webLocks.d.ts.map +1 -0
- package/dist/identity/webLocks.js +67 -0
- package/dist/identity/webLocks.test.d.ts +2 -0
- package/dist/identity/webLocks.test.d.ts.map +1 -0
- package/dist/identity/webLocks.test.js +125 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +7 -0
- package/dist/knock/index.d.ts +10 -0
- package/dist/knock/index.d.ts.map +1 -0
- package/dist/knock/index.js +5 -0
- package/dist/knock/pending-checker.d.ts +19 -0
- package/dist/knock/pending-checker.d.ts.map +1 -0
- package/dist/knock/pending-checker.js +27 -0
- package/dist/knock/pending-checker.test.d.ts +2 -0
- package/dist/knock/pending-checker.test.d.ts.map +1 -0
- package/dist/knock/pending-checker.test.js +93 -0
- package/dist/knock/requester.d.ts +26 -0
- package/dist/knock/requester.d.ts.map +1 -0
- package/dist/knock/requester.js +72 -0
- package/dist/knock/requester.test.d.ts +2 -0
- package/dist/knock/requester.test.d.ts.map +1 -0
- package/dist/knock/requester.test.js +108 -0
- package/dist/knock/responder.d.ts +13 -0
- package/dist/knock/responder.d.ts.map +1 -0
- package/dist/knock/responder.js +40 -0
- package/dist/knock/responder.test.d.ts +2 -0
- package/dist/knock/responder.test.d.ts.map +1 -0
- package/dist/knock/responder.test.js +89 -0
- package/dist/knock/store.d.ts +11 -0
- package/dist/knock/store.d.ts.map +1 -0
- package/dist/knock/store.js +211 -0
- package/dist/knock/store.test.d.ts +2 -0
- package/dist/knock/store.test.d.ts.map +1 -0
- package/dist/knock/store.test.js +195 -0
- package/dist/knock/types.d.ts +144 -0
- package/dist/knock/types.d.ts.map +1 -0
- package/dist/knock/types.js +26 -0
- package/dist/permissions/access.d.ts +54 -0
- package/dist/permissions/access.d.ts.map +1 -0
- package/dist/permissions/access.js +56 -0
- package/dist/permissions/access.test.d.ts +2 -0
- package/dist/permissions/access.test.d.ts.map +1 -0
- package/dist/permissions/access.test.js +72 -0
- package/dist/permissions/index.d.ts +5 -0
- package/dist/permissions/index.d.ts.map +1 -0
- package/dist/permissions/index.js +2 -0
- package/dist/permissions/role-table.d.ts +11 -0
- package/dist/permissions/role-table.d.ts.map +1 -0
- package/dist/permissions/role-table.js +16 -0
- package/dist/protocol/client.d.ts +67 -0
- package/dist/protocol/client.d.ts.map +1 -0
- package/dist/protocol/client.js +195 -0
- package/dist/protocol/client.test.d.ts +2 -0
- package/dist/protocol/client.test.d.ts.map +1 -0
- package/dist/protocol/client.test.js +282 -0
- package/dist/protocol/codec.d.ts +45 -0
- package/dist/protocol/codec.d.ts.map +1 -0
- package/dist/protocol/codec.js +111 -0
- package/dist/protocol/codec.test.d.ts +2 -0
- package/dist/protocol/codec.test.d.ts.map +1 -0
- package/dist/protocol/codec.test.js +93 -0
- package/dist/protocol/extensions.d.ts +33 -0
- package/dist/protocol/extensions.d.ts.map +1 -0
- package/dist/protocol/extensions.js +36 -0
- package/dist/protocol/extensions.test.d.ts +2 -0
- package/dist/protocol/extensions.test.d.ts.map +1 -0
- package/dist/protocol/extensions.test.js +65 -0
- package/dist/protocol/fixtures.test.d.ts +2 -0
- package/dist/protocol/fixtures.test.d.ts.map +1 -0
- package/dist/protocol/fixtures.test.js +51 -0
- package/dist/protocol/index.d.ts +9 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +4 -0
- package/dist/protocol/messages.d.ts +480 -0
- package/dist/protocol/messages.d.ts.map +1 -0
- package/dist/protocol/messages.js +255 -0
- package/dist/protocol/messages.test.d.ts +2 -0
- package/dist/protocol/messages.test.d.ts.map +1 -0
- package/dist/protocol/messages.test.js +75 -0
- package/dist/share/codec.d.ts +58 -0
- package/dist/share/codec.d.ts.map +1 -0
- package/dist/share/codec.js +222 -0
- package/dist/share/codec.test.d.ts +2 -0
- package/dist/share/codec.test.d.ts.map +1 -0
- package/dist/share/codec.test.js +150 -0
- package/dist/share/detect.d.ts +24 -0
- package/dist/share/detect.d.ts.map +1 -0
- package/dist/share/detect.js +25 -0
- package/dist/share/detect.test.d.ts +2 -0
- package/dist/share/detect.test.d.ts.map +1 -0
- package/dist/share/detect.test.js +31 -0
- package/dist/share/index.d.ts +7 -0
- package/dist/share/index.d.ts.map +1 -0
- package/dist/share/index.js +3 -0
- package/dist/share/peer-addr.d.ts +32 -0
- package/dist/share/peer-addr.d.ts.map +1 -0
- package/dist/share/peer-addr.js +72 -0
- package/dist/share/peer-addr.test.d.ts +2 -0
- package/dist/share/peer-addr.test.d.ts.map +1 -0
- package/dist/share/peer-addr.test.js +58 -0
- package/dist/solid/auth-form.d.ts +43 -0
- package/dist/solid/auth-form.d.ts.map +1 -0
- package/dist/solid/auth-form.js +55 -0
- package/dist/solid/auth-form.test.d.ts +2 -0
- package/dist/solid/auth-form.test.d.ts.map +1 -0
- package/dist/solid/auth-form.test.js +131 -0
- package/dist/solid/auth-status.d.ts +21 -0
- package/dist/solid/auth-status.d.ts.map +1 -0
- package/dist/solid/auth-status.js +58 -0
- package/dist/solid/auth-status.test.d.ts +2 -0
- package/dist/solid/auth-status.test.d.ts.map +1 -0
- package/dist/solid/auth-status.test.js +60 -0
- package/dist/solid/index.d.ts +5 -0
- package/dist/solid/index.d.ts.map +1 -0
- package/dist/solid/index.js +10 -0
- package/dist/solid/knock-inbox.d.ts +61 -0
- package/dist/solid/knock-inbox.d.ts.map +1 -0
- package/dist/solid/knock-inbox.js +123 -0
- package/dist/solid/knock-inbox.test.d.ts +2 -0
- package/dist/solid/knock-inbox.test.d.ts.map +1 -0
- package/dist/solid/knock-inbox.test.js +153 -0
- package/dist/solid/reauth-flow.d.ts +26 -0
- package/dist/solid/reauth-flow.d.ts.map +1 -0
- package/dist/solid/reauth-flow.js +34 -0
- package/dist/solid/reauth-flow.test.d.ts +2 -0
- package/dist/solid/reauth-flow.test.d.ts.map +1 -0
- package/dist/solid/reauth-flow.test.js +91 -0
- package/dist/state/endpoint-control.d.ts +22 -0
- package/dist/state/endpoint-control.d.ts.map +1 -0
- package/dist/state/endpoint-control.js +34 -0
- package/dist/state/endpoint-control.test.d.ts +2 -0
- package/dist/state/endpoint-control.test.d.ts.map +1 -0
- package/dist/state/endpoint-control.test.js +63 -0
- package/dist/state/friend-directory.d.ts +37 -0
- package/dist/state/friend-directory.d.ts.map +1 -0
- package/dist/state/friend-directory.js +44 -0
- package/dist/state/friend-directory.test.d.ts +2 -0
- package/dist/state/friend-directory.test.d.ts.map +1 -0
- package/dist/state/friend-directory.test.js +63 -0
- package/dist/state/index.d.ts +6 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +3 -0
- package/dist/state/peer-names.d.ts +7 -0
- package/dist/state/peer-names.d.ts.map +1 -0
- package/dist/state/peer-names.js +21 -0
- package/dist/state/peer-names.test.d.ts +2 -0
- package/dist/state/peer-names.test.d.ts.map +1 -0
- package/dist/state/peer-names.test.js +29 -0
- package/dist/testing/idb-harness.d.ts +9 -0
- package/dist/testing/idb-harness.d.ts.map +1 -0
- package/dist/testing/idb-harness.js +19 -0
- package/dist/testing/idb-harness.test.d.ts +2 -0
- package/dist/testing/idb-harness.test.d.ts.map +1 -0
- package/dist/testing/idb-harness.test.js +18 -0
- package/dist/testing/identity-fixtures.d.ts +10 -0
- package/dist/testing/identity-fixtures.d.ts.map +1 -0
- package/dist/testing/identity-fixtures.js +21 -0
- package/dist/testing/identity-fixtures.test.d.ts +2 -0
- package/dist/testing/identity-fixtures.test.d.ts.map +1 -0
- package/dist/testing/identity-fixtures.test.js +28 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +11 -0
- package/dist/testing/knock-fixtures.d.ts +13 -0
- package/dist/testing/knock-fixtures.d.ts.map +1 -0
- package/dist/testing/knock-fixtures.js +45 -0
- package/dist/testing/knock-fixtures.test.d.ts +2 -0
- package/dist/testing/knock-fixtures.test.d.ts.map +1 -0
- package/dist/testing/knock-fixtures.test.js +65 -0
- package/dist/testing/protocol-fixtures.d.ts +7 -0
- package/dist/testing/protocol-fixtures.d.ts.map +1 -0
- package/dist/testing/protocol-fixtures.js +28 -0
- package/dist/testing/protocol-fixtures.test.d.ts +2 -0
- package/dist/testing/protocol-fixtures.test.d.ts.map +1 -0
- package/dist/testing/protocol-fixtures.test.js +19 -0
- package/dist/testing/state-fixtures.d.ts +33 -0
- package/dist/testing/state-fixtures.d.ts.map +1 -0
- package/dist/testing/state-fixtures.js +67 -0
- package/dist/testing/state-fixtures.test.d.ts +2 -0
- package/dist/testing/state-fixtures.test.d.ts.map +1 -0
- package/dist/testing/state-fixtures.test.js +68 -0
- package/dist/testing/webauthn-mocks.d.ts +27 -0
- package/dist/testing/webauthn-mocks.d.ts.map +1 -0
- package/dist/testing/webauthn-mocks.js +78 -0
- package/dist/testing/webauthn-mocks.test.d.ts +2 -0
- package/dist/testing/webauthn-mocks.test.d.ts.map +1 -0
- package/dist/testing/webauthn-mocks.test.js +40 -0
- package/dist/webauthn/ceremony.d.ts +76 -0
- package/dist/webauthn/ceremony.d.ts.map +1 -0
- package/dist/webauthn/ceremony.js +136 -0
- package/dist/webauthn/ceremony.test.d.ts +2 -0
- package/dist/webauthn/ceremony.test.d.ts.map +1 -0
- package/dist/webauthn/ceremony.test.js +245 -0
- package/dist/webauthn/codec.d.ts +39 -0
- package/dist/webauthn/codec.d.ts.map +1 -0
- package/dist/webauthn/codec.js +118 -0
- package/dist/webauthn/codec.test.d.ts +2 -0
- package/dist/webauthn/codec.test.d.ts.map +1 -0
- package/dist/webauthn/codec.test.js +126 -0
- package/dist/webauthn/index.d.ts +7 -0
- package/dist/webauthn/index.d.ts.map +1 -0
- package/dist/webauthn/index.js +3 -0
- package/dist/webauthn/transport.d.ts +58 -0
- package/dist/webauthn/transport.d.ts.map +1 -0
- package/dist/webauthn/transport.js +6 -0
- package/dist/webauthn/types.d.ts +78 -0
- package/dist/webauthn/types.d.ts.map +1 -0
- package/dist/webauthn/types.js +29 -0
- package/package.json +83 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createRoot } from "solid-js";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { createAuthForm } from "./auth-form.js";
|
|
4
|
+
describe("createAuthForm", () => {
|
|
5
|
+
it("defaults to login mode", () => {
|
|
6
|
+
createRoot((dispose) => {
|
|
7
|
+
const form = createAuthForm();
|
|
8
|
+
expect(form.mode()).toBe("login");
|
|
9
|
+
dispose();
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
it("honors an initialMode", () => {
|
|
13
|
+
createRoot((dispose) => {
|
|
14
|
+
const form = createAuthForm({ initialMode: "register" });
|
|
15
|
+
expect(form.mode()).toBe("register");
|
|
16
|
+
dispose();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
it("isSubmitDisabled is true until a username is entered", () => {
|
|
20
|
+
createRoot((dispose) => {
|
|
21
|
+
const form = createAuthForm();
|
|
22
|
+
expect(form.isSubmitDisabled()).toBe(true);
|
|
23
|
+
form.setUsername("viz");
|
|
24
|
+
expect(form.isSubmitDisabled()).toBe(false);
|
|
25
|
+
dispose();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
it("register mode also requires an invite code", () => {
|
|
29
|
+
createRoot((dispose) => {
|
|
30
|
+
const form = createAuthForm({ initialMode: "register" });
|
|
31
|
+
form.setUsername("viz");
|
|
32
|
+
expect(form.isSubmitDisabled()).toBe(true);
|
|
33
|
+
form.setInviteCode("abc123");
|
|
34
|
+
expect(form.isSubmitDisabled()).toBe(false);
|
|
35
|
+
dispose();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it("isSubmitDisabled is true while loading, regardless of field state", () => {
|
|
39
|
+
createRoot((dispose) => {
|
|
40
|
+
const form = createAuthForm({ loading: () => true });
|
|
41
|
+
form.setUsername("viz");
|
|
42
|
+
expect(form.isSubmitDisabled()).toBe(true);
|
|
43
|
+
dispose();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
it("switchMode flips login <-> register and calls onModeChange", () => {
|
|
47
|
+
createRoot((dispose) => {
|
|
48
|
+
const onModeChange = vi.fn();
|
|
49
|
+
const form = createAuthForm({ onModeChange });
|
|
50
|
+
form.switchMode();
|
|
51
|
+
expect(form.mode()).toBe("register");
|
|
52
|
+
expect(onModeChange).toHaveBeenCalledWith("register");
|
|
53
|
+
form.switchMode();
|
|
54
|
+
expect(form.mode()).toBe("login");
|
|
55
|
+
expect(onModeChange).toHaveBeenCalledWith("login");
|
|
56
|
+
dispose();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
it("handleSubmit calls onSubmit with the current field values, omitting inviteCode in login mode", async () => {
|
|
60
|
+
await createRoot(async (dispose) => {
|
|
61
|
+
const onSubmit = vi.fn();
|
|
62
|
+
const form = createAuthForm({ onSubmit });
|
|
63
|
+
form.setUsername("viz");
|
|
64
|
+
form.setInviteCode("should-be-ignored");
|
|
65
|
+
await form.handleSubmit();
|
|
66
|
+
expect(onSubmit).toHaveBeenCalledWith({
|
|
67
|
+
username: "viz",
|
|
68
|
+
inviteCode: undefined,
|
|
69
|
+
mode: "login",
|
|
70
|
+
});
|
|
71
|
+
dispose();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
it("handleSubmit includes inviteCode in register mode", async () => {
|
|
75
|
+
await createRoot(async (dispose) => {
|
|
76
|
+
const onSubmit = vi.fn();
|
|
77
|
+
const form = createAuthForm({ initialMode: "register", onSubmit });
|
|
78
|
+
form.setUsername("viz");
|
|
79
|
+
form.setInviteCode("abc123");
|
|
80
|
+
await form.handleSubmit();
|
|
81
|
+
expect(onSubmit).toHaveBeenCalledWith({
|
|
82
|
+
username: "viz",
|
|
83
|
+
inviteCode: "abc123",
|
|
84
|
+
mode: "register",
|
|
85
|
+
});
|
|
86
|
+
dispose();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
it("handleSubmit no-ops while loading", async () => {
|
|
90
|
+
await createRoot(async (dispose) => {
|
|
91
|
+
const onSubmit = vi.fn();
|
|
92
|
+
const form = createAuthForm({ loading: () => true, onSubmit });
|
|
93
|
+
form.setUsername("viz");
|
|
94
|
+
await form.handleSubmit();
|
|
95
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
96
|
+
dispose();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
it("handleSubmit calls preventDefault on a passed event", async () => {
|
|
100
|
+
await createRoot(async (dispose) => {
|
|
101
|
+
const form = createAuthForm();
|
|
102
|
+
const preventDefault = vi.fn();
|
|
103
|
+
await form.handleSubmit({ preventDefault });
|
|
104
|
+
expect(preventDefault).toHaveBeenCalled();
|
|
105
|
+
dispose();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
it("handlePasskeyClick calls onPasskeyClick with the current field values", async () => {
|
|
109
|
+
await createRoot(async (dispose) => {
|
|
110
|
+
const onPasskeyClick = vi.fn();
|
|
111
|
+
const form = createAuthForm({ onPasskeyClick });
|
|
112
|
+
form.setUsername("viz");
|
|
113
|
+
await form.handlePasskeyClick();
|
|
114
|
+
expect(onPasskeyClick).toHaveBeenCalledWith({
|
|
115
|
+
username: "viz",
|
|
116
|
+
inviteCode: undefined,
|
|
117
|
+
mode: "login",
|
|
118
|
+
});
|
|
119
|
+
dispose();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
it("handlePasskeyClick no-ops while loading", async () => {
|
|
123
|
+
await createRoot(async (dispose) => {
|
|
124
|
+
const onPasskeyClick = vi.fn();
|
|
125
|
+
const form = createAuthForm({ loading: () => true, onPasskeyClick });
|
|
126
|
+
await form.handlePasskeyClick();
|
|
127
|
+
expect(onPasskeyClick).not.toHaveBeenCalled();
|
|
128
|
+
dispose();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Accessor } from "solid-js";
|
|
2
|
+
export interface AuthStatusStore<T> {
|
|
3
|
+
/** reactive snapshot of every tracked key's entry. */
|
|
4
|
+
status: Accessor<Map<string, T | null>>;
|
|
5
|
+
/** snapshot lookup for one key. `undefined` means never queried. */
|
|
6
|
+
get(key: string): T | null | undefined;
|
|
7
|
+
/** mark one key as pending (query in flight), without touching others. */
|
|
8
|
+
markPending(key: string): void;
|
|
9
|
+
/** mark every one of the given keys as pending in a single update. */
|
|
10
|
+
resetPending(keys: string[]): void;
|
|
11
|
+
/** store a resolved entry for one key. */
|
|
12
|
+
patch(key: string, entry: T): void;
|
|
13
|
+
/** drop one key's entry entirely (e.g. after the remote is deleted). */
|
|
14
|
+
clear(key: string): void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* creates a reactive `key -> resolved-entry-or-pending` cache for any
|
|
18
|
+
* number of independently-tracked remotes/resources.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createAuthStatus<T>(): AuthStatusStore<T>;
|
|
21
|
+
//# sourceMappingURL=auth-status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-status.d.ts","sourceRoot":"","sources":["../../src/solid/auth-status.ts"],"names":[],"mappings":"AAeA,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEvD,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,sDAAsD;IACtD,MAAM,EAAE,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IACxC,oEAAoE;IACpE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC;IACvC,0EAA0E;IAC1E,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,sEAAsE;IACtE,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IACnC,0CAA0C;IAC1C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACnC,wEAAwE;IACxE,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,KAAK,eAAe,CAAC,CAAC,CAAC,CAsCxD"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// a reactive per-remote auth-status cache: tracks whether each remote
|
|
2
|
+
// (keyed by an arbitrary string id) is logged in, being checked, or has
|
|
3
|
+
// never been queried yet, without any vocabulary of its own for how a
|
|
4
|
+
// remote is reached or how "logged in" gets determined.
|
|
5
|
+
//
|
|
6
|
+
// three states per key:
|
|
7
|
+
// - absent from the map: never queried
|
|
8
|
+
// - `null`: a query is in flight (or was reset to pending)
|
|
9
|
+
// - `T`: resolved (the caller decides what a resolved entry looks like -
|
|
10
|
+
// typically something like `{ loggedIn: boolean; username?: string }`)
|
|
11
|
+
//
|
|
12
|
+
// the caller supplies the actual resolution logic (whoami calls, offline
|
|
13
|
+
// heuristics, retry-on-cold-boot timing, and so on) - this module only
|
|
14
|
+
// owns the reactive map itself.
|
|
15
|
+
import { createSignal } from "solid-js";
|
|
16
|
+
/**
|
|
17
|
+
* creates a reactive `key -> resolved-entry-or-pending` cache for any
|
|
18
|
+
* number of independently-tracked remotes/resources.
|
|
19
|
+
*/
|
|
20
|
+
export function createAuthStatus() {
|
|
21
|
+
const [status, setStatus] = createSignal(new Map());
|
|
22
|
+
function patchMap(key, entry) {
|
|
23
|
+
setStatus((prev) => {
|
|
24
|
+
const next = new Map(prev);
|
|
25
|
+
next.set(key, entry);
|
|
26
|
+
return next;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
status,
|
|
31
|
+
get(key) {
|
|
32
|
+
return status().get(key);
|
|
33
|
+
},
|
|
34
|
+
markPending(key) {
|
|
35
|
+
patchMap(key, null);
|
|
36
|
+
},
|
|
37
|
+
resetPending(keys) {
|
|
38
|
+
setStatus(() => {
|
|
39
|
+
const next = new Map();
|
|
40
|
+
for (const key of keys)
|
|
41
|
+
next.set(key, null);
|
|
42
|
+
return next;
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
patch(key, entry) {
|
|
46
|
+
patchMap(key, entry);
|
|
47
|
+
},
|
|
48
|
+
clear(key) {
|
|
49
|
+
setStatus((prev) => {
|
|
50
|
+
if (!prev.has(key))
|
|
51
|
+
return prev;
|
|
52
|
+
const next = new Map(prev);
|
|
53
|
+
next.delete(key);
|
|
54
|
+
return next;
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-status.test.d.ts","sourceRoot":"","sources":["../../src/solid/auth-status.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createRoot } from "solid-js";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { createAuthStatus } from "./auth-status.js";
|
|
4
|
+
describe("createAuthStatus", () => {
|
|
5
|
+
it("starts empty - get returns undefined for an unqueried key", () => {
|
|
6
|
+
createRoot((dispose) => {
|
|
7
|
+
const store = createAuthStatus();
|
|
8
|
+
expect(store.get("remote-a")).toBeUndefined();
|
|
9
|
+
expect(store.status().size).toBe(0);
|
|
10
|
+
dispose();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
it("markPending sets the entry to null (in flight)", () => {
|
|
14
|
+
createRoot((dispose) => {
|
|
15
|
+
const store = createAuthStatus();
|
|
16
|
+
store.markPending("remote-a");
|
|
17
|
+
expect(store.get("remote-a")).toBeNull();
|
|
18
|
+
dispose();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
it("patch stores a resolved entry", () => {
|
|
22
|
+
createRoot((dispose) => {
|
|
23
|
+
const store = createAuthStatus();
|
|
24
|
+
store.patch("remote-a", { loggedIn: true, username: "viz" });
|
|
25
|
+
expect(store.get("remote-a")).toEqual({ loggedIn: true, username: "viz" });
|
|
26
|
+
dispose();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
it("resetPending marks exactly the given keys as pending, dropping others", () => {
|
|
30
|
+
createRoot((dispose) => {
|
|
31
|
+
const store = createAuthStatus();
|
|
32
|
+
store.patch("remote-a", { loggedIn: true });
|
|
33
|
+
store.resetPending(["remote-b", "remote-c"]);
|
|
34
|
+
expect(store.get("remote-a")).toBeUndefined();
|
|
35
|
+
expect(store.get("remote-b")).toBeNull();
|
|
36
|
+
expect(store.get("remote-c")).toBeNull();
|
|
37
|
+
dispose();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
it("clear drops one key without touching others", () => {
|
|
41
|
+
createRoot((dispose) => {
|
|
42
|
+
const store = createAuthStatus();
|
|
43
|
+
store.patch("remote-a", { loggedIn: true });
|
|
44
|
+
store.patch("remote-b", { loggedIn: false });
|
|
45
|
+
store.clear("remote-a");
|
|
46
|
+
expect(store.get("remote-a")).toBeUndefined();
|
|
47
|
+
expect(store.get("remote-b")).toEqual({ loggedIn: false });
|
|
48
|
+
dispose();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
it("clear is a no-op when the key was never present", () => {
|
|
52
|
+
createRoot((dispose) => {
|
|
53
|
+
const store = createAuthStatus();
|
|
54
|
+
const before = store.status();
|
|
55
|
+
store.clear("nonexistent");
|
|
56
|
+
expect(store.status()).toBe(before);
|
|
57
|
+
dispose();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createAuthForm, type AuthMode, type AuthFormData, type AuthFormDeps, type AuthFormState, } from "./auth-form.js";
|
|
2
|
+
export { createReauthFlow, type ReauthFlowDeps, type ReauthFlowState, } from "./reauth-flow.js";
|
|
3
|
+
export { createAuthStatus, type AuthStatusStore } from "./auth-status.js";
|
|
4
|
+
export { createKnockInbox, type KnockAcceptDecision, type KnockRowError, type KnockInboxDeps, type KnockInboxState, } from "./knock-inbox.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/solid/index.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,cAAc,EACd,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,aAAa,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,eAAe,GACrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EACL,gBAAgB,EAChB,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,eAAe,GACrB,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// solid-js integration module: headless auth-form/reauth-flow/auth-status/
|
|
2
|
+
// knock-inbox state primitives (behavior + accessibility-relevant state
|
|
3
|
+
// only, zero markup or styling opinions - consumers render their own
|
|
4
|
+
// components around these), and the AddPeerFlow FSM lives in ./flows for
|
|
5
|
+
// a rendered shell to be built over later. solid-js is an optional peer
|
|
6
|
+
// dep - keep this the only subpath that imports it.
|
|
7
|
+
export { createAuthForm, } from "./auth-form.js";
|
|
8
|
+
export { createReauthFlow, } from "./reauth-flow.js";
|
|
9
|
+
export { createAuthStatus } from "./auth-status.js";
|
|
10
|
+
export { createKnockInbox, } from "./knock-inbox.js";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type Accessor, type Resource } from "solid-js";
|
|
2
|
+
/** one decision made when accepting a knock. */
|
|
3
|
+
export interface KnockAcceptDecision {
|
|
4
|
+
role: string;
|
|
5
|
+
username?: string | null;
|
|
6
|
+
userId?: string | null;
|
|
7
|
+
}
|
|
8
|
+
/** one error surfaced by a row-level action. */
|
|
9
|
+
export interface KnockRowError {
|
|
10
|
+
action: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
detail: string;
|
|
13
|
+
errorType?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface KnockInboxDeps<TRow> {
|
|
16
|
+
/** fetches the current list. `includeAll` reflects the inbox's own
|
|
17
|
+
* toggle (e.g. "show resolved knocks too"). */
|
|
18
|
+
listKnocks(includeAll: boolean): Promise<TRow[]>;
|
|
19
|
+
acceptKnock(knock: TRow, decision: KnockAcceptDecision): Promise<void>;
|
|
20
|
+
rejectKnock(knock: TRow): Promise<void>;
|
|
21
|
+
deleteKnock(knock: TRow): Promise<void>;
|
|
22
|
+
/** optional bulk reject; omit if the caller's api has no such endpoint. */
|
|
23
|
+
rejectAllKnocks?(): Promise<{
|
|
24
|
+
rejected: number;
|
|
25
|
+
}>;
|
|
26
|
+
/** extracts a stable key from a row, used for per-row loading/error
|
|
27
|
+
* tracking. */
|
|
28
|
+
getId(knock: TRow): string;
|
|
29
|
+
/** maps a thrown error into one or more row errors. defaults to a
|
|
30
|
+
* single generic error built from `error.message`/`String(error)`. */
|
|
31
|
+
mapError?(action: string, error: unknown): KnockRowError[];
|
|
32
|
+
/** fired after any successful mutation (accept/reject/delete/rejectAll). */
|
|
33
|
+
onChanged?(): void;
|
|
34
|
+
/** fired on any action's success, for toast-like side effects. */
|
|
35
|
+
onActionSuccess?(action: string, knock: TRow | null, result?: unknown): void;
|
|
36
|
+
/** fired on any action's failure, for toast-like side effects. */
|
|
37
|
+
onActionError?(action: string, knock: TRow | null, error: unknown): void;
|
|
38
|
+
}
|
|
39
|
+
export interface KnockInboxState<TRow> {
|
|
40
|
+
/** the current list resource (undefined while first loading). */
|
|
41
|
+
knocks: Resource<TRow[]>;
|
|
42
|
+
/** true while the list itself is (re)loading. */
|
|
43
|
+
loading: Accessor<boolean>;
|
|
44
|
+
includeAll: Accessor<boolean>;
|
|
45
|
+
setIncludeAll(value: boolean): void;
|
|
46
|
+
/** id of the row currently being accepted, or null. */
|
|
47
|
+
accepting: Accessor<string | null>;
|
|
48
|
+
/** id of the row currently being rejected, or null. */
|
|
49
|
+
rejecting: Accessor<string | null>;
|
|
50
|
+
/** id of the row currently being deleted, or null. */
|
|
51
|
+
deleting: Accessor<string | null>;
|
|
52
|
+
rowErrors: Accessor<Record<string, KnockRowError[]>>;
|
|
53
|
+
clearRowError(id: string): void;
|
|
54
|
+
accept(knock: TRow, decision: KnockAcceptDecision): Promise<void>;
|
|
55
|
+
reject(knock: TRow): Promise<void>;
|
|
56
|
+
remove(knock: TRow): Promise<void>;
|
|
57
|
+
rejectAll(): Promise<void>;
|
|
58
|
+
refetch(): TRow[] | Promise<TRow[] | undefined> | undefined | null;
|
|
59
|
+
}
|
|
60
|
+
export declare function createKnockInbox<TRow>(deps: KnockInboxDeps<TRow>): KnockInboxState<TRow>;
|
|
61
|
+
//# sourceMappingURL=knock-inbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knock-inbox.d.ts","sourceRoot":"","sources":["../../src/solid/knock-inbox.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// headless knock-inbox state: lists pending (or all) knocks for a
|
|
2
|
+
// resource the caller administers, tracks per-row accept/reject/delete
|
|
3
|
+
// loading state and per-row errors, and exposes accept/reject/delete/
|
|
4
|
+
// reject-all actions - all with zero markup, list-transport, or error-
|
|
5
|
+
// shape opinions of its own.
|
|
6
|
+
//
|
|
7
|
+
// the caller supplies how a row is listed/decided (any admin api, any
|
|
8
|
+
// row shape) and how to identify a row's key; this module only owns the
|
|
9
|
+
// resource-loading/action/error-tracking state around those calls.
|
|
10
|
+
import { createResource, createSignal } from "solid-js";
|
|
11
|
+
const defaultMapError = (action, error) => [
|
|
12
|
+
{ action, detail: error instanceof Error ? error.message : String(error) },
|
|
13
|
+
];
|
|
14
|
+
export function createKnockInbox(deps) {
|
|
15
|
+
const mapError = deps.mapError ?? defaultMapError;
|
|
16
|
+
const [includeAll, setIncludeAll] = createSignal(false);
|
|
17
|
+
// createResource skips its fetcher when the source returns a falsy
|
|
18
|
+
// value (false/null/undefined) - wrapping in an object so the initial
|
|
19
|
+
// `false` still triggers the first fetch.
|
|
20
|
+
const [knocks, { refetch }] = createResource(() => ({ all: includeAll() }), async ({ all }) => {
|
|
21
|
+
try {
|
|
22
|
+
return await deps.listKnocks(all);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
deps.onActionError?.("list", null, error);
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
const [accepting, setAccepting] = createSignal(null);
|
|
30
|
+
const [rejecting, setRejecting] = createSignal(null);
|
|
31
|
+
const [deleting, setDeleting] = createSignal(null);
|
|
32
|
+
const [rowErrors, setRowErrors] = createSignal({});
|
|
33
|
+
function setRowError(id, action, error) {
|
|
34
|
+
setRowErrors({ ...rowErrors(), [id]: mapError(action, error) });
|
|
35
|
+
}
|
|
36
|
+
function clearRowError(id) {
|
|
37
|
+
const { [id]: _removed, ...rest } = rowErrors();
|
|
38
|
+
setRowErrors(rest);
|
|
39
|
+
}
|
|
40
|
+
async function notifyChanged() {
|
|
41
|
+
await refetch();
|
|
42
|
+
deps.onChanged?.();
|
|
43
|
+
}
|
|
44
|
+
async function accept(knock, decision) {
|
|
45
|
+
const id = deps.getId(knock);
|
|
46
|
+
setAccepting(id);
|
|
47
|
+
clearRowError(id);
|
|
48
|
+
try {
|
|
49
|
+
await deps.acceptKnock(knock, decision);
|
|
50
|
+
deps.onActionSuccess?.("accept", knock);
|
|
51
|
+
await notifyChanged();
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
setRowError(id, "accept", error);
|
|
55
|
+
deps.onActionError?.("accept", knock, error);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
setAccepting(null);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function reject(knock) {
|
|
62
|
+
const id = deps.getId(knock);
|
|
63
|
+
setRejecting(id);
|
|
64
|
+
clearRowError(id);
|
|
65
|
+
try {
|
|
66
|
+
await deps.rejectKnock(knock);
|
|
67
|
+
deps.onActionSuccess?.("reject", knock);
|
|
68
|
+
await notifyChanged();
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
setRowError(id, "reject", error);
|
|
72
|
+
deps.onActionError?.("reject", knock, error);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
setRejecting(null);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function remove(knock) {
|
|
79
|
+
const id = deps.getId(knock);
|
|
80
|
+
setDeleting(id);
|
|
81
|
+
clearRowError(id);
|
|
82
|
+
try {
|
|
83
|
+
await deps.deleteKnock(knock);
|
|
84
|
+
deps.onActionSuccess?.("delete", knock);
|
|
85
|
+
await notifyChanged();
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
setRowError(id, "delete", error);
|
|
89
|
+
deps.onActionError?.("delete", knock, error);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
setDeleting(null);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function rejectAll() {
|
|
96
|
+
if (!deps.rejectAllKnocks)
|
|
97
|
+
return;
|
|
98
|
+
try {
|
|
99
|
+
const result = await deps.rejectAllKnocks();
|
|
100
|
+
deps.onActionSuccess?.("rejectAll", null, result);
|
|
101
|
+
await notifyChanged();
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
deps.onActionError?.("rejectAll", null, error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
knocks,
|
|
109
|
+
loading: () => knocks.loading,
|
|
110
|
+
includeAll,
|
|
111
|
+
setIncludeAll,
|
|
112
|
+
accepting,
|
|
113
|
+
rejecting,
|
|
114
|
+
deleting,
|
|
115
|
+
rowErrors,
|
|
116
|
+
clearRowError,
|
|
117
|
+
accept,
|
|
118
|
+
reject,
|
|
119
|
+
remove,
|
|
120
|
+
rejectAll,
|
|
121
|
+
refetch,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knock-inbox.test.d.ts","sourceRoot":"","sources":["../../src/solid/knock-inbox.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { createRoot } from "solid-js";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { createKnockInbox } from "./knock-inbox.js";
|
|
4
|
+
function flush() {
|
|
5
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
6
|
+
}
|
|
7
|
+
function makeDeps(overrides = {}) {
|
|
8
|
+
return {
|
|
9
|
+
listKnocks: vi.fn().mockResolvedValue([{ id: "1", status: "pending" }]),
|
|
10
|
+
acceptKnock: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
rejectKnock: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
deleteKnock: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
getId: (row) => row.id,
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
describe("createKnockInbox", () => {
|
|
18
|
+
it("lists knocks on creation with includeAll defaulting to false", async () => {
|
|
19
|
+
await createRoot(async (dispose) => {
|
|
20
|
+
const listKnocks = vi.fn().mockResolvedValue([]);
|
|
21
|
+
createKnockInbox(makeDeps({ listKnocks }));
|
|
22
|
+
await flush();
|
|
23
|
+
expect(listKnocks).toHaveBeenCalledWith(false);
|
|
24
|
+
dispose();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
it("re-lists with includeAll: true when the toggle is set", async () => {
|
|
28
|
+
await createRoot(async (dispose) => {
|
|
29
|
+
const listKnocks = vi.fn().mockResolvedValue([]);
|
|
30
|
+
const inbox = createKnockInbox(makeDeps({ listKnocks }));
|
|
31
|
+
await flush();
|
|
32
|
+
inbox.setIncludeAll(true);
|
|
33
|
+
await flush();
|
|
34
|
+
expect(listKnocks).toHaveBeenLastCalledWith(true);
|
|
35
|
+
dispose();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it("populates knocks() once the list resolves", async () => {
|
|
39
|
+
await createRoot(async (dispose) => {
|
|
40
|
+
const inbox = createKnockInbox(makeDeps());
|
|
41
|
+
await flush();
|
|
42
|
+
expect(inbox.knocks()).toEqual([{ id: "1", status: "pending" }]);
|
|
43
|
+
dispose();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
it("accept sets accepting(id) during the call, clears it after, and refetches", async () => {
|
|
47
|
+
await createRoot(async (dispose) => {
|
|
48
|
+
const acceptKnock = vi.fn().mockResolvedValue(undefined);
|
|
49
|
+
const onChanged = vi.fn();
|
|
50
|
+
const listKnocks = vi.fn().mockResolvedValue([{ id: "1", status: "pending" }]);
|
|
51
|
+
const inbox = createKnockInbox(makeDeps({ acceptKnock, onChanged, listKnocks }));
|
|
52
|
+
await flush();
|
|
53
|
+
const row = { id: "1", status: "pending" };
|
|
54
|
+
const promise = inbox.accept(row, { role: "viewer" });
|
|
55
|
+
expect(inbox.accepting()).toBe("1");
|
|
56
|
+
await promise;
|
|
57
|
+
expect(acceptKnock).toHaveBeenCalledWith(row, { role: "viewer" });
|
|
58
|
+
expect(inbox.accepting()).toBeNull();
|
|
59
|
+
expect(onChanged).toHaveBeenCalled();
|
|
60
|
+
dispose();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
it("accept records a row error and does not call onChanged when acceptKnock throws", async () => {
|
|
64
|
+
await createRoot(async (dispose) => {
|
|
65
|
+
const acceptKnock = vi.fn().mockRejectedValue(new Error("already resolved"));
|
|
66
|
+
const onChanged = vi.fn();
|
|
67
|
+
const onActionError = vi.fn();
|
|
68
|
+
const inbox = createKnockInbox(makeDeps({ acceptKnock, onChanged, onActionError }));
|
|
69
|
+
await flush();
|
|
70
|
+
const row = { id: "1", status: "pending" };
|
|
71
|
+
await inbox.accept(row, { role: "viewer" });
|
|
72
|
+
expect(inbox.rowErrors()["1"]).toEqual([{ action: "accept", detail: "already resolved" }]);
|
|
73
|
+
expect(onChanged).not.toHaveBeenCalled();
|
|
74
|
+
expect(onActionError).toHaveBeenCalledWith("accept", row, expect.any(Error));
|
|
75
|
+
dispose();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
it("clearRowError removes just the given row's errors", async () => {
|
|
79
|
+
await createRoot(async (dispose) => {
|
|
80
|
+
const acceptKnock = vi.fn().mockRejectedValue(new Error("boom"));
|
|
81
|
+
const inbox = createKnockInbox(makeDeps({ acceptKnock }));
|
|
82
|
+
await flush();
|
|
83
|
+
await inbox.accept({ id: "1", status: "pending" }, { role: "viewer" });
|
|
84
|
+
expect(inbox.rowErrors()["1"]).toBeDefined();
|
|
85
|
+
inbox.clearRowError("1");
|
|
86
|
+
expect(inbox.rowErrors()["1"]).toBeUndefined();
|
|
87
|
+
dispose();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
it("reject and remove wire through to their respective deps", async () => {
|
|
91
|
+
await createRoot(async (dispose) => {
|
|
92
|
+
const rejectKnock = vi.fn().mockResolvedValue(undefined);
|
|
93
|
+
const deleteKnock = vi.fn().mockResolvedValue(undefined);
|
|
94
|
+
const inbox = createKnockInbox(makeDeps({ rejectKnock, deleteKnock }));
|
|
95
|
+
await flush();
|
|
96
|
+
const row = { id: "1", status: "pending" };
|
|
97
|
+
await inbox.reject(row);
|
|
98
|
+
expect(rejectKnock).toHaveBeenCalledWith(row);
|
|
99
|
+
expect(inbox.rejecting()).toBeNull();
|
|
100
|
+
await inbox.remove(row);
|
|
101
|
+
expect(deleteKnock).toHaveBeenCalledWith(row);
|
|
102
|
+
expect(inbox.deleting()).toBeNull();
|
|
103
|
+
dispose();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
it("rejectAll is a no-op when rejectAllKnocks isn't provided", async () => {
|
|
107
|
+
await createRoot(async (dispose) => {
|
|
108
|
+
const inbox = createKnockInbox(makeDeps());
|
|
109
|
+
await flush();
|
|
110
|
+
await expect(inbox.rejectAll()).resolves.toBeUndefined();
|
|
111
|
+
dispose();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
it("rejectAll calls the provided rejectAllKnocks and refetches", async () => {
|
|
115
|
+
await createRoot(async (dispose) => {
|
|
116
|
+
const rejectAllKnocks = vi.fn().mockResolvedValue({ rejected: 3 });
|
|
117
|
+
const onChanged = vi.fn();
|
|
118
|
+
const onActionSuccess = vi.fn();
|
|
119
|
+
const inbox = createKnockInbox(makeDeps({ rejectAllKnocks, onChanged, onActionSuccess }));
|
|
120
|
+
await flush();
|
|
121
|
+
await inbox.rejectAll();
|
|
122
|
+
expect(rejectAllKnocks).toHaveBeenCalled();
|
|
123
|
+
expect(onChanged).toHaveBeenCalled();
|
|
124
|
+
expect(onActionSuccess).toHaveBeenCalledWith("rejectAll", null, { rejected: 3 });
|
|
125
|
+
dispose();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
it("uses a custom mapError when provided", async () => {
|
|
129
|
+
await createRoot(async (dispose) => {
|
|
130
|
+
const acceptKnock = vi.fn().mockRejectedValue({ title: "conflict", detail: "already taken" });
|
|
131
|
+
const mapError = vi.fn(() => [{ action: "accept", title: "conflict", detail: "already taken" }]);
|
|
132
|
+
const inbox = createKnockInbox(makeDeps({ acceptKnock, mapError }));
|
|
133
|
+
await flush();
|
|
134
|
+
await inbox.accept({ id: "1", status: "pending" }, { role: "viewer" });
|
|
135
|
+
expect(mapError).toHaveBeenCalled();
|
|
136
|
+
expect(inbox.rowErrors()["1"]).toEqual([
|
|
137
|
+
{ action: "accept", title: "conflict", detail: "already taken" },
|
|
138
|
+
]);
|
|
139
|
+
dispose();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
it("list failures surface via onActionError and leave knocks() empty", async () => {
|
|
143
|
+
await createRoot(async (dispose) => {
|
|
144
|
+
const listKnocks = vi.fn().mockRejectedValue(new Error("offline"));
|
|
145
|
+
const onActionError = vi.fn();
|
|
146
|
+
const inbox = createKnockInbox(makeDeps({ listKnocks, onActionError }));
|
|
147
|
+
await flush();
|
|
148
|
+
expect(inbox.knocks()).toEqual([]);
|
|
149
|
+
expect(onActionError).toHaveBeenCalledWith("list", null, expect.any(Error));
|
|
150
|
+
dispose();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|