diff --git a/services/backend/tests/api/courses/courseYearlyStats/attemptStatistics/mat11001.test.ts b/services/backend/tests/api/courses/courseYearlyStats/attemptStatistics/mat11001.test.ts new file mode 100644 index 000000000..46ff09817 --- /dev/null +++ b/services/backend/tests/api/courses/courseYearlyStats/attemptStatistics/mat11001.test.ts @@ -0,0 +1,22 @@ +import { Express } from 'express' +import { describe, it, beforeAll, assert } from 'vitest' + +import { initTests } from '../../../../utils' +import { getCourseYearlyStats, CourseYearlyStats } from '../helpers' + +void describe('Course yearly attempt statistics - MAT11001 (no substitutions)', () => { + let app: Express + let body: CourseYearlyStats + beforeAll(async () => { + app = await initTests() + body = await getCourseYearlyStats(app, 'codes=MAT11001&combineSubstitutions=false') + }) + + it('should include every attempt of a student who has only failed the course', () => { + const stats = body.unifyStats?.statistics.find(year => year.name === '2018-2019') + assert(stats && 'enrollments' in stats, 'Missing field enrollment in statsitics') + + assert.strictEqual(stats.attempts.categories.failed.length, 1, "Didn't include student's failed attempt") + assert.deepStrictEqual(stats.attempts.categories.failed, ['542874'], "Didn't include student's failed attempt") + }) +}) diff --git a/services/backend/tests/api/courses/courseYearlyStats/attemptStatistics/mat11002.test.ts b/services/backend/tests/api/courses/courseYearlyStats/attemptStatistics/mat11002.test.ts new file mode 100644 index 000000000..b97d01c5a --- /dev/null +++ b/services/backend/tests/api/courses/courseYearlyStats/attemptStatistics/mat11002.test.ts @@ -0,0 +1,44 @@ +import { Express } from 'express' +import { describe, it, beforeAll, assert } from 'vitest' + +import { initTests } from '../../../../utils' +import { getCourseYearlyStats, CourseYearlyStats } from '../helpers' + +void describe('Course yearly attempt statistics - MAT11002 (no substitutions)', () => { + let app: Express + let body: CourseYearlyStats + beforeAll(async () => { + app = await initTests() + body = await getCourseYearlyStats(app, 'codes=MAT11002&combineSubstitutions=false') + }) + + // FIXME: What is this even testing?? + it.skip('should not include attempts for student with only AY credit (501716)', () => { + const stats = body.unifyStats?.statistics.find(year => year.name === '2017-2018') + assert(stats && 'enrollments' in stats, 'Missing field enrollment in statsitics') + assert.strictEqual(stats.attempts.categories.failed.length, 1) + assert.deepStrictEqual(stats.attempts.categories.failed, ['501716']) + }) + + it('should include a failed and a passed attempt for a student with both (501716)', () => { + const year = body.unifyStats?.statistics.find(year => year.name === '2018-2019') + assert(year && 'enrollments' in year, 'Stats missing completely') + assert.include(year.attempts.categories.failed, '501716', 'Failed attempts missing the student in question') + assert.include(year.attempts.categories.passed, '501716', "Passed attempts didn't include the passed attempt") + assert.include(year.attempts.grades['1'] ?? [], '501716', 'Grades should include the attempt in the correct grade') + }) + + it('should include an attempt with only approved grade (543385)', () => { + const year = body.unifyStats?.statistics.find(year => year.name === '2022-2023') + assert(year && 'enrollments' in year, 'Stats missing completely') + assert.notInclude(year.attempts.categories.failed, '543385') + assert.include(year.attempts.categories.passed, '543385', "Passed attempts didn't include the approved attempt") + assert.include(year.attempts.grades['5'] ?? [], '543385', 'Grades should include the attempt in the correct grade') + }) + + it('should include an attempt with only improved grade (509770)', () => { + const year = body.unifyStats?.statistics.find(year => year.name === '2018-2019') + assert(year && 'enrollments' in year, 'Missing stats for 2018-2019') + assert.include(year.attempts.categories.passed, '509770', 'Passed attempts should include in passed attempts') + }) +}) diff --git a/services/backend/tests/api/courses/courseYearlyStats/attemptStatistics/mat21003.test.ts b/services/backend/tests/api/courses/courseYearlyStats/attemptStatistics/mat21003.test.ts new file mode 100644 index 000000000..54f3f1354 --- /dev/null +++ b/services/backend/tests/api/courses/courseYearlyStats/attemptStatistics/mat21003.test.ts @@ -0,0 +1,85 @@ +import { Express } from 'express' +import { describe, it, beforeAll, assert } from 'vitest' + +import { yearToYearCode } from '@oodikone/shared/util' +import { initTests } from '../../../../utils' +import { getCourseYearlyStats, CourseYearlyStats } from '../helpers' + +void describe('Course yearly attempt statistics - MAT21003 (no substitutions)', () => { + let app: Express + beforeAll(async () => { + app = await initTests() + }) + + describe('duplicate failed attempts across a large timespan are counted separately', () => { + let body: CourseYearlyStats + beforeAll(async () => { + body = await getCourseYearlyStats(app, 'codes=MAT21003&combineSubstitutions=false') + }) + + it('2017-2018 should include a single failed attempt (539036)', () => { + const year = body.unifyStats?.statistics.find(year => year.name === '2017-2018') + assert(year && 'enrollments' in year, 'Missing stats for 2017-2018') + assert.deepStrictEqual( + year.attempts.categories.failed, + ['539036'], + 'Failed attempts should not include any students' + ) + }) + + it('2018-2019 should include a failed attempt (539036)', () => { + const year = body.unifyStats?.statistics.find(year => year.name === '2018-2019') + assert(year && 'enrollments' in year, 'Missing stats for 2018-2019') + assert.include(year.attempts.categories.failed, '539036', 'Failed attempts should include the failed attempt') + }) + }) + + describe('year selection for smaller timeframes', () => { + it.each([ + ['2017-2018', 2017, 2017], + ['2020-2023', 2020, 2022], + ['2016-2024', 2016, 2023], + ])('should include correct years for %s', async (name, from, to) => { + const body = await getCourseYearlyStats( + app, + `codes=MAT21003&combineSubstitutions=false&fromYearCode=${yearToYearCode(from)}&toYearCode=${yearToYearCode(to)}` + ) + + const years: string[] = [] + for (let i = from; i <= to; i++) { + years.push(`${i}-${i + 1}`) + } + assert.deepStrictEqual( + body.unifyStats!.statistics.map(({ name }) => name), + years, + `${name} included incorrect years` + ) + }) + }) + + describe('duplicate attempts are counted again once an attempt falls out of the timespan', () => { + it('2017-2018 should include a failed attempt (539036)', async () => { + const body = await getCourseYearlyStats( + app, + `codes=MAT21003&combineSubstitutions=false&fromYearCode=${yearToYearCode(2017)}&toYearCode=${yearToYearCode(2017)}` + ) + const year = body.unifyStats!.statistics.find(year => year.name === '2017-2018') + assert(year && 'enrollments' in year, 'Missing stats for 2017-2018') + assert.deepStrictEqual( + year.attempts.categories.failed, + ['539036'], + 'Failed attempts should have included a student' + ) + }) + + it('2018-2019 should include a failed attempt (539036)', async () => { + const body = await getCourseYearlyStats( + app, + `codes=MAT21003&combineSubstitutions=false&fromYearCode=${yearToYearCode(2018)}&toYearCode=${yearToYearCode(2018)}` + ) + const year = body.unifyStats?.statistics.find(year => year.name === '2018-2019') + assert(year && 'enrollments' in year, 'Missing stats for 2018-2019') + assert.include(year.attempts.categories.failed, '539036', 'Failed attempts included the incorrect students') + }) + }) +}) diff --git a/services/backend/tests/api/courses/courseYearlyStats/attemptStatistics/tkt10002.test.ts b/services/backend/tests/api/courses/courseYearlyStats/attemptStatistics/tkt10002.test.ts new file mode 100644 index 000000000..828749bca --- /dev/null +++ b/services/backend/tests/api/courses/courseYearlyStats/attemptStatistics/tkt10002.test.ts @@ -0,0 +1,149 @@ +import { Express } from 'express' +import { describe, it, beforeAll, assert } from 'vitest' + +import { yearToYearCode } from '@oodikone/shared/util' +import { initTests } from '../../../../utils' +import { getCourseYearlyStats, CourseYearlyStats } from '../helpers' + +void describe('Course yearly attempt statistics - TKT10002 (no substitutions)', () => { + let app: Express + beforeAll(async () => { + app = await initTests() + }) + + describe('full range (2016-2023)', () => { + let body: CourseYearlyStats + beforeAll(async () => { + body = await getCourseYearlyStats( + app, + `codes=TKT10002&combineSubstitutions=false&fromYearCode=${yearToYearCode(2016)}&toYearCode=${yearToYearCode(2023)}` + ) + }) + + it('has one entry per statistic year', () => { + assert.strictEqual(body.unifyStats!.statistics.length, 8, 'unifyStats years') + assert.strictEqual(body.regularStats!.statistics.length, 8, 'regularStats years') + assert.strictEqual(body.openStats!.statistics.length, 4, 'openStats years') + }) + + it.each([ + ['2016-2017', { total: 1, passed: 1, failed: 0 }, { 5: 0, 4: 1, 3: 0, 2: 0, 1: 0, 0: 0, 'Hyl.': 0, 'Hyv.': 0 }], + [ + '2017-2018', + { total: 28, passed: 19, failed: 9 }, + { 5: 11, 4: 5, 3: 2, 2: 1, 1: 0, 0: 0, 'Hyl.': 9, 'Hyv.': 0 }, + ], + [ + '2018-2019', + { total: 32, passed: 26, failed: 6 }, + { 5: 18, 4: 3, 3: 2, 2: 1, 1: 2, 0: 5, 'Hyl.': 1, 'Hyv.': 0 }, + ], + [ + '2019-2020', + { total: 29, passed: 28, failed: 1 }, + { 5: 17, 4: 5, 3: 1, 2: 4, 1: 1, 0: 0, 'Hyl.': 1, 'Hyv.': 0 }, + ], + [ + '2020-2021', + { total: 23, passed: 23, failed: 0 }, + { 5: 18, 4: 2, 3: 0, 2: 2, 1: 0, 0: 0, 'Hyl.': 0, 'Hyv.': 1 }, + ], + [ + '2021-2022', + { total: 15, passed: 15, failed: 0 }, + { 5: 13, 4: 2, 3: 0, 2: 0, 1: 0, 0: 0, 'Hyl.': 0, 'Hyv.': 0 }, + ], + [ + '2022-2023', + { total: 27, passed: 27, failed: 0 }, + { 5: 21, 4: 6, 3: 0, 2: 0, 1: 0, 0: 0, 'Hyl.': 0, 'Hyv.': 0 }, + ], + ['2023-2024', { total: 1, passed: 1, failed: 0 }, { 5: 1, 4: 0, 3: 0, 2: 0, 1: 0, 0: 0, 'Hyl.': 0, 'Hyv.': 0 }], + ])('for attempt statistic content during (%s)', (year, categories, grades) => { + const stats = body.unifyStats!.statistics.find(yearStats => yearStats.name === year)! + assert('enrollments' in stats, 'Missing field enrollment in statsitics') + + assert.strictEqual( + stats.attempts.categories.failed.length, + categories.failed, + `Incorrect amount of failed attempts for ${year}` + ) + assert.strictEqual( + stats.attempts.categories.passed.length, + categories.passed, + `Incorrect amount of passed attempts for ${year}` + ) + assert.strictEqual( + stats.attempts.categories.failed.length + stats.attempts.categories.passed.length, + categories.total, + `Incorrect amount of total attempts for ${year}` + ) + + Object.entries(grades).forEach(([grade, count]) => + assert.strictEqual( + stats.attempts.grades[grade]?.length ?? 0, + count, + `Incorrect amount of attempts with grade ${grade}` + ) + ) + }) + + it('should include every attempt of a student who has failed a course in 2017 and later completed it', () => { + const year2017 = body.unifyStats!.statistics.find(year => year.name === '2017-2018') + const year2018 = body.unifyStats!.statistics.find(year => year.name === '2018-2019') + const year2020 = body.unifyStats!.statistics.find(year => year.name === '2020-2021') + const year2022 = body.unifyStats!.statistics.find(year => year.name === '2022-2023') + assert(year2017 && 'enrollments' in year2017, 'Missing stats for 2017-2018') + assert(year2018 && 'enrollments' in year2018, 'Missing stats for 2018-2019') + assert(year2020 && 'enrollments' in year2020, 'Missing stats for 2020-2021') + assert(year2022 && 'enrollments' in year2022, 'Missing stats for 2021-2022') + + assert.include(year2017.attempts.categories.failed, '457686') + assert.include(year2018.attempts.categories.passed, '457686') + + assert.include(year2017.attempts.categories.failed, '455478') + assert.include(year2020.attempts.categories.passed, '455478') + assert.strictEqual(year2017.attempts.categories.failed.filter(sNum => sNum === '455478').length, 2) // Failed twice + + assert.include(year2017.attempts.categories.failed, '547994') + assert.include(year2022.attempts.categories.passed, '547994') + }) + }) + + describe('single academic year ranges', () => { + it.each([ + // Same as above + ['2016-2017', { total: 1, passed: 1, failed: 0 }], + ['2017-2018', { total: 28, passed: 19, failed: 9 }], + ['2018-2019', { total: 32, passed: 26, failed: 6 }], + ['2019-2020', { total: 29, passed: 28, failed: 1 }], + ['2020-2021', { total: 23, passed: 23, failed: 0 }], + ['2021-2022', { total: 15, passed: 15, failed: 0 }], + ['2022-2023', { total: 27, passed: 27, failed: 0 }], + ['2023-2024', { total: 1, passed: 1, failed: 0 }], + ])('should include correct attempt stats for one academic year (%s)', async (year, categories) => { + const body = await getCourseYearlyStats( + app, + `codes=TKT10002&combineSubstitutions=false&fromYearCode=${yearToYearCode(year.split('-').at(0))}&toYearCode=${yearToYearCode(parseInt(year.split('-').at(1)!)) - 1}` + ) + const stats = body.unifyStats!.statistics.find(yearStats => yearStats.name === year)! + assert('enrollments' in stats, 'Missing field enrollment in statsitics') + + assert.strictEqual( + stats.attempts.categories.failed.length, + categories.failed, + `Incorrect amount of failed attempts for ${year}` + ) + assert.strictEqual( + stats.attempts.categories.passed.length, + categories.passed, + `Incorrect amount of passed attempts for ${year}` + ) + assert.strictEqual( + stats.attempts.categories.failed.length + stats.attempts.categories.passed.length, + categories.total, + `Incorrect amount of total attempts for ${year}` + ) + }) + }) +})