aboutsummaryrefslogtreecommitdiffstats
path: root/tests/check.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/check.py')
-rw-r--r--tests/check.py145
1 files changed, 145 insertions, 0 deletions
diff --git a/tests/check.py b/tests/check.py
index e0d5996..51fbf7f 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -20,6 +20,10 @@
none in the examples.
7. Painting: the bytes the TUI sends, replayed by a small terminal emulator,
give the very frame the dump shows, while sending only what changed.
+8. Web: the GUI surface's screen JSON matches the model after random keys,
+ text and row clicks, holds only the rows asked for around the focus, and
+ every open page hears of each change. In headless Chromium (when node and
+ chromium are there) the page itself takes keys, a click and typed text.
"""
import argparse
import json
@@ -368,6 +372,119 @@ def painting(vega, module, seeds, sizes, steps=200):
return compared, first_mean, later_mean
+def web(vega, module, work, seeds, steps=150, visible=4):
+ """The web surface against the model: POST keys, text and row clicks, then
+ the screen JSON's title, field, rows, focus and the window of items."""
+ import http.client
+ import signal
+ import threading
+ compared = 0
+ for seed in seeds:
+ store = work / f'web-{seed}'
+ process = subprocess.Popen([str(vega), 'web', str(module), '--port', '0', '--store', str(store)],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ port = json.loads(process.stdout.readline())['port']
+ connection = http.client.HTTPConnection('127.0.0.1', port, timeout=10)
+
+ def call(method, path, body=None):
+ connection.request(method, path, body.encode() if isinstance(body, str) else body)
+ response = connection.getresponse()
+ return response.status, response.read()
+
+ heard = []
+ stream = http.client.HTTPConnection('127.0.0.1', port, timeout=10)
+ stream.request('GET', '/events')
+ events = stream.getresponse()
+ assert events.getheader('Content-Type') == 'text/event-stream'
+ listener = threading.Thread(target=lambda: [heard.append(line) for line in iter(events.readline, b'')
+ if line.startswith(b'data:')], daemon=True)
+ listener.start()
+ status, page = call('GET', '/')
+ assert status == 200 and b'EventSource' in page and b"role:'listbox'" in page, page[:80]
+ assert call('GET', '/nowhere')[0] == 404
+ rng, model, first, part = random.Random(seed + 900), Todo(), 0, None
+ for _ in range(steps):
+ roll = rng.random()
+ if roll < 0.3:
+ word = rng.choice(WORDS)
+ status, answer = call('POST', '/text', word)
+ model.event('add', word)
+ elif roll < 0.45 and part is not None:
+ row = rng.randrange(len(model.tasks) + 1)
+ status, answer = call('POST', '/point', f'{part} {row}')
+ assert json.loads(answer)['ok'] == model.event('point', row), (seed, row)
+ else:
+ name = rng.choice(list(KEYS))
+ status, answer = call('POST', '/key', KEYS[name])
+ assert json.loads(answer)['ok'] == model.event(name), (seed, name)
+ assert status == 200
+ status, body = call('GET', f'/screen?rows={visible}&first={first}')
+ screen = json.loads(body)
+ tasks, n = model.tasks, len(model.tasks)
+ (listed,) = screen['parts']
+ part = listed['part']
+ if model.at < first: first = model.at
+ if model.at >= first + visible: first = model.at - visible + 1
+ if first + visible > n: first = max(n - visible, 0)
+ assert screen['title'] == 'To do' and screen['fields'] == [{'name': 'left', 'text': f'{n} tasks'}], screen
+ assert (listed['rows'], listed['focus'], listed['first']) == (n, model.at, first), (seed, listed, model.at)
+ window = tasks[first:first + visible]
+ assert [(item['row'], item['label'], item['badge'], item['tone']) for item in listed['items']] == \
+ [(first + i, task['title'], Todo.badge(task), Todo.tone(task)) for i, task in enumerate(window)], seed
+ assert screen['prompt'] == 'add' and listed['pointable'] and listed['kind'] == 'list'
+ compared += 1
+ process.send_signal(signal.SIGTERM)
+ assert process.wait(timeout=10) == 0, process.stderr.read()
+ listener.join(timeout=5)
+ assert len(heard) > steps // 2, len(heard) # a change each time, more or less merged
+ again = run(vega, 'cli', module, '--store', store, stdin='show\n').stdout.splitlines()
+ assert again == model.cli(), (again, model.cli())
+ return compared
+
+
+class WebSurface:
+ """vega web on a free port; stops with SIGTERM."""
+
+ def __init__(self, vega, module, *args):
+ self.process = subprocess.Popen([str(vega), 'web', str(module), '--port', '0', *map(str, args)],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ self.port = json.loads(self.process.stdout.readline())['port']
+
+ def call(self, method, path, body=None):
+ import http.client
+ connection = http.client.HTTPConnection('127.0.0.1', self.port, timeout=10)
+ try:
+ connection.request(method, path, body.encode() if isinstance(body, str) else body)
+ response = connection.getresponse()
+ return response.status, response.read()
+ finally:
+ connection.close()
+
+ def stop(self):
+ import signal
+ self.process.send_signal(signal.SIGTERM)
+ assert self.process.wait(timeout=10) == 0, self.process.stderr.read()
+
+
+def browser(vega, module, work):
+ """The page in headless Chromium: keys, a click on a row, typed text."""
+ import shutil
+ if not shutil.which('node') or not shutil.which('chromium'):
+ return 'skipped'
+ store = work / 'browser-store'
+ assert run(vega, 'cli', module, '--store', store, stdin='text 61\ntext 62\ntext 63\n').returncode == 0
+ surface = WebSurface(vega, module, '--store', store)
+ try:
+ result = run('node', HERE / 'browser.mjs', f'http://127.0.0.1:{surface.port}/')
+ finally:
+ surface.stop()
+ outcome = json.loads(result.stdout)
+ assert result.returncode == 0 and outcome['ok'], outcome
+ after = run(vega, 'cli', module, '--store', store, stdin='show\n').stdout.splitlines()
+ assert after == ['To do', 'left: 4 tasks', 'tasks_list (4)', ' a [done] (muted)', ' b', ' c', '> 브라우저'], after
+ return len(outcome['steps'])
+
+
def scores(vega, module):
joins = ['ann', '범수', 'Zoë']
script = [f'text {name.encode().hex()}' for name in joins]
@@ -383,6 +500,19 @@ def scores(vega, module):
'| 범수 3 gold\tgood'], tui
say = run(vega, 'say', module, stdin='show\n' + '\n'.join(script) + '\n').stdout.splitlines()
assert 'Item 1 of 3: danger, name ann, score -1, rank -.' in say, say
+ # The web surface: a table's columns and cells in order, with tones.
+ surface = WebSurface(vega, module)
+ try:
+ for line in script[:-1]:
+ command, argument = line.split()
+ if command == 'text': surface.call('POST', '/text', bytes.fromhex(argument))
+ else: surface.call('POST', '/key', argument)
+ board = json.loads(surface.call('GET', '/screen?rows=10')[1])['parts'][0]
+ finally:
+ surface.stop()
+ assert board['kind'] == 'table' and board['columns'] == ['name', 'score', 'rank'] and board['focus'] == 0, board
+ assert [(item['cells'], item['tone']) for item in board['items']] == [
+ (['ann', '-1', '-'], 'danger'), (['범수', '3', 'gold'], 'good'), (['Zoë', '0', '-'], 'normal')], board
return len(out)
@@ -517,6 +647,19 @@ def robustness(vega, aup, work, rounds):
result = run(vega, surface[0], module, *surface[1:], stdin=text)
assert result.returncode == 0, (path, surface, result.stderr[-400:])
runs += 1
+ # The web surface under random posts: every answer well formed, and a clean stop.
+ surface = WebSurface(vega, module)
+ try:
+ for _ in range(40):
+ roll = rng.random()
+ if roll < 0.3: surface.call('POST', '/text', rng.choice(WORDS))
+ elif roll < 0.4: surface.call('POST', '/point', f'{rng.randrange(4)} {rng.randrange(4)}')
+ else: surface.call('POST', '/key', rng.choice(keys))
+ status, body = surface.call('GET', f'/screen?rows={rng.randrange(0, 30)}&first={rng.randrange(9)},1')
+ assert status == 200 and isinstance(json.loads(body)['parts'], list), (path, body[:200])
+ finally:
+ surface.stop()
+ runs += 1
return len(examples), runs
@@ -543,6 +686,8 @@ def main():
report['table_lines'] = scores(vega, modules['scores'])
report['painted_frames'], report['first_paint_bytes'], report['later_paint_bytes'] = \
painting(vega, modules['todo'], range(args.seeds), [(40, 12), (24, 9), (80, 30)])
+ report['web_screens'] = web(vega, modules['todo'], work, range(args.seeds))
+ report['browser_steps'] = browser(vega, modules['todo'], work)
report['interactive_sessions'] = interactive(vega, modules['todo']) + \
interactive(vega, modules['todo'], work / 'interactive-store')
report['stored_surface_lines'] = stored_surfaces(vega, modules['todo'], work, range(args.seeds))