Skip to content

Commit 2b209a5

Browse files
committed
feat: support native ES classes with lazy registration
1 parent 6bc84d4 commit 2b209a5

9 files changed

Lines changed: 871 additions & 4 deletions

File tree

test-app/app/src/main/assets/app/mainpage.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ require("./tests/testGC");
4242
require("./tests/testsMemoryManagement");
4343
require("./tests/testFieldGetSet");
4444
require("./tests/extendedClassesTests");
45+
require("./tests/testNativeESClasses");
4546
//require("./tests/extendClassNameTests"); // as tests now run with SBG, this test fails the whole build process
4647
require("./tests/testJniReferenceLeak");
4748
require("./tests/testNativeModules");
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
describe("Tests native ES class extensions (class X extends NativeType)", function () {
2+
3+
var appClassLoader = com.tns.Runtime.class.getClassLoader();
4+
5+
// DummyClass.method2(Object) returns "obj=" + obj.toString(), forcing a Java-side virtual
6+
// toString dispatch through the generated proxy. (java.lang.String.valueOf is unusable for
7+
// this: accessing `java.lang.String.null` in other suites permanently replaces `valueOf` on
8+
// the ctor function with the runtime's null-returning valueOf.)
9+
function javaToString(obj) {
10+
return new com.tns.tests.DummyClass().method2(obj);
11+
}
12+
13+
it("When_extending_a_class_with_es_class_syntax_instances_should_construct_and_dispatch_overrides", function () {
14+
class EsButton extends com.tns.tests.Button1 {
15+
getIMAGE_ID_PROP() {
16+
return "es class override";
17+
}
18+
}
19+
20+
var button = new EsButton();
21+
22+
expect(button instanceof EsButton).toBe(true);
23+
expect(button instanceof com.tns.tests.Button1).toBe(true);
24+
expect(button.getIMAGE_ID_PROP()).toBe("es class override");
25+
// non-overridden base methods still work
26+
expect(button.echo("hello")).toBe("hello");
27+
});
28+
29+
it("When_java_calls_a_virtual_method_it_should_dispatch_to_the_es_class_override", function () {
30+
class EchoButton extends com.tns.tests.Button1 {
31+
echo(s) {
32+
return "es:" + s;
33+
}
34+
}
35+
36+
var button = new EchoButton();
37+
38+
// triggerEcho calls this.echo(s) on the Java side - it must route
39+
// through the generated proxy back into the ES class method
40+
expect(button.triggerEcho("x")).toBe("es:x");
41+
});
42+
43+
it("When_calling_super_method_from_an_es_class_override_it_should_invoke_the_java_implementation", function () {
44+
class SuperEchoButton extends com.tns.tests.Button1 {
45+
echo(s) {
46+
return "es:" + super.echo(s);
47+
}
48+
}
49+
50+
var button = new SuperEchoButton();
51+
52+
expect(button.echo("y")).toBe("es:y");
53+
// round trip: Java triggerEcho -> proxy echo -> JS override -> super.echo -> Java echo
54+
expect(button.triggerEcho("z")).toBe("es:z");
55+
});
56+
57+
it("When_the_es_class_constructor_passes_arguments_to_super_the_matching_java_constructor_should_be_used", function () {
58+
class CtorButton extends com.tns.tests.Button1 {
59+
constructor(value) {
60+
super(value); // Button1(int) overload
61+
this.value = value;
62+
}
63+
}
64+
65+
var button = new CtorButton(5);
66+
67+
expect(button.value).toBe(5);
68+
expect(button instanceof com.tns.tests.Button1).toBe(true);
69+
});
70+
71+
it("When_accessing_the_class_property_before_any_instance_exists_the_class_should_be_registered_lazily", function () {
72+
class LazyTouch extends java.lang.Object {
73+
toString() {
74+
return "lazy touch";
75+
}
76+
}
77+
78+
// no `new` has happened - static touch must register the proxy class
79+
var clazz = LazyTouch.class;
80+
81+
expect(clazz).not.toBe(null);
82+
expect(clazz.getName()).toContain("LazyTouch");
83+
84+
// the class is fully functional before any JS-side construction:
85+
// Java instantiates it through reflection and dispatches to the JS override
86+
var created = clazz.newInstance();
87+
expect(javaToString(created)).toBe("obj=lazy touch");
88+
});
89+
90+
it("When_passing_the_es_class_to_a_java_method_expecting_a_class_it_should_marshal_to_java_lang_Class", function () {
91+
class MarshalledClass extends com.tns.tests.DummyClass {
92+
}
93+
94+
// Class.isAssignableFrom(Class) - MarshalledClass is registered lazily during marshalling
95+
expect(com.tns.tests.DummyClass.class.isAssignableFrom(MarshalledClass)).toBe(true);
96+
expect(java.lang.Object.class.isAssignableFrom(MarshalledClass)).toBe(true);
97+
});
98+
99+
it("When_passing_the_es_class_where_java_lang_Object_is_expected_it_should_marshal_to_its_java_lang_Class", function () {
100+
class ObjectMarshalledClass extends java.lang.Object {
101+
}
102+
103+
var list = new java.util.ArrayList();
104+
list.add(ObjectMarshalledClass);
105+
106+
var stored = list.get(0);
107+
expect(stored.getName()).toBe(ObjectMarshalledClass.class.getName());
108+
});
109+
110+
it("When_extending_an_es_class_that_extends_a_native_class_each_level_should_get_its_own_proxy", function () {
111+
class LevelOne extends com.tns.tests.Button1 {
112+
getIMAGE_ID_PROP() {
113+
return "level one";
114+
}
115+
echo(s) {
116+
return "L1:" + s;
117+
}
118+
}
119+
120+
class LevelTwo extends LevelOne {
121+
getIMAGE_ID_PROP() {
122+
return "level two + " + super.getIMAGE_ID_PROP();
123+
}
124+
}
125+
126+
var two = new LevelTwo();
127+
expect(two instanceof LevelTwo).toBe(true);
128+
expect(two instanceof LevelOne).toBe(true);
129+
expect(two instanceof com.tns.tests.Button1).toBe(true);
130+
expect(two.getIMAGE_ID_PROP()).toBe("level two + level one");
131+
// method defined only on the intermediate level must be part of the proxy overrides
132+
expect(two.triggerEcho("q")).toBe("L1:q");
133+
134+
var one = new LevelOne();
135+
expect(one.getIMAGE_ID_PROP()).toBe("level one");
136+
expect(one instanceof LevelTwo).toBe(false);
137+
138+
expect(one.getClass().getName()).not.toBe(two.getClass().getName());
139+
});
140+
141+
it("When_constructing_the_es_class_multiple_times_all_instances_should_share_one_proxy_class", function () {
142+
class SharedProxyButton extends com.tns.tests.Button1 {
143+
}
144+
145+
var first = new SharedProxyButton();
146+
var second = new SharedProxyButton();
147+
148+
expect(first.getClass().equals(second.getClass())).toBe(true);
149+
expect(first.getClass().equals(SharedProxyButton.class)).toBe(true);
150+
});
151+
152+
it("When_reading_base_class_statics_through_the_es_class_they_should_resolve_to_the_java_members", function () {
153+
class StaticsButton extends com.tns.tests.Button1 {
154+
static jsStatic = 42;
155+
static jsStaticMethod() {
156+
return "js static";
157+
}
158+
}
159+
160+
// Java statics are reachable through the constructor prototype chain
161+
expect(StaticsButton.STATIC_IMAGE_ID).toBe("static image id");
162+
expect(StaticsButton.SGetStaticImageId()).toBe("static image id");
163+
164+
// plain JS statics live on the JS constructor and are untouched
165+
expect(StaticsButton.jsStatic).toBe(42);
166+
expect(StaticsButton.jsStaticMethod()).toBe("js static");
167+
});
168+
169+
it("When_the_es_class_is_registered_its_proxy_should_be_discoverable_through_the_app_class_loader", function () {
170+
class DiscoverableEsClass extends java.lang.Object {
171+
toString() {
172+
return "discoverable es class";
173+
}
174+
}
175+
176+
var instance = new DiscoverableEsClass();
177+
var className = instance.getClass().getName();
178+
179+
var found = java.lang.Class.forName(className, false, appClassLoader);
180+
181+
expect(found.getName()).toBe(className);
182+
expect(found.equals(instance.getClass())).toBe(true);
183+
expect(found.equals(DiscoverableEsClass.class)).toBe(true);
184+
});
185+
186+
it("When_implementing_an_interface_with_es_class_syntax_java_should_dispatch_to_the_js_methods", function () {
187+
var runCount = 0;
188+
189+
class EsRunnable extends java.lang.Runnable {
190+
run() {
191+
runCount++;
192+
}
193+
}
194+
195+
var runnable = new EsRunnable();
196+
expect(runnable instanceof java.lang.Runnable).toBe(true);
197+
198+
// Thread.run() (not started) invokes target.run() synchronously on the current thread
199+
var thread = new java.lang.Thread(runnable);
200+
thread.run();
201+
202+
expect(runCount).toBe(1);
203+
});
204+
205+
it("When_declaring_static_interfaces_the_proxy_should_implement_them", function () {
206+
var ran = { value: false };
207+
208+
class WithInterfaces extends java.lang.Object {
209+
static interfaces = [java.lang.Runnable];
210+
211+
run() {
212+
ran.value = true;
213+
}
214+
}
215+
216+
var instance = new WithInterfaces();
217+
218+
expect(instance instanceof java.lang.Runnable).toBe(true);
219+
220+
var thread = new java.lang.Thread(instance);
221+
thread.run();
222+
223+
expect(ran.value).toBe(true);
224+
});
225+
226+
it("When_getting_the_class_name_it_should_be_stable_and_descriptive", function () {
227+
class StableNameClass extends java.lang.Object {
228+
}
229+
230+
var name = StableNameClass.class.getName();
231+
232+
// runtime generated proxies are named <base>_es<hash>_<jsClassName>, mirroring the
233+
// legacy <base>_<file>_<line>_<column>_<name> scheme (the com.tns.gen prefix is only
234+
// present on build-time pre-generated bindings)
235+
expect(name).toContain("java.lang.Object_es");
236+
expect(name).toContain("StableNameClass");
237+
// repeated access resolves to the very same class
238+
expect(StableNameClass.class.getName()).toBe(name);
239+
});
240+
241+
it("When_passing_an_es_class_instance_to_java_and_reading_it_back_it_should_be_the_same_object", function () {
242+
class RoundTripObject extends java.lang.Object {
243+
toString() {
244+
return "round trip";
245+
}
246+
}
247+
248+
var instance = new RoundTripObject();
249+
var list = new java.util.ArrayList();
250+
list.add(instance);
251+
252+
var stored = list.get(0);
253+
expect(stored.equals(instance)).toBe(true);
254+
expect(javaToString(stored)).toBe("obj=round trip");
255+
});
256+
257+
it("When_calling_extend_on_an_es_class_it_should_throw_a_descriptive_error", function () {
258+
class NotExtendable extends com.tns.tests.Button1 {
259+
}
260+
261+
expect(function () {
262+
NotExtendable.extend({
263+
toString: function () {
264+
return "should not work";
265+
}
266+
});
267+
}).toThrow();
268+
});
269+
270+
it("When_extending_a_class_created_with_legacy_extend_the_old_behavior_should_be_preserved", function () {
271+
var LegacyButton = com.tns.tests.Button1.extend({
272+
getIMAGE_ID_PROP: function () {
273+
return "legacy override";
274+
}
275+
});
276+
277+
class EsOnLegacy extends LegacyButton {
278+
}
279+
280+
// the legacy proxy is used - ES levels above `.extend()`-created classes get no proxy of their own
281+
var instance = new EsOnLegacy();
282+
expect(instance instanceof com.tns.tests.Button1).toBe(true);
283+
expect(instance.getIMAGE_ID_PROP()).toBe("legacy override");
284+
});
285+
286+
it("When_a_plain_js_class_hierarchy_is_used_the_runtime_should_not_interfere", function () {
287+
class PlainBase {
288+
value() {
289+
return 1;
290+
}
291+
}
292+
293+
class PlainDerived extends PlainBase {
294+
value() {
295+
return super.value() + 1;
296+
}
297+
}
298+
299+
var plain = new PlainDerived();
300+
expect(plain.value()).toBe(2);
301+
expect(function () {
302+
return plain instanceof java.lang.Object;
303+
}).not.toThrow();
304+
});
305+
306+
it("When_the_NativeClass_decorator_is_applied_it_should_be_a_noop", function () {
307+
expect(typeof global.NativeClass).toBe("function");
308+
309+
const DecoratedButton = global.NativeClass(class DecoratedButton extends com.tns.tests.Button1 {
310+
getIMAGE_ID_PROP() {
311+
return "decorated";
312+
}
313+
});
314+
315+
var button = new DecoratedButton();
316+
expect(button.getIMAGE_ID_PROP()).toBe("decorated");
317+
});
318+
319+
it("When_anonymous_es_classes_extend_native_types_each_should_get_a_distinct_proxy", function () {
320+
var First = class extends java.lang.Object {
321+
toString() {
322+
return "first anonymous";
323+
}
324+
};
325+
var Second = class extends java.lang.Object {
326+
toString() {
327+
return "second anonymous";
328+
}
329+
};
330+
331+
var firstInstance = new First();
332+
var secondInstance = new Second();
333+
334+
expect(javaToString(firstInstance)).toBe("obj=first anonymous");
335+
expect(javaToString(secondInstance)).toBe("obj=second anonymous");
336+
expect(firstInstance.getClass().equals(secondInstance.getClass())).toBe(false);
337+
});
338+
});

test-app/app/src/main/assets/internal/ts_helpers.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,14 @@
166166
}
167167
}
168168

169+
// No-op decorator for plain ES classes extending native types.
170+
// The runtime registers such classes lazily (on first construction, static usage or when
171+
// passed to native APIs), so the decorator only exists so shared iOS/Android sources and
172+
// non-transformed code keep working.
173+
function NativeClass(target) {
174+
return target;
175+
}
176+
169177
Object.defineProperty(global, "__native", { value: __native });
170178
Object.defineProperty(global, "__extends", { value: __extends });
171179
Object.defineProperty(global, "__decorate", { value: __decorate });
@@ -174,4 +182,7 @@
174182
global.JavaProxy = JavaProxy;
175183
}
176184
global.Interfaces = Interfaces;
185+
if (!global.NativeClass) {
186+
global.NativeClass = NativeClass;
187+
}
177188
})()

0 commit comments

Comments
 (0)