Files
filip131311 98fe7a86fc test(profiler): type component-names tests with the module's exported types (#771)
Unbreaks the **Dead Code** gate on main, red since #663 merged.

## What happened

#663 branched before the knip workflow existed (#572), so its own PR
checks never ran the gate. Its `component-names.ts` exports three types
with no importers — `StrippedName`, `ComponentAnnotation`,
`ComponentNameResolution` — which pushed the knip second pass to **218**
parked issues against the **215** ceiling. Every open PR now inherits
the red check (e.g. #687, whose own commit adds no parked items).

Local runs hid this: counted the way `knip.jsonc` prescribes — against a
tree with **no build output** — the failure reproduces exactly.

## The fix

Import the types where the tests naturally need them, which also removes
a wart: the two `renderComponentNameMiss(r as never)` casts existed
precisely because the narrowing `ComponentNameResolution` enables was
never written. The new `expectMiss` helper narrows properly and fails
loudly if the resolver ever stops returning a miss.

No ceiling change: 47 + 156 + 12 = **215**, exactly at the limit. Note
the budget now has zero headroom — the next PR that parks an export will
trip it, which is the gate working as designed.

## Checks

- clean-tree `npm run knip` exit 0 (both passes)
- component-names tests 18/18, tool-server test typecheck clean,
prettier clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-11 15:37:28 +02:00

196 lines
7.7 KiB
TypeScript

import { describe, it, expect } from "vitest";
import type {
ComponentAnnotation,
ComponentNameResolution,
StrippedName,
} from "../../src/utils/react-profiler/component-names";
import {
stripComponentWrappers,
annotateComponentName,
resolveComponentName,
renderComponentNameMiss,
astLookupCandidates,
} from "../../src/utils/react-profiler/component-names";
/**
* Names taken verbatim from a real Expo SDK 54 profiling session (RN 0.81.5,
* React Compiler on) — the session that produced issue #632. Includes the one
* genuine collision it contained and a router name whose inner text has
* parentheses, dots and slashes.
*/
const REAL_SESSION_NAMES = [
"Forget(ParallaxScrollView)",
"Forget(Collapsible)",
"Forget(IconSymbol)",
"Forget(ThemedText)",
"Forget(BottomTabItem)",
"Forget(TabTwoScreen(./(tabs)/explore.tsx))",
"StaticContainer",
"Memo(StaticContainer)",
"Anonymous",
"Animated(Anonymous)",
"Animated(View)",
"View",
"Context.Provider",
"InnerScreen",
"ForwardRef(InnerScreen)",
];
describe("stripComponentWrappers", () => {
it("strips the three wrappers React DevTools adds, including nested ones", () => {
expect(stripComponentWrappers("Forget(Foo)").baseName).toBe("Foo");
expect(stripComponentWrappers("Memo(Foo)").baseName).toBe("Foo");
expect(stripComponentWrappers("ForwardRef(Foo)").baseName).toBe("Foo");
expect(stripComponentWrappers("Memo(Forget(Foo))").baseName).toBe("Foo");
expect(stripComponentWrappers("Forget(Memo(ForwardRef(Memo(Deep))))").baseName).toBe("Deep");
});
it("handles an inner name containing parens, dots and slashes", () => {
// expo-router names a screen after its file, so the greedy anchored match
// must not stop at the first inner ")".
expect(stripComponentWrappers("Forget(TabTwoScreen(./(tabs)/explore.tsx))").baseName).toBe(
"TabTwoScreen(./(tabs)/explore.tsx)"
);
});
it("leaves wrappers the report never strips alone", () => {
// Stripping these would make a query for `View` ambiguous in almost every
// session, and no agent ever holds a stripped form of them.
for (const n of ["Animated(View)", "AnimatedComponent(View)", "Motion(View)", "Anonymous"]) {
expect(stripComponentWrappers(n).baseName).toBe(n);
}
expect(stripComponentWrappers("Context.Provider").baseName).toBe("Context.Provider");
});
it("reports which wrappers were present", () => {
const s: StrippedName = stripComponentWrappers("Memo(Forget(Foo))");
expect(s.hasMemo).toBe(true);
expect(s.hasForget).toBe(true);
expect(s.hasForwardRef).toBe(false);
});
});
describe("annotateComponentName", () => {
it("keeps the tag wording and ordering the report has always used", () => {
const ann: ComponentAnnotation = annotateComponentName("Forget(Foo)");
expect(ann).toEqual({
displayName: "Foo",
tag: " [React Compiler]",
rawName: "Forget(Foo)",
});
expect(annotateComponentName("Memo(Forget(ForwardRef(Foo)))").tag).toBe(
" [React.memo + React Compiler + forwardRef]"
);
expect(annotateComponentName("View").tag).toBe("");
});
});
describe("resolveComponentName", () => {
it("resolves the display name the report prints — the issue", () => {
const r = resolveComponentName("ParallaxScrollView", REAL_SESSION_NAMES);
expect(r.kind).toBe("display");
expect(r.kind === "display" && r.rawName).toBe("Forget(ParallaxScrollView)");
});
it("still resolves the raw name unchanged", () => {
const r = resolveComponentName("Forget(ParallaxScrollView)", REAL_SESSION_NAMES);
expect(r.kind).toBe("exact");
expect(r.kind === "exact" && r.rawName).toBe("Forget(ParallaxScrollView)");
});
it("lets an exact match win over a wrapped sibling, and never widens it", () => {
// Both `StaticContainer` and `Memo(StaticContainer)` were recorded in one
// real session. Asking for the bare one must return the bare one.
const r = resolveComponentName("StaticContainer", REAL_SESSION_NAMES);
expect(r.kind).toBe("exact");
expect(r.kind === "exact" && r.rawName).toBe("StaticContainer");
// ...but the caller is told the other fiber exists, instead of silently
// seeing one of two.
expect(r.kind === "exact" && r.alsoMatching).toEqual(["Memo(StaticContainer)"]);
});
it("does not cross-resolve one wrapper into another", () => {
// Matching on the recorded name's DISPLAY form (not a stripped query)
// means asking for a Forget() fiber never silently returns a Memo() one.
const r = resolveComponentName("Forget(StaticContainer)", REAL_SESSION_NAMES);
expect(r.kind).toBe("missing");
});
it("does not resolve a non-stripped wrapper from its inner name", () => {
const r = resolveComponentName("View", REAL_SESSION_NAMES);
expect(r.kind).toBe("exact");
expect(r.kind === "exact" && r.rawName).toBe("View");
// `Animated(View)` is a different component and must not be pulled in.
expect(r.kind === "exact" && r.alsoMatching).toEqual([]);
});
it("refuses rather than merging when several fibers share a display name", () => {
const names = ["Memo(Widget)", "ForwardRef(Widget)"];
const r = resolveComponentName("Widget", names);
expect(r.kind).toBe("ambiguous");
expect(r.kind === "ambiguous" && r.candidates.sort()).toEqual([
"ForwardRef(Widget)",
"Memo(Widget)",
]);
});
it("reports a miss with re-queryable suggestions", () => {
const r = resolveComponentName("ParallaxScroll", REAL_SESSION_NAMES);
expect(r.kind).toBe("missing");
expect(r.kind === "missing" && r.suggestions).toContain("Forget(ParallaxScrollView)");
});
});
// Narrow a resolution to the two outcomes renderComponentNameMiss accepts,
// failing the test if the resolver returned anything else.
function expectMiss(
r: ComponentNameResolution
): Extract<ComponentNameResolution, { kind: "ambiguous" | "missing" }> {
if (r.kind !== "ambiguous" && r.kind !== "missing") {
throw new Error(`expected a non-resolving outcome, got "${r.kind}"`);
}
return r;
}
describe("renderComponentNameMiss", () => {
it("hands back exact strings to retry with on an ambiguity", () => {
const r = resolveComponentName("Widget", ["Memo(Widget)", "ForwardRef(Widget)"]);
const out = renderComponentNameMiss(expectMiss(r));
expect(out).toContain("ambiguous");
expect(out).toContain("`Memo(Widget)`");
expect(out).toContain("`ForwardRef(Widget)`");
expect(out).toContain("are not merged");
});
it("quantifies the session so a miss can be told from an empty capture", () => {
const r = resolveComponentName("Nope", REAL_SESSION_NAMES);
const out = renderComponentNameMiss(expectMiss(r), { fiberRenders: 313, commits: 16 });
expect(out).toContain("313 fiber renders across 16 commits");
expect(out).toContain("profiler-commit-query");
});
});
describe("astLookupCandidates", () => {
it("offers the bare source identifier for a wrapped name", () => {
expect(astLookupCandidates("Forget(IconSymbol)")).toEqual(["Forget(IconSymbol)", "IconSymbol"]);
});
it("offers the declared identifier for an expo-router screen name", () => {
// The report shows `TabTwoScreen(./(tabs)/explore.tsx)`; the source declares
// `export default function TabTwoScreen()`.
expect(astLookupCandidates("Forget(TabTwoScreen(./(tabs)/explore.tsx))")).toEqual([
"Forget(TabTwoScreen(./(tabs)/explore.tsx))",
"TabTwoScreen(./(tabs)/explore.tsx)",
"TabTwoScreen",
]);
});
it("does not invent variants for a plain name", () => {
expect(astLookupCandidates("IconSymbol")).toEqual(["IconSymbol"]);
});
it("only drops a suffix when what remains is a valid identifier", () => {
expect(astLookupCandidates("Context.Provider")).toEqual(["Context.Provider"]);
});
});