154 lines
5.4 KiB
Python
154 lines
5.4 KiB
Python
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}
|
|
|
|
|
|
def test_picker_item_can_own_batch_confirmation_and_run_after_close():
|
|
script = f"""
|
|
const createLaterPicker = require({json.dumps(str(LATER_PICKER))});
|
|
const order=[];
|
|
const picker=createLaterPicker({{
|
|
now:() => new Date(2026,7,14,8,0),
|
|
onConfirm:()=>{{order.push('global');return true;}},
|
|
}});
|
|
picker.open({{confirm:until=>() => order.push(until.toISOString())}},null,'search-batch');
|
|
const saved=picker.submit('2026-08-15T09:00');
|
|
process.stdout.write(JSON.stringify({{saved,order}}));
|
|
"""
|
|
|
|
assert run_node(script) == {
|
|
"saved": True,
|
|
"order": ["2026-08-15T09:00:00.000Z"],
|
|
}
|