rubyuw 0.99.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gitignore +4 -0
  2. data/README.md +81 -0
  3. data/Rakefile +45 -0
  4. data/VERSION +1 -0
  5. data/lib/rubyuw.rb +9 -0
  6. data/lib/rubyuw/base.rb +122 -0
  7. data/lib/rubyuw/connection.rb +145 -0
  8. data/lib/rubyuw/curriculum_enrollment.rb +75 -0
  9. data/lib/rubyuw/errors.rb +16 -0
  10. data/lib/rubyuw/schedule.rb +52 -0
  11. data/lib/rubyuw/sln.rb +108 -0
  12. data/rubyuw.gemspec +97 -0
  13. data/test/live/curriculum_enrollment_test.rb +47 -0
  14. data/test/live/sln_test.rb +54 -0
  15. data/test/live/test_helper.rb +24 -0
  16. data/test/mocked/base_test.rb +171 -0
  17. data/test/mocked/connection_test.rb +178 -0
  18. data/test/mocked/curriculum_enrollment_test.rb +158 -0
  19. data/test/mocked/fixture_pages/bad_request.html +80 -0
  20. data/test/mocked/fixture_pages/curric_courses.html +2659 -0
  21. data/test/mocked/fixture_pages/curric_no_courses.html +88 -0
  22. data/test/mocked/fixture_pages/curric_no_curric_found.html +86 -0
  23. data/test/mocked/fixture_pages/logged_in_relay.html +660 -0
  24. data/test/mocked/fixture_pages/login.html +121 -0
  25. data/test/mocked/fixture_pages/no_form_login.html +122 -0
  26. data/test/mocked/fixture_pages/no_login_button.html +100 -0
  27. data/test/mocked/fixture_pages/not_logged_in_relay.html +15 -0
  28. data/test/mocked/fixture_pages/registration.html +699 -0
  29. data/test/mocked/fixture_pages/registration_error.html +699 -0
  30. data/test/mocked/fixture_pages/registration_error_no_rows.html +627 -0
  31. data/test/mocked/fixture_pages/registration_no_form.html +700 -0
  32. data/test/mocked/fixture_pages/registration_success.html +718 -0
  33. data/test/mocked/fixture_pages/registration_unknown.html +721 -0
  34. data/test/mocked/fixture_pages/schedule.html +166 -0
  35. data/test/mocked/fixture_pages/schedule_diff_credit_row.html +167 -0
  36. data/test/mocked/fixture_pages/schedule_empty.html +130 -0
  37. data/test/mocked/fixture_pages/schedule_no_credit_row.html +169 -0
  38. data/test/mocked/fixture_pages/sln_does_not_exist.html +85 -0
  39. data/test/mocked/fixture_pages/sln_no_course_info.html +94 -0
  40. data/test/mocked/fixture_pages/sln_no_course_notes.html +94 -0
  41. data/test/mocked/fixture_pages/sln_no_enrollment_info.html +94 -0
  42. data/test/mocked/fixture_pages/sln_status.html +92 -0
  43. data/test/mocked/fixture_pages/welcome.html +93 -0
  44. data/test/mocked/schedule_test.rb +89 -0
  45. data/test/mocked/sln_test.rb +146 -0
  46. data/test/mocked/test_helper.rb +67 -0
  47. data/test/password.rb.sample +8 -0
  48. metadata +118 -0
@@ -0,0 +1,16 @@
1
+ module RubyUW
2
+ module Errors
3
+ # Exception signaling an invalid page was encountered.
4
+ class InvalidPageError < Exception
5
+ attr_reader :page
6
+
7
+ def initialize(page, msg=nil)
8
+ @page = page
9
+ super(msg)
10
+ end
11
+ end
12
+
13
+ # Exception when login is required and not logged in.
14
+ class NotLoggedInError < Exception; end
15
+ end
16
+ end
@@ -0,0 +1,52 @@
1
+ module RubyUW
2
+ # RubyUW::Schedule is used to grab the schedule of the
3
+ # authenticated user.
4
+ #
5
+ # <b>Requires authentication with RubyUW::Base prior to use!</b>
6
+ class Schedule
7
+ class << self
8
+ # Scrapes the authenticated user's schedule from MyUW.
9
+ # Authentication is required prior to use.
10
+ #
11
+ # Returns an array of {RubyUW::SLN} objects. The {RubyUW::SLN}
12
+ # object responds to the following data elements:
13
+ #
14
+ # * sln
15
+ # * course
16
+ # * type
17
+ # * credits
18
+ # * title
19
+ # * days
20
+ # * time
21
+ # * location
22
+ # * instructor
23
+ #
24
+ # To get any more details you have to use the {RubyUW::SLN.find}
25
+ # method.
26
+ def get
27
+ raise Errors::NotLoggedInError.new unless Base.authenticated?
28
+
29
+ page = Base.connection.get("https://sdb.admin.washington.edu/students/uwnetid/schedule.asp")
30
+ extract_courses(page)
31
+ end
32
+
33
+ protected
34
+
35
+ def extract_courses(page)
36
+ course_nodes = page.search("//table[@border=1 and @cellpadding=3]//tr[@bgcolor='#d0d0d0'][2]/following-sibling::*//td[@align='center']/parent::*")
37
+ courses = []
38
+ course_nodes.each do |node|
39
+ course = extract_course(node)
40
+ courses << RubyUW::SLN.new(course[:sln], nil, course)
41
+ end
42
+
43
+ courses
44
+ end
45
+
46
+ def extract_course(node)
47
+ keys = [:sln, :course, :type, :credits, :title, :days, :time, :location, :instructor]
48
+ Base.extract(node, 'td', keys)
49
+ end
50
+ end
51
+ end
52
+ end
data/lib/rubyuw/sln.rb ADDED
@@ -0,0 +1,108 @@
1
+ module RubyUW
2
+ # RubyUW::SLN is used to find and extract information about
3
+ # specific SLNs. It grabs SLN information via the MyUW time
4
+ # schedule.
5
+ #
6
+ # <b>Requires authentication with RubyUW::Base prior to use!</b>
7
+ #
8
+ # Additionally, this class should typically never be instantiated
9
+ # by any developer. Instead, the class methods should be used
10
+ # which will return an instance of the SLN object.
11
+ #
12
+ # == Usage Examples
13
+ #
14
+ # require 'rubyuw'
15
+ # RubyUW::Base.authenticate("netid", "password")
16
+ # sln_info = RubyUW::SLN.find("12345", "AUT+2009")
17
+ class SLN
18
+ attr_reader :sln
19
+ attr_reader :data
20
+
21
+ # Initialize a new SLN object. This should never be called on its
22
+ # own, but instead you should use the find method to setup a
23
+ # sln.
24
+ #
25
+ # @param [String] SLN number in string format.
26
+ # @param [String] Term that the SLN corresponds to.
27
+ # @param [Hash] Data of key to value.
28
+ def initialize(sln, term, data)
29
+ @sln = sln
30
+ @term = term
31
+ @data = data
32
+ end
33
+
34
+ # Grab the data of the SLN based on a key. Returns the value of
35
+ # a field for the SLN, or nil otherwise. Supported fields coming
36
+ # soon.
37
+ def data(key)
38
+ @data[key.to_sym]
39
+ end
40
+
41
+ class <<self
42
+ # Finds information about a specific SLN. The SLN information
43
+ # is grabbed from the MyUW time schedule page. Authentication is
44
+ # required prior to use.
45
+ #
46
+ # @param [#to_s] The SLN, which must respond to #to_s
47
+ # @param [String] The term, as represented in the time schedule URL.
48
+ # @return [RubyUW::SLN]
49
+ def find(sln_number, term)
50
+ raise Errors::NotLoggedInError.new unless Base.authenticated?
51
+
52
+ # Grab the SLN page and check for various failure scenarios
53
+ page = Base.connection.get("https://sdb.admin.washington.edu/timeschd/uwnetid/sln.asp?QTRYR=#{term}&SLN=#{sln_number}")
54
+ raise Errors::SLNRequestedTooSoonError.new if requested_too_soon?(page)
55
+ raise Errors::SLNDoesNotExistError.new if !sln_exists?(page)
56
+ raise Errors::SLNServiceClosedError.new if time_schedule_closed?(page)
57
+
58
+ # Now that the page is probably valid, we extract the field data
59
+ # based on various xpaths.
60
+ new(sln_number, term, extract_data_from_page(page))
61
+ end
62
+
63
+ protected
64
+
65
+ def requested_too_soon?(page)
66
+ page.uri.to_s == "http://www.washington.edu/students/timeschd/badrequest.html"
67
+ end
68
+
69
+ def sln_exists?(page)
70
+ page.body.to_s.match(/SLN: (.{5,6}) does not exist./).nil?
71
+ end
72
+
73
+ def time_schedule_closed?(page)
74
+ page.uri.to_s == "http://www.washington.edu/students/timeschd/nots.html"
75
+ end
76
+
77
+ def extract_data_from_page(page)
78
+ extract_data = [
79
+ [[nil, :course, :section, :type, :credits, :title], "//table[@border=1 and @cellpadding=3]//tr[@rowspan=1]//td"],
80
+ [[:current_enrollment, :limit_enrollment, :room_capacity, :space_available, :status], "//table[@border=1 and @cellpadding=3]//tr[count(td)=5]//td"],
81
+ [[:notes], "//table[@border=1 and @cellpadding=3]//tr[count(td)=1]//preceding-sibling::tr[count(th)=1]//following-sibling::tr//td"]
82
+ ]
83
+
84
+ data = {}
85
+ extract_data.each do |keys, xpath|
86
+ data.merge!(Base.extract(page, xpath, keys))
87
+ end
88
+
89
+ data
90
+ end
91
+ end
92
+ end
93
+
94
+ module Errors
95
+ # An error indicating that an SLN was requested back-to-back too
96
+ # quickly. MyUW enforces a timeout between requests for SLN information,
97
+ # and when this is not obeyed, this error will be thrown by RubyUW.
98
+ class SLNRequestedTooSoonError < Exception; end
99
+
100
+ # An error indicating that an SLN requested is invalid (does not
101
+ # exist).
102
+ class SLNDoesNotExistError < Exception; end
103
+
104
+ # An error indicating that the time scheduling service of MyUW
105
+ # is currently closed.
106
+ class SLNServiceClosedError < Exception; end
107
+ end
108
+ end
data/rubyuw.gemspec ADDED
@@ -0,0 +1,97 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{rubyuw}
8
+ s.version = "0.99.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Mitchell Hashimoto"]
12
+ s.date = %q{2009-12-06}
13
+ s.description = %q{Library which provides a ruby interface to the University of Washington student portal.}
14
+ s.email = %q{mitchell.hashimoto@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "README.md"
17
+ ]
18
+ s.files = [
19
+ ".gitignore",
20
+ "README.md",
21
+ "Rakefile",
22
+ "VERSION",
23
+ "lib/rubyuw.rb",
24
+ "lib/rubyuw/base.rb",
25
+ "lib/rubyuw/connection.rb",
26
+ "lib/rubyuw/curriculum_enrollment.rb",
27
+ "lib/rubyuw/errors.rb",
28
+ "lib/rubyuw/schedule.rb",
29
+ "lib/rubyuw/sln.rb",
30
+ "rubyuw.gemspec",
31
+ "test/live/curriculum_enrollment_test.rb",
32
+ "test/live/sln_test.rb",
33
+ "test/live/test_helper.rb",
34
+ "test/mocked/base_test.rb",
35
+ "test/mocked/connection_test.rb",
36
+ "test/mocked/curriculum_enrollment_test.rb",
37
+ "test/mocked/fixture_pages/bad_request.html",
38
+ "test/mocked/fixture_pages/curric_courses.html",
39
+ "test/mocked/fixture_pages/curric_no_courses.html",
40
+ "test/mocked/fixture_pages/curric_no_curric_found.html",
41
+ "test/mocked/fixture_pages/logged_in_relay.html",
42
+ "test/mocked/fixture_pages/login.html",
43
+ "test/mocked/fixture_pages/no_form_login.html",
44
+ "test/mocked/fixture_pages/no_login_button.html",
45
+ "test/mocked/fixture_pages/not_logged_in_relay.html",
46
+ "test/mocked/fixture_pages/registration.html",
47
+ "test/mocked/fixture_pages/registration_error.html",
48
+ "test/mocked/fixture_pages/registration_error_no_rows.html",
49
+ "test/mocked/fixture_pages/registration_no_form.html",
50
+ "test/mocked/fixture_pages/registration_success.html",
51
+ "test/mocked/fixture_pages/registration_unknown.html",
52
+ "test/mocked/fixture_pages/schedule.html",
53
+ "test/mocked/fixture_pages/schedule_diff_credit_row.html",
54
+ "test/mocked/fixture_pages/schedule_empty.html",
55
+ "test/mocked/fixture_pages/schedule_no_credit_row.html",
56
+ "test/mocked/fixture_pages/sln_does_not_exist.html",
57
+ "test/mocked/fixture_pages/sln_no_course_info.html",
58
+ "test/mocked/fixture_pages/sln_no_course_notes.html",
59
+ "test/mocked/fixture_pages/sln_no_enrollment_info.html",
60
+ "test/mocked/fixture_pages/sln_status.html",
61
+ "test/mocked/fixture_pages/welcome.html",
62
+ "test/mocked/schedule_test.rb",
63
+ "test/mocked/sln_test.rb",
64
+ "test/mocked/test_helper.rb",
65
+ "test/password.rb.sample"
66
+ ]
67
+ s.homepage = %q{http://github.com/mitchellh/rubyuw}
68
+ s.rdoc_options = ["--charset=UTF-8"]
69
+ s.require_paths = ["lib"]
70
+ s.rubygems_version = %q{1.3.5}
71
+ s.summary = %q{Library which provides a ruby interface to the University of Washington student portal.}
72
+ s.test_files = [
73
+ "test/live/curriculum_enrollment_test.rb",
74
+ "test/live/sln_test.rb",
75
+ "test/live/test_helper.rb",
76
+ "test/mocked/base_test.rb",
77
+ "test/mocked/connection_test.rb",
78
+ "test/mocked/curriculum_enrollment_test.rb",
79
+ "test/mocked/schedule_test.rb",
80
+ "test/mocked/sln_test.rb",
81
+ "test/mocked/test_helper.rb"
82
+ ]
83
+
84
+ if s.respond_to? :specification_version then
85
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
86
+ s.specification_version = 3
87
+
88
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
89
+ s.add_runtime_dependency(%q<tenderlove-mechanize>, [">= 0.9.3.20090623142847"])
90
+ else
91
+ s.add_dependency(%q<tenderlove-mechanize>, [">= 0.9.3.20090623142847"])
92
+ end
93
+ else
94
+ s.add_dependency(%q<tenderlove-mechanize>, [">= 0.9.3.20090623142847"])
95
+ end
96
+ end
97
+
@@ -0,0 +1,47 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class LiveCurriculumEnrollmentTest < Test::Unit::TestCase
4
+ context "live curriculum info grabbing" do
5
+ setup do
6
+ @curriculum = "CHEM"
7
+ @term = "AUT+2009"
8
+
9
+ @curriculum_info = {
10
+ "11633" => {
11
+ :course => "CHEM 110",
12
+ :section => "A",
13
+ :type => "LC"
14
+ },
15
+ "11635" => {
16
+ :course => "CHEM 110",
17
+ :section => "AB",
18
+ :type => "QZ"
19
+ },
20
+ "11826" => {
21
+ :course => "CHEM 475",
22
+ :section => "A",
23
+ :type => "LC"
24
+ }
25
+ }
26
+ end
27
+
28
+ should "raise an error for an invalid SLN/quarter" do
29
+ authenticate
30
+ assert_raise(RubyUW::Errors::CurriculumDoesNotExistError) { RubyUW::CurriculumEnrollment.find("NOTCHEM", @term) }
31
+ end
32
+
33
+ should "grab all SLNs properly" do
34
+ authenticate
35
+ results = RubyUW::CurriculumEnrollment.find(@curriculum, @term)
36
+
37
+ @curriculum_info.each do |sln, info|
38
+ course = results[sln]
39
+ assert !course.nil?
40
+
41
+ info.each do |k,v|
42
+ assert_equal v, course.data(k)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,54 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class LiveSLNTest < Test::Unit::TestCase
4
+ context "live SLN info grabbing" do
5
+ setup do
6
+ @sln_info = {
7
+ :sln => "17237",
8
+ :quarter => "AUT+2009",
9
+ :info => {
10
+ :section => "AE",
11
+ :type => "QZ",
12
+ :credits => "",
13
+ :title => "INTRO TO LOGIC",
14
+ :current_enrollment => "25",
15
+ :limit_enrollment => "25",
16
+ :room_capacity => "40",
17
+ :space_available => "",
18
+ :status => "** Closed **",
19
+ :notes => "Quiz Section"
20
+ }
21
+ }
22
+ end
23
+
24
+ teardown do
25
+ sleep(5)
26
+ end
27
+
28
+ def get_sln_obj
29
+ RubyUW::SLN.find(@sln_info[:sln], @sln_info[:quarter])
30
+ end
31
+
32
+ should "grab course info for a valid SLN" do
33
+ authenticate
34
+ @sln = get_sln_obj
35
+
36
+ @sln_info[:info].each do |k,v|
37
+ assert_equal v, @sln.data(k), "#{k} should've been #{v}"
38
+ end
39
+ end
40
+
41
+ should "raise an error if requests are back to back too fast" do
42
+ authenticate
43
+ assert_nothing_raised() { get_sln_obj }
44
+ assert_raise(RubyUW::Errors::SLNRequestedTooSoonError) { get_sln_obj }
45
+ end
46
+
47
+ should "raise an error for an invalid SLN/quarter" do
48
+ @sln_info[:sln] = 91919 # Tested to not exist
49
+
50
+ authenticate
51
+ assert_raise(RubyUW::Errors::SLNDoesNotExistError) { get_sln_obj }
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,24 @@
1
+ begin
2
+ require File.join(File.dirname(__FILE__), '..', 'password')
3
+ rescue LoadError => e
4
+ abort("Running live tests requires a password.rb file in the test folder with your MyUW credentials.")
5
+ end
6
+
7
+ require 'rubygems'
8
+ $:.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
9
+ require 'rubyuw'
10
+ require 'test/unit'
11
+ require "contest"
12
+ require "stories"
13
+ require "mocha"
14
+
15
+ class Test::Unit::TestCase
16
+ def setup
17
+ RubyUW::Base.reset_connection!
18
+ end
19
+
20
+ def authenticate
21
+ assert RubyUW::Base.authenticate(MYUW_ID, MYUW_PASSWORD)
22
+ assert RubyUW::Base.authenticated? # sanity
23
+ end
24
+ end
@@ -0,0 +1,171 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class BaseTest < Test::Unit::TestCase
4
+ context "with connection" do
5
+ setup do
6
+ RubyUW::Base.reset_connection!
7
+ end
8
+
9
+ should "initialize a WWW::Mechanize object on the first call" do
10
+ RubyUW::Connection.expects(:new).once
11
+ RubyUW::Base.connection
12
+ end
13
+
14
+ should "not initialize WWW::Mechanize objects on subsequent calls" do
15
+ RubyUW::Connection.expects(:new).once.returns(true)
16
+ 50.times { RubyUW::Base.connection }
17
+ end
18
+
19
+ should "clear out the connection to force recreation of object on reset_connection!" do
20
+ assert !RubyUW::Base.connection.nil?
21
+ RubyUW::Base.reset_connection!
22
+ RubyUW::Connection.expects(:new).once.returns(true)
23
+ 50.times { RubyUW::Base.connection }
24
+ end
25
+ end
26
+
27
+ context "with authentication" do
28
+ setup do
29
+ # initialize the base connection
30
+ @conn = RubyUW::Base.reset_connection!.connection
31
+ @conn.follow_meta_refresh = false
32
+
33
+ @good_id = "foo"
34
+ @good_password = "bar"
35
+ @bad_password = @good_password + "wrong"
36
+ end
37
+
38
+ should "clear the cookie jar of the connection" do
39
+ RubyUW::Base.expects(:logout).once
40
+
41
+ result_page = mock('page')
42
+ result_page.stubs(:form_with).returns(true)
43
+ @conn.stubs(:execute!).returns(result_page)
44
+
45
+ RubyUW::Base.authenticate(@good_id, @good_password)
46
+ end
47
+
48
+ should "succeed login with good ID and password and valid pages" do
49
+ mock_flow([
50
+ [:goto, ["http://myuw.washington.edu"], "welcome"],
51
+ [:submit_form, ["f", anything], "not_logged_in_relay"],
52
+ [:submit_form, ["relay", anything], "login"],
53
+ [:submit_form, ["query", anything], "not_logged_in_relay"]
54
+ ])
55
+
56
+ assert RubyUW::Base.authenticate(@good_id, @good_password)
57
+ end
58
+
59
+ should "fail login with bad ID and password" do
60
+ mock_flow([
61
+ [:goto, ["http://myuw.washington.edu"], "welcome"],
62
+ [:submit_form, ["f", anything], "not_logged_in_relay"],
63
+ [:submit_form, ["relay", anything], "login"],
64
+ [:submit_form, ["query", anything], "login"]
65
+ ])
66
+
67
+ assert !RubyUW::Base.authenticate(@good_id, @bad_password)
68
+ end
69
+ end
70
+
71
+ context "with authentication check" do
72
+ should "return true if it can find the correct xpath" do
73
+ mock_flow([
74
+ [:goto, ["http://myuw.washington.edu"], "welcome"],
75
+ [:submit_form, ["f", anything], "logged_in_relay"]
76
+ ])
77
+
78
+ assert RubyUW::Base.authenticated?
79
+ end
80
+
81
+ should "not return true if it can't find the greeting" do
82
+ mock_flow([
83
+ [:goto, ["http://myuw.washington.edu"], "welcome"],
84
+ [:submit_form, ["f", anything], "login"]
85
+ ])
86
+
87
+ assert !RubyUW::Base.authenticated?
88
+ end
89
+ end
90
+
91
+ context "with logging out" do
92
+ should "clear the cookie jar on logout" do
93
+ cookie_jar = mock('cookie_jar')
94
+ cookie_jar.expects(:clear!).once
95
+
96
+ conn = RubyUW::Base.connection
97
+ conn.expects(:cookie_jar).returns(cookie_jar)
98
+
99
+ RubyUW::Base.logout
100
+ end
101
+ end
102
+
103
+ context "with extracting data from page" do
104
+ setup do
105
+ @page = mock('page')
106
+ @xpath = "foo"
107
+ end
108
+
109
+ def setup_nodes(inner_texts)
110
+ nodes = []
111
+ inner_texts.each do |text|
112
+ node = mock('node')
113
+ node.stubs(:inner_text).returns(text)
114
+
115
+ nodes << node
116
+ end
117
+
118
+ nodes
119
+ end
120
+
121
+ should "run the xpath on the specified page" do
122
+ @page.expects(:search).with(@xpath).returns([])
123
+ RubyUW::Base.extract(@page, @xpath, [])
124
+ end
125
+
126
+ should "return nil if the xpath returns an empty array" do
127
+ @page.expects(:search).with(@xpath).returns([])
128
+ assert_nil RubyUW::Base.extract(@page, @xpath, [])
129
+ end
130
+
131
+ should "map keys to node results if xpath returns valid nodes" do
132
+ expected = { :one => "1", :two => "2", :three => "3" }
133
+ nodes = setup_nodes(expected.values)
134
+ keys = expected.keys
135
+
136
+ @page.expects(:search).with(@xpath).returns(nodes)
137
+ result = RubyUW::Base.extract(@page, @xpath, keys)
138
+ assert expected == result
139
+ end
140
+
141
+ should "skip the nil keys (while continuing to go through the node list)" do
142
+ nodes = setup_nodes(["1","2","3"])
143
+ keys = [:one, nil, :three]
144
+
145
+ @page.expects(:search).with(@xpath).returns(nodes)
146
+ result = RubyUW::Base.extract(@page, @xpath, keys)
147
+ assert_equal 2, result.length
148
+ assert !result.value?("2")
149
+ end
150
+
151
+ should "return what it has so far if it runs out of keys" do
152
+ nodes = setup_nodes((1..50).to_a.map(&:to_s))
153
+ keys = [:one]
154
+
155
+ @page.expects(:search).with(@xpath).returns(nodes)
156
+ result = RubyUW::Base.extract(@page, @xpath, keys)
157
+ assert_equal keys.length, result.length
158
+ assert_equal "1", result[keys[0]]
159
+ end
160
+
161
+ should "get rid of odd characters that MyUW has in text" do
162
+ nodes = setup_nodes(["foo\302\240"])
163
+ keys = [:one]
164
+
165
+ @page.expects(:search).with(@xpath).returns(nodes)
166
+ result = RubyUW::Base.extract(@page, @xpath, keys)
167
+ assert_equal keys.length, result.length
168
+ assert_equal "foo", result[keys[0]]
169
+ end
170
+ end
171
+ end