Discover page: refresh, harvest admin and seed reasons; docs (BL-0051)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0f87fbe948
commit
613db96355
10 files changed
+265
-58
No files matched your search
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
module.exports = async function testDiscoveryUI(url, cookie) {
|
||||
module.exports = async function testDiscoveryUI(url, cookie, { xssTitle } = {}) {
|
||||
const { chromium } = require('playwright');
|
||||
const browser = await chromium.launch({ channel: process.env.QA_BROWSER_CHANNEL || 'msedge', headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
@@ -9,24 +9,71 @@ module.exports = async function testDiscoveryUI(url, cookie) {
|
||||
await context.addCookies([{ name, value: value.join('='), url }]);
|
||||
const page = await context.newPage(), errors = [];
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
// An injected title must never run script; any dialog counts as a failure.
|
||||
page.on('dialog', dialog => { errors.push(`Unexpected dialog: ${dialog.message()}`); dialog.dismiss().catch(() => {}); });
|
||||
try {
|
||||
await page.goto(url + '/discover.html');
|
||||
await page.getByRole('heading', { name: 'Discover', exact: true }).waitFor();
|
||||
await page.getByText('Turn on personalized discovery').waitFor();
|
||||
await page.getByText('the server sends AniList the IDs of titles you liked, without login').waitFor();
|
||||
await page.getByLabel('Personalized discovery').check();
|
||||
// Review focus 5: the seeded feed is the first one shown, before any run replaces it.
|
||||
if (xssTitle) {
|
||||
const seeded = page.locator('.discovery-card').filter({ hasText: 'AniList readers who liked Moon Garden' });
|
||||
await seeded.waitFor();
|
||||
assert.equal(await seeded.locator('h2').textContent(), xssTitle);
|
||||
assert.equal(await page.locator('.discovery-card h2 img').count(), 0);
|
||||
await seeded.getByText('Why this is here · evidence').click();
|
||||
const details = await seeded.locator('details li').allTextContents();
|
||||
assert.ok(details.some(text => text.includes('New: started 2025')), `All reasons listed: ${details}`);
|
||||
assert.ok(details.some(text => /AniList recommendation.* · fetched \S+/.test(text)), `Evidence fetch date: ${details}`);
|
||||
}
|
||||
await page.getByRole('button', { name: 'Run discovery now' }).click();
|
||||
// The seeded store already says "success"; wait until the run's own feed replaced the seeded card.
|
||||
await page.waitForFunction(title => /Status: success/.test(document.querySelector('.discovery-admin')?.textContent || '')
|
||||
&& ![...document.querySelectorAll('.discovery-card h2')].some(heading => heading.textContent === title), xssTitle, { timeout: 60000 });
|
||||
await page.getByText(/suggestions/, { exact: false }).first().waitFor();
|
||||
await page.waitForFunction(() => document.querySelectorAll('.discovery-card').length > 0);
|
||||
await page.locator('.discovery-card').first().getByText('Why this is here · evidence').click();
|
||||
assert.ok(await page.locator('.discovery-card').first().locator('details li').count() > 0);
|
||||
await page.locator('.discovery-card').first().getByRole('button', { name: '👍 Maybe' }).click();
|
||||
await page.getByText('Preference saved').waitFor();
|
||||
|
||||
// Per-user refresh: the unreachable AniList endpoint ends it with upstream errors, then the cooldown shows.
|
||||
const refresh = page.locator('.discovery-refresh');
|
||||
await page.getByRole('button', { name: 'Refresh my suggestions' }).click();
|
||||
await page.waitForFunction(() => /Next refresh possible at \d{1,2}:\d{2}/.test(document.querySelector('.discovery-refresh')?.textContent || ''),
|
||||
null, { timeout: 90000 });
|
||||
assert.equal(await refresh.isDisabled(), true);
|
||||
await page.locator('.discovery-admin').getByText('Last harvest: stopped').waitFor({ timeout: 30000 });
|
||||
await page.locator('.discovery-admin').getByText('upstream-errors').waitFor();
|
||||
|
||||
// Nightly settings save through the settings route and survive a reload.
|
||||
await page.getByLabel('Automatic harvest').check();
|
||||
await page.getByText('Harvest settings saved').waitFor();
|
||||
await page.getByLabel('Harvest time').fill('04:15');
|
||||
await page.getByText(/Harvest settings saved \(on, 04:15\)/).waitFor();
|
||||
await page.reload();
|
||||
await page.getByRole('heading', { name: 'Discovery job' }).waitFor();
|
||||
assert.equal(await page.getByLabel('Harvest time').inputValue(), '04:15');
|
||||
assert.equal(await page.getByLabel('Automatic harvest').isChecked(), true);
|
||||
|
||||
// Harvest now is single-flight: a second click while one runs shows the server's 409 message.
|
||||
await page.getByRole('button', { name: 'Harvest now' }).click();
|
||||
await page.getByText('Harvest started').waitFor();
|
||||
await page.getByRole('button', { name: 'Harvest now' }).click();
|
||||
await page.getByText('A harvest is already queued or running').waitFor();
|
||||
assert.equal(errors.length, 0, `Browser errors: ${errors.join('; ')}`);
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
|
||||
await page.setViewportSize({ width: 375, height: 812 });
|
||||
await page.getByRole('button', { name: /Refresh my suggestions|Next refresh possible|Queued|Fetching|Paused by AniList/ }).waitFor();
|
||||
assert.ok(await refresh.isVisible());
|
||||
const box = await refresh.boundingBox();
|
||||
assert.ok(box && box.x >= 0 && box.x + box.width <= 375, `Refresh button off screen: ${JSON.stringify(box)}`);
|
||||
const overflow = await page.evaluate(() => ({ width: innerWidth, scroll: document.documentElement.scrollWidth,
|
||||
elements: [...document.querySelectorAll('body *')].filter(el => el.getBoundingClientRect().right > innerWidth + 1)
|
||||
.slice(0, 8).map(el => `${el.tagName}.${el.className}: ${Math.round(el.getBoundingClientRect().right)}`) }));
|
||||
assert.ok(overflow.scroll <= overflow.width, `Discover page overflows on mobile: ${JSON.stringify(overflow)}`);
|
||||
assert.ok(overflow.scroll <= 375, `Discover page overflows on mobile: ${JSON.stringify(overflow)}`);
|
||||
const create = await page.evaluate(() => fetch('/api/admin/user', { method: 'POST', headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ name: 'discovery-reader-qa', password: 'disposable-reader-test', admin: false }) }).then(response => response.status));
|
||||
assert.equal(create, 200);
|
||||
@@ -36,7 +83,10 @@ module.exports = async function testDiscoveryUI(url, cookie) {
|
||||
await page.reload();
|
||||
await page.getByRole('heading', { name: 'Discover', exact: true }).waitFor();
|
||||
assert.equal(await page.getByRole('button', { name: 'Run discovery now' }).count(), 0);
|
||||
assert.equal(await page.getByRole('button', { name: 'Harvest now' }).count(), 0);
|
||||
assert.equal(await page.evaluate(() => fetch('/api/admin/discovery').then(response => response.status)), 403);
|
||||
assert.equal(await page.evaluate(() => fetch('/api/admin/discovery/harvest', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
|
||||
.then(response => response.status)), 403);
|
||||
assert.equal(errors.length, 0, `Browser errors: ${errors.join('; ')}`);
|
||||
} finally { await browser.close(); }
|
||||
};
|
||||
Reference in new issue
Block a user