Skip to content

Commit d770123

Browse files
committed
Organize MIDI Studio V2 Export tab ownership and future rendering controls - PR_26146_058-midi-studio-v2-export-tab-usability-and-render-plan
1 parent bbea08c commit d770123

8 files changed

Lines changed: 447 additions & 20 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# PR_26146_058 MIDI Studio V2 Export Ownership Map
2+
3+
Status: PASS
4+
5+
## Export Tab Sections
6+
7+
| Section | Ownership |
8+
| --- | --- |
9+
| Output Type | Export owns the WAV/MP3/OGG Type dropdown and Save/Export action. |
10+
| Render Source | Export displays selected song, playable event count, and canonical song model / octave timeline data source. |
11+
| Output Targets | Export owns editable WAV, MP3, and OGG rendered target path metadata. |
12+
| Future Rendering Options | Export owns future rendering placeholders; they remain red/unwired. |
13+
| Export Status | Export reports renderer availability, WARN not implemented, and FAIL no-song/missing-target states. |
14+
15+
## Editable Export Values
16+
17+
| Value | Editable owner | Derived/read-only locations |
18+
| --- | --- | --- |
19+
| Export Type | Export, `#renderedExportTargetTypeSelect` | None |
20+
| Save/Export action | Export, `#renderedExportSaveButton` | None |
21+
| WAV target path | Export, `#renderedTargetWavInput` | Diagnostics, `#renderedTargetDiagnostics` |
22+
| MP3 target path | Export, `#renderedTargetMp3Input` | Diagnostics, `#renderedTargetDiagnostics` |
23+
| OGG target path | Export, `#renderedTargetOggInput` | Diagnostics, `#renderedTargetDiagnostics` |
24+
25+
## Unwired Rendering Plan
26+
27+
Rendered audio generation is not implemented in this PR.
28+
29+
- Save/Export remains red/unwired and reports WARN when a selected song has a target path.
30+
- Save/Export reports FAIL when there is no selected song.
31+
- Save/Export reports FAIL when the selected output type has no target path.
32+
- No status text claims a file was created, written, or saved.
33+
34+
## Future Rendering Options
35+
36+
These controls are visible roadmap placeholders only and use shared unwired styling/tooltips:
37+
38+
- SoundFont
39+
- Render Quality
40+
- Sample Rate
41+
- Normalize Volume
42+
- Export Stems
43+
- Loop Export
44+
45+
## Non-Export Tabs
46+
47+
- NAV has no editable export/render controls.
48+
- Diagnostics only shows read-only rendered target diagnostics.
49+
- Song Setup remains the owner of editable song metadata.
50+
- Instruments remains the owner of editable instrument configuration.
51+
- MIDI Import remains the owner of source/import/inspect controls.
52+
- Octave Timeline remains the owner of note editing and timeline playback editing surface.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# PR_26146_058 MIDI Studio V2 Export Tab Usability And Render Plan Validation
2+
3+
Status: PASS
4+
5+
## Scope
6+
7+
- Kept Export as the only owner of export/render/output controls.
8+
- Added clear Export tab sections:
9+
- Output Type
10+
- Render Source
11+
- Output Targets
12+
- Future Rendering Options
13+
- Export Status
14+
- Added Export-owned editable rendered target path fields for WAV, MP3, and OGG.
15+
- Added Diagnostics read-only rendered target diagnostics.
16+
- Preserved honest red/unwired styling for future rendering controls and rendered Save/Export.
17+
- Preserved canvas Octave Timeline editing and Play/Stop behavior.
18+
19+
## Validation Commands
20+
21+
```powershell
22+
node --check tools/midi-studio-v2/js/controls/ExportPanelControl.js; node --check tools/midi-studio-v2/js/bootstrap.js; node --check tools/midi-studio-v2/js/MidiStudioV2App.js; node --check tests/playwright/tools/MidiStudioV2.spec.mjs
23+
rg -n -P '<script(?![^>]*\bsrc=)' tools/midi-studio-v2/index.html; if ($LASTEXITCODE -eq 1) { exit 0 } else { exit $LASTEXITCODE }
24+
rg -n '<style| on[a-z]+=' tools/midi-studio-v2/index.html; if ($LASTEXITCODE -eq 1) { exit 0 } else { exit $LASTEXITCODE }
25+
npx playwright test tests/playwright/tools/MidiStudioV2.spec.mjs --grep "keeps Export tab usable|enforces SSoT export ownership|canvas note editing flow supports hover click drag paint erase and playback|canvas octave timeline edits canonical data"
26+
git diff --check
27+
```
28+
29+
## Results
30+
31+
- Changed-file JavaScript syntax checks: PASS.
32+
- MIDI Studio V2 HTML inline script/style/event-handler guard: PASS.
33+
- Targeted Playwright: PASS, 4 tests passed.
34+
- `git diff --check`: PASS. Git reported line-ending normalization warnings only.
35+
- Full samples smoke test: not run per PR instruction.
36+
37+
## Tested Behavior
38+
39+
- Export tab owns Type dropdown and Save/Export.
40+
- WAV/MP3/OGG target paths are editable only in Export.
41+
- Editing target paths updates the canonical selected song model.
42+
- Diagnostics rendered target values are read-only/derived.
43+
- Future Rendering Options render red/unwired with tooltip/status.
44+
- Save/Export reports WARN not implemented when no renderer exists.
45+
- Save/Export reports FAIL when no song is selected.
46+
- Export controls are not duplicated in NAV.
47+
- Play and Stop still work.
48+
49+
## Coverage Artifacts
50+
51+
- `docs/dev/reports/playwright_v8_coverage_report.txt`
52+
- `docs/dev/reports/coverage_changed_js_guardrail.txt`
53+
54+
Coverage is advisory only. The changed runtime JS guardrail reports no warnings.

tests/playwright/tools/MidiStudioV2.spec.mjs

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2466,6 +2466,106 @@ test.describe("MIDI Studio V2", () => {
24662466
}
24672467
});
24682468

2469+
test("keeps Export tab usable while rendered audio export remains planned", async ({ page }) => {
2470+
let server = await openMidiStudio(page);
2471+
try {
2472+
await expect(page.locator('.midi-studio-v2__tool-menu #renderedExportTargetTypeSelect')).toHaveCount(0);
2473+
await expect(page.locator('.midi-studio-v2__tool-menu #renderedExportSaveButton')).toHaveCount(0);
2474+
await selectMidiStudioTab(page, "export");
2475+
await expect(page.locator("#exportOutputTypeSection")).toContainText("Output Type");
2476+
await expect(page.locator("#exportRenderSourceSection")).toContainText("Render Source");
2477+
await expect(page.locator('.accordion-v2__header[aria-controls="renderedTargetsContent"]')).toContainText("Output Targets");
2478+
await expect(page.locator("#futureExportOptionsSection")).toContainText("Future Rendering Options");
2479+
await expect(page.locator("#exportStatusSection")).toContainText("Export Status");
2480+
await expect(page.locator("#renderedExportTargetTypeSelect option")).toContainText(["WAV", "MP3", "OGG"]);
2481+
await expect(page.locator("#renderedExportSaveButton")).toHaveText("Save/Export");
2482+
const exportControlOwners = await page.locator("#renderedExportTargetTypeSelect, #renderedExportSaveButton").evaluateAll((controls) => controls.map((control) => control.closest("[data-midi-studio-tab-panel]")?.dataset.midiStudioTabPanel));
2483+
expect(exportControlOwners).toEqual(["export", "export"]);
2484+
2485+
await expect(page.locator("#exportRenderSource")).toContainText("Main Theme");
2486+
await expect(page.locator("#exportRenderSource")).toContainText("canonical song model / octave timeline data");
2487+
await expect(page.locator("#exportRenderSource")).toContainText("Playable event count");
2488+
expect(await page.locator("#exportRenderSource dd").nth(1).textContent()).toMatch(/^\d+$/);
2489+
expect(Number.isFinite(await page.evaluate(() => window.__midiStudioV2App.playableEventSummary().count))).toBe(true);
2490+
2491+
const targetOwnership = await page.locator("#renderedTargetWavInput, #renderedTargetMp3Input, #renderedTargetOggInput").evaluateAll((inputs) => inputs.map((input) => ({
2492+
disabled: input.disabled,
2493+
id: input.id,
2494+
panel: input.closest("[data-midi-studio-tab-panel]")?.dataset.midiStudioTabPanel,
2495+
value: input.value
2496+
})));
2497+
expect(targetOwnership).toEqual([
2498+
{ disabled: false, id: "renderedTargetWavInput", panel: "export", value: "assets/music/rendered/theme-main.wav" },
2499+
{ disabled: false, id: "renderedTargetMp3Input", panel: "export", value: "assets/music/rendered/theme-main.mp3" },
2500+
{ disabled: false, id: "renderedTargetOggInput", panel: "export", value: "assets/music/rendered/theme-main.ogg" }
2501+
]);
2502+
await page.locator("#renderedTargetMp3Input").fill("assets/music/rendered/custom-main.mp3");
2503+
expect(await page.evaluate(() => window.__midiStudioV2App.selectedSong().rendered.mp3)).toBe("assets/music/rendered/custom-main.mp3");
2504+
2505+
await selectMidiStudioTab(page, "diagnostics");
2506+
await expect(page.locator("#renderedTargetDiagnosticsContent")).toBeVisible();
2507+
await expect(page.locator("#renderedTargetDiagnostics")).toContainText("assets/music/rendered/custom-main.mp3");
2508+
await expect(page.locator("#renderedTargetDiagnostics input, #renderedTargetDiagnostics textarea, #renderedTargetDiagnostics select")).toHaveCount(0);
2509+
await expect(page.locator("#renderedTargetsContent")).toBeHidden();
2510+
2511+
await selectMidiStudioTab(page, "export");
2512+
const futureControls = await page.locator("#futureExportOptionsSection [data-midi-studio-future-control]").evaluateAll((controls) => controls.map((control) => ({
2513+
disabled: control.disabled,
2514+
status: control.dataset.midiStudioUnwired,
2515+
text: control.textContent.trim(),
2516+
title: control.title
2517+
})));
2518+
expect(futureControls).toHaveLength(6);
2519+
expect(futureControls.every((control) => control.disabled && control.status === "not-implemented" && control.title.includes("Not implemented:"))).toBe(true);
2520+
expect(futureControls.map((control) => control.text)).toEqual(expect.arrayContaining([
2521+
"SoundFont",
2522+
"Render Quality",
2523+
"Sample Rate",
2524+
"Normalize Volume",
2525+
"Export Stems",
2526+
"Loop Export"
2527+
]));
2528+
2529+
await page.locator("#renderedExportTargetTypeSelect").selectOption("mp3");
2530+
await page.locator("#renderedExportSaveButton").click();
2531+
await expect(page.locator("#statusLog")).toHaveValue(/WARN Export rendering not implemented for MP3\. Planned target: assets\/music\/rendered\/custom-main\.mp3\./);
2532+
await expect(page.locator("#statusLog")).not.toHaveValue(/created .*custom-main\.mp3|wrote .*custom-main\.mp3|saved .*custom-main\.mp3/i);
2533+
await expect(page.locator("#exportStatusDetails")).toContainText("WARN: Export rendering not implemented for MP3. Planned target: assets/music/rendered/custom-main.mp3.");
2534+
expect(await controlColors(page, "#renderedExportSaveButton")).toMatchObject({
2535+
borderTopColor: "rgb(248, 113, 113)",
2536+
color: "rgb(254, 202, 202)"
2537+
});
2538+
2539+
await selectMidiStudioTab(page, "studio");
2540+
await page.locator("#playButton").click();
2541+
await expect(page.locator("#playButton")).toBeDisabled();
2542+
await expect(page.locator("#stopButton")).toBeEnabled();
2543+
await page.locator("#stopButton").click();
2544+
await expect(page.locator("#stopButton")).toBeDisabled();
2545+
await expect(page.locator("#playButton")).toBeEnabled();
2546+
} finally {
2547+
await workspaceV2CoverageReporter.stop(page);
2548+
await server.close();
2549+
}
2550+
2551+
server = await openMidiStudio(page, {
2552+
music: {
2553+
version: 1,
2554+
songs: [{ name: "Broken Song" }]
2555+
}
2556+
});
2557+
try {
2558+
await selectMidiStudioTab(page, "export");
2559+
await page.locator("#renderedExportTargetTypeSelect").selectOption("wav");
2560+
await page.locator("#renderedExportSaveButton").click();
2561+
await expect(page.locator("#statusLog")).toHaveValue(/FAIL Missing MIDI song for WAV export\. Load or select a song before exporting\./);
2562+
await expect(page.locator("#exportStatusDetails")).toContainText("FAIL: Missing MIDI song for WAV export. Load or select a song before exporting.");
2563+
} finally {
2564+
await workspaceV2CoverageReporter.stop(page);
2565+
await server.close();
2566+
}
2567+
});
2568+
24692569
test("derives primary song, instrument, grid, playback, and diagnostics views from the canonical selected song", async ({ page }) => {
24702570
const server = await openMidiStudioForImport(page);
24712571
try {
@@ -2610,7 +2710,7 @@ test.describe("MIDI Studio V2", () => {
26102710
await expect(page.locator("#playButton")).toHaveText("Play");
26112711
await expect(page.locator("#inspectMidiSourceButton")).toBeEnabled();
26122712
const renderedHeader = page.locator('.accordion-v2__header[aria-controls="renderedTargetsContent"]');
2613-
await expect(renderedHeader).toContainText("Rendered Export Targets");
2713+
await expect(renderedHeader).toContainText("Output Targets");
26142714
await expect(renderedHeader.locator("#exportWavButton")).toHaveCount(0);
26152715
await expect(page.locator("#exportWavButton")).toHaveCount(0);
26162716
await expect(page.locator("#exportMp3Button")).toHaveCount(0);

tools/midi-studio-v2/index.html

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -217,20 +217,26 @@ <h3>Future Instrument Settings</h3>
217217
<span class="accordion-v2__icon" aria-hidden="true">+</span>
218218
</button>
219219
<div id="exportWorkflowContent" class="accordion-v2__content midi-studio-v2__stack">
220-
<div class="midi-studio-v2__export-save-controls">
221-
<label class="midi-studio-v2__compact-field" for="renderedExportTargetTypeSelect">
222-
<span>Output Type</span>
223-
<select id="renderedExportTargetTypeSelect">
224-
<option value="wav">WAV</option>
225-
<option value="mp3">MP3</option>
226-
<option value="ogg">OGG</option>
227-
</select>
228-
</label>
229-
<button id="renderedExportSaveButton" type="button">Save/Export</button>
230-
</div>
231-
<button id="toolExportToolStateButton" type="button" disabled>Export JSON</button>
232-
<div class="midi-studio-v2__future-panel" aria-label="Future export settings">
233-
<h3>Future Export Settings</h3>
220+
<section id="exportOutputTypeSection" class="midi-studio-v2__export-section" aria-labelledby="exportOutputTypeHeading">
221+
<h3 id="exportOutputTypeHeading">Output Type</h3>
222+
<div class="midi-studio-v2__export-save-controls">
223+
<label class="midi-studio-v2__compact-field" for="renderedExportTargetTypeSelect">
224+
<span>Output Type</span>
225+
<select id="renderedExportTargetTypeSelect">
226+
<option value="wav">WAV</option>
227+
<option value="mp3">MP3</option>
228+
<option value="ogg">OGG</option>
229+
</select>
230+
</label>
231+
<button id="renderedExportSaveButton" type="button">Save/Export</button>
232+
</div>
233+
</section>
234+
<section id="exportRenderSourceSection" class="midi-studio-v2__export-section" aria-labelledby="exportRenderSourceHeading">
235+
<h3 id="exportRenderSourceHeading">Render Source</h3>
236+
<dl id="exportRenderSource" class="midi-studio-v2__details"></dl>
237+
</section>
238+
<section id="futureExportOptionsSection" class="midi-studio-v2__export-section midi-studio-v2__future-panel" aria-labelledby="futureExportOptionsHeading">
239+
<h3 id="futureExportOptionsHeading">Future Rendering Options</h3>
234240
<div class="midi-studio-v2__future-controls">
235241
<select id="futureSoundFontSelect" disabled data-midi-studio-future-control data-midi-studio-future-detail="SoundFont rendering is planned but not implemented yet.">
236242
<option>SoundFont</option>
@@ -245,7 +251,12 @@ <h3>Future Export Settings</h3>
245251
<button type="button" disabled data-midi-studio-future-control data-midi-studio-future-detail="Export Stems is planned but not implemented yet.">Export Stems</button>
246252
<button type="button" disabled data-midi-studio-future-control data-midi-studio-future-detail="Loop Export is planned but not implemented yet.">Loop Export</button>
247253
</div>
248-
</div>
254+
</section>
255+
<section id="exportStatusSection" class="midi-studio-v2__export-section" aria-labelledby="exportStatusHeading">
256+
<h3 id="exportStatusHeading">Export Status</h3>
257+
<dl id="exportStatusDetails" class="midi-studio-v2__details"></dl>
258+
<button id="toolExportToolStateButton" type="button" disabled>Export JSON</button>
259+
</section>
249260
</div>
250261
</section>
251262

@@ -420,14 +431,24 @@ <h2 id="instrumentGridHeading">Octave Timeline</h2>
420431

421432
<section class="accordion-v2 tool-starter__accordion is-open" data-accordion-v2-open="true" data-midi-studio-tab-panel="export">
422433
<button class="accordion-v2__header midi-studio-v2__rendered-targets-header" type="button" aria-expanded="true" aria-controls="renderedTargetsContent">
423-
<span>Rendered Export Targets</span>
434+
<span>Output Targets</span>
424435
<span class="accordion-v2__icon" aria-hidden="true">+</span>
425436
</button>
426437
<div id="renderedTargetsContent" class="accordion-v2__content">
427438
<dl id="renderedTargets" class="midi-studio-v2__details"></dl>
428439
</div>
429440
</section>
430441

442+
<section class="accordion-v2 tool-starter__accordion is-open" data-accordion-v2-open="true" data-midi-studio-tab-panel="diagnostics">
443+
<button class="accordion-v2__header midi-studio-v2__rendered-targets-header" type="button" aria-expanded="true" aria-controls="renderedTargetDiagnosticsContent">
444+
<span>Rendered Target Diagnostics</span>
445+
<span class="accordion-v2__icon" aria-hidden="true">+</span>
446+
</button>
447+
<div id="renderedTargetDiagnosticsContent" class="accordion-v2__content">
448+
<dl id="renderedTargetDiagnostics" class="midi-studio-v2__details"></dl>
449+
</div>
450+
</section>
451+
431452
<section class="accordion-v2 tool-starter__accordion is-open" data-accordion-v2-open="true" data-midi-studio-tab-panel="diagnostics">
432453
<div class="accordion-v2__header midi-studio-v2__json-details-header" aria-expanded="true" aria-controls="inspectorContent">
433454
<span>JSON Details</span>

0 commit comments

Comments
 (0)