aboutsummaryrefslogtreecommitdiff
path: root/lib/project/tests/test_gui_browser.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/project/tests/test_gui_browser.py')
-rw-r--r--lib/project/tests/test_gui_browser.py76
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")