mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
* feat(website): rebuild the hero, features, download and support sections Third landing redesign PR: the home page now speaks the app's own language instead of a template's. - Hero: left-aligned headline, a primary button that follows the visitor's OS (server-rendered fallback goes to /download/), an "All platforms" button and a text link for self-hosting. Project numbers (stars, downloads, languages, license) are set in the page's own typography from the GitHub API at build time (`lib/github-stats.ts`); anything the build cannot resolve is left out rather than faked. A small "Now playing" card with fictional demo content replaces the mascot, badges and social chips; the screenshot is lit by a blurred copy of itself, like the player's ambient mode. - Features: a bento of interface fragments (EPG rows with progress, posters with a resume bar, a download queue with a REC dot, a remote D-pad) instead of numbered cards with icon watermarks and a marquee. The marquee CSS is gone. - Download: one row per platform with the release's file names and a "Detected" badge for the visitor's OS (`lib/detect-platform.ts`), package-manager commands in the same panel. - Support becomes a single row; the disclaimer moves into the footer, which also gets the mascot and a link list. - Home sections drop the decorative eyebrows and the divider rules; the unused broadcast-wave, support-signal and watermark illustrations are removed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(website): cover the rebuilt home sections and the OS-aware download CTA `website-home-sections.test.mjs` (in the website test target) reads the built home page — generic hero CTA to /download/, project facts, no badge images or dashed borders, every feature page linked from the bento, one download row per platform with release file names and the Detected badge hidden, the disclaimer and mascot in the footer — and then drives it in Chromium under Windows, macOS, Linux and Android user agents: the primary button and the Detected row follow the platform, the phone keeps the generic markup, and the copy button writes the command to the clipboard. `detectPlatform()` now consults the user-agent string before the client-hints platform and the deprecated `navigator.platform`, so the source a visitor's tools actually change decides first; silent sources fall through instead of vetoing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(website): keep iPads on the generic download path iPadOS asks for desktop sites with a Macintosh user agent and a `MacIntel` platform, so the OS guess sent iPad visitors to the macOS installers. A "mac" verdict is now trusted only on a device without touch points: no Mac has a touch screen, every iPad reports several. The browser test adds an iPad-in-desktop-mode row to the generic-device matrix next to the Android phone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
161 lines
7.9 KiB
JavaScript
161 lines
7.9 KiB
JavaScript
import { readFile } from 'node:fs/promises';
|
|
import { join } from 'node:path';
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { BASE_PATH as BASE, distRoot, launchBrowser, serveDist } from './website-browser-support.mjs';
|
|
|
|
/**
|
|
* Home page sections rebuilt in the landing redesign: the hero, the feature
|
|
* bento, the download panel and the footer.
|
|
*
|
|
* The structural half reads the built HTML and checks that the server-rendered
|
|
* markup is already correct for a visitor whose OS cannot be guessed. The
|
|
* browser half loads the page under desktop and phone user agents and checks
|
|
* the client-side OS detection: the hero's primary button and the "Detected"
|
|
* row in the download panel follow the platform, phones get the generic
|
|
* markup, and the copy buttons write to the clipboard. Without a Chromium the
|
|
* browser half is skipped locally and fails in CI (see
|
|
* website-browser-support.mjs).
|
|
*/
|
|
|
|
const PLATFORMS = ['windows', 'macos', 'linux'];
|
|
|
|
test('hero: generic download CTA, self-host link and project facts', async () => {
|
|
const html = await readFile(join(distRoot, 'index.html'), 'utf8');
|
|
const primary = html.match(/<a[^>]*data-hero-primary[^>]*>[\s\S]*?<\/a>/)?.[0];
|
|
assert.ok(primary, 'hero primary button');
|
|
assert.match(primary, /href="\/iptvnator\/download\/"/, 'server-rendered CTA goes to the download hub');
|
|
assert.match(primary, /Download for desktop/);
|
|
assert.match(html, /data-hero-secondary[^>]*>\s*All platforms/);
|
|
assert.match(html, /href="\/iptvnator\/download\/docker\/"[^>]*>\s*Self-host the web app/);
|
|
assert.match(html, /<dd[^>]*>19<\/dd>/, 'languages fact');
|
|
assert.match(html, /<dd[^>]*>MIT<\/dd>/, 'license fact');
|
|
assert.doesNotMatch(html, /img\.shields\.io/, 'no badge images');
|
|
assert.doesNotMatch(html, /border-dashed/, 'no dashed borders anywhere on the home page');
|
|
});
|
|
|
|
test('features bento links every feature page and shows interface fragments', async () => {
|
|
const html = await readFile(join(distRoot, 'index.html'), 'utf8');
|
|
const section = html.match(/<section id="features"[\s\S]*?<\/section>/)?.[0];
|
|
assert.ok(section, 'features section');
|
|
for (const slug of ['m3u-player', 'xtream-codes-player', 'stalker-portal-player', 'epg', 'remote-control']) {
|
|
assert.match(section, new RegExp(`href="/iptvnator/features/${slug}/"`));
|
|
}
|
|
assert.match(section, /Aurora News/, 'EPG fragment uses fictional channels');
|
|
assert.match(section, /REC/, 'recording fragment');
|
|
assert.doesNotMatch(section, /carousel-track/, 'the marquee is gone');
|
|
});
|
|
|
|
test('download panel: one row per platform with release file names and package commands', async () => {
|
|
const html = await readFile(join(distRoot, 'index.html'), 'utf8');
|
|
const section = html.match(/<section id="download"[\s\S]*?<\/section>/)?.[0];
|
|
assert.ok(section, 'download section');
|
|
assert.match(section, /Get IPTVnator \d+\.\d+\.\d+/);
|
|
for (const platform of PLATFORMS) {
|
|
const row = section.match(new RegExp(`<div[^>]*data-platform-row="${platform}"[\\s\\S]*?data-platform-link[^>]*>[\\s\\S]*?</a>`))?.[0];
|
|
assert.ok(row, `${platform} row`);
|
|
assert.match(row, new RegExp(`href="/iptvnator/download/${platform}/"`));
|
|
assert.match(row, /iptvnator-\d+\.\d+\.\d+-/, 'lists a release file name');
|
|
assert.match(row, /class="hidden[^"]*"[^>]*data-detected-badge/, 'the Detected badge is hidden until the browser decides');
|
|
}
|
|
assert.match(section, /brew install --cask iptvnator/);
|
|
assert.match(section, /sudo snap install iptvnator/);
|
|
assert.match(section, /yay -S iptvnator-bin/);
|
|
});
|
|
|
|
test('footer carries the disclaimer, the mascot and the site links', async () => {
|
|
const html = await readFile(join(distRoot, 'index.html'), 'utf8');
|
|
const footer = html.match(/<footer[\s\S]*?<\/footer>/)?.[0];
|
|
assert.ok(footer, 'footer');
|
|
assert.match(footer, /does not provide, host or sell/);
|
|
assert.match(footer, /iptvnator-1-mascot/, 'the mascot lives in the footer');
|
|
for (const href of ['/iptvnator/features/', '/iptvnator/compare/', '/iptvnator/download/', '/iptvnator/blog/']) {
|
|
assert.match(footer, new RegExp(`href="${href}"`));
|
|
}
|
|
});
|
|
|
|
const AGENTS = {
|
|
windows: {
|
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36',
|
|
label: 'Windows',
|
|
},
|
|
macos: {
|
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36',
|
|
label: 'macOS',
|
|
},
|
|
linux: {
|
|
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36',
|
|
label: 'Linux',
|
|
},
|
|
};
|
|
|
|
test('browser: the download CTA and the Detected row follow the visitor OS, phones stay generic', async (t) => {
|
|
const browser = await launchBrowser();
|
|
if (!browser) {
|
|
t.skip('no Chromium available for the browser half');
|
|
return;
|
|
}
|
|
const { server, origin } = await serveDist();
|
|
const readState = (page) =>
|
|
page.evaluate(() => ({
|
|
primary: document.querySelector('[data-hero-primary]')?.getAttribute('href'),
|
|
label: document.querySelector('[data-hero-primary-label]')?.textContent.trim(),
|
|
detected: [...document.querySelectorAll('[data-detected-badge]')]
|
|
.filter((el) => !el.classList.contains('hidden'))
|
|
.map((el) => el.closest('[data-platform-row]').dataset.platformRow),
|
|
}));
|
|
try {
|
|
for (const [platform, agent] of Object.entries(AGENTS)) {
|
|
const page = await browser.newPage({ userAgent: agent.userAgent, viewport: { width: 1440, height: 900 } });
|
|
const errors = [];
|
|
page.on('pageerror', (error) => errors.push(String(error)));
|
|
await page.goto(`${origin}${BASE}/`, { waitUntil: 'networkidle' });
|
|
const state = await readState(page);
|
|
assert.equal(state.primary, `/iptvnator/download/${platform}/`, `${platform}: primary CTA target`);
|
|
assert.equal(state.label, `Download for ${agent.label}`, `${platform}: primary CTA label`);
|
|
assert.deepEqual(state.detected, [platform], `${platform}: detected row`);
|
|
assert.deepEqual(errors, []);
|
|
await page.close();
|
|
}
|
|
|
|
const generic = [
|
|
{
|
|
name: 'Android phone',
|
|
userAgent: 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Mobile Safari/537.36',
|
|
viewport: { width: 390, height: 844 },
|
|
hasTouch: true,
|
|
},
|
|
{
|
|
// iPadOS asks for the desktop site with a Macintosh user agent; only the touch points give it away.
|
|
name: 'iPad in desktop mode',
|
|
userAgent: AGENTS.macos.userAgent.replace('Chrome/128.0 Safari/537.36', 'Version/17.5 Safari/605.1.15'),
|
|
viewport: { width: 1024, height: 768 },
|
|
hasTouch: true,
|
|
},
|
|
];
|
|
for (const device of generic) {
|
|
const page = await browser.newPage({ userAgent: device.userAgent, viewport: device.viewport, hasTouch: device.hasTouch });
|
|
await page.goto(`${origin}${BASE}/`, { waitUntil: 'networkidle' });
|
|
const state = await readState(page);
|
|
assert.equal(state.primary, '/iptvnator/download/', `${device.name} keeps the generic CTA`);
|
|
assert.equal(state.label, 'Download for desktop', `${device.name} keeps the generic label`);
|
|
assert.deepEqual(state.detected, [], `${device.name} highlights no platform row`);
|
|
await page.close();
|
|
}
|
|
|
|
// Copy buttons write the command and confirm briefly.
|
|
const context = await browser.newContext({ permissions: ['clipboard-read', 'clipboard-write'] });
|
|
const page = await context.newPage();
|
|
await page.goto(`${origin}${BASE}/`, { waitUntil: 'networkidle' });
|
|
const button = page.locator('.copy-btn').first();
|
|
await button.scrollIntoViewIfNeeded();
|
|
await button.click();
|
|
assert.equal(await button.textContent(), 'Copied');
|
|
assert.equal(await page.evaluate(() => navigator.clipboard.readText()), 'brew install --cask iptvnator');
|
|
await context.close();
|
|
} finally {
|
|
await browser.close();
|
|
server.close();
|
|
}
|
|
});
|