Skip to content

Commit bd85529

Browse files
marker-daomarker dao ®
andauthored
STT: Don't change state if SR is unavailable and Reinit SR on customSpeechRecognizer runtime changing (#31213)
Co-authored-by: marker dao ® <[email protected]>
1 parent 5dd5dc5 commit bd85529

File tree

4 files changed

+74
-2
lines changed

4 files changed

+74
-2
lines changed

packages/devextreme/js/__internal/core/speech_recognition_adapter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,8 @@ export class SpeechRecognitionAdapter {
8585
dispose(): void {
8686
this._speechRecognition = null;
8787
}
88+
89+
isAvailable(): boolean {
90+
return Boolean(this._speechRecognition);
91+
}
8892
}

packages/devextreme/js/__internal/ui/speech_to_text/speech_to_text.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,10 @@ class SpeechToText extends Widget<Properties> {
228228
}
229229

230230
private _handleStartClick(e: ClickEvent): void {
231-
if (!this._isCustomSpeechRecognitionEnabled()) {
231+
const isCustomEnabled = this._isCustomSpeechRecognitionEnabled();
232+
const isSRAvailable = this._speechRecognitionAdapter?.isAvailable();
233+
234+
if (!isCustomEnabled && isSRAvailable) {
232235
this._setState(SpeechToTextState.LISTENING);
233236

234237
this._speechRecognitionAdapter?.start();
@@ -306,6 +309,7 @@ class SpeechToText extends Widget<Properties> {
306309
switch (name) {
307310
case 'customSpeechRecognizer':
308311
this._handleCustomEngineState();
312+
this._initSpeechRecognitionAdapter();
309313
break;
310314

311315
case 'speechRecognitionConfig':

packages/devextreme/testing/tests/DevExpress.core/speechRecognitionAdapter.tests.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,5 +165,30 @@ QUnit.module('SpeechRecognitionAdapter', {
165165
speechRecognition.onend();
166166
assert.strictEqual(adapter._isListening, false, 'reset to false after onend');
167167
});
168+
169+
QUnit.test('should return true from isAvailable when SpeechRecognition is supported', function(assert) {
170+
const adapter = this.createAdapter();
171+
172+
assert.strictEqual(adapter.isAvailable(), true, 'isAvailable returns true when SpeechRecognition exists');
173+
});
174+
175+
QUnit.test('should return false from isAvailable when SpeechRecognition is not supported', function(assert) {
176+
window.SpeechRecognition = undefined;
177+
window.webkitSpeechRecognition = undefined;
178+
179+
const adapter = this.createAdapter();
180+
181+
assert.strictEqual(adapter.isAvailable(), false, 'isAvailable returns false when SpeechRecognition is not supported');
182+
});
183+
184+
QUnit.test('should return false from isAvailable after dispose', function(assert) {
185+
const adapter = this.createAdapter();
186+
187+
assert.strictEqual(adapter.isAvailable(), true, 'isAvailable returns true before dispose');
188+
189+
adapter.dispose();
190+
191+
assert.strictEqual(adapter.isAvailable(), false, 'isAvailable returns false after dispose');
192+
});
168193
});
169194

packages/devextreme/testing/tests/DevExpress.ui.widgets/speechToText.tests.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ QUnit.module('State Management', moduleConfig, () => {
259259
const $button = this.getButton();
260260
$button.trigger('dxclick');
261261

262-
assert.ok(this.$element.hasClass(SPEECH_TO_TEXT_LISTENING_CLASS), 'manual control works');
262+
assert.ok(this.$element.hasClass(SPEECH_TO_TEXT_LISTENING_CLASS), 'native speech recognition processes');
263263
});
264264

265265
QUnit.test('should handle state transitions with disabled component', function(assert) {
@@ -648,6 +648,32 @@ QUnit.module('Custom Engine Integration', moduleConfig, () => {
648648

649649
assert.ok(this.$element.hasClass(SPEECH_TO_TEXT_LISTENING_CLASS), 'enabled not missed');
650650
});
651+
652+
QUnit.test('should reinitialize speech recognition adapter when custom engine settings change', function(assert) {
653+
this.reinit({
654+
customSpeechRecognizer: {
655+
enabled: true,
656+
isListening: false,
657+
}
658+
});
659+
660+
assert.strictEqual(this.getAdapter(), undefined, 'adapter is unavailable on init');
661+
662+
this.instance.option('customSpeechRecognizer', {
663+
enabled: false,
664+
isListening: false,
665+
});
666+
667+
const adapter = this.getAdapter();
668+
669+
assert.ok(adapter.isAvailable(), 'adapter is available after CSR reinitialization');
670+
671+
const startSpy = sinon.spy(adapter, 'start');
672+
this.getButton().trigger('dxclick');
673+
674+
assert.ok(startSpy.calledOnce, 'reinitialized adapter works correctly');
675+
assert.ok(this.$element.hasClass(SPEECH_TO_TEXT_LISTENING_CLASS), 'state changes correctly with reinitialized adapter');
676+
});
651677
});
652678

653679
QUnit.module('Options', moduleConfig, () => {
@@ -904,6 +930,19 @@ QUnit.module('SpeechRecognitionAdapter integration', moduleConfig, () => {
904930
assert.ok(startSpy.calledOnce, 'start called once on button click');
905931
});
906932

933+
QUnit.test('should not start recognition when adapter is not available', function(assert) {
934+
this.reinit();
935+
const speechRecognitionAdapter = this.getAdapter();
936+
const startSpy = sinon.spy(speechRecognitionAdapter, 'start');
937+
938+
sinon.stub(speechRecognitionAdapter, 'isAvailable').returns(false);
939+
940+
this.getButton().trigger('dxclick');
941+
942+
assert.ok(startSpy.notCalled, 'start not called when adapter is not available');
943+
assert.ok(!this.$element.hasClass(SPEECH_TO_TEXT_LISTENING_CLASS), 'state not changed to listening when adapter unavailable');
944+
});
945+
907946
QUnit.test('should call stop on speechRecognitionAdapter when stop button clicked', function(assert) {
908947
this.reinit();
909948
const speechRecognitionAdapter = this.getAdapter();

0 commit comments

Comments
 (0)