1 parent
b1a02bdb72
commit
9fb2731848
8 files changed
+246
-40
No files matched your search
@@ -19367,10 +19367,7 @@ def _handle_chat_start(handler, body, diag=None):
|
|||||||
if "model_provider" in body
|
if "model_provider" in body
|
||||||
else getattr(s, "model_provider", None)
|
else getattr(s, "model_provider", None)
|
||||||
)
|
)
|
||||||
<<<<<<< ours
|
|
||||||
_pp_provider, _pp_default, _pp_cfg = _read_profile_model_config(s, requested_provider)
|
_pp_provider, _pp_default, _pp_cfg = _read_profile_model_config(s, requested_provider)
|
||||||
=======
|
|
||||||
>>>>>>> theirs
|
|
||||||
explicit_model_pick = bool(body.get("explicit_model_pick"))
|
explicit_model_pick = bool(body.get("explicit_model_pick"))
|
||||||
moa_config = None
|
moa_config = None
|
||||||
gateway_chat_enabled = webui_gateway_chat_enabled(get_config())
|
gateway_chat_enabled = webui_gateway_chat_enabled(get_config())
|
||||||
@@ -19383,7 +19380,6 @@ def _handle_chat_start(handler, body, diag=None):
|
|||||||
moa_config = resolve_moa_config()
|
moa_config = resolve_moa_config()
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
return bad(handler, str(e), 503)
|
return bad(handler, str(e), 503)
|
||||||
_pp_provider, _pp_default = _read_profile_model_config(s, requested_provider)
|
|
||||||
diag.stage("resolve_model_provider") if diag else None
|
diag.stage("resolve_model_provider") if diag else None
|
||||||
model, model_provider, normalized_model = _resolve_compatible_session_model_state(
|
model, model_provider, normalized_model = _resolve_compatible_session_model_state(
|
||||||
requested_model,
|
requested_model,
|
||||||
|
|||||||
+127
-19
@@ -645,6 +645,11 @@ function _micToastKeyForRecognitionError(error){
|
|||||||
let _finalText='';
|
let _finalText='';
|
||||||
let _prefix='';
|
let _prefix='';
|
||||||
let _isRecording=false;
|
let _isRecording=false;
|
||||||
|
let _micHoldTimer=null;
|
||||||
|
let _micHoldActive=false;
|
||||||
|
let _micPointerDown=false;
|
||||||
|
let _micStartSeq=0;
|
||||||
|
const _micHoldThresholdMs=300;
|
||||||
|
|
||||||
function _setButtonTooltipAndKey(btn, key){
|
function _setButtonTooltipAndKey(btn, key){
|
||||||
const text = t(key);
|
const text = t(key);
|
||||||
@@ -751,14 +756,16 @@ function _micToastKeyForRecognitionError(error){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _stopTracks(){
|
function _stopTracks(stream=mediaStream){
|
||||||
if(mediaStream){
|
if(stream){
|
||||||
mediaStream.getTracks().forEach(track=>track.stop());
|
stream.getTracks().forEach(track=>track.stop());
|
||||||
mediaStream=null;
|
if(mediaStream===stream) mediaStream=null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _stopMic(){
|
function _stopMic(){
|
||||||
|
_micStartSeq+=1;
|
||||||
|
_isRecording=false;
|
||||||
if(!window._micActive) return;
|
if(!window._micActive) return;
|
||||||
// Stop the backend that was ACTIVE WHEN RECORDING STARTED — not whatever
|
// Stop the backend that was ACTIVE WHEN RECORDING STARTED — not whatever
|
||||||
// _rawAudioMode says now. The user can toggle Settings → Sound mid-recording,
|
// _rawAudioMode says now. The user can toggle Settings → Sound mid-recording,
|
||||||
@@ -868,7 +875,31 @@ function _micToastKeyForRecognitionError(error){
|
|||||||
|
|
||||||
_probeServerSttCapability();
|
_probeServerSttCapability();
|
||||||
|
|
||||||
btn.onclick=async()=>{
|
function _clearMicHoldTimer(){
|
||||||
|
if(_micHoldTimer){
|
||||||
|
clearTimeout(_micHoldTimer);
|
||||||
|
_micHoldTimer=null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _resetMicHoldState(){
|
||||||
|
_clearMicHoldTimer();
|
||||||
|
_micHoldActive=false;
|
||||||
|
_micPointerDown=false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _micButtonAvailable(){
|
||||||
|
if(!btn||btn.disabled) return false;
|
||||||
|
if(btn.style.display==='none') return false;
|
||||||
|
if(btn.classList.contains('composer-control-hidden')) return false;
|
||||||
|
if(btn.getAttribute('aria-hidden')==='true') return false;
|
||||||
|
if(window.getComputedStyle&&window.getComputedStyle(btn).display==='none') return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _startMicCapture(holdRequired=false){
|
||||||
|
if(!_micButtonAvailable()) return;
|
||||||
|
const startSeq=++_micStartSeq;
|
||||||
// Race-condition guard: ignore rapid double-clicks
|
// Race-condition guard: ignore rapid double-clicks
|
||||||
if(_isRecording){
|
if(_isRecording){
|
||||||
_stopMic();
|
_stopMic();
|
||||||
@@ -900,26 +931,38 @@ function _micToastKeyForRecognitionError(error){
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try{
|
try{
|
||||||
mediaStream=await navigator.mediaDevices.getUserMedia({audio:true});
|
const captureStream=await navigator.mediaDevices.getUserMedia({audio:true});
|
||||||
|
if(startSeq!==_micStartSeq||!_micButtonAvailable()||(holdRequired&&!_micHoldActive)){
|
||||||
|
_isRecording=false;
|
||||||
|
_stopTracks(captureStream);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mediaStream=captureStream;
|
||||||
const preferredTypes=['audio/webm;codecs=opus','audio/webm','audio/ogg;codecs=opus','audio/ogg'];
|
const preferredTypes=['audio/webm;codecs=opus','audio/webm','audio/ogg;codecs=opus','audio/ogg'];
|
||||||
const mimeType=preferredTypes.find(type=>window.MediaRecorder.isTypeSupported?.(type))||'';
|
const mimeType=preferredTypes.find(type=>window.MediaRecorder.isTypeSupported?.(type))||'';
|
||||||
mediaRecorder=new MediaRecorder(mediaStream,mimeType?{mimeType}:undefined);
|
const captureMode=_rawAudioMode?'media-raw':'media-transcribe';
|
||||||
|
const recorder=new MediaRecorder(captureStream,mimeType?{mimeType}:undefined);
|
||||||
audioChunks=[];
|
audioChunks=[];
|
||||||
mediaRecorder.ondataavailable=e=>{if(e.data&&e.data.size)audioChunks.push(e.data);};
|
const captureChunks=audioChunks;
|
||||||
mediaRecorder.onerror=()=>{
|
recorder.ondataavailable=e=>{if(e.data&&e.data.size)captureChunks.push(e.data);};
|
||||||
|
recorder.onerror=()=>{
|
||||||
|
const isCurrentCapture=mediaRecorder===recorder||mediaStream===captureStream;
|
||||||
_isRecording=false;
|
_isRecording=false;
|
||||||
_setRecording(false);
|
if(mediaRecorder===recorder) mediaRecorder=null;
|
||||||
|
if(isCurrentCapture) _setRecording(false);
|
||||||
window._micPendingSend=false;
|
window._micPendingSend=false;
|
||||||
_stopTracks();
|
_stopTracks(captureStream);
|
||||||
showToast(t('mic_network'));
|
showToast(t('mic_network'));
|
||||||
};
|
};
|
||||||
mediaRecorder.onstop=async()=>{
|
recorder.onstop=async()=>{
|
||||||
|
const isCurrentCapture=mediaRecorder===recorder||mediaStream===captureStream;
|
||||||
|
if(mediaRecorder===recorder) mediaRecorder=null;
|
||||||
_isRecording=false;
|
_isRecording=false;
|
||||||
const blob=new Blob(audioChunks,{type:mediaRecorder.mimeType||mimeType||'audio/webm'});
|
const blob=new Blob(captureChunks,{type:recorder.mimeType||mimeType||'audio/webm'});
|
||||||
_setRecording(false);
|
if(isCurrentCapture) _setRecording(false);
|
||||||
_stopTracks();
|
_stopTracks(captureStream);
|
||||||
if(blob.size){
|
if(blob.size){
|
||||||
if(_activeCaptureMode==='media-raw'){
|
if(captureMode==='media-raw'){
|
||||||
await _sendRawAudio(blob);
|
await _sendRawAudio(blob);
|
||||||
}else{
|
}else{
|
||||||
await _transcribeBlob(blob);
|
await _transcribeBlob(blob);
|
||||||
@@ -930,16 +973,81 @@ function _micToastKeyForRecognitionError(error){
|
|||||||
}
|
}
|
||||||
_applyDeferredServerSttFlip();
|
_applyDeferredServerSttFlip();
|
||||||
};
|
};
|
||||||
_activeCaptureMode=_rawAudioMode?'media-raw':'media-transcribe';
|
_activeCaptureMode=captureMode;
|
||||||
mediaRecorder.start();
|
mediaRecorder=recorder;
|
||||||
|
recorder.start();
|
||||||
_setRecording(true);
|
_setRecording(true);
|
||||||
}catch(err){
|
}catch(err){
|
||||||
|
if(startSeq!==_micStartSeq) return;
|
||||||
_isRecording=false;
|
_isRecording=false;
|
||||||
window._micPendingSend=false;
|
window._micPendingSend=false;
|
||||||
_stopTracks();
|
_stopTracks();
|
||||||
showToast(t(_micToastKeyForRecognitionError('not-allowed')||'mic_denied'));
|
showToast(t(_micToastKeyForRecognitionError('not-allowed')||'mic_denied'));
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
async function _toggleMicCapture(){
|
||||||
|
if(!_micButtonAvailable()) return;
|
||||||
|
if(window._micActive){
|
||||||
|
_stopMic();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _startMicCapture();
|
||||||
|
}
|
||||||
|
window._toggleMicCapture=_toggleMicCapture;
|
||||||
|
|
||||||
|
btn.addEventListener('pointerdown',e=>{
|
||||||
|
if(e.button!==0) return;
|
||||||
|
if(!_micButtonAvailable()) return;
|
||||||
|
_resetMicHoldState();
|
||||||
|
_micPointerDown=true;
|
||||||
|
_micHoldTimer=setTimeout(async()=>{
|
||||||
|
_micHoldTimer=null;
|
||||||
|
if(!_micPointerDown||window._micActive) return;
|
||||||
|
_micHoldActive=true;
|
||||||
|
await _startMicCapture(true);
|
||||||
|
},_micHoldThresholdMs);
|
||||||
|
});
|
||||||
|
|
||||||
|
btn.addEventListener('pointerup',async e=>{
|
||||||
|
if(e.button!==0||!_micPointerDown) return;
|
||||||
|
_clearMicHoldTimer();
|
||||||
|
if(_micHoldActive){
|
||||||
|
_micHoldActive=false;
|
||||||
|
_micPointerDown=false;
|
||||||
|
_stopMic();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_micPointerDown=false;
|
||||||
|
await _toggleMicCapture();
|
||||||
|
});
|
||||||
|
|
||||||
|
btn.addEventListener('pointerleave',()=>{
|
||||||
|
_clearMicHoldTimer();
|
||||||
|
if(_micHoldActive){
|
||||||
|
_micHoldActive=false;
|
||||||
|
_micPointerDown=false;
|
||||||
|
_stopMic();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_micPointerDown=false;
|
||||||
|
});
|
||||||
|
|
||||||
|
btn.addEventListener('pointercancel',()=>{
|
||||||
|
_clearMicHoldTimer();
|
||||||
|
if(_micHoldActive){
|
||||||
|
_micHoldActive=false;
|
||||||
|
_micPointerDown=false;
|
||||||
|
_stopMic();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_micPointerDown=false;
|
||||||
|
});
|
||||||
|
|
||||||
|
btn.addEventListener('click',async e=>{
|
||||||
|
if(e.detail!==0) return;
|
||||||
|
await _toggleMicCapture();
|
||||||
|
});
|
||||||
|
|
||||||
// Wire up the settings checkbox
|
// Wire up the settings checkbox
|
||||||
const rawAudioCheckbox = document.getElementById('settingsRawAudio');
|
const rawAudioCheckbox = document.getElementById('settingsRawAudio');
|
||||||
|
|||||||
+9
-1
@@ -1644,7 +1644,15 @@ function cmdReasoning(args){
|
|||||||
}
|
}
|
||||||
function cmdVoice(){
|
function cmdVoice(){
|
||||||
const mic=document.getElementById('btnMic');
|
const mic=document.getElementById('btnMic');
|
||||||
if(mic&&mic.style.display!=='none'&&!mic.disabled){try{mic.click();return;}catch(_){}}
|
const micVisible=!!(
|
||||||
|
mic
|
||||||
|
&& mic.style.display!=='none'
|
||||||
|
&& !mic.disabled
|
||||||
|
&& !mic.classList.contains('composer-control-hidden')
|
||||||
|
&& mic.getAttribute('aria-hidden')!=='true'
|
||||||
|
&& (!window.getComputedStyle||window.getComputedStyle(mic).display!=='none')
|
||||||
|
);
|
||||||
|
if(micVisible){try{mic.click();return;}catch(_){}}
|
||||||
showToast(t('cmd_voice_use_mic'));
|
showToast(t('cmd_voice_use_mic'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -177,13 +177,15 @@ def test_590_transcribing_status_shown_before_fetch():
|
|||||||
|
|
||||||
|
|
||||||
def test_590_recording_stops_before_transcribe():
|
def test_590_recording_stops_before_transcribe():
|
||||||
"""boot.js: _setRecording(false) must fire in onstop before _transcribeBlob."""
|
"""boot.js: _setRecording(false) must fire in onstop before the pinned send path."""
|
||||||
onstop_start = BOOT_JS.find("mediaRecorder.onstop")
|
onstop_start = BOOT_JS.find("recorder.onstop")
|
||||||
assert onstop_start != -1, "mediaRecorder.onstop not found"
|
assert onstop_start != -1, "recorder.onstop not found"
|
||||||
onstop_body = BOOT_JS[onstop_start:onstop_start + 400]
|
onstop_body = BOOT_JS[onstop_start:onstop_start + 700]
|
||||||
rec_pos = onstop_body.find("_setRecording(false)")
|
rec_pos = onstop_body.find("_setRecording(false)")
|
||||||
blob_pos = onstop_body.find("_transcribeBlob(")
|
blob_pos = onstop_body.find("_transcribeBlob(")
|
||||||
assert rec_pos != -1 and blob_pos != -1
|
raw_pos = onstop_body.find("_sendRawAudio(")
|
||||||
assert rec_pos < blob_pos, (
|
send_pos = raw_pos if raw_pos != -1 and (blob_pos == -1 or raw_pos < blob_pos) else blob_pos
|
||||||
"_setRecording(false) must come before _transcribeBlob so mic icon clears immediately"
|
assert rec_pos != -1 and send_pos != -1
|
||||||
|
assert rec_pos < send_pos, (
|
||||||
|
"_setRecording(false) must come before the pinned send path so the mic icon clears immediately"
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _boot_src() -> str:
|
||||||
|
return Path("static/boot.js").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _commands_src() -> str:
|
||||||
|
return Path("static/commands.js").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_mic_capture_extracted():
|
||||||
|
src = _boot_src()
|
||||||
|
assert re.search(r"async function _startMicCapture\([^)]*\)\{", src)
|
||||||
|
assert "_activeCaptureMode='speech'" in src
|
||||||
|
assert "const captureMode=_rawAudioMode?'media-raw':'media-transcribe';" in src
|
||||||
|
assert "_activeCaptureMode=captureMode;" in src
|
||||||
|
|
||||||
|
|
||||||
|
def test_toggle_mic_capture_exposed_on_window():
|
||||||
|
src = _boot_src()
|
||||||
|
assert "async function _toggleMicCapture()" in src
|
||||||
|
assert "window._toggleMicCapture=_toggleMicCapture;" in src
|
||||||
|
|
||||||
|
|
||||||
|
def test_toggle_helper_respects_hidden_mic_setting():
|
||||||
|
src = _boot_src()
|
||||||
|
assert "function _micButtonAvailable()" in src
|
||||||
|
assert "btn.classList.contains('composer-control-hidden')" in src
|
||||||
|
assert "btn.getAttribute('aria-hidden')==='true'" in src
|
||||||
|
|
||||||
|
|
||||||
|
def test_voice_command_respects_hidden_mic_setting_before_clicking():
|
||||||
|
src = _commands_src()
|
||||||
|
assert "function cmdVoice()" in src
|
||||||
|
assert "mic.classList.contains('composer-control-hidden')" in src
|
||||||
|
assert "mic.getAttribute('aria-hidden')!=='true'" in src
|
||||||
|
assert "showToast(t('cmd_voice_use_mic'));" in src
|
||||||
|
|
||||||
|
|
||||||
|
def test_old_onclick_handler_removed():
|
||||||
|
src = _boot_src()
|
||||||
|
assert "btn.onclick=async()=>{" not in src
|
||||||
|
|
||||||
|
|
||||||
|
def test_pointer_hold_wiring_present():
|
||||||
|
src = _boot_src()
|
||||||
|
assert "let _micHoldTimer=null;" in src
|
||||||
|
assert "let _micHoldActive=false;" in src
|
||||||
|
assert "let _micPointerDown=false;" in src
|
||||||
|
assert "let _micStartSeq=0;" in src
|
||||||
|
assert "const _micHoldThresholdMs=300;" in src
|
||||||
|
assert "btn.addEventListener('pointerdown'" in src
|
||||||
|
assert "btn.addEventListener('pointerup'" in src
|
||||||
|
assert (
|
||||||
|
"btn.addEventListener('pointerleave'" in src
|
||||||
|
or "btn.addEventListener('pointercancel'" in src
|
||||||
|
)
|
||||||
|
assert "_micHoldTimer=setTimeout(async()=>{" in src
|
||||||
|
assert "},_micHoldThresholdMs);" in src
|
||||||
|
|
||||||
|
|
||||||
|
def test_hold_release_routes_to_stop_mic():
|
||||||
|
src = _boot_src()
|
||||||
|
assert re.search(
|
||||||
|
r"btn\.addEventListener\('pointerup',async e=>\{.*?_stopMic\(\);.*?\}\);",
|
||||||
|
src,
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
assert re.search(
|
||||||
|
r"btn\.addEventListener\('pointer(cancel|leave)',\(\)=>\{.*?_stopMic\(\);.*?\}\);",
|
||||||
|
src,
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
assert "const startSeq=++_micStartSeq;" in src
|
||||||
|
assert "if(startSeq!==_micStartSeq||!_micButtonAvailable()||(holdRequired&&!_micHoldActive)){" in src
|
||||||
|
assert "_stopTracks(captureStream);" in src
|
||||||
|
assert "if(startSeq!==_micStartSeq) return;" in src
|
||||||
|
|
||||||
|
|
||||||
|
def test_reserved_ctrl_shift_d_shortcut_is_not_bound():
|
||||||
|
src = _boot_src()
|
||||||
|
assert "(e.metaKey||e.ctrlKey)&&e.shiftKey&&!e.altKey&&(e.key==='d'||e.key==='D')" not in src
|
||||||
|
assert "await window._toggleMicCapture();" not in src
|
||||||
|
|
||||||
|
|
||||||
|
def test_stop_mic_still_exposed_and_active_capture_mode_retained():
|
||||||
|
src = _boot_src()
|
||||||
|
assert "window._stopMic=_stopMic;" in src
|
||||||
|
assert "_activeCaptureMode" in src
|
||||||
|
assert "_stopMic();" in src
|
||||||
@@ -70,8 +70,8 @@ def test_permission_style_speech_errors_use_insecure_origin_key_only_on_that_bra
|
|||||||
def test_dictation_preflights_insecure_origin_before_speech_or_media_capture():
|
def test_dictation_preflights_insecure_origin_before_speech_or_media_capture():
|
||||||
body = _slice_between(
|
body = _slice_between(
|
||||||
BOOT_JS,
|
BOOT_JS,
|
||||||
"btn.onclick=async()=>{",
|
"async function _startMicCapture(holdRequired=false){",
|
||||||
"\n // Wire up the settings checkbox",
|
"\n\n async function _toggleMicCapture(){",
|
||||||
)
|
)
|
||||||
insecure_idx = body.index("_micOriginNeedsSecureContext()")
|
insecure_idx = body.index("_micOriginNeedsSecureContext()")
|
||||||
speech_idx = body.index("recognition.start()")
|
speech_idx = body.index("recognition.start()")
|
||||||
|
|||||||
@@ -159,10 +159,11 @@ def test_stop_mic_uses_pinned_active_capture_mode_not_current_rawmode():
|
|||||||
# _stopMic decides backend from the pinned mode, not _rawAudioMode.
|
# _stopMic decides backend from the pinned mode, not _rawAudioMode.
|
||||||
assert "_activeCaptureMode==='speech'" in _BOOT_JS
|
assert "_activeCaptureMode==='speech'" in _BOOT_JS
|
||||||
# onstop dispatches raw-vs-transcribe from the pinned mode too.
|
# onstop dispatches raw-vs-transcribe from the pinned mode too.
|
||||||
assert "_activeCaptureMode==='media-raw'" in _BOOT_JS
|
assert "if(captureMode==='media-raw')" in _BOOT_JS
|
||||||
# the mode is pinned at both start branches.
|
# the mode is pinned at both start branches.
|
||||||
assert "_activeCaptureMode='speech'" in _BOOT_JS
|
assert "_activeCaptureMode='speech'" in _BOOT_JS
|
||||||
assert "_activeCaptureMode=_rawAudioMode?'media-raw':'media-transcribe'" in _BOOT_JS
|
assert "const captureMode=_rawAudioMode?'media-raw':'media-transcribe';" in _BOOT_JS
|
||||||
|
assert "_activeCaptureMode=captureMode;" in _BOOT_JS
|
||||||
|
|
||||||
|
|
||||||
def test_send_raw_audio_honors_explicit_pending_send():
|
def test_send_raw_audio_honors_explicit_pending_send():
|
||||||
|
|||||||
@@ -425,11 +425,10 @@ def test_boot_js_prefix_variable_declared():
|
|||||||
def test_boot_js_prefix_captured_on_start():
|
def test_boot_js_prefix_captured_on_start():
|
||||||
"""_prefix must be set from ta.value when the user starts recording."""
|
"""_prefix must be set from ta.value when the user starts recording."""
|
||||||
js, _ = get_text("/static/boot.js")
|
js, _ = get_text("/static/boot.js")
|
||||||
# _prefix assignment must happen in the btn.onclick else branch (before recognition.start)
|
start_idx = js.find("async function _startMicCapture")
|
||||||
btn_onclick_idx = js.find("btn.onclick")
|
start_end = js.find("async function _toggleMicCapture", start_idx)
|
||||||
btn_onclick_end = js.find("};", btn_onclick_idx)
|
start_body = js[start_idx:start_end]
|
||||||
onclick_body = js[btn_onclick_idx:btn_onclick_end]
|
assert "_prefix=ta.value" in start_body or "_prefix = ta.value" in start_body
|
||||||
assert "_prefix=ta.value" in onclick_body or "_prefix = ta.value" in onclick_body
|
|
||||||
|
|
||||||
|
|
||||||
def test_boot_js_onresult_prepends_prefix():
|
def test_boot_js_onresult_prepends_prefix():
|
||||||
|
|||||||
Reference in new issue
Block a user