diff --git a/crates/ui/e2e/pages/api.ts b/crates/ui/e2e/pages/api.ts index 6f83e1dc2..916b3431c 100644 --- a/crates/ui/e2e/pages/api.ts +++ b/crates/ui/e2e/pages/api.ts @@ -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 { + 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 diff --git a/crates/ui/e2e/tests/queries.spec.ts b/crates/ui/e2e/tests/queries.spec.ts index 0ca55b34a..ccd4b7111 100644 --- a/crates/ui/e2e/tests/queries.spec.ts +++ b/crates/ui/e2e/tests/queries.spec.ts @@ -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, @@ -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); }); diff --git a/crates/ui/e2e/tests/search-parameters.spec.ts b/crates/ui/e2e/tests/search-parameters.spec.ts index 41be51a4a..fa07e85bc 100644 --- a/crates/ui/e2e/tests/search-parameters.spec.ts +++ b/crates/ui/e2e/tests/search-parameters.spec.ts @@ -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 @@ -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();