Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 63 additions & 2 deletions .github/workflows/frontend-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
28 changes: 19 additions & 9 deletions handleMessage.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,46 @@
'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,
},
},
});
};

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
Expand Down
Loading