Testing

In this section

Run check-ins without a real wallet, test against known-good and deliberately broken counterparts, and read what went wrong when a check-in fails.

Never offer the mock wallet or the testing tools to patients.

The mock wallet

mockWallet() is a Wallet that answers at once, with no consent screen. It runs the real wire layer: it signs and encrypts a response, and runCheckin opens and validates it like any other.

import { runCheckin } from "@smart-health-checkin/client";
import { mockWallet } from "@smart-health-checkin/client/testing";

const result = await runCheckin(request, { wallet: mockWallet() });

With no options it makes up plausible data for every item. Pin exact answers per item with items:

const wallet = mockWallet({
  items: {
    allergies: { fhir: myAllergyBundle },
    coverage: { status: "declined" },
  },
  fallback: { status: "unavailable" },
});

Per-item answers

Spec What the item gets
{ fhir: resourceOrBundle } One application/fhir+json artifact with that value.
{ fhir, fhirVersion } The same, with a FHIR version other than 4.0.1.
{ healthCard: [jws, …] } One application/smart-health-card artifact carrying those cards.
{ status: "declined" } A status and no artifact. Also unavailable, unsupported, error, partial.
{ status, message } The same, with a message.
[spec, spec] Several artifacts for one item.

Add alsoFulfills: ["otherItem"] to a fhir or healthCard spec when one artifact answers several items. Those items are then reported fulfilled without an artifact of their own.

Other options

Option What it does
fallback What items not named in items get: "fabricate" (the default) or a spec applied to all of them.
respond (request) => response: build the whole response yourself.
origin The origin the response is bound to. Defaults to location.origin.

Without the wire layer

To test your own code on a response, skip sealing and opening entirely:

Function Returns
buildMockResponse(request, { items, fallback }) The response the mock wallet would send, as plain JSON.
fabricateResponse(request) Made-up data for every item of any request.

In unit tests

runCheckin binds the response to the page's origin, so it needs location. Browsers have it. In Bun or Node, set a stand-in before the first check-in:

(globalThis as { location?: unknown }).location ??= {
  origin: "https://ehr.example",
  href: "https://ehr.example/checkin",
};

Then test outcomes directly:

import { expect, test } from "bun:test";
import { runCheckin } from "@smart-health-checkin/client";
import { mockWallet } from "@smart-health-checkin/client/testing";

test("a declined item is reported, the rest still arrives", async () => {
  const result = await runCheckin(request, {
    wallet: mockWallet({ items: { allergies: { fhir: myAllergyBundle }, coverage: { status: "declined" } } }),
  });
  if (result.status !== "completed") throw new Error(result.status);
  expect(result.response.status("coverage")).toBe("declined");
  expect(result.response.resources("allergies", { type: "AllergyIntolerance" })).toHaveLength(1);
});

To test failures, wrap a transport that throws with customWallet:

import { CheckinError, customWallet } from "@smart-health-checkin/client";

const blocked = customWallet({
  id: "blocked",
  name: "Blocked",
  open: () => ({
    getCredential: async () => {
      throw new CheckinError("blocked", "tab blocked");
    },
    cancel() {},
  }),
});

The mock in a page

Add the mock attribute to the picker. It offers a "Simulated response" option after the real ones.

<smart-checkin-picker registry="/wallets.json" mock></smart-checkin-picker>

Health cards when testing

By default only cards from trusted issuers reach resources(), and test issuers usually aren't trusted. While testing, loosen accept to include cards from any issuer whose signature verifies, or every card, including ones whose signature fails:

import { configureHealthCardTrust } from "@smart-health-checkin/client";

configureHealthCardTrust({ accept: "any-valid" });
configureHealthCardTrust({ accept: "everything" });

To trust one test issuer without fetching its keys, pass them instead: configureHealthCardTrust({ keys: { [issuer]: jwks } }). SMART Health Cards explains each setting and how to see every card's result.

The connectathon tools

Two hosted tools let you test against a known-good counterpart.

Testing EHR

https://smart-health-checkin.org/connectathon/testing-ehr/

SMART Testing Wallet

https://smart-health-checkin.org/connectathon/testing-wallet/

A web wallet with synthetic patients. It can send a deliberately broken or very large response, so you can check your page's error handling.

To test your own page: open the wallet, set faults or a response size in its testing panel, choose "Copy wallet URL for these settings", and add that URL to your page's wallet list as a web wallet, for example with webWallet(). Then run check-ins from your page as usual; each one gets those options, and the wallet's approval screen shows them. For example, bad signature and 5 MB response. These config URLs are the Testing Wallet's own format, not part of SMART Health Check-in: your page opens them like any wallet URL. The Testing EHR can build them too, under "Testing Wallet options".

Fault What the wallet sends
wrong-canonical A QuestionnaireResponse whose questionnaire drops the version or changes the URL
missing-status One item with no status
duplicate-status One item with two statuses
wrong-request-id A requestId that doesn't match
unaccepted-media-type An artifact in a type the item didn't accept
bad-signature A corrupted issuer signature
bad-encryption A corrupted HPKE ciphertext
wrong-origin A transcript bound to a different origin
bad-shc-signature A SMART Health Card with a broken signature
combine-allergies-meds Allergies and medications in one shared Bundle. A valid response, for scenario shared-artifact.

What this library does with each, and what your page should do (spec §6.4 and §8.5):

The wallet's response size setting makes a valid response of about 512 KB, 1 MB, 2 MB, or 5 MB, by adding earlier records such as past lab results to one item's Bundle. Use it to check that your page handles a large response the way it handles a small one.

Reading a failed result

A failed result has error.code, error.message, and sometimes error.check.

if (result.status === "failed") console.log(result.error.code, result.error.check, result.error.message);
Code What happened What to check
unsupported This browser can't reach that wallet. The wallet was never opened. wallet.available and wallet.unavailableReason. For the platform wallet, detectDcApiSupport().
blocked The browser blocked the wallet's tab. That start() or runCheckin() runs inside the click, before any await.
timeout The web wallet didn't answer in 5 minutes. That the wallet posts ready to its opener and replies with the same requestId.
wallet-error The wallet reported an error, or its transport threw. The message: it's the wallet's own words.
invalid-response The response couldn't be decoded or decrypted, had no SMART response in it, or answers a different request. error.check, the spec requirement that failed: VRS-3 didn't decrypt (usually the wrong origin), VRS-2/VRS-4/VRS-8 couldn't find the parts, XV-1/XV-2 a response that isn't a reply to this request.
server Server-held keys failed to prepare or open the request. Your key server's logs.

A declined check-in is not a failure: result.status is "declined", with no error.

The demos

Each demo page runs the real flow and shows one way to use the library. Source is in demo/src/.

Page What it shows
Clinic check-in A clinic website's check-in page: the picker, the raw response, the optional FHIR helper
Tutorial's finished page The finished page from the tutorial: the picker, a request, and a form filled from the answer
React <CheckinPicker> and response.resources("meds", { type: "MedicationRequest" })
Angular A small service over wallets() and wallet.start
Form autofill The picker, then prefill from response.resources and ask only for what's missing
Kiosk and its phone page handoffWallet(), and the picker in pick mode with answerHandoff
Demo wallet (source) A web wallet built on serveWebWallet

The clinic check-in demo takes options in the URL fragment, such as #wallet=mock to offer only the simulated response or #wallet=platform to offer only the device's own wallet. Clinic check-in demo options lists them.