Skip to content

Commit c4bf667

Browse files
author
DavidQ
committed
Share Session Inspector V2 detail panel height and label Copy All output with session key - PR_26128_026-session-inspector-v2-shared-detail-space
1 parent 65ecfab commit c4bf667

5 files changed

Lines changed: 205 additions & 57 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Playwright Session Inspector V2 Shared Detail Space
2+
3+
## Targeted Coverage
4+
- Verified 1 open detail section receives the full open-section output allocation.
5+
- Verified 2 open detail sections split output height evenly.
6+
- Verified 3 open detail sections split output height evenly.
7+
- Verified 4 open detail sections split output height evenly.
8+
- Verified closed detail sections show only their headers.
9+
- Verified open outputs keep vertical scrolling on the output element.
10+
- Verified open outputs have no horizontal scrollbar and wrap long lines.
11+
- Verified lower detail headers remain reachable.
12+
- Verified Copy All includes the selected storage key before JSON, Data, and Dirty data.
13+
14+
## Commands
15+
- `node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs`
16+
- `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list -g "launches Session Inspector V2 with V2 labels, accordions, theme, and delete controls"`
17+
- `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list -g "shows normalized workspace tool sessions as JSON, Data, and Dirty views"`
18+
- `npm run test:workspace-v2`
19+
20+
## Result
21+
- Focused Session Inspector V2 launch/copy validation passed.
22+
- Focused shared detail space validation passed.
23+
- Full workspace-v2 Playwright validation passed: 15 tests.
24+
25+
## Full Samples Smoke
26+
- Skipped per PR instructions because this change is scoped to Session Inspector V2 layout and Copy All output text.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# PR_26128_026 Session Inspector V2 Shared Detail Space
2+
3+
## Scope
4+
- Updated Session Inspector V2 right-side detail layout.
5+
- Updated Copy All payload formatting for selected storage entries.
6+
- Preserved normalized session object shape, Dirty header values, and Clear Status behavior.
7+
8+
## Changes
9+
- JSON, Data, Dirty, and Status now share the right detail panel through flex layout.
10+
- Open detail sections split available vertical space evenly.
11+
- Closed detail sections keep only their headers visible.
12+
- JSON/Data/Dirty/Status outputs keep vertical scrolling inside the output element.
13+
- JSON/Data/Dirty/Status outputs wrap long lines and suppress horizontal scrolling.
14+
- Copy All now includes `Session: <storageType>:<key>` before each JSON, Data, and Dirty block.
15+
16+
## Guardrails
17+
- No sample JSON files modified.
18+
- No roadmap files modified.
19+
- No cross-tool communication added.
20+
- No schema or normalized session object shape changes.
21+
22+
## Validation
23+
- `node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs` passed.
24+
- Focused Session Inspector V2 launch/copy test passed.
25+
- Focused normalized JSON/Data/Dirty shared-space test passed.
26+
- `npm run test:workspace-v2` passed: 15 tests.
27+
28+
## Full Samples Smoke
29+
- Skipped per PR instructions. This PR is limited to Session Inspector V2 detail-panel layout and Copy All text formatting; targeted Workspace Manager V2 Playwright coverage validates the affected launch and inspector flows.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 108 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,97 @@ async function expectSessionInspectorV2DetailAccordionsIndependent(page) {
217217
}
218218
}
219219

220+
const SESSION_INSPECTOR_V2_DETAIL_SECTIONS = [
221+
{
222+
contentId: "sessionInspectorV2JsonContent",
223+
outputSelector: "#sessionInspectorV2JsonOutput"
224+
},
225+
{
226+
contentId: "sessionInspectorV2DataContent",
227+
outputSelector: "#sessionInspectorV2DataOutput"
228+
},
229+
{
230+
contentId: "sessionInspectorV2DirtyContent",
231+
outputSelector: "#sessionInspectorV2DirtyOutput"
232+
},
233+
{
234+
contentId: "sessionInspectorV2StatusContent",
235+
outputSelector: "#statusLog"
236+
}
237+
];
238+
239+
async function setSessionInspectorV2DetailSectionsOpen(page, openContentIds) {
240+
const openSet = new Set(openContentIds);
241+
for (const { contentId } of SESSION_INSPECTOR_V2_DETAIL_SECTIONS) {
242+
const header = page.locator(`.accordion-v2__header[aria-controls="${contentId}"]`);
243+
const content = page.locator(`#${contentId}`);
244+
const isOpen = await content.evaluate((element) => !element.hidden);
245+
if (isOpen !== openSet.has(contentId)) {
246+
await header.click();
247+
}
248+
if (openSet.has(contentId)) {
249+
await expect(content).toBeVisible();
250+
await expect(header).toHaveAttribute("aria-expanded", "true");
251+
} else {
252+
await expect(content).toBeHidden();
253+
await expect(header).toHaveAttribute("aria-expanded", "false");
254+
}
255+
}
256+
}
257+
258+
async function expectSessionInspectorV2SharedDetailSpace(page, openContentIds) {
259+
await setSessionInspectorV2DetailSectionsOpen(page, openContentIds);
260+
const state = await page.evaluate((sections) => {
261+
const rightPanel = document.querySelector(".session-inspector-v2__panel--right");
262+
const rightRect = rightPanel.getBoundingClientRect();
263+
const details = sections.map((sectionInfo) => {
264+
const content = document.querySelector(`#${sectionInfo.contentId}`);
265+
const section = content.closest(".session-inspector-v2__accordion");
266+
const header = section.querySelector(".session-inspector-v2__accordion-header-row") || section.querySelector(".accordion-v2__header");
267+
const output = document.querySelector(sectionInfo.outputSelector);
268+
const outputStyle = getComputedStyle(output);
269+
const sectionRect = section.getBoundingClientRect();
270+
const headerRect = header.getBoundingClientRect();
271+
const outputRect = output.getBoundingClientRect();
272+
const isOpen = !content.hidden;
273+
return {
274+
contentId: sectionInfo.contentId,
275+
closedHeaderOnly: isOpen || sectionRect.height <= headerRect.height + 5,
276+
headerReachable: headerRect.top >= rightRect.top - 1 && headerRect.bottom <= rightRect.bottom + 1,
277+
isOpen,
278+
noHorizontalScrollbar: output.scrollWidth <= output.clientWidth + 1,
279+
outputHeight: isOpen ? outputRect.height : 0,
280+
overflowY: outputStyle.overflowY,
281+
wrapsLongLines: outputStyle.whiteSpace === "pre-wrap" && outputStyle.overflowWrap === "anywhere"
282+
};
283+
});
284+
const openDetails = details.filter((detail) => detail.isOpen);
285+
const totalOpenOutputHeight = openDetails.reduce((total, detail) => total + detail.outputHeight, 0);
286+
const expectedOpenOutputHeight = totalOpenOutputHeight / Math.max(openDetails.length, 1);
287+
return {
288+
closedSectionsHeaderOnly: details.every((detail) => detail.closedHeaderOnly),
289+
headersReachable: details.every((detail) => detail.headerReachable),
290+
openContentIds: openDetails.map((detail) => detail.contentId),
291+
openCount: openDetails.length,
292+
openOutputHeight: expectedOpenOutputHeight,
293+
openOutputsSplitEvenly: openDetails.every((detail) => Math.abs(detail.outputHeight - expectedOpenOutputHeight) <= 1),
294+
openOutputsUseInternalVerticalScroll: openDetails.every((detail) => detail.overflowY === "auto"),
295+
outputsHaveNoHorizontalScrollbars: openDetails.every((detail) => detail.noHorizontalScrollbar),
296+
outputsWrapLongLines: openDetails.every((detail) => detail.wrapsLongLines)
297+
};
298+
}, SESSION_INSPECTOR_V2_DETAIL_SECTIONS);
299+
300+
expect(state.openContentIds).toEqual(openContentIds);
301+
expect(state.openCount).toBe(openContentIds.length);
302+
expect(state.openOutputsSplitEvenly).toBe(true);
303+
expect(state.openOutputsUseInternalVerticalScroll).toBe(true);
304+
expect(state.outputsHaveNoHorizontalScrollbars).toBe(true);
305+
expect(state.outputsWrapLongLines).toBe(true);
306+
expect(state.closedSectionsHeaderOnly).toBe(true);
307+
expect(state.headersReachable).toBe(true);
308+
return state.openOutputHeight;
309+
}
310+
220311
async function expectSessionInspectorV2FullscreenShell(page) {
221312
const summary = page.locator("[data-session-inspector-v2-summary]");
222313
await summary.click();
@@ -665,9 +756,9 @@ test.describe("Workspace Manager V2 bootstrap", () => {
665756
await page.locator("#copySessionInspectorV2AllButton").click();
666757
await expect(page.locator("#statusLog")).toHaveValue(/WARN Copied JSON, Data, and Dirty sections with empty-state text for missing Data and Dirty\./);
667758
const copiedValidationText = await page.evaluate(() => window.__sessionInspectorV2ClipboardText);
668-
expect(copiedValidationText).toContain("=== JSON ===\ntrue");
669-
expect(copiedValidationText).toContain("=== Data ===\nNo data section is present for sessionStorage:session-inspector-v2-alpha.");
670-
expect(copiedValidationText).toContain("=== Dirty ===\nNo dirty section is present for sessionStorage:session-inspector-v2-alpha.");
759+
expect(copiedValidationText).toContain("=== JSON ===\nSession: sessionStorage:session-inspector-v2-alpha\ntrue");
760+
expect(copiedValidationText).toContain("=== Data ===\nSession: sessionStorage:session-inspector-v2-alpha\nNo data section is present for sessionStorage:session-inspector-v2-alpha.");
761+
expect(copiedValidationText).toContain("=== Dirty ===\nSession: sessionStorage:session-inspector-v2-alpha\nNo dirty section is present for sessionStorage:session-inspector-v2-alpha.");
671762
await page.locator("#clearSessionInspectorV2StatusButton").click();
672763
await expect(page.locator("#statusLog")).toHaveValue("");
673764
await page.locator('[data-session-inspector-v2-delete-entry-id="sessionStorage:session-inspector-v2-alpha"]').click();
@@ -892,62 +983,36 @@ test.describe("Workspace Manager V2 bootstrap", () => {
892983
await expect(page.locator("#sessionInspectorV2DirtyOutput")).not.toContainText('"workspace"');
893984
await expect(page.locator("#sessionInspectorV2DirtyOutput")).not.toContainText('"schema"');
894985
await expect(page.locator("#sessionInspectorV2DirtyHeaderValue")).toHaveText("Dirty: false");
986+
const allDetailContentIds = SESSION_INSPECTOR_V2_DETAIL_SECTIONS.map((section) => section.contentId);
987+
const fourOpenOutputHeight = await expectSessionInspectorV2SharedDetailSpace(page, allDetailContentIds);
988+
const threeOpenOutputHeight = await expectSessionInspectorV2SharedDetailSpace(page, allDetailContentIds.slice(0, 3));
989+
const twoOpenOutputHeight = await expectSessionInspectorV2SharedDetailSpace(page, allDetailContentIds.slice(0, 2));
990+
const oneOpenOutputHeight = await expectSessionInspectorV2SharedDetailSpace(page, allDetailContentIds.slice(0, 1));
991+
expect(oneOpenOutputHeight).toBeGreaterThan(twoOpenOutputHeight);
992+
expect(twoOpenOutputHeight).toBeGreaterThan(threeOpenOutputHeight);
993+
expect(threeOpenOutputHeight).toBeGreaterThan(fourOpenOutputHeight);
994+
await setSessionInspectorV2DetailSectionsOpen(page, allDetailContentIds);
895995
const detailPanelState = await page.evaluate(() => {
896-
const jsonContent = document.querySelector("#sessionInspectorV2JsonContent");
897-
const dataContent = document.querySelector("#sessionInspectorV2DataContent");
898996
const jsonOutput = document.querySelector("#sessionInspectorV2JsonOutput");
899997
const dataOutput = document.querySelector("#sessionInspectorV2DataOutput");
900-
const dirtyHeader = document.querySelector(".session-inspector-v2__dirty-accordion-header");
901-
const statusHeader = document.querySelector(".session-inspector-v2__status-accordion-header");
902-
const rightPanel = document.querySelector(".session-inspector-v2__panel--right");
903-
const statusOutput = document.querySelector("#statusLog");
904-
const jsonOutputStyle = getComputedStyle(jsonOutput);
905-
const dataOutputStyle = getComputedStyle(dataOutput);
906-
const jsonContentStyle = getComputedStyle(jsonContent);
907-
const dataContentStyle = getComputedStyle(dataContent);
908-
const rectFor = (element) => element.getBoundingClientRect();
909-
const rightRect = rectFor(rightPanel);
910-
const dirtyHeaderRect = rectFor(dirtyHeader);
911-
const statusHeaderRect = rectFor(statusHeader);
912-
rightPanel.scrollTop = rightPanel.scrollHeight;
913-
const scrolledDirtyHeaderRect = rectFor(dirtyHeader);
914-
const scrolledStatusHeaderRect = rectFor(statusHeader);
915-
const statusOutputHeight = Math.round(rectFor(statusOutput).height);
916998
return {
917-
dataContentDoesNotOwnScrollbar: dataContentStyle.overflowY === "hidden" && dataContentStyle.overflowX === "hidden",
918-
dataOutputHasNoHorizontalScrollbar: dataOutput.scrollWidth <= dataOutput.clientWidth + 1,
919999
dataOutputScrollsVertically: dataOutput.scrollHeight > dataOutput.clientHeight + 1,
920-
dataOutputHeightMatchesStatus: Math.abs(Math.round(rectFor(dataOutput).height) - statusOutputHeight) <= 1,
921-
dataOutputWrapsLongLines: dataOutputStyle.whiteSpace === "pre-wrap" && dataOutputStyle.overflowWrap === "anywhere",
922-
dirtyHeaderReachable: dirtyHeaderRect.top >= rightRect.top || (scrolledDirtyHeaderRect.top >= rightRect.top && scrolledDirtyHeaderRect.bottom <= rightRect.bottom),
923-
jsonContentDoesNotOwnScrollbar: jsonContentStyle.overflowY === "hidden" && jsonContentStyle.overflowX === "hidden",
924-
jsonOutputHasNoHorizontalScrollbar: jsonOutput.scrollWidth <= jsonOutput.clientWidth + 1,
925-
jsonOutputScrollsVertically: jsonOutput.scrollHeight > jsonOutput.clientHeight + 1,
926-
jsonOutputHeightMatchesStatus: Math.abs(Math.round(rectFor(jsonOutput).height) - statusOutputHeight) <= 1,
927-
jsonOutputWrapsLongLines: jsonOutputStyle.whiteSpace === "pre-wrap" && jsonOutputStyle.overflowWrap === "anywhere",
928-
statusHeaderReachable: statusHeaderRect.top >= rightRect.top || (scrolledStatusHeaderRect.top >= rightRect.top && scrolledStatusHeaderRect.bottom <= rightRect.bottom)
1000+
jsonOutputScrollsVertically: jsonOutput.scrollHeight > jsonOutput.clientHeight + 1
9291001
};
9301002
});
9311003
expect(detailPanelState).toEqual({
932-
dataContentDoesNotOwnScrollbar: true,
933-
dataOutputHasNoHorizontalScrollbar: true,
9341004
dataOutputScrollsVertically: true,
935-
dataOutputHeightMatchesStatus: true,
936-
dataOutputWrapsLongLines: true,
937-
dirtyHeaderReachable: true,
938-
jsonContentDoesNotOwnScrollbar: true,
939-
jsonOutputHasNoHorizontalScrollbar: true,
940-
jsonOutputScrollsVertically: true,
941-
jsonOutputHeightMatchesStatus: true,
942-
jsonOutputWrapsLongLines: true,
943-
statusHeaderReachable: true
1005+
jsonOutputScrollsVertically: true
9441006
});
9451007
await page.locator("#copySessionInspectorV2AllButton").click();
9461008
await expect(page.locator("#statusLog")).toHaveValue(/OK Copied JSON, Data, and Dirty sections to clipboard\./);
9471009
const copiedToolPayload = await page.evaluate(() => window.__sessionInspectorV2ClipboardText);
9481010
expect(copiedToolPayload).toContain("=== JSON ===");
1011+
expect(copiedToolPayload).toContain("=== JSON ===\nSession: sessionStorage:workspace.tools.asset-manager-v2\n");
9491012
expect(copiedToolPayload).toContain("=== Data ===");
1013+
expect(copiedToolPayload).toContain("=== Data ===\nSession: sessionStorage:workspace.tools.asset-manager-v2\n");
9501014
expect(copiedToolPayload).toContain("=== Dirty ===");
1015+
expect(copiedToolPayload).toContain("=== Dirty ===\nSession: sessionStorage:workspace.tools.asset-manager-v2\n");
9511016
expect(copiedToolPayload).toContain('"workspace"');
9521017
expect(copiedToolPayload).toContain('"data"');
9531018
expect(copiedToolPayload).toContain('"dirty"');

tools/session-inspector-v2/js/SessionInspectorV2App.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,15 +131,23 @@ export class SessionInspectorV2App {
131131
this.refresh({ silent: true });
132132
}
133133

134-
copyAllPayload() {
134+
selectedSessionLabel(entry) {
135+
return entry ? `${entry.storageType}:${entry.key}` : "(none)";
136+
}
137+
138+
copyAllPayload(entry) {
139+
const sessionLine = `Session: ${this.selectedSessionLabel(entry)}`;
135140
return [
136141
"=== JSON ===",
142+
sessionLine,
137143
this.json.text().trim(),
138144
"",
139145
"=== Data ===",
146+
sessionLine,
140147
this.data.text().trim(),
141148
"",
142149
"=== Dirty ===",
150+
sessionLine,
143151
this.dirty.text().trim()
144152
].join("\n");
145153
}
@@ -169,7 +177,7 @@ export class SessionInspectorV2App {
169177
this.statusLog.fail("Copy All failed: clipboard API is unavailable.");
170178
return;
171179
}
172-
const payload = this.copyAllPayload();
180+
const payload = this.copyAllPayload(selectedEntry);
173181
const missingSections = this.missingSelectedSections(selectedEntry);
174182
try {
175183
await this.window.navigator.clipboard.writeText(payload);

tools/session-inspector-v2/styles/sessionInspectorV2.css

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ body[data-tool-id="session-inspector-v2"],
1010
--session-inspector-v2-header-surface: var(--tools-shell-glass, var(--panel));
1111
--session-inspector-v2-shadow: var(--tools-shell-shadow, none);
1212
--session-inspector-v2-shadow-strong: var(--tools-shell-shadow-strong, none);
13-
--session-inspector-v2-output-height: 180px;
1413
}
1514

1615
body {
@@ -170,11 +169,22 @@ button:hover {
170169
}
171170

172171
.session-inspector-v2__panel--right {
173-
overflow: auto;
172+
overflow: hidden;
173+
}
174+
175+
.session-inspector-v2__panel--right > .session-inspector-v2__accordion {
176+
flex: 0 0 auto;
177+
}
178+
179+
.session-inspector-v2__panel--right > .session-inspector-v2__accordion.is-open {
180+
flex: 1 1 0;
181+
}
182+
183+
.session-inspector-v2__panel--right > .session-inspector-v2__accordion:not(.is-open) {
184+
flex: 0 0 auto;
174185
}
175186

176187
.session-inspector-v2__accordion--detail.is-open {
177-
flex: 0 1 auto;
178188
min-height: 0;
179189
max-height: none;
180190
}
@@ -255,9 +265,9 @@ button:hover {
255265
}
256266

257267
.session-inspector-v2__accordion--detail .session-inspector-v2__output {
258-
flex: 0 0 var(--session-inspector-v2-output-height);
259-
height: var(--session-inspector-v2-output-height);
260-
max-height: var(--session-inspector-v2-output-height);
268+
flex: 1 1 0;
269+
height: auto;
270+
max-height: none;
261271
min-height: 0;
262272
overflow-x: hidden;
263273
overflow-y: auto;
@@ -421,21 +431,31 @@ button:hover {
421431
.session-inspector-v2__output,
422432
#statusLog {
423433
width: 100%;
424-
height: var(--session-inspector-v2-output-height);
425-
min-height: var(--session-inspector-v2-output-height);
434+
min-height: 0;
426435
box-sizing: border-box;
427436
margin: 0;
428437
padding: 10px;
429-
overflow: auto;
438+
overflow-x: hidden;
439+
overflow-y: auto;
440+
overflow-wrap: anywhere;
430441
white-space: pre-wrap;
442+
word-break: break-word;
431443
}
432444

433-
.session-inspector-v2__output {
434-
flex: 1 1 auto;
445+
.session-inspector-v2__panel--right .accordion-v2__content {
446+
flex: 1 1 0;
447+
overflow: hidden;
448+
padding: 0;
449+
}
450+
451+
.session-inspector-v2__panel--right :is(.session-inspector-v2__output, #statusLog) {
452+
flex: 1 1 0;
453+
height: auto;
454+
max-height: none;
435455
}
436456

437457
#statusLog {
438-
resize: vertical;
458+
resize: none;
439459
}
440460

441461
.session-inspector-v2__json-header-actions,

0 commit comments

Comments
 (0)