| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614 |
- import { provideHttpClient } from '@angular/common/http';
- import { signal } from '@angular/core';
- import { TestBed } from '@angular/core/testing';
- import { By } from '@angular/platform-browser';
- import { provideRouter, Router } from '@angular/router';
- import { RouterTestingHarness } from '@angular/router/testing';
- import { Observable, of, Subject, throwError } from 'rxjs';
- import { CourseCenterPageComponent } from '../features/course-center/course-center-page.component';
- import { AppointmentPageComponent } from '../features/appointment/appointment-page.component';
- import { LearningPageComponent } from '../features/learning/learning-page.component';
- import { StudyRecordsPageComponent } from '../features/study-records/study-records-page.component';
- import { HomePageComponent } from '../features/home/home-page.component';
- import { routes } from '../app.routes';
- import { ApiService } from './api.service';
- import { ApiEnvelope, AppUser } from './models';
- import { PlatformService } from './platform.service';
- import { SessionService } from './session.service';
- class LearningModulesApiService {
- lastScheduleParams: Record<string, unknown> = {};
- lastNodeParams: Record<string, unknown> = {};
- lastAppointmentCourseParams: Record<string, unknown> = {};
- lastWordParams: Record<string, unknown> = {};
- lastLearningWordUpdate: Record<string, unknown> = {};
- lastMemoryParams: Record<string, unknown> = {};
- lastStandaloneLearningRecord: Record<string, unknown> = {};
- appointmentStatus = 0;
- historyWordAudio = '';
- completionRequests: Record<string, unknown>[] = [];
- completionResponse = new Subject<ApiEnvelope<unknown>>();
- lastAppointmentRecordParams: Record<string, unknown> = {};
- studyUpdates: Record<string, unknown>[] = [];
- reviewPageResponse: Subject<ApiEnvelope<unknown>> | null = null;
- unitRows: Record<string, unknown>[] = [
- { NodeID: 811, NodeName: 'Unit 1', yx_word: 1, total: 20 },
- { NodeID: 812, NodeName: 'Unit 2', yx_word: 0, total: 18 },
- ];
- get<T>(action: string, params: Record<string, unknown> = {}): Observable<ApiEnvelope<T>> {
- if (action === 'content_list' && Number(params['ModelID'] ?? params['modelId']) === 56 && String(params['myfield2']).includes('dsid=')) {
- this.lastAppointmentRecordParams = params;
- if (!String(params['myfield2']).includes('|pl=300')) return throwError(() => new Error('无权查看该学员的内容记录'));
- return this.result<T>([{ GeneralID: 9901 }]);
- }
- if (action === 'e_record_detail') {
- return this.result<T>([{ GeneralID: 1001, NodeID: 811, Title: 'apple', sy: '苹果', check: 0 }]);
- }
- if (action === 'e_order_detail') {
- return this.result<T>([{
- GeneralID: 901,
- szyh: 100,
- HoneyName: '张雅茜',
- kcid: 801,
- yykcmc: '七年级上册',
- dszt: this.appointmentStatus,
- dslx: 2,
- bxrq: '2026-08-10',
- sdsd: '19:00',
- jffs: '线上',
- }]);
- }
- if (action === 'content_list' && Number(params['nid']) === 291) {
- return this.result<T>([
- { GeneralID: 201, CreateTime: '2026-08-01 09:30:00', con: '初中英语@Unit 1', xxqs: JSON.stringify([{ Title: 'apple' }]) },
- { GeneralID: 202, CreateTime: '2026-08-04 16:20:00', con: '高中英语@Unit 2' },
- ]);
- }
- if (action === 'content_list' && Number(params['nid']) === 388) {
- return this.result<T>([
- { GeneralID: 100, Title: '张雅茜@历史任务', kywsj: '2026-05-15 09:00', fxzt: 1, yhid: 100, xxjlid: 900 },
- { GeneralID: 101, Title: '张雅茜@任务', kywsj: '2026-08-01 09:00', fxzt: 1, yhid: 100, xxjlid: 900 },
- { GeneralID: 102, Title: '张雅茜@任务', kywsj: '2026-08-02 09:00', fxzt: 0, yhid: 100, xxjlid: 900 },
- ]);
- }
- if (action === 'content_list' && Number(params['nid']) === 3) {
- return this.result<T>([{ GeneralID: 301, Title: '阅读理解专项' }]);
- }
- if (action === 'content_list' && Number(params['nid']) === 389) {
- return this.result<T>([{ GeneralID: 401, df: 18, totalScore: 48, dontKnow: 20, wrong: 10 }]);
- }
- if (action === 'content_list' && Number(params['nid']) === 28) {
- this.lastAppointmentCourseParams = params;
- return this.result<T>([
- { GeneralID: 801, kcid: 801, Title: '初中英语|七年级上册@同步词汇' },
- { GeneralID: 802, kcid: 801, Title2: '【初中英语】阅读训练' },
- ]);
- }
- if (action === 'content_list' && Number(params['ModelID']) === 58) {
- return this.result<T>([{ GeneralID: 701, yhid: 100, kcid: 801, syjd: '{}' }]);
- }
- if (action === 'content_list' && Number(params['nid']) === 29) {
- this.lastScheduleParams = params;
- return this.result<T>([
- { GeneralID: 901, bxrq: '2026-08-10 00:00:00', sdsd: '19:00', Title: '同步伴学', plxm: '张老师', dszt: 10 },
- { GeneralID: 902, bxrq: '2026-08-03 00:00:00', sdsd: '18:00', Title: '历史伴学', plxm: '李老师', dszt: 30 },
- ]);
- }
- if (action === 'node_list' && Number(params['pid']) === 8) {
- return this.result<T>([{ NodeID: 801, NodeName: '小学人教版词库' }]);
- }
- if (action === 'node_list' && Number(params['pid']) === 801) {
- this.lastNodeParams = params;
- return this.result<T>(this.unitRows);
- }
- if (action === 'e_ck_list') {
- this.lastWordParams = params;
- const allWords = Number(params['nids']) === 813
- ? Array.from({ length: 65 }, (_, index) => ({ GeneralID: 2001 + index, NodeID: 813, Title: `word-${index + 1}`, sy: `释义 ${index + 1}` }))
- : [
- { GeneralID: 1001, NodeID: 811, Title: 'apple', sy: 'n. 苹果', yp: 'apple.mp3', w_learned: 1 },
- { GeneralID: 1002, NodeID: 812, Title: 'banana', sy: 'n. 香蕉', yp: 'banana.mp3', w_learned: 0 },
- ];
- const pageIndex = Math.max(1, Number(params['cpage'] ?? 1));
- const pageSize = Math.max(1, Number(params['psize'] ?? 30));
- const result = allWords.slice((pageIndex - 1) * pageSize, pageIndex * pageSize);
- return of({
- retcode: 1,
- result: result as T,
- page: { itemCount: allWords.length, pageCount: Math.ceil(allWords.length / pageSize), pageIndex, pageSize },
- });
- }
- if (action === 'node_get') return this.result<T>([{ NodeID: 3, NodeName: '专项训练' }]);
- if (action === 'vote_ask') return this.result<T>([{ ID: 1, Title: '第一组问题' }]);
- if (action === 'vote_question') {
- return this.result<T>([{ ID: 501, AskID: 1, QType: 1, QTitle: 'apple', QContent: '苹果', QOption: JSON.stringify([{ text: '苹果', value: 1 }]) }]);
- }
- if (action === 'e_words_list') {
- return this.result<T>([{
- GeneralID: 601,
- Title: 'tend@张雅茜',
- detail: [{ GeneralID: 701, Title: 'tend', sy: 'vt.照顾;照料', yp: 'tend.mp3' }],
- }]);
- }
- return this.result<T>([]);
- }
- post<T>(action: string, params: Record<string, unknown> = {}): Observable<ApiEnvelope<T>> {
- if (action === 'stu_record_update_v2') {
- this.studyUpdates.push(params);
- return this.result<T>({ GeneralID: 9901 });
- }
- if (action === 'e_order_complete_feedback') {
- this.completionRequests.push(params);
- return this.completionResponse as Observable<ApiEnvelope<T>>;
- }
- if (action === 'e_add_words') {
- this.lastLearningWordUpdate = params;
- return this.result<T>({ affected: String(params['wordsId'] ?? '').split(',').filter(Boolean).length });
- }
- if (action === 'content_add_zt') {
- this.lastStandaloneLearningRecord = params;
- return this.result<T>({ GeneralID: 9901 });
- }
- if (action === 'e_order_tongji') {
- return this.result<T>({ t30: 4, t60: 6, t_tiyan: 1, t_total: 11, t_shichang: 9, t_yongji: 320 });
- }
- if (action === 'e_user_list') {
- return this.result<T>([{ szyh: 100, honeyname: '张雅茜' }]);
- }
- if (action === 'app_learning_overview') {
- const records = Number(params['studentId']) === 777
- ? Array.from({ length: 45 }, (_, index) => ({
- GeneralID: 5000 + index,
- CreateTime: `2026-08-${String(31 - (index % 20)).padStart(2, '0')} 09:30:00`,
- dqrq: '20260801',
- con: `分页学习记录 ${index + 1}`,
- }))
- : [
- { GeneralID: 201, CreateTime: '2026-08-01 09:30:00', dqrq: '20260801', con: '初中英语@Unit 1', xxqs: JSON.stringify([{ Title: 'apple' }]), readingArticleId: 'readingarticle201', readingCompletedAt: '2026-08-01T03:00:00.000Z' },
- { GeneralID: 202, CreateTime: '2026-08-04 16:20:00', dqrq: '20260804', con: '高中英语@Unit 2', xxqs: JSON.stringify([{ Title: 'banana' }]) },
- ];
- const selected = String(params['date'] ?? '');
- return this.result<T>({
- records: (selected ? records.filter((item) => item.CreateTime.startsWith(selected)) : records).slice((Number(params['page']||1)-1)*20,Number(params['page']||1)*20),
- page:{itemCount:selected?records.filter(item=>item.CreateTime.startsWith(selected)).length:records.length,pageCount:Math.ceil(records.length/20)},
- latestReadingRecord: records[0],
- recordDateCounts: { '2026-08-01': 1, '2026-08-04': 1 },
- scheduleDateCounts: {},
- schedules: [],
- courses: [
- { GeneralID: 801, id: 701, kcid: 801, Title: '初中英语|七年级上册@同步词汇' },
- { GeneralID: 802, id: 702, Title2: '【初中英语】阅读训练', syjd: '{"811":"811@Unit 1@1"}' },
- ],
- refreshedAt: '2026-08-26T00:00:00.000Z',
- });
- }
- if (action === 'app_companion_overview') {
- this.lastScheduleParams = params;
- const schedules = [
- { GeneralID: 901, bxrq: '2026-08-10 00:00:00', sdsd: '19:00', Title: '同步伴学', plxm: '张老师', dszt: 10 },
- { GeneralID: 902, bxrq: '2026-08-03 00:00:00', sdsd: '18:00', Title: '历史伴学', plxm: '李老师', dszt: 30 },
- { GeneralID: 903, bxrq: '2099-01-01 00:00:00', sdsd: '18:00', Title: '未来计划', plxm: '王老师', dszt: 0 },
- ];
- const historySchedules = [
- { GeneralID: 911, bxrq: '2026-07-14 00:00:00', sdsd: '22:00', Title: '谢晨晨(60分钟练习)', plxm: '张老师', dszt: 30 },
- { GeneralID: 912, bxrq: '2026-07-14 00:00:00', sdsd: '17:00', Title: '谢谢(60分钟练习)', plxm: '张老师', dszt: 30 },
- { GeneralID: 913, bxrq: '2026-03-09 00:00:00', sdsd: '10:00', Title: '花花(60分钟练习)', plxm: '张老师', dszt: 30 },
- { GeneralID: 914, bxrq: '2026-01-18 00:00:00', sdsd: '16:30', Title: '小赵(60分钟练习)', plxm: '张老师', dszt: 30 },
- { GeneralID: 915, bxrq: '2026-01-18 00:00:00', sdsd: '16:00', Title: '小黄(60分钟练习)', plxm: '张老师', dszt: 30 },
- ];
- const rangeStart = String(params['rangeStart'] ?? '');
- const rangeEnd = String(params['rangeEnd'] ?? '');
- return this.result<T>({
- courses: [
- { GeneralID: 801, id: 701, kcid: 801, Title: '初中英语|七年级上册@同步词汇' },
- { GeneralID: 802, id: 702, Title2: '【初中英语】阅读训练', syjd: '{"811":"811@Unit 1@1"}' },
- ],
- dates:(params['history']?historySchedules:schedules).map(item=>({date:item.bxrq.slice(0,10),finished:item.dszt>=30?1:0,planned:item.dszt>=30?0:1})),
- schedules: params['history']
- ? historySchedules
- : rangeStart && rangeEnd
- ? schedules.filter((item) => item.bxrq.slice(0, 10) >= rangeStart && item.bxrq.slice(0, 10) <= rangeEnd)
- : schedules,
- refreshedAt: '2026-08-26T00:00:00.000Z',
- });
- }
- if (action === 'app_account_overview') {
- if (Number(params['uid']) === 300) {
- return this.result<T>({
- profile: { userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 },
- coachPanel: { todaySchedules: 2, completedLessons: 8, reviewIncome: 36, reviewRecords: 3, totalLessons: 11, totalHours: 9, course30: 4, course60: 6, trialLessons: 1 },
- refreshedAt: '2026-08-26T00:00:00.000Z',
- });
- }
- return this.result<T>({
- profile: { userId: 100, userName: 'student', honeyName: '张雅茜', groupId: 1 },
- companionSummary: { completedSessions: 1, completedMinutes: 60 },
- antiForgetSummary: { total: 3, completed: 2, progress: 67, todayTotal: 0, todayCompleted: 0, duePending: 1, scheduled: 0, scheduledDays: 3 },
- refreshedAt: '2026-08-26T00:00:00.000Z',
- });
- }
- if (action === 'app_review_page') {
- this.lastMemoryParams=params;
- if (this.reviewPageResponse) return this.reviewPageResponse as Observable<ApiEnvelope<T>>;
- const items=[
- { GeneralID: 100, Title: '张雅茜@历史任务', kc_title: '历史复习', kywsj: '2026-05-15 09:00', learned_at: '2026-05-01T01:15:00.000Z', fxzt: 1, yhid: 100, xxjlid: 900 },
- { GeneralID: 101, Title: '张雅茜@任务', kc_title: '初中英语复习', kywsj: '2026-08-01 09:00', learned_at: '2026-07-20T02:20:00.000Z', fxzt: 1, yhid: 100, xxjlid: 900 },
- { GeneralID: 102, Title: '张雅茜@任务', kc_title: '高中英语复习', kywsj: '2026-08-02 09:00', learned_at: '2026-07-20T02:20:00.000Z', fxzt: 0, yhid: 100, xxjlid: 900 },
- ...(Number(params['shouru']) === 1
- ? [{ GeneralID: 103, Title: '李同学@任务', kc_title: '小学英语复习', kywsj: '2026-08-02 10:00', learned_at: '2026-07-21T02:20:00.000Z', fxzt: 0, yhid: 101, xxjlid: 901 }]
- : []),
- ];return this.result<T>({items,page:{itemCount:items.length,pageCount:1},summary:{total:items.length,completed:items.filter(i=>i.fxzt===1).length,due:items.filter(i=>i.fxzt!==1).length,today:0,income:0},dates:items.map(i=>({date:i.kywsj.slice(0,10),total:1,completed:i.fxzt===1?1:0}))});
- }
- if (action === 'e_get_21list') {
- this.lastMemoryParams = params;
- return this.result<T>([
- { GeneralID: 100, Title: '张雅茜@历史任务', kc_title: '历史复习', kywsj: '2026-05-15 09:00', learned_at: '2026-05-01T01:15:00.000Z', fxzt: 1, yhid: 100, xxjlid: 900 },
- { GeneralID: 101, Title: '张雅茜@任务', kc_title: '初中英语复习', kywsj: '2026-08-01 09:00', learned_at: '2026-07-20T02:20:00.000Z', fxzt: 1, yhid: 100, xxjlid: 900 },
- { GeneralID: 102, Title: '张雅茜@任务', kc_title: '高中英语复习', kywsj: '2026-08-02 09:00', learned_at: '2026-07-20T02:20:00.000Z', fxzt: 0, yhid: 100, xxjlid: 900 },
- ...(Number(params['shouru']) === 1
- ? [{ GeneralID: 103, Title: '李同学@任务', kc_title: '小学英语复习', kywsj: '2026-08-02 10:00', learned_at: '2026-07-21T02:20:00.000Z', fxzt: 0, yhid: 101, xxjlid: 901 }]
- : []),
- ]);
- }
- if (action === 'e_record_detail') {
- return of({
- retcode: 1,
- result: [{ GeneralID: 701, Title: 'apple', detail: [{ GeneralID: 701, Title: 'apple', sy: '苹果', yp: this.historyWordAudio }] }],
- addon: [{ if_writing: '0', if_writing_tips: '先回忆单词,再查看释义' }],
- } as unknown as ApiEnvelope<T>);
- }
- return this.result<T>([]);
- }
- assetUrl(path: unknown): string { return String(path ?? ''); }
- private result<T>(result: unknown): Observable<ApiEnvelope<T>> {
- return of({ retcode: 1, result: result as T });
- }
- }
- describe('home learning modules', () => {
- const user = signal<AppUser | null>({ userId: 100, userName: 'student', honeyName: '张雅茜', groupId: 1 });
- const selectedStudent = signal<AppUser | null>(null);
- const session = {
- user,
- token: signal('test-token'),
- student: selectedStudent,
- isLoggedIn: signal(true),
- groupId: signal(1),
- setStudent: (student: AppUser | null) => selectedStudent.set(student),
- patchUser: () => undefined,
- logout: () => undefined,
- };
- beforeEach(() => {
- localStorage.removeItem('xiaoshu.learning.300.100.901.skip_spelling_stage');
- user.set({ userId: 100, userName: 'student', honeyName: '张雅茜', groupId: 1 });
- session.groupId.set(1);
- session.student.set(null);
- TestBed.configureTestingModule({
- providers: [
- provideRouter(routes),
- provideHttpClient(),
- { provide: ApiService, useClass: LearningModulesApiService },
- { provide: SessionService, useValue: session },
- ],
- });
- });
- it('silently reveals pending and completed words opened from student learning content', async () => {
- const play = spyOn(TestBed.inject(PlatformService), 'play');
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/his/his');
- (harness.routeNativeElement?.querySelector('.current-learning-list button') as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- (harness.routeNativeElement?.querySelector('.node-list button') as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(TestBed.inject(Router).url).toContain('readonly=1');
- const learning = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
- for (const [index, meaning] of ['n. 香蕉', 'n. 苹果'].entries()) {
- (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[index] as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelector('.audio')).toBeNull();
- expect(harness.routeNativeElement?.textContent).not.toContain('读音');
- const word = harness.routeNativeElement?.querySelector('.word-reveal') as HTMLButtonElement;
- word.click();
- harness.fixture.detectChanges();
- expect(word.textContent).toContain(meaning);
- expect(word.getAttribute('aria-expanded')).toBe('true');
- word.click();
- harness.fixture.detectChanges();
- expect(word.getAttribute('aria-expanded')).toBe('false');
- }
- learning.words().forEach((word) => learning.play(word));
- expect(play).not.toHaveBeenCalled();
- });
- it('pages through all words in a history record without requesting a course unit', async () => {
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- const originalPost = api.post.bind(api);
- const post = spyOn(api, 'post').and.callFake((action, params) => action === 'e_record_detail'
- ? of({ retcode: 1, result: Array.from({ length: 31 }, (_, index) => ({
- GeneralID: 700 + index,
- Title: `history-${index + 1}`,
- detail: [{ GeneralID: 700 + index, Title: `history-${index + 1}`, sy: `释义 ${index + 1}` }],
- })), addon: [] } as ApiEnvelope<any>)
- : originalPost(action, params));
- const get = spyOn(api, 'get').and.callThrough();
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/his/his_words?id=201');
- harness.fixture.detectChanges();
- const list = () => harness.routeNativeElement?.querySelectorAll('.readonly-word-list article') ?? [];
- expect(list().length).toBe(30);
- expect(harness.routeNativeElement?.textContent).toContain('共 31 个单词');
- (harness.routeNativeElement?.querySelector('.word-preview-pagination .page-direction:last-of-type') as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(list().length).toBe(1);
- expect(list()[0].textContent).toContain('history-31');
- expect(harness.routeNativeElement?.textContent).toContain('2 / 2 页');
- expect(get.calls.allArgs().some(([action]) => action === 'e_ck_list')).toBeFalse();
- expect(post.calls.allArgs().filter(([action]) => action === 'e_record_detail').length).toBe(1);
- (harness.routeNativeElement?.querySelector('.word-preview-pagination .page-direction:first-child') as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(list().length).toBe(30);
- expect(list()[0].textContent).toContain('history-1');
- });
- it('plays student history words on click without changing learning records or enabling unit preview audio', async () => {
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- api.historyWordAudio = 'apple.mp3';
- const post = spyOn(api, 'post').and.callThrough();
- const platform = TestBed.inject(PlatformService);
- const play = spyOn(platform, 'play');
- const stopAudio = spyOn(platform, 'stopAudio');
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/his/his');
- (harness.routeNativeElement?.querySelector('.learning-record-card .learning-item') as HTMLElement).click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(TestBed.inject(Router).url).toBe('/pages/his/his_words?id=201');
- expect(play).not.toHaveBeenCalled();
- post.calls.reset();
- const button = harness.routeNativeElement?.querySelector('.history-word-audio') as HTMLButtonElement;
- expect(button.disabled).toBeFalse();
- expect(button.getAttribute('aria-label')).toBe('播放单词读音:apple');
- button.querySelector('strong')!.click();
- (button.querySelector('lucide-icon') as HTMLElement).click();
- expect(play).toHaveBeenCalledTimes(2);
- expect(play.calls.mostRecent().args[0]).toContain('apple.mp3');
- expect(post).not.toHaveBeenCalled();
- const learning = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
- learning.words.update((words) => words.map((word) => ({ ...word, audio: '' })));
- harness.fixture.detectChanges();
- expect(button.disabled).toBeTrue();
- expect(harness.routeNativeElement?.textContent).toContain('暂无音频');
- button.click();
- expect(play).toHaveBeenCalledTimes(2);
- stopAudio.calls.reset();
- await harness.navigateByUrl('/pages/home/index');
- expect(stopAudio).toHaveBeenCalled();
- await harness.navigateByUrl('/pages/stu/stu_con?nid=811&readonly=1');
- expect(harness.routeNativeElement?.querySelector('.history-word-audio')).toBeNull();
- const preview = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
- preview.words().forEach((word) => preview.play(word));
- expect(play).toHaveBeenCalledTimes(2);
- });
- 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');
- const harness = await RouterTestingHarness.create();
- const router = TestBed.inject(Router);
- await harness.navigateByUrl('/pages/member/starshop');
- (harness.routeNativeElement?.querySelector('.word-select-list > button') as HTMLButtonElement).click();
- (harness.routeNativeElement?.querySelector('.selection-header .primary-button') as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const learning = () => harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
- expect(router.url).toContain('/pages/stu/stu_con');
- expect(learning().words().map((word) => word.word)).toEqual(['tend']);
- learning().skipStudyList();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(router.url).toContain('/pages/stu/stu3');
- expect(learning().words().map((word) => word.word)).toEqual(['tend']);
- learning().chooseMatch('left', learning().words()[0]);
- learning().chooseMatch('right', learning().words()[0]);
- learning().continueAfterStage();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(router.url).toContain('/pages/stu/stu4');
- expect(learning().words().map((word) => word.word)).toEqual(['tend']);
- learning().chooseMeaning(learning().words()[0]);
- learning().continueAfterStage();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(router.url).toContain('/pages/stu/stu5');
- for (const letter of learning().current()!.word) {
- const index = learning().letters().findIndex((choice) => !choice.used && choice.value === letter);
- expect(index).toBeGreaterThanOrEqual(0);
- learning().spellLetter(index);
- }
- learning().nextSpellingWord();
- learning().continueAfterStage();
- await harness.fixture.whenStable();
- expect(router.url).toBe('/pages/member/starshop');
- expect(localStorage.getItem('xiaoshu.learning.100.100.practice.learn_mode')).toBeNull();
- await harness.navigateByUrl('/pages/stu/stu_con?nids=811&pnid=801');
- await harness.fixture.whenStable();
- expect(router.url).toBe('/pages/home/cart');
- });
- it('separates unlearned and learned wordbook words while keeping selections across tabs', async () => {
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- const get = api.get.bind(api);
- spyOn(api, 'get').and.callFake((action, params) => action === 'e_words_list'
- ? of({ retcode: 1, result: params?.['learningStatus'] === 'completed'
- ? [{ GeneralID: 602, Title: 'prepare@张雅茜', xxcs: 2, detail: [{ GeneralID: 702, Title: 'prepare', sy: '准备' }] }]
- : [{ GeneralID: 601, Title: 'tend@张雅茜', xxcs: 0, detail: [{ GeneralID: 701, Title: 'tend', sy: '照顾' }] }],
- page: { itemCount: 1, pendingCount: 1, completedCount: 1, pageCount: 1, pageIndex: 1, pageSize: 30 } } as ApiEnvelope<any>)
- : get(action, params));
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/member/starshop');
- harness.fixture.detectChanges();
- const tabs = harness.routeNativeElement?.querySelectorAll<HTMLButtonElement>('.word-selection-tabs button') ?? [];
- expect(tabs[0].textContent).toContain('未学习');
- expect(tabs[0].textContent).toContain('1');
- expect(harness.routeNativeElement?.querySelector('.pending-column')?.textContent).toContain('tend');
- (harness.routeNativeElement?.querySelector('.pending-column .word-select-list > button') as HTMLButtonElement).click();
- tabs[1].click();
- harness.fixture.detectChanges();
- expect(tabs[1].textContent).toContain('已学习');
- expect(harness.routeNativeElement?.querySelector('.completed-column')?.textContent).toContain('prepare');
- (harness.routeNativeElement?.querySelector('.completed-column .word-select-list > button') as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelector('.selection-header strong')?.textContent).toContain('已选 2 个单词');
- tabs[0].click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelector('.pending-column .word-select-list > button.selected')).not.toBeNull();
- });
- it('shows 30 wordbook words per page while counting every learning status', async () => {
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- const get = api.get.bind(api);
- const pending = Array.from({ length: 65 }, (_, index) => ({ GeneralID: 601 + index, xxcs: 0, detail: [{ GeneralID: 701 + index, Title: `pending-${index + 1}` }] }));
- const completed = Array.from({ length: 7 }, (_, index) => ({ GeneralID: 801 + index, xxcs: 1, detail: [{ GeneralID: 901 + index, Title: `completed-${index + 1}` }] }));
- const requests: Record<string, unknown>[] = [];
- spyOn(api, 'get').and.callFake((action, params) => {
- if (action !== 'e_words_list') return get(action, params);
- requests.push(params ?? {});
- const rows = params?.['learningStatus'] === 'completed' ? completed : pending;
- const page = Number(params?.['cpage']);
- const size = Number(params?.['psize']);
- return of({ retcode: 1, result: rows.slice((page - 1) * size, page * size),
- page: { itemCount: rows.length, pendingCount: pending.length, completedCount: completed.length,
- pageCount: Math.ceil(rows.length / size), pageIndex: page, pageSize: size } } as ApiEnvelope<any>);
- });
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/member/starshop');
- harness.fixture.detectChanges();
- const view = () => harness.routeNativeElement!;
- const rows = () => view().querySelectorAll('.pending-column .word-select-list > button');
- expect(requests[0]).toEqual(jasmine.objectContaining({ learningStatus: 'pending', cpage: 1, psize: 30 }));
- expect(rows().length).toBe(30);
- expect(view().querySelectorAll('.word-selection-tabs b')[0].textContent).toContain('65');
- expect(view().querySelectorAll('.word-selection-tabs b')[1].textContent).toContain('7');
- (rows()[0] as HTMLButtonElement).click();
- (view().querySelectorAll('.mobile-pagination button')[1] as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(requests[1]).toEqual(jasmine.objectContaining({ learningStatus: 'pending', cpage: 2, psize: 30 }));
- expect(rows().length).toBe(30);
- expect(view().querySelector('.mobile-pagination')?.textContent).toContain('第 2 / 3 页');
- (view().querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(requests[2]).toEqual(jasmine.objectContaining({ learningStatus: 'completed', cpage: 1, psize: 30 }));
- expect(view().querySelectorAll('.completed-column .word-select-list > button').length).toBe(7);
- expect(view().querySelector('.selection-header strong')?.textContent).toContain('已选 1 个单词');
- });
- it('keeps wordbook counts and rows correct when the server only returns the old mixed list', async () => {
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- const get = api.get.bind(api);
- const all = Array.from({ length: 139 }, (_, index) => ({
- GeneralID: 601 + index,
- xxcs: index < 45 ? 1 : 0,
- detail: [{ GeneralID: 701 + index, Title: `word-${index + 1}` }],
- }));
- const requests: Record<string, unknown>[] = [];
- spyOn(api, 'get').and.callFake((action, params) => {
- if (action !== 'e_words_list') return get(action, params);
- requests.push(params ?? {});
- const page = Number(params?.['cpage']);
- const size = Number(params?.['psize']);
- return of({ retcode: 1, result: all.slice((page - 1) * size, page * size),
- page: { itemCount: all.length, pageCount: Math.ceil(all.length / size), pageIndex: page, pageSize: size } } as ApiEnvelope<any>);
- });
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/member/starshop');
- harness.fixture.detectChanges();
- const view = () => harness.routeNativeElement!;
- expect(requests.length).toBe(2);
- expect(requests[0]).toEqual(jasmine.objectContaining({ learningStatus: 'pending', psize: 30 }));
- expect(requests[1]).toEqual(jasmine.objectContaining({ cpage: 1, psize: 500 }));
- expect(view().querySelectorAll('.word-selection-tabs b')[0].textContent).toContain('94');
- expect(view().querySelectorAll('.word-selection-tabs b')[1].textContent).toContain('45');
- expect(view().querySelectorAll('.pending-column .word-select-list > button').length).toBe(30);
- expect(view().querySelector('.mobile-pagination')?.textContent).toContain('第 1 / 4 页 · 共 94 个单词');
- (view().querySelectorAll('.mobile-pagination button')[1] as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(view().querySelector('.mobile-pagination')?.textContent).toContain('第 2 / 4 页');
- (view().querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(view().querySelectorAll('.completed-column .word-select-list > button').length).toBe(30);
- expect(view().querySelector('.mobile-pagination')?.textContent).toContain('第 1 / 2 页 · 共 45 个单词');
- expect(requests.length).toBe(2);
- });
- it('saves completed wordbook study before showing the word in the learned tab', async () => {
- const prefix = 'xiaoshu.learning.100.100.practice.';
- const word = { GeneralID: 701, Title: 'tend', sy: '照顾' };
- localStorage.setItem(prefix + 'learn_mode', JSON.stringify('newword'));
- localStorage.setItem(prefix + 'choose_newword_learn', JSON.stringify([word]));
- localStorage.setItem(prefix + '_page_word_orilist', JSON.stringify([[word]]));
- localStorage.setItem(prefix + '_page_current_learn', JSON.stringify(1));
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- const get = api.get.bind(api);
- spyOn(api, 'get').and.callFake((action, params) => action === 'e_words_list'
- ? of({ retcode: 1, result: api.lastLearningWordUpdate['save'] === 1 && params?.['learningStatus'] === 'completed'
- ? [{ GeneralID: 601, Title: 'tend@张雅茜', xxcs: 1, detail: [word] }]
- : api.lastLearningWordUpdate['save'] === 1 ? [] : [{ GeneralID: 601, Title: 'tend@张雅茜', xxcs: 0, detail: [word] }],
- page: { itemCount: params?.['learningStatus'] === 'completed' ? 1 : api.lastLearningWordUpdate['save'] === 1 ? 0 : 1,
- pendingCount: api.lastLearningWordUpdate['save'] === 1 ? 0 : 1, completedCount: api.lastLearningWordUpdate['save'] === 1 ? 1 : 0,
- pageCount: 1, pageIndex: 1, pageSize: 30 } } as ApiEnvelope<any>)
- : get(action, params));
- const harness = await RouterTestingHarness.create();
- const router = TestBed.inject(Router);
- await harness.navigateByUrl('/pages/stu/stu5');
- const learning = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
- learning.skipSpellingStage();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(api.lastLearningWordUpdate).toEqual(jasmine.objectContaining({ uid: 100, wordsId: '701', save: 1 }));
- expect(router.url).toBe('/pages/member/starshop');
- const tabs = harness.routeNativeElement?.querySelectorAll<HTMLButtonElement>('.word-selection-tabs button') ?? [];
- expect(tabs[0].textContent).toContain('0');
- tabs[1].click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelector('.completed-column')?.textContent).toContain('tend');
- }, 15000);
- it('skips the entire spelling stage for student wordbook self-study in one click', async () => {
- const prefix = 'xiaoshu.learning.100.100.practice.';
- const words = [
- { GeneralID: 701, Title: 'tend', sy: '照顾' },
- { GeneralID: 702, Title: 'work on', sy: '从事' },
- { GeneralID: 703, Title: 'prepare', sy: '准备' },
- ];
- localStorage.setItem(prefix + 'learn_mode', JSON.stringify('newword'));
- localStorage.setItem(prefix + 'choose_newword_learn', JSON.stringify(words.slice(0, 2)));
- localStorage.setItem(prefix + '_page_word_orilist', JSON.stringify([words.slice(0, 2), words.slice(2)]));
- localStorage.setItem(prefix + '_page_current_learn', JSON.stringify(1));
- const harness = await RouterTestingHarness.create();
- const router = TestBed.inject(Router);
- await harness.navigateByUrl('/pages/stu/stu5');
- harness.fixture.detectChanges();
- const learning = () => harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
- expect(learning().words().length).toBe(2);
- expect(learning().currentIndex()).toBe(0);
- const skip = Array.from(harness.routeNativeElement?.querySelectorAll<HTMLButtonElement>('.skippable-spelling-actions button') ?? [])
- .find((button) => button.textContent?.includes('跳到下一关'))!;
- skip.click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(router.url).toContain('/pages/stu/stu_con');
- expect(learning().words().map((word) => word.word)).toEqual(['prepare']);
- learning().skipStudyList();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- learning().chooseMatch('left', learning().words()[0]);
- learning().chooseMatch('right', learning().words()[0]);
- learning().continueAfterStage();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(router.url).toContain('/pages/stu/stu4');
- learning().chooseMeaning(learning().words()[0]);
- learning().continueAfterStage();
- await harness.fixture.whenStable();
- expect(router.url).toBe('/pages/member/starshop');
- expect(localStorage.getItem(prefix + 'learn_mode')).toBeNull();
- }, 15000);
- it('still pronounces pending and completed words when a coach reveals them', async () => {
- user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
- session.groupId.set(3);
- session.student.set({ userId: 100, userName: 'student', groupId: 1 });
- const play = spyOn(TestBed.inject(PlatformService), 'play');
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/stu/stu_abc?nids=811,812&pnid=801');
- for (const index of [0, 1]) {
- (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[index] as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelector('.audio')).not.toBeNull();
- (harness.routeNativeElement?.querySelector('.word-reveal') as HTMLButtonElement).click();
- }
- expect(play).toHaveBeenCalledTimes(2);
- expect(play.calls.argsFor(0)[0]).toContain('banana.mp3');
- expect(play.calls.argsFor(1)[0]).toContain('apple.mp3');
- });
- it('shows named schedule students below the trigger and closes after selection or Escape', async () => {
- user.set({userId:300,userName:'coach',honeyName:'张老师',groupId:3});
- const harness=await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/home/shop');
- const component=harness.fixture.debugElement.query(By.directive(CourseCenterPageComponent)).componentInstance as CourseCenterPageComponent;
- component.serverStudents.set([{id:'100',name:'张雅茜',count:15},{id:'101',name:'花花',count:9},{id:'102',name:' ',count:1}]);
- spyOn(component,'load').and.stub();
- harness.fixture.detectChanges();
- await new Promise(resolve => setTimeout(resolve, 0));
- const trigger=harness.routeNativeElement!.querySelector<HTMLButtonElement>('.student-filter .xs-dropdown-trigger')!;
- expect(trigger.textContent).toContain('全部学员(25)');
- trigger.click();harness.fixture.detectChanges();
- const menu=document.querySelector<HTMLElement>('.xs-dropdown-menu')!;
- expect(menu.textContent).toContain('张雅茜(15)');
- expect(menu.textContent).toContain('学员 102(1)');
- expect(menu.getBoundingClientRect().top).toBeGreaterThan(trigger.getBoundingClientRect().bottom);
- menu.querySelectorAll('button')[2].click();harness.fixture.detectChanges();
- await new Promise(resolve => setTimeout(resolve, 0));
- expect(component.selectedStudent()).toBe('101');
- expect(trigger.textContent).toContain('花花(9)');
- expect(document.querySelector('.xs-dropdown-menu')).toBeNull();
- trigger.click();harness.fixture.detectChanges();
- document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape'}));harness.fixture.detectChanges();
- expect(document.querySelector('.xs-dropdown-menu')).toBeNull();
- });
- it('keeps the anti-forget page visible while a selected date loads locally', async () => {
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/stu/stu_calendar');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const component = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- component.memoryMonth.set(new Date(2026, 8, 1, 12));
- component.memorySelectedDate.set('2026-09-11');
- harness.fixture.detectChanges();
- const response = new Subject<ApiEnvelope<unknown>>();
- api.reviewPageResponse = response;
- (harness.routeNativeElement?.querySelector('[aria-label^="2026-09-12"]') as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(api.lastMemoryParams['date']).toBe('2026-09-12');
- expect(harness.routeNativeElement?.querySelector('.memory-overview')).not.toBeNull();
- expect(harness.routeNativeElement?.querySelector('.memory-calendar')).not.toBeNull();
- expect(harness.routeNativeElement?.querySelector('.memory-list-loading')?.textContent).toContain('正在加载当日复习');
- response.next({
- retcode: 1,
- result: {
- items: [{ GeneralID: 120, Title: '张雅茜@任务', kc_title: '9月12日复习', kywsj: '2026-09-12 09:00', fxzt: 0, yhid: 100, xxjlid: 920 }],
- page: { itemCount: 1, pageCount: 1 },
- summary: { total: 1, completed: 0, due: 0, today: 0, income: 0 },
- dates: [{ date: '2026-09-12', total: 1, completed: 0 }],
- },
- });
- response.complete();
- api.reviewPageResponse = null;
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelector('.memory-list-loading')).toBeNull();
- expect(harness.routeNativeElement?.querySelector('.memory-date-tasks')?.textContent).toContain('9月12日复习');
- });
- it('shows a retryable error instead of an empty review when the detail has no words', async () => {
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- const post = api.post.bind(api);
- spyOn(api, 'post').and.callFake((action, params) => action === 'e_record_detail'
- ? of({ retcode: 1, result: [], addon: [] } as ApiEnvelope<any>) : post(action, params));
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/stu/record?id=900&gid=102&kcid=801');
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelector('.empty-state.error')?.textContent).toContain('没有读取到单词');
- expect(harness.routeNativeElement?.querySelector('.empty-state.error button')?.textContent).toContain('重新加载');
- expect(harness.routeNativeElement?.querySelector('.record-list')).toBeNull();
- });
- it('renders source-shaped data for all four home entries', async () => {
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/home/index');
- expect(harness.fixture.nativeElement.querySelector('.tabbar')).not.toBeNull();
- expect(harness.routeNativeElement?.querySelectorAll('.home-header .header-action').length).toBe(1);
- expect(harness.routeNativeElement?.querySelector('.home-header .header-action')?.getAttribute('href')).toBe('/pages/home/search');
- expect(harness.routeNativeElement?.querySelector('.home-header [aria-label="用户中心"]')).toBeNull();
- expect(harness.routeNativeElement?.querySelector('.memory-hero')).not.toBeNull();
- expect(harness.routeNativeElement?.querySelectorAll('.memory-dot').length).toBe(21);
- expect(harness.routeNativeElement?.querySelector('.today-panel')?.textContent).toContain('学习新词');
- expect(harness.routeNativeElement?.querySelectorAll('.student-stats > div').length).toBe(5);
- expect(harness.routeNativeElement?.querySelectorAll('.quick-grid > a').length).toBe(4);
- expect(harness.routeNativeElement?.querySelector('.continue-button')?.getAttribute('href')).toBe('/pages/stu/stu_calendar');
- expect(harness.routeNativeElement?.querySelector('.task-link')?.getAttribute('href')).toBe('/pages/home/cart');
- await harness.navigateByUrl('/pages/stu/index');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(TestBed.inject(Router).url).toBe('/pages/home/cart');
- expect(harness.routeNativeElement?.textContent).not.toContain('选择词库教材');
- await harness.navigateByUrl('/pages/stu/stu_list?nid=801&bindingId=701');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelectorAll('.node-list button').length).toBe(2);
- expect(harness.routeNativeElement?.querySelector('.node-list button')?.textContent).toContain('已学 1/20 · 进度 5%');
- expect(harness.routeNativeElement?.querySelectorAll('.unit-progress-ring').length).toBe(2);
- expect(harness.routeNativeElement?.querySelector('.unit-progress-ring')?.textContent).toContain('5%');
- expect(harness.routeNativeElement?.querySelector('.unit-selection-summary')).toBeNull();
- expect(harness.routeNativeElement?.querySelector('.unit-actions')).toBeNull();
- const firstUnit = harness.routeNativeElement?.querySelector('.node-list button') as HTMLButtonElement;
- firstUnit.click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const readonlyRouter = TestBed.inject(Router);
- expect(readonlyRouter.url).toBe('/pages/stu/stu_abc?nids=811&pnid=801&readonly=1');
- expect(harness.routeNativeElement?.querySelector('.selection-header')?.textContent).toContain('词库学习进度');
- expect(harness.routeNativeElement?.querySelector('.selection-header .primary-button')).toBeNull();
- expect(harness.routeNativeElement?.querySelector('.pending-column')?.textContent).toContain('banana');
- expect(harness.routeNativeElement?.querySelector('.word-select-check')).toBeNull();
- expect(harness.routeNativeElement?.querySelector('.word-assessment')).toBeNull();
- (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelector('.completed-column')?.textContent).toContain('apple');
- expect(harness.routeNativeElement?.querySelector('.pending-column')).toBeNull();
- await harness.navigateByUrl('/pages/home/index');
- await harness.navigateByUrl('/pages/stu/stu_con?nid=813&pnid=801&readonly=1');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const wordApi = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- expect(wordApi.lastWordParams['psize']).toBe(30);
- expect(harness.routeNativeElement?.querySelectorAll('.readonly-word-list article').length).toBe(30);
- expect(harness.routeNativeElement?.querySelector('.readonly-word-header')?.textContent).toContain('第 1–30 条 · 共 65 个单词');
- expect(harness.routeNativeElement?.querySelector('.word-preview-pagination')?.textContent).toContain('1 / 3 页');
- (harness.routeNativeElement?.querySelector('[aria-label="第 2 页"]') as HTMLButtonElement).click();
- it('syncs a missing learning record and reopens its review words', async () => {
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- const post = api.post.bind(api);
- let detailReads = 0;
- spyOn(api, 'post').and.callFake((action, params) => {
- if (action === 'e_record_detail') {
- detailReads += 1;
- return of({ retcode: 1, result: detailReads === 1 ? [] : [{ GeneralID: 701, Title: 'apple', sy: '苹果' }], addon: [] } as ApiEnvelope<any>);
- }
- if (action === 'app_learning_overview') return of({ retcode: 1, result: {} } as ApiEnvelope<any>);
- return post(action, params);
- });
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/stu/record?id=864038&gid=864047&kcid=398');
- harness.fixture.detectChanges();
- expect(detailReads).toBe(2);
- expect(harness.routeNativeElement?.querySelector('.record-word')?.textContent).toContain('apple');
- expect(harness.routeNativeElement?.querySelector('.empty-state.error')).toBeNull();
- });
- it('reads the original word list when the detail stays empty after syncing', async () => {
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- const post = api.post.bind(api);
- const get = api.get.bind(api);
- spyOn(api, 'post').and.callFake((action, params) => action === 'e_record_detail'
- ? of({ retcode: 1, result: [], addon: [] } as ApiEnvelope<any>) : action === 'app_learning_overview'
- ? of({ retcode: 1, result: {} } as ApiEnvelope<any>) : post(action, params));
- spyOn(api, 'get').and.callFake((action, params) => action === 'content_get'
- ? of({ retcode: 1, result: [{ GeneralID: 864038, xxqs: JSON.stringify([{ GeneralID: 701, Title: 'apple' }]) }] } as ApiEnvelope<any>)
- : get(action, params));
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/stu/record?id=864038&gid=864047&kcid=398');
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelector('.record-word')?.textContent).toContain('apple');
- expect(harness.routeNativeElement?.querySelector('.empty-state.error')).toBeNull();
- });
- it('identifies a legacy writing review with no original words and keeps it incomplete', async () => {
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- const post = api.post.bind(api);
- const get = api.get.bind(api);
- spyOn(api, 'post').and.callFake((action, params) => action === 'e_record_detail'
- ? of({ retcode: 1, result: [], addon: [] } as ApiEnvelope<any>) : action === 'app_learning_overview'
- ? of({ retcode: 1, result: {} } as ApiEnvelope<any>) : post(action, params));
- spyOn(api, 'get').and.callFake((action, params) => action === 'content_get'
- ? of({ retcode: 1, result: [{ GeneralID: 864038, xxqs: '', learned: 0, con: '完成了写作学习' }] } as ApiEnvelope<any>)
- : get(action, params));
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/stu/record?id=864038&gid=864047&kcid=398');
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.textContent).toContain('写作学习记录,原本没有单词');
- expect(harness.routeNativeElement?.querySelector('.record-list')).toBeNull();
- expect(harness.routeNativeElement?.querySelector('a[href="/pages/stu/stu_calendar"]')).not.toBeNull();
- expect(harness.routeNativeElement?.textContent).not.toContain('结束本次复习');
- });
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(wordApi.lastWordParams['cpage']).toBe(2);
- expect(harness.routeNativeElement?.querySelectorAll('.readonly-word-list article').length).toBe(30);
- expect(harness.routeNativeElement?.querySelector('.readonly-word-header')?.textContent).toContain('第 31–60 条 · 共 65 个单词');
- expect(harness.routeNativeElement?.querySelector('.readonly-word-list')?.textContent).toContain('word-31');
- expect(harness.routeNativeElement?.querySelector('.word-preview-pagination')?.textContent).toContain('2 / 3 页');
- await harness.navigateByUrl('/pages/stu/stu_list?nid=999&bindingId=799');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(TestBed.inject(Router).url).toBe('/pages/stu/stu_abc?nids=999&readonly=1');
- expect(harness.routeNativeElement?.querySelector('.selection-header')?.textContent).toContain('词库学习进度');
- await harness.navigateByUrl('/pages/stu/stu_calendar');
- const calendarComponent=harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
- calendarComponent.memoryMonth.set(new Date(2026,7,1));
- calendarComponent.memorySelectedDate.set('2026-08-02');
- (calendarComponent as any).loadCalendar();
- harness.fixture.detectChanges();
- expect(harness.fixture.nativeElement.querySelector('.tabbar')).toBeNull();
- expect(harness.routeNativeElement?.querySelector('.memory-student-filter')).toBeNull();
- expect(harness.routeNativeElement?.querySelectorAll('.memory-date').length).toBe(42);
- expect(harness.routeNativeElement?.querySelectorAll('.memory-date.completed, .memory-date.due').length).toBe(2);
- expect(harness.routeNativeElement?.querySelector('.memory-overview')?.textContent).toContain('2 / 3');
- expect(harness.routeNativeElement?.querySelector('.memory-date-tasks')?.textContent).toContain('高中英语复习');
- expect(harness.routeNativeElement?.querySelector('.review-sequence')?.textContent).toContain('第 3 次复习');
- expect(harness.routeNativeElement?.querySelector('.first-learned-at')?.textContent).toContain('首次学习:2026-07-20 10:20');
- for (let index = 0; index < 3; index += 1) {
- (harness.routeNativeElement?.querySelector('[aria-label="上个月"]') as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- }
- expect(harness.routeNativeElement?.querySelector('[aria-label^="2026-05-15"]')?.classList).toContain('completed');
- expect(harness.routeNativeElement?.textContent).toContain('张雅茜');
- expect(harness.routeNativeElement?.querySelector('.memory-overview header b')?.textContent).toContain('67%');
- expect(harness.routeNativeElement?.querySelector('.memory-stats')?.textContent).toContain('2 / 3');
- await harness.navigateByUrl('/pages/home/ucenter');
- expect(harness.fixture.nativeElement.querySelector('.tabbar')).not.toBeNull();
- expect(harness.routeNativeElement?.querySelector('.new-word-entry')?.getAttribute('href')).toBe('/pages/home/cart');
- expect(harness.routeNativeElement?.querySelector('.course-memory')?.textContent).toContain('21 天抗遗忘计划');
- expect(harness.routeNativeElement?.querySelector('.memory-overview em')?.textContent).toContain('67%');
- expect(harness.routeNativeElement?.querySelector('.memory-overview')?.textContent).toContain('覆盖 3 个复习日');
- expect(harness.routeNativeElement?.querySelector('.memory-counts')?.textContent).toContain('3总任务');
- expect(harness.routeNativeElement?.querySelector('.memory-counts')?.textContent).toContain('2已完成');
- expect(harness.routeNativeElement?.querySelector('.memory-counts')?.textContent).toContain('1到期待复习');
- expect(harness.routeNativeElement?.querySelector('.memory-counts')?.textContent).toContain('0未来待复习');
- await harness.navigateByUrl('/pages/stu/record?id=827482&gid=828268&kcid=203');
- const recordWord = harness.routeNativeElement?.querySelector('.record-word') as HTMLButtonElement;
- expect(recordWord.querySelector('.record-copy strong')?.textContent).toBe('apple');
- expect(harness.routeNativeElement?.textContent).not.toContain('苹果');
- recordWord.click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(recordWord.querySelector('.record-copy strong')?.textContent).toBe('苹果');
- expect(harness.routeNativeElement?.querySelector('.record-overview')?.textContent).toContain('1 / 1');
- await harness.navigateByUrl('/pages/member/starshop');
- expect(harness.fixture.nativeElement.querySelector('.tabbar')).toBeNull();
- expect(harness.routeNativeElement?.querySelector('.word-select-list strong')?.textContent).toBe('tend');
- expect(harness.routeNativeElement?.querySelector('.word-select-list')?.textContent).not.toContain('@张雅茜');
- (harness.routeNativeElement?.querySelector('.word-select-list > button') as HTMLButtonElement).click();
- (harness.routeNativeElement?.querySelector('.selection-header .primary-button') as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- const selectedNewWords = JSON.parse(sessionStorage.getItem('xiaoshu.learning.100.100.practice.selectedWords') ?? '[]') as Array<{ word?: string }>;
- expect(selectedNewWords[0]?.word).toBe('tend');
- expect(sessionStorage.getItem('xiaoshu.learning.100.100.practice.selectedWords')).not.toContain('@张雅茜');
- await harness.navigateByUrl('/pages/his/well?nid=3');
- expect(harness.fixture.nativeElement.querySelector('.tabbar')).toBeNull();
- expect(harness.routeNativeElement?.textContent).toContain('阅读理解专项');
- await harness.navigateByUrl('/pages/home/cart');
- expect(harness.fixture.nativeElement.querySelector('.tabbar')).not.toBeNull();
- expect(harness.fixture.nativeElement.querySelector('.app-header .icon-button')).toBeNull();
- expect(harness.fixture.nativeElement.querySelector('.app-header h1')?.textContent).toContain('伴学中心');
- expect(harness.routeNativeElement?.querySelector('.library-entry')).toBeNull();
- expect(harness.routeNativeElement?.textContent).toContain('我的学习内容');
- expect(harness.routeNativeElement?.querySelectorAll('.course-list li').length).toBe(2);
- expect(harness.routeNativeElement?.querySelectorAll('.course-list button').length).toBe(2);
- expect((harness.routeNativeElement?.querySelector('.course-list button') as HTMLButtonElement).disabled).toBeFalse();
- expect((harness.routeNativeElement?.querySelectorAll('.course-list button')[1] as HTMLButtonElement).disabled).toBeFalse();
- expect(harness.routeNativeElement?.textContent).toContain('【七年级上册】同步词汇');
- expect(harness.routeNativeElement?.textContent).toContain('点击查看老师分配的单词列表');
- (harness.routeNativeElement?.querySelector('.course-list button') as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(TestBed.inject(Router).url).toBe('/pages/stu/stu_list?nid=801&bindingId=701');
- expect(harness.routeNativeElement?.querySelectorAll('.node-list button').length).toBe(2);
- expect((TestBed.inject(ApiService) as unknown as LearningModulesApiService).lastNodeParams['bindingId']).toBe('701');
- await harness.navigateByUrl('/pages/home/cart');
- expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(0);
- expect(harness.routeNativeElement?.querySelectorAll('.calendar-day').length).toBe(42);
- const allDates = Array.from(harness.routeNativeElement?.querySelectorAll('.range-tabs button') ?? [])
- .find((button) => button.textContent?.trim() === '全部') as HTMLButtonElement;
- allDates.click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(3);
- expect(harness.routeNativeElement?.textContent).toContain('同步伴学');
- expect(harness.routeNativeElement?.textContent).toContain('历史伴学');
- const plannedIncompleteFilter = Array.from(harness.routeNativeElement?.querySelectorAll('.schedule-filters button') ?? [])
- .find((button) => button.textContent?.includes('计划未完成')) as HTMLButtonElement;
- expect(plannedIncompleteFilter.textContent).toContain('1');
- plannedIncompleteFilter.click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(1);
- expect(harness.routeNativeElement?.textContent).toContain('计划未完成');
- expect(harness.routeNativeElement?.textContent).not.toContain('未来计划');
- (harness.routeNativeElement?.querySelector('.schedule-filters button') as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- expect(api.lastScheduleParams['mode']).toBe('student');
- expect(api.lastScheduleParams['pageSize']).toBe(20);
- const finishedFilter = Array.from(harness.routeNativeElement?.querySelectorAll('.schedule-filters button') ?? [])
- .find((button) => button.textContent?.includes('已结束')) as HTMLButtonElement;
- finishedFilter.click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(1);
- expect(harness.routeNativeElement?.textContent).toContain('历史伴学');
- const allFilter = harness.routeNativeElement?.querySelector('.schedule-filters button') as HTMLButtonElement;
- allFilter.click();
- harness.fixture.detectChanges();
- const upcomingSchedule = Array.from(harness.routeNativeElement?.querySelectorAll('.schedule-list article') ?? [])
- .find((article) => article.textContent?.includes('同步伴学')) as HTMLElement;
- upcomingSchedule.click();
- await harness.fixture.whenStable();
- expect(TestBed.inject(Router).url).toBe('/pages/shop/stu_get_order?id=901');
- await harness.navigateByUrl('/pages/his/his');
- expect(harness.fixture.nativeElement.querySelector('.tabbar')).toBeNull();
- const aiReadingFeature = harness.routeNativeElement?.querySelector('.ai-reading-feature') as HTMLElement;
- expect(aiReadingFeature.textContent).toContain('AI 阅读巩固');
- expect(aiReadingFeature.textContent).toContain('生成阅读');
- expect(aiReadingFeature.textContent).toContain('AI 批改');
- expect(aiReadingFeature.querySelector('img')?.getAttribute('src')).toBe('/static/images/ai-reading-journey.png');
- const aiReadingCta = aiReadingFeature.querySelector('.ai-reading-cta') as HTMLButtonElement;
- expect(aiReadingCta.disabled).toBeFalse();
- expect(aiReadingFeature.textContent).toContain('最近学过的 1 个单词');
- expect(aiReadingCta.textContent).toContain('立即生成阅读');
- aiReadingCta.click();
- await harness.fixture.whenStable();
- const aiReadingRouter = TestBed.inject(Router);
- expect(aiReadingRouter.url).toContain('/words/go-reading');
- expect(aiReadingRouter.parseUrl(aiReadingRouter.url).queryParams['returnUrl']).toBe('/pages/his/his');
- expect(aiReadingRouter.parseUrl(aiReadingRouter.url).queryParams['GeneralID']).toBe('201');
- await harness.navigateByUrl('/pages/his/his');
- const currentLearningCard = harness.routeNativeElement?.querySelector('.current-learning-card') as HTMLElement;
- const learningRecordCard = harness.routeNativeElement?.querySelector('.learning-record-card') as HTMLElement;
- expect(currentLearningCard.textContent).toContain('我的学习内容');
- const records = harness.fixture.debugElement.query(By.directive(StudyRecordsPageComponent)).componentInstance as StudyRecordsPageComponent;
- expect(records.courseTitle({ Title: '单词课|高中@人教版2022年录', Title2: '人教版2022年录' })).toBe('【高中】人教版2022年录');
- expect(records.courseTitle({ Title: '单词课|初中@人教版2024年9月录', Title2: '人教版2024年9月录' })).toBe('【初中】人教版2024年9月录');
- expect(currentLearningCard.querySelectorAll('.current-learning-list button').length).toBe(2);
- expect((currentLearningCard.querySelectorAll('.current-learning-list button')[1] as HTMLButtonElement).disabled).toBeFalse();
- expect(currentLearningCard.compareDocumentPosition(learningRecordCard) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
- (currentLearningCard.querySelector('.current-learning-list button') as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(TestBed.inject(Router).url).toBe('/pages/stu/stu_list?nid=801&bindingId=701');
- expect(harness.routeNativeElement?.querySelectorAll('.node-list button').length).toBe(2);
- await harness.navigateByUrl('/pages/his/his');
- expect(harness.routeNativeElement?.textContent).toContain('初中英语 / Unit 1');
- expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(2);
- (harness.routeNativeElement?.querySelector('.learning-record-card .learning-item') as HTMLElement).click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(TestBed.inject(Router).url).toBe('/pages/his/his_words?id=201');
- expect(harness.routeNativeElement?.querySelector('.readonly-word-header')?.textContent).toContain('学生只读');
- expect(harness.routeNativeElement?.querySelector('.readonly-word-card')).not.toBeNull();
- expect(harness.routeNativeElement?.querySelector('.selection-header')).toBeNull();
- await harness.navigateByUrl('/pages/his/his');
- (harness.routeNativeElement?.querySelector('.date-trigger') as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- const currentMonth = new Date();
- const monthDifference = (2026 - currentMonth.getFullYear()) * 12 + (7 - currentMonth.getMonth());
- const shiftLabel = monthDifference < 0 ? '上个月' : '下个月';
- for (let index = 0; index < Math.abs(monthDifference); index += 1) {
- (harness.routeNativeElement?.querySelector(`[aria-label="${shiftLabel}"]`) as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- }
- expect(harness.routeNativeElement?.querySelector('.date-picker-popover header strong')?.textContent).toContain('2026年8月');
- expect(harness.routeNativeElement?.querySelectorAll('.date-picker-day').length).toBe(42);
- expect(harness.routeNativeElement?.querySelectorAll('.date-picker-day.has-record').length).toBe(2);
- expect(harness.routeNativeElement?.querySelector('.date-picker-day.has-record')?.getAttribute('aria-label')).toContain('2026-08-01');
- (harness.routeNativeElement?.querySelector('[aria-label^="2026-08-01"]') as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(1);
- expect(harness.routeNativeElement?.querySelector('.learning-record-card')?.textContent).not.toContain('高中英语 / Unit 2');
- (harness.routeNativeElement?.querySelector('.record-actions button') as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(2);
- const completedReading = harness.routeNativeElement?.querySelector('.reading-button.completed') as HTMLButtonElement;
- expect(completedReading.textContent).toContain('已完成阅读');
- expect(completedReading.getAttribute('aria-label')).toBe('查看已完成的历史阅读理解');
- completedReading.click();
- await harness.fixture.whenStable();
- const router = TestBed.inject(Router);
- expect(router.url).toContain('/words/reading');
- expect(router.parseUrl(router.url).queryParams['sid']).toBe('readingarticle201');
- expect(router.parseUrl(router.url).queryParams['returnUrl']).toBe('/pages/his/his');
- await harness.navigateByUrl('/pages/his/his');
- const newReading = harness.routeNativeElement?.querySelector('.reading-button:not(.completed)') as HTMLButtonElement;
- expect(newReading.textContent).toContain('阅读');
- newReading.click();
- await harness.fixture.whenStable();
- expect(router.url).toContain('/words/go-reading');
- expect(router.parseUrl(router.url).queryParams['returnUrl']).toBe('/pages/his/his');
- expect(harness.routeNativeElement?.querySelector('.back-button')).not.toBeNull();
- (harness.routeNativeElement?.querySelector('.back-button') as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- expect(router.url).toBe('/pages/his/his');
- (harness.fixture.nativeElement.querySelector('.app-header .icon-button') as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- expect(router.url).toBe('/pages/home/index');
- await harness.navigateByUrl('/pages/his/exam');
- expect(harness.routeNativeElement?.textContent).toContain('第一组问题');
- expect(harness.routeNativeElement?.textContent).toContain('相当于初二水平');
- expect(harness.routeNativeElement?.textContent).toContain('共 1 题');
- const startButton = harness.routeNativeElement?.querySelector('.exam-intro > button') as HTMLButtonElement;
- startButton.click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelector('.question-word')?.textContent).toContain('apple');
- expect(harness.routeNativeElement?.querySelector('.question-card > h2')).toBeNull();
- expect(harness.routeNativeElement?.querySelectorAll('.exam-options button').length).toBe(1);
- (harness.routeNativeElement?.querySelector('.exam-options button') as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- });
- it('matches coach review tasks to student names by ID when task name fields are blank', async () => {
- user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
- session.groupId.set(3);
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- const response = new Subject<ApiEnvelope<unknown>>();
- api.reviewPageResponse = response;
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/home/index');
- response.next({ retcode: 1, result: {
- items: [
- { GeneralID: 102, yhid: 100, honeyName: '', Title: '旧姓名@复习', kywsj: '2025-07-19', fxzt: 0, xxjlid: 900 },
- { GeneralID: 103, yhid: '101', honeyName: ' ', HoneyName: '', kywsj: '2025-07-15', fxzt: 0, xxjlid: 901 },
- ],
- students: [{ id: '101', name: '李同学', taskCount: 1 }, { id: '100', name: '张雅茜', taskCount: 1 }],
- page: { itemCount: 2, pageCount: 1 },
- summary: { total: 2, completed: 0, due: 2, today: 0, income: 0 }, dates: [],
- } });
- response.complete();
- api.reviewPageResponse = null;
- harness.fixture.detectChanges();
- const names = Array.from(harness.routeNativeElement?.querySelectorAll('.coach-memory-copy strong') ?? []).map((node) => node.textContent);
- expect(names).toEqual(['张雅茜', '李同学']);
- const home = harness.fixture.debugElement.query(By.directive(HomePageComponent)).componentInstance as HomePageComponent;
- expect(home.coachReviewStudent({ yhid: 102, honeyName: '', HoneyName: ' ', Title: '王同学@复习' })).toBe('王同学');
- expect(home.coachReviewStudent({ yhid: 103, honeyName: '', Title: ' ' })).toBe('学员 103');
- expect(home.coachReviewStudent({ honeyName: '' })).toBe('未关联学员');
- (harness.routeNativeElement?.querySelectorAll('.coach-memory-list article > button')[1] as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- expect(session.student()).toEqual(jasmine.objectContaining({ userId: 101, honeyName: '李同学' }));
- expect(TestBed.inject(Router).url).toContain('/pages/stu/record?id=901&gid=103');
- });
- it('keeps the complete point-reading flow available to coaches', async () => {
- user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
- session.groupId.set(3);
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/home/index');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const coachLinks = Array.from(harness.routeNativeElement?.querySelectorAll('.coach-quick-grid > a') ?? []).map((link) => link.getAttribute('href'));
- expect(coachLinks).toEqual(['/pages/home/shop', '/pages/member/ke_xiao', '/pages/his/his', '/pages/member/ke_fuxi']);
- expect(harness.routeNativeElement?.querySelector('.home-header .header-action')).toBeNull();
- const coachAccountTab = (Array.from(harness.fixture.nativeElement.querySelectorAll('.tabbar a')) as HTMLAnchorElement[])
- .find((link) => link.getAttribute('href') === '/pages/home/ucenter');
- expect(coachAccountTab?.textContent).toContain('我的');
- expect(harness.routeNativeElement?.querySelector('.content-section')).toBeNull();
- const memoryApi = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- expect(memoryApi.lastMemoryParams).toEqual(jasmine.objectContaining({ uid: 300, pageSize:20 }));
- expect(harness.routeNativeElement?.querySelector('.coach-memory-board')?.textContent).toContain('抗遗忘带练');
- expect(harness.routeNativeElement?.querySelector('.coach-memory-board')?.textContent).toContain('张雅茜');
- expect(harness.routeNativeElement?.querySelector('.coach-memory-list article.ready > button')?.textContent).toContain('带学生复习');
- expect(harness.routeNativeElement?.querySelector('.coach-memory-footer')?.getAttribute('href')).toBe('/pages/stu/stu_calendar');
- (harness.routeNativeElement?.querySelector('.coach-memory-list article.ready > button') as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- expect(TestBed.inject(Router).url).toContain('/pages/stu/record?id=900&gid=102');
- expect(session.student()?.userId).toBe(100);
- await harness.navigateByUrl('/pages/stu/stu_calendar');
- const calendarComponent=harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
- calendarComponent.memoryMonth.set(new Date(2026,7,1));
- calendarComponent.memorySelectedDate.set('2026-08-02');
- (calendarComponent as any).loadCalendar();
- harness.fixture.detectChanges();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(memoryApi.lastMemoryParams).toEqual(jasmine.objectContaining({ uid: 300, pageSize:20 }));
- expect(harness.routeNativeElement?.querySelector('.memory-copy')?.textContent).toContain('关联学员抗遗忘任务');
- expect(harness.routeNativeElement?.querySelector('.memory-date-tasks')?.textContent).toContain('张雅茜');
- expect(harness.routeNativeElement?.querySelector('.memory-date-tasks button')?.textContent).toContain('带学生复习');
- const memoryStudentFilter = harness.routeNativeElement?.querySelector('[aria-label="筛选抗遗忘学员"] select') as HTMLSelectElement;
- expect(memoryStudentFilter.options.length).toBe(2);
- memoryStudentFilter.value = '100';
- memoryStudentFilter.dispatchEvent(new Event('change'));
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelector('.memory-date-tasks')?.textContent).toContain('张雅茜');
- expect(harness.routeNativeElement?.querySelector('.memory-date-tasks')?.textContent).not.toContain('李同学');
- expect(harness.routeNativeElement?.querySelector('.memory-stats')?.textContent).toContain('2 / 3');
- await harness.navigateByUrl('/pages/member/ke_xiao');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const monthFilter = harness.routeNativeElement?.querySelector('[aria-label="按月份筛选销课记录"]') as HTMLSelectElement;
- const monthPicker = harness.routeNativeElement?.querySelector('.month-control .xs-dropdown-trigger') as HTMLButtonElement;
- expect(monthFilter.value).toBe('all');
- expect(monthFilter.options.length).toBe(4);
- expect(monthPicker).not.toBeNull();
- expect(Number.parseFloat(getComputedStyle(monthPicker).paddingLeft)).toBeGreaterThanOrEqual(40);
- expect(monthPicker.getBoundingClientRect().height).toBe(46);
- expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(5);
- monthFilter.value = '2026-03';
- monthFilter.dispatchEvent(new Event('change'));
- const studentSearch = harness.routeNativeElement?.querySelector('[aria-label="按学生姓名筛选销课记录"]') as HTMLInputElement;
- studentSearch.value = '花';
- studentSearch.dispatchEvent(new Event('input'));
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(1);
- expect(harness.routeNativeElement?.querySelector('.schedule-list')?.textContent).toContain('花花');
- expect(harness.routeNativeElement?.querySelector('.schedule-band > header')?.textContent).toContain('1 / 5');
- (harness.routeNativeElement?.querySelector('.history-filter-panel footer button') as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(5);
- await harness.navigateByUrl('/pages/his/his');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect((harness.routeNativeElement?.querySelector('.student-filter select') as HTMLSelectElement).selectedOptions[0]?.textContent).toContain('张雅茜');
- expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(2);
- expect(session.student()?.userId).toBe(100);
- (harness.routeNativeElement?.querySelector('.learning-record-card .learning-item') as HTMLElement).click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(TestBed.inject(Router).url).toBe('/pages/his/his_words?id=201');
- expect(harness.routeNativeElement?.querySelector('.readonly-word-header')?.textContent).toContain('历史记录');
- expect(harness.routeNativeElement?.querySelector('.readonly-word-card')?.textContent).toContain('apple');
- expect(harness.routeNativeElement?.querySelector('.readonly-word-card')?.textContent).toContain('共 1 个单词');
- expect(harness.routeNativeElement?.querySelector('.selection-header')).toBeNull();
- await harness.navigateByUrl('/pages/his/his');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const teacherCourse = harness.routeNativeElement?.querySelector('.current-learning-list button') as HTMLButtonElement;
- expect(teacherCourse.disabled).toBeFalse();
- expect(teacherCourse.classList.contains('readonly')).toBeFalse();
- expect(teacherCourse.textContent).toContain('选择本次学习单元');
- teacherCourse.click();
- await harness.fixture.whenStable();
- expect(TestBed.inject(Router).url).toContain('/pages/stu/stu_list');
- await harness.navigateByUrl('/pages/home/ucenter');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const teacherServices = Array.from(harness.routeNativeElement?.querySelectorAll('.service-grid > a') ?? []).map((link) => link.getAttribute('href'));
- expect(teacherServices).toEqual(['/pages/home/shop', '/pages/member/ke_xiao', '/pages/his/his', '/pages/member/feedback']);
- expect(harness.routeNativeElement?.querySelector('.coach-wallet-strip')?.textContent).toContain('抗遗忘工资');
- await harness.navigateByUrl('/pages/stu/stu_list?nid=801&bindingId=701');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect((TestBed.inject(ApiService) as unknown as LearningModulesApiService).lastNodeParams['userId']).toBe(100);
- const units = harness.routeNativeElement?.querySelectorAll('.node-list button') as NodeListOf<HTMLButtonElement>;
- units[0].click();
- units[1].click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelector('.unit-selection-summary')?.textContent).toContain('已选 2 个单元');
- (harness.routeNativeElement?.querySelector('.unit-actions .primary-button') as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const router = TestBed.inject(Router);
- expect(router.url).toContain('/pages/stu/stu_abc');
- expect(harness.routeNativeElement?.querySelector('.pending-column')?.textContent).toContain('banana');
- expect(harness.routeNativeElement?.querySelector('.completed-column')).toBeNull();
- expect(harness.routeNativeElement?.querySelectorAll('.pending-column .word-refine-item').length).toBe(1);
- (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelector('.completed-column')?.textContent).toContain('apple');
- expect(harness.routeNativeElement?.querySelector('.pending-column')).toBeNull();
- (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[0] as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- const firstWord = harness.routeNativeElement?.querySelector('.pending-column .word-refine-item') as HTMLElement;
- (firstWord.querySelector('.word-reveal') as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- (firstWord.querySelector('.known-button') as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelectorAll('.pending-column .word-refine-item').length).toBe(0);
- expect(harness.routeNativeElement?.querySelector('.selection-header .primary-button')?.textContent).toContain('保存识词结果');
- (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelectorAll('.completed-column .word-refine-item').length).toBe(2);
- (harness.routeNativeElement?.querySelector('.completed-word-action button') as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelectorAll('.completed-column .word-refine-item').length).toBe(1);
- (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[0] as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelectorAll('.pending-column .word-refine-item').length).toBe(1);
- (harness.routeNativeElement?.querySelector('.selection-header .primary-button') as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(router.url).toContain('/pages/stu/stu_con');
- expect(harness.routeNativeElement?.querySelectorAll('.repeat-list > button').length).toBe(1);
- expect(harness.routeNativeElement?.querySelector('.readonly-word-header')).toBeNull();
- await harness.navigateByUrl('/pages/stu/pl_stu5?nids=811&pnid=801&bindingId=701');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const spellingActions = harness.routeNativeElement?.querySelector('.teacher-spelling-actions') as HTMLElement;
- expect(spellingActions.querySelectorAll('button').length).toBe(3);
- expect(spellingActions.textContent).toContain('跳到下一关');
- expect((harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent).words().length).toBe(1);
- expect(localStorage.getItem('xiaoshu.learning.300.100.701._page_word_orilist')).not.toBeNull();
- (spellingActions.querySelectorAll('button')[1] as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(router.url).toBe('/pages/his/his');
- const standaloneRecord = (TestBed.inject(ApiService) as unknown as LearningModulesApiService).lastStandaloneLearningRecord;
- expect(JSON.parse(String(standaloneRecord['content']))).toEqual(jasmine.objectContaining({ ModelID: 56, Hits: 1, title: '张雅茜' }));
- expect(JSON.parse(String(standaloneRecord['addon']))).toEqual(jasmine.objectContaining({ UserID: 100, pl: 300, learned: 1, createReviewPlan: false }));
- }, 20000);
- it('loads appointment courses and units for the appointment student when no student is selected in the session', async () => {
- user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
- session.groupId.set(3);
- session.student.set(null);
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/shop/get_order?id=901');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- expect(session.student()?.userId).toBe(100);
- expect(api.lastAppointmentCourseParams['myfield']).toBe('yhid=100');
- expect(api.lastNodeParams['userId']).toBe(100);
- const courseSelect = harness.routeNativeElement?.querySelector('.course-section select') as HTMLSelectElement;
- expect(courseSelect).toBeNull();
- expect(harness.routeNativeElement?.querySelector('.course-section')?.textContent).toContain('七年级上册');
- expect(harness.routeNativeElement?.querySelectorAll('.unit-grid article').length).toBe(2);
- const unitButtons = harness.routeNativeElement?.querySelectorAll<HTMLButtonElement>('.unit-select');
- expect(unitButtons?.[0].textContent).toContain('已学 5%');
- expect(unitButtons?.[0].textContent).toContain('1/20 词');
- expect(unitButtons?.[1].textContent).toContain('已学 0%');
- expect(unitButtons?.[1].textContent).toContain('0/18 词');
- expect((unitButtons?.[0].querySelector('.unit-progress-track > span') as HTMLElement).style.width).toBe('5%');
- unitButtons?.[0].click();
- unitButtons?.[1].click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelectorAll('.unit-select[aria-pressed="true"]').length).toBe(2);
- expect(unitButtons?.[0].textContent).toContain('已学 5%');
- });
- it('distinguishes completed, empty and unavailable appointment vocabulary progress', async () => {
- user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
- session.groupId.set(3);
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- api.unitRows = [
- { NodeID: 811, NodeName: '已学全册', yx_word: '20', total: '20' },
- { NodeID: 812, NodeName: '空册', yx_word: 0, total: 0 },
- { NodeID: 813, NodeName: '缺少统计' },
- { NodeID: 814, NodeName: '异常统计', yx_word: 'invalid', total: 20 },
- ];
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/shop/get_order?id=901');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const units = harness.routeNativeElement?.querySelectorAll('.unit-select');
- expect(units?.[0].textContent).toContain('已学 100%');
- expect(units?.[0].textContent).toContain('20/20 词');
- expect(units?.[1].textContent).toContain('已学 0%');
- expect(units?.[1].textContent).toContain('暂无单词');
- for (const index of [2, 3]) {
- expect(units?.[index].textContent).toContain('暂无进度数据');
- expect(units?.[index].querySelector('.unit-progress-track')).toBeNull();
- }
- });
- it('opens feedback only on ending a lesson and submits once after validation', async () => {
- user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
- session.groupId.set(3);
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- api.appointmentStatus = 10;
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/shop/get_order?id=901');
- const component = harness.fixture.debugElement.query(By.directive(AppointmentPageComponent)).componentInstance as AppointmentPageComponent;
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const dialog = harness.routeNativeElement?.querySelector('dialog') as HTMLDialogElement;
- const endButton = harness.routeNativeElement?.querySelector('.appointment-actions .primary-button') as HTMLButtonElement;
- expect(dialog.open).toBeFalse();
- endButton.click();
- harness.fixture.detectChanges();
- expect(dialog.open).toBeTrue();
- expect(api.completionRequests.length).toBe(0);
- const submit = dialog.querySelector('.feedback-actions .primary-button') as HTMLButtonElement;
- submit.click();
- harness.fixture.detectChanges();
- expect(dialog.querySelector('[role="alert"]')?.textContent).toContain('请填写本次学习内容');
- expect(api.completionRequests.length).toBe(0);
- const summary = dialog.querySelector('textarea') as HTMLTextAreaElement;
- summary.value = 'Unit 3 词汇';
- summary.dispatchEvent(new Event('input'));
- await harness.fixture.whenStable();
- (dialog.querySelector('.feedback-actions .secondary-button') as HTMLButtonElement).click();
- expect(dialog.open).toBeFalse();
- expect(api.appointmentStatus).toBe(10);
- endButton.click();
- harness.fixture.detectChanges();
- expect((dialog.querySelector('textarea') as HTMLTextAreaElement).value).toBe('Unit 3 词汇');
- component.reviewStatus = '完成';
- component.trainingStatus = '需要巩固';
- component.trainingSummary = '下次复习 Unit 3';
- component.noHomework = true;
- submit.click();
- submit.click();
- harness.fixture.detectChanges();
- expect(api.completionRequests.length).toBe(1);
- expect(JSON.parse(String(api.completionRequests[0]['feedback']))).toEqual(jasmine.objectContaining({ contentSummary: 'Unit 3 词汇', noHomework: true, homework: '无作业' }));
- expect(submit.disabled).toBeTrue();
- dialog.dispatchEvent(new Event('cancel', { cancelable: true }));
- expect(dialog.open).toBeTrue();
- api.appointmentStatus = 11;
- api.completionResponse.next({ retcode: 1, result: {} });
- api.completionResponse.complete();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelector('dialog')).toBeNull();
- expect(harness.routeNativeElement?.textContent).toContain('等待学员确认');
- });
- it('keeps feedback editable after a failed submission and allows closing with Escape', async () => {
- user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
- session.groupId.set(3);
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- api.appointmentStatus = 10;
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/shop/get_order?id=901');
- const component = harness.fixture.debugElement.query(By.directive(AppointmentPageComponent)).componentInstance as AppointmentPageComponent;
- await harness.fixture.whenStable();
- component.openCompletionFeedback();
- component.contentSummary = 'Unit 3';
- component.reviewStatus = '完成';
- component.trainingStatus = '正常进行';
- component.trainingSummary = '继续 Unit 4';
- component.noHomework = true;
- component.finishCoachLesson();
- api.completionResponse.error(new Error('提交失败,请重试'));
- harness.fixture.detectChanges();
- const dialog = harness.routeNativeElement?.querySelector('dialog') as HTMLDialogElement;
- expect(dialog.open).toBeTrue();
- expect(dialog.querySelector('[role="alert"]')?.textContent).toContain('提交失败,请重试');
- expect((dialog.querySelector('fieldset') as HTMLFieldSetElement).disabled).toBeFalse();
- expect(component.contentSummary).toBe('Unit 3');
- expect(api.appointmentStatus).toBe(10);
- dialog.dispatchEvent(new Event('cancel', { cancelable: true }));
- expect(dialog.open).toBeFalse();
- });
- it('counts words marked as known as completed and persists them for the student', async () => {
- user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
- session.groupId.set(3);
- session.student.set({ userId: 100, userName: 'student', honeyName: '张雅茜', groupId: 1 });
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/stu/stu_abc?nids=811,812&pnid=801');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const pendingWord = harness.routeNativeElement?.querySelector('.pending-column .word-refine-item') as HTMLElement;
- (pendingWord.querySelector('.word-reveal') as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- (pendingWord.querySelector('.known-button') as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- (harness.routeNativeElement?.querySelector('.selection-header .primary-button') as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- expect(api.lastLearningWordUpdate).toEqual(jasmine.objectContaining({ uid: 100, wordsId: '1002', kcid: '801', save: 1 }));
- expect(TestBed.inject(Router).url).toContain('/pages/stu/stu_abc');
- expect(harness.routeNativeElement?.querySelector('.completed-column')).toBeNull();
- (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelectorAll('.completed-column .word-refine-item').length).toBe(2);
- expect(harness.routeNativeElement?.querySelector('.pending-column')).toBeNull();
- });
- it('finishes the last vocabulary group, loads the coach record and opens completion feedback', async () => {
- user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
- session.groupId.set(3);
- session.student.set({ userId: 100, honeyName: '张雅茜', groupId: 1 });
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- api.appointmentStatus = 10;
- const word = { GeneralID: 1001, NodeID: 811, Title: 'apple', sy: '苹果' };
- localStorage.setItem('xiaoshu.learning.300.100.901._page_word_orilist', JSON.stringify([[word]]));
- localStorage.setItem('xiaoshu.learning.300.100.901._page_current_learn', '1');
- // Stale page-count metadata must not create a nonexistent next group.
- localStorage.setItem('xiaoshu.learning.300.100.901._page_total_page', '2');
- localStorage.removeItem('xiaoshu.learning.300.100.901.learn_mode');
- const harness = await RouterTestingHarness.create();
- const router = TestBed.inject(Router);
- const navigate = spyOn(router, 'navigate').and.callThrough();
- await harness.navigateByUrl('/pages/stu/stu_con?nids=811&pnid=801&orderId=901');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const learning = () => harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
- learning().skipStudyList();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(router.url).toContain('/pages/stu/stu3');
- const pair = learning().words()[0];
- learning().chooseMatch('left', pair);
- learning().chooseMatch('right', pair);
- learning().continueAfterStage();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(router.url).toContain('/pages/stu/stu4');
- learning().chooseMeaning(learning().words()[0]);
- learning().continueAfterStage();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(router.url).toContain('/pages/stu/pl_stu5');
- learning().skipSpellingStage();
- expect(learning().hasNextWordBatch).toBeFalse();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- harness.fixture.detectChanges();
- expect(router.url).toContain('/pages/stu/pl_end');
- expect(api.lastAppointmentRecordParams['myfield2']).toBe('UserId=100|dsid=901|pl=300');
- expect(harness.routeNativeElement?.querySelector('.empty-state.error')).toBeNull();
- expect(learning().words()[0].word).toBe('apple');
- learning().markAllKnown();
- learning().submitReview();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- harness.fixture.detectChanges();
- expect(router.url).toBe('/pages/shop/get_order?id=901&feedback=1');
- expect((harness.routeNativeElement?.querySelector('dialog') as HTMLDialogElement).open).toBeTrue();
- expect(api.appointmentStatus).toBe(10);
- expect(api.completionRequests.length).toBe(0);
- expect(api.studyUpdates.length).toBe(2);
- expect(JSON.parse(String(api.studyUpdates[1]['addon']))['ygg']).toBe(1);
- expect(navigate.calls.all().every(call => call.args[1]?.replaceUrl === true)).toBeTrue();
- }, 20000);
- it('advances only to a real next group and returns from the last stage to its course in one click', async () => {
- user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
- session.groupId.set(3);
- session.student.set({ userId: 100, honeyName: '张雅茜', groupId: 1 });
- const pages = [[{ GeneralID: 1001, Title: 'apple', sy: '苹果' }, { GeneralID: 1002, Title: 'banana', sy: '香蕉' }], [{ GeneralID: 1003, Title: 'cherry', sy: '樱桃' }]];
- localStorage.setItem('xiaoshu.learning.300.100.901._page_word_orilist', JSON.stringify(pages));
- localStorage.setItem('xiaoshu.learning.300.100.901._page_total_page', '0');
- localStorage.setItem('xiaoshu.learning.300.100.901._page_current_learn', '1');
- const harness = await RouterTestingHarness.create();
- const router = TestBed.inject(Router);
- await harness.navigateByUrl('/pages/stu/pl_stu5?nids=811&pnid=801&orderId=901');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- const component = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
- expect(component.words().length).toBe(2);
- component.skipSpellingStage();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(router.url).toContain('/pages/stu/stu_con');
- const next = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
- expect(next.words()[0].word).toBe('cherry');
- expect((TestBed.inject(ApiService) as unknown as LearningModulesApiService).studyUpdates.length).toBe(0);
- (harness.routeNativeElement?.querySelector('.app-header button[aria-label="返回"]') as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(router.url).toBe('/pages/shop/get_order?id=901');
- for (const key of ['_page_word_orilist', '_page_total_page', '_page_current_learn', 'skip_spelling_stage']) localStorage.removeItem(`xiaoshu.learning.300.100.901.${key}`);
- });
- it('keeps teacher-led spelling skipped through the remaining word groups', async () => {
- user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
- session.groupId.set(3);
- session.student.set({ userId: 100, honeyName: '张雅茜', groupId: 1 });
- const prefix = 'xiaoshu.learning.300.100.901.';
- const pages = [[{ GeneralID: 1001, Title: 'apple', sy: '苹果' }], [{ GeneralID: 1002, Title: 'banana', sy: '香蕉' }]];
- localStorage.setItem(prefix + '_page_word_orilist', JSON.stringify(pages));
- localStorage.setItem(prefix + '_page_current_learn', '1');
- const harness = await RouterTestingHarness.create();
- const router = TestBed.inject(Router);
- await harness.navigateByUrl('/pages/stu/pl_stu5?nids=811&pnid=801&orderId=901');
- const learning = () => harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
- learning().skipSpellingStage();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(router.url).toContain('/pages/stu/stu_con');
- expect(localStorage.getItem(prefix + 'skip_spelling_stage')).toBe('true');
- learning().skipStudyList();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- learning().chooseMatch('left', learning().words()[0]);
- learning().chooseMatch('right', learning().words()[0]);
- learning().continueAfterStage();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(router.url).toContain('/pages/stu/stu4');
- learning().chooseMeaning(learning().words()[0]);
- expect(learning().stageActionLabel).toBe('查看学习结果');
- learning().continueAfterStage();
- await harness.fixture.whenStable();
- expect(router.url).toContain('/pages/stu/pl_end');
- expect(localStorage.getItem(prefix + 'skip_spelling_stage')).toBe('true');
- localStorage.removeItem(prefix + 'skip_spelling_stage');
- }, 15000);
- it('resolves the appointment student before reading end results and offers an exit on failure', async () => {
- user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
- session.groupId.set(3);
- session.student.set({ userId: 999, honeyName: '之前的学员', groupId: 1 });
- const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
- const get = api.get.bind(api);
- spyOn(api, 'get').and.callFake((action, params) => action === 'e_record_detail'
- ? throwError(() => new Error('学习记录暂时无法加载')) : get(action, params));
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/stu/pl_end?id=901');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- harness.fixture.detectChanges();
- expect(session.student()?.userId).toBe(100);
- expect(api.lastAppointmentRecordParams['myfield2']).toBe('UserId=100|dsid=901|pl=300');
- expect(harness.routeNativeElement?.querySelector('.empty-state.error')?.textContent).toContain('学习记录暂时无法加载');
- (harness.routeNativeElement?.querySelector('.empty-state.error .primary-button') as HTMLButtonElement).click();
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(TestBed.inject(Router).url).toBe('/pages/shop/get_order?id=901');
- });
- it('shows learning history in pages of twenty records', async () => {
- user.set({ userId: 777, userName: 'paged-student', honeyName: '分页学员', groupId: 1 });
- session.groupId.set(1);
- const harness = await RouterTestingHarness.create();
- await harness.navigateByUrl('/pages/his/his');
- await harness.fixture.whenStable();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelector('.record-actions')?.textContent).toContain('45 条');
- expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(20);
- expect(harness.routeNativeElement?.querySelector('.record-pagination')?.textContent).toContain('第 1 / 3 页');
- const pageButtons = harness.routeNativeElement?.querySelectorAll('.record-pagination button') as NodeListOf<HTMLButtonElement>;
- expect(pageButtons[0].disabled).toBeTrue();
- pageButtons[1].click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(20);
- expect(harness.routeNativeElement?.querySelector('.record-pagination')?.textContent).toContain('第 2 / 3 页');
- (harness.routeNativeElement?.querySelectorAll('.record-pagination button')[1] as HTMLButtonElement).click();
- harness.fixture.detectChanges();
- expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(5);
- expect((harness.routeNativeElement?.querySelectorAll('.record-pagination button')[1] as HTMLButtonElement).disabled).toBeTrue();
- });
- });
|