-
Notifications
You must be signed in to change notification settings - Fork 6
/
test-html.rb
365 lines (280 loc) · 11.7 KB
/
test-html.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
require 'yaml'
require 'html-proofer'
# Tests have to inherit the following class
class ValidateCoursePage < ::HTMLProofer::Check
# This class is a test to make sure that each course page
# generated by jekyll has been generated correctly so that
# the assumptions made by the javascript about the structure
# of the page is valid.
# This class checks the following:
# - The page displays the correct title
# - The page contains a "clear selected" button
# - The page corresponding to a course has a div with the
# class 'planner'.
# - The page contains a many years as defined in the data
# - The page has a credit counter for each year
# - Each module is on the page once and once only
# - Each course isn't displaying modules it shouldn't, or missing modules it
# should be showing
# -----------------------------------------------------
#
# DATA FUNCTIONS
#
# -----------------------------------------------------
# These functions are helpers for getting data out of the database for us to
# check our HTML against
# Load the data from the database, and 'attach' it to the class instance
def loadData
@courses = YAML.load_file(File.expand_path('../../_data/courses.yml', __FILE__))
@modules = YAML.load_file(File.expand_path('../../_data/modules.yml', __FILE__))
@settings = YAML.load_file(File.expand_path('../../_data/settings.yml', __FILE__))
end
# Extract all the course filenames from the database
def loadCourseNames
names = @courses.map { |c| c["short"] }
return names
end
# This, given a short name will load the details of a course
def loadCourseDetail(name)
return @courses.detect { |c| c["short"] == name }
end
# This returns all the module codes attached to a course
# NOTE: This function does not preserve degree structure!
def getAllModuleCodes(course)
modules = []
for year in course["modules"]
# Not every year will have core modules
if not year["core"].nil?
modules.concat(year["core"])
end
# Not every year will have optional modules
if not year["optional"].nil?
modules.concat(year["optional"])
end
end
return modules
end
# This function returns the data associated with the given id
def getModuleByCode(id)
for m in @modules
if m["code"] == id
return m
end
end
end
# ----------------------------------------------
#
# LAYOUT FUNCTIONS
#
# ----------------------------------------------
# These functions test the general layout of the page, are there any elements
# missing, too many etc?
# This is the first of the test functions. Quite simple, it just checks
# if the title of the page has a title and that it matches the course name as
# per the yaml data
def correctTitle?(course)
title = @html.css('div.wrapper h1').text
if blank?(title)
add_issue("Page is missing the course name as a title")
elsif title != course["name"]
add_issue("Page is not displaying the correct course title")
end
end
# This tests for the existence of a clear selected button
def clearSelected?
button = @html.css("div.wrapper h2.clear").text
if blank?(button)
add_issue("Page is missing a clear selected button")
end
end
# Checks to see if the page has a div with the planner class
def plannerClass?
div = @html.css("div.planner")
if blank?(div)
add_issue("Page does not contain a planner.")
end
end
# Checks to see that the number of years matches the data and that
# they contain the year as an id number
def allYears?(course)
years = @html.css("div.year")
if years.length != course["modules"].length
add_issue("Page doesn't display the correct number of years " +
"#{years.length} when it should be #{course["modules"].length}")
end
year_numbers = (1..years.length).map { |n| "year#{n}" }
for year in years
y = create_element(year)
# Do they all have an id number?
if not year_numbers.include? y.id
add_issue("Year #{y.id} has unexpected id tag")
end
end
end
# Checks to see that each year on the page has a credit counter.
def allCounters?(course)
years = @html.css("div.year")
counters = years.css("h3 > span.credit")
if counters.length != course["modules"].length
add_issue("Page contains an incorrect number of credit counters")
end
end
# Checks to make sure the correct modules are included once and once only
def allModules?(course)
modules = @html.css("div.module")
# Get all of their ids
modids = modules.map { |m| m["id"] }
if modids.length != modids.uniq.length
add_issue("Page contains duplicate modules!")
end
if modids.length > getAllModuleCodes(course).length
add_issue("Page contains modules that are not offered by this course!")
end
if modules.length < getAllModuleCodes(course).length
add_issue("Page is missing modules from this course!")
end
end
# --------------------------------------------------
#
# MODULE FUNCTIONS
#
# --------------------------------------------------
# These functions are responsible for testing that the layout of each module
# box is correct and that it presents the data in the HTML in a way in which
# our javascript is expecting and will know how to handle
# This function checks to see that the module is displaying the correct title
def moduleName?(m, mspec)
title = m.css("h2").text
if title != mspec["name"]
add_issue("Module #{mspec["code"]} is displaying an incorrect title! (#{title})")
end
end
# This function checks that the module is displaying the correct code
def moduleCode?(m, mspec)
code = m.css("h3").text
if code != mspec["code"].upcase
add_issue("Module #{mspec["code"]} is displaying an incorrect code! (#{code})")
end
end
# This function checks that the module is displaying the correct credit score
def moduleCredits?(m, mspec)
credit_value = m.css("div.group > aside > span.credit").text
if Integer(credit_value) != mspec["credits"]
add_issue("Module #{mspec["code"]} is displaying n incorrect credit value! (#{credit_value})")
end
end
# This test ensures that the module's more info link it pointing at the correct place
def moduleLink?(m, mspec)
link = m.css("div.group > a")[0]['href']
# TODO: Make this link a variable that is easily set by whoever uses this.
if link != "#{@settings["info"]}/#{mspec["code"].upcase}.html"
add_issue("Module #{mspec["code"]} is not linking to the correct resource! (#{link})")
end
end
# This function checks to see that a given module provides for the modules that
# it should
def moduleProvides?(m, mspec)
# This one is interesting since the provides aren't explicitly stored in the
# database. So the way we will test this is to grab all the modules referenced
# and see if they do indeed require this module.
# NOTE: This will only catch the cases where a module is providing for a non
# existent module, or a module which doesn't depend on this. It will NOT catch
# the cases where the module should be providing for a certain moudle and isn't
# We will handle this case when we are checking the requires part of the module.
provides = m.css("div.provides > span")
# Check see if there is anything to test
if not provides.empty?
# For each module
for span in provides
code = span["class"]
# Load its details from the database
mod = getModuleByCode(code)
# Do we really have to provide for this module
if not mod["requires"].include? mspec["code"]
add_issue("Module #{mspec["code"]} is providing for a module that it shouldn't be! (#{mod["code"]})")
end
end
end
end
# This function checks to see that a given module requires for the modules is
# should
def moduleRequires?(m, mspec)
requires = m.css("ul.requires > li")
# Check to see that the module is actaully listing requirements
if mspec["requires"] and requires.empty?
add_issue("Module #{mspec["code"]} is not listing any of its requirements!")
end
# Are there requirements in this moudle to test?
if not requires.empty?
# For each requirement
for requirement in requires
code = requirement["class"]
# Is this module actually required?
if not mspec["requires"].include? code
add_issue("Module #{mspec["code"]} is requiring a module that it shouldn't! (#{code})")
end
# Is there a link to the correct required module on the page?
link = requirement.css("a")[0]["href"]
if link != "##{code}"
add_issue("Requirement #{code} for module #{mspec["code"]} does not have a link!")
end
# Load the module which is required
reqmod = getModuleByCode(code)
# Is the title correct?
name = requirement.text
if reqmod["name"] != name
add_issue("Requirement #{code} for module #{mspec["code"]} has an incorrect title! (#{name})")
end
# Finally is that module providing for us (this will catch the last case
# in the previous test we couldn't cover)
provides = @html.css("div.module##{code} > div.provides > span")
pcodes = provides.map { |p| p["class"]}
if not pcodes.include? mspec["code"]
add_issue("Requirement module #{code} is not providing for #{mspec["code"]}!")
end
end
end
end
# This is actually what is called from HTMLProofer and where we
# raise all our issues from
def run
# First we need to load the data from the database
loadData
# Now get the list of courses from the database
courses = loadCourseNames
# Next extract the name of the page from the path.
name = File.basename(@path, ".html")
# Check to see if we have any work to do
# TODO: Include a check to ensure each course is tested once.
if courses.include? name
# Load the details of the course
course = loadCourseDetail(name)
# Run all the layout tests
correctTitle?(course) # Does the page have the correct title?
clearSelected? # Does the page have a clear selected button?
plannerClass? # Does the page have a planner class?
allYears?(course) # Does the page contain all the years as defined in the data?
allCounters?(course) # Does the page have a credit counter for each year?
allModules?(course) # Does each module appear on the page once only.
# Get all the module codes for the course
modids = getAllModuleCodes(course)
# For each modules, check that it conforms to the expected format
for modid in modids
# Get the module from the page
m = @html.css("div.module##{modid}")
# Get the module from the database
mspec = getModuleByCode(modid)
# Run the module tests
moduleName?(m, mspec) # Is the module displaying the correct name?
moduleCode?(m, mspec) # Is the module displaying the correct code?
moduleCredits?(m, mspec) # Is the module displaying the correct number of credits?
moduleLink?(m, mspec) # Is the more info link going to the right place?
moduleProvides?(m, mspec) # Is the module providing for the correct modules?
moduleRequires?(m, mspec) # Is the module requireing the correct modules?
end
return
end
# As long as we get to the end of the function without raising any issues
# we assume that there wasn't any problems and the test will pass
end
end