#!/usr/bin/env python3 """Minimal CDP client: log in to the controller through its PUBLIC address in headless Chrome, open each app-page tab, assert computed styles, collect console errors, save screenshots. The password is read from a 0600 file and only ever sent inside a CDP message; it is never printed.""" import base64, json, os, socket, struct, subprocess, sys, time, urllib.request CHROME = os.path.expanduser('~/.cache/ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell') BASE = sys.argv[1] # https://felhom.enkisfelhom.hu SLUG = sys.argv[2] # calibre-web PWFILE = sys.argv[3] OUT = sys.argv[4] PORT = 9333 os.makedirs(OUT, exist_ok=True) prof = os.path.join(OUT, '.profile') proc = subprocess.Popen([CHROME, f'--remote-debugging-port={PORT}', '--no-sandbox', f'--user-data-dir={prof}', '--window-size=1280,900', 'about:blank'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) try: for _ in range(50): try: tabs = json.load(urllib.request.urlopen(f'http://127.0.0.1:{PORT}/json')) break except Exception: time.sleep(0.2) ws_url = [t for t in tabs if t['type'] == 'page'][0]['webSocketDebuggerUrl'] host, path = ws_url[len('ws://'):].split('/', 1) h, p = host.split(':') sock = socket.create_connection((h, int(p))) key = base64.b64encode(os.urandom(16)).decode() sock.send(f'GET /{path} HTTP/1.1\r\nHost: {host}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n'.encode()) buf = b'' while b'\r\n\r\n' not in buf: buf += sock.recv(4096) st = {'pending': buf.split(b'\r\n\r\n', 1)[1]} def send(obj): data = json.dumps(obj).encode() hdr = bytearray([0x81]) n = len(data) if n < 126: hdr.append(0x80 | n) elif n < 65536: hdr.append(0x80 | 126); hdr += struct.pack('>H', n) else: hdr.append(0x80 | 127); hdr += struct.pack('>Q', n) mask = os.urandom(4); hdr += mask sock.sendall(bytes(hdr) + bytes(b ^ mask[i % 4] for i, b in enumerate(data))) def recv_exact(n): while len(st['pending']) < n: st['pending'] += sock.recv(1 << 20) out, st['pending'] = st['pending'][:n], st['pending'][n:] return out def recv(): msg = b'' while True: b0, b1 = recv_exact(2) n = b1 & 0x7f if n == 126: n = struct.unpack('>H', recv_exact(2))[0] elif n == 127: n = struct.unpack('>Q', recv_exact(8))[0] msg += recv_exact(n) if b0 & 0x80: return json.loads(msg) console = [] mid = [0] def call(method, **params): mid[0] += 1; i = mid[0] send({'id': i, 'method': method, 'params': params}) while True: m = recv() if m.get('method') == 'Runtime.exceptionThrown': console.append(('exception', m['params']['exceptionDetails'].get('text', ''), m['params']['exceptionDetails'].get('exception', {}).get('description', '')[:200])) elif m.get('method') == 'Runtime.consoleAPICalled' and m['params']['type'] in ('error', 'warning'): console.append((m['params']['type'], ' '.join(str(a.get('value', a.get('description', ''))) for a in m['params']['args'])[:200])) elif m.get('method') == 'Log.entryAdded' and m['params']['entry']['level'] in ('error', 'warning'): e = m['params']['entry']; console.append(('log-' + e['level'], e.get('text', '')[:200], e.get('url', ''))) if m.get('id') == i: if 'error' in m: raise RuntimeError(f'{method}: {m["error"]}') return m.get('result', {}) def ev(expr): r = call('Runtime.evaluate', expression=expr, returnByValue=True, awaitPromise=True) return r.get('result', {}).get('value') def goto(url): call('Page.navigate', url=url) for _ in range(100): time.sleep(0.2) if ev('document.readyState') == 'complete': break time.sleep(1.5) # images' onerror, layout for d in ('Page', 'Runtime', 'Log', 'Network'): call(d + '.enable') pw = open(PWFILE).read().strip() goto(BASE + '/login') ev("(function(){var f=document.querySelector('input[type=password]'); f.value=%s; f.form.submit(); return 1})()" % json.dumps(pw)) del pw time.sleep(3) print('after login url:', ev('location.pathname')) console.clear() # the login page is not under test report = {} for tab in ['install']: n0 = len(console) goto(f'{BASE}/apps/{SLUG}/{tab}') res = ev('''(function(){ function cs(el, props){ if(!el) return null; var s=getComputedStyle(el), o={}; props.forEach(function(p){o[p]=s.getPropertyValue(p)}); return o; } var secs=[].slice.call(document.querySelectorAll('.app-section, .deploy-form .form-section')).filter(function(e){return e.offsetParent!==null}); var col=document.querySelector('.app-page'); var t=document.querySelector('.deploy-form .form-section h4'); var first=secs[0]; var gap = (t && first) ? Math.round(t.getBoundingClientRect().top - t.closest('.app-section, .form-section').getBoundingClientRect().top) : null; var widths = secs.map(function(e){return Math.round(e.getBoundingClientRect().width)}); var spacing = []; for (var i=1;i