Skip to content

Commit e2bc753

Browse files
committed
VPR-104 feat(cts): redesign MyAssessments page and AssessmentBubble
- Add EpaProgressionChart, EpaSupervisionBlend, and EpaVoiceThread components to render assessment history - Add level-labels utility for consistent supervision-level naming - Add AssessmentBubble test coverage
1 parent 8227e33 commit e2bc753

8 files changed

Lines changed: 1615 additions & 127 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { mount } from "@vue/test-utils"
2+
import { Quasar } from "quasar"
3+
4+
import AssessmentBubble from "../components/AssessmentBubble.vue"
5+
6+
/**
7+
* Tests for AssessmentBubble — the rating dot rendered on CTS assessment lists.
8+
*
9+
* Focus areas:
10+
* 1. Privacy: aria-label must surface the descriptive rating label, never the
11+
* numeric value. Students should not hear "Rating 1 of 5" from a screen
12+
* reader when they are low-rated.
13+
* 2. Class mapping: value/maxValue drive the bubbleClass contract consumed
14+
* by cts.css.
15+
* 3. Click contract: clickable variant (id prop set) emits bubble-click with
16+
* the id; non-clickable variant renders as a non-interactive span.
17+
*/
18+
19+
function createWrapper(props: Record<string, unknown>) {
20+
return mount(AssessmentBubble, {
21+
props: props as never,
22+
global: {
23+
plugins: [[Quasar, {}]],
24+
},
25+
})
26+
}
27+
28+
describe(AssessmentBubble, () => {
29+
describe("aria-label privacy", () => {
30+
it("uses levelName on the clickable button and does not expose the numeric value", () => {
31+
const wrapper = createWrapper({
32+
maxValue: 5,
33+
value: 1,
34+
levelName: "Trust with indirect supervision",
35+
id: 42,
36+
})
37+
38+
const label = wrapper.get("button").attributes("aria-label")!
39+
expect(label).toContain("Trust with indirect supervision")
40+
expect(label).not.toMatch(/\b1 of 5\b/i)
41+
expect(label).not.toMatch(/rating\s+\d/i)
42+
})
43+
44+
it("uses levelName on the standalone span and does not expose the numeric value", () => {
45+
const wrapper = createWrapper({
46+
maxValue: 5,
47+
value: 2,
48+
levelName: "Trust with direct supervision",
49+
})
50+
51+
const label = wrapper.get('span[role="img"]').attributes("aria-label")!
52+
expect(label).toBe("Trust with direct supervision")
53+
expect(label).not.toMatch(/\b2 of 5\b/i)
54+
})
55+
56+
it("appends open-details hint on the clickable variant", () => {
57+
const wrapper = createWrapper({
58+
maxValue: 5,
59+
value: 3,
60+
levelName: "Independent remote supervision",
61+
id: 7,
62+
})
63+
64+
expect(wrapper.get("button").attributes("aria-label")).toBe(
65+
"Independent remote supervision, open assessment details",
66+
)
67+
})
68+
69+
it("falls back to a generic hint when levelName is missing on a clickable bubble", () => {
70+
const wrapper = createWrapper({
71+
maxValue: 5,
72+
value: 3,
73+
id: 7,
74+
})
75+
76+
expect(wrapper.get("button").attributes("aria-label")).toBe("Open assessment details")
77+
})
78+
})
79+
80+
describe("bubbleClass contract", () => {
81+
it.each([
82+
[1, "assessmentBubble5_1"],
83+
[2, "assessmentBubble5_2"],
84+
[3, "assessmentBubble5_3"],
85+
[4, "assessmentBubble5_4"],
86+
[5, "assessmentBubble5_5"],
87+
])("maps value=%i to %s", (value, expected) => {
88+
const wrapper = createWrapper({
89+
maxValue: 5,
90+
value,
91+
levelName: "Label",
92+
})
93+
94+
expect(wrapper.get('span[role="img"]').classes()).toContain(expected)
95+
})
96+
97+
it.each([0, 6])("yields no level class for out-of-range value=%i", (value) => {
98+
const wrapper = createWrapper({
99+
maxValue: 5,
100+
value,
101+
levelName: "Label",
102+
})
103+
104+
const classes = wrapper.get('span[role="img"]').classes()
105+
expect(classes.some((c) => c.startsWith("assessmentBubble5_"))).toBeFalsy()
106+
})
107+
108+
it("yields no level class when maxValue is not 5", () => {
109+
const wrapper = createWrapper({
110+
maxValue: 3,
111+
value: 2,
112+
levelName: "Label",
113+
})
114+
115+
const classes = wrapper.get('span[role="img"]').classes()
116+
expect(classes.some((c) => c.startsWith("assessmentBubble5_"))).toBeFalsy()
117+
})
118+
})
119+
120+
describe("click behaviour", () => {
121+
it("renders a button and emits bubble-click with the id when clicked", async () => {
122+
const wrapper = createWrapper({
123+
maxValue: 5,
124+
value: 3,
125+
levelName: "Label",
126+
id: 99,
127+
})
128+
129+
await wrapper.get("button").trigger("click")
130+
131+
expect(wrapper.emitted("bubble-click")).toEqual([[99]])
132+
})
133+
134+
it("renders a non-interactive span and does not emit when id is omitted", () => {
135+
const wrapper = createWrapper({
136+
maxValue: 5,
137+
value: 3,
138+
levelName: "Label",
139+
})
140+
141+
expect(wrapper.find("button").exists()).toBeFalsy()
142+
expect(wrapper.find('span[role="img"]').exists()).toBeTruthy()
143+
expect(wrapper.emitted("bubble-click")).toBeUndefined()
144+
})
145+
})
146+
147+
describe("bubble content", () => {
148+
it("does not render the numeric value inside the bubble", () => {
149+
const wrapper = createWrapper({
150+
maxValue: 5,
151+
value: 4,
152+
levelName: "Label",
153+
id: 1,
154+
})
155+
156+
const bubble = wrapper.get("span.assessmentBubble")
157+
expect(bubble.text()).toBe("")
158+
})
159+
})
160+
})

0 commit comments

Comments
 (0)