daigaku 0.0.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.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/README.md +62 -0
- data/Rakefile +2 -0
- data/bin/daigaku +12 -0
- data/daigaku.gemspec +37 -0
- data/lib/daigaku.rb +27 -0
- data/lib/daigaku/chapter.rb +23 -0
- data/lib/daigaku/configuration.rb +86 -0
- data/lib/daigaku/course.rb +23 -0
- data/lib/daigaku/database.rb +64 -0
- data/lib/daigaku/exceptions.rb +19 -0
- data/lib/daigaku/generator.rb +53 -0
- data/lib/daigaku/loadable.rb +23 -0
- data/lib/daigaku/loading/chapters.rb +9 -0
- data/lib/daigaku/loading/courses.rb +9 -0
- data/lib/daigaku/loading/units.rb +9 -0
- data/lib/daigaku/reference_solution.rb +10 -0
- data/lib/daigaku/solution.rb +42 -0
- data/lib/daigaku/task.rb +10 -0
- data/lib/daigaku/terminal.rb +11 -0
- data/lib/daigaku/terminal/cli.rb +59 -0
- data/lib/daigaku/terminal/courses.rb +114 -0
- data/lib/daigaku/terminal/output.rb +72 -0
- data/lib/daigaku/terminal/setup.rb +115 -0
- data/lib/daigaku/terminal/solutions.rb +46 -0
- data/lib/daigaku/terminal/texts/about.txt +19 -0
- data/lib/daigaku/terminal/texts/courses_empty.txt +3 -0
- data/lib/daigaku/terminal/texts/hint_course_download.txt +13 -0
- data/lib/daigaku/terminal/texts/welcome.txt +12 -0
- data/lib/daigaku/terminal/welcome.rb +98 -0
- data/lib/daigaku/test.rb +46 -0
- data/lib/daigaku/test_result.rb +69 -0
- data/lib/daigaku/unit.rb +28 -0
- data/lib/daigaku/version.rb +3 -0
- data/lib/daigaku/views.rb +59 -0
- data/lib/daigaku/views/chapters_menu.rb +91 -0
- data/lib/daigaku/views/courses_menu.rb +87 -0
- data/lib/daigaku/views/main_menu.rb +37 -0
- data/lib/daigaku/views/splash.rb +57 -0
- data/lib/daigaku/views/task_view.rb +206 -0
- data/lib/daigaku/views/top_bar.rb +48 -0
- data/lib/daigaku/views/units_menu.rb +92 -0
- data/lib/daigaku/window.rb +160 -0
- data/spec/daigaku/chapter_spec.rb +76 -0
- data/spec/daigaku/configuration_spec.rb +161 -0
- data/spec/daigaku/course_spec.rb +75 -0
- data/spec/daigaku/database_spec.rb +79 -0
- data/spec/daigaku/generator_spec.rb +82 -0
- data/spec/daigaku/loading/chapters_spec.rb +16 -0
- data/spec/daigaku/loading/courses_spec.rb +16 -0
- data/spec/daigaku/loading/units_spec.rb +21 -0
- data/spec/daigaku/reference_solution_spec.rb +23 -0
- data/spec/daigaku/solution_spec.rb +79 -0
- data/spec/daigaku/task_spec.rb +23 -0
- data/spec/daigaku/terminal/cli_spec.rb +51 -0
- data/spec/daigaku/terminal/courses_spec.rb +60 -0
- data/spec/daigaku/terminal/output_spec.rb +123 -0
- data/spec/daigaku/terminal/setup_spec.rb +10 -0
- data/spec/daigaku/terminal/solutions_spec.rb +8 -0
- data/spec/daigaku/terminal/welcome_spec.rb +12 -0
- data/spec/daigaku/terminal_spec.rb +14 -0
- data/spec/daigaku/test_example_spec.rb +54 -0
- data/spec/daigaku/test_result_spec.rb +81 -0
- data/spec/daigaku/test_spec.rb +48 -0
- data/spec/daigaku/unit_spec.rb +85 -0
- data/spec/daigaku/views/chapters_menu_spec.rb +8 -0
- data/spec/daigaku/views/courses_menu_spec.rb +8 -0
- data/spec/daigaku/views/task_view_spec.rb +7 -0
- data/spec/daigaku/views/units_menu_spec.rb +8 -0
- data/spec/daigaku/views_spec.rb +23 -0
- data/spec/daigaku_spec.rb +57 -0
- data/spec/path_helpers_spec.rb +60 -0
- data/spec/resource_helpers_spec.rb +33 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/macros/content_helpers.rb +129 -0
- data/spec/support/macros/mock_helpers.rb +20 -0
- data/spec/support/macros/path_helpers.rb +133 -0
- data/spec/support/macros/resource_helpers.rb +119 -0
- data/spec/support/macros/test_helpers.rb +6 -0
- metadata +361 -0
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
module Daigaku
|
4
|
+
class Window < Curses::Window
|
5
|
+
|
6
|
+
COLOR_TEXT = Curses::COLOR_YELLOW unless defined? COLOR_TEXT
|
7
|
+
COLOR_TEXT_EMPHASIZE = Curses::COLOR_CYAN unless defined? COLOR_TEXT_EMPHASIZE
|
8
|
+
COLOR_HEADING = Curses::COLOR_WHITE unless defined? COLOR_HEADING
|
9
|
+
COLOR_RED = Curses::COLOR_BLUE unless defined? COLOR_RED
|
10
|
+
COLOR_GREEN = Curses::COLOR_MAGENTA unless defined? COLOR_GREEN
|
11
|
+
COLOR_YELLOW = Curses::COLOR_RED unless defined? COLOR_YELLOW
|
12
|
+
|
13
|
+
BACKGROUND = Curses::COLOR_WHITE unless defined? BACKGROUND
|
14
|
+
FONT = Curses::COLOR_BLACK unless defined? FONT
|
15
|
+
FONT_HEADING = Curses::COLOR_MAGENTA unless defined? FONT_HEADING
|
16
|
+
FONT_EMPHASIZE = Curses::COLOR_BLUE unless defined? FONT_EMPHASIZE
|
17
|
+
RED = Curses::COLOR_RED unless defined? RED
|
18
|
+
GREEN = Curses::COLOR_GREEN unless defined? GREEN
|
19
|
+
YELLOW = Curses::COLOR_YELLOW unless defined? YELLOW
|
20
|
+
|
21
|
+
def initialize(height = Curses.lines, width = Curses.cols, top = 0, left = 0)
|
22
|
+
super(height, width, top, left)
|
23
|
+
init_colors
|
24
|
+
end
|
25
|
+
|
26
|
+
def write(text, color = COLOR_TEXT, text_decoration = Curses::A_NORMAL )
|
27
|
+
self.attron(Curses.color_pair(color) | text_decoration) { self << text.to_s }
|
28
|
+
end
|
29
|
+
|
30
|
+
def emphasize(text, text_decoration = Curses::A_NORMAL)
|
31
|
+
write(text, Window::COLOR_TEXT_EMPHASIZE, text_decoration)
|
32
|
+
end
|
33
|
+
|
34
|
+
def heading(text, text_decoration = Curses::A_UNDERLINE)
|
35
|
+
write(text, Window::COLOR_HEADING, text_decoration)
|
36
|
+
end
|
37
|
+
|
38
|
+
def red(text, text_decoration = Curses::A_NORMAL, options = {})
|
39
|
+
colored(text, Window::COLOR_RED, text_decoration, options)
|
40
|
+
end
|
41
|
+
|
42
|
+
def yellow(text, text_decoration = Curses::A_NORMAL, options = {})
|
43
|
+
colored(text, Window::COLOR_YELLOW, text_decoration, options)
|
44
|
+
end
|
45
|
+
|
46
|
+
def green(text, text_decoration = Curses::A_NORMAL, options = {})
|
47
|
+
colored(text, Window::COLOR_GREEN, text_decoration, options)
|
48
|
+
end
|
49
|
+
|
50
|
+
def colored(text, color, text_decoration = Curses::A_NORMAL, options = {})
|
51
|
+
if options[:full_line]
|
52
|
+
clear_line(
|
53
|
+
color: color,
|
54
|
+
text_decoration: Curses::A_STANDOUT,
|
55
|
+
start_pos: 1,
|
56
|
+
end_pos: maxx - 2
|
57
|
+
)
|
58
|
+
|
59
|
+
prefix = ' '
|
60
|
+
end
|
61
|
+
|
62
|
+
write("#{prefix}#{text}", color, text_decoration)
|
63
|
+
end
|
64
|
+
|
65
|
+
# clear_line(options = {})
|
66
|
+
# options: [:color, :text_decoration, :start_pos, :end_pos]
|
67
|
+
def clear_line(options = {})
|
68
|
+
color = options[:color] || COLOR_TEXT
|
69
|
+
text_decoration = options[:text_decoration] || Curses::A_NORMAL
|
70
|
+
start = options[:start_pos] || 0
|
71
|
+
stop = options[:end_pos] || maxx
|
72
|
+
|
73
|
+
x = curx
|
74
|
+
setpos(cury, start)
|
75
|
+
write(' ' * (stop - 1), color, text_decoration)
|
76
|
+
setpos(cury, x)
|
77
|
+
refresh
|
78
|
+
end
|
79
|
+
|
80
|
+
def print_indicator(object, text = ' ', text_decoration = Curses::A_STANDOUT)
|
81
|
+
if object.respond_to?(:mastered?) && object.respond_to?(:started?)
|
82
|
+
if object.mastered?
|
83
|
+
green(text, text_decoration)
|
84
|
+
write ' '
|
85
|
+
elsif object.started?
|
86
|
+
yellow(text, text_decoration)
|
87
|
+
write ' '
|
88
|
+
else
|
89
|
+
red(text, text_decoration)
|
90
|
+
write ' '
|
91
|
+
end
|
92
|
+
elsif object.respond_to?(:mastered?)
|
93
|
+
if object.mastered?
|
94
|
+
green(text, text_decoration)
|
95
|
+
write ' '
|
96
|
+
else
|
97
|
+
red(text, text_decoration)
|
98
|
+
write ' '
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def print_markdown(text)
|
104
|
+
clear_line
|
105
|
+
|
106
|
+
h1 = /^\#{1}[^#]+/ # '# heading'
|
107
|
+
h2 = /^\#{2}[^#]+/ # '## sub heading'
|
108
|
+
bold = /(\*[^*]*\*)/ # '*text*'
|
109
|
+
line = /^-{3,}/ # '---' vertical line
|
110
|
+
code = /(\`*\`)/ # '`code line`'
|
111
|
+
|
112
|
+
case text
|
113
|
+
when h1
|
114
|
+
heading(text.gsub(/^#\s?/, ''))
|
115
|
+
when h2
|
116
|
+
text_decoration = Curses::A_UNDERLINE | Curses::A_NORMAL
|
117
|
+
emphasize(text.gsub(/^##\s?/, ''), text_decoration)
|
118
|
+
when bold || code
|
119
|
+
emphasized = false
|
120
|
+
highlighted = false
|
121
|
+
|
122
|
+
text.chars.each do |char|
|
123
|
+
if char == '*'
|
124
|
+
emphasized = !emphasized
|
125
|
+
next
|
126
|
+
end
|
127
|
+
|
128
|
+
if char == '`'
|
129
|
+
highlighted = !highlighted
|
130
|
+
next
|
131
|
+
end
|
132
|
+
|
133
|
+
if emphasized
|
134
|
+
emphasize(char)
|
135
|
+
elsif highlighted
|
136
|
+
red(char)
|
137
|
+
else
|
138
|
+
write(char)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
when line
|
142
|
+
write('-' * (Curses.cols - 2))
|
143
|
+
else
|
144
|
+
write(text)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
protected
|
149
|
+
|
150
|
+
def init_colors
|
151
|
+
Curses.start_color
|
152
|
+
Curses.init_pair(COLOR_TEXT, FONT, BACKGROUND)
|
153
|
+
Curses.init_pair(COLOR_TEXT_EMPHASIZE, FONT_EMPHASIZE, BACKGROUND)
|
154
|
+
Curses.init_pair(COLOR_HEADING, FONT_HEADING, BACKGROUND)
|
155
|
+
Curses.init_pair(COLOR_RED, RED, BACKGROUND)
|
156
|
+
Curses.init_pair(COLOR_GREEN, GREEN, BACKGROUND)
|
157
|
+
Curses.init_pair(COLOR_YELLOW, YELLOW, BACKGROUND)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Daigaku::Chapter do
|
4
|
+
|
5
|
+
it { is_expected.to respond_to :title }
|
6
|
+
it { is_expected.to respond_to :units }
|
7
|
+
it { is_expected.to respond_to :path }
|
8
|
+
|
9
|
+
it { is_expected.to respond_to :mastered? }
|
10
|
+
it { is_expected.to respond_to :started? }
|
11
|
+
|
12
|
+
let(:chapter_path) { chapter_dirs(course_dir_names.first).first }
|
13
|
+
|
14
|
+
before(:all) do
|
15
|
+
prepare_solutions
|
16
|
+
Daigaku.config.solutions_path = solutions_basepath
|
17
|
+
end
|
18
|
+
|
19
|
+
subject { Daigaku::Chapter.new(chapter_path) }
|
20
|
+
|
21
|
+
it "has the prescribed title" do
|
22
|
+
expect(subject.title).to eq chapter_titles.first
|
23
|
+
end
|
24
|
+
|
25
|
+
it "has the prescribed path" do
|
26
|
+
expect(subject.path).to eq chapter_path
|
27
|
+
end
|
28
|
+
|
29
|
+
it "is not started by default" do
|
30
|
+
expect(subject.started?).to be_falsey
|
31
|
+
end
|
32
|
+
|
33
|
+
it "is not mastered by default" do
|
34
|
+
expect(subject.mastered?).to be_falsey
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#units" do
|
38
|
+
it "loads the prescribed number of units" do
|
39
|
+
course_name = course_dir_names.first
|
40
|
+
chapter_name = File.basename(chapter_path)
|
41
|
+
units_count = available_units(course_name, chapter_name).count
|
42
|
+
expect(subject.units.count).to eq units_count
|
43
|
+
end
|
44
|
+
|
45
|
+
it "lazy-loads the units" do
|
46
|
+
expect(subject.instance_variable_get(:@units)).to be_nil
|
47
|
+
subject.units
|
48
|
+
expect(subject.instance_variable_get(:@units)).not_to be_nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#started?" do
|
53
|
+
it "returns true if at least one unit has been verified" do
|
54
|
+
allow(subject.units.first).to receive(:mastered?) { true }
|
55
|
+
expect(subject.started?).to be true
|
56
|
+
end
|
57
|
+
|
58
|
+
it "returns false if no unit has been verified" do
|
59
|
+
allow_any_instance_of(Daigaku::Unit).to receive(:mastered?) { false }
|
60
|
+
expect(subject.started?).to be false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#mastered?" do
|
65
|
+
it "returns true if all units have been verified" do
|
66
|
+
subject.units.each { |unit| unit.solution.verify! }
|
67
|
+
expect(subject.mastered?).to be true
|
68
|
+
end
|
69
|
+
|
70
|
+
it "returns false unless all units have been verified" do
|
71
|
+
allow_any_instance_of(Daigaku::Unit).to receive(:mastered?) { false }
|
72
|
+
allow(subject.units.first).to receive(:mastered?) { true }
|
73
|
+
expect(subject.mastered?).to be false
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Daigaku::Configuration do
|
4
|
+
|
5
|
+
subject { Daigaku::Configuration.send(:new) }
|
6
|
+
|
7
|
+
it { is_expected.to respond_to :solutions_path }
|
8
|
+
it { is_expected.to respond_to :solutions_path= }
|
9
|
+
it { is_expected.to respond_to :courses_path }
|
10
|
+
it { is_expected.to respond_to :courses_path= }
|
11
|
+
it { is_expected.to respond_to :storage_file }
|
12
|
+
it { is_expected.to respond_to :save }
|
13
|
+
it { is_expected.to respond_to :import! }
|
14
|
+
it { is_expected.to respond_to :summary }
|
15
|
+
|
16
|
+
before do
|
17
|
+
subject.instance_variable_set(:@storage_file, local_storage_file)
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#courses_path" do
|
21
|
+
it "returns the appropriate initial local courses path" do
|
22
|
+
courses_path = File.expand_path('~/.daigaku/courses', __FILE__)
|
23
|
+
expect(subject.courses_path).to eq courses_path
|
24
|
+
end
|
25
|
+
|
26
|
+
it "returns the appropriate set courses path" do
|
27
|
+
subject.courses_path = local_courses_path
|
28
|
+
expect(subject.courses_path).to eq local_courses_path
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#solutions_path=" do
|
33
|
+
it "raises an error if the given path is no existent dir" do
|
34
|
+
expect { subject.solutions_path = "no/existent/dir" }
|
35
|
+
.to raise_error(Daigaku::ConfigurationError)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#solutions_path" do
|
40
|
+
it "raises an error if not set" do
|
41
|
+
expect { subject.solutions_path }
|
42
|
+
.to raise_error(Daigaku::ConfigurationError)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "returns the solutions path if set" do
|
46
|
+
subject.solutions_path = test_basepath
|
47
|
+
expect { subject.solutions_path }.not_to raise_error
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "#storage_path" do
|
52
|
+
it "returns the appropriate path to the storge file" do
|
53
|
+
expect(subject.storage_file).to eq local_storage_file
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#save" do
|
58
|
+
it "saves the configured courses path to the daigaku database" do
|
59
|
+
subject.courses_path = local_courses_path
|
60
|
+
subject.save
|
61
|
+
|
62
|
+
expect(Daigaku::Database.courses_path).to eq local_courses_path
|
63
|
+
end
|
64
|
+
|
65
|
+
it "saves the configured solution_path to the daigaku database" do
|
66
|
+
path = File.join(test_basepath, 'test_solutions')
|
67
|
+
FileUtils.makedirs(path)
|
68
|
+
subject.solutions_path = path
|
69
|
+
subject.save
|
70
|
+
|
71
|
+
expect(Daigaku::Database.solutions_path).to eq path
|
72
|
+
end
|
73
|
+
|
74
|
+
it "does not save the storage file path" do
|
75
|
+
subject.save
|
76
|
+
expect(Daigaku::Database.storage_file).to be_nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "#import!" do
|
81
|
+
context "with non-existent daigaku database entries:" do
|
82
|
+
before do
|
83
|
+
FileUtils.rm(local_storage_file) if File.exist?(local_storage_file)
|
84
|
+
end
|
85
|
+
|
86
|
+
it "uses the default configuration" do
|
87
|
+
Daigaku::Database.courses_path = nil
|
88
|
+
Daigaku::Database.solutions_path = nil
|
89
|
+
subject.instance_variable_set(:@courses_path, local_courses_path)
|
90
|
+
|
91
|
+
loaded_config = subject.import!
|
92
|
+
|
93
|
+
expect(loaded_config.courses_path).to eq local_courses_path
|
94
|
+
expect { loaded_config.solutions_path }
|
95
|
+
.to raise_error Daigaku::ConfigurationError
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context "with existing daigaku database file:" do
|
100
|
+
it "returns a Daigaku::Configuration" do
|
101
|
+
expect(subject.import!).to be_a Daigaku::Configuration
|
102
|
+
end
|
103
|
+
|
104
|
+
it "loads the daigaku database entries into the configuration" do
|
105
|
+
wanted_courses_path = "/courses/path"
|
106
|
+
wanted_solutions_path = solutions_basepath
|
107
|
+
temp_solutions_path = File.join(solutions_basepath, 'temp')
|
108
|
+
FileUtils.makedirs(wanted_solutions_path)
|
109
|
+
FileUtils.makedirs(temp_solutions_path)
|
110
|
+
|
111
|
+
# save wanted settings
|
112
|
+
subject.courses_path = wanted_courses_path
|
113
|
+
subject.solutions_path = wanted_solutions_path
|
114
|
+
subject.save
|
115
|
+
|
116
|
+
# overwrite in memory settings
|
117
|
+
subject.courses_path = '/some/other/path/'
|
118
|
+
subject.solutions_path = temp_solutions_path
|
119
|
+
|
120
|
+
# fetch stored settings
|
121
|
+
subject.import!
|
122
|
+
expect(File.exist?(local_storage_file)).to be_truthy
|
123
|
+
expect(subject.courses_path).to eq wanted_courses_path
|
124
|
+
expect(subject.solutions_path).to eq wanted_solutions_path
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe '#summary' do
|
130
|
+
before do
|
131
|
+
subject.courses_path = "wanted/courses/path"
|
132
|
+
subject.solutions_path = solutions_basepath
|
133
|
+
@summary = subject.summary
|
134
|
+
end
|
135
|
+
|
136
|
+
it "returns a string" do
|
137
|
+
expect(@summary).to be_a String
|
138
|
+
end
|
139
|
+
|
140
|
+
it "returns a string with all properties but @configuration_file" do
|
141
|
+
expect(@summary.lines.count).to eq subject.instance_variables.count - 1
|
142
|
+
end
|
143
|
+
|
144
|
+
it "returns a string including the courses path" do
|
145
|
+
expect(@summary =~ /courses path/).to be_truthy
|
146
|
+
end
|
147
|
+
|
148
|
+
it "returns a string including the courses path" do
|
149
|
+
expect(@summary =~ /solutions path/).to be_truthy
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe '#initial_course' do
|
154
|
+
it { is_expected.to respond_to :initial_course }
|
155
|
+
|
156
|
+
it "returns the initial course github repo partial path" do
|
157
|
+
expect(subject.initial_course).to eq 'daigaku-ruby/Get_started_with_Ruby'
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Daigaku::Course do
|
4
|
+
|
5
|
+
it { is_expected.to respond_to :title }
|
6
|
+
it { is_expected.to respond_to :chapters }
|
7
|
+
it { is_expected.to respond_to :path }
|
8
|
+
it { is_expected.to respond_to :author }
|
9
|
+
it { is_expected.to respond_to :link }
|
10
|
+
|
11
|
+
it { is_expected.to respond_to :mastered? }
|
12
|
+
it { is_expected.to respond_to :started? }
|
13
|
+
|
14
|
+
let(:course_path) { course_dirs.first }
|
15
|
+
|
16
|
+
before(:all) do
|
17
|
+
prepare_solutions
|
18
|
+
Daigaku.config.solutions_path = solutions_basepath
|
19
|
+
end
|
20
|
+
|
21
|
+
subject { Daigaku::Course.new(course_path) }
|
22
|
+
|
23
|
+
it "has the prescribed title" do
|
24
|
+
expect(subject.title).to eq course_titles.first
|
25
|
+
end
|
26
|
+
|
27
|
+
it "has the prescribed path" do
|
28
|
+
expect(subject.path).to eq course_path
|
29
|
+
end
|
30
|
+
|
31
|
+
it "is not started by default" do
|
32
|
+
expect(subject.started?).to be_falsey
|
33
|
+
end
|
34
|
+
|
35
|
+
it "is not mastered by default" do
|
36
|
+
expect(subject.mastered?).to be_falsey
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#chapters" do
|
40
|
+
it "loads the prescribed number of chapters" do
|
41
|
+
expect(subject.chapters.count).to eq available_chapters(course_path).count
|
42
|
+
end
|
43
|
+
|
44
|
+
it "lazy-loads the chapters" do
|
45
|
+
expect(subject.instance_variable_get(:@chapters)).to be_nil
|
46
|
+
subject.chapters
|
47
|
+
expect(subject.instance_variable_get(:@chapters)).not_to be_nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "#started?" do
|
52
|
+
it "returns true if at least one chapter has been started" do
|
53
|
+
allow(subject.chapters.first).to receive(:started?) { true }
|
54
|
+
expect(subject.started?).to be true
|
55
|
+
end
|
56
|
+
|
57
|
+
it "returns false if no chapter has been started" do
|
58
|
+
allow_any_instance_of(Daigaku::Chapter).to receive(:started?) { false }
|
59
|
+
expect(subject.started?).to be false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "#mastered?" do
|
64
|
+
it "returns true if all chapters have been mastered" do
|
65
|
+
allow_any_instance_of(Daigaku::Chapter).to receive(:mastered?) { true }
|
66
|
+
expect(subject.mastered?).to be true
|
67
|
+
end
|
68
|
+
|
69
|
+
it "returns false unless all chapters have been mastered" do
|
70
|
+
allow_any_instance_of(Daigaku::Chapter).to receive(:mastered?) { false }
|
71
|
+
allow(subject.chapters.first).to receive(:mastered?) { true }
|
72
|
+
expect(subject.mastered?).to be false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|