// Copyright (c) 2009 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import #import "base/cocoa_protocols_mac.h" #include "base/scoped_nsobject.h" #import "chrome/browser/cocoa/autocomplete_text_field.h" #import "chrome/browser/cocoa/autocomplete_text_field_cell.h" #import "chrome/browser/cocoa/autocomplete_text_field_editor.h" #import "chrome/browser/cocoa/autocomplete_text_field_unittest_helper.h" #import "chrome/browser/cocoa/cocoa_test_helper.h" #include "testing/gtest/include/gtest/gtest.h" #include "testing/platform_test.h" using ::testing::InSequence; @interface AutocompleteTextFieldTestDelegate : NSObject { BOOL receivedControlTextDidBeginEditing_; BOOL receivedControlTextShouldEndEditing_; } - init; - (BOOL)receivedControlTextDidBeginEditing; - (BOOL)receivedControlTextShouldEndEditing; @end namespace { // Mock up an incrementing event number. NSUInteger eventNumber = 0; // Create an event of the indicated |type| at |point| within |view|. // TODO(shess): Would be nice to have a MockApplication which provided // nifty accessors to create these things and inject them. It could // even provide functions for "Click and drag mouse from point A to // point B". NSEvent* Event(NSView* view, const NSPoint point, const NSEventType type, const NSUInteger clickCount) { NSWindow* window([view window]); const NSPoint locationInWindow([view convertPoint:point toView:nil]); const NSPoint location([window convertBaseToScreen:locationInWindow]); return [NSEvent mouseEventWithType:type location:location modifierFlags:0 timestamp:0 windowNumber:[window windowNumber] context:nil eventNumber:eventNumber++ clickCount:clickCount pressure:0.0]; } NSEvent* Event(NSView* view, const NSPoint point, const NSEventType type) { return Event(view, point, type, 1); } class AutocompleteTextFieldTest : public PlatformTest { public: AutocompleteTextFieldTest() { // Make sure this is wide enough to play games with the cell // decorations. NSRect frame = NSMakeRect(0, 0, 300, 30); field_.reset([[AutocompleteTextField alloc] initWithFrame:frame]); [field_ setStringValue:@"Test test"]; [field_ setObserver:&field_observer_]; [cocoa_helper_.contentView() addSubview:field_.get()]; window_delegate_.reset( [[AutocompleteTextFieldWindowTestDelegate alloc] init]); [cocoa_helper_.window() setDelegate:window_delegate_.get()]; } // The removeFromSuperview call is needed to prevent crashes in // later tests. // TODO(shess): -removeromSuperview should not be necessary. Fix // it. Also in autocomplete_text_field_editor_unittest.mm. ~AutocompleteTextFieldTest() { [cocoa_helper_.window() setDelegate:nil]; [field_ removeFromSuperview]; } NSEvent* KeyDownEventWithFlags(NSUInteger flags) { return [NSEvent keyEventWithType:NSKeyDown location:NSZeroPoint modifierFlags:flags timestamp:0.0 windowNumber:[cocoa_helper_.window() windowNumber] context:nil characters:@"a" charactersIgnoringModifiers:@"a" isARepeat:NO keyCode:'a']; } // Helper to return the field-editor frame being used w/in |field_|. NSRect EditorFrame() { EXPECT_TRUE([field_.get() currentEditor]); EXPECT_EQ([[field_.get() subviews] count], 1U); if ([[field_.get() subviews] count] > 0) { return [[[field_.get() subviews] objectAtIndex:0] frame]; } else { // Return something which won't work so the caller can soldier // on. return NSZeroRect; } } CocoaTestHelper cocoa_helper_; // Inits Cocoa, creates window, etc... scoped_nsobject field_; MockAutocompleteTextFieldObserver field_observer_; scoped_nsobject window_delegate_; }; // Test that we have the right cell class. TEST_F(AutocompleteTextFieldTest, CellClass) { EXPECT_TRUE([[field_ cell] isKindOfClass:[AutocompleteTextFieldCell class]]); } // Test adding/removing from the view hierarchy, mostly to ensure nothing // leaks or crashes. TEST_F(AutocompleteTextFieldTest, AddRemove) { EXPECT_EQ(cocoa_helper_.contentView(), [field_ superview]); [field_.get() removeFromSuperview]; EXPECT_FALSE([field_ superview]); } // Test that we get the same cell from -cell and // -autocompleteTextFieldCell. TEST_F(AutocompleteTextFieldTest, Cell) { AutocompleteTextFieldCell* cell = [field_ autocompleteTextFieldCell]; EXPECT_EQ(cell, [field_ cell]); EXPECT_TRUE(cell != nil); } // Test that becoming first responder sets things up correctly. TEST_F(AutocompleteTextFieldTest, FirstResponder) { EXPECT_EQ(nil, [field_ currentEditor]); EXPECT_EQ([[field_ subviews] count], 0U); cocoa_helper_.makeFirstResponder(field_); EXPECT_FALSE(nil == [field_ currentEditor]); EXPECT_EQ([[field_ subviews] count], 1U); EXPECT_TRUE([[field_ currentEditor] isDescendantOf:field_.get()]); // Check that the window delegate is providing the right editor. Class c = [AutocompleteTextFieldEditor class]; EXPECT_TRUE([[field_ currentEditor] isKindOfClass:c]); } // Test drawing, mostly to ensure nothing leaks or crashes. TEST_F(AutocompleteTextFieldTest, Display) { [field_ display]; // Test focussed drawing. cocoa_helper_.makeFirstResponder(field_); [field_ display]; cocoa_helper_.clearFirstResponder(); // Test display of various cell configurations. AutocompleteTextFieldCell* cell = [field_ autocompleteTextFieldCell]; [cell setSearchHintString:@"Type to search"]; [field_ display]; NSImage* image = [NSImage imageNamed:@"NSApplicationIcon"]; [cell setKeywordHintPrefix:@"prefix" image:image suffix:@"suffix"]; [field_ display]; [cell setKeywordString:@"Search Engine:"]; [field_ display]; [cell clearKeywordAndHint]; [field_ display]; } TEST_F(AutocompleteTextFieldTest, FlagsChanged) { InSequence dummy; // Call mock in exactly the order specified. // Test without Control key down, but some other modifier down. EXPECT_CALL(field_observer_, OnControlKeyChanged(false)); [field_ flagsChanged:KeyDownEventWithFlags(NSShiftKeyMask)]; // Test with Control key down. EXPECT_CALL(field_observer_, OnControlKeyChanged(true)); [field_ flagsChanged:KeyDownEventWithFlags(NSControlKeyMask)]; } // This test is here rather than in the editor's tests because the // field catches -flagsChanged: because it's on the responder chain, // the field editor doesn't implement it. TEST_F(AutocompleteTextFieldTest, FieldEditorFlagsChanged) { cocoa_helper_.makeFirstResponder(field_); NSResponder* firstResponder = [[field_ window] firstResponder]; EXPECT_EQ(firstResponder, [field_ currentEditor]); InSequence dummy; // Call mock in exactly the order specified. // Test without Control key down, but some other modifier down. EXPECT_CALL(field_observer_, OnControlKeyChanged(false)); [firstResponder flagsChanged:KeyDownEventWithFlags(NSShiftKeyMask)]; // Test with Control key down. EXPECT_CALL(field_observer_, OnControlKeyChanged(true)); [firstResponder flagsChanged:KeyDownEventWithFlags(NSControlKeyMask)]; } // Test that the field editor gets the same bounds when focus is // delivered by the standard focusing machinery, or by // -resetFieldEditorFrameIfNeeded. TEST_F(AutocompleteTextFieldTest, ResetFieldEditorBase) { AutocompleteTextFieldCell* cell = [field_ autocompleteTextFieldCell]; EXPECT_FALSE([cell fieldEditorNeedsReset]); // Capture the editor frame resulting from the standard focus // machinery. cocoa_helper_.makeFirstResponder(field_); const NSRect baseEditorFrame(EditorFrame()); // Setting a hint should result in a strictly smaller editor frame. [cell setSearchHintString:@"search hint"]; EXPECT_TRUE([cell fieldEditorNeedsReset]); [field_ resetFieldEditorFrameIfNeeded]; EXPECT_FALSE([cell fieldEditorNeedsReset]); EXPECT_FALSE(NSEqualRects(baseEditorFrame, EditorFrame())); EXPECT_TRUE(NSContainsRect(baseEditorFrame, EditorFrame())); // Clearing hint string and using -resetFieldEditorFrameIfNeeded // should result in the same frame as the standard focus machinery. [cell clearKeywordAndHint]; EXPECT_TRUE([cell fieldEditorNeedsReset]); [field_ resetFieldEditorFrameIfNeeded]; EXPECT_FALSE([cell fieldEditorNeedsReset]); EXPECT_TRUE(NSEqualRects(baseEditorFrame, EditorFrame())); } // Test that the field editor gets the same bounds when focus is // delivered by the standard focusing machinery, or by // -resetFieldEditorFrameIfNeeded. TEST_F(AutocompleteTextFieldTest, ResetFieldEditorSearchHint) { AutocompleteTextFieldCell* cell = [field_ autocompleteTextFieldCell]; EXPECT_FALSE([cell fieldEditorNeedsReset]); const NSString* kHintString(@"Type to search"); // Capture the editor frame resulting from the standard focus // machinery. [cell setSearchHintString:kHintString]; EXPECT_TRUE([cell fieldEditorNeedsReset]); [cell setFieldEditorNeedsReset:NO]; EXPECT_FALSE([cell fieldEditorNeedsReset]); cocoa_helper_.makeFirstResponder(field_); const NSRect baseEditorFrame(EditorFrame()); // Clearing the hint should result in a strictly larger editor // frame. [cell clearKeywordAndHint]; EXPECT_TRUE([cell fieldEditorNeedsReset]); [field_ resetFieldEditorFrameIfNeeded]; EXPECT_FALSE([cell fieldEditorNeedsReset]); EXPECT_FALSE(NSEqualRects(baseEditorFrame, EditorFrame())); EXPECT_TRUE(NSContainsRect(EditorFrame(), baseEditorFrame)); // Setting the same hint string and using // -resetFieldEditorFrameIfNeeded should result in the same frame as // the standard focus machinery. [cell setSearchHintString:kHintString]; EXPECT_TRUE([cell fieldEditorNeedsReset]); [field_ resetFieldEditorFrameIfNeeded]; EXPECT_FALSE([cell fieldEditorNeedsReset]); EXPECT_TRUE(NSEqualRects(baseEditorFrame, EditorFrame())); } // Test that the field editor gets the same bounds when focus is // delivered by the standard focusing machinery, or by // -resetFieldEditorFrameIfNeeded. TEST_F(AutocompleteTextFieldTest, ResetFieldEditorKeywordHint) { AutocompleteTextFieldCell* cell = [field_ autocompleteTextFieldCell]; EXPECT_FALSE([cell fieldEditorNeedsReset]); const NSString* kHintString(@"Search Engine:"); // Capture the editor frame resulting from the standard focus // machinery. [cell setKeywordString:kHintString]; EXPECT_TRUE([cell fieldEditorNeedsReset]); [cell setFieldEditorNeedsReset:NO]; EXPECT_FALSE([cell fieldEditorNeedsReset]); cocoa_helper_.makeFirstResponder(field_); const NSRect baseEditorFrame(EditorFrame()); // Clearing the hint should result in a strictly larger editor // frame. [cell clearKeywordAndHint]; EXPECT_TRUE([cell fieldEditorNeedsReset]); [field_ resetFieldEditorFrameIfNeeded]; EXPECT_FALSE([cell fieldEditorNeedsReset]); EXPECT_FALSE(NSEqualRects(baseEditorFrame, EditorFrame())); EXPECT_TRUE(NSContainsRect(EditorFrame(), baseEditorFrame)); // Setting the same hint string and using // -resetFieldEditorFrameIfNeeded should result in the same frame as // the standard focus machinery. [cell setKeywordString:kHintString]; EXPECT_TRUE([cell fieldEditorNeedsReset]); [field_ resetFieldEditorFrameIfNeeded]; EXPECT_FALSE([cell fieldEditorNeedsReset]); EXPECT_TRUE(NSEqualRects(baseEditorFrame, EditorFrame())); } // Test that resetting the field editor bounds does not cause untoward // messages to the field's delegate. TEST_F(AutocompleteTextFieldTest, ResetFieldEditorBlocksEndEditing) { cocoa_helper_.makeFirstResponder(field_); // First, test that -makeFirstResponder: sends // -controlTextDidBeginEditing: and -control:textShouldEndEditing at // the expected times. { scoped_nsobject delegate( [[AutocompleteTextFieldTestDelegate alloc] init]); EXPECT_FALSE([delegate receivedControlTextDidBeginEditing]); EXPECT_FALSE([delegate receivedControlTextShouldEndEditing]); [field_ setDelegate:delegate]; [[field_ window] makeFirstResponder:field_]; NSTextView* editor = static_cast([field_ currentEditor]); EXPECT_TRUE(nil != editor); EXPECT_FALSE([delegate receivedControlTextDidBeginEditing]); EXPECT_FALSE([delegate receivedControlTextShouldEndEditing]); // This should start the begin/end editing state. [editor shouldChangeTextInRange:NSMakeRange(0, 0) replacementString:@""]; EXPECT_TRUE([delegate receivedControlTextDidBeginEditing]); EXPECT_FALSE([delegate receivedControlTextShouldEndEditing]); // This should send the end-editing message. [[field_ window] makeFirstResponder:field_]; EXPECT_TRUE([delegate receivedControlTextShouldEndEditing]); [field_ setDelegate:nil]; } // Then test that -resetFieldEditorFrameIfNeeded manages without // sending that message. { scoped_nsobject delegate( [[AutocompleteTextFieldTestDelegate alloc] init]); [field_ setDelegate:delegate]; EXPECT_FALSE([delegate receivedControlTextDidBeginEditing]); EXPECT_FALSE([delegate receivedControlTextShouldEndEditing]); AutocompleteTextFieldCell* cell = [field_ autocompleteTextFieldCell]; EXPECT_FALSE([cell fieldEditorNeedsReset]); [cell setSearchHintString:@"Type to search"]; EXPECT_TRUE([cell fieldEditorNeedsReset]); EXPECT_FALSE([delegate receivedControlTextShouldEndEditing]); [field_ resetFieldEditorFrameIfNeeded]; EXPECT_FALSE([delegate receivedControlTextShouldEndEditing]); EXPECT_FALSE([delegate receivedControlTextDidBeginEditing]); [field_ setDelegate:nil]; } } // Clicking in the search hint should put the caret in the rightmost // position. TEST_F(AutocompleteTextFieldTest, ClickSearchHintPutsCaretRightmost) { // Set the decoration before becoming responder. EXPECT_FALSE([field_ currentEditor]); AutocompleteTextFieldCell* cell = [field_ autocompleteTextFieldCell]; [cell setSearchHintString:@"Type to search"]; // Can't rely on the window machinery to make us first responder, // here. cocoa_helper_.makeFirstResponder(field_); EXPECT_TRUE([field_ currentEditor]); const NSPoint point(NSMakePoint(300.0 - 20.0, 5.0)); NSEvent* downEvent(Event(field_, point, NSLeftMouseDown)); NSEvent* upEvent(Event(field_, point, NSLeftMouseUp)); [NSApp postEvent:upEvent atStart:YES]; [field_ mouseDown:downEvent]; const NSRange selectedRange([[field_ currentEditor] selectedRange]); EXPECT_EQ(selectedRange.location, [[field_ stringValue] length]); EXPECT_EQ(selectedRange.length, 0U); } // Clicking in the keyword-search should put the caret in the // leftmost position. TEST_F(AutocompleteTextFieldTest, ClickKeywordPutsCaretLeftmost) { // Set the decoration before becoming responder. EXPECT_FALSE([field_ currentEditor]); AutocompleteTextFieldCell* cell = [field_ autocompleteTextFieldCell]; [cell setKeywordString:@"Search Engine:"]; // Can't rely on the window machinery to make us first responder, // here. cocoa_helper_.makeFirstResponder(field_); EXPECT_TRUE([field_ currentEditor]); const NSPoint point(NSMakePoint(20.0, 5.0)); NSEvent* downEvent(Event(field_, point, NSLeftMouseDown)); NSEvent* upEvent(Event(field_, point, NSLeftMouseUp)); [NSApp postEvent:upEvent atStart:YES]; [field_ mouseDown:downEvent]; const NSRange selectedRange([[field_ currentEditor] selectedRange]); EXPECT_EQ(selectedRange.location, 0U); EXPECT_EQ(selectedRange.length, 0U); } // Clicks not in the text area or the cell's decorations fall through // to the editor. TEST_F(AutocompleteTextFieldTest, ClickBorderSelectsAll) { // Can't rely on the window machinery to make us first responder, // here. cocoa_helper_.makeFirstResponder(field_); EXPECT_TRUE([field_ currentEditor]); const NSPoint point(NSMakePoint(20.0, 1.0)); NSEvent* downEvent(Event(field_, point, NSLeftMouseDown)); NSEvent* upEvent(Event(field_, point, NSLeftMouseUp)); [NSApp postEvent:upEvent atStart:YES]; [field_ mouseDown:downEvent]; // Clicking in the narrow border area around a Cocoa NSTextField // does a select-all. Regardless of whether this is a good call, it // works as a test that things get passed down to the editor. const NSRange selectedRange([[field_ currentEditor] selectedRange]); EXPECT_EQ(selectedRange.location, 0U); EXPECT_EQ(selectedRange.length, [[field_ stringValue] length]); } // Single-click with no drag should setup a field editor and // select all. TEST_F(AutocompleteTextFieldTest, ClickSelectsAll) { EXPECT_FALSE([field_ currentEditor]); const NSPoint point(NSMakePoint(20.0, 5.0)); NSEvent* downEvent(Event(field_, point, NSLeftMouseDown)); NSEvent* upEvent(Event(field_, point, NSLeftMouseUp)); [NSApp postEvent:upEvent atStart:YES]; [field_ mouseDown:downEvent]; EXPECT_TRUE([field_ currentEditor]); const NSRange selectedRange([[field_ currentEditor] selectedRange]); EXPECT_EQ(selectedRange.location, 0U); EXPECT_EQ(selectedRange.length, [[field_ stringValue] length]); } // Click-drag selects text, not select all. TEST_F(AutocompleteTextFieldTest, ClickDragSelectsText) { EXPECT_FALSE([field_ currentEditor]); NSEvent* downEvent(Event(field_, NSMakePoint(20.0, 5.0), NSLeftMouseDown)); NSEvent* upEvent(Event(field_, NSMakePoint(0.0, 5.0), NSLeftMouseUp)); [NSApp postEvent:upEvent atStart:YES]; [field_ mouseDown:downEvent]; EXPECT_TRUE([field_ currentEditor]); // Expect this to have selected a prefix of the content. Mostly // just don't want the select-all behavior. const NSRange selectedRange([[field_ currentEditor] selectedRange]); EXPECT_EQ(selectedRange.location, 0U); EXPECT_LT(selectedRange.length, [[field_ stringValue] length]); } // TODO(shess): Test that click/pause/click allows cursor placement. // In this case the first click goes to the field, but the second // click goes to the field editor, so the current testing pattern // can't work. What really needs to happen is to push through the // NSWindow event machinery so that we can say "two independent clicks // at the same location have the right effect". Once that is done, it // might make sense to revise the other tests to use the same // machinery. // Double-click selects word, not select all. TEST_F(AutocompleteTextFieldTest, DoubleClickSelectsWord) { EXPECT_FALSE([field_ currentEditor]); const NSPoint point(NSMakePoint(20.0, 5.0)); NSEvent* downEvent(Event(field_, point, NSLeftMouseDown, 1)); NSEvent* upEvent(Event(field_, point, NSLeftMouseUp, 1)); NSEvent* downEvent2(Event(field_, point, NSLeftMouseDown, 2)); NSEvent* upEvent2(Event(field_, point, NSLeftMouseUp, 2)); [NSApp postEvent:upEvent atStart:YES]; [field_ mouseDown:downEvent]; [NSApp postEvent:upEvent2 atStart:YES]; [field_ mouseDown:downEvent2]; EXPECT_TRUE([field_ currentEditor]); // Selected the first word. const NSRange selectedRange([[field_ currentEditor] selectedRange]); const NSRange spaceRange([[field_ stringValue] rangeOfString:@" "]); EXPECT_GT(spaceRange.location, 0U); EXPECT_LT(spaceRange.length, [[field_ stringValue] length]); EXPECT_EQ(selectedRange.location, 0U); EXPECT_EQ(selectedRange.length, spaceRange.location); } TEST_F(AutocompleteTextFieldTest, TripleClickSelectsAll) { EXPECT_FALSE([field_ currentEditor]); const NSPoint point(NSMakePoint(20.0, 5.0)); NSEvent* downEvent(Event(field_, point, NSLeftMouseDown, 1)); NSEvent* upEvent(Event(field_, point, NSLeftMouseUp, 1)); NSEvent* downEvent2(Event(field_, point, NSLeftMouseDown, 2)); NSEvent* upEvent2(Event(field_, point, NSLeftMouseUp, 2)); NSEvent* downEvent3(Event(field_, point, NSLeftMouseDown, 3)); NSEvent* upEvent3(Event(field_, point, NSLeftMouseUp, 3)); [NSApp postEvent:upEvent atStart:YES]; [field_ mouseDown:downEvent]; [NSApp postEvent:upEvent2 atStart:YES]; [field_ mouseDown:downEvent2]; [NSApp postEvent:upEvent3 atStart:YES]; [field_ mouseDown:downEvent3]; EXPECT_TRUE([field_ currentEditor]); // Selected the first word. const NSRange selectedRange([[field_ currentEditor] selectedRange]); EXPECT_EQ(selectedRange.location, 0U); EXPECT_EQ(selectedRange.length, [[field_ stringValue] length]); } } // namespace @implementation AutocompleteTextFieldTestDelegate - init { self = [super init]; if (self) { receivedControlTextDidBeginEditing_ = NO; receivedControlTextShouldEndEditing_ = NO; } return self; } - (BOOL)receivedControlTextDidBeginEditing { return receivedControlTextDidBeginEditing_; } - (BOOL)receivedControlTextShouldEndEditing { return receivedControlTextShouldEndEditing_; } - (void)controlTextDidBeginEditing:(NSNotification*)aNotification { receivedControlTextDidBeginEditing_ = YES; } - (BOOL)control:(NSControl*)control textShouldEndEditing:(NSText*)fieldEditor { receivedControlTextShouldEndEditing_ = YES; return YES; } @end