1#!/usr/bin/env python3
2"""Capture native page indentation and tab movement in an owned clone."""
3import argparse
4import base64
5import hashlib
6import json
7from pathlib import Path
8import shutil
9import signal
10from tempfile import TemporaryDirectory
11import xml.etree.ElementTree as ET
12
13import native_runner as runner
14
15NAMES = ['Untitled page'] * 3 + [
16 'Same title', 'child', 'grandchild', 'Renamed 🦋 é',
17 'Body trailing 🦀 é preserved', 'Body automatic 🦀 é preserved']
18PHASES = [
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
30def 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
40def 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
82if __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)