starproxima_library 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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