review-student-names.test.mjs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import {test} from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import {readFileSync} from 'node:fs';
  4. import vm from 'node:vm';
  5. import {PGlite} from '@electric-sql/pglite';
  6. const source=readFileSync(new URL('../cloud/mobile-teaching.js',import.meta.url),'utf8');
  7. test('仅本人主动刷新今日第一页时核对旧系统,失败仍返回已同步的任务', async () => {
  8. const db = new PGlite();
  9. try {
  10. await db.exec(readFileSync(new URL('./fixtures/teacher-review-schema.sql',import.meta.url),'utf8'));
  11. const query = async (sql, args) => (await db.query(sql, args)).rows;
  12. const pulls = [];
  13. const coach = { get: () => 3 };
  14. const ctx = vm.createContext({
  15. DEFAULT_COMPANY_ID: 'co', number: (v,f=0) => Number(v)||f, authorizeRequestedUser: async (_current,uid) => uid||30,
  16. ownLegacyId: () => 30, assertLessonTenant: current => assert.equal(current,coach),
  17. appOverviewToday: () => '2026-10-14', legacyAliases: row => row, appLiveVersionSnapshot: async () => ({}),
  18. syncLegacyCoachTodayReviews: async (...args) => { pulls.push(args); return {state:'unavailable'}; },
  19. Psql: {query,one:async (sql,args)=>(await query(sql,args))[0]},
  20. });
  21. vm.runInContext(source,ctx);
  22. const read = overrides => ctx.appReviewPage({scope:'today',includeIncome:false,includeMetadata:false,...overrides},coach);
  23. await read({});
  24. await read({refresh:true,page:2});
  25. await read({refresh:true,scope:'overdue'});
  26. await read({refresh:true,uid:31});
  27. assert.equal(pulls.length,0);
  28. const refreshed = await read({refresh:true,date:'2000-01-01'});
  29. assert.deepEqual(pulls,[[30,coach,'2026-10-14']]);
  30. assert.equal(refreshed.legacyReviewSync.state,'unavailable');
  31. assert.equal(refreshed.items.length,2);
  32. } finally {
  33. await db.close();
  34. }
  35. });
  36. test('历史复习首次学习时间来自原始学习记录,不显示九月一日迁移时间',async()=>{
  37. const db=new PGlite();try{
  38. await db.exec(readFileSync(new URL('./fixtures/teacher-review-schema.sql',import.meta.url),'utf8'));
  39. await db.exec(`INSERT INTO "CommonModel"(company,"modelId","generalId","itemId","createTime") VALUES('co',56,100,101,'2026-07-31T01:20:00Z');
  40. INSERT INTO "DailyStudyRecord"(company,id,learned,xxqs,dqrq,"createdAt") VALUES('co',101,1,'[]','20260731','2026-09-01T02:25:17Z');`);
  41. const query=async(s,a)=>(await db.query(s,a)).rows;
  42. const ctx=vm.createContext({Date,Promise,DEFAULT_COMPANY_ID:'co',number:(v,f=0)=>Number(v)||f,authorizeRequestedUser:async()=>30,appOverviewToday:()=> '2026-10-14',legacyAliases:r=>r,appLiveVersionSnapshot:async()=>({}),Psql:{query,one:async(s,a)=>(await query(s,a))[0]}});vm.runInContext(source,ctx);
  43. const page=()=>ctx.appReviewPage({date:'2026-10-14',includeIncome:false},{get:()=>3});
  44. const result=await page();assert.equal(result.items.length,2);
  45. assert.ok(result.items.every(row=>new Date(row.learned_at).toISOString()==='2026-07-31T01:20:00.000Z'));
  46. await db.exec('UPDATE "CommonModel" SET "createTime"=NULL WHERE "modelId"=56');
  47. const dateOnly=await page();assert.ok(dateOnly.items.every(row=>row.learned_at==null&&row.learned_date==='20260731'));
  48. }finally{await db.close();}
  49. });
  50. test('复习姓名跳过空白,保留真实姓名、昵称、旧姓名及历史任务姓名,无法确认时显示学员编号',async()=>{
  51. const expression=vm.runInNewContext(source+';reviewStudentNameSql()'),db=new PGlite();
  52. try{
  53. for(const [real,nick,legacy,title,login,expected] of [
  54. ['可爱同学','昵称','{}','历史名@21天抗遗忘','login','可爱同学'],
  55. ['', ' 小林 ', '{}','历史名@21天抗遗忘','login','小林'],
  56. [' ','','{"HoneyName":" 小陈 "}','历史名@21天抗遗忘','login','小陈'],
  57. [null,null,'{}','谢星星@21天抗遗忘@2024-09-17',null,'谢星星'],
  58. ['', '', '{}','课程名称','student-login','student-login'],
  59. ['', '', '{}','课程名称',null,'学员 213'],
  60. [null,null,'{}','@21天抗遗忘',null,'学员 213']]){
  61. const r=await db.query(`SELECT ${expression} AS name FROM (SELECT $1::text "realName",$2::text nickname,$3::jsonb "legacyUserData",$5::text username) u CROSS JOIN (SELECT $4::text title) c CROSS JOIN (SELECT 213 AS yhid) m`,[real,nick,legacy,title,login]);assert.equal(r.rows[0].name,expected);
  62. }
  63. }finally{await db.close();}
  64. });
  65. test('老师复习列表待复习、已复习及翻页均返回姓名,保持工资与老师范围',async()=>{
  66. const db=new PGlite();try{
  67. await db.exec(readFileSync(new URL('./fixtures/teacher-review-schema.sql',import.meta.url),'utf8'));
  68. await db.exec(`INSERT INTO "_User" VALUES('co',20,'','', 'login','{"HoneyName":"小陈"}');
  69. UPDATE "CommonModel" SET title='历史学员@21天抗遗忘';
  70. INSERT INTO "ReviewRewardIncome"(company,"taskId",amount,status) VALUES('co','r2',0.5,'earned');`);
  71. const query=async(s,a)=>(await db.query(s,a)).rows;
  72. const ctx=vm.createContext({Date,Promise,DEFAULT_COMPANY_ID:'co',number:(v,f=0)=>Number(v)||f,authorizeRequestedUser:async()=>30,appOverviewToday:()=> '2026-10-14',legacyAliases:r=>r,appLiveVersionSnapshot:async()=>({}),reviewIncomeTotal:async()=>0.5,Psql:{query,one:async(s,a)=>(await query(s,a))[0]}});vm.runInContext(source,ctx);
  73. for(const [status,page] of [['pending',1],['pending',2],['completed',1]]){
  74. const result=await ctx.appReviewPage({status,page,pageSize:10},{get:()=>3});assert.ok(result.items.length);assert.ok(result.items.every(r=>r.honeyName==='小陈'&&Number(r.plid)===30));assert.equal(result.summary.income,status==='completed'?0.5:0);
  75. if(status==='completed'){assert.equal(result.items[0].money,0.5);assert.equal(result.items[0].rewardStatus,'earned');}
  76. }
  77. const monthly=await ctx.appReviewPage({status:'completed',month:'2026-10',pageSize:1},{get:()=>3});
  78. assert.equal(monthly.summary.income,0.5);
  79. const emptyMonth=await ctx.appReviewPage({status:'completed',month:'2026-09'},{get:()=>3});
  80. assert.equal(emptyMonth.summary.income,0);assert.equal(emptyMonth.page.itemCount,0);
  81. await db.exec('DELETE FROM "_User"');
  82. const historical=await ctx.appReviewPage({status:'completed'},{get:()=>3});assert.equal(historical.items[0].honeyName,'历史学员');
  83. }finally{await db.close();}
  84. });
  85. test('无单词语法写作任务进入日历、今日统计和学员筛选,普通空记录仍隐藏',async()=>{
  86. const db=new PGlite();try{
  87. await db.exec(readFileSync(new URL('./fixtures/teacher-review-schema.sql',import.meta.url),'utf8'));
  88. await db.exec(`INSERT INTO "DailyStudyRecord"(company,id,learned,xxqs,dsid,"userId") VALUES('co',101,0,'[]','200',20);
  89. INSERT INTO "CommonModel"(company,"modelId","generalId","itemId",status) VALUES('co',56,100,101,99),('co',54,200,201,99);
  90. INSERT INTO "CourseAppointment"(company,id,szyh,dszt,"courseCategoryKey") VALUES('co',201,'20','30','high_writing');`);
  91. const query=async(s,a)=>(await db.query(s,a)).rows;
  92. const ctx=vm.createContext({Date,Promise,DEFAULT_COMPANY_ID:'co',number:(v,f=0)=>Number(v)||f,authorizeRequestedUser:async()=>30,appOverviewToday:()=> '2026-10-14',legacyAliases:r=>r,appLiveVersionSnapshot:async()=>({}),Psql:{query,one:async(s,a)=>(await query(s,a))[0]}});vm.runInContext(source,ctx);
  93. const page=()=>ctx.appReviewPage({scope:'today',includeIncome:false},{get:()=>3});
  94. const notes=await page();assert.equal(notes.items.length,2);assert.equal(notes.summary.todayTotal,2);assert.equal(notes.summary.todayPending,1);assert.equal(notes.summary.todayCompleted,1);
  95. assert.equal(notes.students.length,1);assert.equal(notes.students[0].id,'20');assert.ok(notes.dates.length>0);
  96. assert.ok(notes.items.every(r=>r.kc_title==='语法写作复习'));
  97. for(const update of [`UPDATE "CourseAppointment" SET "courseCategoryKey"='word'`,`UPDATE "CourseAppointment" SET "courseCategoryKey"='high_writing',szyh='21'`,`UPDATE "CourseAppointment" SET szyh='20',dszt='10'`]){
  98. await db.exec(update);const hidden=await page();assert.equal(hidden.items.length,0);assert.equal(hidden.summary.todayTotal,0);assert.equal(hidden.students.length,0);
  99. }
  100. }finally{await db.close();}
  101. });