"""E2E: the Settings page — connection tests, defaults, schedule, and the way out. Settings is where an owner goes when something is wrong, so every control here has to tell the truth: a Test button that says "Connected" when it isn't, or a Save that silently doesn't, is worse than no button at all. """ from __future__ import annotations import re import pytest from playwright.sync_api import Page, expect from tests.e2e.conftest import ShortlistApp, build_real_rows from tests.fakes.fake_plex import FakePlexState pytestmark = pytest.mark.e2e LOAD = 20_000 SLOW = 60_000 def _open_settings(page: Page, section: str = "connections") -> None: page.goto(f"/settings#{section}") expect(page.get_by_role("heading", name="Settings", exact=True)).to_be_visible(timeout=LOAD) def test_requests_shortcut_reveals_connections_and_preserves_the_request_form(page: Page, app: ShortlistApp): """Section shortcuts switch tabs without resetting the Requests form.""" app.api("PUT", "/api/settings", json={"values": {"requests.enabled": True}}) _open_settings(page, "requests") page.get_by_role("button", name="Go to Connections", exact=True).first.click() expect(page).to_have_url(re.compile(r"/settings/connections#connections$")) expect(page.get_by_test_id("connection-radarr")).to_be_visible() # Requests lives on the Defaults tab now. A tab out of view stays mounted (hidden), so the form # is kept rather than torn down and rebuilt — attached, not visible, is that promise. expect(page.locator("#requests")).to_be_attached() page.goto("/settings#requests") expect(page.get_by_role("button", name="Radarr & Sonarr")).to_have_attribute("aria-pressed", "true") assert app.api("GET", "/api/settings").json()["requests.enabled"] is True class TestConnectionCards: def test_every_test_button_reports_the_real_state_of_its_connection(self, page: Page, app: ShortlistApp): """All four services, each hitting its real client — not one happy-path card. Tautulli is deliberately unconfigured here, which is the state a fresh install is in: the card must say so in plain English instead of claiming success. """ _open_settings(page) plex = page.get_by_test_id("connection-plex") plex.get_by_role("button", name="Test").click() # Spelled out, not "PMS": the abbreviation is ours, and the owner reading this card has no # reason to know it (audit finding, Sep 2026). expect(plex).to_contain_text( f"Connected to {FakePlexState.friendly_name} (Plex Media Server {FakePlexState.version})", timeout=LOAD, ) tmdb = page.get_by_test_id("connection-tmdb") tmdb.get_by_role("button", name="Test").click() expect(tmdb).to_contain_text("TMDB key works", timeout=LOAD) # "No AI" is a choice, not a connection: the card offers one "Set up" and no Test, because # there is nothing to test until a provider is chosen. llm = page.get_by_test_id("connection-llm") expect(llm.get_by_role("button", name="Set up")).to_be_visible(timeout=LOAD) expect(llm.get_by_role("button", name="Test")).to_have_count(0) # An unconfigured connection says so plainly and never claims a connection it doesn't have — # and its Test is disabled until a key is on file (you can't test what isn't set up), rather # than letting the owner test nothing and read a raw error. # # Asserted on the status label the card carries next to its state dot, NOT on a sentence in # the body. The body used to end "Not set up yet — choose Set up to connect." on every # unconfigured card, which is five identical sentences down one page, each directly under a # button reading "Set up". The state is still stated; it is stated once. tautulli = page.get_by_test_id("connection-tautulli") expect(tautulli).to_contain_text("Not set up") # `status` renders only inside an `sr-only` span, and `to_contain_text` reads textContent — # so the line above passes off an invisible string and would stay green if every visible # signal regressed. The visible one is the button, which reads "Set up" precisely when the # connection isn't. expect(tautulli.get_by_role("button", name="Set up")).to_be_visible() # No Test at all until a key is on file: there is nothing to test. expect(tautulli.get_by_role("button", name="Test")).to_have_count(0) expect(tautulli).not_to_contain_text("Connected —") class TestDefaults: def test_a_local_ai_server_can_be_pointed_at_from_settings(self, page: Page, app: ShortlistApp): """The wizard is only walked once; everyone who adds a local AI server later does it HERE. Covers the whole path an existing owner takes: pick the provider, get a URL field at all, type the address their server is known by, and have it persist under the right key (#7). """ page.goto("/settings") # The provider config lives on the merged "AI & Web search" card (testId kept from when it # was the standalone AI-provider card, so this scopes to it without matching field labels). card = page.get_by_test_id("connection-llm") expect(card).to_be_visible(timeout=LOAD) card.get_by_role("button", name=re.compile("^(Edit|Set up)$")).first.click() # A local model has no web search of its own, so it is only offered under a backend that # does the searching for it. Choosing Exa first is what a real owner has to do, and picking # the backend BEFORE the provider is the order the card asks for. page.get_by_label("Where to search").get_by_role("button", name="Exa").click() # "AI provider" renders as a segmented group, not a