Fix for WebAuthnSigningInTest WebAuthn test

Closes #43477

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc
2025-11-14 17:48:17 +01:00
committed by Marek Posolda
parent b9d94d325b
commit f0f776e5c8
7 changed files with 93 additions and 167 deletions

View File

@@ -22,14 +22,15 @@ import java.util.LinkedList;
import org.keycloak.testsuite.webauthn.pages.fragments.ContentAlert;
import org.keycloak.testsuite.webauthn.pages.fragments.ContinueCancelModal;
import org.keycloak.testsuite.webauthn.pages.fragments.LoggedInPageHeader;
import org.keycloak.testsuite.webauthn.pages.fragments.Sidebar;
import org.jboss.arquillian.graphene.page.Page;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import static org.keycloak.testsuite.util.UIUtils.clickLink;
import static org.keycloak.testsuite.util.UIUtils.getTextFromElement;
import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
@@ -40,9 +41,6 @@ public abstract class AbstractLoggedInPage extends AbstractAccountPage {
@FindBy(xpath = "//*[@data-testid='page-header']")
private LoggedInPageHeader header;
@FindBy(id = "page-sidebar")
private Sidebar sidebar;
@Page
private ContentAlert alert;
@@ -71,6 +69,12 @@ public abstract class AbstractLoggedInPage extends AbstractAccountPage {
*/
public abstract String getPageId();
/**
* Title of the account page translated into english. For example "Signing in" or "Device activity".
* @return The translated page title
*/
public abstract String getTranslatedPageTitle();
/**
* In case the page is placed is a subpage, i.e. placed in a subsection. See content.json in themes module for IDs.
*
@@ -80,30 +84,20 @@ public abstract class AbstractLoggedInPage extends AbstractAccountPage {
return null;
}
/**
* This should simulate a user navigating to this page using links in the nav bar. It assume that user is logged in
* and at some Account Console page (not Welcome Screen), i.e. that the nav bar is visible.
*/
public void navigateToUsingSidebar() {
if (sidebar.isCollapsed()) {
sidebar.expand();
}
final String dataTestId = String.join("/", hashPath);
clickLink(driver.findElement(By.xpath("//*[@data-testid='" + dataTestId + "']")));
waitForPageTitle();
}
if (getParentPageId() != null) {
sidebar().clickSubNav(getParentPageId(), getPageId());
} else {
sidebar().clickNav(getPageId());
}
public void waitForPageTitle() {
waitUntilElement(By.xpath("//*[@data-testid='page-heading']")).text().equalTo(getTranslatedPageTitle());
}
public LoggedInPageHeader header() {
return header;
}
public Sidebar sidebar() {
return sidebar;
}
public ContentAlert alert() {
return alert;
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.webauthn.pages;
/**
*
* @author rmartinc
*/
public class DeviceActivityPage extends AbstractLoggedInPage {
@Override
public String getPageId() {
return "device-activity";
}
@Override
public String getParentPageId() {
return ACCOUNT_SECURITY_ID;
}
@Override
public String getTranslatedPageTitle() {
return "Device activity";
}
}

View File

@@ -46,6 +46,11 @@ public class SigningInPage extends AbstractLoggedInPage {
return ACCOUNT_SECURITY_ID;
}
@Override
public String getTranslatedPageTitle() {
return "Signing in";
}
public CredentialType getCredentialType(String type) {
return new CredentialType(type);
}
@@ -150,6 +155,10 @@ public class SigningInPage extends AbstractLoggedInPage {
public String getHelpText() {
return getTextFromElement(getItemElementByTestId(HELP));
}
public void navigateToUsingSidebar() {
SigningInPage.this.navigateToUsingSidebar();
}
}
public class UserCredential {

View File

@@ -1,132 +0,0 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.webauthn.pages.fragments;
import java.util.List;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.graphene.fragment.Root;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import static org.keycloak.testsuite.util.UIUtils.clickLink;
import static org.keycloak.testsuite.util.WaitUtils.pause;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class Sidebar extends AbstractFragmentWithMobileLayout {
public static int MOBILE_WIDTH = 767; // if the page width is less or equal than this, we expect the sidebar to be collapsed by default
public static final String NAV_ITEM_ID_PREFIX = "nav-link-";
@Drone
protected WebDriver driver;
@Root
private WebElement sidebarRoot;
@Override
protected int getMobileWidth() {
return MOBILE_WIDTH;
}
public boolean isCollapsed() {
return sidebarRoot.getAttribute("class").contains("collapsed");
}
public void collapse() {
assertFalse("Sidebar is already collapsed", isCollapsed());
getCollapseToggle().click();
pause(2000); // wait for animation
assertTrue("Sidebar is not collapsed", isCollapsed());
}
public void expand() {
assertTrue("Sidebar is already expanded", isCollapsed());
getCollapseToggle().click();
pause(2000); // wait for animation
assertFalse("Sidebar is not expanded", isCollapsed());
}
private WebElement getCollapseToggle(){
return driver.findElement(By.id("nav-toggle"));
}
protected void performOperationWithSidebarExpanded(Runnable operation) {
if (isMobileLayout()) expand();
operation.run();
if (isMobileLayout()) collapse();
}
protected WebElement getNavElement(String id) {
return sidebarRoot.findElement(By.id(NAV_ITEM_ID_PREFIX + id));
}
public void assertNavNotPresent(String id) {
try {
getNavElement(id).isDisplayed();
throw new AssertionError("Nav element " + id + " shouldn't be present");
}
catch (NoSuchElementException e) {
// ok
}
}
protected WebElement getNavSubsection(WebElement parent) {
return parent.findElement(By.xpath("../section[@aria-labelledby='" + parent.getAttribute("id") + "']"));
}
public void clickNav(String id) {
performOperationWithSidebarExpanded(() -> clickLink(getNavElement(id)));
}
public void clickSubNav(String parentId, String id) {
performOperationWithSidebarExpanded(() -> {
WebElement parentNavItem = getNavElement(parentId);
if (!isNavSubsectionExpanded(parentNavItem)) {
parentNavItem.click();
}
WebElement navItem = getNavSubsection(parentNavItem).findElement(By.id(NAV_ITEM_ID_PREFIX + id));
clickLink(navItem);
});
}
public boolean isNavSubsectionExpanded(String parentId) {
return isNavSubsectionExpanded(getNavElement(parentId));
}
protected boolean isNavSubsectionExpanded(WebElement parent) {
return getNavSubsection(parent).getAttribute("hidden") == null;
}
public String getActiveNavId() {
List<WebElement> activeNavElements = sidebarRoot.findElements(
By.xpath("//*[starts-with(@id,'" + NAV_ITEM_ID_PREFIX + "') and contains(@class,'current')]")
);
assertEquals("There are more than 1 active nav items", 1, activeNavElements.size());
return activeNavElements.get(0).getAttribute("id").split(NAV_ITEM_ID_PREFIX)[1];
}
}

View File

@@ -26,10 +26,9 @@ import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.testsuite.pages.DeleteCredentialPage;
import org.keycloak.testsuite.webauthn.pages.AbstractLoggedInPage;
import org.keycloak.testsuite.webauthn.pages.DeviceActivityPage;
import org.keycloak.testsuite.webauthn.pages.SigningInPage;
import static org.keycloak.testsuite.util.UIUtils.refreshPageAndWaitForLoad;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -66,18 +65,25 @@ public class SigningInPageUtils {
assertThat("Update button visible", userCredential.isUpdateBtnDisplayed(), is(not(removable)));
}
public static void testSetUpLink(RealmResource realmResource, SigningInPage.CredentialType credentialType, String requiredActionProviderId) {
public static void testSetUpLink(RealmResource realmResource, SigningInPage.CredentialType credentialType,
String requiredActionProviderId, DeviceActivityPage deviceActivityPage) {
assertThat("Set up link for \"" + credentialType.getType() + "\" is not visible", credentialType.isSetUpLinkVisible(), is(true));
RequiredActionProviderRepresentation requiredAction = new RequiredActionProviderRepresentation();
RequiredActionProviderRepresentation requiredAction = realmResource.flows().getRequiredAction(requiredActionProviderId);
requiredAction.setEnabled(false);
realmResource.flows().updateRequiredAction(requiredActionProviderId, requiredAction);
refreshPageAndWaitForLoad();
try {
deviceActivityPage.navigateToUsingSidebar();
credentialType.navigateToUsingSidebar();
assertThat("Set up link for \"" + credentialType.getType() + "\" is visible", credentialType.isSetUpLinkVisible(), is(false));
assertThat("Title for \"" + credentialType.getType() + "\" is visible", credentialType.isTitleVisible(), is(false));
assertThat("Set up link for \"" + credentialType.getType() + "\" is visible", credentialType.isNotSetUpLabelVisible(), is(false));
assertThat("Set up link for \"" + credentialType.getType() + "\" is visible", credentialType.isSetUpLinkVisible(), is(false));
assertThat("Title for \"" + credentialType.getType() + "\" is visible", credentialType.isTitleVisible(), is(false));
assertThat("Set up link for \"" + credentialType.getType() + "\" is visible", credentialType.isNotSetUpLabelVisible(), is(false));
} finally {
requiredAction.setEnabled(true);
realmResource.flows().updateRequiredAction(requiredActionProviderId, requiredAction);
}
}
public static void testRemoveCredential(AbstractLoggedInPage accountPage, DeleteCredentialPage deleteCredentialPage, SigningInPage.UserCredential userCredential) {

View File

@@ -123,7 +123,7 @@ public abstract class AbstractWebAuthnAccountTest extends AbstractAuthTest imple
signingInPage.navigateTo();
waitForPageToLoad();
loginToAccount();
signingInPage.assertCurrent();
signingInPage.waitForPageTitle();
}
@Override

View File

@@ -38,6 +38,7 @@ import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.testsuite.arquillian.annotation.IgnoreBrowserDriver;
import org.keycloak.testsuite.page.AbstractPatternFlyAlert;
import org.keycloak.testsuite.webauthn.authenticators.DefaultVirtualAuthOptions;
import org.keycloak.testsuite.webauthn.pages.DeviceActivityPage;
import org.keycloak.testsuite.webauthn.pages.SigningInPage;
import org.keycloak.testsuite.webauthn.pages.WebAuthnAuthenticatorsList;
import org.keycloak.testsuite.webauthn.updaters.AbstractWebAuthnRealmUpdater;
@@ -46,6 +47,7 @@ import org.keycloak.testsuite.webauthn.updaters.WebAuthnRealmAttributeUpdater;
import org.keycloak.theme.DateTimeFormatterUtil;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Test;
import org.openqa.selenium.firefox.FirefoxDriver;
@@ -66,6 +68,9 @@ import static org.hamcrest.Matchers.hasSize;
public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest {
@Page
protected DeviceActivityPage deviceActivityPage;
@Test
public void categoriesTest() {
testContext.setTestRealmReps(emptyList()); // reimport realm after this test
@@ -77,7 +82,8 @@ public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest {
// Delete WebAuthn flow ==> Passwordless category should disappear
testRealmResource().flows().deleteFlow(WEBAUTHN_FLOW_ID);
refreshPageAndWaitForLoad();
deviceActivityPage.navigateToUsingSidebar();
signingInPage.navigateToUsingSidebar();
assertThat(signingInPage.getCategoriesCount(), is(2));
}
@@ -190,8 +196,8 @@ public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest {
@Test
public void setUpLinksTest() {
testSetUpLink(testRealmResource(), webAuthnCredentialType, WebAuthnRegisterFactory.PROVIDER_ID);
testSetUpLink(testRealmResource(), webAuthnPwdlessCredentialType, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID);
testSetUpLink(testRealmResource(), webAuthnCredentialType, WebAuthnRegisterFactory.PROVIDER_ID, deviceActivityPage);
testSetUpLink(testRealmResource(), webAuthnPwdlessCredentialType, WebAuthnPasswordlessRegisterFactory.PROVIDER_ID, deviceActivityPage);
}
@Test
@@ -275,7 +281,7 @@ public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest {
assertThat(credentialId, notNullValue());
testUserResource().removeCredential(credentialId);
driver.navigate().refresh();
refreshPageAndWaitForLoad();
webAuthnLoginPage.assertCurrent();
authenticators = webAuthnLoginPage.getAuthenticators();
@@ -478,11 +484,12 @@ public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest {
assertThat(credentialType.getUserCredentialsCount(), is(2));
assertUserCredential(label2, true, webAuthn2);
RequiredActionProviderRepresentation requiredAction = new RequiredActionProviderRepresentation();
RequiredActionProviderRepresentation requiredAction = testRealmResource().flows().getRequiredAction(providerId);
requiredAction.setEnabled(false);
testRealmResource().flows().updateRequiredAction(providerId, requiredAction);
refreshPageAndWaitForLoad();
deviceActivityPage.navigateToUsingSidebar();
signingInPage.navigateToUsingSidebar();
assertThat("Set up link for \"" + credentialType.getType() + "\" is visible", credentialType.isSetUpLinkVisible(), is(false));
assertThat("Not set up link for \"" + credentialType.getType() + "\" is visible", credentialType.isNotSetUpLabelVisible(), is(false));
@@ -490,5 +497,7 @@ public class WebAuthnSigningInTest extends AbstractWebAuthnAccountTest {
assertThat(credentialType.getUserCredentialsCount(), is(2));
testRemoveCredential(webAuthn1);
requiredAction.setEnabled(true);
testRealmResource().flows().updateRequiredAction(providerId, requiredAction);
}
}