Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions VueApp/src/Students/CareerSelection/__tests__/career-columns.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { OVERVIEW_COLUMNS, REPORT_COLUMNS, previewStatement, STATEMENT_PREVIEW_LENGTH } from "../utils/career-columns"

/**
* Tests for the grid columns the roster and report are built from, and the statement excerpt the
* report shows in place of a 5000-character plan.
*/

function column(columns: typeof OVERVIEW_COLUMNS, name: string) {
return columns.find((c) => c.name === name)
}

describe("statement excerpt", () => {
it("shows a short statement in full", () => {
expect.hasAssertions()
expect(previewStatement("Internship")).toBe("Internship")
})

it("trims the surrounding whitespace", () => {
expect.hasAssertions()
expect(previewStatement(" Internship \n")).toBe("Internship")
})

it("reads an absent statement as empty", () => {
expect.hasAssertions()
expect(previewStatement(null)).toBe("")
})

it("cuts a long statement to the preview length, ellipsis included", () => {
expect.hasAssertions()
const excerpt = previewStatement("x".repeat(500))

expect(excerpt).toHaveLength(STATEMENT_PREVIEW_LENGTH)
expect(excerpt.endsWith("…")).toBeTruthy()
})

it("does not cut a statement that just fits", () => {
expect.hasAssertions()
const exact = "x".repeat(STATEMENT_PREVIEW_LENGTH)

expect(previewStatement(exact)).toBe(exact)
})

it("does not leave a dangling space before the ellipsis", () => {
expect.hasAssertions()
const statement = `${"x".repeat(STATEMENT_PREVIEW_LENGTH - 2)} more words`

expect(previewStatement(statement)).toBe(`${"x".repeat(STATEMENT_PREVIEW_LENGTH - 2)}…`)
})
})

describe("overview columns", () => {
it("opens with the student's identity", () => {
expect.hasAssertions()
expect(OVERVIEW_COLUMNS.slice(0, 3).map((c) => c.name)).toStrictEqual(["classLevel", "fullName", "email"])
})

it("sorts a flagged field on its completeness, not its value", () => {
expect.hasAssertions()
expect(column(OVERVIEW_COLUMNS, "direction")?.field).toBe("directionCompleted")
})

it("sorts the mentor on its value, having no completeness flag", () => {
expect.hasAssertions()
expect(column(OVERVIEW_COLUMNS, "mentor")?.field).toBe("mentorName")
})

it("centres the completeness icons and leaves the mentor ranged left", () => {
expect.hasAssertions()
expect(column(OVERVIEW_COLUMNS, "direction")?.align).toBe("center")
expect(column(OVERVIEW_COLUMNS, "mentor")?.align).toBe("left")
})

it("ends with a formatted last-updated date", () => {
expect.hasAssertions()
const lastUpdated = OVERVIEW_COLUMNS.at(-1)

expect(lastUpdated?.name).toBe("lastUpdated")
expect(lastUpdated?.format?.("2026-04-17T10:00:00", {})).toBe(
new Date("2026-04-17T10:00:00").toLocaleDateString(),
)
expect(lastUpdated?.format?.(null, {})).toBe("")
})
})

describe("report columns", () => {
it("reads each field's selected value", () => {
expect.hasAssertions()
expect(column(REPORT_COLUMNS, "direction")?.field).toBe("direction")
expect(column(REPORT_COLUMNS, "postGrad")?.field).toBe("postGrad")
})

it("leaves statements unformatted, so search reaches past the excerpt", () => {
expect.hasAssertions()
// QTable's search matches the formatted value; the excerpt is the report's cell slot.
expect(column(REPORT_COLUMNS, "shortTerm")?.format).toBeUndefined()
expect(column(REPORT_COLUMNS, "longTerm")?.format).toBeUndefined()
})

it("does not offer to sort on a statement", () => {
expect.hasAssertions()
expect(column(REPORT_COLUMNS, "shortTerm")?.sortable).toBeFalsy()
expect(column(REPORT_COLUMNS, "direction")?.sortable).toBeTruthy()
})

it("carries the same fields in the same order as the roster", () => {
expect.hasAssertions()
expect(REPORT_COLUMNS.map((c) => c.name)).toStrictEqual(OVERVIEW_COLUMNS.map((c) => c.name))
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { isSelectionComplete, missingFieldLabels } from "../utils/career-completeness"
import type { CareerDropdownOption, StudentInfo } from "../types"

/**
* Tests for the form's missing-fields warning. isSelectionComplete mirrors IsSelectionComplete in
* CareerSelectionService.cs, which drives the roster icons and the exports, so the cases here are
* the ones the C# tests cover too.
*/

function option(label: string, value: number, isOther = false): CareerDropdownOption {
return { label, value, isOther }
}

function answered(overrides: Partial<StudentInfo> = {}): StudentInfo {
return {
direction: option("Academia", 1),
directionOther: "",
primaryFocus: option("Equine", 2),
primaryFocusOther: "",
secondaryFocus: option("Bovine", 3),
secondaryFocusOther: "",
postGrad: option("Residency", 4),
shortTermPlans: "Internship",
longTermPlans: "Practice ownership",
...overrides,
}
}

describe("is selection complete", () => {
it("counts an ordinary choice as answered", () => {
expect.hasAssertions()
expect(isSelectionComplete(option("Academia", 1), null)).toBeTruthy()
})

it("counts nothing selected as unanswered", () => {
expect.hasAssertions()
expect(isSelectionComplete(null, "Wildlife")).toBeFalsy()
})

it("wants the free text before the catch-all counts as answered", () => {
expect.hasAssertions()
const other = option("Other", 9, true)
expect(isSelectionComplete(other, null)).toBeFalsy()
expect(isSelectionComplete(other, "Wildlife rehabilitation")).toBeTruthy()
})

it("does not accept whitespace as the catch-all's free text", () => {
expect.hasAssertions()
expect(isSelectionComplete(option("Other", 9, true), " ")).toBeFalsy()
})

it("ignores free text for an ordinary choice", () => {
expect.hasAssertions()
expect(isSelectionComplete(option("Academia", 1), "")).toBeTruthy()
})
})

describe("missing field labels", () => {
it("wants nothing when every field is answered", () => {
expect.hasAssertions()
expect(missingFieldLabels(answered())).toStrictEqual([])
})

it("lists every field of an empty form, in page order", () => {
expect.hasAssertions()
const empty = answered({
direction: null,
primaryFocus: null,
secondaryFocus: null,
postGrad: null,
shortTermPlans: "",
longTermPlans: "",
})

expect(missingFieldLabels(empty)).toStrictEqual([
"Career Direction",
"Primary Focus",
"Secondary Focus",
"Post-Graduation Plans",
"Short Term Plans",
"Long Term Plans",
])
})

it("prompts for the secondary focus even though the roster treats it as optional", () => {
expect.hasAssertions()
expect(missingFieldLabels(answered({ secondaryFocus: null }))).toStrictEqual(["Secondary Focus"])
})

it("reads the short term plans as the post-grad catch-all's explanation", () => {
expect.hasAssertions()
// Post-graduation plans have no free-text field of their own.
const other = option("Other", 9, true)

expect(missingFieldLabels(answered({ postGrad: other, shortTermPlans: "Research fellowship" }))).toStrictEqual(
[],
)
expect(missingFieldLabels(answered({ postGrad: other, shortTermPlans: "" }))).toStrictEqual([
"Post-Graduation Plans",
"Short Term Plans",
])
})

it("treats a whitespace-only statement as unanswered", () => {
expect.hasAssertions()
expect(missingFieldLabels(answered({ longTermPlans: " " }))).toStrictEqual(["Long Term Plans"])
})

it("wants the catch-all's free text before the field counts as answered", () => {
expect.hasAssertions()
const withOther = answered({ direction: option("Other", 9, true), directionOther: "" })

expect(missingFieldLabels(withOther)).toStrictEqual(["Career Direction"])
})
})
107 changes: 107 additions & 0 deletions VueApp/src/Students/CareerSelection/__tests__/career-fields.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { CAREER_FIELDS } from "../utils/career-fields"
import type { StudentCareerListItem, StudentCareerReport } from "../types"

/**
* Tests for the career field metadata. The overview, report, mobile cards and exports all read
* their columns from this one list, so a wrong key here breaks several views at once.
*/

function listItem(): StudentCareerListItem {
return {
personId: 100,
rowKey: "100",
hasDetailRoute: true,
fullName: "Student, Test",
classLevel: "V1",
email: "tstudent@ucdavis.edu",
directionCompleted: true,
primaryFocusCompleted: true,
secondaryFocusCompleted: false,
postGradCompleted: true,
shortTermPlansCompleted: true,
longTermPlansCompleted: false,
mentorName: "Vet, Ann",
lastUpdated: "2026-04-17T10:00:00",
}
}

function report(): StudentCareerReport {
return {
personId: 100,
rowKey: "100",
hasDetailRoute: true,
fullName: "Student, Test",
classLevel: "V1",
email: "tstudent@ucdavis.edu",
direction: "Academia",
primaryFocus: "Equine",
secondaryFocus: "",
postGrad: "Residency",
shortTermPlans: "Internship",
longTermPlans: "",
mentorName: "Vet, Ann",
lastUpdated: "2026-04-17T10:00:00",
}
}

describe("career fields", () => {
it("names each field once", () => {
expect.hasAssertions()
const names = CAREER_FIELDS.map((f) => f.name)
expect(new Set(names).size).toBe(names.length)
})

it("reads a value from the report for every field", () => {
expect.hasAssertions()
const row = report()
for (const field of CAREER_FIELDS) {
expect(row).toHaveProperty(field.valueField)
}
})

it("reads a completeness flag from the overview for every flagged field", () => {
expect.hasAssertions()
const row = listItem()
for (const field of CAREER_FIELDS.filter((f) => f.completedField)) {
expect(row).toHaveProperty(field.completedField!)
}
})

it("labels every completeness icon for screen readers", () => {
expect.hasAssertions()
for (const field of CAREER_FIELDS.filter((f) => f.completedField)) {
expect(field.tooltipLabel).toBeTruthy()
}
})

it("shows the mentor as plain text on both pages", () => {
expect.hasAssertions()
// The mentor is admin-managed, so it is reported rather than flagged as complete.
const mentor = CAREER_FIELDS.find((f) => f.name === "mentor")
expect(mentor?.completedField).toBeUndefined()
expect(mentor?.valueField).toBe("mentorName")
})

it("treats only the second species focus as optional", () => {
expect.hasAssertions()
expect(CAREER_FIELDS.filter((f) => f.optional).map((f) => f.name)).toStrictEqual(["secondarySpecies"])
})

it("treats only the two plan statements as free text", () => {
expect.hasAssertions()
expect(CAREER_FIELDS.filter((f) => f.statement).map((f) => f.name)).toStrictEqual(["shortTerm", "longTerm"])
})

it("keeps the column order the grids and exports share", () => {
expect.hasAssertions()
expect(CAREER_FIELDS.map((f) => f.label)).toStrictEqual([
"Career",
"Species 1",
"Species 2",
"Post Grad",
"Mentor",
"Short Term",
"Long Term",
])
})
})
Loading
Loading