Skip to content

Commit a53477c

Browse files
committed
Repair MIDI Studio V2 UAT layout, accordion, and expanded view behavior - PR_26146_015-midi-studio-v2-uat-layout-repair
1 parent 9855152 commit a53477c

7 files changed

Lines changed: 185 additions & 23 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# PR_26146_015-midi-studio-v2-uat-layout-repair Validation
2+
3+
## Scope
4+
5+
- Moved Export WAV, Export MP3, Export OGG, and export target type controls out of the Rendered Export Targets header and into the MIDI Studio V2 NAV/action bar.
6+
- Kept export actions honest through the existing rendered export action controller: missing song/target reports FAIL, unimplemented rendering reports WARN, and no fake success is claimed.
7+
- Repaired accordion toggle wiring so header buttons toggle themselves while nested header controls remain independently clickable.
8+
- Updated accordion +/- text state to match expanded/collapsed state.
9+
- Added expanded workspace behavior for Hide Headers/Details with status logging, primary work surface expansion, header/detail hiding, and NAV/status/recovery access preserved.
10+
- Preserved guided Song Sheet fields, Use Example Test Song, dropdowns, grid editor, snapping, generated lanes, playhead/loop, MIDI inspection, and rendered preview behavior.
11+
12+
## Validation
13+
14+
- PASS: `node --check` for changed JavaScript and targeted Playwright spec.
15+
- PASS: MIDI Studio V2 inline HTML check for inline scripts, styles, and event handlers returned no matches.
16+
- PASS: targeted MIDI Studio V2 Playwright suite: `32 passed`.
17+
- PASS: `git diff --check` completed successfully. Git reported line-ending warnings for existing LF/CRLF normalization only.
18+
- SKIP: Workspace Manager V2 registration/handoff was not run because this PR did not touch Workspace Manager registration or handoff files.
19+
- SKIP: full samples smoke test per request. Samples decision: SKIP because sample JSON alignment is out of scope.
20+
21+
## Notes
22+
23+
- The first full Playwright run timed out while exposing an accordion self-click guard bug. The isolated accordion test was rerun after the fix and passed, then the full targeted suite passed.

tests/playwright/tools/MidiStudioV2.spec.mjs

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,9 +232,10 @@ test.describe("MIDI Studio V2", () => {
232232
await expect(page.locator("#inspectMidiSourceButton")).toBeEnabled();
233233
const renderedHeader = page.locator('.accordion-v2__header[aria-controls="renderedTargetsContent"]');
234234
await expect(renderedHeader).toContainText("Rendered Export Targets");
235-
await expect(renderedHeader.locator("#exportWavButton")).toBeVisible();
236-
await expect(renderedHeader.locator("#exportMp3Button")).toBeVisible();
237-
await expect(renderedHeader.locator("#exportOggButton")).toBeVisible();
235+
await expect(renderedHeader.locator("#exportWavButton")).toHaveCount(0);
236+
await expect(page.locator(".midi-studio-v2__tool-menu #exportWavButton")).toBeVisible();
237+
await expect(page.locator(".midi-studio-v2__tool-menu #exportMp3Button")).toBeVisible();
238+
await expect(page.locator(".midi-studio-v2__tool-menu #exportOggButton")).toBeVisible();
238239
await expect(page.locator("#midiSourceDetails")).toContainText("No MIDI source inspected.");
239240
await expect(page.locator("#playbackState")).toContainText("Live MIDI synthesis: NOT IMPLEMENTED");
240241
await expect(page.locator("#statusLog")).toHaveValue(/OK Loaded 3 MIDI songs/);
@@ -321,9 +322,11 @@ test.describe("MIDI Studio V2", () => {
321322
}
322323
});
323324

324-
test("reports rendered export header action status without claiming files were written", async ({ page }) => {
325+
test("reports rendered export nav action status without claiming files were written", async ({ page }) => {
325326
const server = await openMidiStudio(page);
326327
try {
328+
await expect(page.locator('.accordion-v2__header[aria-controls="renderedTargetsContent"] #exportWavButton')).toHaveCount(0);
329+
await expect(page.locator(".midi-studio-v2__tool-menu #exportWavButton")).toBeVisible();
327330
await page.locator("#exportWavButton").click();
328331
await page.locator("#exportMp3Button").click();
329332
await page.locator("#exportOggButton").click();
@@ -849,6 +852,70 @@ Am F`);
849852
}
850853
});
851854

855+
test("opens and closes every MIDI Studio accordion with matching icon state", async ({ page }) => {
856+
const server = await openMidiStudio(page);
857+
try {
858+
const headerCount = await page.locator(".accordion-v2__header").count();
859+
for (let index = 0; index < headerCount; index += 1) {
860+
const header = page.locator(".accordion-v2__header").nth(index);
861+
const controls = await header.getAttribute("aria-controls");
862+
expect(controls).toBeTruthy();
863+
const content = page.locator(`#${controls}`);
864+
await expect(content).toHaveCount(1);
865+
await expect(header).toHaveAttribute("aria-expanded", "true");
866+
await expect(header.locator(".accordion-v2__icon")).toHaveText("-");
867+
await header.click({ position: { x: 8, y: 8 } });
868+
await expect(header).toHaveAttribute("aria-expanded", "false");
869+
await expect(header.locator(".accordion-v2__icon")).toHaveText("+");
870+
await expect(content).toBeHidden();
871+
await header.click({ position: { x: 8, y: 8 } });
872+
await expect(header).toHaveAttribute("aria-expanded", "true");
873+
await expect(header.locator(".accordion-v2__icon")).toHaveText("-");
874+
await expect(content).toBeVisible();
875+
}
876+
expect(await page.locator("[id]").evaluateAll((elements) => {
877+
const ids = elements.map((element) => element.id).filter(Boolean);
878+
return ids.length === new Set(ids).size;
879+
})).toBe(true);
880+
} finally {
881+
await workspaceV2CoverageReporter.stop(page);
882+
await server.close();
883+
}
884+
});
885+
886+
test("expands and restores the MIDI Studio workspace from the header details toggle", async ({ page }) => {
887+
const server = await openMidiStudio(page);
888+
try {
889+
const centerPanel = page.locator(".tool-starter__panel--center");
890+
const normalWidth = (await centerPanel.boundingBox())?.width || 0;
891+
await page.locator("[data-midi-studio-summary]").click();
892+
await expect(page.locator("body")).toHaveClass(/midi-studio-v2--expanded/);
893+
await expect(page.locator("[data-midi-studio-summary]")).toContainText("Show Header and Details");
894+
await expect(page.locator("#shared-theme-header")).toBeHidden();
895+
await expect(page.locator(".tool-starter__panel--left")).toBeHidden();
896+
await expect(page.locator("#songDetailsContent")).toBeHidden();
897+
await expect(page.locator("#instrumentGridContent")).toBeVisible();
898+
await expect(page.locator(".midi-studio-v2__tool-menu")).toBeVisible();
899+
await expect(page.locator("#useExampleButton")).toBeVisible();
900+
await expect(page.locator("#statusLogContent")).toBeVisible();
901+
await expect(page.locator("#clearStatusButton")).toBeVisible();
902+
const expandedWidth = (await centerPanel.boundingBox())?.width || 0;
903+
expect(expandedWidth).toBeGreaterThan(normalWidth);
904+
await expect(page.locator("#statusLog")).toHaveValue(/INFO Entered expanded MIDI Studio workspace view/);
905+
906+
await page.locator("[data-midi-studio-summary]").click();
907+
await expect(page.locator("body")).not.toHaveClass(/midi-studio-v2--expanded/);
908+
await expect(page.locator("[data-midi-studio-summary]")).toContainText("Hide Header and Details");
909+
await expect(page.locator("#shared-theme-header")).toBeVisible();
910+
await expect(page.locator(".tool-starter__panel--left")).toBeVisible();
911+
await expect(page.locator("#songDetailsContent")).toBeVisible();
912+
await expect(page.locator("#statusLog")).toHaveValue(/INFO Exited expanded MIDI Studio workspace view/);
913+
} finally {
914+
await workspaceV2CoverageReporter.stop(page);
915+
await server.close();
916+
}
917+
});
918+
852919
test("keeps guided Song Sheet workflow primary alongside the advanced grid editor", async ({ page }) => {
853920
const server = await openMidiStudio(page);
854921
try {

tools/midi-studio-v2/index.html

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
4040
<nav class="tool-starter__menu tool-starter__tool__menu midi-studio-v2__tool-menu" aria-label="Tool actions" data-launch-mode-nav="tool">
4141
<button id="toolImportManifestButton" type="button">Import Manifest</button>
4242
<button id="useExampleButton" type="button">Use Example Test Song</button>
43+
<label class="midi-studio-v2__compact-field" for="renderedExportTargetTypeSelect">
44+
<span>Export target type</span>
45+
<select id="renderedExportTargetTypeSelect">
46+
<option value="wav">WAV</option>
47+
<option value="mp3">MP3</option>
48+
<option value="ogg">OGG</option>
49+
</select>
50+
</label>
51+
<button id="exportWavButton" type="button">Export WAV</button>
52+
<button id="exportMp3Button" type="button">Export MP3</button>
53+
<button id="exportOggButton" type="button">Export OGG</button>
4354
<button id="toolCopyJsonButton" type="button" disabled>Copy JSON</button>
4455
<button id="toolExportToolStateButton" type="button" disabled>Export JSON</button>
4556
<input id="toolImportManifestInput" type="file" accept="application/json,.json" hidden>
@@ -315,23 +326,10 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
315326
</section>
316327

317328
<section class="accordion-v2 tool-starter__accordion is-open" data-accordion-v2-open="true">
318-
<div class="accordion-v2__header midi-studio-v2__rendered-targets-header" aria-expanded="true" aria-controls="renderedTargetsContent">
329+
<button class="accordion-v2__header midi-studio-v2__rendered-targets-header" type="button" aria-expanded="true" aria-controls="renderedTargetsContent">
319330
<span>Rendered Export Targets</span>
320-
<div class="midi-studio-v2__header-actions" aria-label="Rendered export actions">
321-
<label class="midi-studio-v2__compact-field" for="renderedExportTargetTypeSelect">
322-
<span>Export target type</span>
323-
<select id="renderedExportTargetTypeSelect">
324-
<option value="wav">WAV</option>
325-
<option value="mp3">MP3</option>
326-
<option value="ogg">OGG</option>
327-
</select>
328-
</label>
329-
<button id="exportWavButton" type="button">Export WAV</button>
330-
<button id="exportMp3Button" type="button">Export MP3</button>
331-
<button id="exportOggButton" type="button">Export OGG</button>
332-
<span class="accordion-v2__icon" aria-hidden="true">+</span>
333-
</div>
334-
</div>
331+
<span class="accordion-v2__icon" aria-hidden="true">+</span>
332+
</button>
335333
<div id="renderedTargetsContent" class="accordion-v2__content">
336334
<dl id="renderedTargets" class="midi-studio-v2__details"></dl>
337335
</div>

tools/midi-studio-v2/js/MidiStudioV2App.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export class MidiStudioV2App {
115115
}
116116

117117
async start() {
118-
this.shell.mount();
118+
this.shell.mount({ onExpandedChange: (isExpanded) => this.handleExpandedModeChange(isExpanded) });
119119
this.accordions.forEach((accordion) => accordion.mount());
120120
this.statusLog.mount();
121121
this.songList.mount({ onSelect: (songId) => this.selectSong(songId) });
@@ -167,6 +167,14 @@ export class MidiStudioV2App {
167167
this.actionNav.setToolActionsEnabled(false);
168168
}
169169

170+
handleExpandedModeChange(isExpanded) {
171+
if (isExpanded) {
172+
this.statusLog.info("Entered expanded MIDI Studio workspace view. Header details are hidden; NAV, Status, and recovery actions remain available.");
173+
return;
174+
}
175+
this.statusLog.info("Exited expanded MIDI Studio workspace view. Header and details are visible.");
176+
}
177+
170178
applyPayload(rawValue, sourceLabel) {
171179
const normalized = this.serializer.normalize(rawValue);
172180
if (!normalized.ok) {

tools/midi-studio-v2/js/controls/AccordionSection.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,24 @@ export class AccordionSection {
1111
return;
1212
}
1313
this.header.dataset.accordionV2Bound = "true";
14+
if (this.header.tagName !== "BUTTON") {
15+
this.header.setAttribute("role", "button");
16+
this.header.tabIndex = 0;
17+
}
1418
this.setOpen(this.section.dataset.accordionV2Open !== "false");
15-
this.header.addEventListener("click", () => {
19+
this.header.addEventListener("click", (event) => {
20+
const target = event.target instanceof Element ? event.target : null;
21+
const interactiveTarget = target?.closest("button, input, select, textarea, a");
22+
if (interactiveTarget && interactiveTarget !== this.header) {
23+
return;
24+
}
25+
this.setOpen(!this.section.classList.contains("is-open"));
26+
});
27+
this.header.addEventListener("keydown", (event) => {
28+
if (event.key !== "Enter" && event.key !== " ") {
29+
return;
30+
}
31+
event.preventDefault();
1632
this.setOpen(!this.section.classList.contains("is-open"));
1733
});
1834
}
@@ -24,6 +40,7 @@ export class AccordionSection {
2440
this.content.hidden = !isOpen;
2541
if (this.icon) {
2642
this.icon.dataset.accordionV2IconState = isOpen ? "open" : "closed";
43+
this.icon.textContent = isOpen ? "-" : "+";
2744
}
2845
}
2946
}
Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
11
export class ToolShellControl {
22
constructor({ documentRef = document } = {}) {
33
this.document = documentRef;
4+
this.details = this.document.querySelector(".is-collapsible");
5+
this.summary = this.document.querySelector("[data-midi-studio-summary]");
46
this.toolId = "midi-studio-v2";
57
}
68

7-
mount() {
9+
mount({ onExpandedChange = () => {} } = {}) {
810
this.document.body.dataset.toolId = this.toolId;
911
this.document.querySelectorAll("[data-tool-id]").forEach((element) => {
1012
element.setAttribute("data-tool-id", this.toolId);
1113
});
14+
this.applyExpandedState(false);
15+
this.details?.addEventListener("toggle", () => {
16+
const isExpanded = !this.details.open;
17+
this.applyExpandedState(isExpanded);
18+
onExpandedChange(isExpanded);
19+
});
20+
}
21+
22+
applyExpandedState(isExpanded) {
23+
this.document.body.classList.toggle("midi-studio-v2--expanded", isExpanded);
24+
this.document.body.dataset.midiStudioExpanded = String(isExpanded);
25+
if (this.summary) {
26+
this.summary.textContent = isExpanded ? "Show Header and Details" : "Hide Header and Details";
27+
}
1228
}
1329
}

tools/midi-studio-v2/styles/midiStudioV2.css

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,3 +275,36 @@
275275
padding-left: 0;
276276
padding-right: 0;
277277
}
278+
279+
body[data-tool-id="midi-studio-v2"] .accordion-v2.is-open .accordion-v2__icon {
280+
transform: none;
281+
}
282+
283+
body.midi-studio-v2--expanded .tool-starter__menu {
284+
position: sticky;
285+
top: 0;
286+
z-index: 4;
287+
}
288+
289+
body.midi-studio-v2--expanded .tool-starter.app-shell {
290+
height: calc(100vh - 86px);
291+
width: calc(100% - 32px);
292+
grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
293+
}
294+
295+
body.midi-studio-v2--expanded .tool-starter__panel--left {
296+
display: none;
297+
}
298+
299+
body.midi-studio-v2--expanded .tool-starter__panel--center {
300+
grid-column: 1;
301+
}
302+
303+
body.midi-studio-v2--expanded .tool-starter__panel--right {
304+
grid-column: 2;
305+
}
306+
307+
body.midi-studio-v2--expanded .tool-starter__panel--center .accordion-v2:not(:has(#playbackContent)):not(:has(#instrumentGridContent)),
308+
body.midi-studio-v2--expanded .tool-starter__panel--right .accordion-v2:not(:has(#statusLogContent)) {
309+
display: none;
310+
}

0 commit comments

Comments
 (0)