diff --git a/e2e/today-just-in.spec.ts b/e2e/today-just-in.spec.ts
index 2583b460d..6c8853330 100644
--- a/e2e/today-just-in.spec.ts
+++ b/e2e/today-just-in.spec.ts
@@ -162,8 +162,17 @@ test("a fresh arrival flips the day in place", async ({ page }) => {
// swept by the same `e2e-just-in-` purge on the next run.
const priorNight = new Date(Date.now() - 3 * 24 * 60 * 60_000);
const priorWoke = new Date(priorNight.getTime() + 7 * 60 * 60_000);
+ //
+ // Tagged `foreground`, the trigger an open app sends. Since v1.42 a batch
+ // without it only marks the dashboard snapshot stale, and `cachedSwr`
+ // serves the stale cell once while it rebuilds: a snapshot an earlier
+ // spec left for this account (no sleep at all, so no sleep expected)
+ // would then paint the BEFORE state as `final`, depending on which spec
+ // happened to run first in the shard. `foreground` evicts, so the first
+ // read below is built from the rows this spec just wrote.
const priorBatch = await page.request.post("/api/measurements/batch", {
data: {
+ syncTrigger: "foreground",
entries: [
{
hkIdentifier: "HKCategoryTypeIdentifierSleepAnalysis",
@@ -229,8 +238,11 @@ test("a fresh arrival flips the day in place", async ({ page }) => {
const wokeAt = new Date(now.getTime() - 60 * 60_000);
const fellAsleepAt = new Date(wokeAt.getTime() - 7 * 60 * 60_000);
+ // `foreground` for the same reason as the prior night above: the page
+ // is open while it lands.
const batch = await page.request.post("/api/measurements/batch", {
data: {
+ syncTrigger: "foreground",
entries: [
{
hkIdentifier: "HKCategoryTypeIdentifierSleepAnalysis",
diff --git a/e2e/v138-managed-record-capabilities.spec.ts b/e2e/v138-managed-record-capabilities.spec.ts
index adedc711f..2a7207059 100644
--- a/e2e/v138-managed-record-capabilities.spec.ts
+++ b/e2e/v138-managed-record-capabilities.spec.ts
@@ -26,6 +26,7 @@
* the same ones, for the same reasons written there.
*/
import type { Page } from "@playwright/test";
+import pg from "pg";
import { expect, test } from "./setup/test";
import {
@@ -111,15 +112,38 @@ test.describe.serial("a guardian's controls inside a managed profile", () => {
if (!managed) throw new Error("managed record fixture is missing");
test("offers the mood add control and lands the entry", async ({ page }) => {
+ // The profile starts every run without mood entries. The form stamps an
+ // entry to the minute and `(user, date, moodLoggedAt)` is unique, so a
+ // second run inside the same minute (a retry, a repeat) met a 409 for the
+ // entry the first one wrote. An empty record also fixes which control the
+ // page offers: the empty state's add, not the header's.
+ const dbUrl = process.env.DATABASE_URL;
+ test.skip(!dbUrl, "DATABASE_URL is required to reset the profile's mood");
+ const pool = new pg.Pool({ connectionString: dbUrl });
+ try {
+ await pool.query(
+ `DELETE FROM mood_entries
+ WHERE user_id = (SELECT id FROM users WHERE username = $1)`,
+ [managed.username],
+ );
+ } finally {
+ await pool.end();
+ }
+
await openRecord(page, managed.username);
const banner = page.locator('[data-slot="shared-record-banner"]');
await expect(banner).toHaveAttribute("data-record-kind", "managed");
await expect(banner).toHaveAttribute("data-access-level", "manage");
await page.goto("/mood");
- const add = page.locator('[data-slot="mood-add-entry"]');
- await expect(add).toBeVisible({ timeout: 30_000 });
- await add.click();
+ // Since v1.42 an empty list carries the one add control in its empty
+ // state and the header drops its own; the header's returns once there is
+ // an entry to list. Both are asserted on their data-slots.
+ const addFirst = page.locator('[data-slot="mood-add-first"]');
+ const addHeader = page.locator('[data-slot="mood-add-entry"]');
+ await expect(addFirst).toBeVisible({ timeout: 30_000 });
+ await expect(addHeader).toHaveCount(0);
+ await addFirst.click();
const face = page.locator('[data-slot="mood-face"][data-mood="GUT"]');
await expect(face).toBeVisible();
@@ -139,6 +163,8 @@ test.describe.serial("a guardian's controls inside a managed profile", () => {
await expect(page.locator('[data-slot="mood-rows"]').first()).toBeVisible({
timeout: 30_000,
});
+ await expect(addHeader).toBeVisible();
+ await expect(addFirst).toHaveCount(0);
await leaveRecord(page);
});
diff --git a/src/__tests__/retired-route-links-guard.test.ts b/src/__tests__/retired-route-links-guard.test.ts
index b5d11d6d5..874c20989 100644
--- a/src/__tests__/retired-route-links-guard.test.ts
+++ b/src/__tests__/retired-route-links-guard.test.ts
@@ -24,6 +24,11 @@ function walk(dir: string): string[] {
});
}
+/** Every regex metacharacter escaped, the backslash included. */
+function escapeRegExp(text: string): string {
+ return text.replace(/[\\^$.*+?()[\]{}|/]/g, "\\$&");
+}
+
describe("no link targets a retired route", () => {
const files = walk(join(ROOT, "src"));
@@ -33,7 +38,7 @@ describe("no link targets a retired route", () => {
it.each(Object.entries(RETIRED))("%s is linked as %s", (route) => {
const pattern = new RegExp(
- `(?:href|push|replace)[=(]\\s*\\{?\\s*["'\`]${route.replace(/\//g, "\\/")}["'\`?#]`,
+ `(?:href|push|replace)[=(]\\s*\\{?\\s*["'\`]${escapeRegExp(route)}["'\`?#]`,
);
const offenders = files
.filter((file) => pattern.test(readFileSync(file, "utf8")))
diff --git a/src/components/day/__tests__/use-day-value-format.test.tsx b/src/components/day/__tests__/use-day-value-format.test.tsx
index 7fbca9381..e7f1722b4 100644
--- a/src/components/day/__tests__/use-day-value-format.test.tsx
+++ b/src/components/day/__tests__/use-day-value-format.test.tsx
@@ -42,22 +42,38 @@ function Probe({
);
}
+/**
+ * The text of rendered markup: every character outside a `<…>` tag. A walk
+ * rather than a tag-stripping regex, which can leave a tag behind when one
+ * is split across another.
+ */
+function textOf(html: string): string {
+ let out = "";
+ let inTag = false;
+ for (const ch of html) {
+ if (ch === "<") inTag = true;
+ else if (ch === ">") inTag = false;
+ else if (!inTag) out += ch;
+ }
+ return out;
+}
+
function read(
type: string,
value: number,
unit: string,
locale: "en" | "de" = "en",
) {
- return renderToStaticMarkup(
-
-
- ,
- )
- .replace(/<[^>]+>/g, "")
- .replace(//g, "");
+ return textOf(
+ renderToStaticMarkup(
+
+
+ ,
+ ),
+ );
}
describe("a day's values in their charts' spelling", () => {
diff --git a/src/components/insights/__tests__/health-score-card.test.tsx b/src/components/insights/__tests__/health-score-card.test.tsx
index e27243b14..710eef051 100644
--- a/src/components/insights/__tests__/health-score-card.test.tsx
+++ b/src/components/insights/__tests__/health-score-card.test.tsx
@@ -48,6 +48,22 @@ const coverage = {
missing: ["ACTIVITY", "SLEEP"],
};
+/**
+ * The text of rendered markup: every character outside a `<…>` tag. A walk
+ * rather than a tag-stripping regex, which can leave a tag behind when one
+ * is split across another.
+ */
+function textOf(html: string): string {
+ let out = "";
+ let inTag = false;
+ for (const ch of html) {
+ if (ch === "<") inTag = true;
+ else if (ch === ">") inTag = false;
+ else if (!inTag) out += ch;
+ }
+ return out;
+}
+
function render(node: React.ReactNode, locale: Locale = "en"): string {
return renderToStaticMarkup(
{node},
@@ -642,9 +658,7 @@ describe(" composite states", () => {
// The onset is an instant; it reads as the day it falls on in the
// profile timezone, in the user's date format, never as raw ISO.
// v1.42 — the date is a link to its day; the sentence reads the same.
- expect(rest.replace(/<[^>]+>/g, "")).toMatch(
- /Rest Mode active since \d{2}\/\d{2}\/2026\./,
- );
+ expect(textOf(rest)).toMatch(/Rest Mode active since \d{2}\/\d{2}\/2026\./);
expect(rest).not.toContain("T23:30:00");
expect(rest).toContain('data-slot="health-score-tension"');
expect(rest).toContain('data-slot="health-score-return-to-band"');
@@ -672,7 +686,7 @@ describe(" composite states", () => {
/>,
);
const rest = atRest(html);
- expect(rest.replace(/<[^>]+>/g, "")).toMatch(
+ expect(textOf(rest)).toMatch(
/Rest Mode active since (07\/20\/2026|20\/07\/2026)/,
);
// The date opens the day it names, in the profile timezone.
diff --git a/src/components/mood/mood-list.tsx b/src/components/mood/mood-list.tsx
index eab8e9de8..d97fe4327 100644
--- a/src/components/mood/mood-list.tsx
+++ b/src/components/mood/mood-list.tsx
@@ -742,7 +742,11 @@ export function MoodList({ onAddFirst, onEmptyChange }: MoodListProps = {}) {
{t("mood.emptyResetFilter")}
) : onAddFirst && canManageMind ? (
-