starproxima_library 0.1.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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +9 -0
  3. data/Gemfile.lock +69 -0
  4. data/diagrams/add_uthor_seq.png +0 -0
  5. data/diagrams/add_uthor_seq.puml +42 -0
  6. data/diagrams/author.png +0 -0
  7. data/diagrams/author.puml +118 -0
  8. data/diagrams/core.png +0 -0
  9. data/diagrams/core.puml +103 -0
  10. data/diagrams/delete_author.png +0 -0
  11. data/diagrams/delete_author.puml +25 -0
  12. data/diagrams/edit_author.png +0 -0
  13. data/diagrams/edit_author.puml +48 -0
  14. data/diagrams/er.png +0 -0
  15. data/diagrams/er.puml +30 -0
  16. data/diagrams/publisher.png +0 -0
  17. data/diagrams/publisher.puml +118 -0
  18. data/diagrams/requirements.docx +0 -0
  19. data/diagrams/start.png +0 -0
  20. data/diagrams/start.puml +74 -0
  21. data/lib/author/author_db_data_source.rb +90 -0
  22. data/lib/author/controllers/author_controller.rb +57 -0
  23. data/lib/author/controllers/author_input_form_controller_create.rb +44 -0
  24. data/lib/author/controllers/author_input_form_controller_edit.rb +53 -0
  25. data/lib/author/controllers/author_list_controller.rb +107 -0
  26. data/lib/author/ui/author_input_form.rb +69 -0
  27. data/lib/author/ui/author_list_view.rb +170 -0
  28. data/lib/controllers/tab_students_controller.rb +43 -0
  29. data/lib/data_sources/book_db_data_source.rb +43 -0
  30. data/lib/data_sources/db_client.rb +34 -0
  31. data/lib/db_config/config.yaml +5 -0
  32. data/lib/db_config/library_config.yaml +5 -0
  33. data/lib/db_config/migrations/create_db.sql +3 -0
  34. data/lib/db_config/migrations/create_tables.sql +27 -0
  35. data/lib/db_config/mock_data/mock_data.sql +49 -0
  36. data/lib/logger.rb +27 -0
  37. data/lib/main.rb +6 -0
  38. data/lib/models/author.rb +32 -0
  39. data/lib/models/book.rb +31 -0
  40. data/lib/models/publisher.rb +37 -0
  41. data/lib/models/student.rb +102 -0
  42. data/lib/models/student_base.rb +100 -0
  43. data/lib/models/student_short.rb +50 -0
  44. data/lib/publisher/controllers/publisher_input_form_controller_create.rb +44 -0
  45. data/lib/publisher/controllers/publisher_input_form_controller_edit.rb +52 -0
  46. data/lib/publisher/controllers/publisher_list_controller.rb +99 -0
  47. data/lib/publisher/publisher_db_data_source.rb +63 -0
  48. data/lib/publisher/ui/publisher_input_form.rb +69 -0
  49. data/lib/publisher/ui/publisher_list_view.rb +168 -0
  50. data/lib/repositories/adapters/db_source_adapter.rb +54 -0
  51. data/lib/repositories/adapters/file_source_adapter.rb +37 -0
  52. data/lib/repositories/containers/data_list.rb +74 -0
  53. data/lib/repositories/containers/data_list_student_short.rb +18 -0
  54. data/lib/repositories/containers/data_table.rb +35 -0
  55. data/lib/repositories/data_sources/db_data_source.rb +32 -0
  56. data/lib/repositories/data_sources/file_data_source.rb +75 -0
  57. data/lib/repositories/data_sources/transformers/data_transformer_base.rb +15 -0
  58. data/lib/repositories/data_sources/transformers/data_transformer_json.rb +16 -0
  59. data/lib/repositories/data_sources/transformers/data_transformer_yaml.rb +16 -0
  60. data/lib/repositories/student_repository.rb +32 -0
  61. data/lib/state_holders/list_state_notifier.rb +60 -0
  62. data/lib/views/main_window.rb +32 -0
  63. data/lib/views/tab_students.rb +148 -0
  64. data/starproxima_library.gemspec +15 -0
  65. data/test/author_test.rb +51 -0
  66. data/test/book_test.rb +33 -0
  67. data/test/publisher_test.rb +39 -0
  68. data/test/state_notifier_test.rb +80 -0
  69. metadata +123 -0
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './lib/models/student'
4
+ require './lib/models/student_short'
5
+ require './lib/repositories/containers/data_list_student_short'
6
+
7
+ class FileDataSource
8
+ attr_writer :data_transformer
9
+
10
+ def initialize(data_transformer)
11
+ self.students = []
12
+ self.seq_id = 1
13
+ self.data_transformer = data_transformer
14
+ end
15
+
16
+ def load_from_file(file_path)
17
+ hash_list = data_transformer.str_to_hash_list(File.read(file_path))
18
+ self.students = hash_list.map { |h| Student.from_hash(h) }
19
+ update_seq_id
20
+ end
21
+
22
+ def save_to_file(file_path)
23
+ hash_list = students.map(&:to_hash)
24
+ File.write(file_path, data_transformer.hash_list_to_str(hash_list))
25
+ end
26
+
27
+ def student_by_id(student_id)
28
+ students.detect { |s| s.id == student_id }
29
+ end
30
+
31
+ # Получить page по счету count элементов (страница начинается с 1)
32
+ def paginated_short_students(page, count, existing_data_list = nil)
33
+ offset = (page - 1) * count
34
+ slice = students[offset, count].map { |s| StudentShort.from_student(s) }
35
+
36
+ return DataListStudentShort.new(slice) if existing_data_list.nil?
37
+
38
+ existing_data_list.replace_objects(slice)
39
+ existing_data_list
40
+ end
41
+
42
+ def sorted
43
+ students.sort_by(&:last_name_and_initials)
44
+ end
45
+
46
+ def add_student(student)
47
+ student.id = seq_id
48
+ students << student
49
+ self.seq_id += 1
50
+ student.id
51
+ end
52
+
53
+ def replace_student(student_id, student)
54
+ idx = students.find_index { |s| s.id == student_id }
55
+ students[idx] = student
56
+ end
57
+
58
+ def remove_student(student_id)
59
+ students.reject! { |s| s.id == student_id }
60
+ end
61
+
62
+ def student_count
63
+ students.count
64
+ end
65
+
66
+ private
67
+
68
+ # Метод для актуализации seq_id
69
+ def update_seq_id
70
+ self.seq_id = students.max_by(&:id).id + 1
71
+ end
72
+
73
+ attr_reader :data_transformer
74
+ attr_accessor :students, :seq_id
75
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DataTransformerBase
4
+ private_class_method :new
5
+
6
+ protected
7
+
8
+ def str_to_hash_list(str)
9
+ raise NotImplementedError('Should be implemented in child')
10
+ end
11
+
12
+ def hash_list_to_str(hash_list)
13
+ raise NotImplementedError('Should be implemented in child')
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'data_transformer_base'
4
+ require 'json'
5
+
6
+ class DataTransformerJSON < DataTransformerBase
7
+ public_class_method :new
8
+
9
+ def str_to_hash_list(str)
10
+ JSON.parse(str, { symbolize_names: true })
11
+ end
12
+
13
+ def hash_list_to_str(hash_list)
14
+ JSON.pretty_generate(hash_list)
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'data_transformer_base'
4
+ require 'yaml'
5
+
6
+ class DataTransformerYAML < DataTransformerBase
7
+ public_class_method :new
8
+
9
+ def str_to_hash_list(str)
10
+ YAML.safe_load(str).map { |h| h.transform_keys(&:to_sym) }
11
+ end
12
+
13
+ def hash_list_to_str(hash_list)
14
+ hash_list.map { |h| h.transform_keys(&:to_s) }.to_yaml
15
+ end
16
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class StudentRepository
4
+ def initialize(data_source_adapter)
5
+ @data_source_adapter = data_source_adapter
6
+ end
7
+
8
+ def student_by_id(student_id)
9
+ @data_source_adapter.student_by_id(student_id)
10
+ end
11
+
12
+ # Получить page по счету count элементов (страница начинается с 1)
13
+ def paginated_short_students(page, count, existing_data_list = nil)
14
+ @data_source_adapter.paginated_short_students(page, count, existing_data_list)
15
+ end
16
+
17
+ def add_student(student)
18
+ @data_source_adapter.add_student(student)
19
+ end
20
+
21
+ def replace_student(student_id, student)
22
+ @data_source_adapter.replace_student(student_id, student)
23
+ end
24
+
25
+ def remove_student(student_id)
26
+ @data_source_adapter.remove_student(student_id)
27
+ end
28
+
29
+ def student_count
30
+ @data_source_adapter.student_count
31
+ end
32
+ end
@@ -0,0 +1,60 @@
1
+ class ListStateNotifier
2
+ attr_reader :items
3
+
4
+ def initialize
5
+ @items = []
6
+ @listeners = []
7
+ end
8
+
9
+ # устанавливает новое значение для items и уведомляет всех слушателей.
10
+ def set_all(objects)
11
+ LoggerHolder.instance.debug('ListStateNotifier: set_all')
12
+ @items = objects
13
+ notify_listeners
14
+ end
15
+
16
+ # добавляет объект в массив items и уведомляет всех слушателей.
17
+ def add(object)
18
+ LoggerHolder.instance.debug('ListStateNotifier: add')
19
+ @items << object
20
+ notify_listeners
21
+ end
22
+ # возвращает объект из массива items по индексу.
23
+ def get(number)
24
+ LoggerHolder.instance.debug('ListStateNotifier: get')
25
+ @items[number]
26
+ end
27
+
28
+ # удаляет объект из массива items и уведомляет всех слушателей.
29
+ def delete(object)
30
+ LoggerHolder.instance.debug('ListStateNotifier: delete')
31
+ @items.delete(object)
32
+ notify_listeners
33
+ end
34
+
35
+ # заменяет объект в массиве items на новый объект и уведомляет всех слушателей.
36
+ def replace(object, new_object)
37
+ LoggerHolder.instance.debug('ListStateNotifier: replace')
38
+ index = @items.index(object)
39
+ @items[index] = new_object
40
+ notify_listeners
41
+ end
42
+ # добавляет нового слушателя в массив listeners.
43
+ def add_listener(listener)
44
+ LoggerHolder.instance.debug('ListStateNotifier: add_listener')
45
+ @listeners << listener
46
+ end
47
+ # удаляет слушателя из массива listeners.
48
+ def delete_listener(listener)
49
+ LoggerHolder.instance.debug('ListStateNotifier: delete_listener')
50
+ @listeners.delete(listener)
51
+ end
52
+
53
+ # уведомляет всех слушателей о изменении массива items.
54
+ def notify_listeners
55
+ LoggerHolder.instance.debug('notify_listeners')
56
+ @listeners.each do |listener|
57
+ listener.update(@items)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'glimmer-dsl-libui'
4
+ require_relative 'tab_students'
5
+ require './lib/author/ui/author_list_view'
6
+ require './lib/publisher/ui/publisher_list_view'
7
+
8
+ class MainWindow
9
+ include Glimmer
10
+
11
+ def initialize
12
+ @view_tab_students = TabStudentsView.new
13
+ end
14
+
15
+ def create
16
+ window('Библиотека', 1000, 600) {
17
+ tab {
18
+ tab_item('Авторы') {
19
+ AuthorListView.new.create
20
+ }
21
+ tab_item('Издатели') {
22
+ PublisherListView.new.create
23
+ }
24
+
25
+
26
+ # tab_item('Студенты') {
27
+ # @view_tab_students.create
28
+ # }
29
+ }
30
+ }
31
+ end
32
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'glimmer-dsl-libui'
4
+ require './lib/controllers/tab_students_controller'
5
+
6
+ class TabStudentsView
7
+ include Glimmer
8
+
9
+ ITEMS_PER_PAGE = 20
10
+
11
+ def initialize
12
+ @controller = TabStudentsController.new(self)
13
+ @current_page = 1
14
+ @total_count = 0
15
+ end
16
+
17
+ def on_create
18
+ @controller.on_view_created
19
+ @controller.refresh_data(@current_page, ITEMS_PER_PAGE)
20
+ end
21
+
22
+ # Метод наблюдателя datalist
23
+ def on_datalist_changed(new_table)
24
+ arr = new_table.to_2d_array
25
+ arr.map { |row| row[3] = [row[3][:value], contact_color(row[3][:type])] }
26
+ @table.model_array = arr
27
+ end
28
+
29
+ def update_student_count(new_cnt)
30
+ @total_count = new_cnt
31
+ @page_label.text = "#{@current_page} / #{(@total_count / ITEMS_PER_PAGE.to_f).ceil}"
32
+ end
33
+
34
+ def contact_color(type)
35
+ case type
36
+ when 'telegram'
37
+ '#00ADB5'
38
+ when 'email'
39
+ '#F08A5D'
40
+ when 'phone'
41
+ '#B83B5E'
42
+ else
43
+ '#000000'
44
+ end
45
+ end
46
+
47
+ def create
48
+ root_container = horizontal_box {
49
+ # Секция 1
50
+ vertical_box {
51
+ stretchy false
52
+
53
+ form {
54
+ stretchy false
55
+
56
+ @filter_last_name_initials = entry {
57
+ label 'Фамилия И. О.'
58
+ }
59
+
60
+ @filters = {}
61
+ fields = [[:git, 'Гит'], [:email, 'Почта'], [:phone, 'Телефон'], [:telegram, 'Телеграм']]
62
+
63
+ fields.each do |field|
64
+ @filters[field[0]] = {}
65
+
66
+ @filters[field[0]][:combobox] = combobox {
67
+ label "#{field[1]} имеется?"
68
+ items ['Не важно', 'Есть', 'Нет']
69
+ selected 0
70
+
71
+ on_selected do
72
+ if @filters[field[0]][:combobox].selected == 1
73
+ @filters[field[0]][:entry].read_only = false
74
+ else
75
+ @filters[field[0]][:entry].text = ''
76
+ @filters[field[0]][:entry].read_only = true
77
+ end
78
+ end
79
+ }
80
+
81
+ @filters[field[0]][:entry] = entry {
82
+ label field[1]
83
+ read_only true
84
+ }
85
+ end
86
+ }
87
+ }
88
+
89
+ # Секция 2
90
+ vertical_box {
91
+ @table = refined_table(
92
+ table_editable: false,
93
+ filter: lambda do |row_hash, query|
94
+ utf8_query = query.force_encoding("utf-8")
95
+ row_hash['Фамилия И. О'].include?(utf8_query)
96
+ end,
97
+ table_columns: {
98
+ '#' => :text,
99
+ 'Фамилия И. О' => :text,
100
+ 'Гит' => :text,
101
+ 'Контакт' => :text_color
102
+ }
103
+ )
104
+
105
+ @pages = horizontal_box {
106
+ stretchy false
107
+
108
+ button("<") {
109
+ stretchy true
110
+
111
+ on_clicked do
112
+ @current_page = [@current_page - 1, 1].max
113
+ @controller.refresh_data(@current_page, ITEMS_PER_PAGE)
114
+ end
115
+
116
+ }
117
+ @page_label = label("...") { stretchy false }
118
+ button(">") {
119
+ stretchy true
120
+
121
+ on_clicked do
122
+ @current_page = [@current_page + 1, (@total_count / ITEMS_PER_PAGE.to_f).ceil].min
123
+ @controller.refresh_data(@current_page, ITEMS_PER_PAGE)
124
+ end
125
+ }
126
+ }
127
+ }
128
+
129
+ # Секция 3
130
+ vertical_box {
131
+ stretchy false
132
+
133
+ button('Добавить') { stretchy false }
134
+ button('Изменить') { stretchy false }
135
+ button('Удалить') { stretchy false }
136
+ button('Обновить') {
137
+ stretchy false
138
+
139
+ on_clicked {
140
+ @controller.refresh_data(@current_page, ITEMS_PER_PAGE)
141
+ }
142
+ }
143
+ }
144
+ }
145
+ on_create
146
+ root_container
147
+ end
148
+ end
@@ -0,0 +1,15 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = 'starproxima_library'
3
+ spec.version = '0.1.0'
4
+ spec.authors = ['StarProxima']
5
+ spec.email = 'starproxima@yandex.ru'
6
+ spec.summary = 'Starproxima library app'
7
+ spec.description = 'Description Starproxima library app'
8
+ spec.homepage = 'https://github.com/StarProxima/library_system'
9
+ spec.license = 'MIT'
10
+
11
+ spec.files = Dir.glob("**/*")
12
+ spec.require_paths = ['lib']
13
+
14
+ spec.add_dependency 'win32api'
15
+ end
@@ -0,0 +1,51 @@
1
+ require 'minitest/autorun'
2
+ require './lib/models/author'
3
+ class TestAuthor < Minitest::Test
4
+ def test_initialize_sets_attributes
5
+ author = Author.new(1, 'John', 'Doe', 'Smith')
6
+
7
+ assert_equal 1, author.author_id
8
+ assert_equal 'John', author.first_name
9
+ assert_equal 'Doe', author.last_name
10
+ assert_equal 'Smith', author.father_name
11
+ end
12
+
13
+ def test_initialize_raises_error_if_author_id_is_nil
14
+ assert_raises ArgumentError do
15
+ Author.new(nil, 'John', 'Doe')
16
+ end
17
+ end
18
+
19
+ def test_initialize_raises_error_if_first_name_is_nil
20
+ assert_raises ArgumentError do
21
+ Author.new(1, nil, 'Doe')
22
+ end
23
+ end
24
+
25
+ def test_initialize_raises_error_if_last_name_is_nil
26
+ assert_raises ArgumentError do
27
+ Author.new(1, 'John', nil)
28
+ end
29
+ end
30
+
31
+ def test_initialize_raises_error_if_first_name_exceeds_50_characters
32
+ long_name = 'a' * 51
33
+ assert_raises ArgumentError do
34
+ Author.new(1, long_name, 'Doe')
35
+ end
36
+ end
37
+
38
+ def test_initialize_raises_error_if_last_name_exceeds_50_characters
39
+ long_name = 'a' * 51
40
+ assert_raises ArgumentError do
41
+ Author.new(1, 'John', long_name)
42
+ end
43
+ end
44
+
45
+ def test_initialize_raises_error_if_father_name_exceeds_50_characters
46
+ long_name = 'a' * 51
47
+ assert_raises ArgumentError do
48
+ Author.new(1, 'John', 'Doe', long_name)
49
+ end
50
+ end
51
+ end
data/test/book_test.rb ADDED
@@ -0,0 +1,33 @@
1
+ require 'minitest/autorun'
2
+ require './lib/models/book'
3
+ class TestBook < Minitest::Test
4
+ def test_initialize_raises_error_with_null_book_id
5
+ assert_raises(ArgumentError) { Book.new(nil, 'title', 1, 1) }
6
+ end
7
+
8
+ def test_initialize_raises_error_with_null_title
9
+ assert_raises(ArgumentError) { Book.new(1, nil, 1, 1) }
10
+ end
11
+
12
+ def test_initialize_raises_error_with_null_author_id
13
+ assert_raises(ArgumentError) { Book.new(1, 'title', nil, 1) }
14
+ end
15
+
16
+ def test_initialize_raises_error_with_null_publisher_id
17
+ assert_raises(ArgumentError) { Book.new(1, 'title', 1, nil) }
18
+ end
19
+
20
+ def test_initialize_raises_error_with_long_title
21
+ long_title = 'a' * 256
22
+ assert_raises(ArgumentError) { Book.new(1, long_title, 1, 1) }
23
+ end
24
+
25
+ def test_book_attributes_are_set
26
+ book = Book.new(1, 'title', 2, 3)
27
+
28
+ assert_equal 1, book.book_id
29
+ assert_equal 'title', book.title
30
+ assert_equal 2, book.author_id
31
+ assert_equal 3, book.publisher_id
32
+ end
33
+ end
@@ -0,0 +1,39 @@
1
+ require 'minitest/autorun'
2
+ require './lib/models/publisher'
3
+ class TestPublisher < Minitest::Test
4
+ def test_initialize_with_valid_arguments
5
+ publisher = Publisher.new(1, "Test Publisher", "test@example.com")
6
+ assert_equal 1, publisher.publisher_id
7
+ assert_equal "Test Publisher", publisher.name
8
+ assert_equal "test@example.com", publisher.email
9
+ end
10
+
11
+ def test_initialize_with_null_publisher_id
12
+ error = assert_raises(ArgumentError) do
13
+ Publisher.new(nil, "Test Publisher", "test@example.com")
14
+ end
15
+ assert_equal "Argument 'publisher_id' cannot be null", error.message
16
+ end
17
+
18
+ def test_initialize_with_null_name
19
+ error = assert_raises(ArgumentError) do
20
+ Publisher.new(1, nil, "test@example.com")
21
+ end
22
+ assert_equal "Argument 'name' cannot be null", error.message
23
+ end
24
+
25
+ def test_initialize_with_name_length_exceeding_limit
26
+ long_name = "a" * 101
27
+ error = assert_raises(ArgumentError) do
28
+ Publisher.new(1, long_name, "test@example.com")
29
+ end
30
+ assert_equal "Name exceeds 100 characters limit: #{long_name}", error.message
31
+ end
32
+
33
+ def test_initialize_with_invalid_email_format
34
+ error = assert_raises(ArgumentError) do
35
+ Publisher.new(1, "Test Publisher", "invalid-email")
36
+ end
37
+ assert_equal "Invalid email format: invalid-email", error.message
38
+ end
39
+ end
@@ -0,0 +1,80 @@
1
+ require 'minitest/autorun'
2
+ require './lib/state_holders/list_state_notifier'
3
+ class TestListStateNotifier < Minitest::Test
4
+ def setup
5
+ @notifier = ListStateNotifier.new
6
+ end
7
+
8
+ def test_items_initialized_as_empty_array
9
+ assert_equal [], @notifier.items
10
+ end
11
+
12
+ def test_set_all_sets_items_and_notifies_listeners
13
+ listener = MiniTest::Mock.new
14
+ listener.expect(:update, nil, [[1, 2, 3]])
15
+ @notifier.add_listener(listener)
16
+
17
+ @notifier.set_all([1, 2, 3])
18
+
19
+ assert_equal [1, 2, 3], @notifier.items
20
+ listener.verify
21
+ end
22
+
23
+ def test_add_adds_item_and_notifies_listeners
24
+ listener = MiniTest::Mock.new
25
+ listener.expect(:update, nil, [@notifier.items])
26
+ @notifier.add_listener(listener)
27
+
28
+ @notifier.add(4)
29
+
30
+ assert_equal [4], @notifier.items
31
+ listener.verify
32
+ end
33
+
34
+ def test_get_returns_correct_item
35
+ @notifier.set_all([1, 2, 3])
36
+
37
+ assert_equal 2, @notifier.get(1)
38
+ end
39
+
40
+ def test_delete_removes_item_and_notifies_listeners
41
+ @notifier.set_all([1, 2, 3])
42
+ listener = MiniTest::Mock.new
43
+ listener.expect(:update, nil, [@notifier.items])
44
+ @notifier.add_listener(listener)
45
+
46
+ @notifier.delete(2)
47
+
48
+ assert_equal [1, 3], @notifier.items
49
+ listener.verify
50
+ end
51
+
52
+ def test_replace_replaces_item_and_notifies_listeners
53
+ @notifier.set_all([1, 2, 3])
54
+ listener = MiniTest::Mock.new
55
+ listener.expect(:update, nil, [@notifier.items])
56
+ @notifier.add_listener(listener)
57
+
58
+ @notifier.replace(2, 4)
59
+
60
+ assert_equal [1, 4, 3], @notifier.items
61
+ listener.verify
62
+ end
63
+
64
+ def test_add_listener_adds_listener
65
+ listener = MiniTest::Mock.new
66
+
67
+ @notifier.add_listener(listener)
68
+
69
+ assert_includes @notifier.instance_variable_get(:@listeners), listener
70
+ end
71
+
72
+ def test_delete_listener_removes_listener
73
+ listener = MiniTest::Mock.new
74
+ @notifier.add_listener(listener)
75
+
76
+ @notifier.delete_listener(listener)
77
+
78
+ refute_includes @notifier.instance_variable_get(:@listeners), listener
79
+ end
80
+ end