Skip to content

feat: add SplitLayout, Card and AvatarGroup testers - #196

Open
totally-not-ai[bot] wants to merge 3 commits into
mainfrom
issues/192-split-layout-card-avatar-group-testers
Open

feat: add SplitLayout, Card and AvatarGroup testers#196
totally-not-ai[bot] wants to merge 3 commits into
mainfrom
issues/192-split-layout-card-avatar-group-testers

Conversation

@totally-not-ai

@totally-not-ai totally-not-ai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds testers for SplitLayout, Card and AvatarGroup. These three components had no tester, so tests had to drive them through raw component calls. Now they can be used like every other component in a browserless test.

Part of #181.

What changed

  • SplitLayoutTesterdragSplitterTo(percentage) simulates the drag the way the client does: it fires a SplitterDragEndEvent with the resulting flex basis values, so the component recalculates its own splitter position and user listeners get fromClient = true. Values outside 0..100 are rejected. Also exposes getSplitterPosition(), getPrimaryComponent() and getSecondaryComponent().
  • CardTester — reports the title and subtitle text the user actually sees, covering both the string and the component flavour, and returning empty text when a header component takes their place. Also gives access to every visible slot: getHeader(), getHeaderPrefix(), getHeaderSuffix(), getMedia() and getFooterComponents().
  • AvatarGroupTester — splits the items into the ones drawn as individual avatars and the ones collapsed behind the overflow avatar, following the client's maxItemsVisible rule (at least two slots, the overflow avatar taking the last one). Both getters return copies, never a live view of the component's list. Width-based collapsing needs a real layout, so it is out of scope.
  • TesterWrappers gets a test(...) overload for each new tester, and the README component count moves from 65+ to 68+.

Use case

You have a booking page built from Cards, and you want a fast test that the card shows the right title and that the footer button is there — without starting a browser.

@ViewPackages
class BookingViewTest extends BrowserlessTest {

    @Test
    void card_showsDestinationAndBookButton() {
        BookingView view = navigate(BookingView.class);

        CardTester<Card> card = test(view.destinationCard);
        Assertions.assertEquals("Lapland", card.getTitleText());
        Assertions.assertEquals("The Exotic North", card.getSubtitleText());
        Assertions.assertEquals(List.of(view.bookButton),
                card.getFooterComponents());
    }

    @Test
    void splitter_canBeDraggedToShowMoreOfTheMap() {
        MapView view = navigate(MapView.class);

        test(view.splitLayout).dragSplitterTo(30);

        Assertions.assertEquals(30d,
                test(view.splitLayout).getSplitterPosition());
    }
}

API Changes

com.vaadin.browserless.TesterWrappers

// Added
default AvatarGroupTester<AvatarGroup> test(AvatarGroup avatarGroup)
default CardTester<Card> test(Card card)
default SplitLayoutTester<SplitLayout> test(SplitLayout splitLayout)

com.vaadin.flow.component.avatar.AvatarGroupTester

// Added
public class AvatarGroupTester<T extends AvatarGroup> extends ComponentTester<T>
public AvatarGroupTester(T component)
public List<AvatarGroupItem> getVisibleItems() // items drawn as individual avatars
public List<AvatarGroupItem> getOverflowItems() // items collapsed behind the overflow avatar
public String getOverflowAbbreviation() // e.g. "+3", null when nothing overflows

com.vaadin.flow.component.card.CardTester

// Added
public class CardTester<T extends Card> extends ComponentTester<T>
public CardTester(T component)
public String getTitleText() // empty when a header component replaces the title
public String getSubtitleText() // empty when a header component replaces the subtitle
public List<Component> getFooterComponents()
public Component getHeader()
public Component getHeaderPrefix()
public Component getHeaderSuffix()
public Component getMedia()

com.vaadin.flow.component.splitlayout.SplitLayoutTester

// Added
public class SplitLayoutTester<T extends SplitLayout> extends ComponentTester<T>
public SplitLayoutTester(T component)
public void dragSplitterTo(double primaryPercentage) // fires SplitterDragEndEvent with fromClient = true
public Double getSplitterPosition()
public Component getPrimaryComponent()
public Component getSecondaryComponent()

Test summary

# Status What the test verifies Why it matters
1 dragSplitterTo(30) moves the splitter to 30 and notifies the drag end listener with fromClient = true The main action of the tester; if the event were wrong, the component would not recalculate its position and app listeners would not fire
2 A percentage below 0 or above 100 throws IllegalArgumentException and leaves the splitter where it was A refused drag must not silently corrupt the layout state
3 0 and 100 are accepted Boundary values are legal drags, not errors
4 Card title text is read from both the string title and a title component, and is empty when neither is set The two title flavours are stored differently; reading only one would report a wrong or empty title
5 A header component makes both title and subtitle text report empty Matches what the user sees; otherwise the test would assert on a title that is not rendered
6 Each card slot accessor returns the exact component set (and null when the slot is empty), and the footer returns only footer content Slot mix-ups would make assertions pass against the wrong part of the card
7 With no maxItemsVisible, all items are visible, overflow is empty and the abbreviation is null The default case must not invent an overflow avatar
8 With maxItemsVisible = 3 and 5 items, 2 items are visible, 3 overflow and the abbreviation is +3 Pins the rule that the overflow avatar claims the last visible slot
9 maxItemsVisible = 1 is clamped to two slots; with exactly two items nothing overflows The client never collapses below two avatars; an off-by-one here misreports what the user sees
10 maxItemsVisible equal to the item count produces no overflow The "fits exactly" boundary must not trigger the overflow avatar
11 Every getter and dragSplitterTo throw IllegalStateException on a hidden component The usability check is the contract shared by all testers; a missing one lets tests assert on invisible UI
12 gap The lists returned by getVisibleItems() / getOverflowItems() are snapshots, not live views of the component's item list This is exactly what the AvatarGroupTester refactor changed; nothing fails today if a live view leaks back

Tests covering each row:

  • SplitLayoutTesterTest.dragSplitterTo_updatesPositionAndNotifiesListener → 1
  • SplitLayoutTesterTest.dragSplitterTo_positionOutsideRange_throws → 2
  • SplitLayoutTesterTest.dragSplitterTo_positionAtRangeEnds_accepted → 3
  • SplitLayoutTesterTest.getSplitComponents_returnSlottedComponents → 6
  • SplitLayoutTesterTest.dragSplitterTo_notUsable_throws, SplitLayoutTesterTest.getters_notUsable_throw → 11
  • CardTesterTest.getTitleText_returnsStringAndComponentTitles → 4
  • CardTesterTest.getSubtitleText_returnsSubtitle → 4
  • CardTesterTest.headerComponent_hidesTitleAndSubtitle → 5
  • CardTesterTest.getFooterComponents_returnsFooterContentOnly, CardTesterTest.getSlottedComponents_returnContentOfEachSlot → 6
  • CardTesterTest.getters_notUsable_throw → 11
  • AvatarGroupTesterTest.noMaxItemsVisible_allItemsVisible → 7
  • AvatarGroupTesterTest.maxItemsVisible_overflowAvatarTakesLastVisibleSlot → 8
  • AvatarGroupTesterTest.maxItemsVisibleBelowTwo_stillShowsTwoSlots → 9
  • AvatarGroupTesterTest.maxItemsVisibleFitsAllItems_noOverflow → 10
  • AvatarGroupTesterTest.getters_notUsable_throw → 11

Left untested on purpose: the plain pass-through getters are only checked through the assertions above, the generated find*() entry points come from the existing @Tests locator processor and are covered by its own tests, and width-based avatar collapsing is not emulated because it needs a real layout.

None of these components had a @Tests-annotated tester, so the locator
processor emitted no find*() entry point for them and they had to be driven
through raw component calls.

- SplitLayoutTester.dragSplitterTo(percentage) reports the drag the way the
  client does, by firing SplitterDragEndEvent with the resulting flex basis
  values, so the component recalculates its own splitter position and user
  listeners see fromClient=true.
- CardTester reports the title and subtitle text the user actually sees,
  covering both the string and the component flavour and the fact that a
  header component takes their place.
- AvatarGroupTester splits the items into the ones rendered as individual
  avatars and the ones collapsed behind the overflow avatar, following the
  client's maxItemsVisible rule (minimum two slots, the overflow avatar taking
  the last one). Width-based collapsing needs a layout and is out of scope.

Part of #181.
getVisibleItems() returned the component's own unmodifiable view of the item
list when there was no overflow, and a copy otherwise, so the same method
handed out either a live view or a snapshot depending on maxItemsVisible. Both
getters now copy, and the visible count replaces the nullable limit that forced
the two branches.

Also pin three untested boundaries: a group that fits within the clamped
two-slot minimum keeps both avatars and shows no overflow avatar, splitter
drags to 0 and 100 are accepted, and a card with neither a string nor a
component title reports empty title text.
* @throws IllegalStateException
* if the component is not usable
*/
public List<Component> getFooterComponents() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a method to get footer components, but nothing for header, header prefix and suffix.
We should probably add them or remove this one as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — went with adding them rather than dropping the footer one, since the point of these getters is to let a test assert what the card actually shows without reaching past the tester.

CardTester now has getHeader(), getHeaderPrefix(), getHeaderSuffix() and getMedia() alongside getFooterComponents(), each with the same usability check, so every slot is covered. Each returns the component or null, mirroring Card's own API. I included media too, as it is the same kind of single-component slot and would have been the next gap.

Note that getTitleText() / getSubtitleText() deliberately keep reporting empty while a header component is set — the header takes their place in the DOM — whereas getHeader() and friends just report their slot contents.

getFooterComponents() was the only slot accessor, so asserting on the header
or the media meant dropping out of the tester. Add getHeader(),
getHeaderPrefix(), getHeaderSuffix() and getMedia(), each usability-checked
like the rest, so every slot the user can see is reachable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant