|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Offline Electron regression for background AX value editing and explicit focus recovery. |
| 3 | +
|
| 4 | +Start an inactive fixture with electron-live.py <helper> --launch-only --artifacts <new-dir>. |
| 5 | +Then run this script with <helper> --state <new-dir>/state.json --report <report.json>. |
| 6 | +The fixture must already be inactive; this test never focuses an unrelated app to arrange it. |
| 7 | +It submits exactly one synthetic local message, relative to the existing submission baseline. |
| 8 | +""" |
| 9 | +import argparse |
| 10 | +import importlib.util |
| 11 | +import json |
| 12 | +import pathlib |
| 13 | +import time |
| 14 | +import uuid |
| 15 | + |
| 16 | +SOURCE = pathlib.Path(__file__).resolve().parent |
| 17 | +spec = importlib.util.spec_from_file_location('electron_live', SOURCE / 'electron-live.py') |
| 18 | +fixture_tools = importlib.util.module_from_spec(spec) |
| 19 | +spec.loader.exec_module(fixture_tools) |
| 20 | + |
| 21 | + |
| 22 | +def main(): |
| 23 | + parser = argparse.ArgumentParser(description=__doc__) |
| 24 | + parser.add_argument('helper') |
| 25 | + parser.add_argument('--state', required=True, type=pathlib.Path, |
| 26 | + help='State file of an already running, inactive offline composer fixture') |
| 27 | + parser.add_argument('--report', required=True, type=pathlib.Path) |
| 28 | + args = parser.parse_args() |
| 29 | + helper = fixture_tools.Helper(args.helper) |
| 30 | + initial = fixture_tools.fixture_state(args.state) |
| 31 | + nonce = 'background-recovery-' + uuid.uuid4().hex[:12] |
| 32 | + report = {'passed': False, 'fixturePid': initial['pid'], 'checks': []} |
| 33 | + try: |
| 34 | + def active(): |
| 35 | + apps = helper.call('list_apps')['apps'] |
| 36 | + app = next(app for app in apps if app['bundleId'] == fixture_tools.BUNDLE) |
| 37 | + assert app['pid'] == initial['pid'], 'Fixture PID changed' |
| 38 | + return app['isActive'] |
| 39 | + |
| 40 | + def editor(): |
| 41 | + state = helper.state() |
| 42 | + target = fixture_tools.composer(state) |
| 43 | + assert target and target.get('editable'), 'Missing writable rich editor' |
| 44 | + return state, target |
| 45 | + |
| 46 | + assert not active(), 'Fixture must start in the background' |
| 47 | + snapshot, target = editor() |
| 48 | + reply = helper.reply('input_sequence', bundleId=fixture_tools.BUNDLE, |
| 49 | + snapshotId=snapshot['snapshotId'], elementId=target['elementId'], |
| 50 | + steps=[{'action': 'type_text', 'text': 'MUST NOT DISPATCH'}, |
| 51 | + {'action': 'press_key', 'key': 'Enter'}]) |
| 52 | + assert reply.get('error', {}).get('code') == 'activation_required', reply |
| 53 | + assert reply['error'].get('dispatchState') == 'not_started', reply |
| 54 | + unchanged = fixture_tools.fixture_state(args.state) |
| 55 | + assert all(unchanged[key] == initial[key] |
| 56 | + for key in ['composer', 'sink', 'submissions', 'history']) |
| 57 | + assert not active(), 'Background rejection activated the fixture' |
| 58 | + report['checks'].append('background keyboard input rejected before dispatch with activation_required') |
| 59 | + |
| 60 | + snapshot, target = editor() |
| 61 | + helper.call('set_value', bundleId=fixture_tools.BUNDLE, |
| 62 | + snapshotId=snapshot['snapshotId'], elementId=target['elementId'], value=nonce) |
| 63 | + time.sleep(0.2) |
| 64 | + changed = fixture_tools.fixture_state(args.state) |
| 65 | + snapshot, target = editor() |
| 66 | + background_value_verified = target.get('value') == nonce and changed['composer'] == nonce |
| 67 | + assert changed['submissions'] == initial['submissions'] and not active() |
| 68 | + report['backgroundSetValueVerified'] = background_value_verified |
| 69 | + if background_value_verified: |
| 70 | + report['checks'].append('background set_value verified in AX and fixture DOM') |
| 71 | + helper.call('set_value', bundleId=fixture_tools.BUNDLE, |
| 72 | + snapshotId=snapshot['snapshotId'], elementId=target['elementId'], value=initial['composer']) |
| 73 | + fixture_tools.fixture_state(args.state, lambda state: state['composer'] == initial['composer']) |
| 74 | + else: |
| 75 | + assert changed['composer'] == initial['composer'] and target.get('value') != nonce |
| 76 | + report['checks'].append('background AX set_value success was an unverified no-op; no submission or activation') |
| 77 | + |
| 78 | + helper.call('activate_app', bundleId=fixture_tools.BUNDLE) |
| 79 | + snapshot, target = editor() |
| 80 | + helper.call('set_value', bundleId=fixture_tools.BUNDLE, |
| 81 | + snapshotId=snapshot['snapshotId'], elementId=target['elementId'], value=nonce) |
| 82 | + time.sleep(0.2) |
| 83 | + snapshot, target = editor() |
| 84 | + foreground_state = fixture_tools.fixture_state(args.state) |
| 85 | + report['foregroundSetValueVerified'] = target.get('value') == nonce and foreground_state['composer'] == nonce |
| 86 | + assert foreground_state['submissions'] == initial['submissions'] |
| 87 | + result = helper.call('input_sequence', bundleId=fixture_tools.BUNDLE, |
| 88 | + snapshotId=snapshot['snapshotId'], elementId=target['elementId'], |
| 89 | + steps=[{'action': 'press_key', 'key': 'Cmd+A'}, |
| 90 | + {'action': 'type_text', 'text': nonce}, |
| 91 | + {'action': 'press_key', 'key': 'Enter'}]) |
| 92 | + assert result['sequence'] == {'completedSteps': 3, 'totalSteps': 3}, result |
| 93 | + final = fixture_tools.fixture_state(args.state, |
| 94 | + lambda state: state['submissions'] == initial['submissions'] + 1) |
| 95 | + assert final['history'] == initial['history'] + [nonce] |
| 96 | + assert final['composer'] == '' and final['sink'] == initial['sink'] |
| 97 | + time.sleep(0.15) |
| 98 | + assert fixture_tools.fixture_state(args.state)['submissions'] == final['submissions'] |
| 99 | + snapshot, _ = editor() |
| 100 | + assert any(node.get('value') == f"Submissions: {final['submissions']}" |
| 101 | + or node.get('label') == f"Submissions: {final['submissions']}" |
| 102 | + for node in snapshot['nodes']) |
| 103 | + report['checks'].append('explicit activation and fresh observation recovered with exactly one local submission') |
| 104 | + report.update(passed=True, nonce=nonce, submissionsBefore=initial['submissions'], |
| 105 | + submissionsAfter=final['submissions']) |
| 106 | + finally: |
| 107 | + helper.close() |
| 108 | + args.report.parent.mkdir(parents=True, exist_ok=True) |
| 109 | + args.report.write_text(json.dumps(report, indent=2)) |
| 110 | + print(json.dumps(report, indent=2)) |
| 111 | + |
| 112 | + |
| 113 | +if __name__ == '__main__': |
| 114 | + main() |
0 commit comments