rubyuw 0.99.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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