Skip to content

Commit b580d92

Browse files
RenzoMinelliclaude
andcommitted
test: add specs for subject graph feature
Component specs (11): - Node data serialization (id, code, name, url, availability) - Edge extraction from prerequisites (subject, logical/AND, external) - Semester label generation (curriculum categories, planner map) - Empty state handling System specs - Planner Graph (6): - Graph rendering with planned subjects and edges - Empty state with link to add subjects - Legend with highlight indicators (Previa, Dependiente) - Interaction instructions text - Available subject marking System specs - Subjects Graph (7): - Category filtering (single, multiple) - Edge rendering within filtered subjects - Empty state when no categories - Full legend display (all 5 indicators) - Interaction instructions text - Available subjects based on user approvals Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 28ffdcf commit b580d92

3 files changed

Lines changed: 357 additions & 0 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
require "rails_helper"
2+
3+
RSpec.describe SubjectGraphComponent, type: :component do
4+
let(:user) { build_stubbed(:user) }
5+
let(:student) { UserStudent.new(user) }
6+
7+
context "with subjects" do
8+
it "renders the graph container with data attributes" do
9+
subject1 = create(:subject, name: "Subject 1", code: "S1")
10+
11+
render_inline(described_class.new(subjects: [subject1], current_student: student))
12+
13+
expect(page).to have_css "[data-controller='subject-graph']"
14+
expect(page).to have_css "[data-subject-graph-nodes-value]"
15+
expect(page).to have_css "[data-subject-graph-edges-value]"
16+
end
17+
18+
it "includes subject data in nodes" do
19+
subject1 = create(:subject, name: "Test Subject", short_name: "TS", code: "TS1")
20+
21+
render_inline(described_class.new(subjects: [subject1], current_student: student))
22+
23+
node_data = page.find("[data-subject-graph-nodes-value]")["data-subject-graph-nodes-value"]
24+
nodes = JSON.parse(node_data)
25+
26+
expect(nodes.length).to eq(1)
27+
expect(nodes.first["id"]).to eq(subject1.id)
28+
expect(nodes.first["code"]).to eq("TS1")
29+
expect(nodes.first["name"]).to eq("TS")
30+
expect(nodes.first["url"]).to eq("/materias/#{subject1.id}")
31+
end
32+
33+
it "uses full name when short_name is not present" do
34+
subject1 = create(:subject, name: "Full Name Subject", short_name: nil, code: "FN1")
35+
36+
render_inline(described_class.new(subjects: [subject1], current_student: student))
37+
38+
node_data = page.find("[data-subject-graph-nodes-value]")["data-subject-graph-nodes-value"]
39+
nodes = JSON.parse(node_data)
40+
41+
expect(nodes.first["name"]).to eq("Full Name Subject")
42+
end
43+
44+
it "marks available subjects as available" do
45+
subject1 = create(:subject, name: "Available Subject", code: "AV1")
46+
47+
render_inline(described_class.new(subjects: [subject1], current_student: student))
48+
49+
node_data = page.find("[data-subject-graph-nodes-value]")["data-subject-graph-nodes-value"]
50+
nodes = JSON.parse(node_data)
51+
52+
expect(nodes.first["available"]).to be true
53+
end
54+
end
55+
56+
context "with prerequisites" do
57+
it "creates edges for subject prerequisites within the subject set" do
58+
subject1 = create(:subject, name: "Prereq", code: "PR1")
59+
subject2 = create(:subject, name: "Dependent", code: "DE1")
60+
61+
create(:subject_prerequisite, approvable: subject2.course, approvable_needed: subject1.course)
62+
63+
render_inline(described_class.new(subjects: [subject1, subject2], current_student: student))
64+
65+
edge_data = page.find("[data-subject-graph-edges-value]")["data-subject-graph-edges-value"]
66+
edges = JSON.parse(edge_data)
67+
68+
expect(edges.length).to eq(1)
69+
expect(edges.first["source"]).to eq(subject1.id)
70+
expect(edges.first["target"]).to eq(subject2.id)
71+
end
72+
73+
it "does not create edges for prerequisites outside the subject set" do
74+
subject1 = create(:subject, name: "Outside", code: "OU1")
75+
subject2 = create(:subject, name: "Inside", code: "IN1")
76+
77+
create(:subject_prerequisite, approvable: subject2.course, approvable_needed: subject1.course)
78+
79+
# Only include subject2, not subject1
80+
render_inline(described_class.new(subjects: [subject2], current_student: student))
81+
82+
edge_data = page.find("[data-subject-graph-edges-value]")["data-subject-graph-edges-value"]
83+
edges = JSON.parse(edge_data)
84+
85+
expect(edges).to be_empty
86+
end
87+
88+
it "handles logical prerequisites with multiple operands" do
89+
subject1 = create(:subject, name: "Prereq 1", code: "PR1")
90+
subject2 = create(:subject, name: "Prereq 2", code: "PR2")
91+
subject3 = create(:subject, name: "Dependent", code: "DE1")
92+
93+
create(:and_prerequisite, approvable: subject3.course, operands_prerequisites: [
94+
create(:subject_prerequisite, approvable_needed: subject1.course),
95+
create(:subject_prerequisite, approvable_needed: subject2.course),
96+
])
97+
98+
render_inline(described_class.new(subjects: [subject1, subject2, subject3], current_student: student))
99+
100+
edge_data = page.find("[data-subject-graph-edges-value]")["data-subject-graph-edges-value"]
101+
edges = JSON.parse(edge_data)
102+
103+
expect(edges.length).to eq(2)
104+
sources = edges.map { |e| e["source"] }
105+
expect(sources).to contain_exactly(subject1.id, subject2.id)
106+
expect(edges.all? { |e| e["target"] == subject3.id }).to be true
107+
end
108+
end
109+
110+
context "semester labels" do
111+
it "renders semester-labels data attribute" do
112+
subject1 = create(:subject, name: "Subject 1", code: "S1", category: "first_semester")
113+
114+
render_inline(described_class.new(subjects: [subject1], current_student: student))
115+
116+
expect(page).to have_css "[data-subject-graph-semester-labels-value]"
117+
end
118+
119+
it "produces correct labels for curriculum graph" do
120+
subject1 = create(:subject, name: "Subject 1", code: "S1", category: "first_semester")
121+
subject2 = create(:subject, name: "Subject 2", code: "S2", category: "second_semester")
122+
123+
render_inline(described_class.new(subjects: [subject1, subject2], current_student: student))
124+
125+
labels_data = page.find("[data-subject-graph-semester-labels-value]")["data-subject-graph-semester-labels-value"]
126+
labels = JSON.parse(labels_data)
127+
128+
expect(labels["1"]).to eq("Primer semestre")
129+
expect(labels["2"]).to eq("Segundo semestre")
130+
end
131+
132+
it "produces correct labels for planner graph with semester_map" do
133+
subject1 = create(:subject, name: "Subject 1", code: "S1")
134+
subject2 = create(:subject, name: "Subject 2", code: "S2")
135+
136+
semester_map = { subject1.id => 3, subject2.id => 5 }
137+
138+
render_inline(described_class.new(subjects: [subject1, subject2], current_student: student,
139+
semester_map: semester_map))
140+
141+
labels_data = page.find("[data-subject-graph-semester-labels-value]")["data-subject-graph-semester-labels-value"]
142+
labels = JSON.parse(labels_data)
143+
144+
expect(labels["3"]).to eq("Semestre 3")
145+
expect(labels["5"]).to eq("Semestre 5")
146+
end
147+
end
148+
149+
context "without subjects" do
150+
it "renders empty arrays for nodes and edges" do
151+
render_inline(described_class.new(subjects: [], current_student: student))
152+
153+
node_data = page.find("[data-subject-graph-nodes-value]")["data-subject-graph-nodes-value"]
154+
edge_data = page.find("[data-subject-graph-edges-value]")["data-subject-graph-edges-value"]
155+
156+
expect(JSON.parse(node_data)).to be_empty
157+
expect(JSON.parse(edge_data)).to be_empty
158+
end
159+
end
160+
end

spec/system/planner_graph_spec.rb

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
require 'rails_helper'
2+
3+
RSpec.describe "Planner Graph", type: :system do
4+
let(:user) { create(:user) }
5+
6+
before do
7+
sign_in user
8+
end
9+
10+
it "displays graph with planned subjects" do
11+
subject1 = create(:subject, name: "Calculo 1", code: "C1")
12+
subject2 = create(:subject, name: "Calculo 2", code: "C2")
13+
14+
create(:subject_prerequisite, approvable: subject2.course, approvable_needed: subject1.course)
15+
16+
create(:subject_plan, user: user, subject: subject1, semester: 1)
17+
create(:subject_plan, user: user, subject: subject2, semester: 2)
18+
19+
visit planner_graph_path
20+
21+
expect(page).to have_css "[data-controller='subject-graph']"
22+
expect(page).to have_css "[data-subject-graph-nodes-value]"
23+
expect(page).to have_css "[data-subject-graph-edges-value]"
24+
expect(page).to have_css "[data-subject-graph-semester-labels-value]"
25+
26+
node_data = find("[data-subject-graph-nodes-value]")["data-subject-graph-nodes-value"]
27+
nodes = JSON.parse(node_data)
28+
29+
expect(nodes.length).to eq(2)
30+
expect(nodes.map { |n| n["code"] }).to contain_exactly("C1", "C2")
31+
end
32+
33+
it "shows empty state when no planned subjects" do
34+
visit planner_graph_path
35+
36+
expect(page).to have_text "No tienes materias planificadas"
37+
expect(page).to have_link "Agregar materias"
38+
end
39+
40+
it "includes edges for prerequisites within planned subjects" do
41+
subject1 = create(:subject, name: "Base", code: "B1")
42+
subject2 = create(:subject, name: "Advanced", code: "A1")
43+
44+
create(:subject_prerequisite, approvable: subject2.course, approvable_needed: subject1.course)
45+
46+
create(:subject_plan, user: user, subject: subject1, semester: 1)
47+
create(:subject_plan, user: user, subject: subject2, semester: 2)
48+
49+
visit planner_graph_path
50+
51+
edge_data = find("[data-subject-graph-edges-value]")["data-subject-graph-edges-value"]
52+
edges = JSON.parse(edge_data)
53+
54+
expect(edges.length).to eq(1)
55+
expect(edges.first["source"]).to eq(subject1.id)
56+
expect(edges.first["target"]).to eq(subject2.id)
57+
end
58+
59+
it "displays legend with highlight indicators" do
60+
subject1 = create(:subject, name: "Subject", code: "S1")
61+
create(:subject_plan, user: user, subject: subject1, semester: 1)
62+
63+
visit planner_graph_path
64+
65+
expect(page).to have_text "Previa"
66+
expect(page).to have_text "Dependiente"
67+
end
68+
69+
it "displays interaction instructions" do
70+
subject1 = create(:subject, name: "Subject", code: "S1")
71+
create(:subject_plan, user: user, subject: subject1, semester: 1)
72+
73+
visit planner_graph_path
74+
75+
expect(page).to have_text("previas", normalize_ws: true)
76+
end
77+
78+
it "marks available subjects in the graph data" do
79+
subject1 = create(:subject, name: "Available", code: "AV")
80+
create(:subject_plan, user: user, subject: subject1, semester: 1)
81+
82+
visit planner_graph_path
83+
84+
node_data = find("[data-subject-graph-nodes-value]")["data-subject-graph-nodes-value"]
85+
nodes = JSON.parse(node_data)
86+
87+
available_node = nodes.find { |n| n["code"] == "AV" }
88+
expect(available_node["available"]).to be true
89+
end
90+
end

spec/system/subjects_graph_spec.rb

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
require 'rails_helper'
2+
3+
RSpec.describe "Subjects Graph", type: :system do
4+
let(:user) { create(:user) }
5+
6+
before do
7+
sign_in user
8+
end
9+
10+
it "displays graph filtered by single category" do
11+
create(:subject, name: "First Sem Subject", code: "FS1", category: "first_semester")
12+
create(:subject, name: "Second Sem Subject", code: "SS1", category: "second_semester")
13+
14+
visit subjects_graph_path(categories: ["first_semester"])
15+
16+
expect(page).to have_css "[data-controller='subject-graph']"
17+
expect(page).to have_css "[data-subject-graph-semester-labels-value]"
18+
19+
node_data = find("[data-subject-graph-nodes-value]")["data-subject-graph-nodes-value"]
20+
nodes = JSON.parse(node_data)
21+
22+
expect(nodes.length).to eq(1)
23+
expect(nodes.first["code"]).to eq("FS1")
24+
end
25+
26+
it "displays graph filtered by multiple categories" do
27+
create(:subject, name: "First Sem Subject", code: "FS1", category: "first_semester")
28+
create(:subject, name: "Second Sem Subject", code: "SS1", category: "second_semester")
29+
create(:subject, name: "Third Sem Subject", code: "TS1", category: "third_semester")
30+
31+
visit subjects_graph_path(categories: ["first_semester", "second_semester"])
32+
33+
node_data = find("[data-subject-graph-nodes-value]")["data-subject-graph-nodes-value"]
34+
nodes = JSON.parse(node_data)
35+
36+
expect(nodes.length).to eq(2)
37+
expect(nodes.map { |n| n["code"] }).to contain_exactly("FS1", "SS1")
38+
end
39+
40+
it "shows edges for prerequisites within filtered subjects" do
41+
subject1 = create(:subject, name: "Intro", code: "IN1", category: "first_semester")
42+
subject2 = create(:subject, name: "Advanced", code: "AD1", category: "first_semester")
43+
44+
create(:subject_prerequisite, approvable: subject2.course, approvable_needed: subject1.course)
45+
46+
visit subjects_graph_path(categories: ["first_semester"])
47+
48+
edge_data = find("[data-subject-graph-edges-value]")["data-subject-graph-edges-value"]
49+
edges = JSON.parse(edge_data)
50+
51+
expect(edges.length).to eq(1)
52+
expect(edges.first["source"]).to eq(subject1.id)
53+
expect(edges.first["target"]).to eq(subject2.id)
54+
end
55+
56+
it "shows empty state when no categories provided" do
57+
create(:subject, name: "Some Subject", code: "SS1", category: "first_semester")
58+
59+
visit subjects_graph_path
60+
61+
expect(page).to have_text "No hay materias para mostrar"
62+
end
63+
64+
it "displays legend with all state and highlight indicators" do
65+
create(:subject, name: "Subject", code: "S1", category: "first_semester")
66+
67+
visit subjects_graph_path(categories: ["first_semester"])
68+
69+
expect(page).to have_text "No disponible"
70+
expect(page).to have_text "Disponible"
71+
expect(page).to have_text "Completada"
72+
expect(page).to have_text "Previa"
73+
expect(page).to have_text "Dependiente"
74+
end
75+
76+
it "displays interaction instructions" do
77+
create(:subject, name: "Subject", code: "S1", category: "first_semester")
78+
79+
visit subjects_graph_path(categories: ["first_semester"])
80+
81+
expect(page).to have_text("previas", normalize_ws: true)
82+
end
83+
84+
it "marks available subjects based on user approvals" do
85+
prereq_subject = create(:subject, name: "Prerequisite", code: "PR1", category: "first_semester")
86+
dependent_subject = create(:subject, name: "Dependent", code: "DE1", category: "first_semester")
87+
88+
create(:subject_prerequisite, approvable: dependent_subject.course, approvable_needed: prereq_subject.course)
89+
90+
# Approve the prerequisite
91+
user.approvals << prereq_subject.course.id
92+
user.save!
93+
94+
visit subjects_graph_path(categories: ["first_semester"])
95+
96+
node_data = find("[data-subject-graph-nodes-value]")["data-subject-graph-nodes-value"]
97+
nodes = JSON.parse(node_data)
98+
99+
prereq_node = nodes.find { |n| n["code"] == "PR1" }
100+
dependent_node = nodes.find { |n| n["code"] == "DE1" }
101+
102+
# Prereq has no prereqs, so it's available
103+
expect(prereq_node["available"]).to be true
104+
# Dependent should now be available since prereq is approved
105+
expect(dependent_node["available"]).to be true
106+
end
107+
end

0 commit comments

Comments
 (0)