import json import subprocess from pathlib import Path LATER_PICKER = Path(__file__).parents[1] / "frontend" / "later-picker.js" def run_node(script: str) -> dict: result = subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ) return json.loads(result.stdout) def test_exact_local_time_is_validated_and_converted_to_an_instant(): script = f""" const createLaterPicker = require({json.dumps(str(LATER_PICKER))}); const picker = createLaterPicker({{ now: () => new Date(2026, 7, 9, 14, 0, 0), }}); const valid = picker.parse('2026-08-09T16:30'); const past = picker.parse('2026-08-09T13:59'); const invalid = picker.parse('2026-02-30T12:00'); process.stdout.write(JSON.stringify({{ valid: {{ok:valid.ok, local:[valid.value.getFullYear(), valid.value.getMonth() + 1, valid.value.getDate(), valid.value.getHours(), valid.value.getMinutes()]}}, past, invalid, }})); """ output = run_node(script) assert output["valid"] == {"ok": True, "local": [2026, 8, 9, 16, 30]} assert output["past"] == { "ok": False, "message": "Choose a future date and time.", } assert output["invalid"] == { "ok": False, "message": "Choose a valid local date and time.", } def test_picker_uses_history_for_cancel_and_confirms_only_valid_future_time(): script = f""" const createLaterPicker = require({json.dumps(str(LATER_PICKER))}); const listeners = {{}}; const states = []; const confirmed = []; let focused = 0; const history = {{ state: {{workRoute:'issue'}}, pushState(state) {{ this.state = state; states.push(['push', state.laterPicker]); }}, back() {{ states.push(['back']); const previous = {{workRoute:'issue'}}; this.state = previous; listeners.popstate({{state:previous}}); }}, }}; const picker = createLaterPicker({{ now: () => new Date(2026, 7, 9, 14, 0, 0), history, eventTarget: {{addEventListener:(name, callback) => {{ listeners[name] = callback; }}}}, onState: state => states.push([state.open, state.message || '']), onConfirm: (item, value, context) => {{ confirmed.push([item.number, value.toISOString(), context]); return true; }}, }}); picker.start(); picker.open({{number:17}}, {{focus:() => {{ focused += 1; }}}}, 'detail'); const invalid = picker.submit('2026-08-09T13:00'); const valid = picker.submit('2026-08-09T16:30'); picker.submit('2026-08-09T17:30'); process.stdout.write(JSON.stringify({{invalid, valid, confirmed, focused, current:picker.current(), states}})); """ output = run_node(script) assert output["invalid"] is False assert output["valid"] is True assert output["confirmed"] == [[17, "2026-08-09T16:30:00.000Z", "detail"]] assert output["focused"] == 1 assert output["current"] is False assert [False, "Choose a future date and time."] not in output["states"] assert [True, "Choose a future date and time."] in output["states"] assert ["back"] in output["states"] def test_picker_runs_detail_cleanup_only_after_its_history_entry_closes(): script = f""" const createLaterPicker = require({json.dumps(str(LATER_PICKER))}); const listeners = {{}}; const order = []; const history = {{ state: {{workRoute:'issue'}}, pushState(state) {{ this.state = state; }}, back() {{ order.push('picker-back'); this.state = {{workRoute:'issue'}}; listeners.popstate({{state:this.state}}); }}, }}; const picker = createLaterPicker({{ now:() => new Date(2026, 7, 9, 14, 0), history, eventTarget:{{addEventListener:(name, callback) => {{ listeners[name] = callback; }}}}, onConfirm:() => () => order.push('detail-close'), }}); picker.start(); picker.open({{number:17}}, null, 'detail'); const saved = picker.submit('2026-08-09T16:30'); process.stdout.write(JSON.stringify({{saved, order}})); """ output = run_node(script) assert output == {"saved": True, "order": ["picker-back", "detail-close"]} def test_picker_ignores_a_second_submit_while_browser_back_is_pending(): script = f""" const createLaterPicker = require({json.dumps(str(LATER_PICKER))}); const listeners = {{}}; let popstate; let confirmations = 0; const history = {{ state:null, pushState(state) {{ this.state = state; }}, back() {{ popstate = () => {{ this.state = {{}}; listeners.popstate({{state:this.state}}); }}; }}, }}; const picker = createLaterPicker({{ now:() => new Date(2026, 7, 9, 14, 0), history, eventTarget:{{addEventListener:(name, callback) => {{ listeners[name] = callback; }}}}, onConfirm:() => {{ confirmations += 1; return true; }}, }}); picker.start(); picker.open({{number:17}}); const first = picker.submit('2026-08-09T16:30'); const second = picker.submit('2026-08-09T16:30'); popstate(); process.stdout.write(JSON.stringify({{first, second, confirmations}})); """ output = run_node(script) assert output == {"first": True, "second": False, "confirmations": 1}