diff options
| author | historia <historiavg@proton.me> | 2026-09-12 16:48:55 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-09-12 16:48:55 -0400 |
| commit | 18346d8deb858cc77d166ffea8ef76d9c9d48bfe (patch) | |
| tree | e11bb5951a047ed372053906dcbb97b8651cee84 /lib/project/tests/test_gui_browser.py | |
| parent | 464edbc4fd658728f06d3a7d98340aa28ff3e3ec (diff) | |
| download | producer-18346d8deb858cc77d166ffea8ef76d9c9d48bfe.tar.gz | |
feat: automatic stereo mixdown
Diffstat (limited to 'lib/project/tests/test_gui_browser.py')
| -rw-r--r-- | lib/project/tests/test_gui_browser.py | 76 |
1 files changed, 33 insertions, 43 deletions
diff --git a/lib/project/tests/test_gui_browser.py b/lib/project/tests/test_gui_browser.py index 1e38964..385de44 100644 --- a/lib/project/tests/test_gui_browser.py +++ b/lib/project/tests/test_gui_browser.py @@ -1,4 +1,4 @@ -"""Opt-in real Chromium/loopback smoke; no AI models or recording uploads. +"""Opt-in real Chromium/loopback smoke; no AI models. Uploads come from the browser. Install Playwright externally, then run: PRODUCER_BROWSER_TESTS=1 python -m pytest tests/test_gui_browser.py -q @@ -8,8 +8,6 @@ The long overview case uses mocked fixed-size scan metadata, not 4-hour AI infer from dataclasses import asdict import hashlib import json -import os -import re from pathlib import Path import socket import threading @@ -31,7 +29,6 @@ def studio(tmp_path, wav, bypass, ffmpeg, monkeypatch): source = wav(0.15 * np.sin(2 * np.pi * 440 * np.arange(48000 * 8) / 48000), name="Narration.wav") wav(np.zeros((100, 2)), name="Stereo.wav") (tmp_path / "delivery").mkdir() - (tmp_path / "subfolder").mkdir() control = {"hold": False, "started": threading.Event(), "release": threading.Event()} actual = gui_jobs.render_preview @@ -43,12 +40,11 @@ def studio(tmp_path, wav, bypass, ffmpeg, monkeypatch): return actual(*args) monkeypatch.setattr(gui_jobs, "render_preview", controlled_preview) - token = "browser-test-private-credential" sock = socket.socket() sock.bind(("127.0.0.1", 0)) sock.listen(128) port = sock.getsockname()[1] - app = create_app(tmp_path, port, token, overrides=asdict(bypass)) + app = create_app(port, output_dir=tmp_path, overrides=asdict(bypass)) server = uvicorn.Server(uvicorn.Config(app, log_level="error", access_log=False, proxy_headers=False)) thread = threading.Thread(target=server.run, kwargs={"sockets": [sock]}, daemon=True) thread.start() @@ -57,7 +53,7 @@ def studio(tmp_path, wav, bypass, ffmpeg, monkeypatch): time.sleep(0.02) assert server.started try: - yield {"url": f"http://127.0.0.1:{port}", "token": token, "root": tmp_path, "source": source, "control": control} + yield {"url": f"http://127.0.0.1:{port}", "root": tmp_path, "source": source, "control": control} finally: control["release"].set() server.should_exit = True @@ -80,19 +76,21 @@ def test_gui_browser_real_workflow(studio): page.on("pageerror", lambda exc: errors.append(str(exc))) page.on("console", lambda msg: console_errors.append(msg.text) if msg.type == "error" else None) page.on("request", lambda req: media_requests.append(req.url) if "/api/results/" in req.url else None) - page.goto(studio["url"] + "/#token=" + studio["token"]) + page.goto(studio["url"]) expect(page.locator("#workspace")).not_to_have_attribute("disabled", "") - assert "#" not in page.url - assert studio["token"] not in page.locator("body").inner_text() + assert "token" not in page.locator("body").inner_text() assert page.locator("[data-setting]").count() == 47 expect(page.locator("#setting-denoiser")).to_have_value("none") - expect(page.locator("#folder-up")).to_be_disabled() + # Dependent controls of a disabled effect are visibly inactive. + expect(page.locator("#setting-denoise_strength")).to_be_disabled() + expect(page.locator("#setting-fft_reduction_db")).to_be_disabled() + expect(page.locator("#setting-highpass_hz")).to_be_disabled() + expect(page.locator("#setting-highpass_hz")).to_have_value("70") # Unsupported input errors are human-readable, not raw HTML. - page.get_by_role("button", name=re.compile(r"WAV\s*Stereo.wav")).click() + page.locator("#pick-files").set_input_files([studio["root"] / "Stereo.wav", studio["source"]]) expect(page.locator("#error")).to_be_visible() expect(page.locator("#error-text")).to_contain_text("Stereo") page.locator("#dismiss-error").click() - page.get_by_role("button", name=re.compile(r"WAV\s*Narration.wav")).click() expect(page.locator("#wave-status")).to_contain_text("2,048", timeout=15000) expect(page.locator("#input-info")).to_contain_text("00:00:08") # Both drag selection and keyboard-accessible exact inputs. @@ -110,6 +108,7 @@ def test_gui_browser_real_workflow(studio): # Explicit render, requested pacing+MP3 preserved in snapshot but bypassed internally. page.locator("#setting-trim_silence").check() page.locator("#setting-mp3").check() + expect(page.locator("#setting-mp3_bitrate")).not_to_be_disabled() page.locator("#render-preview").click() expect(page.locator("#comparison-status")).to_contain_text("Ready", timeout=30000) assert len(media_requests) == 2 @@ -140,39 +139,39 @@ def test_gui_browser_real_workflow(studio): toml_path = screenshots / "roundtrip-preset.toml" downloaded.value.save_as(toml_path) assert "trim_silence = true" in toml_path.read_text() - assert studio["token"] not in toml_path.read_text() - page.locator("#setting-denoise_strength").fill("0.4") + # Editing a control invalidates the comparison; importing the preset restores it. + page.locator("#setting-eq").check() expect(page.locator("#comparison-status")).to_contain_text("changed since preview") page.locator("#import-preset").set_input_files(toml_path) - expect(page.locator("#setting-denoise_strength")).to_have_value("0.85") + expect(page.locator("#setting-eq")).not_to_be_checked() # The saved copy cannot change when controls are edited later. - page.locator("#setting-denoise_strength").fill("0.5") - saved = page.evaluate("JSON.parse(localStorage.getItem(Object.keys(localStorage).find(k => k.startsWith('producer-studio-v1:'))))") - assert saved["comparisons"][0]["snapshot"]["settings"]["denoise_strength"] == .85 - assert saved["presets"][0]["settings"]["denoise_strength"] == .85 - assert studio["token"] not in json.dumps(saved) + page.locator("#setting-eq").check() + saved = page.evaluate("JSON.parse(localStorage.getItem(Object.keys(localStorage).find(k => k.startsWith('producer-gui-v1'))))") + assert saved["comparisons"][0]["snapshot"]["settings"]["eq"] is False + assert saved["presets"][0]["settings"]["eq"] is False # Delay real preview completion. Editing after submission must never publish it as current. studio["control"]["hold"] = True page.locator("#render-preview").click() assert studio["control"]["started"].wait(10) old_source = page.locator("#comparison-source").inner_text() - page.locator("#setting-denoise_strength").fill("0.6") + page.locator("#setting-eq").uncheck() studio["control"]["release"].set() expect(page.locator("#jobs")).to_contain_text("Older snapshot; not loaded", timeout=30000) assert page.locator("#comparison-source").inner_text() == old_source assert len(media_requests) == 2 # Restore is an explicit render and restores the original immutable requested settings. page.locator("#comparisons button", has_text="Restore & render").click() - expect(page.locator("#setting-denoise_strength")).to_have_value("0.85") + expect(page.locator("#setting-eq")).not_to_be_checked() expect(page.locator("#comparison-status")).to_contain_text("Ready", timeout=30000) # Cross-field errors do not silently enable the limiter. page.locator("#setting-normalize").check() + expect(page.locator("#setting-target_lufs")).not_to_be_disabled() page.locator("#render-preview").click() expect(page.locator("#error-text")).to_contain_text("limiter") assert not page.locator("#setting-limiter").is_checked() page.locator("#dismiss-error").click() page.locator("#setting-normalize").uncheck() - page.locator("#output-dir").fill("delivery") + page.locator("#output-dir").fill(str(studio["root"] / "delivery")) preview_requests = len(media_requests) page.locator("#prepare-export").click() expect(page.locator("#export-dialog")).to_be_visible() @@ -195,7 +194,7 @@ def test_gui_browser_real_workflow(studio): page.locator("#compare").scroll_into_view_if_needed() page.screenshot(path=str(screenshots / "desktop-comparison.png"), full_page=True) page.locator("#compare").screenshot(path=str(screenshots / "comparison-panel.png")) - # Reload retains per-tab authorization and local snapshots, not misleading current audio. + # Reload keeps local snapshots, not misleading current audio. page.reload() expect(page.locator("#workspace")).not_to_have_attribute("disabled", "") expect(page.locator("#comparisons")).to_contain_text("Gentle <snapshot>") @@ -206,18 +205,8 @@ def test_gui_browser_real_workflow(studio): page.evaluate("window.scrollTo(0, 0)") page.screenshot(path=str(screenshots / "mobile-entry.png")) assert page.evaluate("document.documentElement.scrollWidth <= innerWidth") - # A fresh tab without the private fragment cannot read API data. - unauth = context.new_page() - unauth.goto(studio["url"]) - expect(unauth.locator("#error-text")).to_contain_text("private URL") - expect(unauth.locator("#workspace")).to_have_attribute("disabled", "") - unauth.close() - # Simulate server restart credential expiry and verify recovery guidance. - page.route("**/api/jobs", lambda route: route.fulfill(status=401, json={"detail": "expired"})) - expect(page.locator("#workspace")).to_have_attribute("disabled", "", timeout=10000) - expect(page.locator("#connection")).to_contain_text("Session expired") assert errors == [] - # Expected HTTP errors for stereo, invalid constraint and the expiry test are allowed. + # Expected HTTP errors for stereo and the invalid constraint are allowed. assert not [msg for msg in console_errors if "Content Security Policy" in msg or "Refused" in msg] browser.close() @@ -231,19 +220,20 @@ def test_gui_browser_long_overview(studio): requests, errors = [], [] page.on("request", lambda request: requests.append(request.url)) page.on("pageerror", lambda exc: errors.append(str(exc))) - page.goto(studio["url"] + "/#token=" + studio["token"]) - expect(page.locator("#workspace")).not_to_have_attribute("disabled", "") + page.goto(studio["url"]) # Four-hour metadata plus fixed-size peaks: browser never receives full recording samples. - info = {"path": "Narration.wav", "input_id": "long-identity", "frames": 14400 * 8000, - "duration": 14400, "sample_rate": 8000, "channels": 1, "subtype": "PCM_16"} + info = {"upload_id": "long-upload", "name": "Narration.wav", "input_id": "long-identity", + "frames": 14400 * 8000, "duration": 14400, "sample_rate": 8000, + "channels": 1, "subtype": "PCM_16"} result = {"input_id": "long-identity", "duration": 14400, "frames": info["frames"], "sample_rate": 8000, "bins": 2048, "min": [-.3] * 2048, "max": [.4] * 2048} - job = {"id": "mock-scan", "kind": "scan", "snapshot": {"path": "Narration.wav", "input_id": "long-identity"}, + job = {"id": "mock-scan", "kind": "scan", + "snapshot": {"upload_id": "long-upload", "name": "Narration.wav", "input_id": "long-identity"}, "status": "succeeded", "progress": {"stage": "Done", "completed": 1, "total": 1}, "created": time.time(), "result": result, "error": None, "cancel_requested": False, "cached": False} - page.route("**/api/input?*", lambda route: route.fulfill(json=info)) + page.route("**/api/uploads*", lambda route: route.fulfill(json=info)) page.route("**/api/jobs", lambda route: route.fulfill(json=job if route.request.method == "POST" else {"jobs": [job]})) - page.get_by_role("button", name=re.compile(r"WAV\s*Narration.wav")).click() + page.locator("#pick-files").set_input_files(studio["source"]) expect(page.locator("#wave-status")).to_contain_text("2,048", timeout=10000) expect(page.locator("#wave-end")).to_have_text("04:00:00") page.locator("#selection-start").fill("03:59:30") |
