Add lessons Phase 5: grading scales with nested levels
Custom grading scales with ordered levels (value, label, numeric score, color). Supports one-default-per-store constraint, deep create with nested levels, lookup endpoint for dropdowns, and search/pagination. 12 new tests (76 total lessons tests).
This commit is contained in:
@@ -939,4 +939,133 @@ suite('Lessons', { tags: ['lessons'] }, (t) => {
|
||||
t.assert.status(res, 200)
|
||||
t.assert.ok(res.data.data.every((s: any) => s.status === 'scheduled'))
|
||||
})
|
||||
|
||||
// ─── Grading Scales: CRUD ───
|
||||
|
||||
t.test('creates a grading scale with nested levels', { tags: ['grading-scales', 'create'] }, async () => {
|
||||
const res = await t.api.post('/v1/grading-scales', {
|
||||
name: 'Standard Letter',
|
||||
description: 'Traditional letter grades',
|
||||
isDefault: true,
|
||||
levels: [
|
||||
{ value: 'A+', label: 'Excellent Plus', numericValue: 97, colorHex: '#4CAF50', sortOrder: 1 },
|
||||
{ value: 'A', label: 'Excellent', numericValue: 93, colorHex: '#4CAF50', sortOrder: 2 },
|
||||
{ value: 'A-', label: 'Excellent Minus', numericValue: 90, colorHex: '#8BC34A', sortOrder: 3 },
|
||||
{ value: 'B+', label: 'Good Plus', numericValue: 87, colorHex: '#CDDC39', sortOrder: 4 },
|
||||
{ value: 'B', label: 'Good', numericValue: 83, colorHex: '#CDDC39', sortOrder: 5 },
|
||||
{ value: 'F', label: 'Fail', numericValue: 0, colorHex: '#F44336', sortOrder: 10 },
|
||||
],
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
t.assert.equal(res.data.name, 'Standard Letter')
|
||||
t.assert.equal(res.data.isDefault, true)
|
||||
t.assert.equal(res.data.levels.length, 6)
|
||||
t.assert.equal(res.data.levels[0].value, 'A+')
|
||||
t.assert.equal(res.data.levels[0].numericValue, 97)
|
||||
})
|
||||
|
||||
t.test('creates a progress scale', { tags: ['grading-scales', 'create'] }, async () => {
|
||||
const res = await t.api.post('/v1/grading-scales', {
|
||||
name: 'Progress',
|
||||
levels: [
|
||||
{ value: 'Mastered', label: 'Skill fully mastered', numericValue: 100, colorHex: '#4CAF50', sortOrder: 1 },
|
||||
{ value: 'Proficient', label: 'Near mastery', numericValue: 75, colorHex: '#8BC34A', sortOrder: 2 },
|
||||
{ value: 'Developing', label: 'Making progress', numericValue: 50, colorHex: '#FFC107', sortOrder: 3 },
|
||||
{ value: 'Beginning', label: 'Just started', numericValue: 25, colorHex: '#FF9800', sortOrder: 4 },
|
||||
],
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
t.assert.equal(res.data.levels.length, 4)
|
||||
})
|
||||
|
||||
t.test('rejects scale without levels', { tags: ['grading-scales', 'create', 'validation'] }, async () => {
|
||||
const res = await t.api.post('/v1/grading-scales', { name: 'Empty Scale', levels: [] })
|
||||
t.assert.status(res, 400)
|
||||
})
|
||||
|
||||
t.test('rejects scale without name', { tags: ['grading-scales', 'create', 'validation'] }, async () => {
|
||||
const res = await t.api.post('/v1/grading-scales', { levels: [{ value: 'A', label: 'A', numericValue: 90, sortOrder: 1 }] })
|
||||
t.assert.status(res, 400)
|
||||
})
|
||||
|
||||
t.test('gets grading scale with levels', { tags: ['grading-scales', 'read'] }, async () => {
|
||||
const created = await t.api.post('/v1/grading-scales', {
|
||||
name: 'Get Test Scale',
|
||||
levels: [
|
||||
{ value: 'Pass', label: 'Passed', numericValue: 70, sortOrder: 1 },
|
||||
{ value: 'Fail', label: 'Failed', numericValue: 0, sortOrder: 2 },
|
||||
],
|
||||
})
|
||||
|
||||
const res = await t.api.get(`/v1/grading-scales/${created.data.id}`)
|
||||
t.assert.status(res, 200)
|
||||
t.assert.equal(res.data.name, 'Get Test Scale')
|
||||
t.assert.equal(res.data.levels.length, 2)
|
||||
t.assert.equal(res.data.levels[0].value, 'Pass')
|
||||
})
|
||||
|
||||
t.test('returns 404 for missing grading scale', { tags: ['grading-scales', 'read'] }, async () => {
|
||||
const res = await t.api.get('/v1/grading-scales/a0000000-0000-0000-0000-999999999999')
|
||||
t.assert.status(res, 404)
|
||||
})
|
||||
|
||||
t.test('lists grading scales with pagination', { tags: ['grading-scales', 'read', 'pagination'] }, async () => {
|
||||
const res = await t.api.get('/v1/grading-scales', { limit: 100 })
|
||||
t.assert.status(res, 200)
|
||||
t.assert.ok(res.data.data.length >= 2)
|
||||
t.assert.ok(res.data.pagination)
|
||||
})
|
||||
|
||||
t.test('searches grading scales by name', { tags: ['grading-scales', 'search'] }, async () => {
|
||||
const res = await t.api.get('/v1/grading-scales', { q: 'Progress' })
|
||||
t.assert.status(res, 200)
|
||||
t.assert.ok(res.data.data.some((s: any) => s.name === 'Progress'))
|
||||
})
|
||||
|
||||
t.test('lists all scales with levels (lookup endpoint)', { tags: ['grading-scales', 'read'] }, async () => {
|
||||
const res = await t.api.get('/v1/grading-scales/all')
|
||||
t.assert.status(res, 200)
|
||||
t.assert.ok(Array.isArray(res.data))
|
||||
t.assert.ok(res.data.length >= 2)
|
||||
t.assert.ok(res.data[0].levels, 'each scale should include levels')
|
||||
})
|
||||
|
||||
t.test('updates a grading scale', { tags: ['grading-scales', 'update'] }, async () => {
|
||||
const created = await t.api.post('/v1/grading-scales', {
|
||||
name: 'Before Update Scale',
|
||||
levels: [{ value: 'A', label: 'Grade A', numericValue: 90, sortOrder: 1 }],
|
||||
})
|
||||
const res = await t.api.patch(`/v1/grading-scales/${created.data.id}`, { name: 'After Update Scale' })
|
||||
t.assert.status(res, 200)
|
||||
t.assert.equal(res.data.name, 'After Update Scale')
|
||||
})
|
||||
|
||||
t.test('setting new default unsets previous default', { tags: ['grading-scales', 'update'] }, async () => {
|
||||
// First scale was created with isDefault: true
|
||||
const newDefault = await t.api.post('/v1/grading-scales', {
|
||||
name: 'New Default Scale',
|
||||
isDefault: true,
|
||||
levels: [{ value: 'OK', label: 'Okay', numericValue: 70, sortOrder: 1 }],
|
||||
})
|
||||
t.assert.equal(newDefault.data.isDefault, true)
|
||||
|
||||
// Check all scales - only one should be default
|
||||
const allScales = await t.api.get('/v1/grading-scales/all')
|
||||
const defaults = allScales.data.filter((s: any) => s.isDefault === true)
|
||||
t.assert.equal(defaults.length, 1, 'only one scale should be default')
|
||||
t.assert.equal(defaults[0].id, newDefault.data.id)
|
||||
})
|
||||
|
||||
t.test('soft-deletes a grading scale', { tags: ['grading-scales', 'delete'] }, async () => {
|
||||
const created = await t.api.post('/v1/grading-scales', {
|
||||
name: 'To Delete Scale',
|
||||
levels: [{ value: 'X', label: 'Delete Me', numericValue: 0, sortOrder: 1 }],
|
||||
})
|
||||
const res = await t.api.del(`/v1/grading-scales/${created.data.id}`)
|
||||
t.assert.status(res, 200)
|
||||
t.assert.equal(res.data.isActive, false)
|
||||
|
||||
const list = await t.api.get('/v1/grading-scales', { q: 'To Delete Scale', limit: 100 })
|
||||
t.assert.equal(list.data.data.length, 0)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user