Skip to content

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

Open
totally-not-ai[bot] wants to merge 3 commits into
mainfrom
issues/192-splitlayout-card-avatargroup-testers
Open

feat: add SplitLayout, Card and AvatarGroup testers#197
totally-not-ai[bot] wants to merge 3 commits into
mainfrom
issues/192-splitlayout-card-avatargroup-testers

Conversation

@totally-not-ai

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

Copy link
Copy Markdown
Contributor

Summary

SplitLayout, Card and AvatarGroup had no component tester, so tests had to drive them through raw component calls. This PR adds SplitLayoutTester, CardTester and AvatarGroupTester, so these three components now get a generated *Locator and a test(...) entry point like every other supported component.

Part of #181. Fixes #192

What changed

  • SplitLayoutTesterdragSplitterTo(double) fires a real splitter-dragend DOM event carrying the same flex-basis event data the browser sends. The component then recomputes its own splitter position and notifies drag end listeners, instead of the tester writing server-side state behind the component's back. Also exposes the splitter position and the primary/secondary components.
  • CardTester — assertion-only. The title and subtitle accessors throw when a header component is set, because the header replaces them and that text is not on screen. Also exposes header, header prefix/suffix, media and footer components.
  • AvatarGroupTester — assertion-only. Reports the split between visible avatars and the overflow bubble, derived from maxItemsVisible (the overflow avatar takes one slot, and the group never drops below two avatars). Also gives the accessible "active users" label with {count} filled in.
  • TesterWrappers gets the three matching test(...) overloads, and the README tester count goes from 65+ to 70+.

Because there is no browser and no layout, AvatarGroupTester cannot apply the extra truncation the component does when avatars do not fit the available width.

Use case

An app has a master-detail view built on SplitLayout, and it stores the splitter position the user drags to so the view reopens the same way. A developer wants a fast unit test that the position is really saved, without starting a browser.

@ViewPackages
class BoardViewTest extends BrowserlessTest {

    @Test
    void draggingSplitter_savesUserPreference() {
        BoardView view = navigate(BoardView.class);

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

        // the view's own drag end listener wrote the preference
        assertEquals(30.0, view.preferences.getSplitterPosition());
        assertEquals(30.0, test(view.splitLayout).getSplitterPosition());
    }
}

The same style works for assertions on a card or an avatar group:

assertEquals("Sales Q3", test(view.card).getTitleAsText());
assertEquals("+3", test(view.avatarGroup).getOverflowAbbreviation());

API Changes

4 classes affected, 3 new classes, 20 methods added, none removed or changed.

com.vaadin.flow.component.splitlayout.SplitLayoutTester

// Added
public class SplitLayoutTester<T extends SplitLayout> extends ComponentTester<T> // annotated @Tests(SplitLayout.class)
public SplitLayoutTester(T component)
public void dragSplitterTo(double position) // fires a splitter-dragend DOM event; rejects values outside 0..100
public Double getSplitterPosition() // nullable; null until set or dragged
public Component getPrimaryComponent() // nullable
public Component getSecondaryComponent() // nullable

com.vaadin.flow.component.card.CardTester

// Added
public class CardTester<T extends Card> extends ComponentTester<T> // annotated @Tests(Card.class)
public CardTester(T component)
public String getTitleAsText() // throws IllegalStateException if a header component is set
public Component getTitle() // nullable; throws IllegalStateException if a header component is set
public Component getSubtitle() // nullable; throws IllegalStateException if a header component is set
public Component getHeader() // nullable
public Component getHeaderPrefix() // nullable
public Component getHeaderSuffix() // nullable
public Component getMedia() // nullable
public List<Component> getFooterComponents()

com.vaadin.flow.component.avatar.AvatarGroupTester

// Added
public class AvatarGroupTester<T extends AvatarGroup> extends ComponentTester<T> // annotated @Tests(AvatarGroup.class)
public AvatarGroupTester(T component)
public List<AvatarGroupItem> getItems()
public List<AvatarGroupItem> getVisibleItems()
public List<AvatarGroupItem> getOverflowItems()
public String getOverflowAbbreviation() // nullable; e.g. "+3", null when nothing overflows
public String getActiveUsersLabel() // nullable; i18n phrase with {count} substituted

com.vaadin.browserless.TesterWrappers

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

Test summary

# Status What the test verifies Why it matters
1 dragSplitterTo(30) sets the splitter position to 30 and fires exactly one drag end event with isFromClient() == true This is the whole point of the tester: the drag must go through the component, not around it
2 A dragged position replaces a position set earlier on the server Proves the component, not the tester, owns the resulting state
3 Dragging to 33.333 reports 33.33 Documents the component's two-decimal rounding, so it is visible instead of surprising
4 0 and 100 are accepted; -0.5 and 100.5 throw IllegalArgumentException Fully closing or opening a split is a legal drag and must not be rejected
5 Split layout accessors and dragSplitterTo throw IllegalStateException when the layout is hidden Tests must fail loudly instead of asserting on content the user cannot see
6 gap dragSplitterTo on a disabled layout ensureComponentIsUsable also covers disabled, but only the hidden case is pinned
7 Card title and subtitle accessors throw when a header component is set Core contract of CardTester: it must not report content the header replaced
8 getTitleAsText() returns the text title (empty string when unset); getTitle() returns the title component, or null for a text title The two title forms are easy to confuse and must stay distinct
9 getFooterComponents() returns footer components in insertion order, empty list when there is no footer Order is what assertions rely on
10 Card accessors throw IllegalStateException when the card is hidden Same visibility contract as row 5
11 With maxItemsVisible exceeded, items split into visible avatars and trailing overflow items, with the right +N abbreviation; a max of 0 or 1 behaves as 2 The overflow-takes-a-slot and minimum-two rules are the tester's only non-trivial logic
12 With no maxItemsVisible (or one not reached), all items are visible and the abbreviation is null The overflow avatar must not be reported when it is not shown
13 getActiveUsersLabel() substitutes {count}, uses the singular phrase for one item, and returns null when no i18n is set or the phrase for the current count is missing Accessible label is what screen-reader assertions check; a partial i18n must not produce a broken string
14 Avatar group accessors throw IllegalStateException when the group is hidden Same visibility contract as row 5

Test methods added on this branch:

  • SplitLayoutTesterTest.dragSplitterTo_updatesPositionAndNotifiesListener → 1
  • SplitLayoutTesterTest.dragSplitterTo_positionSetOnServer_isReplacedByTheDraggedPosition → 2
  • SplitLayoutTesterTest.dragSplitterTo_positionWithMoreThanTwoDecimals_isRoundedByComponent → 3
  • SplitLayoutTesterTest.dragSplitterTo_positionAtRangeEnds_isAccepted → 4
  • SplitLayoutTesterTest.dragSplitterTo_positionOutsideRange_throws → 4
  • SplitLayoutTesterTest.dragSplitterTo_layoutHidden_throws → 5
  • SplitLayoutTesterTest.accessors_layoutHidden_throw → 5
  • SplitLayoutTesterTest.getSplitterPosition_neverPositioned_returnsNull → 2
  • SplitLayoutTesterTest.getPrimaryAndSecondaryComponent_returnSplitContents → 9 (split contents)
  • SplitLayoutTesterTest.getPrimaryComponent_emptyLayout_returnsNull → 9 (empty splits)
  • CardTesterTest.titleAndSubtitle_headerSet_throw → 7
  • CardTesterTest.getTitleAsText_returnsTitle, getTitleAsText_noTitleSet_returnsEmptyString, getTitle_titleSetAsComponent_returnsComponent, getTitle_titleSetAsText_returnsNull → 8
  • CardTesterTest.getSubtitle_returnsSubtitleComponent, headerAccessors_returnHeaderComponents, getHeader_noHeaderSet_returnsNull, getMedia_returnsMediaComponent → 8 (accessor pass-through)
  • CardTesterTest.getFooterComponents_returnsComponentsInOrder, getFooterComponents_emptyFooter_returnsEmptyList → 9
  • CardTesterTest.accessors_cardHidden_throw → 10
  • AvatarGroupTesterTest.maxItemsVisibleExceeded_itemsSplitBetweenAvatarsAndOverflow → 11
  • AvatarGroupTesterTest.noMaxItemsVisible_allItemsVisibleAndNoOverflow, maxItemsVisibleNotReached_allItemsVisible, getItems_returnsAllItems → 12
  • AvatarGroupTesterTest.getActiveUsersLabel_manyUsers_countIsSubstituted, getActiveUsersLabel_oneUser_usesSingularPhrase, getActiveUsersLabel_noI18nSet_returnsNull, getActiveUsersLabel_phraseMissingForCurrentCount_returnsNull → 13
  • AvatarGroupTesterTest.accessors_groupHidden_throw → 14

Left untested on purpose: the generated *Locator classes and find*() methods, which come from the existing LocatorProcessor and are covered by its own tests; and the three TesterWrappers.test(...) overloads, which every test above goes through anyway.

None of SplitLayout, Card or AvatarGroup had a @Tests-annotated
ComponentTester, so LocatorProcessor emitted no *Locator and no find*()
entry point for them and they had to be driven through raw component
calls.

SplitLayoutTester.dragSplitterTo(double) delivers a splitter-dragend DOM
event with the flex-basis event data the browser sends, so the component
recomputes its own splitter position and notifies drag end listeners,
instead of the tester setting server-side state behind the component's
back. CardTester and AvatarGroupTester are assertion-only: Card gets
content accessors that reject the title and subtitle while a header
component replaces them, and AvatarGroup reports the split between
avatars and the overflow bubble derived from maxItemsVisible.

Part of #181.

Fixes #192
Document in dragSplitterTo() that the component rounds a dragged
position to two decimals, and cover it with a value that does not
round-trip exactly, so the behaviour is visible instead of surprising.

Also cover the AvatarGroupTester branch where i18n is set but has no
phrase for the current item count, add getTitle() to the hidden-card
check, and drop the CsvSource row that repeated the maxItemsVisible
clamp already covered by the preceding one.
@Artur-

Artur- commented Sep 8, 2026

Copy link
Copy Markdown
Member

Is this the same as #196?

Dragging a split fully closed or fully open is a legal drag, not an
argument error, and only the rejected values either side of the range
were pinned so far.
@totally-not-ai

totally-not-ai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

@Artur- Yes — #196 and this PR are duplicates. Both close #192, both add SplitLayoutTester / CardTester / AvatarGroupTester with a test(...) overload each in TesterWrappers, and the two AvatarGroupTester implementations are line-for-line the same overflow maths. #196 was opened a few minutes earlier. One of the two should be closed; I have not closed either, since which one to keep is a maintainer call.

The differences, in case they decide it:

  • Card title/subtitle when a header component is set. The header replaces the title and subtitle in the rendering. feat: add SplitLayout, Card and AvatarGroup testers #196 returns "" from getTitleText() / getSubtitleText(); this PR throws IllegalStateException, following MasterDetailLayoutTester.getDetailPlaceholder(), which throws rather than report content the user cannot see. This PR also exposes getTitle() / getSubtitle() for the component flavour and names the text getter getTitleAsText() to match Card's own method, where feat: add SplitLayout, Card and AvatarGroup testers #196 exposes text only.
  • How the drag is delivered. feat: add SplitLayout, Card and AvatarGroup testers #196 calls ComponentUtil.fireEvent(new SplitterDragEndEvent(...)). This PR fires the splitter-dragend DOM event carrying the two @EventData flex-basis expressions, the way DashboardTester does for dashboard-item-moved-flow, so the DOM-listener plumbing the client actually uses is exercised as well.
  • Which guard the getters use. feat: add SplitLayout, Card and AvatarGroup testers #196 calls ensureComponentIsUsable() everywhere; this PR reserves that for dragSplitterTo() and uses ensureVisible() for the read-only accessors, since a disabled card or avatar group is still perfectly readable.
  • Extra surface here. AvatarGroupTester.getItems() and getActiveUsersLabel() (the i18n'd accessible label with {count} substituted), and the two-decimal rounding SplitLayout applies to a dragged position is documented and pinned by a test.
  • README count. There are 71 testers on main and 74 after either PR, so feat: add SplitLayout, Card and AvatarGroup testers #196's 68+ undercounts; this PR says 70+.

I have also taken the one test #196 had that was missing here — that dragging to 0 or 100 is accepted rather than rejected — and added it in dragSplitterTo_positionAtRangeEnds_isAccepted.

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.

Add testers for SplitLayout, Card and AvatarGroup

1 participant