turbo_e2e.js (5813B)
1 // End-to-end check of instant folder navigation with the REAL Turbo and the REAL server-rendered pages. 2 // Not run directly: tests/test_fast_tree.py writes fixtures to a directory and calls 3 // node tests/turbo_e2e.js <fixtures-dir> 4 // Needs jsdom and @hotwired/turbo@8.0.4 (CI installs both; locally: npm i --no-save --no-package-lock jsdom @hotwired/turbo@8.0.4). 5 const fs = require('node:fs'); 6 const path = require('node:path'); 7 const { JSDOM, VirtualConsole } = require('jsdom'); 8 const { boot } = require('../app/static/fasttree.js'); 9 10 const dir = process.argv[2]; 11 const read = (f) => fs.readFileSync(path.join(dir, f), 'utf8'); 12 const tree = JSON.parse(read('tree.json')); 13 const shell = JSON.parse(read('shell.json')); 14 const turboSrc = fs.readFileSync(require.resolve('@hotwired/turbo/dist/turbo.es2017-umd.js'), 'utf8'); 15 16 const failures = []; 17 const check = (cond, msg) => { if (!cond) failures.push(msg); console.log((cond ? 'ok - ' : 'FAIL - ') + msg); }; 18 const norm = (s) => s.replace(/\s+/g, ' ').trim(); 19 // The parts of a listing page that depend on the folder. (Whole-page equality is checked elsewhere.) 20 const region = (doc) => ['.breadcrumb-container', '.listing-toolbar', '#file-list', 'footer'] 21 .map((sel) => { const el = doc.querySelector(sel); return norm(el ? el.outerHTML : '(none)'); }).join('\n'); 22 const waitFor = async (cond, ms = 4000) => { const end = Date.now() + ms; while (Date.now() < end) { if (cond()) return true; await new Promise((r) => setTimeout(r, 10)); } return cond(); }; 23 const parse = (html) => new JSDOM(html).window.document; 24 25 (async () => { 26 // The page the user is on: the server's own rendering of /bsc (external scripts are not loaded in jsdom). 27 const stripExternal = (h) => h.replace(/<script[^>]*\bsrc="[^"]*"[^>]*><\/script>/g, ''); 28 const dom = new JSDOM(stripExternal(read('from.html')), { 29 url: 'https://cdn.example.com/bsc', runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: new VirtualConsole(), 30 }); 31 const win = dom.window; 32 win.eval(turboSrc); // the real Turbo (what the CDN <script type=module> provides) 33 check(typeof win.Turbo.visit === 'function', 'real Turbo 8.0.4 is loaded'); 34 win.localStorage.setItem('cdn_fast_v1', JSON.stringify({ tree: { v: tree.v, files: tree.files }, shell })); 35 let networkCalls = 0; 36 const requested = []; 37 win.fetch = (u, o) => { 38 const url = String(u && u.url || u); 39 // the tree/shell revalidating in the background is expected; a page request is not 40 if (url !== '/api/tree' && url !== '/api/shell') { networkCalls++; requested.push(`${url} (during step ${step})`); } return Promise.reject(new Error('the test forbids network requests')); }; 41 let step = 'start'; 42 const origPush = win.history.pushState.bind(win.history); 43 const pushed = []; 44 win.history.pushState = (...a) => { pushed.push(a[2]); return origPush(...a); }; 45 boot(win); 46 47 step = 'click'; 48 const link = win.document.querySelector('#file-list a[href="/bsc/semester_1"]'); 49 check(!!link, 'the page has a link to the sub-folder'); 50 await waitFor(() => win.document.readyState === 'complete'); 51 await new Promise((r) => setTimeout(r, 100)); // let Turbo's own start-up settle 52 link.dispatchEvent(new win.MouseEvent('click', { bubbles: true, cancelable: true, button: 0 })); 53 await waitFor(() => win.location.pathname === '/bsc/semester_1' && win.document.querySelector('a[href="/bsc/semester_1/deep"]')); 54 55 check(win.location.pathname === '/bsc/semester_1', `the address changed to /bsc/semester_1 (is ${win.location.pathname})`); 56 check(pushed.length === 1, 'Turbo added a history entry'); 57 check(networkCalls === 0, 'no page was requested from the server for the navigation'); 58 const got = region(win.document); 59 const want = region(parse(read('to.html'))); 60 check(got === want, 'the page now on screen equals the server-rendered page for that folder'); 61 if (got !== want) { 62 const i = [...got].findIndex((c, k) => c !== want[k]); 63 console.log(' got : ...' + got.slice(Math.max(0, i - 80), i + 120) + '\n want: ...' + want.slice(Math.max(0, i - 80), i + 120)); 64 } 65 const visited = JSON.parse(win.localStorage.getItem('cdn_visited_v1') || '[]'); 66 check(visited.some((e) => e.type === 'folder' && e.path === 'bsc/semester_1'), "the page's own script recorded the visit in History"); 67 68 // Back to the page we came from (Turbo restores it), then forward again. 69 step = 'back'; 70 win.history.back(); 71 await waitFor(() => win.location.pathname === '/bsc' && win.document.querySelector('#file-list a[href="/bsc/semester_1"]')); 72 check(win.location.pathname === '/bsc', 'Back returns to /bsc'); 73 check(win.document.querySelector('#file-list a[href="/bsc/semester_1"]') !== null, '...and shows that folder\'s listing'); 74 75 // Going to the root from a deep page: header appears, breadcrumb disappears. 76 step = 'root'; 77 win.document.querySelector('.wordmark').dispatchEvent(new win.MouseEvent('click', { bubbles: true, cancelable: true, button: 0 })); 78 await waitFor(() => win.location.pathname === '/' && win.document.querySelector('h1')); 79 check(win.location.pathname === '/', 'the logo link goes to the root locally'); 80 check(win.document.querySelector('h1') !== null && win.document.querySelector('.breadcrumb-container') === null, 'root shows its header and no breadcrumb'); 81 check(region(win.document) === region(parse(read('root.html'))), 'the root page equals the server-rendered root page'); 82 check(networkCalls === 0, 'still no page requests (Back, logo link)' + (requested.length ? ': ' + requested.join(', ') : '')); 83 84 console.log(failures.length ? `\n${failures.length} FAILURE(S)` : '\nend-to-end navigation works'); 85 process.exit(failures.length ? 1 : 0); 86 })().catch((e) => { console.error('FAIL - exception:', e); process.exit(1); });