@principal-ai/subsystems-react 0.56.11 → 0.56.12
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.
- package/dist/subsystem/DiagnosticToggle.d.ts.map +1 -1
- package/dist/subsystem/DiagnosticToggle.js +2 -1
- package/dist/subsystem/DiagnosticToggle.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +2 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +18 -13
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/SubsystemFileTree.d.ts.map +1 -1
- package/dist/subsystem/SubsystemFileTree.js +2 -1
- package/dist/subsystem/SubsystemFileTree.js.map +1 -1
- package/dist/subsystem/TrailStepsNav.d.ts.map +1 -1
- package/dist/subsystem/TrailStepsNav.js +11 -8
- package/dist/subsystem/TrailStepsNav.js.map +1 -1
- package/dist/subsystem/TrailsPanel.d.ts +1 -1
- package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
- package/dist/subsystem/TrailsPanel.js +42 -66
- package/dist/subsystem/TrailsPanel.js.map +1 -1
- package/package.json +1 -1
- package/src/stories/Canvases/Subsystem/TrailSidebarViews.stories.tsx +2 -2
- package/src/subsystem/DiagnosticToggle.tsx +2 -1
- package/src/subsystem/SubsystemComponentGraph.sidebar.test.tsx +71 -7
- package/src/subsystem/SubsystemComponentGraph.tsx +24 -14
- package/src/subsystem/SubsystemFileTree.tsx +2 -1
- package/src/subsystem/TrailStepsNav.test.tsx +38 -0
- package/src/subsystem/TrailStepsNav.tsx +11 -8
- package/src/subsystem/TrailsPanel.test.tsx +99 -35
- package/src/subsystem/TrailsPanel.tsx +38 -85
|
@@ -404,7 +404,8 @@ export interface SubsystemComponentGraphProps {
|
|
|
404
404
|
/**
|
|
405
405
|
* Verification issues for this graph (audit findings). When diagnostics are
|
|
406
406
|
* active the sidebar's bottom panel becomes the issue list instead of the
|
|
407
|
-
* file tree / trails.
|
|
407
|
+
* file tree / trails. Graph issue badges and issue-driven dimming are only
|
|
408
|
+
* shown in that mode.
|
|
408
409
|
*/
|
|
409
410
|
issues?: SubsystemIssue[];
|
|
410
411
|
/**
|
|
@@ -1262,7 +1263,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1262
1263
|
// node says "diagnostics here" for every finding the sidebar lists.
|
|
1263
1264
|
const issueBadgeByAlias = useMemo(() => {
|
|
1264
1265
|
const map = new Map<string, SubsystemNodeIssue>();
|
|
1265
|
-
if (!issues?.length) return map;
|
|
1266
|
+
if (showIssues !== true || !issues?.length) return map;
|
|
1266
1267
|
for (const issue of issues) {
|
|
1267
1268
|
const rung = issueRung(issue.kind);
|
|
1268
1269
|
if (!rung && !ISSUE_KIND_ICON[issue.kind]) continue;
|
|
@@ -1297,7 +1298,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1297
1298
|
if (kinded) map.set(alias, { ...badge, kind: kinded.kind });
|
|
1298
1299
|
}
|
|
1299
1300
|
return map;
|
|
1300
|
-
}, [issues, issueComponent]);
|
|
1301
|
+
}, [showIssues, issues, issueComponent]);
|
|
1301
1302
|
|
|
1302
1303
|
// Per-frame diagnostics badge: the same fold, but for findings about a
|
|
1303
1304
|
// BOUNDARY rather than a construct. Keyed by the region's React Flow node
|
|
@@ -1308,7 +1309,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1308
1309
|
// borrowing its layer's icon would imply the frame is at fault for it.
|
|
1309
1310
|
const regionIssueByNodeId = useMemo(() => {
|
|
1310
1311
|
const map = new Map<string, SubsystemRegionIssue>();
|
|
1311
|
-
if (!issues?.length) return map;
|
|
1312
|
+
if (showIssues !== true || !issues?.length) return map;
|
|
1312
1313
|
for (const issue of issues) {
|
|
1313
1314
|
const target = issue.target;
|
|
1314
1315
|
if (!target || !ISSUE_KIND_ICON[issue.kind]) continue;
|
|
@@ -1329,7 +1330,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1329
1330
|
});
|
|
1330
1331
|
}
|
|
1331
1332
|
return map;
|
|
1332
|
-
}, [issues]);
|
|
1333
|
+
}, [showIssues, issues]);
|
|
1333
1334
|
|
|
1334
1335
|
// Per-edge step verification badge: maps edge IDs to step verification status.
|
|
1335
1336
|
// Keyed by the React Flow edge id (from → mechanism → to). An edge can have
|
|
@@ -1347,7 +1348,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1347
1348
|
step_span_stale: 1,
|
|
1348
1349
|
};
|
|
1349
1350
|
const map = new Map<string, { kind: StepIssueKind; severity: 'info' | 'error'; count: number }>();
|
|
1350
|
-
if (!issues?.length || !trails?.length) {
|
|
1351
|
+
if (showIssues !== true || !issues?.length || !trails?.length) {
|
|
1351
1352
|
return map;
|
|
1352
1353
|
}
|
|
1353
1354
|
for (const issue of issues) {
|
|
@@ -1374,7 +1375,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1374
1375
|
});
|
|
1375
1376
|
}
|
|
1376
1377
|
return map;
|
|
1377
|
-
}, [issues, trails, issueStep]);
|
|
1378
|
+
}, [showIssues, issues, trails, issueStep]);
|
|
1378
1379
|
|
|
1379
1380
|
// Expanded verification layers in the diagnostics list. When any layer with
|
|
1380
1381
|
// findings is expanded, the canvas dims everything that layer does not
|
|
@@ -1423,7 +1424,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1423
1424
|
// Returns null when nothing is expanded, no expanded layer has findings, or
|
|
1424
1425
|
// the findings implicate nothing — so no dimming is applied.
|
|
1425
1426
|
const issueFocus = useMemo(() => {
|
|
1426
|
-
if (!issues?.length || expandedIssueCategories.length === 0) return null;
|
|
1427
|
+
if (showIssues !== true || !issues?.length || expandedIssueCategories.length === 0) return null;
|
|
1427
1428
|
const active = new Set(expandedIssueCategories);
|
|
1428
1429
|
const activeIssues = issues.filter((i) => active.has(issueCategory(i)));
|
|
1429
1430
|
if (activeIssues.length === 0) return null;
|
|
@@ -1478,7 +1479,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1478
1479
|
if (nodeIds.has(e.source) && nodeIds.has(e.target)) edgeIds.add(e.id);
|
|
1479
1480
|
}
|
|
1480
1481
|
return { nodeIds, edgeIds };
|
|
1481
|
-
}, [issues, expandedIssueCategories, components, trails, resolveAlias]);
|
|
1482
|
+
}, [showIssues, issues, expandedIssueCategories, components, trails, resolveAlias]);
|
|
1482
1483
|
const issueFocusNodeIds = issueFocus?.nodeIds ?? null;
|
|
1483
1484
|
const issueFocusEdgeIds = issueFocus?.edgeIds ?? null;
|
|
1484
1485
|
|
|
@@ -2765,6 +2766,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2765
2766
|
<div style={{ padding: 16, color: theme.colors.textSecondary }}>No files for this trail.</div>
|
|
2766
2767
|
) : (
|
|
2767
2768
|
<SubsystemFileTree
|
|
2769
|
+
headerless
|
|
2768
2770
|
files={treeFilePaths}
|
|
2769
2771
|
selectedFile={openFile ? treePaths.toDisplay.get(openFile) : null}
|
|
2770
2772
|
onSelectFile={(path) => {
|
|
@@ -3413,6 +3415,7 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3413
3415
|
);
|
|
3414
3416
|
const fileCount = sidebarScope.treePaths.entries.length;
|
|
3415
3417
|
const trailViewLabel = sidebarScope.trail ? 'Trail' : 'Trails';
|
|
3418
|
+
const trailCount = props.trails?.length ?? 0;
|
|
3416
3419
|
const hasFiles = fileCount > 0 || sidebarScope.trail != null;
|
|
3417
3420
|
const sidebarDiagnostic: SubsystemDiagnostic = sidebarScope.trail ? {
|
|
3418
3421
|
...props.diagnostic,
|
|
@@ -3422,18 +3425,22 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3422
3425
|
fixableCount: sidebarScope.issues.filter((issue) => issue.fix != null).length,
|
|
3423
3426
|
title: undefined,
|
|
3424
3427
|
} : props.diagnostic ?? { status: 'unknown', issueCount: props.issues?.length };
|
|
3428
|
+
const hasIssues = (props.issues != null
|
|
3429
|
+
? sidebarScope.issues.length
|
|
3430
|
+
: sidebarDiagnostic.issueCount ?? 0) > 0;
|
|
3425
3431
|
useEffect(() => {
|
|
3426
3432
|
if (sidebarTrailId != null && !sidebarScope.trail) setSidebarTrailId(null);
|
|
3427
3433
|
}, [sidebarTrailId, sidebarScope.trail]);
|
|
3428
3434
|
const [diagnosticsOpen, setDiagnosticsOpen] = useState<boolean>(
|
|
3429
3435
|
props.showIssues ?? (props.issues?.length ?? 0) > 0,
|
|
3430
3436
|
);
|
|
3431
|
-
const
|
|
3437
|
+
const issuesRequested = props.showIssues ?? diagnosticsOpen;
|
|
3438
|
+
const issuesActive = hasIssues && issuesRequested;
|
|
3432
3439
|
const filesActive = filesOpen && hasFiles && !issuesActive;
|
|
3433
3440
|
const selectSidebarView = (view: 'trails' | 'files' | 'issues') => {
|
|
3434
3441
|
const nextIssuesActive = view === 'issues';
|
|
3435
3442
|
setFilesOpen(view === 'files');
|
|
3436
|
-
if (nextIssuesActive !==
|
|
3443
|
+
if (nextIssuesActive !== issuesRequested) {
|
|
3437
3444
|
if (props.showIssues === undefined) setDiagnosticsOpen(nextIssuesActive);
|
|
3438
3445
|
props.diagnostic?.onToggle?.();
|
|
3439
3446
|
}
|
|
@@ -3444,7 +3451,7 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3444
3451
|
justifyContent: 'center',
|
|
3445
3452
|
gap: 4,
|
|
3446
3453
|
height: 28,
|
|
3447
|
-
padding: '4px
|
|
3454
|
+
padding: '4px 16px',
|
|
3448
3455
|
fontFamily: theme.fonts.monospace,
|
|
3449
3456
|
fontSize: theme.fontSizes[0],
|
|
3450
3457
|
border: 'none',
|
|
@@ -3535,13 +3542,14 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3535
3542
|
>
|
|
3536
3543
|
<button
|
|
3537
3544
|
type="button"
|
|
3538
|
-
aria-label={
|
|
3545
|
+
aria-label={sidebarScope.trail ? 'Trail' : `Trails (${trailCount} ${trailCount === 1 ? 'trail' : 'trails'})`}
|
|
3539
3546
|
aria-pressed={!filesActive && !issuesActive}
|
|
3540
3547
|
onClick={() => selectSidebarView('trails')}
|
|
3541
3548
|
style={viewButtonStyle(!filesActive && !issuesActive)}
|
|
3542
3549
|
>
|
|
3543
3550
|
<Route size={14} aria-hidden />
|
|
3544
3551
|
<span>{trailViewLabel}</span>
|
|
3552
|
+
{!sidebarScope.trail && <span style={{ fontWeight: 600, marginLeft: 4 }}>{trailCount}</span>}
|
|
3545
3553
|
</button>
|
|
3546
3554
|
{hasFiles && (
|
|
3547
3555
|
<button
|
|
@@ -3554,9 +3562,10 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3554
3562
|
>
|
|
3555
3563
|
<Files size={14} aria-hidden />
|
|
3556
3564
|
<span>Files</span>
|
|
3557
|
-
<span style={{ fontWeight: 600 }}>{fileCount}</span>
|
|
3565
|
+
<span style={{ fontWeight: 600, marginLeft: 4 }}>{fileCount}</span>
|
|
3558
3566
|
</button>
|
|
3559
3567
|
)}
|
|
3568
|
+
{hasIssues && (
|
|
3560
3569
|
<SubsystemDiagnosticToggle
|
|
3561
3570
|
{...sidebarDiagnostic}
|
|
3562
3571
|
labelText="Issues"
|
|
@@ -3564,6 +3573,7 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3564
3573
|
active={issuesActive}
|
|
3565
3574
|
onToggle={() => selectSidebarView('issues')}
|
|
3566
3575
|
/>
|
|
3576
|
+
)}
|
|
3567
3577
|
</div>
|
|
3568
3578
|
</header>
|
|
3569
3579
|
)}
|
|
@@ -143,6 +143,7 @@ export function SubsystemFileTree({ files, selectedFile, hoveredFile, hoveredFol
|
|
|
143
143
|
ref={treeRef}
|
|
144
144
|
onClickCapture={handleContainerClick}
|
|
145
145
|
style={{
|
|
146
|
+
paddingTop: headerless ? 8 : undefined,
|
|
146
147
|
flex: 1,
|
|
147
148
|
minHeight: 0,
|
|
148
149
|
display: 'flex',
|
|
@@ -187,7 +188,7 @@ export function SubsystemFileTree({ files, selectedFile, hoveredFile, hoveredFol
|
|
|
187
188
|
'--trees-fg-muted-override': muted,
|
|
188
189
|
'--trees-accent-override': theme.colors.primary,
|
|
189
190
|
'--trees-font-family-override': theme.fonts.monospace,
|
|
190
|
-
'--trees-font-size-override': `${theme.fontSizes[
|
|
191
|
+
'--trees-font-size-override': `${theme.fontSizes[1]}px`,
|
|
191
192
|
'--trees-theme-list-hover-bg': theme.colors.border,
|
|
192
193
|
'--trees-theme-list-active-selection-bg': theme.colors.border,
|
|
193
194
|
} as CSSProperties
|
|
@@ -56,6 +56,39 @@ function renderNav(props: Record<string, unknown>) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
describe('TrailStepsNav', () => {
|
|
59
|
+
for (const title of ['Short flow', 'Generate the account report and deliver it to every subscribed recipient']) {
|
|
60
|
+
for (const selected of [false, true]) {
|
|
61
|
+
test(`keeps explicit title spacing in the ${selected ? 'selected header' : 'list'} for ${title}`, () => {
|
|
62
|
+
const { getByRole } = renderNav({
|
|
63
|
+
trails: [{ ...TRAILS[0], title }],
|
|
64
|
+
focusedTrailId: selected ? 't1' : null,
|
|
65
|
+
focusedStepIndex: null,
|
|
66
|
+
hoveredTrailStep: null,
|
|
67
|
+
onFocusFlow: () => {},
|
|
68
|
+
onClearFocus: () => {},
|
|
69
|
+
onFocusStep: () => {},
|
|
70
|
+
onHoverStep: () => {},
|
|
71
|
+
onHoverFlow: () => {},
|
|
72
|
+
onLeaveStep: () => {},
|
|
73
|
+
});
|
|
74
|
+
const button = getByRole('button', { name: selected ? title : new RegExp(title), exact: selected });
|
|
75
|
+
const row = selected ? button.parentElement! : button;
|
|
76
|
+
const label = selected ? button : button.children[1] as HTMLElement;
|
|
77
|
+
expect(row.style.paddingTop).toBe('14px');
|
|
78
|
+
expect(row.style.paddingBottom).toBe('14px');
|
|
79
|
+
expect(row.style.paddingLeft).toBe('12px');
|
|
80
|
+
expect(row.style.paddingRight).toBe(row.style.paddingLeft);
|
|
81
|
+
expect(row.style.alignItems).toBe('flex-start');
|
|
82
|
+
expect(label.style.lineHeight).toBe('20px');
|
|
83
|
+
expect((row.firstElementChild as SVGElement).style.marginTop).toBe('3px');
|
|
84
|
+
if (selected) {
|
|
85
|
+
expect(row.style.borderBottomStyle).toBe('');
|
|
86
|
+
expect(getByRole('button', { name: 'Back to trails' }).style.height).toBe('20px');
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
59
92
|
test('keeps the selected trail header when files or issues replace the step body', () => {
|
|
60
93
|
let cleared = false;
|
|
61
94
|
const { getByRole, getByText, queryByText } = renderNav({
|
|
@@ -183,6 +216,11 @@ describe('TrailStepsNav', () => {
|
|
|
183
216
|
expect(getByText('B.go').style.fontFamily).toBe(defaultEditorTheme.fonts.body);
|
|
184
217
|
expect(getByRole('button', { name: /A\.go/ }).style.paddingLeft).toBe('12px');
|
|
185
218
|
expect(getByRole('button', { name: /B\.go/ }).style.paddingLeft).toBe('22px');
|
|
219
|
+
for (const name of [/A\.go/, /B\.go/]) {
|
|
220
|
+
const row = getByRole('button', { name });
|
|
221
|
+
expect(row.style.paddingTop).toBe('14px');
|
|
222
|
+
expect(row.style.paddingBottom).toBe('14px');
|
|
223
|
+
}
|
|
186
224
|
fireEvent.click(getByRole('button', { name: /B\.go/ }));
|
|
187
225
|
expect(focusedStep).toBe(1);
|
|
188
226
|
fireEvent.click(getByLabelText('Back to trails'));
|
|
@@ -152,11 +152,11 @@ export function TrailStepsNav({
|
|
|
152
152
|
onClick={() => onFocusFlow(trail)}
|
|
153
153
|
style={{
|
|
154
154
|
display: 'flex',
|
|
155
|
-
alignItems: '
|
|
155
|
+
alignItems: 'flex-start',
|
|
156
156
|
gap: 8,
|
|
157
157
|
width: '100%',
|
|
158
158
|
minWidth: 0,
|
|
159
|
-
padding: '
|
|
159
|
+
padding: '14px 12px',
|
|
160
160
|
border: 'none',
|
|
161
161
|
borderRadius: 0,
|
|
162
162
|
background: focusedRow || hovered ? hoverBg : 'transparent',
|
|
@@ -171,6 +171,7 @@ export function TrailStepsNav({
|
|
|
171
171
|
aria-hidden
|
|
172
172
|
style={{
|
|
173
173
|
flexShrink: 0,
|
|
174
|
+
marginTop: 3,
|
|
174
175
|
color: focusedRow || hovered ? theme.colors.primary : muted,
|
|
175
176
|
transition: 'color 120ms ease',
|
|
176
177
|
}}
|
|
@@ -183,6 +184,7 @@ export function TrailStepsNav({
|
|
|
183
184
|
fontSize: theme.fontSizes[1],
|
|
184
185
|
fontFamily: theme.fonts.monospace,
|
|
185
186
|
fontWeight: 600,
|
|
187
|
+
lineHeight: '20px',
|
|
186
188
|
color: theme.colors.text,
|
|
187
189
|
}}
|
|
188
190
|
>
|
|
@@ -192,6 +194,7 @@ export function TrailStepsNav({
|
|
|
192
194
|
style={{
|
|
193
195
|
flexShrink: 0,
|
|
194
196
|
fontSize: theme.fontSizes[0],
|
|
197
|
+
lineHeight: '20px',
|
|
195
198
|
fontFamily: theme.fonts.monospace,
|
|
196
199
|
color: muted,
|
|
197
200
|
}}
|
|
@@ -220,12 +223,11 @@ export function TrailStepsNav({
|
|
|
220
223
|
style={{
|
|
221
224
|
flexShrink: 0,
|
|
222
225
|
display: 'flex',
|
|
223
|
-
alignItems: '
|
|
226
|
+
alignItems: 'flex-start',
|
|
224
227
|
gap: 8,
|
|
225
228
|
// Left inset + leading cell + gap match the step rows below, so
|
|
226
229
|
// the trail icon and the step numbers share one column.
|
|
227
|
-
padding: '
|
|
228
|
-
borderBottom: `1px solid ${theme.colors.border}`,
|
|
230
|
+
padding: '14px 12px',
|
|
229
231
|
}}
|
|
230
232
|
>
|
|
231
233
|
<Route
|
|
@@ -234,7 +236,7 @@ export function TrailStepsNav({
|
|
|
234
236
|
aria-hidden
|
|
235
237
|
// The focused trail: the icon stays primary, matching the row's
|
|
236
238
|
// hover colour in the list it came from.
|
|
237
|
-
style={{ flexShrink: 0, color: theme.colors.primary }}
|
|
239
|
+
style={{ flexShrink: 0, marginTop: 3, color: theme.colors.primary }}
|
|
238
240
|
/>
|
|
239
241
|
{/* The title is the back affordance too: clicking it goes back to
|
|
240
242
|
the list, like the trailing close. */}
|
|
@@ -257,6 +259,7 @@ export function TrailStepsNav({
|
|
|
257
259
|
fontSize: theme.fontSizes[1],
|
|
258
260
|
fontFamily: theme.fonts.monospace,
|
|
259
261
|
fontWeight: 600,
|
|
262
|
+
lineHeight: '20px',
|
|
260
263
|
color: theme.colors.text,
|
|
261
264
|
}}
|
|
262
265
|
>
|
|
@@ -275,7 +278,7 @@ export function TrailStepsNav({
|
|
|
275
278
|
justifyContent: 'center',
|
|
276
279
|
flexShrink: 0,
|
|
277
280
|
width: 24,
|
|
278
|
-
height:
|
|
281
|
+
height: 20,
|
|
279
282
|
padding: 0,
|
|
280
283
|
border: 'none',
|
|
281
284
|
borderRadius: 4,
|
|
@@ -311,7 +314,7 @@ export function TrailStepsNav({
|
|
|
311
314
|
gap: 8,
|
|
312
315
|
width: '100%',
|
|
313
316
|
minWidth: 0,
|
|
314
|
-
padding: `
|
|
317
|
+
padding: `14px 16px 14px ${12 + Math.max(0, stackDepth - 1) * 10}px`,
|
|
315
318
|
border: 'none',
|
|
316
319
|
borderRadius: 0,
|
|
317
320
|
background: active || hovered ? hoverBg : 'transparent',
|
|
@@ -115,8 +115,6 @@ function renderPanel(overrides: Partial<TrailsPanelProps> = {}) {
|
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
const COPY_LABEL = 'Copy step 1 of Auth flow for an agent';
|
|
119
|
-
|
|
120
118
|
describe('TrailsPanel drilldown navigation', () => {
|
|
121
119
|
test('clicking a trail opens its steps, and Back restores the list without playback', () => {
|
|
122
120
|
const onFocusFlow = mock(() => {});
|
|
@@ -165,14 +163,17 @@ describe('TrailsPanel drilldown navigation', () => {
|
|
|
165
163
|
expect(queryByRole('button', { name: /Login.submit/ })).toBeNull();
|
|
166
164
|
});
|
|
167
165
|
|
|
168
|
-
test('
|
|
169
|
-
const
|
|
166
|
+
test('supports Command-click copy in the focused pane without extra controls', async () => {
|
|
167
|
+
const onFocusStep = mock(() => {});
|
|
168
|
+
const { getByText, getByLabelText, queryByLabelText, queryByRole } = renderPanel({
|
|
170
169
|
drilldown: true,
|
|
171
170
|
focusedTrailId: 'auth-flow',
|
|
171
|
+
onFocusStep,
|
|
172
172
|
});
|
|
173
|
-
fireEvent.
|
|
174
|
-
fireEvent.click(getByLabelText(COPY_LABEL));
|
|
173
|
+
fireEvent.click(getByText('Login.submit').closest('button')!, { metaKey: true });
|
|
175
174
|
await waitFor(() => expect(writeText).toHaveBeenCalledWith(buildStepBrief(trails[0]!, 0)));
|
|
175
|
+
expect(onFocusStep).not.toHaveBeenCalled();
|
|
176
|
+
expect(queryByRole('button', { name: /Copy step/ })).toBeNull();
|
|
176
177
|
expect(queryByLabelText('Play Auth flow')).toBeNull();
|
|
177
178
|
expect(queryByLabelText('Collapse Auth flow')).toBeNull();
|
|
178
179
|
expect(getByLabelText('Back to trails')).toBeTruthy();
|
|
@@ -188,6 +189,35 @@ describe('TrailsPanel drilldown navigation', () => {
|
|
|
188
189
|
});
|
|
189
190
|
|
|
190
191
|
describe('TrailsPanel trail titles', () => {
|
|
192
|
+
for (const title of ['Short flow', 'Validate every selected checkout item and persist the complete customer order']) {
|
|
193
|
+
for (const collapsed of [true, false]) {
|
|
194
|
+
test(`keeps explicit title spacing for ${collapsed ? 'collapsed' : 'expanded'} ${title}`, () => {
|
|
195
|
+
const { getByRole } = renderPanel({
|
|
196
|
+
trails: [{ ...trails[0]!, title }],
|
|
197
|
+
expandedTrails: collapsed ? new Set() : new Set(['auth-flow']),
|
|
198
|
+
onReorder: () => {},
|
|
199
|
+
});
|
|
200
|
+
const button = getByRole('button', { name: title, exact: true });
|
|
201
|
+
const row = button.parentElement!;
|
|
202
|
+
expect(row.style.paddingTop).toBe('14px');
|
|
203
|
+
expect(row.style.paddingBottom).toBe('14px');
|
|
204
|
+
expect(row.style.paddingLeft).toBe('16px');
|
|
205
|
+
expect(row.style.paddingRight).toBe(row.style.paddingLeft);
|
|
206
|
+
expect(row.style.minHeight).toBe('');
|
|
207
|
+
expect(row.style.alignItems).toBe('flex-start');
|
|
208
|
+
expect(button.style.padding).toBe('0px');
|
|
209
|
+
expect((button.lastElementChild as HTMLElement).style.lineHeight).toBe('20px');
|
|
210
|
+
expect((button.firstElementChild as SVGElement).style.marginTop).toBe('3px');
|
|
211
|
+
if (collapsed) {
|
|
212
|
+
expect(getByRole('button', { name: `Reorder ${title}` }).style.height).toBe('20px');
|
|
213
|
+
expect(getByRole('button', { name: `Reorder ${title}` }).style.marginRight).toBe('');
|
|
214
|
+
} else {
|
|
215
|
+
expect(getByRole('button', { name: `Play ${title}` }).style.height).toBe('20px');
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
191
221
|
test('highlights immediately without a CSS fade while graph preview waits', async () => {
|
|
192
222
|
const onHoverFlow = mock(() => {});
|
|
193
223
|
const onLeaveStep = mock(() => {});
|
|
@@ -253,30 +283,28 @@ describe('TrailsPanel trail titles', () => {
|
|
|
253
283
|
});
|
|
254
284
|
|
|
255
285
|
describe('TrailsPanel step copy', () => {
|
|
256
|
-
test('
|
|
257
|
-
const { getByText,
|
|
258
|
-
expect(queryByLabelText(COPY_LABEL)).toBeNull();
|
|
286
|
+
test('does not show a copy button on hover or keyboard focus', () => {
|
|
287
|
+
const { getByText, queryByRole, container } = renderPanel();
|
|
259
288
|
const row = getByText('Login.submit').closest('button')!;
|
|
260
289
|
fireEvent.mouseEnter(row);
|
|
261
|
-
expect(queryByLabelText(COPY_LABEL)).not.toBeNull();
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test('copy icon is revealed on keyboard focus', () => {
|
|
265
|
-
const { getByText, queryByLabelText } = renderPanel();
|
|
266
|
-
const row = getByText('Login.submit').closest('button')!;
|
|
267
290
|
fireEvent.focus(row);
|
|
268
|
-
expect(
|
|
291
|
+
expect(queryByRole('button', { name: /Copy step/ })).toBeNull();
|
|
292
|
+
expect(container.querySelector('.lucide-copy')).toBeNull();
|
|
293
|
+
expect(row.hasAttribute('title')).toBe(false);
|
|
294
|
+
expect(row.firstElementChild?.textContent).toBe('1');
|
|
269
295
|
});
|
|
270
296
|
|
|
271
|
-
test('
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
fireEvent.
|
|
276
|
-
|
|
277
|
-
|
|
297
|
+
test('Command-click copies the brief without focusing and replaces the number with a checkmark', async () => {
|
|
298
|
+
const onFocusStep = mock(() => {});
|
|
299
|
+
const { getByText, getByRole } = renderPanel({ onFocusStep });
|
|
300
|
+
const row = getByText('Login.submit').closest('button')!;
|
|
301
|
+
fireEvent.click(row, { metaKey: true });
|
|
302
|
+
await waitFor(() => expect(getByRole('status', { name: 'Copied' })).toBeTruthy());
|
|
303
|
+
expect(writeText).toHaveBeenCalledTimes(1);
|
|
278
304
|
expect(writeText).toHaveBeenCalledWith(buildStepBrief(trails[0]!, 0));
|
|
279
|
-
expect(
|
|
305
|
+
expect(onFocusStep).not.toHaveBeenCalled();
|
|
306
|
+
expect(row.hasAttribute('title')).toBe(false);
|
|
307
|
+
expect(row.firstElementChild?.querySelector('.lucide-check')).not.toBeNull();
|
|
280
308
|
});
|
|
281
309
|
|
|
282
310
|
test('hovering the row still focuses the step on click', () => {
|
|
@@ -286,26 +314,40 @@ describe('TrailsPanel step copy', () => {
|
|
|
286
314
|
fireEvent.mouseEnter(row);
|
|
287
315
|
fireEvent.click(row);
|
|
288
316
|
expect(onFocusStep).toHaveBeenCalledWith(trails[0]!, 0);
|
|
317
|
+
expect(writeText).not.toHaveBeenCalled();
|
|
289
318
|
});
|
|
290
319
|
|
|
291
320
|
test('the copied checkmark clears after the feedback window', async () => {
|
|
292
|
-
const { getByText,
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
await waitFor(() => expect(copy.getAttribute('title')).toBe('Copied'));
|
|
321
|
+
const { getByText, getByRole, queryByRole } = renderPanel();
|
|
322
|
+
const row = getByText('Login.submit').closest('button')!;
|
|
323
|
+
fireEvent.click(row, { metaKey: true });
|
|
324
|
+
await waitFor(() => expect(getByRole('status', { name: 'Copied' })).toBeTruthy());
|
|
297
325
|
await new Promise((r) => setTimeout(r, STEP_COPY_FEEDBACK_MS + 20));
|
|
298
|
-
await waitFor(() => expect(
|
|
326
|
+
await waitFor(() => expect(queryByRole('status', { name: 'Copied' })).toBeNull());
|
|
327
|
+
expect(row.firstElementChild?.textContent).toBe('1');
|
|
299
328
|
});
|
|
300
329
|
|
|
301
330
|
test('a failed copy shows no copied state', async () => {
|
|
302
331
|
writeText.mockImplementationOnce(() => Promise.reject(new Error('denied')));
|
|
303
|
-
const { getByText,
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
fireEvent.click(copy);
|
|
332
|
+
const { getByText, queryByRole } = renderPanel();
|
|
333
|
+
const row = getByText('Login.submit').closest('button')!;
|
|
334
|
+
fireEvent.click(row, { metaKey: true });
|
|
307
335
|
await new Promise((r) => setTimeout(r, 10));
|
|
308
|
-
expect(
|
|
336
|
+
expect(writeText).toHaveBeenCalledTimes(1);
|
|
337
|
+
expect(queryByRole('status', { name: 'Copied' })).toBeNull();
|
|
338
|
+
expect(row.firstElementChild?.textContent).toBe('1');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('an unavailable clipboard leaves the number and does not focus the step', () => {
|
|
342
|
+
Object.defineProperty(navigator, 'clipboard', { value: undefined, configurable: true });
|
|
343
|
+
const onFocusStep = mock(() => {});
|
|
344
|
+
const { getByText, queryByRole } = renderPanel({ onFocusStep });
|
|
345
|
+
const row = getByText('Login.submit').closest('button')!;
|
|
346
|
+
fireEvent.click(row, { metaKey: true });
|
|
347
|
+
expect(writeText).not.toHaveBeenCalled();
|
|
348
|
+
expect(onFocusStep).not.toHaveBeenCalled();
|
|
349
|
+
expect(queryByRole('status', { name: 'Copied' })).toBeNull();
|
|
350
|
+
expect(row.firstElementChild?.textContent).toBe('1');
|
|
309
351
|
});
|
|
310
352
|
});
|
|
311
353
|
|
|
@@ -334,9 +376,31 @@ describe('TrailsPanel step semantics', () => {
|
|
|
334
376
|
expect(symbol.style.color).toBe(normal.style.color);
|
|
335
377
|
expect(symbol.style.fontFamily).toBe(defaultEditorTheme.fonts.body);
|
|
336
378
|
expect(symbol.previousElementSibling).toBe(annotation);
|
|
379
|
+
const row = symbol.closest('button')!;
|
|
380
|
+
expect(row.style.paddingTop).toBe('14px');
|
|
381
|
+
expect(row.style.paddingBottom).toBe('14px');
|
|
382
|
+
expect(row.style.paddingLeft).toBe(drilldown ? '12px' : '16px');
|
|
383
|
+
expect((row.firstElementChild as HTMLElement).style.width).toBe('14px');
|
|
384
|
+
expect((row.firstElementChild as HTMLElement).style.textAlign).toBe('center');
|
|
337
385
|
fireEvent.click(getByRole('button', { name: /Submit the login request.*Login.submit/ }));
|
|
338
386
|
expect(onFocusStep).toHaveBeenCalledWith(trail, 0);
|
|
339
387
|
});
|
|
388
|
+
|
|
389
|
+
test(`keeps nested steps offset from the ${drilldown ? 'selected' : 'inline'} header column`, () => {
|
|
390
|
+
const trail: SubsystemTrail = {
|
|
391
|
+
...trails[0]!,
|
|
392
|
+
steps: [
|
|
393
|
+
{ ...trails[0]!.steps[0]!, from: 'a', to: 'b', symbol: 'A.go' },
|
|
394
|
+
{ ...trails[0]!.steps[0]!, from: 'b', to: 'c', symbol: 'B.go' },
|
|
395
|
+
{ ...trails[0]!.steps[0]!, from: 'a', to: 'd', symbol: 'A.next' },
|
|
396
|
+
],
|
|
397
|
+
};
|
|
398
|
+
const { getByText } = renderPanel({ trails: [trail], drilldown, focusedTrailId: trail.id });
|
|
399
|
+
const inset = drilldown ? 12 : 16;
|
|
400
|
+
expect(getByText('A.go').closest('button')!.style.paddingLeft).toBe(`${inset}px`);
|
|
401
|
+
expect(getByText('B.go').closest('button')!.style.paddingLeft).toBe(`${inset + 10}px`);
|
|
402
|
+
expect(getByText('A.next').closest('button')!.style.paddingLeft).toBe(`${inset}px`);
|
|
403
|
+
});
|
|
340
404
|
}
|
|
341
405
|
|
|
342
406
|
test('calls leads with the caller frame', () => {
|