com.android.tools.idea.tests.gui.framework.fixture.EditorFixture.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.tests.gui.framework.fixture.EditorFixture.java

Source

/*
 * 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.manifest.ManifestPanel;
import com.android.tools.idea.editors.strings.StringResourceEditor;
import com.android.tools.idea.editors.theme.ThemeEditorComponent;
import com.android.tools.idea.res.ResourceHelper;
import com.android.tools.idea.tests.gui.framework.fixture.layout.NlEditorFixture;
import com.android.tools.idea.tests.gui.framework.fixture.layout.NlPreviewFixture;
import com.android.tools.idea.tests.gui.framework.fixture.theme.ThemeEditorFixture;
import com.android.tools.idea.tests.gui.framework.fixture.theme.ThemePreviewFixture;
import com.android.tools.idea.tests.gui.framework.matcher.Matchers;
import com.android.tools.idea.uibuilder.editor.NlEditor;
import com.android.tools.idea.uibuilder.editor.NlPreviewManager;
import com.google.common.collect.Lists;
import com.intellij.codeInsight.daemon.impl.HighlightInfo;
import com.intellij.icons.AllIcons;
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.Caret;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.editor.ScrollType;
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.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.ui.EditorNotificationPanel;
import com.intellij.ui.RowIcon;
import com.intellij.ui.components.JBList;
import com.intellij.ui.components.JBLoadingPanel;
import com.intellij.ui.tabs.impl.TabLabel;
import org.fest.swing.core.GenericTypeMatcher;
import org.fest.swing.core.Robot;
import org.fest.swing.core.matcher.JLabelMatcher;
import org.fest.swing.driver.ComponentDriver;
import org.fest.swing.edt.GuiQuery;
import org.fest.swing.edt.GuiTask;
import org.fest.swing.timing.Wait;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.android.tools.idea.tests.gui.framework.GuiTests.*;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static org.fest.reflect.core.Reflection.method;
import static org.fest.swing.edt.GuiActionRunner.execute;
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 {

    /**
     * Performs simulation of user events on <code>{@link #target}</code>
     */
    final Robot robot;
    private final IdeFrameFixture myFrame;

    /**
     * Constructs a new editor fixture, tied to the given project
     */
    EditorFixture(Robot robot, IdeFrameFixture frame) {
        this.robot = robot;
        myFrame = frame;
    }

    /** Returns the selected file with most recent focused editor, or {@code null} if there are no selected files. */
    @Nullable
    public VirtualFile getCurrentFile() {
        VirtualFile[] selectedFiles = FileEditorManager.getInstance(myFrame.getProject()).getSelectedFiles();
        return (selectedFiles.length > 0) ? selectedFiles[0] : 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;
    }

    /**
     * From the currently selected text editor, returns the line number where the primary caret is.
     * <p>
     * This line number is conventionally 1-based, unlike the 0-based line numbers in {@link Editor}.
     *
     * @throws IllegalStateException if there is no currently selected text editor
     */
    public int getCurrentLineNumber() {
        return GuiQuery.getNonNull(() -> {
            Editor editor = FileEditorManager.getInstance(myFrame.getProject()).getSelectedTextEditor();
            checkState(editor != null, "no currently selected text editor");
            int offset = editor.getCaretModel().getPrimaryCaret().getOffset();
            return editor.getDocument().getLineNumber(offset) + 1; // Editor uses 0-based line numbers.
        });
    }

    /**
     * From the currently selected text editor, returns the text of the line where the primary caret is.
     *
     * @throws IllegalStateException if there is no currently selected text editor
     */
    @NotNull
    public String getCurrentLine() {
        return GuiQuery.getNonNull(() -> {
            Editor editor = FileEditorManager.getInstance(myFrame.getProject()).getSelectedTextEditor();
            checkState(editor != null, "no currently selected text editor");
            Caret primaryCaret = editor.getCaretModel().getPrimaryCaret();
            return editor.getDocument()
                    .getText(new TextRange(primaryCaret.getVisualLineStart(), primaryCaret.getVisualLineEnd()));
        });
    }

    /**
     * Returns the text of the document in the currently selected text editor.
     *
     * @throws IllegalStateException if there is no currently selected text editor
     */
    @NotNull
    public String getCurrentFileContents() {
        return GuiQuery.getNonNull(() -> {
            Editor editor = FileEditorManager.getInstance(myFrame.getProject()).getSelectedTextEditor();
            checkState(editor != null, "no currently selected text editor");
            return editor.getDocument().getImmutableCharSequence().toString();
        });
    }

    /**
     * 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;
    }

    /**
     * Requests focus in the editor, waits and returns editor component
     */
    @NotNull
    private JComponent getFocusedEditor() {
        Editor editor = GuiQuery.getNonNull(() -> {
            Editor selectedTextEditor = FileEditorManager.getInstance(myFrame.getProject()).getSelectedTextEditor();
            checkState(selectedTextEditor != null, "no currently selected text editor");
            return selectedTextEditor;
        });

        JComponent contentComponent = editor.getContentComponent();
        new ComponentDriver(robot).focusAndWaitForFocusGain(contentComponent);
        return contentComponent;
    }

    /**
     * Moves the caret to the start of the first occurrence of {@code after} immediately following {@code before} in the selected text editor.
     *
     * @throws IllegalStateException if there is no selected text editor or if no match is found
     */
    @NotNull
    public EditorFixture moveBetween(@NotNull String before, @NotNull String after) {
        return select(String.format("%s()%s", Pattern.quote(before), Pattern.quote(after)));
    }

    /**
     * Given a {@code regex} with one capturing group, selects the subsequence captured in the first match found in the selected text editor.
     *
     * @throws IllegalStateException if there is no currently selected text editor or no match is found
     * @throws IllegalArgumentException if {@code regex} does not have exactly one capturing group
     */
    @NotNull
    public EditorFixture select(String regex) {
        Matcher matcher = Pattern.compile(regex).matcher(getCurrentFileContents());
        checkArgument(matcher.groupCount() == 1, "must have exactly one capturing group: %s", regex);
        matcher.find();
        int start = matcher.start(1);
        int end = matcher.end(1);
        SelectTarget selectTarget = GuiQuery.getNonNull(() -> {
            Editor editor = FileEditorManager.getInstance(myFrame.getProject()).getSelectedTextEditor();
            checkState(editor != null, "no currently selected text editor");
            LogicalPosition startPosition = editor.offsetToLogicalPosition(start);
            LogicalPosition endPosition = editor.offsetToLogicalPosition(end);
            // CENTER_DOWN tries to make endPosition visible; if that fails, write selectWithKeyboard and rename this method selectWithMouse?
            editor.getScrollingModel().scrollTo(startPosition, ScrollType.CENTER_DOWN);

            SelectTarget target = new SelectTarget();
            target.component = editor.getContentComponent();
            target.startPoint = editor.logicalPositionToXY(startPosition);
            target.endPoint = editor.logicalPositionToXY(endPosition);
            return target;
        });
        robot.pressMouse(selectTarget.component, selectTarget.startPoint);
        robot.moveMouse(selectTarget.component, selectTarget.endPoint);
        robot.releaseMouseButtons();
        return this;
    }

    private static class SelectTarget {
        JComponent component;
        Point startPoint;
        Point endPoint;
    }

    /**
     * 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) {
        String tabName = tab.myTabName;
        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);
                }
            }
        });

        selectEditorTab(tab);

        Wait.seconds(5).expecting("file " + quote(file.getPath()) + " to be opened and loaded").until(() -> {
            if (!file.equals(getCurrentFile())) {
                return false;
            }

            FileEditor fileEditor = FileEditorManager.getInstance(myFrame.getProject()).getSelectedEditor(file);
            JComponent editorComponent = fileEditor.getComponent();
            if (editorComponent instanceof JBLoadingPanel) {
                return !((JBLoadingPanel) editorComponent).isLoading();
            }
            return true;
        });

        myFrame.requestFocusIfLost();
        robot.waitForIdle();

        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 {@code editorAction} via its (first) keyboard shortcut in the active keymap. */
    public EditorFixture invokeAction(@NotNull EditorAction editorAction) {
        AnAction anAction = ActionManager.getInstance().getAction(editorAction.id);
        assertTrue(editorAction.id + " is not enabled", anAction.getTemplatePresentation().isEnabled());

        Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
        Shortcut shortcut = keymap.getShortcuts(editorAction.id)[0];
        if (shortcut instanceof KeyboardShortcut) {
            KeyboardShortcut cs = (KeyboardShortcut) shortcut;
            KeyStroke firstKeyStroke = cs.getFirstKeyStroke();
            Component component = getFocusedEditor();
            if (component != null) {
                ComponentDriver driver = new ComponentDriver(robot);
                driver.pressAndReleaseKey(component, firstKeyStroke.getKeyCode(),
                        new int[] { firstKeyStroke.getModifiers() });
                KeyStroke secondKeyStroke = cs.getSecondKeyStroke();
                if (secondKeyStroke != null) {
                    driver.pressAndReleaseKey(component, secondKeyStroke.getKeyCode(),
                            new int[] { secondKeyStroke.getModifiers() });
                }
            } else {
                fail("Editor not focused for action");
            }
        } else {
            fail("Unsupported shortcut type " + shortcut.getClass().getName());
        }
        return this;
    }

    @NotNull
    public EditorNotificationPanelFixture awaitNotification(@NotNull String text) {
        JLabel label = waitUntilShowing(robot, JLabelMatcher.withText(text));
        EditorNotificationPanel notificationPanel = (EditorNotificationPanel) label.getParent().getParent();
        return new EditorNotificationPanelFixture(myFrame, notificationPanel);
    }

    @NotNull
    public EditorFixture checkNoNotification() {
        checkState(robot.finder().findAll(Matchers.byType(EditorNotificationPanel.class)).isEmpty());
        return this;
    }

    @NotNull
    public List<String> getHighlights(HighlightSeverity severity) {
        List<String> infos = Lists.newArrayList();
        for (HighlightInfo info : getCurrentFileFixture().getHighlightInfos(severity)) {
            infos.add(info.getDescription());
        }
        return infos;
    }

    /**
     * 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) {
        // Changing Java source level, for example, triggers compilation first; code analysis starts afterward
        waitForBackgroundTasks(robot);

        FileFixture file = getCurrentFileFixture();
        file.waitForCodeAnalysisHighlightCount(severity, expected);
        return this;
    }

    @NotNull
    public EditorFixture waitUntilErrorAnalysisFinishes() {
        FileFixture file = getCurrentFileFixture();
        file.waitUntilErrorAnalysisFinishes();
        robot.waitForIdle();
        return this;
    }

    @NotNull
    public EditorFixture waitForQuickfix() {
        waitUntilFound(robot, new GenericTypeMatcher<JLabel>(JLabel.class) {
            @Override
            protected boolean isMatching(@NotNull JLabel component) {
                Icon icon = component.getIcon();
                if (icon instanceof RowIcon) {
                    return AllIcons.Actions.QuickfixBulb.equals(((RowIcon) icon).getIcon(0));
                }
                return false;
            }
        });
        return this;
    }

    @NotNull
    private FileFixture getCurrentFileFixture() {
        VirtualFile currentFile = getCurrentFile();
        assertNotNull("Expected a file to be open", currentFile);
        return new FileFixture(myFrame.getProject(), currentFile);
    }

    /**
     * Waits for the quickfix bulb to appear before invoking the show intentions action,
     * then waits for the actions to be displayed and finally picks the one with the given label prefix
     *
     * @param labelPrefix the prefix of the action description to be shown
     */
    @NotNull
    public EditorFixture invokeQuickfixAction(@NotNull String labelPrefix) {
        waitForQuickfix();
        invokeAction(EditorAction.SHOW_INTENTION_ACTIONS);
        JBList popup = waitForPopup(robot);
        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
     * @throws IllegalStateException if there is no selected editor or it is not a {@link NlEditor}
     */
    @NotNull
    public NlEditorFixture getLayoutEditor(boolean switchToTabIfNecessary) {
        if (switchToTabIfNecessary) {
            selectEditorTab(Tab.DESIGN);
        }

        return GuiQuery.getNonNull(() -> {
            FileEditor[] editors = FileEditorManager.getInstance(myFrame.getProject()).getSelectedEditors();
            checkState(editors.length > 0, "no selected editors");
            FileEditor selected = editors[0];
            checkState(selected instanceof NlEditor, "not a %s: %s", NlEditor.class.getSimpleName(), selected);
            return new NlEditorFixture(myFrame.robot(), myFrame, (NlEditor) 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
     */
    @NotNull
    public NlPreviewFixture getLayoutPreview(boolean switchToTabIfNecessary) {
        if (switchToTabIfNecessary) {
            selectEditorTab(Tab.EDITOR);
        }

        boolean visible = GuiQuery.getNonNull(
                () -> NlPreviewManager.getInstance(myFrame.getProject()).getPreviewForm().getSurface().isShowing());
        if (!visible) {
            myFrame.invokeMenuPath("View", "Tool Windows", "Preview");
        }

        Wait.seconds(1).expecting("Preview window to be visible").until(
                () -> NlPreviewManager.getInstance(myFrame.getProject()).getPreviewForm().getSurface().isShowing());

        return new NlPreviewFixture(myFrame.getProject(), myFrame, myFrame.robot());
    }

    /**
     * Returns a fixture around the {@link com.android.tools.idea.editors.strings.StringResourceEditor} <b>if</b> the currently
     * displayed editor is a translations editor.
     */
    @NotNull
    public TranslationsEditorFixture getTranslationsEditor() {
        return GuiQuery.getNonNull(() -> {
            FileEditor[] editors = FileEditorManager.getInstance(myFrame.getProject()).getSelectedEditors();
            checkState(editors.length > 0, "no selected editors");
            FileEditor selected = editors[0];
            checkState(selected instanceof StringResourceEditor, "not a %s: %s",
                    StringResourceEditor.class.getSimpleName(), selected);
            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() {
        return new ThemeEditorFixture(robot, waitUntilFound(robot, Matchers.byType(ThemeEditorComponent.class)));
    }

    @NotNull
    public MergedManifestFixture getMergedManifestEditor() {
        return GuiQuery.getNonNull(() -> {
            FileEditor[] editors = FileEditorManager.getInstance(myFrame.getProject()).getSelectedEditors();
            checkState(editors.length > 0, "no selected editors");
            Component manifestPanel = editors[0].getComponent().getComponent(0);
            checkState(manifestPanel instanceof ManifestPanel, "not a %s: %s", ManifestPanel.class.getSimpleName(),
                    manifestPanel);
            return new MergedManifestFixture(robot, (ManifestPanel) manifestPanel);
        });
    }

    /**
     * Returns a fixture around the theme preview window, <b>if</b> the currently edited file
     * is a styles file and if 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 the theme preview fixture
     */
    @Nullable
    public ThemePreviewFixture getThemePreview(boolean switchToTabIfNecessary) {
        VirtualFile currentFile = getCurrentFile();
        if (ResourceHelper.getFolderType(currentFile) != ResourceFolderType.VALUES) {
            return null;
        }

        if (switchToTabIfNecessary) {
            selectEditorTab(Tab.EDITOR);
        }

        boolean visible = GuiQuery.getNonNull(() -> ToolWindowManager.getInstance(myFrame.getProject())
                .getToolWindow("Theme Preview").isActive());
        if (!visible) {
            myFrame.invokeMenuPath("View", "Tool Windows", "Theme Preview");
        }

        Wait.seconds(1).expecting("Theme Preview window to be visible").until(() -> GuiQuery.getNonNull(() -> {
            ToolWindow window = ToolWindowManager.getInstance(myFrame.getProject()).getToolWindow("Theme Preview");
            return window != null && window.isVisible();
        }));

        // Wait for it to be fully opened
        robot.waitForIdle();
        return new ThemePreviewFixture(robot, myFrame.getProject());
    }

    /**
     * Switch to an open tab
     */
    public EditorFixture switchToTab(@NotNull String tabName) {
        TabLabel tab = waitUntilShowing(robot, new GenericTypeMatcher<TabLabel>(TabLabel.class) {
            @Override
            protected boolean isMatching(@NotNull TabLabel tabLabel) {
                return tabName.equals(tabLabel.getAccessibleContext().getAccessibleName());
            }
        });
        robot.click(tab);
        return this;
    }

    @NotNull
    public IdeFrameFixture getIdeFrame() {
        return myFrame;
    }

    /**
     * Common editor actions, invokable via {@link #invokeAction(EditorAction)}
     */
    public enum EditorAction {
        SHOW_INTENTION_ACTIONS("ShowIntentionActions"), FORMAT("ReformatCode"), SAVE("SaveAll"), UNDO(
                "$Undo"), BACK_SPACE("EditorBackSpace"), COMPLETE_CURRENT_STATEMENT(
                        "EditorCompleteStatement"), DELETE_LINE("EditorDeleteLine"), GOTO_DECLARATION(
                                "GotoDeclaration"), RUN_FROM_CONTEXT("RunClass"), ESCAPE("EditorEscape"), DOWN(
                                        "EditorDown"), TOGGLE_LINE_BREAKPOINT(
                                                "ToggleLineBreakpoint"), GOTO_IMPLEMENTATION("GotoImplementation"),;

        /** The {@code id} of an action mapped to a keyboard shortcut in, for example, {@code $default.xml}. */
        @NotNull
        private final String id;

        EditorAction(@NotNull String actionId) {
            this.id = actionId;
        }
    }

    /**
     * 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("Text"), DESIGN("Design"), DEFAULT(null), MERGED_MANIFEST("Merged Manifest"),;

        /** The label in the editor, or {@code null} for the default (first) tab. */
        private final String myTabName;

        Tab(String tabName) {
            myTabName = tabName;
        }
    }

    public ApkViewerFixture getApkViewer(String name) {
        switchToTab(name);
        return ApkViewerFixture.find(getIdeFrame());
    }
}