dslinter 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,178 @@
1
+ import { useMemo } from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import {
4
+ Table,
5
+ TableBody,
6
+ TableCell,
7
+ TableHead,
8
+ TableHeader,
9
+ TableRow,
10
+ } from "@/components/ui/table";
11
+ import {
12
+ aggregateDeclaredProps,
13
+ aggregateDefinitions,
14
+ } from "../dashboard/aggregate";
15
+ import {
16
+ buildUnusedPropSetForComponent,
17
+ ComponentPropUsageDetail,
18
+ } from "../dashboard/ComponentPropUsageDetail";
19
+ import { ComponentUsageDetails } from "../dashboard/ComponentUsageDetails";
20
+ import { FindingsList } from "../dashboard/FindingsList";
21
+ import { shortPath } from "../dashboard/paths";
22
+ import { findingsForComponent } from "../report/findingsForComponent";
23
+ import type { WorkspaceReport } from "../types/report";
24
+ import { Section } from "./Section";
25
+
26
+ type Props = {
27
+ componentId: string;
28
+ workspaceReport: WorkspaceReport | null;
29
+ reportReady: boolean;
30
+ hasPlaygroundSpec: boolean;
31
+ onBackToGovernance: () => void;
32
+ };
33
+
34
+ export function ComponentInspectPane({
35
+ componentId,
36
+ workspaceReport,
37
+ reportReady,
38
+ hasPlaygroundSpec,
39
+ onBackToGovernance,
40
+ }: Props) {
41
+ const definitions = useMemo(() => {
42
+ if (!workspaceReport) return [];
43
+ return aggregateDefinitions(workspaceReport).get(componentId) ?? [];
44
+ }, [workspaceReport, componentId]);
45
+
46
+ const declared = useMemo(() => {
47
+ if (!workspaceReport) return [];
48
+ return aggregateDeclaredProps(workspaceReport).get(componentId) ?? [];
49
+ }, [workspaceReport, componentId]);
50
+
51
+ const unusedProps = useMemo(() => {
52
+ if (!workspaceReport) return new Set<string>();
53
+ return buildUnusedPropSetForComponent(
54
+ workspaceReport,
55
+ componentId,
56
+ declared,
57
+ );
58
+ }, [workspaceReport, componentId, declared]);
59
+
60
+ const findings = useMemo(
61
+ () => findingsForComponent(workspaceReport, componentId),
62
+ [workspaceReport, componentId],
63
+ );
64
+
65
+ const previewNote = hasPlaygroundSpec
66
+ ? "A preview was expected for this component but the module could not be loaded in the dashboard bundle (check the file path, export name, or that npx dslinter was run from the project root)."
67
+ : "No playable component definition was found in a .tsx/.jsx file under the scanned workspace — only names from JSX usage appear in the catalog.";
68
+
69
+ return (
70
+ <div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background">
71
+ <div className="min-h-0 flex-1 overflow-auto">
72
+ <header className="border-b border-border bg-card px-8 py-6">
73
+ <div className="flex flex-wrap items-start justify-between gap-4">
74
+ <div className="min-w-0">
75
+ <p className="text-sm font-medium text-muted-foreground">
76
+ Components
77
+ </p>
78
+ <h1 className="text-3xl font-semibold tracking-tight text-foreground">
79
+ {componentId}
80
+ </h1>
81
+ <p className="mt-2 max-w-2xl text-sm text-muted-foreground">
82
+ Scan snapshot — no live preview. {previewNote}
83
+ </p>
84
+ </div>
85
+ <Button type="button" size="sm" variant="outline" onClick={onBackToGovernance}>
86
+ Back to governance
87
+ </Button>
88
+ </div>
89
+ </header>
90
+
91
+ <div className="mx-auto w-full max-w-5xl space-y-10 px-8 py-8">
92
+ <Section
93
+ id="definitions"
94
+ title="Definitions"
95
+ description="Source files where this component is defined."
96
+ >
97
+ {definitions.length > 0 ? (
98
+ <Table>
99
+ <TableHeader>
100
+ <TableRow>
101
+ <TableHead>File</TableHead>
102
+ <TableHead className="w-20">Line</TableHead>
103
+ <TableHead>Kind</TableHead>
104
+ </TableRow>
105
+ </TableHeader>
106
+ <TableBody>
107
+ {definitions.map((site) => (
108
+ <TableRow
109
+ key={`${site.path}:${site.line}:${site.kind}`}
110
+ >
111
+ <TableCell className="font-mono text-xs">
112
+ {workspaceReport
113
+ ? shortPath(workspaceReport.root, site.path)
114
+ : site.path}
115
+ </TableCell>
116
+ <TableCell>{site.line}</TableCell>
117
+ <TableCell className="text-muted-foreground">
118
+ {site.kind}
119
+ </TableCell>
120
+ </TableRow>
121
+ ))}
122
+ </TableBody>
123
+ </Table>
124
+ ) : (
125
+ <p className="text-sm text-muted-foreground">
126
+ No definition sites recorded — this name may appear only from JSX
127
+ usage.
128
+ </p>
129
+ )}
130
+ </Section>
131
+
132
+ <Section
133
+ id="props"
134
+ title="Props"
135
+ description="Declared props and workspace usage from the latest scan."
136
+ >
137
+ {reportReady && workspaceReport ? (
138
+ <ComponentPropUsageDetail
139
+ component={componentId}
140
+ declared={declared}
141
+ unusedProps={unusedProps}
142
+ />
143
+ ) : (
144
+ <p className="text-sm text-muted-foreground">
145
+ Load the DSLinter report to see prop usage.
146
+ </p>
147
+ )}
148
+ </Section>
149
+
150
+ <Section
151
+ id="usage"
152
+ title="App usage"
153
+ description="Where this component is used in the workspace."
154
+ >
155
+ <ComponentUsageDetails
156
+ report={workspaceReport}
157
+ componentId={componentId}
158
+ />
159
+ </Section>
160
+
161
+ <Section
162
+ id="findings"
163
+ title="Findings"
164
+ description="DSLinter findings on files where this component is defined."
165
+ >
166
+ {reportReady && workspaceReport ? (
167
+ <FindingsList findings={findings} root={workspaceReport.root} />
168
+ ) : (
169
+ <p className="text-sm text-muted-foreground">
170
+ Load the DSLinter report to see findings.
171
+ </p>
172
+ )}
173
+ </Section>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ );
178
+ }
@@ -13,7 +13,6 @@ import type { HashRoute } from "../shell/hashRoute";
13
13
 
14
14
  type Props = {
15
15
  catalogNames: string[];
16
- playgroundIds: Set<string>;
17
16
  onNavigate: (next: HashRoute) => void;
18
17
  open: boolean;
19
18
  onOpenChange: (open: boolean) => void;
@@ -26,21 +25,8 @@ function eventTargetIsEditable(target: EventTarget | null): boolean {
26
25
  return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
27
26
  }
28
27
 
29
- function navigateToCatalogName(
30
- name: string,
31
- playgroundIds: Set<string>,
32
- onNavigate: (next: HashRoute) => void,
33
- ) {
34
- if (playgroundIds.has(name)) {
35
- onNavigate({ view: "component", componentId: name });
36
- } else {
37
- onNavigate({ view: "governance", catalog: name });
38
- }
39
- }
40
-
41
28
  export function DashboardCommandPalette({
42
29
  catalogNames,
43
- playgroundIds,
44
30
  onNavigate,
45
31
  open,
46
32
  onOpenChange,
@@ -86,7 +72,7 @@ export function DashboardCommandPalette({
86
72
  key={name}
87
73
  value={name}
88
74
  onSelect={() => {
89
- navigateToCatalogName(name, playgroundIds, onNavigate);
75
+ onNavigate({ view: "component", componentId: name });
90
76
  close();
91
77
  }}
92
78
  >
@@ -9,7 +9,7 @@ type Props = {
9
9
  reportUrl?: string;
10
10
  dslinterReportHint?: string;
11
11
  dslinterReport: DslinterReportState;
12
- focusName?: string;
12
+ onOpenComponent?: (name: string) => void;
13
13
  };
14
14
 
15
15
  export function GovernancePane({
@@ -17,7 +17,7 @@ export function GovernancePane({
17
17
  reportUrl: _reportUrl = "/dslint-report.json",
18
18
  dslinterReportHint = "npm run dslint:report",
19
19
  dslinterReport,
20
- focusName,
20
+ onOpenComponent,
21
21
  }: Props) {
22
22
  const { report, error, loading } = dslinterReport;
23
23
  const componentCatalogCount = report
@@ -80,7 +80,7 @@ export function GovernancePane({
80
80
  </p>
81
81
  </header>
82
82
  <div className="min-w-0 w-full px-6 py-8">
83
- <DashboardBody report={report} focusName={focusName} />
83
+ <DashboardBody report={report} onOpenComponent={onOpenComponent} />
84
84
  </div>
85
85
  </div>
86
86
  );
@@ -9,7 +9,6 @@ import type { DashboardThemePreference } from "../shell/DashboardLayout";
9
9
 
10
10
  type Props = {
11
11
  report: WorkspaceReport | null;
12
- playgroundIds: Set<string>;
13
12
  reportLoading: boolean;
14
13
  reportError: string | null;
15
14
  route: HashRoute;
@@ -50,7 +49,6 @@ function sectionLabel(text: string) {
50
49
 
51
50
  export function Sidebar({
52
51
  report,
53
- playgroundIds,
54
52
  reportLoading,
55
53
  reportError,
56
54
  route,
@@ -64,7 +62,8 @@ export function Sidebar({
64
62
  [report],
65
63
  );
66
64
  const tokensActive = route.view === "tokens";
67
- const governanceActive = route.view === "governance";
65
+ const governanceActive =
66
+ route.view === "governance" && route.catalog == null;
68
67
 
69
68
  const onThemeValueChange = (value: string) => {
70
69
  if (value !== "light" && value !== "dark") return;
@@ -173,16 +172,13 @@ export function Sidebar({
173
172
  ) : null}
174
173
  {catalogNames.map((name) => {
175
174
  const active =
176
- (route.view === "component" && route.componentId === name) ||
177
- (route.view === "governance" && route.catalog === name);
175
+ route.view === "component" && route.componentId === name;
178
176
  return (
179
177
  <button
180
178
  key={name}
181
179
  type="button"
182
180
  onClick={() =>
183
- playgroundIds.has(name)
184
- ? onNavigate({ view: "component", componentId: name })
185
- : onNavigate({ view: "governance", catalog: name })
181
+ onNavigate({ view: "component", componentId: name })
186
182
  }
187
183
  className={navButtonClass(active)}
188
184
  >
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo } from "react";
1
+ import { useMemo } from "react";
2
2
  import {
3
3
  HoverCard,
4
4
  HoverCardContent,
@@ -16,9 +16,13 @@ import {
16
16
  aggregateDeclaredProps,
17
17
  aggregateDefinitions,
18
18
  catalogComponentNames,
19
- catalogRowDomId,
20
19
  usageMap,
21
20
  } from "./aggregate";
21
+ import {
22
+ catalogAttributeProps,
23
+ ComponentPropUsageDetail,
24
+ buildUnusedPropSetForComponent,
25
+ } from "./ComponentPropUsageDetail";
22
26
  import { shortPath } from "./paths";
23
27
  import type { WorkspaceReport } from "../types/report";
24
28
  import { pluralize } from "usemods";
@@ -26,26 +30,15 @@ import { pluralize } from "usemods";
26
30
  /** Set of `"ComponentName/propName"` keys for every declared prop with no recorded usage. */
27
31
  function buildUnusedPropSet(report: WorkspaceReport): Set<string> {
28
32
  const s = new Set<string>();
29
- const usageByComponent = new Map(
30
- (report.usage_by_component ?? []).map((usage) => [
31
- usage.component,
32
- usage.prop_frequencies ?? {},
33
- ]),
34
- );
35
-
36
- for (const file of report.files ?? []) {
37
- for (const definition of file.definitions ?? []) {
38
- const componentName = definition.name;
39
- const propFrequencies = usageByComponent.get(componentName) ?? {};
40
-
41
- for (const propName of definition.declared_props ?? []) {
42
- if ((propFrequencies[propName] ?? 0) === 0) {
43
- s.add(`${componentName}/${propName}`);
44
- }
45
- }
46
- }
33
+ const declaredByName = aggregateDeclaredProps(report);
34
+ for (const [componentName, declared] of declaredByName) {
35
+ const unused = buildUnusedPropSetForComponent(
36
+ report,
37
+ componentName,
38
+ declared,
39
+ );
40
+ for (const key of unused) s.add(key);
47
41
  }
48
-
49
42
  return s;
50
43
  }
51
44
 
@@ -63,51 +56,24 @@ function CatalogPropUsageHover({
63
56
  unusedProps: Set<string>;
64
57
  usedPropCount: number;
65
58
  }) {
66
- const used = declared.filter(
67
- (prop) => !unusedProps.has(`${component}/${prop}`),
68
- );
69
- const unused = declared.filter((prop) =>
70
- unusedProps.has(`${component}/${prop}`),
71
- );
59
+ const attributeProps = catalogAttributeProps(declared);
72
60
 
73
61
  return (
74
62
  <HoverCard openDelay={200} closeDelay={100}>
75
63
  <HoverCardTrigger asChild>
76
64
  <button type="button" className={catalogHoverTriggerClass}>
77
65
  <span className="text-muted-foreground">
78
- {usedPropCount}/{declared.length} {pluralize("prop", usedPropCount)}{" "}
79
- used
66
+ {usedPropCount}/{attributeProps.length}{" "}
67
+ {pluralize("prop", usedPropCount)} used
80
68
  </span>
81
69
  </button>
82
70
  </HoverCardTrigger>
83
71
  <HoverCardContent align="start" className="w-56 p-3">
84
- <p className="text-xs font-medium text-foreground">Props</p>
85
- {used.length > 0 ? (
86
- <div className="mt-2">
87
- <p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
88
- Used
89
- </p>
90
- <ul className="mt-1 space-y-0.5 font-mono text-xs text-foreground">
91
- {used.map((prop) => (
92
- <li key={prop}>{prop}</li>
93
- ))}
94
- </ul>
95
- </div>
96
- ) : null}
97
- {unused.length > 0 ? (
98
- <div className={used.length > 0 ? "mt-3" : "mt-2"}>
99
- <p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
100
- Never passed
101
- </p>
102
- <ul className="mt-1 space-y-0.5 font-mono text-xs text-muted-foreground/70">
103
- {unused.map((prop) => (
104
- <li key={prop} className="line-through">
105
- {prop}
106
- </li>
107
- ))}
108
- </ul>
109
- </div>
110
- ) : null}
72
+ <ComponentPropUsageDetail
73
+ component={component}
74
+ declared={declared}
75
+ unusedProps={unusedProps}
76
+ />
111
77
  </HoverCardContent>
112
78
  </HoverCard>
113
79
  );
@@ -149,10 +115,10 @@ function CatalogAppUsageHover({
149
115
 
150
116
  export function ComponentCatalog({
151
117
  report,
152
- focusName,
118
+ onOpenComponent,
153
119
  }: {
154
120
  report: WorkspaceReport;
155
- focusName?: string;
121
+ onOpenComponent?: (name: string) => void;
156
122
  }) {
157
123
  const defs = aggregateDefinitions(report);
158
124
  const usages = usageMap(report);
@@ -163,12 +129,6 @@ export function ComponentCatalog({
163
129
  [report],
164
130
  );
165
131
 
166
- useEffect(() => {
167
- if (!focusName || !names.includes(focusName)) return;
168
- const el = document.getElementById(catalogRowDomId(focusName));
169
- el?.scrollIntoView({ block: "nearest", behavior: "smooth" });
170
- }, [focusName, names]);
171
-
172
132
  return (
173
133
  <Table>
174
134
  <TableHeader>
@@ -182,16 +142,29 @@ export function ComponentCatalog({
182
142
  {names.map((name) => {
183
143
  const use = usages.get(name);
184
144
  const declared = declaredByName.get(name) ?? [];
185
- const usedPropCount = declared.filter(
145
+ const attributeProps = catalogAttributeProps(declared);
146
+ const usedPropCount = attributeProps.filter(
186
147
  (prop) => !unusedProps.has(`${name}/${prop}`),
187
148
  ).length;
188
149
 
189
150
  return (
190
- <TableRow key={name} id={catalogRowDomId(name)}>
191
- <TableCell>{name}</TableCell>
151
+ <TableRow key={name}>
152
+ <TableCell>
153
+ {onOpenComponent ? (
154
+ <button
155
+ type="button"
156
+ onClick={() => onOpenComponent(name)}
157
+ className="text-left font-medium text-foreground underline decoration-transparent underline-offset-2 transition hover:decoration-current"
158
+ >
159
+ {name}
160
+ </button>
161
+ ) : (
162
+ name
163
+ )}
164
+ </TableCell>
192
165
 
193
166
  <TableCell>
194
- {declared.length > 0 ? (
167
+ {attributeProps.length > 0 ? (
195
168
  <CatalogPropUsageHover
196
169
  component={name}
197
170
  declared={declared}
@@ -0,0 +1,90 @@
1
+ import type { WorkspaceReport } from "../types/report";
2
+ import { pluralize } from "usemods";
3
+
4
+ /** React slot props — not attribute props. */
5
+ const SLOT_PROPS = new Set(["children"]);
6
+
7
+ export function catalogAttributeProps(props: string[]): string[] {
8
+ return props.filter((prop) => !SLOT_PROPS.has(prop));
9
+ }
10
+
11
+ /** Set of `"ComponentName/propName"` keys for props with no recorded usage. */
12
+ export function buildUnusedPropSetForComponent(
13
+ report: WorkspaceReport,
14
+ componentName: string,
15
+ declared: string[],
16
+ ): Set<string> {
17
+ const s = new Set<string>();
18
+ const usageRow = (report.usage_by_component ?? []).find(
19
+ (u) => u.component === componentName,
20
+ );
21
+ const propFrequencies = usageRow?.prop_frequencies ?? {};
22
+ for (const propName of catalogAttributeProps(declared)) {
23
+ if ((propFrequencies[propName] ?? 0) === 0) {
24
+ s.add(`${componentName}/${propName}`);
25
+ }
26
+ }
27
+ return s;
28
+ }
29
+
30
+ export function ComponentPropUsageDetail({
31
+ component,
32
+ declared,
33
+ unusedProps,
34
+ }: {
35
+ component: string;
36
+ declared: string[];
37
+ unusedProps: Set<string>;
38
+ }) {
39
+ const attributeProps = catalogAttributeProps(declared);
40
+ const used = attributeProps.filter(
41
+ (prop) => !unusedProps.has(`${component}/${prop}`),
42
+ );
43
+ const unused = attributeProps.filter((prop) =>
44
+ unusedProps.has(`${component}/${prop}`),
45
+ );
46
+ const usedPropCount = used.length;
47
+
48
+ if (attributeProps.length === 0) {
49
+ return (
50
+ <p className="text-sm text-muted-foreground">
51
+ No declared props recorded for this component.
52
+ </p>
53
+ );
54
+ }
55
+
56
+ return (
57
+ <div className="space-y-4">
58
+ <p className="text-sm text-muted-foreground">
59
+ {usedPropCount}/{attributeProps.length}{" "}
60
+ {pluralize("prop", usedPropCount)} used in the workspace snapshot.
61
+ </p>
62
+ {used.length > 0 ? (
63
+ <div>
64
+ <p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
65
+ Used
66
+ </p>
67
+ <ul className="mt-1 space-y-0.5 font-mono text-xs text-foreground">
68
+ {used.map((prop) => (
69
+ <li key={prop}>{prop}</li>
70
+ ))}
71
+ </ul>
72
+ </div>
73
+ ) : null}
74
+ {unused.length > 0 ? (
75
+ <div>
76
+ <p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
77
+ Never passed
78
+ </p>
79
+ <ul className="mt-1 space-y-0.5 font-mono text-xs text-muted-foreground/70">
80
+ {unused.map((prop) => (
81
+ <li key={prop} className="line-through">
82
+ {prop}
83
+ </li>
84
+ ))}
85
+ </ul>
86
+ </div>
87
+ ) : null}
88
+ </div>
89
+ );
90
+ }
@@ -14,10 +14,10 @@ import { ScoreStrip } from "./ScoreStrip";
14
14
 
15
15
  export function DashboardBody({
16
16
  report,
17
- focusName,
17
+ onOpenComponent,
18
18
  }: {
19
19
  report: WorkspaceReport;
20
- focusName?: string;
20
+ onOpenComponent?: (name: string) => void;
21
21
  }) {
22
22
  return (
23
23
  <div className="space-y-10">
@@ -62,7 +62,7 @@ export function DashboardBody({
62
62
  title="Components"
63
63
  description="Definitions and JSX usage from the latest snapshot."
64
64
  >
65
- <ComponentCatalog report={report} focusName={focusName} />
65
+ <ComponentCatalog report={report} onOpenComponent={onOpenComponent} />
66
66
  </Section>
67
67
 
68
68
  <Section
package/src/index.ts CHANGED
@@ -13,6 +13,12 @@ export type {
13
13
  } from "./types/controls";
14
14
  export { defaultArgsFromControls } from "./types/controls";
15
15
  export { definePlayground } from "./playground/definePlayground";
16
+ export {
17
+ buildPlaygroundEntriesFromReport,
18
+ playgroundCatalogId,
19
+ resolvePlaygroundEntry,
20
+ } from "./playground/buildPlaygroundEntriesFromReport";
21
+ export type { BuildPlaygroundModules, BuildPlaygroundOptions } from "./playground/buildPlaygroundEntriesFromReport";
16
22
  export type { DefinedPlayground } from "./playground/definePlayground";
17
23
  export type { PlaygroundPreviewProps, PlaygroundPreviewComponent } from "./types/preview";
18
24
  export type {
@@ -69,6 +75,7 @@ export { a11ySummaryForModule, resolveModuleSourcePath } from "./report/a11yForM
69
75
  export type { A11yModuleSummary } from "./report/a11yForModule";
70
76
  export { tokenStyleFindingsForModule } from "./report/tokenStyleFindingsForModule";
71
77
  export { codeScoreSummaryForModule } from "./report/codeScoreForModule";
78
+ export { findingsForComponent } from "./report/findingsForComponent";
72
79
  export type { CodeScoreModuleSummary } from "./report/codeScoreForModule";
73
80
  export { TokenWall } from "./dashboard/TokenWall";
74
81
  export { DashboardBody } from "./dashboard/DashboardBody";
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { PlaygroundEntry } from "../types/playground";
3
+ import type { WorkspaceReport } from "../types/report";
4
+ import { resolvePlaygroundEntry } from "./buildPlaygroundEntriesFromReport";
5
+
6
+ const entries: PlaygroundEntry[] = [
7
+ {
8
+ id: "PrimaryButton",
9
+ meta: { id: "PrimaryButton", title: "PrimaryButton" },
10
+ modulePath: "@dslint-scan/src/components/PrimaryButton.tsx",
11
+ controls: [],
12
+ Preview: () => null,
13
+ },
14
+ ];
15
+
16
+ describe("resolvePlaygroundEntry", () => {
17
+ it("finds by catalog export name id", () => {
18
+ expect(resolvePlaygroundEntry(entries, "PrimaryButton")).toBe(entries[0]);
19
+ });
20
+
21
+ it("returns undefined for unknown name", () => {
22
+ expect(resolvePlaygroundEntry(entries, "Missing")).toBeUndefined();
23
+ });
24
+ });
25
+
26
+ describe("playground catalog id alignment", () => {
27
+ it("report playgrounds use export_name as id", () => {
28
+ const report: WorkspaceReport = {
29
+ root: "/repo",
30
+ files: [],
31
+ findings: [],
32
+ scores: {
33
+ system_health: 0,
34
+ token_adoption: 0,
35
+ component_adoption: 0,
36
+ ux_consistency: 0,
37
+ },
38
+ ownership: [],
39
+ duplicate_components: [],
40
+ usage_by_component: [],
41
+ playgrounds: [
42
+ {
43
+ id: "Card",
44
+ export_name: "Card",
45
+ rel_path: "src/components/nested/DuplicateCardA.tsx",
46
+ declared_props: [],
47
+ },
48
+ ],
49
+ };
50
+ expect(report.playgrounds?.[0]?.id).toBe("Card");
51
+ expect(report.playgrounds?.[0]?.export_name).toBe("Card");
52
+ });
53
+ });