mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
New post at /blog/epg-wrong-program-fix/: a table separating the three symptoms (an empty row, a channel showing another station's schedule, and every programme shifted by a fixed amount), how the tvg-id → tvg-name → name lookup chain produces the first two, the manual "Map EPG channel" flow, and the display-only EPG time offset with the sign to use. Seven FAQ entries. The three screenshots are mock-backed. The Xtream mock gains /demo/guide.xml, an XMLTV guide for its marketing live channels whose ids deliberately match no playlist tvg-id, which is what makes the manual mapping worth showing; the capture imports it through the settings, accepting the private-network confirmation a loopback source raises, then right-clicks a channel of the M3U fixture and searches the dialog. The new actions live in capture-navigation-epg-actions.ts, alongside the setup, portal and download modules, and borrow one entry point from each of its two neighbours instead of duplicating their navigation. Also fixes assertMockServerIdentity: it checked both expected categories against get_vod_categories, but "Urban Drama" is a series category, so the reuse path rejected every already-running mock and a capture could only run when the port happened to be free. Each category is now checked against its own endpoint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
102 lines
3.5 KiB
JavaScript
102 lines
3.5 KiB
JavaScript
import { access, readFile } from 'node:fs/promises';
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
/**
|
|
* Structural checks for guide posts: they must carry FAQPage structured data
|
|
* next to the BlogPosting entry, link to the download hub, and reference only
|
|
* screenshots that the build actually shipped.
|
|
*/
|
|
|
|
const distRoot = new URL('../../dist/apps/website/', import.meta.url);
|
|
const SITE = 'https://4gray.github.io/iptvnator';
|
|
|
|
const GUIDES = [
|
|
{
|
|
slug: 'xtream-codes-setup-guide',
|
|
screenshots: [
|
|
'blog/guides/screenshots/guide-xtream-add-playlist-dark.png',
|
|
'blog/guides/screenshots/guide-xtream-auto-detect-dark.png',
|
|
'blog/guides/screenshots/guide-xtream-live-dark.png',
|
|
],
|
|
},
|
|
{
|
|
slug: 'stalker-portal-setup-guide',
|
|
screenshots: [
|
|
'blog/guides/screenshots/guide-stalker-add-playlist-dark.png',
|
|
'blog/guides/screenshots/guide-stalker-live-dark.png',
|
|
],
|
|
},
|
|
{
|
|
slug: 'm3u-playlist-epg-setup-guide',
|
|
screenshots: [
|
|
'blog/guides/screenshots/guide-m3u-add-playlist-dark.png',
|
|
'blog/guides/screenshots/guide-m3u-live-groups-dark.png',
|
|
'blog/guides/screenshots/guide-epg-settings-dark.png',
|
|
],
|
|
},
|
|
{
|
|
slug: 'offline-downloads-guide',
|
|
screenshots: [
|
|
'blog/guides/screenshots/guide-downloads-movie-detail-dark.png',
|
|
'blog/guides/screenshots/guide-downloads-manager-dark.png',
|
|
'blog/guides/screenshots/guide-downloads-offline-movie-dark.png',
|
|
],
|
|
},
|
|
{
|
|
slug: 'alternative-sources-guide',
|
|
screenshots: [
|
|
'blog/guides/screenshots/guide-sources-chip-dark.png',
|
|
'blog/guides/screenshots/guide-sources-menu-dark.png',
|
|
],
|
|
},
|
|
{
|
|
slug: 'remote-control-guide',
|
|
screenshots: [
|
|
'blog/guides/screenshots/guide-remote-settings-dark.png',
|
|
'blog/guides/screenshots/guide-remote-phone-dark.png',
|
|
],
|
|
},
|
|
{
|
|
slug: 'epg-wrong-program-fix',
|
|
screenshots: [
|
|
'blog/guides/screenshots/guide-epg-map-menu-dark.png',
|
|
'blog/guides/screenshots/guide-epg-map-dialog-dark.png',
|
|
'blog/guides/screenshots/guide-epg-offset-dark.png',
|
|
],
|
|
},
|
|
];
|
|
|
|
const readDist = (relativePath) => readFile(new URL(relativePath, distRoot), 'utf8');
|
|
|
|
function extractJsonLd(html) {
|
|
const blocks = [...html.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)];
|
|
assert.ok(blocks.length > 0, 'Expected at least one JSON-LD script block.');
|
|
return blocks.flatMap((match) => JSON.parse(match[1]));
|
|
}
|
|
|
|
for (const guide of GUIDES) {
|
|
test(`${guide.slug}: BlogPosting and FAQPage structured data`, async () => {
|
|
const html = await readDist(`blog/${guide.slug}/index.html`);
|
|
const schema = extractJsonLd(html);
|
|
|
|
assert.ok(schema.some((entry) => entry['@type'] === 'BlogPosting'), 'Expected a BlogPosting entry.');
|
|
|
|
const faq = schema.find((entry) => entry['@type'] === 'FAQPage');
|
|
assert.ok(faq, 'Expected a FAQPage entry.');
|
|
assert.ok(faq.mainEntity.length >= 5, 'Expected at least five FAQ questions.');
|
|
assert.match(html, /Frequently asked questions/);
|
|
assert.match(html, new RegExp(`<link rel="canonical" href="${SITE}/blog/${guide.slug}/"`));
|
|
});
|
|
|
|
test(`${guide.slug}: links to the download hub and ships its screenshots`, async () => {
|
|
const html = await readDist(`blog/${guide.slug}/index.html`);
|
|
assert.match(html, /href="\/iptvnator\/download\/"/);
|
|
|
|
for (const screenshot of guide.screenshots) {
|
|
assert.match(html, new RegExp(`src="/iptvnator/${screenshot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`));
|
|
await access(new URL(screenshot, distRoot));
|
|
}
|
|
});
|
|
}
|