"""Firefox regression: python3 tests/venue/move_feedback.py /path/to/frontend/dist.""" import json import mimetypes import pathlib import socket import subprocess import sys import tempfile import threading import time from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import urlparse INJECT = r"""""" def main(dist): class Handler(BaseHTTPRequestHandler): def log_message(self, *args): pass def do_GET(self): path = dist / urlparse(self.path).path.lstrip('/') if path.is_file(): body = path.read_bytes() mime = mimetypes.guess_type(str(path))[0] or 'application/octet-stream' else: body = (dist / 'index.html').read_text().replace('', '' + INJECT).encode() mime = 'text/html' self.send_response(200) self.send_header('Content-Type', mime) self.end_headers() self.wfile.write(body) server = ThreadingHTTPServer(('127.0.0.1', 0), Handler) threading.Thread(target=server.serve_forever, daemon=True).start() with tempfile.TemporaryDirectory(prefix='move-feedback-') as directory: profile = pathlib.Path(directory) with socket.socket() as reservation: reservation.bind(('127.0.0.1', 0)) port = reservation.getsockname()[1] (profile / 'user.js').write_text(f'user_pref("marionette.port", {port});\n') with (profile / 'firefox.log').open('w') as log: firefox = subprocess.Popen(['firefox', '--headless', '--no-remote', '--profile', directory, '--marionette', 'about:blank'], stdout=log, stderr=log) sock = None try: deadline = time.monotonic() + 15 while sock is None and time.monotonic() < deadline: try: sock = socket.create_connection(('127.0.0.1', port), timeout=1) except OSError: time.sleep(.1) assert sock is not None, 'Firefox did not start: ' + (profile / 'firefox.log').read_text() sock.settimeout(30) def read_message(): size = b'' while not size.endswith(b':'): part = sock.recv(1) assert part, 'Firefox disconnected' size += part remaining, data = int(size[:-1]), b'' while remaining: part = sock.recv(remaining) assert part, 'Firefox disconnected' data += part remaining -= len(part) return json.loads(data) read_message() command_id = 0 def command(name, args=None): nonlocal command_id command_id += 1 payload = json.dumps([0, command_id, name, args or {}]).encode() sock.sendall(str(len(payload)).encode() + b':' + payload) result = read_message() assert not result[2], result[2] return result[3] def js(script): result = command('WebDriver:ExecuteScript', {'script': script, 'args': [], 'newSandbox': False, 'sandbox': None}) return result.get('value') if isinstance(result, dict) and 'value' in result else result def wait(expression): deadline = time.monotonic() + 10 while time.monotonic() < deadline: if js('return ' + expression): return time.sleep(.1) raise AssertionError(expression + ': ' + str(js('return document.body.innerText'))[:1000]) def submit(code): js("const input=document.querySelector('#run');input.value=" + json.dumps(code) + ";input.dispatchEvent(new Event('input',{bubbles:true}));") wait("!document.querySelector('.game-button-run').disabled") js("for(let i=0;i<5;i++)document.querySelector('.game-button-run').click();") wait("document.querySelector('.game-button-run').textContent.includes('Отправляем')") def check_position(count): wait(f"document.querySelectorAll('.story-place').length === {count}") wait("document.querySelector('#run').value === ''") assert js("const c=document.querySelector('.center-block-custom');" "const p=[...document.querySelectorAll('.story-place')].at(-1);" "return Math.abs(p.getBoundingClientRect().top-c.getBoundingClientRect().top)<2;") command('WebDriver:NewSession', {'capabilities': {'alwaysMatch': {}}}) command('WebDriver:Navigate', {'url': f'http://127.0.0.1:{server.server_port}/team-story/test?password=test'}) wait("document.querySelector('#run') && !document.querySelector('#run').disabled") js('window.holdStory=true;') wait('heldStories.length === 1') submit('p1') assert js('return actionCalls') == 1 js('window.holdStory=false;finishAction();') check_position(1) js('heldStories.shift()();') time.sleep(.2) assert js("return document.querySelectorAll('.story-place').length") == 1 js("document.querySelector('.center-block-custom').scrollTo({top:0,behavior:'instant'});") submit('p2') assert js('return actionCalls') == 2 js('window.holdStory=true;finishAction();') wait('heldStories.length === 1') time.sleep(2.2) assert js('return heldStories.length') == 1 assert js("return document.querySelector('.game-button-run').disabled") js('window.holdStory=false;heldStories.shift()();') check_position(2) sender = command('WebDriver:GetWindowHandle') if isinstance(sender, dict): sender = sender['value'] teammate = command('WebDriver:NewWindow', {'type': 'tab'})['handle'] command('WebDriver:SwitchToWindow', {'handle': teammate}) command('WebDriver:Navigate', {'url': f'http://127.0.0.1:{server.server_port}/team-story/test?password=test'}) wait("document.querySelector('#run') && !document.querySelector('#run').disabled") js("places.push(makePlace('p1'),makePlace('p2'));") wait("document.querySelectorAll('.story-place').length === 2") command('WebDriver:SwitchToWindow', {'handle': sender}) submit('bad') js('finishAction(true);') wait("!document.querySelector('#run').disabled") assert js("return document.querySelector('#run').value") == 'bad' submit('p3') js('window.failStory=true;finishAction();') wait("document.querySelector('#run').value === '' && document.body.innerText.includes('Ход сохранён')") assert js('return actionCalls') == 4 js('window.failStory=false;') wait("document.querySelectorAll('.story-place').length === 3") # A teammate's move arrives through polling while this player reads old text. js("document.querySelector('.center-block-custom').scrollTo({top:0,behavior:'instant'});" "places.push(makePlace('remote'));") wait("document.querySelectorAll('.story-place').length === 4") assert js("return document.querySelector('.center-block-custom').scrollTop") == 0 assert js('return errors') == [] command('WebDriver:SwitchToWindow', {'handle': teammate}) js("document.querySelector('.center-block-custom').scrollTo({top:0,behavior:'instant'});" "places.push(makePlace('p3'),makePlace('remote'));") wait("document.querySelectorAll('.story-place').length === 4") assert js("return document.querySelector('.center-block-custom').scrollTop") == 0 assert js('return errors') == [] print('PASS: first/subsequent move, rapid clicks, delayed refresh, stale polling, both errors, remote move preserves reading position') finally: if sock: sock.close() firefox.terminate() try: firefox.wait(timeout=5) except subprocess.TimeoutExpired: firefox.kill() firefox.wait() server.shutdown() if __name__ == '__main__': main(pathlib.Path(sys.argv[1]).resolve())