Testing your app
How your project's tests run, what they can reach, and how to give code under test dummy secrets, fake responses and results from Floot services.
Your assistant can write automated tests for your project and run them. Tests are Jasmine spec files in your project's helpers/ folder, and they can cover both screens and backend endpoints. This page is for when you or your assistant write those tests and something will not run.
Where tests live
Spec files live in helpers/ and end in .spec.tsx. They are usually named after the helper they check (helpers/pricing.tsx is tested by helpers/pricing.spec.tsx), and a spec can import any file of the project, a component or an endpoint included. Inside it, describe, it, expect, beforeEach, spyOn and the rest of Jasmine are available without an import, and components are rendered with React Testing Library (@testing-library/react).
Two places tests run
- When your assistant runs them
- The
run_teststool runs the specs on your project's VM, in Node with a simulated browser page. Screens, helpers and endpoints can all be tested here: an endpoint is a function that takes aRequestand returns aResponse, so a test can import it and call it directly. - In the Floot editor
- The editor also runs specs in your browser as you work, for screens and helpers only. A spec that imports an endpoint, reads
process.envor uses a Node module is skipped there, not failed. Run it withrun_testsinstead.
What a test cannot reach
- The network. Any
fetchthrowsNo network requests allowed in tests.Replace it with a fake answer:spyOn(globalThis, "fetch").and.resolveTo(new Response(JSON.stringify({ ok: true }))). - Your secrets. No stored key or secret is available, so
process.env.MY_KEYis undefined, and Floot does not fill in stand-in values: code that checks whether a key is set must behave the same in a test as in your app. See Dummy secrets below. - Your database. Tests have no database connection. Test the logic around a query, or pass the query function in so a test can replace it.
- Floot's built-in services. Storage, email, AI, jobs, realtime and the other
@floot/modules never run in a test. Each of their functions is replaced by a stand-in that fails until the test says what it returns. See Floot services in tests below.
Each test has 2 seconds and each spec file 15 seconds. One run_tests call runs up to 20 spec files; your assistant can name the files to run the rest. Spec files with use in their name (tests of React hooks) run only when named.
Dummy secrets
Code that reads a secret when it is first loaded, such as const SECRET = process.env.JWT_SECRET, needs a value before it is imported. Put the dummy values in a small helper and import it first: imports run in the order they are written, so the values are in place when the next import loads.
// helpers/testEnv.tsx: imported only by spec files
process.env.JWT_SECRET = "test-secret";// helpers/profile.spec.tsx
import "./testEnv"; // first, before anything that reads the secret
import { handle } from "../endpoints/user/profile_GET";
describe("profile_GET", () => {
it("refuses a request without a session", async () => {
const res = await handle(new Request("http://localhost/_api/user/profile"));
expect(res.status).toBe(401);
});
});Never put a real key in a test
Spec files are part of your project's code. Use made-up values like test-secret, never a key you stored for the live app.
A value can also be set inside one test file only: assign it in beforeAll, then load the module there with await import("../endpoints/user/profile_GET"). await works inside beforeAll and it, not at the top level of a spec file. Values a spec file sets are cleared before the next spec file runs.
Replacing functions
spyOn replaces a method on an object: globalThis.fetch, Date.now, console.error, or a method on an object your own code exports. It cannot replace a function another file exports on its own, as in spyOn(auth, "verifyToken") after import * as auth from "./auth"; that fails with verifyToken is not declared writable or has no setter. Write the code under test so it receives what it calls, as an argument or an object with default values, and pass a fake in the test.
// helpers/sendInvite.tsx
import { notify } from "./notify";
export async function sendInvite(email: string, deps = { notify }) {
await deps.notify(email);
}
// helpers/sendInvite.spec.tsx
it("notifies the invitee", async () => {
const notify = jasmine.createSpy("notify").and.resolveTo(undefined);
await sendInvite("a@example.com", { notify });
expect(notify).toHaveBeenCalledWith("a@example.com");
});Floot services in tests
When a test runs code that calls sendEmail, queueTask, flootAi.chat or any other function of an @floot/ module, that call reaches a Jasmine spy, not the service, so nothing is sent, stored or charged. Until the test gives it a result, the spy fails with sendEmail() is not available in tests. Import the same function in the spec and set its result there; the code under test sees it, and you can check how it was called.
// helpers/invite.spec.tsx
import { sendEmail } from "@floot/email";
import { sendInvite } from "./invite";
describe("sendInvite", () => {
it("emails the invitee", async () => {
(sendEmail as jasmine.Spy).and.resolveTo({ ok: true, messageId: "test" });
await sendInvite("a@example.com");
expect(sendEmail).toHaveBeenCalledWith(
jasmine.objectContaining({ to: "a@example.com" }),
);
});
});- Use
.and.resolveTo(…)for a function that returns a promise,.and.returnValue(…)for one that does not,.and.rejectWith(…)or.and.throwError(…)to test a failure, and.and.callFake(…)to decide per call. - Every spy goes back to failing, with no recorded calls, before each test, so set results in the test or in
beforeEach. - Error classes such as
AppCallErrororFlootAiRateLimitErrorare real classes, so code that checksinstanceofworks. - Constants that only the live service knows, such as
FIXED_IP_ADDRESSES, fail when read. Text constants such as the Google and Microsoft scope names hold a placeholder. - The Floot editor's own checker still skips these specs; they run when your assistant uses
run_tests.
What carries over between spec files
Each spec file loads its own fresh copy of your project's files, so a value one file changes in a module of yours does not reach the next file. npm packages are loaded once per run and shared, and tests in a file always run in the order they are written.