profile
viewpoint

Ask questions[BUG] Test case for "next" button of the VKB

Finally, after reporting my problems here and here, I found a test case.

To be more precise: the test case that I created is a bit more problematic than my actual app, because it creates issues with the "next" button of the VKB not only on Android, but also on iOS and on the Simulator. However, I hope that a proper fix of the issues of this test case can also fix my app. In my actual app I have a lot of InputComponents in the same Form and these InputComponents are changed (replaced or hided) according to the user input, so this test case is not too much far from the reality.

Please try to run the following code on Android and on iOS and then try to enter all the inputs switching from an InputComponent to another using the "next" Button (when available) and see what happens. Android and iOS have different issues: Android "goes next" in a random order and iOS "goes next" even to the hidden fields.

Thank you

    public void start() {
        if (current != null) {
            current.show();
            return;
        }
        Random random = new Random(System.currentTimeMillis());
        int numberOfComponents = 100;
        Form hi = new Form("Test Case Android bug", new TextModeLayout(numberOfComponents, 1));
        InputComponent[] fields = new InputComponent[numberOfComponents];
        for (int i = 0; i < fields.length; i++) {
            if (random.nextInt(2) != 0) {
                fields[i] = new TextComponent().label("Type here");
            } else {
                if (random.nextInt(2) != 0) {
                    fields[i] = PickerComponent.createDate(new Date()).label("Date");
                } else {
                    fields[i] = PickerComponent.createDurationMinutes(0).label("Duration");
                }
            }
        }
        hi.addAll(fields);
        hi.show();
        UITimer.timer(100, false, hi, () -> {
            for (int i = 0; i < fields.length; i++) {
                if (random.nextInt(2) != 0) {
                    TextComponent newField = new TextComponent().label("Type here");
                    hi.replace(fields[i], newField, null);
                    fields[i] = newField;
                }
            }
            for (int i = 0; i < fields.length; i++) {
                if (random.nextInt(2) != 0) {
                    fields[i].remove();
                }
            }
            for (int i = 0; i < fields.length; i++) {
                if (random.nextInt(2) != 0) {
                    if (fields[i].getParent() != null) {
                        fields[i].setVisible(false);
                        fields[i].setHidden(true);
                    }
                }
            }
            hi.revalidate();
        });
    }
codenameone/CodenameOne

Answer questions jsfan3

The current implementation of public TabIterator getTabIterator(Component start) inside Form relies on ComponentSelector and on a Comparator that uses getTabIndex(). This is fine in a lot of cases, but it produces unwanted results when we replace, remove or hide components. That's why I think that your API implementation should be fixed or enhanced.

Now I've just wrote and tested a different implementation of getTabIterator that works correctly with the test case that I provided (it fixes the issues on Android, iOS and Simulator). Of course my following implementation cannot be used as it is in all situations. In particular, my implementation is ok if the inputs are in a single column and it considers only TextAreas and Pickers, but it could not be ok if there are more columns (it depends by the specific use case) and if we want to consider all kinds of inputs.

Moreover, I didn't find yet a working solution for my actual app.

    public void start() {
        if (current != null) {
            current.show();
            return;
        }
        Random random = new Random(System.currentTimeMillis());
        int numberOfComponents = 100;
        Form hi = new Form("Test Case Android bug", new TextModeLayout(numberOfComponents, 1)) {
            @Override
            public Form.TabIterator getTabIterator(Component start) {
                // Note: I cannot call the constructor of TabIterator because it has private access,
                // that's why I workaround the problem calling super.getTabIterator
                // and then I remove all its items to get an empty TabIterator
                Form.TabIterator iterator = super.getTabIterator(start);
                while (iterator.hasNext()) {
                    iterator.remove();
                }

                SortedSet<Component> cmpsSet = getAllFocusableCmps(this);
                if (cmpsSet.isEmpty()) {
                    Log.p("getTabIterator returning empty iterator because no focusable components were found", Log.DEBUG);
                    return iterator;
                }
                // Note: if "start" is the last item of the iterator, we can return
                // an empty iterator to avoid a "next" button to the first item
                // on Android VKB
                if (cmpsSet.last() == start) {
                    Log.p("getTabIterator returning empty iterator because the given \"start\" component is the last component", Log.DEBUG);
                    return iterator;
                }

                // Note: if "start" is not contained in the iterator list, maybe it's better
                // to return an empty iterator
                if (!cmpsSet.contains(start)) {
                    Log.p("getTabIterator returning empty iterator because the given \"start\" component is not in the iterator list (that contains " + cmpsSet.size() + " items)", Log.DEBUG);
                    return iterator;
                }
                for (Component cmp : cmpsSet) {
                    iterator.add(cmp);
                }
                iterator.setCurrent(start);
                Log.p("getTabIterator returning " + cmpsSet.size() + " components", Log.DEBUG);

                return iterator;
            }

            private SortedSet<Component> getAllFocusableCmps(Container cnt) {
                TreeSet<Component> list = new TreeSet<>(new Comparator() {
                    @Override
                    public int compare(Object o1, Object o2) {
                        if (o1 instanceof Component && o2 instanceof Component) {
                            Component cmp1 = (Component) o1;
                            Component cmp2 = (Component) o2;
                            if (cmp1.getAbsoluteY() < cmp2.getAbsoluteY()) {
                                return -1;
                            } else if (cmp1.getAbsoluteY() > cmp2.getAbsoluteY()) {
                                return 1;
                            } else if (cmp1.getAbsoluteX() < cmp2.getAbsoluteX()) {
                                return -1;
                            } else if (cmp1.getAbsoluteX() > cmp2.getAbsoluteX()) {
                                return 1;
                            } else {
                                return 0;
                            }
                        } else {
                            // this cannot happen
                            throw new IllegalArgumentException("o1 and o2 must be Components");
                        }
                    }
                });
                for (Component cmp : cnt.getChildrenAsList(true)) {
                    if (cmp.isVisible() && !cmp.isHidden()) {
                        if (cmp instanceof TextArea && ((TextArea) cmp).isEditable()) {
                            list.add(cmp);
                        } else if (cmp instanceof Picker && ((Picker) cmp).isEnabled()) {
                            list.add(cmp);
                        } else if (cmp instanceof Container) {
                            list.addAll(getAllFocusableCmps((Container) cmp));
                        }
                    }
                }
                return list;
            }
        };
        InputComponent[] fields = new InputComponent[numberOfComponents];
        for (int i = 0; i < fields.length; i++) {
            if (random.nextInt(2) != 0) {
                fields[i] = new TextComponent().label("Type here");
            } else {
                if (random.nextInt(2) != 0) {
                    fields[i] = PickerComponent.createDate(new Date()).label("Date");
                } else {
                    fields[i] = PickerComponent.createDurationMinutes(0).label("Duration");
                }
            }
        }
        hi.addAll(fields);
        hi.show();
        UITimer.timer(100, false, hi, () -> {
            for (int i = 0; i < fields.length; i++) {
                if (random.nextInt(2) != 0) {
                    TextComponent newField = new TextComponent().label("Type here");
                    hi.replace(fields[i], newField, null);
                    fields[i] = newField;
                }
            }
            for (int i = 0; i < fields.length; i++) {
                if (random.nextInt(2) != 0) {
                    fields[i].remove();
                }
            }
            for (int i = 0; i < fields.length; i++) {
                if (random.nextInt(2) != 0) {
                    if (fields[i].getParent() != null) {
                        fields[i].setVisible(false);
                        fields[i].setHidden(true);
                    }
                }
            }
            hi.revalidate();
        });
    }
useful!

Related questions

RFE: set a Date before 1970 with a lightweight picker hot 1
Ios build fails due to cocoapods update failure hot 1
Github User Rank List