diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 5e97e61..0070b45 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -56,7 +56,7 @@ jobs: working-directory: ./etherpad-lite shell: bash run: | - pnpm run dev & + pnpm run dev > /tmp/devserver.log 2>&1 & connected=false can_connect() { curl -sSfo /dev/null http://localhost:9001/ || return 1 @@ -67,6 +67,67 @@ jobs: while [ $(($(now) - $start)) -le 30 ] && ! can_connect; do sleep 1 done + # DIAGNOSTIC: directly load a pad page server-side, capture status + sleep 15 + echo "===== curl /p/diagpad (status) =====" + curl -s -o /tmp/padpage.html -w "HTTP %{http_code} size=%{size_download}\n" "http://localhost:9001/p/diagpad12345" + echo "--- pad page entrypoint script tag ---" + grep -oE '/watch/pad[^"]*' /tmp/padpage.html | head -1 + echo "===== curl /watch/pad =====" + curl -s "http://localhost:9001/watch/pad?hash=x" -o /tmp/watchpad.js -w "HTTP %{http_code} size=%{size_download}\n" + echo "===== server log: routeHandlers / errors so far =====" + grep -nE "routeHandlers|is not a function|\[ERROR\]" /tmp/devserver.log | grep -viE "update check|fetch failed" | head -30 || true cd src pnpm exec playwright install chromium --with-deps - pnpm run test-ui --project=chromium + # DIAGNOSTIC: instrumented pad load to find the silent client hang + cat > /tmp/probe.cjs <<'PROBE' + const { chromium } = require(process.cwd() + '/node_modules/@playwright/test'); + (async () => { + const browser = await chromium.launch(); + const page = await (await browser.newContext()).newPage(); + page.on('console', m => console.log('CONSOLE['+m.type()+']', m.text())); + page.on('pageerror', e => console.log('PAGEERROR', e.message, '\n', e.stack)); + await page.addInitScript(() => { + window.addEventListener('error', e => console.log('WINERROR:', e.message, (e.error&&e.error.stack)||'')); + window.addEventListener('unhandledrejection', e => { + const r = e.reason; console.log('UNHANDLEDREJECTION:', (r&&(r.stack||r.message))||String(r)); + }); + }); + await page.goto('http://localhost:9001/p/diagprobe'+Date.now(), {waitUntil:'domcontentloaded', timeout:30000}).catch(e=>console.log('goto:'+e.message)); + await page.waitForTimeout(35000); + const state = await page.evaluate(() => ({ + aceOuter: !!document.querySelector('iframe[name=ace_outer]'), + iframeCount: document.querySelectorAll('iframe').length, + editorContainer: document.querySelector('#editorcontainer') && document.querySelector('#editorcontainer').className, + loadingVisible: !!(document.querySelector('#editorloadingbox') && document.querySelector('#editorloadingbox').offsetParent), + receivedClientVars: !!window.clientVars, + padPresent: typeof window.pad, + padeditor: typeof window.padeditor, + padeditorAce: !!(window.padeditor && window.padeditor.ace), + winKeys: Object.keys(window).filter(k=>/pad|ace|client|collab|handshake/i.test(k)).slice(0,40), + connMsg: (document.querySelector('#connectivity, #editorloadingbox .editorloadingbox-message:not([hidden])') || {}).innerText, + bodyModal: (document.querySelector('.modaldialog, #connectivity .visible, [id*=error]') || {}).innerText, + })).catch(e=>'eval:'+e.message); + console.log('=== FINAL STATE ===', JSON.stringify(state)); + await browser.close(); + })(); + PROBE + echo "===== INSTRUMENTED PROBE OUTPUT =====" + node /tmp/probe.cjs 2>&1 | grep -vE "Could not find string" | head -120 || true + echo "===== END PROBE =====" + - name: Stage diagnostic logs + if: always() + working-directory: ./etherpad-lite + run: cp /tmp/devserver.log ./devserver.log || true + - name: Upload Playwright traces (diagnostic) + if: always() + uses: actions/upload-artifact@v4 + with: + name: pw-traces + path: | + etherpad-lite/src/test-results/** + etherpad-lite/src/playwright-report/** + etherpad-lite/devserver.log + + if-no-files-found: warn + retention-days: 3 diff --git a/handleMessage.js b/handleMessage.js index e273361..15a72eb 100644 --- a/handleMessage.js +++ b/handleMessage.js @@ -1,22 +1,29 @@ 'use strict'; -const wordnet = require('wordnet'); - +// `wordnet` is a server-only package (it reads the WordNet database off disk). +// It MUST NOT be required at module top level: Etherpad's client bundler pulls +// plugin hook modules into the browser bundle, and evaluating wordnet's module +// body in the browser silently hangs the editor bootstrap (ace_outer is never +// created, no error is thrown). Requiring it lazily inside the hook keeps the +// module body from ever running client-side while still working on the server. +let wordnet = null; let initPromise = null; const ensureInit = () => { + if (!wordnet) wordnet = require('wordnet'); if (!initPromise) initPromise = wordnet.init(); return initPromise; }; const sendDefinition = (context, definitions) => { - context.client.json.send({ + const payload = context.message.data.payload; + context.socket.emit('message', { type: 'COLLABROOM', data: { type: 'CUSTOM', payload: { action: 'recieveDefineMessage', - authorId: context.message.data.message.myAuthorId, - padId: context.message.data.message.padId, + authorId: payload.myAuthorId, + padId: payload.padId, message: definitions, }, }, @@ -24,13 +31,16 @@ const sendDefinition = (context, definitions) => { }; exports.handleMessage = async (hookName, context) => { - if (!(context && context.message && context.message.data && - context.message.data.type && context.message.data.action === 'sendDefineMessage')) { - return [null]; + const payload = context?.message?.data?.payload; + if (!(context?.message?.type === 'COLLABROOM' && + context.message.data?.type === 'CLIENT_MESSAGE' && + payload?.type === 'define' && + payload?.action === 'sendDefineMessage')) { + return; } try { await ensureInit(); - const definitions = await wordnet.lookup(context.message.data.message, true); + const definitions = await wordnet.lookup(payload.message, true); sendDefinition(context, definitions); } catch (err) { // wordnet 2.x rejects when the word isn't found; surface a "no