/*
* 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.layout;
import com.android.tools.idea.gradle.invoker.GradleInvocationResult;
import com.android.tools.idea.tests.gui.framework.GuiTestCase;
import com.android.tools.idea.tests.gui.framework.annotation.IdeGuiTest;
import com.android.tools.idea.tests.gui.framework.fixture.EditorFixture;
import com.android.tools.idea.tests.gui.framework.fixture.IdeFrameFixture;
import com.android.tools.idea.tests.gui.framework.fixture.layout.ConfigurationToolbarFixture;
import com.android.tools.idea.tests.gui.framework.fixture.layout.LayoutPreviewFixture;
import com.android.tools.idea.tests.gui.framework.fixture.layout.RenderErrorPanelFixture;
import com.android.tools.lint.detector.api.LintUtils;
import org.jetbrains.annotations.NotNull;
import org.junit.Ignore;
import org.junit.Test;
import java.util.Collections;
import java.util.List;
import static com.android.SdkConstants.DOT_PNG;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.fail;
import static org.junit.Assert.assertTrue;
/**
* Unit test for the layout preview window
* <p>
* Additional things we should test:
* <ul>
* <li>Comprehensive device matching</li>
* <li>Multi configuration editing</li>
* <li>All the render quick fixes</li>
* <li>Editor caret syncing</li>
* <li>Included rendering</li>
* <li>Other file types than layouts (e.g. drawables etc)</li>
* <li>Render thumbnails</li>
* </ul>
*/
public class LayoutPreviewTest extends GuiTestCase {
// Default folder in the GUI test data directory where we're storing rendering thumbnails
public static final String THUMBNAIL_FOLDER = "thumbnails";
@Test
@IdeGuiTest
public void testConfigurationTweaks() throws Exception {
// Open an editor, wait for the layout preview window to open, toggle
// orientation to landscape, create a landscape variation file, ensure
// it's in the right folder, toggle orientation back, ensure file switched
// back to the portrait/original file
IdeFrameFixture projectFrame = openSimpleApplication();
EditorFixture editor = projectFrame.getEditor();
editor.open("app/src/main/res/layout/activity_my.xml", EditorFixture.Tab.EDITOR);
editor.requireFolderName("layout");
LayoutPreviewFixture preview = editor.getLayoutPreview(true);
assertNotNull(preview);
preview.waitForNextRenderToFinish();
preview.requireRenderSuccessful();
ConfigurationToolbarFixture toolbar = preview.getToolbar();
toolbar.requireTheme("@style/AppTheme");
toolbar.requireDevice("Nexus 4");
toolbar.requireOrientation("Portrait");
toolbar.chooseDevice("Nexus 5");
toolbar.toggleOrientation();
preview.waitForNextRenderToFinish();
toolbar.requireOrientation("Landscape");
toolbar.requireDevice("Nexus 5");
toolbar.createLandscapeVariation();
preview.waitForNextRenderToFinish();
preview.requireRenderSuccessful();
editor.requireFolderName("layout-land");
toolbar.requireOrientation("Landscape");
toolbar.toggleOrientation();
preview.waitForNextRenderToFinish();
toolbar.requireOrientation("Portrait");
// We should have switched back to the first file again since -land doesn't match portrait
editor.requireFolderName("layout");
toolbar.createOtherVariation("layout-v17");
preview.waitForNextRenderToFinish();
preview.requireRenderSuccessful();
editor.requireFolderName("layout-v17");
toolbar.requireDevice("Nexus 5"); // The device shouldn't have changed.
toolbar.toggleOrientation();
preview.waitForNextRenderToFinish();
preview.requireRenderSuccessful();
toolbar.requireDevice("Nexus 5"); // We should still be using the same device.
toolbar.requireOrientation("Landscape");
// The file should have switched to layout-land.
editor.requireFolderName("layout-land");
toolbar.toggleOrientation();
preview.waitForNextRenderToFinish();
preview.requireRenderSuccessful();
toolbar.requireDevice("Nexus 5");
toolbar.requireOrientation("Portrait");
editor.requireFolderName("layout");
toolbar.chooseDevice("Nexus 4");
preview.waitForNextRenderToFinish();
preview.requireRenderSuccessful();
// We should still be in the same file.
editor.requireFolderName("layout");
}
@Test
@Ignore // TODO: Remove when issue 76009 is fixed.
@IdeGuiTest(closeProjectBeforeExecution = true)
public void testPreviewConfigurationTweaks() throws Exception {
IdeFrameFixture projectFrame = openSimpleApplication();
EditorFixture editor = projectFrame.getEditor();
editor.open("app/src/main/res/layout/activity_my.xml", EditorFixture.Tab.EDITOR);
editor.requireFolderName("layout");
LayoutPreviewFixture preview = editor.getLayoutPreview(true);
assertNotNull(preview);
preview.waitForNextRenderToFinish();
preview.requireRenderSuccessful();
ConfigurationToolbarFixture toolbar = preview.getToolbar();
String rtl = "RTL";
String original = "Original";
List<String> rtlList = Collections.singletonList(rtl);
List<String> originalList = Collections.singletonList(original);
toolbar.chooseLocale("Preview Right-to-Left Layout");
preview.waitForNextRenderToFinish();
preview.requirePreviewTitles(rtlList);
for (int i = 0; i < 2; i++) {
preview.switchToPreview(rtl);
preview.waitForNextRenderToFinish();
preview.requirePreviewTitles(originalList);
preview.switchToPreview(original);
preview.waitForNextRenderToFinish();
preview.requirePreviewTitles(rtlList);
}
toolbar.removePreviews();
}
@Test
@IdeGuiTest(closeProjectBeforeExecution = true)
public void testEdits() throws Exception {
IdeFrameFixture projectFrame = openSimpleApplication();
// Load layout, wait for render to be shown in the preview window
EditorFixture editor = projectFrame.getEditor();
editor.open("app/src/main/res/layout/activity_my.xml", EditorFixture.Tab.DESIGN);
LayoutPreviewFixture preview = editor.getLayoutPreview(true);
assertNotNull(preview);
editor.requireName("activity_my.xml");
preview.waitForRenderToFinish();
// Move caret to right after the end of the <TextView> element declaration
editor.moveTo(editor.findOffset("android:layout_height=\"wrap_content\" />", null, true));
assertEquals(" <TextView\n" +
" android:text=\"@string/hello_world\"\n" +
" android:layout_width=\"wrap_content\"\n" +
" android:layout_height=\"wrap_content\" />^\n" +
"\n" +
"</RelativeLayout>\n", editor.getCurrentLineContents(false, true, 3));
// Then enter some text, which should trigger render warnings:
editor.enterText("\n <Button android:text=\"New Button\"/>\n");
preview.waitForNextRenderToFinish();
RenderErrorPanelFixture renderErrors = preview.getRenderErrors();
renderErrors.requireHaveRenderError("One or more layouts are missing the layout_width or layout_height attributes");
// Invoke one of the suggested fixes and make sure the XML is updated properly
renderErrors.performSuggestion("Automatically add all missing attributes");
assertEquals(" android:layout_height=\"wrap_content\" />\n" +
" <Button android:text=\"New Button\"\n" +
" android:layout_width=\"wrap_content\"\n" +
" android:layout_height=\"wrap_content\" />\n" +
" ^\n" +
"\n" +
"</RelativeLayout>\n", editor.getCurrentLineContents(false, true, 4));
// And now it should re-render and be successful again
preview.waitForNextRenderToFinish();
preview.requireRenderSuccessful();
}
@Test
@IdeGuiTest(closeProjectBeforeExecution = true)
public void testConfigurationMatching() throws Exception {
// Opens the LayoutTest project, opens a layout with a custom view, checks
// that it can't render yet (because the project hasn't been built),
// builds the project, checks that the render works, edits the custom view
// source code, ensures that the render lists the custom view as out of date,
// applies the suggested fix to build the project, and finally asserts that the
// build is now successful.
IdeFrameFixture projectFrame = openProject("LayoutTest");
EditorFixture editor = projectFrame.getEditor();
editor.open("app/src/main/res/layout/layout2.xml", EditorFixture.Tab.EDITOR);
LayoutPreviewFixture preview = editor.getLayoutPreview(true);
assertNotNull(preview);
ConfigurationToolbarFixture toolbar = preview.getToolbar();
toolbar.chooseDevice("Nexus 5");
preview.waitForNextRenderToFinish();
toolbar.requireDevice("Nexus 5");
editor.requireFolderName("layout");
toolbar.requireOrientation("Portrait");
toolbar.chooseDevice("Nexus 7");
preview.waitForNextRenderToFinish();
toolbar.requireDevice("Nexus 7 2013");
editor.requireFolderName("layout-sw600dp");
toolbar.chooseDevice("Nexus 10");
preview.waitForNextRenderToFinish();
toolbar.requireDevice("Nexus 10");
editor.requireFolderName("layout-sw600dp");
toolbar.requireOrientation("Landscape"); // Default orientation for Nexus 10
editor.open("app/src/main/res/layout/activity_my.xml", EditorFixture.Tab.EDITOR);
preview.waitForNextRenderToFinish();
toolbar.requireDevice("Nexus 10"); // Since we switched to it most recently
toolbar.requireOrientation("Portrait");
toolbar.chooseDevice("Nexus 7");
preview.waitForNextRenderToFinish();
toolbar.chooseDevice("Nexus 4");
preview.waitForNextRenderToFinish();
editor.open("app/src/main/res/layout-sw600dp/layout2.xml", EditorFixture.Tab.EDITOR);
preview.waitForNextRenderToFinish();
editor.requireFolderName("layout-sw600dp");
toolbar.requireDevice("Nexus 7 2013"); // because it's the most recently configured sw600-dp compatible device
editor.open("app/src/main/res/layout/layout2.xml", EditorFixture.Tab.EDITOR);
preview.waitForNextRenderToFinish();
toolbar.requireDevice("Nexus 4"); // because it's the most recently configured small screen compatible device
}
@NotNull
private String suggestName(EditorFixture editor) {
String currentFileName = editor.getCurrentFileName();
assertNotNull(currentFileName);
String prefix = this.getClass().getSimpleName() + "-" + getTestName();
String pngFileName = LintUtils.getBaseName(currentFileName) + DOT_PNG;
return THUMBNAIL_FOLDER + "/" + prefix + "-" + pngFileName;
}
@Test
@IdeGuiTest(closeProjectBeforeExecution = true)
public void testRendering() throws Exception {
// Opens a number of layouts in the layout test project and checks that the rendering looks roughly
// correct.
IdeFrameFixture projectFrame = openProject("LayoutTest");
EditorFixture editor = projectFrame.getEditor();
editor.open("app/src/main/res/layout/widgets.xml", EditorFixture.Tab.EDITOR);
LayoutPreviewFixture preview = editor.getLayoutPreview(true);
assertNotNull(preview);
ConfigurationToolbarFixture toolbar = preview.getToolbar();
preview.waitForNextRenderToFinish();
if (!toolbar.isDevice("Nexus 5")) {
toolbar.chooseDevice("Nexus 5");
preview.waitForNextRenderToFinish();
}
// Basic layout, including action bar, device frame, Holo-based app theme
preview.requireThumbnailMatch(suggestName(editor));
//// Drawable shape
editor.open("app/src/main/res/drawable/progress.xml", EditorFixture.Tab.EDITOR);
preview.waitForNextRenderToFinish();
preview.requireThumbnailMatch(suggestName(editor));
// Using an included layout, and various gradients and text styles
editor.open("app/src/main/res/layout/textstyles.xml", EditorFixture.Tab.EDITOR);
preview.waitForNextRenderToFinish();
preview.requireThumbnailMatch(suggestName(editor));
// Included render: inner layout rendered inside an outer layout
editor.open("app/src/main/res/layout/included.xml", EditorFixture.Tab.EDITOR);
preview.waitForNextRenderToFinish();
preview.requireThumbnailMatch(suggestName(editor));
// Render menu
editor.open("app/src/main/res/menu/my.xml", EditorFixture.Tab.EDITOR);
preview.waitForNextRenderToFinish();
preview.requireThumbnailMatch(suggestName(editor));
// TODO:
// Disabling device frames
// Multi configuration editing
// Material design rendering
// Theme switching
// Menu rendering
// Android Wear, Android TV form factor rendering
// Switching rendering targets
// Editing resource files and making sure style updates
// RTL rendering
// ScrollViews (no device clipping)
}
@Test
@IdeGuiTest(closeProjectBeforeExecution = true)
public void testEditCustomView() throws Exception {
// Opens the LayoutTest project, opens a layout with a custom view, checks
// that it can't render yet (because the project hasn't been built),
// builds the project, checks that the render works, edits the custom view
// source code, ensures that the render lists the custom view as out of date,
// applies the suggested fix to build the project, and finally asserts that the
// build is now successful.
IdeFrameFixture projectFrame = openProject("LayoutTest");
EditorFixture editor = projectFrame.getEditor();
editor.open("app/src/main/res/layout/layout1.xml", EditorFixture.Tab.EDITOR);
LayoutPreviewFixture preview = editor.getLayoutPreview(true);
assertNotNull(preview);
preview.waitForNextRenderToFinish();
String viewClassFile = "app/build/intermediates/classes/debug/com/android/tools/tests/layout/MyButton.class";
if (projectFrame.findFileByRelativePath(viewClassFile, false) != null) {
fail("Project should be clean at the start of this test; when that is not the case it's probably " +
"some caching of loaded projects in tools/adt/idea/android/testData/guiTests/newProjects which " +
"we will soon get rid of.");
}
RenderErrorPanelFixture renderErrors = preview.getRenderErrors();
renderErrors.requireHaveRenderError("The following classes could not be found");
renderErrors.requireHaveRenderError("com.android.tools.tests.layout.MyButton");
renderErrors.requireHaveRenderError("Change to android.widget.Button");
GradleInvocationResult result = projectFrame.invokeProjectMake();
assertTrue(result.isBuildSuccessful());
// Build completion should trigger re-render
preview.waitForNextRenderToFinish();
preview.requireRenderSuccessful();
// Next let's edit the custom view source file
editor.open("app/src/main/java/com/android/tools/tests/layout/MyButton.java", EditorFixture.Tab.EDITOR);
editor.moveTo(editor.findOffset("extends Button {", null, true));
editor.enterText(" // test");
// Switch back; should trigger render:
editor.open("app/src/main/res/layout/layout1.xml", EditorFixture.Tab.EDITOR);
preview.waitForNextRenderToFinish();
renderErrors.requireHaveRenderError("The MyButton custom view has been edited more recently than the last build");
renderErrors.performSuggestion("Build");
preview.waitForNextRenderToFinish();
preview.requireRenderSuccessful();
}
}