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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. import { provideHttpClient } from '@angular/common/http';
  2. import { signal } from '@angular/core';
  3. import { TestBed } from '@angular/core/testing';
  4. import { provideRouter, Router } from '@angular/router';
  5. import { RouterTestingHarness } from '@angular/router/testing';
  6. import { Observable, of } from 'rxjs';
  7. import { routes } from '../app.routes';
  8. import { ApiService } from './api.service';
  9. import { ApiEnvelope, AppUser } from './models';
  10. import { PlatformService } from './platform.service';
  11. import { SessionService } from './session.service';
  12. class LearningModulesApiService {
  13. lastScheduleParams: Record<string, unknown> = {};
  14. lastNodeParams: Record<string, unknown> = {};
  15. lastWordParams: Record<string, unknown> = {};
  16. lastLearningWordUpdate: Record<string, unknown> = {};
  17. lastMemoryParams: Record<string, unknown> = {};
  18. get<T>(action: string, params: Record<string, unknown> = {}): Observable<ApiEnvelope<T>> {
  19. if (action === 'content_list' && Number(params['nid']) === 291) {
  20. return this.result<T>([
  21. { GeneralID: 201, CreateTime: '2026-08-01 09:30:00', con: '初中英语@Unit 1', xxqs: JSON.stringify([{ Title: 'apple' }]) },
  22. { GeneralID: 202, CreateTime: '2026-08-04 16:20:00', con: '高中英语@Unit 2' },
  23. ]);
  24. }
  25. if (action === 'content_list' && Number(params['nid']) === 388) {
  26. return this.result<T>([
  27. { GeneralID: 100, Title: '张雅茜@历史任务', kywsj: '2026-05-15 09:00', fxzt: 1, yhid: 100, xxjlid: 900 },
  28. { GeneralID: 101, Title: '张雅茜@任务', kywsj: '2026-08-01 09:00', fxzt: 1, yhid: 100, xxjlid: 900 },
  29. { GeneralID: 102, Title: '张雅茜@任务', kywsj: '2026-08-02 09:00', fxzt: 0, yhid: 100, xxjlid: 900 },
  30. ]);
  31. }
  32. if (action === 'content_list' && Number(params['nid']) === 3) {
  33. return this.result<T>([{ GeneralID: 301, Title: '阅读理解专项' }]);
  34. }
  35. if (action === 'content_list' && Number(params['nid']) === 389) {
  36. return this.result<T>([{ GeneralID: 401, df: 18, totalScore: 48, dontKnow: 20, wrong: 10 }]);
  37. }
  38. if (action === 'content_list' && Number(params['nid']) === 28) {
  39. return this.result<T>([
  40. { GeneralID: 801, kcid: 801, Title: '初中英语|七年级上册@同步词汇' },
  41. { GeneralID: 802, kcid: 801, Title2: '【初中英语】阅读训练' },
  42. ]);
  43. }
  44. if (action === 'content_list' && Number(params['nid']) === 29) {
  45. this.lastScheduleParams = params;
  46. return this.result<T>([
  47. { GeneralID: 901, bxrq: '2026-08-10 00:00:00', sdsd: '19:00', Title: '同步伴学', plxm: '张老师', dszt: 10 },
  48. { GeneralID: 902, bxrq: '2026-08-03 00:00:00', sdsd: '18:00', Title: '历史伴学', plxm: '李老师', dszt: 30 },
  49. ]);
  50. }
  51. if (action === 'node_list' && Number(params['pid']) === 8) {
  52. return this.result<T>([{ NodeID: 801, NodeName: '小学人教版词库' }]);
  53. }
  54. if (action === 'node_list' && Number(params['pid']) === 801) {
  55. this.lastNodeParams = params;
  56. return this.result<T>([
  57. { NodeID: 811, NodeName: 'Unit 1', yx_word: 1, total: 20 },
  58. { NodeID: 812, NodeName: 'Unit 2', yx_word: 0, total: 18 },
  59. ]);
  60. }
  61. if (action === 'e_ck_list') {
  62. this.lastWordParams = params;
  63. const allWords = Number(params['nids']) === 813
  64. ? Array.from({ length: 65 }, (_, index) => ({ GeneralID: 2001 + index, NodeID: 813, Title: `word-${index + 1}`, sy: `释义 ${index + 1}` }))
  65. : [
  66. { GeneralID: 1001, NodeID: 811, Title: 'apple', sy: 'n. 苹果', yp: 'apple.mp3', w_learned: 1 },
  67. { GeneralID: 1002, NodeID: 812, Title: 'banana', sy: 'n. 香蕉', yp: 'banana.mp3', w_learned: 0 },
  68. ];
  69. const pageIndex = Math.max(1, Number(params['cpage'] ?? 1));
  70. const pageSize = Math.max(1, Number(params['psize'] ?? 30));
  71. const result = allWords.slice((pageIndex - 1) * pageSize, pageIndex * pageSize);
  72. return of({
  73. retcode: 1,
  74. result: result as T,
  75. page: { itemCount: allWords.length, pageCount: Math.ceil(allWords.length / pageSize), pageIndex, pageSize },
  76. });
  77. }
  78. if (action === 'node_get') return this.result<T>([{ NodeID: 3, NodeName: '专项训练' }]);
  79. if (action === 'vote_ask') return this.result<T>([{ ID: 1, Title: '第一组问题' }]);
  80. if (action === 'vote_question') {
  81. return this.result<T>([{ ID: 501, AskID: 1, QType: 1, QTitle: 'apple', QContent: '苹果', QOption: JSON.stringify([{ text: '苹果', value: 1 }]) }]);
  82. }
  83. if (action === 'e_words_list') {
  84. return this.result<T>([{
  85. GeneralID: 601,
  86. Title: 'tend@张雅茜',
  87. detail: [{ GeneralID: 701, Title: 'tend', sy: 'vt.照顾;照料', yp: 'tend.mp3' }],
  88. }]);
  89. }
  90. return this.result<T>([]);
  91. }
  92. post<T>(action: string, params: Record<string, unknown> = {}): Observable<ApiEnvelope<T>> {
  93. if (action === 'e_add_words') {
  94. this.lastLearningWordUpdate = params;
  95. return this.result<T>({ affected: String(params['wordsId'] ?? '').split(',').filter(Boolean).length });
  96. }
  97. if (action === 'e_order_tongji') {
  98. return this.result<T>({ t30: 4, t60: 6, t_tiyan: 1, t_total: 11, t_shichang: 9, t_yongji: 320 });
  99. }
  100. if (action === 'e_user_list') {
  101. return this.result<T>([{ szyh: 100, honeyname: '张雅茜' }]);
  102. }
  103. if (action === 'app_learning_overview') {
  104. const records = [
  105. { GeneralID: 201, CreateTime: '2026-08-01 09:30:00', dqrq: '20260801', con: '初中英语@Unit 1', xxqs: JSON.stringify([{ Title: 'apple' }]) },
  106. { GeneralID: 202, CreateTime: '2026-08-04 16:20:00', dqrq: '20260804', con: '高中英语@Unit 2' },
  107. ];
  108. const selected = String(params['date'] ?? '');
  109. return this.result<T>({
  110. records: selected ? records.filter((item) => item.CreateTime.startsWith(selected)) : records,
  111. latestReadingRecord: records[0],
  112. recordDateCounts: { '2026-08-01': 1, '2026-08-04': 1 },
  113. scheduleDateCounts: {},
  114. schedules: [],
  115. courses: [
  116. { GeneralID: 801, id: 701, kcid: 801, Title: '初中英语|七年级上册@同步词汇' },
  117. { GeneralID: 802, id: 702, Title2: '【初中英语】阅读训练', syjd: '{"811":"811@Unit 1@1"}' },
  118. ],
  119. refreshedAt: '2026-08-26T00:00:00.000Z',
  120. });
  121. }
  122. if (action === 'app_companion_overview') {
  123. this.lastScheduleParams = params;
  124. const schedules = [
  125. { GeneralID: 901, bxrq: '2026-08-10 00:00:00', sdsd: '19:00', Title: '同步伴学', plxm: '张老师', dszt: 10 },
  126. { GeneralID: 902, bxrq: '2026-08-03 00:00:00', sdsd: '18:00', Title: '历史伴学', plxm: '李老师', dszt: 30 },
  127. { GeneralID: 903, bxrq: '2099-01-01 00:00:00', sdsd: '18:00', Title: '未来计划', plxm: '王老师', dszt: 0 },
  128. ];
  129. const historySchedules = [
  130. { GeneralID: 911, bxrq: '2026-07-14 00:00:00', sdsd: '22:00', Title: '谢晨晨(60分钟练习)', plxm: '张老师', dszt: 30 },
  131. { GeneralID: 912, bxrq: '2026-07-14 00:00:00', sdsd: '17:00', Title: '谢谢(60分钟练习)', plxm: '张老师', dszt: 30 },
  132. { GeneralID: 913, bxrq: '2026-03-09 00:00:00', sdsd: '10:00', Title: '花花(60分钟练习)', plxm: '张老师', dszt: 30 },
  133. { GeneralID: 914, bxrq: '2026-01-18 00:00:00', sdsd: '16:30', Title: '小赵(60分钟练习)', plxm: '张老师', dszt: 30 },
  134. { GeneralID: 915, bxrq: '2026-01-18 00:00:00', sdsd: '16:00', Title: '小黄(60分钟练习)', plxm: '张老师', dszt: 30 },
  135. ];
  136. const rangeStart = String(params['rangeStart'] ?? '');
  137. const rangeEnd = String(params['rangeEnd'] ?? '');
  138. return this.result<T>({
  139. courses: [
  140. { GeneralID: 801, id: 701, kcid: 801, Title: '初中英语|七年级上册@同步词汇' },
  141. { GeneralID: 802, id: 702, Title2: '【初中英语】阅读训练', syjd: '{"811":"811@Unit 1@1"}' },
  142. ],
  143. schedules: params['history']
  144. ? historySchedules
  145. : rangeStart && rangeEnd
  146. ? schedules.filter((item) => item.bxrq.slice(0, 10) >= rangeStart && item.bxrq.slice(0, 10) <= rangeEnd)
  147. : schedules,
  148. refreshedAt: '2026-08-26T00:00:00.000Z',
  149. });
  150. }
  151. if (action === 'app_account_overview') {
  152. if (Number(params['uid']) === 300) {
  153. return this.result<T>({
  154. profile: { userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 },
  155. coachPanel: { todaySchedules: 2, completedLessons: 8, reviewIncome: 36, reviewRecords: 3, totalLessons: 11, totalHours: 9, course30: 4, course60: 6, trialLessons: 1 },
  156. refreshedAt: '2026-08-26T00:00:00.000Z',
  157. });
  158. }
  159. return this.result<T>({
  160. profile: { userId: 100, userName: 'student', honeyName: '张雅茜', groupId: 1 },
  161. companionSummary: { completedSessions: 1, completedMinutes: 60 },
  162. antiForgetSummary: { total: 3, completed: 2, progress: 67, todayTotal: 0, todayCompleted: 0, duePending: 1, scheduled: 0, scheduledDays: 3 },
  163. refreshedAt: '2026-08-26T00:00:00.000Z',
  164. });
  165. }
  166. if (action === 'e_get_21list') {
  167. this.lastMemoryParams = params;
  168. return this.result<T>([
  169. { 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 },
  170. { 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 },
  171. { 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 },
  172. ]);
  173. }
  174. if (action === 'e_record_detail') {
  175. return of({
  176. retcode: 1,
  177. result: [{ GeneralID: 701, Title: 'apple', detail: [{ GeneralID: 701, Title: 'apple', sy: '苹果', yp: '' }] }],
  178. addon: [{ if_writing: '0', if_writing_tips: '先回忆单词,再查看释义' }],
  179. } as unknown as ApiEnvelope<T>);
  180. }
  181. return this.result<T>([]);
  182. }
  183. assetUrl(path: unknown): string { return String(path ?? ''); }
  184. private result<T>(result: unknown): Observable<ApiEnvelope<T>> {
  185. return of({ retcode: 1, result: result as T });
  186. }
  187. }
  188. describe('home learning modules', () => {
  189. const user = signal<AppUser | null>({ userId: 100, userName: 'student', honeyName: '张雅茜', groupId: 1 });
  190. const selectedStudent = signal<AppUser | null>(null);
  191. const session = {
  192. user,
  193. token: signal('test-token'),
  194. student: selectedStudent,
  195. isLoggedIn: signal(true),
  196. groupId: signal(1),
  197. setStudent: (student: AppUser | null) => selectedStudent.set(student),
  198. patchUser: () => undefined,
  199. logout: () => undefined,
  200. };
  201. beforeEach(() => {
  202. user.set({ userId: 100, userName: 'student', honeyName: '张雅茜', groupId: 1 });
  203. session.groupId.set(1);
  204. session.student.set(null);
  205. TestBed.configureTestingModule({
  206. providers: [
  207. provideRouter(routes),
  208. provideHttpClient(),
  209. { provide: ApiService, useClass: LearningModulesApiService },
  210. { provide: SessionService, useValue: session },
  211. ],
  212. });
  213. });
  214. it('renders source-shaped data for all four home entries', async () => {
  215. const harness = await RouterTestingHarness.create();
  216. await harness.navigateByUrl('/pages/home/index');
  217. expect(harness.fixture.nativeElement.querySelector('.tabbar')).not.toBeNull();
  218. expect(harness.routeNativeElement?.querySelectorAll('.home-header .header-action').length).toBe(1);
  219. expect(harness.routeNativeElement?.querySelector('.home-header .header-action')?.getAttribute('href')).toBe('/pages/home/search');
  220. expect(harness.routeNativeElement?.querySelector('.home-header [aria-label="用户中心"]')).toBeNull();
  221. expect(harness.routeNativeElement?.querySelector('.memory-hero')).not.toBeNull();
  222. expect(harness.routeNativeElement?.querySelectorAll('.memory-dot').length).toBe(21);
  223. expect(harness.routeNativeElement?.querySelector('.today-panel')?.textContent).toContain('学习新词');
  224. expect(harness.routeNativeElement?.querySelectorAll('.student-stats > div').length).toBe(5);
  225. expect(harness.routeNativeElement?.querySelectorAll('.quick-grid > a').length).toBe(4);
  226. expect(harness.routeNativeElement?.querySelector('.continue-button')?.getAttribute('href')).toBe('/pages/stu/stu_calendar');
  227. expect(harness.routeNativeElement?.querySelector('.task-link')?.getAttribute('href')).toBe('/pages/home/cart');
  228. await harness.navigateByUrl('/pages/stu/index');
  229. await harness.fixture.whenStable();
  230. harness.fixture.detectChanges();
  231. expect(TestBed.inject(Router).url).toBe('/pages/home/cart');
  232. expect(harness.routeNativeElement?.textContent).not.toContain('选择词库教材');
  233. await harness.navigateByUrl('/pages/stu/stu_list?nid=801&bindingId=701');
  234. await harness.fixture.whenStable();
  235. harness.fixture.detectChanges();
  236. expect(harness.routeNativeElement?.querySelectorAll('.node-list button').length).toBe(2);
  237. expect(harness.routeNativeElement?.querySelector('.node-list button')?.textContent).toContain('已学 1/20 · 进度 5%');
  238. expect(harness.routeNativeElement?.querySelectorAll('.unit-progress-ring').length).toBe(2);
  239. expect(harness.routeNativeElement?.querySelector('.unit-progress-ring')?.textContent).toContain('5%');
  240. expect(harness.routeNativeElement?.querySelector('.unit-selection-summary')).toBeNull();
  241. expect(harness.routeNativeElement?.querySelector('.unit-actions')).toBeNull();
  242. const firstUnit = harness.routeNativeElement?.querySelector('.node-list button') as HTMLButtonElement;
  243. firstUnit.click();
  244. await harness.fixture.whenStable();
  245. harness.fixture.detectChanges();
  246. const readonlyRouter = TestBed.inject(Router);
  247. expect(readonlyRouter.url).toBe('/pages/stu/stu_abc?nids=811&pnid=801&readonly=1');
  248. expect(harness.routeNativeElement?.querySelector('.selection-header')?.textContent).toContain('词库学习进度');
  249. expect(harness.routeNativeElement?.querySelector('.selection-header .primary-button')).toBeNull();
  250. expect(harness.routeNativeElement?.querySelector('.pending-column')?.textContent).toContain('banana');
  251. expect(harness.routeNativeElement?.querySelector('.word-select-check')).toBeNull();
  252. expect(harness.routeNativeElement?.querySelector('.word-assessment')).toBeNull();
  253. (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
  254. harness.fixture.detectChanges();
  255. expect(harness.routeNativeElement?.querySelector('.completed-column')?.textContent).toContain('apple');
  256. expect(harness.routeNativeElement?.querySelector('.pending-column')).toBeNull();
  257. await harness.navigateByUrl('/pages/home/index');
  258. await harness.navigateByUrl('/pages/stu/stu_con?nid=813&pnid=801&readonly=1');
  259. await harness.fixture.whenStable();
  260. harness.fixture.detectChanges();
  261. const wordApi = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  262. expect(wordApi.lastWordParams['psize']).toBe(30);
  263. expect(harness.routeNativeElement?.querySelectorAll('.readonly-word-list article').length).toBe(30);
  264. expect(harness.routeNativeElement?.querySelector('.readonly-word-header')?.textContent).toContain('第 1–30 条 · 共 65 个单词');
  265. expect(harness.routeNativeElement?.querySelector('.word-preview-pagination')?.textContent).toContain('1 / 3 页');
  266. (harness.routeNativeElement?.querySelector('[aria-label="第 2 页"]') as HTMLButtonElement).click();
  267. await harness.fixture.whenStable();
  268. harness.fixture.detectChanges();
  269. expect(wordApi.lastWordParams['cpage']).toBe(2);
  270. expect(harness.routeNativeElement?.querySelectorAll('.readonly-word-list article').length).toBe(30);
  271. expect(harness.routeNativeElement?.querySelector('.readonly-word-header')?.textContent).toContain('第 31–60 条 · 共 65 个单词');
  272. expect(harness.routeNativeElement?.querySelector('.readonly-word-list')?.textContent).toContain('word-31');
  273. expect(harness.routeNativeElement?.querySelector('.word-preview-pagination')?.textContent).toContain('2 / 3 页');
  274. await harness.navigateByUrl('/pages/stu/stu_list?nid=999&bindingId=799');
  275. await harness.fixture.whenStable();
  276. harness.fixture.detectChanges();
  277. expect(TestBed.inject(Router).url).toBe('/pages/stu/stu_abc?nids=999&readonly=1');
  278. expect(harness.routeNativeElement?.querySelector('.selection-header')?.textContent).toContain('词库学习进度');
  279. await harness.navigateByUrl('/pages/stu/stu_calendar');
  280. expect(harness.fixture.nativeElement.querySelector('.tabbar')).toBeNull();
  281. expect(harness.routeNativeElement?.querySelectorAll('.memory-date').length).toBe(42);
  282. expect(harness.routeNativeElement?.querySelectorAll('.memory-date.completed, .memory-date.due').length).toBe(2);
  283. expect(harness.routeNativeElement?.querySelector('.memory-overview')?.textContent).toContain('2 / 3');
  284. expect(harness.routeNativeElement?.querySelector('.memory-date-tasks')?.textContent).toContain('高中英语复习');
  285. expect(harness.routeNativeElement?.querySelector('.review-sequence')?.textContent).toContain('第 3 次复习');
  286. expect(harness.routeNativeElement?.querySelector('.first-learned-at')?.textContent).toContain('首次学习:2026-07-20 10:20');
  287. for (let index = 0; index < 3; index += 1) {
  288. (harness.routeNativeElement?.querySelector('[aria-label="上个月"]') as HTMLButtonElement).click();
  289. harness.fixture.detectChanges();
  290. }
  291. expect(harness.routeNativeElement?.querySelector('[aria-label^="2026-05-15"]')?.classList).toContain('completed');
  292. expect(harness.routeNativeElement?.textContent).toContain('张雅茜');
  293. expect(harness.routeNativeElement?.querySelector('.memory-overview header b')?.textContent).toContain('67%');
  294. expect(harness.routeNativeElement?.querySelector('.memory-stats')?.textContent).toContain('2 / 3');
  295. await harness.navigateByUrl('/pages/home/ucenter');
  296. expect(harness.fixture.nativeElement.querySelector('.tabbar')).not.toBeNull();
  297. expect(harness.routeNativeElement?.querySelector('.new-word-entry')?.getAttribute('href')).toBe('/pages/home/cart');
  298. expect(harness.routeNativeElement?.querySelector('.course-memory')?.textContent).toContain('21 天抗遗忘计划');
  299. expect(harness.routeNativeElement?.querySelector('.memory-overview em')?.textContent).toContain('67%');
  300. expect(harness.routeNativeElement?.querySelector('.memory-overview')?.textContent).toContain('覆盖 3 个复习日');
  301. expect(harness.routeNativeElement?.querySelector('.memory-counts')?.textContent).toContain('3总任务');
  302. expect(harness.routeNativeElement?.querySelector('.memory-counts')?.textContent).toContain('2已完成');
  303. expect(harness.routeNativeElement?.querySelector('.memory-counts')?.textContent).toContain('1到期待复习');
  304. expect(harness.routeNativeElement?.querySelector('.memory-counts')?.textContent).toContain('0未来待复习');
  305. await harness.navigateByUrl('/pages/stu/record?id=827482&gid=828268&kcid=203');
  306. const recordWord = harness.routeNativeElement?.querySelector('.record-word') as HTMLButtonElement;
  307. expect(recordWord.querySelector('.record-copy strong')?.textContent).toBe('apple');
  308. expect(harness.routeNativeElement?.textContent).not.toContain('苹果');
  309. recordWord.click();
  310. await harness.fixture.whenStable();
  311. harness.fixture.detectChanges();
  312. expect(recordWord.querySelector('.record-copy strong')?.textContent).toBe('苹果');
  313. expect(harness.routeNativeElement?.querySelector('.record-overview')?.textContent).toContain('1 / 1');
  314. await harness.navigateByUrl('/pages/member/starshop');
  315. expect(harness.fixture.nativeElement.querySelector('.tabbar')).toBeNull();
  316. expect(harness.routeNativeElement?.querySelector('.word-select-list strong')?.textContent).toBe('tend');
  317. expect(harness.routeNativeElement?.querySelector('.word-select-list')?.textContent).not.toContain('@张雅茜');
  318. (harness.routeNativeElement?.querySelector('.word-select-list > button') as HTMLButtonElement).click();
  319. (harness.routeNativeElement?.querySelector('.selection-header .primary-button') as HTMLButtonElement).click();
  320. await harness.fixture.whenStable();
  321. const selectedNewWords = JSON.parse(sessionStorage.getItem('xiaoshu.selectedWords') ?? '[]') as Array<{ word?: string }>;
  322. expect(selectedNewWords[0]?.word).toBe('tend');
  323. expect(sessionStorage.getItem('xiaoshu.selectedWords')).not.toContain('@张雅茜');
  324. await harness.navigateByUrl('/pages/his/well?nid=3');
  325. expect(harness.fixture.nativeElement.querySelector('.tabbar')).toBeNull();
  326. expect(harness.routeNativeElement?.textContent).toContain('阅读理解专项');
  327. await harness.navigateByUrl('/pages/home/cart');
  328. expect(harness.fixture.nativeElement.querySelector('.tabbar')).not.toBeNull();
  329. expect(harness.fixture.nativeElement.querySelector('.app-header .icon-button')).toBeNull();
  330. expect(harness.fixture.nativeElement.querySelector('.app-header h1')?.textContent).toContain('伴学中心');
  331. expect(harness.routeNativeElement?.querySelector('.library-entry')).toBeNull();
  332. expect(harness.routeNativeElement?.textContent).toContain('我的学习内容');
  333. expect(harness.routeNativeElement?.querySelectorAll('.course-list li').length).toBe(2);
  334. expect(harness.routeNativeElement?.querySelectorAll('.course-list button').length).toBe(2);
  335. expect((harness.routeNativeElement?.querySelector('.course-list button') as HTMLButtonElement).disabled).toBeFalse();
  336. expect((harness.routeNativeElement?.querySelectorAll('.course-list button')[1] as HTMLButtonElement).disabled).toBeFalse();
  337. expect(harness.routeNativeElement?.textContent).toContain('【七年级上册】同步词汇');
  338. expect(harness.routeNativeElement?.textContent).toContain('点击查看老师分配的单词列表');
  339. (harness.routeNativeElement?.querySelector('.course-list button') as HTMLButtonElement).click();
  340. await harness.fixture.whenStable();
  341. harness.fixture.detectChanges();
  342. expect(TestBed.inject(Router).url).toBe('/pages/stu/stu_list?nid=801&bindingId=701');
  343. expect(harness.routeNativeElement?.querySelectorAll('.node-list button').length).toBe(2);
  344. expect((TestBed.inject(ApiService) as unknown as LearningModulesApiService).lastNodeParams['bindingId']).toBe('701');
  345. await harness.navigateByUrl('/pages/home/cart');
  346. expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(0);
  347. expect(harness.routeNativeElement?.querySelectorAll('.calendar-day').length).toBe(42);
  348. const allDates = Array.from(harness.routeNativeElement?.querySelectorAll('.range-tabs button') ?? [])
  349. .find((button) => button.textContent?.trim() === '全部') as HTMLButtonElement;
  350. allDates.click();
  351. harness.fixture.detectChanges();
  352. expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(3);
  353. expect(harness.routeNativeElement?.textContent).toContain('同步伴学');
  354. expect(harness.routeNativeElement?.textContent).toContain('历史伴学');
  355. const plannedIncompleteFilter = Array.from(harness.routeNativeElement?.querySelectorAll('.schedule-filters button') ?? [])
  356. .find((button) => button.textContent?.includes('计划未完成')) as HTMLButtonElement;
  357. expect(plannedIncompleteFilter.textContent).toContain('1');
  358. plannedIncompleteFilter.click();
  359. harness.fixture.detectChanges();
  360. expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(1);
  361. expect(harness.routeNativeElement?.textContent).toContain('计划未完成');
  362. expect(harness.routeNativeElement?.textContent).not.toContain('未来计划');
  363. (harness.routeNativeElement?.querySelector('.schedule-filters button') as HTMLButtonElement).click();
  364. harness.fixture.detectChanges();
  365. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  366. expect(api.lastScheduleParams['mode']).toBe('student');
  367. expect(api.lastScheduleParams['limit']).toBe(500);
  368. const finishedFilter = Array.from(harness.routeNativeElement?.querySelectorAll('.schedule-filters button') ?? [])
  369. .find((button) => button.textContent?.includes('已结束')) as HTMLButtonElement;
  370. finishedFilter.click();
  371. harness.fixture.detectChanges();
  372. expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(1);
  373. expect(harness.routeNativeElement?.textContent).toContain('历史伴学');
  374. const allFilter = harness.routeNativeElement?.querySelector('.schedule-filters button') as HTMLButtonElement;
  375. allFilter.click();
  376. harness.fixture.detectChanges();
  377. const upcomingSchedule = Array.from(harness.routeNativeElement?.querySelectorAll('.schedule-list article') ?? [])
  378. .find((article) => article.textContent?.includes('同步伴学')) as HTMLElement;
  379. upcomingSchedule.click();
  380. await harness.fixture.whenStable();
  381. expect(TestBed.inject(Router).url).toBe('/pages/shop/stu_get_order?id=901');
  382. await harness.navigateByUrl('/pages/his/his');
  383. expect(harness.fixture.nativeElement.querySelector('.tabbar')).toBeNull();
  384. const aiReadingFeature = harness.routeNativeElement?.querySelector('.ai-reading-feature') as HTMLElement;
  385. expect(aiReadingFeature.textContent).toContain('AI 阅读巩固');
  386. expect(aiReadingFeature.textContent).toContain('生成阅读');
  387. expect(aiReadingFeature.textContent).toContain('AI 批改');
  388. expect(aiReadingFeature.querySelector('img')?.getAttribute('src')).toBe('/static/images/ai-reading-journey.png');
  389. const aiReadingCta = aiReadingFeature.querySelector('.ai-reading-cta') as HTMLButtonElement;
  390. expect(aiReadingCta.disabled).toBeFalse();
  391. expect(aiReadingFeature.textContent).toContain('最近学过的 1 个单词');
  392. expect(aiReadingCta.textContent).toContain('立即生成阅读');
  393. aiReadingCta.click();
  394. await harness.fixture.whenStable();
  395. const aiReadingRouter = TestBed.inject(Router);
  396. expect(aiReadingRouter.url).toContain('/words/go-reading');
  397. expect(aiReadingRouter.parseUrl(aiReadingRouter.url).queryParams['returnUrl']).toBe('/pages/his/his');
  398. expect(aiReadingRouter.parseUrl(aiReadingRouter.url).queryParams['GeneralID']).toBe('201');
  399. await harness.navigateByUrl('/pages/his/his');
  400. const currentLearningCard = harness.routeNativeElement?.querySelector('.current-learning-card') as HTMLElement;
  401. const learningRecordCard = harness.routeNativeElement?.querySelector('.learning-record-card') as HTMLElement;
  402. expect(currentLearningCard.textContent).toContain('我的学习内容');
  403. expect(currentLearningCard.querySelectorAll('.current-learning-list button').length).toBe(2);
  404. expect((currentLearningCard.querySelectorAll('.current-learning-list button')[1] as HTMLButtonElement).disabled).toBeFalse();
  405. expect(currentLearningCard.compareDocumentPosition(learningRecordCard) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
  406. (currentLearningCard.querySelector('.current-learning-list button') as HTMLButtonElement).click();
  407. await harness.fixture.whenStable();
  408. harness.fixture.detectChanges();
  409. expect(TestBed.inject(Router).url).toBe('/pages/stu/stu_list?nid=801&bindingId=701');
  410. expect(harness.routeNativeElement?.querySelectorAll('.node-list button').length).toBe(2);
  411. await harness.navigateByUrl('/pages/his/his');
  412. expect(harness.routeNativeElement?.textContent).toContain('初中英语 / Unit 1');
  413. expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(2);
  414. (harness.routeNativeElement?.querySelector('.learning-record-card .learning-item') as HTMLElement).click();
  415. await harness.fixture.whenStable();
  416. harness.fixture.detectChanges();
  417. expect(TestBed.inject(Router).url).toBe('/pages/his/his_words?id=201');
  418. expect(harness.routeNativeElement?.querySelector('.readonly-word-header')?.textContent).toContain('学生只读');
  419. expect(harness.routeNativeElement?.querySelector('.readonly-word-card')).not.toBeNull();
  420. expect(harness.routeNativeElement?.querySelector('.selection-header')).toBeNull();
  421. await harness.navigateByUrl('/pages/his/his');
  422. (harness.routeNativeElement?.querySelector('.date-trigger') as HTMLButtonElement).click();
  423. harness.fixture.detectChanges();
  424. const currentMonth = new Date();
  425. const monthDifference = (2026 - currentMonth.getFullYear()) * 12 + (7 - currentMonth.getMonth());
  426. const shiftLabel = monthDifference < 0 ? '上个月' : '下个月';
  427. for (let index = 0; index < Math.abs(monthDifference); index += 1) {
  428. (harness.routeNativeElement?.querySelector(`[aria-label="${shiftLabel}"]`) as HTMLButtonElement).click();
  429. harness.fixture.detectChanges();
  430. }
  431. expect(harness.routeNativeElement?.querySelector('.date-picker-popover header strong')?.textContent).toContain('2026年8月');
  432. expect(harness.routeNativeElement?.querySelectorAll('.date-picker-day').length).toBe(42);
  433. expect(harness.routeNativeElement?.querySelectorAll('.date-picker-day.has-record').length).toBe(2);
  434. expect(harness.routeNativeElement?.querySelector('.date-picker-day.has-record')?.getAttribute('aria-label')).toContain('2026-08-01');
  435. (harness.routeNativeElement?.querySelector('[aria-label^="2026-08-01"]') as HTMLButtonElement).click();
  436. harness.fixture.detectChanges();
  437. expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(1);
  438. expect(harness.routeNativeElement?.querySelector('.learning-record-card')?.textContent).not.toContain('高中英语 / Unit 2');
  439. (harness.routeNativeElement?.querySelector('.record-actions button') as HTMLButtonElement).click();
  440. harness.fixture.detectChanges();
  441. expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(2);
  442. (harness.routeNativeElement?.querySelector('.reading-button') as HTMLButtonElement).click();
  443. await harness.fixture.whenStable();
  444. const router = TestBed.inject(Router);
  445. expect(router.url).toContain('/words/go-reading');
  446. expect(router.parseUrl(router.url).queryParams['returnUrl']).toBe('/pages/his/his');
  447. expect(harness.routeNativeElement?.querySelector('.back-button')).not.toBeNull();
  448. (harness.routeNativeElement?.querySelector('.back-button') as HTMLButtonElement).click();
  449. await harness.fixture.whenStable();
  450. expect(router.url).toBe('/pages/his/his');
  451. (harness.fixture.nativeElement.querySelector('.app-header .icon-button') as HTMLButtonElement).click();
  452. await harness.fixture.whenStable();
  453. expect(router.url).toBe('/pages/home/index');
  454. await harness.navigateByUrl('/pages/his/exam');
  455. expect(harness.routeNativeElement?.textContent).toContain('第一组问题');
  456. expect(harness.routeNativeElement?.textContent).toContain('相当于初二水平');
  457. expect(harness.routeNativeElement?.textContent).toContain('共 1 题');
  458. const startButton = harness.routeNativeElement?.querySelector('.exam-intro > button') as HTMLButtonElement;
  459. startButton.click();
  460. harness.fixture.detectChanges();
  461. expect(harness.routeNativeElement?.querySelector('.question-word')?.textContent).toContain('apple');
  462. expect(harness.routeNativeElement?.querySelector('.question-card > h2')).toBeNull();
  463. expect(harness.routeNativeElement?.querySelectorAll('.exam-options button').length).toBe(1);
  464. (harness.routeNativeElement?.querySelector('.exam-options button') as HTMLButtonElement).click();
  465. harness.fixture.detectChanges();
  466. });
  467. it('keeps the complete point-reading flow available to coaches', async () => {
  468. user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
  469. session.groupId.set(3);
  470. const harness = await RouterTestingHarness.create();
  471. await harness.navigateByUrl('/pages/home/index');
  472. await harness.fixture.whenStable();
  473. harness.fixture.detectChanges();
  474. const coachLinks = Array.from(harness.routeNativeElement?.querySelectorAll('.coach-quick-grid > a') ?? []).map((link) => link.getAttribute('href'));
  475. expect(coachLinks).toEqual(['/pages/home/shop', '/pages/member/ke_xiao', '/pages/his/his', '/pages/member/ke_fuxi']);
  476. expect(harness.routeNativeElement?.querySelector('.home-header .header-action')?.getAttribute('href')).toBe('/pages/home/ucenter');
  477. expect(harness.routeNativeElement?.querySelector('.content-section')).toBeNull();
  478. const memoryApi = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  479. expect(memoryApi.lastMemoryParams).toEqual(jasmine.objectContaining({ uid: 300, shouru: 1 }));
  480. expect(harness.routeNativeElement?.querySelector('.coach-memory-board')?.textContent).toContain('抗遗忘带练');
  481. expect(harness.routeNativeElement?.querySelector('.coach-memory-board')?.textContent).toContain('张雅茜');
  482. expect(harness.routeNativeElement?.querySelector('.coach-memory-list article.ready > button')?.textContent).toContain('带学生复习');
  483. expect(harness.routeNativeElement?.querySelector('.coach-memory-footer')?.getAttribute('href')).toBe('/pages/stu/stu_calendar');
  484. (harness.routeNativeElement?.querySelector('.coach-memory-list article.ready > button') as HTMLButtonElement).click();
  485. await harness.fixture.whenStable();
  486. expect(TestBed.inject(Router).url).toContain('/pages/stu/record?id=900&gid=102');
  487. expect(session.student()?.userId).toBe(100);
  488. await harness.navigateByUrl('/pages/stu/stu_calendar');
  489. await harness.fixture.whenStable();
  490. harness.fixture.detectChanges();
  491. expect(memoryApi.lastMemoryParams).toEqual(jasmine.objectContaining({ uid: 300, shouru: 1, psize: 500 }));
  492. expect(harness.routeNativeElement?.querySelector('.memory-copy')?.textContent).toContain('关联学员抗遗忘任务');
  493. expect(harness.routeNativeElement?.querySelector('.memory-date-tasks')?.textContent).toContain('张雅茜');
  494. expect(harness.routeNativeElement?.querySelector('.memory-date-tasks button')?.textContent).toContain('带学生复习');
  495. await harness.navigateByUrl('/pages/member/ke_xiao');
  496. await harness.fixture.whenStable();
  497. harness.fixture.detectChanges();
  498. const monthFilter = harness.routeNativeElement?.querySelector('[aria-label="按月份筛选销课记录"]') as HTMLSelectElement;
  499. expect(monthFilter.value).toBe('2026-07');
  500. expect(monthFilter.options.length).toBe(4);
  501. expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(2);
  502. monthFilter.value = '2026-03';
  503. monthFilter.dispatchEvent(new Event('change'));
  504. const studentSearch = harness.routeNativeElement?.querySelector('[aria-label="按学生姓名筛选销课记录"]') as HTMLInputElement;
  505. studentSearch.value = '花';
  506. studentSearch.dispatchEvent(new Event('input'));
  507. harness.fixture.detectChanges();
  508. expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(1);
  509. expect(harness.routeNativeElement?.querySelector('.schedule-list')?.textContent).toContain('花花');
  510. expect(harness.routeNativeElement?.querySelector('.schedule-band > header')?.textContent).toContain('1 / 5');
  511. (harness.routeNativeElement?.querySelector('.history-filter-panel footer button') as HTMLButtonElement).click();
  512. harness.fixture.detectChanges();
  513. expect(harness.routeNativeElement?.querySelectorAll('.schedule-list article').length).toBe(5);
  514. await harness.navigateByUrl('/pages/his/his');
  515. await harness.fixture.whenStable();
  516. harness.fixture.detectChanges();
  517. expect((harness.routeNativeElement?.querySelector('.student-filter select') as HTMLSelectElement).selectedOptions[0]?.textContent).toContain('张雅茜');
  518. expect(harness.routeNativeElement?.querySelectorAll('.learning-record-card .learning-item').length).toBe(2);
  519. expect(session.student()?.userId).toBe(100);
  520. (harness.routeNativeElement?.querySelector('.learning-record-card .learning-item') as HTMLElement).click();
  521. await harness.fixture.whenStable();
  522. harness.fixture.detectChanges();
  523. expect(TestBed.inject(Router).url).toBe('/pages/his/his_words?id=201');
  524. expect(harness.routeNativeElement?.querySelector('.readonly-word-header')?.textContent).toContain('历史记录');
  525. expect(harness.routeNativeElement?.querySelector('.readonly-word-card')?.textContent).toContain('apple');
  526. expect(harness.routeNativeElement?.querySelector('.readonly-word-card')?.textContent).toContain('共 1 个单词');
  527. expect(harness.routeNativeElement?.querySelector('.selection-header')).toBeNull();
  528. await harness.navigateByUrl('/pages/his/his');
  529. await harness.fixture.whenStable();
  530. harness.fixture.detectChanges();
  531. const teacherCourse = harness.routeNativeElement?.querySelector('.current-learning-list button') as HTMLButtonElement;
  532. expect(teacherCourse.disabled).toBeFalse();
  533. expect(teacherCourse.classList.contains('readonly')).toBeFalse();
  534. expect(teacherCourse.textContent).toContain('选择本次学习单元');
  535. teacherCourse.click();
  536. await harness.fixture.whenStable();
  537. expect(TestBed.inject(Router).url).toContain('/pages/stu/stu_list');
  538. await harness.navigateByUrl('/pages/home/ucenter');
  539. await harness.fixture.whenStable();
  540. harness.fixture.detectChanges();
  541. const teacherServices = Array.from(harness.routeNativeElement?.querySelectorAll('.service-grid > a') ?? []).map((link) => link.getAttribute('href'));
  542. expect(teacherServices).toEqual(['/pages/home/shop', '/pages/member/ke_xiao', '/pages/his/his', '/pages/member/feedback']);
  543. expect(harness.routeNativeElement?.querySelector('.coach-wallet-strip')?.textContent).toContain('复习收入');
  544. await harness.navigateByUrl('/pages/stu/stu_list?nid=801&bindingId=701');
  545. await harness.fixture.whenStable();
  546. harness.fixture.detectChanges();
  547. expect((TestBed.inject(ApiService) as unknown as LearningModulesApiService).lastNodeParams['userId']).toBe(100);
  548. const units = harness.routeNativeElement?.querySelectorAll('.node-list button') as NodeListOf<HTMLButtonElement>;
  549. units[0].click();
  550. units[1].click();
  551. harness.fixture.detectChanges();
  552. expect(harness.routeNativeElement?.querySelector('.unit-selection-summary')?.textContent).toContain('已选 2 个单元');
  553. (harness.routeNativeElement?.querySelector('.unit-actions .primary-button') as HTMLButtonElement).click();
  554. await harness.fixture.whenStable();
  555. harness.fixture.detectChanges();
  556. const router = TestBed.inject(Router);
  557. expect(router.url).toContain('/pages/stu/stu_abc');
  558. expect(harness.routeNativeElement?.querySelector('.pending-column')?.textContent).toContain('banana');
  559. expect(harness.routeNativeElement?.querySelector('.completed-column')).toBeNull();
  560. expect(harness.routeNativeElement?.querySelectorAll('.pending-column .word-refine-item').length).toBe(1);
  561. (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
  562. harness.fixture.detectChanges();
  563. expect(harness.routeNativeElement?.querySelector('.completed-column')?.textContent).toContain('apple');
  564. expect(harness.routeNativeElement?.querySelector('.pending-column')).toBeNull();
  565. (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[0] as HTMLButtonElement).click();
  566. harness.fixture.detectChanges();
  567. const firstWord = harness.routeNativeElement?.querySelector('.pending-column .word-refine-item') as HTMLElement;
  568. (firstWord.querySelector('.word-reveal') as HTMLButtonElement).click();
  569. harness.fixture.detectChanges();
  570. (firstWord.querySelector('.known-button') as HTMLButtonElement).click();
  571. harness.fixture.detectChanges();
  572. expect(harness.routeNativeElement?.querySelectorAll('.pending-column .word-refine-item').length).toBe(0);
  573. expect(harness.routeNativeElement?.querySelector('.selection-header .primary-button')?.textContent).toContain('保存识词结果');
  574. (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
  575. harness.fixture.detectChanges();
  576. expect(harness.routeNativeElement?.querySelectorAll('.completed-column .word-refine-item').length).toBe(2);
  577. (harness.routeNativeElement?.querySelector('.completed-word-action button') as HTMLButtonElement).click();
  578. harness.fixture.detectChanges();
  579. expect(harness.routeNativeElement?.querySelectorAll('.completed-column .word-refine-item').length).toBe(1);
  580. (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[0] as HTMLButtonElement).click();
  581. harness.fixture.detectChanges();
  582. expect(harness.routeNativeElement?.querySelectorAll('.pending-column .word-refine-item').length).toBe(1);
  583. (harness.routeNativeElement?.querySelector('.selection-header .primary-button') as HTMLButtonElement).click();
  584. await harness.fixture.whenStable();
  585. harness.fixture.detectChanges();
  586. expect(router.url).toContain('/pages/stu/stu_con');
  587. expect(harness.routeNativeElement?.querySelectorAll('.repeat-list > button').length).toBe(1);
  588. expect(harness.routeNativeElement?.querySelector('.readonly-word-header')).toBeNull();
  589. });
  590. it('counts words marked as known as completed and persists them for the student', async () => {
  591. user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
  592. session.groupId.set(3);
  593. session.student.set({ userId: 100, userName: 'student', honeyName: '张雅茜', groupId: 1 });
  594. const harness = await RouterTestingHarness.create();
  595. await harness.navigateByUrl('/pages/stu/stu_abc?nids=811,812&pnid=801');
  596. await harness.fixture.whenStable();
  597. harness.fixture.detectChanges();
  598. const pendingWord = harness.routeNativeElement?.querySelector('.pending-column .word-refine-item') as HTMLElement;
  599. (pendingWord.querySelector('.word-reveal') as HTMLButtonElement).click();
  600. harness.fixture.detectChanges();
  601. (pendingWord.querySelector('.known-button') as HTMLButtonElement).click();
  602. harness.fixture.detectChanges();
  603. (harness.routeNativeElement?.querySelector('.selection-header .primary-button') as HTMLButtonElement).click();
  604. await harness.fixture.whenStable();
  605. harness.fixture.detectChanges();
  606. const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
  607. expect(api.lastLearningWordUpdate).toEqual(jasmine.objectContaining({ uid: 100, wordsId: '1002', kcid: '801', save: 1 }));
  608. expect(TestBed.inject(Router).url).toContain('/pages/stu/stu_abc');
  609. expect(harness.routeNativeElement?.querySelector('.completed-column')).toBeNull();
  610. (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
  611. harness.fixture.detectChanges();
  612. expect(harness.routeNativeElement?.querySelectorAll('.completed-column .word-refine-item').length).toBe(2);
  613. expect(harness.routeNativeElement?.querySelector('.pending-column')).toBeNull();
  614. });
  615. });