// The directory's public pages (#395), directory.ipxreader.app in production: what anyone can // browse, served by ipx-directory itself (crates/directory/src/site.rs), with one script of its own // (site.ts, the Copy button). The suite's directory lists the fixture feeds the suite's ipx shares // with it. Nothing here signs in: the pages are public, and need no account. // // After app.spec.ts, file order being run order, whose tests may have changed a feed's category: // these follow the categories the pages give, rather than expecting one. import type { Page } from '@playwright/test'; import { DIRECTORY } from './global-setup.ts'; import { expect, test } from './coverage.ts'; const at = (p: string) => DIRECTORY + p; /// Script errors, anything the pages' security policy refused, and any answer of the directory's /// own that failed: none is expected on any page. A cover is the publisher's, loaded from it, and /// the fixtures' Test Show names one that is not there, so a cover's failure is not counted. function watch(page: Page) { const bad: string[] = []; page.on('pageerror', (e) => bad.push(e.message)); page.on('console', (m) => { if (m.type() === 'error' && /Refused|Content Security Policy/.test(m.text())) bad.push(m.text()); }); page.on('response', (r) => { if (r.url().startsWith(DIRECTORY) && r.status() >= 400) bad.push(`${r.status()} ${r.url()}`); }); return bad; } test.beforeAll(async ({ request }) => { // The directory reads a feed once a server has shared it: until then a page says nothing is listed. await expect.poll(async () => (await (await request.get(at('/all'))).text()).includes('Test Show'), { timeout: 30_000 }).toBe(true); }); test('the home page leads to the most subscribed, a category and every feed A to Z', async ({ page }) => { const bad = watch(page); const home = await page.goto(at('/')); expect(home?.status()).toBe(200); // Its own script and stylesheet only. expect(home?.headers()['content-security-policy']).toContain("script-src 'self'"); await expect(page.locator('h2', { hasText: 'Most subscribed' })).toBeVisible(); await expect(page.locator('ol.chart li a', { hasText: 'Test Show' })).toBeVisible(); // A category: its page is named for it, and lists only what it holds. const cat = page.locator('.cats a.cat').first(); const name = (await cat.locator('b').textContent()) ?? ''; await cat.click(); await expect(page).toHaveURL(/\/category\//); await expect(page.locator('h2')).toHaveText(name); await expect(page.locator('.tiles a.tile').first()).toBeVisible(); await page.locator('.back a', { hasText: 'Directory' }).click(); await expect(page).toHaveURL(at('/')); // A to Z: as many tiles as the link says, sorted by name. const more = page.locator('a.more'); const count = Number((await more.textContent())?.match(/All (\d+) feeds/)?.[1]); await more.click(); await expect(page).toHaveURL(at('/all')); await expect(page.locator('.tiles a.tile')).toHaveCount(count); const names = await page.locator('.tiles a.tile b').allTextContents(); expect(names).toEqual([...names].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base', numeric: true }))); expect(bad).toEqual([]); }); test('podcasts and blogs are told apart', async ({ page }) => { await page.goto(at('/all')); const all = await page.locator('.tiles a.tile').count(); await page.locator('nav.tabs a', { hasText: 'Podcasts' }).click(); await expect(page).toHaveURL(/kind=podcasts/); const podcasts = await page.locator('.tiles a.tile').count(); await page.locator('nav.tabs a', { hasText: 'Blogs' }).click(); await expect(page).toHaveURL(/kind=blogs/); const blogs = await page.locator('.tiles a.tile').count(); // Test Show has episodes; Picture Blog has none, only posts. expect(podcasts).toBeGreaterThan(0); expect(blogs).toBeGreaterThan(0); expect(podcasts + blogs).toBe(all); await expect(page.locator('.tiles a.tile', { hasText: 'Test Show' })).toHaveCount(0); }); test('a search from the header finds a feed by its name', async ({ page }) => { await page.goto(at('/')); const box = page.getByRole('searchbox', { name: 'Find a feed by name' }); await box.fill('test show'); await box.press('Enter'); await expect(page).toHaveURL(/\/search\?q=test\+show/); await expect(page.locator('.tiles a.tile', { hasText: 'Test Show' })).toBeVisible(); await box.fill('nothing is called this'); await box.press('Enter'); await expect(page.locator('p.hint')).toHaveText('Nothing here.'); }); test('a feed\'s page gives its address, and Copy puts it on the clipboard', async ({ page, context }) => { const bad = watch(page); await context.grantPermissions(['clipboard-read', 'clipboard-write'], { origin: DIRECTORY }); await page.goto(at('/')); await page.locator('ol.chart li a', { hasText: 'Test Show' }).click(); await expect(page).toHaveURL(/\/feed\//); await expect(page.locator('.show .meta h2')).toHaveText('Test Show'); await expect(page.locator('p.about')).toHaveText('A fixture feed.'); const address = page.locator('#addr'); await expect(address).toHaveValue('http://127.0.0.1:8792/show.xml'); // site.js, the pages' one script (#215, #392). By its attribute: its name is Copy, then Copied. const copy = page.locator('button[data-copy]'); await copy.click(); await expect(copy).toHaveText('Copied'); expect(await page.evaluate(() => navigator.clipboard.readText())).toBe('http://127.0.0.1:8792/show.xml'); await expect(copy).toHaveText('Copy', { timeout: 5_000 }); expect(bad).toEqual([]); }); test('a feed the directory does not list is a page saying so, answered 404', async ({ page }) => { const r = await page.goto(at('/feed/no-such-feed')); expect(r?.status()).toBe(404); await expect(page.locator('h2')).toHaveText('Not found'); await expect(page.locator('p.hint')).toHaveText('That feed is not in the directory.'); await page.locator('.back a').click(); await expect(page).toHaveURL(at('/')); }); test('crawlers are told every page there is, and to leave search and the API alone', async ({ request }) => { const robots = await (await request.get(at('/robots.txt'))).text(); expect(robots).toContain('Disallow: /search'); expect(robots).toContain('Disallow: /v1/'); expect(robots).toContain(`Sitemap: ${DIRECTORY}/sitemap.xml`); const sitemap = await (await request.get(at('/sitemap.xml'))).text(); const locs = [...sitemap.matchAll(/([^<]+)<\/loc>/g)].map((m) => m[1]); expect(locs).toContain(`${DIRECTORY}/`); expect(locs).toContain(`${DIRECTORY}/all`); expect(locs.some((l) => l?.startsWith(`${DIRECTORY}/category/`))).toBe(true); // Every feed on A to Z has its page in it. const all = await (await request.get(at('/all'))).text(); for (const id of [...all.matchAll(/href="\/feed\/([^"]+)"/g)].map((m) => m[1])) expect(locs).toContain(`${DIRECTORY}/feed/${id}`); });