Skip to content
Merged
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
15 changes: 13 additions & 2 deletions crates/ui/assets/saved-queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@
var etag = null;
var lang = document.documentElement.lang || undefined;

/* Result-header counts follow the page's own locale (#1426), the same way
* `whenText` localizes dates: `lang` is the negotiated `<html lang>`, and
* an absent attribute leaves the choice to the platform. Only the rendered
* text is grouped — the wire query, `Bundle.total`, paging, and the numbers
* the script keeps for itself are untouched. */
function formatCount(value) {
var count = Number(value);
if (!Number.isFinite(count)) return String(value);
return count.toLocaleString(lang);
}

function fetchDocument() {
return fetch(SETTINGS, {
headers: { Accept: "application/json" },
Expand Down Expand Up @@ -2589,11 +2600,11 @@
!hasTotal && hasNext
? results.card.dataset.msgTotalPartial
: results.card.dataset.msgTotal
).replace("{count}", total);
).replace("{count}", formatCount(total));
if (included > 0)
meta +=
" · " +
results.card.dataset.msgIncluded.replace("{count}", included);
results.card.dataset.msgIncluded.replace("{count}", formatCount(included));

var columns = elementColumns(context.query);
/* No _elements: one column per attribute the server actually returned, so
Expand Down
90 changes: 90 additions & 0 deletions crates/ui/e2e/tests/resources.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,96 @@ test("the results heading sits on the same row as its count", async ({ resources
expect(geometry.headingMarginBottom).toBe("0px");
});

// #1426: the results header groups digits for the active UI locale — English
// 12345 reads "12,345 results", Spanish "12.345 resultados", German "12.345
// Ergebnisse" — while the typed search still goes out exactly as before. The
// Bundles are intercepted: five digits and a grouped include count would
// otherwise mean seeding thousands of real resources.
test("the results header groups counts in the active locale", async ({ page, resources }) => {
const patient = (id: string) => ({ resource: { resourceType: "Patient", id } });
const searchset = (
total: number | null,
entry: Array<{ resource: { resourceType: string; id: string } }>,
next = false,
) => ({
resourceType: "Bundle",
type: "searchset",
...(total === null ? {} : { total }),
...(next ? { link: [{ relation: "next", url: "/Patient?_id=more&page=2" }] } : {}),
entry,
});

// One route serves every case: the run's own `_id` picks its Bundle, and
// anything else (the page's default Patient listing) answers empty.
const bundles: Record<string, unknown> = {
// Five digits is the smallest exact count *all* the locales below group:
// Spanish CLDR leaves four digits ungrouped ("1234 resultados").
"count-en": searchset(12345, [patient("count-en")]),
"count-es": searchset(12345, [patient("count-es")]),
"count-de": searchset(12345, [patient("count-de")]),
// One page whose total matches its entries: the include count runs
// through the same formatter.
"count-included": searchset(1235, [
patient("count-included"),
...Array.from({ length: 1234 }, (_, index) => ({
resource: { resourceType: "Organization", id: `org-${index}` },
})),
]),
// No `Bundle.total` with a next page: the page count is grouped and keeps
// its `+` wording (#1003).
"count-partial": searchset(
null,
Array.from({ length: 1234 }, (_, index) => patient(`p-${index}`)),
true,
),
"count-small": searchset(42, [patient("count-small")]),
"count-zero": searchset(0, []),
};
const wire: string[] = [];
await page.route(
(url) => url.pathname.endsWith("/Patient") && url.search !== "",
async (route) => {
const url = new URL(route.request().url());
const marker = url.searchParams.get("_id") || "";
if (marker) wire.push(url.search);
await route.fulfill({
status: 200,
contentType: "application/fhir+json",
body: JSON.stringify(bundles[marker] ?? searchset(0, [])),
});
},
);

const scenarios = [
{ lang: "en", marker: "count-en", expected: "12,345 results" },
{ lang: "es", marker: "count-es", expected: "12.345 resultados" },
{ lang: "de", marker: "count-de", expected: "12.345 Ergebnisse" },
{ lang: "en", marker: "count-included", expected: "1,235 results · 1,234 included" },
{ lang: "en", marker: "count-partial", expected: "1,234+ results" },
{ lang: "en", marker: "count-small", expected: "42 results" },
{ lang: "en", marker: "count-zero", expected: "0 results" },
] as const;
for (const scenario of scenarios) {
// `?lang=` is the page's own language switch (the fixture starts from a
// clean cookie jar), so each case states the locale it asserts.
await page.goto(`/ui/resources?type=Patient&lang=${scenario.lang}`, {
waitUntil: "networkidle",
});
await expect(page.locator("html")).toHaveAttribute("lang", scenario.lang);
await switchToBuilderMode(resources);
await resources.results.waitShown();

await resources.builder.run(`Patient?_id=${scenario.marker}`);
await expect(resources.results.meta).toHaveText(scenario.expected);
}

// Grouping is a rendering concern only: every run still asked for exactly
// the query the user typed, plus the existing `_total=accurate` (#1003).
await expect
.poll(() => wire)
.toEqual(scenarios.map((scenario) => `?_id=${scenario.marker}&_total=accurate`));
});

test("selecting a type updates the Create label and the URL", async ({ resources, page }) => {
await resources.goto("Patient");
await expect(resources.createLabel).toHaveText("Create new Patient");
Expand Down
Loading