From 2e55e3c88f28d638556b0c63d1132edd0589da96 Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Tue, 13 Oct 2026 14:51:45 +0700 Subject: [PATCH] up --- tests/venue/move_feedback.py | 226 +++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 tests/venue/move_feedback.py diff --git a/tests/venue/move_feedback.py b/tests/venue/move_feedback.py new file mode 100644 index 0000000..3fad92c --- /dev/null +++ b/tests/venue/move_feedback.py @@ -0,0 +1,226 @@ +"""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())