@qooxdoo/framework 8.0.0-beta.1 → 8.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/Manifest.json +1 -1
  3. package/lib/compiler/compile-info.json +54 -55
  4. package/lib/compiler/index.js +19039 -23607
  5. package/lib/resource/qx/tool/compiler/cli/templates/class/default.tmpl.js +6 -7
  6. package/lib/resource/qx/tool/compiler/cli/templates/class/singleton.tmpl.js +5 -6
  7. package/lib/resource/qx/tool/compiler/schema/compile-1-0-0.json +6 -2
  8. package/package.json +8 -10
  9. package/source/class/qx/Class.js +26 -7
  10. package/source/class/qx/Mixin.js +15 -6
  11. package/source/class/qx/core/BaseInit.js +14 -13
  12. package/source/class/qx/core/MObjectId.js +16 -0
  13. package/source/class/qx/core/MProperty.js +147 -175
  14. package/source/class/qx/core/check/AbstractCheck.js +5 -1
  15. package/source/class/qx/core/check/CheckFactory.js +6 -0
  16. package/source/class/qx/core/check/DynamicTypeCheck.js +9 -0
  17. package/source/class/qx/core/property/ExplicitPropertyStorage.js +7 -19
  18. package/source/class/qx/core/property/IPropertyStorage.js +2 -21
  19. package/source/class/qx/core/property/Property.js +115 -90
  20. package/source/class/qx/core/property/SimplePropertyStorage.js +2 -18
  21. package/source/class/qx/data/MBinding.js +1 -1
  22. package/source/class/qx/data/SingleValueBinding.js +63 -107
  23. package/source/class/qx/data/binding/AbstractSegment.js +16 -11
  24. package/source/class/qx/data/binding/ArrayIndexSegment.js +17 -10
  25. package/source/class/qx/data/binding/IInputReceiver.js +1 -1
  26. package/source/class/qx/data/binding/PropNameSegment.js +35 -12
  27. package/source/class/qx/dev/unit/TestCase.js +4 -1
  28. package/source/class/qx/event/handler/Focus.js +2 -1
  29. package/source/class/qx/html/Jsx.js +2 -3
  30. package/source/class/qx/html/Node.js +3 -3
  31. package/source/class/qx/io/jsonrpc/Client.js +1 -1
  32. package/source/class/qx/promise/NativeWrapper.js +1 -1
  33. package/source/class/qx/test/Mixin.js +219 -0
  34. package/source/class/qx/test/Promise.js +10 -11
  35. package/source/class/qx/test/core/Property.js +50 -16
  36. package/source/class/qx/test/data/singlevalue/Async.js +17 -4
  37. package/source/class/qx/test/data/singlevalue/Simple.js +6 -0
  38. package/source/class/qx/test/locale/Date.js +2 -2
  39. package/source/class/qx/test/performance/Property.js +0 -1
  40. package/source/class/qx/test/ui/core/SingleSelectionManager.js +150 -0
  41. package/source/class/qx/theme/classic/Appearance.js +21 -0
  42. package/source/class/qx/theme/modern/Appearance.js +21 -0
  43. package/source/class/qx/theme/simple/Appearance.js +21 -0
  44. package/source/class/qx/theme/tangible/Appearance.js +2 -0
  45. package/source/class/qx/tool/cli/AbstractCliApp.js +18 -2
  46. package/source/class/qx/tool/compiler/ClassFile.js +0 -4
  47. package/source/class/qx/tool/compiler/MetaDatabase.js +47 -0
  48. package/source/class/qx/tool/compiler/cli/api/CompilerApi.js +1 -2
  49. package/source/class/qx/tool/compiler/cli/commands/Compile.js +139 -8
  50. package/source/class/qx/tool/compiler/cli/commands/Create.js +1 -1
  51. package/source/class/qx/tool/compiler/cli/commands/Serve.js +1 -1
  52. package/source/class/qx/tool/compiler/cli/commands/Typescript.js +26 -39
  53. package/source/class/qx/tool/compiler/cli/commands/add/Script.js +1 -1
  54. package/source/class/qx/tool/compiler/cli/commands/package/Publish.js +3 -2
  55. package/source/class/qx/tool/compiler/cli/commands/package/Update.js +2 -2
  56. package/source/class/qx/tool/compiler/targets/TypeScriptWriter.js +3 -0
  57. package/source/class/qx/tool/compiler/targets/meta/Browserify.js +142 -80
  58. package/source/class/qx/tool/migration/M8_0_0.js +4 -4
  59. package/source/class/qx/ui/core/SingleSelectionManager.js +4 -4
  60. package/source/class/qx/ui/form/validation/Manager.js +1 -1
  61. package/source/class/qx/ui/toolbar/ToolBar.js +4 -4
  62. package/source/resource/qx/decoration/Modern/table/boolean-false.png +0 -0
  63. package/source/resource/qx/decoration/Modern/table/boolean-true.png +0 -0
  64. package/source/resource/qx/tool/compiler/cli/templates/class/default.tmpl.js +6 -7
  65. package/source/resource/qx/tool/compiler/cli/templates/class/singleton.tmpl.js +5 -6
  66. package/source/resource/qx/tool/compiler/schema/compile-1-0-0.json +6 -2
@@ -631,6 +631,225 @@ qx.Class.define("qx.test.Mixin", {
631
631
  o.dispose();
632
632
  },
633
633
 
634
+ /**
635
+ * Primary bug: when the same mixin is included in two different classes,
636
+ * addMembers() overwrites the shared mixin function's .base pointer on the
637
+ * second include, causing the first class's super() call to resolve to the
638
+ * wrong base method. Per-mixin-per-class $mixinBases storage fixes this.
639
+ */
640
+ testMixinInMultipleClasses() {
641
+ // Two unrelated base classes, each with a 'describe' method
642
+ qx.Class.define("qx.MBase1", {
643
+ extend: qx.core.Object,
644
+ members: {
645
+ describe() {
646
+ return "Base1";
647
+ }
648
+ }
649
+ });
650
+ qx.Class.define("qx.MBase2", {
651
+ extend: qx.core.Object,
652
+ members: {
653
+ describe() {
654
+ return "Base2";
655
+ }
656
+ }
657
+ });
658
+
659
+ // Same mixin included in both classes
660
+ qx.Mixin.define("qx.MDual", {
661
+ members: {
662
+ describe() {
663
+ return super.describe() + " + Mixin";
664
+ }
665
+ }
666
+ });
667
+
668
+ qx.Class.define("qx.MChild1", { extend: qx.MBase1, include: [qx.MDual] });
669
+ qx.Class.define("qx.MChild2", { extend: qx.MBase2, include: [qx.MDual] });
670
+
671
+ var c1 = new qx.MChild1();
672
+ var c2 = new qx.MChild2();
673
+
674
+ // Without the fix, c1.describe() would call Base2 (last include clobbered fn.base)
675
+ this.assertEquals("Base1 + Mixin", c1.describe());
676
+ this.assertEquals("Base2 + Mixin", c2.describe());
677
+
678
+ c1.dispose();
679
+ c2.dispose();
680
+ qx.Class.undefine("qx.MChild1");
681
+ qx.Class.undefine("qx.MChild2");
682
+ qx.Class.undefine("qx.MDual");
683
+ qx.Class.undefine("qx.MBase1");
684
+ qx.Class.undefine("qx.MBase2");
685
+ },
686
+
687
+ /**
688
+ * Regression test: when two different mixins both override the same method
689
+ * and are both patched into the same class, a naive per-class base cache
690
+ * (keyed by method name only) would be overwritten by the second patch,
691
+ * making the first mixin's super() resolve back to itself → infinite recursion.
692
+ * The per-mixin-per-class $mixinBases Map avoids this by using the mixin as
693
+ * the lookup key, so each mixin retains its own base reference.
694
+ */
695
+ testPatchTwoMixinsSameMethodNoInfiniteRecursion() {
696
+ qx.Class.define("qx.IRBase", {
697
+ extend: qx.core.Object,
698
+ members: {
699
+ describe() {
700
+ return "base";
701
+ }
702
+ }
703
+ });
704
+
705
+ qx.Mixin.define("qx.MIR1", {
706
+ members: {
707
+ describe() {
708
+ return super.describe() + " [m1]";
709
+ }
710
+ }
711
+ });
712
+
713
+ qx.Mixin.define("qx.MIR2", {
714
+ members: {
715
+ describe() {
716
+ return super.describe() + " [m2]";
717
+ }
718
+ }
719
+ });
720
+
721
+ qx.Class.patch(qx.IRBase, qx.MIR1);
722
+ qx.Class.patch(qx.IRBase, qx.MIR2);
723
+
724
+ var obj = new qx.IRBase();
725
+ // Without per-mixin base storage, MIR1's super() resolves to MIR1 itself → stack overflow
726
+ this.assertEquals("base [m1] [m2]", obj.describe());
727
+
728
+ obj.dispose();
729
+ qx.Class.undefine("qx.IRBase");
730
+ qx.Class.undefine("qx.MIR1");
731
+ qx.Class.undefine("qx.MIR2");
732
+ },
733
+
734
+ /**
735
+ * Mixin-in-mixin: an outer mixin composes an inner mixin (via include:)
736
+ * with no members of its own. When the outer mixin is applied to two
737
+ * different classes, the inner mixin's super() call must resolve to each
738
+ * class's own base — not the last class to include it (which would clobber
739
+ * the shared .base pointer on the inner mixin's function object).
740
+ */
741
+ testMixinInMixinMultipleClasses() {
742
+ // Inner mixin with a greet() calling super
743
+ qx.Mixin.define("qx.MInner", {
744
+ members: {
745
+ greet() {
746
+ return super.greet() + " [inner]";
747
+ }
748
+ }
749
+ });
750
+
751
+ // Pure composition mixin: no own members, just includes MInner
752
+ qx.Mixin.define("qx.MOuter", {
753
+ include: [qx.MInner]
754
+ });
755
+
756
+ qx.Class.define("qx.MIMBase1", {
757
+ extend: qx.core.Object,
758
+ members: {
759
+ greet() {
760
+ return "Alpha";
761
+ }
762
+ }
763
+ });
764
+ qx.Class.define("qx.MIMBase2", {
765
+ extend: qx.core.Object,
766
+ members: {
767
+ greet() {
768
+ return "Beta";
769
+ }
770
+ }
771
+ });
772
+
773
+ // Both classes include MOuter — MInner.greet's .base gets clobbered on the
774
+ // second include without per-mixin-per-class storage
775
+ qx.Class.define("qx.MIMClass1", { extend: qx.MIMBase1, include: [qx.MOuter] });
776
+ qx.Class.define("qx.MIMClass2", { extend: qx.MIMBase2, include: [qx.MOuter] });
777
+
778
+ var c1 = new qx.MIMClass1();
779
+ var c2 = new qx.MIMClass2();
780
+ // MInner.greet's super must resolve to each class's own base, not the last one applied
781
+ this.assertEquals("Alpha [inner]", c1.greet());
782
+ this.assertEquals("Beta [inner]", c2.greet());
783
+
784
+ c1.dispose();
785
+ c2.dispose();
786
+ qx.Class.undefine("qx.MIMClass1");
787
+ qx.Class.undefine("qx.MIMClass2");
788
+ qx.Class.undefine("qx.MIMBase1");
789
+ qx.Class.undefine("qx.MIMBase2");
790
+ qx.Class.undefine("qx.MOuter");
791
+ qx.Class.undefine("qx.MInner");
792
+ },
793
+
794
+ /**
795
+ * Deep inheritance: the mixin is included in an ancestor class several
796
+ * levels up. A second unrelated class also includes the same mixin,
797
+ * clobbering the shared .base pointer. The baseClassMethod lookup must
798
+ * traverse the inheritance chain to find $mixinBases on the ancestor and
799
+ * return the correct (per-class) base function.
800
+ */
801
+ testDeepInheritanceMixinBaseResolution() {
802
+ qx.Class.define("qx.DIBase1", {
803
+ extend: qx.core.Object,
804
+ members: {
805
+ describe() {
806
+ return "Base1";
807
+ }
808
+ }
809
+ });
810
+ qx.Class.define("qx.DIBase2", {
811
+ extend: qx.core.Object,
812
+ members: {
813
+ describe() {
814
+ return "Base2";
815
+ }
816
+ }
817
+ });
818
+
819
+ qx.Mixin.define("qx.MDI", {
820
+ members: {
821
+ describe() {
822
+ return super.describe() + " [mixin]";
823
+ }
824
+ }
825
+ });
826
+
827
+ // Mixin applied at Parent1; Child1 and Grandchild1 inherit it
828
+ qx.Class.define("qx.DIParent1", { extend: qx.DIBase1, include: [qx.MDI] });
829
+ qx.Class.define("qx.DIChild1", { extend: qx.DIParent1 });
830
+ qx.Class.define("qx.DIGrandchild1", { extend: qx.DIChild1 });
831
+
832
+ // Second unrelated class including the same mixin — clobbers MDI.$$members.describe.base
833
+ qx.Class.define("qx.DIParent2", { extend: qx.DIBase2, include: [qx.MDI] });
834
+
835
+ var deep = new qx.DIGrandchild1();
836
+ var other = new qx.DIParent2();
837
+
838
+ // $mixinBases must be found on DIParent1 (ancestor), not on the instance's own class
839
+ this.assertEquals("Base1 [mixin]", deep.describe());
840
+ this.assertEquals("Base2 [mixin]", other.describe());
841
+
842
+ deep.dispose();
843
+ other.dispose();
844
+ qx.Class.undefine("qx.DIGrandchild1");
845
+ qx.Class.undefine("qx.DIChild1");
846
+ qx.Class.undefine("qx.DIParent1");
847
+ qx.Class.undefine("qx.DIParent2");
848
+ qx.Class.undefine("qx.MDI");
849
+ qx.Class.undefine("qx.DIBase1");
850
+ qx.Class.undefine("qx.DIBase2");
851
+ },
852
+
634
853
  testDoubleMixin() {
635
854
  qx.Class.define("qx.D", {
636
855
  extend: qx.core.Object,
@@ -566,7 +566,7 @@ qx.Class.define("qx.test.Promise", {
566
566
 
567
567
  /**
568
568
  * Tests that a property apply method can return a promise; in this case, the
569
- * property *is* marked as async, and we use the setAlphaAsync to test chaining
569
+ * and we use the setAlphaAsync to test chaining
570
570
  */
571
571
  testPropertySetValueAsyncApply2() {
572
572
  var t = this;
@@ -576,7 +576,6 @@ qx.Class.define("qx.test.Promise", {
576
576
  alpha: {
577
577
  init: null,
578
578
  nullable: true,
579
- async: true,
580
579
  apply: "_applyAlpha",
581
580
 
582
581
  event: "changeAlpha"
@@ -632,9 +631,8 @@ qx.Class.define("qx.test.Promise", {
632
631
  alpha: {
633
632
  init: null,
634
633
  nullable: true,
635
- async: true,
636
634
  event: "changeAlpha",
637
- apply: () => { }
635
+ apply: () => {}
638
636
  }
639
637
  }
640
638
  });
@@ -685,8 +683,9 @@ qx.Class.define("qx.test.Promise", {
685
683
  });
686
684
 
687
685
  asyncObj.getAlphaAsync();
688
- asyncObj.bind("alpha", syncObj, "bravo");
689
- asyncObj.setAlphaAsync("zyx");
686
+ asyncObj.bindAsync("alpha", syncObj, "bravo").then(() => {
687
+ asyncObj.setAlphaAsync("zyx");
688
+ });
690
689
  qx.Promise.all([p1, p2]).then(function () {
691
690
  var p3 = new qx.Promise();
692
691
  syncObj.addListenerOnce("changeBravo", evt => {
@@ -707,10 +706,11 @@ qx.Class.define("qx.test.Promise", {
707
706
  /*
708
707
  * Test binding a "normal" sync property to an async property
709
708
  */
710
- if (!qx.core.Environment.get("qx.core.property.Property.applyDuringConstruct")) {
711
- console.warn("Not working with applyDuringConstruct = false, skipping testBinding async-to-sync part");
712
- return; // Test wird übersprungen, zählt als "ok"
713
- }
709
+ //2026-FEB-25 - Commented this out because it causes this unit test to fail.
710
+ // if (!qx.core.Environment.get("qx.core.property.Property.applyDuringConstruct")) {
711
+ // console.warn("Not working with applyDuringConstruct = false, skipping testBinding async-to-sync part");
712
+ // return;
713
+ // }
714
714
  asyncToSync.then(function () {
715
715
  var asyncObj = new AsyncClazz();
716
716
  var syncObj = new SyncClazz();
@@ -755,7 +755,6 @@ qx.Class.define("qx.test.Promise", {
755
755
  alpha: {
756
756
  init: null,
757
757
  nullable: true,
758
- async: true,
759
758
  apply: "_applyAlpha",
760
759
  event: "changeAlpha"
761
760
  }
@@ -107,7 +107,18 @@ qx.Class.define("qx.test.core.Property", {
107
107
 
108
108
  readOnly: {
109
109
  initFunction: () => 42,
110
- immutable: "readonly"
110
+ immutable: "readonly",
111
+ apply() {
112
+ this.appliedReadOnly = true;
113
+ }
114
+ },
115
+
116
+ noAutoApply: {
117
+ initFunction:() => 24,
118
+ autoApply: false,
119
+ apply() {
120
+ throw new Error("Should not call here!")
121
+ }
111
122
  }
112
123
  },
113
124
 
@@ -159,11 +170,6 @@ qx.Class.define("qx.test.core.Property", {
159
170
  qx.test.cpnfv8.ExternalStorage._subclassStorage[property.getPropertyName()] = value;
160
171
  },
161
172
 
162
- /**@override */
163
- isAsyncStorage() {
164
- return false;
165
- },
166
-
167
173
  /**@override */
168
174
  get(thisObj, prop) {
169
175
  console.log("in externallyStored getter");
@@ -176,6 +182,11 @@ qx.Class.define("qx.test.core.Property", {
176
182
  qx.test.cpnfv8.ExternalStorage._subclassStorage[property.getPropertyName()] = value;
177
183
  },
178
184
 
185
+ /**@override */
186
+ supportsGetAsync() {
187
+ return false;
188
+ },
189
+
179
190
  /**@override */
180
191
  dereference(prop, property) {}
181
192
  },
@@ -224,7 +235,6 @@ qx.Class.define("qx.test.core.Property", {
224
235
  },
225
236
  delay: {
226
237
  init: 0,
227
- async: true,
228
238
  getAsync: async (prop, self) => {
229
239
  let p;
230
240
  p = new qx.Promise((resolve, reject) => {
@@ -540,6 +550,7 @@ qx.Class.define("qx.test.core.Property", {
540
550
 
541
551
  testApply() {
542
552
  let obj = new qx.test.cpnfv8.Superclass();
553
+ this.assertTrue(obj.appliedReadOnly, "Expected apply method for property 'readOnly' to be called, but wasn't");
543
554
  this.assertEquals(3, obj.applyCount, "initial apply count should be 3");
544
555
  this.assertArrayEquals([null, undefined], obj.lastApply, "last apply call during construction");
545
556
  obj.setRunning(false);
@@ -574,6 +585,7 @@ qx.Class.define("qx.test.core.Property", {
574
585
  //old, deprecated behaviour
575
586
  qx.core.Environment.set("qx.core.property.Property.applyDuringConstruct", false);
576
587
  obj = new qx.test.cpnfv8.Superclass();
588
+ this.assertTrue(obj.appliedReadOnly, "Expected apply method for property 'readOnly' to be called, but wasn't");
577
589
  this.assertEquals(1, obj.applyCount, "old behaviour: initial apply count should be 1");
578
590
  obj.resetAnyProp();
579
591
  this.assertEquals(1, obj.applyCount, "old behaviour: apply count after reset should still be 1");
@@ -821,6 +833,21 @@ qx.Class.define("qx.test.core.Property", {
821
833
  let second = inst.getJsdocProp();
822
834
  this.assertArrayEquals([10, 20, 30], initial, "properties created with initFunction returned by getter should be the same");
823
835
  this.assertIdentical(initial, second, "properties created with initFunction returned by getter should be the same");
836
+
837
+ try {
838
+ qx.Class.define(null, {
839
+ extend: qx.core.Object,
840
+ properties: {
841
+ badProp: {
842
+ init: 5,
843
+ initFunction: () => 10
844
+ }
845
+ }
846
+ });
847
+ this.fail("Defining a property with both init and initFunction should throw an error");
848
+ } catch (e) {
849
+ // expected
850
+ }
824
851
  },
825
852
 
826
853
  testDefinesThanSubClassWithInterface() {
@@ -1672,7 +1699,6 @@ qx.Class.define("qx.test.core.Property", {
1672
1699
  propTwo: {
1673
1700
  init: null,
1674
1701
  nullable: true,
1675
- async: true,
1676
1702
  apply: "_applyPropTwo",
1677
1703
  event: "changePropTwo"
1678
1704
  }
@@ -2295,7 +2321,6 @@ qx.Class.define("qx.test.core.Property", {
2295
2321
  extend: qx.core.Object,
2296
2322
  properties: {
2297
2323
  foo: {
2298
- async: true,
2299
2324
  init: 7,
2300
2325
  apply: function (value, old) {
2301
2326
  return new qx.Promise((resolve, reject) => {
@@ -2325,27 +2350,36 @@ qx.Class.define("qx.test.core.Property", {
2325
2350
 
2326
2351
  /**
2327
2352
  * Ensures that an error is thrown when trying to get an async property synchronously
2328
- * when the property is not ready yet, even if it has an init value.
2353
+ * when the property is not ready yet.
2329
2354
  */
2330
2355
  testGetSyncWhenNotReady() {
2331
2356
  qx.Class.undefine("qx.test.cpnfv8.GetSyncTest");
2357
+
2358
+ let fooCached = undefined;
2332
2359
  var Clazz = qx.Class.define("qx.test.cpnfv8.GetSyncTest", {
2333
2360
  extend: qx.core.Object,
2334
2361
  properties: {
2335
2362
  foo: {
2336
- async: true,
2337
- init: 7
2363
+ get() {
2364
+ return fooCached;
2365
+ },
2366
+ getAsync() {
2367
+ if (fooCached === undefined) {
2368
+ fooCached = 7;
2369
+ }
2370
+ return fooCached;
2371
+ },
2372
+ set(value) {
2373
+ fooCached = value;
2374
+ }
2338
2375
  }
2339
2376
  }
2340
2377
  });
2341
2378
 
2342
2379
  const doit = async () => {
2343
2380
  let instance = new Clazz();
2344
- //The property storage will be set to the init value during construction,
2345
- //so we must reset it manually.
2346
- delete instance.$$propertyValues.foo;
2347
2381
  let prop = qx.Class.getByProperty(Clazz, "foo");
2348
- this.assertFalse(prop.isInitialized(instance), "Property foo should not be initialized");
2382
+ this.assertFalse(prop.hasLocalValue(instance), "Property foo should not be locally defined");
2349
2383
  this.assertUndefined(instance.getSafe("foo"));
2350
2384
  try {
2351
2385
  instance.getFoo();
@@ -30,6 +30,19 @@ qx.Class.define("qx.test.data.singlevalue.Async", {
30
30
  __b2: null,
31
31
 
32
32
  setUp() {
33
+ qx.Bootstrap.undefine("qx.test.data.singlevalue.async.AsyncPropertyStorage");
34
+ qx.Bootstrap.define("qx.test.data.singlevalue.async.AsyncPropertyStorage", {
35
+ extend: qx.core.property.SimplePropertyStorage,
36
+ members: {
37
+ /**
38
+ * @Override
39
+ */
40
+ supportsGetAsync() {
41
+ return true;
42
+ }
43
+ }
44
+ });
45
+
33
46
  qx.Class.undefine("qx.test.data.singlevalue.async.Test");
34
47
  const Clazz = qx.Class.define("qx.test.data.singlevalue.async.Test", {
35
48
  extend: qx.core.Object,
@@ -38,17 +51,17 @@ qx.Class.define("qx.test.data.singlevalue.Async", {
38
51
  * The async input
39
52
  */
40
53
  ai: {
41
- async: true,
42
54
  nullable: true,
43
- init: null
55
+ init: null,
56
+ storage: qx.test.data.singlevalue.async.AsyncPropertyStorage
44
57
  },
45
58
  /**
46
59
  * The async output
47
60
  */
48
61
  ao: {
49
- async: true,
50
62
  nullable: true,
51
- init: null
63
+ init: null,
64
+ storage: qx.test.data.singlevalue.async.AsyncPropertyStorage
52
65
  },
53
66
  /**
54
67
  * Syncronous input
@@ -791,6 +791,12 @@ qx.Class.define("qx.test.data.singlevalue.Simple", {
791
791
  person2.setFriend(null);
792
792
  this.assertEquals(3, timesCalled, "Converter should not be called when chain is broken and ignoreConverter is set and matches.");
793
793
  this.assertEquals("Patryk", person3.getName(), "Target should be reset when chain is broken");
794
+
795
+ //ensure that during the initial set, if the chain is incomplete (i.e. has a null value),
796
+ //the target property is reset (not set to null)
797
+ let person4 = new Person("Harry");
798
+ person3.bind("friend.friend.name", person4, "name");
799
+ this.assertEquals("Patryk", person4.getName(), "Target should be reset when chain has null element during initial set");
794
800
  }
795
801
  }
796
802
  });
@@ -32,7 +32,7 @@ qx.Class.define("qx.test.locale.Date", {
32
32
  testGetAmMarker() {
33
33
  var Date = qx.locale.Date;
34
34
  var items = [
35
- { locale: "ko", expected: "오전" },
35
+ { locale: "zh", expected: "上午" },
36
36
  { locale: "fr", expected: "AM" },
37
37
  { locale: "af", expected: "vm." }
38
38
  ];
@@ -46,7 +46,7 @@ qx.Class.define("qx.test.locale.Date", {
46
46
  testGetPMMarker() {
47
47
  var Date = qx.locale.Date;
48
48
  var items = [
49
- { locale: "ko", expected: "오후" },
49
+ { locale: "zh", expected: "下午" },
50
50
  { locale: "fr", expected: "PM" },
51
51
  { locale: "af", expected: "nm." }
52
52
  ];
@@ -50,7 +50,6 @@ qx.Class.define("qx.test.performance.Property", {
50
50
  nullable: true,
51
51
  check: "String",
52
52
  event: "changeAlpha",
53
- async: true,
54
53
  apply: () => {}
55
54
  }
56
55
  }
@@ -0,0 +1,150 @@
1
+ /* ************************************************************************
2
+
3
+ qooxdoo - the new era of web development
4
+
5
+ http://qooxdoo.org
6
+
7
+ Copyright:
8
+ 2025 Qooxdoo contributors
9
+
10
+ License:
11
+ MIT: https://opensource.org/licenses/MIT
12
+ See the LICENSE file in the project's top-level directory for details.
13
+
14
+ ************************************************************************ */
15
+
16
+ /**
17
+ * Minimal ISingleSelectionProvider implementation for unit testing.
18
+ *
19
+ * @internal
20
+ * @ignore(qx.test.ui.core.MockSingleSelectionProvider)
21
+ */
22
+ qx.Class.define("qx.test.ui.core.MockSingleSelectionProvider", {
23
+ extend: qx.core.Object,
24
+ implement: [qx.ui.core.ISingleSelectionProvider],
25
+
26
+ construct(items) {
27
+ super();
28
+ this._items = items;
29
+ },
30
+
31
+ members: {
32
+ _items: null,
33
+
34
+ getItems() {
35
+ return this._items || [];
36
+ },
37
+
38
+ isItemSelectable(item) {
39
+ return true;
40
+ }
41
+ }
42
+ });
43
+
44
+ /**
45
+ * Tests for qx.ui.core.SingleSelectionManager
46
+ */
47
+ qx.Class.define("qx.test.ui.core.SingleSelectionManager", {
48
+ extend: qx.dev.unit.TestCase,
49
+
50
+ members: {
51
+ __manager: null,
52
+ __provider: null,
53
+ __items: null,
54
+
55
+ setUp() {
56
+ this.__items = [new qx.core.Object(), new qx.core.Object(), new qx.core.Object()];
57
+ this.__provider = new qx.test.ui.core.MockSingleSelectionProvider(this.__items);
58
+ this.__manager = new qx.ui.core.SingleSelectionManager(this.__provider);
59
+ },
60
+
61
+ tearDown() {
62
+ this.__manager.dispose();
63
+ this.__provider.dispose();
64
+ for (var item of this.__items) {
65
+ item.dispose();
66
+ }
67
+ this.__items = null;
68
+ },
69
+
70
+ /**
71
+ * Regression test for the fix: resetSelected() with allowEmptySelection=false
72
+ * must NOT fire changeSelected when the first item is already selected.
73
+ */
74
+ testNoSpuriousEventWhenResetSelectedWithFirstItemAlreadySelected() {
75
+ this.__manager.setAllowEmptySelection(false);
76
+ this.__manager.resetSelected();
77
+ this.assertIdentical(this.__items[0], this.__manager.getSelected(), "item[0] should be auto-selected");
78
+
79
+ var eventCount = 0;
80
+ this.__manager.addListener("changeSelected", function () {
81
+ eventCount++;
82
+ });
83
+
84
+ this.__manager.resetSelected();
85
+
86
+ this.assertEquals(0, eventCount, "changeSelected must not fire when selection is unchanged");
87
+ this.assertIdentical(this.__items[0], this.__manager.getSelected(), "Selection must still be item[0]");
88
+ },
89
+
90
+ /**
91
+ * resetSelected() when another item is selected must fire changeSelected
92
+ * and switch back to item[0].
93
+ */
94
+ testEventFiredWhenResetSelectedChangesSelection() {
95
+ this.__manager.setAllowEmptySelection(false);
96
+ this.__manager.setSelected(this.__items[1]);
97
+
98
+ var eventCount = 0;
99
+ var eventData = null;
100
+ this.__manager.addListener("changeSelected", function (e) {
101
+ eventCount++;
102
+ eventData = e.getData();
103
+ });
104
+
105
+ this.__manager.resetSelected();
106
+
107
+ this.assertEquals(1, eventCount, "changeSelected must fire when selection changes");
108
+ this.assertIdentical(this.__items[0], eventData, "Event data should be item[0]");
109
+ this.assertIdentical(this.__items[0], this.__manager.getSelected(), "item[0] should be selected after reset");
110
+ },
111
+
112
+ /**
113
+ * setSelected() with the already-selected item must not fire changeSelected
114
+ * (pre-existing early-exit guard).
115
+ */
116
+ testNoEventWhenSetSelectedWithSameItem() {
117
+ this.__manager.setSelected(this.__items[0]);
118
+
119
+ var eventCount = 0;
120
+ this.__manager.addListener("changeSelected", function () {
121
+ eventCount++;
122
+ });
123
+
124
+ this.__manager.setSelected(this.__items[0]);
125
+
126
+ this.assertEquals(0, eventCount, "changeSelected must not fire when setting the same item");
127
+ },
128
+
129
+ /**
130
+ * Switching allowEmptySelection from true to false on an empty manager
131
+ * must auto-select item[0] and fire changeSelected exactly once.
132
+ */
133
+ testAllowEmptySelectionFalseTriggersAutoSelect() {
134
+ this.assertTrue(this.__manager.isSelectionEmpty(), "Selection should be empty initially");
135
+
136
+ var eventCount = 0;
137
+ var eventData = null;
138
+ this.__manager.addListener("changeSelected", function (e) {
139
+ eventCount++;
140
+ eventData = e.getData();
141
+ });
142
+
143
+ this.__manager.setAllowEmptySelection(false);
144
+
145
+ this.assertEquals(1, eventCount, "changeSelected must fire when auto-selecting the first item");
146
+ this.assertIdentical(this.__items[0], eventData, "Event data should be item[0]");
147
+ this.assertIdentical(this.__items[0], this.__manager.getSelected(), "item[0] should be auto-selected");
148
+ }
149
+ }
150
+ });