mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
test(guards): make path-pinned source reads fail when their subject moves
A test that reads a source file by path and asserts an absence
(`expect(source).not.toMatch(…)`) stays green after a split: the code it
watched moved next door and the old path no longer holds it. A missing file
throws; a file that kept its name but lost its contents does not.
- `readSource(path, anchors)` in helpers/source-files.ts reads one file and
requires every anchor (a string or pattern the watched code carries) to
match at least once; it throws on a missing file, a lost anchor, or a read
without an anchor.
- `readSourceFolder(dir, { floor })` is the folder-scan form for a subject
that was split into several files.
- The twelve reads that were asserted only negatively now go through
`readSource` with an anchor: the batch route, the integration-sync
registrar, the dashboard client, the mood insights client, the sleep
overview and sleep page, the sidebar, the baseline form, the account
section (two guards), api-handler's requireAuth and the Google Health sync.
- The two schema parsers the wipe and enum guards share now refuse a parse
of the real schema that finds fewer than 50 models (and 10 enums), so a
moved file or a drifted block regex cannot leave those guards comparing
an allowlist against nothing. Synthetic schemas a test writes stay exempt.
Proof: renaming `setSaving` in the baseline form made the converted
confirm-failure guard throw "the file no longer contains setSaving"; the
helper's own test plants a missing file, a lost string anchor, a lost regex
anchor, an empty anchor list and an under-floor folder, each of which throws.
This commit is contained in:
1 parent
c86c7d1a79
commit
f3d7ab31e2
16 files changed
+204
-44
No files matched your search
@@ -12,6 +12,7 @@ import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { isSettingsSectionSlug } from "@/lib/settings/section-slugs";
|
||||
import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
describe("sleep-overview empty-state CTA href", () => {
|
||||
it("links to /settings/integrations (a real settings slug)", () => {
|
||||
@@ -30,9 +31,9 @@ describe("sleep-overview empty-state CTA href", () => {
|
||||
});
|
||||
|
||||
it("never links to the non-existent /settings/devices slug", () => {
|
||||
const source = readFileSync(
|
||||
join(process.cwd(), "src/components/insights/sleep-overview.tsx"),
|
||||
"utf8",
|
||||
const source = readSource(
|
||||
"src/components/insights/sleep-overview.tsx",
|
||||
"<EmptyState",
|
||||
);
|
||||
expect(source).not.toContain("/settings/devices");
|
||||
});
|
||||
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
import { ADMIN_SECTIONS } from "@/components/admin/admin-shell";
|
||||
import { visibleUtilityDestinations } from "../nav-model";
|
||||
import { delegatedDomains } from "@/lib/sharing/domain-write-support";
|
||||
import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
function render({
|
||||
pathname = "/",
|
||||
@@ -565,9 +566,9 @@ describe("<SidebarNav> carries no rules for a viewport it never shows on", () =>
|
||||
// A touch screen at most 520 px tall gets the phone shell (the
|
||||
// `shell-mobile` variant), so the rail is hidden there and a class keyed
|
||||
// on that query can never apply.
|
||||
const source = readFileSync(
|
||||
join(__dirname, "..", "sidebar-nav.tsx"),
|
||||
"utf8",
|
||||
const source = readSource(
|
||||
"src/components/layout/sidebar-nav.tsx",
|
||||
"export function SidebarNav",
|
||||
);
|
||||
expect(source).not.toMatch(/pointer:coarse\)_and_\(height<=520px\)/);
|
||||
});
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
* component tests render server-side and cannot click.
|
||||
*/
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import {
|
||||
defaultOnboardingSteps,
|
||||
@@ -27,6 +25,7 @@ import {
|
||||
|
||||
import { runBaselineAttempt } from "../baseline-form";
|
||||
import { runConfirmFinish } from "../confirm-screen";
|
||||
import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
type PassableStep = Parameters<
|
||||
Parameters<typeof runConfirmFinish>[0]["passStep"]
|
||||
@@ -92,10 +91,10 @@ describe("runBaselineAttempt", () => {
|
||||
it("is the form's only way to move the pending flag", () => {
|
||||
// A second, inline `setSaving(false)` is how the flag stopped being
|
||||
// reset on one of the three exits in the first place.
|
||||
const source = readFileSync(
|
||||
join(process.cwd(), "src/components/onboarding/baseline-form.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
const source = readSource("src/components/onboarding/baseline-form.tsx", [
|
||||
"export function BaselineForm",
|
||||
"setSaving",
|
||||
]);
|
||||
expect(source).not.toContain("setSaving(false)");
|
||||
expect(source).not.toContain("setSaving(true)");
|
||||
});
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type React from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { AnamnesisSection } from "../anamnesis-section";
|
||||
import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
/**
|
||||
* #159 — the "About me" note and the free-text allergies line moved from the
|
||||
@@ -144,9 +143,9 @@ describe("Anamnese — the About-me note card (#159)", () => {
|
||||
|
||||
describe("Account settings — the About-me card is gone (#159)", () => {
|
||||
it("the account section carries no about-me mount any more", () => {
|
||||
const source = readFileSync(
|
||||
join(process.cwd(), "src/components/settings/account-section/index.tsx"),
|
||||
"utf8",
|
||||
const source = readSource(
|
||||
"src/components/settings/account-section/index.tsx",
|
||||
"export function AccountSection",
|
||||
);
|
||||
expect(source).not.toContain("AboutMeSection");
|
||||
expect(source).not.toContain("about-me-section");
|
||||
|
||||
@@ -41,6 +41,7 @@ vi.mock("@/hooks/use-auth", () => ({
|
||||
|
||||
import { PasswordCard } from "../password-card";
|
||||
import { SecuritySection } from "../security-section";
|
||||
import { readSource } from "@/__tests__/helpers/source-files";
|
||||
|
||||
const SLOT = 'data-slot="settings-password-card"';
|
||||
|
||||
@@ -108,9 +109,9 @@ describe("change-password card placement", () => {
|
||||
});
|
||||
|
||||
it("left no password form behind in the Account section", () => {
|
||||
const account = readFileSync(
|
||||
join(ROOT, "src/components/settings/account-section/index.tsx"),
|
||||
"utf8",
|
||||
const account = readSource(
|
||||
"src/components/settings/account-section/index.tsx",
|
||||
"export function AccountSection",
|
||||
);
|
||||
for (const marker of [
|
||||
"settings.passwordTitle",
|
||||
|
||||
Reference in new issue
Block a user