From 2c87a5b63c49e2cbe45ab049501e2bc7fe444d18 Mon Sep 17 00:00:00 2001 From: sto Date: Wed, 10 Dec 2025 15:12:19 +0100 Subject: [PATCH] Add url helpers & specs for offline participation forms https://gitea.puzzle-scoreboard.org/sto/puzzle-scoreboard/issues/5 --- app/controllers/contests_controller.rb | 4 +- config/routes.rb | 20 ++++ spec/factories/contests.rb | 4 + spec/features/offline_spec.rb | 145 +++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 spec/features/offline_spec.rb diff --git a/app/controllers/contests_controller.rb b/app/controllers/contests_controller.rb index d71f010..f6e8567 100644 --- a/app/controllers/contests_controller.rb +++ b/app/controllers/contests_controller.rb @@ -192,7 +192,7 @@ class ContestsController < ApplicationController @offline.contest = @contest @offline.start_time = Time.now() if @offline.save - redirect_to "/public/#{@contest.friendly_id}/offline/#{@offline.generate_token_for(:token)}" + redirect_to offline_form_edit_path(@contest, @offline) else render :offline_new, status: :unprocessable_entity end @@ -242,7 +242,7 @@ class ContestsController < ApplicationController remaining_pieces: @offline.remaining_pieces) extend_completions!(contestant) end - redirect_to "/public/#{@contest.friendly_id}/offline/#{@offline.generate_token_for(:token)}/completed" + redirect_to offline_form_completed_path(@contest, @offline) else render :offline_edit, status: :unprocessable_entity end diff --git a/config/routes.rb b/config/routes.rb index 8b1f712..f3224e8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -61,4 +61,24 @@ Rails.application.routes.draw do get "public/p/:contestant_id", to: "contestants#get_public_completion" post "public/p/:contestant_id", to: "contestants#post_public_completion" get "public/p/:contestant_id/updated", to: "contestants#public_completion_updated" + + direct :direct_test do + "https://lol.com" + end + + direct :public_scoreboard do |contest| + "/public/#{contest.friendly_id}/public" + end + + direct :offline_form do |contest| + "/public/#{contest.friendly_id}/offline" + end + + direct :offline_form_edit do |contest, offline| + "/public/#{contest.friendly_id}/offline/#{offline.generate_token_for(:token)}" + end + + direct :offline_form_completed do |contest, offline| + "/public/#{contest.friendly_id}/offline/#{offline.generate_token_for(:token)}/completed" + end end diff --git a/spec/factories/contests.rb b/spec/factories/contests.rb index 7460ea5..0ace6f5 100644 --- a/spec/factories/contests.rb +++ b/spec/factories/contests.rb @@ -36,4 +36,8 @@ FactoryBot.define do duration { "2:00" } ranking_mode { "actual" } end + + trait :offline do + offline_form { true } + end end diff --git a/spec/features/offline_spec.rb b/spec/features/offline_spec.rb new file mode 100644 index 0000000..bd64385 --- /dev/null +++ b/spec/features/offline_spec.rb @@ -0,0 +1,145 @@ +require 'rails_helper' + +RSpec.feature "Users", type: :feature do + context "when the contest doesn't allow offline participation" do + let!(:contest) { create(:contest, user: create(:user)) } + + it "shouldn't be possible to load the offline participation form" do + visit offline_form_path(contest) + + expect(page).to have_http_status(404) + end + end + + context "when the contest allows offline participation" do + let!(:contest) { create(:contest, :offline, user: create(:user)) } + + it "should be possible to load the offline participation form" do + visit offline_form_path(contest) + + expect(page).to have_http_status(200) + expect(page).to have_content(contest.name) + end + + it "shouldn't be possible to validate the form without a pseudo" do + visit offline_form_path(contest) + + fill_in I18n.t("activerecord.attributes.offline.name"), with: "" + click_button I18n.t("helpers.buttons.start") + + expect(page).to have_http_status(422) + expect(page).to have_content(I18n.t("activerecord.errors.models.offline.attributes.name.blank")) + end + + it "shouldn't be possible to validate the form without an image" do + visit offline_form_path(contest) + + fill_in I18n.t("activerecord.attributes.offline.name"), with: "my_name" + + expect { click_button I18n.t("helpers.buttons.start") }.not_to change { contest.reload.offlines.size } + expect(page).to have_http_status(422) + expect(page).to have_content(I18n.t("activerecord.errors.models.offline.attributes.start_image.blank")) + end + + it "should be possible to start the offline participation with a valid name and start image" do + start_image_file = Tempfile.new('start_image') + begin + visit offline_form_path(contest) + + fill_in I18n.t("activerecord.attributes.offline.name"), with: "my_name" + attach_file("offline[images]", start_image_file.path) + + expect { click_button I18n.t("helpers.buttons.start") }.to change { contest.reload.offlines.size }.to(1) + expect(page).to have_http_status(200) + expect(page).to have_current_path(offline_form_edit_path(contest, contest.offlines[0])) + ensure + start_image_file.close + start_image_file.unlink + end + end + + it "shouldn't be possible to complete the offline participation without an end image" do + start_image_file = Tempfile.new('start_image') + begin + visit offline_form_path(contest) + + fill_in I18n.t("activerecord.attributes.offline.name"), with: "my_name" + attach_file("offline[images]", start_image_file.path) + click_button I18n.t("helpers.buttons.start") + + expect { click_button I18n.t("helpers.buttons.end") }.not_to change { contest.offlines[0].images.size } + expect(page).to have_http_status(422) + expect(page).to have_content(I18n.t("activerecord.errors.models.offline.attributes.end_image.blank")) + ensure + start_image_file.close + start_image_file.unlink + end + end + + it "shouldn't be possible to complete the offline participation without remaining pieces count when the puzzle isn't completed" do + start_image_file = Tempfile.new('start_image') + begin + visit offline_form_path(contest) + + fill_in I18n.t("activerecord.attributes.offline.name"), with: "my_name" + attach_file("offline[images]", start_image_file.path) + click_button I18n.t("helpers.buttons.start") + + expect { click_button I18n.t("helpers.buttons.end") }.not_to change { contest.offlines[0].images.size } + expect(page).to have_http_status(422) + expect(page).to have_content(I18n.t("activerecord.errors.models.offline.attributes.remaining_pieces.blank")) + ensure + start_image_file.close + start_image_file.unlink + end + end + + it "should be possible to complete the offline participation with the end image and remaining pieces count when the puzzle isn't completed" do + start_image_file = Tempfile.new('start_image') + end_image_file = Tempfile.new('end_image') + begin + visit offline_form_path(contest) + + fill_in I18n.t("activerecord.attributes.offline.name"), with: "my_name" + attach_file("offline[images]", start_image_file.path) + click_button I18n.t("helpers.buttons.start") + + fill_in I18n.t("activerecord.attributes.offline.remaining_pieces"), with: "10" + attach_file("offline[end_image]", end_image_file.path) + + expect { click_button I18n.t("helpers.buttons.end") }.to change { contest.offlines[0].images.size }.to(2) + expect(page).to have_http_status(200) + expect(page).to have_current_path(offline_form_completed_path(contest, contest.offlines[0])) + ensure + start_image_file.close + start_image_file.unlink + end_image_file.close + end_image_file.unlink + end + end + + it "should be possible to complete the offline participation with the end image solely when the puzzle is completed" do + start_image_file = Tempfile.new('start_image') + end_image_file = Tempfile.new('end_image') + begin + visit offline_form_path(contest) + + fill_in I18n.t("activerecord.attributes.offline.name"), with: "my_name" + attach_file("offline[images]", start_image_file.path) + click_button I18n.t("helpers.buttons.start") + + check I18n.t("activerecord.attributes.offline.completed") + attach_file("offline[end_image]", end_image_file.path) + + expect { click_button I18n.t("helpers.buttons.end") }.to change { contest.offlines[0].images.size }.to(2) + expect(page).to have_http_status(200) + expect(page).to have_current_path(offline_form_completed_path(contest, contest.offlines[0])) + ensure + start_image_file.close + start_image_file.unlink + end_image_file.close + end_image_file.unlink + end + end + end +end