From 5b124c0413dc072f88aaecf6061a82f9bc7df20c Mon Sep 17 00:00:00 2001 From: Armin Kirchner Date: Wed, 18 Jun 2025 11:20:31 +0200 Subject: [PATCH 1/7] test: Add system tests for exercise administration The exercise administration form is a critical pass. The test will secure the functionality with the upcoming changes. Relates to #2922 --- spec/system/exercises_system_spec.rb | 163 +++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 spec/system/exercises_system_spec.rb diff --git a/spec/system/exercises_system_spec.rb b/spec/system/exercises_system_spec.rb new file mode 100644 index 000000000..0deb38121 --- /dev/null +++ b/spec/system/exercises_system_spec.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Exercise creation', :js do + let!(:ruby) { create(:ruby) } + + before do + visit(sign_in_path) + fill_in('email', with: teacher.email) + fill_in('password', with: attributes_for(:teacher)[:password]) + click_button(I18n.t('sessions.new.link')) + wait_for_ajax + end + + context 'when an exercise created' do + let(:teacher) { create(:teacher) } + let(:submission_deadline) { 3.months.from_now.beginning_of_minute } + let(:late_submission_deadline) { submission_deadline + 1.week } + let(:title) { 'Ruby challenge' } + let(:internal_title) { 'Project Ruby: Table flip' } + + let(:code) do + <<~RUBY + def self.❨╯°□°❩╯︵┻━┻ + puts "Calm down, bro" + end + RUBY + end + + let(:description) do + <<~TEXT + Ruby challenge + + Do something with Ruby. + TEXT + end + + before do + visit(exercises_path) + click_on I18n.t('shared.new_model', model: Exercise.model_name.human) + wait_for_ajax + end + + it 'creates an exercise with nested data' do + fill_in Exercise.human_attribute_name(:title), with: title + fill_in Exercise.human_attribute_name(:internal_title), with: internal_title + + # description + within('.markdown-editor__wrapper') do + find('.ProseMirror').set(description) + end + + chosen_select(Exercise.human_attribute_name(:execution_environment), ruby.name) + + chosen_date_time_select(Exercise.human_attribute_name(:submission_deadline), submission_deadline) + + chosen_date_time_select(Exercise.human_attribute_name(:late_submission_deadline), late_submission_deadline) + + check Exercise.human_attribute_name(:public) + + click_on I18n.t('exercises.form.add_file') + + within(find_by_id('files').all('li').last) do + fill_in CodeOcean::File.human_attribute_name(:name), with: 'main' + + chosen_select(CodeOcean::File.human_attribute_name(:file_type), ruby.file_type.name) + chosen_select(CodeOcean::File.human_attribute_name(:role), I18n.t('code_ocean/files.roles.main_file')) + + check(CodeOcean::File.human_attribute_name(:read_only)) + + find_by_id('editor-edit').click + send_keys code.strip + end + + click_button I18n.t('shared.create', model: Exercise.model_name.human) + + expect(page).to have_text 'Exercise has successfully been created.' + + # Exercise is created with expected attributes + expect(page).to have_text(title) + expect(page).to have_text(internal_title) + expect(page).to have_text(submission_deadline.to_s) + expect(page).to have_text(late_submission_deadline.to_s) + expect(page).to have_text(ruby.name) + + description.lines.each do |line| + expect(page).to have_text(line.delete("\n")) + end + + # Exercise includes the code + find('span', text: 'main.rb').click + + code.lines.each do |code_line| + expect(page).to have_text(code_line.delete("\n")) + end + end + end + + context 'when an exercise is updated' do + let!(:exercise) { create(:fibonacci) } + let(:teacher) { exercise.user } + let(:deleted_file_name) { 'reference.rb' } + let(:updated_file_name) { 'exercise.rb' } + + before do + visit(exercises_path) + end + + it 'updates an exercise with nested data' do + click_on exercise.title + click_on 'Edit' + click_on I18n.t('shared.edit') + + fill_in Exercise.human_attribute_name(:difficulty), with: 5 + + find('span', text: updated_file_name).click + + within('.card', text: updated_file_name) do + fill_in CodeOcean::File.human_attribute_name(:name), with: 'main_exercise' + end + + find('span', text: deleted_file_name).click + + within('.card', text: deleted_file_name) do + accept_confirm do + find('div.btn', text: I18n.t('shared.destroy')).click + end + end + + click_button I18n.t('shared.update', model: Exercise.model_name.human) + + expect(page).to have_text("#{Exercise.human_attribute_name(:difficulty)}\n5") + expect(page).to have_text('main_exercise.rb') + expect(page).to have_no_text(deleted_file_name) + end + end + + def chosen_select(name, value) + id = first('label', text: name)[:for] + + set_value_for_chosen_element(id, value) + end + + def chosen_date_time_select(name, date) + id = first('label', text: name)[:for] + + set_value_for_chosen_element("#{id}_1i", date.year.to_s) + set_value_for_chosen_element("#{id}_2i", date.strftime('%B')) + set_value_for_chosen_element("#{id}_3i", date.day.to_s) + set_value_for_chosen_element("#{id}_4i", date.hour.to_s) + set_value_for_chosen_element("#{id}_5i", date.min.to_s) + end + + def set_value_for_chosen_element(id, value) + element = find_by_id("#{id}_chosen") + element.click + + within(element) do + first('.chosen-results li', text: value).click + end + end +end From a68573aa9e94e932c3be808416bc48ce8c0f708f Mon Sep 17 00:00:00 2001 From: Armin Kirchner Date: Wed, 9 Jul 2025 13:39:09 +0200 Subject: [PATCH 2/7] Removed double edit click --- spec/system/exercises_system_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/system/exercises_system_spec.rb b/spec/system/exercises_system_spec.rb index 0deb38121..5ae42be0c 100644 --- a/spec/system/exercises_system_spec.rb +++ b/spec/system/exercises_system_spec.rb @@ -109,7 +109,6 @@ def self.❨╯°□°❩╯︵┻━┻ it 'updates an exercise with nested data' do click_on exercise.title - click_on 'Edit' click_on I18n.t('shared.edit') fill_in Exercise.human_attribute_name(:difficulty), with: 5 From 2e679b6b8a43131ec646f5f0aa4b4a1795a8add3 Mon Sep 17 00:00:00 2001 From: Armin Kirchner Date: Wed, 16 Jul 2025 09:53:15 +0200 Subject: [PATCH 3/7] Update spec/system/exercises_system_spec.rb Co-authored-by: kkoehn --- spec/system/exercises_system_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/system/exercises_system_spec.rb b/spec/system/exercises_system_spec.rb index 5ae42be0c..2332c51a5 100644 --- a/spec/system/exercises_system_spec.rb +++ b/spec/system/exercises_system_spec.rb @@ -13,7 +13,7 @@ wait_for_ajax end - context 'when an exercise created' do + context 'when an exercise is created' do let(:teacher) { create(:teacher) } let(:submission_deadline) { 3.months.from_now.beginning_of_minute } let(:late_submission_deadline) { submission_deadline + 1.week } From dee7d0ad90daf2006891637915f490db9795b4f7 Mon Sep 17 00:00:00 2001 From: Armin Kirchner Date: Wed, 16 Jul 2025 23:16:35 +0200 Subject: [PATCH 4/7] Use translation to check exercise creation. --- spec/system/exercises_system_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/system/exercises_system_spec.rb b/spec/system/exercises_system_spec.rb index 2332c51a5..f57b28444 100644 --- a/spec/system/exercises_system_spec.rb +++ b/spec/system/exercises_system_spec.rb @@ -75,7 +75,8 @@ def self.❨╯°□°❩╯︵┻━┻ click_button I18n.t('shared.create', model: Exercise.model_name.human) - expect(page).to have_text 'Exercise has successfully been created.' + expect(page).to have_text \ + I18n.t('shared.object_created', model: Exercise.model_name.human) # Exercise is created with expected attributes expect(page).to have_text(title) From 2e438505fa4494270c9ce6a374eb7329c38d627d Mon Sep 17 00:00:00 2001 From: Armin Kirchner Date: Wed, 16 Jul 2025 23:22:58 +0200 Subject: [PATCH 5/7] Moved teacher creation up in the test context. --- spec/system/exercises_system_spec.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/system/exercises_system_spec.rb b/spec/system/exercises_system_spec.rb index f57b28444..0f75cbf92 100644 --- a/spec/system/exercises_system_spec.rb +++ b/spec/system/exercises_system_spec.rb @@ -4,6 +4,7 @@ RSpec.describe 'Exercise creation', :js do let!(:ruby) { create(:ruby) } + let(:teacher) { create(:teacher) } before do visit(sign_in_path) @@ -14,7 +15,6 @@ end context 'when an exercise is created' do - let(:teacher) { create(:teacher) } let(:submission_deadline) { 3.months.from_now.beginning_of_minute } let(:late_submission_deadline) { submission_deadline + 1.week } let(:title) { 'Ruby challenge' } @@ -99,8 +99,7 @@ def self.❨╯°□°❩╯︵┻━┻ end context 'when an exercise is updated' do - let!(:exercise) { create(:fibonacci) } - let(:teacher) { exercise.user } + let!(:exercise) { create(:fibonacci, user: teacher) } let(:deleted_file_name) { 'reference.rb' } let(:updated_file_name) { 'exercise.rb' } From 3503961cd19c495a4d567e67a245c46820290c21 Mon Sep 17 00:00:00 2001 From: Armin Kirchner Date: Wed, 16 Jul 2025 23:29:34 +0200 Subject: [PATCH 6/7] fix rubocop --- spec/system/exercises_system_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/system/exercises_system_spec.rb b/spec/system/exercises_system_spec.rb index 0f75cbf92..c6cb62f63 100644 --- a/spec/system/exercises_system_spec.rb +++ b/spec/system/exercises_system_spec.rb @@ -4,7 +4,7 @@ RSpec.describe 'Exercise creation', :js do let!(:ruby) { create(:ruby) } - let(:teacher) { create(:teacher) } + let(:teacher) { create(:teacher) } before do visit(sign_in_path) From 370e5abc9cf7e70280e58f66348d13df09da760b Mon Sep 17 00:00:00 2001 From: Armin Kirchner Date: Tue, 29 Jul 2025 19:09:02 +0200 Subject: [PATCH 7/7] Imporved test helper naming Make it clear this test helper target choosen JS. --- spec/system/exercises_system_spec.rb | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/spec/system/exercises_system_spec.rb b/spec/system/exercises_system_spec.rb index c6cb62f63..9614a174f 100644 --- a/spec/system/exercises_system_spec.rb +++ b/spec/system/exercises_system_spec.rb @@ -51,11 +51,11 @@ def self.❨╯°□°❩╯︵┻━┻ find('.ProseMirror').set(description) end - chosen_select(Exercise.human_attribute_name(:execution_environment), ruby.name) + chosenjs_select_helper(Exercise.human_attribute_name(:execution_environment), ruby.name) - chosen_date_time_select(Exercise.human_attribute_name(:submission_deadline), submission_deadline) + chosenjs_date_time_select_helper(Exercise.human_attribute_name(:submission_deadline), submission_deadline) - chosen_date_time_select(Exercise.human_attribute_name(:late_submission_deadline), late_submission_deadline) + chosenjs_date_time_select_helper(Exercise.human_attribute_name(:late_submission_deadline), late_submission_deadline) check Exercise.human_attribute_name(:public) @@ -64,8 +64,8 @@ def self.❨╯°□°❩╯︵┻━┻ within(find_by_id('files').all('li').last) do fill_in CodeOcean::File.human_attribute_name(:name), with: 'main' - chosen_select(CodeOcean::File.human_attribute_name(:file_type), ruby.file_type.name) - chosen_select(CodeOcean::File.human_attribute_name(:role), I18n.t('code_ocean/files.roles.main_file')) + chosenjs_select_helper(CodeOcean::File.human_attribute_name(:file_type), ruby.file_type.name) + chosenjs_select_helper(CodeOcean::File.human_attribute_name(:role), I18n.t('code_ocean/files.roles.main_file')) check(CodeOcean::File.human_attribute_name(:read_only)) @@ -135,23 +135,23 @@ def self.❨╯°□°❩╯︵┻━┻ end end - def chosen_select(name, value) + def chosenjs_select_helper(name, value) id = first('label', text: name)[:for] - set_value_for_chosen_element(id, value) + set_value_for_chosenjs_element(id, value) end - def chosen_date_time_select(name, date) + def chosenjs_date_time_select_helper(name, date) id = first('label', text: name)[:for] - set_value_for_chosen_element("#{id}_1i", date.year.to_s) - set_value_for_chosen_element("#{id}_2i", date.strftime('%B')) - set_value_for_chosen_element("#{id}_3i", date.day.to_s) - set_value_for_chosen_element("#{id}_4i", date.hour.to_s) - set_value_for_chosen_element("#{id}_5i", date.min.to_s) + set_value_for_chosenjs_element("#{id}_1i", date.year.to_s) + set_value_for_chosenjs_element("#{id}_2i", date.strftime('%B')) + set_value_for_chosenjs_element("#{id}_3i", date.day.to_s) + set_value_for_chosenjs_element("#{id}_4i", date.hour.to_s) + set_value_for_chosenjs_element("#{id}_5i", date.min.to_s) end - def set_value_for_chosen_element(id, value) + def set_value_for_chosenjs_element(id, value) element = find_by_id("#{id}_chosen") element.click