Sfoglia il codice sorgente

Fix listening quiz choices staying in sync with audio

彭峰 1 settimana fa
parent
commit
792a324895

+ 42 - 0
projects/xiaoshu-mobile/src/app/core/home-learning-modules.integration.spec.ts

@@ -405,6 +405,48 @@ describe('home learning modules', () => {
     expect(play).toHaveBeenCalledTimes(2);
   });
 
+  it('keeps the spoken word among listening choices and discards answers from an earlier question', async () => {
+    const prefix = 'xiaoshu.learning.100.100.practice.';
+    const selectedWords = [
+      { GeneralID: 1001, Title: 'apple', sy: '苹果', yp: 'apple.mp3' },
+      { GeneralID: 1002, Title: 'banana', sy: '香蕉', yp: 'banana.mp3' },
+    ];
+    localStorage.setItem(prefix + 'learn_mode', 'newword');
+    localStorage.setItem(prefix + 'choose_newword_learn', JSON.stringify(selectedWords));
+    localStorage.setItem(prefix + '_page_word_orilist', JSON.stringify([selectedWords]));
+    localStorage.setItem(prefix + '_page_current_learn', '1');
+    const responses = [new Subject<ApiEnvelope<any>>(), new Subject<ApiEnvelope<any>>()];
+    let requestCount = 0;
+    const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
+    const get = api.get.bind(api);
+    spyOn(api, 'get').and.callFake((action, params) => action === 'content_list' && params?.['orders'] === 'NEWID()'
+      ? responses[requestCount++] ?? of({ retcode: 1, result: [] } as ApiEnvelope<any>)
+      : get(action, params));
+    spyOn(TestBed.inject(PlatformService), 'play');
+    const harness = await RouterTestingHarness.create();
+    await harness.navigateByUrl('/pages/stu/stu4');
+    const learning = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
+    const firstWord = learning.current()!;
+    const firstResponse = responses[0];
+    expect(requestCount).toBe(1);
+    expect(learning.choiceOptions().some((option) => option.id === firstWord.id)).toBeTrue();
+    learning.chooseMeaning(firstWord);
+    const secondWord = learning.current()!;
+    expect(secondWord.id).not.toBe(firstWord.id);
+    const secondResponse = responses[1];
+    firstResponse?.next({ retcode: 1, result: [{ GeneralID: 201, Title: 'meeting', sy: '集会' }] });
+    expect(learning.choiceOptions().some((option) => option.id === secondWord.id)).toBeTrue();
+    secondResponse?.next({ retcode: 1, result: Array.from({ length: 8 }, (_, index) => ({
+      GeneralID: 301 + index, Title: `extra-${index}`, sy: `干扰项 ${index}`,
+    })) });
+    expect(learning.choiceOptions().length).toBe(4);
+    expect(learning.choiceOptions().some((option) => option.id === secondWord.id)).toBeTrue();
+    localStorage.removeItem(prefix + 'learn_mode');
+    localStorage.removeItem(prefix + 'choose_newword_learn');
+    localStorage.removeItem(prefix + '_page_word_orilist');
+    localStorage.removeItem(prefix + '_page_current_learn');
+  });
+
   it('lets a student study selected words from their wordbook while keeping course stages teacher-led', async () => {
     localStorage.removeItem('xiaoshu.learning.100.100.practice.learn_mode');
     localStorage.removeItem('xiaoshu.learning.100.100.practice.choose_newword_learn');

+ 20 - 4
projects/xiaoshu-mobile/src/app/features/learning/learning-page.component.ts

@@ -283,6 +283,8 @@ export class LearningPageComponent implements OnInit, OnDestroy {
   private examTimer: ReturnType<typeof setInterval> | null = null;
   private antiStartedAt = Date.now();
   private calendarRequest?: Subscription;
+  private choiceOptionsRequest?: Subscription;
+  private choiceOptionsVersion = 0;
   private stopLiveRefresh?: () => void;
 
   ngOnInit(): void {
@@ -307,6 +309,8 @@ export class LearningPageComponent implements OnInit, OnDestroy {
 
   ngOnDestroy(): void {
     this.calendarRequestId++;this.calendarRequest?.unsubscribe();this.selectionRequest?.unsubscribe();
+    this.choiceOptionsVersion++;
+    this.choiceOptionsRequest?.unsubscribe();
     this.stopLiveRefresh?.();
     this.stopExamTimer();
     this.platform.stopAudio();
@@ -1397,18 +1401,28 @@ export class LearningPageComponent implements OnInit, OnDestroy {
   }
 
   private prepareChoiceOptions(): void {
+    this.choiceOptionsRequest?.unsubscribe();
+    const version = ++this.choiceOptionsVersion;
     const current = this.current();
     if (!current) {
       this.choiceOptions.set([]);
       return;
     }
-    const distractors = this.shuffle(this.words().filter((word) => word.id !== current.id)).slice(0, 3);
+    const distractors = this.shuffle(this.words().filter((word) => word.id !== current.id && word.meaning !== current.meaning)).slice(0, 3);
     this.choiceOptions.set(this.shuffle([current, ...distractors]));
     if (distractors.length < 3) {
-      this.api.get<unknown>('content_list', { modelId: 52, orders: 'NEWID()', cpage: 1, psize: 8 }).subscribe({
+      this.choiceOptionsRequest = this.api.get<unknown>('content_list', { modelId: 52, orders: 'NEWID()', cpage: 1, psize: 8 }).subscribe({
         next: ({ result }) => {
-          const extras = this.findList(result).map((item) => this.toWord(item)).filter((word) => word.id !== current.id && !distractors.some((item) => item.id === word.id));
-          this.choiceOptions.set(this.shuffle([current, ...distractors, ...extras]).slice(0, 4));
+          if (version !== this.choiceOptionsVersion || this.current() !== current || this.completed()) return;
+          const usedIds = new Set([current.id, ...distractors.map((word) => word.id)]);
+          const usedMeanings = new Set([current.meaning, ...distractors.map((word) => word.meaning)]);
+          const extras = this.shuffle(this.findList(result).map((item) => this.toWord(item))).filter((word) => {
+            if (!word.id || !word.meaning || usedIds.has(word.id) || usedMeanings.has(word.meaning)) return false;
+            usedIds.add(word.id);
+            usedMeanings.add(word.meaning);
+            return true;
+          }).slice(0, 3 - distractors.length);
+          this.choiceOptions.set(this.shuffle([current, ...distractors, ...extras]));
         },
         error: () => undefined,
       });
@@ -2026,6 +2040,8 @@ export class LearningPageComponent implements OnInit, OnDestroy {
   private clearLearningStorage(): void { this.draft().clear(); }
 
   private resetPageState(): void {
+    this.choiceOptionsVersion++;
+    this.choiceOptionsRequest?.unsubscribe();
     this.stopExamTimer();
     this.nodes.set([]);
     this.selectedNodes.set(new Set());