From b48f7abf72a231bc9014c4eb2c4e7ab1fb7d9dfb Mon Sep 17 00:00:00 2001 From: qzl Date: Tue, 10 Mar 2026 17:45:08 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E5=A2=9E=E5=8A=A0=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E9=94=AE=E7=9B=98=E5=BC=B9=E5=87=BA=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/test/widget_test.dart | 24 +++++++ infra/scripts/app.sh | 124 ++++++++++++++++++++++++++++++------- 2 files changed, 125 insertions(+), 23 deletions(-) diff --git a/apps/test/widget_test.dart b/apps/test/widget_test.dart index 2f6d8e3..ce52c23 100644 --- a/apps/test/widget_test.dart +++ b/apps/test/widget_test.dart @@ -60,4 +60,28 @@ void main() { expect((topSpace - bottomSpace).abs(), lessThanOrEqualTo(2)); }); + + testWidgets('Login screen does not overflow when keyboard is visible', ( + WidgetTester tester, + ) async { + final mockAuthBloc = MockAuthBloc(); + when(() => mockAuthBloc.state).thenReturn(AuthInitial()); + when( + () => mockAuthBloc.stream, + ).thenAnswer((_) => Stream.value(AuthInitial())); + + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData( + size: Size(390, 844), + viewInsets: EdgeInsets.only(bottom: 320), + ), + child: LinksyApp(authBloc: mockAuthBloc), + ), + ); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(find.text('登录'), findsOneWidget); + }); } diff --git a/infra/scripts/app.sh b/infra/scripts/app.sh index 871458f..96a65d5 100755 --- a/infra/scripts/app.sh +++ b/infra/scripts/app.sh @@ -7,14 +7,85 @@ COMPOSE_FILE="$ROOT_DIR/infra/docker/docker-compose.yml" ENV_FILE="$ROOT_DIR/.env" usage() { - echo "Usage: $0 {start|stop}" + echo "Usage: $0 {start|stop|restart}" echo "" echo "Commands:" echo " start Start web + worker processes in tmux" - echo " stop Stop and clean up tmux session" + echo " stop Stop tmux session and clean orphaned processes" + echo " restart Stop then start all app processes" exit 1 } +load_env_if_exists() { + if [ -f "$ENV_FILE" ]; then + set -a + # shellcheck disable=SC1090 + . "$ENV_FILE" + set +a + fi +} + +is_port_in_use() { + local port="$1" + + if command -v lsof >/dev/null 2>&1; then + lsof -iTCP:"$port" -sTCP:LISTEN -t >/dev/null 2>&1 + return $? + fi + + if command -v ss >/dev/null 2>&1; then + ss -ltn "sport = :$port" | awk 'NR > 1 {exit 0} END {exit 1}' + return $? + fi + + return 1 +} + +collect_listening_pids() { + local port="$1" + + if command -v lsof >/dev/null 2>&1; then + lsof -iTCP:"$port" -sTCP:LISTEN -t | sort -u + return + fi + + if command -v ss >/dev/null 2>&1; then + ss -lptn "sport = :$port" | awk -F 'pid=' 'NF > 1 {split($2, tmp, ","); print tmp[1]}' | sort -u + fi +} + +kill_pids_gracefully() { + local label="$1" + shift + local pids=("$@") + local alive=() + + if [ "${#pids[@]}" -eq 0 ]; then + return + fi + + echo "Stopping ${label}: ${pids[*]}" + kill -TERM "${pids[@]}" 2>/dev/null || true + + for _ in {1..10}; do + alive=() + for pid in "${pids[@]}"; do + if kill -0 "$pid" 2>/dev/null; then + alive+=("$pid") + fi + done + + if [ "${#alive[@]}" -eq 0 ]; then + return + fi + + sleep 1 + done + + echo "Force killing ${label}: ${alive[*]}" + kill -KILL "${alive[@]}" 2>/dev/null || true +} + start() { echo "=== App Up ===" echo "This script starts web + worker processes in tmux." @@ -36,10 +107,7 @@ start() { exit 1 fi - set -a - # shellcheck disable=SC1090 - . "$ENV_FILE" - set +a + load_env_if_exists UVICORN_LOG_LEVEL="${SOCIAL_RUNTIME__LOG_LEVEL:-info}" UVICORN_LOG_LEVEL="${UVICORN_LOG_LEVEL,,}" @@ -51,12 +119,10 @@ start() { exit 1 fi - if command -v ss >/dev/null 2>&1; then - if ss -ltn | awk '{print $4}' | grep -qE "[:.]${WEB_PORT}$"; then - echo "Error: web port ${WEB_PORT} is already in use." >&2 - echo "Hint: run '$0 stop' or change SOCIAL_WEB__PORT in .env" >&2 - exit 1 - fi + if is_port_in_use "$WEB_PORT"; then + echo "Error: web port ${WEB_PORT} is already in use." >&2 + echo "Hint: run '$0 stop' or change SOCIAL_WEB__PORT in .env" >&2 + exit 1 fi echo "Starting web + worker processes in tmux session '$SESSION_NAME'..." @@ -88,6 +154,8 @@ ${SOCIAL_WEB__WORKERS:-2} --log-level ${UVICORN_LOG_LEVEL}" stop() { echo "=== App Down ===" + load_env_if_exists + WEB_PORT="${SOCIAL_WEB__PORT:-5775}" if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then echo "Stopping tmux session '$SESSION_NAME'..." @@ -97,24 +165,34 @@ stop() { fi echo "Checking for orphaned processes..." - if pgrep -f "uvicorn.*app:app" > /dev/null 2>&1; then - echo "Killing orphaned uvicorn processes..." - pkill -f "uvicorn.*app:app" - fi - if pgrep -f "gunicorn.*app:app" > /dev/null 2>&1; then - echo "Killing orphaned gunicorn processes..." - pkill -f "gunicorn.*app:app" - fi - if pgrep -f "taskiq.*worker" > /dev/null 2>&1; then - echo "Killing orphaned taskiq processes..." - pkill -f "taskiq.*worker" + + mapfile -t uvicorn_pids < <(pgrep -f "uv run uvicorn app:app" || true) + kill_pids_gracefully "uvicorn" "${uvicorn_pids[@]}" + + mapfile -t taskiq_pids < <(pgrep -f "uv run taskiq worker core.taskiq.app:" || true) + kill_pids_gracefully "taskiq workers" "${taskiq_pids[@]}" + + mapfile -t port_pids < <(collect_listening_pids "$WEB_PORT" || true) + kill_pids_gracefully "port ${WEB_PORT} listeners" "${port_pids[@]}" + + if is_port_in_use "$WEB_PORT"; then + echo "Warning: port ${WEB_PORT} is still in use after cleanup." >&2 + echo "Hint: check process with 'lsof -iTCP:${WEB_PORT} -sTCP:LISTEN'" >&2 + return 1 fi echo "Session stopped and cleaned up." } +restart() { + stop + echo "" + start +} + case "${1:-}" in start) start ;; stop) stop ;; + restart) restart ;; *) usage ;; esac