How to test MainLayout with KaribuTest?

I can’t seem to assert against things in MainLayout. It acts like AuthenticatedUser is not present.

Here’s the test. Everything passes until the final assertion.

@Test
    void givenUserHasZeroCredit_whenNavigatingToAnyPage_thenCreditBalanceVisibleWithZero() {
        saveAccountAndDeleteAll();
        loginAndNavigate(KeywordCollectionsView.class);
        assertThat(authenticatedUser.get()).isPresent();
        @NotNull Paragraph creditBalanceParagraph = LocatorJ._get(Paragraph.class, s -> s.withId("credit-balance"));
        assertThat(creditBalanceParagraph).isNotNull();
        assertThat(creditBalanceParagraph.getText()).isEqualTo("Credit balance: 0");
    }

Here’s the result:

org.opentest4j.AssertionFailedError:
expected: “Credit balance: 0”
but was: “Credit balance: ?”

Here’s the relevant production code:

 Optional<Account> currentAccount = authenticatedUser.get();
        String credit = "?";
        if (currentAccount.isPresent()) {
            credit = String.valueOf(currentAccount.get().appCredit().balance());
        }
        Paragraph creditBalanceParagraph = new Paragraph("Credit balance: " + credit);

Full test class:

@Tag("ui")
class MainLayoutTest extends KaribuTest {
    @Autowired
    AuthenticatedUser authenticatedUser;

    @Test
    void givenUserHasZeroCredit_whenNavigatingToAnyPage_thenCreditBalanceVisibleWithZero() {
        saveAccountAndDeleteAll();
        loginAndNavigate(KeywordCollectionsView.class);
        assertThat(authenticatedUser.get()).isPresent();
        @NotNull Paragraph creditBalanceParagraph = LocatorJ._get(Paragraph.class, s -> s.withId("credit-balance"));
        assertThat(creditBalanceParagraph).isNotNull();
        assertThat(creditBalanceParagraph.getText()).isEqualTo("Credit balance: 0");
    }

    @Test
    void givenUserHasCredit100_whenNavigatingToAnyPage_thenCreditBalanceVisibleWith100() {
        accountRepository.deleteAllEverywhere();
        Account account = new AccountBuilder().withAppCredit(100).build();
        accountRepository.save(account);
        loginAndNavigate("");
        @NotNull Paragraph creditBalanceParagraph = LocatorJ._get(Paragraph.class, s -> s.withId("credit-balance"));
        assertThat(creditBalanceParagraph).isNotNull();
        assertThat(creditBalanceParagraph.getText()).isEqualTo("Credit balance: 100");
    }

}

And full MainLayout:

@JsModule("./prefers-color-scheme.js")
@Layout
@AnonymousAllowed
public class MainLayout extends AppLayout {

    private H1 viewTitle;
    private final AuthenticatedUser authenticatedUser;
    private Registration broadcasterRegistration;

    public MainLayout(AuthenticatedUser authenticatedUser) {
        this.authenticatedUser = authenticatedUser;
        setPrimarySection(Section.DRAWER);
        addDrawerContent();
        addHeaderContent();
    }

    private void addHeaderContent() {
        DrawerToggle toggle = new DrawerToggle();
        toggle.setAriaLabel("Menu toggle");

        viewTitle = new H1();
        viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE);

        Optional<Account> currentAccount = authenticatedUser.get();
        String credit = "?";
        if (currentAccount.isPresent()) {
            credit = String.valueOf(currentAccount.get().appCredit().balance());
        }
        Paragraph creditBalanceParagraph = new Paragraph("Credit balance: " + credit);
        creditBalanceParagraph.setId("credit-balance");
        creditBalanceParagraph.addClassNames(LumoUtility.FontSize.SMALL, LumoUtility.Margin.NONE);

        HorizontalLayout headerLayout = new HorizontalLayout(viewTitle, creditBalanceParagraph);
        headerLayout.setWidthFull();
        headerLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
        headerLayout.addClassNames(LumoUtility.Padding.Right.LARGE);

        addToNavbar(true, toggle, headerLayout);
    }

    private void addDrawerContent() {
        Span appName = new Span("B2B Demand Generation Strategy");
        appName.addClassNames(LumoUtility.FontWeight.SEMIBOLD, LumoUtility.FontSize.LARGE);
        Header header = new Header(appName);
        header.addClickListener(_ -> header.getUI().ifPresent(ui -> ui.navigate(KeywordCollectionsView.class)));
        Scroller scroller = new Scroller(createNavigation());
        addToDrawer(header, scroller, createFooter());
    }

    private SideNav createNavigation() {
        SideNav nav = new SideNav();

        List<MenuEntry> menuEntries = MenuConfiguration.getMenuEntries();
        menuEntries.forEach(entry -> {
            if (entry.icon() != null) {
                nav.addItem(new SideNavItem(entry.title(), entry.path(), new SvgIcon(entry.icon())));
            } else {
                nav.addItem(new SideNavItem(entry.title(), entry.path()));
            }
        });

        return nav;
    }

    private Footer createFooter() {
        Footer layout = new Footer();

        Optional<Account> maybeUser = authenticatedUser.get();
        if (maybeUser.isPresent()) {
            Account account = maybeUser.get();
            String name;
            if (account.person() != null && account.person().name() != null) {
                name = account.person().name();
            } else {
                name = account.username();
            }
            Avatar avatar = new Avatar(name);
            avatar.setThemeName("xsmall");
            avatar.getElement().setAttribute("tabindex", "-1");

            MenuBar userMenu = new MenuBar();
            userMenu.setThemeName("tertiary-inline contrast");

            MenuItem userName = userMenu.addItem("");
            Div div = new Div();
            div.add(avatar);
            div.add(name);
            div.add(new Icon("lumo", "dropdown"));
            div.getElement().getStyle().set("display", "flex");
            div.getElement().getStyle().set("align-items", "center");
            div.getElement().getStyle().set("gap", "var(--lumo-space-s)");
            userName.add(div);
            SubMenu subMenu = userName.getSubMenu();
            subMenu.addItem("Sign out", _ -> authenticatedUser.logout());

            layout.add(userMenu);
        } else {
            Anchor loginLink = new Anchor("login", "Sign in");
            layout.add(loginLink);
        }

        return layout;
    }

    @Override
    protected void afterNavigation() {
        super.afterNavigation();
        viewTitle.setText(getCurrentPageTitle());
    }

    private String getCurrentPageTitle() {
        return MenuConfiguration.getPageHeader(getContent()).orElse("");
    }

    @Override
    protected void onAttach(AttachEvent attachEvent) {
        UI ui = attachEvent.getUI();
        broadcasterRegistration = Broadcaster.register(message -> ui.access(() -> {
            if (!"REFRESH_GRIDS".equals(message)) {
                Notification notification = Notification.show(message);
                notification.setPosition(Notification.Position.TOP_CENTER);
            }
        }));
    }

    @Override
    protected void onDetach(DetachEvent detachEvent) {
        if (broadcasterRegistration != null) {
            broadcasterRegistration.remove();
            broadcasterRegistration = null;
        }
    }
}

Yup, Spring Security needs a bit of wiring - this is the limitation of KaribuTesting since no Spring Security Filters/Servlets are triggered by default. Please see karibu-testing/karibu-testing-v10-spring at master · mvysny/karibu-testing · GitHub

1 Like

Thanks! I did try:

@BeforeEach
    public void setupKaribu() {
        MockVaadin.INSTANCE.setMockRequestFactory(session -> new FakeRequest(session) {
            @Override
            public Principal getUserPrincipal() {
                return SecurityContextHolder.getContext().getAuthentication();
            }
        });

        final Function0<UI> uiFactory = UI::new;
        final SpringServlet servlet = new MockSpringServlet(routes, ctx, uiFactory);
        MockVaadin.setup(uiFactory, servlet);
    }

Same result.

I also tried just moving the tests to a normal view class test. Same result.

Why do you think it should change anything? Your tests aren’t annotated with any Spring Security Test annotation like “WithMockUser” - so even now that you are setting the correct context… nobody is filling that context.

1 Like

Hey Christian, security is handled in the KaribuTest class. Browserless Testing of Vaadin Applications with Karibu Testing

I agree with Christian: the code above simply populates the VaadinRequest with user grabbed from Spring Security; but if you’re not setting any user to Spring Security then it will simply be left unpopulated.

You should either configure your Spring tests to login with a user, or you should then create a fake Principal and return it from the FakeRequest.

Maybe this example app could help: GitHub - mvysny/vaadin-spring-karibu-testing

1 Like

Thanks Martin. I’ll take a look. So, just to be clear, you are saying that the test setup outline in the blog post that I linked to does not log the user in? I guess I thought that’s what the login method in KaribuTest was handling.

Yes, that’s true. I’ve updated the documentation to mention this, here’s a copy of the docs:

Note that this will only carry the currently logged-in user (Principal) from Spring Security over to the faked Vaadin environment - a job that’s usually done by Spring servlet filter. In this faked environment the filter is not triggered, and therefore this manual step is necessary. Note that the code above doesn’t log in the user: you’ll need to log in user in Spring Security beforehand, either via annotations or manually.

1 Like