diff --git a/butterknife-integration-test/build.gradle b/butterknife-integration-test/build.gradle index 5b9e1bb4b..4358fda8d 100644 --- a/butterknife-integration-test/build.gradle +++ b/butterknife-integration-test/build.gradle @@ -63,6 +63,7 @@ dependencies { codegenImplementation project(':butterknife') codegenAnnotationProcessor project(':butterknife-compiler') + androidTestCodegenAnnotationProcessor project(':butterknife-compiler') androidTestImplementation deps.junit androidTestImplementation deps.truth diff --git a/butterknife-integration-test/src/androidTest/java/com/example/butterknife/functional/OnClickTest.java b/butterknife-integration-test/src/androidTest/java/com/example/butterknife/functional/OnClickTest.java new file mode 100644 index 000000000..8099272e3 --- /dev/null +++ b/butterknife-integration-test/src/androidTest/java/com/example/butterknife/functional/OnClickTest.java @@ -0,0 +1,270 @@ +package com.example.butterknife.functional; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.TextView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.Optional; +import butterknife.Unbinder; +import com.example.butterknife.BuildConfig; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assume.assumeFalse; + +@SuppressWarnings("unused") // Used reflectively / by code gen. +public final class OnClickTest { + static final class Simple { + int clicks = 0; + + @OnClick(1) void click() { + clicks++; + } + } + + @Test public void simple() { + View tree = treeWithIds(1); + View view1 = tree.findViewById(1); + + Simple target = new Simple(); + Unbinder unbinder = ButterKnife.bind(target, tree); + assertEquals(0, target.clicks); + + view1.performClick(); + assertEquals(1, target.clicks); + + unbinder.unbind(); + view1.performClick(); + assertEquals(1, target.clicks); + } + + static final class MultipleBindings { + int clicks = 0; + + @OnClick(1) void click1() { + clicks++; + } + + @OnClick(1) void clicks2() { + clicks++; + } + } + + @Test public void multipleBindings() { + assumeFalse("Not implemented", BuildConfig.FLAVOR.equals("reflect")); // TODO + + View tree = treeWithIds(1); + View view1 = tree.findViewById(1); + + MultipleBindings target = new MultipleBindings(); + Unbinder unbinder = ButterKnife.bind(target, tree); + assertEquals(0, target.clicks); + + view1.performClick(); + assertEquals(2, target.clicks); + + unbinder.unbind(); + view1.performClick(); + assertEquals(2, target.clicks); + } + + static final class Visibilities { + int clicks = 0; + + @OnClick(1) public void publicClick() { + clicks++; + } + + @OnClick(2) void packageClick() { + clicks++; + } + + @OnClick(3) protected void protectedClick() { + clicks++; + } + } + + @Test public void visibilities() { + View tree = treeWithIds(1, 2, 3); + View view1 = tree.findViewById(1); + View view2 = tree.findViewById(2); + View view3 = tree.findViewById(3); + + Visibilities target = new Visibilities(); + ButterKnife.bind(target, tree); + assertEquals(0, target.clicks); + + view1.performClick(); + assertEquals(1, target.clicks); + + view2.performClick(); + assertEquals(2, target.clicks); + + view3.performClick(); + assertEquals(3, target.clicks); + } + + static final class MultipleIds { + int clicks = 0; + + @OnClick({1, 2}) void click() { + clicks++; + } + } + + @Test public void multipleIds() { + View tree = treeWithIds(1, 2); + View view1 = tree.findViewById(1); + View view2 = tree.findViewById(2); + + MultipleIds target = new MultipleIds(); + Unbinder unbinder = ButterKnife.bind(target, tree); + assertEquals(0, target.clicks); + + view1.performClick(); + assertEquals(1, target.clicks); + + view2.performClick(); + assertEquals(2, target.clicks); + + unbinder.unbind(); + view1.performClick(); + view2.performClick(); + assertEquals(2, target.clicks); + } + + static final class OptionalId { + int clicks = 0; + + @Optional @OnClick(1) public void click() { + clicks++; + } + } + + @Test public void optionalIdPresent() { + View tree = treeWithIds(1); + View view1 = tree.findViewById(1); + + OptionalId target = new OptionalId(); + Unbinder unbinder = ButterKnife.bind(target, tree); + assertEquals(0, target.clicks); + + view1.performClick(); + assertEquals(1, target.clicks); + + unbinder.unbind(); + view1.performClick(); + assertEquals(1, target.clicks); + } + + @Test public void optionalIdAbsent() { + View tree = treeWithIds(2); + View view2 = tree.findViewById(2); + + OptionalId target = new OptionalId(); + Unbinder unbinder = ButterKnife.bind(target, tree); + assertEquals(0, target.clicks); + + view2.performClick(); + assertEquals(0, target.clicks); + + unbinder.unbind(); + view2.performClick(); + assertEquals(0, target.clicks); + } + + static final class ArgumentCast { + interface MyInterface {} + + View last; + + @OnClick(1) void clickView(View view) { + last = view; + } + + @OnClick(2) void clickTextView(TextView view) { + last = view; + } + + @OnClick(3) void clickButton(Button view) { + last = view; + } + + @OnClick(4) void clickMyInterface(MyInterface view) { + last = (View) view; + } + } + + @Test public void argumentCast() { + class MyView extends Button implements ArgumentCast.MyInterface { + MyView(Context context) { + super(context); + } + + @Override public boolean post(Runnable action) { + // Because of DebouncingOnClickListener, we run any posted Runnables synchronously. + action.run(); + return true; + } + } + + View view1 = new MyView(InstrumentationRegistry.getContext()); + view1.setId(1); + View view2 = new MyView(InstrumentationRegistry.getContext()); + view2.setId(2); + View view3 = new MyView(InstrumentationRegistry.getContext()); + view3.setId(3); + View view4 = new MyView(InstrumentationRegistry.getContext()); + view4.setId(4); + ViewGroup tree = new FrameLayout(InstrumentationRegistry.getContext()); + tree.addView(view1); + tree.addView(view2); + tree.addView(view3); + tree.addView(view4); + + ArgumentCast target = new ArgumentCast(); + ButterKnife.bind(target, tree); + + view1.performClick(); + assertSame(view1, target.last); + + view2.performClick(); + assertSame(view2, target.last); + + view3.performClick(); + assertSame(view3, target.last); + + view4.performClick(); + assertSame(view4, target.last); + } + + private static View treeWithIds(int... ids) { + Context context = InstrumentationRegistry.getContext(); + FrameLayout group = new FrameLayout(context); + + class SuperGrossView extends View { + SuperGrossView(Context context) { + super(context); + } + + @Override public boolean post(Runnable action) { + // Because of DebouncingOnClickListener, we run any posted Runnables synchronously. + action.run(); + return true; + } + } + + for (int id : ids) { + View view = new SuperGrossView(context); + view.setId(id); + group.addView(view); + } + return group; + } +} diff --git a/butterknife-integration-test/src/main/proguard.pro b/butterknife-integration-test/src/main/proguard.pro index 2893c55e9..c3054308d 100644 --- a/butterknife-integration-test/src/main/proguard.pro +++ b/butterknife-integration-test/src/main/proguard.pro @@ -3,6 +3,10 @@ # STUFF USED BY TESTS: +-keep class butterknife.internal.Utils { + ; +} + -keep class butterknife.Unbinder { void unbind(); } diff --git a/butterknife-runtime/src/test/java/butterknife/OnClickTest.java b/butterknife-runtime/src/test/java/butterknife/OnClickTest.java index 9d06efce0..324f727e4 100644 --- a/butterknife-runtime/src/test/java/butterknife/OnClickTest.java +++ b/butterknife-runtime/src/test/java/butterknife/OnClickTest.java @@ -9,182 +9,6 @@ import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource; public class OnClickTest { - @Test public void onClickBinding() { - JavaFileObject source = JavaFileObjects.forSourceString("test.Test", "" - + "package test;\n" - + "import butterknife.OnClick;\n" - + "public class Test {\n" - + " @OnClick(1) void doStuff() {}\n" - + "}" - ); - - JavaFileObject bindingSource = JavaFileObjects.forSourceString("test/Test_ViewBinding", "" - + "package test;\n" - + "import android.support.annotation.CallSuper;\n" - + "import android.support.annotation.UiThread;\n" - + "import android.view.View;\n" - + "import butterknife.Unbinder;\n" - + "import butterknife.internal.DebouncingOnClickListener;\n" - + "import butterknife.internal.Utils;\n" - + "import java.lang.IllegalStateException;\n" - + "import java.lang.Override;\n" - + "public class Test_ViewBinding implements Unbinder {\n" - + " private Test target;\n" - + " private View view1;\n" - + " @UiThread\n" - + " public Test_ViewBinding(final Test target, View source) {\n" - + " this.target = target;\n" - + " View view;\n" - + " view = Utils.findRequiredView(source, 1, \"method 'doStuff'\");\n" - + " view1 = view;\n" - + " view.setOnClickListener(new DebouncingOnClickListener() {\n" - + " @Override\n" - + " public void doClick(View p0) {\n" - + " target.doStuff();\n" - + " }\n" - + " });\n" - + " }\n" - + " @Override\n" - + " @CallSuper\n" - + " public void unbind() {\n" - + " if (target == null) throw new IllegalStateException(\"Bindings already cleared.\");\n" - + " target = null;\n" - + " view1.setOnClickListener(null);\n" - + " view1 = null;\n" - + " }\n" - + "}" - ); - - assertAbout(javaSource()).that(source) - .withCompilerOptions("-Xlint:-processing") - .processedWith(new ButterKnifeProcessor()) - .compilesWithoutWarnings() - .and() - .generatesSources(bindingSource); - } - - @Test public void onClickBindingFinalType() { - JavaFileObject source = JavaFileObjects.forSourceString("test.Test", "" - + "package test;\n" - + "import butterknife.OnClick;\n" - + "public final class Test {\n" - + " @OnClick(1) void doStuff() {}\n" - + "}" - ); - - JavaFileObject bindingSource = JavaFileObjects.forSourceString("test/Test_ViewBinding", "" - + "package test;\n" - + "import android.support.annotation.UiThread;\n" - + "import android.view.View;\n" - + "import butterknife.Unbinder;\n" - + "import butterknife.internal.DebouncingOnClickListener;\n" - + "import butterknife.internal.Utils;\n" - + "import java.lang.IllegalStateException;\n" - + "import java.lang.Override;\n" - + "public final class Test_ViewBinding implements Unbinder {\n" - + " private Test target;\n" - + " private View view1;\n" - + " @UiThread\n" - + " public Test_ViewBinding(final Test target, View source) {\n" - + " this.target = target;\n" - + " View view;\n" - + " view = Utils.findRequiredView(source, 1, \"method 'doStuff'\");\n" - + " view1 = view;\n" - + " view.setOnClickListener(new DebouncingOnClickListener() {\n" - + " @Override\n" - + " public void doClick(View p0) {\n" - + " target.doStuff();\n" - + " }\n" - + " });\n" - + " }\n" - + " @Override\n" - + " public void unbind() {\n" - + " if (target == null) throw new IllegalStateException(\"Bindings already cleared.\");\n" - + " target = null;\n" - + " view1.setOnClickListener(null);\n" - + " view1 = null;\n" - + " }\n" - + "}" - ); - - assertAbout(javaSource()).that(source) - .withCompilerOptions("-Xlint:-processing") - .processedWith(new ButterKnifeProcessor()) - .compilesWithoutWarnings() - .and() - .generatesSources(bindingSource); - } - - @Test public void onClickMultipleBindings() { - JavaFileObject source = JavaFileObjects.forSourceString("test.Test", "" - + "package test;\n" - + "import android.view.View;\n" - + "import butterknife.OnClick;\n" - + "public class Test {\n" - + " @OnClick(1) void doStuff1() {}\n" - + " @OnClick(1) void doStuff2() {}\n" - + " @OnClick({1, 2}) void doStuff3(View v) {}\n" - + "}" - ); - - JavaFileObject bindingSource = JavaFileObjects.forSourceString("test/Test_ViewBinding", "" - + "package test;\n" - + "import android.support.annotation.CallSuper;\n" - + "import android.support.annotation.UiThread;\n" - + "import android.view.View;\n" - + "import butterknife.Unbinder;\n" - + "import butterknife.internal.DebouncingOnClickListener;\n" - + "import butterknife.internal.Utils;\n" - + "import java.lang.IllegalStateException;\n" - + "import java.lang.Override;\n" - + "public class Test_ViewBinding implements Unbinder {\n" - + " private Test target;\n" - + " private View view1;\n" - + " private View view2;\n" - + " @UiThread\n" - + " public Test_ViewBinding(final Test target, View source) {\n" - + " this.target = target;\n" - + " View view;\n" - + " view = Utils.findRequiredView(source, 1, \"method 'doStuff1', method 'doStuff2', and method 'doStuff3'\");\n" - + " view1 = view;\n" - + " view.setOnClickListener(new DebouncingOnClickListener() {\n" - + " @Override\n" - + " public void doClick(View p0) {\n" - + " target.doStuff1();\n" - + " target.doStuff2();\n" - + " target.doStuff3(p0);\n" - + " }\n" - + " });\n" - + " view = Utils.findRequiredView(source, 2, \"method 'doStuff3'\");\n" - + " view2 = view;\n" - + " view.setOnClickListener(new DebouncingOnClickListener() {\n" - + " @Override\n" - + " public void doClick(View p0) {\n" - + " target.doStuff3(p0);\n" - + " }\n" - + " });\n" - + " }\n" - + " @Override\n" - + " @CallSuper\n" - + " public void unbind() {\n" - + " if (target == null) throw new IllegalStateException(\"Bindings already cleared.\");\n" - + " target = null;\n" - + " view1.setOnClickListener(null);\n" - + " view1 = null;\n" - + " view2.setOnClickListener(null);\n" - + " view2 = null;\n" - + " }\n" - + "}" - ); - - assertAbout(javaSource()).that(source) - .withCompilerOptions("-Xlint:-processing") - .processedWith(new ButterKnifeProcessor()) - .compilesWithoutWarnings() - .and() - .generatesSources(bindingSource); - } - @Test public void findOnlyCalledOnce() { JavaFileObject source = JavaFileObjects.forSourceString("test.Test", "" + "package test;\n" @@ -245,132 +69,6 @@ public class OnClickTest { .generatesSources(bindingSource); } - @Test public void methodVisibility() { - JavaFileObject source = JavaFileObjects.forSourceString("test.Test", "" - + "package test;\n" - + "import android.view.View;\n" - + "import butterknife.OnClick;\n" - + "public class Test {\n" - + " @OnClick(1) public void thing1() {}\n" - + " @OnClick(2) void thing2() {}\n" - + " @OnClick(3) protected void thing3() {}\n" - + "}" - ); - - assertAbout(javaSource()).that(source) - .withCompilerOptions("-Xlint:-processing") - .processedWith(new ButterKnifeProcessor()) - .compilesWithoutWarnings(); - } - - @Test public void methodCastsArgument() { - JavaFileObject source = JavaFileObjects.forSourceString("test.Test", "" - + "package test;\n" - + "import android.view.View;\n" - + "import android.widget.Button;\n" - + "import android.widget.TextView;\n" - + "import butterknife.OnClick;\n" - + "public class Test {\n" - + " interface TestInterface {}\n" - + " @OnClick(0) void click0() {}\n" - + " @OnClick(1) void click1(View view) {}\n" - + " @OnClick(2) void click2(TextView view) {}\n" - + " @OnClick(3) void click3(Button button) {}\n" - + " @OnClick(4) void click4(TestInterface thing) {}\n" - + "}" - ); - - JavaFileObject bindingSource = JavaFileObjects.forSourceString("test/Test_ViewBinding", "" - + "package test;\n" - + "import android.support.annotation.CallSuper;\n" - + "import android.support.annotation.UiThread;\n" - + "import android.view.View;\n" - + "import android.widget.Button;\n" - + "import android.widget.TextView;\n" - + "import butterknife.Unbinder;\n" - + "import butterknife.internal.DebouncingOnClickListener;\n" - + "import butterknife.internal.Utils;\n" - + "import java.lang.IllegalStateException;\n" - + "import java.lang.Override;\n" - + "public class Test_ViewBinding implements Unbinder {\n" - + " private Test target;\n" - + " private View view0;\n" - + " private View view1;\n" - + " private View view2;\n" - + " private View view3;\n" - + " private View view4;\n" - + " @UiThread\n" - + " public Test_ViewBinding(final Test target, View source) {\n" - + " this.target = target;\n" - + " View view;\n" - + " view = Utils.findRequiredView(source, 0, \"method 'click0'\");\n" - + " view0 = view;\n" - + " view.setOnClickListener(new DebouncingOnClickListener() {\n" - + " @Override\n" - + " public void doClick(View p0) {\n" - + " target.click0();\n" - + " }\n" - + " });\n" - + " view = Utils.findRequiredView(source, 1, \"method 'click1'\");\n" - + " view1 = view;\n" - + " view.setOnClickListener(new DebouncingOnClickListener() {\n" - + " @Override\n" - + " public void doClick(View p0) {\n" - + " target.click1(p0);\n" - + " }\n" - + " });\n" - + " view = Utils.findRequiredView(source, 2, \"method 'click2'\");\n" - + " view2 = view;\n" - + " view.setOnClickListener(new DebouncingOnClickListener() {\n" - + " @Override\n" - + " public void doClick(View p0) {\n" - + " target.click2(Utils.castParam(p0, \"doClick\", 0, \"click2\", 0, TextView.class));\n" - + " }\n" - + " });\n" - + " view = Utils.findRequiredView(source, 3, \"method 'click3'\");\n" - + " view3 = view;\n" - + " view.setOnClickListener(new DebouncingOnClickListener() {\n" - + " @Override\n" - + " public void doClick(View p0) {\n" - + " target.click3(Utils.castParam(p0, \"doClick\", 0, \"click3\", 0, Button.class));\n" - + " }\n" - + " });\n" - + " view = Utils.findRequiredView(source, 4, \"method 'click4'\");\n" - + " view4 = view;\n" - + " view.setOnClickListener(new DebouncingOnClickListener() {\n" - + " @Override\n" - + " public void doClick(View p0) {\n" - + " target.click4(Utils.castParam(p0, \"doClick\", 0, \"click4\", 0, Test.TestInterface.class));\n" - + " }\n" - + " });\n" - + " }\n" - + " @Override\n" - + " @CallSuper\n" - + " public void unbind() {\n" - + " if (target == null) throw new IllegalStateException(\"Bindings already cleared.\");\n" - + " target = null;\n" - + " view0.setOnClickListener(null);\n" - + " view0 = null;\n" - + " view1.setOnClickListener(null);\n" - + " view1 = null;\n" - + " view2.setOnClickListener(null);\n" - + " view2 = null;\n" - + " view3.setOnClickListener(null);\n" - + " view3 = null;\n" - + " view4.setOnClickListener(null);\n" - + " view4 = null;\n" - + " }\n" - + "}" - ); - - assertAbout(javaSource()).that(source) - .withCompilerOptions("-Xlint:-processing") - .processedWith(new ButterKnifeProcessor()) - .compilesWithoutWarnings() - .and() - .generatesSources(bindingSource); - } - @Test public void methodCastsArgumentNonDebuggable() { JavaFileObject source = JavaFileObjects.forSourceString("test.Test", "" + "package test;\n" @@ -478,140 +176,6 @@ public class OnClickTest { .generatesSources(bindingSource); } - @Test public void methodWithMultipleIds() { - JavaFileObject source = JavaFileObjects.forSourceString("test.Test", "" - + "package test;\n" - + "import android.view.View;\n" - + "import butterknife.OnClick;\n" - + "public class Test {\n" - + " @OnClick({1, 2, 3}) void click() {}\n" - + "}" - ); - - JavaFileObject bindingSource = JavaFileObjects.forSourceString("test/Test_ViewBinding", "" - + "package test;\n" - + "import android.support.annotation.CallSuper;\n" - + "import android.support.annotation.UiThread;\n" - + "import android.view.View;\n" - + "import butterknife.Unbinder;\n" - + "import butterknife.internal.DebouncingOnClickListener;\n" - + "import butterknife.internal.Utils;\n" - + "import java.lang.IllegalStateException;\n" - + "import java.lang.Override;\n" - + "public class Test_ViewBinding implements Unbinder {\n" - + " private Test target;\n" - + " private View view1;\n" - + " private View view2;\n" - + " private View view3;\n" - + " @UiThread\n" - + " public Test_ViewBinding(final Test target, View source) {\n" - + " this.target = target;\n" - + " View view;\n" - + " view = Utils.findRequiredView(source, 1, \"method 'click'\");\n" - + " view1 = view;\n" - + " view.setOnClickListener(new DebouncingOnClickListener() {\n" - + " @Override\n" - + " public void doClick(View p0) {\n" - + " target.click();\n" - + " }\n" - + " });\n" - + " view = Utils.findRequiredView(source, 2, \"method 'click'\");\n" - + " view2 = view;\n" - + " view.setOnClickListener(new DebouncingOnClickListener() {\n" - + " @Override\n" - + " public void doClick(View p0) {\n" - + " target.click();\n" - + " }\n" - + " });\n" - + " view = Utils.findRequiredView(source, 3, \"method 'click'\");\n" - + " view3 = view;\n" - + " view.setOnClickListener(new DebouncingOnClickListener() {\n" - + " @Override\n" - + " public void doClick(View p0) {\n" - + " target.click();\n" - + " }\n" - + " });\n" - + " }\n" - + " @Override\n" - + " @CallSuper\n" - + " public void unbind() {\n" - + " if (target == null) throw new IllegalStateException(\"Bindings already cleared.\");\n" - + " target = null;\n" - + " view1.setOnClickListener(null);\n" - + " view1 = null;\n" - + " view2.setOnClickListener(null);\n" - + " view2 = null;\n" - + " view3.setOnClickListener(null);\n" - + " view3 = null;\n" - + " }\n" - + "}" - ); - - assertAbout(javaSource()).that(source) - .withCompilerOptions("-Xlint:-processing") - .processedWith(new ButterKnifeProcessor()) - .compilesWithoutWarnings() - .and() - .generatesSources(bindingSource); - } - - @Test public void nullable() { - JavaFileObject source = JavaFileObjects.forSourceString("test.Test", "" - + "package test;\n" - + "import butterknife.OnClick;\n" - + "import butterknife.Optional;\n" - + "public class Test {\n" - + " @Optional @OnClick(1) void doStuff() {}\n" - + "}"); - - JavaFileObject bindingSource = JavaFileObjects.forSourceString("test/Test_ViewBinding", "" - + "package test;\n" - + "import android.support.annotation.CallSuper;\n" - + "import android.support.annotation.UiThread;\n" - + "import android.view.View;\n" - + "import butterknife.Unbinder;\n" - + "import butterknife.internal.DebouncingOnClickListener;\n" - + "import java.lang.IllegalStateException;\n" - + "import java.lang.Override;\n" - + "public class Test_ViewBinding implements Unbinder {\n" - + " private Test target;\n" - + " private View view1;\n" - + " @UiThread\n" - + " public Test_ViewBinding(final Test target, View source) {\n" - + " this.target = target;\n" - + " View view;\n" - + " view = source.findViewById(1);\n" - + " if (view != null) {\n" - + " view1 = view;\n" - + " view.setOnClickListener(new DebouncingOnClickListener() {\n" - + " @Override\n" - + " public void doClick(View p0) {\n" - + " target.doStuff();\n" - + " }\n" - + " });\n" - + " }\n" - + " }\n" - + " @Override\n" - + " @CallSuper\n" - + " public void unbind() {\n" - + " if (target == null) throw new IllegalStateException(\"Bindings already cleared.\");\n" - + " target = null;\n" - + " if (view1 != null) {\n" - + " view1.setOnClickListener(null);\n" - + " view1 = null;\n" - + " }\n" - + " }\n" - + "}" - ); - - assertAbout(javaSource()).that(source) - .withCompilerOptions("-Xlint:-processing") - .processedWith(new ButterKnifeProcessor()) - .compilesWithoutWarnings() - .and() - .generatesSources(bindingSource); - } - @Test public void optionalAndRequiredSkipsNullCheck() { JavaFileObject source = JavaFileObjects.forSourceString("test.Test", "" + "package test;\n"