blob: dcc2bdf37dedbdc60a3ac378c5335c92af54a9ab [file] [log] [blame]
/*
* Copyright (C) 2019 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 android.databinding.tool.writer
import android.databinding.tool.LayoutResourceRule
import android.databinding.tool.assert
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.fail
import org.junit.Rule
import org.junit.Test
class ViewBinderGenerateJavaTest {
@get:Rule val layouts = LayoutResourceRule()
@Test fun nullableFieldsJavadocTheirConfigurations() {
layouts.write("example", "layout", """
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:id="@+id/name" />
</LinearLayout>
""".trimIndent())
layouts.write("example", "layout-sw600dp", """
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:id="@+id/name" />
</LinearLayout>
""".trimIndent())
layouts.write("example", "layout-land", """
<LinearLayout />
""".trimIndent())
val model = layouts.parse().getValue("example")
model.toViewBinder().toJavaFile().assert {
contains("""
| /**
| * This binding is not available in all configurations.
| * <p>
| * Present:
| * <ul>
| * <li>layout/</li>
| * <li>layout-sw600dp/</li>
| * </ul>
| *
| * Absent:
| * <ul>
| * <li>layout-land/</li>
| * </ul>
| */
| @Nullable
| public final TextView name;
""".trimMargin())
}
}
@Test fun zeroBindingsDoesNotGenerateErrorHandling() {
layouts.write("example", "layout", "<View />")
val model = layouts.parse().getValue("example")
model.toViewBinder().toJavaFile().assert {
parsesAs("""
|package com.example.databinding;
|
|import android.view.LayoutInflater;
|import android.view.View;
|import android.view.ViewGroup;
|import androidx.annotation.NonNull;
|import androidx.annotation.Nullable;
|import androidx.viewbinding.ViewBinding;
|import com.example.R;
|import java.lang.NullPointerException;
|import java.lang.Override;
|
|public final class ExampleBinding implements ViewBinding {
| @NonNull
| private final View rootView;
|
| private ExampleBinding(@NonNull View rootView) {
| this.rootView = rootView;
| }
|
| @Override
| @NonNull
| public View getRoot() {
| return rootView;
| }
|
| @NonNull
| public static ExampleBinding inflate(@NonNull LayoutInflater inflater) {
| return inflate(inflater, null, false);
| }
|
| @NonNull
| public static ExampleBinding inflate(@NonNull LayoutInflater inflater,
| @Nullable ViewGroup parent, boolean attachToParent) {
| View root = inflater.inflate(R.layout.example, parent, false);
| if (attachToParent) {
| parent.addView(root);
| }
| return bind(root);
| }
|
| @NonNull
| public static ExampleBinding bind(@NonNull View rootView) {
| if (rootView == null) {
| throw new NullPointerException("rootView");
| }
| return new ExampleBinding(rootView);
| }
|}
""".trimMargin())
}
}
@Test fun allOptionalBindingsDoesNotGenerateErrorHandling() {
layouts.write("example", "layout", """
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:id="@+id/name" />
<TextView android:id="@+id/email" />
</LinearLayout>
""".trimIndent())
layouts.write("example", "layout-sw600dp", """
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:id="@+id/name" />
</LinearLayout>
""".trimIndent())
layouts.write("example", "layout-land", """
<LinearLayout />
""".trimIndent())
val model = layouts.parse().getValue("example")
model.toViewBinder().toJavaFile().assert {
parsesAs("""
|package com.example.databinding;
|
|import android.view.LayoutInflater;
|import android.view.View;
|import android.view.ViewGroup;
|import android.widget.LinearLayout;
|import android.widget.TextView;
|import androidx.annotation.NonNull;
|import androidx.annotation.Nullable;
|import androidx.viewbinding.ViewBinding;
|import com.example.R;
|import java.lang.Override;
|
|public final class ExampleBinding implements ViewBinding {
| @NonNull
| private final LinearLayout rootView;
|
| @Nullable
| public final TextView email;
|
| @Nullable
| public final TextView name;
|
| private ExampleBinding(@NonNull LinearLayout rootView, @Nullable TextView email,
| @Nullable TextView name) {
| this.rootView = rootView;
| this.email = email;
| this.name = name;
| }
|
| @Override
| @NonNull
| public LinearLayout getRoot() {
| return rootView;
| }
|
| @NonNull
| public static ExampleBinding inflate(@NonNull LayoutInflater inflater) {
| return inflate(inflater, null, false);
| }
|
| @NonNull
| public static ExampleBinding inflate(@NonNull LayoutInflater inflater,
| @Nullable ViewGroup parent, boolean attachToParent) {
| View root = inflater.inflate(R.layout.example, parent, false);
| if (attachToParent) {
| parent.addView(root);
| }
| return bind(root);
| }
|
| @NonNull
| public static ExampleBinding bind(@NonNull View rootView) {
| TextView email = rootView.findViewById(R.id.email);
| TextView name = rootView.findViewById(R.id.name);
| return new ExampleBinding((LinearLayout) rootView, email, name);
| }
|}
""".trimMargin())
}
}
@Test fun bindingNameCollisions() {
layouts.write("other", "layout", "<FrameLayout/>")
layouts.write("example", "layout", """
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:id="@+id/root_view" />
<TextView android:id="@+id/missing_id" />
<View android:id="@+id/id" />
</LinearLayout>
""".trimIndent())
val model = layouts.parse().getValue("example")
model.toViewBinder().toJavaFile().assert {
parsesAs("""
|package com.example.databinding;
|
|import android.view.LayoutInflater;
|import android.view.View;
|import android.view.ViewGroup;
|import android.widget.LinearLayout;
|import android.widget.TextView;
|import androidx.annotation.NonNull;
|import androidx.annotation.Nullable;
|import androidx.viewbinding.ViewBinding;
|import com.example.R;
|import java.lang.NullPointerException;
|import java.lang.Override;
|import java.lang.String;
|
|public final class ExampleBinding implements ViewBinding {
| @NonNull
| private final LinearLayout rootView_;
|
| @NonNull
| public final View id;
|
| @NonNull
| public final TextView missingId;
|
| @NonNull
| public final TextView rootView;
|
| private ExampleBinding(@NonNull LinearLayout rootView_, @NonNull View id,
| @NonNull TextView missingId, @NonNull TextView rootView) {
| this.rootView_ = rootView_;
| this.id = id;
| this.missingId = missingId;
| this.rootView = rootView;
| }
|
| @Override
| @NonNull
| public LinearLayout getRoot() {
| return rootView_;
| }
|
| @NonNull
| public static ExampleBinding inflate(@NonNull LayoutInflater inflater) {
| return inflate(inflater, null, false);
| }
|
| @NonNull
| public static ExampleBinding inflate(@NonNull LayoutInflater inflater,
| @Nullable ViewGroup parent, boolean attachToParent) {
| View root = inflater.inflate(R.layout.example, parent, false);
| if (attachToParent) {
| parent.addView(root);
| }
| return bind(root);
| }
|
| @NonNull
| public static ExampleBinding bind(@NonNull View rootView) {
| int id;
| missingId: {
| id = R.id.id;
| View id_ = rootView.findViewById(id);
| if (id_ == null) {
| break missingId;
| }
|
| id = R.id.missing_id;
| TextView missingId = rootView.findViewById(id);
| if (missingId == null) {
| break missingId;
| }
|
| id = R.id.root_view;
| TextView rootView_ = rootView.findViewById(id);
| if (rootView_ == null) {
| break missingId;
| }
|
| return new ExampleBinding((LinearLayout) rootView, id_, missingId,
| rootView_);
| }
|
| String missingId_ = rootView.getResources().getResourceName(id);
| throw new NullPointerException(
| "Missing required view with ID: ".concat(missingId_));
| }
|}
""".trimMargin())
}
}
@Test fun ignoreLayoutTruthyValues() {
layouts.write("example1", "layout", """
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
/>
""".trimIndent())
layouts.write("example2", "layout", """
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="TRUE"
/>
""".trimIndent())
layouts.write("example3", "layout", """
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="tRuE"
/>
""".trimIndent())
assertThat(layouts.parse()).apply {
doesNotContainKey("example1")
doesNotContainKey("example2")
doesNotContainKey("example3")
}
}
@Test fun ignoreLayoutFalseyValues() {
layouts.write("example1", "layout", """
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="false"
/>
""".trimIndent())
layouts.write("example2", "layout", """
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="yes"
/>
""".trimIndent())
layouts.write("example3", "layout", """
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore=" true "
/>
""".trimIndent())
layouts.write("example4", "layout", """
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore=""
/>
""".trimIndent())
assertThat(layouts.parse()).apply {
containsKey("example1")
containsKey("example2")
containsKey("example3")
containsKey("example4")
}
}
@Test fun ignoreLayoutSingleConfiguration() {
layouts.write("example", "layout", """
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:id="@+id/name" />
</LinearLayout>
""".trimIndent())
layouts.write("example", "layout-land", """
<LinearLayout
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
/>
""".trimIndent())
val model = layouts.parse().getValue("example")
// This would create a @Nullable field if the second layout was parsed.
model.toViewBinder().toJavaFile().assert {
contains("""
| @NonNull
| public final TextView name;
""".trimMargin())
}
}
@Test fun mergeRemovesSingleArgumentInflateAndAttachParam() {
layouts.write("example", "layout", "<merge/>")
val model = layouts.parse().getValue("example")
model.toViewBinder().toJavaFile().assert {
parsesAs("""
|package com.example.databinding;
|
|import android.view.LayoutInflater;
|import android.view.View;
|import android.view.ViewGroup;
|import androidx.annotation.NonNull;
|import androidx.viewbinding.ViewBinding;
|import com.example.R;
|import java.lang.NullPointerException;
|import java.lang.Override;
|
|public final class ExampleBinding implements ViewBinding {
| @NonNull
| private final View rootView;
|
| private ExampleBinding(@NonNull View rootView) {
| this.rootView = rootView;
| }
|
| @Override
| @NonNull
| public View getRoot() {
| return rootView;
| }
|
| @NonNull
| public static ExampleBinding inflate(@NonNull LayoutInflater inflater,
| @NonNull ViewGroup parent) {
| if (parent == null) {
| throw new NullPointerException("parent");
| }
| inflater.inflate(R.layout.example, parent);
| return bind(parent);
| }
|
| @NonNull
| public static ExampleBinding bind(@NonNull View rootView) {
| if (rootView == null) {
| throw new NullPointerException("rootView");
| }
| return new ExampleBinding(rootView);
| }
|}
""".trimMargin())
}
}
@Test fun configurationsMustAgreeOnRootMergeTag() {
layouts.write("example", "layout", "<merge/>")
layouts.write("example", "layout-land", "<FrameLayout/>")
layouts.write("example", "layout-sw600dp", "<FrameLayout/>")
val model = layouts.parse().getValue("example")
try {
model.toViewBinder()
fail()
} catch (e: IllegalStateException) {
assertThat(e).hasMessageThat().isEqualTo("""
Configurations for example.xml must agree on the use of a root <merge> tag.
Present:
- layout
Absent:
- layout-sw600dp
- layout-land
""".trimIndent()
)
}
}
@Test fun matchingRootViewsGetCovariantRootReturnType() {
layouts.write("example", "layout", "<LinearLayout/>")
layouts.write("example", "layout-land", "<LinearLayout/>")
val model = layouts.parse().getValue("example")
model.toViewBinder().toJavaFile().assert {
contains("""
| public LinearLayout getRoot() {
""".trimMargin())
}
}
@Test fun matchingRootViewsWithDifferentDeclarationsGetCovariantRootReturnType() {
layouts.write("example", "layout", "<LinearLayout/>")
layouts.write("example", "layout-land", "<android.widget.LinearLayout/>")
layouts.write("example", "layout-sw600dp", """<view class="android.widget.LinearLayout"/>""")
val model = layouts.parse().getValue("example")
model.toViewBinder().toJavaFile().assert {
contains("""
| public LinearLayout getRoot() {
""".trimMargin())
}
}
@Test fun conflictingRootViewsDoNotGetCovariantRootReturnType() {
layouts.write("example", "layout", "<LinearLayout/>")
layouts.write("example", "layout-land", "<FrameLayout/>")
val model = layouts.parse().getValue("example")
model.toViewBinder().toJavaFile().assert {
contains("""
| public View getRoot() {
""".trimMargin())
}
}
@Test fun mergeRootViewsDoNotGetCovariantRootReturnType() {
layouts.write("example", "layout", """
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:id="@+id/name" />
</merge>
""".trimIndent())
val model = layouts.parse().getValue("example")
model.toViewBinder().toJavaFile().assert {
contains("""
| public View getRoot() {
""".trimMargin())
}
}
@Test fun optionalIncludeConditionallyCallsBind() {
layouts.write("other", "layout", "<FrameLayout/>")
layouts.write("example", "layout", """
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android">
<include
android:id="@+id/other"
layout="@layout/other"
/>
</FrameLayout>
""".trimIndent())
layouts.write("example", "layout-land", "<FrameLayout/>")
val model = layouts.parse().getValue("example")
model.toViewBinder().toJavaFile().assert {
contains("""
| View other = rootView.findViewById(R.id.other);
| OtherBinding binding_other = other != null
| ? OtherBinding.bind(other)
| : null;
""".trimMargin())
}
}
@Test fun rootNodeAgreeingOnIdDoesNotCallFindViewById() {
layouts.write("example", "layout", """
<View
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
/>
""".trimIndent())
val model = layouts.parse().getValue("example")
model.toViewBinder().toJavaFile().assert {
contains("View root = rootView;")
}
}
@Test fun rootNodeAgreeingOnIdDoesNotCallFindViewByIdAndCastsWhenNeeded() {
layouts.write("example", "layout", """
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
/>
""".trimIndent())
val model = layouts.parse().getValue("example")
model.toViewBinder().toJavaFile().assert {
contains("FrameLayout root = (FrameLayout) rootView;")
}
}
@Test fun rootNodeDisagreeingOnIdFails() {
layouts.write("example", "layout", """
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/one"
/>
""".trimIndent())
layouts.write("example", "layout-land", """
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/two"
/>
""".trimIndent())
val model = layouts.parse().getValue("example")
try {
model.toViewBinder()
} catch (e: IllegalStateException) {
assertThat(e).hasMessageThat().isEqualTo("""
Configurations for example.xml must agree on the root element's ID.
@+id/one:
- layout
@+id/two:
- layout-land
""".trimIndent()
)
}
}
@Test fun rootNodePartialIdFails() {
layouts.write("example", "layout", """
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/partial"
/>
""".trimIndent())
layouts.write("example", "layout-land", "<FrameLayout/>")
val model = layouts.parse().getValue("example")
try {
model.toViewBinder()
fail()
} catch (e: IllegalStateException) {
assertThat(e).hasMessageThat().isEqualTo("""
Configurations for example.xml must agree on the root element's ID.
Missing ID:
- layout-land
@+id/partial:
- layout
""".trimIndent()
)
}
}
@Test fun invalidNodeNameFailsWithNiceMessage() {
layouts.write("example", "layout", """
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"/>
""".trimIndent())
val model = layouts.parse().getValue("example")
try {
model.toViewBinder()
fail()
} catch (e: IllegalArgumentException) {
assertThat(e).hasMessageThat()
.isEqualTo("Unable to parse \"android.widget.layer-list\" as class in example.xml")
}
}
@Test fun fragmentNodesAreNotExposed() {
layouts.write("as_root", "layout", """
<fragment
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
""".trimIndent())
layouts.write("as_child", "layout", """
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
<View
android:id="@+id/one"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<fragment
class="android.app.Fragment"
android:id="@+id/two"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>
""".trimIndent())
val fragmentAsRootModel = layouts.parse().getValue("as_root")
fragmentAsRootModel.toViewBinder().toJavaFile().assert {
contains("View getRoot()")
doesNotContain("fragment")
}
val fragmentAsChildModel = layouts.parse().getValue("as_child")
fragmentAsChildModel.toViewBinder().toJavaFile().assert {
contains("one")
doesNotContain("two")
}
}
}