Throws and Writes
elements man tests/throws Read as markdownHow 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.tsimport { 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.tsimport { 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.tstest("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.tsexport function postComment(view: LiveView<Comment>, text: string) { view.insert({ text, author: session.getOrThrow("userName"), }); } // test.ts postComment(comments.view(), "hi");