Manual Tests Throws and Writes

Throws and Writes

elements man tests/throws Read as markdown

How to test that a call is refused, and how to test a LiveTable write through a view, as a template would.

Testing That Code Throws

A bare thrown error records a Crash and stops the block, so to assert that a call should throw, catch it yourself. Verify both that it threw and that the error was the expected kind.

test.ts
import { test, session, assert, AuthError } from "@elements/app"; test("posting while logged out is rejected", () => { let threw = false; try { createPost({ title: "hi" }); } catch (err) { threw = true; assert(err instanceof AuthError, `got ${err}`); } assert(threw); });

Put the error in the message. Without `got ${err}`, when the call throws something unexpected (usually a real bug, not the rejection you were testing for), the assertion fails with assertion failed and the actual error, its type and its message are discarded. Interpolating err costs nothing when the test passes and is the whole diagnosis when it does not.

A failed assert records and continues, and anchors to the line it is written on. Factor this shape into a helper and every throw-test in the file reports at that one line inside the helper, not at the test that called it. Interpolating the error is what keeps such a helper readable.

A helper that takes the call as a callback is async and awaits it. Call it as await expectError(...) in an async test, the same as any function you declare async, so it runs inside the test's savepoint and session.

The threw flag makes the test fail when the call unexpectedly succeeds; without it, a call that never throws passes. Use the same shape for ValidationError on bad input, ForbiddenError on a role check, and so on.

Testing a LiveTable Write

insert, update, and delete work on the server, so a test calls them the same way a template does: on a view. table.view() opens one, exactly as a route would, and the write runs the real path: partition check, your handler or auto-SQL, then the broadcast.

test.ts
import { test, equal, session, sql } from "@elements/app"; import { comments } from "./services"; test("a member can comment", () => { session.login({ userId: "u1", userName: "alice", }); comments.view().insert({ text: "hi", author: "alice", }); equal(sql(`select text from comments`).firstOrThrow().text, "hi"); });

A partition is passed here too: comments.view({ roomId }).

The session is the one the test is running in, so a handler's session check sees exactly what a request would. A test that logs nobody in is an anonymous caller, which is how you test that a write is refused:

test.ts
test("an anonymous visitor cannot comment", () => { let threw = false; try { comments.view().insert({ text: "nope", author: "nobody", }); } catch (err) { threw = true; assert(err instanceof AuthError, `got ${err}`); } assert(threw); equal(sql(`select count(*) from comments where author = 'nobody'`).firstOrThrow().count, 0); });

Export the mutation so a test can import it, from the page's template.ehtml, its services.ts, or app/shared/services/. Give it the view as a parameter. The template passes the one the route opened, and the test passes one of its own:

services.ts
export function postComment(view: LiveView<Comment>, text: string) { view.insert({ text, author: session.getOrThrow("userName"), }); } // test.ts postComment(comments.view(), "hi");