188 lines
6.3 KiB
Python
188 lines
6.3 KiB
Python
"""Behavioral coverage for desktop-backgrounded notification delivery (#4753)."""
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
MESSAGES_SRC = (ROOT / "static" / "messages.js").read_text(encoding="utf-8")
|
|
NODE = shutil.which("node")
|
|
|
|
pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
|
|
|
|
def _extract_function(source: str, name: str) -> str:
|
|
start = source.index(f"function {name}(")
|
|
body_start = source.index("){", start) + 1
|
|
depth = 0
|
|
for idx in range(body_start, len(source)):
|
|
char = source[idx]
|
|
if char == "{":
|
|
depth += 1
|
|
elif char == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return source[start : idx + 1]
|
|
raise AssertionError(f"{name} function body did not close")
|
|
|
|
|
|
def _notification_contract_source() -> str:
|
|
start = MESSAGES_SRC.index("let _desktopBackgroundedForNotifications=false;")
|
|
end = MESSAGES_SRC.index("function _isSessionCurrentPane", start)
|
|
tracker_start = MESSAGES_SRC.index("const _STREAM_NOTIFICATION_BACKGROUND={};")
|
|
tracker_end = MESSAGES_SRC.index("\n", tracker_start)
|
|
send = _extract_function(MESSAGES_SRC, "sendBrowserNotification")
|
|
return MESSAGES_SRC[start:end] + "\n" + MESSAGES_SRC[tracker_start:tracker_end] + "\n" + send
|
|
|
|
|
|
def _background_history_contract_source() -> str:
|
|
notification_start = MESSAGES_SRC.index("let _desktopBackgroundedForNotifications=false;")
|
|
notification_end = MESSAGES_SRC.index("function _isSessionCurrentPane", notification_start)
|
|
tracker_start = MESSAGES_SRC.index("const LIVE_STREAMS={};")
|
|
tracker_end = MESSAGES_SRC.index("function closeLiveStream", tracker_start)
|
|
return (
|
|
MESSAGES_SRC[notification_start:notification_end]
|
|
+ "\n"
|
|
+ MESSAGES_SRC[tracker_start:tracker_end]
|
|
)
|
|
|
|
|
|
def _run_notification_case(*, document_hidden: bool, desktop_backgrounded: bool = False, options=None):
|
|
payload = {
|
|
"documentHidden": document_hidden,
|
|
"desktopBackgrounded": desktop_backgrounded,
|
|
"options": options or {},
|
|
}
|
|
script = (
|
|
"const source = " + json.dumps(_notification_contract_source()) + ";\n"
|
|
+ "const params = " + json.dumps(payload) + ";\n"
|
|
+ r"""
|
|
const vm = require('vm');
|
|
const shown = [];
|
|
const direct = [];
|
|
|
|
function Notification(title, options) {
|
|
direct.push({ title, options });
|
|
}
|
|
Notification.permission = 'granted';
|
|
|
|
const context = {
|
|
document: { hidden: params.documentHidden },
|
|
window: { _notificationsEnabled: true },
|
|
Notification,
|
|
assistantDisplayName: () => 'Hermes',
|
|
_notificationOptions: (body, options) => ({ body, tag: options && options.sid ? options.sid : '' }),
|
|
_showPwaNotification: (title, body, options) => {
|
|
shown.push({ title, body, options });
|
|
return Promise.resolve();
|
|
},
|
|
};
|
|
context.window.Notification = Notification;
|
|
|
|
vm.createContext(context);
|
|
vm.runInContext(source, context);
|
|
context.window.__hermesSetBackgrounded(params.desktopBackgrounded);
|
|
vm.runInContext(
|
|
"sendBrowserNotification('Response complete','Task finished'," + JSON.stringify(params.options) + ");",
|
|
context
|
|
);
|
|
|
|
console.log(JSON.stringify({
|
|
shown,
|
|
direct,
|
|
documentHidden: context.document.hidden,
|
|
setterType: typeof context.window.__hermesSetBackgrounded,
|
|
}));
|
|
"""
|
|
)
|
|
temp = tempfile.NamedTemporaryFile(mode="w", suffix=".js", delete=False, encoding="utf-8")
|
|
temp.write(script)
|
|
temp.close()
|
|
try:
|
|
result = subprocess.run([NODE, temp.name], capture_output=True, text=True, timeout=30)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"node failed: {result.stderr}")
|
|
return json.loads(result.stdout.strip().splitlines()[-1])
|
|
finally:
|
|
os.unlink(temp.name)
|
|
|
|
|
|
def test_visible_desktop_backgrounded_tab_notifies_without_page_visibility_hidden():
|
|
result = _run_notification_case(document_hidden=False, desktop_backgrounded=True)
|
|
|
|
assert result["setterType"] == "function"
|
|
assert result["documentHidden"] is False
|
|
assert len(result["shown"]) == 1
|
|
assert result["shown"][0]["title"] == "Response complete"
|
|
|
|
|
|
def test_visible_foreground_tab_stays_silent_without_force():
|
|
result = _run_notification_case(document_hidden=False, desktop_backgrounded=False)
|
|
|
|
assert result["shown"] == []
|
|
assert result["direct"] == []
|
|
|
|
|
|
def test_real_hidden_tab_still_notifies_without_desktop_background_flag():
|
|
result = _run_notification_case(document_hidden=True, desktop_backgrounded=False)
|
|
|
|
assert len(result["shown"]) == 1
|
|
assert result["shown"][0]["body"] == "Task finished"
|
|
|
|
|
|
def test_force_hidden_still_notifies_visible_documents():
|
|
result = _run_notification_case(
|
|
document_hidden=False,
|
|
desktop_backgrounded=False,
|
|
options={"forceHidden": True},
|
|
)
|
|
|
|
assert len(result["shown"]) == 1
|
|
|
|
|
|
def test_late_done_still_notifies_after_desktop_backgrounded_tab_returns_foreground():
|
|
script = (
|
|
"const source = " + json.dumps(_background_history_contract_source()) + ";\n"
|
|
+ r"""
|
|
const vm = require('vm');
|
|
const context = {
|
|
document: {
|
|
hidden: false,
|
|
addEventListener: () => {},
|
|
},
|
|
window: {},
|
|
};
|
|
vm.createContext(context);
|
|
vm.runInContext(source, context);
|
|
vm.runInContext(
|
|
"_STREAM_WAS_HIDDEN['session-1']={streamId:'stream-1',wasHidden:false};" +
|
|
"_STREAM_NOTIFICATION_BACKGROUND['session-1']={streamId:'stream-1',wasBackgrounded:false};",
|
|
context
|
|
);
|
|
context.window.__hermesSetBackgrounded(true);
|
|
context.window.__hermesSetBackgrounded(false);
|
|
const first = vm.runInContext("_shouldForceCompletionNotification('session-1','stream-1')", context);
|
|
const second = vm.runInContext("_shouldForceCompletionNotification('session-1','stream-1')", context);
|
|
console.log(JSON.stringify({ first, second }));
|
|
"""
|
|
)
|
|
temp = tempfile.NamedTemporaryFile(mode="w", suffix=".js", delete=False, encoding="utf-8")
|
|
temp.write(script)
|
|
temp.close()
|
|
try:
|
|
result = subprocess.run([NODE, temp.name], capture_output=True, text=True, timeout=30)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"node failed: {result.stderr}")
|
|
payload = json.loads(result.stdout.strip().splitlines()[-1])
|
|
finally:
|
|
os.unlink(temp.name)
|
|
|
|
assert payload["first"] is True
|
|
assert payload["second"] is False
|