bnet_scraper 0.3.1 → 0.4.0

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 (51) hide show
  1. data/CHANGELOG.md +9 -0
  2. data/Gemfile +1 -1
  3. data/LICENSE +1 -1
  4. data/README.md +51 -31
  5. data/bnet_scraper.gemspec +3 -1
  6. data/fixtures/vcr_cassettes/demon_achievements.yml +246 -0
  7. data/fixtures/vcr_cassettes/demon_leagues.yml +359 -0
  8. data/fixtures/vcr_cassettes/demon_match_history.yml +241 -0
  9. data/fixtures/vcr_cassettes/demon_matches.yml +240 -0
  10. data/fixtures/vcr_cassettes/demon_profile.yml +1428 -0
  11. data/fixtures/vcr_cassettes/demon_profile_leagues.yml +1291 -0
  12. data/fixtures/vcr_cassettes/full_demon_scrape.yml +5449 -0
  13. data/fixtures/vcr_cassettes/invalid_achievement.yml +199 -0
  14. data/fixtures/vcr_cassettes/invalid_leagues.yml +199 -0
  15. data/fixtures/vcr_cassettes/invalid_matches.yml +199 -0
  16. data/fixtures/vcr_cassettes/invalid_profile.yml +199 -0
  17. data/fixtures/vcr_cassettes/new_league.yml +595 -0
  18. data/fixtures/vcr_cassettes/profile_invalid.yml +199 -0
  19. data/fixtures/vcr_cassettes/profile_not_laddered.yml +443 -0
  20. data/fixtures/vcr_cassettes/realm_status.yml +578 -0
  21. data/lib/bnet_scraper/starcraft2.rb +7 -8
  22. data/lib/bnet_scraper/starcraft2/achievement_scraper.rb +5 -5
  23. data/lib/bnet_scraper/starcraft2/league.rb +54 -0
  24. data/lib/bnet_scraper/starcraft2/league_scraper.rb +3 -1
  25. data/lib/bnet_scraper/starcraft2/match.rb +15 -0
  26. data/lib/bnet_scraper/starcraft2/match_history_scraper.rb +9 -18
  27. data/lib/bnet_scraper/starcraft2/profile.rb +45 -0
  28. data/lib/bnet_scraper/starcraft2/profile_scraper.rb +68 -44
  29. data/spec/spec_helper.rb +14 -0
  30. data/spec/starcraft2/achievement_scraper_spec.rb +67 -83
  31. data/spec/starcraft2/league_scraper_spec.rb +25 -59
  32. data/spec/starcraft2/league_spec.rb +43 -0
  33. data/spec/starcraft2/match_history_scraper_spec.rb +19 -39
  34. data/spec/starcraft2/profile_scraper_spec.rb +36 -141
  35. data/spec/starcraft2/profile_spec.rb +46 -0
  36. data/spec/starcraft2/status_scraper_spec.rb +12 -5
  37. data/spec/starcraft2_spec.rb +19 -36
  38. data/spec/support/shared/sc2_scraper.rb +28 -13
  39. metadata +57 -42
  40. data/spec/support/achievements.html +0 -1156
  41. data/spec/support/failure.html +0 -565
  42. data/spec/support/initial_league.html +0 -1082
  43. data/spec/support/initial_leagues.html +0 -3598
  44. data/spec/support/league.html +0 -8310
  45. data/spec/support/leagues.html +0 -3810
  46. data/spec/support/load_fakeweb.rb +0 -42
  47. data/spec/support/matches.html +0 -1228
  48. data/spec/support/no_ladder.html +0 -967
  49. data/spec/support/no_ladder_leagues.html +0 -664
  50. data/spec/support/profile.html +0 -1074
  51. data/spec/support/status.html +0 -1
@@ -94,15 +94,14 @@ module BnetScraper
94
94
  # scraped from the website
95
95
  def self.full_profile_scrape bnet_id, account, region = 'na'
96
96
  profile_scraper = ProfileScraper.new bnet_id: bnet_id, account: account, region: region
97
- profile_output = profile_scraper.scrape
98
-
99
- parsed_leagues = []
100
- profile_output[:leagues].each do |league|
101
- league_scraper = LeagueScraper.new url: league[:href]
102
- parsed_leagues << league_scraper.scrape
97
+ profile = profile_scraper.scrape
98
+ profile.leagues.each do |league|
99
+ league.scrape_league
103
100
  end
104
- profile_output[:leagues] = parsed_leagues
105
- return profile_output
101
+ profile.achievements
102
+ profile.match_history
103
+
104
+ return profile
106
105
  end
107
106
 
108
107
  # Determine if Supplied profile is valid. Useful for validating now before an
@@ -72,11 +72,11 @@ module BnetScraper
72
72
  def scrape_progress
73
73
  progress_ach = response.css("#progress-module .achievements-progress:nth(2) span")
74
74
  @progress = {
75
- liberty_campaign: progress_ach[0].inner_text,
76
- exploration: progress_ach[1].inner_text,
77
- custom_game: progress_ach[2].inner_text,
78
- cooperative: progress_ach[3].inner_text,
79
- quick_match: progress_ach[4].inner_text,
75
+ liberty_campaign: response.css(".progress-tile:nth-child(1) .profile-progress span").inner_text,
76
+ exploration: response.css(".progress-tile:nth-child(2) .profile-progress span").inner_text,
77
+ custom_game: response.css(".progress-tile:nth-child(3) .profile-progress span").inner_text,
78
+ cooperative: response.css(".progress-tile:nth-child(4) .profile-progress span").inner_text,
79
+ quick_match: response.css(".progress-tile:nth-child(5) .profile-progress span").inner_text,
80
80
  }
81
81
  end
82
82
 
@@ -0,0 +1,54 @@
1
+ module BnetScraper
2
+ module Starcraft2
3
+ class League
4
+ attr_accessor :id, :name, :href, :season, :name, :division, :size, :random, :bnet_id,
5
+ :account
6
+
7
+ def initialize options = {}
8
+ options.each_key do |key|
9
+ self.send "#{key}=", options[key]
10
+ end
11
+ end
12
+
13
+ def name
14
+ scrape_or_return :@name
15
+ end
16
+
17
+ def season
18
+ scrape_or_return :@season
19
+ end
20
+
21
+ def division
22
+ scrape_or_return :@division
23
+ end
24
+
25
+ def size
26
+ scrape_or_return :@size
27
+ end
28
+
29
+ def bnet_id
30
+ scrape_or_return :@bnet_id
31
+ end
32
+
33
+ def account
34
+ scrape_or_return :@account
35
+ end
36
+
37
+ def scrape_or_return attribute
38
+ if self.instance_variable_get(attribute)
39
+ return self.instance_variable_get(attribute)
40
+ else
41
+ scrape_league
42
+ self.instance_variable_get(attribute)
43
+ end
44
+ end
45
+
46
+ def scrape_league
47
+ scraped_data = LeagueScraper.new(url: href).scrape
48
+ scraped_data.each_key do |key|
49
+ self.send "#{key}=", scraped_data[key]
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,3 +1,5 @@
1
+ require 'bnet_scraper/starcraft2/league'
2
+
1
3
  module BnetScraper
2
4
  module Starcraft2
3
5
  # This pulls information on a specific league for a specific account. It is best used either in conjunction with a
@@ -34,7 +36,7 @@ module BnetScraper
34
36
  if @response.success?
35
37
  @response = Nokogiri::HTML(@response.body)
36
38
  value = @response.css(".data-title .data-label h3").inner_text().strip
37
- header_regex = /Season (\d{1}) - \s+(\dv\d)( Random)? (\w+)\s+Division (.+)/
39
+ header_regex = /(.+) -\s+(\dv\d)( Random)? (\w+)\s+Division (.+)/
38
40
  header_values = value.match(header_regex).to_a
39
41
  header_values.shift()
40
42
  @season, @size, @random, @division, @name = header_values
@@ -0,0 +1,15 @@
1
+ module BnetScraper
2
+ module Starcraft2
3
+ class Match
4
+ attr_accessor :outcome, :map_name, :type, :date
5
+
6
+ def won?
7
+ @outcome == :won
8
+ end
9
+
10
+ def lost?
11
+ @outcome == :loss
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,3 +1,5 @@
1
+ require 'bnet_scraper/starcraft2/match'
2
+
1
3
  module BnetScraper
2
4
  module Starcraft2
3
5
  # This pulls the 25 most recent matches played for an account. Note that this is only as up-to-date as battle.net is, and
@@ -42,29 +44,18 @@ module BnetScraper
42
44
 
43
45
  response.css('.match-row').each do |m|
44
46
  match = {}
47
+ match = Match.new
45
48
 
46
49
  cells = m.css('td')
47
- match[:map_name] = cells[1].inner_text
48
- match[:type] = cells[2].inner_text
49
- match[:outcome] = (cells[3].inner_text.strip == 'Win' ? :win : :loss)
50
- match[:date] = cells[4].inner_text.strip
50
+ match.map_name = cells[1].inner_text
51
+ match.type = cells[2].inner_text
52
+ match.outcome = (cells.css('.match-loss') ? :win : :loss)
53
+ match.date = cells[4].inner_text.strip
54
+
51
55
  @matches << match
52
- if match[:outcome] == :win
53
- @wins += 1
54
- else
55
- @losses += 1
56
- end
57
- output
58
56
  end
59
57
 
60
- end
61
-
62
- def output
63
- {
64
- matches: @matches,
65
- wins: @wins,
66
- losses: @losses
67
- }
58
+ @matches
68
59
  end
69
60
  end
70
61
  end
@@ -0,0 +1,45 @@
1
+
2
+ module BnetScraper
3
+ module Starcraft2
4
+ class Profile
5
+ attr_accessor :portrait, :url, :achievement_points, :current_solo_league,
6
+ :highest_solo_league, :current_team_league, :highest_team_league,
7
+ :career_games, :games_this_season, :terran_swarm_level, :protoss_swarm_level,
8
+ :zerg_swarm_level, :leagues, :swarm_levels
9
+
10
+ def initialize options = {}
11
+ options.each_key do |key|
12
+ self.send "#{key}=", options[key]
13
+ end
14
+ end
15
+
16
+ def achievements
17
+ @achievements ||= AchievementScraper.new(url: url).scrape
18
+ end
19
+
20
+ def recent_achievements
21
+ achievements[:recent]
22
+ end
23
+
24
+ def progress_achievements
25
+ achievements[:progress]
26
+ end
27
+
28
+ def showcase_achievements
29
+ achievements[:showcase]
30
+ end
31
+
32
+ def match_history
33
+ @match_history ||= MatchHistoryScraper.new(url: url).scrape
34
+ end
35
+
36
+ def swarm_levels
37
+ {
38
+ zerg: @zerg_swarm_level,
39
+ protoss: @protoss_swarm_level,
40
+ terran: @terran_swarm_level
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,3 +1,5 @@
1
+ require 'bnet_scraper/starcraft2/profile'
2
+
1
3
  module BnetScraper
2
4
  module Starcraft2
3
5
  # This pulls basic profile information for an account, as well as an array of league URLs. This is a good starting
@@ -28,19 +30,22 @@ module BnetScraper
28
30
  # ]
29
31
  # }
30
32
  class ProfileScraper < BaseScraper
31
- attr_reader :achievement_points, :career_games, :race, :leagues, :most_played,
32
- :games_this_season, :highest_solo_league, :current_solo_league, :highest_team_league,
33
- :current_team_league, :portrait
33
+ attr_reader :achievement_points, :career_games, :leagues, :games_this_season,
34
+ :highest_solo_league, :current_solo_league, :highest_team_league,
35
+ :current_team_league, :portrait, :terran_swarm_level, :protoss_swarm_level,
36
+ :zerg_swarm_level, :profile
34
37
 
35
38
  def initialize options = {}
36
39
  super
37
40
  @leagues = []
41
+ @profile ||= Profile.new url: profile_url
38
42
  end
39
43
 
40
44
  def scrape
41
45
  get_profile_data
42
46
  get_league_list
43
- output
47
+
48
+ @profile
44
49
  end
45
50
 
46
51
  # scrapes the profile page for basic account information
@@ -50,64 +55,83 @@ module BnetScraper
50
55
  if response.success?
51
56
  html = Nokogiri::HTML(response.body)
52
57
 
53
- # Portraits use spritemaps, so we extract positions and map to
54
- # PORTRAITS.
55
- @portrait = begin
56
- portrait = html.css("#profile-header #portrait span").attr('style').to_s.scan(/url\('(.*?)'\) ([\-\d]+)px ([\-\d]+)px/).flatten
57
- portrait_map, portrait_size = portrait[0].scan(/(\d)\-(\d+)\.jpg/)[0]
58
- portrait_position = (((0-portrait[2].to_i) / portrait_size.to_i) * 6) + ((0-portrait[1].to_i) / portrait_size.to_i + 1)
59
- PORTRAITS[portrait_map.to_i][portrait_position-1]
60
- rescue
61
- nil
62
- end
58
+ @profile.achievement_points = html.css("#profile-header h3").inner_html()
59
+ @profile.career_games = html.css(".career-stat-block:nth-child(4) .stat-value").inner_html()
60
+ @profile.games_this_season = html.css(".career-stat-block:nth-child(5) .stat-value").inner_html()
61
+
62
+ get_portrait html
63
+ get_solo_league_info html
64
+ get_team_league_info html
65
+ get_swarm_levels html
66
+ else
67
+ raise BnetScraper::InvalidProfileError
68
+ end
69
+ end
70
+
71
+ def get_portrait html
72
+ # Portraits use spritemaps, so we extract positions and map to
73
+ # PORTRAITS.
74
+ @profile.portrait = begin
75
+ portrait = html.css("#profile-header #portrait span").attr('style').to_s.scan(/url\('(.*?)'\) ([\-\d]+)px ([\-\d]+)px/).flatten
76
+ portrait_map, portrait_size = portrait[0].scan(/(\d)\-(\d+)\.jpg/)[0]
77
+ portrait_position = (((0-portrait[2].to_i) / portrait_size.to_i) * 6) + ((0-portrait[1].to_i) / portrait_size.to_i + 1)
78
+ PORTRAITS[portrait_map.to_i][portrait_position-1]
79
+ rescue
80
+ nil
81
+ end
82
+ end
63
83
 
64
- @race = html.css(".stat-block:nth-child(4) h2").inner_html()
65
- @achievement_points = html.css("#profile-header h3").inner_html()
66
- @career_games = html.css(".stat-block:nth-child(3) h2").inner_html()
67
- @most_played = html.css(".stat-block:nth-child(2) h2").inner_html()
68
- @games_this_season = html.css(".stat-block:nth-child(1) h2").inner_html()
69
-
70
- if html.css("#best-finish-SOLO div")[0]
71
- @highest_solo_league = html.css("#best-finish-SOLO div")[0].children[2].inner_text.strip
72
- if html.css("#best-finish-SOLO div")[0].children[8]
73
- @current_solo_league = html.css("#best-finish-SOLO div")[0].children[8].inner_text.strip
74
- else
75
- @current_solo_league = html.css("#best-finish-SOLO div")[0].children[5].inner_text.strip
76
- end
84
+ def get_solo_league_info html
85
+ if html.css("#best-finish-SOLO div")[0]
86
+ @profile.highest_solo_league = html.css("#best-finish-SOLO div")[0].children[2].inner_text.strip
87
+ if html.css("#best-finish-SOLO div")[0].children[8]
88
+ @profile.current_solo_league = html.css("#best-finish-SOLO div")[0].children[8].inner_text.strip
77
89
  else
78
- @highest_solo_league = "Not Yet Ranked"
79
- @current_solo_league = "Not Yet Ranked"
90
+ @profile.current_solo_league = "Not Yet Ranked"
80
91
  end
92
+ else
93
+ @profile.highest_solo_league = "Not Yet Ranked"
94
+ @profile.current_solo_league = "Not Yet Ranked"
95
+ end
96
+ end
81
97
 
82
- if html.css("#best-finish-TEAM div")[0]
83
- @highest_team_league = html.css("#best-finish-TEAM div")[0].children[2].inner_text.strip
84
- if html.css("#best-finish-TEAM div")[0].children[8]
85
- @current_team_league = html.css("#best-finish-TEAM div")[0].children[8].inner_text.strip
86
- else
87
- @current_team_league = html.css("#best-finish-TEAM div")[0].children[5].inner_text.strip
88
- end
98
+ def get_team_league_info html
99
+ if html.css("#best-finish-TEAM div")[0]
100
+ @profile.highest_team_league = html.css("#best-finish-TEAM div")[0].children[2].inner_text.strip
101
+ if html.css("#best-finish-TEAM div")[0].children[8]
102
+ @profile.current_team_league = html.css("#best-finish-TEAM div")[0].children[8].inner_text.strip
89
103
  else
90
- @highest_team_league = "Not Yet Ranked"
91
- @current_team_league = "Not Yet Ranked"
104
+ @profile.current_team_league = "Not Yet Ranked"
92
105
  end
93
-
94
106
  else
95
- raise BnetScraper::InvalidProfileError
107
+ @profile.highest_team_league = "Not Yet Ranked"
108
+ @profile.current_team_league = "Not Yet Ranked"
96
109
  end
97
110
  end
98
111
 
112
+ def get_swarm_levels html
113
+ @profile.protoss_swarm_level = get_swarm_level :protoss, html
114
+ @profile.terran_swarm_level = get_swarm_level :terran, html
115
+ @profile.zerg_swarm_level = get_swarm_level :zerg, html
116
+ end
117
+
118
+ def get_swarm_level race, html
119
+ level = html.css(".race-level-block.#{race} .level-value").inner_html
120
+ level.match(/Level (\d+)/)[1].to_i
121
+ end
122
+
99
123
  # scrapes the league list from account's league page and sets an array of URLs
100
124
  def get_league_list
101
125
  response = Faraday.get profile_url + "ladder/leagues"
102
126
  if response.success?
103
127
  html = Nokogiri::HTML(response.body)
104
128
 
105
- @leagues = html.css("a[href*='#current-rank']").map do |league|
106
- {
107
- name: league.inner_html().strip,
129
+ @profile.leagues = html.css("a[href*='#current-rank']").map do |league|
130
+ League.new({
131
+ name: league.inner_text().strip,
108
132
  id: league.attr('href').sub('#current-rank',''),
109
133
  href: "#{profile_url}ladder/#{league.attr('href')}"
110
- }
134
+ })
111
135
  end
112
136
  else
113
137
  raise BnetScraper::InvalidProfileError
@@ -1,5 +1,19 @@
1
1
  $:.push File.join(File.dirname(__FILE__), '..', 'lib')
2
2
 
3
3
  require 'bnet_scraper'
4
+ require 'pry'
5
+ require 'vcr'
4
6
  Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f }
5
7
 
8
+ VCR.configure do |config|
9
+ config.cassette_library_dir = 'fixtures/vcr_cassettes'
10
+ config.hook_into :fakeweb
11
+ end
12
+
13
+ RSpec.configure do |config|
14
+ config.treat_symbols_as_metadata_keys_with_true_values = true
15
+ config.around(:each, :vcr) do |example|
16
+ name = example.metadata[:full_description].split(/\s+/, 2).join("/").underscore.gsub(/[^\w\/]+/, "_")
17
+ VCR.use_cassette(name) { example.call }
18
+ end
19
+ end
@@ -9,97 +9,81 @@ describe BnetScraper::Starcraft2::AchievementScraper do
9
9
  let(:subject) { scraper_class.new(url: url) }
10
10
  end
11
11
 
12
- describe '#get_response' do
13
- it 'should get the HTML response to be scraped' do
14
- subject.response.should be_nil
15
- subject.get_response
16
- subject.response.should_not be_nil
17
- end
18
- end
19
-
20
- describe '#scrape' do
21
- it 'should call get_response and trigger scraper methods' do
22
- subject.should_receive(:get_response)
23
- subject.should_receive(:scrape_progress)
24
- subject.should_receive(:scrape_recent)
25
- subject.should_receive(:scrape_showcase)
26
- subject.scrape
27
- end
28
-
12
+ describe 'scrape' do
29
13
  it 'should return InvalidProfileError if response is 404' do
30
- url = 'http://us.battle.net/sc2/en/profile/2377239/1/SomeDude/achievements/'
31
- scraper = BnetScraper::Starcraft2::AchievementScraper.new(url: url)
32
- expect { scraper.scrape }.to raise_error(BnetScraper::InvalidProfileError)
33
- end
34
- end
35
-
36
- describe '#scrape_showcase' do
37
- before :each do
38
- subject.get_response
39
- subject.scrape_showcase
40
- end
41
-
42
- it 'should set the showcase' do
43
- subject.showcase.should have(5).achievements
44
- end
45
- end
46
-
47
- describe '#scrape_recent' do
48
- before :each do
49
- subject.get_response
50
- subject.scrape_recent
51
- end
52
-
53
- it 'should have the title of the achievement' do
54
- subject.recent[0][:title].should == 'Blink of an Eye'
55
- end
56
-
57
- it 'should have the description of the achievement' do
58
- # this is a cop-out because the string contains UTF-8. Please fix this. - Cad
59
- subject.recent[0][:description].should be_a String
60
- end
61
-
62
- it 'should have the date the achievement was earned' do
63
- subject.recent[0][:earned].should == '3/5/2012'
64
- end
65
- end
66
-
67
- describe '#scrape_progress' do
68
- before :each do
69
- subject.get_response
70
- subject.scrape_progress
71
- end
72
-
73
- it 'should set the liberty campaign progress' do
74
- subject.progress[:liberty_campaign].should == '1580'
75
- end
76
-
77
- it 'should set the exploration progress' do
78
- subject.progress[:exploration].should == '480'
79
- end
80
-
81
- it 'should set the custom game progress' do
82
- subject.progress[:custom_game].should == '330'
83
- end
84
-
85
- it 'should set the cooperative progress' do
86
- subject.progress[:cooperative].should == '660'
14
+ VCR.use_cassette('invalid_achievement') do
15
+ url = 'http://us.battle.net/sc2/en/profile/2377239/1/SomeDude/achievements/'
16
+ scraper = BnetScraper::Starcraft2::AchievementScraper.new(url: url)
17
+ expect { scraper.scrape }.to raise_error(BnetScraper::InvalidProfileError)
18
+ end
87
19
  end
88
20
 
89
- it 'should set the quick match progress' do
90
- subject.progress[:quick_match].should == '170'
21
+ context 'valid' do
22
+ before do
23
+ VCR.use_cassette('demon_achievements') do
24
+ subject.get_response
25
+ end
26
+ end
27
+
28
+ describe 'showcase' do
29
+ before { subject.scrape_showcase }
30
+ its(:showcase) { should have(5).achievements }
31
+ end
32
+
33
+ describe 'recent' do
34
+ before { subject.scrape_recent }
35
+
36
+ it 'should have the title of the achievement' do
37
+ subject.recent[0][:title].should == 'Three-way Dominant'
38
+ end
39
+
40
+ it 'should have the description of the achievement' do
41
+ # this is a cop-out because the string contains UTF-8. Please fix this. - Cad
42
+ subject.recent[0][:description].should be_a String
43
+ end
44
+
45
+ it 'should have the date the achievement was earned' do
46
+ subject.recent[0][:earned].should == '2/7/2013'
47
+ end
48
+ end
49
+
50
+ describe 'progress' do
51
+ before { subject.scrape_progress }
52
+
53
+ it 'should set the liberty campaign progress' do
54
+ subject.progress[:liberty_campaign].should == '1580'
55
+ end
56
+
57
+ it 'should set the exploration progress' do
58
+ subject.progress[:exploration].should == '0'
59
+ end
60
+
61
+ it 'should set the custom game progress' do
62
+ subject.progress[:custom_game].should == '1280'
63
+ end
64
+
65
+ it 'should set the cooperative progress' do
66
+ subject.progress[:cooperative].should == '120'
67
+ end
68
+
69
+ it 'should set the quick match progress' do
70
+ subject.progress[:quick_match].should == '220'
71
+ end
72
+ end
91
73
  end
92
74
  end
93
75
 
94
76
  describe '#output' do
95
77
  it 'should return the scraped data when scrape has been called' do
96
- subject.scrape
97
- expected = {
98
- recent: subject.recent,
99
- showcase: subject.showcase,
100
- progress: subject.progress
101
- }
102
- subject.output.should == expected
78
+ VCR.use_cassette('demon_achievements') do
79
+ subject.scrape
80
+ expected = {
81
+ recent: subject.recent,
82
+ showcase: subject.showcase,
83
+ progress: subject.progress
84
+ }
85
+ subject.output.should == expected
86
+ end
103
87
  end
104
88
  end
105
89
  end