[Progress] combined programme criteria fix

thanks claude opus 5.5 high
This commit is contained in:
joonas-a
2026-10-12 15:20:23 +03:00
parent 09b743fb9b
commit dcfac28fa6
10 changed files with 268 additions and 140 deletions
@@ -193,8 +193,7 @@ export const computePercentiles = (
return values
}
// In the object programmes should be {bachelorCode: masterCode}
export const combinedStudyProgrammes = { KH90_001: 'MH90_001' } as const
export { combinedStudyProgrammes } from '@oodikone/shared/util/studyProgramme'
// There are 9 course_unit_types
// 1. urn:code:course-unit-type:regular
@@ -32,7 +32,7 @@ export const FilterDegreeCoursesModal = ({
<Dialog maxWidth="md" onClose={() => setModalOpen(false)} open={modalOpen}>
<DialogTitle>Degree programme settings</DialogTitle>
<DialogContent>
<DegreeCoursesTab combinedProgramme="" degreeProgramme={degreeProgramme} initialTab={initialTab} />
<DegreeCoursesTab degreeProgramme={degreeProgramme} initialTab={initialTab} />
<Button color="primary" onClick={() => setModalOpen(false)} style={{ marginTop: '10px' }} variant="contained">
Close
</Button>
@@ -28,21 +28,31 @@ import { StyledTable } from '@/components/common/StyledTable'
const reloadNote = 'Reload the page to see the change in other views.'
export type DegreeModule = Module & {
/** Programme whose curriculum the module is from. Course visibility is saved separately for each programme. */
programmeCode: string
}
const getModuleKey = (module: DegreeModule) => `${module.programmeCode}/${module.code}`
export const DegreeCourseTable = ({
combinedProgramme,
criteria,
curriculumVersion,
modules: initialModules,
programmeNames,
showCriteria,
showVisibility,
studyProgramme,
}: {
combinedProgramme: string
criteria: ProgressCriteria
curriculumVersion: string[]
modules: Module[]
/** Modules of the first programme first, then those of the second programme of a combined programme */
modules: DegreeModule[]
/** Shown above each programme's modules when the modules are from more than one programme */
programmeNames: Record<string, string>
showCriteria: boolean
showVisibility: boolean
/** Programme whose progress criteria are edited */
studyProgramme: string
}) => {
const [expandedModules, setExpandedModules] = useState<Record<string, Record<string, boolean>>>({})
@@ -60,7 +70,6 @@ export const DegreeCourseTable = ({
const [addProgressCriteriaCourse] = useAddProgressCriteriaCourseMutation()
const version = curriculumVersion.join(',')
const exclusionProgramme = combinedProgramme === '' ? studyProgramme : combinedProgramme
const criteriaYears = showCriteria ? getCriteriaYears(studyProgramme) : []
const criteriaCourses = pendingCriteria?.source === criteria ? pendingCriteria.courses : criteria.courses
@@ -68,74 +77,93 @@ export const DegreeCourseTable = ({
setModules(initialModules)
}, [initialModules])
const getModule = (moduleCode: string) => modules.find(module => module.code === moduleCode)!
const getModule = (moduleKey: string) => modules.find(module => getModuleKey(module) === moduleKey)!
const getModuleName = (moduleCode: string) => {
const firstCourse = getModule(moduleCode).courses[0]
return firstCourse?.parent_name ? getTextIn(firstCourse.parent_name)! : moduleCode
const getModuleName = (module: DegreeModule) => {
const firstCourse = module.courses[0]
return firstCourse?.parent_name ? getTextIn(firstCourse.parent_name)! : module.code
}
const setCoursesVisibility = (courseCodes: string[], newVisibility: boolean) => {
// Same course can be in many modules, so change them all
const setCoursesVisibility = (programmeCode: string, courseCodes: string[], newVisibility: boolean) => {
// Same course can be in many modules of the programme, so change them all
setModules(prevModules =>
prevModules.map(module => ({
...module,
courses: module.courses.map(course =>
courseCodes.includes(course.code)
? { ...course, visible: { ...course.visible, visibility: newVisibility } }
: course
),
}))
prevModules.map(module =>
module.programmeCode !== programmeCode
? module
: {
...module,
courses: module.courses.map(course =>
courseCodes.includes(course.code)
? { ...course, visible: { ...course.visible, visibility: newVisibility } }
: course
),
}
)
)
}
const changeVisibility = (courses: ProgrammeCourse[], newVisibility: boolean, successMessage: string) => {
const request = { courseGroupIds: courses.map(course => course.group_id), curriculumVersion: version }
const mutation = newVisibility
? removeExclusion({ ...request, programmeCode: exclusionProgramme })
: setExclusion({ ...request, programmeCode: exclusionProgramme })
setCoursesVisibility(
courses.map(course => course.code),
newVisibility
)
const changeVisibility = (
programmeCode: string,
courses: ProgrammeCourse[],
newVisibility: boolean,
successMessage: string
) => {
const request = {
courseGroupIds: courses.map(course => course.group_id),
curriculumVersion: version,
programmeCode,
}
const mutation = newVisibility ? removeExclusion(request) : setExclusion(request)
const courseCodes = courses.map(course => course.code)
setCoursesVisibility(programmeCode, courseCodes, newVisibility)
mutation
.unwrap()
.then(() => setStatusNotification(`${successMessage} ${reloadNote}`, 'success'))
.catch(() => {
setCoursesVisibility(
courses.map(course => course.code),
!newVisibility
)
setCoursesVisibility(programmeCode, courseCodes, !newVisibility)
setStatusNotification('Failed to change visibility', 'error')
})
}
const toggleCourseVisibility = (course: ProgrammeCourse) => {
const toggleCourseVisibility = (module: DegreeModule, course: ProgrammeCourse) => {
const newVisibility = !course.visible.visibility
const name = getTextIn(course.name)
changeVisibility([course], newVisibility, `${name} is now ${newVisibility ? 'shown' : 'hidden'}.`)
changeVisibility(
module.programmeCode,
[course],
newVisibility,
`${name} is now ${newVisibility ? 'shown' : 'hidden'}.`
)
}
const toggleModuleVisibility = (moduleCode: string) => {
const { courses } = getModule(moduleCode)
const visibility = getVisibility(moduleCode)
const name = getModuleName(moduleCode)
const toggleModuleVisibility = (module: DegreeModule) => {
const visibility = getVisibility(module)
if (visibility === CourseVisibility.HIDDEN) {
changeVisibility(courses, true, `All courses in ${name} are now shown.`)
changeVisibility(
module.programmeCode,
module.courses,
true,
`All courses in ${getModuleName(module)} are now shown.`
)
return
}
if (visibility === CourseVisibility.PARTIAL) {
setModuleToHide(moduleCode)
setModuleToHide(getModuleKey(module))
return
}
hideModule(moduleCode)
hideModule(module)
}
const hideModule = (moduleCode: string) => {
const visibleCourses = getModule(moduleCode).courses.filter(course => course.visible.visibility)
changeVisibility(visibleCourses, false, `All courses in ${getModuleName(moduleCode)} are now hidden.`)
const hideModule = (module: DegreeModule) => {
const visibleCourses = module.courses.filter(course => course.visible.visibility)
changeVisibility(
module.programmeCode,
visibleCourses,
false,
`All courses in ${getModuleName(module)} are now hidden.`
)
}
const toggleCriteriaYear = (course: ProgrammeCourse, { key, year }: CriteriaYearOption) => {
@@ -161,17 +189,16 @@ export const DegreeCourseTable = ({
})
}
const isModuleExpanded = (moduleCode: string) => expandedModules[version]?.[moduleCode] ?? false
const isModuleExpanded = (moduleKey: string) => expandedModules[version]?.[moduleKey] ?? false
const toggleExpanded = (moduleCode: string) => {
const toggleExpanded = (moduleKey: string) => {
setExpandedModules({
...expandedModules,
[version]: { ...expandedModules[version], [moduleCode]: !isModuleExpanded(moduleCode) },
[version]: { ...expandedModules[version], [moduleKey]: !isModuleExpanded(moduleKey) },
})
}
const getVisibility = (moduleCode: string) => {
const module = getModule(moduleCode)
const getVisibility = (module: DegreeModule) => {
if (!module.courses?.length) {
return CourseVisibility.NO_COURSES
}
@@ -188,9 +215,10 @@ export const DegreeCourseTable = ({
courses.filter(course => criteriaCourses[key]?.includes(course.group_id)).length
const headerRowSpan = showCriteria ? 2 : 1
const remainingToHide = moduleToHide
? getModule(moduleToHide).courses.filter(course => course.visible.visibility).length
: 0
const columnCount = 2 + Number(showVisibility) + criteriaYears.length
const showProgrammeRows = new Set(modules.map(module => module.programmeCode)).size > 1
const pendingHideModule = moduleToHide ? getModule(moduleToHide) : null
const remainingToHide = pendingHideModule?.courses.filter(course => course.visible.visibility).length ?? 0
return (
<>
@@ -217,12 +245,24 @@ export const DegreeCourseTable = ({
)}
</TableHead>
<TableBody>
{modules.map(({ code: moduleCode, courses }) => {
const moduleName = getModuleName(moduleCode)
const expanded = isModuleExpanded(moduleCode)
{modules.map((module, index) => {
const { code: moduleCode, courses, programmeCode } = module
const moduleKey = getModuleKey(module)
const moduleName = getModuleName(module)
const expanded = isModuleExpanded(moduleKey)
const startsProgramme = index === 0 || modules[index - 1].programmeCode !== programmeCode
return (
<Fragment key={`${moduleCode}-fragment`}>
<TableRow hover onClick={() => toggleExpanded(moduleCode)} sx={{ cursor: 'pointer' }}>
<Fragment key={moduleKey}>
{showProgrammeRows && startsProgramme && (
<TableRow data-cy={`programme-row-${programmeCode}`}>
<TableCell colSpan={columnCount} sx={{ bgcolor: 'grey.100' }}>
<Typography fontWeight="bold" variant="body2">
{programmeNames[programmeCode] ?? programmeCode} ({programmeCode})
</Typography>
</TableCell>
</TableRow>
)}
<TableRow hover onClick={() => toggleExpanded(moduleKey)} sx={{ cursor: 'pointer' }}>
<TableCell>
<Box alignItems="center" display="flex" justifyContent="left">
<IconButton
@@ -243,8 +283,8 @@ export const DegreeCourseTable = ({
<VisibilitySwitch
ariaLabel={`Show all courses of ${moduleName}`}
cypress={`visibility-switch-${moduleCode}`}
onChange={() => toggleModuleVisibility(moduleCode)}
visibility={getVisibility(moduleCode)}
onChange={() => toggleModuleVisibility(module)}
visibility={getVisibility(module)}
/>
</TableCell>
)}
@@ -271,7 +311,7 @@ export const DegreeCourseTable = ({
.map(course => {
const courseName = getTextIn(course.name)
return (
<TableRow key={`${moduleCode}/${course.code}`} sx={{ bgcolor: 'grey.50' }}>
<TableRow key={`${moduleKey}/${course.code}`} sx={{ bgcolor: 'grey.50' }}>
<TableCell sx={{ pl: 6 }}>{courseName}</TableCell>
<TableCell>{course.code}</TableCell>
{showVisibility && (
@@ -279,7 +319,7 @@ export const DegreeCourseTable = ({
<VisibilitySwitch
ariaLabel={`Show ${courseName}`}
cypress={`visibility-switch-${course.code}`}
onChange={() => toggleCourseVisibility(course)}
onChange={() => toggleCourseVisibility(module, course)}
visibility={
course.visible.visibility ? CourseVisibility.VISIBLE : CourseVisibility.HIDDEN
}
@@ -309,8 +349,8 @@ export const DegreeCourseTable = ({
<DialogTitle>Hide the whole module?</DialogTitle>
<DialogContent>
<DialogContentText>
{moduleToHide &&
`Some courses in ${getModuleName(moduleToHide)} are already hidden. ` +
{pendingHideModule &&
`Some courses in ${getModuleName(pendingHideModule)} are already hidden. ` +
`Hiding the module hides its remaining ${remainingToHide} ` +
`${remainingToHide === 1 ? 'course' : 'courses'} for all Oodikone users.`}
</DialogContentText>
@@ -320,7 +360,7 @@ export const DegreeCourseTable = ({
<Button
data-cy="confirm-hide-module-button"
onClick={() => {
hideModule(moduleToHide!)
hideModule(pendingHideModule!)
setModuleToHide(null)
}}
variant="contained"
@@ -4,22 +4,43 @@ import Tab from '@mui/material/Tab'
import Tabs from '@mui/material/Tabs'
import { orderBy } from 'lodash-es'
import { useEffect, useState } from 'react'
import { useMemo, useState } from 'react'
import { useCurriculum } from '@/hooks/useCurriculum'
import { CreditCriteriaSection } from '@/pages/StudyProgramme/DegreeCoursesTab/CreditCriteriaSection'
import { CurriculumSection } from '@/pages/StudyProgramme/DegreeCoursesTab/CurriculumSection'
import { DegreeCourseTable } from '@/pages/StudyProgramme/DegreeCoursesTab/DegreeCourseTable'
import { DegreeCourseTable, DegreeModule } from '@/pages/StudyProgramme/DegreeCoursesTab/DegreeCourseTable'
import { ManageCoursesShownInfo } from '@/pages/StudyProgramme/DegreeCoursesTab/InfoBox'
import { useGetProgrammesQuery } from '@/redux/populations'
import { useGetProgressCriteriaQuery } from '@/redux/progressCriteria'
import { isBachelorOrLicentiateProgramme } from '@/util/studyProgramme'
import { Module, ProgrammeCourse } from '@oodikone/shared/types'
import { ProgrammeCourse } from '@oodikone/shared/types'
import { getCombinedProgramme } from '@oodikone/shared/util/studyProgramme'
import { Section } from '@/components/Section'
import { useLanguage } from '@/components/LanguagePicker/useLanguage'
import { studyProgrammeToolTips } from '@/common/InfoToolTips'
export type DegreeSettingsTab = 'criteria' | 'visibility'
const getModules = (courses: ProgrammeCourse[], programmeCode: string): DegreeModule[] => {
const modules: Record<string, ProgrammeCourse[]> = {}
courses.forEach(course => {
const code = course.parent_code!
modules[code] ??= []
modules[code].push(course)
})
return orderBy(
Object.entries(modules).map(([code, courses]) => ({
code,
courses,
order: courses[0].module_order,
programmeCode,
})),
module => module.code
)
}
const sectionTexts = {
criteria: {
title: 'Course completion academic year targets',
@@ -32,82 +53,55 @@ const sectionTexts = {
}
export const DegreeCoursesTab = ({
combinedProgramme,
degreeProgramme,
initialTab = 'criteria',
}: {
combinedProgramme: string
degreeProgramme: string
initialTab?: DegreeSettingsTab
}) => {
const [currentTab, setCurrentTab] = useState<DegreeSettingsTab>(initialTab)
const [defaultProgrammeModules, setDefaultProgrammeModules] = useState<Module[]>([])
const [secondProgrammeModules, setSecondProgrammeModules] = useState<Module[]>([])
const { data: criteria } = useGetProgressCriteriaQuery({ programmeCode: degreeProgramme })
const { data: programmes } = useGetProgrammesQuery()
const { getTextIn } = useLanguage()
const { curriculum, curriculumList, setSelectedCurriculum: setCurriculum } = useCurriculum()
const secondProgramme = getCombinedProgramme(degreeProgramme)
const getModules = (courses: ProgrammeCourse[]): Module[] => {
const modules: Record<string, ProgrammeCourse[]> = {}
courses.forEach(course => {
const code = course.parent_code!
modules[code] ??= []
modules[code].push(course)
})
// A combined programme has courses from two curriculums. Show them in one table, first programme's modules first.
const modules = useMemo(
() =>
curriculum
? [
...getModules(curriculum.defaultProgrammeCourses ?? [], degreeProgramme),
...(secondProgramme ? getModules(curriculum.secondProgrammeCourses ?? [], secondProgramme) : []),
]
: [],
[curriculum, degreeProgramme, secondProgramme]
)
return orderBy(
Object.entries(modules).map(([code, courses]) => ({
code,
courses,
order: courses[0].module_order,
})),
module => module.code
)
}
useEffect(() => {
if (!curriculum?.defaultProgrammeCourses?.length) {
return
}
setDefaultProgrammeModules(getModules(curriculum.defaultProgrammeCourses))
if (combinedProgramme) {
setSecondProgrammeModules(getModules(curriculum.secondProgrammeCourses))
}
}, [combinedProgramme, curriculum])
const programmeNames = Object.fromEntries(
[degreeProgramme, secondProgramme]
.filter(code => code !== undefined)
.map(code => {
const programme = programmes?.allProgrammes?.[code] ?? programmes?.filteredProgrammes?.[code]
return [code, programme ? getTextIn(programme.name)! : code]
})
)
if (!curriculum || !criteria) return null
const hasCriteria = isBachelorOrLicentiateProgramme(degreeProgramme)
const hasModules = !!defaultProgrammeModules.length || !!secondProgrammeModules.length
const renderCourseTables = (tab: DegreeSettingsTab) =>
hasModules && (
const renderCourseTable = (tab: DegreeSettingsTab) =>
!!modules.length && (
<Section infoBoxContent={getTextIn(sectionTexts[tab].toolTip) ?? ''} title={sectionTexts[tab].title}>
<Stack gap={2}>
{!!defaultProgrammeModules.length && (
<DegreeCourseTable
combinedProgramme=""
criteria={criteria}
curriculumVersion={curriculum.periodIds}
modules={defaultProgrammeModules}
showCriteria={tab === 'criteria'}
showVisibility={tab === 'visibility'}
studyProgramme={degreeProgramme}
/>
)}
{!!secondProgrammeModules.length && (
<DegreeCourseTable
combinedProgramme={combinedProgramme}
criteria={criteria}
curriculumVersion={curriculum.periodIds}
modules={secondProgrammeModules}
showCriteria={tab === 'criteria'}
showVisibility={tab === 'visibility'}
studyProgramme={degreeProgramme}
/>
)}
</Stack>
<DegreeCourseTable
criteria={criteria}
curriculumVersion={curriculum.periodIds}
modules={modules}
programmeNames={programmeNames}
showCriteria={tab === 'criteria'}
showVisibility={tab === 'visibility'}
studyProgramme={degreeProgramme}
/>
</Section>
)
@@ -118,7 +112,7 @@ export const DegreeCoursesTab = ({
const visibilityPanel = (
<Stack gap={2}>
{curriculumSection}
{renderCourseTables('visibility')}
{renderCourseTable('visibility')}
</Stack>
)
@@ -140,7 +134,7 @@ export const DegreeCoursesTab = ({
<Stack gap={2}>
{curriculumSection}
<CreditCriteriaSection criteria={criteria} degreeProgramme={degreeProgramme} />
{renderCourseTables('criteria')}
{renderCourseTable('criteria')}
</Stack>
</Box>
<Box data-cy="visibility-panel" hidden={currentTab !== 'visibility'}>
@@ -138,7 +138,7 @@ export const StudyProgramme = () => {
label: 'Degree programme settings',
component: (
<CurriculumProvider key="DegreeCoursesTabContent" programmeCode={programmeId} year={new Date().getFullYear()}>
<DegreeCoursesTab combinedProgramme={secondProgrammeId} degreeProgramme={programmeId} />
<DegreeCoursesTab degreeProgramme={programmeId} />
</CurriculumProvider>
),
},
@@ -34,6 +34,40 @@ test.describe('DegreeCourseTable', () => {
'Year 3',
])
await expect(component.getByText('1 / 2')).toBeVisible()
await expect(component.locator('[data-cy^="programme-row-"]')).toHaveCount(0)
})
test('combined programme shows both curriculums in one table, first programme first', async ({ mount, page }) => {
const component = await mount(<DegreeCourseTableWrapper combined studyProgramme="KH90_001" />)
await expect(page.locator('[data-cy="degree-course-table"]')).toHaveCount(1)
await expect(component.getByRole('columnheader', { name: 'Year 6' })).toBeVisible()
await expect(component.locator('tbody tr')).toHaveText([
'First programme (KH90_001)',
/^Basic studies/,
'Second programme (MH90_001)',
/^Clinical studies/,
])
})
test('hiding a course of the second programme only hides it there', async ({ mount }) => {
const component = await mount(<DegreeCourseTableWrapper combined showCriteria={false} studyProgramme="KH90_001" />)
await component.getByText('Clinical studies').click()
await component.getByRole('checkbox', { name: 'Show Calculus I' }).uncheck()
await component.getByText('Basic studies').click()
const calculusSwitches = component.getByRole('checkbox', { name: 'Show Calculus I' })
await expect(calculusSwitches.first()).toBeChecked()
await expect(calculusSwitches.last()).not.toBeChecked()
await expect
.poll(() => exclusionRequests)
.toEqual([
{
method: 'POST',
body: { courseGroupIds: ['group-MAT101'], curriculumVersion: 'period-1', programmeCode: 'MH90_001' },
},
])
})
test('shows six years for medicine', async ({ mount }) => {
@@ -1,15 +1,15 @@
import { DegreeCourseTable } from '@/pages/StudyProgramme/DegreeCoursesTab/DegreeCourseTable'
import { Module, ProgrammeCourse, ProgressCriteria } from '@oodikone/shared/types'
import { DegreeCourseTable, DegreeModule } from '@/pages/StudyProgramme/DegreeCoursesTab/DegreeCourseTable'
import { ProgrammeCourse, ProgressCriteria } from '@oodikone/shared/types'
import { ReduxWrapper } from '../../ReduxWrapper'
const course = (code: string, name: string, visible = true) =>
const course = (code: string, name: string, visible = true, moduleName = 'Basic studies') =>
({
code,
group_id: `group-${code}`,
module_order: 0,
name: { fi: name, en: name },
parent_code: 'MOD1',
parent_name: { fi: 'Basic studies', en: 'Basic studies' },
parent_name: { fi: moduleName, en: moduleName },
visible: { id: null, visibility: visible },
}) as unknown as ProgrammeCourse
@@ -29,33 +29,45 @@ const criteria: ProgressCriteria = {
/**
* Mounts `DegreeCourseTable` with one module of two courses. MAT101 is required for year 1.
* With `partlyHidden`, MAT102 is hidden.
* With `combined`, a second programme (MH90_001) adds a module with the same code that also contains MAT101.
*/
export const DegreeCourseTableWrapper = ({
combined = false,
partlyHidden = false,
showCriteria = true,
showVisibility = true,
studyProgramme = 'KH50_001',
}: {
combined?: boolean
partlyHidden?: boolean
showCriteria?: boolean
showVisibility?: boolean
studyProgramme?: string
}) => {
const modules: Module[] = [
const modules: DegreeModule[] = [
{
code: 'MOD1',
courses: [course('MAT101', 'Calculus I'), course('MAT102', 'Linear algebra', !partlyHidden)],
order: 0,
programmeCode: studyProgramme,
},
]
if (combined) {
modules.push({
code: 'MOD1',
courses: [course('MAT101', 'Calculus I', true, 'Clinical studies')],
order: 0,
programmeCode: 'MH90_001',
})
}
return (
<ReduxWrapper
component={
<DegreeCourseTable
combinedProgramme=""
criteria={criteria}
curriculumVersion={['period-1']}
modules={modules}
programmeNames={{ [studyProgramme]: 'First programme', MH90_001: 'Second programme' }}
showCriteria={showCriteria}
showVisibility={showVisibility}
studyProgramme={studyProgramme}
@@ -18,6 +18,14 @@ test.describe('DegreeCoursesTab', () => {
test.beforeEach(async ({ router }) => {
await router.route('**/api/login', route => route.fulfill({ json: { user: { roles: [] } } }))
await router.route('**/api/banners', route => route.fulfill({ json: [] }))
await router.route('**/api/populationstatistics/studyprogrammes', route =>
route.fulfill({
json: {
allProgrammes: { KH90_001: { name: { fi: 'Lääketieteen kandiohjelma', en: 'Medicine (bachelor)' } } },
filteredProgrammes: {},
},
})
)
await router.route('**/api/curriculum-options/*', route =>
route.fulfill({ json: [{ id: 'cur-1', name: '2023–2026', periodIds: ['period-1'], validFrom: '2023-08-01' }] })
)
@@ -48,7 +56,7 @@ test.describe('DegreeCoursesTab', () => {
await expect(component.getByRole('alert')).toContainText(/non-personal|eivät ole henkilökohtaisia/)
await expect(component.locator('[data-cy="criteria-panel"] [data-cy="curriculum-section"]')).toBeVisible()
await expect(component.getByRole('tab')).toHaveText(['Progress tab criteria', 'Course visibility'])
await expect(component.getByRole('tab')).toHaveText(['Progress tab criteria', 'Course & module visibility'])
await expect(component.getByRole('tab', { name: 'Progress tab criteria' })).toHaveAttribute('aria-selected', 'true')
await expect(component.locator('[data-cy="credit-criteria-section"]')).toBeVisible()
await expect(component.locator('[data-cy="visibility-panel"]')).toBeHidden()
@@ -74,7 +82,7 @@ test.describe('DegreeCoursesTab', () => {
await visibilityPanel.getByText('Basic studies').click()
await visibilityPanel.getByRole('checkbox', { name: 'Show Calculus I' }).uncheck()
await component.getByRole('tab', { name: 'Progress tab criteria' }).click()
await component.getByRole('tab', { name: 'Course visibility' }).click()
await component.getByRole('tab', { name: 'Course & module visibility' }).click()
await expect(visibilityPanel.getByRole('checkbox', { name: 'Show Calculus I' })).not.toBeChecked()
})
@@ -86,4 +94,38 @@ test.describe('DegreeCoursesTab', () => {
await expect(component.getByRole('tab')).toHaveCount(0)
await expect(component.locator('[data-cy="credit-criteria-section"]')).toHaveCount(0)
})
test('combined programme shows both curriculums in one table', async ({ mount, router }) => {
await router.route('**/api/curriculum/*/*', route =>
route.fulfill({
json: {
defaultProgrammeCourses: [course],
defaultProgrammeModules: [],
secondProgrammeCourses: [
{
...course,
code: 'LK101',
group_id: 'group-LK101',
name: { fi: 'Anatomy', en: 'Anatomy' },
parent_code: 'MOD2',
parent_name: { fi: 'Clinical studies', en: 'Clinical studies' },
},
],
secondProgrammeModules: [],
},
})
)
const component = await mount(<DegreeCoursesTabWrapper programme="KH90_001" />)
const criteriaPanel = component.locator('[data-cy="criteria-panel"]')
await expect(criteriaPanel.locator('[data-cy="degree-course-table"]')).toHaveCount(1)
await expect(criteriaPanel.locator('[data-cy="programme-row-KH90_001"]')).toContainText(
/Lääketieteen kandiohjelma|Medicine \(bachelor\)/
)
// Programme without a known name falls back to its code
await expect(criteriaPanel.locator('[data-cy="programme-row-MH90_001"]')).toHaveText('MH90_001 (MH90_001)')
await expect(criteriaPanel.getByText('Basic studies')).toBeVisible()
await expect(criteriaPanel.getByText('Clinical studies')).toBeVisible()
await expect(criteriaPanel.getByRole('columnheader', { name: 'Year 6' })).toBeVisible()
})
})
@@ -15,7 +15,7 @@ export const DegreeCoursesTabWrapper = ({
<ReduxWrapper
component={
<CurriculumProvider programmeCode={programme} year={2024}>
<DegreeCoursesTab combinedProgramme="" degreeProgramme={programme} initialTab={initialTab} />
<DegreeCoursesTab degreeProgramme={programme} initialTab={initialTab} />
</CurriculumProvider>
}
/>
+7
View File
@@ -2,6 +2,13 @@ import { StudyProgrammeCourse } from '../types'
export type YearStats = StudyProgrammeCourse['years'][number]
// In the object programmes should be {bachelorCode: masterCode}
export const combinedStudyProgrammes = { KH90_001: 'MH90_001' } as const
/** Second programme of a combined programme, eg. the licentiate programme of a combined bachelor programme */
export const getCombinedProgramme = (programmeCode: string): string | undefined =>
combinedStudyProgrammes[programmeCode as keyof typeof combinedStudyProgrammes]
export const createEmptyStats = (isStudyModule = false): YearStats => ({
allCredits: 0,
allStudents: 0,