feat(trainer): P6 — учительская аналитика класса + общий прогресс
- GET /api/practice/class-stats (classStats): агрегаты по навыкам + матрица ученик×навык; доступ владелец класса/админ - клиент: кнопка «Аналитика класса» (учителю) → модалка с тепловой картой (точность/освоено) + пикер классов; LS.practiceClassStats - лёгкая геймификация: строка «Освоено навыков M из N · решено всего K» из агрегатов practice_progress - тесты practice.test.js +4 (владелец видит; чужой/ученик → 403; без class_id → 400); смоук страницы 27/27; план P6 → DONE Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
const { describe, it, before } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { app, inject, getToken, cleanup } = require('./setup');
|
||||
const { app, db, inject, getToken, cleanup } = require('./setup');
|
||||
|
||||
// Mount /api/practice on the shared test app (setup.js не монтирует новые роуты).
|
||||
app.use('/api/practice', require('../src/routes/practice'));
|
||||
@@ -114,3 +114,49 @@ describe('/api/practice progress', () => {
|
||||
assert.equal(res.status, 400, `got ${res.status}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/api/practice/class-stats (аналитика класса)', () => {
|
||||
let teacher, other, s1, s2, classId;
|
||||
|
||||
before(async () => {
|
||||
teacher = await getToken('teacher');
|
||||
other = await getToken('teacher');
|
||||
s1 = await getToken('student');
|
||||
s2 = await getToken('student');
|
||||
// класс учителя + два ученика в нём
|
||||
const info = db.prepare("INSERT INTO classes (name, teacher_id, invite_code) VALUES ('P6 класс', ?, ?)").run(teacher.userId, 'P6CODE');
|
||||
classId = info.lastInsertRowid;
|
||||
db.prepare('INSERT INTO class_members (class_id, user_id) VALUES (?, ?)').run(classId, s1.userId);
|
||||
db.prepare('INSERT INTO class_members (class_id, user_id) VALUES (?, ?)').run(classId, s2.userId);
|
||||
// прогресс: s1 решил lin-basic верно, s2 ошибся на lin-basic
|
||||
await inject('POST', '/api/practice/attempt', { skill: 'lin-basic', correct: true }, s1.token);
|
||||
await inject('POST', '/api/practice/attempt', { skill: 'lin-basic', correct: false }, s2.token);
|
||||
await inject('POST', '/api/practice/attempt', { skill: 'lin-paren', correct: true }, s1.token);
|
||||
});
|
||||
|
||||
it('владелец класса видит агрегаты и матрицу', async () => {
|
||||
const res = await inject('GET', `/api/practice/class-stats?class_id=${classId}`, null, teacher.token);
|
||||
assert.equal(res.status, 200, `got ${res.status}`);
|
||||
assert.equal(res.body.students.length, 2, 'два ученика');
|
||||
assert.ok(res.body.skills.includes('lin-basic'), 'навык в списке');
|
||||
const lb = res.body.perSkill.find(s => s.skill === 'lin-basic');
|
||||
assert.ok(lb, 'агрегат по lin-basic есть');
|
||||
assert.equal(lb.attempted, 2, 'оба пробовали lin-basic');
|
||||
assert.equal(lb.accuracy, 50, '1 верный из 2 попыток → 50%');
|
||||
});
|
||||
|
||||
it('чужой класс → 403', async () => {
|
||||
const res = await inject('GET', `/api/practice/class-stats?class_id=${classId}`, null, other.token);
|
||||
assert.equal(res.status, 403, `got ${res.status}`);
|
||||
});
|
||||
|
||||
it('ученику запрещено (требуется роль) → 403', async () => {
|
||||
const res = await inject('GET', `/api/practice/class-stats?class_id=${classId}`, null, s1.token);
|
||||
assert.equal(res.status, 403, `got ${res.status}`);
|
||||
});
|
||||
|
||||
it('без class_id → 400', async () => {
|
||||
const res = await inject('GET', '/api/practice/class-stats', null, teacher.token);
|
||||
assert.equal(res.status, 400, `got ${res.status}`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user