blob: 75b8d097d812cf2333b06d00bfd5f536adcbfd52 [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.tests.gui.framework.fixture;
import com.android.resources.ResourceFolderType;
import com.android.tools.idea.editors.strings.StringResourceEditor;
import com.android.tools.idea.editors.strings.StringsVirtualFile;
import com.android.tools.idea.editors.theme.ThemeEditorComponent;
import com.android.tools.idea.rendering.ResourceHelper;
import com.android.tools.idea.tests.gui.framework.GuiTests;
import com.android.tools.idea.tests.gui.framework.fixture.layout.LayoutEditorFixture;
import com.android.tools.idea.tests.gui.framework.fixture.layout.LayoutPreviewFixture;
import com.android.tools.idea.tests.gui.framework.fixture.theme.ThemeEditorFixture;
import com.google.common.collect.Lists;
import com.intellij.android.designer.AndroidDesignerEditor;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.KeyboardShortcut;
import com.intellij.openapi.actionSystem.Shortcut;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.keymap.Keymap;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.components.JBList;
import org.fest.swing.core.GenericTypeMatcher;
import org.fest.swing.core.Robot;
import org.fest.swing.driver.ComponentDriver;
import org.fest.swing.edt.GuiActionRunner;
import org.fest.swing.edt.GuiQuery;
import org.fest.swing.edt.GuiTask;
import org.fest.swing.fixture.DialogFixture;
import org.fest.swing.timing.Condition;
import org.jetbrains.android.uipreview.AndroidLayoutPreviewToolWindowManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.FocusManager;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.KeyStroke;
import java.awt.Component;
import java.awt.event.KeyEvent;
import java.util.List;
import static com.android.tools.idea.tests.gui.framework.GuiTests.SHORT_TIMEOUT;
import static org.fest.assertions.Assertions.assertThat;
import static org.fest.reflect.core.Reflection.method;
import static org.fest.swing.edt.GuiActionRunner.execute;
import static org.fest.swing.timing.Pause.pause;
import static org.fest.util.Strings.quote;
import static org.junit.Assert.*;
/**
* Fixture wrapping the IDE source editor, providing convenience methods
* for controlling the source editor and verifying editor state. Note that unlike
* the IntelliJ Editor class, which is one per file, this fixture represents an
* editor in the more traditional sense: a container for multiple files, so you
* ask "the" editor its current file, to select text in that file, to switch to
* a different file, etc.
*/
public class EditorFixture {
public static final String CARET = "^";
public static final String SELECT_BEGIN = "|>";
public static final String SELECT_END = "<|";
/**
* Performs simulation of user events on <code>{@link #target}</code>
*/
public final Robot robot;
private final IdeFrameFixture myFrame;
/**
* Constructs a new editor fixture, tied to the given project
*/
public EditorFixture(Robot robot, IdeFrameFixture frame) {
this.robot = robot;
myFrame = frame;
}
/**
* Returns the current file being shown in the editor, if there is a current
* editor open and it's a file editor
*
* @return the currently edited file or null
*/
@Nullable
public VirtualFile getCurrentFile() {
FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject());
VirtualFile[] selectedFiles = manager.getSelectedFiles();
if (selectedFiles.length > 0) {
return selectedFiles[0];
}
return null;
}
/**
* Returns the name of the current file, if any. Convenience method
* for {@link #getCurrentFile()}.getName().
*
* @return the current file name, or null
*/
@Nullable
public String getCurrentFileName() {
VirtualFile currentFile = getCurrentFile();
return currentFile != null ? currentFile.getName() : null;
}
/**
* Returns the line number of the current caret position (0-based).
*
* @return the current 0-based line number, or -1 if there is no current file
*/
public int getCurrentLineNumber() {
//noinspection ConstantConditions
return execute(new GuiQuery<Integer>() {
@Override
@Nullable
protected Integer executeInEDT() throws Throwable {
FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject());
Editor editor = manager.getSelectedTextEditor();
if (editor != null) {
CaretModel caretModel = editor.getCaretModel();
Caret primaryCaret = caretModel.getPrimaryCaret();
int offset = primaryCaret.getOffset();
Document document = editor.getDocument();
return document.getLineNumber(offset);
}
return -1;
}
});
}
/**
* Returns the contents of the current line, or null if there is no
* file open. The caret position is indicated by {@code ^}, and
* the selection text range, if on the current line, is indicated by
* the text inside {@code |> <|}.
*
* @param trim if true, trim whitespace around the line
* @param showPositions if true, show the editor positions (carets, selection)
* @param additionalLines 0, or a count for additional number of lines to include on each side of the current line
* @return the text contents at the current caret position
*/
@Nullable
public String getCurrentLineContents(boolean trim, boolean showPositions, int additionalLines) {
if (showPositions) {
return getCurrentLineContents(trim, CARET, SELECT_BEGIN, SELECT_END, additionalLines);
}
else {
return getCurrentLineContents(trim, null, null, null, additionalLines);
}
}
/**
* Returns the contents of the current line, or null if there is no
* file open.
*
* @param trim if true, trim whitespace around the line
* @param caretString typically "^" which will insert "^" to indicate the
* caret position. If null, the caret position is not shown.
* @param selectBegin the text string to insert at the beginning of the selection boundary
* @param selectEnd the text string to insert at the end of the selection boundary
* @return the text contents at the current caret position
*/
@Nullable
public String getCurrentLineContents(final boolean trim,
@Nullable final String caret,
@Nullable final String selectBegin,
@Nullable final String selectEnd,
final int additionalLines) {
return execute(new GuiQuery<String>() {
@Override
@Nullable
protected String executeInEDT() throws Throwable {
FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject());
Editor editor = manager.getSelectedTextEditor();
if (editor != null) {
CaretModel caretModel = editor.getCaretModel();
Caret primaryCaret = caretModel.getPrimaryCaret();
int offset = primaryCaret.getOffset();
int start = primaryCaret.getSelectionStart();
int end = primaryCaret.getSelectionEnd();
if (start == end) {
start = -1;
end = -1;
}
Document document = editor.getDocument();
int lineNumber = document.getLineNumber(offset);
int lineStart = document.getLineStartOffset(lineNumber);
int lineEnd = document.getLineEndOffset(lineNumber);
int lineCount = document.getLineCount();
for (int i = 1; i <= additionalLines; i++) {
if (lineNumber - i >= 0) {
lineStart = document.getLineStartOffset(lineNumber - i);
}
if (lineNumber + i < lineCount) {
lineEnd = document.getLineEndOffset(lineNumber + i);
}
}
String line = document.getText(new TextRange(lineStart, lineEnd));
offset -= lineStart;
start -= lineStart;
end -= lineStart;
StringBuilder sb = new StringBuilder(line.length() + 10);
for (int i = 0, n = line.length(); i < n; i++) {
if (selectBegin != null && start == i) {
sb.append(selectBegin);
}
if (caret != null && offset == i) {
sb.append(caret);
}
sb.append(line.charAt(i));
if (selectEnd != null && end == i + 1) {
sb.append(selectEnd);
}
}
String result = sb.toString();
if (trim) {
result = result.trim();
}
return result;
}
return null;
}
});
}
/**
* Returns the contents of the current file, or null if there is no
* file open. The caret position is indicated by {@code ^}, and
* the selection text range, if on the current line, is indicated by
* the text inside {@code |> <|}.
*
* @param showPositions if true, show the editor positions (carets, selection)
* @return the text contents at the current caret position
*/
@Nullable
public String getCurrentFileContents(boolean showPositions) {
if (showPositions) {
return getCurrentFileContents(CARET, SELECT_BEGIN, SELECT_END);
}
else {
return getCurrentFileContents(null, null, null);
}
}
/**
* Returns the contents of the current file, or null if there is no
* file open.
*
* @param caretString typically "^" which will insert "^" to indicate the
* caret position. If null, the caret position is not shown.
* @param selectBegin the text string to insert at the beginning of the selection boundary
* @param selectEnd the text string to insert at the end of the selection boundary
* @return the text contents at the current caret position
*/
@Nullable
public String getCurrentFileContents(@Nullable final String caret, @Nullable final String selectBegin, @Nullable final String selectEnd) {
return execute(new GuiQuery<String>() {
@Override
@Nullable
protected String executeInEDT() throws Throwable {
FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject());
Editor editor = manager.getSelectedTextEditor();
if (editor != null) {
CaretModel caretModel = editor.getCaretModel();
Caret primaryCaret = caretModel.getPrimaryCaret();
int offset = primaryCaret.getOffset();
int start = primaryCaret.getSelectionStart();
int end = primaryCaret.getSelectionEnd();
if (start == end) {
start = -1;
end = -1;
}
Document document = editor.getDocument();
int lineStart = 0;
int lineEnd = document.getTextLength();
String text = document.getText(new TextRange(lineStart, lineEnd));
StringBuilder sb = new StringBuilder(text.length() + 10);
for (int i = 0, n = text.length(); i < n; i++) {
if (selectBegin != null && start == i) {
sb.append(selectBegin);
}
if (caret != null && offset == i) {
sb.append(caret);
}
sb.append(text.charAt(i));
if (selectEnd != null && end == i + 1) {
sb.append(selectEnd);
}
}
return sb.toString();
}
return null;
}
});
}
/**
* Type the given text into the editor
*
* @param text the text to type at the current editor position
*/
public EditorFixture enterText(@NotNull final String text) {
Component component = getFocusedEditor();
if (component != null) {
robot.enterText(text);
}
return this;
}
/**
* Press and release the given key as indicated by the {@code VK_} codes in {@link java.awt.event.KeyEvent}.
* Used to transfer key presses to the editor which may have an effect but does not insert text into
* the editor (e.g. pressing an arrow key to move the caret)
*
* @param keyCode the key code to press
*/
public EditorFixture typeKey(int keyCode) {
Component component = getFocusedEditor();
if (component != null) {
new ComponentDriver(robot).pressAndReleaseKeys(component, keyCode);
}
return this;
}
/**
* Press (but don't release yet) the given key as indicated by the {@code VK_} codes in {@link java.awt.event.KeyEvent}.
* Used to transfer key presses to the editor which may have an effect but does not insert text into
* the editor (e.g. pressing an arrow key to move the caret)
*
* @param keyCode the key code to press
*/
public EditorFixture pressKey(int keyCode) {
Component component = getFocusedEditor();
if (component != null) {
new ComponentDriver(robot).pressKey(component, keyCode);
}
return this;
}
/**
* Release the given key (as indicated by the {@code VK_} codes in {@link java.awt.event.KeyEvent}) which
* must be currently pressed by a previous call to {@link #pressKey(int)}.
*
* @param keyCode the key code
*/
public EditorFixture releaseKey(int keyCode) {
Component component = getFocusedEditor();
if (component != null) {
new ComponentDriver(robot).releaseKey(component, keyCode);
}
return this;
}
/**
* Requests focus in the editor
*/
public EditorFixture requestFocus() {
getFocusedEditor();
return this;
}
/**
* Requests focus in the editor, waits and returns editor component
*/
@Nullable
private JComponent getFocusedEditor() {
Editor editor = execute(new GuiQuery<Editor>() {
@Override
@Nullable
protected Editor executeInEDT() throws Throwable {
FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject());
return manager.getSelectedTextEditor(); // Must be called from the EDT
}
});
if (editor != null) {
JComponent contentComponent = editor.getContentComponent();
new ComponentDriver(robot).focusAndWaitForFocusGain(contentComponent);
assertSame(contentComponent, FocusManager.getCurrentManager().getFocusOwner());
return contentComponent;
} else {
fail("Expected to find editor to focus, but there is no current editor");
return null;
}
}
/**
* Moves the caret to the given caret offset (0-based).
*
* @param offset the character offset
*/
public EditorFixture moveTo(final int offset) {
assertTrue(offset >= 0);
execute(new GuiTask() {
@Override
protected void executeInEDT() throws Throwable {
// TODO: Do this via mouse clicks!
FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject());
Editor editor = manager.getSelectedTextEditor();
if (editor != null) {
editor.getCaretModel().moveToOffset(offset);
editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
}
}
});
return this;
}
/**
* Selects the given range. If the first and second offsets are the same, it simply
* moves the caret to the given position. The caret is always placed at the second offset,
* <b>which is allowed to be smaller than the first offset</b>. Calling {@code select(10, 7)}
* would be the same as dragging the mouse from offset 10 to offset 7 and releasing the mouse
* button; the caret is now at the beginning of the selection.
*
* @param firstOffset the character offset where we start the selection, or -1 to remove the selection
* @param secondOffset the character offset where we end the selection, which can be an earlier
* offset than the firstOffset
*/
public EditorFixture select(final int firstOffset, final int secondOffset) {
execute(new GuiTask() {
@Override
protected void executeInEDT() throws Throwable {
// TODO: Do this via mouse drags!
FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject());
Editor editor = manager.getSelectedTextEditor();
if (editor != null) {
editor.getCaretModel().getPrimaryCaret().setSelection(firstOffset, secondOffset);
editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
}
}
});
return this;
}
/**
* Finds the next position (or if {@code searchFromTop} is true, from the beginning) of
* the given string indicated by a prefix and a suffix. The offset returned will be the position exactly
* in the middle of the two. For example, if you have the text "The quick brown fox jumps over the lazy dog"
* and you search via {@code moveTo("The qui", "ck brown", true)} the returned offset will be at the 7th
* position in the string, between the "i" and "c".
* <p/>
* Note that on Windows, any {@code \r}'s are hidden from the editor, so they never count in offset
* computations.
*
* @param prefix the target prefix which must immediately precede the returned position
* @param suffix the target string, which must immediately follow the given prefix
* @param searchFromTop if true, search from the beginning of the file instead of from the current editor position
* @return the 0-based offset in the document, or -1 if not found.
*/
public int findOffset(@Nullable final String prefix, @Nullable final String suffix, final boolean searchFromTop) {
assertTrue(prefix != null || suffix != null);
//noinspection ConstantConditions
return execute(new GuiQuery<Integer>() {
@Override
@Nullable
protected Integer executeInEDT() throws Throwable {
FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject());
Editor editor = manager.getSelectedTextEditor();
if (editor != null) {
CaretModel caretModel = editor.getCaretModel();
Caret primaryCaret = caretModel.getPrimaryCaret();
Document document = editor.getDocument();
String contents = document.getCharsSequence().toString();
String target = (prefix != null ? prefix : "") + (suffix != null ? suffix : "");
int targetIndex = contents.indexOf(target, searchFromTop ? 0 : primaryCaret.getOffset());
return targetIndex != -1 ? targetIndex + (prefix != null ? prefix.length() : 0) : -1;
}
return -1;
}
});
}
/**
* Finds the first position in the editor document indicated by the given text segment, where ^ (or if not defined, |) indicates
* the caret position.
*
* @param line the line segment to search for (with ^ or | indicating the caret position)
* @return the 0-based offset in the document, or -1 if not found.
*/
public int findOffset(@NotNull final String line) {
int index = line.indexOf('^');
if (index == -1) {
// Also look for |. ^ has higher precedence since in many Android XML files we'll have | appearing as
// the XML value flag delimiter.
index = line.indexOf('|');
}
assertTrue("The text segment should contain a caret position indicated by ^ or |", index != -1);
String prefix = line.substring(0, index);
if (prefix.isEmpty()) {
prefix = null;
}
String suffix = line.substring(index + 1);
if (suffix.isEmpty()) {
suffix = null;
}
assertTrue("The text segment should have more text than just the caret position", prefix != null || suffix != null);
return findOffset(prefix, suffix, true);
}
/**
* Closes the current editor
*/
public EditorFixture close() {
execute(new GuiTask() {
@Override
protected void executeInEDT() throws Throwable {
VirtualFile currentFile = getCurrentFile();
if (currentFile != null) {
FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject());
manager.closeFile(currentFile);
}
}
});
return this;
}
/**
* Selects the given tab in the current editor. Used to switch between
* design mode and editor mode for example.
*
* @param tab the tab to switch to
*/
public EditorFixture selectEditorTab(@NotNull final Tab tab) {
switch (tab) {
case EDITOR:
selectEditorTab("Text");
break;
case DESIGN:
selectEditorTab("Design");
break;
case DEFAULT:
selectEditorTab((String)null);
break;
default:
fail("Unknown tab " + tab);
}
return this;
}
/**
* Selects the given tab in the current editor. Used to switch between
* design mode and editor mode for example.
*
* @param tabName the label in the editor, or null for the default (first) tab
*/
public EditorFixture selectEditorTab(@Nullable final String tabName) {
execute(new GuiTask() {
@Override
protected void executeInEDT() throws Throwable {
VirtualFile currentFile = getCurrentFile();
assertNotNull("Can't switch to tab " + tabName + " when no file is open in the editor", currentFile);
FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject());
FileEditor[] editors = manager.getAllEditors(currentFile);
FileEditor target = null;
for (FileEditor editor : editors) {
if (tabName == null || tabName.equals(editor.getName())) {
target = editor;
break;
}
}
if (target != null) {
// Have to use reflection
//FileEditorManagerImpl#setSelectedEditor(final FileEditor editor)
method("setSelectedEditor").withParameterTypes(FileEditor.class).in(manager).invoke(target);
return;
}
List<String> tabNames = Lists.newArrayList();
for (FileEditor editor : editors) {
tabNames.add(editor.getName());
}
fail("Could not find editor tab \"" + (tabName != null ? tabName : "<default>") + "\": Available tabs = " + tabNames);
}
});
return this;
}
/**
* Opens up a different file. This will run through the "Open File..." dialog to
* find and select the given file.
*
* @param file the file to open
* @param tab which tab to open initially, if there are multiple editors
*/
public EditorFixture open(@NotNull final VirtualFile file, @NotNull final Tab tab) {
execute(new GuiTask() {
@Override
protected void executeInEDT() throws Throwable {
// TODO: Use UI to navigate to the file instead
Project project = myFrame.getProject();
FileEditorManager manager = FileEditorManager.getInstance(project);
if (tab == Tab.EDITOR) {
manager.openTextEditor(new OpenFileDescriptor(project, file), true);
}
else {
manager.openFile(file, true);
}
}
});
pause(new Condition("File " + quote(file.getPath()) + " to be opened") {
@Override
public boolean test() {
//noinspection ConstantConditions
return execute(new GuiQuery<Boolean>() {
@Override
protected Boolean executeInEDT() throws Throwable {
return file.equals(getCurrentFile());
}
});
}
}, SHORT_TIMEOUT);
return this;
}
/**
* Opens up a different file. This will run through the "Open File..." dialog to
* find and select the given file.
*
* @param file the project-relative path (with /, not File.separator, as the path separator)
* @param tab which tab to open initially, if there are multiple editors
*/
public EditorFixture open(@NotNull final String relativePath, @NotNull Tab tab) {
assertFalse("Should use '/' in test relative paths, not File.separator", relativePath.contains("\\"));
VirtualFile file = myFrame.findFileByRelativePath(relativePath, true);
return open(file, tab);
}
/**
* Like {@link #open(String, com.android.tools.idea.tests.gui.framework.fixture.EditorFixture.Tab)} but
* always uses the default tab
*
* @param file the project-relative path (with /, not File.separator, as the path separator)
*/
public EditorFixture open(@NotNull final String relativePath) {
return open(relativePath, Tab.DEFAULT);
}
/**
* Invokes the given action. This will look up the corresponding action's key bindings, if any, and invoke
* it. It will fail if the action is not enabled, or if it is interactive.
*
* @param action the action to invoke
*/
public EditorFixture invokeAction(@NotNull EditorAction action) {
switch (action) {
case BACK_SPACE:
invokeActionViaKeystroke("EditorBackSpace");
break;
case UNDO:
invokeActionViaKeystroke("$Undo");
break;
case REDO:
invokeActionViaKeystroke("$Redo");
break;
case CUT:
invokeActionViaKeystroke("$Cut");
break;
case COPY:
invokeActionViaKeystroke("$Copy");
break;
case PASTE:
invokeActionViaKeystroke("$Paste");
break;
case SELECT_ALL:
invokeActionViaKeystroke("$SelectAll");
break;
case FORMAT: {
// To format without showing dialog:
// invokeActionViaKeystroke("ReformatCode");
// However, before we replace this, make sure the dialog isn't shown in some scenarios (e.g. first users)
invokeActionViaKeystroke("ShowReformatFileDialog");
JDialog dialog = robot.finder().find(new GenericTypeMatcher<JDialog>(JDialog.class) {
@Override
protected boolean isMatching(@NotNull JDialog dialog) {
return dialog.isShowing() && dialog.getTitle().contains("Reformat");
}
});
DialogFixture dialogFixture = new DialogFixture(robot, dialog);
// For some reason, the button is not found
//dialogFixture.button("Run").click();
dialogFixture.click();
break;
}
case GOTO_DECLARATION:
invokeActionViaKeystroke("GotoDeclaration");
break;
case COMPLETE_CURRENT_STATEMENT:
invokeActionViaKeystroke("EditorCompleteStatement");
break;
case SAVE:
invokeActionViaKeystroke("SaveAll");
break;
case TOGGLE_COMMENT:
invokeActionViaKeystroke("CommentByLineComment");
break;
case DUPLICATE_LINES:
invokeActionViaKeystroke("EditorDuplicate");
break;
case DELETE_LINE:
invokeActionViaKeystroke("EditorDeleteLine");
break;
case NEXT_METHOD:
invokeActionViaKeystroke("MethodDown");
break;
case PREVIOUS_METHOD:
invokeActionViaKeystroke("MethodUp");
break;
case NEXT_ERROR:
invokeActionViaKeystroke("GotoNextError");
break;
case PREVIOUS_ERROR:
invokeActionViaKeystroke("GotoPreviousError");
break;
case JOIN_LINES:
invokeActionViaKeystroke("EditorJoinLines");
break;
case SHOW_INTENTION_ACTIONS:
invokeActionViaKeystroke("ShowIntentionActions");
break;
case RUN_FROM_CONTEXT:
invokeActionViaKeystroke("RunClass");
break;
case EXTEND_SELECTION:
case SHRINK_SELECTION:
// Need to find the right action id's for these; didn't see them in the default keymap
default:
fail("Not yet implemented");
break;
}
return this;
}
private void invokeActionViaKeystroke(@NotNull String actionId) {
AnAction action = ActionManager.getInstance().getAction(actionId);
assertNotNull(actionId, action);
assertTrue(actionId + " is not enabled", action.getTemplatePresentation().isEnabled());
Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
Shortcut[] shortcuts = keymap.getShortcuts(actionId);
assertNotNull(shortcuts);
assertThat(shortcuts).isNotEmpty();
Shortcut shortcut = shortcuts[0];
if (shortcut instanceof KeyboardShortcut) {
KeyboardShortcut cs = (KeyboardShortcut)shortcut;
KeyStroke firstKeyStroke = cs.getFirstKeyStroke();
Component component = getFocusedEditor();
if (component != null) {
ComponentDriver driver = new ComponentDriver(robot);
System.out.println("Invoking editor action " + actionId + " via shortcut "
+ KeyEvent.getKeyModifiersText(firstKeyStroke.getModifiers())
+ KeyEvent.getKeyText(firstKeyStroke.getKeyCode()));
driver.pressAndReleaseKey(component, firstKeyStroke.getKeyCode(), new int[]{firstKeyStroke.getModifiers()});
KeyStroke secondKeyStroke = cs.getSecondKeyStroke();
if (secondKeyStroke != null) {
System.out.println(" and "
+ KeyEvent.getKeyModifiersText(secondKeyStroke.getModifiers())
+ KeyEvent.getKeyText(secondKeyStroke.getKeyCode()));
driver.pressAndReleaseKey(component, secondKeyStroke.getKeyCode(), new int[]{secondKeyStroke.getModifiers()});
}
} else {
fail("Editor not focused for action");
}
}
else {
fail("Unsupported shortcut type " + shortcut.getClass().getName());
}
}
/**
* Checks that the editor has a given number of issues. This is a convenience wrapper
* for {@link FileFixture#requireCodeAnalysisHighlightCount(HighlightSeverity, int)}
*
* @param severity the severity of the issues you want to count
* @param expected the expected count
* @return this
*/
@NotNull
public EditorFixture requireCodeAnalysisHighlightCount(@NotNull final HighlightSeverity severity, int expected) {
VirtualFile currentFile = getCurrentFile();
assertNotNull("Expected a file to be open", currentFile);
FileFixture file = new FileFixture(myFrame.getProject(), currentFile);
file.requireCodeAnalysisHighlightCount(severity, expected);
return this;
}
/**
* Waits until the editor has the given number of errors at the given severity.
* Typically used when you want to invoke an intention action, but need to wait until
* the code analyzer has found an error it needs to resolve first.
*
* @param severity the severity of the issues you want to count
* @param expected the expected count
* @return this
*/
@NotNull
public EditorFixture waitForCodeAnalysisHighlightCount(@NotNull final HighlightSeverity severity, int expected) {
VirtualFile currentFile = getCurrentFile();
assertNotNull("Expected a file to be open", currentFile);
FileFixture file = new FileFixture(myFrame.getProject(), currentFile);
file.waitForCodeAnalysisHighlightCount(severity, expected);
return this;
}
@NotNull
public EditorFixture waitUntilErrorAnalysisFinishes() {
VirtualFile currentFile = getCurrentFile();
assertNotNull("Expected a file to be open", currentFile);
FileFixture file = new FileFixture(myFrame.getProject(), currentFile);
file.waitUntilErrorAnalysisFinishes();
return this;
}
/**
* Invokes the show intentions action, waits for the actions to be displayed and then picks the
* one with the given label prefix
*
* @param labelPrefix the prefix of the action description to be shown
* @return this
*/
@NotNull
public EditorFixture invokeIntentionAction(@NotNull String labelPrefix) {
invokeAction(EditorFixture.EditorAction.SHOW_INTENTION_ACTIONS);
JBList popup = GuiTests.waitForPopup(robot);
GuiTests.clickPopupMenuItem(labelPrefix, popup, robot);
return this;
}
/**
* Returns a fixture around the layout editor, <b>if</b> the currently edited file
* is a layout file and it is currently showing the layout editor tab or the parameter
* requests that it be opened if necessary
*
* @param switchToTabIfNecessary if true, switch to the design tab if it is not already showing
* @return a layout editor fixture, or null if the current file is not a layout file or the
* wrong tab is showing
*/
@Nullable
public LayoutEditorFixture getLayoutEditor(boolean switchToTabIfNecessary) {
VirtualFile currentFile = getCurrentFile();
if (ResourceHelper.getFolderType(currentFile) != ResourceFolderType.LAYOUT) {
return null;
}
if (switchToTabIfNecessary) {
selectEditorTab(Tab.DESIGN);
}
return execute(new GuiQuery<LayoutEditorFixture>() {
@Override
@Nullable
protected LayoutEditorFixture executeInEDT() throws Throwable {
FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject());
FileEditor[] editors = manager.getSelectedEditors();
if (editors.length == 0) {
return null;
}
FileEditor selected = editors[0];
if (!(selected instanceof AndroidDesignerEditor)) {
return null;
}
return new LayoutEditorFixture(robot, (AndroidDesignerEditor)selected);
}
});
}
/**
* Returns a fixture around the layout preview window, <b>if</b> the currently edited file
* is a layout file and it the XML editor tab of the layout is currently showing.
*
* @param switchToTabIfNecessary if true, switch to the editor tab if it is not already showing
* @return a layout preview fixture, or null if the current file is not a layout file or the
* wrong tab is showing
*/
@Nullable
public LayoutPreviewFixture getLayoutPreview(boolean switchToTabIfNecessary) {
VirtualFile currentFile = getCurrentFile();
if (ResourceHelper.getFolderType(currentFile) != ResourceFolderType.LAYOUT) {
return null;
}
if (switchToTabIfNecessary) {
selectEditorTab(Tab.EDITOR);
}
Boolean visible = GuiActionRunner.execute(new GuiQuery<Boolean>() {
@Override
protected Boolean executeInEDT() throws Throwable {
AndroidLayoutPreviewToolWindowManager manager = AndroidLayoutPreviewToolWindowManager.getInstance(myFrame.getProject());
return manager.getToolWindowForm() != null;
}
});
if (visible == null || !visible) {
myFrame.invokeMenuPath("View", "Tool Windows", "Preview");
}
pause(new Condition("Preview window is visible") {
@Override
public boolean test() {
AndroidLayoutPreviewToolWindowManager manager = AndroidLayoutPreviewToolWindowManager.getInstance(myFrame.getProject());
return manager.getToolWindowForm() != null;
}
}, SHORT_TIMEOUT);
return new LayoutPreviewFixture(robot, myFrame.getProject());
}
/**
* Returns a fixture around the {@link com.android.tools.idea.editors.strings.StringResourceEditor} <b>if</b> the currently
* displayed editor is a translations editor.
*/
@Nullable
public TranslationsEditorFixture getTranslationsEditor() {
VirtualFile currentFile = getCurrentFile();
if (!(currentFile instanceof StringsVirtualFile)) {
return null;
}
return execute(new GuiQuery<TranslationsEditorFixture>() {
@Override
@Nullable
protected TranslationsEditorFixture executeInEDT() throws Throwable {
FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject());
FileEditor[] editors = manager.getSelectedEditors();
if (editors.length == 0) {
return null;
}
FileEditor selected = editors[0];
if (!(selected instanceof StringResourceEditor)) {
return null;
}
return new TranslationsEditorFixture(robot, (StringResourceEditor)selected);
}
});
}
/**
* Returns a fixture around the {@link com.android.tools.idea.editors.theme.ThemeEditor} <b>if</b> the currently
* displayed editor is a theme editor.
*/
@NotNull
public ThemeEditorFixture getThemeEditor() {
final ThemeEditorComponent themeEditorComponent =
GuiTests.waitUntilFound(robot, new GenericTypeMatcher<ThemeEditorComponent>(ThemeEditorComponent.class) {
@Override
protected boolean isMatching(@NotNull ThemeEditorComponent component) {
return true;
}
});
return new ThemeEditorFixture(robot, themeEditorComponent);
}
/**
* Requires the source editor's current file name to be the given name (or if null, for there
* to be no current file)
*/
public void requireName(@Nullable String name) {
VirtualFile currentFile = getCurrentFile();
if (name == null) {
assertNull("Expected editor to not have an open file, but is showing " + currentFile, currentFile);
} else if (currentFile == null) {
fail("Expected file " + name + " to be showing, but the editor is not showing anything");
} else {
assertEquals(name, currentFile.getName());
}
}
/**
* Requires the source editor's current file to be in the given folder (or if null, for there
* to be no current file)
*/
public void requireFolderName(@Nullable String name) {
VirtualFile currentFile = getCurrentFile();
if (name == null) {
assertNull("Expected editor to not have an open file, but is showing " + currentFile, currentFile);
} else if (currentFile == null) {
fail("Expected file " + name + " to be showing, but the editor is not showing anything");
} else {
VirtualFile parent = currentFile.getParent();
assertNotNull("File " + currentFile.getName() + " does not have a parent", parent);
assertEquals(name, parent.getName());
}
}
/**
* Common editor actions, invokable via {@link #invokeAction(EditorAction)}
*/
public enum EditorAction {
SHOW_INTENTION_ACTIONS,
FORMAT,
SAVE,
UNDO,
REDO,
COPY,
PASTE,
CUT,
BACK_SPACE,
COMPLETE_CURRENT_STATEMENT,
EXTEND_SELECTION,
SHRINK_SELECTION,
SELECT_ALL,
JOIN_LINES,
DUPLICATE_LINES,
DELETE_LINE,
TOGGLE_COMMENT,
GOTO_DECLARATION,
NEXT_ERROR,
PREVIOUS_ERROR,
NEXT_METHOD,
PREVIOUS_METHOD,
RUN_FROM_CONTEXT
}
/**
* The different tabs of an editor; used by for example {@link #open(VirtualFile, EditorFixture.Tab)} to indicate which
* tab should be opened
*/
public enum Tab { EDITOR, DESIGN, DEFAULT }
}