|
@@ -2402,12 +2402,12 @@ describe('home learning modules', () => {
|
|
|
expect(harness.routeNativeElement?.querySelector('.review-complete')?.textContent).toContain('复习已完成');expect(harness.routeNativeElement?.querySelector('.review-complete')?.textContent).not.toMatch(/收入|费用|计薪|¥/);expect(component.canDeactivate()).toBeTrue();
|
|
expect(harness.routeNativeElement?.querySelector('.review-complete')?.textContent).toContain('复习已完成');expect(harness.routeNativeElement?.querySelector('.review-complete')?.textContent).not.toMatch(/收入|费用|计薪|¥/);expect(component.canDeactivate()).toBeTrue();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- async function teacherSelection(count: number, pass = 0) {
|
|
|
|
|
|
|
+ async function teacherSelection(count: number, pass = 0, orderId = '') {
|
|
|
user.set({userId:300,groupId:3}); session.groupId.set(3); session.student.set({userId:100,groupId:1});
|
|
user.set({userId:300,groupId:3}); session.groupId.set(3); session.student.set({userId:100,groupId:1});
|
|
|
const api=TestBed.inject(ApiService) as unknown as LearningModulesApiService;
|
|
const api=TestBed.inject(ApiService) as unknown as LearningModulesApiService;
|
|
|
api.courseWords=Array.from({length:count},(_,i)=>({GeneralID:1001+i,NodeID:811,Title:'word-'+(i+1),sy:'释义 '+(i+1),w_learned:pass}));
|
|
api.courseWords=Array.from({length:count},(_,i)=>({GeneralID:1001+i,NodeID:811,Title:'word-'+(i+1),sy:'释义 '+(i+1),w_learned:pass}));
|
|
|
const harness=await RouterTestingHarness.create();
|
|
const harness=await RouterTestingHarness.create();
|
|
|
- await harness.navigateByUrl('/pages/stu/stu_abc?nids=811&pnid=801');
|
|
|
|
|
|
|
+ await harness.navigateByUrl('/pages/stu/stu_abc?nids=811&pnid=801' + (orderId ? '&orderId=' + orderId : ''));
|
|
|
await harness.fixture.whenStable(); harness.fixture.detectChanges();
|
|
await harness.fixture.whenStable(); harness.fixture.detectChanges();
|
|
|
const component=harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
|
|
const component=harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
|
|
|
if(pass>0){component.selectCoursePass(pass);await harness.fixture.whenStable();harness.fixture.detectChanges();}
|
|
if(pass>0){component.selectCoursePass(pass);await harness.fixture.whenStable();harness.fixture.detectChanges();}
|
|
@@ -2473,7 +2473,7 @@ describe('home learning modules', () => {
|
|
|
const select=view().querySelector('.bulk-select-toggle') as HTMLButtonElement;
|
|
const select=view().querySelector('.bulk-select-toggle') as HTMLButtonElement;
|
|
|
expect(select).not.toBeNull();select?.click();harness.fixture.detectChanges();
|
|
expect(select).not.toBeNull();select?.click();harness.fixture.detectChanges();
|
|
|
expect(view().querySelectorAll('.word-select-check[aria-pressed="true"]').length).toBe(8);
|
|
expect(view().querySelectorAll('.word-select-check[aria-pressed="true"]').length).toBe(8);
|
|
|
- expect(component.selected().size).toBe(0); // Temporary bulk checks are independent of the 30-word study round.
|
|
|
|
|
|
|
+ expect(component.selected().size).toBe(0); // Temporary bulk checks do not change the actual learning selection.
|
|
|
const checks=view().querySelectorAll<HTMLButtonElement>('.word-select-check');checks[1].click();checks[6].click();harness.fixture.detectChanges();
|
|
const checks=view().querySelectorAll<HTMLButtonElement>('.word-select-check');checks[1].click();checks[6].click();harness.fixture.detectChanges();
|
|
|
(view().querySelector('.bulk-assessment .known-button') as HTMLButtonElement).click();
|
|
(view().querySelector('.bulk-assessment .known-button') as HTMLButtonElement).click();
|
|
|
await harness.fixture.whenStable();harness.fixture.detectChanges();
|
|
await harness.fixture.whenStable();harness.fixture.detectChanges();
|
|
@@ -2495,13 +2495,15 @@ describe('home learning modules', () => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
it('keeps bulk unknown checks intact when they exceed a round and clears them across pass switches', async () => {
|
|
it('keeps bulk unknown checks intact when they exceed a round and clears them across pass switches', async () => {
|
|
|
- const {api,harness,component}=await teacherSelection(31);
|
|
|
|
|
|
|
+ const {api,harness,component}=await teacherSelection(101);
|
|
|
|
|
+ component.loadMoreSelection();await harness.fixture.whenStable();harness.fixture.detectChanges();
|
|
|
|
|
+ component.toggleWord(component.words()[100]);
|
|
|
const toast=spyOn(TestBed.inject(PlatformService),'toast');
|
|
const toast=spyOn(TestBed.inject(PlatformService),'toast');
|
|
|
(harness.routeNativeElement!.querySelector('.bulk-select-toggle') as HTMLButtonElement)?.click();harness.fixture.detectChanges();
|
|
(harness.routeNativeElement!.querySelector('.bulk-select-toggle') as HTMLButtonElement)?.click();harness.fixture.detectChanges();
|
|
|
(harness.routeNativeElement!.querySelector('.bulk-assessment .unknown-button') as HTMLButtonElement)?.click();harness.fixture.detectChanges();
|
|
(harness.routeNativeElement!.querySelector('.bulk-assessment .unknown-button') as HTMLButtonElement)?.click();harness.fixture.detectChanges();
|
|
|
- expect(toast).toHaveBeenCalledWith(jasmine.stringMatching(/30/),'error');
|
|
|
|
|
- expect(harness.routeNativeElement!.querySelectorAll('.word-select-check[aria-pressed="true"]').length).toBe(31);
|
|
|
|
|
- expect(component.selected().size).toBe(0);expect(api.courseIncrements.size).toBe(0);
|
|
|
|
|
|
|
+ expect(toast).toHaveBeenCalledWith(jasmine.stringMatching(/100/),'error');
|
|
|
|
|
+ expect(harness.routeNativeElement!.querySelectorAll('.word-select-check[aria-pressed="true"]').length).toBe(100);
|
|
|
|
|
+ expect(component.selected().size).toBe(1);expect(api.courseIncrements.size).toBe(0);
|
|
|
component.selectCoursePass(1);await harness.fixture.whenStable();harness.fixture.detectChanges();
|
|
component.selectCoursePass(1);await harness.fixture.whenStable();harness.fixture.detectChanges();
|
|
|
expect(harness.routeNativeElement!.querySelector('.bulk-assessment')).toBeNull();
|
|
expect(harness.routeNativeElement!.querySelector('.bulk-assessment')).toBeNull();
|
|
|
component.selectCoursePass(0);await harness.fixture.whenStable();harness.fixture.detectChanges();
|
|
component.selectCoursePass(0);await harness.fixture.whenStable();harness.fixture.detectChanges();
|
|
@@ -2537,6 +2539,8 @@ describe('home learning modules', () => {
|
|
|
}) as typeof api.post);
|
|
}) as typeof api.post);
|
|
|
const first=component.words()[0];
|
|
const first=component.words()[0];
|
|
|
(harness.routeNativeElement!.querySelector('.bulk-select-toggle') as HTMLButtonElement).click();harness.fixture.detectChanges();
|
|
(harness.routeNativeElement!.querySelector('.bulk-select-toggle') as HTMLButtonElement).click();harness.fixture.detectChanges();
|
|
|
|
|
+ for (const word of component.bulkCandidates().slice(100)) component.toggleWord(word);
|
|
|
|
|
+ harness.fixture.detectChanges();
|
|
|
(harness.routeNativeElement!.querySelector('.bulk-assessment .known-button') as HTMLButtonElement).click();
|
|
(harness.routeNativeElement!.querySelector('.bulk-assessment .known-button') as HTMLButtonElement).click();
|
|
|
await harness.fixture.whenStable();harness.fixture.detectChanges();
|
|
await harness.fixture.whenStable();harness.fixture.detectChanges();
|
|
|
expect(harness.routeNativeElement!.querySelector('.selection-retry-note')?.textContent).toContain('尚待确认');
|
|
expect(harness.routeNativeElement!.querySelector('.selection-retry-note')?.textContent).toContain('尚待确认');
|
|
@@ -2613,6 +2617,142 @@ describe('home learning modules', () => {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ it('marks only the requested 20 course words known without learning the other 80', async () => {
|
|
|
|
|
+ const {api,harness,component} = await teacherSelection(100);
|
|
|
|
|
+ spyOn(TestBed.inject(PlatformService), 'toast');
|
|
|
|
|
+ const picker = harness.routeNativeElement!.querySelector<HTMLSelectElement>('.bulk-select-size')!;
|
|
|
|
|
+ picker.value='20';picker.dispatchEvent(new Event('change'));harness.fixture.detectChanges();
|
|
|
|
|
+ (harness.routeNativeElement!.querySelector('.bulk-select-toggle') as HTMLButtonElement).click();harness.fixture.detectChanges();
|
|
|
|
|
+ (harness.routeNativeElement!.querySelector('.bulk-assessment .known-button') as HTMLButtonElement).click();
|
|
|
|
|
+ await harness.fixture.whenStable();harness.fixture.detectChanges();
|
|
|
|
|
+ expect(api.courseIncrements.size).toBe(20);expect(component.selected().size).toBe(0);
|
|
|
|
|
+ expect(component.coursePassCounts()).toEqual([80,20,0,0]);
|
|
|
|
|
+ expect(TestBed.inject(Router).url).toContain('/pages/stu/stu_abc');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('preserves individual wordbook choices when a quantity selection request fails', async () => {
|
|
|
|
|
+ spyOn(TestBed.inject(PlatformService), 'toast');
|
|
|
|
|
+ const api=TestBed.inject(ApiService), original=api.get.bind(api), pending=new Subject<ApiEnvelope<unknown>>();
|
|
|
|
|
+ let reads=0;
|
|
|
|
|
+ spyOn(api,'get').and.callFake(((action:string,params:Record<string,unknown>={})=>{
|
|
|
|
|
+ if(action!=='e_words_list')return original(action,params);
|
|
|
|
|
+ if(reads++)return pending;
|
|
|
|
|
+ return of({retcode:0,result:[{GeneralID:601,detail:[{GeneralID:701,Title:'apple',sy:'苹果'}]}],page:{itemCount:1,pendingCount:1,completedCount:0}});
|
|
|
|
|
+ }) as typeof api.get);
|
|
|
|
|
+ const harness=await RouterTestingHarness.create('/pages/member/starshop');
|
|
|
|
|
+ await harness.fixture.whenStable();harness.fixture.detectChanges();
|
|
|
|
|
+ const component=harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
|
|
|
|
|
+ (harness.routeNativeElement!.querySelector('.word-select-list > button') as HTMLButtonElement).click();harness.fixture.detectChanges();
|
|
|
|
|
+ (harness.routeNativeElement!.querySelector('.selection-header .quiet-button') as HTMLButtonElement).click();harness.fixture.detectChanges();
|
|
|
|
|
+ expect(component.selectionLoading()).toBeTrue();
|
|
|
|
|
+ pending.error(new Error('temporary failure'));harness.fixture.detectChanges();
|
|
|
|
|
+ expect([...component.selected()]).toEqual(['701']);expect(component.selectionLoading()).toBeFalse();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ for (const entry of ['course-0', 'course-1', 'course-2', 'coach-wordbook', 'student-wordbook']) for (const startCount of [20,50,100]) {
|
|
|
|
|
+ it(`selects only the requested quantity and starts ${startCount} words for ${entry}`, async () => {
|
|
|
|
|
+ spyOn(TestBed.inject(PlatformService), 'toast');
|
|
|
|
|
+ let harness: RouterTestingHarness;
|
|
|
|
|
+ if (entry.startsWith('course-')) {
|
|
|
|
|
+ harness = (await teacherSelection(100, Number(entry.slice(-1)), entry === 'course-0' ? '901' : '')).harness;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (entry === 'coach-wordbook') {
|
|
|
|
|
+ user.set({userId:300,groupId:3});session.groupId.set(3);
|
|
|
|
|
+ }
|
|
|
|
|
+ const api = TestBed.inject(ApiService), original = api.get.bind(api);
|
|
|
|
|
+ spyOn(api, 'get').and.callFake(((action: string, params: Record<string, unknown> = {}) => {
|
|
|
|
|
+ if (action === 'e_user_list') return of({retcode:0,result:[{userId:100,honeyName:'学员甲'}]});
|
|
|
|
|
+ if (action === 'e_words_list') {
|
|
|
|
|
+ const all = Array.from({length:100}, (_,i) => ({GeneralID:601+i,detail:[{GeneralID:1001+i,Title:'word-'+i,sy:'释义 '+i}]}));
|
|
|
|
|
+ const size = Number(params['psize']), page = Number(params['cpage']);
|
|
|
|
|
+ return of({retcode:0,result:all.slice((page-1)*size,page*size),page:{itemCount:100,pendingCount:100,completedCount:0,pageCount:Math.ceil(100/size)}});
|
|
|
|
|
+ }
|
|
|
|
|
+ return original(action,params);
|
|
|
|
|
+ }) as typeof api.get);
|
|
|
|
|
+ harness = await RouterTestingHarness.create('/pages/member/starshop');
|
|
|
|
|
+ await harness.fixture.whenStable();harness.fixture.detectChanges();
|
|
|
|
|
+ }
|
|
|
|
|
+ const component = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
|
|
|
|
|
+ const picker = harness.routeNativeElement!.querySelector<HTMLSelectElement>('.bulk-select-size');
|
|
|
|
|
+ expect(picker).withContext('every editable selection entry has a quantity picker').not.toBeNull();
|
|
|
|
|
+ if (!picker) return;
|
|
|
|
|
+ expect(Array.from(picker.options).map(option => option.value)).toEqual(['20','50','100']);
|
|
|
|
|
+ expect(picker.value).toBe('100');
|
|
|
|
|
+ const clickAll = () => {
|
|
|
|
|
+ (harness.routeNativeElement!.querySelector('.bulk-select-toggle, .quiet-button') as HTMLButtonElement).click();
|
|
|
|
|
+ harness.fixture.detectChanges();
|
|
|
|
|
+ };
|
|
|
|
|
+ for (const count of [20,50,100]) {
|
|
|
|
|
+ picker.value = String(count);picker.dispatchEvent(new Event('change'));harness.fixture.detectChanges();
|
|
|
|
|
+ clickAll();
|
|
|
|
|
+ expect((component.bulkSelection() ?? component.selected()).size).toBe(count);
|
|
|
|
|
+ if (count < 100) {clickAll();expect((component.bulkSelection() ?? component.selected()).size).toBe(0);}
|
|
|
|
|
+ }
|
|
|
|
|
+ if (startCount !== 100) {
|
|
|
|
|
+ clickAll();picker.value = String(startCount);picker.dispatchEvent(new Event('change'));harness.fixture.detectChanges();clickAll();
|
|
|
|
|
+ }
|
|
|
|
|
+ const button = entry.startsWith('course-') ? '.bulk-assessment .unknown-button' : '.selection-header .primary-button';
|
|
|
|
|
+ (harness.routeNativeElement!.querySelector(button) as HTMLButtonElement).click();
|
|
|
|
|
+ await harness.fixture.whenStable();harness.fixture.detectChanges();
|
|
|
|
|
+ expect(TestBed.inject(Router).url).toContain('/pages/stu/stu_con');
|
|
|
|
|
+ const pages = JSON.parse(localStorage.getItem(entry === 'coach-wordbook'
|
|
|
|
|
+ ? 'xiaoshu.learning.300.100.wordbook._page_word_orilist'
|
|
|
|
|
+ : entry === 'student-wordbook' ? 'xiaoshu.learning.100.100.practice._page_word_orilist'
|
|
|
|
|
+ : entry === 'course-0' ? 'xiaoshu.learning.300.100.901.unbound._page_word_orilist'
|
|
|
|
|
+ : 'xiaoshu.learning.300.100.practice._page_word_orilist') || '[]');
|
|
|
|
|
+ expect(pages.flat().length).toBe(startCount);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const entry of ['course', 'coach-wordbook', 'student-wordbook'] as const) {
|
|
|
|
|
+ it(`hides the answer on the third point-reading tap in every round for ${entry}`, async () => {
|
|
|
|
|
+ let harness: RouterTestingHarness;
|
|
|
|
|
+ const platform = TestBed.inject(PlatformService);
|
|
|
|
|
+ spyOn(platform, 'toast');
|
|
|
|
|
+ if (entry === 'course') {
|
|
|
|
|
+ const selection = await teacherSelection(2,0,'901');
|
|
|
|
|
+ harness = selection.harness;
|
|
|
|
|
+ selection.component.selected.set(new Set(selection.component.words().map(word => word.id)));
|
|
|
|
|
+ selection.component.startSelected();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (entry === 'coach-wordbook') {
|
|
|
|
|
+ user.set({userId:300,groupId:3});session.groupId.set(3);session.student.set({userId:100,groupId:1});
|
|
|
|
|
+ }
|
|
|
|
|
+ const api = TestBed.inject(ApiService), original = api.get.bind(api);
|
|
|
|
|
+ spyOn(api, 'get').and.callFake(((action: string, params: Record<string, unknown> = {}) => {
|
|
|
|
|
+ if (action === 'e_user_list') return of({retcode:0,result:[{userId:100,honeyName:'学员甲'}]});
|
|
|
|
|
+ if (action === 'e_words_list') return of({retcode:0,result:[{GeneralID:601,detail:[{GeneralID:701,Title:'apple',sy:'苹果',yb:'/ˈæpəl/'}]},{GeneralID:602,detail:[{GeneralID:702,Title:'banana',sy:'香蕉',yb:'/bəˈnɑːnə/'}]}]});
|
|
|
|
|
+ return original(action,params);
|
|
|
|
|
+ }) as typeof api.get);
|
|
|
|
|
+ harness = await RouterTestingHarness.create('/pages/member/starshop' + (entry === 'coach-wordbook' ? '?wordbookStudentId=100' : ''));
|
|
|
|
|
+ await harness.fixture.whenStable();harness.fixture.detectChanges();
|
|
|
|
|
+ for (const button of Array.from(harness.routeNativeElement!.querySelectorAll<HTMLButtonElement>('.word-select-list > button'))) button.click();
|
|
|
|
|
+ harness.fixture.detectChanges();
|
|
|
|
|
+ (harness.routeNativeElement!.querySelector('.selection-header .primary-button') as HTMLButtonElement).click();
|
|
|
|
|
+ }
|
|
|
|
|
+ await harness.fixture.whenStable();harness.fixture.detectChanges();
|
|
|
|
|
+ expect(TestBed.inject(Router).url).toContain('/pages/stu/stu_con');
|
|
|
|
|
+ const component = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
|
|
|
|
|
+ expect(component.words().length).toBe(2);
|
|
|
|
|
+ component.words.update(words => words.map(word => ({...word,phonetic:word.phonetic || '/wɜːd/'})));
|
|
|
|
|
+ spyOn(component,'play').and.resolveTo();
|
|
|
|
|
+ for (const step of [1,2,3] as const) {
|
|
|
|
|
+ expect(component.studyStep()).toBe(step);harness.fixture.detectChanges();
|
|
|
|
|
+ const first = component.words()[0], row = harness.routeNativeElement!.querySelector<HTMLButtonElement>('.repeat-list > button')!;
|
|
|
|
|
+ const answer = () => row.querySelector('small:not(.study-phonetic)');
|
|
|
|
|
+ expect(row.querySelector('strong')?.textContent).toBe(step===3 ? first.meaning : first.word);
|
|
|
|
|
+ expect(answer()).toBeNull();
|
|
|
|
|
+ row.click();harness.fixture.detectChanges();expect(answer()).toBeNull();
|
|
|
|
|
+ if(step!==3)expect(row.querySelector('.study-phonetic')?.textContent).toBe(first.phonetic);
|
|
|
|
|
+ row.click();harness.fixture.detectChanges();expect(answer()?.textContent).toBe(step===3 ? first.word : first.meaning);
|
|
|
|
|
+ row.click();harness.fixture.detectChanges();expect(answer()).toBeNull();expect(row.querySelectorAll('.repeat-dots .done').length).toBe(3);
|
|
|
|
|
+ if(step!==3)expect(row.querySelector('.study-phonetic')?.textContent).toBe(first.phonetic);
|
|
|
|
|
+ row.click();harness.fixture.detectChanges();expect(answer()).toBeNull();expect(component.repetitions()[first.id]).toBe(3);
|
|
|
|
|
+ if(step<3){for(const [index,word] of component.words().entries())if(index>0)for(let tap=0;tap<3;tap++)component.repeatWord(word,index);}
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
it('uses different save identities for consecutive learned passes and leaves the third pass read-only', async () => {
|
|
it('uses different save identities for consecutive learned passes and leaves the third pass read-only', async () => {
|
|
|
const {api,harness,component}=await teacherSelection(2,1);
|
|
const {api,harness,component}=await teacherSelection(2,1);
|
|
|
spyOn(TestBed.inject(PlatformService),'toast'); // Two consecutive saves test state, not toast dismissal timers.
|
|
spyOn(TestBed.inject(PlatformService),'toast'); // Two consecutive saves test state, not toast dismissal timers.
|