|
4 | 4 | "errors" |
5 | 5 | "strings" |
6 | 6 | "testing" |
| 7 | + "time" |
7 | 8 |
|
8 | 9 | "tungo/domain/mode" |
9 | 10 | clientConfiguration "tungo/infrastructure/PAL/configuration/client" |
@@ -3032,3 +3033,215 @@ func TestUpdateClientSelectScreen_SelectorError_Exits(t *testing.T) { |
3032 | 3033 | t.Fatal("expected quit cmd") |
3033 | 3034 | } |
3034 | 3035 | } |
| 3036 | + |
| 3037 | +// --- Non-key message forwarding to active input (fixes Ctrl+V paste on Windows) --- |
| 3038 | + |
| 3039 | +func TestUpdate_NonKeyMsg_ForwardedToInput_AddName(t *testing.T) { |
| 3040 | + m := newTestSessionModel(t) |
| 3041 | + m.screen = configuratorScreenClientAddName |
| 3042 | + |
| 3043 | + // Any non-key, non-window-size message should be forwarded to the textinput, |
| 3044 | + // not silently dropped. This is required for clipboard paste results and cursor blinks. |
| 3045 | + type customMsg struct{} |
| 3046 | + result, _ := m.Update(customMsg{}) |
| 3047 | + s := result.(configuratorSessionModel) |
| 3048 | + if s.screen != configuratorScreenClientAddName { |
| 3049 | + t.Fatalf("expected to stay on add name screen, got %v", s.screen) |
| 3050 | + } |
| 3051 | +} |
| 3052 | + |
| 3053 | +func TestUpdate_NonKeyMsg_ForwardedToInput_AddJSON(t *testing.T) { |
| 3054 | + m := newTestSessionModel(t) |
| 3055 | + m.screen = configuratorScreenClientAddJSON |
| 3056 | + |
| 3057 | + // Any non-key, non-window-size message should be forwarded to the textarea, |
| 3058 | + // not silently dropped. This is required for clipboard paste results and cursor blinks. |
| 3059 | + type customMsg struct{} |
| 3060 | + result, _ := m.Update(customMsg{}) |
| 3061 | + s := result.(configuratorSessionModel) |
| 3062 | + if s.screen != configuratorScreenClientAddJSON { |
| 3063 | + t.Fatalf("expected to stay on add JSON screen, got %v", s.screen) |
| 3064 | + } |
| 3065 | +} |
| 3066 | + |
| 3067 | +func TestUpdate_JSONScreen_EnterDebouncedDuringPaste(t *testing.T) { |
| 3068 | + m := newTestSessionModel(t) |
| 3069 | + m.screen = configuratorScreenClientAddJSON |
| 3070 | + // Simulate recent non-Enter input (as if paste just happened). |
| 3071 | + m.lastInputAt = time.Now() |
| 3072 | + |
| 3073 | + // Enter within debounce window should be forwarded to textarea as newline, |
| 3074 | + // not treated as submit. |
| 3075 | + result, _ := m.updateClientAddJSONScreen(keyNamed(tea.KeyEnter)) |
| 3076 | + s := result.(configuratorSessionModel) |
| 3077 | + if s.screen != configuratorScreenClientAddJSON { |
| 3078 | + t.Fatal("expected Enter to be debounced during paste") |
| 3079 | + } |
| 3080 | + // lastInputAt should be refreshed so the debounce window extends. |
| 3081 | + if s.lastInputAt.IsZero() { |
| 3082 | + t.Fatal("expected lastInputAt to be refreshed during debounce") |
| 3083 | + } |
| 3084 | +} |
| 3085 | + |
| 3086 | +func TestUpdate_JSONScreen_EnterAcceptedAfterDebounce(t *testing.T) { |
| 3087 | + m := newTestSessionModel(t) |
| 3088 | + m.screen = configuratorScreenClientAddJSON |
| 3089 | + m.addJSONInput.SetValue("not valid json") |
| 3090 | + // No recent input — lastInputAt is zero, Enter should be accepted. |
| 3091 | + |
| 3092 | + result, _ := m.updateClientAddJSONScreen(keyNamed(tea.KeyEnter)) |
| 3093 | + s := result.(configuratorSessionModel) |
| 3094 | + if s.screen != configuratorScreenClientInvalid { |
| 3095 | + t.Fatalf("expected Enter to be accepted (goes to invalid screen for bad JSON), got %v", s.screen) |
| 3096 | + } |
| 3097 | +} |
| 3098 | + |
| 3099 | +func TestUpdate_JSONScreen_NonEnterKeySetsLastInputAt(t *testing.T) { |
| 3100 | + m := newTestSessionModel(t) |
| 3101 | + m.screen = configuratorScreenClientAddJSON |
| 3102 | + |
| 3103 | + if !m.lastInputAt.IsZero() { |
| 3104 | + t.Fatal("expected lastInputAt to be zero initially") |
| 3105 | + } |
| 3106 | + result, _ := m.updateClientAddJSONScreen(keyRunes('x')) |
| 3107 | + s := result.(configuratorSessionModel) |
| 3108 | + if s.lastInputAt.IsZero() { |
| 3109 | + t.Fatal("expected lastInputAt to be set after key input") |
| 3110 | + } |
| 3111 | +} |
| 3112 | + |
| 3113 | +func TestUpdate_PasteSettledMsg_FormatsJSON(t *testing.T) { |
| 3114 | + m := newTestSessionModel(t) |
| 3115 | + m.screen = configuratorScreenClientAddJSON |
| 3116 | + m.pasteSeq = 5 |
| 3117 | + m.addJSONInput.SetValue(`{"a":1,"b":2}`) |
| 3118 | + |
| 3119 | + result, _ := m.Update(pasteSettledMsg{seq: 5}) |
| 3120 | + s := result.(configuratorSessionModel) |
| 3121 | + got := s.addJSONInput.Value() |
| 3122 | + if !strings.Contains(got, "\n") { |
| 3123 | + t.Fatalf("expected formatted JSON with newlines, got %q", got) |
| 3124 | + } |
| 3125 | +} |
| 3126 | + |
| 3127 | +func TestUpdate_PasteSettledMsg_StaleSeqIgnored(t *testing.T) { |
| 3128 | + m := newTestSessionModel(t) |
| 3129 | + m.screen = configuratorScreenClientAddJSON |
| 3130 | + m.pasteSeq = 5 |
| 3131 | + m.addJSONInput.SetValue(`{"a":1}`) |
| 3132 | + |
| 3133 | + result, _ := m.Update(pasteSettledMsg{seq: 3}) // stale |
| 3134 | + s := result.(configuratorSessionModel) |
| 3135 | + got := s.addJSONInput.Value() |
| 3136 | + if strings.Contains(got, "\n") { |
| 3137 | + t.Fatalf("stale seq should not reformat, got %q", got) |
| 3138 | + } |
| 3139 | +} |
| 3140 | + |
| 3141 | +func TestTryFormatJSON_EmptyInput(t *testing.T) { |
| 3142 | + m := newTestSessionModel(t) |
| 3143 | + m.screen = configuratorScreenClientAddJSON |
| 3144 | + m.pasteSeq = 1 |
| 3145 | + m.addJSONInput.SetValue("") |
| 3146 | + |
| 3147 | + // Should not panic or change anything. |
| 3148 | + result, _ := m.Update(pasteSettledMsg{seq: 1}) |
| 3149 | + s := result.(configuratorSessionModel) |
| 3150 | + if s.addJSONInput.Value() != "" { |
| 3151 | + t.Fatalf("expected empty value unchanged, got %q", s.addJSONInput.Value()) |
| 3152 | + } |
| 3153 | +} |
| 3154 | + |
| 3155 | +func TestTryFormatJSON_InvalidJSON(t *testing.T) { |
| 3156 | + m := newTestSessionModel(t) |
| 3157 | + m.screen = configuratorScreenClientAddJSON |
| 3158 | + m.pasteSeq = 1 |
| 3159 | + m.addJSONInput.SetValue("not json at all") |
| 3160 | + |
| 3161 | + result, _ := m.Update(pasteSettledMsg{seq: 1}) |
| 3162 | + s := result.(configuratorSessionModel) |
| 3163 | + if s.addJSONInput.Value() != "not json at all" { |
| 3164 | + t.Fatalf("expected invalid JSON unchanged, got %q", s.addJSONInput.Value()) |
| 3165 | + } |
| 3166 | +} |
| 3167 | + |
| 3168 | +func TestTryFormatJSON_AlreadyFormatted(t *testing.T) { |
| 3169 | + m := newTestSessionModel(t) |
| 3170 | + m.screen = configuratorScreenClientAddJSON |
| 3171 | + m.pasteSeq = 1 |
| 3172 | + formatted := "{\n \"a\": 1\n}" |
| 3173 | + m.addJSONInput.SetValue(formatted) |
| 3174 | + |
| 3175 | + result, _ := m.Update(pasteSettledMsg{seq: 1}) |
| 3176 | + s := result.(configuratorSessionModel) |
| 3177 | + if s.addJSONInput.Value() != formatted { |
| 3178 | + t.Fatalf("expected already-formatted JSON unchanged, got %q", s.addJSONInput.Value()) |
| 3179 | + } |
| 3180 | +} |
| 3181 | + |
| 3182 | +func TestView_ClientAddJSONScreen_MultilineContent(t *testing.T) { |
| 3183 | + m := newTestSessionModel(t) |
| 3184 | + m.screen = configuratorScreenClientAddJSON |
| 3185 | + m.width = 80 |
| 3186 | + m.height = 30 |
| 3187 | + m.addJSONInput.SetValue("{\n \"key\": \"value\"\n}") |
| 3188 | + |
| 3189 | + view := m.View() |
| 3190 | + if !strings.Contains(view, "Lines: 3") { |
| 3191 | + t.Fatalf("expected 'Lines: 3' in view for multiline content, got: %s", view) |
| 3192 | + } |
| 3193 | +} |
| 3194 | + |
| 3195 | +func TestUpdateClientSelectScreen_EmptyMenuOptions(t *testing.T) { |
| 3196 | + m := newTestSessionModel(t) |
| 3197 | + m.screen = configuratorScreenClientSelect |
| 3198 | + m.clientMenuOptions = nil |
| 3199 | + |
| 3200 | + result, _ := m.updateClientSelectScreen(keyNamed(tea.KeyEnter)) |
| 3201 | + s := result.(configuratorSessionModel) |
| 3202 | + if s.screen != configuratorScreenClientSelect { |
| 3203 | + t.Fatalf("expected to stay on client select with empty options, got %v", s.screen) |
| 3204 | + } |
| 3205 | +} |
| 3206 | + |
| 3207 | +func TestUpdateServerSelectScreen_DefaultFallthrough(t *testing.T) { |
| 3208 | + m := newTestSessionModel(t) |
| 3209 | + m.screen = configuratorScreenServerSelect |
| 3210 | + // Set options to something that doesn't match any known case. |
| 3211 | + m.serverMenuOptions = []string{"unknown option"} |
| 3212 | + m.cursor = 0 |
| 3213 | + |
| 3214 | + result, _ := m.updateServerSelectScreen(keyNamed(tea.KeyEnter)) |
| 3215 | + s := result.(configuratorSessionModel) |
| 3216 | + // Should fall through to default return m, nil. |
| 3217 | + if s.screen != configuratorScreenServerSelect { |
| 3218 | + t.Fatalf("expected to stay on server select for unknown option, got %v", s.screen) |
| 3219 | + } |
| 3220 | +} |
| 3221 | + |
| 3222 | +func TestUpdateServerManageScreen_EmptyPeersOnEnter(t *testing.T) { |
| 3223 | + m := newTestSessionModel(t) |
| 3224 | + m.screen = configuratorScreenServerManage |
| 3225 | + m.serverManagePeers = nil |
| 3226 | + |
| 3227 | + result, _ := m.updateServerManageScreen(keyNamed(tea.KeyEnter)) |
| 3228 | + s := result.(configuratorSessionModel) |
| 3229 | + if s.screen != configuratorScreenServerManage { |
| 3230 | + t.Fatalf("expected to stay on manage screen with empty peers, got %v", s.screen) |
| 3231 | + } |
| 3232 | +} |
| 3233 | + |
| 3234 | +func TestUpdate_NonKeyMsg_DroppedOnOtherScreens(t *testing.T) { |
| 3235 | + m := newTestSessionModel(t) |
| 3236 | + m.screen = configuratorScreenMode |
| 3237 | + |
| 3238 | + type customMsg struct{} |
| 3239 | + result, cmd := m.Update(customMsg{}) |
| 3240 | + s := result.(configuratorSessionModel) |
| 3241 | + if s.screen != configuratorScreenMode { |
| 3242 | + t.Fatalf("expected to stay on mode screen, got %v", s.screen) |
| 3243 | + } |
| 3244 | + if cmd != nil { |
| 3245 | + t.Fatal("expected nil cmd for dropped message") |
| 3246 | + } |
| 3247 | +} |
0 commit comments