tests/e2e/test_issue_page_e2e.py
Rama dev · réplica de lectura del repositorio original.
"""E2E: the "Have an issue?" page, in a real browser, against the real API and a fake Plex server.
This layer exists for the cross-boundary breaks the other two suites cannot see. The integration
tests prove the endpoints; the vitest tests prove the page against a MOCKED api client. Neither
notices a route that was never registered, a nav link pointing at the old path, or a check that
403s because the mode toggle didn't actually reach the server.
The flow asserted here is the one a maintainer talks someone through over chat, in order:
open the page → switch the checks on → run one → copy the result.
"""
from __future__ import annotations
import re
import pytest
from playwright.sync_api import Page, expect
from tests.e2e.conftest import ShortlistApp
pytestmark = pytest.mark.e2e
class TestGettingThere:
def test_the_sidebar_offers_one_door_for_problems(self, page: Page, app: ShortlistApp):
"""It replaced two separate sidebar actions, so the link has to actually be there and go to
the right place — a stale `to=` would still render and still look fine."""
page.goto("/")
page.get_by_role("link", name=re.compile("have an issue", re.IGNORECASE)).click()
expect(page).to_have_url(re.compile(r"/issue$"))
expect(page.get_by_role("heading", name=re.compile("have an issue", re.IGNORECASE))).to_be_visible(
timeout=20_000
)
def test_the_old_sidebar_actions_are_gone(self, page: Page, app: ShortlistApp):
"""Two competing entry points is the state this change removed; one of them produced reports
with no diagnostics attached."""
page.goto("/")
expect(page.get_by_role("link", name=re.compile(r"^report a bug$", re.IGNORECASE))).to_have_count(0)
expect(page.get_by_role("button", name=re.compile("copy diagnostics", re.IGNORECASE))).to_have_count(0)
class TestTheChecksAreOffUntilAsked:
def test_no_check_runs_before_the_mode_is_switched_on(self, page: Page, app: ShortlistApp):
"""Off by default even for the owner. The surface reads share filters and per-user tokens, so
an install that is not currently debugging anything should not expose it."""
page.goto("/issue")
expect(page.get_by_role("button", name=re.compile("switch on the checks", re.IGNORECASE))).to_be_visible(
timeout=20_000
)
expect(page.get_by_text(re.compile("what's the problem", re.IGNORECASE))).to_have_count(0)
def test_switching_on_reaches_the_server_and_unlocks_the_checks(self, page: Page, app: ShortlistApp):
"""The mutation contract: the SPA must send the CSRF header, and the server must actually
record the mode — a client-side-only toggle would light the UI up and then 403 every check."""
page.goto("/issue")
page.get_by_role("button", name=re.compile("switch on the checks", re.IGNORECASE)).click()
expect(page.get_by_text(re.compile("checks are switched on", re.IGNORECASE))).to_be_visible(timeout=20_000)
# Health runs by itself once unlocked, and it only renders after a 200.
expect(page.get_by_text(re.compile("plex server", re.IGNORECASE)).first).to_be_visible(timeout=20_000)
assert app.api("GET", "/api/support/status").json()["enabled"] is True
class TestRunningACheck:
def test_the_title_check_round_trips_and_shows_what_will_be_copied(self, page: Page, app: ShortlistApp):
"""The whole loop in one test. The copy block is the deliverable — a maintainer reads that
text and nothing else — so the page must show the SERVER's block, not one it rebuilt."""
page.goto("/issue")
page.get_by_role("button", name=re.compile("switch on the checks", re.IGNORECASE)).click()
expect(page.get_by_text(re.compile("checks are switched on", re.IGNORECASE))).to_be_visible(timeout=20_000)
page.get_by_role("button", name=re.compile("keep seeing something", re.IGNORECASE)).click()
page.get_by_label("Title").fill("Teacup")
page.get_by_role("button", name=re.compile(r"^check$", re.IGNORECASE)).click()
block = page.locator("pre").first
expect(block).to_be_visible(timeout=20_000)
# Stamped and terminated: without both, a paste cannot be told apart from a truncated one.
expect(block).to_contain_text("=== Shortlist support")
expect(block).to_contain_text("=== end ===")
expect(page.get_by_text(re.compile("exactly what", re.IGNORECASE))).to_be_visible()
def test_a_check_that_needs_a_name_will_not_run_without_one(self, page: Page, app: ShortlistApp):
"""Running with an empty name would return every person's data under a heading naming one."""
page.goto("/issue")
page.get_by_role("button", name=re.compile("switch on the checks", re.IGNORECASE)).click()
expect(page.get_by_text(re.compile("checks are switched on", re.IGNORECASE))).to_be_visible(timeout=20_000)
page.get_by_role("button", name=re.compile("one person's recommendations", re.IGNORECASE)).click()
expect(page.get_by_role("button", name=re.compile(r"^check$", re.IGNORECASE))).to_be_disabled()
def test_every_check_is_reachable_not_just_the_shortcuts(self, page: Page, app: ShortlistApp):
page.goto("/issue")
page.get_by_role("button", name=re.compile("switch on the checks", re.IGNORECASE)).click()
expect(page.get_by_text(re.compile("checks are switched on", re.IGNORECASE))).to_be_visible(timeout=20_000)
page.get_by_role("button", name=re.compile("show all 22 checks", re.IGNORECASE)).click()
expect(page.get_by_role("button", name=re.compile("ask plex directly", re.IGNORECASE))).to_be_visible()
expect(
page.get_by_role("button", name=re.compile("does plex match our records", re.IGNORECASE))
).to_be_visible()
class TestFilingTheReport:
def test_the_bug_link_and_the_diagnostics_sit_together(self, page: Page, app: ShortlistApp):
page.goto("/issue")
page.get_by_role("button", name=re.compile("switch on the checks", re.IGNORECASE)).click()
expect(page.get_by_text(re.compile("still stuck", re.IGNORECASE))).to_be_visible(timeout=20_000)
report = page.get_by_role("link", name=re.compile("report a bug on github", re.IGNORECASE))
expect(report).to_have_attribute("href", re.compile(r"github\.com.*issues/new"))
expect(page.get_by_role("button", name=re.compile("copy the summary", re.IGNORECASE))).to_be_visible()
# Says what it masks AND what it keeps, and does not overpromise. The button beside it
# publishes, so "no passwords or tokens" alone was true and misleading — and any absolute
# framing is wrong outright: three separate leaks reached a real report while one was on
# screen. Names are kept and the copy says so; there is no toggle claiming otherwise.
expect(page.get_by_text(re.compile("passwords, tokens, api keys, ip addresses", re.IGNORECASE))).to_be_visible()
expect(page.get_by_text(re.compile("rather than a guarantee", re.IGNORECASE))).to_be_visible()
expect(page.get_by_text(re.compile("plex usernames of people on your server", re.IGNORECASE))).to_be_visible()
expect(page.get_by_role("checkbox")).to_have_count(0)
def test_the_downloadable_report_is_real_text_and_carries_no_secrets(self, page: Page, app: ShortlistApp):
"""Fetched through the API rather than clicked, because what matters is the CONTENT: someone
is about to attach this to a public GitHub issue."""
app.api("POST", "/api/support/enable")
body = app.api("GET", "/api/support/bundle.txt").text
assert body.startswith("=== Shortlist support")
assert body.rstrip().endswith("=== end ===")
assert "X-Plex-Token=" not in body or "X-Plex-Token=<redacted>" in body
for line in body.splitlines():
assert len(line) <= 76, line
class TestNoConsoleErrors:
def test_the_page_runs_clean(self, page: Page, app: ShortlistApp):
errors: list[str] = []
page.on("console", lambda m: errors.append(m.text) if m.type == "error" else None)
page.goto("/issue")
page.get_by_role("button", name=re.compile("switch on the checks", re.IGNORECASE)).click()
# The page has finished reacting once the unlocked state shows and the network goes quiet.
expect(page.get_by_text(re.compile("checks are switched on", re.IGNORECASE))).to_be_visible(timeout=20_000)
page.wait_for_load_state("networkidle")
assert not errors, errors
class TestTheLivePlexChecksActuallyReachPlex:
"""The unit tests monkeypatch the clients, so they cannot catch a method that does not exist.
One did: `sharing` called `client.users()` where the real method is `list_users()`, and the stub
in the unit tests carried the same wrong name — so the whole class went green while the tool
reported "COULD NOT READ: AttributeError" against a real server. These go through the actual
clients against the fake PMS and fake plex.tv, which is the only layer that would have noticed.
"""
def test_sharing_reads_the_roster_and_classifies_by_label(self, app: ShortlistApp):
app.api("POST", "/api/support/enable")
body = app.api("GET", "/api/support/sharing").json()
assert body["error"] is None, body["error"]
assert body["accounts"], "the fake plex.tv roster came back empty"
for account in body["accounts"]:
# Labels, never whole clauses — every entry must be one `shortlist_*` value.
for label in account["shortlist_excludes"]:
assert label.startswith("shortlist_"), label
# Nobody is ever expected to hide their OWN row.
assert f"shortlist_{account['user']}".lower() not in account["should_hide"]
def test_connection_reads_the_share_tokens(self, app: ShortlistApp):
app.api("POST", "/api/support/enable")
body = app.api("GET", "/api/support/connection").json()
assert body["error"] is None, body["error"]
assert body["users"], "no enabled users came back"
def test_drift_compares_the_ledger_against_the_real_server(self, app: ShortlistApp):
app.api("POST", "/api/support/enable")
body = app.api("GET", "/api/support/drift").json()
assert body["error"] is None, body["error"]
assert "COULD NOT READ PLEX" not in body["text"]
def test_health_reports_the_fake_server_as_reachable_with_a_real_timing(self, app: ShortlistApp):
app.api("POST", "/api/support/enable")
checks = {c["name"]: c for c in app.api("GET", "/api/support/health").json()["checks"]}
assert checks["Plex server"]["ok"] is True, checks["Plex server"]
assert checks["Libraries"]["ok"] is True, checks["Libraries"]