| 1 | #!/usr/bin/env python3 |
| 2 | """Capture native page indentation and tab movement in an owned clone.""" |
| 3 | import argparse |
| 4 | import base64 |
| 5 | import hashlib |
| 6 | import json |
| 7 | from pathlib import Path |
| 8 | import shutil |
| 9 | import signal |
| 10 | from tempfile import TemporaryDirectory |
| 11 | import xml.etree.ElementTree as ET |
| 12 | |
| 13 | import native_runner as runner |
| 14 | |
| 15 | NAMES = ['Untitled page'] * 3 + [ |
| 16 | 'Same title', 'child', 'grandchild', 'Renamed 🦋 é', |
| 17 | 'Body trailing 🦀 é preserved', 'Body automatic 🦀 é preserved'] |
| 18 | PHASES = [ |
| 19 | ('01-demoted-parent', 3, [('menu', 179)], list(range(9)), [1, 1, 1, 2, 2, 3, 1, 1, 1]), |
| 20 | ('02-promoted-parent', 3, [('menu', 202)], list(range(9)), [1, 1, 1, 1, 2, 3, 1, 1, 1]), |
| 21 | ('03-selected-group-move', 3, [('select', 2), ('drag', 308)], [0, 1, 2, 6, 7, 8, 3, 4, 5], [1, 1, 1, 1, 1, 1, 1, 2, 3]), |
| 22 | ('04-subpage-move', 4, [('drag', 182)], [0, 1, 2, 4, 6, 7, 8, 3, 5], [1, 1, 1, 1, 1, 1, 1, 1, 3]), |
| 23 | ('05-insert-before-subpage', 4, [('drag', 286)], [0, 1, 2, 6, 7, 8, 3, 4, 5], [1, 1, 1, 1, 1, 1, 1, 3, 3]), |
| 24 | ('06-promoted-level-two', 4, [('menu', 202)], [0, 1, 2, 6, 7, 8, 3, 4, 5], [1, 1, 1, 1, 1, 1, 1, 2, 3]), |
| 25 | ('07-promoted-root', 4, [('menu', 202)], [0, 1, 2, 6, 7, 8, 3, 4, 5], [1, 1, 1, 1, 1, 1, 1, 1, 3]), |
| 26 | ('08-collapsed-group-move', 4, [('menu', 224), ('drag', 117)], [4, 5, 0, 1, 2, 6, 7, 8, 3], [1, 3, 1, 1, 1, 1, 1, 1, 1]), |
| 27 | ] |
| 28 | |
| 29 | |
| 30 | def ui(target, source, destination): |
| 31 | destination.with_suffix('.ahk').write_text(source) |
| 32 | result = runner.windows.do_exec(source, target=target, timeout_ms=15000, shot_delay_ms=300) |
| 33 | destination.with_suffix('.json').write_text(json.dumps({k: v for k, v in result.items() if k != 'png_b64'}, indent=2)) |
| 34 | if result.get('png_b64'): |
| 35 | destination.with_suffix('.png').write_bytes(base64.b64decode(result['png_b64'])) |
| 36 | if result.get('exit') != 0 or result.get('error') or not result.get('png_b64'): |
| 37 | raise RuntimeError('Native desktop action failed; inspect its saved capture.') |
| 38 | |
| 39 | |
| 40 | def interact(target, output): |
| 41 | helper = runner.ROOT / 'tools/native/page-lifecycle.ps1' |
| 42 | shutil.copy2(helper, output / 'scripts/page-lifecycle.ps1') |
| 43 | shutil.copy2(__file__, output / 'controller.py') |
| 44 | (output / 'interaction.json').write_text(json.dumps({ |
| 45 | 'controller_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), |
| 46 | 'helper_sha256': hashlib.sha256(helper.read_bytes()).hexdigest(), |
| 47 | }, indent=2)) |
| 48 | result = runner.windows.do_put(helper, r'C:\one-tests\page-lifecycle.ps1', target) |
| 49 | if result.get('error'): |
| 50 | raise RuntimeError(result['error']) |
| 51 | hierarchy = output / 'read/hierarchy.xml' |
| 52 | for phase, selected, actions, order, levels in PHASES: |
| 53 | pages = list(ET.parse(hierarchy).getroot().iter('{http://schemas.microsoft.com/office/onenote/2010/onenote}Page')) |
| 54 | row, page = next((i, page) for i, page in enumerate(pages) if page.get('name') == NAMES[selected]) |
| 55 | destination = output / 'desktop' / phase |
| 56 | destination.mkdir(parents=True) |
| 57 | ui(target, 'if A_ScreenWidth != 800 || A_ScreenHeight != 600\n throw Error("This fixture requires an 800 by 600 desktop")\n' |
| 58 | + runner.navigation_script(page.get('ID')), destination / 'navigate') |
| 59 | y = 120 + row * 22 |
| 60 | for ordinal, (action, value) in enumerate(actions): |
| 61 | if action == 'menu': |
| 62 | ui(target, f'Click(710,{y},"Right")\nSleep(500)', destination / f'{ordinal}-menu') |
| 63 | script = f'Click(578,{y + value})\nSleep(700)' |
| 64 | elif action == 'select': |
| 65 | script = f'Click(710,{y})\nSend("{{Shift down}}")\nClick(710,{y + value * 22})\nSend("{{Shift up}}")\nSleep(700)' |
| 66 | else: |
| 67 | script = f'MouseMove(710,{y})\nSend("{{LButton down}}")\nSleep(250)\nMouseMove(710,{value})\nSleep(500)\nSend("{{LButton up}}")\nSleep(700)' |
| 68 | ui(target, script, destination / f'{ordinal}-action') |
| 69 | runner.command(target, 'powershell -NoProfile -ExecutionPolicy Bypass -File C:\\one-tests\\page-lifecycle.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-' + target.upper() + ' -CaptureOnly ' + phase, output, 60000) |
| 70 | hierarchy = destination / 'reopened-hierarchy.xml' |
| 71 | result = runner.windows.do_get('C:\\one-tests\\runs\\capture\\' + phase + '\\reopened-hierarchy.xml', hierarchy, target) |
| 72 | if result.get('error'): |
| 73 | raise RuntimeError(result['error']) |
| 74 | pages = list(ET.parse(hierarchy).getroot()) |
| 75 | actual = [(page.get('name'), int(page.get('pageLevel'))) for page in pages] |
| 76 | expected = [(NAMES[i], level) for i, level in zip(order, levels, strict=True)] |
| 77 | if actual != expected: |
| 78 | raise AssertionError((phase, actual, expected)) |
| 79 | print('Verified native desktop phase: ' + phase, flush=True) |
| 80 | |
| 81 | |
| 82 | if __name__ == '__main__': |
| 83 | parser = argparse.ArgumentParser(description=__doc__) |
| 84 | parser.add_argument('output', type=Path) |
| 85 | args = parser.parse_args() |
| 86 | def interrupted(_signum, _frame): |
| 87 | raise KeyboardInterrupt |
| 88 | signal.signal(signal.SIGTERM, interrupted) |
| 89 | with TemporaryDirectory() as temporary: |
| 90 | source = Path(temporary) |
| 91 | shutil.copy2(runner.ROOT / 'corpus/page-lifecycle/04-nested/notebook/Lifecycle.one', source / 'Lifecycle.one') |
| 92 | runner.capture(source, args.output, expected_pages=9, collect_notebook=True, interaction=interact) |