diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fdbebc9adb3..0cd46a03b71 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,7 +12,7 @@ concurrency:
jobs:
typescript:
- name: typescript
+ name: TypeScript check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
@@ -31,7 +31,7 @@ jobs:
- name: tsc on resulting generated files
run: yarn run tsc --noEmit
vitest:
- name: vitest
+ name: Vitest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
@@ -69,7 +69,7 @@ jobs:
path: coverage/*
minitest:
permissions: write-all
- name: minitest
+ name: Minitest
runs-on: ubuntu-latest
env:
TEST_DATABASE_URL: postgres://postgres:postgres@localhost/intercode_test
@@ -124,8 +124,78 @@ jobs:
with:
name: minitest-coverage
path: coverage/*
+ minitest-system:
+ permissions: write-all
+ name: Minitest system tests
+ runs-on: ubuntu-latest
+ env:
+ TEST_DATABASE_URL: postgres://postgres:postgres@localhost/intercode_test
+ RAILS_ENV: test
+ services:
+ postgres:
+ image: postgres:18
+ env:
+ POSTGRES_PASSWORD: postgres
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ - 5432:5432
+ steps:
+ - uses: actions/checkout@v5
+ - name: Install libvips42
+ run: sudo apt-get update && sudo apt-get install libvips42
+ - name: Upgrade postgres client utilities
+ run: |
+ sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
+ wget -qO- https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo tee /etc/apt/trusted.gpg.d/pgdg.asc &>/dev/null
+ sudo apt-get update
+ sudo apt-get install postgresql-client-18 -y
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ bundler-cache: true
+ - name: Read .node-version
+ id: node-version
+ run: echo "node-version=$(cat .node-version)" >> $GITHUB_OUTPUT
+ - name: install node
+ uses: actions/setup-node@v6
+ with:
+ cache: yarn
+ node-version: ${{ steps.node-version.outputs.node-version }}
+ - name: yarn install
+ run: yarn install
+ - name: build
+ run: yarn run build
+ - name: Database setup
+ run: bundle exec rails db:create db:migrate
+ - name: Run tests
+ run: TERM=xterm-color bundle exec rails test:system
+ - name: Publish Test Report
+ uses: mikepenz/action-junit-report@v6
+ if: always() # always run even if the previous step fails
+ with:
+ check_name: "Minitest System Test Report"
+ report_paths: "test/reports/TEST-*.xml"
+ detailed_summary: true
+ skip_success_summary: true
+ - name: Archive HTML test reports
+ uses: actions/upload-artifact@v5
+ if: always()
+ with:
+ name: minitest-system-reports
+ path: test/html_reports/*
+ - name: Archive coverage report
+ uses: actions/upload-artifact@v5
+ if: always()
+ with:
+ name: minitest-system-coverage
+ path: coverage/*
docker-build:
runs-on: ubuntu-latest
+ name: Build containers
steps:
- uses: actions/checkout@v5
- name: Read .node-version
@@ -231,23 +301,30 @@ jobs:
path: doc-site.tar.gz
coverage-report:
runs-on: ubuntu-latest
+ name: Test coverage report
if: github.actor != 'dependabot[bot]'
needs:
- vitest
- minitest
+ - minitest-system
steps:
- name: Download Minitest coverage
uses: actions/download-artifact@v6
with:
name: minitest-coverage
path: minitest-coverage
+ - name: Download Minitest system test coverage
+ uses: actions/download-artifact@v6
+ with:
+ name: minitest-system-coverage
+ path: minitest-system-coverage
- name: Download Vitest coverage
uses: actions/download-artifact@v6
with:
name: vitest-coverage
path: vitest-coverage
- name: Merge coverage reports
- run: npx cobertura-merge -o merged-coverage.xml package1=minitest-coverage/coverage.xml package2=vitest-coverage/cobertura-coverage.xml
+ run: npx cobertura-merge -o merged-coverage.xml package1=minitest-coverage/coverage.xml package2=vitest-coverage/cobertura-coverage.xml package3=minitest-system-coverage/coverage.xml
- name: Generate Coverage Report
uses: clearlyip/code-coverage-report-action@v6
id: code_coverage_report_action
@@ -264,11 +341,13 @@ jobs:
path: code-coverage-results.md
update-release-draft:
runs-on: ubuntu-latest
+ name: Update release draft
if: github.event_name == 'push' && github.event.ref == 'refs/heads/main'
needs:
- typescript
- vitest
- minitest
+ - minitest-system
- docker-build
- doc-site
outputs:
diff --git a/.rubocop.yml b/.rubocop.yml
index 5e93c218724..77b3a2ba263 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,11 +1,12 @@
inherit_from: .rubocop_todo.yml
-require:
+plugins:
- rubocop-performance
- rubocop-rails
- rubocop-sequel
- rubocop-factory_bot
- rubocop-graphql
+ - rubocop-capybara
inherit_gem:
prettier: rubocop.yml
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index fc2ccd6da20..7ca668941c7 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -175,7 +175,7 @@ Naming/MethodParameterName:
# ForbiddenPrefixes: is_, has_, have_
# AllowedMethods: is_a?
# MethodDefinitionMacros: define_method, define_singleton_method
-Naming/PredicateName:
+Naming/PredicatePrefix:
Exclude:
- 'app/models/registration_policy/bucket.rb'
- 'app/models/run.rb'
diff --git a/Gemfile b/Gemfile
index 9a95000f874..ed1b3a85863 100644
--- a/Gemfile
+++ b/Gemfile
@@ -144,6 +144,7 @@ group :development do
gem "rubocop-factory_bot", require: false
gem "rubocop-graphql", require: false
gem "rubocop-rspec", require: false
+ gem "rubocop-capybara", require: false
gem "prettier", "4.0.4"
gem "prettier_print"
gem "syntax_tree"
@@ -171,6 +172,9 @@ group :intercode1_import do
end
group :test do
+ gem "capybara"
+ gem "cuprite"
+ gem "database_cleaner-active_record"
gem "minitest-spec-rails"
gem "minitest-reporters"
gem "minitest-focus"
diff --git a/Gemfile.lock b/Gemfile.lock
index bc78592a5dd..a8f9caa1edb 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -112,6 +112,8 @@ GEM
uri (>= 0.13.1)
acts_as_list (0.9.16)
activerecord (>= 3.0)
+ addressable (2.8.8)
+ public_suffix (>= 2.0.2, < 8.0)
ahoy_matey (5.4.1)
activesupport (>= 7.1)
device_detector (>= 1)
@@ -180,13 +182,29 @@ GEM
acts_as_list
cadmus
rails (>= 5.0.0)
+ capybara (3.40.0)
+ addressable
+ matrix
+ mini_mime (>= 0.1.3)
+ nokogiri (~> 1.11)
+ rack (>= 1.6.0)
+ rack-test (>= 0.6.3)
+ regexp_parser (>= 1.5, < 3.0)
+ xpath (~> 3.2)
cgi (0.4.2)
coderay (1.1.3)
concurrent-ruby (1.3.5)
connection_pool (2.5.4)
crass (1.0.6)
csv (3.3.5)
+ cuprite (0.17)
+ capybara (~> 3.0)
+ ferrum (~> 0.17.0)
dalli (3.2.8)
+ database_cleaner-active_record (2.2.2)
+ activerecord (>= 5.a)
+ database_cleaner-core (~> 2.0)
+ database_cleaner-core (2.0.1)
date (3.5.0)
dead_end (4.0.0)
debug (1.11.0)
@@ -260,6 +278,12 @@ GEM
logger
faraday-net_http (3.4.2)
net-http (~> 0.5)
+ ferrum (0.17.1)
+ addressable (~> 2.5)
+ base64 (~> 0.2)
+ concurrent-ruby (~> 1.1)
+ webrick (~> 1.7)
+ websocket-driver (~> 0.7)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
flamegraph (0.9.5)
@@ -338,6 +362,7 @@ GEM
net-pop
net-smtp
marcel (1.1.0)
+ matrix (0.4.3)
memory_profiler (1.1.0)
method_source (1.0.0)
mini_histogram (0.3.1)
@@ -432,6 +457,7 @@ GEM
psych (5.2.6)
date
stringio
+ public_suffix (7.0.0)
puma (7.1.0)
nio4r (~> 2.0)
pundit (2.5.2)
@@ -527,6 +553,9 @@ GEM
rubocop-ast (1.48.0)
parser (>= 3.3.7.2)
prism (~> 1.4)
+ rubocop-capybara (2.22.1)
+ lint_roller (~> 1.1)
+ rubocop (~> 1.72, >= 1.72.1)
rubocop-factory_bot (2.28.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
@@ -647,6 +676,8 @@ GEM
with_advisory_lock (7.0.2)
activerecord (>= 7.2)
zeitwerk (>= 2.7)
+ xpath (3.2.0)
+ nokogiri (~> 1.8)
yard (0.9.37)
zeitwerk (2.7.3)
@@ -672,10 +703,13 @@ DEPENDENCIES
business_time
cadmus!
cadmus_navbar (~> 0.1.0)
+ capybara
civil_service!
cloudwatch_scheduler!
csv
+ cuprite
dalli
+ database_cleaner-active_record
dead_end
debug
derailed_benchmarks
@@ -732,6 +766,7 @@ DEPENDENCIES
reverse_markdown
rollbar
rubocop (= 1.81.7)
+ rubocop-capybara
rubocop-factory_bot
rubocop-graphql
rubocop-performance
diff --git a/app/javascript/EventsApp/EventPage/EventPageRunCard.tsx b/app/javascript/EventsApp/EventPage/EventPageRunCard.tsx
index 61c617e7d68..2efec72f973 100644
--- a/app/javascript/EventsApp/EventPage/EventPageRunCard.tsx
+++ b/app/javascript/EventsApp/EventPage/EventPageRunCard.tsx
@@ -141,7 +141,7 @@ function EventPageRunCard({
return response.data?.createMySignup.signup;
}
},
- [event, run, revalidator],
+ [event, run, revalidator, client],
);
const createSignup = (signupOption: SignupOption) => {
diff --git a/app/javascript/packs/application.tsx b/app/javascript/packs/application.tsx
index 3aa5af2d80f..428c75c20d4 100644
--- a/app/javascript/packs/application.tsx
+++ b/app/javascript/packs/application.tsx
@@ -2,10 +2,7 @@ import 'regenerator-runtime/runtime';
import mountReactComponents from '../mountReactComponents';
import { StrictMode, use, useMemo } from 'react';
-import AuthenticityTokensManager, {
- AuthenticityTokensContext,
- getAuthenticityTokensURL,
-} from 'AuthenticityTokensContext';
+import AuthenticityTokensManager, { getAuthenticityTokensURL } from 'AuthenticityTokensContext';
import { createBrowserRouter, RouterContextProvider, RouterProvider } from 'react-router';
import { buildBrowserApolloClient } from 'useIntercodeApolloClient';
import {
@@ -16,7 +13,6 @@ import {
sessionContext,
} from 'AppContexts';
import { ClientConfigurationQueryData } from 'serverQueries.generated';
-import { ApolloProvider } from '@apollo/client/react';
import { appRootRoutes } from 'AppRouter';
const manager = new AuthenticityTokensManager(fetch, undefined, getAuthenticityTokensURL());
@@ -76,11 +72,7 @@ function DataModeApplicationEntry({
return (
-
-
-
-
-
+
);
}
diff --git a/app/javascript/root.tsx b/app/javascript/root.tsx
index 80d39c05960..aa00d573da4 100644
--- a/app/javascript/root.tsx
+++ b/app/javascript/root.tsx
@@ -1,22 +1,24 @@
+import { ApolloClient } from '@apollo/client';
import { ApolloProvider } from '@apollo/client/react';
-import { authenticityTokensManagerContext, clientConfigurationDataContext } from 'AppContexts';
+import { apolloClientContext, authenticityTokensManagerContext, clientConfigurationDataContext } from 'AppContexts';
import { ProviderStack } from 'AppWrapper';
import AuthenticityTokensManager, { AuthenticityTokensContext } from 'AuthenticityTokensContext';
import { ClientConfiguration } from 'graphqlTypes.generated';
-import { StrictMode, useMemo } from 'react';
+import { StrictMode } from 'react';
import { LoaderFunction, useLoaderData } from 'react-router';
import { ClientConfigurationQueryData } from 'serverQueries.generated';
-import { buildBrowserApolloClient } from 'useIntercodeApolloClient';
type RootLoaderData = {
clientConfigurationData: ClientConfigurationQueryData;
authenticityTokensManager: AuthenticityTokensManager;
+ client: ApolloClient;
};
export const loader: LoaderFunction = ({ context }) => {
const clientConfigurationData = context.get(clientConfigurationDataContext);
const authenticityTokensManager = context.get(authenticityTokensManagerContext);
- return { clientConfigurationData, authenticityTokensManager } satisfies RootLoaderData;
+ const client = context.get(apolloClientContext);
+ return { clientConfigurationData, client, authenticityTokensManager } satisfies RootLoaderData;
};
function RootProviderStack({ clientConfiguration }: { clientConfiguration: ClientConfiguration }) {
@@ -31,15 +33,11 @@ function RootProviderStack({ clientConfiguration }: { clientConfiguration: Clien
export default function Root() {
const loaderData = useLoaderData() as RootLoaderData;
- const client = useMemo(
- () => buildBrowserApolloClient(loaderData.authenticityTokensManager),
- [loaderData.authenticityTokensManager],
- );
return (
-
+
diff --git a/app/models/registration_policy.rb b/app/models/registration_policy.rb
index dcc067e71e1..d9ecddcf22f 100644
--- a/app/models/registration_policy.rb
+++ b/app/models/registration_policy.rb
@@ -142,7 +142,7 @@ def ==(other)
end
def as_json(*)
- super.merge(buckets: buckets.as_json(*))
+ super.merge("buckets" => buckets.as_json(*))
end
def blank?
diff --git a/config/application.rb b/config/application.rb
index f74ef636838..cb630c83262 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -20,6 +20,7 @@ class Application < Rails::Application
config.hosts << ENV.fetch("ASSETS_HOST", nil) if ENV["ASSETS_HOST"].present?
config.hosts << /.*#{Regexp.escape(ENV.fetch("INTERCODE_HOST", nil))}/ if ENV["INTERCODE_HOST"].present?
config.hosts << ->(host) { Convention.where(domain: host).any? }
+ config.hosts = [/.*/] if Rails.env.test?
config.host_authorization = { exclude: ->(request) { request.path =~ %r{\A/healthz(\z|/)} } }
# Settings in config/environments/* take precedence over those specified here.
diff --git a/lib/intercode/dynamic_cookie_domain.rb b/lib/intercode/dynamic_cookie_domain.rb
index f9b21161d59..959b30148b9 100644
--- a/lib/intercode/dynamic_cookie_domain.rb
+++ b/lib/intercode/dynamic_cookie_domain.rb
@@ -17,6 +17,7 @@ def app_level_domain(host)
def cookie_domain(env)
host = env['HTTP_HOST']&.split(':')&.first
return :all unless host
+ return :all if Rails.env.test?
# Safari blocks cross-domain cookies on .test domains :(
if host =~ /\.test$/
diff --git a/lib/intercode/virtual_host_constraint.rb b/lib/intercode/virtual_host_constraint.rb
index a4c1df8f678..494e1ff4dad 100644
--- a/lib/intercode/virtual_host_constraint.rb
+++ b/lib/intercode/virtual_host_constraint.rb
@@ -1,7 +1,21 @@
module Intercode
class VirtualHostConstraint
def matches?(request)
- request.env['intercode.convention']
+ request.env["intercode.convention"]
+ end
+ end
+
+ class << self
+ attr_accessor :overridden_virtual_host_domain
+ end
+
+ def self.with_virtual_host_domain(domain)
+ self.overridden_virtual_host_domain = domain
+
+ begin
+ yield
+ ensure
+ self.overridden_virtual_host_domain = nil
end
end
@@ -13,11 +27,13 @@ def initialize(app)
def call(env)
request = Rack::Request.new(env)
unless request.path =~ %r{\A#{Rails.application.config.assets.prefix}/}
- env['intercode.convention'] ||= Convention.find_by(domain: request.host)
- if ENV['FIND_VIRTUAL_HOST_DEBUG'].present?
- if env['intercode.convention']
- Rails.logger.info "Intercode::FindVirtualHost: request to #{request.host} mapped to \
-#{env['intercode.convention'].name}"
+ env["intercode.convention"] ||= Convention.find_by(
+ domain: Intercode.overridden_virtual_host_domain || request.host
+ )
+ if ENV["FIND_VIRTUAL_HOST_DEBUG"].present?
+ if env["intercode.convention"]
+ Rails.logger.info "Intercode::FindVirtualHost: request to #{request.host} mapped to
+#{env["intercode.convention"].name}"
else
Rails.logger.info "Intercode::FindVirtualHost: request to #{request.host} mapped to root site"
end
diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb
new file mode 100644
index 00000000000..d14cab51843
--- /dev/null
+++ b/test/application_system_test_case.rb
@@ -0,0 +1,16 @@
+require "test_helper"
+
+class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ include Devise::Test::IntegrationHelpers
+
+ driven_by :cuprite, screen_size: [1200, 800], options: {
+ headless: %w[0 false].exclude?(ENV.fetch("HEADLESS", nil)),
+ js_errors: true
+ }
+
+ self.use_transactional_tests = false
+
+ teardown do
+ DatabaseCleaner.clean
+ end
+end
diff --git a/test/controllers/authenticity_tokens_controller_test.rb b/test/controllers/authenticity_tokens_controller_test.rb
deleted file mode 100644
index 0237ae18c5f..00000000000
--- a/test/controllers/authenticity_tokens_controller_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class AuthenticityTokensControllerTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/test/controllers/calendars_controller_test.rb b/test/controllers/calendars_controller_test.rb
deleted file mode 100644
index 7a8ef455619..00000000000
--- a/test/controllers/calendars_controller_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class CalendarsControllerTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/test/controllers/csv_exports_controller_test.rb b/test/controllers/csv_exports_controller_test.rb
deleted file mode 100644
index 6b8020ccdec..00000000000
--- a/test/controllers/csv_exports_controller_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class CsvExportsControllerTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/test/controllers/email_forwarders_controller_test.rb b/test/controllers/email_forwarders_controller_test.rb
deleted file mode 100644
index e0e7940b804..00000000000
--- a/test/controllers/email_forwarders_controller_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require "test_helper"
-
-class EmailForwardersControllerTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/test/controllers/graphiql_controller_test.rb b/test/controllers/graphiql_controller_test.rb
deleted file mode 100644
index df044962466..00000000000
--- a/test/controllers/graphiql_controller_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class GraphiQLControllerTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/test/controllers/graphql_controller_test.rb b/test/controllers/graphql_controller_test.rb
index 8c7d2c2d876..e296b0270d0 100644
--- a/test/controllers/graphql_controller_test.rb
+++ b/test/controllers/graphql_controller_test.rb
@@ -1,7 +1,7 @@
require 'test_helper'
class GraphqlControllerTest < ActionDispatch::IntegrationTest
- let(:user_con_profile) { create :user_con_profile }
+ let(:user_con_profile) { create(:user_con_profile) }
let(:convention) { user_con_profile.convention }
setup do
@@ -25,7 +25,7 @@ class GraphqlControllerTest < ActionDispatch::IntegrationTest
post graphql_url, params: { 'query' => query }
assert_response :success
- json = JSON.parse(response.body)
+ json = response.parsed_body
refute json['errors'].present?, json['errors'].to_s
assert_equal 'UserConProfile', json['data']['convention']['my_profile']['__typename']
assert_equal user_con_profile.id.to_s, json['data']['convention']['my_profile']['id']
diff --git a/test/controllers/passwords_controller_test.rb b/test/controllers/passwords_controller_test.rb
deleted file mode 100644
index 18daa2159cb..00000000000
--- a/test/controllers/passwords_controller_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class PasswordsControllerTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/test/controllers/registrations_controller_test.rb b/test/controllers/registrations_controller_test.rb
index 3571cdd05ae..5413a5d775b 100644
--- a/test/controllers/registrations_controller_test.rb
+++ b/test/controllers/registrations_controller_test.rb
@@ -1,6 +1,6 @@
require 'test_helper'
-class RegistrationsControllerTest < ActionController::TestCase
+class RegistrationsControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
diff --git a/test/controllers/reports_controller_test.rb b/test/controllers/reports_controller_test.rb
deleted file mode 100644
index 1c1cc3320cb..00000000000
--- a/test/controllers/reports_controller_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class ReportsControllerTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/test/controllers/single_page_app_controller_test.rb b/test/controllers/single_page_app_controller_test.rb
deleted file mode 100644
index 1447441adc7..00000000000
--- a/test/controllers/single_page_app_controller_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class SinglePageAppControllerTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/test/controllers/sitemaps_controller_test.rb b/test/controllers/sitemaps_controller_test.rb
deleted file mode 100644
index 543c92706fa..00000000000
--- a/test/controllers/sitemaps_controller_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class SitemapsControllerTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/test/controllers/sns_notifications_controller_test.rb b/test/controllers/sns_notifications_controller_test.rb
deleted file mode 100644
index 2972e689d12..00000000000
--- a/test/controllers/sns_notifications_controller_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class SnsNotificationsControllerTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/test/controllers/stripe_account_controller_test.rb b/test/controllers/stripe_account_controller_test.rb
deleted file mode 100644
index 91ec72f108e..00000000000
--- a/test/controllers/stripe_account_controller_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class StripeAccountControllerTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/test/controllers/stripe_webhooks_controller_test.rb b/test/controllers/stripe_webhooks_controller_test.rb
deleted file mode 100644
index bee17f40e88..00000000000
--- a/test/controllers/stripe_webhooks_controller_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class StripeWebhooksControllerTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/test/controllers/user_con_profiles_controller_test.rb b/test/controllers/user_con_profiles_controller_test.rb
index 7fbbf27ae85..55ea1713f55 100644
--- a/test/controllers/user_con_profiles_controller_test.rb
+++ b/test/controllers/user_con_profiles_controller_test.rb
@@ -1,7 +1,7 @@
require 'test_helper'
describe UserConProfilesController do
- let(:user_con_profile) { create :user_con_profile }
+ let(:user_con_profile) { create(:user_con_profile) }
let(:convention) { user_con_profile.convention }
let(:con_admin_staff_position) { create(:admin_staff_position, convention: convention) }
let(:con_admin_profile) do
diff --git a/test/factories/cms_content_groups.rb b/test/factories/cms_content_groups.rb
index 5b5a9876d8a..60209ac0ceb 100644
--- a/test/factories/cms_content_groups.rb
+++ b/test/factories/cms_content_groups.rb
@@ -19,6 +19,6 @@
FactoryBot.define do
factory :cms_content_group do
sequence(:name) { |n| "content_group_#{n}" }
- association :parent, factory: :convention
+ parent factory: %i[convention]
end
end
diff --git a/test/factories/cms_files.rb b/test/factories/cms_files.rb
index b07c8c8303b..d2e16ccd51b 100644
--- a/test/factories/cms_files.rb
+++ b/test/factories/cms_files.rb
@@ -24,7 +24,7 @@
FactoryBot.define do
factory :cms_file do
file { Rack::Test::UploadedFile.new(File.expand_path('test/files/war_bond.png', Rails.root), 'image/png') }
- association :parent, factory: :convention
- association :uploader, factory: :user
+ parent factory: %i[convention]
+ uploader factory: %i[user]
end
end
diff --git a/test/factories/cms_graphql_queries.rb b/test/factories/cms_graphql_queries.rb
index f377c8061f6..acc9a545bd7 100644
--- a/test/factories/cms_graphql_queries.rb
+++ b/test/factories/cms_graphql_queries.rb
@@ -22,6 +22,6 @@
factory :cms_graphql_query do
sequence(:identifier) { |n| "graphql_query_#{n}" }
query { "query { convention: conventionByRequestHost { id } }" }
- association :parent, factory: :convention
+ parent factory: %i[convention]
end
end
diff --git a/test/factories/cms_layouts.rb b/test/factories/cms_layouts.rb
index 3b6ba9bd2a5..ee79973729e 100644
--- a/test/factories/cms_layouts.rb
+++ b/test/factories/cms_layouts.rb
@@ -23,6 +23,6 @@
factory :cms_layout do
sequence(:name) { |n| "layout_#{n}" }
content { "Some text" }
- association :parent, factory: :convention
+ parent factory: %i[convention]
end
end
diff --git a/test/factories/cms_navigation_items.rb b/test/factories/cms_navigation_items.rb
index ae8af61d1b5..2cc26e99214 100644
--- a/test/factories/cms_navigation_items.rb
+++ b/test/factories/cms_navigation_items.rb
@@ -29,6 +29,6 @@
FactoryBot.define do
factory :cms_navigation_item do
sequence(:title) { |n| "navigation item #{n}" }
- association :parent, factory: :convention
+ parent factory: %i[convention]
end
end
diff --git a/test/factories/cms_partials.rb b/test/factories/cms_partials.rb
index 06796c73c62..e93fb2f3e83 100644
--- a/test/factories/cms_partials.rb
+++ b/test/factories/cms_partials.rb
@@ -23,6 +23,6 @@
factory :cms_partial do
sequence(:name) { |n| "partial_#{n}" }
content { 'Some text' }
- association :parent, factory: :convention
+ parent factory: %i[convention]
end
end
diff --git a/test/factories/cms_variables.rb b/test/factories/cms_variables.rb
index e0e51bf2848..691eacca43f 100644
--- a/test/factories/cms_variables.rb
+++ b/test/factories/cms_variables.rb
@@ -22,6 +22,6 @@
factory :cms_variable do
sequence(:key) { |n| "variable_#{n}" }
value { "foobar" }
- association :parent, factory: :convention
+ parent factory: %i[convention]
end
end
diff --git a/test/factories/doorkeeper_applications.rb b/test/factories/doorkeeper_applications.rb
index c755ef2afc7..1008082f9a9 100644
--- a/test/factories/doorkeeper_applications.rb
+++ b/test/factories/doorkeeper_applications.rb
@@ -1,7 +1,7 @@
# Read about factories at https://github.com/thoughtbot/factory_bot
FactoryBot.define do
- factory :doorkeeper_application, class: Doorkeeper::Application do
+ factory :doorkeeper_application, class: 'Doorkeeper::Application' do
sequence(:name) { |n| "Application #{n}" }
redirect_uri { 'https://butt.holdings' }
end
diff --git a/test/factories/forms.rb b/test/factories/forms.rb
index 40111514501..cb866310003 100644
--- a/test/factories/forms.rb
+++ b/test/factories/forms.rb
@@ -22,7 +22,7 @@
FactoryBot.define do
factory :form do
- association :convention
+ convention
end
factory :event_form, parent: :form do
diff --git a/test/factories/maximum_event_provided_tickets_overrides.rb b/test/factories/maximum_event_provided_tickets_overrides.rb
index b1de2c71ffa..1f0b57953d2 100644
--- a/test/factories/maximum_event_provided_tickets_overrides.rb
+++ b/test/factories/maximum_event_provided_tickets_overrides.rb
@@ -24,7 +24,7 @@
FactoryBot.define do
factory :maximum_event_provided_tickets_override do
- association :event
+ event
override_value { 42 }
after(:build) do |mepto|
diff --git a/test/factories/order_entries.rb b/test/factories/order_entries.rb
index 005c0600f99..9f261d84a41 100644
--- a/test/factories/order_entries.rb
+++ b/test/factories/order_entries.rb
@@ -32,8 +32,8 @@
FactoryBot.define do
factory :order_entry do
- association :order
- association :product
+ order
+ product
quantity { 1 }
end
end
diff --git a/test/factories/orders.rb b/test/factories/orders.rb
index b8c97b015c4..c3723268cd2 100644
--- a/test/factories/orders.rb
+++ b/test/factories/orders.rb
@@ -28,6 +28,6 @@
FactoryBot.define do
factory :order do
status { "pending" }
- association :user_con_profile
+ user_con_profile
end
end
diff --git a/test/factories/organization_roles.rb b/test/factories/organization_roles.rb
index 8e9466239a6..91a73e0d51b 100644
--- a/test/factories/organization_roles.rb
+++ b/test/factories/organization_roles.rb
@@ -22,6 +22,6 @@
FactoryBot.define do
factory :organization_role do
sequence(:name) { |n| "Organization role #{n}" }
- association :organization
+ organization
end
end
diff --git a/test/factories/pages.rb b/test/factories/pages.rb
index 6db153a2585..a5b9669012c 100644
--- a/test/factories/pages.rb
+++ b/test/factories/pages.rb
@@ -32,6 +32,6 @@
factory :page do
sequence(:name) { |n| "Page #{n}" }
content { "MyText" }
- association :parent, factory: :convention
+ parent factory: %i[convention]
end
end
diff --git a/test/factories/permissions.rb b/test/factories/permissions.rb
index f6683ff5d0f..93800a546d2 100644
--- a/test/factories/permissions.rb
+++ b/test/factories/permissions.rb
@@ -33,12 +33,12 @@
# rubocop:enable Layout/LineLength, Lint/RedundantCopDisableDirective
FactoryBot.define do
- factory :organization_permission, class: Permission do
- association :organization_role
+ factory :organization_permission, class: 'Permission' do
+ organization_role
permission { "manage_organization_access" }
end
- factory :event_category_permission, class: Permission do
+ factory :event_category_permission, class: 'Permission' do
permission { "update_events" }
before(:create) do |permission|
diff --git a/test/factories/rooms.rb b/test/factories/rooms.rb
index 33d42f4975c..417e480cf48 100644
--- a/test/factories/rooms.rb
+++ b/test/factories/rooms.rb
@@ -21,7 +21,7 @@
FactoryBot.define do
factory :room do
- association(:convention)
+ convention
name { 'MyString' }
end
end
diff --git a/test/factories/staff_positions.rb b/test/factories/staff_positions.rb
index ac289ee587f..9887b01ed4f 100644
--- a/test/factories/staff_positions.rb
+++ b/test/factories/staff_positions.rb
@@ -29,7 +29,7 @@
name { 'Wrangler' }
end
- factory :admin_staff_position, class: StaffPosition do
+ factory :admin_staff_position, class: 'StaffPosition' do
convention
name { 'Chief Wrangler' }
after(:create) do |staff_position|
diff --git a/test/factories/ticket_types.rb b/test/factories/ticket_types.rb
index 74f75c6b4c9..183767acde1 100644
--- a/test/factories/ticket_types.rb
+++ b/test/factories/ticket_types.rb
@@ -26,13 +26,13 @@
#
# rubocop:enable Layout/LineLength, Lint/RedundantCopDisableDirective
FactoryBot.define do
- factory :free_ticket_type, class: TicketType do
+ factory :free_ticket_type, class: 'TicketType' do
convention
name { 'free' }
description { 'Free ticket' }
end
- factory :paid_ticket_type, class: TicketType do
+ factory :paid_ticket_type, class: 'TicketType' do
convention
name { 'paid' }
description { 'Paid ticket' }
@@ -48,14 +48,14 @@
end
end
- factory :event_provided_ticket_type, class: TicketType do
+ factory :event_provided_ticket_type, class: 'TicketType' do
convention
name { 'event_comp' }
description { 'Comp ticket for event' }
maximum_event_provided_tickets { 2 }
end
- factory :event_specific_ticket_type, class: TicketType do
+ factory :event_specific_ticket_type, class: 'TicketType' do
event
name { 'event_ticket' }
description { 'Event-specific ticket' }
diff --git a/test/graphql/intercode_schema_test.rb b/test/graphql/intercode_schema_test.rb
index d1e2e4297d6..59491b74077 100644
--- a/test/graphql/intercode_schema_test.rb
+++ b/test/graphql/intercode_schema_test.rb
@@ -1,8 +1,8 @@
require 'test_helper'
-class IntercodeSchemaTest < ActiveSupport::TestCase
+class IntercodeSchemaTest < ActiveSupport::TestCase # rubocop:disable GraphQL/ObjectDescription
it 'generates a schema definition without throwing an exception' do
definition = IntercodeSchema.to_definition
- assert_match /type Query/, definition
+ assert_match(/type Query/, definition)
end
end
diff --git a/test/graphql/queries/app_root_query_test.rb b/test/graphql/queries/app_root_query_test.rb
index 79bb8e8e24c..638d2382f25 100644
--- a/test/graphql/queries/app_root_query_test.rb
+++ b/test/graphql/queries/app_root_query_test.rb
@@ -1,6 +1,6 @@
require "test_helper"
-class Queries::AppRootQueryTest < ActiveSupport::TestCase
+class Queries::AppRootQueryTest < ActiveSupport::TestCase # rubocop:disable GraphQL/ObjectDescription
let(:convention) { create(:convention, :with_standard_content) }
let(:user_con_profile) { create(:user_con_profile, convention:) }
diff --git a/test/helpers/events_helper_test.rb b/test/helpers/events_helper_test.rb
deleted file mode 100644
index 2e7567e00cb..00000000000
--- a/test/helpers/events_helper_test.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-require 'test_helper'
-
-class EventsHelperTest < ActionView::TestCase
-end
diff --git a/test/helpers/team_member_helper_test.rb b/test/helpers/team_member_helper_test.rb
deleted file mode 100644
index 3c7ad568922..00000000000
--- a/test/helpers/team_member_helper_test.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-require 'test_helper'
-
-class TeamMemberHelperTest < ActionView::TestCase
-end
diff --git a/test/javascript/EventPage/EventPageRunCard.test.tsx b/test/javascript/EventPage/EventPageRunCard.test.tsx
new file mode 100644
index 00000000000..bd2a1477e6e
--- /dev/null
+++ b/test/javascript/EventPage/EventPageRunCard.test.tsx
@@ -0,0 +1,528 @@
+import { render, fireEvent, waitFor } from '../testUtils';
+import EventPageRunCard, { EventPageRunCardProps } from '../../../app/javascript/EventsApp/EventPage/EventPageRunCard';
+import { EventPageQueryData } from '../../../app/javascript/EventsApp/EventPage/queries.generated';
+import {
+ SignupMode,
+ SignupState,
+ SignupRankedChoiceState,
+ FormType,
+ SignupRequestState,
+} from '../../../app/javascript/graphqlTypes.generated';
+import {
+ CreateMySignupDocument,
+ CreateMySignupMutationData,
+ CreateSignupRankedChoiceDocument,
+ CreateSignupRankedChoiceMutationData,
+ WithdrawSignupRequestDocument,
+ WithdrawSignupRequestMutationData,
+} from '../../../app/javascript/EventsApp/EventPage/mutations.generated';
+import { MockLink } from '@apollo/client/testing';
+import { vi } from 'vitest';
+
+describe('EventPageRunCard', () => {
+ const mockEvent: EventPageQueryData['convention']['event'] = {
+ __typename: 'Event',
+ id: '1',
+ title: 'Test Event',
+ length_seconds: 14400,
+ private_signup_list: false,
+ my_rating: null,
+ can_play_concurrently: false,
+ form_response_attrs_json_with_rendered_markdown: '{}',
+ event_category: {
+ __typename: 'EventCategory',
+ id: '1',
+ team_member_name: 'GM',
+ teamMemberNamePlural: 'GMs',
+ },
+ ticket_types: [],
+ form: {
+ __typename: 'Form',
+ id: '1',
+ form_type: FormType.Event,
+ title: 'Event Form',
+ form_sections: [],
+ },
+ team_members: [],
+ registration_policy: {
+ __typename: 'RegistrationPolicy',
+ slots_limited: true,
+ prevent_no_preference_signups: false,
+ total_slots_including_not_counted: 10,
+ buckets: [
+ {
+ __typename: 'RegistrationPolicyBucket',
+ key: 'player',
+ name: 'Player',
+ description: 'Regular player',
+ not_counted: false,
+ slots_limited: true,
+ anything: false,
+ minimum_slots: 0,
+ total_slots: 10,
+ },
+ ],
+ },
+ runs: [],
+ };
+
+ const mockRun: EventPageQueryData['convention']['event']['runs'][0] = {
+ __typename: 'Run',
+ id: '1',
+ title_suffix: 'Friday Night',
+ starts_at: '2025-01-01T19:00:00Z',
+ current_ability_can_signup_summary_run: false,
+ grouped_signup_counts: [
+ {
+ __typename: 'GroupedSignupCount',
+ bucket_key: 'player',
+ count: 5,
+ counted: true,
+ state: SignupState.Confirmed,
+ team_member: false,
+ },
+ ],
+ rooms: [
+ {
+ __typename: 'Room',
+ id: '1',
+ name: 'Room 101',
+ },
+ ],
+ my_signups: [],
+ my_signup_requests: [],
+ my_signup_ranked_choices: [],
+ };
+
+ const mockMyProfile: EventPageQueryData['convention']['my_profile'] = {
+ __typename: 'UserConProfile',
+ id: '1',
+ signup_constraints: {
+ __typename: 'UserSignupConstraints',
+ at_maximum_signups: false,
+ },
+ };
+
+ const mockCurrentAbility: EventPageQueryData['currentAbility'] = {
+ __typename: 'Ability',
+ can_read_schedule: true,
+ can_update_event: false,
+ can_read_event_signups: false,
+ };
+
+ const mockSignupRounds: EventPageQueryData['convention']['signup_rounds'] = [];
+
+ const defaultProps: EventPageRunCardProps = {
+ event: mockEvent,
+ run: mockRun,
+ myProfile: mockMyProfile,
+ currentAbility: mockCurrentAbility,
+ signupRounds: mockSignupRounds,
+ addToQueue: false,
+ };
+
+ const renderEventPageRunCard = async (
+ props: Partial = {},
+ apolloMocks: MockLink.MockedResponse[] = [],
+ ) => {
+ return await render(, {
+ apolloMocks,
+ appRootContextValue: {
+ signupMode: SignupMode.SelfService,
+ timezoneName: 'America/New_York',
+ },
+ });
+ };
+
+ describe('rendering states', () => {
+ test('renders run card without signup', async () => {
+ const { getByText, getByRole, container } = await renderEventPageRunCard();
+ // Check for run title suffix
+ expect(getByText('Friday Night')).toBeTruthy();
+ // Check for signup button
+ expect(getByRole('button', { name: /Sign up now/i })).toBeTruthy();
+ // Check that bucket name is displayed in the capacity graph
+ expect(container.textContent).toMatch(/Player/);
+ });
+
+ test('renders with existing confirmed signup', async () => {
+ const runWithSignup = {
+ ...mockRun,
+ my_signups: [
+ {
+ __typename: 'Signup' as const,
+ id: '1',
+ state: SignupState.Confirmed,
+ waitlist_position: null,
+ counted: true,
+ expires_at: null,
+ },
+ ],
+ };
+
+ const { getByRole } = await renderEventPageRunCard({ run: runWithSignup });
+ // Should have a withdraw button when signed up
+ expect(getByRole('button', { name: /withdraw/i })).toBeTruthy();
+ });
+
+ test('renders with waitlisted signup', async () => {
+ const runWithWaitlist = {
+ ...mockRun,
+ my_signups: [
+ {
+ __typename: 'Signup' as const,
+ id: '1',
+ state: SignupState.Waitlisted,
+ waitlist_position: 3,
+ counted: true,
+ expires_at: null,
+ },
+ ],
+ };
+
+ const { container, getByRole } = await renderEventPageRunCard({ run: runWithWaitlist });
+ // Should show waitlist position in the card
+ expect(container.textContent).toMatch(/3/);
+ expect(getByRole('button', { name: /withdraw/i })).toBeTruthy();
+ });
+
+ test('renders with pending signup request', async () => {
+ const runWithRequest: EventPageRunCardProps['run'] = {
+ ...mockRun,
+ my_signup_requests: [
+ {
+ __typename: 'SignupRequest' as const,
+ id: '1',
+ state: SignupRequestState.Pending,
+ target_run: {
+ __typename: 'Run' as const,
+ id: '1',
+ },
+ requested_bucket_key: 'player',
+ replace_signup: null,
+ },
+ ],
+ };
+
+ const { getByRole } = await renderEventPageRunCard({ run: runWithRequest });
+ // Should have a withdraw signup request button
+ expect(getByRole('button', { name: /withdraw/i })).toBeTruthy();
+ });
+ });
+
+ describe('signup creation', () => {
+ test('creates self-service signup successfully', async () => {
+ const mockSignup = {
+ __typename: 'Signup' as const,
+ id: '2',
+ state: SignupState.Confirmed,
+ waitlist_position: null,
+ counted: true,
+ expires_at: null,
+ };
+
+ const mutationCalled = vi.fn();
+ const createSignupMock: MockLink.MockedResponse = {
+ request: {
+ query: CreateMySignupDocument,
+ variables: {
+ runId: '1',
+ requestedBucketKey: 'player',
+ noRequestedBucket: false,
+ },
+ },
+ result: () => {
+ mutationCalled();
+ return {
+ data: {
+ __typename: 'Mutation',
+ createMySignup: {
+ __typename: 'CreateMySignupPayload' as const,
+ signup: { ...mockSignup, run: { ...mockRun, my_signups: [...mockRun.my_signups, mockSignup] } },
+ },
+ },
+ };
+ },
+ };
+
+ const { getByRole } = await renderEventPageRunCard({}, [createSignupMock]);
+
+ // Find and click the signup button
+ const signupButton = getByRole('button', { name: /Sign up now/i }) as HTMLButtonElement;
+
+ // Verify button is initially enabled
+ expect(signupButton.disabled).toBe(false);
+
+ fireEvent.click(signupButton);
+
+ // Verify the button becomes disabled during the mutation
+ await waitFor(() => {
+ expect(signupButton.disabled).toBe(true);
+ });
+
+ // Verify the mutation was called
+ await waitFor(() => {
+ expect(mutationCalled).toHaveBeenCalledTimes(1);
+ });
+
+ // Verify button is re-enabled after mutation completes
+ await waitFor(() => {
+ expect(signupButton.disabled).toBe(false);
+ });
+ });
+
+ test('creates ranked choice for queue', async () => {
+ const mutationCalled = vi.fn();
+ const createRankedChoiceMock: MockLink.MockedResponse = {
+ request: {
+ query: CreateSignupRankedChoiceDocument,
+ variables: {
+ targetRunId: '1',
+ requestedBucketKey: 'player',
+ },
+ },
+ result: () => {
+ mutationCalled();
+ return {
+ data: {
+ __typename: 'Mutation',
+ createSignupRankedChoice: {
+ __typename: 'CreateSignupRankedChoicePayload' as const,
+ signup_ranked_choice: {
+ __typename: 'SignupRankedChoice' as const,
+ id: '1',
+ state: SignupRankedChoiceState.Pending,
+ priority: 1,
+ requested_bucket_key: 'player',
+ target_run: {
+ __typename: 'Run' as const,
+ id: '1',
+ },
+ },
+ },
+ },
+ };
+ },
+ };
+
+ const profileAtMaxSignups = {
+ ...mockMyProfile,
+ signup_constraints: {
+ __typename: 'UserSignupConstraints' as const,
+ at_maximum_signups: true,
+ },
+ };
+
+ const { getByRole } = await renderEventPageRunCard(
+ {
+ myProfile: profileAtMaxSignups,
+ addToQueue: true,
+ },
+ [createRankedChoiceMock],
+ );
+
+ // Should show "Add to queue" option
+ const queueButton = getByRole('button', { name: /Add to.*queue/i }) as HTMLButtonElement;
+ expect(queueButton).toBeTruthy();
+
+ fireEvent.click(queueButton);
+
+ // Verify the mutation was called
+ await waitFor(() => {
+ expect(mutationCalled).toHaveBeenCalledTimes(1);
+ });
+
+ // After adding to queue, the mutation completes successfully
+ // Note: The UI won't update to show "in queue" state without a full revalidation
+ // which would require mocking the EventPageQuery to return updated data
+ });
+ });
+
+ describe('signup withdrawal', () => {
+ test('withdraws pending signup request', async () => {
+ const runWithRequest: EventPageRunCardProps['run'] = {
+ ...mockRun,
+ my_signup_requests: [
+ {
+ __typename: 'SignupRequest' as const,
+ id: '1',
+ state: SignupRequestState.Pending,
+ target_run: {
+ __typename: 'Run' as const,
+ id: '1',
+ },
+ requested_bucket_key: 'player',
+ replace_signup: null,
+ },
+ ],
+ };
+
+ const mutationCalled = vi.fn();
+ const withdrawRequestMock: MockLink.MockedResponse = {
+ request: {
+ query: WithdrawSignupRequestDocument,
+ variables: {
+ id: '1',
+ },
+ },
+ result: () => {
+ mutationCalled();
+ return {
+ data: {
+ __typename: 'Mutation',
+ withdrawSignupRequest: {
+ __typename: 'WithdrawSignupRequestPayload' as const,
+ signup_request: {
+ __typename: 'SignupRequest' as const,
+ id: '1',
+ state: SignupRequestState.Withdrawn,
+ target_run: mockRun,
+ requested_bucket_key: 'player',
+ replace_signup: null,
+ },
+ },
+ },
+ };
+ },
+ };
+
+ const { getByText, getByRole } = await renderEventPageRunCard({ run: runWithRequest }, [withdrawRequestMock]);
+
+ const withdrawButton = getByText(/withdraw.*request/i);
+ fireEvent.click(withdrawButton);
+
+ // Should show confirmation dialog
+ await waitFor(() => {
+ expect(getByText(/Test Event/)).toBeTruthy();
+ });
+
+ // Click OK on the confirmation dialog to proceed with withdrawal
+ const okButton = await waitFor(() => getByRole('button', { name: 'OK' }));
+ fireEvent.click(okButton);
+
+ // Verify the mutation was called
+ await waitFor(() => {
+ expect(mutationCalled).toHaveBeenCalledTimes(1);
+ });
+
+ // After withdrawal, the mutation completes successfully
+ // Note: The UI won't update to remove the pending request without a full revalidation
+ // which would require mocking the EventPageQuery to return updated data
+ });
+ });
+
+ describe('moderated signup mode', () => {
+ test('renders signup button in moderated mode', async () => {
+ const { getByRole } = await renderEventPageRunCard({}, []);
+
+ const signupButton = getByRole('button', { name: /Sign up now/i });
+
+ // In moderated mode, clicking would open modal instead of creating signup directly
+ // The modal component is rendered but not visible until clicked
+ expect(signupButton).toBeTruthy();
+ });
+ });
+
+ describe('team member signup', () => {
+ test('shows team member signup option for team members', async () => {
+ const eventWithTeamMember = {
+ ...mockEvent,
+ team_members: [
+ {
+ __typename: 'TeamMember' as const,
+ id: '1',
+ email: 'gm@example.com',
+ display_team_member: true,
+ user_con_profile: {
+ __typename: 'UserConProfile' as const,
+ id: '1',
+ name_without_nickname: 'Test GM',
+ gravatar_enabled: false,
+ gravatar_url: '',
+ },
+ },
+ ],
+ };
+
+ const { getByText } = await renderEventPageRunCard({ event: eventWithTeamMember });
+ expect(getByText('GM')).toBeTruthy();
+ });
+ });
+
+ describe('signup options', () => {
+ test('shows no preference option when multiple buckets exist', async () => {
+ const eventWithMultipleBuckets = {
+ ...mockEvent,
+ registration_policy: {
+ ...mockEvent.registration_policy!,
+ buckets: [
+ {
+ __typename: 'RegistrationPolicyBucket' as const,
+ key: 'player',
+ name: 'Player',
+ description: 'Regular player',
+ not_counted: false,
+ slots_limited: true,
+ anything: false,
+ minimum_slots: 0,
+ total_slots: 5,
+ },
+ {
+ __typename: 'RegistrationPolicyBucket' as const,
+ key: 'premium',
+ name: 'Premium',
+ description: 'Premium player',
+ not_counted: false,
+ slots_limited: true,
+ anything: false,
+ minimum_slots: 0,
+ total_slots: 5,
+ },
+ ],
+ },
+ };
+
+ const { getByText } = await renderEventPageRunCard({ event: eventWithMultipleBuckets });
+ expect(getByText('Player')).toBeTruthy();
+ expect(getByText('Premium')).toBeTruthy();
+ expect(getByText('No preference')).toBeTruthy();
+ });
+
+ test('hides no preference when prevent_no_preference_signups is true', async () => {
+ const eventPreventingNoPreference = {
+ ...mockEvent,
+ registration_policy: {
+ ...mockEvent.registration_policy!,
+ prevent_no_preference_signups: true,
+ buckets: [
+ {
+ __typename: 'RegistrationPolicyBucket' as const,
+ key: 'player',
+ name: 'Player',
+ description: 'Regular player',
+ not_counted: false,
+ slots_limited: true,
+ anything: false,
+ minimum_slots: 0,
+ total_slots: 5,
+ },
+ {
+ __typename: 'RegistrationPolicyBucket' as const,
+ key: 'premium',
+ name: 'Premium',
+ description: 'Premium player',
+ not_counted: false,
+ slots_limited: true,
+ anything: false,
+ minimum_slots: 0,
+ total_slots: 5,
+ },
+ ],
+ },
+ };
+
+ const { queryByText } = await renderEventPageRunCard({ event: eventPreventingNoPreference });
+ expect(queryByText('No preference')).toBeNull();
+ });
+ });
+});
diff --git a/test/jobs/notify_event_proposal_changes_job_test.rb b/test/jobs/notify_event_proposal_changes_job_test.rb
deleted file mode 100644
index 65d66589695..00000000000
--- a/test/jobs/notify_event_proposal_changes_job_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class NotifyEventProposalChangesJobTest < ActiveJob::TestCase
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/test/mailers/user_mailer_test.rb b/test/mailers/user_mailer_test.rb
deleted file mode 100644
index 67a1629cc9f..00000000000
--- a/test/mailers/user_mailer_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class UserMailerTest < ActionMailer::TestCase
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/test/models/signup_round_test.rb b/test/models/signup_round_test.rb
index 6a7f7f7fea3..25f7601c471 100644
--- a/test/models/signup_round_test.rb
+++ b/test/models/signup_round_test.rb
@@ -29,7 +29,7 @@ class SignupRoundTest < ActiveSupport::TestCase
describe "#execute!" do
it "does nothing when automation_action is nil" do
mock_service = Minitest::Mock.new
- signup_round = FactoryBot.create(:signup_round, automation_action: nil)
+ signup_round = create(:signup_round, automation_action: nil)
ExecuteRankedChoiceSignupRoundService.stub :new, mock_service do
signup_round.execute!
@@ -41,9 +41,9 @@ class SignupRoundTest < ActiveSupport::TestCase
it "runs ranked choice signups when configured to do so" do
mock_service = Minitest::Mock.new
mock_service.expect :call!, nil
- convention = FactoryBot.create(:convention, signup_automation_mode: "ranked_choice")
+ convention = create(:convention, signup_automation_mode: "ranked_choice")
signup_round =
- FactoryBot.create(
+ create(
:signup_round,
convention:,
automation_action: "execute_ranked_choice",
@@ -59,8 +59,8 @@ class SignupRoundTest < ActiveSupport::TestCase
it "does not run ranked choice signups when automation_action is nil, even if in a ranked-choice con" do
mock_service = Minitest::Mock.new
- convention = FactoryBot.create(:convention, signup_automation_mode: "ranked_choice")
- signup_round = FactoryBot.create(:signup_round, convention:, automation_action: nil)
+ convention = create(:convention, signup_automation_mode: "ranked_choice")
+ signup_round = create(:signup_round, convention:, automation_action: nil)
ExecuteRankedChoiceSignupRoundService.stub :new, mock_service do
signup_round.execute!
diff --git a/test/services/create_team_member_service_test.rb b/test/services/create_team_member_service_test.rb
index 18ac6149d86..521173cf35d 100644
--- a/test/services/create_team_member_service_test.rb
+++ b/test/services/create_team_member_service_test.rb
@@ -1,11 +1,11 @@
require 'test_helper'
class CreateTeamMemberServiceTest < ActiveSupport::TestCase
- let(:convention) { create :convention, :with_notification_templates }
+ let(:convention) { create(:convention, :with_notification_templates) }
let(:event_category) { create(:event_category, convention: convention, can_provide_tickets: true) }
- let(:event) { create :event, convention: convention, event_category: event_category }
- let(:the_run) { create :run, event: event }
- let(:user_con_profile) { create :user_con_profile, convention: convention }
+ let(:event) { create(:event, convention: convention, event_category: event_category) }
+ let(:the_run) { create(:run, event: event) }
+ let(:user_con_profile) { create(:user_con_profile, convention: convention) }
let(:user) { user_con_profile.user }
let(:team_member_attrs) { {} }
let(:provide_ticket_type_id) { nil }
diff --git a/test/services/event_change_registration_policy_service_test.rb b/test/services/event_change_registration_policy_service_test.rb
index 04119e7bf77..5037bfa9ac3 100644
--- a/test/services/event_change_registration_policy_service_test.rb
+++ b/test/services/event_change_registration_policy_service_test.rb
@@ -4,8 +4,8 @@ class EventChangeRegistrationPolicyServiceTest < ActiveSupport::TestCase
include ActiveJob::TestHelper
let(:convention) { create(:convention, :with_notification_templates) }
- let(:event) { create :event, convention: convention }
- let(:the_run) { create :run, event: event }
+ let(:event) { create(:event, convention: convention) }
+ let(:the_run) { create(:run, event: event) }
let(:new_registration_policy) do
RegistrationPolicy.new(
buckets: [
@@ -15,8 +15,8 @@ class EventChangeRegistrationPolicyServiceTest < ActiveSupport::TestCase
]
)
end
- let(:whodunit) { create :user_con_profile, convention: convention }
- let(:team_member) { create :team_member, event: event, receive_signup_email: 'all_signups' }
+ let(:whodunit) { create(:user_con_profile, convention: convention) }
+ let(:team_member) { create(:team_member, event: event, receive_signup_email: 'all_signups') }
subject { EventChangeRegistrationPolicyService.new(event, new_registration_policy, whodunit) }
@@ -40,7 +40,7 @@ class EventChangeRegistrationPolicyServiceTest < ActiveSupport::TestCase
end
describe 'with existing signups in buckets that will be removed' do
- let(:user_con_profile) { create :user_con_profile, convention: convention }
+ let(:user_con_profile) { create(:user_con_profile, convention: convention) }
let(:signup) do
create(
:signup,
@@ -93,8 +93,8 @@ class EventChangeRegistrationPolicyServiceTest < ActiveSupport::TestCase
)
end
- let(:user_con_profile1) { create :user_con_profile, convention: convention }
- let(:user_con_profile2) { create :user_con_profile, convention: convention }
+ let(:user_con_profile1) { create(:user_con_profile, convention: convention) }
+ let(:user_con_profile2) { create(:user_con_profile, convention: convention) }
let(:signup1) do
create(
@@ -156,7 +156,7 @@ class EventChangeRegistrationPolicyServiceTest < ActiveSupport::TestCase
)
end
- let(:user_con_profile3) { create :user_con_profile, convention: convention }
+ let(:user_con_profile3) { create(:user_con_profile, convention: convention) }
let(:signup3) do
create(
:signup,
@@ -201,7 +201,7 @@ class EventChangeRegistrationPolicyServiceTest < ActiveSupport::TestCase
end
describe 'with an impossible situation' do
- let(:user_con_profile3) { create :user_con_profile, convention: convention }
+ let(:user_con_profile3) { create(:user_con_profile, convention: convention) }
let(:signup3) do
create(
:signup,
@@ -250,8 +250,8 @@ class EventChangeRegistrationPolicyServiceTest < ActiveSupport::TestCase
)
end
- let(:user_con_profile1) { create :user_con_profile, convention: convention }
- let(:user_con_profile2) { create :user_con_profile, convention: convention }
+ let(:user_con_profile1) { create(:user_con_profile, convention: convention) }
+ let(:user_con_profile2) { create(:user_con_profile, convention: convention) }
let(:signup1) do
create(
diff --git a/test/services/provide_event_ticket_service_test.rb b/test/services/provide_event_ticket_service_test.rb
index 43fdb99bc78..a8c2900188e 100644
--- a/test/services/provide_event_ticket_service_test.rb
+++ b/test/services/provide_event_ticket_service_test.rb
@@ -26,7 +26,7 @@
it 'fails' do
result = service.call
assert result.failure?
- assert_match /already has a ticket/, result.errors.full_messages.join("\n")
+ assert_match(/already has a ticket/, result.errors.full_messages.join("\n"))
end
end
@@ -36,7 +36,7 @@
it 'fails' do
result = service.call
assert result.failure?
- assert_match /cannot provide tickets/, result.errors.full_messages.join("\n")
+ assert_match(/cannot provide tickets/, result.errors.full_messages.join("\n"))
end
end
@@ -46,7 +46,7 @@
it 'fails' do
result = service.call
assert result.failure?
- assert_match /cannot be provided/, result.errors.full_messages.join("\n")
+ assert_match(/cannot be provided/, result.errors.full_messages.join("\n"))
end
end
@@ -61,7 +61,7 @@
it 'fails' do
result = service.call
assert result.failure?
- assert_match /has already provided/, result.errors.full_messages.join("\n")
+ assert_match(/has already provided/, result.errors.full_messages.join("\n"))
end
end
end
diff --git a/test/system/event_page_test.rb b/test/system/event_page_test.rb
new file mode 100644
index 00000000000..a968371ff8e
--- /dev/null
+++ b/test/system/event_page_test.rb
@@ -0,0 +1,54 @@
+require "application_system_test_case"
+
+class EventPageTest < ApplicationSystemTestCase
+ before do
+ Intercode.overridden_virtual_host_domain = convention.domain
+ end
+
+ after do
+ Intercode.overridden_virtual_host_domain = nil
+ end
+
+ let(:root_site) { create(:root_site) }
+ let(:convention) { create(:convention, :with_standard_content) }
+ let(:event_category) do
+ create(:event_category, convention:, event_form: convention.forms.find_by!(title: "Regular event form"))
+ end
+ let(:signup_round) { create(:signup_round, convention:, start: 1.day.ago, maximum_event_signups: "unlimited") }
+ let(:event) { create(:event, convention:, event_category: convention.event_categories.first) }
+ let(:the_run) { create(:run, event:, starts_at: 1.day.from_now) }
+
+ before do
+ root_site
+ event_category
+ signup_round
+ the_run
+ end
+
+ it "renders the page" do
+ visit "/events/#{event.to_param}"
+ assert page.has_content?("Log in to sign up for")
+ assert page.has_content?(event.title)
+ end
+
+ it "lets you sign up" do
+ user_con_profile = create(:user_con_profile, convention:)
+ sign_in user_con_profile.user
+
+ visit "/events/#{event.to_param}"
+ click_button "Sign up now"
+ assert page.has_content?("You are signed up.")
+ end
+
+ it "lets you withdraw" do
+ user_con_profile = create(:user_con_profile, convention:)
+ create(:signup, user_con_profile:, run: the_run)
+ sign_in user_con_profile.user
+
+ visit "/events/#{event.to_param}"
+ click_button "Withdraw"
+ check "Yes, I’m sure I want to withdraw my confirmed signup from #{event.title}."
+ click_button "Confirm"
+ assert page.has_content?("Sign up now")
+ end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 6c5ae2f1748..c2d03979bba 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -31,6 +31,22 @@
Minitest::Reporters.use!(Minitest::Reporters::ProgressReporter.new, ENV, Minitest.backtrace_filter)
end
+require "capybara/cuprite"
+Capybara.javascript_driver = :cuprite
+Capybara.register_driver(:cuprite) do |app|
+ Capybara::Cuprite::Driver.new(
+ app,
+ window_size: [1200, 800],
+ headless: %w[0 false].exclude?(ENV.fetch("HEADLESS", nil)),
+ js_errors: true
+ )
+end
+
+require "capybara/rails"
+require "capybara/minitest"
+
+DatabaseCleaner.strategy = :truncation
+
class ActiveSupport::TestCase
include FactoryBot::Syntax::Methods
include ActionMailer::TestCase::ClearTestDeliveries
@@ -81,7 +97,7 @@ class GraphqlTestExecutionError < StandardError
def initialize(result)
@result = result
@errors = result["errors"]
- super(errors.map { |error| error["message"] }.join(", "))
+ super(errors.pluck("message").join(", "))
end
def backtrace