home-learning-modules.integration.spec.ts 101 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614
  1. import { provideHttpClient } from '@angular/common/http';
  2. import { signal } from '@angular/core';
  3. import { TestBed } from '@angular/core/testing';
  4. import { By } from '@angular/platform-browser';
  5. import { provideRouter, Router } from '@angular/router';
  6. import { RouterTestingHarness } from '@angular/router/testing';
  7. import { Observable, of, Subject, throwError } from 'rxjs';
  8. import { CourseCenterPageComponent } from '../features/course-center/course-center-page.component';
  9. import { AppointmentPageComponent } from '../features/appointment/appointment-page.component';
  10. import { LearningPageComponent } from '../features/learning/learning-page.component';
  11. import { StudyRecordsPageComponent } from '../features/study-records/study-records-page.component';
  12. import { HomePageComponent } from '../features/home/home-page.component';
  13. import { routes } from '../app.routes';
  14. import { ApiService } from './api.service';
  15. import { ApiEnvelope, AppUser } from './models';
  16. import { PlatformService } from './platform.service';
  17. import { SessionService } from './session.service';
  18. class LearningModulesApiService {
  19. lastScheduleParams: Record<string, unknown> = {};
  20. lastNodeParams: Record<string, unknown> = {};
  21. lastAppointmentCourseParams: Record<string, unknown> = {};
  22. lastWordParams: Record<string, unknown> = {};
  23. lastLearningWordUpdate: Record<string, unknown> = {};
  24. lastMemoryParams: Record<string, unknown> = {};
  25. lastStandaloneLearningRecord: Record<string, unknown> = {};
  26. appointmentStatus = 0;
  27. historyWordAudio = '';
  28. completionRequests: Record<string, unknown>[] = [];
  29. completionResponse = new Subject<ApiEnvelope<unknown>>();
  30. lastAppointmentRecordParams: Record<string, unknown> = {};
  31. studyUpdates: Record<string, unknown>[] = [];
  32. reviewPageResponse: Subject<ApiEnvelope<unknown>> | null = null;
  33. unitRows: Record<string, unknown>[] = [
  34. { NodeID: 811, NodeName: 'Unit 1', yx_word: 1, total: 20 },
  35. { NodeID: 812, NodeName: 'Unit 2', yx_word: 0, total: 18 },
  36. ];
  37. get<T>(action: string, params: Record<string, unknown> = {}): Observable<ApiEnvelope<T>> {
  38. if (action === 'content_list' && Number(params['ModelID'] ?? params['modelId']) === 56 && String(params['myfield2']).includes('dsid=')) {
  39. this.lastAppointmentRecordParams = params;
  40. if (!String(params['myfield2']).includes('|pl=300')) return throwError(() => new Error('无权查看该学员的内容记录'));
  41. return this.result<T>([{ GeneralID: 9901 }]);
  42. }
  43. if (action === 'e_record_detail') {
  44. return this.result<T>([{ GeneralID: 1001, NodeID: 811, Title: 'apple', sy: '苹果', check: 0 }]);
  45. }
  46. if (action === 'e_order_detail') {
  47. return this.result<T>([{
  48. GeneralID: 901,
  49. szyh: 100,
  50. HoneyName: '张雅茜',
  51. kcid: 801,
  52. yykcmc: '七年级上册',
  53. dszt: this.appointmentStatus,
  54. dslx: 2,
  55. bxrq: '2026-08-10',
  56. sdsd: '19:00',
  57. jffs: '线上',
  58. }]);
  59. }
  60. if (action === 'content_list' && Number(params['nid']) === 291) {
  61. return this.result<T>([
  62. { GeneralID: 201, CreateTime: '2026-08-01 09:30:00', con: '初中英语@Unit 1', xxqs: JSON.stringify([{ Title: 'apple' }]) },
  63. { GeneralID: 202, CreateTime: '2026-08-04 16:20:00', con: '高中英语@Unit 2' },
  64. ]);
  65. }
  66. if (action === 'content_list' && Number(params['nid']) === 388) {
  67. return this.result<T>([
  68. { GeneralID: 100, Title: '张雅茜@历史任务', kywsj: '2026-05-15 09:00', fxzt: 1, yhid: 100, xxjlid: 900 },
  69. { GeneralID: 101, Title: '张雅茜@任务', kywsj: '2026-08-01 09:00', fxzt: 1, yhid: 100, xxjlid: 900 },
  70. { GeneralID: 102, Title: '张雅茜@任务', kywsj: '2026-08-02 09:00', fxzt: 0, yhid: 100, xxjlid: 900 },
  71. ]);
  72. }
  73. if (action === 'content_list' && Number(params['nid']) === 3) {
  74. return this.result<T>([{ GeneralID: 301, Title: '阅读理解专项' }]);
  75. }
  76. if (action === 'content_list' && Number(params['nid']) === 389) {
  77. return this.result<T>([{ GeneralID: 401, df: 18, totalScore: 48, dontKnow: 20, wrong: 10 }]);
  78. }
  79. if (action === 'content_list' && Number(params['nid']) === 28) {
  80. this.lastAppointmentCourseParams = params;
  81. return this.result<T>([
  82. { GeneralID: 801, kcid: 801, Title: '初中英语|七年级上册@同步词汇' },
  83. { GeneralID: 802, kcid: 801, Title2: '【初中英语】阅读训练' },
  84. ]);
  85. }
  86. if (action === 'content_list' && Number(params['ModelID']) === 58) {
  87. return this.result<T>([{ GeneralID: 701, yhid: 100, kcid: 801, syjd: '{}' }]);
  88. }
  89. if (action === 'content_list' && Number(params['nid']) === 29) {
  90. this.lastScheduleParams = params;
  91. return this.result<T>([
  92. { GeneralID: 901, bxrq: '2026-08-10 00:00:00', sdsd: '19:00', Title: '同步伴学', plxm: '张老师', dszt: 10 },
  93. { GeneralID: 902, bxrq: '2026-08-03 00:00:00', sdsd: '18:00', Title: '历史伴学', plxm: '李老师', dszt: 30 },
  94. ]);
  95. }
  96. if (action === 'node_list' && Number(params['pid']) === 8) {
  97. return this.result<T>([{ NodeID: 801, NodeName: '小学人教版词库' }]);
  98. }
  99. if (action === 'node_list' && Number(params['pid']) === 801) {
  100. this.lastNodeParams = params;
  101. return this.result<T>(this.unitRows);
  102. }
  103. if (action === 'e_ck_list') {
  104. this.lastWordParams = params;
  105. const allWords = Number(params['nids']) === 813
  106. ? Array.from({ length: 65 }, (_, index) => ({ GeneralID: 2001 + index, NodeID: 813, Title: `word-${index + 1}`, sy: `释义 ${index + 1}` }))
  107. : [
  108. { GeneralID: 1001, NodeID: 811, Title: 'apple', sy: 'n. 苹果', yp: 'apple.mp3', w_learned: 1 },
  109. { GeneralID: 1002, NodeID: 812, Title: 'banana', sy: 'n. 香蕉', yp: 'banana.mp3', w_learned: 0 },
  110. ];
  111. const pageIndex = Math.max(1, Number(params['cpage'] ?? 1));
  112. const pageSize = Math.max(1, Number(params['psize'] ?? 30));
  113. const result = allWords.slice((pageIndex - 1) * pageSize, pageIndex * pageSize);
  114. return of({
  115. retcode: 1,
  116. result: result as T,
  117. page: { itemCount: allWords.length, pageCount: Math.ceil(allWords.length / pageSize), pageIndex, pageSize },
  118. });
  119. }
  120. if (action === 'node_get') return this.result<T>([{ NodeID: 3, NodeName: '专项训练' }]);
  121. if (action === 'vote_ask') return this.result<T>([{ ID: 1, Title: '第一组问题' }]);
  122. if (action === 'vote_question') {
  123. return this.result<T>([{ ID: 501, AskID: 1, QType: 1, QTitle: 'apple', QContent: '苹果', QOption: JSON.stringify([{ text: '苹果', value: 1 }]) }]);
  124. }
  125. if (action === 'e_words_list') {
  126. return this.result<T>([{
  127. GeneralID: 601,
  128. Title: 'tend@张雅茜',
  129. detail: [{ GeneralID: 701, Title: 'tend', sy: 'vt.照顾;照料', yp: 'tend.mp3' }],
  130. }]);
  131. }
  132. return this.result<T>([]);
  133. }
  134. post<T>(action: string, params: Record<string, unknown> = {}): Observable<ApiEnvelope<T>> {
  135. if (action === 'stu_record_update_v2') {
  136. this.studyUpdates.push(params);
  137. return this.result<T>({ GeneralID: 9901 });
  138. }
  139. if (action === 'e_order_complete_feedback') {
  140. this.completionRequests.push(params);
  141. return this.completionResponse as Observable<ApiEnvelope<T>>;
  142. }
  143. if (action === 'e_add_words') {
  144. this.lastLearningWordUpdate = params;
  145. return this.result<T>({ affected: String(params['wordsId'] ?? '').split(',').filter(Boolean).length });
  146. }
  147. if (action === 'content_add_zt') {
  148. this.lastStandaloneLearningRecord = params;
  149. return this.result<T>({ GeneralID: 9901 });
  150. }
  151. if (action === 'e_order_tongji') {
  152. return this.result<T>({ t30: 4, t60: 6, t_tiyan: 1, t_total: 11, t_shichang: 9, t_yongji: 320 });
  153. }
  154. if (action === 'e_user_list') {
  155. return this.result<T>([{ szyh: 100, honeyname: '张雅茜' }]);
  156. }
  157. if (action === 'app_learning_overview') {
  158. const records = Number(params['studentId']) === 777
  159. ? Array.from({ length: 45 }, (_, index) => ({
  160. GeneralID: 5000 + index,
  161. CreateTime: `2026-08-${String(31 - (index % 20)).padStart(2, '0')} 09:30:00`,
  162. dqrq: '20260801',
  163. con: `分页学习记录 ${index + 1}`,
  164. }))
  165. : [
  166. { 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' },
  167. { GeneralID: 202, CreateTime: '2026-08-04 16:20:00', dqrq: '20260804', con: '高中英语@Unit 2', xxqs: JSON.stringify([{ Title: 'banana' }]) },
  168. ];
  169. const selected = String(params['date'] ?? '');
  170. return this.result<T>({
  171. records: (selected ? records.filter((item) => item.CreateTime.startsWith(selected)) : records).slice((Number(params['page']||1)-1)*20,Number(params['page']||1)*20),
  172. page:{itemCount:selected?records.filter(item=>item.CreateTime.startsWith(selected)).length:records.length,pageCount:Math.ceil(records.length/20)},
  173. latestReadingRecord: records[0],
  174. recordDateCounts: { '2026-08-01': 1, '2026-08-04': 1 },
  175. scheduleDateCounts: {},
  176. schedules: [],
  177. courses: [
  178. { GeneralID: 801, id: 701, kcid: 801, Title: '初中英语|七年级上册@同步词汇' },
  179. { GeneralID: 802, id: 702, Title2: '【初中英语】阅读训练', syjd: '{"811":"811@Unit 1@1"}' },
  180. ],
  181. refreshedAt: '2026-08-26T00:00:00.000Z',
  182. });
  183. }
  184. if (action === 'app_companion_overview') {
  185. this.lastScheduleParams = params;
  186. const schedules = [
  187. { GeneralID: 901, bxrq: '2026-08-10 00:00:00', sdsd: '19:00', Title: '同步伴学', plxm: '张老师', dszt: 10 },
  188. { GeneralID: 902, bxrq: '2026-08-03 00:00:00', sdsd: '18:00', Title: '历史伴学', plxm: '李老师', dszt: 30 },
  189. { GeneralID: 903, bxrq: '2099-01-01 00:00:00', sdsd: '18:00', Title: '未来计划', plxm: '王老师', dszt: 0 },
  190. ];
  191. const historySchedules = [
  192. { GeneralID: 911, bxrq: '2026-07-14 00:00:00', sdsd: '22:00', Title: '谢晨晨(60分钟练习)', plxm: '张老师', dszt: 30 },
  193. { GeneralID: 912, bxrq: '2026-07-14 00:00:00', sdsd: '17:00', Title: '谢谢(60分钟练习)', plxm: '张老师', dszt: 30 },
  194. { GeneralID: 913, bxrq: '2026-03-09 00:00:00', sdsd: '10:00', Title: '花花(60分钟练习)', plxm: '张老师', dszt: 30 },
  195. { GeneralID: 914, bxrq: '2026-01-18 00:00:00', sdsd: '16:30', Title: '小赵(60分钟练习)', plxm: '张老师', dszt: 30 },
  196. { GeneralID: 915, bxrq: '2026-01-18 00:00:00', sdsd: '16:00', Title: '小黄(60分钟练习)', plxm: '张老师', dszt: 30 },
  197. ];
  198. const rangeStart = String(params['rangeStart'] ?? '');
  199. const rangeEnd = String(params['rangeEnd'] ?? '');
  200. return this.result<T>({
  201. courses: [
  202. { GeneralID: 801, id: 701, kcid: 801, Title: '初中英语|七年级上册@同步词汇' },
  203. { GeneralID: 802, id: 702, Title2: '【初中英语】阅读训练', syjd: '{"811":"811@Unit 1@1"}' },
  204. ],
  205. 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})),
  206. schedules: params['history']
  207. ? historySchedules
  208. : rangeStart && rangeEnd
  209. ? schedules.filter((item) => item.bxrq.slice(0, 10) >= rangeStart && item.bxrq.slice(0, 10) <= rangeEnd)
  210. : schedules,
  211. refreshedAt: '2026-08-26T00:00:00.000Z',
  212. });
  213. }
  214. if (action === 'app_account_overview') {
  215. if (Number(params['uid']) === 300) {
  216. return this.result<T>({
  217. profile: { userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 },
  218. coachPanel: { todaySchedules: 2, completedLessons: 8, reviewIncome: 36, reviewRecords: 3, totalLessons: 11, totalHours: 9, course30: 4, course60: 6, trialLessons: 1 },
  219. refreshedAt: '2026-08-26T00:00:00.000Z',
  220. });
  221. }
  222. return this.result<T>({
  223. profile: { userId: 100, userName: 'student', honeyName: '张雅茜', groupId: 1 },
  224. companionSummary: { completedSessions: 1, completedMinutes: 60 },
  225. antiForgetSummary: { total: 3, completed: 2, progress: 67, todayTotal: 0, todayCompleted: 0, duePending: 1, scheduled: 0, scheduledDays: 3 },
  226. refreshedAt: '2026-08-26T00:00:00.000Z',
  227. });
  228. }
  229. if (action === 'app_review_page') {
  230. this.lastMemoryParams=params;
  231. if (this.reviewPageResponse) return this.reviewPageResponse as Observable<ApiEnvelope<T>>;
  232. const items=[
  233. { 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 },
  234. { 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 },
  235. { 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 },
  236. ...(Number(params['shouru']) === 1
  237. ? [{ 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 }]
  238. : []),
  239. ];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}))});
  240. }
  241. if (action === 'e_get_21list') {
  242. this.lastMemoryParams = params;
  243. return this.result<T>([
  244. { 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 },
  245. { 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 },
  246. { 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 },
  247. ...(Number(params['shouru']) === 1
  248. ? [{ 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 }]
  249. : []),
  250. ]);
  251. }
  252. if (action === 'e_record_detail') {
  253. return of({
  254. retcode: 1,
  255. result: [{ GeneralID: 701, Title: 'apple', detail: [{ GeneralID: 701, Title: 'apple', sy: '苹果', yp: this.historyWordAudio }] }],
  256. addon: [{ if_writing: '0', if_writing_tips: '先回忆单词,再查看释义' }],
  257. } as unknown as ApiEnvelope<T>);
  258. }
  259. return this.result<T>([]);
  260. }
  261. assetUrl(path: unknown): string { return String(path ?? ''); }
  262. private result<T>(result: unknown): Observable<ApiEnvelope<T>> {
  263. return of({ retcode: 1, result: result as T });
  264. }
  265. }
  266. describe('home learning modules', () => {
  267. const user = signal<AppUser | null>({ userId: 100, userName: 'student', honeyName: '张雅茜', groupId: 1 });
  268. const selectedStudent = signal<AppUser | null>(null);
  269. const session = {
  270. user,
  271. token: signal('test-token'),
  272. student: selectedStudent,
  273. isLoggedIn: signal(true),
  274. groupId: signal(1),
  275. setStudent: (student: AppUser | null) => selectedStudent.set(student),
  276. patchUser: () => undefined,
  277. logout: () => undefined,
  278. };
  279. beforeEach(() => {
  280. localStorage.removeItem('xiaoshu.learning.300.100.901.skip_spelling_stage');
  281. user.set({ userId: 100, userName: 'student', honeyName: '张雅茜', groupId: 1 });
  282. session.groupId.set(1);
  283. session.student.set(null);
  284. TestBed.configureTestingModule({
  285. providers: [
  286. provideRouter(routes),
  287. provideHttpClient(),
  288. { provide: ApiService, useClass: LearningModulesApiService },
  289. { provide: SessionService, useValue: session },
  290. ],
  291. });
  292. });
  293. it('silently reveals pending and completed words opened from student learning content', async () => {
  294. const play = spyOn(TestBed.inject(PlatformService), 'play');
  295. const harness = await RouterTestingHarness.create();
  296. await harness.navigateByUrl('/pages/his/his');
  297. (harness.routeNativeElement?.querySelector('.current-learning-list button') as HTMLButtonElement).click();
  298. await harness.fixture.whenStable();
  299. harness.fixture.detectChanges();
  300. (harness.routeNativeElement?.querySelector('.node-list button') as HTMLButtonElement).click();
  301. await harness.fixture.whenStable();
  302. harness.fixture.detectChanges();
  303. expect(TestBed.inject(Router).url).toContain('readonly=1');
  304. const learning = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
  305. for (const [index, meaning] of ['n. 香蕉', 'n. 苹果'].entries()) {
  306. (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[index] as HTMLButtonElement).click();
  307. harness.fixture.detectChanges();
  308. expect(harness.routeNativeElement?.querySelector('.audio')).toBeNull();
  309. expect(harness.routeNativeElement?.textContent).not.toContain('读音');
  310. const word = harness.routeNativeElement?.querySelector('.word-reveal') as HTMLButtonElement;
  311. word.click();
  312. harness.fixture.detectChanges();
  313. expect(word.textContent).toContain(meaning);
  314. expect(word.getAttribute('aria-expanded')).toBe('true');
  315. word.click();
  316. harness.fixture.detectChanges();
  317. expect(word.getAttribute('aria-expanded')).toBe('false');
  318. }
  319. learning.words().forEach((word) => learning.play(word));
  320. expect(play).not.toHaveBeenCalled();
  321. });
  322. it('pages through all words in a history record without requesting a course unit', async () => {
  323. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  324. const originalPost = api.post.bind(api);
  325. const post = spyOn(api, 'post').and.callFake((action, params) => action === 'e_record_detail'
  326. ? of({ retcode: 1, result: Array.from({ length: 31 }, (_, index) => ({
  327. GeneralID: 700 + index,
  328. Title: `history-${index + 1}`,
  329. detail: [{ GeneralID: 700 + index, Title: `history-${index + 1}`, sy: `释义 ${index + 1}` }],
  330. })), addon: [] } as ApiEnvelope<any>)
  331. : originalPost(action, params));
  332. const get = spyOn(api, 'get').and.callThrough();
  333. const harness = await RouterTestingHarness.create();
  334. await harness.navigateByUrl('/pages/his/his_words?id=201');
  335. harness.fixture.detectChanges();
  336. const list = () => harness.routeNativeElement?.querySelectorAll('.readonly-word-list article') ?? [];
  337. expect(list().length).toBe(30);
  338. expect(harness.routeNativeElement?.textContent).toContain('共 31 个单词');
  339. (harness.routeNativeElement?.querySelector('.word-preview-pagination .page-direction:last-of-type') as HTMLButtonElement).click();
  340. await harness.fixture.whenStable();
  341. harness.fixture.detectChanges();
  342. expect(list().length).toBe(1);
  343. expect(list()[0].textContent).toContain('history-31');
  344. expect(harness.routeNativeElement?.textContent).toContain('2 / 2 页');
  345. expect(get.calls.allArgs().some(([action]) => action === 'e_ck_list')).toBeFalse();
  346. expect(post.calls.allArgs().filter(([action]) => action === 'e_record_detail').length).toBe(1);
  347. (harness.routeNativeElement?.querySelector('.word-preview-pagination .page-direction:first-child') as HTMLButtonElement).click();
  348. await harness.fixture.whenStable();
  349. harness.fixture.detectChanges();
  350. expect(list().length).toBe(30);
  351. expect(list()[0].textContent).toContain('history-1');
  352. });
  353. it('plays student history words on click without changing learning records or enabling unit preview audio', async () => {
  354. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  355. api.historyWordAudio = 'apple.mp3';
  356. const post = spyOn(api, 'post').and.callThrough();
  357. const platform = TestBed.inject(PlatformService);
  358. const play = spyOn(platform, 'play');
  359. const stopAudio = spyOn(platform, 'stopAudio');
  360. const harness = await RouterTestingHarness.create();
  361. await harness.navigateByUrl('/pages/his/his');
  362. (harness.routeNativeElement?.querySelector('.learning-record-card .learning-item') as HTMLElement).click();
  363. await harness.fixture.whenStable();
  364. harness.fixture.detectChanges();
  365. expect(TestBed.inject(Router).url).toBe('/pages/his/his_words?id=201');
  366. expect(play).not.toHaveBeenCalled();
  367. post.calls.reset();
  368. const button = harness.routeNativeElement?.querySelector('.history-word-audio') as HTMLButtonElement;
  369. expect(button.disabled).toBeFalse();
  370. expect(button.getAttribute('aria-label')).toBe('播放单词读音:apple');
  371. button.querySelector('strong')!.click();
  372. (button.querySelector('lucide-icon') as HTMLElement).click();
  373. expect(play).toHaveBeenCalledTimes(2);
  374. expect(play.calls.mostRecent().args[0]).toContain('apple.mp3');
  375. expect(post).not.toHaveBeenCalled();
  376. const learning = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
  377. learning.words.update((words) => words.map((word) => ({ ...word, audio: '' })));
  378. harness.fixture.detectChanges();
  379. expect(button.disabled).toBeTrue();
  380. expect(harness.routeNativeElement?.textContent).toContain('暂无音频');
  381. button.click();
  382. expect(play).toHaveBeenCalledTimes(2);
  383. stopAudio.calls.reset();
  384. await harness.navigateByUrl('/pages/home/index');
  385. expect(stopAudio).toHaveBeenCalled();
  386. await harness.navigateByUrl('/pages/stu/stu_con?nid=811&readonly=1');
  387. expect(harness.routeNativeElement?.querySelector('.history-word-audio')).toBeNull();
  388. const preview = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
  389. preview.words().forEach((word) => preview.play(word));
  390. expect(play).toHaveBeenCalledTimes(2);
  391. });
  392. it('lets a student study selected words from their wordbook while keeping course stages teacher-led', async () => {
  393. localStorage.removeItem('xiaoshu.learning.100.100.practice.learn_mode');
  394. localStorage.removeItem('xiaoshu.learning.100.100.practice.choose_newword_learn');
  395. const harness = await RouterTestingHarness.create();
  396. const router = TestBed.inject(Router);
  397. await harness.navigateByUrl('/pages/member/starshop');
  398. (harness.routeNativeElement?.querySelector('.word-select-list > button') as HTMLButtonElement).click();
  399. (harness.routeNativeElement?.querySelector('.selection-header .primary-button') as HTMLButtonElement).click();
  400. await harness.fixture.whenStable();
  401. harness.fixture.detectChanges();
  402. const learning = () => harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
  403. expect(router.url).toContain('/pages/stu/stu_con');
  404. expect(learning().words().map((word) => word.word)).toEqual(['tend']);
  405. learning().skipStudyList();
  406. await harness.fixture.whenStable();
  407. harness.fixture.detectChanges();
  408. expect(router.url).toContain('/pages/stu/stu3');
  409. expect(learning().words().map((word) => word.word)).toEqual(['tend']);
  410. learning().chooseMatch('left', learning().words()[0]);
  411. learning().chooseMatch('right', learning().words()[0]);
  412. learning().continueAfterStage();
  413. await harness.fixture.whenStable();
  414. harness.fixture.detectChanges();
  415. expect(router.url).toContain('/pages/stu/stu4');
  416. expect(learning().words().map((word) => word.word)).toEqual(['tend']);
  417. learning().chooseMeaning(learning().words()[0]);
  418. learning().continueAfterStage();
  419. await harness.fixture.whenStable();
  420. harness.fixture.detectChanges();
  421. expect(router.url).toContain('/pages/stu/stu5');
  422. for (const letter of learning().current()!.word) {
  423. const index = learning().letters().findIndex((choice) => !choice.used && choice.value === letter);
  424. expect(index).toBeGreaterThanOrEqual(0);
  425. learning().spellLetter(index);
  426. }
  427. learning().nextSpellingWord();
  428. learning().continueAfterStage();
  429. await harness.fixture.whenStable();
  430. expect(router.url).toBe('/pages/member/starshop');
  431. expect(localStorage.getItem('xiaoshu.learning.100.100.practice.learn_mode')).toBeNull();
  432. await harness.navigateByUrl('/pages/stu/stu_con?nids=811&pnid=801');
  433. await harness.fixture.whenStable();
  434. expect(router.url).toBe('/pages/home/cart');
  435. });
  436. it('separates unlearned and learned wordbook words while keeping selections across tabs', async () => {
  437. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  438. const get = api.get.bind(api);
  439. spyOn(api, 'get').and.callFake((action, params) => action === 'e_words_list'
  440. ? of({ retcode: 1, result: params?.['learningStatus'] === 'completed'
  441. ? [{ GeneralID: 602, Title: 'prepare@张雅茜', xxcs: 2, detail: [{ GeneralID: 702, Title: 'prepare', sy: '准备' }] }]
  442. : [{ GeneralID: 601, Title: 'tend@张雅茜', xxcs: 0, detail: [{ GeneralID: 701, Title: 'tend', sy: '照顾' }] }],
  443. page: { itemCount: 1, pendingCount: 1, completedCount: 1, pageCount: 1, pageIndex: 1, pageSize: 30 } } as ApiEnvelope<any>)
  444. : get(action, params));
  445. const harness = await RouterTestingHarness.create();
  446. await harness.navigateByUrl('/pages/member/starshop');
  447. harness.fixture.detectChanges();
  448. const tabs = harness.routeNativeElement?.querySelectorAll<HTMLButtonElement>('.word-selection-tabs button') ?? [];
  449. expect(tabs[0].textContent).toContain('未学习');
  450. expect(tabs[0].textContent).toContain('1');
  451. expect(harness.routeNativeElement?.querySelector('.pending-column')?.textContent).toContain('tend');
  452. (harness.routeNativeElement?.querySelector('.pending-column .word-select-list > button') as HTMLButtonElement).click();
  453. tabs[1].click();
  454. harness.fixture.detectChanges();
  455. expect(tabs[1].textContent).toContain('已学习');
  456. expect(harness.routeNativeElement?.querySelector('.completed-column')?.textContent).toContain('prepare');
  457. (harness.routeNativeElement?.querySelector('.completed-column .word-select-list > button') as HTMLButtonElement).click();
  458. harness.fixture.detectChanges();
  459. expect(harness.routeNativeElement?.querySelector('.selection-header strong')?.textContent).toContain('已选 2 个单词');
  460. tabs[0].click();
  461. harness.fixture.detectChanges();
  462. expect(harness.routeNativeElement?.querySelector('.pending-column .word-select-list > button.selected')).not.toBeNull();
  463. });
  464. it('shows 30 wordbook words per page while counting every learning status', async () => {
  465. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  466. const get = api.get.bind(api);
  467. const pending = Array.from({ length: 65 }, (_, index) => ({ GeneralID: 601 + index, xxcs: 0, detail: [{ GeneralID: 701 + index, Title: `pending-${index + 1}` }] }));
  468. const completed = Array.from({ length: 7 }, (_, index) => ({ GeneralID: 801 + index, xxcs: 1, detail: [{ GeneralID: 901 + index, Title: `completed-${index + 1}` }] }));
  469. const requests: Record<string, unknown>[] = [];
  470. spyOn(api, 'get').and.callFake((action, params) => {
  471. if (action !== 'e_words_list') return get(action, params);
  472. requests.push(params ?? {});
  473. const rows = params?.['learningStatus'] === 'completed' ? completed : pending;
  474. const page = Number(params?.['cpage']);
  475. const size = Number(params?.['psize']);
  476. return of({ retcode: 1, result: rows.slice((page - 1) * size, page * size),
  477. page: { itemCount: rows.length, pendingCount: pending.length, completedCount: completed.length,
  478. pageCount: Math.ceil(rows.length / size), pageIndex: page, pageSize: size } } as ApiEnvelope<any>);
  479. });
  480. const harness = await RouterTestingHarness.create();
  481. await harness.navigateByUrl('/pages/member/starshop');
  482. harness.fixture.detectChanges();
  483. const view = () => harness.routeNativeElement!;
  484. const rows = () => view().querySelectorAll('.pending-column .word-select-list > button');
  485. expect(requests[0]).toEqual(jasmine.objectContaining({ learningStatus: 'pending', cpage: 1, psize: 30 }));
  486. expect(rows().length).toBe(30);
  487. expect(view().querySelectorAll('.word-selection-tabs b')[0].textContent).toContain('65');
  488. expect(view().querySelectorAll('.word-selection-tabs b')[1].textContent).toContain('7');
  489. (rows()[0] as HTMLButtonElement).click();
  490. (view().querySelectorAll('.mobile-pagination button')[1] as HTMLButtonElement).click();
  491. harness.fixture.detectChanges();
  492. expect(requests[1]).toEqual(jasmine.objectContaining({ learningStatus: 'pending', cpage: 2, psize: 30 }));
  493. expect(rows().length).toBe(30);
  494. expect(view().querySelector('.mobile-pagination')?.textContent).toContain('第 2 / 3 页');
  495. (view().querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
  496. harness.fixture.detectChanges();
  497. expect(requests[2]).toEqual(jasmine.objectContaining({ learningStatus: 'completed', cpage: 1, psize: 30 }));
  498. expect(view().querySelectorAll('.completed-column .word-select-list > button').length).toBe(7);
  499. expect(view().querySelector('.selection-header strong')?.textContent).toContain('已选 1 个单词');
  500. });
  501. it('keeps wordbook counts and rows correct when the server only returns the old mixed list', async () => {
  502. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  503. const get = api.get.bind(api);
  504. const all = Array.from({ length: 139 }, (_, index) => ({
  505. GeneralID: 601 + index,
  506. xxcs: index < 45 ? 1 : 0,
  507. detail: [{ GeneralID: 701 + index, Title: `word-${index + 1}` }],
  508. }));
  509. const requests: Record<string, unknown>[] = [];
  510. spyOn(api, 'get').and.callFake((action, params) => {
  511. if (action !== 'e_words_list') return get(action, params);
  512. requests.push(params ?? {});
  513. const page = Number(params?.['cpage']);
  514. const size = Number(params?.['psize']);
  515. return of({ retcode: 1, result: all.slice((page - 1) * size, page * size),
  516. page: { itemCount: all.length, pageCount: Math.ceil(all.length / size), pageIndex: page, pageSize: size } } as ApiEnvelope<any>);
  517. });
  518. const harness = await RouterTestingHarness.create();
  519. await harness.navigateByUrl('/pages/member/starshop');
  520. harness.fixture.detectChanges();
  521. const view = () => harness.routeNativeElement!;
  522. expect(requests.length).toBe(2);
  523. expect(requests[0]).toEqual(jasmine.objectContaining({ learningStatus: 'pending', psize: 30 }));
  524. expect(requests[1]).toEqual(jasmine.objectContaining({ cpage: 1, psize: 500 }));
  525. expect(view().querySelectorAll('.word-selection-tabs b')[0].textContent).toContain('94');
  526. expect(view().querySelectorAll('.word-selection-tabs b')[1].textContent).toContain('45');
  527. expect(view().querySelectorAll('.pending-column .word-select-list > button').length).toBe(30);
  528. expect(view().querySelector('.mobile-pagination')?.textContent).toContain('第 1 / 4 页 · 共 94 个单词');
  529. (view().querySelectorAll('.mobile-pagination button')[1] as HTMLButtonElement).click();
  530. harness.fixture.detectChanges();
  531. expect(view().querySelector('.mobile-pagination')?.textContent).toContain('第 2 / 4 页');
  532. (view().querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
  533. harness.fixture.detectChanges();
  534. expect(view().querySelectorAll('.completed-column .word-select-list > button').length).toBe(30);
  535. expect(view().querySelector('.mobile-pagination')?.textContent).toContain('第 1 / 2 页 · 共 45 个单词');
  536. expect(requests.length).toBe(2);
  537. });
  538. it('saves completed wordbook study before showing the word in the learned tab', async () => {
  539. const prefix = 'xiaoshu.learning.100.100.practice.';
  540. const word = { GeneralID: 701, Title: 'tend', sy: '照顾' };
  541. localStorage.setItem(prefix + 'learn_mode', JSON.stringify('newword'));
  542. localStorage.setItem(prefix + 'choose_newword_learn', JSON.stringify([word]));
  543. localStorage.setItem(prefix + '_page_word_orilist', JSON.stringify([[word]]));
  544. localStorage.setItem(prefix + '_page_current_learn', JSON.stringify(1));
  545. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  546. const get = api.get.bind(api);
  547. spyOn(api, 'get').and.callFake((action, params) => action === 'e_words_list'
  548. ? of({ retcode: 1, result: api.lastLearningWordUpdate['save'] === 1 && params?.['learningStatus'] === 'completed'
  549. ? [{ GeneralID: 601, Title: 'tend@张雅茜', xxcs: 1, detail: [word] }]
  550. : api.lastLearningWordUpdate['save'] === 1 ? [] : [{ GeneralID: 601, Title: 'tend@张雅茜', xxcs: 0, detail: [word] }],
  551. page: { itemCount: params?.['learningStatus'] === 'completed' ? 1 : api.lastLearningWordUpdate['save'] === 1 ? 0 : 1,
  552. pendingCount: api.lastLearningWordUpdate['save'] === 1 ? 0 : 1, completedCount: api.lastLearningWordUpdate['save'] === 1 ? 1 : 0,
  553. pageCount: 1, pageIndex: 1, pageSize: 30 } } as ApiEnvelope<any>)
  554. : get(action, params));
  555. const harness = await RouterTestingHarness.create();
  556. const router = TestBed.inject(Router);
  557. await harness.navigateByUrl('/pages/stu/stu5');
  558. const learning = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
  559. learning.skipSpellingStage();
  560. await harness.fixture.whenStable();
  561. harness.fixture.detectChanges();
  562. expect(api.lastLearningWordUpdate).toEqual(jasmine.objectContaining({ uid: 100, wordsId: '701', save: 1 }));
  563. expect(router.url).toBe('/pages/member/starshop');
  564. const tabs = harness.routeNativeElement?.querySelectorAll<HTMLButtonElement>('.word-selection-tabs button') ?? [];
  565. expect(tabs[0].textContent).toContain('0');
  566. tabs[1].click();
  567. harness.fixture.detectChanges();
  568. expect(harness.routeNativeElement?.querySelector('.completed-column')?.textContent).toContain('tend');
  569. }, 15000);
  570. it('skips the entire spelling stage for student wordbook self-study in one click', async () => {
  571. const prefix = 'xiaoshu.learning.100.100.practice.';
  572. const words = [
  573. { GeneralID: 701, Title: 'tend', sy: '照顾' },
  574. { GeneralID: 702, Title: 'work on', sy: '从事' },
  575. { GeneralID: 703, Title: 'prepare', sy: '准备' },
  576. ];
  577. localStorage.setItem(prefix + 'learn_mode', JSON.stringify('newword'));
  578. localStorage.setItem(prefix + 'choose_newword_learn', JSON.stringify(words.slice(0, 2)));
  579. localStorage.setItem(prefix + '_page_word_orilist', JSON.stringify([words.slice(0, 2), words.slice(2)]));
  580. localStorage.setItem(prefix + '_page_current_learn', JSON.stringify(1));
  581. const harness = await RouterTestingHarness.create();
  582. const router = TestBed.inject(Router);
  583. await harness.navigateByUrl('/pages/stu/stu5');
  584. harness.fixture.detectChanges();
  585. const learning = () => harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
  586. expect(learning().words().length).toBe(2);
  587. expect(learning().currentIndex()).toBe(0);
  588. const skip = Array.from(harness.routeNativeElement?.querySelectorAll<HTMLButtonElement>('.skippable-spelling-actions button') ?? [])
  589. .find((button) => button.textContent?.includes('跳到下一关'))!;
  590. skip.click();
  591. await harness.fixture.whenStable();
  592. harness.fixture.detectChanges();
  593. expect(router.url).toContain('/pages/stu/stu_con');
  594. expect(learning().words().map((word) => word.word)).toEqual(['prepare']);
  595. learning().skipStudyList();
  596. await harness.fixture.whenStable();
  597. harness.fixture.detectChanges();
  598. learning().chooseMatch('left', learning().words()[0]);
  599. learning().chooseMatch('right', learning().words()[0]);
  600. learning().continueAfterStage();
  601. await harness.fixture.whenStable();
  602. harness.fixture.detectChanges();
  603. expect(router.url).toContain('/pages/stu/stu4');
  604. learning().chooseMeaning(learning().words()[0]);
  605. learning().continueAfterStage();
  606. await harness.fixture.whenStable();
  607. expect(router.url).toBe('/pages/member/starshop');
  608. expect(localStorage.getItem(prefix + 'learn_mode')).toBeNull();
  609. }, 15000);
  610. it('still pronounces pending and completed words when a coach reveals them', async () => {
  611. user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
  612. session.groupId.set(3);
  613. session.student.set({ userId: 100, userName: 'student', groupId: 1 });
  614. const play = spyOn(TestBed.inject(PlatformService), 'play');
  615. const harness = await RouterTestingHarness.create();
  616. await harness.navigateByUrl('/pages/stu/stu_abc?nids=811,812&pnid=801');
  617. for (const index of [0, 1]) {
  618. (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[index] as HTMLButtonElement).click();
  619. harness.fixture.detectChanges();
  620. expect(harness.routeNativeElement?.querySelector('.audio')).not.toBeNull();
  621. (harness.routeNativeElement?.querySelector('.word-reveal') as HTMLButtonElement).click();
  622. }
  623. expect(play).toHaveBeenCalledTimes(2);
  624. expect(play.calls.argsFor(0)[0]).toContain('banana.mp3');
  625. expect(play.calls.argsFor(1)[0]).toContain('apple.mp3');
  626. });
  627. it('shows named schedule students below the trigger and closes after selection or Escape', async () => {
  628. user.set({userId:300,userName:'coach',honeyName:'张老师',groupId:3});
  629. const harness=await RouterTestingHarness.create();
  630. await harness.navigateByUrl('/pages/home/shop');
  631. const component=harness.fixture.debugElement.query(By.directive(CourseCenterPageComponent)).componentInstance as CourseCenterPageComponent;
  632. component.serverStudents.set([{id:'100',name:'张雅茜',count:15},{id:'101',name:'花花',count:9},{id:'102',name:' ',count:1}]);
  633. spyOn(component,'load').and.stub();
  634. harness.fixture.detectChanges();
  635. await new Promise(resolve => setTimeout(resolve, 0));
  636. const trigger=harness.routeNativeElement!.querySelector<HTMLButtonElement>('.student-filter .xs-dropdown-trigger')!;
  637. expect(trigger.textContent).toContain('全部学员(25)');
  638. trigger.click();harness.fixture.detectChanges();
  639. const menu=document.querySelector<HTMLElement>('.xs-dropdown-menu')!;
  640. expect(menu.textContent).toContain('张雅茜(15)');
  641. expect(menu.textContent).toContain('学员 102(1)');
  642. expect(menu.getBoundingClientRect().top).toBeGreaterThan(trigger.getBoundingClientRect().bottom);
  643. menu.querySelectorAll('button')[2].click();harness.fixture.detectChanges();
  644. await new Promise(resolve => setTimeout(resolve, 0));
  645. expect(component.selectedStudent()).toBe('101');
  646. expect(trigger.textContent).toContain('花花(9)');
  647. expect(document.querySelector('.xs-dropdown-menu')).toBeNull();
  648. trigger.click();harness.fixture.detectChanges();
  649. document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape'}));harness.fixture.detectChanges();
  650. expect(document.querySelector('.xs-dropdown-menu')).toBeNull();
  651. });
  652. it('keeps the anti-forget page visible while a selected date loads locally', async () => {
  653. const harness = await RouterTestingHarness.create();
  654. await harness.navigateByUrl('/pages/stu/stu_calendar');
  655. await harness.fixture.whenStable();
  656. harness.fixture.detectChanges();
  657. const component = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
  658. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  659. component.memoryMonth.set(new Date(2026, 8, 1, 12));
  660. component.memorySelectedDate.set('2026-09-11');
  661. harness.fixture.detectChanges();
  662. const response = new Subject<ApiEnvelope<unknown>>();
  663. api.reviewPageResponse = response;
  664. (harness.routeNativeElement?.querySelector('[aria-label^="2026-09-12"]') as HTMLButtonElement).click();
  665. harness.fixture.detectChanges();
  666. expect(api.lastMemoryParams['date']).toBe('2026-09-12');
  667. expect(harness.routeNativeElement?.querySelector('.memory-overview')).not.toBeNull();
  668. expect(harness.routeNativeElement?.querySelector('.memory-calendar')).not.toBeNull();
  669. expect(harness.routeNativeElement?.querySelector('.memory-list-loading')?.textContent).toContain('正在加载当日复习');
  670. response.next({
  671. retcode: 1,
  672. result: {
  673. items: [{ GeneralID: 120, Title: '张雅茜@任务', kc_title: '9月12日复习', kywsj: '2026-09-12 09:00', fxzt: 0, yhid: 100, xxjlid: 920 }],
  674. page: { itemCount: 1, pageCount: 1 },
  675. summary: { total: 1, completed: 0, due: 0, today: 0, income: 0 },
  676. dates: [{ date: '2026-09-12', total: 1, completed: 0 }],
  677. },
  678. });
  679. response.complete();
  680. api.reviewPageResponse = null;
  681. await harness.fixture.whenStable();
  682. harness.fixture.detectChanges();
  683. expect(harness.routeNativeElement?.querySelector('.memory-list-loading')).toBeNull();
  684. expect(harness.routeNativeElement?.querySelector('.memory-date-tasks')?.textContent).toContain('9月12日复习');
  685. });
  686. it('shows a retryable error instead of an empty review when the detail has no words', async () => {
  687. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  688. const post = api.post.bind(api);
  689. spyOn(api, 'post').and.callFake((action, params) => action === 'e_record_detail'
  690. ? of({ retcode: 1, result: [], addon: [] } as ApiEnvelope<any>) : post(action, params));
  691. const harness = await RouterTestingHarness.create();
  692. await harness.navigateByUrl('/pages/stu/record?id=900&gid=102&kcid=801');
  693. harness.fixture.detectChanges();
  694. expect(harness.routeNativeElement?.querySelector('.empty-state.error')?.textContent).toContain('没有读取到单词');
  695. expect(harness.routeNativeElement?.querySelector('.empty-state.error button')?.textContent).toContain('重新加载');
  696. expect(harness.routeNativeElement?.querySelector('.record-list')).toBeNull();
  697. });
  698. it('renders source-shaped data for all four home entries', async () => {
  699. const harness = await RouterTestingHarness.create();
  700. await harness.navigateByUrl('/pages/home/index');
  701. expect(harness.fixture.nativeElement.querySelector('.tabbar')).not.toBeNull();
  702. expect(harness.routeNativeElement?.querySelectorAll('.home-header .header-action').length).toBe(1);
  703. expect(harness.routeNativeElement?.querySelector('.home-header .header-action')?.getAttribute('href')).toBe('/pages/home/search');
  704. expect(harness.routeNativeElement?.querySelector('.home-header [aria-label="用户中心"]')).toBeNull();
  705. expect(harness.routeNativeElement?.querySelector('.memory-hero')).not.toBeNull();
  706. expect(harness.routeNativeElement?.querySelectorAll('.memory-dot').length).toBe(21);
  707. expect(harness.routeNativeElement?.querySelector('.today-panel')?.textContent).toContain('学习新词');
  708. expect(harness.routeNativeElement?.querySelectorAll('.student-stats > div').length).toBe(5);
  709. expect(harness.routeNativeElement?.querySelectorAll('.quick-grid > a').length).toBe(4);
  710. expect(harness.routeNativeElement?.querySelector('.continue-button')?.getAttribute('href')).toBe('/pages/stu/stu_calendar');
  711. expect(harness.routeNativeElement?.querySelector('.task-link')?.getAttribute('href')).toBe('/pages/home/cart');
  712. await harness.navigateByUrl('/pages/stu/index');
  713. await harness.fixture.whenStable();
  714. harness.fixture.detectChanges();
  715. expect(TestBed.inject(Router).url).toBe('/pages/home/cart');
  716. expect(harness.routeNativeElement?.textContent).not.toContain('选择词库教材');
  717. await harness.navigateByUrl('/pages/stu/stu_list?nid=801&bindingId=701');
  718. await harness.fixture.whenStable();
  719. harness.fixture.detectChanges();
  720. expect(harness.routeNativeElement?.querySelectorAll('.node-list button').length).toBe(2);
  721. expect(harness.routeNativeElement?.querySelector('.node-list button')?.textContent).toContain('已学 1/20 · 进度 5%');
  722. expect(harness.routeNativeElement?.querySelectorAll('.unit-progress-ring').length).toBe(2);
  723. expect(harness.routeNativeElement?.querySelector('.unit-progress-ring')?.textContent).toContain('5%');
  724. expect(harness.routeNativeElement?.querySelector('.unit-selection-summary')).toBeNull();
  725. expect(harness.routeNativeElement?.querySelector('.unit-actions')).toBeNull();
  726. const firstUnit = harness.routeNativeElement?.querySelector('.node-list button') as HTMLButtonElement;
  727. firstUnit.click();
  728. await harness.fixture.whenStable();
  729. harness.fixture.detectChanges();
  730. const readonlyRouter = TestBed.inject(Router);
  731. expect(readonlyRouter.url).toBe('/pages/stu/stu_abc?nids=811&pnid=801&readonly=1');
  732. expect(harness.routeNativeElement?.querySelector('.selection-header')?.textContent).toContain('词库学习进度');
  733. expect(harness.routeNativeElement?.querySelector('.selection-header .primary-button')).toBeNull();
  734. expect(harness.routeNativeElement?.querySelector('.pending-column')?.textContent).toContain('banana');
  735. expect(harness.routeNativeElement?.querySelector('.word-select-check')).toBeNull();
  736. expect(harness.routeNativeElement?.querySelector('.word-assessment')).toBeNull();
  737. (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
  738. harness.fixture.detectChanges();
  739. expect(harness.routeNativeElement?.querySelector('.completed-column')?.textContent).toContain('apple');
  740. expect(harness.routeNativeElement?.querySelector('.pending-column')).toBeNull();
  741. await harness.navigateByUrl('/pages/home/index');
  742. await harness.navigateByUrl('/pages/stu/stu_con?nid=813&pnid=801&readonly=1');
  743. await harness.fixture.whenStable();
  744. harness.fixture.detectChanges();
  745. const wordApi = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  746. expect(wordApi.lastWordParams['psize']).toBe(30);
  747. expect(harness.routeNativeElement?.querySelectorAll('.readonly-word-list article').length).toBe(30);
  748. expect(harness.routeNativeElement?.querySelector('.readonly-word-header')?.textContent).toContain('第 1–30 条 · 共 65 个单词');
  749. expect(harness.routeNativeElement?.querySelector('.word-preview-pagination')?.textContent).toContain('1 / 3 页');
  750. (harness.routeNativeElement?.querySelector('[aria-label="第 2 页"]') as HTMLButtonElement).click();
  751. it('syncs a missing learning record and reopens its review words', async () => {
  752. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  753. const post = api.post.bind(api);
  754. let detailReads = 0;
  755. spyOn(api, 'post').and.callFake((action, params) => {
  756. if (action === 'e_record_detail') {
  757. detailReads += 1;
  758. return of({ retcode: 1, result: detailReads === 1 ? [] : [{ GeneralID: 701, Title: 'apple', sy: '苹果' }], addon: [] } as ApiEnvelope<any>);
  759. }
  760. if (action === 'app_learning_overview') return of({ retcode: 1, result: {} } as ApiEnvelope<any>);
  761. return post(action, params);
  762. });
  763. const harness = await RouterTestingHarness.create();
  764. await harness.navigateByUrl('/pages/stu/record?id=864038&gid=864047&kcid=398');
  765. harness.fixture.detectChanges();
  766. expect(detailReads).toBe(2);
  767. expect(harness.routeNativeElement?.querySelector('.record-word')?.textContent).toContain('apple');
  768. expect(harness.routeNativeElement?.querySelector('.empty-state.error')).toBeNull();
  769. });
  770. it('reads the original word list when the detail stays empty after syncing', async () => {
  771. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  772. const post = api.post.bind(api);
  773. const get = api.get.bind(api);
  774. spyOn(api, 'post').and.callFake((action, params) => action === 'e_record_detail'
  775. ? of({ retcode: 1, result: [], addon: [] } as ApiEnvelope<any>) : action === 'app_learning_overview'
  776. ? of({ retcode: 1, result: {} } as ApiEnvelope<any>) : post(action, params));
  777. spyOn(api, 'get').and.callFake((action, params) => action === 'content_get'
  778. ? of({ retcode: 1, result: [{ GeneralID: 864038, xxqs: JSON.stringify([{ GeneralID: 701, Title: 'apple' }]) }] } as ApiEnvelope<any>)
  779. : get(action, params));
  780. const harness = await RouterTestingHarness.create();
  781. await harness.navigateByUrl('/pages/stu/record?id=864038&gid=864047&kcid=398');
  782. harness.fixture.detectChanges();
  783. expect(harness.routeNativeElement?.querySelector('.record-word')?.textContent).toContain('apple');
  784. expect(harness.routeNativeElement?.querySelector('.empty-state.error')).toBeNull();
  785. });
  786. it('identifies a legacy writing review with no original words and keeps it incomplete', async () => {
  787. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  788. const post = api.post.bind(api);
  789. const get = api.get.bind(api);
  790. spyOn(api, 'post').and.callFake((action, params) => action === 'e_record_detail'
  791. ? of({ retcode: 1, result: [], addon: [] } as ApiEnvelope<any>) : action === 'app_learning_overview'
  792. ? of({ retcode: 1, result: {} } as ApiEnvelope<any>) : post(action, params));
  793. spyOn(api, 'get').and.callFake((action, params) => action === 'content_get'
  794. ? of({ retcode: 1, result: [{ GeneralID: 864038, xxqs: '', learned: 0, con: '完成了写作学习' }] } as ApiEnvelope<any>)
  795. : get(action, params));
  796. const harness = await RouterTestingHarness.create();
  797. await harness.navigateByUrl('/pages/stu/record?id=864038&gid=864047&kcid=398');
  798. harness.fixture.detectChanges();
  799. expect(harness.routeNativeElement?.textContent).toContain('写作学习记录,原本没有单词');
  800. expect(harness.routeNativeElement?.querySelector('.record-list')).toBeNull();
  801. expect(harness.routeNativeElement?.querySelector('a[href="/pages/stu/stu_calendar"]')).not.toBeNull();
  802. expect(harness.routeNativeElement?.textContent).not.toContain('结束本次复习');
  803. });
  804. await harness.fixture.whenStable();
  805. harness.fixture.detectChanges();
  806. expect(wordApi.lastWordParams['cpage']).toBe(2);
  807. expect(harness.routeNativeElement?.querySelectorAll('.readonly-word-list article').length).toBe(30);
  808. expect(harness.routeNativeElement?.querySelector('.readonly-word-header')?.textContent).toContain('第 31–60 条 · 共 65 个单词');
  809. expect(harness.routeNativeElement?.querySelector('.readonly-word-list')?.textContent).toContain('word-31');
  810. expect(harness.routeNativeElement?.querySelector('.word-preview-pagination')?.textContent).toContain('2 / 3 页');
  811. await harness.navigateByUrl('/pages/stu/stu_list?nid=999&bindingId=799');
  812. await harness.fixture.whenStable();
  813. harness.fixture.detectChanges();
  814. expect(TestBed.inject(Router).url).toBe('/pages/stu/stu_abc?nids=999&readonly=1');
  815. expect(harness.routeNativeElement?.querySelector('.selection-header')?.textContent).toContain('词库学习进度');
  816. await harness.navigateByUrl('/pages/stu/stu_calendar');
  817. const calendarComponent=harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
  818. calendarComponent.memoryMonth.set(new Date(2026,7,1));
  819. calendarComponent.memorySelectedDate.set('2026-08-02');
  820. (calendarComponent as any).loadCalendar();
  821. harness.fixture.detectChanges();
  822. expect(harness.fixture.nativeElement.querySelector('.tabbar')).toBeNull();
  823. expect(harness.routeNativeElement?.querySelector('.memory-student-filter')).toBeNull();
  824. expect(harness.routeNativeElement?.querySelectorAll('.memory-date').length).toBe(42);
  825. expect(harness.routeNativeElement?.querySelectorAll('.memory-date.completed, .memory-date.due').length).toBe(2);
  826. expect(harness.routeNativeElement?.querySelector('.memory-overview')?.textContent).toContain('2 / 3');
  827. expect(harness.routeNativeElement?.querySelector('.memory-date-tasks')?.textContent).toContain('高中英语复习');
  828. expect(harness.routeNativeElement?.querySelector('.review-sequence')?.textContent).toContain('第 3 次复习');
  829. expect(harness.routeNativeElement?.querySelector('.first-learned-at')?.textContent).toContain('首次学习:2026-07-20 10:20');
  830. for (let index = 0; index < 3; index += 1) {
  831. (harness.routeNativeElement?.querySelector('[aria-label="上个月"]') as HTMLButtonElement).click();
  832. harness.fixture.detectChanges();
  833. }
  834. expect(harness.routeNativeElement?.querySelector('[aria-label^="2026-05-15"]')?.classList).toContain('completed');
  835. expect(harness.routeNativeElement?.textContent).toContain('张雅茜');
  836. expect(harness.routeNativeElement?.querySelector('.memory-overview header b')?.textContent).toContain('67%');
  837. expect(harness.routeNativeElement?.querySelector('.memory-stats')?.textContent).toContain('2 / 3');
  838. await harness.navigateByUrl('/pages/home/ucenter');
  839. expect(harness.fixture.nativeElement.querySelector('.tabbar')).not.toBeNull();
  840. expect(harness.routeNativeElement?.querySelector('.new-word-entry')?.getAttribute('href')).toBe('/pages/home/cart');
  841. expect(harness.routeNativeElement?.querySelector('.course-memory')?.textContent).toContain('21 天抗遗忘计划');
  842. expect(harness.routeNativeElement?.querySelector('.memory-overview em')?.textContent).toContain('67%');
  843. expect(harness.routeNativeElement?.querySelector('.memory-overview')?.textContent).toContain('覆盖 3 个复习日');
  844. expect(harness.routeNativeElement?.querySelector('.memory-counts')?.textContent).toContain('3总任务');
  845. expect(harness.routeNativeElement?.querySelector('.memory-counts')?.textContent).toContain('2已完成');
  846. expect(harness.routeNativeElement?.querySelector('.memory-counts')?.textContent).toContain('1到期待复习');
  847. expect(harness.routeNativeElement?.querySelector('.memory-counts')?.textContent).toContain('0未来待复习');
  848. await harness.navigateByUrl('/pages/stu/record?id=827482&gid=828268&kcid=203');
  849. const recordWord = harness.routeNativeElement?.querySelector('.record-word') as HTMLButtonElement;
  850. expect(recordWord.querySelector('.record-copy strong')?.textContent).toBe('apple');
  851. expect(harness.routeNativeElement?.textContent).not.toContain('苹果');
  852. recordWord.click();
  853. await harness.fixture.whenStable();
  854. harness.fixture.detectChanges();
  855. expect(recordWord.querySelector('.record-copy strong')?.textContent).toBe('苹果');
  856. expect(harness.routeNativeElement?.querySelector('.record-overview')?.textContent).toContain('1 / 1');
  857. await harness.navigateByUrl('/pages/member/starshop');
  858. expect(harness.fixture.nativeElement.querySelector('.tabbar')).toBeNull();
  859. expect(harness.routeNativeElement?.querySelector('.word-select-list strong')?.textContent).toBe('tend');
  860. expect(harness.routeNativeElement?.querySelector('.word-select-list')?.textContent).not.toContain('@张雅茜');
  861. (harness.routeNativeElement?.querySelector('.word-select-list > button') as HTMLButtonElement).click();
  862. (harness.routeNativeElement?.querySelector('.selection-header .primary-button') as HTMLButtonElement).click();
  863. await harness.fixture.whenStable();
  864. const selectedNewWords = JSON.parse(sessionStorage.getItem('xiaoshu.learning.100.100.practice.selectedWords') ?? '[]') as Array<{ word?: string }>;
  865. expect(selectedNewWords[0]?.word).toBe('tend');
  866. expect(sessionStorage.getItem('xiaoshu.learning.100.100.practice.selectedWords')).not.toContain('@张雅茜');
  867. await harness.navigateByUrl('/pages/his/well?nid=3');
  868. expect(harness.fixture.nativeElement.querySelector('.tabbar')).toBeNull();
  869. expect(harness.routeNativeElement?.textContent).toContain('阅读理解专项');
  870. await harness.navigateByUrl('/pages/home/cart');
  871. expect(harness.fixture.nativeElement.querySelector('.tabbar')).not.toBeNull();
  872. expect(harness.fixture.nativeElement.querySelector('.app-header .icon-button')).toBeNull();
  873. expect(harness.fixture.nativeElement.querySelector('.app-header h1')?.textContent).toContain('伴学中心');
  874. expect(harness.routeNativeElement?.querySelector('.library-entry')).toBeNull();
  875. expect(harness.routeNativeElement?.textContent).toContain('我的学习内容');
  876. expect(harness.routeNativeElement?.querySelectorAll('.course-list li').length).toBe(2);
  877. expect(harness.routeNativeElement?.querySelectorAll('.course-list button').length).toBe(2);
  878. expect((harness.routeNativeElement?.querySelector('.course-list button') as HTMLButtonElement).disabled).toBeFalse();
  879. expect((harness.routeNativeElement?.querySelectorAll('.course-list button')[1] as HTMLButtonElement).disabled).toBeFalse();
  880. expect(harness.routeNativeElement?.textContent).toContain('【七年级上册】同步词汇');
  881. expect(harness.routeNativeElement?.textContent).toContain('点击查看老师分配的单词列表');
  882. (harness.routeNativeElement?.querySelector('.course-list button') as HTMLButtonElement).click();
  883. await harness.fixture.whenStable();
  884. harness.fixture.detectChanges();
  885. expect(TestBed.inject(Router).url).toBe('/pages/stu/stu_list?nid=801&bindingId=701');
  886. expect(harness.routeNativeElement?.querySelectorAll('.node-list button').length).toBe(2);
  887. expect((TestBed.inject(ApiService) as unknown as LearningModulesApiService).lastNodeParams['bindingId']).toBe('701');
  888. await harness.navigateByUrl('/pages/home/cart');
  889. expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(0);
  890. expect(harness.routeNativeElement?.querySelectorAll('.calendar-day').length).toBe(42);
  891. const allDates = Array.from(harness.routeNativeElement?.querySelectorAll('.range-tabs button') ?? [])
  892. .find((button) => button.textContent?.trim() === '全部') as HTMLButtonElement;
  893. allDates.click();
  894. harness.fixture.detectChanges();
  895. expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(3);
  896. expect(harness.routeNativeElement?.textContent).toContain('同步伴学');
  897. expect(harness.routeNativeElement?.textContent).toContain('历史伴学');
  898. const plannedIncompleteFilter = Array.from(harness.routeNativeElement?.querySelectorAll('.schedule-filters button') ?? [])
  899. .find((button) => button.textContent?.includes('计划未完成')) as HTMLButtonElement;
  900. expect(plannedIncompleteFilter.textContent).toContain('1');
  901. plannedIncompleteFilter.click();
  902. harness.fixture.detectChanges();
  903. expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(1);
  904. expect(harness.routeNativeElement?.textContent).toContain('计划未完成');
  905. expect(harness.routeNativeElement?.textContent).not.toContain('未来计划');
  906. (harness.routeNativeElement?.querySelector('.schedule-filters button') as HTMLButtonElement).click();
  907. harness.fixture.detectChanges();
  908. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  909. expect(api.lastScheduleParams['mode']).toBe('student');
  910. expect(api.lastScheduleParams['pageSize']).toBe(20);
  911. const finishedFilter = Array.from(harness.routeNativeElement?.querySelectorAll('.schedule-filters button') ?? [])
  912. .find((button) => button.textContent?.includes('已结束')) as HTMLButtonElement;
  913. finishedFilter.click();
  914. harness.fixture.detectChanges();
  915. expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(1);
  916. expect(harness.routeNativeElement?.textContent).toContain('历史伴学');
  917. const allFilter = harness.routeNativeElement?.querySelector('.schedule-filters button') as HTMLButtonElement;
  918. allFilter.click();
  919. harness.fixture.detectChanges();
  920. const upcomingSchedule = Array.from(harness.routeNativeElement?.querySelectorAll('.schedule-list article') ?? [])
  921. .find((article) => article.textContent?.includes('同步伴学')) as HTMLElement;
  922. upcomingSchedule.click();
  923. await harness.fixture.whenStable();
  924. expect(TestBed.inject(Router).url).toBe('/pages/shop/stu_get_order?id=901');
  925. await harness.navigateByUrl('/pages/his/his');
  926. expect(harness.fixture.nativeElement.querySelector('.tabbar')).toBeNull();
  927. const aiReadingFeature = harness.routeNativeElement?.querySelector('.ai-reading-feature') as HTMLElement;
  928. expect(aiReadingFeature.textContent).toContain('AI 阅读巩固');
  929. expect(aiReadingFeature.textContent).toContain('生成阅读');
  930. expect(aiReadingFeature.textContent).toContain('AI 批改');
  931. expect(aiReadingFeature.querySelector('img')?.getAttribute('src')).toBe('/static/images/ai-reading-journey.png');
  932. const aiReadingCta = aiReadingFeature.querySelector('.ai-reading-cta') as HTMLButtonElement;
  933. expect(aiReadingCta.disabled).toBeFalse();
  934. expect(aiReadingFeature.textContent).toContain('最近学过的 1 个单词');
  935. expect(aiReadingCta.textContent).toContain('立即生成阅读');
  936. aiReadingCta.click();
  937. await harness.fixture.whenStable();
  938. const aiReadingRouter = TestBed.inject(Router);
  939. expect(aiReadingRouter.url).toContain('/words/go-reading');
  940. expect(aiReadingRouter.parseUrl(aiReadingRouter.url).queryParams['returnUrl']).toBe('/pages/his/his');
  941. expect(aiReadingRouter.parseUrl(aiReadingRouter.url).queryParams['GeneralID']).toBe('201');
  942. await harness.navigateByUrl('/pages/his/his');
  943. const currentLearningCard = harness.routeNativeElement?.querySelector('.current-learning-card') as HTMLElement;
  944. const learningRecordCard = harness.routeNativeElement?.querySelector('.learning-record-card') as HTMLElement;
  945. expect(currentLearningCard.textContent).toContain('我的学习内容');
  946. const records = harness.fixture.debugElement.query(By.directive(StudyRecordsPageComponent)).componentInstance as StudyRecordsPageComponent;
  947. expect(records.courseTitle({ Title: '单词课|高中@人教版2022年录', Title2: '人教版2022年录' })).toBe('【高中】人教版2022年录');
  948. expect(records.courseTitle({ Title: '单词课|初中@人教版2024年9月录', Title2: '人教版2024年9月录' })).toBe('【初中】人教版2024年9月录');
  949. expect(currentLearningCard.querySelectorAll('.current-learning-list button').length).toBe(2);
  950. expect((currentLearningCard.querySelectorAll('.current-learning-list button')[1] as HTMLButtonElement).disabled).toBeFalse();
  951. expect(currentLearningCard.compareDocumentPosition(learningRecordCard) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
  952. (currentLearningCard.querySelector('.current-learning-list button') as HTMLButtonElement).click();
  953. await harness.fixture.whenStable();
  954. harness.fixture.detectChanges();
  955. expect(TestBed.inject(Router).url).toBe('/pages/stu/stu_list?nid=801&bindingId=701');
  956. expect(harness.routeNativeElement?.querySelectorAll('.node-list button').length).toBe(2);
  957. await harness.navigateByUrl('/pages/his/his');
  958. expect(harness.routeNativeElement?.textContent).toContain('初中英语 / Unit 1');
  959. expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(2);
  960. (harness.routeNativeElement?.querySelector('.learning-record-card .learning-item') as HTMLElement).click();
  961. await harness.fixture.whenStable();
  962. harness.fixture.detectChanges();
  963. expect(TestBed.inject(Router).url).toBe('/pages/his/his_words?id=201');
  964. expect(harness.routeNativeElement?.querySelector('.readonly-word-header')?.textContent).toContain('学生只读');
  965. expect(harness.routeNativeElement?.querySelector('.readonly-word-card')).not.toBeNull();
  966. expect(harness.routeNativeElement?.querySelector('.selection-header')).toBeNull();
  967. await harness.navigateByUrl('/pages/his/his');
  968. (harness.routeNativeElement?.querySelector('.date-trigger') as HTMLButtonElement).click();
  969. harness.fixture.detectChanges();
  970. const currentMonth = new Date();
  971. const monthDifference = (2026 - currentMonth.getFullYear()) * 12 + (7 - currentMonth.getMonth());
  972. const shiftLabel = monthDifference < 0 ? '上个月' : '下个月';
  973. for (let index = 0; index < Math.abs(monthDifference); index += 1) {
  974. (harness.routeNativeElement?.querySelector(`[aria-label="${shiftLabel}"]`) as HTMLButtonElement).click();
  975. harness.fixture.detectChanges();
  976. }
  977. expect(harness.routeNativeElement?.querySelector('.date-picker-popover header strong')?.textContent).toContain('2026年8月');
  978. expect(harness.routeNativeElement?.querySelectorAll('.date-picker-day').length).toBe(42);
  979. expect(harness.routeNativeElement?.querySelectorAll('.date-picker-day.has-record').length).toBe(2);
  980. expect(harness.routeNativeElement?.querySelector('.date-picker-day.has-record')?.getAttribute('aria-label')).toContain('2026-08-01');
  981. (harness.routeNativeElement?.querySelector('[aria-label^="2026-08-01"]') as HTMLButtonElement).click();
  982. harness.fixture.detectChanges();
  983. expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(1);
  984. expect(harness.routeNativeElement?.querySelector('.learning-record-card')?.textContent).not.toContain('高中英语 / Unit 2');
  985. (harness.routeNativeElement?.querySelector('.record-actions button') as HTMLButtonElement).click();
  986. harness.fixture.detectChanges();
  987. expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(2);
  988. const completedReading = harness.routeNativeElement?.querySelector('.reading-button.completed') as HTMLButtonElement;
  989. expect(completedReading.textContent).toContain('已完成阅读');
  990. expect(completedReading.getAttribute('aria-label')).toBe('查看已完成的历史阅读理解');
  991. completedReading.click();
  992. await harness.fixture.whenStable();
  993. const router = TestBed.inject(Router);
  994. expect(router.url).toContain('/words/reading');
  995. expect(router.parseUrl(router.url).queryParams['sid']).toBe('readingarticle201');
  996. expect(router.parseUrl(router.url).queryParams['returnUrl']).toBe('/pages/his/his');
  997. await harness.navigateByUrl('/pages/his/his');
  998. const newReading = harness.routeNativeElement?.querySelector('.reading-button:not(.completed)') as HTMLButtonElement;
  999. expect(newReading.textContent).toContain('阅读');
  1000. newReading.click();
  1001. await harness.fixture.whenStable();
  1002. expect(router.url).toContain('/words/go-reading');
  1003. expect(router.parseUrl(router.url).queryParams['returnUrl']).toBe('/pages/his/his');
  1004. expect(harness.routeNativeElement?.querySelector('.back-button')).not.toBeNull();
  1005. (harness.routeNativeElement?.querySelector('.back-button') as HTMLButtonElement).click();
  1006. await harness.fixture.whenStable();
  1007. expect(router.url).toBe('/pages/his/his');
  1008. (harness.fixture.nativeElement.querySelector('.app-header .icon-button') as HTMLButtonElement).click();
  1009. await harness.fixture.whenStable();
  1010. expect(router.url).toBe('/pages/home/index');
  1011. await harness.navigateByUrl('/pages/his/exam');
  1012. expect(harness.routeNativeElement?.textContent).toContain('第一组问题');
  1013. expect(harness.routeNativeElement?.textContent).toContain('相当于初二水平');
  1014. expect(harness.routeNativeElement?.textContent).toContain('共 1 题');
  1015. const startButton = harness.routeNativeElement?.querySelector('.exam-intro > button') as HTMLButtonElement;
  1016. startButton.click();
  1017. harness.fixture.detectChanges();
  1018. expect(harness.routeNativeElement?.querySelector('.question-word')?.textContent).toContain('apple');
  1019. expect(harness.routeNativeElement?.querySelector('.question-card > h2')).toBeNull();
  1020. expect(harness.routeNativeElement?.querySelectorAll('.exam-options button').length).toBe(1);
  1021. (harness.routeNativeElement?.querySelector('.exam-options button') as HTMLButtonElement).click();
  1022. harness.fixture.detectChanges();
  1023. });
  1024. it('matches coach review tasks to student names by ID when task name fields are blank', async () => {
  1025. user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
  1026. session.groupId.set(3);
  1027. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  1028. const response = new Subject<ApiEnvelope<unknown>>();
  1029. api.reviewPageResponse = response;
  1030. const harness = await RouterTestingHarness.create();
  1031. await harness.navigateByUrl('/pages/home/index');
  1032. response.next({ retcode: 1, result: {
  1033. items: [
  1034. { GeneralID: 102, yhid: 100, honeyName: '', Title: '旧姓名@复习', kywsj: '2025-07-19', fxzt: 0, xxjlid: 900 },
  1035. { GeneralID: 103, yhid: '101', honeyName: ' ', HoneyName: '', kywsj: '2025-07-15', fxzt: 0, xxjlid: 901 },
  1036. ],
  1037. students: [{ id: '101', name: '李同学', taskCount: 1 }, { id: '100', name: '张雅茜', taskCount: 1 }],
  1038. page: { itemCount: 2, pageCount: 1 },
  1039. summary: { total: 2, completed: 0, due: 2, today: 0, income: 0 }, dates: [],
  1040. } });
  1041. response.complete();
  1042. api.reviewPageResponse = null;
  1043. harness.fixture.detectChanges();
  1044. const names = Array.from(harness.routeNativeElement?.querySelectorAll('.coach-memory-copy strong') ?? []).map((node) => node.textContent);
  1045. expect(names).toEqual(['张雅茜', '李同学']);
  1046. const home = harness.fixture.debugElement.query(By.directive(HomePageComponent)).componentInstance as HomePageComponent;
  1047. expect(home.coachReviewStudent({ yhid: 102, honeyName: '', HoneyName: ' ', Title: '王同学@复习' })).toBe('王同学');
  1048. expect(home.coachReviewStudent({ yhid: 103, honeyName: '', Title: ' ' })).toBe('学员 103');
  1049. expect(home.coachReviewStudent({ honeyName: '' })).toBe('未关联学员');
  1050. (harness.routeNativeElement?.querySelectorAll('.coach-memory-list article > button')[1] as HTMLButtonElement).click();
  1051. await harness.fixture.whenStable();
  1052. expect(session.student()).toEqual(jasmine.objectContaining({ userId: 101, honeyName: '李同学' }));
  1053. expect(TestBed.inject(Router).url).toContain('/pages/stu/record?id=901&gid=103');
  1054. });
  1055. it('keeps the complete point-reading flow available to coaches', async () => {
  1056. user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
  1057. session.groupId.set(3);
  1058. const harness = await RouterTestingHarness.create();
  1059. await harness.navigateByUrl('/pages/home/index');
  1060. await harness.fixture.whenStable();
  1061. harness.fixture.detectChanges();
  1062. const coachLinks = Array.from(harness.routeNativeElement?.querySelectorAll('.coach-quick-grid > a') ?? []).map((link) => link.getAttribute('href'));
  1063. expect(coachLinks).toEqual(['/pages/home/shop', '/pages/member/ke_xiao', '/pages/his/his', '/pages/member/ke_fuxi']);
  1064. expect(harness.routeNativeElement?.querySelector('.home-header .header-action')).toBeNull();
  1065. const coachAccountTab = (Array.from(harness.fixture.nativeElement.querySelectorAll('.tabbar a')) as HTMLAnchorElement[])
  1066. .find((link) => link.getAttribute('href') === '/pages/home/ucenter');
  1067. expect(coachAccountTab?.textContent).toContain('我的');
  1068. expect(harness.routeNativeElement?.querySelector('.content-section')).toBeNull();
  1069. const memoryApi = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  1070. expect(memoryApi.lastMemoryParams).toEqual(jasmine.objectContaining({ uid: 300, pageSize:20 }));
  1071. expect(harness.routeNativeElement?.querySelector('.coach-memory-board')?.textContent).toContain('抗遗忘带练');
  1072. expect(harness.routeNativeElement?.querySelector('.coach-memory-board')?.textContent).toContain('张雅茜');
  1073. expect(harness.routeNativeElement?.querySelector('.coach-memory-list article.ready > button')?.textContent).toContain('带学生复习');
  1074. expect(harness.routeNativeElement?.querySelector('.coach-memory-footer')?.getAttribute('href')).toBe('/pages/stu/stu_calendar');
  1075. (harness.routeNativeElement?.querySelector('.coach-memory-list article.ready > button') as HTMLButtonElement).click();
  1076. await harness.fixture.whenStable();
  1077. expect(TestBed.inject(Router).url).toContain('/pages/stu/record?id=900&gid=102');
  1078. expect(session.student()?.userId).toBe(100);
  1079. await harness.navigateByUrl('/pages/stu/stu_calendar');
  1080. const calendarComponent=harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
  1081. calendarComponent.memoryMonth.set(new Date(2026,7,1));
  1082. calendarComponent.memorySelectedDate.set('2026-08-02');
  1083. (calendarComponent as any).loadCalendar();
  1084. harness.fixture.detectChanges();
  1085. await harness.fixture.whenStable();
  1086. harness.fixture.detectChanges();
  1087. expect(memoryApi.lastMemoryParams).toEqual(jasmine.objectContaining({ uid: 300, pageSize:20 }));
  1088. expect(harness.routeNativeElement?.querySelector('.memory-copy')?.textContent).toContain('关联学员抗遗忘任务');
  1089. expect(harness.routeNativeElement?.querySelector('.memory-date-tasks')?.textContent).toContain('张雅茜');
  1090. expect(harness.routeNativeElement?.querySelector('.memory-date-tasks button')?.textContent).toContain('带学生复习');
  1091. const memoryStudentFilter = harness.routeNativeElement?.querySelector('[aria-label="筛选抗遗忘学员"] select') as HTMLSelectElement;
  1092. expect(memoryStudentFilter.options.length).toBe(2);
  1093. memoryStudentFilter.value = '100';
  1094. memoryStudentFilter.dispatchEvent(new Event('change'));
  1095. harness.fixture.detectChanges();
  1096. expect(harness.routeNativeElement?.querySelector('.memory-date-tasks')?.textContent).toContain('张雅茜');
  1097. expect(harness.routeNativeElement?.querySelector('.memory-date-tasks')?.textContent).not.toContain('李同学');
  1098. expect(harness.routeNativeElement?.querySelector('.memory-stats')?.textContent).toContain('2 / 3');
  1099. await harness.navigateByUrl('/pages/member/ke_xiao');
  1100. await harness.fixture.whenStable();
  1101. harness.fixture.detectChanges();
  1102. const monthFilter = harness.routeNativeElement?.querySelector('[aria-label="按月份筛选销课记录"]') as HTMLSelectElement;
  1103. const monthPicker = harness.routeNativeElement?.querySelector('.month-control .xs-dropdown-trigger') as HTMLButtonElement;
  1104. expect(monthFilter.value).toBe('all');
  1105. expect(monthFilter.options.length).toBe(4);
  1106. expect(monthPicker).not.toBeNull();
  1107. expect(Number.parseFloat(getComputedStyle(monthPicker).paddingLeft)).toBeGreaterThanOrEqual(40);
  1108. expect(monthPicker.getBoundingClientRect().height).toBe(46);
  1109. expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(5);
  1110. monthFilter.value = '2026-03';
  1111. monthFilter.dispatchEvent(new Event('change'));
  1112. const studentSearch = harness.routeNativeElement?.querySelector('[aria-label="按学生姓名筛选销课记录"]') as HTMLInputElement;
  1113. studentSearch.value = '花';
  1114. studentSearch.dispatchEvent(new Event('input'));
  1115. harness.fixture.detectChanges();
  1116. expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(1);
  1117. expect(harness.routeNativeElement?.querySelector('.schedule-list')?.textContent).toContain('花花');
  1118. expect(harness.routeNativeElement?.querySelector('.schedule-band > header')?.textContent).toContain('1 / 5');
  1119. (harness.routeNativeElement?.querySelector('.history-filter-panel footer button') as HTMLButtonElement).click();
  1120. harness.fixture.detectChanges();
  1121. expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(5);
  1122. await harness.navigateByUrl('/pages/his/his');
  1123. await harness.fixture.whenStable();
  1124. harness.fixture.detectChanges();
  1125. expect((harness.routeNativeElement?.querySelector('.student-filter select') as HTMLSelectElement).selectedOptions[0]?.textContent).toContain('张雅茜');
  1126. expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(2);
  1127. expect(session.student()?.userId).toBe(100);
  1128. (harness.routeNativeElement?.querySelector('.learning-record-card .learning-item') as HTMLElement).click();
  1129. await harness.fixture.whenStable();
  1130. harness.fixture.detectChanges();
  1131. expect(TestBed.inject(Router).url).toBe('/pages/his/his_words?id=201');
  1132. expect(harness.routeNativeElement?.querySelector('.readonly-word-header')?.textContent).toContain('历史记录');
  1133. expect(harness.routeNativeElement?.querySelector('.readonly-word-card')?.textContent).toContain('apple');
  1134. expect(harness.routeNativeElement?.querySelector('.readonly-word-card')?.textContent).toContain('共 1 个单词');
  1135. expect(harness.routeNativeElement?.querySelector('.selection-header')).toBeNull();
  1136. await harness.navigateByUrl('/pages/his/his');
  1137. await harness.fixture.whenStable();
  1138. harness.fixture.detectChanges();
  1139. const teacherCourse = harness.routeNativeElement?.querySelector('.current-learning-list button') as HTMLButtonElement;
  1140. expect(teacherCourse.disabled).toBeFalse();
  1141. expect(teacherCourse.classList.contains('readonly')).toBeFalse();
  1142. expect(teacherCourse.textContent).toContain('选择本次学习单元');
  1143. teacherCourse.click();
  1144. await harness.fixture.whenStable();
  1145. expect(TestBed.inject(Router).url).toContain('/pages/stu/stu_list');
  1146. await harness.navigateByUrl('/pages/home/ucenter');
  1147. await harness.fixture.whenStable();
  1148. harness.fixture.detectChanges();
  1149. const teacherServices = Array.from(harness.routeNativeElement?.querySelectorAll('.service-grid > a') ?? []).map((link) => link.getAttribute('href'));
  1150. expect(teacherServices).toEqual(['/pages/home/shop', '/pages/member/ke_xiao', '/pages/his/his', '/pages/member/feedback']);
  1151. expect(harness.routeNativeElement?.querySelector('.coach-wallet-strip')?.textContent).toContain('抗遗忘工资');
  1152. await harness.navigateByUrl('/pages/stu/stu_list?nid=801&bindingId=701');
  1153. await harness.fixture.whenStable();
  1154. harness.fixture.detectChanges();
  1155. expect((TestBed.inject(ApiService) as unknown as LearningModulesApiService).lastNodeParams['userId']).toBe(100);
  1156. const units = harness.routeNativeElement?.querySelectorAll('.node-list button') as NodeListOf<HTMLButtonElement>;
  1157. units[0].click();
  1158. units[1].click();
  1159. harness.fixture.detectChanges();
  1160. expect(harness.routeNativeElement?.querySelector('.unit-selection-summary')?.textContent).toContain('已选 2 个单元');
  1161. (harness.routeNativeElement?.querySelector('.unit-actions .primary-button') as HTMLButtonElement).click();
  1162. await harness.fixture.whenStable();
  1163. harness.fixture.detectChanges();
  1164. const router = TestBed.inject(Router);
  1165. expect(router.url).toContain('/pages/stu/stu_abc');
  1166. expect(harness.routeNativeElement?.querySelector('.pending-column')?.textContent).toContain('banana');
  1167. expect(harness.routeNativeElement?.querySelector('.completed-column')).toBeNull();
  1168. expect(harness.routeNativeElement?.querySelectorAll('.pending-column .word-refine-item').length).toBe(1);
  1169. (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
  1170. harness.fixture.detectChanges();
  1171. expect(harness.routeNativeElement?.querySelector('.completed-column')?.textContent).toContain('apple');
  1172. expect(harness.routeNativeElement?.querySelector('.pending-column')).toBeNull();
  1173. (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[0] as HTMLButtonElement).click();
  1174. harness.fixture.detectChanges();
  1175. const firstWord = harness.routeNativeElement?.querySelector('.pending-column .word-refine-item') as HTMLElement;
  1176. (firstWord.querySelector('.word-reveal') as HTMLButtonElement).click();
  1177. harness.fixture.detectChanges();
  1178. (firstWord.querySelector('.known-button') as HTMLButtonElement).click();
  1179. harness.fixture.detectChanges();
  1180. expect(harness.routeNativeElement?.querySelectorAll('.pending-column .word-refine-item').length).toBe(0);
  1181. expect(harness.routeNativeElement?.querySelector('.selection-header .primary-button')?.textContent).toContain('保存识词结果');
  1182. (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
  1183. harness.fixture.detectChanges();
  1184. expect(harness.routeNativeElement?.querySelectorAll('.completed-column .word-refine-item').length).toBe(2);
  1185. (harness.routeNativeElement?.querySelector('.completed-word-action button') as HTMLButtonElement).click();
  1186. harness.fixture.detectChanges();
  1187. expect(harness.routeNativeElement?.querySelectorAll('.completed-column .word-refine-item').length).toBe(1);
  1188. (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[0] as HTMLButtonElement).click();
  1189. harness.fixture.detectChanges();
  1190. expect(harness.routeNativeElement?.querySelectorAll('.pending-column .word-refine-item').length).toBe(1);
  1191. (harness.routeNativeElement?.querySelector('.selection-header .primary-button') as HTMLButtonElement).click();
  1192. await harness.fixture.whenStable();
  1193. harness.fixture.detectChanges();
  1194. expect(router.url).toContain('/pages/stu/stu_con');
  1195. expect(harness.routeNativeElement?.querySelectorAll('.repeat-list > button').length).toBe(1);
  1196. expect(harness.routeNativeElement?.querySelector('.readonly-word-header')).toBeNull();
  1197. await harness.navigateByUrl('/pages/stu/pl_stu5?nids=811&pnid=801&bindingId=701');
  1198. await harness.fixture.whenStable();
  1199. harness.fixture.detectChanges();
  1200. const spellingActions = harness.routeNativeElement?.querySelector('.teacher-spelling-actions') as HTMLElement;
  1201. expect(spellingActions.querySelectorAll('button').length).toBe(3);
  1202. expect(spellingActions.textContent).toContain('跳到下一关');
  1203. expect((harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent).words().length).toBe(1);
  1204. expect(localStorage.getItem('xiaoshu.learning.300.100.701._page_word_orilist')).not.toBeNull();
  1205. (spellingActions.querySelectorAll('button')[1] as HTMLButtonElement).click();
  1206. await harness.fixture.whenStable();
  1207. harness.fixture.detectChanges();
  1208. expect(router.url).toBe('/pages/his/his');
  1209. const standaloneRecord = (TestBed.inject(ApiService) as unknown as LearningModulesApiService).lastStandaloneLearningRecord;
  1210. expect(JSON.parse(String(standaloneRecord['content']))).toEqual(jasmine.objectContaining({ ModelID: 56, Hits: 1, title: '张雅茜' }));
  1211. expect(JSON.parse(String(standaloneRecord['addon']))).toEqual(jasmine.objectContaining({ UserID: 100, pl: 300, learned: 1, createReviewPlan: false }));
  1212. }, 20000);
  1213. it('loads appointment courses and units for the appointment student when no student is selected in the session', async () => {
  1214. user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
  1215. session.groupId.set(3);
  1216. session.student.set(null);
  1217. const harness = await RouterTestingHarness.create();
  1218. await harness.navigateByUrl('/pages/shop/get_order?id=901');
  1219. await harness.fixture.whenStable();
  1220. harness.fixture.detectChanges();
  1221. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  1222. expect(session.student()?.userId).toBe(100);
  1223. expect(api.lastAppointmentCourseParams['myfield']).toBe('yhid=100');
  1224. expect(api.lastNodeParams['userId']).toBe(100);
  1225. const courseSelect = harness.routeNativeElement?.querySelector('.course-section select') as HTMLSelectElement;
  1226. expect(courseSelect).toBeNull();
  1227. expect(harness.routeNativeElement?.querySelector('.course-section')?.textContent).toContain('七年级上册');
  1228. expect(harness.routeNativeElement?.querySelectorAll('.unit-grid article').length).toBe(2);
  1229. const unitButtons = harness.routeNativeElement?.querySelectorAll<HTMLButtonElement>('.unit-select');
  1230. expect(unitButtons?.[0].textContent).toContain('已学 5%');
  1231. expect(unitButtons?.[0].textContent).toContain('1/20 词');
  1232. expect(unitButtons?.[1].textContent).toContain('已学 0%');
  1233. expect(unitButtons?.[1].textContent).toContain('0/18 词');
  1234. expect((unitButtons?.[0].querySelector('.unit-progress-track > span') as HTMLElement).style.width).toBe('5%');
  1235. unitButtons?.[0].click();
  1236. unitButtons?.[1].click();
  1237. harness.fixture.detectChanges();
  1238. expect(harness.routeNativeElement?.querySelectorAll('.unit-select[aria-pressed="true"]').length).toBe(2);
  1239. expect(unitButtons?.[0].textContent).toContain('已学 5%');
  1240. });
  1241. it('distinguishes completed, empty and unavailable appointment vocabulary progress', async () => {
  1242. user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
  1243. session.groupId.set(3);
  1244. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  1245. api.unitRows = [
  1246. { NodeID: 811, NodeName: '已学全册', yx_word: '20', total: '20' },
  1247. { NodeID: 812, NodeName: '空册', yx_word: 0, total: 0 },
  1248. { NodeID: 813, NodeName: '缺少统计' },
  1249. { NodeID: 814, NodeName: '异常统计', yx_word: 'invalid', total: 20 },
  1250. ];
  1251. const harness = await RouterTestingHarness.create();
  1252. await harness.navigateByUrl('/pages/shop/get_order?id=901');
  1253. await harness.fixture.whenStable();
  1254. harness.fixture.detectChanges();
  1255. const units = harness.routeNativeElement?.querySelectorAll('.unit-select');
  1256. expect(units?.[0].textContent).toContain('已学 100%');
  1257. expect(units?.[0].textContent).toContain('20/20 词');
  1258. expect(units?.[1].textContent).toContain('已学 0%');
  1259. expect(units?.[1].textContent).toContain('暂无单词');
  1260. for (const index of [2, 3]) {
  1261. expect(units?.[index].textContent).toContain('暂无进度数据');
  1262. expect(units?.[index].querySelector('.unit-progress-track')).toBeNull();
  1263. }
  1264. });
  1265. it('opens feedback only on ending a lesson and submits once after validation', async () => {
  1266. user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
  1267. session.groupId.set(3);
  1268. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  1269. api.appointmentStatus = 10;
  1270. const harness = await RouterTestingHarness.create();
  1271. await harness.navigateByUrl('/pages/shop/get_order?id=901');
  1272. const component = harness.fixture.debugElement.query(By.directive(AppointmentPageComponent)).componentInstance as AppointmentPageComponent;
  1273. await harness.fixture.whenStable();
  1274. harness.fixture.detectChanges();
  1275. const dialog = harness.routeNativeElement?.querySelector('dialog') as HTMLDialogElement;
  1276. const endButton = harness.routeNativeElement?.querySelector('.appointment-actions .primary-button') as HTMLButtonElement;
  1277. expect(dialog.open).toBeFalse();
  1278. endButton.click();
  1279. harness.fixture.detectChanges();
  1280. expect(dialog.open).toBeTrue();
  1281. expect(api.completionRequests.length).toBe(0);
  1282. const submit = dialog.querySelector('.feedback-actions .primary-button') as HTMLButtonElement;
  1283. submit.click();
  1284. harness.fixture.detectChanges();
  1285. expect(dialog.querySelector('[role="alert"]')?.textContent).toContain('请填写本次学习内容');
  1286. expect(api.completionRequests.length).toBe(0);
  1287. const summary = dialog.querySelector('textarea') as HTMLTextAreaElement;
  1288. summary.value = 'Unit 3 词汇';
  1289. summary.dispatchEvent(new Event('input'));
  1290. await harness.fixture.whenStable();
  1291. (dialog.querySelector('.feedback-actions .secondary-button') as HTMLButtonElement).click();
  1292. expect(dialog.open).toBeFalse();
  1293. expect(api.appointmentStatus).toBe(10);
  1294. endButton.click();
  1295. harness.fixture.detectChanges();
  1296. expect((dialog.querySelector('textarea') as HTMLTextAreaElement).value).toBe('Unit 3 词汇');
  1297. component.reviewStatus = '完成';
  1298. component.trainingStatus = '需要巩固';
  1299. component.trainingSummary = '下次复习 Unit 3';
  1300. component.noHomework = true;
  1301. submit.click();
  1302. submit.click();
  1303. harness.fixture.detectChanges();
  1304. expect(api.completionRequests.length).toBe(1);
  1305. expect(JSON.parse(String(api.completionRequests[0]['feedback']))).toEqual(jasmine.objectContaining({ contentSummary: 'Unit 3 词汇', noHomework: true, homework: '无作业' }));
  1306. expect(submit.disabled).toBeTrue();
  1307. dialog.dispatchEvent(new Event('cancel', { cancelable: true }));
  1308. expect(dialog.open).toBeTrue();
  1309. api.appointmentStatus = 11;
  1310. api.completionResponse.next({ retcode: 1, result: {} });
  1311. api.completionResponse.complete();
  1312. await harness.fixture.whenStable();
  1313. harness.fixture.detectChanges();
  1314. expect(harness.routeNativeElement?.querySelector('dialog')).toBeNull();
  1315. expect(harness.routeNativeElement?.textContent).toContain('等待学员确认');
  1316. });
  1317. it('keeps feedback editable after a failed submission and allows closing with Escape', async () => {
  1318. user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
  1319. session.groupId.set(3);
  1320. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  1321. api.appointmentStatus = 10;
  1322. const harness = await RouterTestingHarness.create();
  1323. await harness.navigateByUrl('/pages/shop/get_order?id=901');
  1324. const component = harness.fixture.debugElement.query(By.directive(AppointmentPageComponent)).componentInstance as AppointmentPageComponent;
  1325. await harness.fixture.whenStable();
  1326. component.openCompletionFeedback();
  1327. component.contentSummary = 'Unit 3';
  1328. component.reviewStatus = '完成';
  1329. component.trainingStatus = '正常进行';
  1330. component.trainingSummary = '继续 Unit 4';
  1331. component.noHomework = true;
  1332. component.finishCoachLesson();
  1333. api.completionResponse.error(new Error('提交失败,请重试'));
  1334. harness.fixture.detectChanges();
  1335. const dialog = harness.routeNativeElement?.querySelector('dialog') as HTMLDialogElement;
  1336. expect(dialog.open).toBeTrue();
  1337. expect(dialog.querySelector('[role="alert"]')?.textContent).toContain('提交失败,请重试');
  1338. expect((dialog.querySelector('fieldset') as HTMLFieldSetElement).disabled).toBeFalse();
  1339. expect(component.contentSummary).toBe('Unit 3');
  1340. expect(api.appointmentStatus).toBe(10);
  1341. dialog.dispatchEvent(new Event('cancel', { cancelable: true }));
  1342. expect(dialog.open).toBeFalse();
  1343. });
  1344. it('counts words marked as known as completed and persists them for the student', async () => {
  1345. user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
  1346. session.groupId.set(3);
  1347. session.student.set({ userId: 100, userName: 'student', honeyName: '张雅茜', groupId: 1 });
  1348. const harness = await RouterTestingHarness.create();
  1349. await harness.navigateByUrl('/pages/stu/stu_abc?nids=811,812&pnid=801');
  1350. await harness.fixture.whenStable();
  1351. harness.fixture.detectChanges();
  1352. const pendingWord = harness.routeNativeElement?.querySelector('.pending-column .word-refine-item') as HTMLElement;
  1353. (pendingWord.querySelector('.word-reveal') as HTMLButtonElement).click();
  1354. harness.fixture.detectChanges();
  1355. (pendingWord.querySelector('.known-button') as HTMLButtonElement).click();
  1356. harness.fixture.detectChanges();
  1357. (harness.routeNativeElement?.querySelector('.selection-header .primary-button') as HTMLButtonElement).click();
  1358. await harness.fixture.whenStable();
  1359. harness.fixture.detectChanges();
  1360. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  1361. expect(api.lastLearningWordUpdate).toEqual(jasmine.objectContaining({ uid: 100, wordsId: '1002', kcid: '801', save: 1 }));
  1362. expect(TestBed.inject(Router).url).toContain('/pages/stu/stu_abc');
  1363. expect(harness.routeNativeElement?.querySelector('.completed-column')).toBeNull();
  1364. (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
  1365. harness.fixture.detectChanges();
  1366. expect(harness.routeNativeElement?.querySelectorAll('.completed-column .word-refine-item').length).toBe(2);
  1367. expect(harness.routeNativeElement?.querySelector('.pending-column')).toBeNull();
  1368. });
  1369. it('finishes the last vocabulary group, loads the coach record and opens completion feedback', async () => {
  1370. user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
  1371. session.groupId.set(3);
  1372. session.student.set({ userId: 100, honeyName: '张雅茜', groupId: 1 });
  1373. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  1374. api.appointmentStatus = 10;
  1375. const word = { GeneralID: 1001, NodeID: 811, Title: 'apple', sy: '苹果' };
  1376. localStorage.setItem('xiaoshu.learning.300.100.901._page_word_orilist', JSON.stringify([[word]]));
  1377. localStorage.setItem('xiaoshu.learning.300.100.901._page_current_learn', '1');
  1378. // Stale page-count metadata must not create a nonexistent next group.
  1379. localStorage.setItem('xiaoshu.learning.300.100.901._page_total_page', '2');
  1380. localStorage.removeItem('xiaoshu.learning.300.100.901.learn_mode');
  1381. const harness = await RouterTestingHarness.create();
  1382. const router = TestBed.inject(Router);
  1383. const navigate = spyOn(router, 'navigate').and.callThrough();
  1384. await harness.navigateByUrl('/pages/stu/stu_con?nids=811&pnid=801&orderId=901');
  1385. await harness.fixture.whenStable();
  1386. harness.fixture.detectChanges();
  1387. const learning = () => harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
  1388. learning().skipStudyList();
  1389. await harness.fixture.whenStable();
  1390. harness.fixture.detectChanges();
  1391. expect(router.url).toContain('/pages/stu/stu3');
  1392. const pair = learning().words()[0];
  1393. learning().chooseMatch('left', pair);
  1394. learning().chooseMatch('right', pair);
  1395. learning().continueAfterStage();
  1396. await harness.fixture.whenStable();
  1397. harness.fixture.detectChanges();
  1398. expect(router.url).toContain('/pages/stu/stu4');
  1399. learning().chooseMeaning(learning().words()[0]);
  1400. learning().continueAfterStage();
  1401. await harness.fixture.whenStable();
  1402. harness.fixture.detectChanges();
  1403. expect(router.url).toContain('/pages/stu/pl_stu5');
  1404. learning().skipSpellingStage();
  1405. expect(learning().hasNextWordBatch).toBeFalse();
  1406. await harness.fixture.whenStable();
  1407. harness.fixture.detectChanges();
  1408. harness.fixture.detectChanges();
  1409. expect(router.url).toContain('/pages/stu/pl_end');
  1410. expect(api.lastAppointmentRecordParams['myfield2']).toBe('UserId=100|dsid=901|pl=300');
  1411. expect(harness.routeNativeElement?.querySelector('.empty-state.error')).toBeNull();
  1412. expect(learning().words()[0].word).toBe('apple');
  1413. learning().markAllKnown();
  1414. learning().submitReview();
  1415. await harness.fixture.whenStable();
  1416. harness.fixture.detectChanges();
  1417. harness.fixture.detectChanges();
  1418. expect(router.url).toBe('/pages/shop/get_order?id=901&feedback=1');
  1419. expect((harness.routeNativeElement?.querySelector('dialog') as HTMLDialogElement).open).toBeTrue();
  1420. expect(api.appointmentStatus).toBe(10);
  1421. expect(api.completionRequests.length).toBe(0);
  1422. expect(api.studyUpdates.length).toBe(2);
  1423. expect(JSON.parse(String(api.studyUpdates[1]['addon']))['ygg']).toBe(1);
  1424. expect(navigate.calls.all().every(call => call.args[1]?.replaceUrl === true)).toBeTrue();
  1425. }, 20000);
  1426. it('advances only to a real next group and returns from the last stage to its course in one click', async () => {
  1427. user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
  1428. session.groupId.set(3);
  1429. session.student.set({ userId: 100, honeyName: '张雅茜', groupId: 1 });
  1430. const pages = [[{ GeneralID: 1001, Title: 'apple', sy: '苹果' }, { GeneralID: 1002, Title: 'banana', sy: '香蕉' }], [{ GeneralID: 1003, Title: 'cherry', sy: '樱桃' }]];
  1431. localStorage.setItem('xiaoshu.learning.300.100.901._page_word_orilist', JSON.stringify(pages));
  1432. localStorage.setItem('xiaoshu.learning.300.100.901._page_total_page', '0');
  1433. localStorage.setItem('xiaoshu.learning.300.100.901._page_current_learn', '1');
  1434. const harness = await RouterTestingHarness.create();
  1435. const router = TestBed.inject(Router);
  1436. await harness.navigateByUrl('/pages/stu/pl_stu5?nids=811&pnid=801&orderId=901');
  1437. await harness.fixture.whenStable();
  1438. harness.fixture.detectChanges();
  1439. const component = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
  1440. expect(component.words().length).toBe(2);
  1441. component.skipSpellingStage();
  1442. await harness.fixture.whenStable();
  1443. harness.fixture.detectChanges();
  1444. expect(router.url).toContain('/pages/stu/stu_con');
  1445. const next = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
  1446. expect(next.words()[0].word).toBe('cherry');
  1447. expect((TestBed.inject(ApiService) as unknown as LearningModulesApiService).studyUpdates.length).toBe(0);
  1448. (harness.routeNativeElement?.querySelector('.app-header button[aria-label="返回"]') as HTMLButtonElement).click();
  1449. await harness.fixture.whenStable();
  1450. harness.fixture.detectChanges();
  1451. expect(router.url).toBe('/pages/shop/get_order?id=901');
  1452. for (const key of ['_page_word_orilist', '_page_total_page', '_page_current_learn', 'skip_spelling_stage']) localStorage.removeItem(`xiaoshu.learning.300.100.901.${key}`);
  1453. });
  1454. it('keeps teacher-led spelling skipped through the remaining word groups', async () => {
  1455. user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
  1456. session.groupId.set(3);
  1457. session.student.set({ userId: 100, honeyName: '张雅茜', groupId: 1 });
  1458. const prefix = 'xiaoshu.learning.300.100.901.';
  1459. const pages = [[{ GeneralID: 1001, Title: 'apple', sy: '苹果' }], [{ GeneralID: 1002, Title: 'banana', sy: '香蕉' }]];
  1460. localStorage.setItem(prefix + '_page_word_orilist', JSON.stringify(pages));
  1461. localStorage.setItem(prefix + '_page_current_learn', '1');
  1462. const harness = await RouterTestingHarness.create();
  1463. const router = TestBed.inject(Router);
  1464. await harness.navigateByUrl('/pages/stu/pl_stu5?nids=811&pnid=801&orderId=901');
  1465. const learning = () => harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
  1466. learning().skipSpellingStage();
  1467. await harness.fixture.whenStable();
  1468. harness.fixture.detectChanges();
  1469. expect(router.url).toContain('/pages/stu/stu_con');
  1470. expect(localStorage.getItem(prefix + 'skip_spelling_stage')).toBe('true');
  1471. learning().skipStudyList();
  1472. await harness.fixture.whenStable();
  1473. harness.fixture.detectChanges();
  1474. learning().chooseMatch('left', learning().words()[0]);
  1475. learning().chooseMatch('right', learning().words()[0]);
  1476. learning().continueAfterStage();
  1477. await harness.fixture.whenStable();
  1478. harness.fixture.detectChanges();
  1479. expect(router.url).toContain('/pages/stu/stu4');
  1480. learning().chooseMeaning(learning().words()[0]);
  1481. expect(learning().stageActionLabel).toBe('查看学习结果');
  1482. learning().continueAfterStage();
  1483. await harness.fixture.whenStable();
  1484. expect(router.url).toContain('/pages/stu/pl_end');
  1485. expect(localStorage.getItem(prefix + 'skip_spelling_stage')).toBe('true');
  1486. localStorage.removeItem(prefix + 'skip_spelling_stage');
  1487. }, 15000);
  1488. it('resolves the appointment student before reading end results and offers an exit on failure', async () => {
  1489. user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
  1490. session.groupId.set(3);
  1491. session.student.set({ userId: 999, honeyName: '之前的学员', groupId: 1 });
  1492. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  1493. const get = api.get.bind(api);
  1494. spyOn(api, 'get').and.callFake((action, params) => action === 'e_record_detail'
  1495. ? throwError(() => new Error('学习记录暂时无法加载')) : get(action, params));
  1496. const harness = await RouterTestingHarness.create();
  1497. await harness.navigateByUrl('/pages/stu/pl_end?id=901');
  1498. await harness.fixture.whenStable();
  1499. harness.fixture.detectChanges();
  1500. harness.fixture.detectChanges();
  1501. expect(session.student()?.userId).toBe(100);
  1502. expect(api.lastAppointmentRecordParams['myfield2']).toBe('UserId=100|dsid=901|pl=300');
  1503. expect(harness.routeNativeElement?.querySelector('.empty-state.error')?.textContent).toContain('学习记录暂时无法加载');
  1504. (harness.routeNativeElement?.querySelector('.empty-state.error .primary-button') as HTMLButtonElement).click();
  1505. await harness.fixture.whenStable();
  1506. harness.fixture.detectChanges();
  1507. expect(TestBed.inject(Router).url).toBe('/pages/shop/get_order?id=901');
  1508. });
  1509. it('shows learning history in pages of twenty records', async () => {
  1510. user.set({ userId: 777, userName: 'paged-student', honeyName: '分页学员', groupId: 1 });
  1511. session.groupId.set(1);
  1512. const harness = await RouterTestingHarness.create();
  1513. await harness.navigateByUrl('/pages/his/his');
  1514. await harness.fixture.whenStable();
  1515. harness.fixture.detectChanges();
  1516. expect(harness.routeNativeElement?.querySelector('.record-actions')?.textContent).toContain('45 条');
  1517. expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(20);
  1518. expect(harness.routeNativeElement?.querySelector('.record-pagination')?.textContent).toContain('第 1 / 3 页');
  1519. const pageButtons = harness.routeNativeElement?.querySelectorAll('.record-pagination button') as NodeListOf<HTMLButtonElement>;
  1520. expect(pageButtons[0].disabled).toBeTrue();
  1521. pageButtons[1].click();
  1522. harness.fixture.detectChanges();
  1523. expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(20);
  1524. expect(harness.routeNativeElement?.querySelector('.record-pagination')?.textContent).toContain('第 2 / 3 页');
  1525. (harness.routeNativeElement?.querySelectorAll('.record-pagination button')[1] as HTMLButtonElement).click();
  1526. harness.fixture.detectChanges();
  1527. expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(5);
  1528. expect((harness.routeNativeElement?.querySelectorAll('.record-pagination button')[1] as HTMLButtonElement).disabled).toBeTrue();
  1529. });
  1530. });