Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { usePermissionsStore } from "../stores/permissions"
import { AuditLogService } from "../services/audit-log-service"
import { RotationService } from "../services/rotation-service"
import { PageDataService } from "../services/page-data-service"
import AuditLogPage from "../pages/AuditLogPage.vue"
import {
setupTest,
createTestWrapper,
waitForAsync,
createMockPermissionsStore,
createMockUserPermissions,
} from "./test-utils"
import type { UserPermissions } from "../types"

type MockPermissionsStore = Omit<ReturnType<typeof createMockPermissionsStore>, "userPermissions"> & {
hasManagePermission?: boolean
userPermissions: UserPermissions | null
}

// Mock the permissions store and the services the page calls
vi.mock("../stores/permissions")
vi.mock("../services/audit-log-service")
vi.mock("../services/rotation-service")
vi.mock("../services/page-data-service")

// Stub child components that hit the network or rely on named routes
vi.mock("../components/AccessDeniedCard.vue", () => ({
default: {
name: "AccessDeniedCard",
props: ["message", "subtitle"],
template: '<div class="no-access-card"><div>Access Denied</div><div>{{message}}</div></div>',
},
}))
vi.mock("../components/SchedulerNavigation.vue", () => ({
default: { name: "SchedulerNavigation", template: "<div class='scheduler-nav'></div>" },
}))

describe("auditLogPage - Access Control", () => {
let mockPermissionsStore: MockPermissionsStore = {} as never
let router: any = {}

beforeEach(() => {
const { router: testRouter, mockStore } = setupTest()
router = testRouter
mockPermissionsStore = mockStore
vi.mocked(usePermissionsStore).mockReturnValue(mockPermissionsStore as any)
vi.mocked(AuditLogService.getAuditLog).mockResolvedValue({ result: [], success: true, errors: [] })
vi.mocked(AuditLogService.getModifiers).mockResolvedValue({ result: [], success: true, errors: [] })
vi.mocked(AuditLogService.getPersons).mockResolvedValue({ result: [], success: true, errors: [] })
vi.mocked(AuditLogService.getTerms).mockResolvedValue({ result: [], success: true, errors: [] })
vi.mocked(RotationService.getRotations).mockResolvedValue({ result: [], success: true, errors: [] })
vi.mocked(PageDataService.getPageData).mockResolvedValue({ currentGradYear: 2026, availableGradYears: [2026] })
})

it("shows access denied when user lacks the manage permission", () => {
mockPermissionsStore.hasManagePermission = false
mockPermissionsStore.userPermissions = createMockUserPermissions()

const wrapper = createTestWrapper({ component: AuditLogPage, router })

expect(wrapper.find(".no-access-card").exists()).toBeTruthy()
// The results table (and its "Modified By" column) only renders for managers.
expect(wrapper.text()).not.toContain("Modified By")
})

it("shows the audit trail and loads filter options for managers", async () => {
mockPermissionsStore.hasManagePermission = true
mockPermissionsStore.userPermissions = createMockUserPermissions({
permissions: {
hasAdminPermission: false,
hasManagePermission: true,
hasEditClnSchedulesPermission: false,
hasEditOwnSchedulePermission: false,
servicePermissions: {},
editableServiceCount: 0,
},
})

const wrapper = createTestWrapper({ component: AuditLogPage, router })
await waitForAsync()

expect(wrapper.find(".no-access-card").exists()).toBeFalsy()
expect(wrapper.text()).toContain("Modified By")
expect(RotationService.getRotations).toHaveBeenCalledWith()
expect(AuditLogService.getModifiers).toHaveBeenCalledWith()
})

it("initializes the permissions store on mount when not yet loaded", async () => {
mockPermissionsStore.hasManagePermission = true
mockPermissionsStore.userPermissions = null

createTestWrapper({ component: AuditLogPage, router })
await waitForAsync()

expect(mockPermissionsStore.initialize).toHaveBeenCalledWith()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import AuditLogResultsTable from "../components/AuditLogResultsTable.vue"
import StatusBadge from "@/components/StatusBadge.vue"
import { createTestWrapper } from "./test-utils"
import type { AuditLogEntry } from "../types/audit-types"

function makeEntry(overrides: Partial<AuditLogEntry> = {}): AuditLogEntry {
return {
scheduleAuditId: 1,
area: "Clinicians",
mothraId: "abc123",
personName: "Dr. Example",
action: "Added to rotation",
rotationId: 5,
rotationName: "Cardiology",
weekId: 10,
weekNum: 3,
weekStart: "2026-01-05",
term: "Spring 2026",
modifiedBy: "mgr456",
modifiedByName: "Manager Person",
timeStamp: "2026-01-06T08:30:00",
...overrides,
}
}

function mountTable(entries: AuditLogEntry[], isLoading = false) {
return createTestWrapper({
component: AuditLogResultsTable,
props: { entries, isLoading },
})
}

describe("auditLogResultsTable", () => {
it("renders audit entries", () => {
expect.assertions(4)

const wrapper = mountTable([makeEntry()])

expect(wrapper.text()).toContain("Dr. Example")
expect(wrapper.text()).toContain("Added to rotation")
expect(wrapper.text()).toContain("Cardiology")
expect(wrapper.text()).toContain("Manager Person")
})

it("renders one row per entry", () => {
expect.assertions(3)

const wrapper = mountTable([
makeEntry(),
makeEntry({ scheduleAuditId: 2, personName: "Dr. Second", action: "Removed from rotation" }),
])

// One action badge renders per entry across every responsive table variant.
expect(wrapper.findAllComponents(StatusBadge)).toHaveLength(2)
expect(wrapper.text()).toContain("Dr. Second")
expect(wrapper.text()).toContain("Removed from rotation")
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("marks the table as loading", () => {
expect.assertions(1)

const wrapper = mountTable([], true)

expect(wrapper.findComponent({ name: "QTable" }).props("loading")).toBeTruthy()
})
})
153 changes: 153 additions & 0 deletions VueApp/src/ClinicalScheduler/__tests__/week-history-content.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import WeekHistoryContent from "../components/WeekHistoryContent.vue"
import { createTestWrapper } from "./test-utils"
import { useDateFunctions } from "@/composables/DateFunctions"
import type { AuditLogEntry } from "../types/audit-types"

function makeEntry(overrides: Partial<AuditLogEntry> = {}): AuditLogEntry {
return {
scheduleAuditId: 1,
area: "Clinicians",
mothraId: "abc123",
personName: "Dr. Example",
action: "Added to rotation",
rotationId: 5,
rotationName: "Cardiology",
weekId: 10,
weekNum: 3,
weekStart: "2026-01-05",
term: "Spring 2026",
modifiedBy: "mgr456",
modifiedByName: "Manager Person",
timeStamp: "2026-01-06T08:30:00",
...overrides,
}
}

function mountContent(props: Record<string, unknown> = {}) {
return createTestWrapper({
component: WeekHistoryContent,
props: {
titleId: "week-history-title-test",
viewMode: "rotation",
weekNumber: 3,
weekDateStart: "2026-01-05",
contextLabel: "Cardiology",
entries: [],
isLoading: false,
error: null,
...props,
},
})
}

describe("weekHistoryContent", () => {
it("shows skeleton rows while loading", () => {
expect.assertions(2)

const wrapper = mountContent({ isLoading: true })

expect(wrapper.findAll(".week-history__skeleton-row")).toHaveLength(3)
expect(wrapper.attributes("aria-busy")).toBe("true")
})

it("shows the error state and emits retry", async () => {
expect.assertions(2)

const wrapper = mountContent({ error: "Failed to load the audit trail" })

expect(wrapper.find("[role='alert']").text()).toContain("Failed to load the audit trail")

await wrapper.find("[role='alert'] button").trigger("click")

expect(wrapper.emitted("retry")).toHaveLength(1)
})

it("shows an empty state when there are no entries", () => {
expect.assertions(1)

const wrapper = mountContent({ entries: [] })

expect(wrapper.text()).toContain("No audit entries for this week.")
})

it("lists entries with the clinician as subject in rotation view", () => {
expect.assertions(3)

const wrapper = mountContent({ viewMode: "rotation", entries: [makeEntry()] })

expect(wrapper.find(".week-history__subject").text()).toBe("Dr. Example")
expect(wrapper.text()).toContain("Added to rotation")
expect(wrapper.text()).toContain("Manager Person")
})

it("lists entries with the rotation as subject in clinician view", () => {
expect.assertions(1)

const wrapper = mountContent({ viewMode: "clinician", entries: [makeEntry()] })

expect(wrapper.find(".week-history__subject").text()).toBe("Cardiology")
})

it("labels the week navigation with the current week and date", () => {
expect.assertions(2)

const { formatDate } = useDateFunctions()
const wrapper = mountContent({ weekNumber: 3, weekDateStart: "2026-01-05" })

const label = wrapper.find(".week-history__nav-label").text()
expect(label).toContain("Week 3")
expect(label).toContain(formatDate("2026-01-05"))
})

it("emits prev/next when the enabled nav controls are clicked", async () => {
expect.assertions(2)

const wrapper = mountContent({ canPrev: true, canNext: true })

await wrapper.find("[aria-label='Previous week']").trigger("click")
await wrapper.find("[aria-label='Next week']").trigger("click")

expect(wrapper.emitted("prev")).toHaveLength(1)
expect(wrapper.emitted("next")).toHaveLength(1)
})

it("disables nav controls at the schedule ends", () => {
expect.assertions(2)

const wrapper = mountContent({ canPrev: false, canNext: false })

expect(wrapper.find("[aria-label='Previous week']").classes()).toContain("disabled")
expect(wrapper.find("[aria-label='Next week']").classes()).toContain("disabled")
})

it("shows skeleton rows only on the initial load (no entries yet)", () => {
expect.assertions(2)

const wrapper = mountContent({ isLoading: true, entries: [] })

expect(wrapper.findAll(".week-history__skeleton-row")).toHaveLength(3)
expect(wrapper.find(".week-history__list").exists()).toBeFalsy()
})

it("keeps the current rows (dimmed) instead of a skeleton while paging weeks", () => {
expect.assertions(3)

// Entries already present while loading = paging to another week
const wrapper = mountContent({ isLoading: true, entries: [makeEntry()] })

expect(wrapper.findAll(".week-history__skeleton-row")).toHaveLength(0)
const list = wrapper.find(".week-history__list")
expect(list.exists()).toBeTruthy()
expect(list.classes()).toContain("week-history__list--loading")
})

it("shows the loading progress bar only while a fetch is in flight", () => {
expect.assertions(2)

const loading = mountContent({ isLoading: true, entries: [makeEntry()] })
const idle = mountContent({ isLoading: false, entries: [makeEntry()] })

expect(loading.find(".week-history__progress").exists()).toBeTruthy()
expect(idle.find(".week-history__progress").exists()).toBeFalsy()
})
})
Loading
Loading