Add post-login welcome and friendly how-it-works guide
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
@@ -36,6 +37,13 @@ def _build_site_info(include_changelog: bool) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
if include_changelog:
|
if include_changelog:
|
||||||
info["changelog"] = (CHANGELOG or "").strip()
|
info["changelog"] = (CHANGELOG or "").strip()
|
||||||
|
playback_url = (runtime.jellyfin_public_url or "").strip()
|
||||||
|
try:
|
||||||
|
parsed = urlsplit(playback_url)
|
||||||
|
valid = parsed.scheme in {"http", "https"} and bool(parsed.hostname) and not parsed.username and not parsed.password
|
||||||
|
except ValueError:
|
||||||
|
valid = False
|
||||||
|
info["mediaServerUrl"] = playback_url if valid else None
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
from backend.app.config import Settings
|
||||||
|
from backend.app.routers.site import _build_site_info
|
||||||
|
|
||||||
|
|
||||||
|
class WelcomeSiteTests(unittest.TestCase):
|
||||||
|
def test_public_response_does_not_expose_playback_url(self):
|
||||||
|
with patch('backend.app.routers.site.get_runtime_settings', return_value=Settings().model_copy(update={'jellyfin_public_url': 'https://watch.example.com'})):
|
||||||
|
self.assertNotIn('mediaServerUrl', _build_site_info(False))
|
||||||
|
|
||||||
|
def test_authenticated_response_uses_public_playback_url(self):
|
||||||
|
with patch('backend.app.routers.site.get_runtime_settings', return_value=Settings().model_copy(update={'jellyfin_public_url': 'https://watch.example.com/web/'})):
|
||||||
|
self.assertEqual(_build_site_info(True)['mediaServerUrl'], 'https://watch.example.com/web/')
|
||||||
|
|
||||||
|
def test_missing_unsafe_or_credential_urls_have_no_watch_link(self):
|
||||||
|
for url in ['', 'javascript:alert(1)', '//internal', 'https://user:secret@example.com', 'https://[broken']:
|
||||||
|
with self.subTest(url=url), patch('backend.app.routers.site.get_runtime_settings', return_value=Settings().model_copy(update={'jellyfin_public_url': url})):
|
||||||
|
self.assertIsNone(_build_site_info(True)['mediaServerUrl'])
|
||||||
@@ -1,181 +1,56 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import PageHeading from '../ui/PageHeading'
|
import PageHeading from '../ui/PageHeading'
|
||||||
|
import '../welcome.css'
|
||||||
|
|
||||||
export default function HowItWorksPage() {
|
export default function HowItWorksPage() {
|
||||||
return (
|
return <main className="friendly-guide">
|
||||||
<main className="card how-page">
|
<PageHeading title="A little help getting started." description="Magent looks after your requests. GrizzlyFlix is where you watch them." />
|
||||||
<PageHeading title="How it works" description="Request something to watch, follow its progress, and get help when you need it." />
|
<nav aria-label="Quick links"><a href="/welcome">Welcome page</a><a href="/">My Requests</a><a href="/profile">My profile</a></nav>
|
||||||
|
<details open><summary>Request a movie or TV show</summary>
|
||||||
<section className="how-flow">
|
<ol>
|
||||||
<h2>What Magent is for</h2>
|
<li><strong>Choose Movie or TV show.</strong><p>Open <a href="/new-requests">02 New Requests</a> and pick what you’re looking for.</p></li>
|
||||||
<div className="how-grid">
|
<li><strong>Search and choose the right title.</strong><p>For TV, choose the seasons you want. If it’s already requested, open that request to see its progress.</p></li>
|
||||||
<article className="how-card">
|
<li><strong>Check your choices and send it.</strong><p>Choose from the quality options shown. These come from the library’s settings.</p></li>
|
||||||
<h3>Track requests</h3>
|
<li><strong>Follow it in My Requests.</strong><p>We’ll show what’s happening and any next step you can take. Some titles need approval or may not have a suitable download yet.</p></li>
|
||||||
<p>
|
</ol>
|
||||||
Search by title, year, or request number to open the request page and see where an
|
</details>
|
||||||
item is up to.
|
<details><summary>Understand the six progress steps</summary>
|
||||||
</p>
|
<ol>
|
||||||
</article>
|
<li><strong>Requested:</strong> Your request has been received.</li>
|
||||||
<article className="how-card">
|
<li><strong>Approved:</strong> It has permission to go ahead.</li>
|
||||||
<h3>See live progress</h3>
|
<li><strong>Library collection:</strong> The library is tracking what’s collected and what’s missing.</li>
|
||||||
<p>
|
<li><strong>Release search:</strong> A suitable download is being looked for. Waiting here can mean there isn’t a good match yet.</li>
|
||||||
Request status, timeline events, and download progress update live while you are
|
<li><strong>Download:</strong> The files are being downloaded. TV requests can include several episodes or a season pack.</li>
|
||||||
viewing the page.
|
<li><strong>Available to watch:</strong> GrizzlyFlix has added the content. Use the watch button to open it.</li>
|
||||||
</p>
|
</ol>
|
||||||
</article>
|
<p>A finished download still needs to be added to the media library. Wait for “Available to watch” before heading over.</p>
|
||||||
<article className="how-card">
|
</details>
|
||||||
<h3>Know when it is ready</h3>
|
<details><summary>Something looks stuck</summary>
|
||||||
<p>
|
<ol>
|
||||||
When the request is fully imported and available, Magent shows it as ready and links
|
<li><strong>Open the request.</strong><p>Read its current status and next step.</p></li>
|
||||||
you through to Jellyfin.
|
<li><strong>Choose Recheck request.</strong><p>Magent checks the connected services again to refresh where things are up to.</p></li>
|
||||||
</p>
|
<li><strong>Follow the action offered.</strong><p>You may be able to restart a search or review suitable releases. Choose “Best pick” when offered if you’re unsure.</p></li>
|
||||||
</article>
|
</ol>
|
||||||
</div>
|
<p>Remote activity explains the latest check. Open it to see the full list. A successful search doesn’t always mean a download was found.</p>
|
||||||
</section>
|
</details>
|
||||||
|
<details><summary>Report a problem and follow the fix</summary>
|
||||||
<section className="how-flow">
|
<ol>
|
||||||
<h2>The request pipeline</h2>
|
<li><strong>Open <a href="/portal/issues">03 Issues</a>.</strong><p>Choose what’s wrong: missing content, broken picture, wrong download, audio, subtitles, or playback.</p></li>
|
||||||
<ol className="how-steps">
|
<li><strong>Choose the affected content.</strong><p>Find the movie or show. For TV, select the affected seasons or episodes; you can choose more than one.</p></li>
|
||||||
<li>
|
<li><strong>Read “What will happen”, then submit.</strong><p>It tells you whether the selected files will be replaced, missing content searched for, subtitles checked, or playback investigated.</p></li>
|
||||||
<strong>You request a movie or show</strong> through Seerr.
|
<li><strong>Follow the issue’s progress.</strong><p>Open your reported issue to see the work recorded and where the fix is up to.</p></li>
|
||||||
</li>
|
<li><strong>Tell us if it worked.</strong><p>When a supported repair is detected as ready to check, Magent can email you. Try the content, then choose “Yes” if it’s fixed or “No” if you still need help.</p></li>
|
||||||
<li>
|
</ol>
|
||||||
<strong>Magent picks up the request</strong> and shows its current state.
|
<p>Add your email in <a href="/profile">My profile</a> so updates can reach you. Reminder and automatic closure timings depend on the site’s settings.</p>
|
||||||
</li>
|
</details>
|
||||||
<li>
|
<details><summary>Invite someone</summary>
|
||||||
<strong>The automation stack searches and downloads it</strong> if it can find a valid
|
<ol>
|
||||||
release.
|
<li><strong>Open <a href="/profile/invites">04 Invites</a>.</strong><p>If invites are enabled for your account, give your invite a name you’ll recognise.</p></li>
|
||||||
</li>
|
<li><strong>Add a welcome note, or skip it.</strong><p>A custom invite code is optional too.</p></li>
|
||||||
<li>
|
<li><strong>Choose how to share it.</strong><p>Copy the link yourself, or enter an email address to send it directly.</p></li>
|
||||||
<strong>The file is imported into the library</strong>.
|
<li><strong>Manage it later.</strong><p>You can return to your invites to check them or disable a link. Your account’s invite limits apply automatically.</p></li>
|
||||||
</li>
|
</ol>
|
||||||
<li>
|
</details>
|
||||||
<strong>Jellyfin serves it</strong> once it is ready to watch.
|
<details><summary>Update your account</summary><p>Open the account menu and choose <a href="/profile">My profile</a> to update your contact email, view your activity, or use the password options available for your account.</p><p>Looking for your downloads instead? <a href="/">01 My Requests</a> is your starting point.</p></details>
|
||||||
</li>
|
<footer>Ready? <a href="/welcome">Choose where to go next →</a></footer>
|
||||||
</ol>
|
</main>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="how-flow">
|
|
||||||
<h2>What the statuses usually mean</h2>
|
|
||||||
<div className="how-grid">
|
|
||||||
<article className="how-card">
|
|
||||||
<h3>Pending</h3>
|
|
||||||
<p>The request exists, but it is still waiting for approval or the next step.</p>
|
|
||||||
</article>
|
|
||||||
<article className="how-card">
|
|
||||||
<h3>Approved / Processing</h3>
|
|
||||||
<p>The request has been accepted and the automation tools are working on it.</p>
|
|
||||||
</article>
|
|
||||||
<article className="how-card">
|
|
||||||
<h3>Downloading</h3>
|
|
||||||
<p>Magent can show live progress while the content is still being downloaded.</p>
|
|
||||||
</article>
|
|
||||||
<article className="how-card">
|
|
||||||
<h3>Ready</h3>
|
|
||||||
<p>The item has been imported and should now be available in Jellyfin.</p>
|
|
||||||
</article>
|
|
||||||
<article className="how-card">
|
|
||||||
<h3>Partial / Waiting</h3>
|
|
||||||
<p>
|
|
||||||
Part of the workflow completed, but the request is still waiting on another service or
|
|
||||||
on content becoming available.
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
<article className="how-card">
|
|
||||||
<h3>Declined</h3>
|
|
||||||
<p>The request was rejected or cannot proceed in its current form.</p>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="how-flow">
|
|
||||||
<h2>Live updates you can expect</h2>
|
|
||||||
<div className="how-step-grid">
|
|
||||||
<article className="how-step-card step-seerr">
|
|
||||||
<div className="step-badge">1</div>
|
|
||||||
<h3>Recent requests refresh automatically</h3>
|
|
||||||
<p className="step-note">
|
|
||||||
Your request list and landing-page activity update automatically while you are signed
|
|
||||||
in.
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
<article className="how-step-card step-qbit">
|
|
||||||
<div className="step-badge">2</div>
|
|
||||||
<h3>Request pages update in real time</h3>
|
|
||||||
<p className="step-note">
|
|
||||||
State changes, timeline steps, and downloader progress are pushed to the page live.
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
<article className="how-step-card step-jellyfin">
|
|
||||||
<div className="step-badge">3</div>
|
|
||||||
<h3>Ready state appears as soon as the import completes</h3>
|
|
||||||
<p className="step-note">
|
|
||||||
Once the content is actually available, Magent updates the request page without a hard
|
|
||||||
refresh.
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="how-flow">
|
|
||||||
<h2>User actions you may see</h2>
|
|
||||||
<div className="how-grid">
|
|
||||||
<article className="how-card">
|
|
||||||
<h3>Open request</h3>
|
|
||||||
<p>Jump into the full request page to inspect the current state and activity.</p>
|
|
||||||
</article>
|
|
||||||
<article className="how-card">
|
|
||||||
<h3>Open in Jellyfin</h3>
|
|
||||||
<p>Appears when the request is ready and Magent can link you through for playback.</p>
|
|
||||||
</article>
|
|
||||||
<article className="how-card">
|
|
||||||
<h3>Search + auto-download</h3>
|
|
||||||
<p>
|
|
||||||
Only appears for accounts that have been granted self-service download access by the
|
|
||||||
admin team.
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
<article className="how-card">
|
|
||||||
<h3>My invites</h3>
|
|
||||||
<p>
|
|
||||||
If your account is allowed to invite others, you can create and manage invite links
|
|
||||||
from your profile.
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="how-flow">
|
|
||||||
<h2>Invites and signup</h2>
|
|
||||||
<ol className="how-steps">
|
|
||||||
<li>
|
|
||||||
<strong>You receive an invite link</strong> by email or directly from the person who
|
|
||||||
invited you.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>You sign up through Magent</strong> and your account is linked into the media
|
|
||||||
stack.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Your account defaults apply</strong> based on the invite or your assigned
|
|
||||||
profile.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>You sign in and track requests</strong> from the landing page and your request
|
|
||||||
pages.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="how-callout">
|
|
||||||
<h2>If a request looks stuck</h2>
|
|
||||||
<p>
|
|
||||||
A waiting request usually means no usable release has been found yet, the download is
|
|
||||||
still in progress, or the import has not completed. Magent will keep updating as the
|
|
||||||
underlying services move forward.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export default function LoginPage() {
|
|||||||
if (!data?.authenticated) { setError('Could not sign in. Please try again.'); return }
|
if (!data?.authenticated) { setError('Could not sign in. Please try again.'); return }
|
||||||
setToken('cookie')
|
setToken('cookie')
|
||||||
const next = new URLSearchParams(window.location.search).get('next') || ''
|
const next = new URLSearchParams(window.location.search).get('next') || ''
|
||||||
window.location.assign(/^\/issues\/confirm\/\d+$/.test(next) ? next : '/')
|
window.location.assign(/^\/issues\/confirm\/\d+$/.test(next) ? next : '/welcome')
|
||||||
} catch {
|
} catch {
|
||||||
setError('Could not reach Magent. Check your connection and try again.')
|
setError('Could not reach Magent. Check your connection and try again.')
|
||||||
} finally { setLoading(false) }
|
} finally { setLoading(false) }
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import WorkspaceNavigation from './WorkspaceNavigation'
|
|||||||
|
|
||||||
export default function ApplicationChrome() {
|
export default function ApplicationChrome() {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
if (['/coming-soon', '/login', '/forgot-password', '/reset-password', '/signup'].includes(pathname)) return null
|
if (['/welcome', '/coming-soon', '/login', '/forgot-password', '/reset-password', '/signup'].includes(pathname)) return null
|
||||||
return <>
|
return <>
|
||||||
<header className="header">
|
<header className="header">
|
||||||
<div className="header-left"><a className="brand-link" href="/"><BrandingLogo className="brand-logo brand-logo--header" /><div className="brand-stack"><div className="brand">Magent</div><div className="tagline">GrizzlyFlix media operations</div></div></a></div>
|
<div className="header-left"><a className="brand-link" href="/"><BrandingLogo className="brand-logo brand-logo--header" /><div className="brand-stack"><div className="brand">Magent</div><div className="tagline">GrizzlyFlix media operations</div></div></a></div>
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ export default function HeaderIdentity() {
|
|||||||
{viewAsUser ? <span>Previewing user view</span> : null}
|
{viewAsUser ? <span>Previewing user view</span> : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="signed-in-actions">
|
<div className="signed-in-actions">
|
||||||
|
<a href="/welcome" onClick={() => setOpen(false)}>Welcome page</a>
|
||||||
|
<a href="/how-it-works" onClick={() => setOpen(false)}>How it works</a>
|
||||||
<a href="/profile" onClick={() => setOpen(false)}>
|
<a href="/profile" onClick={() => setOpen(false)}>
|
||||||
My profile
|
My profile
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.welcome-page, .friendly-guide { width: min(100%, 1040px); margin: 40px auto; color: var(--ops-text); }
|
||||||
|
.welcome-page > header { text-align: center; margin-bottom: 32px; }
|
||||||
|
.welcome-kicker { color: var(--ops-cyan); font-size: 12px; letter-spacing: .12em; text-transform: uppercase; }
|
||||||
|
.welcome-page h1 { font-size: clamp(30px, 5vw, 48px); line-height: 1.15; margin: 16px 0; }
|
||||||
|
.welcome-page p, .friendly-guide p { color: var(--ops-muted); line-height: 1.65; }
|
||||||
|
.welcome-choices { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
|
||||||
|
.welcome-choice { display: flex; flex-direction: column; align-items: flex-start; padding: 32px; gap: 16px; background: var(--ops-panel); border: 1px solid var(--ops-line); border-radius: 16px; text-decoration: none; color: inherit; }
|
||||||
|
.welcome-choice h2, .welcome-choice p { margin: 0; }
|
||||||
|
.welcome-choice strong { color: var(--ops-primary-2); margin-top: auto; padding-top: 16px; }
|
||||||
|
a.welcome-choice:hover { border-color: var(--ops-cyan); background: var(--ops-panel-2); }
|
||||||
|
.welcome-icon { color: var(--ops-cyan); font-size: 36px; line-height: 1; }
|
||||||
|
.welcome-page footer { text-align: center; margin-top: 28px; color: var(--ops-muted); }
|
||||||
|
.welcome-page a:focus-visible, .friendly-guide a:focus-visible, .friendly-guide summary:focus-visible { outline: 3px solid var(--ops-cyan); outline-offset: 5px; }
|
||||||
|
.friendly-guide > nav { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 24px; }
|
||||||
|
.friendly-guide details { border: 1px solid var(--ops-line); border-radius: 12px; background: var(--ops-panel); margin: 12px 0; padding: 20px 24px; }
|
||||||
|
.friendly-guide summary { font-weight: 700; font-size: 19px; cursor: pointer; }
|
||||||
|
.friendly-guide ol { padding-left: 24px; }
|
||||||
|
.friendly-guide li { padding: 8px 0 8px 8px; line-height: 1.6; }
|
||||||
|
.friendly-guide li p { margin: 4px 0; }
|
||||||
|
.friendly-guide > footer { padding: 20px 0; }
|
||||||
|
@media (max-width: 640px) { .welcome-choices { grid-template-columns: 1fr; } .welcome-page, .friendly-guide { margin: 24px auto; } .welcome-choice { padding: 24px; } }
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { authFetch, clearToken, getApiBase } from '../lib/auth'
|
||||||
|
import '../welcome.css'
|
||||||
|
|
||||||
|
export default function WelcomePage() {
|
||||||
|
const [ready, setReady] = useState(false)
|
||||||
|
const [url, setUrl] = useState<string | null>(null)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController()
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const response = await authFetch(`${getApiBase()}/site/info`, { signal: controller.signal })
|
||||||
|
if (response.status === 401) { clearToken(); window.location.replace('/login'); return }
|
||||||
|
if (!response.ok) throw new Error('Unavailable')
|
||||||
|
const data = await response.json()
|
||||||
|
const candidate = data.mediaServerUrl ? new URL(data.mediaServerUrl) : null
|
||||||
|
if (candidate && ['https:', 'http:'].includes(candidate.protocol) && !candidate.username && !candidate.password) setUrl(candidate.href)
|
||||||
|
setReady(true)
|
||||||
|
} catch {
|
||||||
|
if (!controller.signal.aborted) setError('We couldn’t load your welcome page. Please try again.')
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
return () => controller.abort()
|
||||||
|
}, [])
|
||||||
|
return <main className="welcome-page">
|
||||||
|
<header><span className="welcome-kicker">GrizzlyFlix + Magent</span><h1>Make yourself at home.</h1><p>Something to watch, or something to sort out?</p></header>
|
||||||
|
{error ? <div role="alert"><p>{error}</p><button onClick={() => window.location.reload()}>Try again</button> <a href="/login">Back to sign in</a></div> : !ready ? <p role="status">Getting things ready…</p> : <div className="welcome-choices">
|
||||||
|
{url ? <a className="welcome-choice" href={url}><span className="welcome-icon" aria-hidden="true">▷</span><h2>Go to GrizzlyFlix</h2><p>Find your next favourite. Watch movies and TV shows.</p><strong>Let’s watch <span aria-hidden="true">→</span></strong></a> : <section className="welcome-choice"><span className="welcome-icon" aria-hidden="true">▷</span><h2>Go to GrizzlyFlix</h2><p>The watch link hasn’t been set up yet. Please ask an admin to add the public playback URL.</p></section>}
|
||||||
|
<a className="welcome-choice" href="/"><span className="welcome-icon" aria-hidden="true">☷</span><h2>Manage your account</h2><p>Track requests, report a problem, or update your profile.</p><strong>Open 01 My Requests <span aria-hidden="true">→</span></strong></a>
|
||||||
|
</div>}
|
||||||
|
<footer>First time here? <a href="/how-it-works">Here’s how it works</a>.</footer>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
const { chromium } = require(process.env.REVIEW_PLAYWRIGHT);
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
(async () => {
|
||||||
|
const browser = await chromium.launch({ headless: true });
|
||||||
|
try {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
let mode = 'ready';
|
||||||
|
await page.route('**/api/**', route => {
|
||||||
|
if (route.request().url().endsWith('/auth/me')) return route.fulfill({ json: { username: 'Tester', role: 'user' } });
|
||||||
|
if (route.request().url().endsWith('/site/info')) return route.fulfill({ status: mode === 'unauthorized' ? 401 : 200, json: { mediaServerUrl: mode === 'missing' ? null : 'https://watch.example.com/' } });
|
||||||
|
return route.fulfill({ json: {} });
|
||||||
|
});
|
||||||
|
for (const width of [1440, 390]) {
|
||||||
|
await page.setViewportSize({ width, height: 900 });
|
||||||
|
await page.goto('http://127.0.0.1:3103/welcome');
|
||||||
|
await page.getByRole('link', { name: /Go to GrizzlyFlix/ }).waitFor();
|
||||||
|
assert.equal(await page.getByRole('link', { name: /Go to GrizzlyFlix/ }).getAttribute('href'), 'https://watch.example.com/');
|
||||||
|
assert.equal(await page.getByRole('link', { name: /Manage your account/ }).getAttribute('href'), '/');
|
||||||
|
assert.equal(await page.locator('.header').count(), 0);
|
||||||
|
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
||||||
|
await page.goto('http://127.0.0.1:3103/how-it-works');
|
||||||
|
await page.getByText('Understand the six progress steps').click();
|
||||||
|
await page.getByText('Your request has been received.').waitFor();
|
||||||
|
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
||||||
|
}
|
||||||
|
mode = 'missing';
|
||||||
|
await page.goto('http://127.0.0.1:3103/welcome');
|
||||||
|
await page.getByText(/watch link hasn’t been set up/).waitFor();
|
||||||
|
mode = 'unauthorized';
|
||||||
|
await page.goto('http://127.0.0.1:3103/welcome');
|
||||||
|
await page.waitForURL('**/login');
|
||||||
|
console.log('Welcome and guide checks passed: desktop, mobile, links, disclosure, missing URL, auth redirect.');
|
||||||
|
} finally { await browser.close(); }
|
||||||
|
})().catch(error => { console.error(error); process.exit(1); });
|
||||||
Reference in New Issue
Block a user