Fix mobile web gestures, compact chat layout and notification diagnostics
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
MAX_AGENT_PUSH_REVOCATIONS, parseAgentPushConfig, parseAgentPushConsent,
|
||||
} from "../src/lib/agent-push.ts";
|
||||
import { agentAuthFrame, applyAgentEvent, createAgentRoomState, parseAgentRoomEvent } from "../src/lib/agent-room.ts";
|
||||
import { AgentPushError, browserPushOperation } from "../src/lib/agent-push-errors.ts";
|
||||
|
||||
const sourceId = "4599d348-e053-4937-987d-1392746fb2ce";
|
||||
const replacementSourceId = "22222222-2222-4222-8222-222222222222";
|
||||
@@ -120,7 +121,72 @@ test("a stuck cross-tab lock cannot prevent server retirement or local logout cl
|
||||
test("synchronous permission API failures remain a recoverable opt-in error", async () => {
|
||||
const f = fixture({ platform: { requestPermission: () => { throw new Error("user gesture rejected"); } } });
|
||||
await f.controller.refresh(); await f.controller.enable();
|
||||
assert.equal(f.record().enabled, false); assert.match(f.controller.state().error, /could not be requested/);
|
||||
assert.equal(f.record().enabled, false); assert.match(f.controller.state().error, /browser.*system settings/);
|
||||
});
|
||||
|
||||
test("browser subscription failures retain safe diagnoses across background refreshes", async () => {
|
||||
for (const [name, message] of [["AbortError", /push service \(AbortError\)/], ["NotAllowedError", /site settings.*system settings/],
|
||||
["NotSupportedError", /cannot create a push subscription/], ["InvalidStateError", /incompatible notification subscription/], ["InvalidAccessError", /notification key/]]) {
|
||||
const f = fixture({ platform: { subscribe: () => browserPushOperation(() => Promise.reject(new DOMException("private-endpoint-and-key", name)), "subscription") } });
|
||||
await f.controller.refresh(); await f.controller.enable();
|
||||
assert.match(f.controller.state().error, message);
|
||||
assert.equal(f.controller.state().enabled, false);
|
||||
assert.ok(!f.calls.some(([name]) => name === "register"));
|
||||
await f.controller.refresh();
|
||||
assert.match(f.controller.state().error, message, "heartbeat cannot erase a failed opt-in");
|
||||
assert.doesNotMatch(JSON.stringify(f.states), /private-endpoint-and-key/);
|
||||
await f.controller.refresh(true);
|
||||
assert.equal(f.controller.state().error, null, "explicit refresh clears the previous attempt");
|
||||
}
|
||||
});
|
||||
|
||||
test("worker startup and subscription timeouts have distinct actionable errors", async () => {
|
||||
for (const [stage, message] of [["worker", /worker could not start/], ["subscription", /push service \(TimeoutError\)/]]) {
|
||||
const f = fixture({ platform: { subscribe: () => browserPushOperation(() => new Promise(() => {}), stage, 5) } });
|
||||
await f.controller.refresh(); await f.controller.enable();
|
||||
assert.match(f.controller.state().error, message); assert.equal(f.controller.state().busy, false);
|
||||
assert.equal(f.controller.state().enabled, false);
|
||||
}
|
||||
});
|
||||
|
||||
test("dismissed prompts and insecure contexts do not attempt a subscription", async () => {
|
||||
const f = fixture({ platform: { requestPermission: async () => "default" } });
|
||||
await f.controller.refresh(); await f.controller.enable();
|
||||
assert.match(f.controller.state().error, /dismissed/); assert.equal(f.calls.length, 0);
|
||||
f.setSupport("insecure");
|
||||
assert.equal(f.controller.state().canEnable, false); assert.match(f.controller.state().message, /HTTPS/);
|
||||
});
|
||||
|
||||
test("notification API errors retain status-specific guidance through rollback without echoing bodies", async () => {
|
||||
for (const [status, message] of [[401, /sign-in.*authorized/], [403, /sign-in.*authorized/], [400, /rejected.*subscription/],
|
||||
[409, /expired/], [429, /busy/], [503, /unavailable on the Sybil server/], [500, /server.*save notification settings/]]) {
|
||||
const api = new AgentPushClient({ baseUrl: "https://sybil.example", token: () => "private-auth-token",
|
||||
fetch: async () => new Response(JSON.stringify({ error: "private-endpoint-and-key" }), { status }) });
|
||||
const f = fixture({ api: { register: (id, subscription) => api.register(id, subscription) } });
|
||||
await f.controller.refresh(); await f.controller.enable();
|
||||
assert.match(f.controller.state().error, message); assert.equal(f.controller.state().enabled, false);
|
||||
assert.ok(f.calls.some(([name]) => name === "unsubscribe"));
|
||||
assert.doesNotMatch(JSON.stringify(f.states), /private-auth-token|private-endpoint-and-key/);
|
||||
}
|
||||
});
|
||||
|
||||
test("failed storage after subscribing still unsubscribes and retains cleanup intent", async () => {
|
||||
const f = fixture(); let writes = 0;
|
||||
const write = f.options.write;
|
||||
f.options.write = value => { if (++writes > 1) throw new DOMException("private-storage-detail", "QuotaExceededError"); write(value); };
|
||||
await f.controller.refresh(); await f.controller.enable();
|
||||
assert.match(f.controller.state().error, /storage.*free some space/);
|
||||
assert.equal(f.controller.state().enabled, false);
|
||||
assert.ok(f.calls.some(([name]) => name === "unsubscribe"));
|
||||
assert.ok(f.calls.some(([name]) => name === "revoke"));
|
||||
assert.ok(!f.calls.some(([name]) => name === "register"));
|
||||
assert.doesNotMatch(JSON.stringify(f.states), /private-storage-detail/);
|
||||
});
|
||||
|
||||
test("server errors during refresh are distinct from unavailable browser push", async () => {
|
||||
const f = fixture({ api: { config: async () => { throw new AgentPushError("Web Push is unavailable on the Sybil server."); } } });
|
||||
await f.controller.refresh();
|
||||
assert.match(f.controller.state().error, /Sybil server/); assert.equal(f.controller.state().canEnable, false);
|
||||
});
|
||||
|
||||
test("subscription rotation upserts the same installation; permission revocation removes it", async () => {
|
||||
@@ -418,6 +484,7 @@ test("read frames validate source/cursor and preserve existing read semantics",
|
||||
assert.throws(() => parseAgentRoomEvent(JSON.stringify({ type: "read_state", cursor: "1", updatedAt: null, sourceId: "https://evil.test" })));
|
||||
assert.equal(parseAgentPushConsent('{"enabled":true}'), null);
|
||||
assert.throws(() => parseAgentPushConfig({ ...configuration(), policy: { presenceHeartbeatMs: 1, presenceLeaseMs: 60000 } }));
|
||||
assert.throws(() => parseAgentPushConfig({ ...configuration(), vapidPublicKey: undefined }), /invalid notification configuration/);
|
||||
});
|
||||
|
||||
test("API keeps credentials in headers, validates receipts, handles 204 and redacts provider bodies", async () => {
|
||||
|
||||
Reference in New Issue
Block a user