Show read-only server reading hours in settings
This commit is contained in:
1 parent
de8e7ed988
commit
ddd9c62c40
9 files changed
+89
-7
No files matched your search
@@ -222,6 +222,10 @@ main { max-width: 1200px; margin: 0 auto; padding: 16px; }
|
|||||||
.settings-title-row { display: flex; align-items: center; gap: 12px; }
|
.settings-title-row { display: flex; align-items: center; gap: 12px; }
|
||||||
.settings-hint { margin: 5px 0 20px; }
|
.settings-hint { margin: 5px 0 20px; }
|
||||||
.settings-content .card { max-width: none; }
|
.settings-content .card { max-width: none; }
|
||||||
|
.reading-hours-list { border: 1px solid var(--line); border-radius: 9px; overflow: hidden; }
|
||||||
|
.reading-hours-day { display: grid; grid-template-columns: minmax(95px, 8em) minmax(0, 1fr); gap: 12px; padding: 10px 12px; }
|
||||||
|
.reading-hours-day + .reading-hours-day { border-top: 1px solid var(--line); }
|
||||||
|
.reading-hours-day span { overflow-wrap: anywhere; }
|
||||||
.peer-card { min-width: 0; overflow-wrap: anywhere; }
|
.peer-card { min-width: 0; overflow-wrap: anywhere; }
|
||||||
.peer-card textarea, .peer-card input { min-width: 0; max-width: 100%; }
|
.peer-card textarea, .peer-card input { min-width: 0; max-width: 100%; }
|
||||||
.peer-card .row > .field { min-width: 0; }
|
.peer-card .row > .field { min-width: 0; }
|
||||||
|
|||||||
+11
-1
@@ -3,6 +3,7 @@
|
|||||||
// Two navigation levels; scraper tabs stay within their section, not another menu.
|
// Two navigation levels; scraper tabs stay within their section, not another menu.
|
||||||
const SETTINGS_SECTIONS = [
|
const SETTINGS_SECTIONS = [
|
||||||
{ id: 'user/reading', group: 'User', title: 'Reading', hint: 'Reader modes, page spacing and zoom.' },
|
{ id: 'user/reading', group: 'User', title: 'Reading', hint: 'Reader modes, page spacing and zoom.' },
|
||||||
|
{ id: 'user/reading-hours', group: 'User', title: 'Reading hours', hint: 'When this server allows chapter reading.' },
|
||||||
{ id: 'user/reading-time', group: 'User', title: 'Reading time & privacy', hint: 'Your clock, Reading Story and personal data.' },
|
{ id: 'user/reading-time', group: 'User', title: 'Reading time & privacy', hint: 'Your clock, Reading Story and personal data.' },
|
||||||
{ id: 'user/notifications', group: 'User', title: 'Notifications', hint: 'Chapter alerts and your optional webhook.' },
|
{ id: 'user/notifications', group: 'User', title: 'Notifications', hint: 'Chapter alerts and your optional webhook.' },
|
||||||
{ id: 'user/account', group: 'User', title: 'Account', hint: 'Your account and password.' },
|
{ id: 'user/account', group: 'User', title: 'Account', hint: 'Your account and password.' },
|
||||||
@@ -15,6 +16,15 @@ const SETTINGS_SECTIONS = [
|
|||||||
{ id: 'admin/health', group: 'Admin', title: 'Service health', hint: 'Service status, incidents and recovery.', href: 'health.html' },
|
{ id: 'admin/health', group: 'Admin', title: 'Service health', hint: 'Service status, incidents and recovery.', href: 'health.html' },
|
||||||
];
|
];
|
||||||
const SCRAPER_TABS = ['activity', 'queue', 'updates', 'sources'];
|
const SCRAPER_TABS = ['activity', 'queue', 'updates', 'sources'];
|
||||||
|
const READING_DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||||
|
|
||||||
|
function readingDayText(schedule, index) {
|
||||||
|
const previous = (index + 6) % 7;
|
||||||
|
const carried = schedule.days[previous].filter(([start, end]) => end < start)
|
||||||
|
.map(([, end]) => `00:00–${end} (from ${READING_DAYS[previous]})`);
|
||||||
|
const starting = schedule.days[index].map(([start, end]) => `${start}–${end}${end < start ? ' (next day)' : ''}`);
|
||||||
|
return [...carried, ...starting].join(' · ') || 'Closed all day';
|
||||||
|
}
|
||||||
|
|
||||||
function settingsRoute(hash, admin) {
|
function settingsRoute(hash, admin) {
|
||||||
const parts = String(hash || '').replace(/^#/, '').split('/');
|
const parts = String(hash || '').replace(/^#/, '').split('/');
|
||||||
@@ -59,4 +69,4 @@ function settingsShell(me, active = 'user/reading') {
|
|||||||
return { content, titleRow, select };
|
return { content, titleRow, select };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof module !== 'undefined') module.exports = { SETTINGS_SECTIONS, SCRAPER_TABS, settingsRoute };
|
if (typeof module !== 'undefined') module.exports = { SETTINGS_SECTIONS, SCRAPER_TABS, settingsRoute, readingDayText };
|
||||||
@@ -85,6 +85,22 @@
|
|||||||
h('button', { class: 'btn small', onclick: async () => { await save({ zoom: {} }); zoomText.textContent = zooms(); } }, 'Reset to fit'),
|
h('button', { class: 'btn small', onclick: async () => { await save({ zoom: {} }); zoomText.textContent = zooms(); } }, 'Reset to fit'),
|
||||||
h('div', { class: 'muted small' }, '100% = the page fits the screen. Change it in the reader with Ctrl + mouse wheel, a pinch, or the + / - keys.'))));
|
h('div', { class: 'muted small' }, '100% = the page fits the screen. Change it in the reader with Ctrl + mouse wheel, a pinch, or the + / - keys.'))));
|
||||||
|
|
||||||
|
const readingHoursBody = h('div', {}, h('p', { class: 'muted' }, 'Loading server reading hours…'));
|
||||||
|
section('user/reading-hours').append(h('section', { class: 'card' },
|
||||||
|
h('h2', {}, 'Server reading hours'), readingHoursBody,
|
||||||
|
h('p', { class: 'muted small' }, 'These hours are set in the server deployment and cannot be changed here. Overnight windows continue into the next day.')));
|
||||||
|
api('api/reading-schedule').then(schedule => {
|
||||||
|
readingHoursBody.textContent = '';
|
||||||
|
if (!schedule.enabled) {
|
||||||
|
readingHoursBody.append(h('p', {}, 'Reading is available at any time. No schedule is configured.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
readingHoursBody.append(h('p', { class: 'muted' }, `Server timezone: ${schedule.timezone}`),
|
||||||
|
h('div', { class: 'reading-hours-list' }, READING_DAYS.map((day, index) =>
|
||||||
|
h('div', { class: 'reading-hours-day' }, h('strong', {}, day),
|
||||||
|
h('span', {}, readingDayText(schedule, index))))));
|
||||||
|
}).catch(error => { readingHoursBody.replaceChildren(h('p', { class: 'error', role: 'alert' }, `Could not load reading hours: ${error.message}`)); });
|
||||||
|
|
||||||
section('user/reading-time').append(h('section', { class: 'card' },
|
section('user/reading-time').append(h('section', { class: 'card' },
|
||||||
h('h2', {}, 'Clock & Reading Story'),
|
h('h2', {}, 'Clock & Reading Story'),
|
||||||
check('readingClock', 'Track and show the Active Reading Clock',
|
check('readingClock', 'Track and show the Active Reading Clock',
|
||||||
|
|||||||
+6
-1
@@ -44,6 +44,11 @@ function scheduled(schedule, date = new Date()) {
|
|||||||
schedule.days[(day + 6) % 7].some(({ start, end }) => end < start && now < end);
|
schedule.days[(day + 6) % 7].some(({ start, end }) => end < start && now < end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function publicSchedule(schedule) {
|
||||||
|
return { enabled: !!schedule, timezone: schedule?.timezone || null,
|
||||||
|
days: schedule ? schedule.days.map(ranges => ranges.map(({ label }) => [...label])) : [] };
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeAnswer(value) {
|
function normalizeAnswer(value) {
|
||||||
return String(value || '').normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
|
return String(value || '').normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
|
||||||
.replace(/[^a-z0-9]+/g, ' ').trim().replace(/\s+/g, ' ');
|
.replace(/[^a-z0-9]+/g, ' ').trim().replace(/\s+/g, ' ');
|
||||||
@@ -136,4 +141,4 @@ function createReadingAccess({ schedule, bank = {}, unlockMinutes = 60, grants,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { parseSchedule, scheduled, normalizeAnswer, answerMatches, loadQuestionBank, createReadingAccess };
|
module.exports = { parseSchedule, scheduled, publicSchedule, normalizeAnswer, answerMatches, loadQuestionBank, createReadingAccess };
|
||||||
@@ -25,7 +25,7 @@ const { generate: generateDiscovery } = require('./discovery/engine');
|
|||||||
const {
|
const {
|
||||||
applyPulse, applyVisit, chapterInsight, eligiblePages, emptyRecord, libraryInsight, mangaInsight, publicSummary,
|
applyPulse, applyVisit, chapterInsight, eligiblePages, emptyRecord, libraryInsight, mangaInsight, publicSummary,
|
||||||
} = require('./reading-analytics');
|
} = require('./reading-analytics');
|
||||||
const { parseSchedule, loadQuestionBank, createReadingAccess } = require('./reading-access');
|
const { parseSchedule, publicSchedule, loadQuestionBank, createReadingAccess } = require('./reading-access');
|
||||||
|
|
||||||
const PORT = +process.env.PORT || 8080;
|
const PORT = +process.env.PORT || 8080;
|
||||||
const LIB = path.resolve(process.env.LIBRARY || 'library');
|
const LIB = path.resolve(process.env.LIBRARY || 'library');
|
||||||
@@ -401,6 +401,7 @@ const routes = {
|
|||||||
},
|
},
|
||||||
'GET /api/me': ({ u }) => ({ name: u.name, admin: !!u.admin, settings: settingsOf(u), unread: unreadOf(u) }),
|
'GET /api/me': ({ u }) => ({ name: u.name, admin: !!u.admin, settings: settingsOf(u), unread: unreadOf(u) }),
|
||||||
|
|
||||||
|
'GET /api/reading-schedule': () => publicSchedule(readingSchedule),
|
||||||
'GET /api/reading-access': ({ u, q }) => { mangaOf(q.get('m')); return readingAccess.status(u.name, true); },
|
'GET /api/reading-access': ({ u, q }) => { mangaOf(q.get('m')); return readingAccess.status(u.name, true); },
|
||||||
'POST /api/reading-quiz/start': ({ u, b }) => {
|
'POST /api/reading-quiz/start': ({ u, b }) => {
|
||||||
try { return readingAccess.start(u.name); }
|
try { return readingAccess.start(u.name); }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
const test = require('node:test');
|
const test = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
const { parseSchedule, scheduled, answerMatches, createReadingAccess } = require('../reading-access');
|
const { parseSchedule, scheduled, publicSchedule, answerMatches, createReadingAccess } = require('../reading-access');
|
||||||
|
|
||||||
const windows = parseSchedule(JSON.stringify({
|
const windows = parseSchedule(JSON.stringify({
|
||||||
0: [['08:00', '01:00']], 1: [['07:00', '12:00'], ['13:00', '24:00']],
|
0: [['08:00', '01:00']], 1: [['07:00', '12:00'], ['13:00', '24:00']],
|
||||||
@@ -25,6 +25,15 @@ test('schedule config rejects malformed and zero-length windows', () => {
|
|||||||
assert.throws(() => parseSchedule('{"1":[["25:00","26:00"]]}', 'Europe/Berlin'));
|
assert.throws(() => parseSchedule('{"1":[["25:00","26:00"]]}', 'Europe/Berlin'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('public schedule exposes only the configured read-only windows', () => {
|
||||||
|
const view = publicSchedule(windows);
|
||||||
|
assert.equal(view.enabled, true);
|
||||||
|
assert.equal(view.timezone, 'Europe/Berlin');
|
||||||
|
assert.deepEqual(view.days[1], [['07:00', '12:00'], ['13:00', '24:00']]);
|
||||||
|
assert.deepEqual(view.days[6], [['08:00', '01:00']]);
|
||||||
|
assert.deepEqual(publicSchedule(null), { enabled: false, timezone: null, days: [] });
|
||||||
|
});
|
||||||
|
|
||||||
test('answers accept aliases and small typos but not unrelated words', () => {
|
test('answers accept aliases and small typos but not unrelated words', () => {
|
||||||
assert.equal(answerMatches(' luffy ', ['Monkey D. Luffy', 'Luffy']), true);
|
assert.equal(answerMatches(' luffy ', ['Monkey D. Luffy', 'Luffy']), true);
|
||||||
assert.equal(answerMatches('Lufy', ['Luffy']), true);
|
assert.equal(answerMatches('Lufy', ['Luffy']), true);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
const test = require('node:test');
|
const test = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
const { SETTINGS_SECTIONS, SCRAPER_TABS, settingsRoute } = require('../public/settings-nav');
|
const { SETTINGS_SECTIONS, SCRAPER_TABS, settingsRoute, readingDayText } = require('../public/settings-nav');
|
||||||
|
|
||||||
test('settings organize unique routes into User and Admin, with scraper-local tabs', () => {
|
test('settings organize unique routes into User and Admin, with scraper-local tabs', () => {
|
||||||
assert.equal(new Set(SETTINGS_SECTIONS.map(s => s.id)).size, SETTINGS_SECTIONS.length);
|
assert.equal(new Set(SETTINGS_SECTIONS.map(s => s.id)).size, SETTINGS_SECTIONS.length);
|
||||||
@@ -21,3 +21,12 @@ test('settings deep links default safely and never route ordinary readers into a
|
|||||||
assert.deepEqual(settingsRoute('#admin/scraper/sources', true), { id: 'admin/scraper', tab: 'sources' });
|
assert.deepEqual(settingsRoute('#admin/scraper/sources', true), { id: 'admin/scraper', tab: 'sources' });
|
||||||
assert.equal(settingsRoute('#admin/scraper/unknown', true).tab, 'activity');
|
assert.equal(settingsRoute('#admin/scraper/unknown', true).tab, 'activity');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('reading-hours rows include overnight carry from the previous day', () => {
|
||||||
|
const schedule = { days: [
|
||||||
|
[['08:00', '01:00']], [['07:00', '24:00']], [], [], [], [], [['08:00', '01:00']],
|
||||||
|
] };
|
||||||
|
assert.equal(readingDayText(schedule, 0), '00:00–01:00 (from Saturday) · 08:00–01:00 (next day)');
|
||||||
|
assert.equal(readingDayText(schedule, 1), '00:00–01:00 (from Sunday) · 07:00–24:00');
|
||||||
|
assert.equal(readingDayText(schedule, 2), 'Closed all day');
|
||||||
|
});
|
||||||
+10
-1
@@ -49,8 +49,10 @@ for (const [index, name] of ['Moon Garden', 'Before the Rain', 'Iron Harbor'].en
|
|||||||
}
|
}
|
||||||
const env = { ...process.env, LIBRARY_PATH: library, DATA_PATH: data, SCRAPER_DATA_PATH: path.join(data, 'scraper'), SENTINEL_DATA_PATH: path.join(data, 'sentinel'),
|
const env = { ...process.env, LIBRARY_PATH: library, DATA_PATH: data, SCRAPER_DATA_PATH: path.join(data, 'scraper'), SENTINEL_DATA_PATH: path.join(data, 'sentinel'),
|
||||||
SCRAPER_TOKEN_PATH: tokenFile, READER_PORT: port, READER_BIND: '127.0.0.1', COOKIE_SECURE: '0', APP_IMAGE: process.env.APP_IMAGE || 'mangacircus:local',
|
SCRAPER_TOKEN_PATH: tokenFile, READER_PORT: port, READER_BIND: '127.0.0.1', COOKIE_SECURE: '0', APP_IMAGE: process.env.APP_IMAGE || 'mangacircus:local',
|
||||||
|
READER_IMAGE: process.env.APP_IMAGE || 'mangacircus:local',
|
||||||
...(readingGateQa ? { READING_WINDOWS_JSON: '{"0":[],"1":[],"2":[],"3":[],"4":[],"5":[],"6":[]}',
|
...(readingGateQa ? { READING_WINDOWS_JSON: '{"0":[],"1":[],"2":[],"3":[],"4":[],"5":[],"6":[]}',
|
||||||
READING_TIMEZONE: 'Europe/Berlin', READING_QUIZ_UNLOCK_MINUTES: '60', READING_QUIZ_CATALOG_PATH: '/data/reading-quiz.json' } : {}),
|
READING_TIMEZONE: 'Europe/Berlin', READING_QUIZ_UNLOCK_MINUTES: '60', READING_QUIZ_CATALOG_PATH: '/data/reading-quiz.json' }
|
||||||
|
: { READING_WINDOWS_JSON: '', READING_QUIZ_CATALOG_PATH: '' }),
|
||||||
...(peerGatewayQa ? { PEER_DATA_PATH: path.join(data, 'peering'), PEER_USAGE_PATH: path.join(data, 'peering-usage'), PEER_PORT: process.env.QA_PEER_PORT || '8193' } : {}) };
|
...(peerGatewayQa ? { PEER_DATA_PATH: path.join(data, 'peering'), PEER_USAGE_PATH: path.join(data, 'peering-usage'), PEER_PORT: process.env.QA_PEER_PORT || '8193' } : {}) };
|
||||||
const args = ['compose', '-p', project, '-f', path.join(root, 'docker-compose.yml'), ...(peerGatewayQa ? ['--profile', 'peering'] : [])];
|
const args = ['compose', '-p', project, '-f', path.join(root, 'docker-compose.yml'), ...(peerGatewayQa ? ['--profile', 'peering'] : [])];
|
||||||
const docker = (...command) => new Promise((resolve, reject) => {
|
const docker = (...command) => new Promise((resolve, reject) => {
|
||||||
@@ -81,6 +83,13 @@ let keep = false;
|
|||||||
const signup = await fetch(url + '/api/signup', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'qa', password: 'disposable-local-test' }) });
|
const signup = await fetch(url + '/api/signup', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'qa', password: 'disposable-local-test' }) });
|
||||||
assert.equal(signup.status, 200); const cookie = signup.headers.get('set-cookie').split(';')[0];
|
assert.equal(signup.status, 200); const cookie = signup.headers.get('set-cookie').split(';')[0];
|
||||||
const api = (endpoint, body) => fetch(url + endpoint, { headers: { cookie, 'content-type': 'application/json' }, ...(body ? { method: 'POST', body: JSON.stringify(body) } : {}) });
|
const api = (endpoint, body) => fetch(url + endpoint, { headers: { cookie, 'content-type': 'application/json' }, ...(body ? { method: 'POST', body: JSON.stringify(body) } : {}) });
|
||||||
|
assert.equal((await fetch(url + '/api/reading-schedule')).status, 401);
|
||||||
|
const schedule = await (await api('/api/reading-schedule')).json();
|
||||||
|
assert.equal(schedule.enabled, readingGateQa);
|
||||||
|
if (readingGateQa) {
|
||||||
|
assert.equal(schedule.timezone, 'Europe/Berlin');
|
||||||
|
assert.deepEqual(schedule.days, Array.from({ length: 7 }, () => []));
|
||||||
|
}
|
||||||
let inventory;
|
let inventory;
|
||||||
await until(async () => { inventory = await (await api('/api/library')).json(); return inventory.mangas?.length === 3; }, 'Fixture inventory missing');
|
await until(async () => { inventory = await (await api('/api/library')).json(); return inventory.mangas?.length === 3; }, 'Fixture inventory missing');
|
||||||
const byTitle = title => inventory.mangas.find(m => m.title === title).id;
|
const byTitle = title => inventory.mangas.find(m => m.title === title).id;
|
||||||
|
|||||||
@@ -24,7 +24,22 @@ module.exports = async function testSettingsUI(url, cookie) {
|
|||||||
await page.goto(url + '/settings.html');
|
await page.goto(url + '/settings.html');
|
||||||
await page.getByRole('heading', { name: 'Reading', exact: true, level: 1 }).waitFor();
|
await page.getByRole('heading', { name: 'Reading', exact: true, level: 1 }).waitFor();
|
||||||
assert.equal(await page.locator('.settings-menu-group').count(), 2);
|
assert.equal(await page.locator('.settings-menu-group').count(), 2);
|
||||||
assert.equal(requests.filter(p => p.startsWith('/api/admin')).length, 0, 'Personal settings fetched admin data');
|
assert.equal(requests.filter(p => p.startsWith('/api/admin') && p !== '/api/admin/peering').length, 0,
|
||||||
|
'Personal settings fetched admin data beyond the top navigation peer indicator');
|
||||||
|
await clickSection('user/reading-hours', 'Reading hours');
|
||||||
|
await page.getByRole('heading', { name: 'Server reading hours' }).waitFor();
|
||||||
|
const schedule = await page.evaluate(() => fetch('/api/reading-schedule').then(r => r.json()));
|
||||||
|
if (schedule.enabled) {
|
||||||
|
await page.getByText(`Server timezone: ${schedule.timezone}`).waitFor();
|
||||||
|
assert.equal(await page.locator('.reading-hours-day').count(), 7);
|
||||||
|
} else await page.getByText('Reading is available at any time. No schedule is configured.').waitFor();
|
||||||
|
assert.equal(await page.locator('[data-settings-section="user/reading-hours"] input').count(), 0);
|
||||||
|
await page.screenshot({ path: path.join(shots, 'desktop-reading-hours.png'), fullPage: true });
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await noOverflow();
|
||||||
|
await page.screenshot({ path: path.join(shots, 'mobile-reading-hours.png'), fullPage: true });
|
||||||
|
await page.setViewportSize({ width: 1280, height: 900 });
|
||||||
|
await clickSection('user/reading', 'Reading');
|
||||||
await page.getByLabel('Skip chapter cover pages', { exact: true }).check();
|
await page.getByLabel('Skip chapter cover pages', { exact: true }).check();
|
||||||
await page.waitForFunction(async () => (await (await fetch('/api/me')).json()).settings.skipCovers === true);
|
await page.waitForFunction(async () => (await (await fetch('/api/me')).json()).settings.skipCovers === true);
|
||||||
await page.screenshot({ path: path.join(shots, 'desktop-reading.png'), fullPage: true });
|
await page.screenshot({ path: path.join(shots, 'desktop-reading.png'), fullPage: true });
|
||||||
@@ -96,6 +111,10 @@ module.exports = async function testSettingsUI(url, cookie) {
|
|||||||
await page.getByRole('heading', { name: 'Reading', exact: true, level: 1 }).waitFor();
|
await page.getByRole('heading', { name: 'Reading', exact: true, level: 1 }).waitFor();
|
||||||
assert.equal(await page.locator('.settings-menu-group').count(), 1);
|
assert.equal(await page.locator('.settings-menu-group').count(), 1);
|
||||||
assert.equal(await page.locator('.settings-menu a[href*="admin"]').count(), 0);
|
assert.equal(await page.locator('.settings-menu a[href*="admin"]').count(), 0);
|
||||||
|
await clickSection('user/reading-hours', 'Reading hours');
|
||||||
|
await page.getByRole('heading', { name: 'Server reading hours' }).waitFor();
|
||||||
|
assert.equal(await page.evaluate(() => fetch('/api/reading-schedule').then(r => r.status)), 200);
|
||||||
|
await noOverflow();
|
||||||
assert.equal(requests.slice(readerRequestStart).filter(p => p.startsWith('/api/admin')).length, 0, 'Ordinary reader fetched admin data');
|
assert.equal(requests.slice(readerRequestStart).filter(p => p.startsWith('/api/admin')).length, 0, 'Ordinary reader fetched admin data');
|
||||||
assert.equal(await page.evaluate(() => fetch('/api/admin').then(r => r.status)), 403);
|
assert.equal(await page.evaluate(() => fetch('/api/admin').then(r => r.status)), 403);
|
||||||
await noOverflow();
|
await noOverflow();
|
||||||
|
|||||||
Reference in new issue
Block a user