Initial Wave of Updates
This commit is contained in:
parent
7e9406a8ae
commit
53b8f497cb
|
|
@ -19,7 +19,8 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"alpinejs": "^3.14.1",
|
"alpinejs": "^3.14.1",
|
||||||
"beercss": "^3.7.0",
|
"beercss": "^3.7.0",
|
||||||
"material-dynamic-colors": "^1.1.2"
|
"material-dynamic-colors": "^1.1.2",
|
||||||
|
"playwright": "^1.61.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@material/material-color-utilities": {
|
"node_modules/@material/material-color-utilities": {
|
||||||
|
|
@ -577,6 +578,21 @@
|
||||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
|
@ -949,6 +965,38 @@
|
||||||
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.61.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz",
|
||||||
|
"integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.61.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.61.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz",
|
||||||
|
"integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prebuild-install": {
|
"node_modules/prebuild-install": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"alpinejs": "^3.14.1",
|
"alpinejs": "^3.14.1",
|
||||||
"beercss": "^3.7.0",
|
"beercss": "^3.7.0",
|
||||||
"material-dynamic-colors": "^1.1.2"
|
"material-dynamic-colors": "^1.1.2",
|
||||||
|
"playwright": "^1.61.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,11 @@
|
||||||
<link rel="stylesheet" href="/vendor/beer.min.css" />
|
<link rel="stylesheet" href="/vendor/beer.min.css" />
|
||||||
<link rel="stylesheet" href="/css/app.css" />
|
<link rel="stylesheet" href="/css/app.css" />
|
||||||
<script src="/vendor/material-dynamic-colors.min.js"></script>
|
<script src="/vendor/material-dynamic-colors.min.js"></script>
|
||||||
<script src="/vendor/beer.min.js"></script>
|
<!-- Beer CSS is an ES module that only exports `ui`; expose it as the global our code calls. -->
|
||||||
|
<script type="module">
|
||||||
|
import { ui } from '/vendor/beer.min.js';
|
||||||
|
window.ui = ui;
|
||||||
|
</script>
|
||||||
<script src="/js/api.js"></script>
|
<script src="/js/api.js"></script>
|
||||||
<script src="/js/theme.js"></script>
|
<script src="/js/theme.js"></script>
|
||||||
<script src="/js/admin.js" defer></script>
|
<script src="/js/admin.js" defer></script>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,11 @@
|
||||||
<link rel="stylesheet" href="/vendor/beer.min.css" />
|
<link rel="stylesheet" href="/vendor/beer.min.css" />
|
||||||
<link rel="stylesheet" href="/css/app.css" />
|
<link rel="stylesheet" href="/css/app.css" />
|
||||||
<script src="/vendor/material-dynamic-colors.min.js"></script>
|
<script src="/vendor/material-dynamic-colors.min.js"></script>
|
||||||
<script src="/vendor/beer.min.js"></script>
|
<!-- Beer CSS is an ES module that only exports `ui`; expose it as the global our code calls. -->
|
||||||
|
<script type="module">
|
||||||
|
import { ui } from '/vendor/beer.min.js';
|
||||||
|
window.ui = ui;
|
||||||
|
</script>
|
||||||
<script src="/js/api.js"></script>
|
<script src="/js/api.js"></script>
|
||||||
<script src="/js/theme.js"></script>
|
<script src="/js/theme.js"></script>
|
||||||
<script src="/js/app.js" defer></script>
|
<script src="/js/app.js" defer></script>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,14 @@ window.API = (function () {
|
||||||
try { data = JSON.parse(text); } catch (_) { data = { raw: text }; }
|
try { data = JSON.parse(text); } catch (_) { data = { raw: text }; }
|
||||||
}
|
}
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
// An expired/invalid session (401) on any call other than the login POST
|
||||||
|
// means we should send the user back to sign in, rather than leaving the
|
||||||
|
// page in a broken half-loaded state with only a console error.
|
||||||
|
const onLoginPage = window.location.pathname.endsWith('/login.html');
|
||||||
|
const isLoginCall = url.endsWith('/api/login');
|
||||||
|
if (res.status === 401 && !onLoginPage && !isLoginCall) {
|
||||||
|
window.location.href = '/login.html';
|
||||||
|
}
|
||||||
const err = new Error((data && data.error) || `Request failed (${res.status})`);
|
const err = new Error((data && data.error) || `Request failed (${res.status})`);
|
||||||
err.status = res.status;
|
err.status = res.status;
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,11 @@
|
||||||
<link rel="stylesheet" href="/vendor/beer.min.css" />
|
<link rel="stylesheet" href="/vendor/beer.min.css" />
|
||||||
<link rel="stylesheet" href="/css/app.css" />
|
<link rel="stylesheet" href="/css/app.css" />
|
||||||
<script src="/vendor/material-dynamic-colors.min.js"></script>
|
<script src="/vendor/material-dynamic-colors.min.js"></script>
|
||||||
<script src="/vendor/beer.min.js"></script>
|
<!-- Beer CSS is an ES module that only exports `ui`; expose it as the global our code calls. -->
|
||||||
|
<script type="module">
|
||||||
|
import { ui } from '/vendor/beer.min.js';
|
||||||
|
window.ui = ui;
|
||||||
|
</script>
|
||||||
<script src="/js/api.js"></script>
|
<script src="/js/api.js"></script>
|
||||||
<script src="/js/theme.js"></script>
|
<script src="/js/theme.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@ const OUT = path.join(ROOT, 'public', 'vendor');
|
||||||
const FILES = [
|
const FILES = [
|
||||||
['beercss/dist/cdn/beer.min.css', 'beer.min.css'],
|
['beercss/dist/cdn/beer.min.css', 'beer.min.css'],
|
||||||
['beercss/dist/cdn/beer.min.js', 'beer.min.js'],
|
['beercss/dist/cdn/beer.min.js', 'beer.min.js'],
|
||||||
|
// Material Symbols icon fonts. beer.min.css references these by relative URL
|
||||||
|
// (with a CDN fallback our CSP blocks), so they must sit beside it in /vendor.
|
||||||
|
['beercss/dist/cdn/material-symbols-outlined.woff2', 'material-symbols-outlined.woff2'],
|
||||||
|
['beercss/dist/cdn/material-symbols-rounded.woff2', 'material-symbols-rounded.woff2'],
|
||||||
|
['beercss/dist/cdn/material-symbols-sharp.woff2', 'material-symbols-sharp.woff2'],
|
||||||
|
['beercss/dist/cdn/material-symbols-subset.woff2', 'material-symbols-subset.woff2'],
|
||||||
['material-dynamic-colors/dist/cdn/material-dynamic-colors.min.js', 'material-dynamic-colors.min.js'],
|
['material-dynamic-colors/dist/cdn/material-dynamic-colors.min.js', 'material-dynamic-colors.min.js'],
|
||||||
['alpinejs/dist/cdn.min.js', 'alpine.min.js'],
|
['alpinejs/dist/cdn.min.js', 'alpine.min.js'],
|
||||||
];
|
];
|
||||||
|
|
@ -29,5 +35,35 @@ for (const [src, dest] of FILES) {
|
||||||
console.error(`FAILED to vendor ${src}: ${e.message}`);
|
console.error(`FAILED to vendor ${src}: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Strip Beer CSS's remote @font-face fallbacks (jsdelivr CDN) so the icon font
|
||||||
|
// resolves only to our local /vendor copy — no CDN load, no CSP violation.
|
||||||
|
const beerCss = path.join(OUT, 'beer.min.css');
|
||||||
|
try {
|
||||||
|
const before = fs.readFileSync(beerCss, 'utf8');
|
||||||
|
const after = before.replace(/,url\(https:\/\/cdn\.jsdelivr\.net[^)]*\)\s*format\("woff2"\)/g, '');
|
||||||
|
if (after !== before) {
|
||||||
|
fs.writeFileSync(beerCss, after);
|
||||||
|
console.log('stripped remote CDN font fallbacks from beer.min.css');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`WARN: could not post-process beer.min.css: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// material-dynamic-colors ships as an ES module: it sets the
|
||||||
|
// globalThis.materialDynamicColors global (which Beer CSS's ui("theme") needs)
|
||||||
|
// but ALSO ends with `export default ...`, which breaks classic <script> loading
|
||||||
|
// ("Unexpected token 'export'"). Strip the export so it loads as a plain script.
|
||||||
|
const mdc = path.join(OUT, 'material-dynamic-colors.min.js');
|
||||||
|
try {
|
||||||
|
const before = fs.readFileSync(mdc, 'utf8');
|
||||||
|
const after = before.replace(/\s*export\s+default[^;]*;\s*$/, '\n');
|
||||||
|
if (after !== before) {
|
||||||
|
fs.writeFileSync(mdc, after);
|
||||||
|
console.log('stripped ES export from material-dynamic-colors.min.js');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`WARN: could not post-process material-dynamic-colors.min.js: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`\n${ok}/${FILES.length} files vendored into public/vendor`);
|
console.log(`\n${ok}/${FILES.length} files vendored into public/vendor`);
|
||||||
if (ok < FILES.length) process.exitCode = 1;
|
if (ok < FILES.length) process.exitCode = 1;
|
||||||
|
|
|
||||||
29
server.js
29
server.js
|
|
@ -23,7 +23,10 @@ app.use(
|
||||||
contentSecurityPolicy: {
|
contentSecurityPolicy: {
|
||||||
directives: {
|
directives: {
|
||||||
defaultSrc: ["'self'"],
|
defaultSrc: ["'self'"],
|
||||||
scriptSrc: ["'self'", "'unsafe-inline'"],
|
// Alpine.js compiles its x-/@/: expressions with new Function(), which
|
||||||
|
// requires 'unsafe-eval'. All scripts are self-hosted/vendored (no
|
||||||
|
// remote or user-injected scripts), so the added risk is minimal.
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
|
||||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||||
imgSrc: ["'self'", 'data:'],
|
imgSrc: ["'self'", 'data:'],
|
||||||
fontSrc: ["'self'", 'data:'],
|
fontSrc: ["'self'", 'data:'],
|
||||||
|
|
@ -50,6 +53,14 @@ app.use(
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Never let the browser cache API responses. Without this, a GET like /api/me
|
||||||
|
// can be served stale from cache (e.g. a 200 from a prior session) while other
|
||||||
|
// calls hit the network and 401 on an expired session — confusing and buggy.
|
||||||
|
app.use('/api', (req, res, next) => {
|
||||||
|
res.set('Cache-Control', 'no-store');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
app.use('/api', require('./src/routes/auth.routes'));
|
app.use('/api', require('./src/routes/auth.routes'));
|
||||||
app.use('/api/users', require('./src/routes/users.routes'));
|
app.use('/api/users', require('./src/routes/users.routes'));
|
||||||
|
|
@ -59,8 +70,20 @@ app.use('/api/projects', require('./src/routes/projects.routes'));
|
||||||
app.use('/api/kpis', require('./src/routes/kpis.routes'));
|
app.use('/api/kpis', require('./src/routes/kpis.routes'));
|
||||||
app.use('/api/entries', require('./src/routes/entries.routes'));
|
app.use('/api/entries', require('./src/routes/entries.routes'));
|
||||||
|
|
||||||
// Static frontend
|
// Silence the browser's automatic /favicon.ico request (otherwise a 404 shows
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
// up in the console as a scary-looking "Failed to load resource").
|
||||||
|
app.get('/favicon.ico', (req, res) => res.status(204).end());
|
||||||
|
|
||||||
|
// Static frontend. Force revalidation of HTML/JS so the browser never runs a
|
||||||
|
// stale cached build after an update (a frequent source of "I fixed it but it
|
||||||
|
// still breaks" confusion). Fonts/CSS may still be cached via ETag/Last-Modified.
|
||||||
|
app.use(
|
||||||
|
express.static(path.join(__dirname, 'public'), {
|
||||||
|
setHeaders(res, filePath) {
|
||||||
|
if (/\.(html|js)$/i.test(filePath)) res.set('Cache-Control', 'no-cache');
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// JSON 404 for unmatched API routes
|
// JSON 404 for unmatched API routes
|
||||||
app.use('/api', (req, res) => res.status(404).json({ error: 'Not found' }));
|
app.use('/api', (req, res) => res.status(404).json({ error: 'Not found' }));
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,10 @@ const countSchemes = db.prepare('SELECT COUNT(*) AS n FROM color_schemes');
|
||||||
|
|
||||||
const HEX = /^#[0-9a-fA-F]{6}$/;
|
const HEX = /^#[0-9a-fA-F]{6}$/;
|
||||||
|
|
||||||
// GET /api/schemes is readable by any logged-in user (the picker needs it);
|
// GET /api/schemes is public: schemes are just non-sensitive color definitions,
|
||||||
// writes are admin-only.
|
// and the login page themes itself from them before anyone authenticates.
|
||||||
router.get('/', requireAuth, (req, res) => res.json({ schemes: listSchemes.all() }));
|
// Writes (below) are admin-only.
|
||||||
|
router.get('/', (req, res) => res.json({ schemes: listSchemes.all() }));
|
||||||
|
|
||||||
router.use(requireAuth, requireAdmin);
|
router.use(requireAuth, requireAdmin);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Drives the app in a real browser (Edge) and reports console errors, page
|
||||||
|
// errors, and all network responses so we can see exactly what the browser
|
||||||
|
// experiences that curl-style tests don't.
|
||||||
|
const { chromium } = require('playwright');
|
||||||
|
|
||||||
|
const BASE = 'http://localhost:3000';
|
||||||
|
const USER = process.env.ADMIN_USERNAME || 'admin';
|
||||||
|
const PASS = process.env.ADMIN_PASSWORD || 'BowlerAdmin!2026';
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const browser = await chromium.launch({ channel: 'msedge', headless: true });
|
||||||
|
const context = await browser.newContext();
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
const log = [];
|
||||||
|
page.on('console', (m) => log.push(` [console.${m.type()}] ${m.text()}`));
|
||||||
|
page.on('pageerror', (e) => log.push(` [pageerror] ${e.message}`));
|
||||||
|
page.on('requestfailed', (r) =>
|
||||||
|
log.push(` [requestfailed] ${r.method()} ${r.url()} :: ${r.failure() && r.failure().errorText}`)
|
||||||
|
);
|
||||||
|
page.on('response', (r) => {
|
||||||
|
const u = r.url();
|
||||||
|
if (u.includes('/api/')) log.push(` [response] ${r.status()} ${r.request().method()} ${u}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
function section(t) { log.push(`\n=== ${t} ===`); }
|
||||||
|
|
||||||
|
section('1) GET / (fresh, no cookies)');
|
||||||
|
await page.goto(`${BASE}/`, { waitUntil: 'networkidle' });
|
||||||
|
log.push(` landed on: ${page.url()}`);
|
||||||
|
|
||||||
|
section('2) Go to /login.html and sign in');
|
||||||
|
if (!page.url().includes('login.html')) await page.goto(`${BASE}/login.html`, { waitUntil: 'networkidle' });
|
||||||
|
await page.fill('#username', USER);
|
||||||
|
await page.fill('#password', PASS);
|
||||||
|
await page.click('#login-form button[type="submit"]');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
log.push(` after login, url: ${page.url()}`);
|
||||||
|
|
||||||
|
// Inspect cookies the browser is holding.
|
||||||
|
const cookies = await context.cookies();
|
||||||
|
log.push(` cookies: ${cookies.map((c) => `${c.name}(sameSite=${c.sameSite},secure=${c.secure},httpOnly=${c.httpOnly})`).join(', ') || 'NONE'}`);
|
||||||
|
|
||||||
|
section('3) On index — wait for chart fetch');
|
||||||
|
await page.waitForTimeout(1200);
|
||||||
|
log.push(` url: ${page.url()}`);
|
||||||
|
|
||||||
|
section('4) Click Admin button');
|
||||||
|
const adminBtn = page.locator('button[title="Admin"]');
|
||||||
|
if (await adminBtn.count()) {
|
||||||
|
await adminBtn.click();
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
log.push(` url after admin click: ${page.url()}`);
|
||||||
|
// Did users/schemes render?
|
||||||
|
const userRows = await page.locator('table tbody tr').count().catch(() => -1);
|
||||||
|
log.push(` admin table rows visible: ${userRows}`);
|
||||||
|
} else {
|
||||||
|
log.push(' Admin button NOT found (not admin, or page not loaded)');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(log.join('\n'));
|
||||||
|
await browser.close();
|
||||||
|
})().catch((e) => {
|
||||||
|
console.error('SCRIPT ERROR:', e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Reproduces a user with a STALE/invalid session cookie and confirms the app
|
||||||
|
// now redirects to login gracefully instead of dead-ending on 401 errors.
|
||||||
|
const { chromium } = require('playwright');
|
||||||
|
const BASE = 'http://localhost:3000';
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const browser = await chromium.launch({ channel: 'msedge', headless: true });
|
||||||
|
const log = [];
|
||||||
|
|
||||||
|
async function scenario(name, urlPath, setBadCookie) {
|
||||||
|
const context = await browser.newContext();
|
||||||
|
if (setBadCookie) {
|
||||||
|
await context.addCookies([
|
||||||
|
{ name: 'connect.sid', value: 's%3Abogus-stale-session-id.signature', url: BASE },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
const page = await context.newPage();
|
||||||
|
const errs = [];
|
||||||
|
page.on('console', (m) => { if (m.type() === 'error') errs.push(m.text()); });
|
||||||
|
page.on('pageerror', (e) => errs.push('pageerror: ' + e.message));
|
||||||
|
await page.goto(`${BASE}${urlPath}`, { waitUntil: 'networkidle' });
|
||||||
|
await page.waitForTimeout(800);
|
||||||
|
log.push(`\n=== ${name} ===`);
|
||||||
|
log.push(` start: ${urlPath} (badCookie=${!!setBadCookie})`);
|
||||||
|
log.push(` ended on: ${page.url()}`);
|
||||||
|
log.push(` console errors: ${errs.length ? errs.join(' | ') : 'none'}`);
|
||||||
|
await context.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
await scenario('Stale cookie visits /', '/', true);
|
||||||
|
await scenario('Stale cookie opens /admin.html directly', '/admin.html', true);
|
||||||
|
await scenario('No session opens /admin.html directly', '/admin.html', false);
|
||||||
|
|
||||||
|
console.log(log.join('\n'));
|
||||||
|
await browser.close();
|
||||||
|
})().catch((e) => { console.error('SCRIPT ERROR:', e); process.exit(1); });
|
||||||
Loading…
Reference in New Issue