|
| 1 | +package main |
| 2 | + |
| 3 | +import "testing" |
| 4 | + |
| 5 | +// The keyboard-only accessibility exemption and the key-hold check that keeps a |
| 6 | +// keyboard-driven agent from claiming it. See keyboardOnlyUser in scoring.go. |
| 7 | + |
| 8 | +func keyboardSignals(keyEvents float64, extra map[string]interface{}) map[string]interface{} { |
| 9 | + b := map[string]interface{}{"totalPoints": 0.0, "trajectoryLength": 0.0, "keyEvents": keyEvents, "touchEvents": 0.0} |
| 10 | + for k, v := range extra { |
| 11 | + b[k] = v |
| 12 | + } |
| 13 | + return map[string]interface{}{ |
| 14 | + "behavioral": b, |
| 15 | + "environmental": map[string]interface{}{"automationFlags": map[string]interface{}{}}, |
| 16 | + } |
| 17 | +} |
| 18 | + |
| 19 | +func TestKeyboardExemptionStandsWithoutHoldData(t *testing.T) { |
| 20 | + // An older widget, or a visitor who tabbed to the checkbox and pressed |
| 21 | + // Space without typing into a field, reports counts and nothing else. |
| 22 | + sig := keyboardSignals(8, nil) |
| 23 | + if !keyboardOnlyUser(sig, getMap(sig, "behavioral")) { |
| 24 | + t.Error("counts alone must keep the exemption") |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +func TestKeyboardExemptionStandsForFingers(t *testing.T) { |
| 29 | + sig := keyboardSignals(12, map[string]interface{}{"keyHoldSamples": 6.0, "keyHoldAvg": 85.0}) |
| 30 | + if !keyboardOnlyUser(sig, getMap(sig, "behavioral")) { |
| 31 | + t.Error("85ms holds are a hand on a key") |
| 32 | + } |
| 33 | +} |
| 34 | + |
| 35 | +func TestKeyboardExemptionDeniedForMechanicalHolds(t *testing.T) { |
| 36 | + sig := keyboardSignals(12, map[string]interface{}{"keyHoldSamples": 6.0, "keyHoldAvg": 4.0}) |
| 37 | + if keyboardOnlyUser(sig, getMap(sig, "behavioral")) { |
| 38 | + t.Error("4ms holds are an automation protocol, not a keyboard user") |
| 39 | + } |
| 40 | +} |
| 41 | + |
| 42 | +func TestKeyboardExemptionPoolsFormFieldDwell(t *testing.T) { |
| 43 | + // The form analyser's per-field dwell times count too, so a widget that |
| 44 | + // typed into a field is judged even without the session-level summary. |
| 45 | + mechanical := keyboardSignals(9, nil) |
| 46 | + mechanical["formAnalysis"] = map[string]interface{}{"textareaKeyboard": map[string]interface{}{ |
| 47 | + "message": map[string]interface{}{"dwellTimes": []interface{}{2.0, 3.0, 1.0, 2.0}}, |
| 48 | + }} |
| 49 | + if keyboardOnlyUser(mechanical, getMap(mechanical, "behavioral")) { |
| 50 | + t.Error("mechanical field dwell must deny the exemption") |
| 51 | + } |
| 52 | + human := keyboardSignals(9, nil) |
| 53 | + human["formAnalysis"] = map[string]interface{}{"textareaKeyboard": map[string]interface{}{ |
| 54 | + "message": map[string]interface{}{"dwellTimes": []interface{}{60.0, 75.0, 90.0}}, |
| 55 | + }} |
| 56 | + if !keyboardOnlyUser(human, getMap(human, "behavioral")) { |
| 57 | + t.Error("human field dwell must keep the exemption") |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +func TestKeyboardExemptionNeedsEnoughHoldsToJudge(t *testing.T) { |
| 62 | + sig := keyboardSignals(4, map[string]interface{}{"keyHoldSamples": 2.0, "keyHoldAvg": 3.0}) |
| 63 | + if !keyboardOnlyUser(sig, getMap(sig, "behavioral")) { |
| 64 | + t.Error("two holds are too few to take an exemption away on") |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +func TestKeyboardExemptionRequiresKeysAndNoPointer(t *testing.T) { |
| 69 | + one := keyboardSignals(1, nil) |
| 70 | + if keyboardOnlyUser(one, getMap(one, "behavioral")) { |
| 71 | + t.Error("one key event is not keyboard use") |
| 72 | + } |
| 73 | + moved := keyboardSignals(8, map[string]interface{}{"totalPoints": 3.0}) |
| 74 | + if keyboardOnlyUser(moved, getMap(moved, "behavioral")) { |
| 75 | + t.Error("a visitor who moved the pointer is not keyboard-only") |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +// End to end: a keyboard-driven agent is scored as pointerless — both movement |
| 80 | +// views fire and corroborate — while a keyboard-only person is not. |
| 81 | +func TestKeyboardAgentIsScoredAsPointerless(t *testing.T) { |
| 82 | + e := NewScoringEngine("test-secret") |
| 83 | + agent := keyboardSignals(14, map[string]interface{}{"keyHoldSamples": 14.0, "keyHoldAvg": 2.0}) |
| 84 | + dets := append(e.detectVisionAI(agent), e.detectBehavioral(agent)...) |
| 85 | + if !hasReasonContaining(dets, "Zero mouse, touch, or keyboard events") { |
| 86 | + t.Errorf("agent should lose the exemption and score as pointerless, got %+v", dets) |
| 87 | + } |
| 88 | + if !hasReasonContaining(dets, "No mouse movement detected before click") { |
| 89 | + t.Errorf("agent should trip the vision_ai movement check, got %+v", dets) |
| 90 | + } |
| 91 | + if got := applyCorroborationFloor(0.1, dets); got < corroborationFloor { |
| 92 | + t.Errorf("two movement views should corroborate to %v, got %v", corroborationFloor, got) |
| 93 | + } |
| 94 | + |
| 95 | + person := keyboardSignals(14, map[string]interface{}{"keyHoldSamples": 14.0, "keyHoldAvg": 80.0}) |
| 96 | + dets = append(e.detectVisionAI(person), e.detectBehavioral(person)...) |
| 97 | + if hasReasonContaining(dets, "Zero mouse") || hasReasonContaining(dets, "No mouse movement") { |
| 98 | + t.Errorf("a keyboard-only person must keep the exemption, got %+v", dets) |
| 99 | + } |
| 100 | +} |
0 commit comments