Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7e758ff
Add preliminary full-stack tests
backspace Feb 5, 2026
70ebba4
Add preliminary CI setup
backspace Feb 5, 2026
996f21a
Change workflow name
backspace Feb 5, 2026
225b19b
Update Android version
backspace Feb 5, 2026
5b043ee
Change job name
backspace Feb 5, 2026
dbbae78
Update Gradle etc
backspace Feb 5, 2026
03c9455
Add Gradle workaround
backspace Feb 5, 2026
e58388e
Change Gradle fix
backspace Feb 5, 2026
bc9c587
Fix build by switching motion sensors package
backspace Feb 7, 2026
f2a0e0f
Add routing to API
backspace Feb 7, 2026
bd986a0
Change Phoenix binding in test environment
backspace Feb 7, 2026
08d59dd
Add fill_in_the_blank integration test
backspace Feb 7, 2026
71c0ce1
Fix fill_in_the_blank test assertions
backspace Feb 7, 2026
7560ab4
Fix fill_in_the_blank test: add required answer_id
backspace Feb 7, 2026
ce8c654
Fix TestController: insert Answer separately
backspace Feb 8, 2026
8025118
Add string_collector integration test and publish test results
backspace Feb 8, 2026
445696b
Add orientation_memory integration test
backspace Feb 8, 2026
e6b5ed7
Fix orientation_memory test: use specification.id filter
backspace Feb 8, 2026
9ea35b3
Use TRUNCATE CASCADE for test database reset
backspace Feb 8, 2026
97dabec
Change tests to exercise UI
backspace Feb 12, 2026
4a47cec
Update iOS Podife
backspace Feb 16, 2026
6a00ebe
Add some test fixes
backspace Feb 16, 2026
59a185c
Disable test timeout for full-stack CI
backspace Feb 16, 2026
3fcf84b
Make game button taps more robust for CI emulator
backspace Feb 16, 2026
d0205d7
Replace pumpAndSettle with fixed pump after ensureVisible
backspace Feb 16, 2026
4130cf2
Fix CI string_collector WebSocket timeout: increase ready wait, disab…
backspace Feb 17, 2026
a592e86
Add WebSocket connection timeout with retry, better test diagnostics
backspace Feb 17, 2026
982481f
Fix FutureBuilder anti-pattern: cache _gameInfoFuture in didChangeDep…
backspace Feb 17, 2026
2b702f0
Add diagnostics for string_collector CI failure
backspace Feb 17, 2026
63a97d8
Fix ready button not found: scroll ListView on small screens
backspace Feb 22, 2026
cf57c3d
Remove diagnostic prints, keep timeout text dump in waitFor
backspace Feb 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci-waydowntown-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ jobs:
steps:
- uses: actions/checkout@v4

- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'

- uses: subosito/flutter-action@v2
with:
channel: "stable"
Expand Down
103 changes: 103 additions & 0 deletions .github/workflows/ci-waydowntown-full-stack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
name: waydowntown full-stack tests

on:
push:
branches: [main]
pull_request:
paths:
- "waydowntown_app/**"
- "registrations/**"
- ".github/workflows/ci-waydowntown-full-stack.yml"

jobs:
waydowntown-full-stack-test:
runs-on: ubuntu-latest
services:
db:
image: postgis/postgis:16-3.4
ports: ["5432:5432"]
env:
POSTGRES_PASSWORD: postgres
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5

steps:
- uses: actions/checkout@v4

# Setup Elixir/Erlang
- name: Setup Erlang/OTP environment
uses: erlef/setup-beam@v1
with:
version-file: .tool-versions
version-type: strict

- name: Retrieve cached Elixir dependencies
uses: actions/cache@v4
id: mix-cache
with:
path: |
registrations/deps
registrations/_build
key: ${{ runner.os }}-mix-${{ hashFiles('registrations/mix.lock') }}

- name: Install Elixir dependencies
working-directory: registrations
run: |
mix local.rebar --force
mix local.hex --force
mix deps.get
mix deps.compile

# Setup and start Phoenix
- name: Setup database
working-directory: registrations
run: MIX_ENV=test mix ecto.reset

- name: Start Phoenix server
working-directory: registrations
run: |
MIX_ENV=test mix phx.server &
# Wait for server to be ready
for i in {1..30}; do
if curl -s http://localhost:4001/powapi/session > /dev/null 2>&1; then
echo "Phoenix server is ready"
break
fi
echo "Waiting for Phoenix server... ($i)"
sleep 1
done

# Setup Java 17 (required for AGP 8.x)
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'

# Setup Flutter
- uses: subosito/flutter-action@v2
with:
channel: "stable"

- name: Create Flutter env file
run: touch .env.local
working-directory: waydowntown_app

- name: Get Flutter dependencies
run: flutter pub get
working-directory: waydowntown_app

# Run integration tests on Android emulator
- name: Run full-stack integration tests
uses: reactivecircus/android-emulator-runner@v2
with:
working-directory: waydowntown_app
api-level: 24
arch: x86_64
profile: Nexus 6
script: flutter test integration_test/token_refresh_test.dart integration_test/string_collector_test.dart integration_test/fill_in_the_blank_test.dart --flavor local --dart-define=API_BASE_URL=http://10.0.2.2:4001 --timeout none --file-reporter json:test-results.json

- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
check_name: "Full-stack integration test results"
files: waydowntown_app/test-results.json
5 changes: 3 additions & 2 deletions registrations/config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ config :registrations, Registrations.Repo,
pool: Ecto.Adapters.SQL.Sandbox

config :registrations, RegistrationsWeb.Endpoint,
http: [port: 4001],
server: true
http: [ip: {0, 0, 0, 0}, port: 4001],
server: true,
check_origin: false
160 changes: 160 additions & 0 deletions registrations/lib/registrations_web/controllers/test_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
defmodule RegistrationsWeb.TestController do
@moduledoc """
Controller for test-only endpoints. Only available in test environment.
Provides database reset functionality for integration testing.
"""
use RegistrationsWeb, :controller

alias Registrations.Repo
alias Registrations.Waydowntown.Answer
alias Registrations.Waydowntown.Region
alias Registrations.Waydowntown.Specification

@test_email "test@example.com"
@test_password "TestPassword1234"

def reset(conn, params) do
# Truncate all waydowntown tables - CASCADE handles foreign key dependencies
Repo.query!("TRUNCATE waydowntown.reveals, waydowntown.submissions, waydowntown.participations, waydowntown.runs, waydowntown.answers, waydowntown.specifications, waydowntown.regions CASCADE")

response =
if params["create_user"] == "true" do
user = create_or_reset_test_user()

base_response = %{
user_id: user.id,
email: @test_email,
password: @test_password
}

# Optionally create game data based on concept parameter
case params["game"] do
"fill_in_the_blank" ->
game_data = create_fill_in_the_blank_game()
Map.merge(base_response, game_data)

"string_collector" ->
game_data = create_string_collector_game()
Map.merge(base_response, game_data)

"orientation_memory" ->
game_data = create_orientation_memory_game()
Map.merge(base_response, game_data)

_ ->
base_response
end
else
%{message: "Database reset complete"}
end

conn
|> put_status(:ok)
|> json(response)
end

defp create_fill_in_the_blank_game do
region = Repo.insert!(%Region{name: "Test Region"})

specification =
Repo.insert!(%Specification{
concept: "fill_in_the_blank",
task_description: "What is the answer to this test?",
region: region,
duration: 300
})

# Insert answer separately (has_many relationship)
answer =
Repo.insert!(%Answer{
label: "The answer is ____",
answer: "correct",
specification_id: specification.id
})

%{
specification_id: specification.id,
answer_id: answer.id,
correct_answer: "correct"
}
end

defp create_string_collector_game do
region = Repo.insert!(%Region{name: "Test Region"})

specification =
Repo.insert!(%Specification{
concept: "string_collector",
task_description: "Find all the hidden words",
start_description: "Look around for words",
region: region,
duration: 300
})

# Insert answers separately (has_many relationship)
answer1 = Repo.insert!(%Answer{answer: "apple", specification_id: specification.id})
answer2 = Repo.insert!(%Answer{answer: "banana", specification_id: specification.id})
answer3 = Repo.insert!(%Answer{answer: "cherry", specification_id: specification.id})

%{
specification_id: specification.id,
correct_answers: ["apple", "banana", "cherry"],
total_answers: 3,
answer_ids: [answer1.id, answer2.id, answer3.id]
}
end

defp create_orientation_memory_game do
region = Repo.insert!(%Region{name: "Test Region"})

specification =
Repo.insert!(%Specification{
concept: "orientation_memory",
task_description: "Remember the sequence of directions",
start_description: "Watch the pattern carefully",
region: region,
duration: 300
})

# Insert ordered answers (order field is required for orientation_memory)
answer1 = Repo.insert!(%Answer{answer: "north", order: 1, specification_id: specification.id})
answer2 = Repo.insert!(%Answer{answer: "east", order: 2, specification_id: specification.id})
answer3 = Repo.insert!(%Answer{answer: "south", order: 3, specification_id: specification.id})

%{
specification_id: specification.id,
ordered_answers: ["north", "east", "south"],
total_answers: 3,
answer_ids: [answer1.id, answer2.id, answer3.id]
}
end

defp create_or_reset_test_user do
case Repo.get_by(RegistrationsWeb.User, email: @test_email) do
nil ->
%RegistrationsWeb.User{}
|> RegistrationsWeb.User.changeset(%{
email: @test_email,
password: @test_password,
password_confirmation: @test_password
})
|> Repo.insert!()
|> Ecto.Changeset.change(%{name: "Test User"})
|> Repo.update!()

existing_user ->
# Delete and recreate to ensure clean state
Repo.delete!(existing_user)

%RegistrationsWeb.User{}
|> RegistrationsWeb.User.changeset(%{
email: @test_email,
password: @test_password,
password_confirmation: @test_password
})
|> Repo.insert!()
|> Ecto.Changeset.change(%{name: "Test User"})
|> Repo.update!()
end
end
end
8 changes: 8 additions & 0 deletions registrations/lib/registrations_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,12 @@ defmodule RegistrationsWeb.Router do
get("/session", SessionController, :show)
post("/me", ApiUserController, :update)
end

if Mix.env() == :test do
scope "/test", RegistrationsWeb do
pipe_through(:pow_api)

post("/reset", TestController, :reset)
end
end
end
8 changes: 6 additions & 2 deletions waydowntown_app/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,12 @@ android {
ndkVersion = flutter.ndkVersion

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = "17"
}

defaultConfig {
Expand Down
34 changes: 34 additions & 0 deletions waydowntown_app/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,40 @@ allprojects {
google()
mavenCentral()
}

// Fix for AGP 8.x: auto-set namespace for library plugins
pluginManager.withPlugin("com.android.library") {
def androidExtension = project.extensions.getByType(com.android.build.gradle.LibraryExtension)

// Set namespace from manifest if not specified
if (androidExtension.namespace == null) {
def manifestFile = project.file("src/main/AndroidManifest.xml")
if (manifestFile.exists()) {
def manifest = new XmlSlurper().parse(manifestFile)
def packageName = manifest.@package.toString()
if (packageName) {
androidExtension.namespace = packageName
}
}
}
}
}

// Force Java 17 and Kotlin JVM target 17 for all subprojects AFTER they're evaluated
gradle.afterProject { project ->
if (project.plugins.hasPlugin("com.android.library")) {
project.android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
jvmTarget = "17"
}
}
}
}

rootProject.buildDir = "../build"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
2 changes: 1 addition & 1 deletion waydowntown_app/android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pluginManagement {

plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "com.android.application" version "8.6.0" apply false
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
}

Expand Down
Loading
Loading