fix(#4753): notify background desktop tabs without hiding streams
This commit is contained in:
1 parent
8a98626967
commit
94e25ad494
3 files changed
+272
-11
No files matched your search
+45
-5
@@ -56,6 +56,23 @@ function _isDocumentVisibleAndFocused() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _desktopBackgroundedForNotifications=false;
|
||||||
|
// Desktop shells can background a visible document; keep that signal notification-only.
|
||||||
|
if(typeof window!=='undefined'){
|
||||||
|
window.__hermesSetBackgrounded=(value)=>{
|
||||||
|
_desktopBackgroundedForNotifications=!!value;
|
||||||
|
if(_desktopBackgroundedForNotifications){
|
||||||
|
for(const k in _STREAM_NOTIFICATION_BACKGROUND){
|
||||||
|
const e=_STREAM_NOTIFICATION_BACKGROUND[k];
|
||||||
|
if(e) e.wasBackgrounded=true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function _isBackgroundedForBrowserNotification(){
|
||||||
|
return !!(typeof document!=='undefined'&&document.hidden)||_desktopBackgroundedForNotifications;
|
||||||
|
}
|
||||||
|
|
||||||
function _isSessionCurrentPane(sid) {
|
function _isSessionCurrentPane(sid) {
|
||||||
if(!sid || !S.session || S.session.session_id!==sid) return false;
|
if(!sid || !S.session || S.session.session_id!==sid) return false;
|
||||||
// During session switching, S.session still points at the previous row until
|
// During session switching, S.session still points at the previous row until
|
||||||
@@ -1521,6 +1538,7 @@ async function send(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LIVE_STREAMS={};
|
const LIVE_STREAMS={};
|
||||||
|
const _STREAM_NOTIFICATION_BACKGROUND={};
|
||||||
|
|
||||||
// #4416: track whether the tab was hidden at ANY point during a live stream, so
|
// #4416: track whether the tab was hidden at ANY point during a live stream, so
|
||||||
// the response-complete notification fires for a backgrounded tab even when
|
// the response-complete notification fires for a backgrounded tab even when
|
||||||
@@ -1550,6 +1568,22 @@ function _clearStreamHidden(sid, streamId){
|
|||||||
if(streamId&&e.streamId&&e.streamId!==streamId) return;
|
if(streamId&&e.streamId&&e.streamId!==streamId) return;
|
||||||
delete _STREAM_WAS_HIDDEN[sid];
|
delete _STREAM_WAS_HIDDEN[sid];
|
||||||
}
|
}
|
||||||
|
function _clearStreamNotificationBackground(sid, streamId){
|
||||||
|
if(!sid) return;
|
||||||
|
const e=_STREAM_NOTIFICATION_BACKGROUND[sid];
|
||||||
|
if(!e) return;
|
||||||
|
if(streamId&&e.streamId&&e.streamId!==streamId) return;
|
||||||
|
delete _STREAM_NOTIFICATION_BACKGROUND[sid];
|
||||||
|
}
|
||||||
|
function _shouldForceCompletionNotification(sid, streamId){
|
||||||
|
const hiddenEntry=_STREAM_WAS_HIDDEN[sid];
|
||||||
|
const backgroundEntry=_STREAM_NOTIFICATION_BACKGROUND[sid];
|
||||||
|
const wasHidden=!!(hiddenEntry&&hiddenEntry.wasHidden);
|
||||||
|
const wasBackgrounded=!!(backgroundEntry&&backgroundEntry.wasBackgrounded);
|
||||||
|
_clearStreamHidden(sid, streamId);
|
||||||
|
_clearStreamNotificationBackground(sid, streamId);
|
||||||
|
return wasHidden||wasBackgrounded;
|
||||||
|
}
|
||||||
|
|
||||||
function closeLiveStream(sessionId, streamId, source){
|
function closeLiveStream(sessionId, streamId, source){
|
||||||
const live=LIVE_STREAMS[sessionId];
|
const live=LIVE_STREAMS[sessionId];
|
||||||
@@ -1628,6 +1662,11 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||||||
if(!_keep){
|
if(!_keep){
|
||||||
_STREAM_WAS_HIDDEN[activeSid]={streamId,wasHidden:(typeof document!=='undefined'&&!!document.hidden)};
|
_STREAM_WAS_HIDDEN[activeSid]={streamId,wasHidden:(typeof document!=='undefined'&&!!document.hidden)};
|
||||||
}
|
}
|
||||||
|
const _prevBackground=_STREAM_NOTIFICATION_BACKGROUND[activeSid];
|
||||||
|
const _keepBackground=reconnecting&&_prevBackground&&_prevBackground.streamId===streamId;
|
||||||
|
if(!_keepBackground){
|
||||||
|
_STREAM_NOTIFICATION_BACKGROUND[activeSid]={streamId,wasBackgrounded:_desktopBackgroundedForNotifications};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(!INFLIGHT[activeSid]) INFLIGHT[activeSid]={messages:[...S.messages],uploaded:[...uploaded],toolCalls:[]};
|
if(!INFLIGHT[activeSid]) INFLIGHT[activeSid]={messages:[...S.messages],uploaded:[...uploaded],toolCalls:[]};
|
||||||
else {
|
else {
|
||||||
@@ -1898,6 +1937,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||||||
if(typeof finalizeThinkingCard==='function') finalizeThinkingCard();
|
if(typeof finalizeThinkingCard==='function') finalizeThinkingCard();
|
||||||
_clearOwnerInflightState();
|
_clearOwnerInflightState();
|
||||||
_clearStreamHidden(activeSid, streamId); // #4416: terminal path, drop hidden tracker
|
_clearStreamHidden(activeSid, streamId); // #4416: terminal path, drop hidden tracker
|
||||||
|
_clearStreamNotificationBackground(activeSid, streamId);
|
||||||
_flushReasoningToAnchor();
|
_flushReasoningToAnchor();
|
||||||
_scheduleAnchorRegistryCleanup();
|
_scheduleAnchorRegistryCleanup();
|
||||||
_clearApprovalForOwner();
|
_clearApprovalForOwner();
|
||||||
@@ -4414,10 +4454,8 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||||||
// delivers late — after the user returns and document.hidden is false).
|
// delivers late — after the user returns and document.hidden is false).
|
||||||
// If the user watched the whole stream, _wasEverHidden stays false and
|
// If the user watched the whole stream, _wasEverHidden stays false and
|
||||||
// the notification is suppressed (matches Slack/Discord/Gmail/Claude).
|
// the notification is suppressed (matches Slack/Discord/Gmail/Claude).
|
||||||
const _hiddenEntry=_STREAM_WAS_HIDDEN[activeSid];
|
const _wasEverBackgrounded=_shouldForceCompletionNotification(activeSid, streamId);
|
||||||
const _wasEverHidden=!!(_hiddenEntry&&_hiddenEntry.wasHidden);
|
sendBrowserNotification('Response complete',assistantText?assistantText.slice(0,100):'Task finished',{forceHidden:_wasEverBackgrounded,sid:activeSid});
|
||||||
_clearStreamHidden(activeSid, streamId);
|
|
||||||
sendBrowserNotification('Response complete',assistantText?assistantText.slice(0,100):'Task finished',{forceHidden:_wasEverHidden,sid:activeSid});
|
|
||||||
};
|
};
|
||||||
if(_shouldUseStreamFade()&&assistantBody){
|
if(_shouldUseStreamFade()&&assistantBody){
|
||||||
_cancelAnimationFramePendingStreamRender();
|
_cancelAnimationFramePendingStreamRender();
|
||||||
@@ -4583,6 +4621,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||||||
try{if(source&&source.readyState!==2)source.close();}catch(_){ }
|
try{if(source&&source.readyState!==2)source.close();}catch(_){ }
|
||||||
_clearOwnerInflightState();
|
_clearOwnerInflightState();
|
||||||
_clearStreamHidden(activeSid, streamId); // #4416: terminal path, drop hidden tracker
|
_clearStreamHidden(activeSid, streamId); // #4416: terminal path, drop hidden tracker
|
||||||
|
_clearStreamNotificationBackground(activeSid, streamId);
|
||||||
_clearApprovalForOwner();
|
_clearApprovalForOwner();
|
||||||
_clearClarifyForOwner('terminal');
|
_clearClarifyForOwner('terminal');
|
||||||
let d={};
|
let d={};
|
||||||
@@ -4760,6 +4799,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
|||||||
try{if(source&&source.readyState!==2)source.close();}catch(_){ }
|
try{if(source&&source.readyState!==2)source.close();}catch(_){ }
|
||||||
_clearOwnerInflightState();
|
_clearOwnerInflightState();
|
||||||
_clearStreamHidden(activeSid, streamId); // #4416: terminal path, drop hidden tracker
|
_clearStreamHidden(activeSid, streamId); // #4416: terminal path, drop hidden tracker
|
||||||
|
_clearStreamNotificationBackground(activeSid, streamId);
|
||||||
_clearApprovalForOwner();
|
_clearApprovalForOwner();
|
||||||
_clearClarifyForOwner('cancelled');
|
_clearClarifyForOwner('cancelled');
|
||||||
let _cancelData={};
|
let _cancelData={};
|
||||||
@@ -6307,7 +6347,7 @@ function sendBrowserNotification(title,body,options={}){
|
|||||||
// explicit "Send test" override); only the visibility gate is bypassed.
|
// explicit "Send test" override); only the visibility gate is bypassed.
|
||||||
const forceHidden=!!(options&&options.forceHidden);
|
const forceHidden=!!(options&&options.forceHidden);
|
||||||
if(!force&&!window._notificationsEnabled) return;
|
if(!force&&!window._notificationsEnabled) return;
|
||||||
if(!force&&!forceHidden&&!document.hidden) return;
|
if(!force&&!forceHidden&&!_isBackgroundedForBrowserNotification()) return;
|
||||||
if(!('Notification' in window)) return;
|
if(!('Notification' in window)) return;
|
||||||
if(Notification.permission==='granted'){
|
if(Notification.permission==='granted'){
|
||||||
_showPwaNotification(title,body,options).catch(()=>{try{new Notification(title||assistantDisplayName(),_notificationOptions(body,options));}catch(_err){}});
|
_showPwaNotification(title,body,options).catch(()=>{try{new Notification(title||assistantDisplayName(),_notificationOptions(body,options));}catch(_err){}});
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
"""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
|
||||||
@@ -10,6 +10,18 @@ PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
|
|||||||
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
|
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
|
||||||
CHANGELOG = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
|
CHANGELOG = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
DESKTOP_BACKGROUND_NOTIFICATION_NAMES = (
|
||||||
|
"_desktopBackgroundedForNotifications",
|
||||||
|
"__hermesSetBackgrounded",
|
||||||
|
"_isBackgroundedForBrowserNotification",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _source_between(start_marker: str, end_marker: str) -> str:
|
||||||
|
start = MESSAGES_JS.index(start_marker)
|
||||||
|
end = MESSAGES_JS.index(end_marker, start)
|
||||||
|
return MESSAGES_JS[start:end]
|
||||||
|
|
||||||
|
|
||||||
def test_browser_notifications_use_service_worker_when_available():
|
def test_browser_notifications_use_service_worker_when_available():
|
||||||
assert "function _showPwaNotification" in MESSAGES_JS
|
assert "function _showPwaNotification" in MESSAGES_JS
|
||||||
@@ -25,7 +37,7 @@ def test_notification_payload_uses_completion_session_when_provided():
|
|||||||
assert "_sessionUrlForSid(sid)" in MESSAGES_JS
|
assert "_sessionUrlForSid(sid)" in MESSAGES_JS
|
||||||
assert "data:{url}" in MESSAGES_JS
|
assert "data:{url}" in MESSAGES_JS
|
||||||
assert "tag:sid?`hermes-${sid}`" in MESSAGES_JS
|
assert "tag:sid?`hermes-${sid}`" in MESSAGES_JS
|
||||||
assert "sendBrowserNotification('Response complete',assistantText?assistantText.slice(0,100):'Task finished',{forceHidden:_wasEverHidden,sid:activeSid})" in MESSAGES_JS
|
assert "sendBrowserNotification('Response complete',assistantText?assistantText.slice(0,100):'Task finished',{forceHidden:_wasEverBackgrounded,sid:activeSid})" in MESSAGES_JS
|
||||||
assert "sendBrowserNotification('Approval required',d.description||'Tool approval needed',{sid:activeSid})" in MESSAGES_JS
|
assert "sendBrowserNotification('Approval required',d.description||'Tool approval needed',{sid:activeSid})" in MESSAGES_JS
|
||||||
assert "sendBrowserNotification('Clarification needed',d.question||'Tool clarification needed',{sid:activeSid})" in MESSAGES_JS
|
assert "sendBrowserNotification('Clarification needed',d.question||'Tool clarification needed',{sid:activeSid})" in MESSAGES_JS
|
||||||
|
|
||||||
@@ -42,16 +54,38 @@ def test_completion_notification_fires_when_tab_was_hidden_during_stream():
|
|||||||
assert "function _bindStreamHiddenTracker" in MESSAGES_JS
|
assert "function _bindStreamHiddenTracker" in MESSAGES_JS
|
||||||
# Entries are stream-owned ({streamId, wasHidden}) so a stale entry from a
|
# Entries are stream-owned ({streamId, wasHidden}) so a stale entry from a
|
||||||
# non-`done` terminal path can't be mis-attributed to a later same-sid stream.
|
# non-`done` terminal path can't be mis-attributed to a later same-sid stream.
|
||||||
assert "const _wasEverHidden=!!(_hiddenEntry&&_hiddenEntry.wasHidden);" in MESSAGES_JS
|
assert "function _shouldForceCompletionNotification(sid, streamId){" in MESSAGES_JS
|
||||||
|
assert "return wasHidden||wasBackgrounded;" in MESSAGES_JS
|
||||||
assert "function _clearStreamHidden" in MESSAGES_JS
|
assert "function _clearStreamHidden" in MESSAGES_JS
|
||||||
# Cleared on the non-done terminal paths too (belt-and-suspenders alongside
|
assert "function _clearStreamNotificationBackground" in MESSAGES_JS
|
||||||
# the streamId-ownership guard).
|
# Done-path cleanup lives inside _shouldForceCompletionNotification(); the
|
||||||
assert MESSAGES_JS.count("_clearStreamHidden(activeSid, streamId)") >= 4
|
# activeSid call sites are the non-done terminal paths.
|
||||||
|
assert "_clearStreamHidden(sid, streamId);" in MESSAGES_JS
|
||||||
|
assert "_clearStreamNotificationBackground(sid, streamId);" in MESSAGES_JS
|
||||||
|
assert MESSAGES_JS.count("_clearStreamHidden(activeSid, streamId)") >= 3
|
||||||
|
assert MESSAGES_JS.count("_clearStreamNotificationBackground(activeSid, streamId)") >= 3
|
||||||
# sendBrowserNotification honors forceHidden but still respects the
|
# sendBrowserNotification honors forceHidden but still respects the
|
||||||
# notifications-enabled setting (forceHidden is NOT the test-button force).
|
# notifications-enabled setting (forceHidden is NOT the test-button force).
|
||||||
assert "const forceHidden=!!(options&&options.forceHidden);" in MESSAGES_JS
|
assert "const forceHidden=!!(options&&options.forceHidden);" in MESSAGES_JS
|
||||||
assert "if(!force&&!window._notificationsEnabled) return;" in MESSAGES_JS
|
assert "if(!force&&!window._notificationsEnabled) return;" in MESSAGES_JS
|
||||||
assert "if(!force&&!forceHidden&&!document.hidden) return;" in MESSAGES_JS
|
assert "function _isBackgroundedForBrowserNotification(){" in MESSAGES_JS
|
||||||
|
assert "window.__hermesSetBackgrounded=(value)=>{" in MESSAGES_JS
|
||||||
|
assert "if(!force&&!forceHidden&&!_isBackgroundedForBrowserNotification()) return;" in MESSAGES_JS
|
||||||
|
|
||||||
|
|
||||||
|
def test_desktop_background_notification_signal_stays_out_of_stream_visibility():
|
||||||
|
stream_tracker = _source_between(
|
||||||
|
"const LIVE_STREAMS={};",
|
||||||
|
"function closeLiveStream(sessionId, streamId, source){",
|
||||||
|
)
|
||||||
|
deferred_recovery = _source_between(
|
||||||
|
"function _reattachOrRestoreAfterDeferredStreamError(source){",
|
||||||
|
" // Bug A fix (#631):",
|
||||||
|
)
|
||||||
|
|
||||||
|
for name in DESKTOP_BACKGROUND_NOTIFICATION_NAMES:
|
||||||
|
assert name not in stream_tracker
|
||||||
|
assert name not in deferred_recovery
|
||||||
|
|
||||||
|
|
||||||
def test_service_worker_handles_notification_clicks_without_hijacking_other_sessions():
|
def test_service_worker_handles_notification_clicks_without_hijacking_other_sessions():
|
||||||
|
|||||||
Reference in new issue
Block a user