Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions crates/ui/e2e/pages/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,46 @@ export async function readResource(
return res.json();
}

/**
* Wait until a just-created resource is visible to FHIR search.
*
* On composite backends (sqlite/s3 + Elasticsearch) create returns as soon as
* the write store acknowledges; the search index can lag a few hundred ms.
* Nightly ui-tests-matrix flakes when the next step searches immediately.
*/
export async function waitForSearchHit(
request: APIRequestContext,
type: string,
query: string,
opts: { timeoutMs?: number } = {},
): Promise<void> {
const timeoutMs = opts.timeoutMs ?? 15_000;
const deadline = Date.now() + timeoutMs;
let lastStatus = 0;
let lastTotal = "n/a";
while (Date.now() < deadline) {
const res = await request.get(`/${type}?${query}`, {
headers: { Accept: FHIR_JSON },
});
lastStatus = res.status();
if (res.ok()) {
const bundle = await res.json();
const total =
typeof bundle.total === "number"
? bundle.total
: Array.isArray(bundle.entry)
? bundle.entry.length
: 0;
lastTotal = String(total);
if (total > 0) return;
}
await new Promise((r) => setTimeout(r, 200));
}
throw new Error(
`timed out waiting for ${type}?${query} (last status=${lastStatus}, total=${lastTotal})`,
);
}

/**
* Create a resource and immediately update it, leaving two versions — the
* minimum a history diff needs. Returns the id. `mutate` produces the second
Expand Down
11 changes: 9 additions & 2 deletions crates/ui/e2e/tests/queries.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "../pages/fixtures";
import { createResource } from "../pages/api";
import { createResource, waitForSearchHit } from "../pages/api";

// The Saved Queries workspace (/ui/queries): the shared query builder and the
// in-page results table — run a query, add builder rows, page through results,
Expand Down Expand Up @@ -68,12 +68,19 @@ test.describe("query builder", () => {
subject: { reference: `Patient/${patient}` },
});

// ES composite backends need the Observation (+ Practitioner name) indexed
// before chained/_has search can see them.
await waitForSearchHit(request, "Observation", `code=chain-94-${tag}`);
await waitForSearchHit(request, "Practitioner", `name=ChainSmith${tag}`);

await queries.goto();
await queries.builder.run(
`Patient?_has:Observation:patient:code=chain-94-${tag}&general-practitioner.name=ChainSmith${tag}`,
);
await queries.results.waitShown();
await expect(queries.results.rows).toHaveCount(1);
await expect
.poll(async () => queries.results.rows.count(), { timeout: 15_000 })
.toBe(1);
await expect(queries.results.rows.first()).toContainText(patient);
});

Expand Down
18 changes: 12 additions & 6 deletions crates/ui/e2e/tests/search-parameters.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "../pages/fixtures";
import { createResource } from "../pages/api";
import { createResource, waitForSearchHit } from "../pages/api";

// The SearchParameter registry viewer (/ui/search-parameters): the htmx filter
// rail, the type/source facet chips, row selection into the detail panel, and
Expand Down Expand Up @@ -48,26 +48,32 @@ test("a stored parameter can be created, offers Edit, and deletes", async ({
}) => {
const stamp = Date.now();
const url = `http://example.org/e2e/SearchParameter/crud-${stamp}`;
const code = `e2e-crud-${stamp}`;
const id = await createResource(request, "SearchParameter", {
url,
name: "e2eCrud",
code: `e2e-crud-${stamp}`,
code,
status: "active",
type: "token",
base: ["Patient"],
expression: "Patient.identifier",
});

// Composite ES backends index asynchronously; wait before UI refresh.
await waitForSearchHit(request, "SearchParameter", `url=${encodeURIComponent(url)}`);

// refresh=1 drops the server's cached snapshot so the new parameter shows.
await searchParameters.goto(`?refresh=1&sel=${encodeURIComponent(url)}`);
await expect(page.locator(".page-head__actions a.btn--primary")).toHaveAttribute(
"href",
"/ui/editor?type=SearchParameter",
);
await expect(page.locator(".detail__actions a.btn")).toHaveAttribute(
"href",
`/ui/editor?type=SearchParameter&id=${id}`,
);
// Detail actions can land a beat after the registry swap on slow backends.
await expect
.poll(async () => page.locator(".detail__actions a.btn").getAttribute("href"), {
timeout: 15_000,
})
.toBe(`/ui/editor?type=SearchParameter&id=${id}`);

page.once("dialog", (d) => d.accept());
await page.locator(".detail__actions [data-crud-delete]").click();
Expand Down