Skip to content

Commit e450e37

Browse files
author
DavidQ
committed
Normalize Session Inspector V2 tool objects and copy JSON Data Dirty validation payload - PR_26128_020-session-inspector-v2-data-dirty-model
1 parent 178fc29 commit e450e37

8 files changed

Lines changed: 99 additions & 21 deletions

File tree

docs/dev/reports/playwright_session_inspector_v2_data_dirty_model.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
- JSON shows the full selected object.
1818
- Data shows only selected `data`.
1919
- Dirty shows only selected `dirty`.
20+
- Copy is renamed to Copy All.
21+
- Copy All copies one payload with labeled JSON, Data, and Dirty sections.
22+
- Copy All includes empty-state text for missing Data or Dirty sections.
23+
- Copy All logs OK for complete normalized entries, WARN for copied missing-section empty states, and FAIL when no item is selected.
2024
- JSON, Data, Dirty, Controls, Filters, Entries, and Status accordions all open and close.
2125
- Split `workspace.tools.<tool-id>.schema` and `workspace.tools.<tool-id>.state` keys are not recreated.
2226

docs/dev/reports/session_inspector_v2_data_dirty_model.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
- JSON: full stored object
2020
- Data: selected item `data`
2121
- Dirty: selected item `dirty`
22+
- Renamed the JSON header Copy button to Copy All.
23+
- Copy All now copies one labeled validation payload containing JSON, Data, and Dirty sections.
2224
- Removed the Workspace Manager V2 Session Inspector tile subtitle `Session storage inspector`.
2325

2426
## Implementation Notes
@@ -27,6 +29,11 @@
2729
- Preview Generator V2 reads workspace session context from `workspace.tools.preview-generator-v2.workspace`.
2830
- Preview Generator V2 image generation behavior was not changed.
2931
- Data and Dirty views show actionable empty states when the selected storage item has no matching section.
32+
- Copy All includes Data/Dirty empty-state text in the copied payload when those sections are missing.
33+
- Copy All logs:
34+
- OK when all three sections copy for a selected normalized tool entry.
35+
- WARN when the copied payload includes missing Data/Dirty empty states.
36+
- FAIL when no storage entry is selected or the clipboard API fails.
3037
- Per-tile Delete and Delete All behavior were preserved.
3138

3239
## Guardrails
@@ -43,6 +50,10 @@
4350
- Verified dirty defaults to clean.
4451
- Verified Session Inspector V2 has JSON, Data, and Dirty views.
4552
- Verified State and Schema controls are absent from Session Inspector V2 UI.
53+
- Verified Copy is renamed to Copy All.
54+
- Verified Copy All copies one payload with clearly labeled JSON, Data, and Dirty sections.
55+
- Verified Copy All includes empty-state text for missing Data/Dirty sections.
56+
- Verified Copy All logs OK/WARN/FAIL outcomes, including an actionable no-selection error.
4657
- Verified JSON/Data/Dirty accordion behavior does not conflict with other accordions.
4758

4859
## Skipped

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
434434
await expect(page.locator(".session-inspector-v2__status-accordion-header #clearSessionInspectorV2StatusButton")).toHaveText("Clear Status");
435435
await expect(page.locator(".session-inspector-v2__json-accordion-header")).toContainText("JSON");
436436
await expect(page.locator(".session-inspector-v2__json-accordion-header")).not.toContainText("Details");
437-
await expect(page.locator(".session-inspector-v2__json-accordion-header #copySessionInspectorV2JsonButton")).toHaveText("Copy");
437+
await expect(page.locator(".session-inspector-v2__json-accordion-header #copySessionInspectorV2AllButton")).toHaveText("Copy All");
438438
await expect(page.locator(".session-inspector-v2__data-accordion-header")).toContainText("Data");
439439
await expect(page.locator(".session-inspector-v2__dirty-accordion-header")).toContainText("Dirty");
440440
await expect(page.locator(".session-inspector-v2__state-accordion-header")).toHaveCount(0);
@@ -476,7 +476,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
476476
const filterLabel = rectFor('label[for="sessionInspectorV2FilterInput"] span');
477477
const filterInput = rectFor("#sessionInspectorV2FilterInput");
478478
const jsonIcon = rectFor(".session-inspector-v2__json-accordion-header .accordion-v2__icon");
479-
const copyButton = rectFor("#copySessionInspectorV2JsonButton");
479+
const copyButton = rectFor("#copySessionInspectorV2AllButton");
480480
return {
481481
buttonsFit: [refresh, deleteAll, clearStatus].every((rect) => rect.scrollWidth <= rect.clientWidth + 1),
482482
clearStatusCompact: clearStatus.height <= 34,
@@ -634,10 +634,12 @@ test.describe("Workspace Manager V2 bootstrap", () => {
634634
await expect(page.locator("#sessionInspectorV2JsonOutput")).toHaveText("true");
635635
await expect(page.locator("#sessionInspectorV2DataOutput")).toContainText("No data section is present for sessionStorage:session-inspector-v2-alpha.");
636636
await expect(page.locator("#sessionInspectorV2DirtyOutput")).toContainText("No dirty section is present for sessionStorage:session-inspector-v2-alpha.");
637-
const copiedJsonText = await page.locator("#sessionInspectorV2JsonOutput").textContent();
638-
await page.locator("#copySessionInspectorV2JsonButton").click();
639-
await expect(page.locator("#statusLog")).toHaveValue(/OK Copied JSON content to clipboard\./);
640-
expect(await page.evaluate(() => window.__sessionInspectorV2ClipboardText)).toBe(copiedJsonText);
637+
await page.locator("#copySessionInspectorV2AllButton").click();
638+
await expect(page.locator("#statusLog")).toHaveValue(/WARN Copied JSON, Data, and Dirty sections with empty-state text for missing Data and Dirty\./);
639+
const copiedValidationText = await page.evaluate(() => window.__sessionInspectorV2ClipboardText);
640+
expect(copiedValidationText).toContain("=== JSON ===\ntrue");
641+
expect(copiedValidationText).toContain("=== Data ===\nNo data section is present for sessionStorage:session-inspector-v2-alpha.");
642+
expect(copiedValidationText).toContain("=== Dirty ===\nNo dirty section is present for sessionStorage:session-inspector-v2-alpha.");
641643
await page.locator('[data-session-inspector-v2-delete-entry-id="sessionStorage:session-inspector-v2-alpha"]').click();
642644
await expect(page.locator("#sessionInspectorV2EntryList [data-session-inspector-v2-entry-id]")).toHaveCount(4);
643645
await expect(page.locator("#sessionInspectorV2JsonOutput")).toHaveText('"plain beta value"');
@@ -672,8 +674,8 @@ test.describe("Workspace Manager V2 bootstrap", () => {
672674
"(0) LocalStorage."
673675
]);
674676
await expect(page.locator("#statusLog")).toHaveValue(/OK Deleted 4 shown storage entries\./);
675-
await page.locator("#copySessionInspectorV2JsonButton").click();
676-
await expect(page.locator("#statusLog")).toHaveValue(/WARN Copy skipped: no JSON content is shown\./);
677+
await page.locator("#copySessionInspectorV2AllButton").click();
678+
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Copy All failed: select a storage entry before copying JSON, Data, and Dirty\./);
677679
await page.locator("#deleteAllSessionInspectorV2Button").click();
678680
await expect(page.locator("#statusLog")).toHaveValue(/WARN Delete All skipped: no matching storage entries are shown\./);
679681
expect(await page.evaluate(() => window.localStorage.getItem("session-inspector-v2-local"))).toBe("local value");
@@ -695,6 +697,14 @@ test.describe("Workspace Manager V2 bootstrap", () => {
695697
test("shows normalized workspace tool sessions as JSON, Data, and Dirty views", async ({ page }) => {
696698
const pageErrors = [];
697699
await page.addInitScript(() => {
700+
Object.defineProperty(window.navigator, "clipboard", {
701+
configurable: true,
702+
value: {
703+
async writeText(value) {
704+
window.__sessionInspectorV2ClipboardText = value;
705+
}
706+
}
707+
});
698708
window.sessionStorage.setItem("workspace.tools.preview-generator-v2", JSON.stringify({
699709
schema: {
700710
source: "workspace-manager-v2",
@@ -816,6 +826,17 @@ test.describe("Workspace Manager V2 bootstrap", () => {
816826
await expect(page.locator("#sessionInspectorV2DirtyOutput")).not.toContainText('"data"');
817827
await expect(page.locator("#sessionInspectorV2DirtyOutput")).not.toContainText('"workspace"');
818828
await expect(page.locator("#sessionInspectorV2DirtyOutput")).not.toContainText('"schema"');
829+
await page.locator("#copySessionInspectorV2AllButton").click();
830+
await expect(page.locator("#statusLog")).toHaveValue(/OK Copied JSON, Data, and Dirty sections to clipboard\./);
831+
const copiedToolPayload = await page.evaluate(() => window.__sessionInspectorV2ClipboardText);
832+
expect(copiedToolPayload).toContain("=== JSON ===");
833+
expect(copiedToolPayload).toContain("=== Data ===");
834+
expect(copiedToolPayload).toContain("=== Dirty ===");
835+
expect(copiedToolPayload).toContain('"workspace"');
836+
expect(copiedToolPayload).toContain('"data"');
837+
expect(copiedToolPayload).toContain('"dirty"');
838+
expect(copiedToolPayload).toContain('"assets.image.preview.preview"');
839+
expect(copiedToolPayload).toContain('"isDirty": false');
819840

820841
await page.locator('[data-session-inspector-v2-entry-id="sessionStorage:workspace.tools.no-data-test"]').click();
821842
await expect(page.locator("#sessionInspectorV2DataOutput")).toContainText("No data section is present for sessionStorage:workspace.tools.no-data-test.");

tools/session-inspector-v2/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ <h2 class="tools-platform-frame__eyebrow">Session storage visibility</h2>
101101
<span>JSON</span>
102102
<div class="session-inspector-v2__json-header-actions">
103103
<span class="accordion-v2__icon" aria-hidden="true">+</span>
104-
<button id="copySessionInspectorV2JsonButton" type="button">Copy</button>
104+
<button id="copySessionInspectorV2AllButton" type="button">Copy All</button>
105105
</div>
106106
</div>
107107
<div id="sessionInspectorV2JsonContent" class="accordion-v2__content">

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

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export class SessionInspectorV2App {
22
constructor({
33
accordions,
4-
copyJsonButton,
4+
copyAllButton,
55
data,
66
deleteAllButton,
77
dirty,
@@ -16,7 +16,7 @@ export class SessionInspectorV2App {
1616
windowRef = window
1717
}) {
1818
this.accordions = accordions;
19-
this.copyJsonButton = copyJsonButton;
19+
this.copyAllButton = copyAllButton;
2020
this.data = data;
2121
this.deleteAllButton = deleteAllButton;
2222
this.dirty = dirty;
@@ -48,8 +48,8 @@ export class SessionInspectorV2App {
4848
onSelected: (entryId) => this.selectEntry(entryId)
4949
});
5050
this.refreshButton.addEventListener("click", () => this.refresh());
51-
this.copyJsonButton.addEventListener("click", () => {
52-
void this.copyJson();
51+
this.copyAllButton.addEventListener("click", () => {
52+
void this.copyAll();
5353
});
5454
this.deleteAllButton.addEventListener("click", () => this.deleteAllShownEntries());
5555
this.returnToWorkspaceButton.addEventListener("click", () => this.returnToWorkspace());
@@ -131,21 +131,55 @@ export class SessionInspectorV2App {
131131
this.refresh({ silent: true });
132132
}
133133

134-
async copyJson() {
134+
copyAllPayload() {
135+
return [
136+
"=== JSON ===",
137+
this.json.text().trim(),
138+
"",
139+
"=== Data ===",
140+
this.data.text().trim(),
141+
"",
142+
"=== Dirty ===",
143+
this.dirty.text().trim()
144+
].join("\n");
145+
}
146+
147+
missingSelectedSections(entry) {
148+
const value = entry?.parseOk ? entry.parsedValue : null;
149+
if (!value || typeof value !== "object" || Array.isArray(value)) {
150+
return ["Data", "Dirty"];
151+
}
152+
return ["Data", "Dirty"].filter((sectionName) => (
153+
!Object.prototype.hasOwnProperty.call(value, sectionName.toLowerCase())
154+
));
155+
}
156+
157+
async copyAll() {
158+
const selectedEntry = this.entries.find((entry) => entry.id === this.selectedId);
159+
if (!selectedEntry) {
160+
this.statusLog.fail("Copy All failed: select a storage entry before copying JSON, Data, and Dirty.");
161+
return;
162+
}
135163
const jsonText = this.json.text().trim();
136-
if (!jsonText || jsonText === "{}") {
137-
this.statusLog.warn("Copy skipped: no JSON content is shown.");
164+
if (!jsonText) {
165+
this.statusLog.warn("Copy All skipped: no JSON content is shown for the selected storage entry.");
138166
return;
139167
}
140168
if (typeof this.window.navigator?.clipboard?.writeText !== "function") {
141-
this.statusLog.fail("Copy failed: clipboard API is unavailable.");
169+
this.statusLog.fail("Copy All failed: clipboard API is unavailable.");
142170
return;
143171
}
172+
const payload = this.copyAllPayload();
173+
const missingSections = this.missingSelectedSections(selectedEntry);
144174
try {
145-
await this.window.navigator.clipboard.writeText(jsonText);
146-
this.statusLog.ok("Copied JSON content to clipboard.");
175+
await this.window.navigator.clipboard.writeText(payload);
176+
if (missingSections.length) {
177+
this.statusLog.warn(`Copied JSON, Data, and Dirty sections with empty-state text for missing ${missingSections.join(" and ")}.`);
178+
return;
179+
}
180+
this.statusLog.ok("Copied JSON, Data, and Dirty sections to clipboard.");
147181
} catch (error) {
148-
this.statusLog.fail(`Copy failed: ${error.message}`);
182+
this.statusLog.fail(`Copy All failed: ${error.message}`);
149183
}
150184
}
151185

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function requireElement(selector) {
2020
window.addEventListener("DOMContentLoaded", () => {
2121
const app = new SessionInspectorV2App({
2222
accordions: Array.from(document.querySelectorAll(".accordion-v2"), (section) => new AccordionSection(section)),
23-
copyJsonButton: requireElement("#copySessionInspectorV2JsonButton"),
23+
copyAllButton: requireElement("#copySessionInspectorV2AllButton"),
2424
data: new DataControl({
2525
output: requireElement("#sessionInspectorV2DataOutput")
2626
}),

tools/session-inspector-v2/js/controls/DataControl.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ export class DataControl {
77
this.output.textContent = "Select a normalized tool entry with a top-level data section.";
88
}
99

10+
text() {
11+
return String(this.output.textContent || "");
12+
}
13+
1014
render(entry) {
1115
if (!entry) {
1216
this.clear();

tools/session-inspector-v2/js/controls/DirtyControl.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ export class DirtyControl {
77
this.output.textContent = "Select a normalized tool entry with a top-level dirty section.";
88
}
99

10+
text() {
11+
return String(this.output.textContent || "");
12+
}
13+
1014
render(entry) {
1115
if (!entry) {
1216
this.clear();

0 commit comments

Comments
 (0)